Compare commits
150 commits
leo-2021.7
...
master
Author | SHA1 | Date | |
---|---|---|---|
![]() |
2212e3c7d8 | ||
![]() |
8c0ad582f7 | ||
![]() |
584767b352 | ||
![]() |
d1b123100e | ||
![]() |
0a9c9f8ef0 | ||
![]() |
ee7418f552 | ||
![]() |
c44ad926a5 | ||
![]() |
10ee20354b | ||
![]() |
7214e3d260 | ||
![]() |
97df236b54 | ||
![]() |
cac466462b | ||
![]() |
063d529f09 | ||
![]() |
b343a06ef6 | ||
![]() |
fc09308d11 | ||
![]() |
7a18055c18 | ||
![]() |
e271c41239 | ||
![]() |
5d879f99e2 | ||
![]() |
df44ad0635 | ||
![]() |
aff43d3e98 | ||
![]() |
211201caea | ||
![]() |
7a29dfe600 | ||
![]() |
c467738845 | ||
![]() |
8a0940ccce | ||
![]() |
3b4300a8eb | ||
![]() |
55bbb95cfb | ||
![]() |
62e31afff2 | ||
![]() |
da5223cab8 | ||
![]() |
db286f7883 | ||
![]() |
25684173fa | ||
![]() |
74d841e9e1 | ||
![]() |
53064c0e09 | ||
![]() |
9a21ce2b2a | ||
![]() |
29a639ff90 | ||
![]() |
f18e537561 | ||
![]() |
bac8e3d642 | ||
![]() |
65594f62a3 | ||
![]() |
e30d823225 | ||
![]() |
17de2a89c2 | ||
![]() |
95b2b1e39c | ||
![]() |
fe520f2c0d | ||
![]() |
9a86bf7857 | ||
![]() |
88f5c0cbed | ||
![]() |
c0e71fe0f3 | ||
![]() |
ffffe5da52 | ||
![]() |
cd9bdf61d2 | ||
![]() |
e2f002292b | ||
![]() |
6c2d89ee82 | ||
![]() |
76ef89b518 | ||
![]() |
b414abbb81 | ||
![]() |
53a845feb7 | ||
![]() |
7134c4ac07 | ||
![]() |
585de42a46 | ||
![]() |
2007d116a2 | ||
![]() |
104e49615d | ||
![]() |
6041564835 | ||
![]() |
64bae2140c | ||
![]() |
c3cbc96499 | ||
![]() |
37a3e83f3a | ||
![]() |
db5e90281c | ||
![]() |
92bf020cdf | ||
![]() |
d0e0dda2b3 | ||
![]() |
b0c928c691 | ||
![]() |
c454c035a3 | ||
![]() |
f839166082 | ||
![]() |
3cfee9dd81 | ||
![]() |
6295de9fd6 | ||
![]() |
2a7e1c419e | ||
![]() |
aa07380c57 | ||
![]() |
4f3f22f3bc | ||
![]() |
1b8b8204e5 | ||
![]() |
8c74aa67a1 | ||
![]() |
c7c9bc3b5d | ||
![]() |
709ad758dc | ||
![]() |
3d0f869356 | ||
![]() |
8c93b090b4 | ||
![]() |
60fca48e8c | ||
![]() |
3ccf80d442 | ||
![]() |
0f53fdde5f | ||
![]() |
f0c4f9c5d5 | ||
![]() |
c0cfcda102 | ||
![]() |
078ad2584a | ||
![]() |
7200a9e2f0 | ||
![]() |
7d589c7e2f | ||
![]() |
cf57bdbc9e | ||
![]() |
ab420f5400 | ||
![]() |
84ab6cf478 | ||
![]() |
d39fa6acf2 | ||
![]() |
0a4b6de0fa | ||
![]() |
b7b4fa0c94 | ||
![]() |
c33982f97c | ||
![]() |
59c733735c | ||
![]() |
1e251a27b1 | ||
![]() |
a78aa37f7c | ||
![]() |
bf2f09381e | ||
![]() |
0607152f51 | ||
![]() |
27c9dd4f3e | ||
![]() |
d0eae64241 | ||
![]() |
6ecf1e7b96 | ||
![]() |
4bbd3e1cbb | ||
![]() |
ce29cf57be | ||
![]() |
692538f0c2 | ||
![]() |
97afabb3e0 | ||
![]() |
1df6ccb1c9 | ||
![]() |
a04a66ab6d | ||
![]() |
47cfad4624 | ||
![]() |
d60e1fbcfa | ||
![]() |
90a45dab55 | ||
![]() |
14f4e06788 | ||
![]() |
9de6a8f5c0 | ||
![]() |
20f47110cd | ||
![]() |
4d562d07d0 | ||
![]() |
fa7b03109f | ||
![]() |
eaac412cb3 | ||
![]() |
3eec52f647 | ||
![]() |
a28cbc5a8a | ||
![]() |
c7ef91e9ed | ||
![]() |
a5b0faae77 | ||
![]() |
e711ff0f7c | ||
![]() |
fb0b489c82 | ||
![]() |
a7089373dd | ||
![]() |
f393670528 | ||
![]() |
3f8a314b94 | ||
![]() |
1ad1e6976f | ||
![]() |
c793949b37 | ||
![]() |
01d7de2e46 | ||
![]() |
0558cdec49 | ||
![]() |
e25140b529 | ||
![]() |
e942a9b803 | ||
![]() |
4b1b3bbeed | ||
![]() |
9d6fabe8c2 | ||
![]() |
bd94d75086 | ||
![]() |
16d01207a7 | ||
![]() |
5477f9371d | ||
![]() |
b057026328 | ||
![]() |
28d9290959 | ||
![]() |
3297d9bd8c | ||
![]() |
08d62d1e85 | ||
![]() |
9bbc9af285 | ||
![]() |
e8d4153333 | ||
![]() |
14e9e996e5 | ||
![]() |
8c5de7d74d | ||
![]() |
56e6640f38 | ||
![]() |
4d67bef903 | ||
![]() |
9dfa851016 | ||
![]() |
5138feed4e | ||
![]() |
abc65220ec | ||
![]() |
826f042dbb | ||
![]() |
6f90f19ad1 | ||
![]() |
52fd477d39 | ||
![]() |
f4af3aad45 |
3
.gitignore
vendored
|
@ -22,6 +22,9 @@
|
|||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||
hs_err_pid*
|
||||
|
||||
# Mac-OS file
|
||||
.DS_Store
|
||||
|
||||
# IDE Folders
|
||||
.idea/
|
||||
.vs/
|
||||
|
|
14
.prettierrc
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 4,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"quoteProps": "consistent",
|
||||
"jsxSingleQuote": false,
|
||||
"trailingComma": "none",
|
||||
"bracketSpacing": false,
|
||||
"jsxBracketSameLine": false,
|
||||
"arrowParens": "always",
|
||||
"endOfLine": "lf"
|
||||
}
|
1
.vscode/settings.json
vendored
|
@ -1,2 +1,3 @@
|
|||
{
|
||||
"editor.formatOnSave": true
|
||||
}
|
238
lang/en.json
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"ACTOR.TypeCharacter": "Player Character",
|
||||
"ACTOR.TypeNpc": "Non-Player Character",
|
||||
"ACTOR.TypeStarship": "Starship",
|
||||
"ACTOR.TypeVehicle": "Vehicle",
|
||||
"ITEM.TypeArchetype": "Archetype",
|
||||
"ITEM.TypeBackground": "Background",
|
||||
|
@ -8,15 +9,22 @@
|
|||
"ITEM.TypeClass": "Class",
|
||||
"ITEM.TypeClassfeature": "Class Feature",
|
||||
"ITEM.TypeConsumable": "Consumable",
|
||||
"ITEM.TypeDeployment": "Deployment",
|
||||
"ITEM.TypeDeploymentfeature": "Deployment Feature",
|
||||
"ITEM.TypeEquipment": "Equipment",
|
||||
"ITEM.TypeFeat": "Feat",
|
||||
"ITEM.TypeFightingmastery": "FightingMastery",
|
||||
"ITEM.TypeFightingmastery": "Fighting Mastery",
|
||||
"ITEM.TypeFightingstyle": "Fighting Style",
|
||||
"ITEM.TypeLightsaberform": "Lightsaber Form",
|
||||
"ITEM.TypeLoot": "Loot",
|
||||
"ITEM.TypePower": "Power",
|
||||
"ITEM.TypeSpecies": "Species",
|
||||
"ITEM.TypeStarshipfeature": "Starship Feature",
|
||||
"ITEM.TypeStarshipfeaturePl": "Starship Features",
|
||||
"ITEM.TypeStarshipmod": "Starship Modification",
|
||||
"ITEM.TypeStarshipmodPl": "Starship Modifications",
|
||||
"ITEM.TypeTool": "Tool",
|
||||
"ITEM.TypeVenture": "Venture",
|
||||
"ITEM.TypeWeapon": "Weapon",
|
||||
"SETTINGS.5eAllowPolymorphingL": "Allow players to polymorph their own actors.",
|
||||
"SETTINGS.5eAllowPolymorphingN": "Allow Polymorphing",
|
||||
|
@ -35,11 +43,13 @@
|
|||
"SETTINGS.5eInitTBN": "Initiative Dexterity Tiebreaker",
|
||||
"SETTINGS.5eNoExpL": "Remove experience bars from character sheets.",
|
||||
"SETTINGS.5eNoExpN": "Disable Experience Tracking",
|
||||
"SETTINGS.5eReset": "Reset",
|
||||
"SETTINGS.5eRestEpic": "Epic Heroism (LR: 1 hour, SR: 1 min)",
|
||||
"SETTINGS.5eRestGritty": "Gritty Realism (LR: 7 days, SR: 8 hours)",
|
||||
"SETTINGS.5eRestL": "Configure which rest variant should be used for games within this system.",
|
||||
"SETTINGS.5eRestN": "Rest Variant",
|
||||
"SETTINGS.5eRestPHB": "Player's Handbook (LR: 8 hours, SR: 1 hour)",
|
||||
"SETTINGS.5eUndoChanges": "Undo Changes",
|
||||
"SETTINGS.SWColorDark": "Dark Theme",
|
||||
"SETTINGS.SWColorL": "Set the color theme of the game",
|
||||
"SETTINGS.SWColorLight": "Light Theme",
|
||||
|
@ -70,6 +80,7 @@
|
|||
"SW5E.AbilityUseCast": "Cast Power",
|
||||
"SW5E.AbilityUseChargedHint": "This {type} is charged and ready to use!",
|
||||
"SW5E.AbilityUseChargesLabel": "{value} Charges",
|
||||
"SW5E.AbilityUseConfig": "Usage Configuration",
|
||||
"SW5E.AbilityUseConsumableChargeHint": "Using this {type} will consume 1 charge of {value} remaining.",
|
||||
"SW5E.AbilityUseConsumableDestroyHint": "Using this {type} will consume its final charge and it will be destroyed.",
|
||||
"SW5E.AbilityUseConsumableLabel": "{max} per {per}",
|
||||
|
@ -96,7 +107,9 @@
|
|||
"SW5E.ActionUtil": "Utility",
|
||||
"SW5E.ActionWarningNoItem": "The requested item {item} no longer exists on Actor {name}",
|
||||
"SW5E.ActionWarningNoToken": "You must have one or more controlled Tokens in order to use this option.",
|
||||
"SW5E.ActorWarningInvalidItem": "{itemType} items cannot be added to a {actorType}.",
|
||||
"SW5E.Add": "Add",
|
||||
"SW5E.AddEmbeddedItemPromptHint": "Do you want to add these items to your character sheet?",
|
||||
"SW5E.AdditionalNotes": "Additional Notes",
|
||||
"SW5E.Advantage": "Advantage",
|
||||
"SW5E.Alignment": "Alignment",
|
||||
|
@ -110,6 +123,7 @@
|
|||
"SW5E.AlignmentND": "Neutral Dark",
|
||||
"SW5E.AlignmentNL": "Neutral Light",
|
||||
"SW5E.Appearance": "Appearance",
|
||||
"SW5E.Apply": "Apply",
|
||||
"SW5E.ArchetypeName": "Archetype Name",
|
||||
"SW5E.Archetypes": "Archetypes",
|
||||
"SW5E.ArmorClass": "Armor Class",
|
||||
|
@ -179,6 +193,9 @@
|
|||
"SW5E.BonusSaveForm": "Update Bonuses",
|
||||
"SW5E.BonusTechPowerDC": "Global Tech Power DC Bonus",
|
||||
"SW5E.BonusTitle": "Configure Actor Bonuses",
|
||||
"SW5E.BurnFuel": "Burn",
|
||||
"SW5E.CapacityMultiplier": "Capacity Multiplier",
|
||||
"SW5E.CentStorageCapacity": "Central Storage Capacity",
|
||||
"SW5E.ChallengeRating": "Challenge Rating",
|
||||
"SW5E.Charged": "Charged",
|
||||
"SW5E.Charges": "Charges",
|
||||
|
@ -188,9 +205,14 @@
|
|||
"SW5E.ChatContextHealing": "Apply Healing",
|
||||
"SW5E.ChatFlavor": "Chat Message Flavor",
|
||||
"SW5E.ClassLevels": "Class Levels",
|
||||
"SW5E.ClassMakeOriginal": "Original Class",
|
||||
"SW5E.ClassMakeOriginalHint": "First class taken by character used to determine certain class traits when multiclassing.",
|
||||
"SW5E.ClassName": "Class Name",
|
||||
"SW5E.ClassOriginal": "Original Class",
|
||||
"SW5E.ClassSaves": "Saving Throws",
|
||||
"SW5E.ClassSkillsChosen": "Chosen Class Skills",
|
||||
"SW5E.ClassSkillsNumber": "Number of Starting Skills",
|
||||
"SW5E.Collapse": "Collapse/Expand",
|
||||
"SW5E.ComponentMaterial": "Material",
|
||||
"SW5E.ComponentSomatic": "Somatic",
|
||||
"SW5E.ComponentVerbal": "Verbal",
|
||||
|
@ -249,6 +271,32 @@
|
|||
"SW5E.CoverHalf": "Half",
|
||||
"SW5E.CoverThreeQuarters": "Three Quarters",
|
||||
"SW5E.CoverTotal": "Total",
|
||||
"SW5E.CreatureAberration": "Aberration",
|
||||
"SW5E.CreatureAberrationPl": "Aberrations",
|
||||
"SW5E.CreatureBeast": "Beast",
|
||||
"SW5E.CreatureBeastPl": "Beasts",
|
||||
"SW5E.CreatureConstruct": "Construct",
|
||||
"SW5E.CreatureConstructPl": "Constructs",
|
||||
"SW5E.CreatureDroid": "Droid",
|
||||
"SW5E.CreatureDroidPl": "Droids",
|
||||
"SW5E.CreatureForceEntity": "Force Entity",
|
||||
"SW5E.CreatureForceEntityPl": "Force Entities",
|
||||
"SW5E.CreatureHumanoid": "Humanoid",
|
||||
"SW5E.CreatureHumanoidPl": "Humanoids",
|
||||
"SW5E.CreaturePlant": "Plant",
|
||||
"SW5E.CreaturePlantPl": "Plants",
|
||||
"SW5E.CreatureSwarm": "Swarm",
|
||||
"SW5E.CreatureSwarmPhrase": "Swarm of {size} {type}",
|
||||
"SW5E.CreatureSwarmSize": "Swarm Size",
|
||||
"SW5E.CreatureType": "Creature Type",
|
||||
"SW5E.CreatureTypeConfig": "Configure Creature Type",
|
||||
"SW5E.CreatureTypeSelectorCustom": "Custom Type",
|
||||
"SW5E.CreatureTypeSelectorSubtype": "Subtype",
|
||||
"SW5E.CreatureTypeTitle": "Configure Creature Type",
|
||||
"SW5E.CreatureUndead": "Undead",
|
||||
"SW5E.CreatureUndeadPl": "Undead",
|
||||
"SW5E.CrewCap": "Crew Capacity",
|
||||
"SW5E.Crewed": "Crewed",
|
||||
"SW5E.Critical": "Critical",
|
||||
"SW5E.CriticalHit": "Critical Hit",
|
||||
"SW5E.Currency": "Currency",
|
||||
|
@ -282,8 +330,11 @@
|
|||
"SW5E.DeathSavingThrow": "Death Saving Throw",
|
||||
"SW5E.Default": "Default",
|
||||
"SW5E.DefaultAbilityCheck": "Default Ability Check",
|
||||
"SW5E.Deployment": "Deployment",
|
||||
"SW5E.DeploymentPl": "Deployments",
|
||||
"SW5E.description": "A comprehensive game system for running games of Star Wars 5th Edition in the Foundry VTT environment.",
|
||||
"SW5E.Description": "Description",
|
||||
"SW5E.DestructionSave": "Destruction Saves",
|
||||
"SW5E.Details": "Details",
|
||||
"SW5E.Dimensions": "Dimensions",
|
||||
"SW5E.Disadvantage": "Disadvantage",
|
||||
|
@ -292,38 +343,55 @@
|
|||
"SW5E.DistMi": "Miles",
|
||||
"SW5E.DistSelf": "Self",
|
||||
"SW5E.DistTouch": "Touch",
|
||||
"SW5E.DmgRed": "Damage Reduction",
|
||||
"SW5E.Duration": "Duration",
|
||||
"SW5E.EffectsCategoryTemporary": "Temporary Effects",
|
||||
"SW5E.EffectsCategoryPassive": "Passive Effects",
|
||||
"SW5E.EffectsCategoryInactive": "Inactive Effects",
|
||||
"SW5E.EffectCreate": "Create Effect",
|
||||
"SW5E.EffectDelete": "Delete Effect",
|
||||
"SW5E.EffectEdit": "Edit Effect",
|
||||
"SW5E.EffectInactive": "Inactive Effects",
|
||||
"SW5E.EffectNew": "New Effect",
|
||||
"SW5E.EffectPassive": "Passive Effects",
|
||||
"SW5E.Effects": "Effects",
|
||||
"SW5E.EffectTemporary": "Temporary Effects",
|
||||
"SW5E.EffectsCategoryInactive": "Inactive Effects",
|
||||
"SW5E.EffectsCategoryPassive": "Passive Effects",
|
||||
"SW5E.EffectsCategoryTemporary": "Temporary Effects",
|
||||
"SW5E.EffectToggle": "Toggle Effect",
|
||||
"SW5E.Engine": "Engine",
|
||||
"SW5E.EnginePl": "Engines",
|
||||
"SW5E.EquipmentBonus": "Magical Bonus",
|
||||
"SW5E.EquipmentClothing": "Clothing",
|
||||
"SW5E.EquipmentHeavy": "Heavy Armor",
|
||||
"SW5E.EquipmentHyperdrive": "Hyperdrive",
|
||||
"SW5E.EquipmentLight": "Light Armor",
|
||||
"SW5E.EquipmentMedium": "Medium Armor",
|
||||
"SW5E.EquipmentNatural": "Natural Armor",
|
||||
"SW5E.EquipmentPowerCoupling": "Power Coupling",
|
||||
"SW5E.EquipmentReactor": "Reactor",
|
||||
"SW5E.EquipmentShield": "Shield",
|
||||
"SW5E.EquipmentShieldProficiency": "Shields",
|
||||
"SW5E.EquipmentStarshipArmor": "Starship Armor",
|
||||
"SW5E.EquipmentStarshipShield": "Starship Shield",
|
||||
"SW5E.EquipmentTrinket": "Trinket",
|
||||
"SW5E.EquipmentVehicle": "Vehicle Equipment",
|
||||
"SW5E.Equipped": "Equipped",
|
||||
"SW5E.Exhaustion": "Exhaustion",
|
||||
"SW5E.Expand": "Expand",
|
||||
"SW5E.Expertise": "Expertise",
|
||||
"SW5E.Favorites": "Favoris",
|
||||
"SW5E.Favorites": "Favorites",
|
||||
"SW5E.FavoritesAndNotes": "Favorites & Notes",
|
||||
"SW5E.Feats": "Feats",
|
||||
"SW5E.FeatureActionRecharge": "Action Recharge",
|
||||
"SW5E.FeatureActive": "Active Abilities",
|
||||
"SW5E.FeatureAdd": "Create Feature",
|
||||
"SW5E.FeatureAttack": "Feature Attack",
|
||||
"SW5E.FeatureCollapse": "Collapse Feature",
|
||||
"SW5E.FeatureExpand": "Expand Feature",
|
||||
"SW5E.FeaturePassive": "Passive Abilities",
|
||||
"SW5E.FeatureRechargeOn": "Recharge On",
|
||||
"SW5E.FeatureRechargeResult": "1d6 Result",
|
||||
"SW5E.Features": "Features",
|
||||
"SW5E.FeatureType": "Feature Type",
|
||||
"SW5E.FeatureUsage": "Feature Usage",
|
||||
"SW5E.FeetAbbr": "ft.",
|
||||
"SW5E.Filter": "Filter",
|
||||
|
@ -339,9 +407,9 @@
|
|||
"SW5E.FlagsArmorIntegration": "Armor Integration",
|
||||
"SW5E.FlagsArmorIntegrationHint": "You cannot wear armor, but you can have the armor professionally integrated into your chassis over the course of a long rest. This work must be done by someone proficient with astrotech’s implements. You must be proficient in armor in order to have it integrated.",
|
||||
"SW5E.FlagsBusinessSavvy": "Business Savvy",
|
||||
"SW5E.FlagsBusinessSavvyHint":"Whenever you make a Charisma (Persuasion) check involving haggling you are considered to have expertise in the Persuasion skill.",
|
||||
"SW5E.FlagsBusinessSavvyHint": "Whenever you make a Charisma (Persuasion) check involving haggling you are considered to have expertise in the Persuasion skill.",
|
||||
"SW5E.FlagsCannibalize": "Cannibalize",
|
||||
"SW5E.FlagsCannibalizeHint":"If you spend at least 1 minute devouring the corpse of a beast or humanoid, you gain temporary hit points equal to your Constitution modifier. Once you've used this feature, you must complete a short or long rest before you can use it again.",
|
||||
"SW5E.FlagsCannibalizeHint": "If you spend at least 1 minute devouring the corpse of a beast or humanoid, you gain temporary hit points equal to your Constitution modifier. Once you've used this feature, you must complete a short or long rest before you can use it again.",
|
||||
"SW5E.FlagsClosedMind": "Closed Mind",
|
||||
"SW5E.FlagsClosedMindHint": "Members of your species have a natural attunement to the Force, which makes them resistant to its powers. You have advantage on Wisdom and Charisma saving throws against force powers.",
|
||||
"SW5E.FlagsCrudeWeaponSpecialists": "Crude Weapon Specialists",
|
||||
|
@ -359,7 +427,7 @@
|
|||
"SW5E.FlagsForceContention": "Force Contention",
|
||||
"SW5E.FlagsForceContentionHint": "Due to their unique physiology, members of your species exhibit a hardiness that allows them to overcome use of the Force. You have advantage on Strength and Constitution saving throws against force powers.",
|
||||
"SW5E.FlagsForceInsensitive": "Force Insensitive",
|
||||
"SW5E.FlagsForceInsensitiveHint" : "While droids can be manipulated by many force powers, they cannot sense the Force. You can not use force powers or take levels in forcecasting classes.",
|
||||
"SW5E.FlagsForceInsensitiveHint": "While droids can be manipulated by many force powers, they cannot sense the Force. You can not use force powers or take levels in forcecasting classes.",
|
||||
"SW5E.FlagsForeignBiology": "Foreign Biology",
|
||||
"SW5E.FlagsForeignBiologyHint": "You wear a breathing apparatus because many atmospheres in the galaxy differ from that of your species' homeworld. If your apparatus is removed while you are in such an environment, you lose consciousness.",
|
||||
"SW5E.FlagsFuryOfTheSmall": "Fury of the Small",
|
||||
|
@ -382,7 +450,7 @@
|
|||
"SW5E.FlagsMaintenanceMode": "Maintenance Mode",
|
||||
"SW5E.FlagsMaintenanceModeHint": "Rather than sleep, you enter an inactive state to perform routine maintenance for 4 hours each day. You have disadvantage on Wisdom (Perception) checks while performing maintenance.",
|
||||
"SW5E.FlagsMaskOfTheWild": "Mask of the Wild",
|
||||
"SW5E.FlagsMaskOfTheWildHint":"You can attempt to hide even when you are only lightly obscured by foliage, heavy rain, falling snow, mist, and other natural phenomena.",
|
||||
"SW5E.FlagsMaskOfTheWildHint": "You can attempt to hide even when you are only lightly obscured by foliage, heavy rain, falling snow, mist, and other natural phenomena.",
|
||||
"SW5E.FlagsMeleeCriticalDice": "Melee Critical Damage Dice",
|
||||
"SW5E.FlagsMeleeCriticalDiceHint": "A number of additional damage dice added to melee weapon critical hits.",
|
||||
"SW5E.FlagsMultipleHearts": "Multiple Hearts",
|
||||
|
@ -453,19 +521,31 @@
|
|||
"SW5E.Flaws": "Flaws",
|
||||
"SW5E.ForcePowerbook": "Force Powers",
|
||||
"SW5E.Formula": "Formula",
|
||||
"SW5E.FuelCapacity": "Fuel Capacity",
|
||||
"SW5E.FuelCostPerUnit": "Fuel Cost per Unit",
|
||||
"SW5E.FuelCostsMod": "Fuel Costs Modifier",
|
||||
"SW5E.GrantedAbilities": "Granted Abilities",
|
||||
"SW5E.HalfProficient": "Half Proficient",
|
||||
"SW5E.HardpointSizeMod": "Hardpoint Size Modifier",
|
||||
"SW5E.Healing": "Healing",
|
||||
"SW5E.HealingTemp": "Healing (Temporary)",
|
||||
"SW5E.Health": "Health",
|
||||
"SW5E.HealthConditions": "Health Conditions",
|
||||
"SW5E.HealthFormula": "Health Formula",
|
||||
"SW5E.HitDice": "Hit Dice",
|
||||
"SW5E.HitDiceConfig": "Adjust Hit Dice",
|
||||
"SW5E.HitDiceConfigHint": "Adjust remaining hit dice levels for each class.",
|
||||
"SW5E.HitDiceMax": "Maximum Hit Dice",
|
||||
"SW5E.HitDiceRemaining": "Remaining Hit Dice",
|
||||
"SW5E.HitDiceRoll": "Roll Hit Dice",
|
||||
"SW5E.HitDiceUsed": "Hit Dice Used",
|
||||
"SW5E.HitDiceWarn": "{name} has no available {formula} Hit Dice remaining!",
|
||||
"SW5E.HP": "Health",
|
||||
"SW5E.HPFormula": "Health Formula",
|
||||
"SW5E.HullDice": "Hull Dice",
|
||||
"SW5E.HullPoints": "Hull Points",
|
||||
"SW5E.HullPointsFormula": "Hull Points Formula",
|
||||
"SW5E.HyperdriveClass": "Hyperdrive Class",
|
||||
"SW5E.Ideals": "Ideals",
|
||||
"SW5E.Identified": "Identified",
|
||||
"SW5E.Initiative": "Initiative",
|
||||
|
@ -509,6 +589,7 @@
|
|||
"SW5E.ItemTypeArchetype": "Archetype",
|
||||
"SW5E.ItemTypeBackground": "Background",
|
||||
"Sw5E.ItemTypeBackgroundPl": "Backgrounds",
|
||||
"SW5E.ItemTypeBackpack": "Container",
|
||||
"SW5E.ItemTypeClass": "Class",
|
||||
"SW5E.ItemTypeClassFeat": "Class Feature",
|
||||
"SW5E.ItemTypeClassFeats": "Class Features",
|
||||
|
@ -517,6 +598,10 @@
|
|||
"SW5E.ItemTypeConsumablePl": "Consumables",
|
||||
"SW5E.ItemTypeContainer": "Container",
|
||||
"SW5E.ItemTypeContainerPl": "Containers",
|
||||
"SW5E.ItemTypeDeployment": "Deployment",
|
||||
"SW5E.ItemTypeDeploymentFeature": "Deployment Feature",
|
||||
"SW5E.ItemTypeDeploymentFeaturePl": "Deployment Features",
|
||||
"SW5E.ItemTypeDeploymentPl": "Deployments",
|
||||
"SW5E.ItemTypeEquipment": "Equipment",
|
||||
"SW5E.ItemTypeEquipmentPl": "Equipment",
|
||||
"SW5E.ItemTypeFeat": "Feat",
|
||||
|
@ -533,13 +618,19 @@
|
|||
"SW5E.ItemTypePowerPl": "Powers",
|
||||
"SW5E.ItemTypeSpecies": "Species",
|
||||
"SW5E.ItemTypeSpeciesPl": "Species",
|
||||
"SW5E.ItemTypeStarshipMod": "Starship Modification",
|
||||
"SW5E.ItemTypeStarshipModPl": "Starship Modifications",
|
||||
"SW5E.ItemTypeTool": "Tool",
|
||||
"SW5E.ItemTypeToolPl": "Tools",
|
||||
"SW5E.ItemTypeVenture": "Venture",
|
||||
"SW5E.ItemTypeVenturePl": "Ventures",
|
||||
"SW5E.ItemTypeWeapon": "Weapon",
|
||||
"SW5E.ItemTypeWeaponPl": "Weapons",
|
||||
"SW5E.ItemWeaponAttack": "Weapon Attack",
|
||||
"SW5E.ItemWeaponDetails": "Weapon Details",
|
||||
"SW5E.ItemWeaponProperties": "Weapon Properties",
|
||||
"SW5E.ItemWeaponSize": "Starship Weapon Size",
|
||||
"SW5E.ItemWeaponSizePl": "Starship Weapon Sizes",
|
||||
"SW5E.ItemWeaponStatus": "Weapon Status",
|
||||
"SW5E.ItemWeaponType": "Weapon Type",
|
||||
"SW5E.ItemWeaponUsage": "Weapon Usage",
|
||||
|
@ -665,6 +756,7 @@
|
|||
"SW5E.LongRest": "Long Rest",
|
||||
"SW5E.LongRestEpic": "Long Rest (1 hour)",
|
||||
"SW5E.LongRestGritty": "Long Rest (7 days)",
|
||||
"SW5E.LongRestHint": "Take a long rest? On a long rest you will recover hit points, half your maximum hit dice, class resources, limited use item charges, and power points.",
|
||||
"SW5E.LongRestNormal": "Long Rest (8 hours)",
|
||||
"SW5E.LongRestOvernight": "Long Rest (New Day)",
|
||||
"SW5E.LongRestResult": "{name} takes a long rest.",
|
||||
|
@ -684,18 +776,25 @@
|
|||
"SW5E.LongRestResultTP": "{name} takes a long rest and recovers {tech} Tech Points.",
|
||||
"SW5E.LongRestResultTPHD": "{name} takes a long rest and recovers {tech} Tech Points and {dice} Hit Dice.",
|
||||
"SW5E.Max": "Max",
|
||||
"SW5E.ModCap": "Modification Capacity",
|
||||
"SW5E.Modifier": "Modifier",
|
||||
"SW5E.Movement": "Movement",
|
||||
"SW5E.MovementBurrow": "Burrow",
|
||||
"SW5E.MovementClimb": "Climb",
|
||||
"SW5E.MovementConfig": "Configure Movement Speed",
|
||||
"SW5E.MovementConfigHint": "Configure the movement speed and special movement attributes of this creature.",
|
||||
"SW5E.MovementCrawl": "Crawl",
|
||||
"SW5E.MovementFly": "Fly",
|
||||
"SW5E.MovementHover": "Hover",
|
||||
"SW5E.MovementRoll": "Roll",
|
||||
"SW5E.MovementSpace": "Space Flight",
|
||||
"SW5E.MovementSwim": "Swim",
|
||||
"SW5E.MovementTurn": "Turning",
|
||||
"SW5E.MovementUnits": "Units",
|
||||
"SW5E.MovementWalk": "Walk",
|
||||
"SW5E.Name": "Character Name",
|
||||
"SW5E.NewDay": "Is New Day?",
|
||||
"SW5E.NewDayHint": "Recover limited use abilities which recharge \"per day\"?",
|
||||
"SW5E.NoCharges": "No Charges",
|
||||
"SW5E.None": "None",
|
||||
"SW5E.NoPowerLevels": "This character has no powercaster levels, but you may add powers manually.",
|
||||
|
@ -748,7 +847,12 @@
|
|||
"SW5E.PowerCreate": "Create Power",
|
||||
"SW5E.PowerDC": "Power DC",
|
||||
"SW5E.PowerDetails": "Power Details",
|
||||
"SW5E.PowerDiceRecovery": "Power Dice Recovery",
|
||||
"SW5E.PowerDie": "Power Die",
|
||||
"SW5E.PowerDieAlloc": "Power Die Allocation",
|
||||
"SW5E.PowerDiePl": "Power Dice",
|
||||
"SW5E.PowerEffects": "Power Effects",
|
||||
"SW5E.PowerfulCritical": "Powerful Critical",
|
||||
"SW5E.PowerLevel": "Power Level",
|
||||
"SW5E.PowerLevel0": "At-Will",
|
||||
"SW5E.PowerLevel1": "1st Level",
|
||||
|
@ -778,6 +882,7 @@
|
|||
"SW5E.PowerProgression": "Power Progression",
|
||||
"SW5E.PowerProgSct": "Scout",
|
||||
"SW5E.PowerProgSnt": "Sentinel",
|
||||
"SW5E.PowerRouting": "Power Routing",
|
||||
"SW5E.PowerSchool": "Power School",
|
||||
"SW5E.PowersKnown": "Powers Known",
|
||||
"SW5E.PowerTarget": "Power Target",
|
||||
|
@ -789,21 +894,29 @@
|
|||
"SW5E.Proficient": "Proficient",
|
||||
"SW5E.Quantity": "Quantity",
|
||||
"SW5E.Range": "Range",
|
||||
"SW5E.Rank": "Rank",
|
||||
"SW5E.RankPl": "Ranks",
|
||||
"SW5E.Rarity": "Rarity",
|
||||
"SW5E.Reaction": "Reaction",
|
||||
"SW5E.ReactionPl": "Reactions",
|
||||
"SW5E.Recharge": "Recharge",
|
||||
"SW5E.Refitting": "Refitting",
|
||||
"SW5E.Refuel": "Refuel",
|
||||
"SW5E.RegenerationRateCoefficient": "Regeneration Rate Coefficient",
|
||||
"SW5E.RequiredMaterials": "Required Materials",
|
||||
"SW5E.Requirements": "Requirements",
|
||||
"SW5E.ResourcesAndTraits": "Resources & Traits",
|
||||
"SW5E.ResourcePrimary": "Resource 1",
|
||||
"SW5E.ResourcesAndTraits": "Resources & Traits",
|
||||
"SW5E.ResourceSecondary": "Resource 2",
|
||||
"SW5E.ResourceTertiary": "Resource 3",
|
||||
"SW5E.Rest": "Rest",
|
||||
"SW5E.RestL": "L. Rest",
|
||||
"SW5E.RestS": "S. Rest",
|
||||
"SW5E.Ritual": "Ritual",
|
||||
"SW5E.Role": "Role",
|
||||
"SW5E.RolePl": "Roles",
|
||||
"SW5E.Roll": "Roll",
|
||||
"SW5E.RollExample": "e.g. +1d4",
|
||||
"SW5E.RollExample": "e.g. 1d4",
|
||||
"SW5E.RollMode": "Roll Mode",
|
||||
"SW5E.RollSituationalBonus": "Situational Bonus?",
|
||||
"SW5E.Save": "Save",
|
||||
|
@ -816,6 +929,7 @@
|
|||
"SW5E.SchoolLgt": "Light",
|
||||
"SW5E.SchoolTec": "Tech",
|
||||
"SW5E.SchoolUni": "Universal",
|
||||
"SW5E.SelectItemsPromptTitle": "Select Items",
|
||||
"SW5E.SenseBlindsight": "Blindsight",
|
||||
"SW5E.SenseBS": "Blindsight",
|
||||
"SW5E.SenseDarkvision": "Darkvision",
|
||||
|
@ -834,6 +948,10 @@
|
|||
"SW5E.SheetClassNPC": "Default NPC Sheet",
|
||||
"SW5E.SheetClassNPCOld": "Old NPC Sheet",
|
||||
"SW5E.SheetClassVehicle": "Default Vehicle Sheet",
|
||||
"SW5E.ShieldDice": "Shield Dice",
|
||||
"SW5E.ShieldPoints": "Shield Points",
|
||||
"SW5E.ShieldPointsFormula": "Shield Points Formula",
|
||||
"SW5E.ShieldRegen": "Regen",
|
||||
"SW5E.ShortRest": "Short Rest",
|
||||
"SW5E.ShortRestEpic": "Short Rest (5 minutes)",
|
||||
"SW5E.ShortRestGritty": "Short Rest (8 hours)",
|
||||
|
@ -873,6 +991,7 @@
|
|||
"SW5E.SkillSte": "Stealth",
|
||||
"SW5E.SkillSur": "Survival",
|
||||
"SW5E.SkillTec": "Technology",
|
||||
"SW5E.Skip": "Skip",
|
||||
"SW5E.Slots": "Slots",
|
||||
"SW5E.Source": "Source",
|
||||
"SW5E.Special": "Special",
|
||||
|
@ -882,8 +1001,62 @@
|
|||
"SW5E.SpeciesTraits": "Species Traits",
|
||||
"SW5E.Speed": "Speed",
|
||||
"SW5E.SpeedSpecial": "Special Movement",
|
||||
"SW5E.StarshipAmbassador": "Ambassador",
|
||||
"SW5E.StarshipArmorandShieldProps": "Starship Armor & Shield Properties",
|
||||
"SW5E.StarshipArmorandShields": "Starship Armor and Shields",
|
||||
"SW5E.StarshipBattleship": "Battleship",
|
||||
"SW5E.StarshipBlockadeShip": "Blockade Ship",
|
||||
"SW5E.StarshipBomber": "Bomber",
|
||||
"SW5E.StarshipCarrier": "Carrier",
|
||||
"SW5E.StarshipColonizer": "Colonizer",
|
||||
"SW5E.StarshipCommandShip": "Command Ship",
|
||||
"SW5E.StarshipCorvette": "Corvette",
|
||||
"SW5E.StarshipCourier": "Courier",
|
||||
"SW5E.StarshipCruiser": "Cruiser",
|
||||
"SW5E.StarshipEquipment": "Starship Equipment",
|
||||
"SW5E.StarshipEquipmentProps": "Starship Equipment Properties",
|
||||
"SW5E.StarshipExplorer": "Explorer",
|
||||
"SW5E.StarshipfeaturePl": "Starship Features",
|
||||
"SW5E.StarshipFlagship": "Flagship",
|
||||
"SW5E.StarshipFreighter": "Freighter",
|
||||
"SW5E.StarshipGunboat": "Gunboat",
|
||||
"SW5E.StarshipIndustrialCenter": "Industrial Center",
|
||||
"SW5E.StarshipInterceptor": "Interceptor",
|
||||
"SW5E.StarshipInterdictor": "Interdictor",
|
||||
"SW5E.StarshipJuggernaut": "Juggernaut",
|
||||
"SW5E.StarshipMissileBoat": "Missile Boat",
|
||||
"SW5E.StarshipMobileMetropolis": "Mobile Metropolis",
|
||||
"SW5E.StarshipmodPl": "Starship Modifications",
|
||||
"SW5E.StarshipNavigator": "Navigator",
|
||||
"SW5E.StarshipPicketShip": "Picket Ship",
|
||||
"SW5E.StarshipResearcher": "Researcher",
|
||||
"SW5E.StarshipScout": "Scout",
|
||||
"SW5E.StarshipScrambler": "Scrambler",
|
||||
"SW5E.StarshipShipsTender": "Ship's Tender",
|
||||
"SW5E.StarshipShuttle": "Shuttle",
|
||||
"SW5E.StarshipSkillAst": "Astrogation",
|
||||
"SW5E.StarshipSkillBst": "Boost",
|
||||
"SW5E.StarshipSkillDat": "Data",
|
||||
"SW5E.StarshipSkillHid": "Hide",
|
||||
"SW5E.StarshipSkillImp": "Impress",
|
||||
"SW5E.StarshipSkillInt": "Interfere",
|
||||
"SW5E.StarshipSkillMan": "Maneuvering",
|
||||
"SW5E.StarshipSkillMen": "Menace",
|
||||
"SW5E.StarshipSkillPat": "Patch",
|
||||
"SW5E.StarshipSkillPrb": "Probe",
|
||||
"SW5E.StarshipSkillRam": "Ram",
|
||||
"SW5E.StarshipSkillReg": "Regulation",
|
||||
"SW5E.StarshipSkillScn": "Scan",
|
||||
"SW5E.StarshipSkillSwn": "Swindle",
|
||||
"SW5E.StarshipStrikeFighter": "Strike Fighter",
|
||||
"SW5E.StarshipTier": "Tier",
|
||||
"SW5E.StarshipWarship": "Warship",
|
||||
"SW5E.StarshipYacht": "Yacht",
|
||||
"SW5E.StealthDisadvantage": "Stealth Disadvantage",
|
||||
"SW5E.SuiteCap": "Suite Capacity",
|
||||
"SW5E.Supply": "Supply",
|
||||
"SW5E.SysStorageCapacity": "System Storage Capacity",
|
||||
"SW5E.SystemDrainage": "System Drainage",
|
||||
"SW5E.Target": "Target",
|
||||
"SW5E.TargetAlly": "Ally",
|
||||
"SW5E.TargetCone": "Cone",
|
||||
|
@ -899,6 +1072,7 @@
|
|||
"SW5E.TargetSpace": "Space",
|
||||
"SW5E.TargetSphere": "Sphere",
|
||||
"SW5E.TargetSquare": "Square",
|
||||
"SW5E.TargetStarship": "Starship",
|
||||
"SW5E.TargetWall": "Wall",
|
||||
"SW5E.TargetWeapon": "Weapon",
|
||||
"SW5E.TargetWidth": "Line Width",
|
||||
|
@ -954,7 +1128,8 @@
|
|||
"SW5E.TraitToolProf": "Tool Proficiencies",
|
||||
"SW5E.TraitWeaponProf": "Weapon Proficiencies",
|
||||
"SW5E.Type": "Type",
|
||||
"SW5E.Unequipped": "Not Equipped",
|
||||
"SW5E.Uncrewed": "Uncrewed",
|
||||
"SW5E.Unequipped": "Unequipped",
|
||||
"SW5E.UniversalPowerDC": "Universal Power DC",
|
||||
"SW5E.Unlimited": "Unlimited",
|
||||
"SW5E.Usage": "Usage",
|
||||
|
@ -978,50 +1153,85 @@
|
|||
"SW5E.VersatileDamage": "Versatile Damage",
|
||||
"SW5E.VsDC": "vs DC.",
|
||||
"SW5E.WeaponAmmo": "Ammunition",
|
||||
"SW5E.WeaponBlasterPistolProficiency": "Blaster Pistol",
|
||||
"SW5E.WeaponChakramProficiency": "Chakrams",
|
||||
"SW5E.WeaponDoubleBladeProficiency": "Doubleblade",
|
||||
"SW5E.WeaponDoubleSaberProficiency": "Doublesaber",
|
||||
"SW5E.WeaponDoubleShotoProficiency": "Doubleshoto",
|
||||
"SW5E.WeaponDoubleSwordProficiency": "Doublesword",
|
||||
"SW5E.WeaponHiddenBladeProficiency": "Hidden Blade",
|
||||
"SW5E.WeaponImprov": "Improvised",
|
||||
"SW5E.WeaponImprovisedProficiency": "Improvised Weapons",
|
||||
"SW5E.WeaponLightFoilProficiency": "Lightfoil",
|
||||
"SW5E.WeaponLightRingProficiency": "Light Ring",
|
||||
"SW5E.WeaponMartialB": "Martial Blaster",
|
||||
"SW5E.WeaponMartialBlasterProficiency": "Martial Blasters",
|
||||
"SW5E.WeaponMartialLightweaponProficiency": "Martial Lightweapons",
|
||||
"SW5E.WeaponMartialLW": "Martial Lightweapon",
|
||||
"SW5E.WeaponMartialProficiency": "Martial Weapons",
|
||||
"SW5E.WeaponMartialVibroweaponProficiency": "Martial Vibroweapons",
|
||||
"SW5E.WeaponMartialVW": "Martial Vibroweapon",
|
||||
"SW5E.WeaponNatural": "Natural",
|
||||
"SW5E.WeaponNaturalProficiency": "Natural Weapons",
|
||||
"SW5E.WeaponPrimarySW": "Primary (Starship)",
|
||||
"SW5E.WeaponPropertiesAmm": "Ammunition",
|
||||
"SW5E.WeaponPropertiesAut": "Auto",
|
||||
"SW5E.WeaponPropertiesBur": "Burst",
|
||||
"SW5E.WeaponPropertiesCon": "Constitution",
|
||||
"SW5E.WeaponPropertiesDef": "Defensive",
|
||||
"SW5E.WeaponPropertiesDex": "Dexterity Rqmt.",
|
||||
"SW5E.WeaponPropertiesDex": "Dexterity Rqt",
|
||||
"SW5E.WeaponPropertiesDgd": "Disguised",
|
||||
"SW5E.WeaponPropertiesDir": "Dire",
|
||||
"SW5E.WeaponPropertiesDis": "Disintegrate",
|
||||
"SW5E.WeaponPropertiesDou": "Double",
|
||||
"SW5E.WeaponPropertiesDpt": "Disruptive",
|
||||
"SW5E.WeaponPropertiesDrm": "Disarming",
|
||||
"SW5E.WeaponPropertiesExp": "Explosive",
|
||||
"SW5E.WeaponPropertiesFin": "Finesse",
|
||||
"SW5E.WeaponPropertiesFix": "Fixed",
|
||||
"SW5E.WeaponPropertiesFoc": "Focus",
|
||||
"SW5E.WeaponPropertiesHid": "Hidden",
|
||||
"SW5E.WeaponPropertiesHom": "Homing",
|
||||
"SW5E.WeaponPropertiesHvy": "Heavy",
|
||||
"SW5E.WeaponPropertiesIon": "Ionizing",
|
||||
"SW5E.WeaponPropertiesKen": "Keen",
|
||||
"SW5E.WeaponPropertiesLgt": "Light",
|
||||
"SW5E.WeaponPropertiesLum": "Luminous",
|
||||
"SW5E.WeaponPropertiesMig": "Mighty",
|
||||
"SW5E.WeaponPropertiesMlt": "Melt",
|
||||
"SW5E.WeaponPropertiesOvr": "Overheat",
|
||||
"SW5E.WeaponPropertiesPic": "Piercing",
|
||||
"SW5E.WeaponPropertiesPow": "Power",
|
||||
"SW5E.WeaponPropertiesRan": "Range",
|
||||
"SW5E.WeaponPropertiesRap": "Rapid",
|
||||
"SW5E.WeaponPropertiesRch": "Reach",
|
||||
"SW5E.WeaponPropertiesRel": "Reload",
|
||||
"SW5E.WeaponPropertiesRet": "Returning",
|
||||
"SW5E.WeaponPropertiesSat": "Saturate",
|
||||
"SW5E.WeaponPropertiesShk": "Shocking",
|
||||
"SW5E.WeaponPropertiesSil": "Silent",
|
||||
"SW5E.WeaponPropertiesSpc": "Special",
|
||||
"SW5E.WeaponPropertiesStr": "Strength Rqmt.",
|
||||
"SW5E.WeaponPropertiesStr": "Strength Rqt",
|
||||
"SW5E.WeaponPropertiesThr": "Thrown",
|
||||
"SW5E.WeaponPropertiesTwo": "Two-Handed",
|
||||
"SW5E.WeaponPropertiesVer": "Versatile",
|
||||
"SW5E.WeaponPropertiesVic": "Vicious",
|
||||
"SW5E.WeaponPropertiesZon": "Zone",
|
||||
"SW5E.WeaponQuaternarySW": "Quaternary (Starship)",
|
||||
"SW5E.WeaponSaberWhipProficiency": "Saberwhip",
|
||||
"SW5E.WeaponSecondarySW": "Secondary (Starship)",
|
||||
"SW5E.WeaponSiege": "Siege",
|
||||
"SW5E.WeaponSimpleB": "Simple Blaster",
|
||||
"SW5E.WeaponSimpleBlasterProficiency": "Simple Blasters",
|
||||
"SW5E.WeaponSimpleLightweaponProficiency": "Simple Lightweapons",
|
||||
"SW5E.WeaponSimpleLW": "Simple Lightweapon",
|
||||
"SW5E.WeaponSimpleProficiency": "Simple Weapons",
|
||||
"SW5E.WeaponSimpleVibroweaponProficiency": "Simple Vibroweapons",
|
||||
"SW5E.WeaponSimpleVW": "Simple Vibroweapon",
|
||||
"SW5E.WeaponSizeAbb": "Size",
|
||||
"SW5E.WeaponTechbladeProficiency": "Techblades",
|
||||
"SW5E.WeaponTertiarySW": "Tertiary (Starship)",
|
||||
"SW5E.WeaponVibrorapierProficiency": "Vibrorapier",
|
||||
"SW5E.WeaponVibrowhipProficiency": "Vibrowhip",
|
||||
"SW5E.Weight": "Weight"
|
||||
}
|
|
@ -339,9 +339,9 @@
|
|||
"SW5E.FlagsArmorIntegration": "Intégration d'armure",
|
||||
"SW5E.FlagsArmorIntegrationHint": "Chaque fois que vous effectuez un test de Charisme (Persuasion) impliquant du marchandage, vous êtes considéré comme ayant une expertise dans la compétence Persuasion.",
|
||||
"SW5E.FlagsBusinessSavvy": "Sens des affaires",
|
||||
"SW5E.FlagsBusinessSavvyHint":"Chaque fois que vous effectuez un test de Charisme (Persuasion) impliquant du marchandage, vous êtes considéré comme ayant une expertise dans la compétence Persuasion.",
|
||||
"SW5E.FlagsBusinessSavvyHint": "Chaque fois que vous effectuez un test de Charisme (Persuasion) impliquant du marchandage, vous êtes considéré comme ayant une expertise dans la compétence Persuasion.",
|
||||
"SW5E.FlagsCannibalize": "Cannibale",
|
||||
"SW5E.FlagsCannibalizeHint":"Si vous passez au moins 1 minute à dévorer le cadavre d'une bête ou d'un humanoïde, vous gagnez des points de vie temporaires égaux à votre modificateur de Constitution. Une fois que vous avez utilisé cette fonctionnalité, vous devez effectuer un repos court ou long avant de pouvoir l'utiliser à nouveau.",
|
||||
"SW5E.FlagsCannibalizeHint": "Si vous passez au moins 1 minute à dévorer le cadavre d'une bête ou d'un humanoïde, vous gagnez des points de vie temporaires égaux à votre modificateur de Constitution. Une fois que vous avez utilisé cette fonctionnalité, vous devez effectuer un repos court ou long avant de pouvoir l'utiliser à nouveau.",
|
||||
"SW5E.FlagsClosedMind": "Esprit Fortifié",
|
||||
"SW5E.FlagsClosedMindHint": "Les membres de votre espèce ont une harmonisation naturelle avec la Force, ce qui les rend résistants à ses pouvoirs. Vous avez l'avantage aux jets de sauvegarde de Sagesse et de Charisme contre les pouvoirs de force.",
|
||||
"SW5E.FlagsCrudeWeaponSpecialists": "Spécialiste des armes improvisées",
|
||||
|
@ -359,7 +359,7 @@
|
|||
"SW5E.FlagsForceContention": "Force Contention",
|
||||
"SW5E.FlagsForceContentionHint": "En raison de leur physiologie unique, les membres de votre espèce présentent une rusticité qui leur permet de surmonter l'utilisation de la Force. Vous avez un avantage aux jets de sauvegarde de Force et de Constitution contre les pouvoirs de la force.",
|
||||
"SW5E.FlagsForceInsensitive": "Insensible à la Force",
|
||||
"SW5E.FlagsForceInsensitiveHint" : "Bien que les droïdes puissent être manipulés par de nombreux pouvoirs de la force, ils ne peuvent pas sentir la Force. Vous ne pouvez pas utiliser de pouvoirs de la force ou prendre des niveaux dans les classes d'utilisateurs de la force.",
|
||||
"SW5E.FlagsForceInsensitiveHint": "Bien que les droïdes puissent être manipulés par de nombreux pouvoirs de la force, ils ne peuvent pas sentir la Force. Vous ne pouvez pas utiliser de pouvoirs de la force ou prendre des niveaux dans les classes d'utilisateurs de la force.",
|
||||
"SW5E.FlagsForeignBiology": "Biologie Étrangère",
|
||||
"SW5E.FlagsForeignBiologyHint": "Vous portez un appareil respiratoire parce que de nombreuses atmosphères dans la galaxie diffèrent de celle de votre monde natal. Si votre appareil est retiré alors que vous êtes dans un tel environnement, vous perdez connaissance.",
|
||||
"SW5E.FlagsFuryOfTheSmall": "Rage des Petits",
|
||||
|
@ -382,7 +382,7 @@
|
|||
"SW5E.FlagsMaintenanceMode": "Maintenance Mode",
|
||||
"SW5E.FlagsMaintenanceModeHint": "Plutôt que de dormir, vous entrez dans un état inactif pour effectuer une maintenance de routine pendant 4 heures par jour. Vous avez un désavantage aux tests de Sagesse (Perception) lors de la maintenance.",
|
||||
"SW5E.FlagsMaskOfTheWild": "Cachette Naturelle",
|
||||
"SW5E.FlagsMaskOfTheWildHint":"Vous pouvez tenter de vous cacher dans une zone à visibilité réduite, comme en présence de branchages, de forte pluie, de neige qui tombe, de brume ou autre phénomène naturel.",
|
||||
"SW5E.FlagsMaskOfTheWildHint": "Vous pouvez tenter de vous cacher dans une zone à visibilité réduite, comme en présence de branchages, de forte pluie, de neige qui tombe, de brume ou autre phénomène naturel.",
|
||||
"SW5E.FlagsMeleeCriticalDice": "Dé de dégâts pour les coups critiques au corps-à-corps",
|
||||
"SW5E.FlagsMeleeCriticalDiceHint": "Un certain nombre de dés de dégâts supplémentaires ont été ajoutés aux coups critiques des armes de corps-à-corps.",
|
||||
"SW5E.FlagsMultipleHearts": "Plusieurs Coeurs",
|
||||
|
|
1027
lang/it.json
Normal file
|
@ -71,7 +71,7 @@
|
|||
}
|
||||
|
||||
// Movement Configuration
|
||||
.movement {
|
||||
.movement, .hit-dice {
|
||||
h4.attribute-name {
|
||||
position: relative;
|
||||
}
|
||||
|
@ -655,6 +655,15 @@
|
|||
// Empty powerbook controls
|
||||
.powerbook-empty .item-controls { flex: 1; }
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* Features Tab */
|
||||
/* ----------------------------------------- */
|
||||
|
||||
// Original class icon
|
||||
.features i.original-class {
|
||||
color: #4b4a44
|
||||
}
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* TinyMCE */
|
||||
/* ----------------------------------------- */
|
||||
|
|
|
@ -10,9 +10,7 @@
|
|||
|
||||
.sw5e {
|
||||
.window-content {
|
||||
background: @sheetBackground;
|
||||
font-size: 13px;
|
||||
color: @colorDark;
|
||||
}
|
||||
|
||||
/* ----------------------------------------- */
|
||||
|
@ -44,6 +42,8 @@
|
|||
select:disabled,
|
||||
textarea:disabled {
|
||||
color: @colorOlive;
|
||||
border: 1px solid transparent !important;
|
||||
outline: none !important;
|
||||
&:hover,
|
||||
&:focus {
|
||||
box-shadow: none !important;
|
||||
|
@ -58,28 +58,6 @@
|
|||
border: @borderGroove;
|
||||
}
|
||||
|
||||
// Checkbox Labels
|
||||
// TODO: THIS CAN BE MOSTLY REMOVED NOW THAT IT IS IN CORE, see core forms.less
|
||||
label.checkbox {
|
||||
flex: auto;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
height: 22px;
|
||||
line-height: 22px;
|
||||
font-size: 11px;
|
||||
> input[type="checkbox"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 0 2px 0 0;
|
||||
position: relative;
|
||||
top: 4px;
|
||||
}
|
||||
&.right > input[type="checkbox"] {
|
||||
margin: 0 0 0 2px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Form Groups */
|
||||
.form-group {
|
||||
label {
|
||||
|
@ -98,11 +76,12 @@
|
|||
|
||||
// Stacked Groups
|
||||
.form-group.stacked {
|
||||
label {
|
||||
> label {
|
||||
flex: 0 0 100%;
|
||||
margin: 0;
|
||||
}
|
||||
label.checkbox {
|
||||
label.checkbox,
|
||||
label.radio {
|
||||
flex: auto;
|
||||
text-align: left;
|
||||
}
|
||||
|
@ -131,6 +110,34 @@
|
|||
}
|
||||
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* Hit Dice Config Sheet Specifically */
|
||||
/* ----------------------------------------- */
|
||||
|
||||
.sw5e.hd-config {
|
||||
.form-group {
|
||||
button.increment, button.decrement {
|
||||
flex: 0 0 1rem;
|
||||
line-height: 1rem;
|
||||
}
|
||||
|
||||
button.decrement {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
span.sep {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 0 0 2rem;
|
||||
text-align: center;
|
||||
margin-left: 2px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* Entity Sheets Specifically */
|
||||
/* ----------------------------------------- */
|
||||
|
@ -475,7 +482,7 @@
|
|||
/* Trait Selector
|
||||
/* ----------------------------------------- */
|
||||
|
||||
#trait-selector {
|
||||
.trait-selector {
|
||||
.trait-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
|
@ -488,6 +495,59 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* Actor Type Config Sheet Specifically */
|
||||
/* ----------------------------------------- */
|
||||
|
||||
.actor-type {
|
||||
.trait-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
li {
|
||||
flex-basis: 50%;
|
||||
flex-grow: 1;
|
||||
}
|
||||
li.form-group {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
}
|
||||
label.radio {
|
||||
display: flex;
|
||||
flex: auto;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
font-weight: normal;
|
||||
> input[type="radio"] {
|
||||
margin: 0 5px 0 0;
|
||||
}
|
||||
}
|
||||
li.custom-type input[type="radio"] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* Add Feature Prompt Specifically */
|
||||
/* ----------------------------------------- */
|
||||
|
||||
.sw5e.select-items-prompt {
|
||||
.dialog-content {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.items-list {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.item-name > label, .item-image, input {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.item-name > label {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* HUD
|
||||
/* ----------------------------------------- */
|
||||
|
|
|
@ -89,7 +89,7 @@
|
|||
|
||||
// Custom Resources
|
||||
.resource .attribute-value {
|
||||
input {
|
||||
> input {
|
||||
flex: 0 0 25%;
|
||||
}
|
||||
label.recharge {
|
||||
|
@ -99,6 +99,7 @@
|
|||
font-size: 11px;
|
||||
text-align: center;
|
||||
color: @colorOlive;
|
||||
align-items: center;
|
||||
input[type="checkbox"] {
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
|
|
|
@ -106,17 +106,17 @@
|
|||
&:nth-child(even) {
|
||||
width: 150px;
|
||||
margin: 0.5em 0.5em;
|
||||
padding: 0px 10px 0px 10px;
|
||||
padding: 0 10px 0 10px;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
thead {
|
||||
border-bottom: 0px;
|
||||
border-bottom: 0;
|
||||
}
|
||||
th {
|
||||
color: #000000;
|
||||
text-shadow: none;
|
||||
border-bottom: 0px;
|
||||
border-bottom: 0;
|
||||
background-color: #bdc8cc;
|
||||
text-transform: none;
|
||||
font-weight: bold;
|
||||
|
@ -129,7 +129,7 @@
|
|||
&:nth-child(even) {
|
||||
width: 150px;
|
||||
margin: 0.5em 0.5em;
|
||||
padding: 0px 10px 0px 10px;
|
||||
padding: 0 10px 0 10px;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
@ -137,7 +137,7 @@
|
|||
.medtable {
|
||||
table {
|
||||
width: 500px;
|
||||
border: 0px;
|
||||
border: 0;
|
||||
margin: 0.5em 0.5em;
|
||||
}
|
||||
td {
|
||||
|
@ -149,17 +149,17 @@
|
|||
&:nth-child(even) {
|
||||
width: 450px;
|
||||
margin: 0.5em 0.5em;
|
||||
padding: 0px 10px 0px 0px;
|
||||
padding: 0 10px 0 0;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
thead {
|
||||
border-bottom: 0px;
|
||||
border-bottom: 0;
|
||||
}
|
||||
th {
|
||||
color: #000000;
|
||||
text-shadow: none;
|
||||
border-bottom: 0px;
|
||||
border-bottom: 0;
|
||||
background-color: #bdc8cc;
|
||||
text-transform: none;
|
||||
font-weight: bold;
|
||||
|
@ -174,8 +174,8 @@
|
|||
}
|
||||
.classtable {
|
||||
blockquote {
|
||||
border-left: 0px;
|
||||
border-right: 0px;
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
background-color: #bdc8cc;
|
||||
width: 600px;
|
||||
h3 {
|
||||
|
@ -189,8 +189,8 @@
|
|||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border-left: 0px;
|
||||
border-right: 0px;
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
border-top: 0;
|
||||
border-bottom: 0;
|
||||
margin: 0.5em 0;
|
||||
|
@ -200,7 +200,7 @@
|
|||
thead {
|
||||
color: #000000;
|
||||
text-shadow: none;
|
||||
border-bottom: 0px;
|
||||
border-bottom: 0;
|
||||
background-color: #bdc8cc;
|
||||
text-transform: none;
|
||||
font-style: normal;
|
||||
|
@ -209,7 +209,7 @@
|
|||
th {
|
||||
color: #000000;
|
||||
text-shadow: none;
|
||||
border-bottom: 0px;
|
||||
border-bottom: 0;
|
||||
background-color: #bdc8cc;
|
||||
text-transform: none;
|
||||
font-style: normal;
|
||||
|
@ -246,7 +246,7 @@
|
|||
width: 100%;
|
||||
line-height: 18px;
|
||||
margin-bottom: 15px;
|
||||
border: 0 0 0 0;
|
||||
border: 0;
|
||||
border-bottom: none;
|
||||
overflow-x: auto;
|
||||
tbody {
|
||||
|
|
|
@ -30,5 +30,28 @@
|
|||
|
||||
.summary {
|
||||
font-size: 18px;
|
||||
|
||||
li.creature-type {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 1em;
|
||||
padding: 0 3px;
|
||||
|
||||
span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.config-button {
|
||||
display: none;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
line-height: 2em;
|
||||
}
|
||||
&:hover .config-button {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -140,6 +140,7 @@
|
|||
height: auto;
|
||||
.russoOne(17px);
|
||||
line-height: 24px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.proficiency {
|
||||
|
@ -184,7 +185,7 @@
|
|||
display: inline-block;
|
||||
text-align: right;
|
||||
|
||||
padding: 0px 3px;
|
||||
padding: 0 3px;
|
||||
|
||||
&:last-child {
|
||||
text-align: left;
|
||||
|
@ -779,7 +780,7 @@
|
|||
display: block;
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
padding: 0px 3px;
|
||||
padding: 0 3px;
|
||||
&:last-child {
|
||||
text-align: left;
|
||||
}
|
||||
|
@ -955,7 +956,7 @@
|
|||
display: block;
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
padding: 0px 3px;
|
||||
padding: 0 3px;
|
||||
&:last-child {
|
||||
text-align: left;
|
||||
}
|
||||
|
@ -1053,10 +1054,35 @@
|
|||
h1.character-name {
|
||||
align-self: auto;
|
||||
}
|
||||
.npc-size {
|
||||
.npc-size, .creature-type {
|
||||
.russoOne(18px);
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
div.creature-type {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 1px 4px;
|
||||
border: 1px solid transparent;
|
||||
overflow-x: auto;
|
||||
|
||||
span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.config-button {
|
||||
display: none;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
line-height: 2em;
|
||||
}
|
||||
&:hover .config-button {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.attributes {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
footer {
|
||||
|
|
|
@ -408,6 +408,9 @@
|
|||
&.npc {
|
||||
.swalt-sheet {
|
||||
header {
|
||||
div.creature-type:hover {
|
||||
border-color: @inputBorderFocus;
|
||||
}
|
||||
.experience {
|
||||
color: @actorProficiencyTextColor;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
input[type="text"], input[type="number"], input[type="password"], input[type="date"], input[type="time"], select, textarea {
|
||||
input[type="text"], input[type="number"], input[type="password"], input[type="date"], input[type="time"], select, textarea, .roundTransition {
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s;
|
||||
&:hover {
|
||||
|
|
|
@ -166,6 +166,12 @@
|
|||
.token-name {
|
||||
text-shadow: none;
|
||||
}
|
||||
.ce-image-wrapper {
|
||||
.token-image {
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
h4 {
|
||||
color: @colorBlack;
|
||||
}
|
||||
|
@ -225,7 +231,7 @@
|
|||
padding-bottom: 4px;
|
||||
.folder {
|
||||
& > .folder-header {
|
||||
line-height: default;
|
||||
line-height: initial;
|
||||
padding: 0 0 0 8px;
|
||||
position: relative;
|
||||
border: none;
|
||||
|
|
|
@ -302,7 +302,7 @@
|
|||
}
|
||||
.folder {
|
||||
& > .folder-header {
|
||||
line-height: default;
|
||||
line-height: initial;
|
||||
padding: 0 0 0 8px;
|
||||
position: relative;
|
||||
border: none;
|
||||
|
|
2126
module/actor/old_entity.js
Normal file
|
@ -1,9 +1,11 @@
|
|||
import Item5e from "../../../item/entity.js";
|
||||
import TraitSelector from "../../../apps/trait-selector.js";
|
||||
import ActorSheetFlags from "../../../apps/actor-flags.js";
|
||||
import ActorHitDiceConfig from "../../../apps/hit-dice-config.js";
|
||||
import ActorMovementConfig from "../../../apps/movement-config.js";
|
||||
import ActorSensesConfig from "../../../apps/senses-config.js";
|
||||
import {SW5E} from '../../../config.js';
|
||||
import ActorTypeConfig from "../../../apps/actor-type.js";
|
||||
import {SW5E} from "../../../config.js";
|
||||
import {onManageActiveEffect, prepareActiveEffectCategories} from "../../../effects.js";
|
||||
|
||||
/**
|
||||
|
@ -46,76 +48,96 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A set of item types that should be prevented from being dropped on this type of actor sheet.
|
||||
* @type {Set<string>}
|
||||
*/
|
||||
static unsupportedItemTypes = new Set();
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
get template() {
|
||||
if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/newActor/expanded-limited-sheet.html";
|
||||
if (!game.user.isGM && this.actor.limited)
|
||||
return "systems/sw5e/templates/actors/newActor/expanded-limited-sheet.html";
|
||||
return `systems/sw5e/templates/actors/newActor/${this.actor.data.type}-sheet.html`;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
getData() {
|
||||
|
||||
getData(options) {
|
||||
// Basic data
|
||||
let isOwner = this.entity.owner;
|
||||
let isOwner = this.actor.isOwner;
|
||||
const data = {
|
||||
owner: isOwner,
|
||||
limited: this.entity.limited,
|
||||
limited: this.actor.limited,
|
||||
options: this.options,
|
||||
editable: this.isEditable,
|
||||
cssClass: isOwner ? "editable" : "locked",
|
||||
isCharacter: this.entity.data.type === "character",
|
||||
isNPC: this.entity.data.type === "npc",
|
||||
isVehicle: this.entity.data.type === 'vehicle',
|
||||
isCharacter: this.actor.type === "character",
|
||||
isNPC: this.actor.type === "npc",
|
||||
isStarship: this.actor.type === "starship",
|
||||
isVehicle: this.actor.type === "vehicle",
|
||||
config: CONFIG.SW5E,
|
||||
rollData: this.actor.getRollData.bind(this.actor)
|
||||
};
|
||||
|
||||
// The Actor and its Items
|
||||
data.actor = duplicate(this.actor.data);
|
||||
data.items = this.actor.items.map(i => {
|
||||
i.data.labels = i.labels;
|
||||
return i.data;
|
||||
});
|
||||
// The Actor's data
|
||||
const actorData = this.actor.data.toObject(false);
|
||||
data.actor = actorData;
|
||||
data.data = actorData.data;
|
||||
|
||||
// Owned Items
|
||||
data.items = actorData.items;
|
||||
for (let i of data.items) {
|
||||
const item = this.actor.items.get(i._id);
|
||||
i.labels = item.labels;
|
||||
}
|
||||
data.items.sort((a, b) => (a.sort || 0) - (b.sort || 0));
|
||||
data.data = data.actor.data;
|
||||
|
||||
// Labels and filters
|
||||
data.labels = this.actor.labels || {};
|
||||
data.filters = this._filters;
|
||||
|
||||
// Ability Scores
|
||||
for ( let [a, abl] of Object.entries(data.actor.data.abilities)) {
|
||||
for (let [a, abl] of Object.entries(actorData.data.abilities)) {
|
||||
abl.icon = this._getProficiencyIcon(abl.proficient);
|
||||
abl.hover = CONFIG.SW5E.proficiencyLevels[abl.proficient];
|
||||
abl.label = CONFIG.SW5E.abilities[a];
|
||||
}
|
||||
|
||||
// Skills
|
||||
if (data.actor.data.skills) {
|
||||
for ( let [s, skl] of Object.entries(data.actor.data.skills)) {
|
||||
if (actorData.data.skills) {
|
||||
for (let [s, skl] of Object.entries(actorData.data.skills)) {
|
||||
skl.ability = CONFIG.SW5E.abilityAbbreviations[skl.ability];
|
||||
skl.icon = this._getProficiencyIcon(skl.value);
|
||||
skl.hover = CONFIG.SW5E.proficiencyLevels[skl.value];
|
||||
if (data.actor.type === "starship") {
|
||||
skl.label = CONFIG.SW5E.starshipSkills[s];
|
||||
} else {
|
||||
skl.label = CONFIG.SW5E.skills[s];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Movement speeds
|
||||
data.movement = this._getMovementSpeed(data.actor);
|
||||
data.movement = this._getMovementSpeed(actorData);
|
||||
|
||||
// Senses
|
||||
data.senses = this._getSenses(data.actor);
|
||||
data.senses = this._getSenses(actorData);
|
||||
|
||||
// Update traits
|
||||
this._prepareTraits(data.actor.data.traits);
|
||||
this._prepareTraits(actorData.data.traits);
|
||||
|
||||
// Prepare owned items
|
||||
this._prepareItems(data);
|
||||
|
||||
// Prepare active effects
|
||||
data.effects = prepareActiveEffectCategories(this.entity.effects);
|
||||
data.effects = prepareActiveEffectCategories(this.actor.effects);
|
||||
|
||||
// Return data to the sheet
|
||||
return data
|
||||
return data;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -127,38 +149,42 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
* @returns {{primary: string, special: string}}
|
||||
* @private
|
||||
*/
|
||||
_getMovementSpeed(actorData, largestPrimary=false) {
|
||||
_getMovementSpeed(actorData, largestPrimary = false) {
|
||||
const movement = actorData.data.attributes.movement || {};
|
||||
|
||||
// Prepare an array of available movement speeds
|
||||
let speeds = [
|
||||
[movement.burrow, `${game.i18n.localize("SW5E.MovementBurrow")} ${movement.burrow}`],
|
||||
[movement.climb, `${game.i18n.localize("SW5E.MovementClimb")} ${movement.climb}`],
|
||||
[movement.fly, `${game.i18n.localize("SW5E.MovementFly")} ${movement.fly}` + (movement.hover ? ` (${game.i18n.localize("SW5E.MovementHover")})` : "")],
|
||||
[
|
||||
movement.fly,
|
||||
`${game.i18n.localize("SW5E.MovementFly")} ${movement.fly}` +
|
||||
(movement.hover ? ` (${game.i18n.localize("SW5E.MovementHover")})` : "")
|
||||
],
|
||||
[movement.swim, `${game.i18n.localize("SW5E.MovementSwim")} ${movement.swim}`]
|
||||
]
|
||||
if ( largestPrimary ) {
|
||||
];
|
||||
if (largestPrimary) {
|
||||
speeds.push([movement.walk, `${game.i18n.localize("SW5E.MovementWalk")} ${movement.walk}`]);
|
||||
}
|
||||
|
||||
// Filter and sort speeds on their values
|
||||
speeds = speeds.filter(s => !!s[0]).sort((a, b) => b[0] - a[0]);
|
||||
speeds = speeds.filter((s) => !!s[0]).sort((a, b) => b[0] - a[0]);
|
||||
|
||||
// Case 1: Largest as primary
|
||||
if ( largestPrimary ) {
|
||||
if (largestPrimary) {
|
||||
let primary = speeds.shift();
|
||||
return {
|
||||
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
|
||||
else {
|
||||
return {
|
||||
primary: `${movement.walk || 0} ${movement.units}`,
|
||||
special: speeds.length ? speeds.map(s => s[1]).join(", ") : ""
|
||||
}
|
||||
special: speeds.length ? speeds.map((s) => s[1]).join(", ") : ""
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -167,12 +193,12 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
_getSenses(actorData) {
|
||||
const senses = actorData.data.attributes.senses || {};
|
||||
const tags = {};
|
||||
for ( let [k, label] of Object.entries(CONFIG.SW5E.senses) ) {
|
||||
const v = senses[k] ?? 0
|
||||
if ( v === 0 ) continue;
|
||||
for (let [k, label] of Object.entries(CONFIG.SW5E.senses)) {
|
||||
const v = senses[k] ?? 0;
|
||||
if (v === 0) continue;
|
||||
tags[k] = `${game.i18n.localize(label)} ${v} ${senses.units}`;
|
||||
}
|
||||
if ( !!senses.special ) tags["special"] = senses.special;
|
||||
if (!!senses.special) tags["special"] = senses.special;
|
||||
return tags;
|
||||
}
|
||||
|
||||
|
@ -185,20 +211,20 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
*/
|
||||
_prepareTraits(traits) {
|
||||
const map = {
|
||||
"dr": CONFIG.SW5E.damageResistanceTypes,
|
||||
"di": CONFIG.SW5E.damageResistanceTypes,
|
||||
"dv": CONFIG.SW5E.damageResistanceTypes,
|
||||
"ci": CONFIG.SW5E.conditionTypes,
|
||||
"languages": CONFIG.SW5E.languages,
|
||||
"armorProf": CONFIG.SW5E.armorProficiencies,
|
||||
"weaponProf": CONFIG.SW5E.weaponProficiencies,
|
||||
"toolProf": CONFIG.SW5E.toolProficiencies
|
||||
dr: CONFIG.SW5E.damageResistanceTypes,
|
||||
di: CONFIG.SW5E.damageResistanceTypes,
|
||||
dv: CONFIG.SW5E.damageResistanceTypes,
|
||||
ci: CONFIG.SW5E.conditionTypes,
|
||||
languages: CONFIG.SW5E.languages,
|
||||
armorProf: CONFIG.SW5E.armorProficiencies,
|
||||
weaponProf: CONFIG.SW5E.weaponProficiencies,
|
||||
toolProf: CONFIG.SW5E.toolProficiencies
|
||||
};
|
||||
for ( let [t, choices] of Object.entries(map) ) {
|
||||
for (let [t, choices] of Object.entries(map)) {
|
||||
const trait = traits[t];
|
||||
if ( !trait ) continue;
|
||||
if (!trait) continue;
|
||||
let values = [];
|
||||
if ( trait.value ) {
|
||||
if (trait.value) {
|
||||
values = trait.value instanceof Array ? trait.value : [trait.value];
|
||||
}
|
||||
trait.selected = values.reduce((obj, t) => {
|
||||
|
@ -207,8 +233,8 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
}, {});
|
||||
|
||||
// Add custom entry
|
||||
if ( trait.custom ) {
|
||||
trait.custom.split(";").forEach((c, i) => trait.selected[`custom${i+1}`] = c.trim());
|
||||
if (trait.custom) {
|
||||
trait.custom.split(";").forEach((c, i) => (trait.selected[`custom${i + 1}`] = c.trim()));
|
||||
}
|
||||
trait.cssClass = !isObjectEmpty(trait.selected) ? "" : "inactive";
|
||||
}
|
||||
|
@ -220,17 +246,18 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
* Insert a power into the powerbook object when rendering the character sheet
|
||||
* @param {Object} data The Actor data being prepared
|
||||
* @param {Array} powers The power data being prepared
|
||||
* @param {string} school The school of the powerbook being prepared
|
||||
* @private
|
||||
*/
|
||||
_preparePowerbook(data, powers, school) {
|
||||
const owner = this.actor.owner;
|
||||
const owner = this.actor.isOwner;
|
||||
const levels = data.data.powers;
|
||||
const powerbook = {};
|
||||
|
||||
// Define some mappings
|
||||
const sections = {
|
||||
"atwill": -20,
|
||||
"innate": -10,
|
||||
atwill: -20,
|
||||
innate: -10
|
||||
};
|
||||
|
||||
// Label power slot uses headers
|
||||
|
@ -241,32 +268,37 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
};
|
||||
|
||||
// Format a powerbook entry for a certain indexed level
|
||||
const registerSection = (sl, i, label, {prepMode="prepared", value, max, override}={}) => {
|
||||
const registerSection = (sl, i, label, {prepMode = "prepared", value, max, override} = {}) => {
|
||||
powerbook[i] = {
|
||||
order: i,
|
||||
label: label,
|
||||
usesSlots: i > 0,
|
||||
canCreate: owner,
|
||||
canPrepare: (data.actor.type === "character") && (i >= 1),
|
||||
canPrepare: data.actor.type === "character" && i >= 1,
|
||||
powers: [],
|
||||
uses: useLabels[i] || value || 0,
|
||||
slots: useLabels[i] || max || 0,
|
||||
override: override || 0,
|
||||
dataset: {"type": "power", "level": prepMode in sections ? 1 : i, "preparation.mode": prepMode, "school": school},
|
||||
dataset: {
|
||||
"type": "power",
|
||||
"level": prepMode in sections ? 1 : i,
|
||||
"preparation.mode": prepMode,
|
||||
"school": school
|
||||
},
|
||||
prop: sl
|
||||
};
|
||||
};
|
||||
|
||||
// Determine the maximum power level which has a slot
|
||||
const maxLevel = Array.fromRange(10).reduce((max, i) => {
|
||||
if ( i === 0 ) return max;
|
||||
if (i === 0) return max;
|
||||
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;
|
||||
}, 0);
|
||||
|
||||
// Level-based powercasters have cantrips and leveled slots
|
||||
if ( maxLevel > 0 ) {
|
||||
if (maxLevel > 0) {
|
||||
registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]);
|
||||
for (let lvl = 1; lvl <= maxLevel; lvl++) {
|
||||
const sl = `power${lvl}`;
|
||||
|
@ -275,15 +307,15 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
}
|
||||
|
||||
// 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";
|
||||
let s = power.data.level || 0;
|
||||
const sl = `power${s}`;
|
||||
|
||||
// Specialized powercasting modes (if they exist)
|
||||
if ( mode in sections ) {
|
||||
if (mode in sections) {
|
||||
s = sections[mode];
|
||||
if ( !powerbook[s] ){
|
||||
if (!powerbook[s]) {
|
||||
const l = levels[mode] || {};
|
||||
const config = CONFIG.SW5E.powerPreparationModes[mode];
|
||||
registerSection(mode, s, config, {
|
||||
|
@ -296,7 +328,7 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
}
|
||||
|
||||
// Sections for higher-level powers which the caster "should not" have, but power items exist for
|
||||
else if ( !powerbook[s] ) {
|
||||
else if (!powerbook[s]) {
|
||||
registerSection(sl, s, CONFIG.SW5E.powerLevels[s], {levels: levels[sl]});
|
||||
}
|
||||
|
||||
|
@ -318,32 +350,33 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
* @private
|
||||
*/
|
||||
_filterItems(items, filters) {
|
||||
return items.filter(item => {
|
||||
return items.filter((item) => {
|
||||
const data = item.data;
|
||||
|
||||
// Action usage
|
||||
for ( let f of ["action", "bonus", "reaction"] ) {
|
||||
if ( filters.has(f) ) {
|
||||
if ((data.activation && (data.activation.type !== f))) return false;
|
||||
for (let f of ["action", "bonus", "reaction"]) {
|
||||
if (filters.has(f)) {
|
||||
if (data.activation && data.activation.type !== f) return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Power-specific filters
|
||||
if ( filters.has("ritual") ) {
|
||||
if (filters.has("ritual")) {
|
||||
if (data.components.ritual !== true) return false;
|
||||
}
|
||||
if ( filters.has("concentration") ) {
|
||||
if (filters.has("concentration")) {
|
||||
if (data.components.concentration !== true) return false;
|
||||
}
|
||||
if ( filters.has("prepared") ) {
|
||||
if ( data.level === 0 || ["innate", "always"].includes(data.preparation.mode) ) return true;
|
||||
if ( this.actor.data.type === "npc" ) return true;
|
||||
if (filters.has("prepared")) {
|
||||
if (data.level === 0 || ["innate", "always"].includes(data.preparation.mode)) return true;
|
||||
if (this.actor.data.type === "npc") return true;
|
||||
if (this.actor.data.type === "starship") return true;
|
||||
return data.preparation.prepared;
|
||||
}
|
||||
|
||||
// Equipment-specific filters
|
||||
if ( filters.has("equipped") ) {
|
||||
if ( data.equipped !== true ) return false;
|
||||
if (filters.has("equipped")) {
|
||||
if (data.equipped !== true) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
@ -369,64 +402,64 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
/* Event Listeners and Handlers
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Activate event listeners using the prepared sheet HTML
|
||||
* @param html {HTML} The prepared HTML object ready to be rendered into the DOM
|
||||
*/
|
||||
/** @inheritdoc */
|
||||
activateListeners(html) {
|
||||
|
||||
// Activate Item Filters
|
||||
const filterLists = html.find(".filter-list");
|
||||
filterLists.each(this._initializeFilterItemList.bind(this));
|
||||
filterLists.on("click", ".filter-item", this._onToggleFilter.bind(this));
|
||||
|
||||
// Item summaries
|
||||
html.find('.item .item-name.rollable h4').click(event => this._onItemSummary(event));
|
||||
html.find(".item .item-name.rollable h4").click((event) => this._onItemSummary(event));
|
||||
|
||||
// View Item Sheets
|
||||
html.find(".item-edit").click(this._onItemEdit.bind(this));
|
||||
|
||||
// Editable Only Listeners
|
||||
if ( this.isEditable ) {
|
||||
|
||||
if (this.isEditable) {
|
||||
// Input focus and update
|
||||
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));
|
||||
|
||||
// Ability Proficiency
|
||||
html.find('.ability-proficiency').click(this._onToggleAbilityProficiency.bind(this));
|
||||
html.find(".ability-proficiency").click(this._onToggleAbilityProficiency.bind(this));
|
||||
|
||||
// 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
|
||||
html.find('.trait-selector').click(this._onTraitSelector.bind(this));
|
||||
html.find(".trait-selector").click(this._onTraitSelector.bind(this));
|
||||
|
||||
// Configure Special Flags
|
||||
html.find('.config-button').click(this._onConfigMenu.bind(this));
|
||||
html.find(".config-button").click(this._onConfigMenu.bind(this));
|
||||
|
||||
// Owned Item management
|
||||
html.find('.item-create').click(this._onItemCreate.bind(this));
|
||||
html.find('.item-edit').click(this._onItemEdit.bind(this));
|
||||
html.find('.item-delete').click(this._onItemDelete.bind(this));
|
||||
html.find('.item-uses input').click(ev => ev.target.select()).change(this._onUsesChange.bind(this));
|
||||
html.find('.slot-max-override').click(this._onPowerSlotOverride.bind(this));
|
||||
html.find(".item-create").click(this._onItemCreate.bind(this));
|
||||
html.find(".item-delete").click(this._onItemDelete.bind(this));
|
||||
html.find(".item-collapse").click(this._onItemCollapse.bind(this));
|
||||
html.find(".item-uses input")
|
||||
.click((ev) => ev.target.select())
|
||||
.change(this._onUsesChange.bind(this));
|
||||
html.find(".slot-max-override").click(this._onPowerSlotOverride.bind(this));
|
||||
html.find(".increment-class-level").click(this._onIncrementClassLevel.bind(this));
|
||||
html.find(".decrement-class-level").click(this._onDecrementClassLevel.bind(this));
|
||||
|
||||
// Active Effect management
|
||||
html.find(".effect-control").click(ev => onManageActiveEffect(ev, this.entity));
|
||||
html.find(".effect-control").click((ev) => onManageActiveEffect(ev, this.actor));
|
||||
}
|
||||
|
||||
// Owner Only Listeners
|
||||
if ( this.actor.owner ) {
|
||||
|
||||
if (this.actor.isOwner) {
|
||||
// Ability Checks
|
||||
html.find('.ability-name').click(this._onRollAbilityTest.bind(this));
|
||||
|
||||
html.find(".ability-name").click(this._onRollAbilityTest.bind(this));
|
||||
|
||||
// Roll Skill Checks
|
||||
html.find('.skill-name').click(this._onRollSkillCheck.bind(this));
|
||||
html.find(".skill-name").click(this._onRollSkillCheck.bind(this));
|
||||
|
||||
// Item Rolling
|
||||
html.find('.item .item-image').click(event => this._onItemRoll(event));
|
||||
html.find('.item .item-recharge').click(event => this._onItemRecharge(event));
|
||||
html.find(".item .item-image").click((event) => this._onItemRoll(event));
|
||||
html.find(".item .item-recharge").click((event) => this._onItemRecharge(event));
|
||||
}
|
||||
|
||||
// Otherwise remove rollable classes
|
||||
|
@ -447,8 +480,8 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
_initializeFilterItemList(i, ul) {
|
||||
const set = this._filters[ul.dataset.filter];
|
||||
const filters = ul.querySelectorAll(".filter-item");
|
||||
for ( let li of filters ) {
|
||||
if ( set.has(li.dataset.filter) ) li.classList.add("active");
|
||||
for (let li of filters) {
|
||||
if (set.has(li.dataset.filter)) li.classList.add("active");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -464,10 +497,10 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
_onChangeInputDelta(event) {
|
||||
const input = event.target;
|
||||
const value = input.value;
|
||||
if ( ["+", "-"].includes(value[0]) ) {
|
||||
if (["+", "-"].includes(value[0])) {
|
||||
let delta = parseFloat(value);
|
||||
input.value = getProperty(this.actor.data, input.name) + delta;
|
||||
} else if ( value[0] === "=" ) {
|
||||
} else if (value[0] === "=") {
|
||||
input.value = value.slice(1);
|
||||
}
|
||||
}
|
||||
|
@ -482,17 +515,25 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
_onConfigMenu(event) {
|
||||
event.preventDefault();
|
||||
const button = event.currentTarget;
|
||||
switch ( button.dataset.action ) {
|
||||
let app;
|
||||
switch (button.dataset.action) {
|
||||
case "hit-dice":
|
||||
app = new ActorHitDiceConfig(this.object);
|
||||
break;
|
||||
case "movement":
|
||||
new ActorMovementConfig(this.object).render(true);
|
||||
app = new ActorMovementConfig(this.object);
|
||||
break;
|
||||
case "flags":
|
||||
new ActorSheetFlags(this.object).render(true);
|
||||
app = new ActorSheetFlags(this.object);
|
||||
break;
|
||||
case "senses":
|
||||
new ActorSensesConfig(this.object).render(true);
|
||||
app = new ActorSensesConfig(this.object);
|
||||
break;
|
||||
case "type":
|
||||
new ActorTypeConfig(this.object).render(true);
|
||||
break;
|
||||
}
|
||||
app?.render(true);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -512,10 +553,10 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
let idx = levels.indexOf(level);
|
||||
|
||||
// Toggle next level - forward on click, backwards on right
|
||||
if ( event.type === "click" ) {
|
||||
field.val(levels[(idx === levels.length - 1) ? 0 : idx + 1]);
|
||||
} else if ( event.type === "contextmenu" ) {
|
||||
field.val(levels[(idx === 0) ? levels.length - 1 : idx - 1]);
|
||||
if (event.type === "click") {
|
||||
field.val(levels[idx === levels.length - 1 ? 0 : idx + 1]);
|
||||
} else if (event.type === "contextmenu") {
|
||||
field.val(levels[idx === 0 ? levels.length - 1 : idx - 1]);
|
||||
}
|
||||
|
||||
// Update the field value and save the form
|
||||
|
@ -526,49 +567,51 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
|
||||
/** @override */
|
||||
async _onDropActor(event, data) {
|
||||
const canPolymorph = game.user.isGM || (this.actor.owner && game.settings.get('sw5e', 'allowPolymorphing'));
|
||||
if ( !canPolymorph ) return false;
|
||||
const canPolymorph = game.user.isGM || (this.actor.isOwner && game.settings.get("sw5e", "allowPolymorphing"));
|
||||
if (!canPolymorph) return false;
|
||||
|
||||
// Get the target actor
|
||||
let sourceActor = null;
|
||||
if (data.pack) {
|
||||
const pack = game.packs.find(p => p.collection === data.pack);
|
||||
const pack = game.packs.find((p) => p.collection === data.pack);
|
||||
sourceActor = await pack.getEntity(data.id);
|
||||
} else {
|
||||
sourceActor = game.actors.get(data.id);
|
||||
}
|
||||
if ( !sourceActor ) return;
|
||||
if (!sourceActor) return;
|
||||
|
||||
// Define a function to record polymorph settings for future use
|
||||
const rememberOptions = html => {
|
||||
const rememberOptions = (html) => {
|
||||
const options = {};
|
||||
html.find('input').each((i, el) => {
|
||||
html.find("input").each((i, el) => {
|
||||
options[el.name] = el.checked;
|
||||
});
|
||||
const settings = mergeObject(game.settings.get('sw5e', 'polymorphSettings') || {}, options);
|
||||
game.settings.set('sw5e', 'polymorphSettings', settings);
|
||||
const settings = mergeObject(game.settings.get("sw5e", "polymorphSettings") || {}, options);
|
||||
game.settings.set("sw5e", "polymorphSettings", settings);
|
||||
return settings;
|
||||
};
|
||||
|
||||
// Create and render the Dialog
|
||||
return new Dialog({
|
||||
title: game.i18n.localize('SW5E.PolymorphPromptTitle'),
|
||||
return new Dialog(
|
||||
{
|
||||
title: game.i18n.localize("SW5E.PolymorphPromptTitle"),
|
||||
content: {
|
||||
options: game.settings.get('sw5e', 'polymorphSettings'),
|
||||
options: game.settings.get("sw5e", "polymorphSettings"),
|
||||
i18n: SW5E.polymorphSettings,
|
||||
isToken: this.actor.isToken
|
||||
},
|
||||
default: 'accept',
|
||||
default: "accept",
|
||||
buttons: {
|
||||
accept: {
|
||||
icon: '<i class="fas fa-check"></i>',
|
||||
label: game.i18n.localize('SW5E.PolymorphAcceptSettings'),
|
||||
callback: html => this.actor.transformInto(sourceActor, rememberOptions(html))
|
||||
label: game.i18n.localize("SW5E.PolymorphAcceptSettings"),
|
||||
callback: (html) => this.actor.transformInto(sourceActor, rememberOptions(html))
|
||||
},
|
||||
wildshape: {
|
||||
icon: '<i class="fas fa-paw"></i>',
|
||||
label: game.i18n.localize('SW5E.PolymorphWildShape'),
|
||||
callback: html => this.actor.transformInto(sourceActor, {
|
||||
label: game.i18n.localize("SW5E.PolymorphWildShape"),
|
||||
callback: (html) =>
|
||||
this.actor.transformInto(sourceActor, {
|
||||
keepBio: true,
|
||||
keepClass: true,
|
||||
keepMental: true,
|
||||
|
@ -579,37 +622,66 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
},
|
||||
polymorph: {
|
||||
icon: '<i class="fas fa-pastafarianism"></i>',
|
||||
label: game.i18n.localize('SW5E.Polymorph'),
|
||||
callback: html => this.actor.transformInto(sourceActor, {
|
||||
label: game.i18n.localize("SW5E.Polymorph"),
|
||||
callback: (html) =>
|
||||
this.actor.transformInto(sourceActor, {
|
||||
transformTokens: rememberOptions(html).transformTokens
|
||||
})
|
||||
},
|
||||
cancel: {
|
||||
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,
|
||||
template: 'systems/sw5e/templates/apps/polymorph-prompt.html'
|
||||
}).render(true);
|
||||
template: "systems/sw5e/templates/apps/polymorph-prompt.html"
|
||||
}
|
||||
).render(true);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
async _onDropItemCreate(itemData) {
|
||||
// Check to make sure items of this type are allowed on this actor
|
||||
if (this.constructor.unsupportedItemTypes.has(itemData.type)) {
|
||||
return ui.notifications.warn(
|
||||
game.i18n.format("SW5E.ActorWarningInvalidItem", {
|
||||
itemType: game.i18n.localize(CONFIG.Item.typeLabels[itemData.type]),
|
||||
actorType: game.i18n.localize(CONFIG.Actor.typeLabels[this.actor.type])
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Create a Consumable power scroll on the Inventory tab
|
||||
if ( (itemData.type === "power") && (this._tabs[0].active === "inventory") ) {
|
||||
if (itemData.type === "power" && this._tabs[0].active === "inventory") {
|
||||
const scroll = await Item5e.createScrollFromPower(itemData);
|
||||
itemData = scroll.data;
|
||||
}
|
||||
|
||||
if (itemData.data) {
|
||||
// Ignore certain statuses
|
||||
if ( itemData.data ) {
|
||||
["attunement", "equipped", "proficient", "prepared"].forEach(k => delete itemData.data[k]);
|
||||
["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
|
||||
|
@ -623,7 +695,7 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
* @param {MouseEvent} event The originating click event
|
||||
* @private
|
||||
*/
|
||||
async _onPowerSlotOverride (event) {
|
||||
async _onPowerSlotOverride(event) {
|
||||
const span = event.currentTarget.parentElement;
|
||||
const level = span.dataset.level;
|
||||
const override = this.actor.data.data.powers[level].override || span.dataset.slots;
|
||||
|
@ -650,10 +722,10 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
async _onUsesChange(event) {
|
||||
event.preventDefault();
|
||||
const itemId = event.currentTarget.closest(".item").dataset.itemId;
|
||||
const item = this.actor.getOwnedItem(itemId);
|
||||
const item = this.actor.items.get(itemId);
|
||||
const uses = Math.clamped(0, parseInt(event.target.value), item.data.data.uses.max);
|
||||
event.target.value = uses;
|
||||
return item.update({ 'data.uses.value': uses });
|
||||
return item.update({"data.uses.value": uses});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -665,7 +737,7 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
_onItemRoll(event) {
|
||||
event.preventDefault();
|
||||
const itemId = event.currentTarget.closest(".item").dataset.itemId;
|
||||
const item = this.actor.getOwnedItem(itemId);
|
||||
const item = this.actor.items.get(itemId);
|
||||
return item.roll();
|
||||
}
|
||||
|
||||
|
@ -679,9 +751,9 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
_onItemRecharge(event) {
|
||||
event.preventDefault();
|
||||
const itemId = event.currentTarget.closest(".item").dataset.itemId;
|
||||
const item = this.actor.getOwnedItem(itemId);
|
||||
const item = this.actor.items.get(itemId);
|
||||
return item.rollRecharge();
|
||||
};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
|
@ -692,17 +764,17 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
_onItemSummary(event) {
|
||||
event.preventDefault();
|
||||
let li = $(event.currentTarget).parents(".item"),
|
||||
item = this.actor.getOwnedItem(li.data("item-id")),
|
||||
chatData = item.getChatData({secrets: this.actor.owner});
|
||||
item = this.actor.items.get(li.data("item-id")),
|
||||
chatData = item.getChatData({secrets: this.actor.isOwner});
|
||||
|
||||
// Toggle summary
|
||||
if ( li.hasClass("expanded") ) {
|
||||
if (li.hasClass("expanded")) {
|
||||
let summary = li.children(".item-summary");
|
||||
summary.slideUp(200, () => summary.remove());
|
||||
} else {
|
||||
let div = $(`<div class="item-summary">${chatData.description.value}</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);
|
||||
li.append(div.hide());
|
||||
div.slideDown(200);
|
||||
|
@ -722,12 +794,12 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
const header = event.currentTarget;
|
||||
const type = header.dataset.type;
|
||||
const itemData = {
|
||||
name: game.i18n.format("SW5E.ItemNew", {type: type.capitalize()}),
|
||||
name: game.i18n.format("SW5E.ItemNew", {type: game.i18n.localize(`SW5E.ItemType${type.capitalize()}`)}),
|
||||
type: type,
|
||||
data: duplicate(header.dataset)
|
||||
data: foundry.utils.deepClone(header.dataset)
|
||||
};
|
||||
delete itemData.data["type"];
|
||||
return this.actor.createEmbeddedEntity("OwnedItem", itemData);
|
||||
return this.actor.createEmbeddedDocuments("Item", [itemData]);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -740,8 +812,8 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
_onItemEdit(event) {
|
||||
event.preventDefault();
|
||||
const li = event.currentTarget.closest(".item");
|
||||
const item = this.actor.getOwnedItem(li.dataset.itemId);
|
||||
item.sheet.render(true);
|
||||
const item = this.actor.items.get(li.dataset.itemId);
|
||||
return item.sheet.render(true);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -754,7 +826,77 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
_onItemDelete(event) {
|
||||
event.preventDefault();
|
||||
const li = event.currentTarget.closest(".item");
|
||||
this.actor.deleteOwnedItem(li.dataset.itemId);
|
||||
const item = this.actor.items.get(li.dataset.itemId);
|
||||
if (item) return item.delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle collapsing a Feature row on the actor sheet
|
||||
* @param {Event} event The originating click event
|
||||
* @private
|
||||
*/
|
||||
|
||||
_onItemCollapse(event) {
|
||||
event.preventDefault();
|
||||
|
||||
event.currentTarget.classList.toggle("active");
|
||||
|
||||
const li = event.currentTarget.closest("li");
|
||||
const content = li.querySelector(".content");
|
||||
|
||||
if (content.style.display === "none") {
|
||||
content.style.display = "block";
|
||||
} else {
|
||||
content.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incrementing class level on the actor sheet
|
||||
* @param {Event} event The originating click event
|
||||
* @private
|
||||
*/
|
||||
|
||||
_onIncrementClassLevel(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const div = event.currentTarget.closest(".character");
|
||||
const li = event.currentTarget.closest("li");
|
||||
|
||||
const actorId = div.id.split("-")[1];
|
||||
const itemId = li.dataset.itemId;
|
||||
|
||||
const actor = game.actors.get(actorId);
|
||||
const item = actor.items.get(itemId);
|
||||
|
||||
let levels = item.data.data.levels;
|
||||
const update = {_id: item.data._id, data: {levels: levels + 1}};
|
||||
|
||||
actor.updateEmbeddedDocuments("Item", [update]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle decrementing class level on the actor sheet
|
||||
* @param {Event} event The originating click event
|
||||
* @private
|
||||
*/
|
||||
|
||||
_onDecrementClassLevel(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const div = event.currentTarget.closest(".character");
|
||||
const li = event.currentTarget.closest("li");
|
||||
|
||||
const actorId = div.id.split("-")[1];
|
||||
const itemId = li.dataset.itemId;
|
||||
|
||||
const actor = game.actors.get(actorId);
|
||||
const item = actor.items.get(itemId);
|
||||
|
||||
let levels = item.data.data.levels;
|
||||
const update = {_id: item.data._id, data: {levels: levels - 1}};
|
||||
|
||||
actor.updateEmbeddedDocuments("Item", [update]);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -767,7 +909,7 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
_onRollAbilityTest(event) {
|
||||
event.preventDefault();
|
||||
let ability = event.currentTarget.parentElement.dataset.ability;
|
||||
this.actor.rollAbility(ability, {event: event});
|
||||
return this.actor.rollAbility(ability, {event: event});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -780,7 +922,7 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
_onRollSkillCheck(event) {
|
||||
event.preventDefault();
|
||||
const skill = event.currentTarget.parentElement.dataset.skill;
|
||||
this.actor.rollSkill(skill, {event: event});
|
||||
return this.actor.rollSkill(skill, {event: event});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -793,7 +935,7 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
_onToggleAbilityProficiency(event) {
|
||||
event.preventDefault();
|
||||
const field = event.currentTarget.previousElementSibling;
|
||||
this.actor.update({[field.name]: 1 - parseInt(field.value)});
|
||||
return this.actor.update({[field.name]: 1 - parseInt(field.value)});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -808,9 +950,9 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
const li = event.currentTarget;
|
||||
const set = this._filters[li.parentElement.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);
|
||||
this.render();
|
||||
return this.render();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -825,8 +967,8 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
const a = event.currentTarget;
|
||||
const label = a.parentElement.querySelector("label");
|
||||
const choices = CONFIG.SW5E[a.dataset.options];
|
||||
const options = { name: a.dataset.target, title: label.innerText, choices };
|
||||
new TraitSelector(this.actor, options).render(true)
|
||||
const options = {name: a.dataset.target, title: label.innerText, choices};
|
||||
return new TraitSelector(this.actor, options).render(true);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -834,15 +976,14 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
/** @override */
|
||||
_getHeaderButtons() {
|
||||
let buttons = super._getHeaderButtons();
|
||||
|
||||
// Add button to revert polymorph
|
||||
if ( !this.actor.isPolymorphed || this.actor.isToken ) return buttons;
|
||||
if (this.actor.isPolymorphed) {
|
||||
buttons.unshift({
|
||||
label: 'SW5E.PolymorphRestoreTransformation',
|
||||
label: "SW5E.PolymorphRestoreTransformation",
|
||||
class: "restore-transformation",
|
||||
icon: "fas fa-backward",
|
||||
onclick: ev => this.actor.revertOriginalForm()
|
||||
onclick: () => this.actor.revertOriginalForm()
|
||||
});
|
||||
}
|
||||
return buttons;
|
||||
}
|
||||
}
|
|
@ -7,7 +7,6 @@ import Actor5e from "../../entity.js";
|
|||
* @type {ActorSheet5e}
|
||||
*/
|
||||
export default class ActorSheet5eCharacterNew extends ActorSheet5e {
|
||||
|
||||
get template() {
|
||||
if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/newActor/limited-sheet.html";
|
||||
return "systems/sw5e/templates/actors/newActor/character-sheet.html";
|
||||
|
@ -17,17 +16,18 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
|
|||
* @return {Object}
|
||||
*/
|
||||
static get defaultOptions() {
|
||||
|
||||
return mergeObject(super.defaultOptions, {
|
||||
classes: ["swalt", "sw5e", "sheet", "actor", "character"],
|
||||
blockFavTab: true,
|
||||
subTabs: null,
|
||||
width: 800,
|
||||
tabs: [{
|
||||
tabs: [
|
||||
{
|
||||
navSelector: ".root-tabs",
|
||||
contentSelector: ".sheet-body",
|
||||
initial: "attributes"
|
||||
}],
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -48,7 +48,7 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
|
|||
sheetData["resources"] = ["primary", "secondary", "tertiary"].reduce((arr, r) => {
|
||||
const res = sheetData.data.resources[r] || {};
|
||||
res.name = r;
|
||||
res.placeholder = game.i18n.localize("SW5E.Resource"+r.titleCase());
|
||||
res.placeholder = game.i18n.localize("SW5E.Resource" + r.titleCase());
|
||||
if (res && res.value === 0) delete res.value;
|
||||
if (res && res.max === 0) delete res.max;
|
||||
return arr.concat([res]);
|
||||
|
@ -56,10 +56,12 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
|
|||
|
||||
// Experience Tracking
|
||||
sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking");
|
||||
sheetData["classLabels"] = this.actor.itemTypes.class.map(c => c.name).join(", ");
|
||||
sheetData["multiclassLabels"] = this.actor.itemTypes.class.map(c => {
|
||||
return [c.data.data.archetype, c.name, c.data.data.levels].filterJoin(' ')
|
||||
}).join(', ');
|
||||
sheetData["classLabels"] = this.actor.itemTypes.class.map((c) => c.name).join(", ");
|
||||
sheetData["multiclassLabels"] = this.actor.itemTypes.class
|
||||
.map((c) => {
|
||||
return [c.data.data.archetype, c.name, c.data.data.levels].filterJoin(" ");
|
||||
})
|
||||
.join(", ");
|
||||
|
||||
// Return data for rendering
|
||||
return sheetData;
|
||||
|
@ -72,23 +74,38 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
|
|||
* @private
|
||||
*/
|
||||
_prepareItems(data) {
|
||||
|
||||
// Categorize items as inventory, powerbook, features, and classes
|
||||
const inventory = {
|
||||
weapon: { label: "SW5E.ItemTypeWeaponPl", items: [], dataset: {type: "weapon"} },
|
||||
equipment: { label: "SW5E.ItemTypeEquipmentPl", items: [], dataset: {type: "equipment"} },
|
||||
consumable: { label: "SW5E.ItemTypeConsumablePl", items: [], dataset: {type: "consumable"} },
|
||||
tool: { label: "SW5E.ItemTypeToolPl", items: [], dataset: {type: "tool"} },
|
||||
backpack: { label: "SW5E.ItemTypeContainerPl", items: [], dataset: {type: "backpack"} },
|
||||
loot: { label: "SW5E.ItemTypeLootPl", items: [], dataset: {type: "loot"} }
|
||||
weapon: {label: "SW5E.ItemTypeWeaponPl", items: [], dataset: {type: "weapon"}},
|
||||
equipment: {label: "SW5E.ItemTypeEquipmentPl", items: [], dataset: {type: "equipment"}},
|
||||
consumable: {label: "SW5E.ItemTypeConsumablePl", items: [], dataset: {type: "consumable"}},
|
||||
tool: {label: "SW5E.ItemTypeToolPl", items: [], dataset: {type: "tool"}},
|
||||
backpack: {label: "SW5E.ItemTypeContainerPl", items: [], dataset: {type: "backpack"}},
|
||||
loot: {label: "SW5E.ItemTypeLootPl", items: [], dataset: {type: "loot"}}
|
||||
};
|
||||
|
||||
// Partition items by category
|
||||
let [items, forcepowers, techpowers, feats, classes, species, archetypes, classfeatures, backgrounds, fightingstyles, fightingmasteries, lightsaberforms] = data.items.reduce((arr, item) => {
|
||||
|
||||
let [
|
||||
items,
|
||||
forcepowers,
|
||||
techpowers,
|
||||
feats,
|
||||
classes,
|
||||
deployments,
|
||||
deploymentfeatures,
|
||||
ventures,
|
||||
species,
|
||||
archetypes,
|
||||
classfeatures,
|
||||
backgrounds,
|
||||
fightingstyles,
|
||||
fightingmasteries,
|
||||
lightsaberforms
|
||||
] = data.items.reduce(
|
||||
(arr, item) => {
|
||||
// Item details
|
||||
item.img = item.img || DEFAULT_TOKEN;
|
||||
item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
|
||||
item.img = item.img || CONST.DEFAULT_TOKEN;
|
||||
item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1;
|
||||
item.attunement = {
|
||||
[CONFIG.SW5E.attunementTypes.REQUIRED]: {
|
||||
icon: "fa-sun",
|
||||
|
@ -103,29 +120,39 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
|
|||
}[item.data.attunement];
|
||||
|
||||
// Item usage
|
||||
item.hasUses = item.data.uses && (item.data.uses.max > 0);
|
||||
item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false);
|
||||
item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0));
|
||||
item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type));
|
||||
item.hasUses = item.data.uses && item.data.uses.max > 0;
|
||||
item.isOnCooldown =
|
||||
item.data.recharge && !!item.data.recharge.value && item.data.recharge.charged === false;
|
||||
item.isDepleted = item.isOnCooldown && item.data.uses.per && item.data.uses.value > 0;
|
||||
item.hasTarget = !!item.data.target && !["none", ""].includes(item.data.target.type);
|
||||
|
||||
// Item toggle state
|
||||
this._prepareItemToggleState(item);
|
||||
|
||||
// Primary Class
|
||||
if (item.type === "class")
|
||||
item.isOriginalClass = item._id === this.actor.data.data.details.originalClass;
|
||||
|
||||
// Classify items into types
|
||||
if ( item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school) ) arr[1].push(item);
|
||||
else if ( item.type === "power" && ["tec"].includes(item.data.school) ) arr[2].push(item);
|
||||
else if ( item.type === "feat" ) arr[3].push(item);
|
||||
else if ( item.type === "class" ) arr[4].push(item);
|
||||
else if ( item.type === "species" ) arr[5].push(item);
|
||||
else if ( item.type === "archetype" ) arr[6].push(item);
|
||||
else if ( item.type === "classfeature" ) arr[7].push(item);
|
||||
else if ( item.type === "background" ) arr[8].push(item);
|
||||
else if ( item.type === "fightingstyle" ) arr[9].push(item);
|
||||
else if ( item.type === "fightingmastery" ) arr[10].push(item);
|
||||
else if ( item.type === "lightsaberform" ) arr[11].push(item);
|
||||
else if ( Object.keys(inventory).includes(item.type ) ) arr[0].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 === "feat") arr[3].push(item);
|
||||
else if (item.type === "class") arr[4].push(item);
|
||||
else if (item.type === "deployment") arr[5].push(item);
|
||||
else if (item.type === "deploymentfeature") arr[6].push(item);
|
||||
else if (item.type === "venture") arr[7].push(item);
|
||||
else if (item.type === "species") arr[8].push(item);
|
||||
else if (item.type === "archetype") arr[9].push(item);
|
||||
else if (item.type === "classfeature") arr[10].push(item);
|
||||
else if (item.type === "background") arr[11].push(item);
|
||||
else if (item.type === "fightingstyle") arr[12].push(item);
|
||||
else if (item.type === "fightingmastery") arr[13].push(item);
|
||||
else if (item.type === "lightsaberform") arr[14].push(item);
|
||||
else if (Object.keys(inventory).includes(item.type)) arr[0].push(item);
|
||||
return arr;
|
||||
}, [[], [], [], [], [], [], [], [], [], [], [], []]);
|
||||
},
|
||||
[[], [], [], [], [], [], [], [], [], [], [], [], [], [], []]
|
||||
);
|
||||
|
||||
// Apply active item filters
|
||||
items = this._filterItems(items, this._filters.inventory);
|
||||
|
@ -134,10 +161,10 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
|
|||
feats = this._filterItems(feats, this._filters.features);
|
||||
|
||||
// Organize items
|
||||
for ( let i of items ) {
|
||||
for (let i of items) {
|
||||
i.data.quantity = i.data.quantity || 0;
|
||||
i.data.weight = i.data.weight || 0;
|
||||
i.totalWeight = Math.round(i.data.quantity * i.data.weight * 10) / 10;
|
||||
i.totalWeight = (i.data.quantity * i.data.weight).toNearest(0.1);
|
||||
inventory[i.type].items.push(i);
|
||||
}
|
||||
|
||||
|
@ -147,25 +174,102 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
|
|||
|
||||
// Organize Features
|
||||
const features = {
|
||||
classes: { label: "SW5E.ItemTypeClassPl", items: [], hasActions: false, dataset: {type: "class"}, isClass: true },
|
||||
classfeatures: { label: "SW5E.ItemTypeClassFeats", items: [], hasActions: true, dataset: {type: "classfeature"}, isClassfeature: true },
|
||||
archetype: { label: "SW5E.ItemTypeArchetype", items: [], hasActions: false, dataset: {type: "archetype"}, isArchetype: true },
|
||||
species: { label: "SW5E.ItemTypeSpecies", items: [], hasActions: false, dataset: {type: "species"}, isSpecies: true },
|
||||
background: { label: "SW5E.ItemTypeBackground", items: [], hasActions: false, dataset: {type: "background"}, isBackground: true },
|
||||
fightingstyles: { label: "SW5E.ItemTypeFightingStylePl", items: [], hasActions: false, dataset: {type: "fightingstyle"}, isFightingstyle: true },
|
||||
fightingmasteries: { label: "SW5E.ItemTypeFightingMasteryPl", items: [], hasActions: false, dataset: {type: "fightingmastery"}, isFightingmastery: true },
|
||||
lightsaberforms: { label: "SW5E.ItemTypeLightsaberFormPl", items: [], hasActions: false, dataset: {type: "lightsaberform"}, isLightsaberform: true },
|
||||
active: { label: "SW5E.FeatureActive", items: [], hasActions: true, dataset: {type: "feat", "activation.type": "action"} },
|
||||
passive: { label: "SW5E.FeaturePassive", items: [], hasActions: false, dataset: {type: "feat"} }
|
||||
classes: {
|
||||
label: "SW5E.ItemTypeClassPl",
|
||||
items: [],
|
||||
hasActions: false,
|
||||
dataset: {type: "class"},
|
||||
isClass: true
|
||||
},
|
||||
classfeatures: {
|
||||
label: "SW5E.ItemTypeClassFeats",
|
||||
items: [],
|
||||
hasActions: true,
|
||||
dataset: {type: "classfeature"},
|
||||
isClassfeature: true
|
||||
},
|
||||
archetype: {
|
||||
label: "SW5E.ItemTypeArchetype",
|
||||
items: [],
|
||||
hasActions: false,
|
||||
dataset: {type: "archetype"},
|
||||
isArchetype: true
|
||||
},
|
||||
deployments: {
|
||||
label: "SW5E.ItemTypeDeploymentPl",
|
||||
items: [],
|
||||
hasActions: false,
|
||||
dataset: {type: "deployment"},
|
||||
isDeployment: true
|
||||
},
|
||||
deploymentfeatures: {
|
||||
label: "SW5E.ItemTypeDeploymentFeaturePl",
|
||||
items: [],
|
||||
hasActions: true,
|
||||
dataset: {type: "deploymentfeature"},
|
||||
isDeploymentfeature: true
|
||||
},
|
||||
ventures: {
|
||||
label: "SW5E.ItemTypeVenturePl",
|
||||
items: [],
|
||||
hasActions: false,
|
||||
dataset: {type: "venture"},
|
||||
isVenture: true
|
||||
},
|
||||
species: {
|
||||
label: "SW5E.ItemTypeSpecies",
|
||||
items: [],
|
||||
hasActions: false,
|
||||
dataset: {type: "species"},
|
||||
isSpecies: true
|
||||
},
|
||||
background: {
|
||||
label: "SW5E.ItemTypeBackground",
|
||||
items: [],
|
||||
hasActions: false,
|
||||
dataset: {type: "background"},
|
||||
isBackground: true
|
||||
},
|
||||
fightingstyles: {
|
||||
label: "SW5E.ItemTypeFightingStylePl",
|
||||
items: [],
|
||||
hasActions: false,
|
||||
dataset: {type: "fightingstyle"},
|
||||
isFightingstyle: true
|
||||
},
|
||||
fightingmasteries: {
|
||||
label: "SW5E.ItemTypeFightingMasteryPl",
|
||||
items: [],
|
||||
hasActions: false,
|
||||
dataset: {type: "fightingmastery"},
|
||||
isFightingmastery: true
|
||||
},
|
||||
lightsaberforms: {
|
||||
label: "SW5E.ItemTypeLightsaberFormPl",
|
||||
items: [],
|
||||
hasActions: false,
|
||||
dataset: {type: "lightsaberform"},
|
||||
isLightsaberform: true
|
||||
},
|
||||
active: {
|
||||
label: "SW5E.FeatureActive",
|
||||
items: [],
|
||||
hasActions: true,
|
||||
dataset: {"type": "feat", "activation.type": "action"}
|
||||
},
|
||||
passive: {label: "SW5E.FeaturePassive", items: [], hasActions: false, dataset: {type: "feat"}}
|
||||
};
|
||||
for ( let f of feats ) {
|
||||
if ( f.data.activation.type ) features.active.items.push(f);
|
||||
for (let f of feats) {
|
||||
if (f.data.activation.type) features.active.items.push(f);
|
||||
else features.passive.items.push(f);
|
||||
}
|
||||
classes.sort((a, b) => b.levels - a.levels);
|
||||
classes.sort((a, b) => b.data.levels - a.data.levels);
|
||||
features.classes.items = classes;
|
||||
features.classfeatures.items = classfeatures;
|
||||
features.archetype.items = archetypes;
|
||||
features.deployments.items = deployments;
|
||||
features.deploymentfeatures.items = deploymentfeatures;
|
||||
features.ventures.items = ventures;
|
||||
features.species.items = species;
|
||||
features.background.items = backgrounds;
|
||||
features.fightingstyles.items = fightingstyles;
|
||||
|
@ -191,12 +295,11 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
|
|||
const isAlways = getProperty(item.data, "preparation.mode") === "always";
|
||||
const isPrepared = getProperty(item.data, "preparation.prepared");
|
||||
item.toggleClass = isPrepared ? "active" : "";
|
||||
if ( isAlways ) item.toggleClass = "fixed";
|
||||
if ( isAlways ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.always;
|
||||
else if ( isPrepared ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.prepared;
|
||||
if (isAlways) item.toggleClass = "fixed";
|
||||
if (isAlways) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.always;
|
||||
else if (isPrepared) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.prepared;
|
||||
else item.toggleTitle = game.i18n.localize("SW5E.PowerUnprepared");
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
const isActive = getProperty(item.data, "equipped");
|
||||
item.toggleClass = isActive ? "active" : "";
|
||||
item.toggleTitle = game.i18n.localize(isActive ? "SW5E.Equipped" : "SW5E.Unequipped");
|
||||
|
@ -209,33 +312,35 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
|
|||
|
||||
/**
|
||||
* Activate event listeners using the prepared sheet HTML
|
||||
* @param html {HTML} The prepared HTML object ready to be rendered into the DOM
|
||||
* @param html {jQuery} The prepared HTML object ready to be rendered into the DOM
|
||||
*/
|
||||
activateListeners(html) {
|
||||
super.activateListeners(html);
|
||||
if ( !this.options.editable ) return;
|
||||
if (!this.isEditable) return;
|
||||
|
||||
// Inventory Functions
|
||||
// html.find(".currency-convert").click(this._onConvertCurrency.bind(this));
|
||||
|
||||
// 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
|
||||
html.find('.short-rest').click(this._onShortRest.bind(this));
|
||||
html.find('.long-rest').click(this._onLongRest.bind(this));
|
||||
html.find(".short-rest").click(this._onShortRest.bind(this));
|
||||
html.find(".long-rest").click(this._onLongRest.bind(this));
|
||||
|
||||
// Rollable sheet actions
|
||||
html.find(".rollable[data-action]").click(this._onSheetAction.bind(this));
|
||||
|
||||
// Send Languages to Chat onClick
|
||||
html.find('[data-options="share-languages"]').click(event => {
|
||||
html.find('[data-options="share-languages"]').click((event) => {
|
||||
event.preventDefault();
|
||||
let langs = this.actor.data.data.traits.languages.value.map(l => SW5E.languages[l] || l).join(", ");
|
||||
let langs = this.actor.data.data.traits.languages.value
|
||||
.map((l) => CONFIG.SW5E.languages[l] || l)
|
||||
.join(", ");
|
||||
let custom = this.actor.data.data.traits.languages.custom;
|
||||
if (custom) langs += ", " + custom.replace(/;/g, ",");
|
||||
let content = `
|
||||
<div class="sw5e chat-card item-card" data-acor-id="${this.actor._id}">
|
||||
<div class="sw5e chat-card item-card" data-acor-id="${this.actor.data._id}">
|
||||
<header class="card-header flexrow">
|
||||
<img src="${this.actor.data.token.img}" title="" width="36" height="36" style="border: none;"/>
|
||||
<h3>Known Languages</h3>
|
||||
|
@ -245,46 +350,50 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
|
|||
`;
|
||||
|
||||
// Send to Chat
|
||||
let rollWhisper = null;
|
||||
let rollBlind = false;
|
||||
let rollMode = game.settings.get("core", "rollMode");
|
||||
if (["gmroll", "blindroll"].includes(rollMode)) rollWhisper = ChatMessage.getWhisperIDs("GM");
|
||||
if (rollMode === "blindroll") rollBlind = true;
|
||||
ChatMessage.create({
|
||||
user: game.user._id,
|
||||
let data = {
|
||||
user: game.user.data._id,
|
||||
content: content,
|
||||
blind: rollBlind,
|
||||
speaker: {
|
||||
actor: this.actor._id,
|
||||
actor: this.actor.data._id,
|
||||
token: this.actor.token,
|
||||
alias: this.actor.name
|
||||
},
|
||||
type: CONST.CHAT_MESSAGE_TYPES.OTHER
|
||||
});
|
||||
};
|
||||
|
||||
if (["gmroll", "blindroll"].includes(rollMode)) data["whisper"] = ChatMessage.getWhisperRecipients("GM");
|
||||
else if (rollMode === "selfroll") data["whisper"] = [game.users.get(game.user.data._id)];
|
||||
|
||||
ChatMessage.create(data);
|
||||
});
|
||||
|
||||
// Item Delete Confirmation
|
||||
html.find('.item-delete').off("click");
|
||||
html.find('.item-delete').click(event => {
|
||||
let li = $(event.currentTarget).parents('.item');
|
||||
html.find(".item-delete").off("click");
|
||||
html.find(".item-delete").click((event) => {
|
||||
let li = $(event.currentTarget).parents(".item");
|
||||
let itemId = li.attr("data-item-id");
|
||||
let item = this.actor.getOwnedItem(itemId);
|
||||
let item = this.actor.items.get(itemId);
|
||||
new Dialog({
|
||||
title: `Deleting ${item.data.name}`,
|
||||
content: `<p>Are you sure you want to delete ${item.data.name}?</p>`,
|
||||
buttons: {
|
||||
Yes: {
|
||||
icon: '<i class="fa fa-check"></i>',
|
||||
label: 'Yes',
|
||||
callback: dlg => {
|
||||
label: "Yes",
|
||||
callback: (dlg) => {
|
||||
this.actor.deleteOwnedItem(itemId);
|
||||
}
|
||||
},
|
||||
cancel: {
|
||||
icon: '<i class="fas fa-times"></i>',
|
||||
label: 'No'
|
||||
label: "No"
|
||||
}
|
||||
},
|
||||
},
|
||||
default: 'cancel'
|
||||
default: "cancel"
|
||||
}).render(true);
|
||||
});
|
||||
}
|
||||
|
@ -299,7 +408,7 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
|
|||
_onSheetAction(event) {
|
||||
event.preventDefault();
|
||||
const button = event.currentTarget;
|
||||
switch( button.dataset.action ) {
|
||||
switch (button.dataset.action) {
|
||||
case "rollDeathSave":
|
||||
return this.actor.rollDeathSave({event: event});
|
||||
case "rollInitiative":
|
||||
|
@ -309,7 +418,6 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
|
||||
/**
|
||||
* Handle toggling the state of an Owned Item within the Actor
|
||||
* @param {Event} event The triggering click event
|
||||
|
@ -318,7 +426,7 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
|
|||
_onToggleItem(event) {
|
||||
event.preventDefault();
|
||||
const itemId = event.currentTarget.closest(".item").dataset.itemId;
|
||||
const item = this.actor.getOwnedItem(itemId);
|
||||
const item = this.actor.items.get(itemId);
|
||||
const attr = item.data.type === "power" ? "data.preparation.prepared" : "data.equipped";
|
||||
return item.update({[attr]: !getProperty(item.data, attr)});
|
||||
}
|
||||
|
@ -353,22 +461,34 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
|
|||
|
||||
/** @override */
|
||||
async _onDropItemCreate(itemData) {
|
||||
|
||||
// Increment the number of class levels a character instead of creating a new item
|
||||
if ( itemData.type === "class" ) {
|
||||
const cls = this.actor.itemTypes.class.find(c => c.name === itemData.name);
|
||||
// Increment the number of class levels of a character instead of creating a new item
|
||||
if (itemData.type === "class") {
|
||||
const cls = this.actor.itemTypes.class.find((c) => c.name === itemData.name);
|
||||
let priorLevel = cls?.data.data.levels ?? 0;
|
||||
if ( !!cls ) {
|
||||
if (!!cls) {
|
||||
const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level);
|
||||
if ( next > priorLevel ) {
|
||||
if (next > priorLevel) {
|
||||
itemData.levels = next;
|
||||
return cls.update({"data.levels": next});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Increment the number of deployment ranks of a character instead of creating a new item
|
||||
// else if ( itemData.type === "deployment" ) {
|
||||
// const rnk = this.actor.itemTypes.deployment.find(c => c.name === itemData.name);
|
||||
// let priorRank = rnk?.data.data.ranks ?? 0;
|
||||
// if ( !!rnk ) {
|
||||
// const next = Math.min(priorLevel + 1, 5 + priorRank - this.actor.data.data.details.rank);
|
||||
// if ( next > priorRank ) {
|
||||
// itemData.ranks = next;
|
||||
// return rnk.update({"data.ranks": next});
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// Default drop handling if levels were not added
|
||||
super._onDropItemCreate(itemData);
|
||||
return super._onDropItemCreate(itemData);
|
||||
}
|
||||
}
|
||||
async function addFavorites(app, html, data) {
|
||||
|
@ -428,9 +548,9 @@ async function addFavorites(app, html, data) {
|
|||
value: data.actor.data.powers.power9.value,
|
||||
max: data.actor.data.powers.power9.max
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let powerCount = 0
|
||||
let powerCount = 0;
|
||||
let items = data.actor.items;
|
||||
for (let item of items) {
|
||||
if (item.type == "class") continue;
|
||||
|
@ -441,24 +561,28 @@ async function addFavorites(app, html, data) {
|
|||
}
|
||||
let isFav = item.flags.favtab.isFavourite;
|
||||
if (app.options.editable) {
|
||||
let favBtn = $(`<a class="item-control item-toggle item-fav ${isFav ? "active" : ""}" data-fav="${isFav}" title="${isFav ? "Remove from Favourites" : "Add to Favourites"}"><i class="fas fa-star"></i></a>`);
|
||||
favBtn.click(ev => {
|
||||
app.actor.getOwnedItem(item._id).update({
|
||||
let favBtn = $(
|
||||
`<a class="item-control item-toggle item-fav ${isFav ? "active" : ""}" data-fav="${isFav}" title="${
|
||||
isFav ? "Remove from Favourites" : "Add to Favourites"
|
||||
}"><i class="fas fa-star"></i></a>`
|
||||
);
|
||||
favBtn.click((ev) => {
|
||||
app.actor.items.get(item.data._id).update({
|
||||
"flags.favtab.isFavourite": !item.flags.favtab.isFavourite
|
||||
});
|
||||
});
|
||||
html.find(`.item[data-item-id="${item._id}"]`).find('.item-controls').prepend(favBtn);
|
||||
html.find(`.item[data-item-id="${item.data._id}"]`).find(".item-controls").prepend(favBtn);
|
||||
}
|
||||
|
||||
if (isFav) {
|
||||
item.powerComps = "";
|
||||
if (item.data.components) {
|
||||
let comps = item.data.components;
|
||||
let v = (comps.vocal) ? "V" : "";
|
||||
let s = (comps.somatic) ? "S" : "";
|
||||
let m = (comps.material) ? "M" : "";
|
||||
let c = (comps.concentration) ? true : false;
|
||||
let r = (comps.ritual) ? true : false;
|
||||
let v = comps.vocal ? "V" : "";
|
||||
let s = comps.somatic ? "S" : "";
|
||||
let m = comps.material ? "M" : "";
|
||||
let c = !!comps.concentration;
|
||||
let r = !!comps.ritual;
|
||||
item.powerComps = `${v}${s}${m}`;
|
||||
item.powerCon = c;
|
||||
item.powerRit = r;
|
||||
|
@ -466,15 +590,15 @@ async function addFavorites(app, html, data) {
|
|||
|
||||
item.editable = app.options.editable;
|
||||
switch (item.type) {
|
||||
case 'feat':
|
||||
case "feat":
|
||||
if (item.flags.favtab.sort === undefined) {
|
||||
item.flags.favtab.sort = (favFeats.count + 1) * 100000; // initial sort key if not present
|
||||
}
|
||||
favFeats.push(item);
|
||||
break;
|
||||
case 'power':
|
||||
case "power":
|
||||
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) {
|
||||
favPowers[item.data.level].powers.push(item);
|
||||
|
@ -500,62 +624,62 @@ async function addFavorites(app, html, data) {
|
|||
// html.find('.favourite .item-controls').css('flex', '0 0 22px');
|
||||
// }
|
||||
|
||||
let tabContainer = html.find('.favtabtarget');
|
||||
data.favItems = favItems.length > 0 ? favItems.sort((a, b) => (a.flags.favtab.sort) - (b.flags.favtab.sort)) : false;
|
||||
data.favFeats = favFeats.length > 0 ? favFeats.sort((a, b) => (a.flags.favtab.sort) - (b.flags.favtab.sort)) : false;
|
||||
let tabContainer = html.find(".favtabtarget");
|
||||
data.favItems = favItems.length > 0 ? favItems.sort((a, b) => a.flags.favtab.sort - b.flags.favtab.sort) : false;
|
||||
data.favFeats = favFeats.length > 0 ? favFeats.sort((a, b) => a.flags.favtab.sort - b.flags.favtab.sort) : false;
|
||||
data.favPowers = powerCount > 0 ? favPowers : false;
|
||||
data.editable = app.options.editable;
|
||||
|
||||
await loadTemplates(['systems/sw5e/templates/actors/newActor/item.hbs']);
|
||||
let favtabHtml = $(await renderTemplate('systems/sw5e/templates/actors/newActor/template.hbs', data));
|
||||
favtabHtml.find('.item-name h4').click(event => app._onItemSummary(event));
|
||||
await loadTemplates(["systems/sw5e/templates/actors/newActor/item.hbs"]);
|
||||
let favtabHtml = $(await renderTemplate("systems/sw5e/templates/actors/newActor/template.hbs", data));
|
||||
favtabHtml.find(".item-name h4").click((event) => app._onItemSummary(event));
|
||||
|
||||
if (app.options.editable) {
|
||||
favtabHtml.find('.item-image').click(ev => app._onItemRoll(ev));
|
||||
let handler = ev => app._onDragStart(ev);
|
||||
favtabHtml.find('.item').each((i, li) => {
|
||||
favtabHtml.find(".item-image").click((ev) => app._onItemRoll(ev));
|
||||
let handler = (ev) => app._onDragStart(ev);
|
||||
favtabHtml.find(".item").each((i, li) => {
|
||||
if (li.classList.contains("inventory-header")) return;
|
||||
li.setAttribute("draggable", true);
|
||||
li.addEventListener("dragstart", handler, false);
|
||||
});
|
||||
//favtabHtml.find('.item-toggle').click(event => app._onToggleItem(event));
|
||||
favtabHtml.find('.item-edit').click(ev => {
|
||||
let itemId = $(ev.target).parents('.item')[0].dataset.itemId;
|
||||
app.actor.getOwnedItem(itemId).sheet.render(true);
|
||||
favtabHtml.find(".item-edit").click((ev) => {
|
||||
let itemId = $(ev.target).parents(".item")[0].dataset.itemId;
|
||||
app.actor.items.get(itemId).sheet.render(true);
|
||||
});
|
||||
favtabHtml.find('.item-fav').click(ev => {
|
||||
let itemId = $(ev.target).parents('.item')[0].dataset.itemId;
|
||||
let val = !app.actor.getOwnedItem(itemId).data.flags.favtab.isFavourite
|
||||
app.actor.getOwnedItem(itemId).update({
|
||||
favtabHtml.find(".item-fav").click((ev) => {
|
||||
let itemId = $(ev.target).parents(".item")[0].dataset.itemId;
|
||||
let val = !app.actor.items.get(itemId).data.flags.favtab.isFavourite;
|
||||
app.actor.items.get(itemId).update({
|
||||
"flags.favtab.isFavourite": val
|
||||
});
|
||||
});
|
||||
|
||||
// Sorting
|
||||
favtabHtml.find('.item').on('drop', ev => {
|
||||
favtabHtml.find(".item").on("drop", (ev) => {
|
||||
ev.preventDefault();
|
||||
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) return;
|
||||
let list = null;
|
||||
if (dropData.data.type === 'feat') list = favFeats;
|
||||
if (dropData.data.type === "feat") list = favFeats;
|
||||
else list = favItems;
|
||||
let dragSource = list.find(i => i._id === dropData.data._id);
|
||||
let siblings = list.filter(i => i._id !== dropData.data._id);
|
||||
let targetId = ev.target.closest('.item').dataset.itemId;
|
||||
let dragTarget = siblings.find(s => s._id === targetId);
|
||||
let dragSource = list.find((i) => i.data._id === dropData.data._id);
|
||||
let siblings = list.filter((i) => i.data._id !== dropData.data._id);
|
||||
let targetId = ev.target.closest(".item").dataset.itemId;
|
||||
let dragTarget = siblings.find((s) => s.data._id === targetId);
|
||||
|
||||
if (dragTarget === undefined) return;
|
||||
const sortUpdates = SortingHelpers.performIntegerSort(dragSource, {
|
||||
target: dragTarget,
|
||||
siblings: siblings,
|
||||
sortKey: 'flags.favtab.sort'
|
||||
sortKey: "flags.favtab.sort"
|
||||
});
|
||||
const updateData = sortUpdates.map(u => {
|
||||
const updateData = sortUpdates.map((u) => {
|
||||
const update = u.update;
|
||||
update._id = u.target._id;
|
||||
update._id = u.target.data._id;
|
||||
return update;
|
||||
});
|
||||
app.actor.updateEmbeddedEntity("OwnedItem", updateData);
|
||||
|
@ -579,53 +703,47 @@ async function addFavorites(app, html, data) {
|
|||
Hooks.callAll("renderedSwaltSheet", app, html, data);
|
||||
}
|
||||
async function addSubTabs(app, html, data) {
|
||||
if(data.options.subTabs == null) {
|
||||
if (data.options.subTabs == null) {
|
||||
//let subTabs = []; //{subgroup: '', target: '', active: false}
|
||||
data.options.subTabs = {};
|
||||
html.find('[data-subgroup-selection] [data-subgroup]').each((idx, el) => {
|
||||
let subgroup = el.getAttribute('data-subgroup');
|
||||
let target = el.getAttribute('data-target');
|
||||
let targetObj = {target: target, active: el.classList.contains("active")}
|
||||
if(data.options.subTabs.hasOwnProperty(subgroup)) {
|
||||
html.find("[data-subgroup-selection] [data-subgroup]").each((idx, el) => {
|
||||
let subgroup = el.getAttribute("data-subgroup");
|
||||
let target = el.getAttribute("data-target");
|
||||
let targetObj = {target: target, active: el.classList.contains("active")};
|
||||
if (data.options.subTabs.hasOwnProperty(subgroup)) {
|
||||
data.options.subTabs[subgroup].push(targetObj);
|
||||
} else {
|
||||
data.options.subTabs[subgroup] = [];
|
||||
data.options.subTabs[subgroup].push(targetObj);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
for(const group in data.options.subTabs) {
|
||||
data.options.subTabs[group].forEach(tab => {
|
||||
if(tab.active) {
|
||||
html.find(`[data-subgroup=${group}][data-target=${tab.target}]`).addClass('active');
|
||||
} else {
|
||||
html.find(`[data-subgroup=${group}][data-target=${tab.target}]`).removeClass('active');
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
html.find('[data-subgroup-selection]').children().on('click', event => {
|
||||
let subgroup = event.target.closest('[data-subgroup]').getAttribute('data-subgroup');
|
||||
let target = event.target.closest('[data-target]').getAttribute('data-target');
|
||||
html.find(`[data-subgroup=${subgroup}]`).removeClass('active');
|
||||
html.find(`[data-subgroup=${subgroup}][data-target=${target}]`).addClass('active');
|
||||
let tabId = data.options.subTabs[subgroup].find(tab => {
|
||||
return tab.target == target
|
||||
});
|
||||
data.options.subTabs[subgroup].map(el => {
|
||||
if(el.target == target) {
|
||||
el.active = true;
|
||||
} else {
|
||||
el.active = false;
|
||||
}
|
||||
|
||||
for (const group in data.options.subTabs) {
|
||||
data.options.subTabs[group].forEach((tab) => {
|
||||
if (tab.active) {
|
||||
html.find(`[data-subgroup=${group}][data-target=${tab.target}]`).addClass("active");
|
||||
} else {
|
||||
html.find(`[data-subgroup=${group}][data-target=${tab.target}]`).removeClass("active");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
html.find("[data-subgroup-selection]")
|
||||
.children()
|
||||
.on("click", (event) => {
|
||||
let subgroup = event.target.closest("[data-subgroup]").getAttribute("data-subgroup");
|
||||
let target = event.target.closest("[data-target]").getAttribute("data-target");
|
||||
html.find(`[data-subgroup=${subgroup}]`).removeClass("active");
|
||||
html.find(`[data-subgroup=${subgroup}][data-target=${target}]`).addClass("active");
|
||||
let tabId = data.options.subTabs[subgroup].find((tab) => {
|
||||
return tab.target == target;
|
||||
});
|
||||
data.options.subTabs[subgroup].map((el) => {
|
||||
el.active = el.target == target;
|
||||
return el;
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Hooks.on("renderActorSheet5eCharacterNew", (app, html, data) => {
|
||||
|
|
|
@ -6,10 +6,9 @@ import ActorSheet5e from "./base.js";
|
|||
* @extends {ActorSheet5e}
|
||||
*/
|
||||
export default class ActorSheet5eNPCNew extends ActorSheet5e {
|
||||
|
||||
/** @override */
|
||||
get template() {
|
||||
if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/newActor/limited-sheet.html";
|
||||
if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/newActor/limited-sheet.html";
|
||||
return `systems/sw5e/templates/actors/newActor/npc-sheet.html`;
|
||||
}
|
||||
/** @override */
|
||||
|
@ -17,43 +16,63 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
|
|||
return mergeObject(super.defaultOptions, {
|
||||
classes: ["sw5e", "sheet", "actor", "npc"],
|
||||
width: 800,
|
||||
tabs: [{
|
||||
tabs: [
|
||||
{
|
||||
navSelector: ".root-tabs",
|
||||
contentSelector: ".sheet-body",
|
||||
initial: "attributes"
|
||||
}],
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
static unsupportedItemTypes = new Set(["class"]);
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Organize Owned Items for rendering the NPC sheet
|
||||
* @private
|
||||
*/
|
||||
_prepareItems(data) {
|
||||
|
||||
// Categorize Items as Features and Powers
|
||||
const features = {
|
||||
weapons: { label: game.i18n.localize("SW5E.AttackPl"), items: [] , hasActions: true, dataset: {type: "weapon", "weapon-type": "natural"} },
|
||||
actions: { label: game.i18n.localize("SW5E.ActionPl"), items: [] , hasActions: true, dataset: {type: "feat", "activation.type": "action"} },
|
||||
passive: { label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"} },
|
||||
equipment: { label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}}
|
||||
weapons: {
|
||||
label: game.i18n.localize("SW5E.AttackPl"),
|
||||
items: [],
|
||||
hasActions: true,
|
||||
dataset: {"type": "weapon", "weapon-type": "natural"}
|
||||
},
|
||||
actions: {
|
||||
label: game.i18n.localize("SW5E.ActionPl"),
|
||||
items: [],
|
||||
hasActions: true,
|
||||
dataset: {"type": "feat", "activation.type": "action"}
|
||||
},
|
||||
passive: {label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"}},
|
||||
equipment: {label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}}
|
||||
};
|
||||
|
||||
// Start by classifying items into groups for rendering
|
||||
let [forcepowers, techpowers, other] = data.items.reduce((arr, item) => {
|
||||
item.img = item.img || DEFAULT_TOKEN;
|
||||
item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
|
||||
item.hasUses = item.data.uses && (item.data.uses.max > 0);
|
||||
item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false);
|
||||
item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0));
|
||||
item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type));
|
||||
if ( item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school) ) arr[0].push(item);
|
||||
else if ( item.type === "power" && ["tec"].includes(item.data.school) ) arr[1].push(item);
|
||||
let [forcepowers, techpowers, other] = data.items.reduce(
|
||||
(arr, item) => {
|
||||
item.img = item.img || CONST.DEFAULT_TOKEN;
|
||||
item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1;
|
||||
item.hasUses = item.data.uses && item.data.uses.max > 0;
|
||||
item.isOnCooldown =
|
||||
item.data.recharge && !!item.data.recharge.value && item.data.recharge.charged === false;
|
||||
item.isDepleted = item.isOnCooldown && item.data.uses.per && item.data.uses.value > 0;
|
||||
item.hasTarget = !!item.data.target && !["none", ""].includes(item.data.target.type);
|
||||
if (item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school)) arr[0].push(item);
|
||||
else if (item.type === "power" && ["tec"].includes(item.data.school)) arr[1].push(item);
|
||||
else arr[2].push(item);
|
||||
return arr;
|
||||
}, [[], [], []]);
|
||||
},
|
||||
[[], [], []]
|
||||
);
|
||||
|
||||
// Apply item filters
|
||||
forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook);
|
||||
|
@ -65,13 +84,12 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
|
|||
const techPowerbook = this._preparePowerbook(data, techpowers, "tec");
|
||||
|
||||
// Organize Features
|
||||
for ( let item of other ) {
|
||||
if ( item.type === "weapon" ) features.weapons.items.push(item);
|
||||
else if ( item.type === "feat" ) {
|
||||
if ( item.data.activation.type ) features.actions.items.push(item);
|
||||
for (let item of other) {
|
||||
if (item.type === "weapon") features.weapons.items.push(item);
|
||||
else if (item.type === "feat") {
|
||||
if (item.data.activation.type) features.actions.items.push(item);
|
||||
else features.passive.items.push(item);
|
||||
}
|
||||
else features.equipment.items.push(item);
|
||||
} else features.equipment.items.push(item);
|
||||
}
|
||||
|
||||
// Assign and return
|
||||
|
@ -80,17 +98,19 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
|
|||
data.techPowerbook = techPowerbook;
|
||||
}
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
getData() {
|
||||
const data = super.getData();
|
||||
/** @inheritdoc */
|
||||
getData(options) {
|
||||
const data = super.getData(options);
|
||||
|
||||
// Challenge Rating
|
||||
const cr = parseFloat(data.data.details.cr || 0);
|
||||
const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"};
|
||||
data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1;
|
||||
|
||||
// Creature Type
|
||||
data.labels["type"] = this.actor.labels.creatureType;
|
||||
return data;
|
||||
}
|
||||
|
||||
|
@ -99,17 +119,16 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
|
|||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_updateObject(event, formData) {
|
||||
|
||||
async _updateObject(event, formData) {
|
||||
// Format NPC Challenge Rating
|
||||
const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5};
|
||||
let crv = "data.details.cr";
|
||||
let cr = formData[crv];
|
||||
cr = crs[cr] || parseFloat(cr);
|
||||
if ( cr ) formData[crv] = cr < 1 ? cr : parseInt(cr);
|
||||
if (cr) formData[crv] = cr < 1 ? cr : parseInt(cr);
|
||||
|
||||
// Parent ActorSheet update steps
|
||||
super._updateObject(event, formData);
|
||||
return super._updateObject(event, formData);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -132,10 +151,9 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
|
|||
_onRollHPFormula(event) {
|
||||
event.preventDefault();
|
||||
const formula = this.actor.data.data.attributes.hp.formula;
|
||||
if ( !formula ) return;
|
||||
if (!formula) return;
|
||||
const hp = new Roll(formula).roll().total;
|
||||
AudioHelper.play({src: CONFIG.sounds.dice});
|
||||
this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
169
module/actor/sheets/newSheet/starship.js
Normal file
|
@ -0,0 +1,169 @@
|
|||
import ActorSheet5e from "./base.js";
|
||||
|
||||
/**
|
||||
* An Actor sheet for starships in the SW5E system.
|
||||
* Extends the base ActorSheet5e class.
|
||||
* @extends {ActorSheet5e}
|
||||
*/
|
||||
export default class ActorSheet5eStarship extends ActorSheet5e {
|
||||
/** @override */
|
||||
get template() {
|
||||
if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/newActor/limited-sheet.html";
|
||||
return `systems/sw5e/templates/actors/newActor/starship.html`;
|
||||
}
|
||||
/** @override */
|
||||
static get defaultOptions() {
|
||||
return mergeObject(super.defaultOptions, {
|
||||
classes: ["sw5e", "sheet", "actor", "starship"],
|
||||
width: 800,
|
||||
tabs: [
|
||||
{
|
||||
navSelector: ".root-tabs",
|
||||
contentSelector: ".sheet-body",
|
||||
initial: "attributes"
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Organize Owned Items for rendering the starship sheet
|
||||
* @private
|
||||
*/
|
||||
_prepareItems(data) {
|
||||
// Categorize Items as Features and Powers
|
||||
const features = {
|
||||
weapons: {
|
||||
label: game.i18n.localize("SW5E.ItemTypeWeaponPl"),
|
||||
items: [],
|
||||
hasActions: true,
|
||||
dataset: {"type": "weapon", "weapon-type": "natural"}
|
||||
},
|
||||
passive: {label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"}},
|
||||
equipment: {label: game.i18n.localize("SW5E.StarshipEquipment"), items: [], dataset: {type: "equipment"}},
|
||||
starshipfeatures: {
|
||||
label: game.i18n.localize("SW5E.StarshipfeaturePl"),
|
||||
items: [],
|
||||
hasActions: true,
|
||||
dataset: {type: "starshipfeature"}
|
||||
},
|
||||
starshipmods: {
|
||||
label: game.i18n.localize("SW5E.StarshipmodPl"),
|
||||
items: [],
|
||||
hasActions: false,
|
||||
dataset: {type: "starshipmod"}
|
||||
}
|
||||
};
|
||||
|
||||
// Start by classifying items into groups for rendering
|
||||
let [forcepowers, techpowers, other] = data.items.reduce(
|
||||
(arr, item) => {
|
||||
item.img = item.img || CONST.DEFAULT_TOKEN;
|
||||
item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1;
|
||||
item.hasUses = item.data.uses && item.data.uses.max > 0;
|
||||
item.isOnCooldown =
|
||||
item.data.recharge && !!item.data.recharge.value && item.data.recharge.charged === false;
|
||||
item.isDepleted = item.isOnCooldown && item.data.uses.per && item.data.uses.value > 0;
|
||||
item.hasTarget = !!item.data.target && !["none", ""].includes(item.data.target.type);
|
||||
if (item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school)) arr[0].push(item);
|
||||
else if (item.type === "power" && ["tec"].includes(item.data.school)) arr[1].push(item);
|
||||
else arr[2].push(item);
|
||||
return arr;
|
||||
},
|
||||
[[], [], []]
|
||||
);
|
||||
|
||||
// Apply item filters
|
||||
forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook);
|
||||
techpowers = this._filterItems(techpowers, this._filters.techPowerbook);
|
||||
other = this._filterItems(other, this._filters.features);
|
||||
|
||||
// Organize Powerbook
|
||||
// const forcePowerbook = this._preparePowerbook(data, forcepowers, "uni");
|
||||
// const techPowerbook = this._preparePowerbook(data, techpowers, "tec");
|
||||
|
||||
// Organize Features
|
||||
for (let item of other) {
|
||||
if (item.type === "weapon") features.weapons.items.push(item);
|
||||
else if (item.type === "feat") {
|
||||
if (item.data.activation.type) features.actions.items.push(item);
|
||||
else features.passive.items.push(item);
|
||||
} else if (item.type === "starshipfeature") {
|
||||
features.starshipfeatures.items.push(item);
|
||||
} else if (item.type === "starshipmod") {
|
||||
features.starshipmods.items.push(item);
|
||||
} else features.equipment.items.push(item);
|
||||
}
|
||||
|
||||
// Assign and return
|
||||
data.features = Object.values(features);
|
||||
// data.forcePowerbook = forcePowerbook;
|
||||
// data.techPowerbook = techPowerbook;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
getData(options) {
|
||||
const data = super.getData(options);
|
||||
|
||||
// Add Size info
|
||||
data.isTiny = data.actor.data.traits.size === "tiny";
|
||||
data.isSmall = data.actor.data.traits.size === "sm";
|
||||
data.isMedium = data.actor.data.traits.size === "med";
|
||||
data.isLarge = data.actor.data.traits.size === "lg";
|
||||
data.isHuge = data.actor.data.traits.size === "huge";
|
||||
data.isGargantuan = data.actor.data.traits.size === "grg";
|
||||
|
||||
// Challenge Rating
|
||||
const cr = parseFloat(data.data.details.cr || 0);
|
||||
const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"};
|
||||
data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1;
|
||||
return data;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Object Updates */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
async _updateObject(event, formData) {
|
||||
// Format NPC Challenge Rating
|
||||
const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5};
|
||||
let crv = "data.details.cr";
|
||||
let cr = formData[crv];
|
||||
cr = crs[cr] || parseFloat(cr);
|
||||
if (cr) formData[crv] = cr < 1 ? cr : parseInt(cr);
|
||||
|
||||
// Parent ActorSheet update steps
|
||||
return super._updateObject(event, formData);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Event Listeners and Handlers */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
activateListeners(html) {
|
||||
super.activateListeners(html);
|
||||
html.find(".health .rollable").click(this._onRollHPFormula.bind(this));
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle rolling NPC health values using the provided formula
|
||||
* @param {Event} event The original click event
|
||||
* @private
|
||||
*/
|
||||
_onRollHPFormula(event) {
|
||||
event.preventDefault();
|
||||
const formula = this.actor.data.data.attributes.hp.formula;
|
||||
if (!formula) return;
|
||||
const hp = new Roll(formula).roll().total;
|
||||
AudioHelper.play({src: CONFIG.sounds.dice});
|
||||
this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp});
|
||||
}
|
||||
}
|
|
@ -20,12 +20,17 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
static unsupportedItemTypes = new Set(["class"]);
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Creates a new cargo entry for a vehicle Actor.
|
||||
*/
|
||||
static get newCargo() {
|
||||
return {
|
||||
name: '',
|
||||
name: "",
|
||||
quantity: 1
|
||||
};
|
||||
}
|
||||
|
@ -40,7 +45,6 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
|
|||
* @private
|
||||
*/
|
||||
_computeEncumbrance(totalWeight, actorData) {
|
||||
|
||||
// Compute currency weight
|
||||
const totalCoins = Object.values(actorData.data.currency).reduce((acc, denom) => acc + denom, 0);
|
||||
totalWeight += totalCoins / CONFIG.SW5E.encumbrance.currencyPerWeight;
|
||||
|
@ -57,7 +61,7 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
|
|||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_getMovementSpeed(actorData, largestPrimary=true) {
|
||||
_getMovementSpeed(actorData, largestPrimary = true) {
|
||||
return super._getMovementSpeed(actorData, largestPrimary);
|
||||
}
|
||||
|
||||
|
@ -69,25 +73,24 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
|
|||
* @private
|
||||
*/
|
||||
_prepareCrewedItem(item) {
|
||||
|
||||
// Determine crewed status
|
||||
const isCrewed = item.data.crewed;
|
||||
item.toggleClass = isCrewed ? 'active' : '';
|
||||
item.toggleTitle = game.i18n.localize(`SW5E.${isCrewed ? 'Crewed' : 'Uncrewed'}`);
|
||||
item.toggleClass = isCrewed ? "active" : "";
|
||||
item.toggleTitle = game.i18n.localize(`SW5E.${isCrewed ? "Crewed" : "Uncrewed"}`);
|
||||
|
||||
// Handle crew actions
|
||||
if (item.type === 'feat' && item.data.activation.type === 'crew') {
|
||||
if (item.type === "feat" && item.data.activation.type === "crew") {
|
||||
item.crew = item.data.activation.cost;
|
||||
item.cover = game.i18n.localize(`SW5E.${item.data.cover ? 'CoverTotal' : 'None'}`);
|
||||
if (item.data.cover === .5) item.cover = '½';
|
||||
else if (item.data.cover === .75) item.cover = '¾';
|
||||
else if (item.data.cover === null) item.cover = '—';
|
||||
if (item.crew < 1 || item.crew === null) item.crew = '—';
|
||||
item.cover = game.i18n.localize(`SW5E.${item.data.cover ? "CoverTotal" : "None"}`);
|
||||
if (item.data.cover === 0.5) item.cover = "½";
|
||||
else if (item.data.cover === 0.75) item.cover = "¾";
|
||||
else if (item.data.cover === null) item.cover = "—";
|
||||
if (item.crew < 1 || item.crew === null) item.crew = "—";
|
||||
}
|
||||
|
||||
// Prepare vehicle weapons
|
||||
if (item.type === 'equipment' || item.type === 'weapon') {
|
||||
item.threshold = item.data.hp.dt ? item.data.hp.dt : '—';
|
||||
if (item.type === "equipment" || item.type === "weapon") {
|
||||
item.threshold = item.data.hp.dt ? item.data.hp.dt : "—";
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -98,132 +101,162 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
|
|||
* @private
|
||||
*/
|
||||
_prepareItems(data) {
|
||||
const cargoColumns = [{
|
||||
label: game.i18n.localize('SW5E.Quantity'),
|
||||
css: 'item-qty',
|
||||
property: 'quantity',
|
||||
editable: 'Number'
|
||||
}];
|
||||
const cargoColumns = [
|
||||
{
|
||||
label: game.i18n.localize("SW5E.Quantity"),
|
||||
css: "item-qty",
|
||||
property: "quantity",
|
||||
editable: "Number"
|
||||
}
|
||||
];
|
||||
|
||||
const equipmentColumns = [{
|
||||
label: game.i18n.localize('SW5E.Quantity'),
|
||||
css: 'item-qty',
|
||||
property: 'data.quantity'
|
||||
}, {
|
||||
label: game.i18n.localize('SW5E.AC'),
|
||||
css: 'item-ac',
|
||||
property: 'data.armor.value'
|
||||
}, {
|
||||
label: game.i18n.localize('SW5E.HP'),
|
||||
css: 'item-hp',
|
||||
property: 'data.hp.value',
|
||||
editable: 'Number'
|
||||
}, {
|
||||
label: game.i18n.localize('SW5E.Threshold'),
|
||||
css: 'item-threshold',
|
||||
property: 'threshold'
|
||||
}];
|
||||
const equipmentColumns = [
|
||||
{
|
||||
label: game.i18n.localize("SW5E.Quantity"),
|
||||
css: "item-qty",
|
||||
property: "data.quantity"
|
||||
},
|
||||
{
|
||||
label: game.i18n.localize("SW5E.AC"),
|
||||
css: "item-ac",
|
||||
property: "data.armor.value"
|
||||
},
|
||||
{
|
||||
label: game.i18n.localize("SW5E.HP"),
|
||||
css: "item-hp",
|
||||
property: "data.hp.value",
|
||||
editable: "Number"
|
||||
},
|
||||
{
|
||||
label: game.i18n.localize("SW5E.Threshold"),
|
||||
css: "item-threshold",
|
||||
property: "threshold"
|
||||
}
|
||||
];
|
||||
|
||||
const features = {
|
||||
actions: {
|
||||
label: game.i18n.localize('SW5E.ActionPl'),
|
||||
label: game.i18n.localize("SW5E.ActionPl"),
|
||||
items: [],
|
||||
crewable: true,
|
||||
dataset: {type: 'feat', 'activation.type': 'crew'},
|
||||
columns: [{
|
||||
label: game.i18n.localize('SW5E.VehicleCrew'),
|
||||
css: 'item-crew',
|
||||
property: 'crew'
|
||||
}, {
|
||||
label: game.i18n.localize('SW5E.Cover'),
|
||||
css: 'item-cover',
|
||||
property: 'cover'
|
||||
}]
|
||||
dataset: {"type": "feat", "activation.type": "crew"},
|
||||
columns: [
|
||||
{
|
||||
label: game.i18n.localize("SW5E.VehicleCrew"),
|
||||
css: "item-crew",
|
||||
property: "crew"
|
||||
},
|
||||
{
|
||||
label: game.i18n.localize("SW5E.Cover"),
|
||||
css: "item-cover",
|
||||
property: "cover"
|
||||
}
|
||||
]
|
||||
},
|
||||
equipment: {
|
||||
label: game.i18n.localize('SW5E.ItemTypeEquipment'),
|
||||
label: game.i18n.localize("SW5E.ItemTypeEquipment"),
|
||||
items: [],
|
||||
crewable: true,
|
||||
dataset: {type: 'equipment', 'armor.type': 'vehicle'},
|
||||
dataset: {"type": "equipment", "armor.type": "vehicle"},
|
||||
columns: equipmentColumns
|
||||
},
|
||||
passive: {
|
||||
label: game.i18n.localize('SW5E.Features'),
|
||||
label: game.i18n.localize("SW5E.Features"),
|
||||
items: [],
|
||||
dataset: {type: 'feat'}
|
||||
dataset: {type: "feat"}
|
||||
},
|
||||
reactions: {
|
||||
label: game.i18n.localize('SW5E.ReactionPl'),
|
||||
label: game.i18n.localize("SW5E.ReactionPl"),
|
||||
items: [],
|
||||
dataset: {type: 'feat', 'activation.type': 'reaction'}
|
||||
dataset: {"type": "feat", "activation.type": "reaction"}
|
||||
},
|
||||
weapons: {
|
||||
label: game.i18n.localize('SW5E.ItemTypeWeaponPl'),
|
||||
label: game.i18n.localize("SW5E.ItemTypeWeaponPl"),
|
||||
items: [],
|
||||
crewable: true,
|
||||
dataset: {type: 'weapon', 'weapon-type': 'siege'},
|
||||
dataset: {"type": "weapon", "weapon-type": "siege"},
|
||||
columns: equipmentColumns
|
||||
}
|
||||
};
|
||||
|
||||
const cargo = {
|
||||
crew: {
|
||||
label: game.i18n.localize('SW5E.VehicleCrew'),
|
||||
label: game.i18n.localize("SW5E.VehicleCrew"),
|
||||
items: data.data.cargo.crew,
|
||||
css: 'cargo-row crew',
|
||||
css: "cargo-row crew",
|
||||
editableName: true,
|
||||
dataset: {type: 'crew'},
|
||||
dataset: {type: "crew"},
|
||||
columns: cargoColumns
|
||||
},
|
||||
passengers: {
|
||||
label: game.i18n.localize('SW5E.VehiclePassengers'),
|
||||
label: game.i18n.localize("SW5E.VehiclePassengers"),
|
||||
items: data.data.cargo.passengers,
|
||||
css: 'cargo-row passengers',
|
||||
css: "cargo-row passengers",
|
||||
editableName: true,
|
||||
dataset: {type: 'passengers'},
|
||||
dataset: {type: "passengers"},
|
||||
columns: cargoColumns
|
||||
},
|
||||
cargo: {
|
||||
label: game.i18n.localize('SW5E.VehicleCargo'),
|
||||
label: game.i18n.localize("SW5E.VehicleCargo"),
|
||||
items: [],
|
||||
dataset: {type: 'loot'},
|
||||
columns: [{
|
||||
label: game.i18n.localize('SW5E.Quantity'),
|
||||
css: 'item-qty',
|
||||
property: 'data.quantity',
|
||||
editable: 'Number'
|
||||
}, {
|
||||
label: game.i18n.localize('SW5E.Price'),
|
||||
css: 'item-price',
|
||||
property: 'data.price',
|
||||
editable: 'Number'
|
||||
}, {
|
||||
label: game.i18n.localize('SW5E.Weight'),
|
||||
css: 'item-weight',
|
||||
property: 'data.weight',
|
||||
editable: 'Number'
|
||||
}]
|
||||
dataset: {type: "loot"},
|
||||
columns: [
|
||||
{
|
||||
label: game.i18n.localize("SW5E.Quantity"),
|
||||
css: "item-qty",
|
||||
property: "data.quantity",
|
||||
editable: "Number"
|
||||
},
|
||||
{
|
||||
label: game.i18n.localize("SW5E.Price"),
|
||||
css: "item-price",
|
||||
property: "data.price",
|
||||
editable: "Number"
|
||||
},
|
||||
{
|
||||
label: game.i18n.localize("SW5E.Weight"),
|
||||
css: "item-weight",
|
||||
property: "data.weight",
|
||||
editable: "Number"
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
// Classify items owned by the vehicle and compute total cargo weight
|
||||
let totalWeight = 0;
|
||||
for (const item of data.items) {
|
||||
this._prepareCrewedItem(item);
|
||||
if (item.type === 'weapon') features.weapons.items.push(item);
|
||||
else if (item.type === 'equipment') features.equipment.items.push(item);
|
||||
else if (item.type === 'loot') {
|
||||
|
||||
// Handle cargo explicitly
|
||||
const isCargo = item.flags.sw5e?.vehicleCargo === true;
|
||||
if (isCargo) {
|
||||
totalWeight += (item.data.weight || 0) * item.data.quantity;
|
||||
cargo.cargo.items.push(item);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle non-cargo item types
|
||||
switch (item.type) {
|
||||
case "weapon":
|
||||
features.weapons.items.push(item);
|
||||
break;
|
||||
case "equipment":
|
||||
features.equipment.items.push(item);
|
||||
break;
|
||||
case "feat":
|
||||
if (!item.data.activation.type || item.data.activation.type === "none")
|
||||
features.passive.items.push(item);
|
||||
else if (item.data.activation.type === "reaction") features.reactions.items.push(item);
|
||||
else features.actions.items.push(item);
|
||||
break;
|
||||
default:
|
||||
totalWeight += (item.data.weight || 0) * item.data.quantity;
|
||||
cargo.cargo.items.push(item);
|
||||
}
|
||||
else if (item.type === 'feat') {
|
||||
if (!item.data.activation.type || item.data.activation.type === 'none') {
|
||||
features.passive.items.push(item);
|
||||
}
|
||||
else if (item.data.activation.type === 'reaction') features.reactions.items.push(item);
|
||||
else features.actions.items.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the rendering context data
|
||||
data.features = Object.values(features);
|
||||
data.cargo = Object.values(cargo);
|
||||
data.data.attributes.encumbrance = this._computeEncumbrance(totalWeight, data);
|
||||
|
@ -236,23 +269,23 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
|
|||
/** @override */
|
||||
activateListeners(html) {
|
||||
super.activateListeners(html);
|
||||
if (!this.options.editable) return;
|
||||
if (!this.isEditable) return;
|
||||
|
||||
html.find('.item-toggle').click(this._onToggleItem.bind(this));
|
||||
html.find('.item-hp input')
|
||||
.click(evt => evt.target.select())
|
||||
html.find(".item-toggle").click(this._onToggleItem.bind(this));
|
||||
html.find(".item-hp input")
|
||||
.click((evt) => evt.target.select())
|
||||
.change(this._onHPChange.bind(this));
|
||||
|
||||
html.find('.item:not(.cargo-row) input[data-property]')
|
||||
.click(evt => evt.target.select())
|
||||
html.find(".item:not(.cargo-row) input[data-property]")
|
||||
.click((evt) => evt.target.select())
|
||||
.change(this._onEditInSheet.bind(this));
|
||||
|
||||
html.find('.cargo-row input')
|
||||
.click(evt => evt.target.select())
|
||||
html.find(".cargo-row input")
|
||||
.click((evt) => evt.target.select())
|
||||
.change(this._onCargoRowChange.bind(this));
|
||||
|
||||
if (this.actor.data.data.attributes.actions.stations) {
|
||||
html.find('.counter.actions, .counter.action-thresholds').hide();
|
||||
html.find(".counter.actions, .counter.action-thresholds").hide();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -267,20 +300,20 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
|
|||
_onCargoRowChange(event) {
|
||||
event.preventDefault();
|
||||
const target = event.currentTarget;
|
||||
const row = target.closest('.item');
|
||||
const row = target.closest(".item");
|
||||
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
|
||||
const cargo = duplicate(this.actor.data.data.cargo[property]);
|
||||
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[property]);
|
||||
const entry = cargo[idx];
|
||||
if (!entry) return null;
|
||||
|
||||
// Update the cargo value
|
||||
const key = target.dataset.property || 'name';
|
||||
const key = target.dataset.property || "name";
|
||||
const type = target.dataset.dtype;
|
||||
let value = target.value;
|
||||
if (type === 'Number') value = Number(value);
|
||||
if (type === "Number") value = Number(value);
|
||||
entry[key] = value;
|
||||
|
||||
// Perform the Actor update
|
||||
|
@ -297,14 +330,18 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
|
|||
*/
|
||||
_onEditInSheet(event) {
|
||||
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 property = event.currentTarget.dataset.property;
|
||||
const type = event.currentTarget.dataset.dtype;
|
||||
let value = event.currentTarget.value;
|
||||
switch (type) {
|
||||
case 'Number': value = parseInt(value); break;
|
||||
case 'Boolean': value = value === 'true'; break;
|
||||
case "Number":
|
||||
value = parseInt(value);
|
||||
break;
|
||||
case "Boolean":
|
||||
value = value === "true";
|
||||
break;
|
||||
}
|
||||
return item.update({[`${property}`]: value});
|
||||
}
|
||||
|
@ -321,8 +358,8 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
|
|||
event.preventDefault();
|
||||
const target = event.currentTarget;
|
||||
const type = target.dataset.type;
|
||||
if (type === 'crew' || type === 'passengers') {
|
||||
const cargo = duplicate(this.actor.data.data.cargo[type]);
|
||||
if (type === "crew" || type === "passengers") {
|
||||
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]);
|
||||
cargo.push(this.constructor.newCargo);
|
||||
return this.actor.update({[`data.cargo.${type}`]: cargo});
|
||||
}
|
||||
|
@ -339,11 +376,11 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
|
|||
*/
|
||||
_onItemDelete(event) {
|
||||
event.preventDefault();
|
||||
const row = event.currentTarget.closest('.item');
|
||||
if (row.classList.contains('cargo-row')) {
|
||||
const row = event.currentTarget.closest(".item");
|
||||
if (row.classList.contains("cargo-row")) {
|
||||
const idx = Number(row.dataset.itemId);
|
||||
const type = row.classList.contains('crew') ? 'crew' : 'passengers';
|
||||
const cargo = duplicate(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx);
|
||||
const type = row.classList.contains("crew") ? "crew" : "passengers";
|
||||
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx);
|
||||
return this.actor.update({[`data.cargo.${type}`]: cargo});
|
||||
}
|
||||
|
||||
|
@ -352,6 +389,16 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
async _onDropItemCreate(itemData) {
|
||||
const cargoTypes = ["weapon", "equipment", "consumable", "tool", "loot", "backpack"];
|
||||
const isCargo = cargoTypes.includes(itemData.type) && this._tabs[0].active === "cargo";
|
||||
foundry.utils.setProperty(itemData, "flags.sw5e.vehicleCargo", isCargo);
|
||||
return super._onDropItemCreate(itemData);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Special handling for editing HP to clamp it within appropriate range.
|
||||
* @param event {Event}
|
||||
|
@ -360,11 +407,11 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
|
|||
*/
|
||||
_onHPChange(event) {
|
||||
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 hp = Math.clamped(0, parseInt(event.currentTarget.value), item.data.data.hp.max);
|
||||
event.currentTarget.value = hp;
|
||||
return item.update({'data.hp.value': hp});
|
||||
return item.update({"data.hp.value": hp});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -377,9 +424,9 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
|
|||
*/
|
||||
_onToggleItem(event) {
|
||||
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 crewed = !!item.data.data.crewed;
|
||||
return item.update({'data.crewed': !crewed});
|
||||
return item.update({"data.crewed": !crewed});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import Item5e from "../../../item/entity.js";
|
||||
import TraitSelector from "../../../apps/trait-selector.js";
|
||||
import ActorSheetFlags from "../../../apps/actor-flags.js";
|
||||
import ActorHitDiceConfig from "../../../apps/hit-dice-config.js";
|
||||
import ActorMovementConfig from "../../../apps/movement-config.js";
|
||||
import ActorSensesConfig from "../../../apps/senses-config.js";
|
||||
import {SW5E} from '../../../config.js';
|
||||
import ActorTypeConfig from "../../../apps/actor-type.js";
|
||||
import {SW5E} from "../../../config.js";
|
||||
import {onManageActiveEffect, prepareActiveEffectCategories} from "../../../effects.js";
|
||||
|
||||
/**
|
||||
|
@ -44,52 +46,67 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A set of item types that should be prevented from being dropped on this type of actor sheet.
|
||||
* @type {Set<string>}
|
||||
*/
|
||||
static unsupportedItemTypes = new Set();
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
get template() {
|
||||
if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/oldActor/limited-sheet.html";
|
||||
if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/oldActor/limited-sheet.html";
|
||||
return `systems/sw5e/templates/actors/oldActor/${this.actor.data.type}-sheet.html`;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
getData() {
|
||||
|
||||
getData(options) {
|
||||
// Basic data
|
||||
let isOwner = this.entity.owner;
|
||||
let isOwner = this.actor.isOwner;
|
||||
const data = {
|
||||
owner: isOwner,
|
||||
limited: this.entity.limited,
|
||||
limited: this.actor.limited,
|
||||
options: this.options,
|
||||
editable: this.isEditable,
|
||||
cssClass: isOwner ? "editable" : "locked",
|
||||
isCharacter: this.entity.data.type === "character",
|
||||
isNPC: this.entity.data.type === "npc",
|
||||
isVehicle: this.entity.data.type === 'vehicle',
|
||||
isCharacter: this.actor.type === "character",
|
||||
isNPC: this.actor.type === "npc",
|
||||
isStarship: this.actor.type === "starship",
|
||||
isVehicle: this.actor.type === "vehicle",
|
||||
config: CONFIG.SW5E,
|
||||
rollData: this.actor.getRollData.bind(this.actor)
|
||||
};
|
||||
|
||||
// The Actor and its Items
|
||||
data.actor = duplicate(this.actor.data);
|
||||
data.items = this.actor.items.map(i => {
|
||||
i.data.labels = i.labels;
|
||||
return i.data;
|
||||
});
|
||||
// The Actor's data
|
||||
const actorData = this.actor.data.toObject(false);
|
||||
data.actor = actorData;
|
||||
data.data = actorData.data;
|
||||
|
||||
// Owned Items
|
||||
data.items = actorData.items;
|
||||
for (let i of data.items) {
|
||||
const item = this.actor.items.get(i._id);
|
||||
i.labels = item.labels;
|
||||
}
|
||||
data.items.sort((a, b) => (a.sort || 0) - (b.sort || 0));
|
||||
data.data = data.actor.data;
|
||||
|
||||
// Labels and filters
|
||||
data.labels = this.actor.labels || {};
|
||||
data.filters = this._filters;
|
||||
|
||||
// Ability Scores
|
||||
for ( let [a, abl] of Object.entries(data.actor.data.abilities)) {
|
||||
for (let [a, abl] of Object.entries(actorData.data.abilities)) {
|
||||
abl.icon = this._getProficiencyIcon(abl.proficient);
|
||||
abl.hover = CONFIG.SW5E.proficiencyLevels[abl.proficient];
|
||||
abl.label = CONFIG.SW5E.abilities[a];
|
||||
}
|
||||
|
||||
// Skills
|
||||
if (data.actor.data.skills) {
|
||||
for ( let [s, skl] of Object.entries(data.actor.data.skills)) {
|
||||
if (actorData.data.skills) {
|
||||
for (let [s, skl] of Object.entries(actorData.data.skills)) {
|
||||
skl.ability = CONFIG.SW5E.abilityAbbreviations[skl.ability];
|
||||
skl.icon = this._getProficiencyIcon(skl.value);
|
||||
skl.hover = CONFIG.SW5E.proficiencyLevels[skl.value];
|
||||
|
@ -98,22 +115,22 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
}
|
||||
|
||||
// Movement speeds
|
||||
data.movement = this._getMovementSpeed(data.actor);
|
||||
data.movement = this._getMovementSpeed(actorData);
|
||||
|
||||
// Senses
|
||||
data.senses = this._getSenses(data.actor);
|
||||
data.senses = this._getSenses(actorData);
|
||||
|
||||
// Update traits
|
||||
this._prepareTraits(data.actor.data.traits);
|
||||
this._prepareTraits(actorData.data.traits);
|
||||
|
||||
// Prepare owned items
|
||||
this._prepareItems(data);
|
||||
|
||||
// Prepare active effects
|
||||
data.effects = prepareActiveEffectCategories(this.entity.effects);
|
||||
data.effects = prepareActiveEffectCategories(this.actor.effects);
|
||||
|
||||
// Return data to the sheet
|
||||
return data
|
||||
return data;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -125,38 +142,42 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
* @returns {{primary: string, special: string}}
|
||||
* @private
|
||||
*/
|
||||
_getMovementSpeed(actorData, largestPrimary=false) {
|
||||
_getMovementSpeed(actorData, largestPrimary = false) {
|
||||
const movement = actorData.data.attributes.movement || {};
|
||||
|
||||
// Prepare an array of available movement speeds
|
||||
let speeds = [
|
||||
[movement.burrow, `${game.i18n.localize("SW5E.MovementBurrow")} ${movement.burrow}`],
|
||||
[movement.climb, `${game.i18n.localize("SW5E.MovementClimb")} ${movement.climb}`],
|
||||
[movement.fly, `${game.i18n.localize("SW5E.MovementFly")} ${movement.fly}` + (movement.hover ? ` (${game.i18n.localize("SW5E.MovementHover")})` : "")],
|
||||
[
|
||||
movement.fly,
|
||||
`${game.i18n.localize("SW5E.MovementFly")} ${movement.fly}` +
|
||||
(movement.hover ? ` (${game.i18n.localize("SW5E.MovementHover")})` : "")
|
||||
],
|
||||
[movement.swim, `${game.i18n.localize("SW5E.MovementSwim")} ${movement.swim}`]
|
||||
]
|
||||
if ( largestPrimary ) {
|
||||
];
|
||||
if (largestPrimary) {
|
||||
speeds.push([movement.walk, `${game.i18n.localize("SW5E.MovementWalk")} ${movement.walk}`]);
|
||||
}
|
||||
|
||||
// Filter and sort speeds on their values
|
||||
speeds = speeds.filter(s => !!s[0]).sort((a, b) => b[0] - a[0]);
|
||||
speeds = speeds.filter((s) => !!s[0]).sort((a, b) => b[0] - a[0]);
|
||||
|
||||
// Case 1: Largest as primary
|
||||
if ( largestPrimary ) {
|
||||
if (largestPrimary) {
|
||||
let primary = speeds.shift();
|
||||
return {
|
||||
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
|
||||
else {
|
||||
return {
|
||||
primary: `${movement.walk || 0} ${movement.units}`,
|
||||
special: speeds.length ? speeds.map(s => s[1]).join(", ") : ""
|
||||
}
|
||||
special: speeds.length ? speeds.map((s) => s[1]).join(", ") : ""
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -165,12 +186,12 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
_getSenses(actorData) {
|
||||
const senses = actorData.data.attributes.senses || {};
|
||||
const tags = {};
|
||||
for ( let [k, label] of Object.entries(CONFIG.SW5E.senses) ) {
|
||||
const v = senses[k] ?? 0
|
||||
if ( v === 0 ) continue;
|
||||
for (let [k, label] of Object.entries(CONFIG.SW5E.senses)) {
|
||||
const v = senses[k] ?? 0;
|
||||
if (v === 0) continue;
|
||||
tags[k] = `${game.i18n.localize(label)} ${v} ${senses.units}`;
|
||||
}
|
||||
if ( !!senses.special ) tags["special"] = senses.special;
|
||||
if (!!senses.special) tags["special"] = senses.special;
|
||||
return tags;
|
||||
}
|
||||
|
||||
|
@ -183,20 +204,20 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
*/
|
||||
_prepareTraits(traits) {
|
||||
const map = {
|
||||
"dr": CONFIG.SW5E.damageResistanceTypes,
|
||||
"di": CONFIG.SW5E.damageResistanceTypes,
|
||||
"dv": CONFIG.SW5E.damageResistanceTypes,
|
||||
"ci": CONFIG.SW5E.conditionTypes,
|
||||
"languages": CONFIG.SW5E.languages,
|
||||
"armorProf": CONFIG.SW5E.armorProficiencies,
|
||||
"weaponProf": CONFIG.SW5E.weaponProficiencies,
|
||||
"toolProf": CONFIG.SW5E.toolProficiencies
|
||||
dr: CONFIG.SW5E.damageResistanceTypes,
|
||||
di: CONFIG.SW5E.damageResistanceTypes,
|
||||
dv: CONFIG.SW5E.damageResistanceTypes,
|
||||
ci: CONFIG.SW5E.conditionTypes,
|
||||
languages: CONFIG.SW5E.languages,
|
||||
armorProf: CONFIG.SW5E.armorProficiencies,
|
||||
weaponProf: CONFIG.SW5E.weaponProficiencies,
|
||||
toolProf: CONFIG.SW5E.toolProficiencies
|
||||
};
|
||||
for ( let [t, choices] of Object.entries(map) ) {
|
||||
for (let [t, choices] of Object.entries(map)) {
|
||||
const trait = traits[t];
|
||||
if ( !trait ) continue;
|
||||
if (!trait) continue;
|
||||
let values = [];
|
||||
if ( trait.value ) {
|
||||
if (trait.value) {
|
||||
values = trait.value instanceof Array ? trait.value : [trait.value];
|
||||
}
|
||||
trait.selected = values.reduce((obj, t) => {
|
||||
|
@ -205,8 +226,8 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
}, {});
|
||||
|
||||
// Add custom entry
|
||||
if ( trait.custom ) {
|
||||
trait.custom.split(";").forEach((c, i) => trait.selected[`custom${i+1}`] = c.trim());
|
||||
if (trait.custom) {
|
||||
trait.custom.split(";").forEach((c, i) => (trait.selected[`custom${i + 1}`] = c.trim()));
|
||||
}
|
||||
trait.cssClass = !isObjectEmpty(trait.selected) ? "" : "inactive";
|
||||
}
|
||||
|
@ -221,15 +242,15 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
* @private
|
||||
*/
|
||||
_preparePowerbook(data, powers) {
|
||||
const owner = this.actor.owner;
|
||||
const owner = this.actor.isOwner;
|
||||
const levels = data.data.powers;
|
||||
const powerbook = {};
|
||||
|
||||
// Define some mappings
|
||||
const sections = {
|
||||
"atwill": -20,
|
||||
"innate": -10,
|
||||
"pact": 0.5
|
||||
atwill: -20,
|
||||
innate: -10,
|
||||
pact: 0.5
|
||||
};
|
||||
|
||||
// Label power slot uses headers
|
||||
|
@ -240,13 +261,13 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
};
|
||||
|
||||
// Format a powerbook entry for a certain indexed level
|
||||
const registerSection = (sl, i, label, {prepMode="prepared", value, max, override}={}) => {
|
||||
const registerSection = (sl, i, label, {prepMode = "prepared", value, max, override} = {}) => {
|
||||
powerbook[i] = {
|
||||
order: i,
|
||||
label: label,
|
||||
usesSlots: i > 0,
|
||||
canCreate: owner,
|
||||
canPrepare: (data.actor.type === "character") && (i >= 1),
|
||||
canPrepare: data.actor.type === "character" && i >= 1,
|
||||
powers: [],
|
||||
uses: useLabels[i] || value || 0,
|
||||
slots: useLabels[i] || max || 0,
|
||||
|
@ -258,14 +279,14 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
|
||||
// Determine the maximum power level which has a slot
|
||||
const maxLevel = Array.fromRange(10).reduce((max, i) => {
|
||||
if ( i === 0 ) return max;
|
||||
if (i === 0) return max;
|
||||
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;
|
||||
}, 0);
|
||||
|
||||
// Level-based powercasters have cantrips and leveled slots
|
||||
if ( maxLevel > 0 ) {
|
||||
if (maxLevel > 0) {
|
||||
registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]);
|
||||
for (let lvl = 1; lvl <= maxLevel; lvl++) {
|
||||
const sl = `power${lvl}`;
|
||||
|
@ -274,11 +295,14 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
}
|
||||
|
||||
// Pact magic users have cantrips and a pact magic section
|
||||
if ( levels.pact && levels.pact.max ) {
|
||||
if ( !powerbook["0"] ) registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]);
|
||||
// TODO: Check if this is needed, we've removed pacts everywhere else
|
||||
if (levels.pact && levels.pact.max) {
|
||||
if (!powerbook["0"]) registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]);
|
||||
const l = levels.pact;
|
||||
const config = CONFIG.SW5E.powerPreparationModes.pact;
|
||||
registerSection("pact", sections.pact, config, {
|
||||
const level = game.i18n.localize(`SW5E.PowerLevel${levels.pact.level}`);
|
||||
const label = `${config} — ${level}`;
|
||||
registerSection("pact", sections.pact, label, {
|
||||
prepMode: "pact",
|
||||
value: l.value,
|
||||
max: l.max,
|
||||
|
@ -287,15 +311,15 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
}
|
||||
|
||||
// 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";
|
||||
let s = power.data.level || 0;
|
||||
const sl = `power${s}`;
|
||||
|
||||
// Specialized powercasting modes (if they exist)
|
||||
if ( mode in sections ) {
|
||||
if (mode in sections) {
|
||||
s = sections[mode];
|
||||
if ( !powerbook[s] ){
|
||||
if (!powerbook[s]) {
|
||||
const l = levels[mode] || {};
|
||||
const config = CONFIG.SW5E.powerPreparationModes[mode];
|
||||
registerSection(mode, s, config, {
|
||||
|
@ -308,7 +332,7 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
}
|
||||
|
||||
// Sections for higher-level powers which the caster "should not" have, but power items exist for
|
||||
else if ( !powerbook[s] ) {
|
||||
else if (!powerbook[s]) {
|
||||
registerSection(sl, s, CONFIG.SW5E.powerLevels[s], {levels: levels[sl]});
|
||||
}
|
||||
|
||||
|
@ -330,32 +354,32 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
* @private
|
||||
*/
|
||||
_filterItems(items, filters) {
|
||||
return items.filter(item => {
|
||||
return items.filter((item) => {
|
||||
const data = item.data;
|
||||
|
||||
// Action usage
|
||||
for ( let f of ["action", "bonus", "reaction"] ) {
|
||||
if ( filters.has(f) ) {
|
||||
if ((data.activation && (data.activation.type !== f))) return false;
|
||||
for (let f of ["action", "bonus", "reaction"]) {
|
||||
if (filters.has(f)) {
|
||||
if (data.activation && data.activation.type !== f) return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Power-specific filters
|
||||
if ( filters.has("ritual") ) {
|
||||
if (filters.has("ritual")) {
|
||||
if (data.components.ritual !== true) return false;
|
||||
}
|
||||
if ( filters.has("concentration") ) {
|
||||
if (filters.has("concentration")) {
|
||||
if (data.components.concentration !== true) return false;
|
||||
}
|
||||
if ( filters.has("prepared") ) {
|
||||
if ( data.level === 0 || ["innate", "always"].includes(data.preparation.mode) ) return true;
|
||||
if ( this.actor.data.type === "npc" ) return true;
|
||||
if (filters.has("prepared")) {
|
||||
if (data.level === 0 || ["innate", "always"].includes(data.preparation.mode)) return true;
|
||||
if (this.actor.data.type === "npc") return true;
|
||||
return data.preparation.prepared;
|
||||
}
|
||||
|
||||
// Equipment-specific filters
|
||||
if ( filters.has("equipped") ) {
|
||||
if ( data.equipped !== true ) return false;
|
||||
if (filters.has("equipped")) {
|
||||
if (data.equipped !== true) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
@ -381,64 +405,61 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
/* Event Listeners and Handlers
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Activate event listeners using the prepared sheet HTML
|
||||
* @param html {HTML} The prepared HTML object ready to be rendered into the DOM
|
||||
*/
|
||||
/** @inheritdoc */
|
||||
activateListeners(html) {
|
||||
|
||||
// Activate Item Filters
|
||||
const filterLists = html.find(".filter-list");
|
||||
filterLists.each(this._initializeFilterItemList.bind(this));
|
||||
filterLists.on("click", ".filter-item", this._onToggleFilter.bind(this));
|
||||
|
||||
// Item summaries
|
||||
html.find('.item .item-name.rollable h4').click(event => this._onItemSummary(event));
|
||||
html.find(".item .item-name.rollable h4").click((event) => this._onItemSummary(event));
|
||||
|
||||
// View Item Sheets
|
||||
html.find(".item-edit").click(this._onItemEdit.bind(this));
|
||||
|
||||
// Editable Only Listeners
|
||||
if ( this.isEditable ) {
|
||||
|
||||
if (this.isEditable) {
|
||||
// Input focus and update
|
||||
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));
|
||||
|
||||
// Ability Proficiency
|
||||
html.find('.ability-proficiency').click(this._onToggleAbilityProficiency.bind(this));
|
||||
html.find(".ability-proficiency").click(this._onToggleAbilityProficiency.bind(this));
|
||||
|
||||
// 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
|
||||
html.find('.trait-selector').click(this._onTraitSelector.bind(this));
|
||||
html.find(".trait-selector").click(this._onTraitSelector.bind(this));
|
||||
|
||||
// Configure Special Flags
|
||||
html.find('.config-button').click(this._onConfigMenu.bind(this));
|
||||
html.find(".config-button").click(this._onConfigMenu.bind(this));
|
||||
|
||||
// Owned Item management
|
||||
html.find('.item-create').click(this._onItemCreate.bind(this));
|
||||
html.find('.item-edit').click(this._onItemEdit.bind(this));
|
||||
html.find('.item-delete').click(this._onItemDelete.bind(this));
|
||||
html.find('.item-uses input').click(ev => ev.target.select()).change(this._onUsesChange.bind(this));
|
||||
html.find('.slot-max-override').click(this._onPowerSlotOverride.bind(this));
|
||||
html.find(".item-create").click(this._onItemCreate.bind(this));
|
||||
html.find(".item-delete").click(this._onItemDelete.bind(this));
|
||||
html.find(".item-uses input")
|
||||
.click((ev) => ev.target.select())
|
||||
.change(this._onUsesChange.bind(this));
|
||||
html.find(".slot-max-override").click(this._onPowerSlotOverride.bind(this));
|
||||
|
||||
// Active Effect management
|
||||
html.find(".effect-control").click(ev => onManageActiveEffect(ev, this.entity));
|
||||
html.find(".effect-control").click((ev) => onManageActiveEffect(ev, this.actor));
|
||||
}
|
||||
|
||||
// Owner Only Listeners
|
||||
if ( this.actor.owner ) {
|
||||
|
||||
if (this.actor.isOwner) {
|
||||
// Ability Checks
|
||||
html.find('.ability-name').click(this._onRollAbilityTest.bind(this));
|
||||
|
||||
html.find(".ability-name").click(this._onRollAbilityTest.bind(this));
|
||||
|
||||
// Roll Skill Checks
|
||||
html.find('.skill-name').click(this._onRollSkillCheck.bind(this));
|
||||
html.find(".skill-name").click(this._onRollSkillCheck.bind(this));
|
||||
|
||||
// Item Rolling
|
||||
html.find('.item .item-image').click(event => this._onItemRoll(event));
|
||||
html.find('.item .item-recharge').click(event => this._onItemRecharge(event));
|
||||
html.find(".item .item-image").click((event) => this._onItemRoll(event));
|
||||
html.find(".item .item-recharge").click((event) => this._onItemRecharge(event));
|
||||
}
|
||||
|
||||
// Otherwise remove rollable classes
|
||||
|
@ -459,8 +480,8 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
_initializeFilterItemList(i, ul) {
|
||||
const set = this._filters[ul.dataset.filter];
|
||||
const filters = ul.querySelectorAll(".filter-item");
|
||||
for ( let li of filters ) {
|
||||
if ( set.has(li.dataset.filter) ) li.classList.add("active");
|
||||
for (let li of filters) {
|
||||
if (set.has(li.dataset.filter)) li.classList.add("active");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -476,10 +497,10 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
_onChangeInputDelta(event) {
|
||||
const input = event.target;
|
||||
const value = input.value;
|
||||
if ( ["+", "-"].includes(value[0]) ) {
|
||||
if (["+", "-"].includes(value[0])) {
|
||||
let delta = parseFloat(value);
|
||||
input.value = getProperty(this.actor.data, input.name) + delta;
|
||||
} else if ( value[0] === "=" ) {
|
||||
} else if (value[0] === "=") {
|
||||
input.value = value.slice(1);
|
||||
}
|
||||
}
|
||||
|
@ -494,17 +515,25 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
_onConfigMenu(event) {
|
||||
event.preventDefault();
|
||||
const button = event.currentTarget;
|
||||
switch ( button.dataset.action ) {
|
||||
let app;
|
||||
switch (button.dataset.action) {
|
||||
case "hit-dice":
|
||||
app = new ActorHitDiceConfig(this.object);
|
||||
break;
|
||||
case "movement":
|
||||
new ActorMovementConfig(this.object).render(true);
|
||||
app = new ActorMovementConfig(this.object);
|
||||
break;
|
||||
case "flags":
|
||||
new ActorSheetFlags(this.object).render(true);
|
||||
app = new ActorSheetFlags(this.object);
|
||||
break;
|
||||
case "senses":
|
||||
new ActorSensesConfig(this.object).render(true);
|
||||
app = new ActorSensesConfig(this.object);
|
||||
break;
|
||||
case "type":
|
||||
new ActorTypeConfig(this.object).render(true);
|
||||
break;
|
||||
}
|
||||
app?.render(true);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -524,10 +553,10 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
let idx = levels.indexOf(level);
|
||||
|
||||
// Toggle next level - forward on click, backwards on right
|
||||
if ( event.type === "click" ) {
|
||||
field.val(levels[(idx === levels.length - 1) ? 0 : idx + 1]);
|
||||
} else if ( event.type === "contextmenu" ) {
|
||||
field.val(levels[(idx === 0) ? levels.length - 1 : idx - 1]);
|
||||
if (event.type === "click") {
|
||||
field.val(levels[idx === levels.length - 1 ? 0 : idx + 1]);
|
||||
} else if (event.type === "contextmenu") {
|
||||
field.val(levels[idx === 0 ? levels.length - 1 : idx - 1]);
|
||||
}
|
||||
|
||||
// Update the field value and save the form
|
||||
|
@ -538,49 +567,51 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
|
||||
/** @override */
|
||||
async _onDropActor(event, data) {
|
||||
const canPolymorph = game.user.isGM || (this.actor.owner && game.settings.get('sw5e', 'allowPolymorphing'));
|
||||
if ( !canPolymorph ) return false;
|
||||
const canPolymorph = game.user.isGM || (this.actor.isOwner && game.settings.get("sw5e", "allowPolymorphing"));
|
||||
if (!canPolymorph) return false;
|
||||
|
||||
// Get the target actor
|
||||
let sourceActor = null;
|
||||
if (data.pack) {
|
||||
const pack = game.packs.find(p => p.collection === data.pack);
|
||||
const pack = game.packs.find((p) => p.collection === data.pack);
|
||||
sourceActor = await pack.getEntity(data.id);
|
||||
} else {
|
||||
sourceActor = game.actors.get(data.id);
|
||||
}
|
||||
if ( !sourceActor ) return;
|
||||
if (!sourceActor) return;
|
||||
|
||||
// Define a function to record polymorph settings for future use
|
||||
const rememberOptions = html => {
|
||||
const rememberOptions = (html) => {
|
||||
const options = {};
|
||||
html.find('input').each((i, el) => {
|
||||
html.find("input").each((i, el) => {
|
||||
options[el.name] = el.checked;
|
||||
});
|
||||
const settings = mergeObject(game.settings.get('sw5e', 'polymorphSettings') || {}, options);
|
||||
game.settings.set('sw5e', 'polymorphSettings', settings);
|
||||
const settings = mergeObject(game.settings.get("sw5e", "polymorphSettings") || {}, options);
|
||||
game.settings.set("sw5e", "polymorphSettings", settings);
|
||||
return settings;
|
||||
};
|
||||
|
||||
// Create and render the Dialog
|
||||
return new Dialog({
|
||||
title: game.i18n.localize('SW5E.PolymorphPromptTitle'),
|
||||
return new Dialog(
|
||||
{
|
||||
title: game.i18n.localize("SW5E.PolymorphPromptTitle"),
|
||||
content: {
|
||||
options: game.settings.get('sw5e', 'polymorphSettings'),
|
||||
options: game.settings.get("sw5e", "polymorphSettings"),
|
||||
i18n: SW5E.polymorphSettings,
|
||||
isToken: this.actor.isToken
|
||||
},
|
||||
default: 'accept',
|
||||
default: "accept",
|
||||
buttons: {
|
||||
accept: {
|
||||
icon: '<i class="fas fa-check"></i>',
|
||||
label: game.i18n.localize('SW5E.PolymorphAcceptSettings'),
|
||||
callback: html => this.actor.transformInto(sourceActor, rememberOptions(html))
|
||||
label: game.i18n.localize("SW5E.PolymorphAcceptSettings"),
|
||||
callback: (html) => this.actor.transformInto(sourceActor, rememberOptions(html))
|
||||
},
|
||||
wildshape: {
|
||||
icon: '<i class="fas fa-paw"></i>',
|
||||
label: game.i18n.localize('SW5E.PolymorphWildShape'),
|
||||
callback: html => this.actor.transformInto(sourceActor, {
|
||||
label: game.i18n.localize("SW5E.PolymorphWildShape"),
|
||||
callback: (html) =>
|
||||
this.actor.transformInto(sourceActor, {
|
||||
keepBio: true,
|
||||
keepClass: true,
|
||||
keepMental: true,
|
||||
|
@ -591,37 +622,66 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
},
|
||||
polymorph: {
|
||||
icon: '<i class="fas fa-pastafarianism"></i>',
|
||||
label: game.i18n.localize('SW5E.Polymorph'),
|
||||
callback: html => this.actor.transformInto(sourceActor, {
|
||||
label: game.i18n.localize("SW5E.Polymorph"),
|
||||
callback: (html) =>
|
||||
this.actor.transformInto(sourceActor, {
|
||||
transformTokens: rememberOptions(html).transformTokens
|
||||
})
|
||||
},
|
||||
cancel: {
|
||||
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,
|
||||
template: 'systems/sw5e/templates/apps/polymorph-prompt.html'
|
||||
}).render(true);
|
||||
template: "systems/sw5e/templates/apps/polymorph-prompt.html"
|
||||
}
|
||||
).render(true);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
async _onDropItemCreate(itemData) {
|
||||
// Check to make sure items of this type are allowed on this actor
|
||||
if (this.constructor.unsupportedItemTypes.has(itemData.type)) {
|
||||
return ui.notifications.warn(
|
||||
game.i18n.format("SW5E.ActorWarningInvalidItem", {
|
||||
itemType: game.i18n.localize(CONFIG.Item.typeLabels[itemData.type]),
|
||||
actorType: game.i18n.localize(CONFIG.Actor.typeLabels[this.actor.type])
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Create a Consumable power scroll on the Inventory tab
|
||||
if ( (itemData.type === "power") && (this._tabs[0].active === "inventory") ) {
|
||||
// TODO: This is pretty non functional as the base items for the scrolls, and the powers, are not defined, maybe consider using holocrons
|
||||
if (itemData.type === "power" && this._tabs[0].active === "inventory") {
|
||||
const scroll = await Item5e.createScrollFromPower(itemData);
|
||||
itemData = scroll.data;
|
||||
}
|
||||
|
||||
if (itemData.data) {
|
||||
// Ignore certain statuses
|
||||
if ( itemData.data ) {
|
||||
["attunement", "equipped", "proficient", "prepared"].forEach(k => delete itemData.data[k]);
|
||||
["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
|
||||
|
@ -635,7 +695,7 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
* @param {MouseEvent} event The originating click event
|
||||
* @private
|
||||
*/
|
||||
async _onPowerSlotOverride (event) {
|
||||
async _onPowerSlotOverride(event) {
|
||||
const span = event.currentTarget.parentElement;
|
||||
const level = span.dataset.level;
|
||||
const override = this.actor.data.data.powers[level].override || span.dataset.slots;
|
||||
|
@ -662,10 +722,10 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
async _onUsesChange(event) {
|
||||
event.preventDefault();
|
||||
const itemId = event.currentTarget.closest(".item").dataset.itemId;
|
||||
const item = this.actor.getOwnedItem(itemId);
|
||||
const item = this.actor.items.get(itemId);
|
||||
const uses = Math.clamped(0, parseInt(event.target.value), item.data.data.uses.max);
|
||||
event.target.value = uses;
|
||||
return item.update({ 'data.uses.value': uses });
|
||||
return item.update({"data.uses.value": uses});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -677,7 +737,7 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
_onItemRoll(event) {
|
||||
event.preventDefault();
|
||||
const itemId = event.currentTarget.closest(".item").dataset.itemId;
|
||||
const item = this.actor.getOwnedItem(itemId);
|
||||
const item = this.actor.items.get(itemId);
|
||||
return item.roll();
|
||||
}
|
||||
|
||||
|
@ -691,9 +751,9 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
_onItemRecharge(event) {
|
||||
event.preventDefault();
|
||||
const itemId = event.currentTarget.closest(".item").dataset.itemId;
|
||||
const item = this.actor.getOwnedItem(itemId);
|
||||
const item = this.actor.items.get(itemId);
|
||||
return item.rollRecharge();
|
||||
};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
|
@ -704,17 +764,17 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
_onItemSummary(event) {
|
||||
event.preventDefault();
|
||||
let li = $(event.currentTarget).parents(".item"),
|
||||
item = this.actor.getOwnedItem(li.data("item-id")),
|
||||
chatData = item.getChatData({secrets: this.actor.owner});
|
||||
item = this.actor.items.get(li.data("item-id")),
|
||||
chatData = item.getChatData({secrets: this.actor.isOwner});
|
||||
|
||||
// Toggle summary
|
||||
if ( li.hasClass("expanded") ) {
|
||||
if (li.hasClass("expanded")) {
|
||||
let summary = li.children(".item-summary");
|
||||
summary.slideUp(200, () => summary.remove());
|
||||
} else {
|
||||
let div = $(`<div class="item-summary">${chatData.description.value}</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);
|
||||
li.append(div.hide());
|
||||
div.slideDown(200);
|
||||
|
@ -734,12 +794,12 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
const header = event.currentTarget;
|
||||
const type = header.dataset.type;
|
||||
const itemData = {
|
||||
name: game.i18n.format("SW5E.ItemNew", {type: type.capitalize()}),
|
||||
name: game.i18n.format("SW5E.ItemNew", {type: game.i18n.localize(`SW5E.ItemType${type.capitalize()}`)}),
|
||||
type: type,
|
||||
data: duplicate(header.dataset)
|
||||
data: foundry.utils.deepClone(header.dataset)
|
||||
};
|
||||
delete itemData.data["type"];
|
||||
return this.actor.createEmbeddedEntity("OwnedItem", itemData);
|
||||
return this.actor.createEmbeddedDocuments("Item", [itemData]);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -752,8 +812,8 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
_onItemEdit(event) {
|
||||
event.preventDefault();
|
||||
const li = event.currentTarget.closest(".item");
|
||||
const item = this.actor.getOwnedItem(li.dataset.itemId);
|
||||
item.sheet.render(true);
|
||||
const item = this.actor.items.get(li.dataset.itemId);
|
||||
return item.sheet.render(true);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -766,7 +826,8 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
_onItemDelete(event) {
|
||||
event.preventDefault();
|
||||
const li = event.currentTarget.closest(".item");
|
||||
this.actor.deleteOwnedItem(li.dataset.itemId);
|
||||
const item = this.actor.items.get(li.dataset.itemId);
|
||||
if (item) return item.delete();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -779,7 +840,7 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
_onRollAbilityTest(event) {
|
||||
event.preventDefault();
|
||||
let ability = event.currentTarget.parentElement.dataset.ability;
|
||||
this.actor.rollAbility(ability, {event: event});
|
||||
return this.actor.rollAbility(ability, {event: event});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -792,7 +853,7 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
_onRollSkillCheck(event) {
|
||||
event.preventDefault();
|
||||
const skill = event.currentTarget.parentElement.dataset.skill;
|
||||
this.actor.rollSkill(skill, {event: event});
|
||||
return this.actor.rollSkill(skill, {event: event});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -805,7 +866,7 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
_onToggleAbilityProficiency(event) {
|
||||
event.preventDefault();
|
||||
const field = event.currentTarget.previousElementSibling;
|
||||
this.actor.update({[field.name]: 1 - parseInt(field.value)});
|
||||
return this.actor.update({[field.name]: 1 - parseInt(field.value)});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -820,9 +881,9 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
const li = event.currentTarget;
|
||||
const set = this._filters[li.parentElement.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);
|
||||
this.render();
|
||||
return this.render();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -837,8 +898,8 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
const a = event.currentTarget;
|
||||
const label = a.parentElement.querySelector("label");
|
||||
const choices = CONFIG.SW5E[a.dataset.options];
|
||||
const options = { name: a.dataset.target, title: label.innerText, choices };
|
||||
new TraitSelector(this.actor, options).render(true)
|
||||
const options = {name: a.dataset.target, title: label.innerText, choices};
|
||||
return new TraitSelector(this.actor, options).render(true);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -846,15 +907,14 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
/** @override */
|
||||
_getHeaderButtons() {
|
||||
let buttons = super._getHeaderButtons();
|
||||
|
||||
// Add button to revert polymorph
|
||||
if ( !this.actor.isPolymorphed || this.actor.isToken ) return buttons;
|
||||
if (this.actor.isPolymorphed) {
|
||||
buttons.unshift({
|
||||
label: 'SW5E.PolymorphRestoreTransformation',
|
||||
label: "SW5E.PolymorphRestoreTransformation",
|
||||
class: "restore-transformation",
|
||||
icon: "fas fa-backward",
|
||||
onclick: ev => this.actor.revertOriginalForm()
|
||||
onclick: () => this.actor.revertOriginalForm()
|
||||
});
|
||||
}
|
||||
return buttons;
|
||||
}
|
||||
}
|
|
@ -7,7 +7,6 @@ import Actor5e from "../../entity.js";
|
|||
* @type {ActorSheet5e}
|
||||
*/
|
||||
export default class ActorSheet5eCharacter extends ActorSheet5e {
|
||||
|
||||
/**
|
||||
* Define default rendering options for the NPC sheet
|
||||
* @return {Object}
|
||||
|
@ -37,7 +36,7 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
|
|||
sheetData["resources"] = ["primary", "secondary", "tertiary"].reduce((arr, r) => {
|
||||
const res = sheetData.data.resources[r] || {};
|
||||
res.name = r;
|
||||
res.placeholder = game.i18n.localize("SW5E.Resource"+r.titleCase());
|
||||
res.placeholder = game.i18n.localize("SW5E.Resource" + r.titleCase());
|
||||
if (res && res.value === 0) delete res.value;
|
||||
if (res && res.max === 0) delete res.max;
|
||||
return arr.concat([res]);
|
||||
|
@ -45,10 +44,12 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
|
|||
|
||||
// Experience Tracking
|
||||
sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking");
|
||||
sheetData["classLabels"] = this.actor.itemTypes.class.map(c => c.name).join(", ");
|
||||
sheetData["multiclassLabels"] = this.actor.itemTypes.class.map(c => {
|
||||
return [c.data.data.archetype, c.name, c.data.data.levels].filterJoin(' ')
|
||||
}).join(', ');
|
||||
sheetData["classLabels"] = this.actor.itemTypes.class.map((c) => c.name).join(", ");
|
||||
sheetData["multiclassLabels"] = this.actor.itemTypes.class
|
||||
.map((c) => {
|
||||
return [c.data.data.archetype, c.name, c.data.data.levels].filterJoin(" ");
|
||||
})
|
||||
.join(", ");
|
||||
|
||||
// Return data for rendering
|
||||
return sheetData;
|
||||
|
@ -61,23 +62,34 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
|
|||
* @private
|
||||
*/
|
||||
_prepareItems(data) {
|
||||
|
||||
// Categorize items as inventory, powerbook, features, and classes
|
||||
const inventory = {
|
||||
weapon: { label: "SW5E.ItemTypeWeaponPl", items: [], dataset: {type: "weapon"} },
|
||||
equipment: { label: "SW5E.ItemTypeEquipmentPl", items: [], dataset: {type: "equipment"} },
|
||||
consumable: { label: "SW5E.ItemTypeConsumablePl", items: [], dataset: {type: "consumable"} },
|
||||
tool: { label: "SW5E.ItemTypeToolPl", items: [], dataset: {type: "tool"} },
|
||||
backpack: { label: "SW5E.ItemTypeContainerPl", items: [], dataset: {type: "backpack"} },
|
||||
loot: { label: "SW5E.ItemTypeLootPl", items: [], dataset: {type: "loot"} }
|
||||
weapon: {label: "SW5E.ItemTypeWeaponPl", items: [], dataset: {type: "weapon"}},
|
||||
equipment: {label: "SW5E.ItemTypeEquipmentPl", items: [], dataset: {type: "equipment"}},
|
||||
consumable: {label: "SW5E.ItemTypeConsumablePl", items: [], dataset: {type: "consumable"}},
|
||||
tool: {label: "SW5E.ItemTypeToolPl", items: [], dataset: {type: "tool"}},
|
||||
backpack: {label: "SW5E.ItemTypeContainerPl", items: [], dataset: {type: "backpack"}},
|
||||
loot: {label: "SW5E.ItemTypeLootPl", items: [], dataset: {type: "loot"}}
|
||||
};
|
||||
|
||||
// Partition items by category
|
||||
let [items, powers, feats, classes, species, archetypes, classfeatures, backgrounds, fightingstyles, fightingmasteries, lightsaberforms] = data.items.reduce((arr, item) => {
|
||||
|
||||
let [
|
||||
items,
|
||||
powers,
|
||||
feats,
|
||||
classes,
|
||||
species,
|
||||
archetypes,
|
||||
classfeatures,
|
||||
backgrounds,
|
||||
fightingstyles,
|
||||
fightingmasteries,
|
||||
lightsaberforms
|
||||
] = data.items.reduce(
|
||||
(arr, item) => {
|
||||
// Item details
|
||||
item.img = item.img || DEFAULT_TOKEN;
|
||||
item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
|
||||
item.img = item.img || CONST.DEFAULT_TOKEN;
|
||||
item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1;
|
||||
item.attunement = {
|
||||
[CONFIG.SW5E.attunementTypes.REQUIRED]: {
|
||||
icon: "fa-sun",
|
||||
|
@ -92,28 +104,35 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
|
|||
}[item.data.attunement];
|
||||
|
||||
// Item usage
|
||||
item.hasUses = item.data.uses && (item.data.uses.max > 0);
|
||||
item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false);
|
||||
item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0));
|
||||
item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type));
|
||||
item.hasUses = item.data.uses && item.data.uses.max > 0;
|
||||
item.isOnCooldown =
|
||||
item.data.recharge && !!item.data.recharge.value && item.data.recharge.charged === false;
|
||||
item.isDepleted = item.isOnCooldown && item.data.uses.per && item.data.uses.value > 0;
|
||||
item.hasTarget = !!item.data.target && !["none", ""].includes(item.data.target.type);
|
||||
|
||||
// Item toggle state
|
||||
this._prepareItemToggleState(item);
|
||||
|
||||
// Primary Class
|
||||
if (item.type === "class")
|
||||
item.isOriginalClass = item._id === this.actor.data.data.details.originalClass;
|
||||
|
||||
// Classify items into types
|
||||
if ( item.type === "power" ) arr[1].push(item);
|
||||
else if ( item.type === "feat" ) arr[2].push(item);
|
||||
else if ( item.type === "class" ) arr[3].push(item);
|
||||
else if ( item.type === "species" ) arr[4].push(item);
|
||||
else if ( item.type === "archetype" ) arr[5].push(item);
|
||||
else if ( item.type === "classfeature" ) arr[6].push(item);
|
||||
else if ( item.type === "background" ) arr[7].push(item);
|
||||
else if ( item.type === "fightingstyle" ) arr[8].push(item);
|
||||
else if ( item.type === "fightingmastery" ) arr[9].push(item);
|
||||
else if ( item.type === "lightsaberform" ) arr[10].push(item);
|
||||
else if ( Object.keys(inventory).includes(item.type ) ) arr[0].push(item);
|
||||
if (item.type === "power") arr[1].push(item);
|
||||
else if (item.type === "feat") arr[2].push(item);
|
||||
else if (item.type === "class") arr[3].push(item);
|
||||
else if (item.type === "species") arr[4].push(item);
|
||||
else if (item.type === "archetype") arr[5].push(item);
|
||||
else if (item.type === "classfeature") arr[6].push(item);
|
||||
else if (item.type === "background") arr[7].push(item);
|
||||
else if (item.type === "fightingstyle") arr[8].push(item);
|
||||
else if (item.type === "fightingmastery") arr[9].push(item);
|
||||
else if (item.type === "lightsaberform") arr[10].push(item);
|
||||
else if (Object.keys(inventory).includes(item.type)) arr[0].push(item);
|
||||
return arr;
|
||||
}, [[], [], [], [], [], [], [], [], [], [], []]);
|
||||
},
|
||||
[[], [], [], [], [], [], [], [], [], [], []]
|
||||
);
|
||||
|
||||
// Apply active item filters
|
||||
items = this._filterItems(items, this._filters.inventory);
|
||||
|
@ -121,7 +140,7 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
|
|||
feats = this._filterItems(feats, this._filters.features);
|
||||
|
||||
// Organize items
|
||||
for ( let i of items ) {
|
||||
for (let i of items) {
|
||||
i.data.quantity = i.data.quantity || 0;
|
||||
i.data.weight = i.data.weight || 0;
|
||||
i.totalWeight = (i.data.quantity * i.data.weight).toNearest(0.1);
|
||||
|
@ -130,28 +149,81 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
|
|||
|
||||
// Organize Powerbook and count the number of prepared powers (excluding always, at will, etc...)
|
||||
const powerbook = this._preparePowerbook(data, powers);
|
||||
const nPrepared = powers.filter(s => {
|
||||
return (s.data.level > 0) && (s.data.preparation.mode === "prepared") && s.data.preparation.prepared;
|
||||
const nPrepared = powers.filter((s) => {
|
||||
return s.data.level > 0 && s.data.preparation.mode === "prepared" && s.data.preparation.prepared;
|
||||
}).length;
|
||||
|
||||
// Organize Features
|
||||
const features = {
|
||||
classes: { label: "SW5E.ItemTypeClassPl", items: [], hasActions: false, dataset: {type: "class"}, isClass: true },
|
||||
classfeatures: { label: "SW5E.ItemTypeClassFeats", items: [], hasActions: true, dataset: {type: "classfeature"}, isClassfeature: true },
|
||||
archetype: { label: "SW5E.ItemTypeArchetype", items: [], hasActions: false, dataset: {type: "archetype"}, isArchetype: true },
|
||||
species: { label: "SW5E.ItemTypeSpecies", items: [], hasActions: false, dataset: {type: "species"}, isSpecies: true },
|
||||
background: { label: "SW5E.ItemTypeBackground", items: [], hasActions: false, dataset: {type: "background"}, isBackground: true },
|
||||
fightingstyles: { label: "SW5E.ItemTypeFightingStylePl", items: [], hasActions: false, dataset: {type: "fightingstyle"}, isFightingstyle: true },
|
||||
fightingmasteries: { label: "SW5E.ItemTypeFightingMasteryPl", items: [], hasActions: false, dataset: {type: "fightingmastery"}, isFightingmastery: true },
|
||||
lightsaberforms: { label: "SW5E.ItemTypeLightsaberFormPl", items: [], hasActions: false, dataset: {type: "lightsaberform"}, isLightsaberform: true },
|
||||
active: { label: "SW5E.FeatureActive", items: [], hasActions: true, dataset: {type: "feat", "activation.type": "action"} },
|
||||
passive: { label: "SW5E.FeaturePassive", items: [], hasActions: false, dataset: {type: "feat"} }
|
||||
classes: {
|
||||
label: "SW5E.ItemTypeClassPl",
|
||||
items: [],
|
||||
hasActions: false,
|
||||
dataset: {type: "class"},
|
||||
isClass: true
|
||||
},
|
||||
classfeatures: {
|
||||
label: "SW5E.ItemTypeClassFeats",
|
||||
items: [],
|
||||
hasActions: true,
|
||||
dataset: {type: "classfeature"},
|
||||
isClassfeature: true
|
||||
},
|
||||
archetype: {
|
||||
label: "SW5E.ItemTypeArchetype",
|
||||
items: [],
|
||||
hasActions: false,
|
||||
dataset: {type: "archetype"},
|
||||
isArchetype: true
|
||||
},
|
||||
species: {
|
||||
label: "SW5E.ItemTypeSpecies",
|
||||
items: [],
|
||||
hasActions: false,
|
||||
dataset: {type: "species"},
|
||||
isSpecies: true
|
||||
},
|
||||
background: {
|
||||
label: "SW5E.ItemTypeBackground",
|
||||
items: [],
|
||||
hasActions: false,
|
||||
dataset: {type: "background"},
|
||||
isBackground: true
|
||||
},
|
||||
fightingstyles: {
|
||||
label: "SW5E.ItemTypeFightingStylePl",
|
||||
items: [],
|
||||
hasActions: false,
|
||||
dataset: {type: "fightingstyle"},
|
||||
isFightingstyle: true
|
||||
},
|
||||
fightingmasteries: {
|
||||
label: "SW5E.ItemTypeFightingMasteryPl",
|
||||
items: [],
|
||||
hasActions: false,
|
||||
dataset: {type: "fightingmastery"},
|
||||
isFightingmastery: true
|
||||
},
|
||||
lightsaberforms: {
|
||||
label: "SW5E.ItemTypeLightsaberFormPl",
|
||||
items: [],
|
||||
hasActions: false,
|
||||
dataset: {type: "lightsaberform"},
|
||||
isLightsaberform: true
|
||||
},
|
||||
active: {
|
||||
label: "SW5E.FeatureActive",
|
||||
items: [],
|
||||
hasActions: true,
|
||||
dataset: {"type": "feat", "activation.type": "action"}
|
||||
},
|
||||
passive: {label: "SW5E.FeaturePassive", items: [], hasActions: false, dataset: {type: "feat"}}
|
||||
};
|
||||
for ( let f of feats ) {
|
||||
if ( f.data.activation.type ) features.active.items.push(f);
|
||||
for (let f of feats) {
|
||||
if (f.data.activation.type) features.active.items.push(f);
|
||||
else features.passive.items.push(f);
|
||||
}
|
||||
classes.sort((a, b) => b.levels - a.levels);
|
||||
classes.sort((a, b) => b.data.levels - a.data.levels);
|
||||
features.classes.items = classes;
|
||||
features.classfeatures.items = classfeatures;
|
||||
features.archetype.items = archetypes;
|
||||
|
@ -180,12 +252,11 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
|
|||
const isAlways = getProperty(item.data, "preparation.mode") === "always";
|
||||
const isPrepared = getProperty(item.data, "preparation.prepared");
|
||||
item.toggleClass = isPrepared ? "active" : "";
|
||||
if ( isAlways ) item.toggleClass = "fixed";
|
||||
if ( isAlways ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.always;
|
||||
else if ( isPrepared ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.prepared;
|
||||
if (isAlways) item.toggleClass = "fixed";
|
||||
if (isAlways) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.always;
|
||||
else if (isPrepared) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.prepared;
|
||||
else item.toggleTitle = game.i18n.localize("SW5E.PowerUnprepared");
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
const isActive = getProperty(item.data, "equipped");
|
||||
item.toggleClass = isActive ? "active" : "";
|
||||
item.toggleTitle = game.i18n.localize(isActive ? "SW5E.Equipped" : "SW5E.Unequipped");
|
||||
|
@ -198,18 +269,18 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
|
|||
|
||||
/**
|
||||
* Activate event listeners using the prepared sheet HTML
|
||||
* @param html {HTML} The prepared HTML object ready to be rendered into the DOM
|
||||
* @param html {jQuery} The prepared HTML object ready to be rendered into the DOM
|
||||
*/
|
||||
activateListeners(html) {
|
||||
super.activateListeners(html);
|
||||
if ( !this.options.editable ) return;
|
||||
if (!this.isEditable) return;
|
||||
|
||||
// Item State Toggling
|
||||
html.find('.item-toggle').click(this._onToggleItem.bind(this));
|
||||
html.find(".item-toggle").click(this._onToggleItem.bind(this));
|
||||
|
||||
// Short and Long Rest
|
||||
html.find('.short-rest').click(this._onShortRest.bind(this));
|
||||
html.find('.long-rest').click(this._onLongRest.bind(this));
|
||||
html.find(".short-rest").click(this._onShortRest.bind(this));
|
||||
html.find(".long-rest").click(this._onLongRest.bind(this));
|
||||
|
||||
// Rollable sheet actions
|
||||
html.find(".rollable[data-action]").click(this._onSheetAction.bind(this));
|
||||
|
@ -225,7 +296,7 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
|
|||
_onSheetAction(event) {
|
||||
event.preventDefault();
|
||||
const button = event.currentTarget;
|
||||
switch( button.dataset.action ) {
|
||||
switch (button.dataset.action) {
|
||||
case "rollDeathSave":
|
||||
return this.actor.rollDeathSave({event: event});
|
||||
case "rollInitiative":
|
||||
|
@ -243,7 +314,7 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
|
|||
_onToggleItem(event) {
|
||||
event.preventDefault();
|
||||
const itemId = event.currentTarget.closest(".item").dataset.itemId;
|
||||
const item = this.actor.getOwnedItem(itemId);
|
||||
const item = this.actor.items.get(itemId);
|
||||
const attr = item.data.type === "power" ? "data.preparation.prepared" : "data.equipped";
|
||||
return item.update({[attr]: !getProperty(item.data, attr)});
|
||||
}
|
||||
|
@ -278,14 +349,13 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
|
|||
|
||||
/** @override */
|
||||
async _onDropItemCreate(itemData) {
|
||||
|
||||
// Increment the number of class levels a character instead of creating a new item
|
||||
if ( itemData.type === "class" ) {
|
||||
const cls = this.actor.itemTypes.class.find(c => c.name === itemData.name);
|
||||
if (itemData.type === "class") {
|
||||
const cls = this.actor.itemTypes.class.find((c) => c.name === itemData.name);
|
||||
let priorLevel = cls?.data.data.levels ?? 0;
|
||||
if ( !!cls ) {
|
||||
if (!!cls) {
|
||||
const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level);
|
||||
if ( next > priorLevel ) {
|
||||
if (next > priorLevel) {
|
||||
itemData.levels = next;
|
||||
return cls.update({"data.levels": next});
|
||||
}
|
||||
|
@ -293,6 +363,6 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
|
|||
}
|
||||
|
||||
// Default drop handling if levels were not added
|
||||
super._onDropItemCreate(itemData);
|
||||
return super._onDropItemCreate(itemData);
|
||||
}
|
||||
}
|
|
@ -6,7 +6,6 @@ import ActorSheet5e from "./base.js";
|
|||
* @extends {ActorSheet5e}
|
||||
*/
|
||||
export default class ActorSheet5eNPC extends ActorSheet5e {
|
||||
|
||||
/** @override */
|
||||
static get defaultOptions() {
|
||||
return mergeObject(super.defaultOptions, {
|
||||
|
@ -18,32 +17,50 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
static unsupportedItemTypes = new Set(["class"]);
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Organize Owned Items for rendering the NPC sheet
|
||||
* @private
|
||||
*/
|
||||
_prepareItems(data) {
|
||||
|
||||
// Categorize Items as Features and Powers
|
||||
const features = {
|
||||
weapons: { label: game.i18n.localize("SW5E.AttackPl"), items: [] , hasActions: true, dataset: {type: "weapon", "weapon-type": "natural"} },
|
||||
actions: { label: game.i18n.localize("SW5E.ActionPl"), items: [] , hasActions: true, dataset: {type: "feat", "activation.type": "action"} },
|
||||
passive: { label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"} },
|
||||
equipment: { label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}}
|
||||
weapons: {
|
||||
label: game.i18n.localize("SW5E.AttackPl"),
|
||||
items: [],
|
||||
hasActions: true,
|
||||
dataset: {"type": "weapon", "weapon-type": "natural"}
|
||||
},
|
||||
actions: {
|
||||
label: game.i18n.localize("SW5E.ActionPl"),
|
||||
items: [],
|
||||
hasActions: true,
|
||||
dataset: {"type": "feat", "activation.type": "action"}
|
||||
},
|
||||
passive: {label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"}},
|
||||
equipment: {label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}}
|
||||
};
|
||||
|
||||
// Start by classifying items into groups for rendering
|
||||
let [powers, other] = data.items.reduce((arr, item) => {
|
||||
item.img = item.img || DEFAULT_TOKEN;
|
||||
item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
|
||||
item.hasUses = item.data.uses && (item.data.uses.max > 0);
|
||||
item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false);
|
||||
item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0));
|
||||
item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type));
|
||||
if ( item.type === "power" ) arr[0].push(item);
|
||||
let [powers, other] = data.items.reduce(
|
||||
(arr, item) => {
|
||||
item.img = item.img || CONST.DEFAULT_TOKEN;
|
||||
item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1;
|
||||
item.hasUses = item.data.uses && item.data.uses.max > 0;
|
||||
item.isOnCooldown =
|
||||
item.data.recharge && !!item.data.recharge.value && item.data.recharge.charged === false;
|
||||
item.isDepleted = item.isOnCooldown && item.data.uses.per && item.data.uses.value > 0;
|
||||
item.hasTarget = !!item.data.target && !["none", ""].includes(item.data.target.type);
|
||||
if (item.type === "power") arr[0].push(item);
|
||||
else arr[1].push(item);
|
||||
return arr;
|
||||
}, [[], []]);
|
||||
},
|
||||
[[], []]
|
||||
);
|
||||
|
||||
// Apply item filters
|
||||
powers = this._filterItems(powers, this._filters.powerbook);
|
||||
|
@ -53,13 +70,12 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
|
|||
const powerbook = this._preparePowerbook(data, powers);
|
||||
|
||||
// Organize Features
|
||||
for ( let item of other ) {
|
||||
if ( item.type === "weapon" ) features.weapons.items.push(item);
|
||||
else if ( item.type === "feat" ) {
|
||||
if ( item.data.activation.type ) features.actions.items.push(item);
|
||||
for (let item of other) {
|
||||
if (item.type === "weapon") features.weapons.items.push(item);
|
||||
else if (item.type === "feat") {
|
||||
if (item.data.activation.type) features.actions.items.push(item);
|
||||
else features.passive.items.push(item);
|
||||
}
|
||||
else features.equipment.items.push(item);
|
||||
} else features.equipment.items.push(item);
|
||||
}
|
||||
|
||||
// Assign and return
|
||||
|
@ -67,17 +83,19 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
|
|||
data.powerbook = powerbook;
|
||||
}
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
getData() {
|
||||
const data = super.getData();
|
||||
/** @inheritdoc */
|
||||
getData(options) {
|
||||
const data = super.getData(options);
|
||||
|
||||
// Challenge Rating
|
||||
const cr = parseFloat(data.data.details.cr || 0);
|
||||
const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"};
|
||||
data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1;
|
||||
|
||||
// Creature Type
|
||||
data.labels["type"] = this.actor.labels.creatureType;
|
||||
return data;
|
||||
}
|
||||
|
||||
|
@ -86,17 +104,16 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
|
|||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_updateObject(event, formData) {
|
||||
|
||||
async _updateObject(event, formData) {
|
||||
// Format NPC Challenge Rating
|
||||
const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5};
|
||||
let crv = "data.details.cr";
|
||||
let cr = formData[crv];
|
||||
cr = crs[cr] || parseFloat(cr);
|
||||
if ( cr ) formData[crv] = cr < 1 ? cr : parseInt(cr);
|
||||
if (cr) formData[crv] = cr < 1 ? cr : parseInt(cr);
|
||||
|
||||
// Parent ActorSheet update steps
|
||||
super._updateObject(event, formData);
|
||||
return super._updateObject(event, formData);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -119,7 +136,7 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
|
|||
_onRollHPFormula(event) {
|
||||
event.preventDefault();
|
||||
const formula = this.actor.data.data.attributes.hp.formula;
|
||||
if ( !formula ) return;
|
||||
if (!formula) return;
|
||||
const hp = new Roll(formula).roll().total;
|
||||
AudioHelper.play({src: CONFIG.sounds.dice});
|
||||
this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp});
|
||||
|
|
|
@ -20,12 +20,17 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
static unsupportedItemTypes = new Set(["class"]);
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Creates a new cargo entry for a vehicle Actor.
|
||||
*/
|
||||
static get newCargo() {
|
||||
return {
|
||||
name: '',
|
||||
name: "",
|
||||
quantity: 1
|
||||
};
|
||||
}
|
||||
|
@ -40,7 +45,6 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
|
|||
* @private
|
||||
*/
|
||||
_computeEncumbrance(totalWeight, actorData) {
|
||||
|
||||
// Compute currency weight
|
||||
const totalCoins = Object.values(actorData.data.currency).reduce((acc, denom) => acc + denom, 0);
|
||||
totalWeight += totalCoins / CONFIG.SW5E.encumbrance.currencyPerWeight;
|
||||
|
@ -57,7 +61,7 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
|
|||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_getMovementSpeed(actorData, largestPrimary=true) {
|
||||
_getMovementSpeed(actorData, largestPrimary = true) {
|
||||
return super._getMovementSpeed(actorData, largestPrimary);
|
||||
}
|
||||
|
||||
|
@ -69,25 +73,24 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
|
|||
* @private
|
||||
*/
|
||||
_prepareCrewedItem(item) {
|
||||
|
||||
// Determine crewed status
|
||||
const isCrewed = item.data.crewed;
|
||||
item.toggleClass = isCrewed ? 'active' : '';
|
||||
item.toggleTitle = game.i18n.localize(`SW5E.${isCrewed ? 'Crewed' : 'Uncrewed'}`);
|
||||
item.toggleClass = isCrewed ? "active" : "";
|
||||
item.toggleTitle = game.i18n.localize(`SW5E.${isCrewed ? "Crewed" : "Uncrewed"}`);
|
||||
|
||||
// Handle crew actions
|
||||
if (item.type === 'feat' && item.data.activation.type === 'crew') {
|
||||
if (item.type === "feat" && item.data.activation.type === "crew") {
|
||||
item.crew = item.data.activation.cost;
|
||||
item.cover = game.i18n.localize(`SW5E.${item.data.cover ? 'CoverTotal' : 'None'}`);
|
||||
if (item.data.cover === .5) item.cover = '½';
|
||||
else if (item.data.cover === .75) item.cover = '¾';
|
||||
else if (item.data.cover === null) item.cover = '—';
|
||||
if (item.crew < 1 || item.crew === null) item.crew = '—';
|
||||
item.cover = game.i18n.localize(`SW5E.${item.data.cover ? "CoverTotal" : "None"}`);
|
||||
if (item.data.cover === 0.5) item.cover = "½";
|
||||
else if (item.data.cover === 0.75) item.cover = "¾";
|
||||
else if (item.data.cover === null) item.cover = "—";
|
||||
if (item.crew < 1 || item.crew === null) item.crew = "—";
|
||||
}
|
||||
|
||||
// Prepare vehicle weapons
|
||||
if (item.type === 'equipment' || item.type === 'weapon') {
|
||||
item.threshold = item.data.hp.dt ? item.data.hp.dt : '—';
|
||||
if (item.type === "equipment" || item.type === "weapon") {
|
||||
item.threshold = item.data.hp.dt ? item.data.hp.dt : "—";
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -98,132 +101,162 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
|
|||
* @private
|
||||
*/
|
||||
_prepareItems(data) {
|
||||
const cargoColumns = [{
|
||||
label: game.i18n.localize('SW5E.Quantity'),
|
||||
css: 'item-qty',
|
||||
property: 'quantity',
|
||||
editable: 'Number'
|
||||
}];
|
||||
const cargoColumns = [
|
||||
{
|
||||
label: game.i18n.localize("SW5E.Quantity"),
|
||||
css: "item-qty",
|
||||
property: "quantity",
|
||||
editable: "Number"
|
||||
}
|
||||
];
|
||||
|
||||
const equipmentColumns = [{
|
||||
label: game.i18n.localize('SW5E.Quantity'),
|
||||
css: 'item-qty',
|
||||
property: 'data.quantity'
|
||||
}, {
|
||||
label: game.i18n.localize('SW5E.AC'),
|
||||
css: 'item-ac',
|
||||
property: 'data.armor.value'
|
||||
}, {
|
||||
label: game.i18n.localize('SW5E.HP'),
|
||||
css: 'item-hp',
|
||||
property: 'data.hp.value',
|
||||
editable: 'Number'
|
||||
}, {
|
||||
label: game.i18n.localize('SW5E.Threshold'),
|
||||
css: 'item-threshold',
|
||||
property: 'threshold'
|
||||
}];
|
||||
const equipmentColumns = [
|
||||
{
|
||||
label: game.i18n.localize("SW5E.Quantity"),
|
||||
css: "item-qty",
|
||||
property: "data.quantity"
|
||||
},
|
||||
{
|
||||
label: game.i18n.localize("SW5E.AC"),
|
||||
css: "item-ac",
|
||||
property: "data.armor.value"
|
||||
},
|
||||
{
|
||||
label: game.i18n.localize("SW5E.HP"),
|
||||
css: "item-hp",
|
||||
property: "data.hp.value",
|
||||
editable: "Number"
|
||||
},
|
||||
{
|
||||
label: game.i18n.localize("SW5E.Threshold"),
|
||||
css: "item-threshold",
|
||||
property: "threshold"
|
||||
}
|
||||
];
|
||||
|
||||
const features = {
|
||||
actions: {
|
||||
label: game.i18n.localize('SW5E.ActionPl'),
|
||||
label: game.i18n.localize("SW5E.ActionPl"),
|
||||
items: [],
|
||||
crewable: true,
|
||||
dataset: {type: 'feat', 'activation.type': 'crew'},
|
||||
columns: [{
|
||||
label: game.i18n.localize('SW5E.VehicleCrew'),
|
||||
css: 'item-crew',
|
||||
property: 'crew'
|
||||
}, {
|
||||
label: game.i18n.localize('SW5E.Cover'),
|
||||
css: 'item-cover',
|
||||
property: 'cover'
|
||||
}]
|
||||
dataset: {"type": "feat", "activation.type": "crew"},
|
||||
columns: [
|
||||
{
|
||||
label: game.i18n.localize("SW5E.VehicleCrew"),
|
||||
css: "item-crew",
|
||||
property: "crew"
|
||||
},
|
||||
{
|
||||
label: game.i18n.localize("SW5E.Cover"),
|
||||
css: "item-cover",
|
||||
property: "cover"
|
||||
}
|
||||
]
|
||||
},
|
||||
equipment: {
|
||||
label: game.i18n.localize('SW5E.ItemTypeEquipment'),
|
||||
label: game.i18n.localize("SW5E.ItemTypeEquipment"),
|
||||
items: [],
|
||||
crewable: true,
|
||||
dataset: {type: 'equipment', 'armor.type': 'vehicle'},
|
||||
dataset: {"type": "equipment", "armor.type": "vehicle"},
|
||||
columns: equipmentColumns
|
||||
},
|
||||
passive: {
|
||||
label: game.i18n.localize('SW5E.Features'),
|
||||
label: game.i18n.localize("SW5E.Features"),
|
||||
items: [],
|
||||
dataset: {type: 'feat'}
|
||||
dataset: {type: "feat"}
|
||||
},
|
||||
reactions: {
|
||||
label: game.i18n.localize('SW5E.ReactionPl'),
|
||||
label: game.i18n.localize("SW5E.ReactionPl"),
|
||||
items: [],
|
||||
dataset: {type: 'feat', 'activation.type': 'reaction'}
|
||||
dataset: {"type": "feat", "activation.type": "reaction"}
|
||||
},
|
||||
weapons: {
|
||||
label: game.i18n.localize('SW5E.ItemTypeWeaponPl'),
|
||||
label: game.i18n.localize("SW5E.ItemTypeWeaponPl"),
|
||||
items: [],
|
||||
crewable: true,
|
||||
dataset: {type: 'weapon', 'weapon-type': 'siege'},
|
||||
dataset: {"type": "weapon", "weapon-type": "siege"},
|
||||
columns: equipmentColumns
|
||||
}
|
||||
};
|
||||
|
||||
const cargo = {
|
||||
crew: {
|
||||
label: game.i18n.localize('SW5E.VehicleCrew'),
|
||||
label: game.i18n.localize("SW5E.VehicleCrew"),
|
||||
items: data.data.cargo.crew,
|
||||
css: 'cargo-row crew',
|
||||
css: "cargo-row crew",
|
||||
editableName: true,
|
||||
dataset: {type: 'crew'},
|
||||
dataset: {type: "crew"},
|
||||
columns: cargoColumns
|
||||
},
|
||||
passengers: {
|
||||
label: game.i18n.localize('SW5E.VehiclePassengers'),
|
||||
label: game.i18n.localize("SW5E.VehiclePassengers"),
|
||||
items: data.data.cargo.passengers,
|
||||
css: 'cargo-row passengers',
|
||||
css: "cargo-row passengers",
|
||||
editableName: true,
|
||||
dataset: {type: 'passengers'},
|
||||
dataset: {type: "passengers"},
|
||||
columns: cargoColumns
|
||||
},
|
||||
cargo: {
|
||||
label: game.i18n.localize('SW5E.VehicleCargo'),
|
||||
label: game.i18n.localize("SW5E.VehicleCargo"),
|
||||
items: [],
|
||||
dataset: {type: 'loot'},
|
||||
columns: [{
|
||||
label: game.i18n.localize('SW5E.Quantity'),
|
||||
css: 'item-qty',
|
||||
property: 'data.quantity',
|
||||
editable: 'Number'
|
||||
}, {
|
||||
label: game.i18n.localize('SW5E.Price'),
|
||||
css: 'item-price',
|
||||
property: 'data.price',
|
||||
editable: 'Number'
|
||||
}, {
|
||||
label: game.i18n.localize('SW5E.Weight'),
|
||||
css: 'item-weight',
|
||||
property: 'data.weight',
|
||||
editable: 'Number'
|
||||
}]
|
||||
dataset: {type: "loot"},
|
||||
columns: [
|
||||
{
|
||||
label: game.i18n.localize("SW5E.Quantity"),
|
||||
css: "item-qty",
|
||||
property: "data.quantity",
|
||||
editable: "Number"
|
||||
},
|
||||
{
|
||||
label: game.i18n.localize("SW5E.Price"),
|
||||
css: "item-price",
|
||||
property: "data.price",
|
||||
editable: "Number"
|
||||
},
|
||||
{
|
||||
label: game.i18n.localize("SW5E.Weight"),
|
||||
css: "item-weight",
|
||||
property: "data.weight",
|
||||
editable: "Number"
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
// Classify items owned by the vehicle and compute total cargo weight
|
||||
let totalWeight = 0;
|
||||
for (const item of data.items) {
|
||||
this._prepareCrewedItem(item);
|
||||
if (item.type === 'weapon') features.weapons.items.push(item);
|
||||
else if (item.type === 'equipment') features.equipment.items.push(item);
|
||||
else if (item.type === 'loot') {
|
||||
|
||||
// Handle cargo explicitly
|
||||
const isCargo = item.flags.sw5e?.vehicleCargo === true;
|
||||
if (isCargo) {
|
||||
totalWeight += (item.data.weight || 0) * item.data.quantity;
|
||||
cargo.cargo.items.push(item);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle non-cargo item types
|
||||
switch (item.type) {
|
||||
case "weapon":
|
||||
features.weapons.items.push(item);
|
||||
break;
|
||||
case "equipment":
|
||||
features.equipment.items.push(item);
|
||||
break;
|
||||
case "feat":
|
||||
if (!item.data.activation.type || item.data.activation.type === "none")
|
||||
features.passive.items.push(item);
|
||||
else if (item.data.activation.type === "reaction") features.reactions.items.push(item);
|
||||
else features.actions.items.push(item);
|
||||
break;
|
||||
default:
|
||||
totalWeight += (item.data.weight || 0) * item.data.quantity;
|
||||
cargo.cargo.items.push(item);
|
||||
}
|
||||
else if (item.type === 'feat') {
|
||||
if (!item.data.activation.type || item.data.activation.type === 'none') {
|
||||
features.passive.items.push(item);
|
||||
}
|
||||
else if (item.data.activation.type === 'reaction') features.reactions.items.push(item);
|
||||
else features.actions.items.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the rendering context data
|
||||
data.features = Object.values(features);
|
||||
data.cargo = Object.values(cargo);
|
||||
data.data.attributes.encumbrance = this._computeEncumbrance(totalWeight, data);
|
||||
|
@ -236,23 +269,23 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
|
|||
/** @override */
|
||||
activateListeners(html) {
|
||||
super.activateListeners(html);
|
||||
if (!this.options.editable) return;
|
||||
if (!this.isEditable) return;
|
||||
|
||||
html.find('.item-toggle').click(this._onToggleItem.bind(this));
|
||||
html.find('.item-hp input')
|
||||
.click(evt => evt.target.select())
|
||||
html.find(".item-toggle").click(this._onToggleItem.bind(this));
|
||||
html.find(".item-hp input")
|
||||
.click((evt) => evt.target.select())
|
||||
.change(this._onHPChange.bind(this));
|
||||
|
||||
html.find('.item:not(.cargo-row) input[data-property]')
|
||||
.click(evt => evt.target.select())
|
||||
html.find(".item:not(.cargo-row) input[data-property]")
|
||||
.click((evt) => evt.target.select())
|
||||
.change(this._onEditInSheet.bind(this));
|
||||
|
||||
html.find('.cargo-row input')
|
||||
.click(evt => evt.target.select())
|
||||
html.find(".cargo-row input")
|
||||
.click((evt) => evt.target.select())
|
||||
.change(this._onCargoRowChange.bind(this));
|
||||
|
||||
if (this.actor.data.data.attributes.actions.stations) {
|
||||
html.find('.counter.actions, .counter.action-thresholds').hide();
|
||||
html.find(".counter.actions, .counter.action-thresholds").hide();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -267,20 +300,20 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
|
|||
_onCargoRowChange(event) {
|
||||
event.preventDefault();
|
||||
const target = event.currentTarget;
|
||||
const row = target.closest('.item');
|
||||
const row = target.closest(".item");
|
||||
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
|
||||
const cargo = duplicate(this.actor.data.data.cargo[property]);
|
||||
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[property]);
|
||||
const entry = cargo[idx];
|
||||
if (!entry) return null;
|
||||
|
||||
// Update the cargo value
|
||||
const key = target.dataset.property || 'name';
|
||||
const key = target.dataset.property || "name";
|
||||
const type = target.dataset.dtype;
|
||||
let value = target.value;
|
||||
if (type === 'Number') value = Number(value);
|
||||
if (type === "Number") value = Number(value);
|
||||
entry[key] = value;
|
||||
|
||||
// Perform the Actor update
|
||||
|
@ -297,14 +330,18 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
|
|||
*/
|
||||
_onEditInSheet(event) {
|
||||
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 property = event.currentTarget.dataset.property;
|
||||
const type = event.currentTarget.dataset.dtype;
|
||||
let value = event.currentTarget.value;
|
||||
switch (type) {
|
||||
case 'Number': value = parseInt(value); break;
|
||||
case 'Boolean': value = value === 'true'; break;
|
||||
case "Number":
|
||||
value = parseInt(value);
|
||||
break;
|
||||
case "Boolean":
|
||||
value = value === "true";
|
||||
break;
|
||||
}
|
||||
return item.update({[`${property}`]: value});
|
||||
}
|
||||
|
@ -321,8 +358,8 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
|
|||
event.preventDefault();
|
||||
const target = event.currentTarget;
|
||||
const type = target.dataset.type;
|
||||
if (type === 'crew' || type === 'passengers') {
|
||||
const cargo = duplicate(this.actor.data.data.cargo[type]);
|
||||
if (type === "crew" || type === "passengers") {
|
||||
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]);
|
||||
cargo.push(this.constructor.newCargo);
|
||||
return this.actor.update({[`data.cargo.${type}`]: cargo});
|
||||
}
|
||||
|
@ -339,11 +376,11 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
|
|||
*/
|
||||
_onItemDelete(event) {
|
||||
event.preventDefault();
|
||||
const row = event.currentTarget.closest('.item');
|
||||
if (row.classList.contains('cargo-row')) {
|
||||
const row = event.currentTarget.closest(".item");
|
||||
if (row.classList.contains("cargo-row")) {
|
||||
const idx = Number(row.dataset.itemId);
|
||||
const type = row.classList.contains('crew') ? 'crew' : 'passengers';
|
||||
const cargo = duplicate(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx);
|
||||
const type = row.classList.contains("crew") ? "crew" : "passengers";
|
||||
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx);
|
||||
return this.actor.update({[`data.cargo.${type}`]: cargo});
|
||||
}
|
||||
|
||||
|
@ -352,6 +389,16 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
async _onDropItemCreate(itemData) {
|
||||
const cargoTypes = ["weapon", "equipment", "consumable", "tool", "loot", "backpack"];
|
||||
const isCargo = cargoTypes.includes(itemData.type) && this._tabs[0].active === "cargo";
|
||||
foundry.utils.setProperty(itemData, "flags.sw5e.vehicleCargo", isCargo);
|
||||
return super._onDropItemCreate(itemData);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Special handling for editing HP to clamp it within appropriate range.
|
||||
* @param event {Event}
|
||||
|
@ -360,11 +407,11 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
|
|||
*/
|
||||
_onHPChange(event) {
|
||||
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 hp = Math.clamped(0, parseInt(event.currentTarget.value), item.data.data.hp.max);
|
||||
event.currentTarget.value = hp;
|
||||
return item.update({'data.hp.value': hp});
|
||||
return item.update({"data.hp.value": hp});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -377,9 +424,9 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
|
|||
*/
|
||||
_onToggleItem(event) {
|
||||
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 crewed = !!item.data.data.crewed;
|
||||
return item.update({'data.crewed': !crewed});
|
||||
return item.update({"data.crewed": !crewed});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* @type {Dialog}
|
||||
*/
|
||||
export default class AbilityUseDialog extends Dialog {
|
||||
constructor(item, dialogData={}, options={}) {
|
||||
constructor(item, dialogData = {}, options = {}) {
|
||||
super(dialogData, options);
|
||||
this.options.classes = ["sw5e", "dialog"];
|
||||
|
||||
|
@ -25,7 +25,7 @@ export default class AbilityUseDialog extends Dialog {
|
|||
* @return {Promise}
|
||||
*/
|
||||
static async create(item) {
|
||||
if ( !item.isOwned ) throw new Error("You cannot display an ability usage dialog for an unowned item");
|
||||
if (!item.isOwned) throw new Error("You cannot display an ability usage dialog for an unowned item");
|
||||
|
||||
// Prepare data
|
||||
const actorData = item.actor.data.data;
|
||||
|
@ -39,17 +39,20 @@ export default class AbilityUseDialog extends Dialog {
|
|||
// Prepare dialog form data
|
||||
const data = {
|
||||
item: item.data,
|
||||
title: game.i18n.format("SW5E.AbilityUseHint", item.data),
|
||||
title: game.i18n.format("SW5E.AbilityUseHint", {
|
||||
type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`),
|
||||
name: item.name
|
||||
}),
|
||||
note: this._getAbilityUseNote(item.data, uses, recharge),
|
||||
consumePowerSlot: false,
|
||||
consumeRecharge: recharges,
|
||||
consumeResource: !!itemData.consume.target,
|
||||
consumeUses: uses.max,
|
||||
consumeUses: uses.per && uses.max > 0,
|
||||
canUse: recharges ? recharge.charged : sufficientUses,
|
||||
createTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget,
|
||||
errors: []
|
||||
};
|
||||
if ( item.data.type === "power" ) this._getPowerData(actorData, itemData, data);
|
||||
if (item.data.type === "power") this._getPowerData(actorData, itemData, data);
|
||||
|
||||
// Render the ability usage template
|
||||
const html = await renderTemplate("systems/sw5e/templates/apps/ability-use.html", data);
|
||||
|
@ -59,13 +62,13 @@ export default class AbilityUseDialog extends Dialog {
|
|||
const label = game.i18n.localize("SW5E.AbilityUse" + (data.isPower ? "Cast" : "Use"));
|
||||
return new Promise((resolve) => {
|
||||
const dlg = new this(item, {
|
||||
title: `${item.name}: Usage Configuration`,
|
||||
title: `${item.name}: ${game.i18n.localize("SW5E.AbilityUseConfig")}`,
|
||||
content: html,
|
||||
buttons: {
|
||||
use: {
|
||||
icon: `<i class="fas ${icon}"></i>`,
|
||||
label: label,
|
||||
callback: html => {
|
||||
callback: (html) => {
|
||||
const fd = new FormDataExtended(html[0].querySelector("form"));
|
||||
resolve(fd.toObject());
|
||||
}
|
||||
|
@ -87,14 +90,13 @@ export default class AbilityUseDialog extends Dialog {
|
|||
* @private
|
||||
*/
|
||||
static _getPowerData(actorData, itemData, data) {
|
||||
|
||||
// Determine whether the power may be up-cast
|
||||
const lvl = itemData.level;
|
||||
const consumePowerSlot = (lvl > 0) && CONFIG.SW5E.powerUpcastModes.includes(itemData.preparation.mode);
|
||||
const consumePowerSlot = lvl > 0 && CONFIG.SW5E.powerUpcastModes.includes(itemData.preparation.mode);
|
||||
|
||||
// If can't upcast, return early and don't bother calculating available power slots
|
||||
if (!consumePowerSlot) {
|
||||
mergeObject(data, { isPower: true, consumePowerSlot });
|
||||
mergeObject(data, {isPower: true, consumePowerSlot});
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -102,74 +104,78 @@ export default class AbilityUseDialog extends Dialog {
|
|||
let lmax = 0;
|
||||
let points;
|
||||
let powerType;
|
||||
switch (itemData.school){
|
||||
switch (itemData.school) {
|
||||
case "lgt":
|
||||
case "uni":
|
||||
case "drk": {
|
||||
powerType = "force"
|
||||
powerType = "force";
|
||||
points = actorData.attributes.force.points.value + actorData.attributes.force.points.temp;
|
||||
break;
|
||||
}
|
||||
case "tec": {
|
||||
powerType = "tech"
|
||||
powerType = "tech";
|
||||
points = actorData.attributes.tech.points.value + actorData.attributes.tech.points.temp;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// eliminate point usage for innate casters
|
||||
if (actorData.attributes.powercasting === 'innate') points = 999;
|
||||
if (actorData.attributes.powercasting === "innate") points = 999;
|
||||
|
||||
|
||||
let powerLevels
|
||||
if (powerType === "force"){
|
||||
powerLevels = Array.fromRange(10).reduce((arr, i) => {
|
||||
if ( i < lvl ) return arr;
|
||||
let powerLevels;
|
||||
if (powerType === "force") {
|
||||
powerLevels = Array.fromRange(10)
|
||||
.reduce((arr, i) => {
|
||||
if (i < lvl) return arr;
|
||||
const label = CONFIG.SW5E.powerLevels[i];
|
||||
const l = actorData.powers["power"+i] || {fmax: 0, foverride: null};
|
||||
const l = actorData.powers["power" + i] || {fmax: 0, foverride: null};
|
||||
let max = parseInt(l.foverride || l.fmax || 0);
|
||||
let slots = Math.clamped(parseInt(l.fvalue || 0), 0, max);
|
||||
if ( max > 0 ) lmax = i;
|
||||
if ((max > 0) && (slots > 0) && (points > i)){
|
||||
if (max > 0) lmax = i;
|
||||
if (max > 0 && slots > 0 && points > i) {
|
||||
arr.push({
|
||||
level: i,
|
||||
label: i > 0 ? game.i18n.format('SW5E.PowerLevelSlot', {level: label, n: slots}) : label,
|
||||
label: i > 0 ? game.i18n.format("SW5E.PowerLevelSlot", {level: label, n: slots}) : label,
|
||||
canCast: max > 0,
|
||||
hasSlots: slots > 0
|
||||
});
|
||||
}
|
||||
return arr;
|
||||
}, []).filter(sl => sl.level <= lmax);
|
||||
}else if (powerType === "tech"){
|
||||
powerLevels = Array.fromRange(10).reduce((arr, i) => {
|
||||
if ( i < lvl ) return arr;
|
||||
}, [])
|
||||
.filter((sl) => sl.level <= lmax);
|
||||
} else if (powerType === "tech") {
|
||||
powerLevels = Array.fromRange(10)
|
||||
.reduce((arr, i) => {
|
||||
if (i < lvl) return arr;
|
||||
const label = CONFIG.SW5E.powerLevels[i];
|
||||
const l = actorData.powers["power"+i] || {tmax: 0, toverride: null};
|
||||
const l = actorData.powers["power" + i] || {tmax: 0, toverride: null};
|
||||
let max = parseInt(l.override || l.tmax || 0);
|
||||
let slots = Math.clamped(parseInt(l.tvalue || 0), 0, max);
|
||||
if ( max > 0 ) lmax = i;
|
||||
if ((max > 0) && (slots > 0) && (points > i)){
|
||||
if (max > 0) lmax = i;
|
||||
if (max > 0 && slots > 0 && points > i) {
|
||||
arr.push({
|
||||
level: i,
|
||||
label: i > 0 ? game.i18n.format('SW5E.PowerLevelSlot', {level: label, n: slots}) : label,
|
||||
label: i > 0 ? game.i18n.format("SW5E.PowerLevelSlot", {level: label, n: slots}) : label,
|
||||
canCast: max > 0,
|
||||
hasSlots: slots > 0
|
||||
});
|
||||
}
|
||||
return arr;
|
||||
}, []).filter(sl => sl.level <= lmax);
|
||||
}, [])
|
||||
.filter((sl) => sl.level <= lmax);
|
||||
}
|
||||
|
||||
|
||||
|
||||
const canCast = powerLevels.some(l => l.hasSlots);
|
||||
if ( !canCast ) data.errors.push(game.i18n.format("SW5E.PowerCastNoSlots", {
|
||||
const canCast = powerLevels.some((l) => l.hasSlots);
|
||||
if (!canCast)
|
||||
data.errors.push(
|
||||
game.i18n.format("SW5E.PowerCastNoSlots", {
|
||||
level: CONFIG.SW5E.powerLevels[lvl],
|
||||
name: data.item.name
|
||||
}));
|
||||
})
|
||||
);
|
||||
|
||||
// Merge power casting data
|
||||
return mergeObject(data, { isPower: true, consumePowerSlot, powerLevels });
|
||||
return foundry.utils.mergeObject(data, {isPower: true, consumePowerSlot, powerLevels});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -179,29 +185,28 @@ export default class AbilityUseDialog extends Dialog {
|
|||
* @private
|
||||
*/
|
||||
static _getAbilityUseNote(item, uses, recharge) {
|
||||
|
||||
// Zero quantity
|
||||
const quantity = item.data.quantity;
|
||||
if ( quantity <= 0 ) return game.i18n.localize("SW5E.AbilityUseUnavailableHint");
|
||||
if (quantity <= 0) return game.i18n.localize("SW5E.AbilityUseUnavailableHint");
|
||||
|
||||
// Abilities which use Recharge
|
||||
if ( !!recharge.value ) {
|
||||
if (!!recharge.value) {
|
||||
return game.i18n.format(recharge.charged ? "SW5E.AbilityUseChargedHint" : "SW5E.AbilityUseRechargeHint", {
|
||||
type: item.type,
|
||||
})
|
||||
type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`)
|
||||
});
|
||||
}
|
||||
|
||||
// Does not use any resource
|
||||
if ( !uses.per || !uses.max ) return "";
|
||||
if (!uses.per || !uses.max) return "";
|
||||
|
||||
// Consumables
|
||||
if ( item.type === "consumable" ) {
|
||||
if (item.type === "consumable") {
|
||||
let str = "SW5E.AbilityUseNormalHint";
|
||||
if ( uses.value > 1 ) str = "SW5E.AbilityUseConsumableChargeHint";
|
||||
else if ( item.data.quantity === 1 && uses.autoDestroy ) str = "SW5E.AbilityUseConsumableDestroyHint";
|
||||
else if ( item.data.quantity > 1 ) str = "SW5E.AbilityUseConsumableQuantityHint";
|
||||
if (uses.value > 1) str = "SW5E.AbilityUseConsumableChargeHint";
|
||||
else if (item.data.quantity === 1 && uses.autoDestroy) str = "SW5E.AbilityUseConsumableDestroyHint";
|
||||
else if (item.data.quantity > 1) str = "SW5E.AbilityUseConsumableQuantityHint";
|
||||
return game.i18n.format(str, {
|
||||
type: item.data.consumableType,
|
||||
type: game.i18n.localize(`SW5E.Consumable${item.data.consumableType.capitalize()}`),
|
||||
value: uses.value,
|
||||
quantity: item.data.quantity,
|
||||
max: uses.max,
|
||||
|
@ -212,17 +217,11 @@ export default class AbilityUseDialog extends Dialog {
|
|||
// Other Items
|
||||
else {
|
||||
return game.i18n.format("SW5E.AbilityUseNormalHint", {
|
||||
type: item.type,
|
||||
type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`),
|
||||
value: uses.value,
|
||||
max: uses.max,
|
||||
per: CONFIG.SW5E.limitedUsePeriods[uses.per]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
static _handleSubmit(formData, item) {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
/**
|
||||
* An application class which provides advanced configuration for special character flags which modify an Actor
|
||||
* @implements {BaseEntitySheet}
|
||||
* @implements {DocumentSheet}
|
||||
*/
|
||||
export default class ActorSheetFlags extends BaseEntitySheet {
|
||||
export default class ActorSheetFlags extends DocumentSheet {
|
||||
static get defaultOptions() {
|
||||
const options = super.defaultOptions;
|
||||
return mergeObject(options, {
|
||||
return foundry.utils.mergeObject(super.defaultOptions, {
|
||||
id: "actor-flags",
|
||||
classes: ["sw5e"],
|
||||
template: "systems/sw5e/templates/apps/actor-flags.html",
|
||||
|
@ -18,7 +17,7 @@ export default class ActorSheetFlags extends BaseEntitySheet {
|
|||
|
||||
/** @override */
|
||||
get title() {
|
||||
return `${game.i18n.localize('SW5E.FlagsTitle')}: ${this.object.name}`;
|
||||
return `${game.i18n.localize("SW5E.FlagsTitle")}: ${this.object.name}`;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -27,6 +26,7 @@ export default class ActorSheetFlags extends BaseEntitySheet {
|
|||
getData() {
|
||||
const data = {};
|
||||
data.actor = this.object;
|
||||
data.classes = this._getClasses();
|
||||
data.flags = this._getFlags();
|
||||
data.bonuses = this._getBonuses();
|
||||
return data;
|
||||
|
@ -34,20 +34,38 @@ export default class ActorSheetFlags extends BaseEntitySheet {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Prepare an object of sorted classes.
|
||||
* @return {object}
|
||||
* @private
|
||||
*/
|
||||
_getClasses() {
|
||||
const classes = this.object.items.filter((i) => i.type === "class");
|
||||
return classes
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.reduce((obj, i) => {
|
||||
obj[i.id] = i.name;
|
||||
return obj;
|
||||
}, {});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Prepare an object of flags data which groups flags by section
|
||||
* Add some additional data for rendering
|
||||
* @return {object}
|
||||
* @private
|
||||
*/
|
||||
_getFlags() {
|
||||
const flags = {};
|
||||
const baseData = this.entity._data;
|
||||
for ( let [k, v] of Object.entries(CONFIG.SW5E.characterFlags) ) {
|
||||
if ( !flags.hasOwnProperty(v.section) ) flags[v.section] = {};
|
||||
let flag = duplicate(v);
|
||||
const baseData = this.document.toJSON();
|
||||
for (let [k, v] of Object.entries(CONFIG.SW5E.characterFlags)) {
|
||||
if (!flags.hasOwnProperty(v.section)) flags[v.section] = {};
|
||||
let flag = foundry.utils.deepClone(v);
|
||||
flag.type = v.type.name;
|
||||
flag.isCheckbox = v.type === Boolean;
|
||||
flag.isSelect = v.hasOwnProperty('choices');
|
||||
flag.isSelect = v.hasOwnProperty("choices");
|
||||
flag.value = getProperty(baseData.flags, `sw5e.${k}`);
|
||||
flags[v.section][`flags.sw5e.${k}`] = flag;
|
||||
}
|
||||
|
@ -80,7 +98,7 @@ export default class ActorSheetFlags extends BaseEntitySheet {
|
|||
{name: "data.bonuses.power.forceUnivDC", label: "SW5E.BonusForceUnivPowerDC"},
|
||||
{name: "data.bonuses.power.techDC", label: "SW5E.BonusTechPowerDC"}
|
||||
];
|
||||
for ( let b of bonuses ) {
|
||||
for (let b of bonuses) {
|
||||
b.value = getProperty(this.object._data, b.name) || "";
|
||||
}
|
||||
return bonuses;
|
||||
|
@ -97,11 +115,11 @@ export default class ActorSheetFlags extends BaseEntitySheet {
|
|||
let unset = false;
|
||||
const flags = updateData.flags.sw5e;
|
||||
//clone flags to dnd5e for module compatability
|
||||
updateData.flags.dnd5e = updateData.flags.sw5e
|
||||
for ( let [k, v] of Object.entries(flags) ) {
|
||||
if ( [undefined, null, "", false, 0].includes(v) ) {
|
||||
updateData.flags.dnd5e = updateData.flags.sw5e;
|
||||
for (let [k, v] of Object.entries(flags)) {
|
||||
if ([undefined, null, "", false, 0].includes(v)) {
|
||||
delete flags[k];
|
||||
if ( hasProperty(actor._data.flags, `sw5e.${k}`) ) {
|
||||
if (hasProperty(actor._data.flags, `sw5e.${k}`)) {
|
||||
unset = true;
|
||||
flags[`-=${k}`] = null;
|
||||
}
|
||||
|
@ -109,8 +127,8 @@ export default class ActorSheetFlags extends BaseEntitySheet {
|
|||
}
|
||||
|
||||
// Clear any bonuses which are whitespace only
|
||||
for ( let b of Object.values(updateData.data.bonuses ) ) {
|
||||
for ( let [k, v] of Object.entries(b) ) {
|
||||
for (let b of Object.values(updateData.data.bonuses)) {
|
||||
for (let [k, v] of Object.entries(b)) {
|
||||
b[k] = v.trim();
|
||||
}
|
||||
}
|
||||
|
|
111
module/apps/actor-type.js
Normal file
|
@ -0,0 +1,111 @@
|
|||
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);
|
||||
}
|
||||
}
|
92
module/apps/hit-dice-config.js
Normal file
|
@ -0,0 +1,92 @@
|
|||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
|
@ -37,30 +37,28 @@ export default class LongRestDialog extends Dialog {
|
|||
* @param {Actor5e} actor
|
||||
* @return {Promise}
|
||||
*/
|
||||
static async longRestDialog({ actor } = {}) {
|
||||
static async longRestDialog({actor} = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const dlg = new this(actor, {
|
||||
title: "Long Rest",
|
||||
title: game.i18n.localize("SW5E.LongRest"),
|
||||
buttons: {
|
||||
rest: {
|
||||
icon: '<i class="fas fa-bed"></i>',
|
||||
label: "Rest",
|
||||
callback: html => {
|
||||
let newDay = false;
|
||||
if (game.settings.get("sw5e", "restVariant") === "normal")
|
||||
label: game.i18n.localize("SW5E.Rest"),
|
||||
callback: (html) => {
|
||||
let newDay = true;
|
||||
if (game.settings.get("sw5e", "restVariant") !== "gritty")
|
||||
newDay = html.find('input[name="newDay"]')[0].checked;
|
||||
else if(game.settings.get("sw5e", "restVariant") === "gritty")
|
||||
newDay = true;
|
||||
resolve(newDay);
|
||||
}
|
||||
},
|
||||
cancel: {
|
||||
icon: '<i class="fas fa-times"></i>',
|
||||
label: "Cancel",
|
||||
label: game.i18n.localize("Cancel"),
|
||||
callback: reject
|
||||
}
|
||||
},
|
||||
default: 'rest',
|
||||
default: "rest",
|
||||
close: reject
|
||||
});
|
||||
dlg.render(true);
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
/**
|
||||
* A simple form to set actor movement speeds
|
||||
* @implements {BaseEntitySheet}
|
||||
* @extends {DocumentSheet}
|
||||
*/
|
||||
export default class ActorMovementConfig extends BaseEntitySheet {
|
||||
|
||||
export default class ActorMovementConfig extends DocumentSheet {
|
||||
/** @override */
|
||||
static get defaultOptions() {
|
||||
return mergeObject(super.defaultOptions, {
|
||||
return foundry.utils.mergeObject(super.defaultOptions, {
|
||||
classes: ["sw5e"],
|
||||
template: "systems/sw5e/templates/apps/movement-config.html",
|
||||
width: 300,
|
||||
|
@ -18,19 +17,20 @@ export default class ActorMovementConfig extends BaseEntitySheet {
|
|||
|
||||
/** @override */
|
||||
get title() {
|
||||
return `${game.i18n.localize("SW5E.MovementConfig")}: ${this.entity.name}`;
|
||||
return `${game.i18n.localize("SW5E.MovementConfig")}: ${this.document.name}`;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
getData(options) {
|
||||
const sourceMovement = foundry.utils.getProperty(this.document.data._source, "data.attributes.movement") || {};
|
||||
const data = {
|
||||
movement: duplicate(this.entity._data.data.attributes.movement),
|
||||
movement: foundry.utils.deepClone(sourceMovement),
|
||||
units: CONFIG.SW5E.movementUnits
|
||||
}
|
||||
for ( let [k, v] of Object.entries(data.movement) ) {
|
||||
if ( ["units", "hover"].includes(k) ) continue;
|
||||
};
|
||||
for (let [k, v] of Object.entries(data.movement)) {
|
||||
if (["units", "hover"].includes(k)) continue;
|
||||
data.movement[k] = Number.isNumeric(v) ? v.toNearest(0.1) : 0;
|
||||
}
|
||||
return data;
|
||||
|
|
66
module/apps/select-items-prompt.js
Normal file
|
@ -0,0 +1,66 @@
|
|||
/**
|
||||
* 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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,12 +1,11 @@
|
|||
/**
|
||||
* A simple form to set actor movement speeds
|
||||
* @implements {BaseEntitySheet}
|
||||
* A simple form to set Actor movement speeds.
|
||||
* @extends {DocumentSheet}
|
||||
*/
|
||||
export default class ActorSensesConfig extends BaseEntitySheet {
|
||||
|
||||
/** @override */
|
||||
export default class ActorSensesConfig extends DocumentSheet {
|
||||
/** @inheritdoc */
|
||||
static get defaultOptions() {
|
||||
return mergeObject(super.defaultOptions, {
|
||||
return foundry.utils.mergeObject(super.defaultOptions, {
|
||||
classes: ["sw5e"],
|
||||
template: "systems/sw5e/templates/apps/senses-config.html",
|
||||
width: 300,
|
||||
|
@ -16,27 +15,28 @@ export default class ActorSensesConfig extends BaseEntitySheet {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
/** @inheritdoc */
|
||||
get title() {
|
||||
return `${game.i18n.localize("SW5E.SensesConfig")}: ${this.entity.name}`;
|
||||
return `${game.i18n.localize("SW5E.SensesConfig")}: ${this.document.name}`;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
/** @inheritdoc */
|
||||
getData(options) {
|
||||
const senses = this.entity._data.data.attributes?.senses ?? {};
|
||||
const senses = foundry.utils.getProperty(this.document.data._source, "data.attributes.senses") || {};
|
||||
const data = {
|
||||
senses: {},
|
||||
special: senses.special ?? "",
|
||||
units: senses.units, movementUnits: CONFIG.SW5E.movementUnits
|
||||
units: senses.units,
|
||||
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];
|
||||
data.senses[name] = {
|
||||
label: game.i18n.localize(label),
|
||||
value: Number.isNumeric(v) ? v.toNearest(0.1) : 0
|
||||
}
|
||||
};
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import LongRestDialog from "./long-rest.js";
|
|||
* @extends {Dialog}
|
||||
*/
|
||||
export default class ShortRestDialog extends Dialog {
|
||||
constructor(actor, dialogData={}, options={}) {
|
||||
constructor(actor, dialogData = {}, options = {}) {
|
||||
super(dialogData, options);
|
||||
|
||||
/**
|
||||
|
@ -39,8 +39,8 @@ export default class ShortRestDialog extends Dialog {
|
|||
|
||||
// Determine Hit Dice
|
||||
data.availableHD = this.actor.data.items.reduce((hd, item) => {
|
||||
if ( item.type === "class" ) {
|
||||
const d = item.data;
|
||||
if (item.type === "class") {
|
||||
const d = item.data.data;
|
||||
const denom = d.hitDice || "d6";
|
||||
const available = parseInt(d.levels || 1) - parseInt(d.hitDiceUsed || 0);
|
||||
hd[denom] = denom in hd ? hd[denom] + available : available;
|
||||
|
@ -59,7 +59,6 @@ export default class ShortRestDialog extends Dialog {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
|
||||
/** @override */
|
||||
activateListeners(html) {
|
||||
super.activateListeners(html);
|
||||
|
@ -90,15 +89,15 @@ export default class ShortRestDialog extends Dialog {
|
|||
* @param {Actor5e} actor
|
||||
* @return {Promise}
|
||||
*/
|
||||
static async shortRestDialog({actor}={}) {
|
||||
static async shortRestDialog({actor} = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const dlg = new this(actor, {
|
||||
title: "Short Rest",
|
||||
title: game.i18n.localize("SW5E.ShortRest"),
|
||||
buttons: {
|
||||
rest: {
|
||||
icon: '<i class="fas fa-bed"></i>',
|
||||
label: "Rest",
|
||||
callback: html => {
|
||||
label: game.i18n.localize("SW5E.Rest"),
|
||||
callback: (html) => {
|
||||
let newDay = false;
|
||||
if (game.settings.get("sw5e", "restVariant") === "gritty")
|
||||
newDay = html.find('input[name="newDay"]')[0].checked;
|
||||
|
@ -107,7 +106,7 @@ export default class ShortRestDialog extends Dialog {
|
|||
},
|
||||
cancel: {
|
||||
icon: '<i class="fas fa-times"></i>',
|
||||
label: "Cancel",
|
||||
label: game.i18n.localize("Cancel"),
|
||||
callback: reject
|
||||
}
|
||||
},
|
||||
|
@ -126,8 +125,10 @@ export default class ShortRestDialog extends Dialog {
|
|||
* @param {Actor5e} actor
|
||||
* @return {Promise}
|
||||
*/
|
||||
static async longRestDialog({actor}={}) {
|
||||
console.warn("WARNING! ShortRestDialog.longRestDialog has been deprecated, use LongRestDialog.longRestDialog instead.");
|
||||
static async longRestDialog({actor} = {}) {
|
||||
console.warn(
|
||||
"WARNING! ShortRestDialog.longRestDialog has been deprecated, use LongRestDialog.longRestDialog instead."
|
||||
);
|
||||
return LongRestDialog.longRestDialog(...arguments);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
/**
|
||||
* A specialized form used to select from a checklist of attributes, traits, or properties
|
||||
* @implements {FormApplication}
|
||||
* @extends {DocumentSheet}
|
||||
*/
|
||||
export default class TraitSelector extends FormApplication {
|
||||
|
||||
/** @override */
|
||||
export default class TraitSelector extends DocumentSheet {
|
||||
/** @inheritdoc */
|
||||
static get defaultOptions() {
|
||||
return mergeObject(super.defaultOptions, {
|
||||
return foundry.utils.mergeObject(super.defaultOptions, {
|
||||
id: "trait-selector",
|
||||
classes: ["sw5e"],
|
||||
classes: ["sw5e", "trait-selector", "subconfig"],
|
||||
title: "Actor Trait Selection",
|
||||
template: "systems/sw5e/templates/apps/trait-selector.html",
|
||||
width: 320,
|
||||
|
@ -16,7 +15,9 @@ export default class TraitSelector extends FormApplication {
|
|||
choices: {},
|
||||
allowCustom: true,
|
||||
minimum: 0,
|
||||
maximum: null
|
||||
maximum: null,
|
||||
valueKey: "value",
|
||||
customKey: "custom"
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -24,7 +25,7 @@ export default class TraitSelector extends FormApplication {
|
|||
|
||||
/**
|
||||
* Return a reference to the target attribute
|
||||
* @type {String}
|
||||
* @type {string}
|
||||
*/
|
||||
get attribute() {
|
||||
return this.options.name;
|
||||
|
@ -34,52 +35,50 @@ export default class TraitSelector extends FormApplication {
|
|||
|
||||
/** @override */
|
||||
getData() {
|
||||
|
||||
// Get current values
|
||||
let attr = getProperty(this.object._data, this.attribute);
|
||||
if ( getType(attr) !== "Object" ) attr = {value: [], custom: ""};
|
||||
const attr = foundry.utils.getProperty(this.object.data, this.attribute);
|
||||
const o = this.options;
|
||||
const value = o.valueKey ? attr[o.valueKey] ?? [] : attr;
|
||||
const custom = o.customKey ? attr[o.customKey] ?? "" : "";
|
||||
|
||||
// Populate choices
|
||||
const choices = duplicate(this.options.choices);
|
||||
for ( let [k, v] of Object.entries(choices) ) {
|
||||
choices[k] = {
|
||||
label: v,
|
||||
chosen: attr ? attr.value.includes(k) : false
|
||||
}
|
||||
}
|
||||
const choices = Object.entries(o.choices).reduce((obj, e) => {
|
||||
let [k, v] = e;
|
||||
obj[k] = {label: v, chosen: attr ? value.includes(k) : false};
|
||||
return obj;
|
||||
}, {});
|
||||
|
||||
// Return data
|
||||
return {
|
||||
allowCustom: this.options.allowCustom,
|
||||
allowCustom: o.allowCustom,
|
||||
choices: choices,
|
||||
custom: attr ? attr.custom : ""
|
||||
}
|
||||
custom: custom
|
||||
};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_updateObject(event, formData) {
|
||||
const updateData = {};
|
||||
async _updateObject(event, formData) {
|
||||
const o = this.options;
|
||||
|
||||
// Obtain choices
|
||||
const chosen = [];
|
||||
for ( let [k, v] of Object.entries(formData) ) {
|
||||
if ( (k !== "custom") && v ) chosen.push(k);
|
||||
for (let [k, v] of Object.entries(formData)) {
|
||||
if (k !== "custom" && v) chosen.push(k);
|
||||
}
|
||||
updateData[`${this.attribute}.value`] = chosen;
|
||||
|
||||
// Object including custom data
|
||||
const updateData = {};
|
||||
if (o.valueKey) updateData[`${this.attribute}.${o.valueKey}`] = chosen;
|
||||
else updateData[this.attribute] = chosen;
|
||||
if (o.allowCustom) updateData[`${this.attribute}.${o.customKey}`] = formData.custom;
|
||||
|
||||
// Validate the number chosen
|
||||
if ( this.options.minimum && (chosen.length < this.options.minimum) ) {
|
||||
return ui.notifications.error(`You must choose at least ${this.options.minimum} options`);
|
||||
if (o.minimum && chosen.length < o.minimum) {
|
||||
return ui.notifications.error(`You must choose at least ${o.minimum} options`);
|
||||
}
|
||||
if ( this.options.maximum && (chosen.length > this.options.maximum) ) {
|
||||
return ui.notifications.error(`You may choose no more than ${this.options.maximum} options`);
|
||||
}
|
||||
|
||||
// Include custom
|
||||
if ( this.options.allowCustom ) {
|
||||
updateData[`${this.attribute}.custom`] = formData.custom;
|
||||
if (o.maximum && chosen.length > o.maximum) {
|
||||
return ui.notifications.error(`You may choose no more than ${o.maximum} options`);
|
||||
}
|
||||
|
||||
// Update the object
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/** @override */
|
||||
export const measureDistances = function(segments, options={}) {
|
||||
if ( !options.gridSpaces ) return BaseGrid.prototype.measureDistances.call(this, segments, options);
|
||||
export const measureDistances = function (segments, options = {}) {
|
||||
if (!options.gridSpaces) return BaseGrid.prototype.measureDistances.call(this, segments, options);
|
||||
|
||||
// Track the total number of diagonals
|
||||
let nDiagonal = 0;
|
||||
|
@ -8,7 +8,7 @@ export const measureDistances = function(segments, options={}) {
|
|||
const d = canvas.dimensions;
|
||||
|
||||
// Iterate over measured segments
|
||||
return segments.map(s => {
|
||||
return segments.map((s) => {
|
||||
let r = s.ray;
|
||||
|
||||
// Determine the total distance traveled
|
||||
|
@ -23,7 +23,7 @@ export const measureDistances = function(segments, options={}) {
|
|||
// Alternative DMG Movement
|
||||
if (rule === "5105") {
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -36,19 +36,3 @@ export const measureDistances = function(segments, options={}) {
|
|||
else return (ns + nd) * canvas.scene.data.gridDistance;
|
||||
});
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Hijack Token health bar rendering to include temporary and temp-max health in the bar display
|
||||
* TODO: This should probably be replaced with a formal Token class extension
|
||||
*/
|
||||
const _TokenGetBarAttribute = Token.prototype.getBarAttribute;
|
||||
export const getBarAttribute = function(...args) {
|
||||
const data = _TokenGetBarAttribute.bind(this)(...args);
|
||||
if ( data && (data.attribute === "attributes.hp") ) {
|
||||
data.value += parseInt(getProperty(this.actor.data, "data.attributes.hp.temp") || 0);
|
||||
data.max += parseInt(getProperty(this.actor.data, "data.attributes.hp.tempmax") || 0);
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
|
|
@ -1,52 +1,113 @@
|
|||
export default class CharacterImporter {
|
||||
|
||||
// transform JSON from sw5e.com to Foundry friendly format
|
||||
// and insert new actor
|
||||
static async transform(rawCharacter){
|
||||
static async transform(rawCharacter) {
|
||||
const sourceCharacter = JSON.parse(rawCharacter); //source character
|
||||
|
||||
const details = {
|
||||
species: sourceCharacter.attribs.find(e => e.name == "race").current,
|
||||
background: sourceCharacter.attribs.find(e => e.name == "background").current,
|
||||
alignment: sourceCharacter.attribs.find(e => e.name == "alignment").current
|
||||
}
|
||||
|
||||
const hp = {
|
||||
value: sourceCharacter.attribs.find(e => e.name == "hp").current,
|
||||
min: 0,
|
||||
max: sourceCharacter.attribs.find(e => e.name == "hp").current,
|
||||
temp: sourceCharacter.attribs.find(e => e.name == "hp_temp").current
|
||||
species: sourceCharacter.attribs.find((e) => e.name == "race").current,
|
||||
background: sourceCharacter.attribs.find((e) => e.name == "background").current,
|
||||
alignment: sourceCharacter.attribs.find((e) => e.name == "alignment").current
|
||||
};
|
||||
|
||||
const ac = {
|
||||
value: sourceCharacter.attribs.find(e => e.name == "ac").current
|
||||
const hp = {
|
||||
value: sourceCharacter.attribs.find((e) => e.name == "hp").current,
|
||||
min: 0,
|
||||
max: sourceCharacter.attribs.find((e) => e.name == "hp").current,
|
||||
temp: sourceCharacter.attribs.find((e) => e.name == "hp_temp").current
|
||||
};
|
||||
|
||||
const abilities = {
|
||||
str: {
|
||||
value: sourceCharacter.attribs.find(e => e.name == "strength").current,
|
||||
proficient: sourceCharacter.attribs.find(e => e.name == 'strength_save_prof').current ? 1 : 0
|
||||
value: sourceCharacter.attribs.find((e) => e.name == "strength").current,
|
||||
proficient: sourceCharacter.attribs.find((e) => e.name == "strength_save_prof").current ? 1 : 0
|
||||
},
|
||||
dex: {
|
||||
value: sourceCharacter.attribs.find(e => e.name == "dexterity").current,
|
||||
proficient: sourceCharacter.attribs.find(e => e.name == 'dexterity_save_prof').current ? 1 : 0
|
||||
value: sourceCharacter.attribs.find((e) => e.name == "dexterity").current,
|
||||
proficient: sourceCharacter.attribs.find((e) => e.name == "dexterity_save_prof").current ? 1 : 0
|
||||
},
|
||||
con: {
|
||||
value: sourceCharacter.attribs.find(e => e.name == "constitution").current,
|
||||
proficient: sourceCharacter.attribs.find(e => e.name == 'constitution_save_prof').current ? 1 : 0
|
||||
value: sourceCharacter.attribs.find((e) => e.name == "constitution").current,
|
||||
proficient: sourceCharacter.attribs.find((e) => e.name == "constitution_save_prof").current ? 1 : 0
|
||||
},
|
||||
int: {
|
||||
value: sourceCharacter.attribs.find(e => e.name == "intelligence").current,
|
||||
proficient: sourceCharacter.attribs.find(e => e.name == 'intelligence_save_prof').current ? 1 : 0
|
||||
value: sourceCharacter.attribs.find((e) => e.name == "intelligence").current,
|
||||
proficient: sourceCharacter.attribs.find((e) => e.name == "intelligence_save_prof").current ? 1 : 0
|
||||
},
|
||||
wis: {
|
||||
value: sourceCharacter.attribs.find(e => e.name == "wisdom").current,
|
||||
proficient: sourceCharacter.attribs.find(e => e.name == 'wisdom_save_prof').current ? 1 : 0
|
||||
value: sourceCharacter.attribs.find((e) => e.name == "wisdom").current,
|
||||
proficient: sourceCharacter.attribs.find((e) => e.name == "wisdom_save_prof").current ? 1 : 0
|
||||
},
|
||||
cha: {
|
||||
value: sourceCharacter.attribs.find(e => e.name == "charisma").current,
|
||||
proficient: sourceCharacter.attribs.find(e => e.name == 'charisma_save_prof').current ? 1 : 0
|
||||
value: sourceCharacter.attribs.find((e) => e.name == "charisma").current,
|
||||
proficient: sourceCharacter.attribs.find((e) => e.name == "charisma_save_prof").current ? 1 : 0
|
||||
}
|
||||
};
|
||||
|
||||
/* ----------------------------------------------------------------- */
|
||||
/* character.data.skills.<skill_name>.value is all that matters
|
||||
/* values can be 0, 0.5, 1 or 2
|
||||
/* 0 = regular
|
||||
/* 0.5 = half-proficient
|
||||
/* 1 = proficient
|
||||
/* 2 = expertise
|
||||
/* foundry takes care of calculating the rest
|
||||
/* ----------------------------------------------------------------- */
|
||||
const skills = {
|
||||
acr: {
|
||||
value: sourceCharacter.attribs.find((e) => e.name == "acrobatics_type").current
|
||||
},
|
||||
ani: {
|
||||
value: sourceCharacter.attribs.find((e) => e.name == "animal_handling_type").current
|
||||
},
|
||||
ath: {
|
||||
value: sourceCharacter.attribs.find((e) => e.name == "athletics_type").current
|
||||
},
|
||||
dec: {
|
||||
value: sourceCharacter.attribs.find((e) => e.name == "deception_type").current
|
||||
},
|
||||
ins: {
|
||||
value: sourceCharacter.attribs.find((e) => e.name == "insight_type").current
|
||||
},
|
||||
inv: {
|
||||
value: sourceCharacter.attribs.find((e) => e.name == "investigation_type").current
|
||||
},
|
||||
itm: {
|
||||
value: sourceCharacter.attribs.find((e) => e.name == "intimidation_type").current
|
||||
},
|
||||
lor: {
|
||||
value: sourceCharacter.attribs.find((e) => e.name == "lore_type").current
|
||||
},
|
||||
med: {
|
||||
value: sourceCharacter.attribs.find((e) => e.name == "medicine_type").current
|
||||
},
|
||||
nat: {
|
||||
value: sourceCharacter.attribs.find((e) => e.name == "nature_type").current
|
||||
},
|
||||
per: {
|
||||
value: sourceCharacter.attribs.find((e) => e.name == "persuasion_type").current
|
||||
},
|
||||
pil: {
|
||||
value: sourceCharacter.attribs.find((e) => e.name == "piloting_type").current
|
||||
},
|
||||
prc: {
|
||||
value: sourceCharacter.attribs.find((e) => e.name == "perception_type").current
|
||||
},
|
||||
prf: {
|
||||
value: sourceCharacter.attribs.find((e) => e.name == "performance_type").current
|
||||
},
|
||||
slt: {
|
||||
value: sourceCharacter.attribs.find((e) => e.name == "sleight_of_hand_type").current
|
||||
},
|
||||
ste: {
|
||||
value: sourceCharacter.attribs.find((e) => e.name == "stealth_type").current
|
||||
},
|
||||
sur: {
|
||||
value: sourceCharacter.attribs.find((e) => e.name == "survival_type").current
|
||||
},
|
||||
tec: {
|
||||
value: sourceCharacter.attribs.find((e) => e.name == "technology_type").current
|
||||
}
|
||||
};
|
||||
|
||||
const targetCharacter = {
|
||||
|
@ -55,66 +116,210 @@ export default class CharacterImporter {
|
|||
data: {
|
||||
abilities: abilities,
|
||||
details: details,
|
||||
skills: skills,
|
||||
attributes: {
|
||||
ac: ac,
|
||||
hp: hp
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let actor = await Actor.create(targetCharacter);
|
||||
|
||||
const profession = sourceCharacter.attribs.find(e => e.name == "class").current;
|
||||
let professionLevel = sourceCharacter.attribs.find(e => e.name == "class_display").current;
|
||||
professionLevel = parseInt( professionLevel.replace(/[^0-9]/g,'') ); //remove a-z, leaving only integers
|
||||
CharacterImporter.addClasses(profession, professionLevel, actor);
|
||||
CharacterImporter.addProfessions(sourceCharacter, actor);
|
||||
}
|
||||
|
||||
static async addClasses(profession, level, actor){
|
||||
let classes = await game.packs.get('sw5e.classes').getContent();
|
||||
let assignedClass = classes.find( c => c.name === profession );
|
||||
// Parse all classes and add them to already created actor.
|
||||
// "class" is a reserved word, therefore I use profession where I can.
|
||||
static async addProfessions(sourceCharacter, actor) {
|
||||
let result = [];
|
||||
|
||||
// parse all class and multiclassX items
|
||||
// couldn't get Array.filter to work here for some reason
|
||||
// result = array of objects. each object is a separate class
|
||||
sourceCharacter.attribs.forEach((e) => {
|
||||
if (CharacterImporter.classOrMulticlass(e.name)) {
|
||||
var t = {
|
||||
profession: CharacterImporter.capitalize(e.current),
|
||||
type: CharacterImporter.baseOrMulti(e.name),
|
||||
level: CharacterImporter.getLevel(e, sourceCharacter)
|
||||
};
|
||||
result.push(t);
|
||||
}
|
||||
});
|
||||
|
||||
// pull classes directly from system compendium and add them to current actor
|
||||
const professionsPack = await game.packs.get("sw5e.classes").getDocuments();
|
||||
result.forEach((prof) => {
|
||||
let assignedProfession = professionsPack.find((o) => o.name === prof.profession);
|
||||
assignedProfession.data.data.levels = prof.level;
|
||||
actor.createEmbeddedDocuments("Item", [assignedProfession.data], {displaySheet: false});
|
||||
});
|
||||
|
||||
this.addSpecies(sourceCharacter.attribs.find((e) => e.name == "race").current, actor);
|
||||
|
||||
this.addPowers(
|
||||
sourceCharacter.attribs
|
||||
.filter((e) => e.name.search(/repeating_power.+_powername/g) != -1)
|
||||
.map((e) => e.current),
|
||||
actor
|
||||
);
|
||||
|
||||
const discoveredItems = sourceCharacter.attribs.filter(
|
||||
(e) => e.name.search(/repeating_inventory.+_itemname/g) != -1
|
||||
);
|
||||
const items = discoveredItems.map((item) => {
|
||||
const id = item.name.match(/-\w{19}/g);
|
||||
|
||||
return {
|
||||
name: item.current,
|
||||
quantity: sourceCharacter.attribs.find((e) => e.name === `repeating_inventory_${id}_itemcount`).current
|
||||
};
|
||||
});
|
||||
|
||||
this.addItems(items, actor);
|
||||
}
|
||||
|
||||
static async addClasses(profession, level, actor) {
|
||||
let classes = await game.packs.get("sw5e.classes").getDocuments();
|
||||
let assignedClass = classes.find((c) => c.name === profession);
|
||||
assignedClass.data.data.levels = level;
|
||||
await actor.createEmbeddedEntity("OwnedItem", assignedClass.data, { displaySheet: false });
|
||||
await actor.createEmbeddedDocuments("Item", [assignedClass.data], {displaySheet: false});
|
||||
}
|
||||
|
||||
static addImportButton(html){
|
||||
const header = $("#actors").find("header.directory-header");
|
||||
const search = $("#actors").children().find("div.header-search");
|
||||
const newImportButtonDiv = $("#actors").children().find("div.header-actions").clone();
|
||||
const newSearch = search.clone();
|
||||
search.remove();
|
||||
newImportButtonDiv.attr('id', 'character-sheet-import');
|
||||
header.append(newImportButtonDiv);
|
||||
newImportButtonDiv.children("button").remove();
|
||||
newImportButtonDiv.append("<button class='create-entity' id='cs-import-button'><i class='fas fa-upload'></i> Import Character</button>");
|
||||
newSearch.appendTo(header);
|
||||
static classOrMulticlass(name) {
|
||||
return name === "class" || (name.includes("multiclass") && name.length <= 12);
|
||||
}
|
||||
|
||||
let characterImportButton = $("#cs-import-button");
|
||||
characterImportButton.click(ev => {
|
||||
let content = '<h1>Saved Character JSON Import</h1> '
|
||||
+ '<label for="character-json">Paste character JSON here:</label> '
|
||||
+ '</br>'
|
||||
+ '<textarea id="character-json" name="character-json" rows="10" cols="50"></textarea>';
|
||||
static baseOrMulti(name) {
|
||||
if (name === "class") {
|
||||
return "base_class";
|
||||
} else {
|
||||
return "multi_class";
|
||||
}
|
||||
}
|
||||
|
||||
static getLevel(item, sourceCharacter) {
|
||||
if (item.name === "class") {
|
||||
let result = sourceCharacter.attribs.find((e) => e.name === "base_level").current;
|
||||
return parseInt(result);
|
||||
} else {
|
||||
let result = sourceCharacter.attribs.find((e) => e.name === `${item.name}_lvl`).current;
|
||||
return parseInt(result);
|
||||
}
|
||||
}
|
||||
|
||||
static capitalize(str) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
static async addSpecies(race, actor) {
|
||||
const species = await game.packs.get("sw5e.species").getDocuments();
|
||||
const assignedSpecies = species.find((c) => c.name === race);
|
||||
const activeEffects = [...assignedSpecies.data.effects][0].data.changes;
|
||||
const actorData = {data: {abilities: {...actor.data.data.abilities}}};
|
||||
|
||||
activeEffects.map((effect) => {
|
||||
switch (effect.key) {
|
||||
case "data.abilities.str.value":
|
||||
actorData.data.abilities.str.value -= effect.value;
|
||||
break;
|
||||
|
||||
case "data.abilities.dex.value":
|
||||
actorData.data.abilities.dex.value -= effect.value;
|
||||
break;
|
||||
|
||||
case "data.abilities.con.value":
|
||||
actorData.data.abilities.con.value -= effect.value;
|
||||
break;
|
||||
|
||||
case "data.abilities.int.value":
|
||||
actorData.data.abilities.int.value -= effect.value;
|
||||
break;
|
||||
|
||||
case "data.abilities.wis.value":
|
||||
actorData.data.abilities.wis.value -= effect.value;
|
||||
break;
|
||||
|
||||
case "data.abilities.cha.value":
|
||||
actorData.data.abilities.cha.value -= effect.value;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
actor.update(actorData);
|
||||
|
||||
await actor.createEmbeddedDocuments("Item", [assignedSpecies.data], {displaySheet: false});
|
||||
}
|
||||
|
||||
static async addPowers(powers, actor) {
|
||||
const forcePowers = await game.packs.get("sw5e.forcepowers").getDocuments();
|
||||
const techPowers = await game.packs.get("sw5e.techpowers").getDocuments();
|
||||
|
||||
for (const power of powers) {
|
||||
const createdPower = forcePowers.find((c) => c.name === power) || techPowers.find((c) => c.name === power);
|
||||
|
||||
if (createdPower) {
|
||||
await actor.createEmbeddedDocuments("Item", [createdPower.data], {displaySheet: false});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static async addItems(items, actor) {
|
||||
const weapons = await game.packs.get("sw5e.weapons").getDocuments();
|
||||
const armors = await game.packs.get("sw5e.armor").getDocuments();
|
||||
const adventuringGear = await game.packs.get("sw5e.adventuringgear").getDocuments();
|
||||
|
||||
for (const item of items) {
|
||||
const createdItem =
|
||||
weapons.find((c) => c.name.toLowerCase() === item.name.toLowerCase()) ||
|
||||
armors.find((c) => c.name.toLowerCase() === item.name.toLowerCase()) ||
|
||||
adventuringGear.find((c) => c.name.toLowerCase() === item.name.toLowerCase());
|
||||
|
||||
if (createdItem) {
|
||||
if (item.quantity != 1) {
|
||||
createdItem.data.data.quantity = item.quantity;
|
||||
}
|
||||
|
||||
await actor.createEmbeddedDocuments("Item", [createdItem.data], {displaySheet: false});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static addImportButton(html) {
|
||||
const actionButtons = html.find(".header-actions");
|
||||
actionButtons[0].insertAdjacentHTML(
|
||||
"afterend",
|
||||
`<div class="header-actions action-buttons flexrow"><button class="create-entity cs-import-button"><i class="fas fa-upload"></i> Import Character</button></div>`
|
||||
);
|
||||
|
||||
let characterImportButton = $(".cs-import-button");
|
||||
characterImportButton.click(() => {
|
||||
let content = `<h1>Saved Character JSON Import</h1>
|
||||
<label for="character-json">Paste character JSON here:</label>
|
||||
</br>
|
||||
<textarea id="character-json" name="character-json" rows="10" cols="50"></textarea>`;
|
||||
let importDialog = new Dialog({
|
||||
title: "Import Character from SW5e.com",
|
||||
content: content,
|
||||
buttons: {
|
||||
"Import": {
|
||||
icon: '<i class="fas fa-file-import"></i>',
|
||||
Import: {
|
||||
icon: `<i class="fas fa-file-import"></i>`,
|
||||
label: "Import Character",
|
||||
callback: (e) => {
|
||||
let characterData = $('#character-json').val();
|
||||
console.log('Parsing Character JSON');
|
||||
callback: () => {
|
||||
let characterData = $("#character-json").val();
|
||||
console.log("Parsing Character JSON");
|
||||
CharacterImporter.transform(characterData);
|
||||
}
|
||||
},
|
||||
"Cancel": {
|
||||
icon: '<i class="fas fa-times-circle"></i>',
|
||||
Cancel: {
|
||||
icon: `<i class="fas fa-times-circle"></i>`,
|
||||
label: "Cancel",
|
||||
callback: () => {},
|
||||
callback: () => {}
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
importDialog.render(true);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,28 +1,27 @@
|
|||
|
||||
/**
|
||||
* Highlight critical success or failure on d20 rolls
|
||||
*/
|
||||
export const highlightCriticalSuccessFailure = function(message, html, data) {
|
||||
if ( !message.isRoll || !message.isContentVisible ) return;
|
||||
export const highlightCriticalSuccessFailure = function (message, html, data) {
|
||||
if (!message.isRoll || !message.isContentVisible) return;
|
||||
|
||||
// Highlight rolls where the first part is a d20 roll
|
||||
const roll = message.roll;
|
||||
if ( !roll.dice.length ) return;
|
||||
if (!roll.dice.length) return;
|
||||
const d = roll.dice[0];
|
||||
|
||||
// Ensure it is an un-modified d20 roll
|
||||
const isD20 = (d.faces === 20) && ( d.values.length === 1 );
|
||||
if ( !isD20 ) return;
|
||||
const isModifiedRoll = ("success" in d.results[0]) || d.options.marginSuccess || d.options.marginFailure;
|
||||
if ( isModifiedRoll ) return;
|
||||
const isD20 = d.faces === 20 && d.values.length === 1;
|
||||
if (!isD20) return;
|
||||
const isModifiedRoll = "success" in d.results[0] || d.options.marginSuccess || d.options.marginFailure;
|
||||
if (isModifiedRoll) return;
|
||||
|
||||
// Highlight successes and failures
|
||||
const critical = d.options.critical || 20;
|
||||
const fumble = d.options.fumble || 1;
|
||||
if ( d.total >= critical ) html.find(".dice-total").addClass("critical");
|
||||
else if ( d.total <= fumble ) html.find(".dice-total").addClass("fumble");
|
||||
else if ( d.options.target ) {
|
||||
if ( roll.total >= d.options.target ) html.find(".dice-total").addClass("success");
|
||||
if (d.total >= critical) html.find(".dice-total").addClass("critical");
|
||||
else if (d.total <= fumble) html.find(".dice-total").addClass("fumble");
|
||||
else if (d.options.target) {
|
||||
if (roll.total >= d.options.target) html.find(".dice-total").addClass("success");
|
||||
else html.find(".dice-total").addClass("failure");
|
||||
}
|
||||
};
|
||||
|
@ -32,22 +31,22 @@ export const highlightCriticalSuccessFailure = function(message, html, data) {
|
|||
/**
|
||||
* Optionally hide the display of chat card action buttons which cannot be performed by the user
|
||||
*/
|
||||
export const displayChatActionButtons = function(message, html, data) {
|
||||
export const displayChatActionButtons = function (message, html, data) {
|
||||
const chatCard = html.find(".sw5e.chat-card");
|
||||
if ( chatCard.length > 0 ) {
|
||||
if (chatCard.length > 0) {
|
||||
const flavor = html.find(".flavor-text");
|
||||
if ( flavor.text() === html.find(".item-name").text() ) flavor.remove();
|
||||
if (flavor.text() === html.find(".item-name").text()) flavor.remove();
|
||||
|
||||
// If the user is the message author or the actor owner, proceed
|
||||
let actor = game.actors.get(data.message.speaker.actor);
|
||||
if ( actor && actor.owner ) return;
|
||||
else if ( game.user.isGM || (data.author.id === game.user.id)) return;
|
||||
if (actor && actor.isOwner) return;
|
||||
else if (game.user.isGM || data.author.id === game.user.id) return;
|
||||
|
||||
// Otherwise conceal action buttons except for saving throw
|
||||
const buttons = chatCard.find("button[data-action]");
|
||||
buttons.each((i, btn) => {
|
||||
if ( btn.dataset.action === "save" ) return;
|
||||
btn.style.display = "none"
|
||||
if (btn.dataset.action === "save") return;
|
||||
btn.style.display = "none";
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -63,35 +62,35 @@ export const displayChatActionButtons = function(message, html, data) {
|
|||
*
|
||||
* @return {Array} The extended options Array including new context choices
|
||||
*/
|
||||
export const addChatMessageContextOptions = function(html, options) {
|
||||
let canApply = li => {
|
||||
export const addChatMessageContextOptions = function (html, options) {
|
||||
let canApply = (li) => {
|
||||
const message = game.messages.get(li.data("messageId"));
|
||||
return message?.isRoll && message?.isContentVisible && canvas?.tokens.controlled.length;
|
||||
return message?.isRoll && message?.isContentVisible && canvas.tokens?.controlled.length;
|
||||
};
|
||||
options.push(
|
||||
{
|
||||
name: game.i18n.localize("SW5E.ChatContextDamage"),
|
||||
icon: '<i class="fas fa-user-minus"></i>',
|
||||
condition: canApply,
|
||||
callback: li => applyChatCardDamage(li, 1)
|
||||
callback: (li) => applyChatCardDamage(li, 1)
|
||||
},
|
||||
{
|
||||
name: game.i18n.localize("SW5E.ChatContextHealing"),
|
||||
icon: '<i class="fas fa-user-plus"></i>',
|
||||
condition: canApply,
|
||||
callback: li => applyChatCardDamage(li, -1)
|
||||
callback: (li) => applyChatCardDamage(li, -1)
|
||||
},
|
||||
{
|
||||
name: game.i18n.localize("SW5E.ChatContextDoubleDamage"),
|
||||
icon: '<i class="fas fa-user-injured"></i>',
|
||||
condition: canApply,
|
||||
callback: li => applyChatCardDamage(li, 2)
|
||||
callback: (li) => applyChatCardDamage(li, 2)
|
||||
},
|
||||
{
|
||||
name: game.i18n.localize("SW5E.ChatContextHalfDamage"),
|
||||
icon: '<i class="fas fa-user-shield"></i>',
|
||||
condition: canApply,
|
||||
callback: li => applyChatCardDamage(li, 0.5)
|
||||
callback: (li) => applyChatCardDamage(li, 0.5)
|
||||
}
|
||||
);
|
||||
return options;
|
||||
|
@ -110,10 +109,12 @@ export const addChatMessageContextOptions = function(html, options) {
|
|||
function applyChatCardDamage(li, multiplier) {
|
||||
const message = game.messages.get(li.data("messageId"));
|
||||
const roll = message.roll;
|
||||
return Promise.all(canvas.tokens.controlled.map(t => {
|
||||
return Promise.all(
|
||||
canvas.tokens.controlled.map((t) => {
|
||||
const a = t.actor;
|
||||
return a.applyDamage(roll.total, multiplier);
|
||||
}));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
|
|
@ -1,4 +1 @@
|
|||
export const ClassFeatures = {
|
||||
|
||||
};
|
||||
|
||||
export const ClassFeatures = {};
|
||||
|
|
|
@ -1,28 +1,31 @@
|
|||
|
||||
/**
|
||||
* Override the default Initiative formula to customize special behaviors of the SW5e system.
|
||||
* Apply advantage, proficiency, or bonuses where appropriate
|
||||
* Apply the dexterity score as a decimal tiebreaker if requested
|
||||
* See Combat._getInitiativeFormula for more detail.
|
||||
*/
|
||||
export const _getInitiativeFormula = function(combatant) {
|
||||
const actor = combatant.actor;
|
||||
if ( !actor ) return "1d20";
|
||||
export const _getInitiativeFormula = function () {
|
||||
const actor = this.actor;
|
||||
if (!actor) return "1d20";
|
||||
const init = actor.data.data.attributes.init;
|
||||
|
||||
// Construct initiative formula parts
|
||||
let nd = 1;
|
||||
let mods = "";
|
||||
|
||||
if (actor.getFlag("sw5e", "halflingLucky")) mods += "r1=1";
|
||||
if (actor.getFlag("sw5e", "initiativeAdv")) {
|
||||
nd = 2;
|
||||
mods += "kh";
|
||||
}
|
||||
|
||||
const parts = [`${nd}d20${mods}`, init.mod, (init.prof !== 0) ? init.prof : null, (init.bonus !== 0) ? init.bonus : null];
|
||||
const parts = [
|
||||
`${nd}d20${mods}`,
|
||||
init.mod,
|
||||
init.prof !== 0 ? init.prof : null,
|
||||
init.bonus !== 0 ? init.bonus : null
|
||||
];
|
||||
|
||||
// Optionally apply Dexterity tiebreaker
|
||||
const tiebreaker = game.settings.get("sw5e", "initiativeDexTiebreaker");
|
||||
if ( tiebreaker ) parts.push(actor.data.data.abilities.dex.value / 100);
|
||||
return parts.filter(p => p !== null).join(" + ");
|
||||
if (tiebreaker) parts.push(actor.data.data.abilities.dex.value / 100);
|
||||
return parts.filter((p) => p !== null).join(" + ");
|
||||
};
|
||||
|
|
1418
module/config.js
493
module/dice.js
|
@ -1,3 +1,6 @@
|
|||
export {default as D20Roll} from "./dice/d20-roll.js";
|
||||
export {default as DamageRoll} from "./dice/damage-roll.js";
|
||||
|
||||
/**
|
||||
* A standardized helper function for simplifying the constant parts of a multipart roll formula
|
||||
*
|
||||
|
@ -20,14 +23,19 @@ export function simplifyRollFormula(formula, data, {constantFirst = false} = {})
|
|||
const constantTerms = []; // Terms that are constant, and their associated operators
|
||||
let operators = []; // Temporary storage for operators before they are moved to one of the above
|
||||
|
||||
for (let term of terms) { // For each term
|
||||
if (["+", "-"].includes(term)) operators.push(term); // If the term is an addition/subtraction operator, push the term into the operators array
|
||||
else { // Otherwise the term is not an operator
|
||||
if (term instanceof DiceTerm) { // If the term is something rollable
|
||||
for (let term of terms) {
|
||||
// For each term
|
||||
if (term instanceof OperatorTerm) operators.push(term);
|
||||
// If the term is an addition/subtraction operator, push the term into the operators array
|
||||
else {
|
||||
// Otherwise the term is not an operator
|
||||
if (term instanceof DiceTerm) {
|
||||
// If the term is something rollable
|
||||
rollableTerms.push(...operators); // Place all the operators into the rollableTerms array
|
||||
rollableTerms.push(term); // Then place this rollable term into it as well
|
||||
} //
|
||||
else { // Otherwise, this must be a constant
|
||||
else {
|
||||
// Otherwise, this must be a constant
|
||||
constantTerms.push(...operators); // Place the operators into the constantTerms array
|
||||
constantTerms.push(term); // Then also add this constant term to that array.
|
||||
} //
|
||||
|
@ -35,13 +43,21 @@ export function simplifyRollFormula(formula, data, {constantFirst = false} = {})
|
|||
}
|
||||
}
|
||||
|
||||
const constantFormula = Roll.cleanFormula(constantTerms); // Cleans up the constant terms and produces a new formula string
|
||||
const rollableFormula = Roll.cleanFormula(rollableTerms); // Cleans up the non-constant terms and produces a new formula string
|
||||
const constantFormula = Roll.getFormula(constantTerms); // Cleans up the constant terms and produces a new formula string
|
||||
const rollableFormula = Roll.getFormula(rollableTerms); // Cleans up the non-constant terms and produces a new formula string
|
||||
|
||||
const constantPart = roll._safeEval(constantFormula); // Mathematically evaluate the constant formula to produce a single constant term
|
||||
// Mathematically evaluate the constant formula to produce a single constant term
|
||||
let constantPart = undefined;
|
||||
if (constantFormula) {
|
||||
try {
|
||||
constantPart = Roll.safeEval(constantFormula);
|
||||
} catch (err) {
|
||||
console.warn(`Unable to evaluate constant term ${constantFormula} in simplifyRollFormula`);
|
||||
}
|
||||
}
|
||||
|
||||
const parts = constantFirst ? // Order the rollable and constant terms, either constant first or second depending on the optional argumen
|
||||
[constantPart, rollableFormula] : [rollableFormula, constantPart];
|
||||
// Order the rollable and constant terms, either constant first or second depending on the optional argument
|
||||
const parts = constantFirst ? [constantPart, rollableFormula] : [rollableFormula, constantPart];
|
||||
|
||||
// Join the parts with a + sign, pass them to `Roll` once again to clean up the formula
|
||||
return new Roll(parts.filterJoin(" + ")).formula;
|
||||
|
@ -56,315 +72,242 @@ export function simplifyRollFormula(formula, data, {constantFirst = false} = {})
|
|||
*/
|
||||
function _isUnsupportedTerm(term) {
|
||||
const diceTerm = term instanceof DiceTerm;
|
||||
const operator = ["+", "-"].includes(term);
|
||||
const number = !isNaN(Number(term));
|
||||
const operator = term instanceof OperatorTerm && ["+", "-"].includes(term.operator);
|
||||
const number = term instanceof NumericTerm;
|
||||
|
||||
return !(diceTerm || operator || number);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* D20 Roll */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A standardized helper function for managing core 5e "d20 rolls"
|
||||
*
|
||||
* A standardized helper function for managing core 5e d20 rolls.
|
||||
* Holding SHIFT, ALT, or CTRL when the attack is rolled will "fast-forward".
|
||||
* This chooses the default options of a normal attack with no bonus, Advantage, or Disadvantage respectively
|
||||
*
|
||||
* @param {Array} parts The dice roll component parts, excluding the initial d20
|
||||
* @param {Object} data Actor or item data against which to parse the roll
|
||||
* @param {Event|object} event The triggering event which initiated the roll
|
||||
* @param {string} rollMode A specific roll mode to apply as the default for the resulting roll
|
||||
* @param {string|null} template The HTML template used to render the roll dialog
|
||||
* @param {string|null} title The dice roll UI window title
|
||||
* @param {Object} speaker The ChatMessage speaker to pass when creating the chat
|
||||
* @param {string|null} flavor Flavor text to use in the posted chat message
|
||||
* @param {Boolean} fastForward Allow fast-forward advantage selection
|
||||
* @param {Function} onClose Callback for actions to take when the dialog form is closed
|
||||
* @param {Object} dialogOptions Modal dialog options
|
||||
* @param {boolean} advantage Apply advantage to the roll (unless otherwise specified)
|
||||
* @param {boolean} disadvantage Apply disadvantage to the roll (unless otherwise specified)
|
||||
* @param {number} critical The value of d20 result which represents a critical success
|
||||
* @param {number} fumble The value of d20 result which represents a critical failure
|
||||
* @param {number} targetValue Assign a target value against which the result of this roll should be compared
|
||||
* @param {boolean} elvenAccuracy Allow Elven Accuracy to modify this roll?
|
||||
* @param {boolean} halflingLucky Allow Halfling Luck to modify this roll?
|
||||
* @param {boolean} reliableTalent Allow Reliable Talent to modify this roll?
|
||||
* @param {boolean} chatMessage Automatically create a Chat Message for the result of this roll
|
||||
* @param {object} messageData Additional data which is applied to the created Chat Message, if any
|
||||
* @param {string[]} parts The dice roll component parts, excluding the initial d20
|
||||
* @param {object} data Actor or item data against which to parse the roll
|
||||
*
|
||||
* @return {Promise} A Promise which resolves once the roll workflow has completed
|
||||
* @param {boolean} [advantage] Apply advantage to the roll (unless otherwise specified)
|
||||
* @param {boolean} [disadvantage] Apply disadvantage to the roll (unless otherwise specified)
|
||||
* @param {number} [critical] The value of d20 result which represents a critical success
|
||||
* @param {number} [fumble] The value of d20 result which represents a critical failure
|
||||
* @param {number} [targetValue] Assign a target value against which the result of this roll should be compared
|
||||
* @param {boolean} [elvenAccuracy] Allow Elven Accuracy to modify this roll?
|
||||
* @param {boolean} [halflingLucky] Allow Halfling Luck to modify this roll?
|
||||
* @param {boolean} [reliableTalent] Allow Reliable Talent to modify this roll?
|
||||
|
||||
* @param {boolean} [chooseModifier=false] Choose the ability modifier that should be used when the roll is made
|
||||
* @param {boolean} [fastForward=false] Allow fast-forward advantage selection
|
||||
* @param {Event} [event] The triggering event which initiated the roll
|
||||
* @param {string} [rollMode] A specific roll mode to apply as the default for the resulting roll
|
||||
* @param {string} [template] The HTML template used to render the roll dialog
|
||||
* @param {string} [title] The dialog window title
|
||||
* @param {Object} [dialogOptions] Modal dialog options
|
||||
*
|
||||
* @param {boolean} [chatMessage=true] Automatically create a Chat Message for the result of this roll
|
||||
* @param {object} [messageData={}] Additional data which is applied to the created Chat Message, if any
|
||||
* @param {string} [rollMode] A specific roll mode to apply as the default for the resulting roll
|
||||
* @param {object} [speaker] The ChatMessage speaker to pass when creating the chat
|
||||
* @param {string} [flavor] Flavor text to use in the posted chat message
|
||||
*
|
||||
* @return {Promise<D20Roll|null>} The evaluated D20Roll, or null if the workflow was cancelled
|
||||
*/
|
||||
export async function d20Roll({parts=[], data={}, event={}, rollMode=null, template=null, title=null, speaker=null,
|
||||
flavor=null, fastForward=null, dialogOptions,
|
||||
advantage=null, disadvantage=null, critical=20, fumble=1, targetValue=null,
|
||||
elvenAccuracy=false, halflingLucky=false, reliableTalent=false,
|
||||
chatMessage=true, messageData={}}={}) {
|
||||
export async function d20Roll({
|
||||
parts = [],
|
||||
data = {}, // Roll creation
|
||||
advantage,
|
||||
disadvantage,
|
||||
fumble = 1,
|
||||
critical = 20,
|
||||
targetValue,
|
||||
elvenAccuracy,
|
||||
halflingLucky,
|
||||
reliableTalent, // Roll customization
|
||||
chooseModifier = false,
|
||||
fastForward = false,
|
||||
event,
|
||||
template,
|
||||
title,
|
||||
dialogOptions, // Dialog configuration
|
||||
chatMessage = true,
|
||||
messageData = {},
|
||||
rollMode,
|
||||
speaker,
|
||||
flavor // Chat Message customization
|
||||
} = {}) {
|
||||
// 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";
|
||||
|
||||
// Prepare Message Data
|
||||
messageData.flavor = flavor || title;
|
||||
messageData.speaker = speaker || ChatMessage.getSpeaker();
|
||||
const messageOptions = {rollMode: rollMode || game.settings.get("core", "rollMode")};
|
||||
parts = parts.concat(["@bonus"]);
|
||||
// Construct the D20Roll instance
|
||||
const roll = new CONFIG.Dice.D20Roll(formula, data, {
|
||||
flavor: flavor || title,
|
||||
advantageMode,
|
||||
defaultRollMode,
|
||||
critical,
|
||||
fumble,
|
||||
targetValue,
|
||||
elvenAccuracy,
|
||||
halflingLucky,
|
||||
reliableTalent
|
||||
});
|
||||
|
||||
// Handle fast-forward events
|
||||
let adv = 0;
|
||||
fastForward = fastForward ?? (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
|
||||
if (fastForward) {
|
||||
if ( advantage ?? event.altKey ) adv = 1;
|
||||
else if ( disadvantage ?? (event.ctrlKey || event.metaKey) ) adv = -1;
|
||||
// Prompt a Dialog to further configure the D20Roll
|
||||
if (!isFF) {
|
||||
const configured = await roll.configureDialog(
|
||||
{
|
||||
title,
|
||||
chooseModifier,
|
||||
defaultRollMode: defaultRollMode,
|
||||
defaultAction: advantageMode,
|
||||
defaultAbility: data?.item?.ability,
|
||||
template
|
||||
},
|
||||
dialogOptions
|
||||
);
|
||||
if (configured === null) return null;
|
||||
}
|
||||
|
||||
// Define the inner roll function
|
||||
const _roll = (parts, adv, form) => {
|
||||
|
||||
// Determine the d20 roll and modifiers
|
||||
let nd = 1;
|
||||
let mods = halflingLucky ? "r1=1" : "";
|
||||
|
||||
// Handle advantage
|
||||
if (adv === 1) {
|
||||
nd = elvenAccuracy ? 3 : 2;
|
||||
messageData.flavor += ` (${game.i18n.localize("SW5E.Advantage")})`;
|
||||
if ( "flags.sw5e.roll" in messageData ) messageData["flags.sw5e.roll"].advantage = true;
|
||||
mods += "kh";
|
||||
}
|
||||
|
||||
// Handle disadvantage
|
||||
else if (adv === -1) {
|
||||
nd = 2;
|
||||
messageData.flavor += ` (${game.i18n.localize("SW5E.Disadvantage")})`;
|
||||
if ( "flags.sw5e.roll" in messageData ) messageData["flags.sw5e.roll"].disadvantage = true;
|
||||
mods += "kl";
|
||||
}
|
||||
|
||||
// Prepend the d20 roll
|
||||
let formula = `${nd}d20${mods}`;
|
||||
if (reliableTalent) formula = `{${nd}d20${mods},10}kh`;
|
||||
parts.unshift(formula);
|
||||
|
||||
// Optionally include a situational bonus
|
||||
if ( form ) {
|
||||
data['bonus'] = form.bonus.value;
|
||||
messageOptions.rollMode = form.rollMode.value;
|
||||
}
|
||||
if (!data["bonus"]) parts.pop();
|
||||
|
||||
// Optionally include an ability score selection (used for tool checks)
|
||||
const ability = form ? form.ability : null;
|
||||
if (ability && ability.value) {
|
||||
data.ability = ability.value;
|
||||
const abl = data.abilities[data.ability];
|
||||
if (abl) {
|
||||
data.mod = abl.mod;
|
||||
messageData.flavor += ` (${CONFIG.SW5E.abilities[data.ability]})`;
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the roll
|
||||
let roll = new Roll(parts.join(" + "), data);
|
||||
try {
|
||||
roll.roll();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
ui.notifications.error(`Dice roll evaluation failed: ${err.message}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Flag d20 options for any 20-sided dice in the roll
|
||||
for (let d of roll.dice) {
|
||||
if (d.faces === 20) {
|
||||
d.options.critical = critical;
|
||||
d.options.fumble = fumble;
|
||||
if ( adv === 1 ) d.options.advantage = true;
|
||||
else if ( adv === -1 ) d.options.disadvantage = true;
|
||||
if (targetValue) d.options.target = targetValue;
|
||||
}
|
||||
}
|
||||
|
||||
// If reliable talent was applied, add it to the flavor text
|
||||
if (reliableTalent && roll.dice[0].total < 10) {
|
||||
messageData.flavor += ` (${game.i18n.localize("SW5E.FlagsReliableTalent")})`;
|
||||
}
|
||||
return roll;
|
||||
};
|
||||
|
||||
// Create the Roll instance
|
||||
const roll = fastForward ? _roll(parts, adv) :
|
||||
await _d20RollDialog({template, title, parts, data, rollMode: messageOptions.rollMode, dialogOptions, roll: _roll});
|
||||
// Evaluate the configured roll
|
||||
await roll.evaluate({async: true});
|
||||
|
||||
// Create a Chat Message
|
||||
if ( roll && chatMessage ) roll.toMessage(messageData, messageOptions);
|
||||
if (speaker) {
|
||||
console.warn(
|
||||
`You are passing the speaker argument to the d20Roll function directly which should instead be passed as an internal key of messageData`
|
||||
);
|
||||
messageData.speaker = speaker;
|
||||
}
|
||||
if (roll && chatMessage) await roll.toMessage(messageData);
|
||||
return roll;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Present a Dialog form which creates a d20 roll once submitted
|
||||
* @return {Promise<Roll>}
|
||||
* @private
|
||||
* Determines whether this d20 roll should be fast-forwarded, and whether advantage or disadvantage should be applied
|
||||
* @returns {{isFF: boolean, advantageMode: number}} Whether the roll is fast-forward, and its advantage mode
|
||||
*/
|
||||
async function _d20RollDialog({template, title, parts, data, rollMode, dialogOptions, roll}={}) {
|
||||
|
||||
// Render modal dialog
|
||||
template = template || "systems/sw5e/templates/chat/roll-dialog.html";
|
||||
let dialogData = {
|
||||
formula: parts.join(" + "),
|
||||
data: data,
|
||||
rollMode: rollMode,
|
||||
rollModes: CONFIG.Dice.rollModes,
|
||||
config: CONFIG.SW5E
|
||||
};
|
||||
const html = await renderTemplate(template, dialogData);
|
||||
|
||||
// Create the Dialog window
|
||||
return new Promise(resolve => {
|
||||
new Dialog({
|
||||
title: title,
|
||||
content: html,
|
||||
buttons: {
|
||||
advantage: {
|
||||
label: game.i18n.localize("SW5E.Advantage"),
|
||||
callback: html => resolve(roll(parts, 1, html[0].querySelector("form")))
|
||||
},
|
||||
normal: {
|
||||
label: game.i18n.localize("SW5E.Normal"),
|
||||
callback: html => resolve(roll(parts, 0, html[0].querySelector("form")))
|
||||
},
|
||||
disadvantage: {
|
||||
label: game.i18n.localize("SW5E.Disadvantage"),
|
||||
callback: html => resolve(roll(parts, -1, html[0].querySelector("form")))
|
||||
}
|
||||
},
|
||||
default: "normal",
|
||||
close: () => resolve(null)
|
||||
}, dialogOptions).render(true);
|
||||
});
|
||||
function _determineAdvantageMode({event, advantage = false, disadvantage = false, fastForward = false} = {}) {
|
||||
const isFF = fastForward || (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
|
||||
let advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.NORMAL;
|
||||
if (advantage || event?.altKey) advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.ADVANTAGE;
|
||||
else if (disadvantage || event?.ctrlKey || event?.metaKey)
|
||||
advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.DISADVANTAGE;
|
||||
return {isFF, advantageMode};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Damage Roll */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A standardized helper function for managing core 5e "d20 rolls"
|
||||
* A standardized helper function for managing core 5e damage rolls.
|
||||
*
|
||||
* Holding SHIFT, ALT, or CTRL when the attack is rolled will "fast-forward".
|
||||
* This chooses the default options of a normal attack with no bonus, Critical, or no bonus respectively
|
||||
*
|
||||
* @param {Array} parts The dice roll component parts, excluding the initial d20
|
||||
* @param {Actor} actor The Actor making the damage roll
|
||||
* @param {Object} data Actor or item data against which to parse the roll
|
||||
* @param {Event|object}[event The triggering event which initiated the roll
|
||||
* @param {string} rollMode A specific roll mode to apply as the default for the resulting roll
|
||||
* @param {String} template The HTML template used to render the roll dialog
|
||||
* @param {String} title The dice roll UI window title
|
||||
* @param {Object} speaker The ChatMessage speaker to pass when creating the chat
|
||||
* @param {string} flavor Flavor text to use in the posted chat message
|
||||
* @param {boolean} allowCritical Allow the opportunity for a critical hit to be rolled
|
||||
* @param {Boolean} critical Flag this roll as a critical hit for the purposes of fast-forward rolls
|
||||
* @param {number} criticalBonusDice A number of bonus damage dice that are added for critical hits
|
||||
* @param {number} criticalMultiplier A critical hit multiplier which is applied to critical hits
|
||||
* @param {Boolean} fastForward Allow fast-forward advantage selection
|
||||
* @param {Function} onClose Callback for actions to take when the dialog form is closed
|
||||
* @param {Object} dialogOptions Modal dialog options
|
||||
* @param {boolean} chatMessage Automatically create a Chat Message for the result of this roll
|
||||
* @param {object} messageData Additional data which is applied to the created Chat Message, if any
|
||||
* @param {string[]} parts The dice roll component parts, excluding the initial d20
|
||||
* @param {object} [data] Actor or item data against which to parse the roll
|
||||
*
|
||||
* @return {Promise} A Promise which resolves once the roll workflow has completed
|
||||
* @param {boolean} [critical=false] Flag this roll as a critical hit for the purposes of fast-forward or default dialog action
|
||||
* @param {number} [criticalBonusDice=0] A number of bonus damage dice that are added for critical hits
|
||||
* @param {number} [criticalMultiplier=2] A critical hit multiplier which is applied to critical hits
|
||||
* @param {boolean} [multiplyNumeric=false] Multiply numeric terms by the critical multiplier
|
||||
* @param {boolean} [powerfulCritical=false] Apply the "powerful criticals" house rule to critical hits
|
||||
|
||||
* @param {boolean} [fastForward=false] Allow fast-forward advantage selection
|
||||
* @param {Event}[event] The triggering event which initiated the roll
|
||||
* @param {boolean} [allowCritical=true] Allow the opportunity for a critical hit to be rolled
|
||||
* @param {string} [template] The HTML template used to render the roll dialog
|
||||
* @param {string} [title] The dice roll UI window title
|
||||
* @param {object} [dialogOptions] Configuration dialog options
|
||||
*
|
||||
* @param {boolean} [chatMessage=true] Automatically create a Chat Message for the result of this roll
|
||||
* @param {object} [messageData={}] Additional data which is applied to the created Chat Message, if any
|
||||
* @param {string} [rollMode] A specific roll mode to apply as the default for the resulting roll
|
||||
* @param {object} [speaker] The ChatMessage speaker to pass when creating the chat
|
||||
* @param {string} [flavor] Flavor text to use in the posted chat message
|
||||
*
|
||||
* @return {Promise<DamageRoll|null>} The evaluated DamageRoll, or null if the workflow was canceled
|
||||
*/
|
||||
export async function damageRoll({parts, actor, data, event={}, rollMode=null, template, title, speaker, flavor,
|
||||
allowCritical=true, critical=false, criticalBonusDice=0, criticalMultiplier=2, fastForward=null,
|
||||
dialogOptions={}, chatMessage=true, messageData={}}={}) {
|
||||
export async function damageRoll({
|
||||
parts = [],
|
||||
data, // Roll creation
|
||||
critical = false,
|
||||
criticalBonusDice,
|
||||
criticalMultiplier,
|
||||
multiplyNumeric,
|
||||
powerfulCritical, // Damage customization
|
||||
fastForward = false,
|
||||
event,
|
||||
allowCritical = true,
|
||||
template,
|
||||
title,
|
||||
dialogOptions, // Dialog configuration
|
||||
chatMessage = true,
|
||||
messageData = {},
|
||||
rollMode,
|
||||
speaker,
|
||||
flavor // Chat Message customization
|
||||
} = {}) {
|
||||
// Handle input arguments
|
||||
const defaultRollMode = rollMode || game.settings.get("core", "rollMode");
|
||||
|
||||
// Prepare Message Data
|
||||
messageData.flavor = flavor || title;
|
||||
messageData.speaker = speaker || ChatMessage.getSpeaker();
|
||||
const messageOptions = {rollMode: rollMode || game.settings.get("core", "rollMode")};
|
||||
parts = parts.concat(["@bonus"]);
|
||||
|
||||
// Define inner roll function
|
||||
const _roll = function(parts, crit, form) {
|
||||
|
||||
// Optionally include a situational bonus
|
||||
if ( form ) {
|
||||
data['bonus'] = form.bonus.value;
|
||||
messageOptions.rollMode = form.rollMode.value;
|
||||
}
|
||||
if (!data["bonus"]) parts.pop();
|
||||
|
||||
// Create the damage roll
|
||||
let roll = new Roll(parts.join("+"), data);
|
||||
|
||||
// Modify the damage formula for critical hits
|
||||
if ( crit === true ) {
|
||||
roll.alter(criticalMultiplier, 0); // Multiply all dice
|
||||
if ( roll.terms[0] instanceof Die ) { // Add bonus dice for only the main dice term
|
||||
roll.terms[0].alter(1, criticalBonusDice);
|
||||
roll._formula = roll.formula;
|
||||
}
|
||||
messageData.flavor += ` (${game.i18n.localize("SW5E.Critical")})`;
|
||||
if ( "flags.sw5e.roll" in messageData ) messageData["flags.sw5e.roll"].critical = true;
|
||||
}
|
||||
|
||||
// Execute the roll
|
||||
try {
|
||||
roll.evaluate()
|
||||
if ( crit ) roll.dice.forEach(d => d.options.critical = true); // TODO workaround core bug which wipes Roll#options on roll
|
||||
return roll;
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
ui.notifications.error(`Dice roll evaluation failed: ${err.message}`);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Create the Roll instance
|
||||
const roll = fastForward ? _roll(parts, critical) : await _damageRollDialog({
|
||||
template, title, parts, data, allowCritical, rollMode: messageOptions.rollMode, dialogOptions, roll: _roll
|
||||
// Construct the DamageRoll instance
|
||||
const formula = parts.join(" + ");
|
||||
const {isCritical, isFF} = _determineCriticalMode({critical, fastForward, event});
|
||||
const roll = new CONFIG.Dice.DamageRoll(formula, data, {
|
||||
flavor: flavor || title,
|
||||
critical: isCritical,
|
||||
criticalBonusDice,
|
||||
criticalMultiplier,
|
||||
multiplyNumeric,
|
||||
powerfulCritical
|
||||
});
|
||||
|
||||
// Create a Chat Message
|
||||
if ( roll && chatMessage ) roll.toMessage(messageData, messageOptions);
|
||||
return roll;
|
||||
// Prompt a Dialog to further configure the DamageRoll
|
||||
if (!isFF) {
|
||||
const configured = await roll.configureDialog(
|
||||
{
|
||||
title,
|
||||
defaultRollMode: defaultRollMode,
|
||||
defaultCritical: isCritical,
|
||||
template,
|
||||
allowCritical
|
||||
},
|
||||
dialogOptions
|
||||
);
|
||||
if (configured === null) return null;
|
||||
}
|
||||
|
||||
// Evaluate the configured roll
|
||||
await roll.evaluate({async: true});
|
||||
|
||||
// Create a Chat Message
|
||||
if (speaker) {
|
||||
console.warn(
|
||||
`You are passing the speaker argument to the damageRoll function directly which should instead be passed as an internal key of messageData`
|
||||
);
|
||||
messageData.speaker = speaker;
|
||||
}
|
||||
if (roll && chatMessage) await roll.toMessage(messageData);
|
||||
return roll;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Present a Dialog form which creates a damage roll once submitted
|
||||
* @return {Promise<Roll>}
|
||||
* @private
|
||||
* Determines whether this d20 roll should be fast-forwarded, and whether advantage or disadvantage should be applied
|
||||
* @returns {{isFF: boolean, isCritical: boolean}} Whether the roll is fast-forward, and whether it is a critical hit
|
||||
*/
|
||||
async function _damageRollDialog({template, title, parts, data, allowCritical, rollMode, dialogOptions, roll}={}) {
|
||||
|
||||
// Render modal dialog
|
||||
template = template || "systems/sw5e/templates/chat/roll-dialog.html";
|
||||
let dialogData = {
|
||||
formula: parts.join(" + "),
|
||||
data: data,
|
||||
rollMode: rollMode,
|
||||
rollModes: CONFIG.Dice.rollModes
|
||||
};
|
||||
const html = await renderTemplate(template, dialogData);
|
||||
|
||||
// Create the Dialog window
|
||||
return new Promise(resolve => {
|
||||
new Dialog({
|
||||
title: title,
|
||||
content: html,
|
||||
buttons: {
|
||||
critical: {
|
||||
condition: allowCritical,
|
||||
label: game.i18n.localize("SW5E.CriticalHit"),
|
||||
callback: html => resolve(roll(parts, true, html[0].querySelector("form")))
|
||||
},
|
||||
normal: {
|
||||
label: game.i18n.localize(allowCritical ? "SW5E.Normal" : "SW5E.Roll"),
|
||||
callback: html => resolve(roll(parts, false, html[0].querySelector("form")))
|
||||
},
|
||||
},
|
||||
default: "normal",
|
||||
close: () => resolve(null)
|
||||
}, dialogOptions).render(true);
|
||||
});
|
||||
function _determineCriticalMode({event, critical = false, fastForward = false} = {}) {
|
||||
const isFF = fastForward || (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
|
||||
if (event?.altKey) critical = true;
|
||||
return {isFF, isCritical: critical};
|
||||
}
|
||||
|
|
230
module/dice/d20-roll.js
Normal file
|
@ -0,0 +1,230 @@
|
|||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
186
module/dice/damage-roll.js
Normal file
|
@ -0,0 +1,186 @@
|
|||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
15
module/dice/roll-dialog.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
/**
|
||||
* @deprecated since 1.3.0
|
||||
* @ignore
|
||||
*/
|
||||
async function d20Dialog(data, options) {
|
||||
throw new Error(`The d20Dialog helper method is deprecated in favor of D20Roll#configureDialog`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since 1.3.0
|
||||
* @ignore
|
||||
*/
|
||||
async function damageDialog(data, options) {
|
||||
throw new Error(`The damageDialog helper method is deprecated in favor of DamageRoll#configureDialog`);
|
||||
}
|
29
module/effects.js
vendored
|
@ -8,15 +8,17 @@ export function onManageActiveEffect(event, owner) {
|
|||
const a = event.currentTarget;
|
||||
const li = a.closest("li");
|
||||
const effect = li.dataset.effectId ? owner.effects.get(li.dataset.effectId) : null;
|
||||
switch ( a.dataset.action ) {
|
||||
switch (a.dataset.action) {
|
||||
case "create":
|
||||
return ActiveEffect.create({
|
||||
label: "New Effect",
|
||||
icon: "icons/svg/aura.svg",
|
||||
origin: owner.uuid,
|
||||
return owner.createEmbeddedDocuments("ActiveEffect", [
|
||||
{
|
||||
"label": game.i18n.localize("SW5E.EffectNew"),
|
||||
"icon": "icons/svg/aura.svg",
|
||||
"origin": owner.uuid,
|
||||
"duration.rounds": li.dataset.effectType === "temporary" ? 1 : undefined,
|
||||
disabled: li.dataset.effectType === "inactive"
|
||||
}, owner).create();
|
||||
"disabled": li.dataset.effectType === "inactive"
|
||||
}
|
||||
]);
|
||||
case "edit":
|
||||
return effect.sheet.render(true);
|
||||
case "delete":
|
||||
|
@ -32,31 +34,30 @@ export function onManageActiveEffect(event, owner) {
|
|||
* @return {object} Data for rendering
|
||||
*/
|
||||
export function prepareActiveEffectCategories(effects) {
|
||||
|
||||
// Define effect header categories
|
||||
const categories = {
|
||||
temporary: {
|
||||
type: "temporary",
|
||||
label: "SW5E.EffectsCategoryTemporary",
|
||||
label: game.i18n.localize("SW5E.EffectTemporary"),
|
||||
effects: []
|
||||
},
|
||||
passive: {
|
||||
type: "passive",
|
||||
label: "SW5E.EffectsCategoryPassive",
|
||||
label: game.i18n.localize("SW5E.EffectPassive"),
|
||||
effects: []
|
||||
},
|
||||
inactive: {
|
||||
type: "inactive",
|
||||
label: "SW5E.EffectsCategoryInactive",
|
||||
label: game.i18n.localize("SW5E.EffectInactive"),
|
||||
effects: []
|
||||
}
|
||||
};
|
||||
|
||||
// Iterate over active effects, classifying them into categories
|
||||
for ( let e of effects ) {
|
||||
for (let e of effects) {
|
||||
e._getSourceName(); // Trigger a lookup for the source name
|
||||
if ( e.data.disabled ) categories.inactive.effects.push(e);
|
||||
else if ( e.isTemporary ) categories.temporary.effects.push(e);
|
||||
if (e.data.disabled) categories.inactive.effects.push(e);
|
||||
else if (e.isTemporary) categories.temporary.effects.push(e);
|
||||
else categories.passive.effects.push(e);
|
||||
}
|
||||
return categories;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import TraitSelector from "../apps/trait-selector.js";
|
||||
import { onManageActiveEffect, prepareActiveEffectCategories } from "../effects.js";
|
||||
import {onManageActiveEffect, prepareActiveEffectCategories} from "../effects.js";
|
||||
|
||||
/**
|
||||
* Override and extend the core ItemSheet implementation to handle specific item types
|
||||
|
@ -18,21 +18,21 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
/** @inheritdoc */
|
||||
static get defaultOptions() {
|
||||
return mergeObject(super.defaultOptions, {
|
||||
return foundry.utils.mergeObject(super.defaultOptions, {
|
||||
width: 560,
|
||||
height: 400,
|
||||
classes: ["sw5e", "sheet", "item"],
|
||||
resizable: true,
|
||||
scrollY: [".tab.details"],
|
||||
tabs: [{ navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description" }]
|
||||
tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}]
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
/** @inheritdoc */
|
||||
get template() {
|
||||
const path = "systems/sw5e/templates/items/";
|
||||
return `${path}/${this.item.data.type}.html`;
|
||||
|
@ -43,33 +43,39 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
/** @override */
|
||||
async getData(options) {
|
||||
const data = super.getData(options);
|
||||
const itemData = data.data;
|
||||
data.labels = this.item.labels;
|
||||
data.config = CONFIG.SW5E;
|
||||
|
||||
// Item Type, Status, and Details
|
||||
data.itemType = game.i18n.localize(`ITEM.Type${data.item.type.titleCase()}`);
|
||||
data.itemStatus = this._getItemStatus(data.item);
|
||||
data.itemProperties = this._getItemProperties(data.item);
|
||||
data.isPhysical = data.item.data.hasOwnProperty("quantity");
|
||||
data.itemStatus = this._getItemStatus(itemData);
|
||||
data.itemProperties = this._getItemProperties(itemData);
|
||||
data.isPhysical = itemData.data.hasOwnProperty("quantity");
|
||||
|
||||
// Potential consumption targets
|
||||
data.abilityConsumptionTargets = this._getItemConsumptionTargets(data.item);
|
||||
data.abilityConsumptionTargets = this._getItemConsumptionTargets(itemData);
|
||||
|
||||
// Action Detail
|
||||
// Action Details
|
||||
data.hasAttackRoll = this.item.hasAttack;
|
||||
data.isHealing = data.item.data.actionType === "heal";
|
||||
data.isFlatDC = getProperty(data.item.data, "save.scaling") === "flat";
|
||||
data.isLine = ["line", "wall"].includes(data.item.data.target?.type);
|
||||
data.isHealing = itemData.data.actionType === "heal";
|
||||
data.isFlatDC = getProperty(itemData, "data.save.scaling") === "flat";
|
||||
data.isLine = ["line", "wall"].includes(itemData.data.target?.type);
|
||||
|
||||
// Original maximum uses formula
|
||||
if (this.item._data.data?.uses?.max) data.data.uses.max = this.item._data.data.uses.max;
|
||||
const sourceMax = foundry.utils.getProperty(this.item.data._source, "data.uses.max");
|
||||
if (sourceMax) itemData.data.uses.max = sourceMax;
|
||||
|
||||
// Vehicles
|
||||
data.isCrewed = data.item.data.activation?.type === "crew";
|
||||
data.isMountable = this._isItemMountable(data.item);
|
||||
data.isCrewed = itemData.data.activation?.type === "crew";
|
||||
data.isMountable = this._isItemMountable(itemData);
|
||||
|
||||
// Prepare Active Effects
|
||||
data.effects = prepareActiveEffectCategories(this.entity.effects);
|
||||
data.effects = prepareActiveEffectCategories(this.item.effects);
|
||||
|
||||
// Re-define the template data references (backwards compatible)
|
||||
data.item = itemData;
|
||||
data.data = itemData.data;
|
||||
return data;
|
||||
}
|
||||
|
||||
|
@ -96,15 +102,17 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
}
|
||||
return ammo;
|
||||
},
|
||||
{ [item._id]: `${item.name} (${item.data.quantity})` }
|
||||
{[item._id]: `${item.name} (${item.data.quantity})`}
|
||||
);
|
||||
}
|
||||
|
||||
// Attributes
|
||||
else if (consume.type === "attribute") {
|
||||
const attributes = Object.values(CombatTrackerConfig.prototype.getAttributeChoices())[0]; // Bit of a hack
|
||||
return attributes.reduce((obj, a) => {
|
||||
obj[a] = a;
|
||||
const attributes = TokenDocument.getTrackedAttributes(actor.data.data);
|
||||
attributes.bar.forEach((a) => a.push("value"));
|
||||
return attributes.bar.concat(attributes.value).reduce((obj, a) => {
|
||||
let k = a.join(".");
|
||||
obj[k] = k;
|
||||
return obj;
|
||||
}, {});
|
||||
}
|
||||
|
@ -127,8 +135,11 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
if (uses.per && uses.max) {
|
||||
const label =
|
||||
uses.per === "charges"
|
||||
? ` (${game.i18n.format("SW5E.AbilityUseChargesLabel", { value: uses.value })})`
|
||||
: ` (${game.i18n.format("SW5E.AbilityUseConsumableLabel", { max: uses.max, per: uses.per })})`;
|
||||
? ` (${game.i18n.format("SW5E.AbilityUseChargesLabel", {value: uses.value})})`
|
||||
: ` (${game.i18n.format("SW5E.AbilityUseConsumableLabel", {
|
||||
max: uses.max,
|
||||
per: uses.per
|
||||
})})`;
|
||||
obj[i.id] = i.name + label;
|
||||
}
|
||||
|
||||
|
@ -176,7 +187,6 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
);
|
||||
} else if (item.type === "power") {
|
||||
props.push(
|
||||
labels.components,
|
||||
labels.materials,
|
||||
item.data.components.concentration ? game.i18n.localize("SW5E.Concentration") : null,
|
||||
item.data.components.ritual ? game.i18n.localize("SW5E.Ritual") : null
|
||||
|
@ -186,6 +196,7 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
props.push(labels.armor);
|
||||
} else if (item.type === "feat") {
|
||||
props.push(labels.featType);
|
||||
//TODO: Work out these
|
||||
} else if (item.type === "species") {
|
||||
//props.push(labels.species);
|
||||
} else if (item.type === "archetype") {
|
||||
|
@ -194,6 +205,10 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
//props.push(labels.background);
|
||||
} else if (item.type === "classfeature") {
|
||||
//props.push(labels.classfeature);
|
||||
} else if (item.type === "deployment") {
|
||||
//props.push(labels.deployment);
|
||||
} else if (item.type === "venture") {
|
||||
//props.push(labels.venture);
|
||||
} else if (item.type === "fightingmastery") {
|
||||
//props.push(labels.fightingmastery);
|
||||
} else if (item.type === "fightingstyle") {
|
||||
|
@ -234,7 +249,7 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
/** @inheritdoc */
|
||||
setPosition(position = {}) {
|
||||
if (!(this._minimized || position.height)) {
|
||||
position.height = this._tabs[0].active === "details" ? "auto" : this.options.height;
|
||||
|
@ -246,10 +261,10 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
/* Form Submission */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
/** @inheritdoc */
|
||||
_getSubmitData(updateData = {}) {
|
||||
// Create the expanded update data object
|
||||
const fd = new FormDataExtended(this.form, { editors: this.editors });
|
||||
const fd = new FormDataExtended(this.form, {editors: this.editors});
|
||||
let data = fd.toObject();
|
||||
if (updateData) data = mergeObject(data, updateData);
|
||||
else data = expandObject(data);
|
||||
|
@ -264,12 +279,12 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
/** @inheritdoc */
|
||||
activateListeners(html) {
|
||||
super.activateListeners(html);
|
||||
if (this.isEditable) {
|
||||
html.find(".damage-control").click(this._onDamageControl.bind(this));
|
||||
html.find(".trait-selector.class-skills").click(this._onConfigureClassSkills.bind(this));
|
||||
html.find(".trait-selector.class-skills").click(this._onConfigureTraits.bind(this));
|
||||
html.find(".effect-control").click((ev) => {
|
||||
if (this.item.isOwned)
|
||||
return ui.notifications.warn(
|
||||
|
@ -296,49 +311,58 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
if (a.classList.contains("add-damage")) {
|
||||
await this._onSubmit(event); // Submit any unsaved changes
|
||||
const damage = this.item.data.data.damage;
|
||||
return this.item.update({ "data.damage.parts": damage.parts.concat([["", ""]]) });
|
||||
return this.item.update({"data.damage.parts": damage.parts.concat([["", ""]])});
|
||||
}
|
||||
|
||||
// Remove a damage component
|
||||
if (a.classList.contains("delete-damage")) {
|
||||
await this._onSubmit(event); // Submit any unsaved changes
|
||||
const li = a.closest(".damage-part");
|
||||
const damage = duplicate(this.item.data.data.damage);
|
||||
const damage = foundry.utils.deepClone(this.item.data.data.damage);
|
||||
damage.parts.splice(Number(li.dataset.damagePart), 1);
|
||||
return this.item.update({ "data.damage.parts": damage.parts });
|
||||
return this.item.update({"data.damage.parts": damage.parts});
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle spawning the TraitSelector application which allows a checkbox of multiple trait options
|
||||
* Handle spawning the TraitSelector application for selection various options.
|
||||
* @param {Event} event The click event which originated the selection
|
||||
* @private
|
||||
*/
|
||||
_onConfigureClassSkills(event) {
|
||||
_onConfigureTraits(event) {
|
||||
event.preventDefault();
|
||||
const skills = this.item.data.data.skills;
|
||||
const choices = skills.choices && skills.choices.length ? skills.choices : Object.keys(CONFIG.SW5E.skills);
|
||||
const a = event.currentTarget;
|
||||
const label = a.parentElement;
|
||||
|
||||
// Render the Trait Selector dialog
|
||||
new TraitSelector(this.item, {
|
||||
const options = {
|
||||
name: a.dataset.target,
|
||||
title: label.innerText,
|
||||
choices: Object.entries(CONFIG.SW5E.skills).reduce((obj, e) => {
|
||||
if (choices.includes(e[0])) obj[e[0]] = e[1];
|
||||
return obj;
|
||||
}, {}),
|
||||
minimum: skills.number,
|
||||
maximum: skills.number
|
||||
}).render(true);
|
||||
title: a.parentElement.innerText,
|
||||
choices: [],
|
||||
allowCustom: false
|
||||
};
|
||||
|
||||
switch (a.dataset.options) {
|
||||
case "saves":
|
||||
options.choices = CONFIG.SW5E.abilities;
|
||||
options.valueKey = null;
|
||||
break;
|
||||
case "skills":
|
||||
const skills = this.item.data.data.skills;
|
||||
const choiceSet =
|
||||
skills.choices && skills.choices.length ? skills.choices : Object.keys(CONFIG.SW5E.skills);
|
||||
options.choices = Object.fromEntries(
|
||||
Object.entries(CONFIG.SW5E.skills).filter((skill) => choiceSet.includes(skill[0]))
|
||||
);
|
||||
options.maximum = skills.number;
|
||||
break;
|
||||
}
|
||||
new TraitSelector(this.item, options).render(true);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
/** @inheritdoc */
|
||||
async _onSubmit(...args) {
|
||||
if (this._tabs[0].active === "details") this.position.height = "auto";
|
||||
await super._onSubmit(...args);
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
/* -------------------------------------------- */
|
||||
/* Hotbar Macros */
|
||||
/* -------------------------------------------- */
|
||||
|
@ -11,14 +10,14 @@
|
|||
* @returns {Promise}
|
||||
*/
|
||||
export async function create5eMacro(data, slot) {
|
||||
if ( data.type !== "Item" ) return;
|
||||
if (!( "data" in data ) ) return ui.notifications.warn("You can only create macro buttons for owned Items");
|
||||
if (data.type !== "Item") return;
|
||||
if (!("data" in data)) return ui.notifications.warn("You can only create macro buttons for owned Items");
|
||||
const item = data.data;
|
||||
|
||||
// Create the macro command
|
||||
const command = `game.sw5e.rollItemMacro("${item.name}");`;
|
||||
let macro = game.macros.entities.find(m => (m.name === item.name) && (m.command === command));
|
||||
if ( !macro ) {
|
||||
let macro = game.macros.entities.find((m) => m.name === item.name && m.command === command);
|
||||
if (!macro) {
|
||||
macro = await Macro.create({
|
||||
name: item.name,
|
||||
type: "script",
|
||||
|
@ -42,14 +41,16 @@ export async function create5eMacro(data, slot) {
|
|||
export function rollItemMacro(itemName) {
|
||||
const speaker = ChatMessage.getSpeaker();
|
||||
let actor;
|
||||
if ( speaker.token ) actor = game.actors.tokens[speaker.token];
|
||||
if ( !actor ) actor = game.actors.get(speaker.actor);
|
||||
if (speaker.token) actor = game.actors.tokens[speaker.token];
|
||||
if (!actor) actor = game.actors.get(speaker.actor);
|
||||
|
||||
// Get matching items
|
||||
const items = actor ? actor.items.filter(i => i.name === itemName) : [];
|
||||
if ( items.length > 1 ) {
|
||||
ui.notifications.warn(`Your controlled Actor ${actor.name} has more than one Item with name ${itemName}. The first matched item will be chosen.`);
|
||||
} else if ( items.length === 0 ) {
|
||||
const items = actor ? actor.items.filter((i) => i.name === itemName) : [];
|
||||
if (items.length > 1) {
|
||||
ui.notifications.warn(
|
||||
`Your controlled Actor ${actor.name} has more than one Item with name ${itemName}. The first matched item will be chosen.`
|
||||
);
|
||||
} else if (items.length === 0) {
|
||||
return ui.notifications.warn(`Your controlled Actor does not have an item named ${itemName}`);
|
||||
}
|
||||
const item = items[0];
|
||||
|
|
|
@ -2,56 +2,62 @@
|
|||
* Perform a system migration for the entire World, applying migrations for Actors, Items, and Compendium packs
|
||||
* @return {Promise} A Promise which resolves once the migration is completed
|
||||
*/
|
||||
export const migrateWorld = async function() {
|
||||
ui.notifications.info(`Applying SW5e System Migration for version ${game.system.data.version}. Please be patient and do not close your game or shut down your server.`, {permanent: true});
|
||||
export const migrateWorld = async function () {
|
||||
ui.notifications.info(
|
||||
`Applying SW5e System Migration for version ${game.system.data.version}. Please be patient and do not close your game or shut down your server.`,
|
||||
{permanent: true}
|
||||
);
|
||||
|
||||
// Migrate World Actors
|
||||
for await ( let a of game.actors.entities ) {
|
||||
for await (let a of game.actors.contents) {
|
||||
try {
|
||||
console.log(`Checking Actor entity ${a.name} for migration needs`);
|
||||
const updateData = await migrateActorData(a.data);
|
||||
if ( !isObjectEmpty(updateData) ) {
|
||||
if (!foundry.utils.isObjectEmpty(updateData)) {
|
||||
console.log(`Migrating Actor entity ${a.name}`);
|
||||
await a.update(updateData, {enforceTypes: false});
|
||||
}
|
||||
} catch(err) {
|
||||
} catch (err) {
|
||||
err.message = `Failed sw5e system migration for Actor ${a.name}: ${err.message}`;
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate World Items
|
||||
for ( let i of game.items.entities ) {
|
||||
for (let i of game.items.contents) {
|
||||
try {
|
||||
const updateData = migrateItemData(i.data);
|
||||
if ( !isObjectEmpty(updateData) ) {
|
||||
const updateData = migrateItemData(i.toObject());
|
||||
if (!foundry.utils.isObjectEmpty(updateData)) {
|
||||
console.log(`Migrating Item entity ${i.name}`);
|
||||
await i.update(updateData, {enforceTypes: false});
|
||||
}
|
||||
} catch(err) {
|
||||
} catch (err) {
|
||||
err.message = `Failed sw5e system migration for Item ${i.name}: ${err.message}`;
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate Actor Override Tokens
|
||||
for ( let s of game.scenes.entities ) {
|
||||
for (let s of game.scenes.contents) {
|
||||
try {
|
||||
const updateData = await migrateSceneData(s.data);
|
||||
if ( !isObjectEmpty(updateData) ) {
|
||||
if (!foundry.utils.isObjectEmpty(updateData)) {
|
||||
console.log(`Migrating Scene entity ${s.name}`);
|
||||
await s.update(updateData, {enforceTypes: false});
|
||||
// If we do not do this, then synthetic token actors remain in cache
|
||||
// with the un-updated actorData.
|
||||
s.tokens.contents.forEach((t) => (t._actor = null));
|
||||
}
|
||||
} catch(err) {
|
||||
} catch (err) {
|
||||
err.message = `Failed sw5e system migration for Scene ${s.name}: ${err.message}`;
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate World Compendium Packs
|
||||
for ( let p of game.packs ) {
|
||||
if ( p.metadata.package !== "world" ) continue;
|
||||
if ( !["Actor", "Item", "Scene"].includes(p.metadata.entity) ) continue;
|
||||
for (let p of game.packs) {
|
||||
if (p.metadata.package !== "world") continue;
|
||||
if (!["Actor", "Item", "Scene"].includes(p.metadata.entity)) continue;
|
||||
await migrateCompendium(p);
|
||||
}
|
||||
|
||||
|
@ -67,9 +73,9 @@ export const migrateWorld = async function() {
|
|||
* @param pack
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const migrateCompendium = async function(pack) {
|
||||
export const migrateCompendium = async function (pack) {
|
||||
const entity = pack.metadata.entity;
|
||||
if ( !["Actor", "Item", "Scene"].includes(entity) ) return;
|
||||
if (!["Actor", "Item", "Scene"].includes(entity)) return;
|
||||
|
||||
// Unlock the pack for editing
|
||||
const wasLocked = pack.locked;
|
||||
|
@ -77,40 +83,37 @@ export const migrateCompendium = async function(pack) {
|
|||
|
||||
// Begin by requesting server-side data model migration and get the migrated content
|
||||
await pack.migrate();
|
||||
const content = await pack.getContent();
|
||||
const documents = await pack.getDocuments();
|
||||
|
||||
// Iterate over compendium entries - applying fine-tuned migration functions
|
||||
for await ( let ent of content ) {
|
||||
for await (let doc of documents) {
|
||||
let updateData = {};
|
||||
try {
|
||||
switch (entity) {
|
||||
case "Actor":
|
||||
updateData = await migrateActorData(ent.data);
|
||||
updateData = await migrateActorData(doc.data);
|
||||
break;
|
||||
case "Item":
|
||||
updateData = migrateItemData(ent.data);
|
||||
updateData = migrateItemData(doc.toObject());
|
||||
break;
|
||||
case "Scene":
|
||||
updateData = await migrateSceneData(ent.data);
|
||||
updateData = await migrateSceneData(doc.data);
|
||||
break;
|
||||
}
|
||||
if ( isObjectEmpty(updateData) ) continue;
|
||||
if (foundry.utils.isObjectEmpty(updateData)) continue;
|
||||
|
||||
// Save the entry, if data was changed
|
||||
updateData["_id"] = ent._id;
|
||||
await pack.updateEntity(updateData);
|
||||
console.log(`Migrated ${entity} entity ${ent.name} in Compendium ${pack.collection}`);
|
||||
}
|
||||
|
||||
await doc.update(updateData);
|
||||
console.log(`Migrated ${entity} entity ${doc.name} in Compendium ${pack.collection}`);
|
||||
} catch (err) {
|
||||
// Handle migration failures
|
||||
catch(err) {
|
||||
err.message = `Failed sw5e system migration for entity ${ent.name} in pack ${pack.collection}: ${err.message}`;
|
||||
err.message = `Failed sw5e system migration for entity ${doc.name} in pack ${pack.collection}: ${err.message}`;
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the original locked status for the pack
|
||||
pack.configure({locked: wasLocked});
|
||||
await pack.configure({locked: wasLocked});
|
||||
console.log(`Migrated all ${entity} entities from Compendium ${pack.collection}`);
|
||||
};
|
||||
|
||||
|
@ -124,38 +127,44 @@ export const migrateCompendium = async function(pack) {
|
|||
* @param {object} actor The actor data object to update
|
||||
* @return {Object} The updateData to apply
|
||||
*/
|
||||
export const migrateActorData = async function(actor) {
|
||||
export const migrateActorData = async function (actor) {
|
||||
const updateData = {};
|
||||
|
||||
// Actor Data Updates
|
||||
if (actor.data) {
|
||||
_migrateActorMovement(actor, updateData);
|
||||
_migrateActorSenses(actor, updateData);
|
||||
_migrateActorType(actor, updateData);
|
||||
}
|
||||
|
||||
// Migrate Owned Items
|
||||
if ( !!actor.items ) {
|
||||
let hasItemUpdates = false;
|
||||
if (!!actor.items) {
|
||||
const items = await actor.items.reduce(async (memo, i) => {
|
||||
const results = await memo;
|
||||
|
||||
// Migrate the Owned Item
|
||||
let itemUpdate = await migrateActorItemData(i, actor);
|
||||
const itemData = i instanceof CONFIG.Item.documentClass ? i.toObject() : i;
|
||||
let itemUpdate = await migrateActorItemData(itemData, actor);
|
||||
|
||||
// Prepared, Equipped, and Proficient for NPC actors
|
||||
if ( actor.type === "npc" ) {
|
||||
if (getProperty(i.data, "preparation.prepared") === false) itemUpdate["data.preparation.prepared"] = true;
|
||||
if (getProperty(i.data, "equipped") === false) itemUpdate["data.equipped"] = true;
|
||||
if (getProperty(i.data, "proficient") === false) itemUpdate["data.proficient"] = true;
|
||||
if (actor.type === "npc") {
|
||||
if (getProperty(itemData.data, "preparation.prepared") === false)
|
||||
itemUpdate["data.preparation.prepared"] = true;
|
||||
if (getProperty(itemData.data, "equipped") === false) itemUpdate["data.equipped"] = true;
|
||||
if (getProperty(itemData.data, "proficient") === false) itemUpdate["data.proficient"] = true;
|
||||
}
|
||||
|
||||
// Update the Owned Item
|
||||
if ( !isObjectEmpty(itemUpdate) ) {
|
||||
hasItemUpdates = true;
|
||||
if (!isObjectEmpty(itemUpdate)) {
|
||||
itemUpdate._id = itemData._id;
|
||||
console.log(`Migrating Actor ${actor.name}'s ${i.name}`);
|
||||
return [...results, mergeObject(i, itemUpdate, {enforceTypes: false, inplace: false})];
|
||||
} else return [...results, i];
|
||||
results.push(expandObject(itemUpdate));
|
||||
}
|
||||
|
||||
return results;
|
||||
}, []);
|
||||
|
||||
if ( hasItemUpdates ) updateData.items = items;
|
||||
if (items.length > 0) updateData.items = items;
|
||||
}
|
||||
|
||||
// Update NPC data with new datamodel information
|
||||
|
@ -171,14 +180,12 @@ export const migrateActorData = async function(actor) {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
|
||||
/**
|
||||
* Scrub an Actor's system data, removing all keys which are not explicitly defined in the system template
|
||||
* @param {Object} actorData The data object for an Actor
|
||||
* @return {Object} The scrubbed Actor data
|
||||
*/
|
||||
function cleanActorData(actorData) {
|
||||
|
||||
// Scrub system data
|
||||
const model = game.system.model.Actor[actorData.type];
|
||||
actorData.data = filterObject(actorData.data, model);
|
||||
|
@ -188,7 +195,7 @@ function cleanActorData(actorData) {
|
|||
obj[f] = null;
|
||||
return obj;
|
||||
}, {});
|
||||
if ( actorData.flags.sw5e ) {
|
||||
if (actorData.flags.sw5e) {
|
||||
actorData.flags.sw5e = filterObject(actorData.flags.sw5e, allowedFlags);
|
||||
}
|
||||
|
||||
|
@ -196,14 +203,15 @@ function cleanActorData(actorData) {
|
|||
return actorData;
|
||||
}
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Migrate a single Item entity to incorporate latest data model changes
|
||||
* @param item
|
||||
*
|
||||
* @param {object} item Item data to migrate
|
||||
* @return {object} The updateData to apply
|
||||
*/
|
||||
export const migrateItemData = function(item) {
|
||||
export const migrateItemData = function (item) {
|
||||
const updateData = {};
|
||||
_migrateItemClassPowerCasting(item, updateData);
|
||||
_migrateItemAttunement(item, updateData);
|
||||
|
@ -217,7 +225,7 @@ export const migrateItemData = function(item) {
|
|||
* @param item
|
||||
* @param actor
|
||||
*/
|
||||
export const migrateActorItemData = async function(item, actor) {
|
||||
export const migrateActorItemData = async function (item, actor) {
|
||||
const updateData = {};
|
||||
_migrateItemClassPowerCasting(item, updateData);
|
||||
_migrateItemAttunement(item, updateData);
|
||||
|
@ -233,25 +241,35 @@ export const migrateActorItemData = async function(item, actor) {
|
|||
* @param {Object} scene The Scene data to Update
|
||||
* @return {Object} The updateData to apply
|
||||
*/
|
||||
export const migrateSceneData = async function(scene) {
|
||||
const tokens = duplicate(scene.tokens);
|
||||
return {
|
||||
tokens: await Promise.all(tokens.map(async (t) => {
|
||||
if (!t.actorId || t.actorLink || !t.actorData.data) {
|
||||
export const migrateSceneData = async function (scene) {
|
||||
const tokens = await Promise.all(
|
||||
scene.tokens.map(async (token) => {
|
||||
const t = token.toJSON();
|
||||
if (!t.actorId || t.actorLink) {
|
||||
t.actorData = {};
|
||||
return t;
|
||||
}
|
||||
const token = new Token(t);
|
||||
if ( !token.actor ) {
|
||||
} else if (!game.actors.has(t.actorId)) {
|
||||
t.actorId = null;
|
||||
t.actorData = {};
|
||||
} else if ( !t.actorLink ) {
|
||||
const updateData = await migrateActorData(token.data.actorData);
|
||||
t.actorData = mergeObject(token.data.actorData, updateData);
|
||||
} else if (!t.actorLink) {
|
||||
const actorData = duplicate(t.actorData);
|
||||
actorData.type = token.actor?.type;
|
||||
const update = migrateActorData(actorData);
|
||||
["items", "effects"].forEach((embeddedName) => {
|
||||
if (!update[embeddedName]?.length) return;
|
||||
const updates = new Map(update[embeddedName].map((u) => [u._id, u]));
|
||||
t.actorData[embeddedName].forEach((original) => {
|
||||
const update = updates.get(original._id);
|
||||
if (update) mergeObject(original, update);
|
||||
});
|
||||
delete update[embeddedName];
|
||||
});
|
||||
|
||||
mergeObject(t.actorData, update);
|
||||
}
|
||||
return t;
|
||||
}))
|
||||
};
|
||||
})
|
||||
);
|
||||
return {tokens};
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -266,7 +284,6 @@ export const migrateActorItemData = async function(item, actor) {
|
|||
* @return {Object} The updated Actor
|
||||
*/
|
||||
function _updateNPCData(actor) {
|
||||
|
||||
let actorData = actor.data;
|
||||
const updateData = {};
|
||||
// check for flag.core, if not there is no compendium monster so exit
|
||||
|
@ -274,13 +291,20 @@ function _updateNPCData(actor) {
|
|||
if (!hasSource) return actor;
|
||||
// shortcut out if dataVersion flag is set to 1.2.4 or higher
|
||||
const hasDataVersion = actor?.flags?.sw5e?.dataVersion !== undefined;
|
||||
if (hasDataVersion && (actor.flags.sw5e.dataVersion === "1.2.4" || isNewerVersion("1.2.4", actor.flags.sw5e.dataVersion))) return actor;
|
||||
if (
|
||||
hasDataVersion &&
|
||||
(actor.flags.sw5e.dataVersion === "1.2.4" || isNewerVersion("1.2.4", actor.flags.sw5e.dataVersion))
|
||||
)
|
||||
return actor;
|
||||
// Check to see what the source of NPC is
|
||||
const sourceId = actor.flags.core.sourceId;
|
||||
const coreSource = sourceId.substr(0,sourceId.length-17);
|
||||
const core_id = sourceId.substr(sourceId.length-16,16);
|
||||
if (coreSource === "Compendium.sw5e.monsters"){
|
||||
game.packs.get("sw5e.monsters").getEntity(core_id).then(monster => {
|
||||
const coreSource = sourceId.substr(0, sourceId.length - 17);
|
||||
const core_id = sourceId.substr(sourceId.length - 16, 16);
|
||||
if (coreSource === "Compendium.sw5e.monsters") {
|
||||
game.packs
|
||||
.get("sw5e.monsters")
|
||||
.getEntity(core_id)
|
||||
.then((monster) => {
|
||||
const monsterData = monster.data.data;
|
||||
// copy movement[], senses[], powercasting, force[], tech[], powerForceLevel, powerTechLevel
|
||||
updateData["data.attributes.movement"] = monsterData.attributes.movement;
|
||||
|
@ -292,11 +316,13 @@ function _updateNPCData(actor) {
|
|||
updateData["data.details.powerTechLevel"] = monsterData.details.powerTechLevel;
|
||||
// push missing powers onto actor
|
||||
let newPowers = [];
|
||||
for ( let i of monster.items ) {
|
||||
for (let i of monster.items) {
|
||||
const itemData = i.data;
|
||||
if ( itemData.type === "power" ) {
|
||||
if (itemData.type === "power") {
|
||||
const itemCompendium_id = itemData.flags?.core?.sourceId.split(".").slice(-1)[0];
|
||||
let hasPower = !!actor.items.find(item => item.flags?.core?.sourceId.split(".").slice(-1)[0] === itemCompendium_id);
|
||||
let hasPower = !!actor.items.find(
|
||||
(item) => item.flags?.core?.sourceId.split(".").slice(-1)[0] === itemCompendium_id
|
||||
);
|
||||
if (!hasPower) {
|
||||
// Clone power to new object. Don't know if it is technically needed, but seems to prevent some weirdness.
|
||||
const newPower = JSON.parse(JSON.stringify(itemData));
|
||||
|
@ -313,17 +339,15 @@ function _updateNPCData(actor) {
|
|||
|
||||
// set flag to check to see if migration has been done so we don't do it again.
|
||||
liveActor.setFlag("sw5e", "dataVersion", "1.2.4");
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
//merge object
|
||||
actorData = mergeObject(actorData, updateData);
|
||||
// Return the scrubbed data
|
||||
return actor;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Migrate the actor speed string to movement object
|
||||
* @private
|
||||
|
@ -332,21 +356,21 @@ function _migrateActorMovement(actorData, updateData) {
|
|||
const ad = actorData.data;
|
||||
|
||||
// Work is needed if old data is present
|
||||
const old = actorData.type === 'vehicle' ? ad?.attributes?.speed : ad?.attributes?.speed?.value;
|
||||
const old = actorData.type === "vehicle" ? ad?.attributes?.speed : ad?.attributes?.speed?.value;
|
||||
const hasOld = old !== undefined;
|
||||
if ( hasOld ) {
|
||||
|
||||
if (hasOld) {
|
||||
// If new data is not present, migrate the old data
|
||||
const hasNew = ad?.attributes?.movement?.walk !== undefined;
|
||||
if ( !hasNew && (typeof old === "string") ) {
|
||||
if (!hasNew && typeof old === "string") {
|
||||
const s = (old || "").split(" ");
|
||||
if ( s.length > 0 ) updateData["data.attributes.movement.walk"] = Number.isNumeric(s[0]) ? parseInt(s[0]) : null;
|
||||
if (s.length > 0)
|
||||
updateData["data.attributes.movement.walk"] = Number.isNumeric(s[0]) ? parseInt(s[0]) : null;
|
||||
}
|
||||
|
||||
// Remove the old attribute
|
||||
updateData["data.attributes.-=speed"] = null;
|
||||
}
|
||||
return updateData
|
||||
return updateData;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -360,35 +384,35 @@ function _migrateActorPowers(actorData, updateData) {
|
|||
|
||||
// If new Force & Tech data is not present, create it
|
||||
let hasNewAttrib = ad?.attributes?.force?.level !== undefined;
|
||||
if ( !hasNewAttrib ) {
|
||||
if (!hasNewAttrib) {
|
||||
updateData["data.attributes.force.known.value"] = 0;
|
||||
updateData["data.attributes.force.known.max"] = 0;
|
||||
updateData["data.attributes.force.points.value"] = 0;
|
||||
updateData["data.attributes.force.points.min"] = 0;
|
||||
updateData["data.attributes.force.points.max"] = 0;
|
||||
updateData["data.attributes.force.points.temp"] = 0;
|
||||
updateData["data.attributes.force.points.tempmax"] = 0;
|
||||
updateData["data.attributes.force.points.temp"] = null;
|
||||
updateData["data.attributes.force.points.tempmax"] = null;
|
||||
updateData["data.attributes.force.level"] = 0;
|
||||
updateData["data.attributes.tech.known.value"] = 0;
|
||||
updateData["data.attributes.tech.known.max"] = 0;
|
||||
updateData["data.attributes.tech.points.value"] = 0;
|
||||
updateData["data.attributes.tech.points.min"] = 0;
|
||||
updateData["data.attributes.tech.points.max"] = 0;
|
||||
updateData["data.attributes.tech.points.temp"] = 0;
|
||||
updateData["data.attributes.tech.points.tempmax"] = 0;
|
||||
updateData["data.attributes.tech.points.temp"] = null;
|
||||
updateData["data.attributes.tech.points.tempmax"] = null;
|
||||
updateData["data.attributes.tech.level"] = 0;
|
||||
}
|
||||
|
||||
// If new Power F/T split data is not present, create it
|
||||
const hasNewLimit = ad?.powers?.power1?.foverride !== undefined;
|
||||
if ( !hasNewLimit ) {
|
||||
if (!hasNewLimit) {
|
||||
for (let i = 1; i <= 9; i++) {
|
||||
// add new
|
||||
updateData["data.powers.power" + i + ".fvalue"] = getProperty(ad.powers,"power" + i + ".value");
|
||||
updateData["data.powers.power" + i + ".fmax"] = getProperty(ad.powers,"power" + i + ".max");
|
||||
updateData["data.powers.power" + i + ".fvalue"] = getProperty(ad.powers, "power" + i + ".value");
|
||||
updateData["data.powers.power" + i + ".fmax"] = getProperty(ad.powers, "power" + i + ".max");
|
||||
updateData["data.powers.power" + i + ".foverride"] = null;
|
||||
updateData["data.powers.power" + i + ".tvalue"] = getProperty(ad.powers,"power" + i + ".value");
|
||||
updateData["data.powers.power" + i + ".tmax"] = getProperty(ad.powers,"power" + i + ".max");
|
||||
updateData["data.powers.power" + i + ".tvalue"] = getProperty(ad.powers, "power" + i + ".value");
|
||||
updateData["data.powers.power" + i + ".tmax"] = getProperty(ad.powers, "power" + i + ".max");
|
||||
updateData["data.powers.power" + i + ".toverride"] = null;
|
||||
//remove old
|
||||
updateData["data.powers.power" + i + ".-=value"] = null;
|
||||
|
@ -397,7 +421,7 @@ function _migrateActorPowers(actorData, updateData) {
|
|||
}
|
||||
// If new Bonus Power DC data is not present, create it
|
||||
const hasNewBonus = ad?.bonuses?.power?.forceLightDC !== undefined;
|
||||
if ( !hasNewBonus ) {
|
||||
if (!hasNewBonus) {
|
||||
updateData["data.bonuses.power.forceLightDC"] = "";
|
||||
updateData["data.bonuses.power.forceDarkDC"] = "";
|
||||
updateData["data.bonuses.power.forceUnivDC"] = "";
|
||||
|
@ -407,7 +431,7 @@ function _migrateActorPowers(actorData, updateData) {
|
|||
// Remove the Power DC Bonus
|
||||
updateData["data.bonuses.power.-=dc"] = null;
|
||||
|
||||
return updateData
|
||||
return updateData;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -418,27 +442,28 @@ function _migrateActorPowers(actorData, updateData) {
|
|||
*/
|
||||
function _migrateActorSenses(actor, updateData) {
|
||||
const ad = actor.data;
|
||||
if ( ad?.traits?.senses === undefined ) return;
|
||||
if (ad?.traits?.senses === undefined) return;
|
||||
const original = ad.traits.senses || "";
|
||||
if (typeof original !== "string") return;
|
||||
|
||||
// Try to match old senses with the format like "Darkvision 60 ft, Blindsight 30 ft"
|
||||
const pattern = /([A-z]+)\s?([0-9]+)\s?([A-z]+)?/;
|
||||
let wasMatched = false;
|
||||
|
||||
// Match each comma-separated term
|
||||
for ( let s of original.split(",") ) {
|
||||
for (let s of original.split(",")) {
|
||||
s = s.trim();
|
||||
const match = s.match(pattern);
|
||||
if ( !match ) continue;
|
||||
if (!match) continue;
|
||||
const type = match[1].toLowerCase();
|
||||
if ( type in CONFIG.SW5E.senses ) {
|
||||
if (type in CONFIG.SW5E.senses) {
|
||||
updateData[`data.attributes.senses.${type}`] = Number(match[2]).toNearest(0.5);
|
||||
wasMatched = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If nothing was matched, but there was an old string - put the whole thing in "special"
|
||||
if ( !wasMatched && !!original ) {
|
||||
if (!wasMatched && !!original) {
|
||||
updateData["data.attributes.senses.special"] = original;
|
||||
}
|
||||
|
||||
|
@ -449,26 +474,121 @@ 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
|
||||
*/
|
||||
function _migrateItemClassPowerCasting(item, updateData) {
|
||||
if (item.type === "class"){
|
||||
switch (item.name){
|
||||
if (item.type === "class") {
|
||||
switch (item.name) {
|
||||
case "Consular":
|
||||
updateData["data.powercasting"] = "consular";
|
||||
updateData["data.powercasting"] = {
|
||||
progression: "consular",
|
||||
ability: ""
|
||||
};
|
||||
break;
|
||||
case "Engineer":
|
||||
updateData["data.powercasting"] = "engineer";
|
||||
updateData["data.powercasting"] = {
|
||||
progression: "engineer",
|
||||
ability: ""
|
||||
};
|
||||
break;
|
||||
case "Guardian":
|
||||
updateData["data.powercasting"] = "guardian";
|
||||
updateData["data.powercasting"] = {
|
||||
progression: "guardian",
|
||||
ability: ""
|
||||
};
|
||||
break;
|
||||
case "Scout":
|
||||
updateData["data.powercasting"] = "scout";
|
||||
updateData["data.powercasting"] = {
|
||||
progression: "scout",
|
||||
ability: ""
|
||||
};
|
||||
break;
|
||||
case "Sentinel":
|
||||
updateData["data.powercasting"] = "sentinel";
|
||||
updateData["data.powercasting"] = {
|
||||
progression: "sentinel",
|
||||
ability: ""
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -494,7 +614,11 @@ async function _migrateItemPower(item, actor, updateData) {
|
|||
|
||||
// shortcut out if dataVersion flag is set to 1.2.4 or higher
|
||||
const hasDataVersion = item?.flags?.sw5e?.dataVersion !== undefined;
|
||||
if (hasDataVersion && (item.flags.sw5e.dataVersion === "1.2.4" || isNewerVersion("1.2.4", item.flags.sw5e.dataVersion))) return updateData;
|
||||
if (
|
||||
hasDataVersion &&
|
||||
(item.flags.sw5e.dataVersion === "1.2.4" || isNewerVersion("1.2.4", item.flags.sw5e.dataVersion))
|
||||
)
|
||||
return updateData;
|
||||
|
||||
// Check to see what the source of Power is
|
||||
const sourceId = item.flags.core.sourceId;
|
||||
|
@ -512,11 +636,10 @@ async function _migrateItemPower(item, actor, updateData) {
|
|||
const corePowerData = corePower.data;
|
||||
// copy Core Power Data over original Power
|
||||
updateData["data"] = corePowerData;
|
||||
updateData["flags"] = {"sw5e": {"dataVersion": "1.2.4"}};
|
||||
updateData["flags"] = {sw5e: {dataVersion: "1.2.4"}};
|
||||
|
||||
return updateData;
|
||||
|
||||
|
||||
//game.packs.get(powerType).getEntity(core_id).then(corePower => {
|
||||
|
||||
//})
|
||||
|
@ -526,10 +649,14 @@ async function _migrateItemPower(item, actor, updateData) {
|
|||
|
||||
/**
|
||||
* Delete the old data.attuned boolean
|
||||
*
|
||||
* @param {object} item Item data to migrate
|
||||
* @param {object} updateData Existing update to expand upon
|
||||
* @return {object} The updateData to apply
|
||||
* @private
|
||||
*/
|
||||
function _migrateItemAttunement(item, updateData) {
|
||||
if ( item.data.attuned === undefined ) return;
|
||||
if (item.data?.attuned === undefined) return updateData;
|
||||
updateData["data.attunement"] = CONFIG.SW5E.attunementTypes.NONE;
|
||||
updateData["data.-=attuned"] = null;
|
||||
return updateData;
|
||||
|
@ -549,13 +676,13 @@ export async function purgeFlags(pack) {
|
|||
};
|
||||
await pack.configure({locked: false});
|
||||
const content = await pack.getContent();
|
||||
for ( let entity of content ) {
|
||||
for (let entity of content) {
|
||||
const update = {_id: entity.id, flags: cleanFlags(entity.data.flags)};
|
||||
if ( pack.entity === "Actor" ) {
|
||||
update.items = entity.data.items.map(i => {
|
||||
if (pack.entity === "Actor") {
|
||||
update.items = entity.data.items.map((i) => {
|
||||
i.flags = cleanFlags(i.flags);
|
||||
return i;
|
||||
})
|
||||
});
|
||||
}
|
||||
await pack.updateEntity(update, {recursive: false});
|
||||
console.log(`Purged flags from ${entity.name}`);
|
||||
|
@ -565,20 +692,18 @@ export async function purgeFlags(pack) {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
|
||||
/**
|
||||
* Purge the data model of any inner objects which have been flagged as _deprecated.
|
||||
* @param {object} data The data to clean
|
||||
* @private
|
||||
*/
|
||||
export function removeDeprecatedObjects(data) {
|
||||
for ( let [k, v] of Object.entries(data) ) {
|
||||
if ( getType(v) === "Object" ) {
|
||||
for (let [k, v] of Object.entries(data)) {
|
||||
if (getType(v) === "Object") {
|
||||
if (v._deprecated === true) {
|
||||
console.log(`Deleting deprecated object key ${k}`);
|
||||
delete data[k];
|
||||
}
|
||||
else removeDeprecatedObjects(v);
|
||||
} else removeDeprecatedObjects(v);
|
||||
}
|
||||
}
|
||||
return data;
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import { SW5E } from "../config.js";
|
||||
import {SW5E} from "../config.js";
|
||||
|
||||
/**
|
||||
* A helper class for building MeasuredTemplates for 5e powers and abilities
|
||||
* @extends {MeasuredTemplate}
|
||||
*/
|
||||
export default class AbilityTemplate extends MeasuredTemplate {
|
||||
|
||||
/**
|
||||
* A factory method to create an AbilityTemplate instance using provided data from an Item5e instance
|
||||
* @param {Item5e} item The Item object for which to construct the template
|
||||
|
@ -14,12 +13,12 @@ export default class AbilityTemplate extends MeasuredTemplate {
|
|||
static fromItem(item) {
|
||||
const target = getProperty(item.data, "data.target") || {};
|
||||
const templateShape = SW5E.areaTargetTypes[target.type];
|
||||
if ( !templateShape ) return null;
|
||||
if (!templateShape) return null;
|
||||
|
||||
// Prepare template data
|
||||
const templateData = {
|
||||
t: templateShape,
|
||||
user: game.user._id,
|
||||
user: game.user.data._id,
|
||||
distance: target.value,
|
||||
direction: 0,
|
||||
x: 0,
|
||||
|
@ -28,7 +27,7 @@ export default class AbilityTemplate extends MeasuredTemplate {
|
|||
};
|
||||
|
||||
// Additional type-specific data
|
||||
switch ( templateShape ) {
|
||||
switch (templateShape) {
|
||||
case "cone":
|
||||
templateData.angle = CONFIG.MeasuredTemplate.defaults.angle;
|
||||
break;
|
||||
|
@ -45,10 +44,12 @@ export default class AbilityTemplate extends MeasuredTemplate {
|
|||
}
|
||||
|
||||
// Return the template constructed from the item data
|
||||
const template = new this(templateData);
|
||||
template.item = item;
|
||||
template.actorSheet = item.actor?.sheet || null;
|
||||
return template;
|
||||
const cls = CONFIG.MeasuredTemplate.documentClass;
|
||||
const template = new cls(templateData, {parent: canvas.scene});
|
||||
const object = new this(template);
|
||||
object.item = item;
|
||||
object.actorSheet = item.actor?.sheet || null;
|
||||
return object;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -65,7 +66,7 @@ export default class AbilityTemplate extends MeasuredTemplate {
|
|||
this.layer.preview.addChild(this);
|
||||
|
||||
// Hide the sheet that originated the preview
|
||||
if ( this.actorSheet ) this.actorSheet.minimize();
|
||||
if (this.actorSheet) this.actorSheet.minimize();
|
||||
|
||||
// Activate interactivity
|
||||
this.activatePreviewListeners(initialLayer);
|
||||
|
@ -82,20 +83,19 @@ export default class AbilityTemplate extends MeasuredTemplate {
|
|||
let moveTime = 0;
|
||||
|
||||
// Update placement (mouse-move)
|
||||
handlers.mm = event => {
|
||||
handlers.mm = (event) => {
|
||||
event.stopPropagation();
|
||||
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 snapped = canvas.grid.getSnappedPosition(center.x, center.y, 2);
|
||||
this.data.x = snapped.x;
|
||||
this.data.y = snapped.y;
|
||||
this.data.update({x: snapped.x, y: snapped.y});
|
||||
this.refresh();
|
||||
moveTime = now;
|
||||
};
|
||||
|
||||
// Cancel the workflow (right-click)
|
||||
handlers.rc = event => {
|
||||
handlers.rc = (event) => {
|
||||
this.layer.preview.removeChildren();
|
||||
canvas.stage.off("mousemove", handlers.mm);
|
||||
canvas.stage.off("mousedown", handlers.lc);
|
||||
|
@ -106,25 +106,20 @@ export default class AbilityTemplate extends MeasuredTemplate {
|
|||
};
|
||||
|
||||
// Confirm the workflow (left-click)
|
||||
handlers.lc = event => {
|
||||
handlers.lc = (event) => {
|
||||
handlers.rc(event);
|
||||
|
||||
// Confirm final snapped position
|
||||
const destination = canvas.grid.getSnappedPosition(this.x, this.y, 2);
|
||||
this.data.x = destination.x;
|
||||
this.data.y = destination.y;
|
||||
|
||||
// Create the template
|
||||
canvas.scene.createEmbeddedEntity("MeasuredTemplate", this.data);
|
||||
const destination = canvas.grid.getSnappedPosition(this.data.x, this.data.y, 2);
|
||||
this.data.update(destination);
|
||||
canvas.scene.createEmbeddedDocuments("MeasuredTemplate", [this.data]);
|
||||
};
|
||||
|
||||
// Rotate the template by 3 degree increments (mouse-wheel)
|
||||
handlers.mw = event => {
|
||||
if ( event.ctrlKey ) event.preventDefault(); // Avoid zooming the browser window
|
||||
handlers.mw = (event) => {
|
||||
if (event.ctrlKey) event.preventDefault(); // Avoid zooming the browser window
|
||||
event.stopPropagation();
|
||||
let delta = canvas.grid.type > CONST.GRID_TYPES.SQUARE ? 30 : 15;
|
||||
let snap = event.shiftKey ? delta : 5;
|
||||
this.data.direction += (snap * Math.sign(event.deltaY));
|
||||
this.data.update({direction: this.data.direction + snap * Math.sign(event.deltaY)});
|
||||
this.refresh();
|
||||
};
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
export const registerSystemSettings = function() {
|
||||
|
||||
export const registerSystemSettings = function () {
|
||||
/**
|
||||
* Track the system version upon which point a migration was last applied
|
||||
*/
|
||||
|
@ -8,7 +7,7 @@ export const registerSystemSettings = function() {
|
|||
scope: "world",
|
||||
config: false,
|
||||
type: String,
|
||||
default: ""
|
||||
default: game.system.data.version
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -22,9 +21,9 @@ export const registerSystemSettings = function() {
|
|||
default: "normal",
|
||||
type: String,
|
||||
choices: {
|
||||
"normal": "SETTINGS.5eRestPHB",
|
||||
"gritty": "SETTINGS.5eRestGritty",
|
||||
"epic": "SETTINGS.5eRestEpic",
|
||||
normal: "SETTINGS.5eRestPHB",
|
||||
gritty: "SETTINGS.5eRestGritty",
|
||||
epic: "SETTINGS.5eRestEpic"
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -39,11 +38,11 @@ export const registerSystemSettings = function() {
|
|||
default: "555",
|
||||
type: String,
|
||||
choices: {
|
||||
"555": "SETTINGS.5eDiagPHB",
|
||||
"5105": "SETTINGS.5eDiagDMG",
|
||||
"EUCL": "SETTINGS.5eDiagEuclidean",
|
||||
555: "SETTINGS.5eDiagPHB",
|
||||
5105: "SETTINGS.5eDiagDMG",
|
||||
EUCL: "SETTINGS.5eDiagEuclidean"
|
||||
},
|
||||
onChange: rule => canvas.grid.diagonalRule = rule
|
||||
onChange: (rule) => (canvas.grid.diagonalRule = rule)
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -79,7 +78,7 @@ export const registerSystemSettings = function() {
|
|||
scope: "world",
|
||||
config: true,
|
||||
default: false,
|
||||
type: Boolean,
|
||||
type: Boolean
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -92,7 +91,7 @@ export const registerSystemSettings = function() {
|
|||
config: true,
|
||||
default: false,
|
||||
type: Boolean,
|
||||
onChange: s => {
|
||||
onChange: (s) => {
|
||||
ui.chat.render();
|
||||
}
|
||||
});
|
||||
|
@ -100,10 +99,10 @@ export const registerSystemSettings = function() {
|
|||
/**
|
||||
* Option to allow GMs to restrict polymorphing to GMs only.
|
||||
*/
|
||||
game.settings.register('sw5e', 'allowPolymorphing', {
|
||||
name: 'SETTINGS.5eAllowPolymorphingN',
|
||||
hint: 'SETTINGS.5eAllowPolymorphingL',
|
||||
scope: 'world',
|
||||
game.settings.register("sw5e", "allowPolymorphing", {
|
||||
name: "SETTINGS.5eAllowPolymorphingN",
|
||||
hint: "SETTINGS.5eAllowPolymorphingL",
|
||||
scope: "world",
|
||||
config: true,
|
||||
default: false,
|
||||
type: Boolean
|
||||
|
@ -112,8 +111,8 @@ export const registerSystemSettings = function() {
|
|||
/**
|
||||
* Remember last-used polymorph settings.
|
||||
*/
|
||||
game.settings.register('sw5e', 'polymorphSettings', {
|
||||
scope: 'client',
|
||||
game.settings.register("sw5e", "polymorphSettings", {
|
||||
scope: "client",
|
||||
default: {
|
||||
keepPhysical: false,
|
||||
keepMental: false,
|
||||
|
@ -138,8 +137,8 @@ export const registerSystemSettings = function() {
|
|||
default: "light",
|
||||
type: String,
|
||||
choices: {
|
||||
"light": "SETTINGS.SWColorLight",
|
||||
"dark": "SETTINGS.SWColorDark"
|
||||
light: "SETTINGS.SWColorLight",
|
||||
dark: "SETTINGS.SWColorDark"
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -3,9 +3,8 @@
|
|||
* Pre-loaded templates are compiled and cached for fast access when rendering
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const preloadHandlebarsTemplates = async function() {
|
||||
export const preloadHandlebarsTemplates = async function () {
|
||||
return loadTemplates([
|
||||
|
||||
// Shared Partials
|
||||
"systems/sw5e/templates/actors/parts/active-effects.html",
|
||||
|
||||
|
@ -18,6 +17,7 @@ export const preloadHandlebarsTemplates = async function() {
|
|||
|
||||
"systems/sw5e/templates/actors/newActor/parts/swalt-biography.html",
|
||||
"systems/sw5e/templates/actors/newActor/parts/swalt-core.html",
|
||||
"systems/sw5e/templates/actors/newActor/parts/swalt-crew.html",
|
||||
"systems/sw5e/templates/actors/newActor/parts/swalt-active-effects.html",
|
||||
"systems/sw5e/templates/actors/newActor/parts/swalt-features.html",
|
||||
"systems/sw5e/templates/actors/newActor/parts/swalt-inventory.html",
|
||||
|
|
102
module/token.js
Normal file
|
@ -0,0 +1,102 @@
|
|||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
6
package-lock.json
generated
|
@ -1266,9 +1266,9 @@
|
|||
}
|
||||
},
|
||||
"hosted-git-info": {
|
||||
"version": "2.8.8",
|
||||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
|
||||
"integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg=="
|
||||
"version": "2.8.9",
|
||||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
|
||||
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw=="
|
||||
},
|
||||
"image-size": {
|
||||
"version": "0.5.5",
|
||||
|
|
BIN
packs/Icons/Archetypes/Bolstering Practice.webp
Normal file
After Width: | Height: | Size: 8.8 KiB |
BIN
packs/Icons/Archetypes/Exhibition Specialist.webp
Normal file
After Width: | Height: | Size: 9.4 KiB |
BIN
packs/Icons/Archetypes/Mechanist Technique.webp
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
packs/Icons/Archetypes/Path of Meditation.webp
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
packs/Icons/Archetypes/Teras Kasi Order.webp
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
packs/Icons/Archetypes/Triage Technique.webp
Normal file
After Width: | Height: | Size: 9.8 KiB |
BIN
packs/Icons/Backgrounds/Clone.webp
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
packs/Icons/Deployments/Coordinator.webp
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
packs/Icons/Deployments/Gunner.webp
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
packs/Icons/Deployments/Mechanic.webp
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
packs/Icons/Deployments/Operator.webp
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
packs/Icons/Deployments/Pilot.webp
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
packs/Icons/Deployments/Technician.webp
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
packs/Icons/Deployments/coordinator_01.webp
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
packs/Icons/Deployments/gunner_01.webp
Normal file
After Width: | Height: | Size: 56 KiB |
BIN
packs/Icons/Deployments/mechanic_01.webp
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
packs/Icons/Deployments/operator_01.webp
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
packs/Icons/Deployments/pilot_01.webp
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
packs/Icons/Deployments/technician_01.webp
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
packs/Icons/Force Powers/Battlemind.webp
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
packs/Icons/Force Powers/Break.webp
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
packs/Icons/Force Powers/Defensive Technique.webp
Normal file
After Width: | Height: | Size: 7.8 KiB |
BIN
packs/Icons/Force Powers/Force Current.webp
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
packs/Icons/Force Powers/Kinetite.webp
Normal file
After Width: | Height: | Size: 8.7 KiB |
BIN
packs/Icons/Force Powers/Probe Mind.webp
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
packs/Icons/Force Powers/Pull Earthward.webp
Normal file
After Width: | Height: | Size: 3 KiB |
BIN
packs/Icons/Force Powers/Tapas.webp
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
packs/Icons/Force Powers/Telepathic Link.webp
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
packs/Icons/Force Powers/Wakefulness.webp
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
packs/Icons/Martial Blasters/BKG.webp
Normal file
After Width: | Height: | Size: 9.5 KiB |
BIN
packs/Icons/Martial Blasters/Shoulder Cannon.webp
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
packs/Icons/Martial Blasters/Sonic Pistol.webp
Normal file
After Width: | Height: | Size: 9.4 KiB |
BIN
packs/Icons/Martial Blasters/Sonic Rifle.webp
Normal file
After Width: | Height: | Size: 9.3 KiB |
BIN
packs/Icons/Martial Blasters/Switch Cannon.webp
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
packs/Icons/Martial Blasters/Switch Pistol.webp
Normal file
After Width: | Height: | Size: 9.4 KiB |
BIN
packs/Icons/Martial Blasters/Switch Rifle.webp
Normal file
After Width: | Height: | Size: 8.1 KiB |
BIN
packs/Icons/Martial Blasters/Switch Sniper.webp
Normal file
After Width: | Height: | Size: 8.1 KiB |
BIN
packs/Icons/Martial Blasters/Torpedo Launcher.webp
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
packs/Icons/Martial Blasters/Vapor Projector.webp
Normal file
After Width: | Height: | Size: 8 KiB |
BIN
packs/Icons/Martial Lightweapons/Chained Light Dagger.webp
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
packs/Icons/Martial Lightweapons/Crossguard Saber.webp
Normal file
After Width: | Height: | Size: 8 KiB |
BIN
packs/Icons/Martial Vibroweapons/Bo-rifle.webp
Normal file
After Width: | Height: | Size: 8.9 KiB |
BIN
packs/Icons/Martial Vibroweapons/Bolas.webp
Normal file
After Width: | Height: | Size: 9 KiB |