Compare commits
152 commits
add-stylin
...
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 | ||
![]() |
4f0c6addcf | ||
![]() |
7c03cd4b04 | ||
![]() |
0558cdec49 | ||
![]() |
e25140b529 | ||
![]() |
e942a9b803 | ||
![]() |
4b1b3bbeed | ||
![]() |
9d6fabe8c2 | ||
![]() |
bd94d75086 | ||
![]() |
16d01207a7 | ||
![]() |
5477f9371d | ||
![]() |
b057026328 | ||
![]() |
28d9290959 | ||
![]() |
3297d9bd8c | ||
![]() |
08d62d1e85 | ||
![]() |
9bbc9af285 | ||
![]() |
56e6640f38 | ||
![]() |
fee77e2172 | ||
![]() |
87d615babc | ||
![]() |
9dfa851016 | ||
![]() |
5138feed4e | ||
![]() |
04dcaf332d | ||
![]() |
4ee235566d | ||
![]() |
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
|
||||
}
|
228
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,6 +9,8 @@
|
|||
"ITEM.TypeClass": "Class",
|
||||
"ITEM.TypeClassfeature": "Class Feature",
|
||||
"ITEM.TypeConsumable": "Consumable",
|
||||
"ITEM.TypeDeployment": "Deployment",
|
||||
"ITEM.TypeDeploymentfeature": "Deployment Feature",
|
||||
"ITEM.TypeEquipment": "Equipment",
|
||||
"ITEM.TypeFeat": "Feat",
|
||||
"ITEM.TypeFightingmastery": "Fighting Mastery",
|
||||
|
@ -16,7 +19,12 @@
|
|||
"ITEM.TypeLoot": "Loot",
|
||||
"ITEM.TypePower": "Power",
|
||||
"ITEM.TypeSpecies": "Species",
|
||||
"ITEM.TypeStarshipfeature": "Starship Feature",
|
||||
"ITEM.TypeStarshipfeaturePl": "Starship Features",
|
||||
"ITEM.TypeStarshipmod": "Starship Modification",
|
||||
"ITEM.TypeStarshipmodPl": "Starship Modifications",
|
||||
"ITEM.TypeTool": "Tool",
|
||||
"ITEM.TypeVenture": "Venture",
|
||||
"ITEM.TypeWeapon": "Weapon",
|
||||
"SETTINGS.5eAllowPolymorphingL": "Allow players to polymorph their own actors.",
|
||||
"SETTINGS.5eAllowPolymorphingN": "Allow Polymorphing",
|
||||
|
@ -35,11 +43,13 @@
|
|||
"SETTINGS.5eInitTBN": "Initiative Dexterity Tiebreaker",
|
||||
"SETTINGS.5eNoExpL": "Remove experience bars from character sheets.",
|
||||
"SETTINGS.5eNoExpN": "Disable Experience Tracking",
|
||||
"SETTINGS.5eReset": "Reset",
|
||||
"SETTINGS.5eRestEpic": "Epic Heroism (LR: 1 hour, SR: 1 min)",
|
||||
"SETTINGS.5eRestGritty": "Gritty Realism (LR: 7 days, SR: 8 hours)",
|
||||
"SETTINGS.5eRestL": "Configure which rest variant should be used for games within this system.",
|
||||
"SETTINGS.5eRestN": "Rest Variant",
|
||||
"SETTINGS.5eRestPHB": "Player's Handbook (LR: 8 hours, SR: 1 hour)",
|
||||
"SETTINGS.5eUndoChanges": "Undo Changes",
|
||||
"SETTINGS.SWColorDark": "Dark Theme",
|
||||
"SETTINGS.SWColorL": "Set the color theme of the game",
|
||||
"SETTINGS.SWColorLight": "Light Theme",
|
||||
|
@ -70,6 +80,7 @@
|
|||
"SW5E.AbilityUseCast": "Cast Power",
|
||||
"SW5E.AbilityUseChargedHint": "This {type} is charged and ready to use!",
|
||||
"SW5E.AbilityUseChargesLabel": "{value} Charges",
|
||||
"SW5E.AbilityUseConfig": "Usage Configuration",
|
||||
"SW5E.AbilityUseConsumableChargeHint": "Using this {type} will consume 1 charge of {value} remaining.",
|
||||
"SW5E.AbilityUseConsumableDestroyHint": "Using this {type} will consume its final charge and it will be destroyed.",
|
||||
"SW5E.AbilityUseConsumableLabel": "{max} per {per}",
|
||||
|
@ -96,7 +107,9 @@
|
|||
"SW5E.ActionUtil": "Utility",
|
||||
"SW5E.ActionWarningNoItem": "The requested item {item} no longer exists on Actor {name}",
|
||||
"SW5E.ActionWarningNoToken": "You must have one or more controlled Tokens in order to use this option.",
|
||||
"SW5E.ActorWarningInvalidItem": "{itemType} items cannot be added to a {actorType}.",
|
||||
"SW5E.Add": "Add",
|
||||
"SW5E.AddEmbeddedItemPromptHint": "Do you want to add these items to your character sheet?",
|
||||
"SW5E.AdditionalNotes": "Additional Notes",
|
||||
"SW5E.Advantage": "Advantage",
|
||||
"SW5E.Alignment": "Alignment",
|
||||
|
@ -110,6 +123,7 @@
|
|||
"SW5E.AlignmentND": "Neutral Dark",
|
||||
"SW5E.AlignmentNL": "Neutral Light",
|
||||
"SW5E.Appearance": "Appearance",
|
||||
"SW5E.Apply": "Apply",
|
||||
"SW5E.ArchetypeName": "Archetype Name",
|
||||
"SW5E.Archetypes": "Archetypes",
|
||||
"SW5E.ArmorClass": "Armor Class",
|
||||
|
@ -179,6 +193,9 @@
|
|||
"SW5E.BonusSaveForm": "Update Bonuses",
|
||||
"SW5E.BonusTechPowerDC": "Global Tech Power DC Bonus",
|
||||
"SW5E.BonusTitle": "Configure Actor Bonuses",
|
||||
"SW5E.BurnFuel": "Burn",
|
||||
"SW5E.CapacityMultiplier": "Capacity Multiplier",
|
||||
"SW5E.CentStorageCapacity": "Central Storage Capacity",
|
||||
"SW5E.ChallengeRating": "Challenge Rating",
|
||||
"SW5E.Charged": "Charged",
|
||||
"SW5E.Charges": "Charges",
|
||||
|
@ -188,9 +205,14 @@
|
|||
"SW5E.ChatContextHealing": "Apply Healing",
|
||||
"SW5E.ChatFlavor": "Chat Message Flavor",
|
||||
"SW5E.ClassLevels": "Class Levels",
|
||||
"SW5E.ClassMakeOriginal": "Original Class",
|
||||
"SW5E.ClassMakeOriginalHint": "First class taken by character used to determine certain class traits when multiclassing.",
|
||||
"SW5E.ClassName": "Class Name",
|
||||
"SW5E.ClassOriginal": "Original Class",
|
||||
"SW5E.ClassSaves": "Saving Throws",
|
||||
"SW5E.ClassSkillsChosen": "Chosen Class Skills",
|
||||
"SW5E.ClassSkillsNumber": "Number of Starting Skills",
|
||||
"SW5E.Collapse": "Collapse/Expand",
|
||||
"SW5E.ComponentMaterial": "Material",
|
||||
"SW5E.ComponentSomatic": "Somatic",
|
||||
"SW5E.ComponentVerbal": "Verbal",
|
||||
|
@ -249,6 +271,32 @@
|
|||
"SW5E.CoverHalf": "Half",
|
||||
"SW5E.CoverThreeQuarters": "Three Quarters",
|
||||
"SW5E.CoverTotal": "Total",
|
||||
"SW5E.CreatureAberration": "Aberration",
|
||||
"SW5E.CreatureAberrationPl": "Aberrations",
|
||||
"SW5E.CreatureBeast": "Beast",
|
||||
"SW5E.CreatureBeastPl": "Beasts",
|
||||
"SW5E.CreatureConstruct": "Construct",
|
||||
"SW5E.CreatureConstructPl": "Constructs",
|
||||
"SW5E.CreatureDroid": "Droid",
|
||||
"SW5E.CreatureDroidPl": "Droids",
|
||||
"SW5E.CreatureForceEntity": "Force Entity",
|
||||
"SW5E.CreatureForceEntityPl": "Force Entities",
|
||||
"SW5E.CreatureHumanoid": "Humanoid",
|
||||
"SW5E.CreatureHumanoidPl": "Humanoids",
|
||||
"SW5E.CreaturePlant": "Plant",
|
||||
"SW5E.CreaturePlantPl": "Plants",
|
||||
"SW5E.CreatureSwarm": "Swarm",
|
||||
"SW5E.CreatureSwarmPhrase": "Swarm of {size} {type}",
|
||||
"SW5E.CreatureSwarmSize": "Swarm Size",
|
||||
"SW5E.CreatureType": "Creature Type",
|
||||
"SW5E.CreatureTypeConfig": "Configure Creature Type",
|
||||
"SW5E.CreatureTypeSelectorCustom": "Custom Type",
|
||||
"SW5E.CreatureTypeSelectorSubtype": "Subtype",
|
||||
"SW5E.CreatureTypeTitle": "Configure Creature Type",
|
||||
"SW5E.CreatureUndead": "Undead",
|
||||
"SW5E.CreatureUndeadPl": "Undead",
|
||||
"SW5E.CrewCap": "Crew Capacity",
|
||||
"SW5E.Crewed": "Crewed",
|
||||
"SW5E.Critical": "Critical",
|
||||
"SW5E.CriticalHit": "Critical Hit",
|
||||
"SW5E.Currency": "Currency",
|
||||
|
@ -282,8 +330,11 @@
|
|||
"SW5E.DeathSavingThrow": "Death Saving Throw",
|
||||
"SW5E.Default": "Default",
|
||||
"SW5E.DefaultAbilityCheck": "Default Ability Check",
|
||||
"SW5E.Deployment": "Deployment",
|
||||
"SW5E.DeploymentPl": "Deployments",
|
||||
"SW5E.description": "A comprehensive game system for running games of Star Wars 5th Edition in the Foundry VTT environment.",
|
||||
"SW5E.Description": "Description",
|
||||
"SW5E.DestructionSave": "Destruction Saves",
|
||||
"SW5E.Details": "Details",
|
||||
"SW5E.Dimensions": "Dimensions",
|
||||
"SW5E.Disadvantage": "Disadvantage",
|
||||
|
@ -292,38 +343,55 @@
|
|||
"SW5E.DistMi": "Miles",
|
||||
"SW5E.DistSelf": "Self",
|
||||
"SW5E.DistTouch": "Touch",
|
||||
"SW5E.DmgRed": "Damage Reduction",
|
||||
"SW5E.Duration": "Duration",
|
||||
"SW5E.EffectsCategoryTemporary": "Temporary Effects",
|
||||
"SW5E.EffectsCategoryPassive": "Passive Effects",
|
||||
"SW5E.EffectsCategoryInactive": "Inactive Effects",
|
||||
"SW5E.EffectCreate": "Create Effect",
|
||||
"SW5E.EffectDelete": "Delete Effect",
|
||||
"SW5E.EffectEdit": "Edit Effect",
|
||||
"SW5E.EffectInactive": "Inactive Effects",
|
||||
"SW5E.EffectNew": "New Effect",
|
||||
"SW5E.EffectPassive": "Passive Effects",
|
||||
"SW5E.Effects": "Effects",
|
||||
"SW5E.EffectTemporary": "Temporary Effects",
|
||||
"SW5E.EffectsCategoryInactive": "Inactive Effects",
|
||||
"SW5E.EffectsCategoryPassive": "Passive Effects",
|
||||
"SW5E.EffectsCategoryTemporary": "Temporary Effects",
|
||||
"SW5E.EffectToggle": "Toggle Effect",
|
||||
"SW5E.Engine": "Engine",
|
||||
"SW5E.EnginePl": "Engines",
|
||||
"SW5E.EquipmentBonus": "Magical Bonus",
|
||||
"SW5E.EquipmentClothing": "Clothing",
|
||||
"SW5E.EquipmentHeavy": "Heavy Armor",
|
||||
"SW5E.EquipmentHyperdrive": "Hyperdrive",
|
||||
"SW5E.EquipmentLight": "Light Armor",
|
||||
"SW5E.EquipmentMedium": "Medium Armor",
|
||||
"SW5E.EquipmentNatural": "Natural Armor",
|
||||
"SW5E.EquipmentPowerCoupling": "Power Coupling",
|
||||
"SW5E.EquipmentReactor": "Reactor",
|
||||
"SW5E.EquipmentShield": "Shield",
|
||||
"SW5E.EquipmentShieldProficiency": "Shields",
|
||||
"SW5E.EquipmentStarshipArmor": "Starship Armor",
|
||||
"SW5E.EquipmentStarshipShield": "Starship Shield",
|
||||
"SW5E.EquipmentTrinket": "Trinket",
|
||||
"SW5E.EquipmentVehicle": "Vehicle Equipment",
|
||||
"SW5E.Equipped": "Equipped",
|
||||
"SW5E.Exhaustion": "Exhaustion",
|
||||
"SW5E.Expand": "Expand",
|
||||
"SW5E.Expertise": "Expertise",
|
||||
"SW5E.Favorites": "Favoris",
|
||||
"SW5E.Favorites": "Favorites",
|
||||
"SW5E.FavoritesAndNotes": "Favorites & Notes",
|
||||
"SW5E.Feats": "Feats",
|
||||
"SW5E.FeatureActionRecharge": "Action Recharge",
|
||||
"SW5E.FeatureActive": "Active Abilities",
|
||||
"SW5E.FeatureAdd": "Create Feature",
|
||||
"SW5E.FeatureAttack": "Feature Attack",
|
||||
"SW5E.FeatureCollapse": "Collapse Feature",
|
||||
"SW5E.FeatureExpand": "Expand Feature",
|
||||
"SW5E.FeaturePassive": "Passive Abilities",
|
||||
"SW5E.FeatureRechargeOn": "Recharge On",
|
||||
"SW5E.FeatureRechargeResult": "1d6 Result",
|
||||
"SW5E.Features": "Features",
|
||||
"SW5E.FeatureType": "Feature Type",
|
||||
"SW5E.FeatureUsage": "Feature Usage",
|
||||
"SW5E.FeetAbbr": "ft.",
|
||||
"SW5E.Filter": "Filter",
|
||||
|
@ -453,19 +521,31 @@
|
|||
"SW5E.Flaws": "Flaws",
|
||||
"SW5E.ForcePowerbook": "Force Powers",
|
||||
"SW5E.Formula": "Formula",
|
||||
"SW5E.FuelCapacity": "Fuel Capacity",
|
||||
"SW5E.FuelCostPerUnit": "Fuel Cost per Unit",
|
||||
"SW5E.FuelCostsMod": "Fuel Costs Modifier",
|
||||
"SW5E.GrantedAbilities": "Granted Abilities",
|
||||
"SW5E.HalfProficient": "Half Proficient",
|
||||
"SW5E.HardpointSizeMod": "Hardpoint Size Modifier",
|
||||
"SW5E.Healing": "Healing",
|
||||
"SW5E.HealingTemp": "Healing (Temporary)",
|
||||
"SW5E.Health": "Health",
|
||||
"SW5E.HealthConditions": "Health Conditions",
|
||||
"SW5E.HealthFormula": "Health Formula",
|
||||
"SW5E.HitDice": "Hit Dice",
|
||||
"SW5E.HitDiceConfig": "Adjust Hit Dice",
|
||||
"SW5E.HitDiceConfigHint": "Adjust remaining hit dice levels for each class.",
|
||||
"SW5E.HitDiceMax": "Maximum Hit Dice",
|
||||
"SW5E.HitDiceRemaining": "Remaining Hit Dice",
|
||||
"SW5E.HitDiceRoll": "Roll Hit Dice",
|
||||
"SW5E.HitDiceUsed": "Hit Dice Used",
|
||||
"SW5E.HitDiceWarn": "{name} has no available {formula} Hit Dice remaining!",
|
||||
"SW5E.HP": "Health",
|
||||
"SW5E.HPFormula": "Health Formula",
|
||||
"SW5E.HullDice": "Hull Dice",
|
||||
"SW5E.HullPoints": "Hull Points",
|
||||
"SW5E.HullPointsFormula": "Hull Points Formula",
|
||||
"SW5E.HyperdriveClass": "Hyperdrive Class",
|
||||
"SW5E.Ideals": "Ideals",
|
||||
"SW5E.Identified": "Identified",
|
||||
"SW5E.Initiative": "Initiative",
|
||||
|
@ -509,6 +589,7 @@
|
|||
"SW5E.ItemTypeArchetype": "Archetype",
|
||||
"SW5E.ItemTypeBackground": "Background",
|
||||
"Sw5E.ItemTypeBackgroundPl": "Backgrounds",
|
||||
"SW5E.ItemTypeBackpack": "Container",
|
||||
"SW5E.ItemTypeClass": "Class",
|
||||
"SW5E.ItemTypeClassFeat": "Class Feature",
|
||||
"SW5E.ItemTypeClassFeats": "Class Features",
|
||||
|
@ -517,6 +598,10 @@
|
|||
"SW5E.ItemTypeConsumablePl": "Consumables",
|
||||
"SW5E.ItemTypeContainer": "Container",
|
||||
"SW5E.ItemTypeContainerPl": "Containers",
|
||||
"SW5E.ItemTypeDeployment": "Deployment",
|
||||
"SW5E.ItemTypeDeploymentFeature": "Deployment Feature",
|
||||
"SW5E.ItemTypeDeploymentFeaturePl": "Deployment Features",
|
||||
"SW5E.ItemTypeDeploymentPl": "Deployments",
|
||||
"SW5E.ItemTypeEquipment": "Equipment",
|
||||
"SW5E.ItemTypeEquipmentPl": "Equipment",
|
||||
"SW5E.ItemTypeFeat": "Feat",
|
||||
|
@ -533,13 +618,19 @@
|
|||
"SW5E.ItemTypePowerPl": "Powers",
|
||||
"SW5E.ItemTypeSpecies": "Species",
|
||||
"SW5E.ItemTypeSpeciesPl": "Species",
|
||||
"SW5E.ItemTypeStarshipMod": "Starship Modification",
|
||||
"SW5E.ItemTypeStarshipModPl": "Starship Modifications",
|
||||
"SW5E.ItemTypeTool": "Tool",
|
||||
"SW5E.ItemTypeToolPl": "Tools",
|
||||
"SW5E.ItemTypeVenture": "Venture",
|
||||
"SW5E.ItemTypeVenturePl": "Ventures",
|
||||
"SW5E.ItemTypeWeapon": "Weapon",
|
||||
"SW5E.ItemTypeWeaponPl": "Weapons",
|
||||
"SW5E.ItemWeaponAttack": "Weapon Attack",
|
||||
"SW5E.ItemWeaponDetails": "Weapon Details",
|
||||
"SW5E.ItemWeaponProperties": "Weapon Properties",
|
||||
"SW5E.ItemWeaponSize": "Starship Weapon Size",
|
||||
"SW5E.ItemWeaponSizePl": "Starship Weapon Sizes",
|
||||
"SW5E.ItemWeaponStatus": "Weapon Status",
|
||||
"SW5E.ItemWeaponType": "Weapon Type",
|
||||
"SW5E.ItemWeaponUsage": "Weapon Usage",
|
||||
|
@ -665,6 +756,7 @@
|
|||
"SW5E.LongRest": "Long Rest",
|
||||
"SW5E.LongRestEpic": "Long Rest (1 hour)",
|
||||
"SW5E.LongRestGritty": "Long Rest (7 days)",
|
||||
"SW5E.LongRestHint": "Take a long rest? On a long rest you will recover hit points, half your maximum hit dice, class resources, limited use item charges, and power points.",
|
||||
"SW5E.LongRestNormal": "Long Rest (8 hours)",
|
||||
"SW5E.LongRestOvernight": "Long Rest (New Day)",
|
||||
"SW5E.LongRestResult": "{name} takes a long rest.",
|
||||
|
@ -684,18 +776,25 @@
|
|||
"SW5E.LongRestResultTP": "{name} takes a long rest and recovers {tech} Tech Points.",
|
||||
"SW5E.LongRestResultTPHD": "{name} takes a long rest and recovers {tech} Tech Points and {dice} Hit Dice.",
|
||||
"SW5E.Max": "Max",
|
||||
"SW5E.ModCap": "Modification Capacity",
|
||||
"SW5E.Modifier": "Modifier",
|
||||
"SW5E.Movement": "Movement",
|
||||
"SW5E.MovementBurrow": "Burrow",
|
||||
"SW5E.MovementClimb": "Climb",
|
||||
"SW5E.MovementConfig": "Configure Movement Speed",
|
||||
"SW5E.MovementConfigHint": "Configure the movement speed and special movement attributes of this creature.",
|
||||
"SW5E.MovementCrawl": "Crawl",
|
||||
"SW5E.MovementFly": "Fly",
|
||||
"SW5E.MovementHover": "Hover",
|
||||
"SW5E.MovementRoll": "Roll",
|
||||
"SW5E.MovementSpace": "Space Flight",
|
||||
"SW5E.MovementSwim": "Swim",
|
||||
"SW5E.MovementTurn": "Turning",
|
||||
"SW5E.MovementUnits": "Units",
|
||||
"SW5E.MovementWalk": "Walk",
|
||||
"SW5E.Name": "Character Name",
|
||||
"SW5E.NewDay": "Is New Day?",
|
||||
"SW5E.NewDayHint": "Recover limited use abilities which recharge \"per day\"?",
|
||||
"SW5E.NoCharges": "No Charges",
|
||||
"SW5E.None": "None",
|
||||
"SW5E.NoPowerLevels": "This character has no powercaster levels, but you may add powers manually.",
|
||||
|
@ -748,7 +847,12 @@
|
|||
"SW5E.PowerCreate": "Create Power",
|
||||
"SW5E.PowerDC": "Power DC",
|
||||
"SW5E.PowerDetails": "Power Details",
|
||||
"SW5E.PowerDiceRecovery": "Power Dice Recovery",
|
||||
"SW5E.PowerDie": "Power Die",
|
||||
"SW5E.PowerDieAlloc": "Power Die Allocation",
|
||||
"SW5E.PowerDiePl": "Power Dice",
|
||||
"SW5E.PowerEffects": "Power Effects",
|
||||
"SW5E.PowerfulCritical": "Powerful Critical",
|
||||
"SW5E.PowerLevel": "Power Level",
|
||||
"SW5E.PowerLevel0": "At-Will",
|
||||
"SW5E.PowerLevel1": "1st Level",
|
||||
|
@ -778,6 +882,7 @@
|
|||
"SW5E.PowerProgression": "Power Progression",
|
||||
"SW5E.PowerProgSct": "Scout",
|
||||
"SW5E.PowerProgSnt": "Sentinel",
|
||||
"SW5E.PowerRouting": "Power Routing",
|
||||
"SW5E.PowerSchool": "Power School",
|
||||
"SW5E.PowersKnown": "Powers Known",
|
||||
"SW5E.PowerTarget": "Power Target",
|
||||
|
@ -789,21 +894,29 @@
|
|||
"SW5E.Proficient": "Proficient",
|
||||
"SW5E.Quantity": "Quantity",
|
||||
"SW5E.Range": "Range",
|
||||
"SW5E.Rank": "Rank",
|
||||
"SW5E.RankPl": "Ranks",
|
||||
"SW5E.Rarity": "Rarity",
|
||||
"SW5E.Reaction": "Reaction",
|
||||
"SW5E.ReactionPl": "Reactions",
|
||||
"SW5E.Recharge": "Recharge",
|
||||
"SW5E.Refitting": "Refitting",
|
||||
"SW5E.Refuel": "Refuel",
|
||||
"SW5E.RegenerationRateCoefficient": "Regeneration Rate Coefficient",
|
||||
"SW5E.RequiredMaterials": "Required Materials",
|
||||
"SW5E.Requirements": "Requirements",
|
||||
"SW5E.ResourcesAndTraits": "Resources & Traits",
|
||||
"SW5E.ResourcePrimary": "Resource 1",
|
||||
"SW5E.ResourcesAndTraits": "Resources & Traits",
|
||||
"SW5E.ResourceSecondary": "Resource 2",
|
||||
"SW5E.ResourceTertiary": "Resource 3",
|
||||
"SW5E.Rest": "Rest",
|
||||
"SW5E.RestL": "L. Rest",
|
||||
"SW5E.RestS": "S. Rest",
|
||||
"SW5E.Ritual": "Ritual",
|
||||
"SW5E.Role": "Role",
|
||||
"SW5E.RolePl": "Roles",
|
||||
"SW5E.Roll": "Roll",
|
||||
"SW5E.RollExample": "e.g. +1d4",
|
||||
"SW5E.RollExample": "e.g. 1d4",
|
||||
"SW5E.RollMode": "Roll Mode",
|
||||
"SW5E.RollSituationalBonus": "Situational Bonus?",
|
||||
"SW5E.Save": "Save",
|
||||
|
@ -816,6 +929,7 @@
|
|||
"SW5E.SchoolLgt": "Light",
|
||||
"SW5E.SchoolTec": "Tech",
|
||||
"SW5E.SchoolUni": "Universal",
|
||||
"SW5E.SelectItemsPromptTitle": "Select Items",
|
||||
"SW5E.SenseBlindsight": "Blindsight",
|
||||
"SW5E.SenseBS": "Blindsight",
|
||||
"SW5E.SenseDarkvision": "Darkvision",
|
||||
|
@ -834,6 +948,10 @@
|
|||
"SW5E.SheetClassNPC": "Default NPC Sheet",
|
||||
"SW5E.SheetClassNPCOld": "Old NPC Sheet",
|
||||
"SW5E.SheetClassVehicle": "Default Vehicle Sheet",
|
||||
"SW5E.ShieldDice": "Shield Dice",
|
||||
"SW5E.ShieldPoints": "Shield Points",
|
||||
"SW5E.ShieldPointsFormula": "Shield Points Formula",
|
||||
"SW5E.ShieldRegen": "Regen",
|
||||
"SW5E.ShortRest": "Short Rest",
|
||||
"SW5E.ShortRestEpic": "Short Rest (5 minutes)",
|
||||
"SW5E.ShortRestGritty": "Short Rest (8 hours)",
|
||||
|
@ -873,6 +991,7 @@
|
|||
"SW5E.SkillSte": "Stealth",
|
||||
"SW5E.SkillSur": "Survival",
|
||||
"SW5E.SkillTec": "Technology",
|
||||
"SW5E.Skip": "Skip",
|
||||
"SW5E.Slots": "Slots",
|
||||
"SW5E.Source": "Source",
|
||||
"SW5E.Special": "Special",
|
||||
|
@ -882,8 +1001,62 @@
|
|||
"SW5E.SpeciesTraits": "Species Traits",
|
||||
"SW5E.Speed": "Speed",
|
||||
"SW5E.SpeedSpecial": "Special Movement",
|
||||
"SW5E.StarshipAmbassador": "Ambassador",
|
||||
"SW5E.StarshipArmorandShieldProps": "Starship Armor & Shield Properties",
|
||||
"SW5E.StarshipArmorandShields": "Starship Armor and Shields",
|
||||
"SW5E.StarshipBattleship": "Battleship",
|
||||
"SW5E.StarshipBlockadeShip": "Blockade Ship",
|
||||
"SW5E.StarshipBomber": "Bomber",
|
||||
"SW5E.StarshipCarrier": "Carrier",
|
||||
"SW5E.StarshipColonizer": "Colonizer",
|
||||
"SW5E.StarshipCommandShip": "Command Ship",
|
||||
"SW5E.StarshipCorvette": "Corvette",
|
||||
"SW5E.StarshipCourier": "Courier",
|
||||
"SW5E.StarshipCruiser": "Cruiser",
|
||||
"SW5E.StarshipEquipment": "Starship Equipment",
|
||||
"SW5E.StarshipEquipmentProps": "Starship Equipment Properties",
|
||||
"SW5E.StarshipExplorer": "Explorer",
|
||||
"SW5E.StarshipfeaturePl": "Starship Features",
|
||||
"SW5E.StarshipFlagship": "Flagship",
|
||||
"SW5E.StarshipFreighter": "Freighter",
|
||||
"SW5E.StarshipGunboat": "Gunboat",
|
||||
"SW5E.StarshipIndustrialCenter": "Industrial Center",
|
||||
"SW5E.StarshipInterceptor": "Interceptor",
|
||||
"SW5E.StarshipInterdictor": "Interdictor",
|
||||
"SW5E.StarshipJuggernaut": "Juggernaut",
|
||||
"SW5E.StarshipMissileBoat": "Missile Boat",
|
||||
"SW5E.StarshipMobileMetropolis": "Mobile Metropolis",
|
||||
"SW5E.StarshipmodPl": "Starship Modifications",
|
||||
"SW5E.StarshipNavigator": "Navigator",
|
||||
"SW5E.StarshipPicketShip": "Picket Ship",
|
||||
"SW5E.StarshipResearcher": "Researcher",
|
||||
"SW5E.StarshipScout": "Scout",
|
||||
"SW5E.StarshipScrambler": "Scrambler",
|
||||
"SW5E.StarshipShipsTender": "Ship's Tender",
|
||||
"SW5E.StarshipShuttle": "Shuttle",
|
||||
"SW5E.StarshipSkillAst": "Astrogation",
|
||||
"SW5E.StarshipSkillBst": "Boost",
|
||||
"SW5E.StarshipSkillDat": "Data",
|
||||
"SW5E.StarshipSkillHid": "Hide",
|
||||
"SW5E.StarshipSkillImp": "Impress",
|
||||
"SW5E.StarshipSkillInt": "Interfere",
|
||||
"SW5E.StarshipSkillMan": "Maneuvering",
|
||||
"SW5E.StarshipSkillMen": "Menace",
|
||||
"SW5E.StarshipSkillPat": "Patch",
|
||||
"SW5E.StarshipSkillPrb": "Probe",
|
||||
"SW5E.StarshipSkillRam": "Ram",
|
||||
"SW5E.StarshipSkillReg": "Regulation",
|
||||
"SW5E.StarshipSkillScn": "Scan",
|
||||
"SW5E.StarshipSkillSwn": "Swindle",
|
||||
"SW5E.StarshipStrikeFighter": "Strike Fighter",
|
||||
"SW5E.StarshipTier": "Tier",
|
||||
"SW5E.StarshipWarship": "Warship",
|
||||
"SW5E.StarshipYacht": "Yacht",
|
||||
"SW5E.StealthDisadvantage": "Stealth Disadvantage",
|
||||
"SW5E.SuiteCap": "Suite Capacity",
|
||||
"SW5E.Supply": "Supply",
|
||||
"SW5E.SysStorageCapacity": "System Storage Capacity",
|
||||
"SW5E.SystemDrainage": "System Drainage",
|
||||
"SW5E.Target": "Target",
|
||||
"SW5E.TargetAlly": "Ally",
|
||||
"SW5E.TargetCone": "Cone",
|
||||
|
@ -899,6 +1072,7 @@
|
|||
"SW5E.TargetSpace": "Space",
|
||||
"SW5E.TargetSphere": "Sphere",
|
||||
"SW5E.TargetSquare": "Square",
|
||||
"SW5E.TargetStarship": "Starship",
|
||||
"SW5E.TargetWall": "Wall",
|
||||
"SW5E.TargetWeapon": "Weapon",
|
||||
"SW5E.TargetWidth": "Line Width",
|
||||
|
@ -954,7 +1128,8 @@
|
|||
"SW5E.TraitToolProf": "Tool Proficiencies",
|
||||
"SW5E.TraitWeaponProf": "Weapon Proficiencies",
|
||||
"SW5E.Type": "Type",
|
||||
"SW5E.Unequipped": "Not Equipped",
|
||||
"SW5E.Uncrewed": "Uncrewed",
|
||||
"SW5E.Unequipped": "Unequipped",
|
||||
"SW5E.UniversalPowerDC": "Universal Power DC",
|
||||
"SW5E.Unlimited": "Unlimited",
|
||||
"SW5E.Usage": "Usage",
|
||||
|
@ -978,50 +1153,85 @@
|
|||
"SW5E.VersatileDamage": "Versatile Damage",
|
||||
"SW5E.VsDC": "vs DC.",
|
||||
"SW5E.WeaponAmmo": "Ammunition",
|
||||
"SW5E.WeaponBlasterPistolProficiency": "Blaster Pistol",
|
||||
"SW5E.WeaponChakramProficiency": "Chakrams",
|
||||
"SW5E.WeaponDoubleBladeProficiency": "Doubleblade",
|
||||
"SW5E.WeaponDoubleSaberProficiency": "Doublesaber",
|
||||
"SW5E.WeaponDoubleShotoProficiency": "Doubleshoto",
|
||||
"SW5E.WeaponDoubleSwordProficiency": "Doublesword",
|
||||
"SW5E.WeaponHiddenBladeProficiency": "Hidden Blade",
|
||||
"SW5E.WeaponImprov": "Improvised",
|
||||
"SW5E.WeaponImprovisedProficiency": "Improvised Weapons",
|
||||
"SW5E.WeaponLightFoilProficiency": "Lightfoil",
|
||||
"SW5E.WeaponLightRingProficiency": "Light Ring",
|
||||
"SW5E.WeaponMartialB": "Martial Blaster",
|
||||
"SW5E.WeaponMartialBlasterProficiency": "Martial Blasters",
|
||||
"SW5E.WeaponMartialLightweaponProficiency": "Martial Lightweapons",
|
||||
"SW5E.WeaponMartialLW": "Martial Lightweapon",
|
||||
"SW5E.WeaponMartialProficiency": "Martial Weapons",
|
||||
"SW5E.WeaponMartialVibroweaponProficiency": "Martial Vibroweapons",
|
||||
"SW5E.WeaponMartialVW": "Martial Vibroweapon",
|
||||
"SW5E.WeaponNatural": "Natural",
|
||||
"SW5E.WeaponNaturalProficiency": "Natural Weapons",
|
||||
"SW5E.WeaponPrimarySW": "Primary (Starship)",
|
||||
"SW5E.WeaponPropertiesAmm": "Ammunition",
|
||||
"SW5E.WeaponPropertiesAut": "Auto",
|
||||
"SW5E.WeaponPropertiesBur": "Burst",
|
||||
"SW5E.WeaponPropertiesCon": "Constitution",
|
||||
"SW5E.WeaponPropertiesDef": "Defensive",
|
||||
"SW5E.WeaponPropertiesDex": "Dexterity Rqmt.",
|
||||
"SW5E.WeaponPropertiesDex": "Dexterity Rqt",
|
||||
"SW5E.WeaponPropertiesDgd": "Disguised",
|
||||
"SW5E.WeaponPropertiesDir": "Dire",
|
||||
"SW5E.WeaponPropertiesDis": "Disintegrate",
|
||||
"SW5E.WeaponPropertiesDou": "Double",
|
||||
"SW5E.WeaponPropertiesDpt": "Disruptive",
|
||||
"SW5E.WeaponPropertiesDrm": "Disarming",
|
||||
"SW5E.WeaponPropertiesExp": "Explosive",
|
||||
"SW5E.WeaponPropertiesFin": "Finesse",
|
||||
"SW5E.WeaponPropertiesFix": "Fixed",
|
||||
"SW5E.WeaponPropertiesFoc": "Focus",
|
||||
"SW5E.WeaponPropertiesHid": "Hidden",
|
||||
"SW5E.WeaponPropertiesHom": "Homing",
|
||||
"SW5E.WeaponPropertiesHvy": "Heavy",
|
||||
"SW5E.WeaponPropertiesIon": "Ionizing",
|
||||
"SW5E.WeaponPropertiesKen": "Keen",
|
||||
"SW5E.WeaponPropertiesLgt": "Light",
|
||||
"SW5E.WeaponPropertiesLum": "Luminous",
|
||||
"SW5E.WeaponPropertiesMig": "Mighty",
|
||||
"SW5E.WeaponPropertiesMlt": "Melt",
|
||||
"SW5E.WeaponPropertiesOvr": "Overheat",
|
||||
"SW5E.WeaponPropertiesPic": "Piercing",
|
||||
"SW5E.WeaponPropertiesPow": "Power",
|
||||
"SW5E.WeaponPropertiesRan": "Range",
|
||||
"SW5E.WeaponPropertiesRap": "Rapid",
|
||||
"SW5E.WeaponPropertiesRch": "Reach",
|
||||
"SW5E.WeaponPropertiesRel": "Reload",
|
||||
"SW5E.WeaponPropertiesRet": "Returning",
|
||||
"SW5E.WeaponPropertiesSat": "Saturate",
|
||||
"SW5E.WeaponPropertiesShk": "Shocking",
|
||||
"SW5E.WeaponPropertiesSil": "Silent",
|
||||
"SW5E.WeaponPropertiesSpc": "Special",
|
||||
"SW5E.WeaponPropertiesStr": "Strength Rqmt.",
|
||||
"SW5E.WeaponPropertiesStr": "Strength Rqt",
|
||||
"SW5E.WeaponPropertiesThr": "Thrown",
|
||||
"SW5E.WeaponPropertiesTwo": "Two-Handed",
|
||||
"SW5E.WeaponPropertiesVer": "Versatile",
|
||||
"SW5E.WeaponPropertiesVic": "Vicious",
|
||||
"SW5E.WeaponPropertiesZon": "Zone",
|
||||
"SW5E.WeaponQuaternarySW": "Quaternary (Starship)",
|
||||
"SW5E.WeaponSaberWhipProficiency": "Saberwhip",
|
||||
"SW5E.WeaponSecondarySW": "Secondary (Starship)",
|
||||
"SW5E.WeaponSiege": "Siege",
|
||||
"SW5E.WeaponSimpleB": "Simple Blaster",
|
||||
"SW5E.WeaponSimpleBlasterProficiency": "Simple Blasters",
|
||||
"SW5E.WeaponSimpleLightweaponProficiency": "Simple Lightweapons",
|
||||
"SW5E.WeaponSimpleLW": "Simple Lightweapon",
|
||||
"SW5E.WeaponSimpleProficiency": "Simple Weapons",
|
||||
"SW5E.WeaponSimpleVibroweaponProficiency": "Simple Vibroweapons",
|
||||
"SW5E.WeaponSimpleVW": "Simple Vibroweapon",
|
||||
"SW5E.WeaponSizeAbb": "Size",
|
||||
"SW5E.WeaponTechbladeProficiency": "Techblades",
|
||||
"SW5E.WeaponTertiarySW": "Tertiary (Starship)",
|
||||
"SW5E.WeaponVibrorapierProficiency": "Vibrorapier",
|
||||
"SW5E.WeaponVibrowhipProficiency": "Vibrowhip",
|
||||
"SW5E.Weight": "Weight"
|
||||
}
|
1027
lang/it.json
Normal file
|
@ -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;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -134,31 +156,35 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
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) {
|
||||
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) {
|
||||
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(", ") : ""
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -168,7 +194,7 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
const senses = actorData.data.attributes.senses || {};
|
||||
const tags = {};
|
||||
for (let [k, label] of Object.entries(CONFIG.SW5E.senses)) {
|
||||
const v = senses[k] ?? 0
|
||||
const v = senses[k] ?? 0;
|
||||
if (v === 0) continue;
|
||||
tags[k] = `${game.i18n.localize(label)} ${v} ${senses.units}`;
|
||||
}
|
||||
|
@ -185,14 +211,14 @@ 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)) {
|
||||
const trait = traits[t];
|
||||
|
@ -208,7 +234,7 @@ 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());
|
||||
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
|
||||
|
@ -247,12 +274,17 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
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
|
||||
};
|
||||
};
|
||||
|
@ -261,7 +293,7 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
const maxLevel = Array.fromRange(10).reduce((max, i) => {
|
||||
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);
|
||||
|
||||
|
@ -275,7 +307,7 @@ 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}`;
|
||||
|
@ -318,13 +350,13 @@ 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;
|
||||
if (data.activation && data.activation.type !== f) return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -338,6 +370,7 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
if (filters.has("prepared")) {
|
||||
if (data.level === 0 || ["innate", "always"].includes(data.preparation.mode)) return true;
|
||||
if (this.actor.data.type === "npc") return true;
|
||||
if (this.actor.data.type === "starship") return true;
|
||||
return data.preparation.prepared;
|
||||
}
|
||||
|
||||
|
@ -369,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) {
|
||||
|
||||
// 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
|
||||
|
@ -482,17 +515,25 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
_onConfigMenu(event) {
|
||||
event.preventDefault();
|
||||
const button = event.currentTarget;
|
||||
let app;
|
||||
switch (button.dataset.action) {
|
||||
case "hit-dice":
|
||||
app = new ActorHitDiceConfig(this.object);
|
||||
break;
|
||||
case "movement":
|
||||
new ActorMovementConfig(this.object).render(true);
|
||||
app = new ActorMovementConfig(this.object);
|
||||
break;
|
||||
case "flags":
|
||||
new ActorSheetFlags(this.object).render(true);
|
||||
app = new ActorSheetFlags(this.object);
|
||||
break;
|
||||
case "senses":
|
||||
new ActorSensesConfig(this.object).render(true);
|
||||
app = new ActorSensesConfig(this.object);
|
||||
break;
|
||||
case "type":
|
||||
new ActorTypeConfig(this.object).render(true);
|
||||
break;
|
||||
}
|
||||
app?.render(true);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -513,9 +554,9 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
|
||||
// Toggle next level - forward on click, backwards on right
|
||||
if (event.type === "click") {
|
||||
field.val(levels[(idx === levels.length - 1) ? 0 : idx + 1]);
|
||||
field.val(levels[idx === levels.length - 1 ? 0 : idx + 1]);
|
||||
} else if (event.type === "contextmenu") {
|
||||
field.val(levels[(idx === 0) ? levels.length - 1 : idx - 1]);
|
||||
field.val(levels[idx === 0 ? levels.length - 1 : idx - 1]);
|
||||
}
|
||||
|
||||
// Update the field value and save the form
|
||||
|
@ -526,13 +567,13 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
|
||||
/** @override */
|
||||
async _onDropActor(event, data) {
|
||||
const canPolymorph = game.user.isGM || (this.actor.owner && game.settings.get('sw5e', 'allowPolymorphing'));
|
||||
const canPolymorph = game.user.isGM || (this.actor.isOwner && game.settings.get("sw5e", "allowPolymorphing"));
|
||||
if (!canPolymorph) return false;
|
||||
|
||||
// Get the target actor
|
||||
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);
|
||||
|
@ -540,35 +581,37 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
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;
|
||||
}
|
||||
|
||||
// Ignore certain statuses
|
||||
if (itemData.data) {
|
||||
["attunement", "equipped", "proficient", "prepared"].forEach(k => delete itemData.data[k]);
|
||||
// Ignore certain statuses
|
||||
["equipped", "proficient", "prepared"].forEach((k) => delete itemData.data[k]);
|
||||
|
||||
// Downgrade ATTUNED to REQUIRED
|
||||
itemData.data.attunement = Math.min(itemData.data.attunement, CONFIG.SW5E.attunementTypes.REQUIRED);
|
||||
}
|
||||
|
||||
// Stack identical consumables
|
||||
if (itemData.type === "consumable" && itemData.flags.core?.sourceId) {
|
||||
const similarItem = this.actor.items.find((i) => {
|
||||
const sourceId = i.getFlag("core", "sourceId");
|
||||
return sourceId && sourceId === itemData.flags.core?.sourceId && i.type === "consumable";
|
||||
});
|
||||
if (similarItem && itemData.name !== "Power Cell") {
|
||||
// Always create a new powercell instead of increasing quantity
|
||||
return similarItem.update({
|
||||
"data.quantity": similarItem.data.data.quantity + Math.max(itemData.data.quantity, 1)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Create the owned item as normal
|
||||
|
@ -650,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,8 +764,8 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
_onItemSummary(event) {
|
||||
event.preventDefault();
|
||||
let li = $(event.currentTarget).parents(".item"),
|
||||
item = this.actor.getOwnedItem(li.data("item-id")),
|
||||
chatData = item.getChatData({secrets: this.actor.owner});
|
||||
item = this.actor.items.get(li.data("item-id")),
|
||||
chatData = item.getChatData({secrets: this.actor.isOwner});
|
||||
|
||||
// Toggle summary
|
||||
if (li.hasClass("expanded")) {
|
||||
|
@ -702,7 +774,7 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
} 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)});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -810,7 +952,7 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
const filter = li.dataset.filter;
|
||||
if (set.has(filter)) set.delete(filter);
|
||||
else set.add(filter);
|
||||
this.render();
|
||||
return this.render();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -826,7 +968,7 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
const label = a.parentElement.querySelector("label");
|
||||
const choices = CONFIG.SW5E[a.dataset.options];
|
||||
const options = {name: a.dataset.target, title: label.innerText, choices};
|
||||
new TraitSelector(this.actor, options).render(true)
|
||||
return new TraitSelector(this.actor, options).render(true);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -834,15 +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"
|
||||
}],
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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,7 +74,6 @@ 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"}},
|
||||
|
@ -84,11 +85,27 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
|
|||
};
|
||||
|
||||
// Partition items by category
|
||||
let [items, forcepowers, techpowers, feats, classes, species, archetypes, classfeatures, backgrounds, fightingstyles, fightingmasteries, lightsaberforms] = data.items.reduce((arr, item) => {
|
||||
|
||||
let [
|
||||
items,
|
||||
forcepowers,
|
||||
techpowers,
|
||||
feats,
|
||||
classes,
|
||||
deployments,
|
||||
deploymentfeatures,
|
||||
ventures,
|
||||
species,
|
||||
archetypes,
|
||||
classfeatures,
|
||||
backgrounds,
|
||||
fightingstyles,
|
||||
fightingmasteries,
|
||||
lightsaberforms
|
||||
] = data.items.reduce(
|
||||
(arr, item) => {
|
||||
// Item details
|
||||
item.img = item.img || DEFAULT_TOKEN;
|
||||
item.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 (item.type === "deployment") arr[5].push(item);
|
||||
else if (item.type === "deploymentfeature") arr[6].push(item);
|
||||
else if (item.type === "venture") arr[7].push(item);
|
||||
else if (item.type === "species") arr[8].push(item);
|
||||
else if (item.type === "archetype") arr[9].push(item);
|
||||
else if (item.type === "classfeature") arr[10].push(item);
|
||||
else if (item.type === "background") arr[11].push(item);
|
||||
else if (item.type === "fightingstyle") arr[12].push(item);
|
||||
else if (item.type === "fightingmastery") arr[13].push(item);
|
||||
else if (item.type === "lightsaberform") arr[14].push(item);
|
||||
else if (Object.keys(inventory).includes(item.type)) arr[0].push(item);
|
||||
return arr;
|
||||
}, [[], [], [], [], [], [], [], [], [], [], [], []]);
|
||||
},
|
||||
[[], [], [], [], [], [], [], [], [], [], [], [], [], [], []]
|
||||
);
|
||||
|
||||
// Apply active item filters
|
||||
items = this._filterItems(items, this._filters.inventory);
|
||||
|
@ -137,7 +164,7 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
|
|||
for (let i of items) {
|
||||
i.data.quantity = i.data.quantity || 0;
|
||||
i.data.weight = i.data.weight || 0;
|
||||
i.totalWeight = Math.round(i.data.quantity * i.data.weight * 10) / 10;
|
||||
i.totalWeight = (i.data.quantity * i.data.weight).toNearest(0.1);
|
||||
inventory[i.type].items.push(i);
|
||||
}
|
||||
|
||||
|
@ -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"} },
|
||||
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);
|
||||
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;
|
||||
|
@ -195,8 +299,7 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
|
|||
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);
|
||||
});
|
||||
}
|
||||
|
@ -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,10 +461,9 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
|
|||
|
||||
/** @override */
|
||||
async _onDropItemCreate(itemData) {
|
||||
|
||||
// Increment the number of class levels a character instead of creating a new item
|
||||
// Increment the number of class levels of a character instead of creating a new item
|
||||
if (itemData.type === "class") {
|
||||
const cls = this.actor.itemTypes.class.find(c => c.name === itemData.name);
|
||||
const cls = this.actor.itemTypes.class.find((c) => c.name === itemData.name);
|
||||
let priorLevel = cls?.data.data.levels ?? 0;
|
||||
if (!!cls) {
|
||||
const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level);
|
||||
|
@ -367,8 +474,21 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
|
|||
}
|
||||
}
|
||||
|
||||
// Increment the number of deployment ranks of a character instead of creating a new item
|
||||
// else if ( itemData.type === "deployment" ) {
|
||||
// const rnk = this.actor.itemTypes.deployment.find(c => c.name === itemData.name);
|
||||
// let priorRank = rnk?.data.data.ranks ?? 0;
|
||||
// if ( !!rnk ) {
|
||||
// const next = Math.min(priorLevel + 1, 5 + priorRank - this.actor.data.data.details.rank);
|
||||
// if ( next > priorRank ) {
|
||||
// itemData.ranks = next;
|
||||
// return rnk.update({"data.ranks": next});
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// Default drop handling if levels were not added
|
||||
super._onDropItemCreate(itemData);
|
||||
return super._onDropItemCreate(itemData);
|
||||
}
|
||||
}
|
||||
async function addFavorites(app, html, data) {
|
||||
|
@ -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);
|
||||
|
@ -582,50 +706,44 @@ async function addSubTabs(app, html, data) {
|
|||
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")}
|
||||
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 => {
|
||||
data.options.subTabs[group].forEach((tab) => {
|
||||
if (tab.active) {
|
||||
html.find(`[data-subgroup=${group}][data-target=${tab.target}]`).addClass('active');
|
||||
html.find(`[data-subgroup=${group}][data-target=${tab.target}]`).addClass("active");
|
||||
} else {
|
||||
html.find(`[data-subgroup=${group}][data-target=${tab.target}]`).removeClass('active');
|
||||
html.find(`[data-subgroup=${group}][data-target=${tab.target}]`).removeClass("active");
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
html.find('[data-subgroup-selection]').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;
|
||||
}
|
||||
|
||||
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,7 +6,6 @@ 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";
|
||||
|
@ -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"} },
|
||||
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));
|
||||
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);
|
||||
|
@ -70,8 +89,7 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
|
|||
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,8 +119,7 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
|
|||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_updateObject(event, formData) {
|
||||
|
||||
async _updateObject(event, formData) {
|
||||
// Format NPC Challenge Rating
|
||||
const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5};
|
||||
let crv = "data.details.cr";
|
||||
|
@ -109,7 +128,7 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
|
|||
if (cr) formData[crv] = cr < 1 ? cr : parseInt(cr);
|
||||
|
||||
// Parent ActorSheet update steps
|
||||
super._updateObject(event, formData);
|
||||
return super._updateObject(event, formData);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -138,4 +157,3 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
|
|||
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;
|
||||
|
@ -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,6 +46,14 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A set of item types that should be prevented from being dropped on this type of actor sheet.
|
||||
* @type {Set<string>}
|
||||
*/
|
||||
static unsupportedItemTypes = new Set();
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
get template() {
|
||||
if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/oldActor/limited-sheet.html";
|
||||
|
@ -53,43 +63,50 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
getData() {
|
||||
|
||||
getData(options) {
|
||||
// Basic data
|
||||
let isOwner = this.entity.owner;
|
||||
let isOwner = this.actor.isOwner;
|
||||
const data = {
|
||||
owner: isOwner,
|
||||
limited: this.entity.limited,
|
||||
limited: this.actor.limited,
|
||||
options: this.options,
|
||||
editable: this.isEditable,
|
||||
cssClass: isOwner ? "editable" : "locked",
|
||||
isCharacter: this.entity.data.type === "character",
|
||||
isNPC: this.entity.data.type === "npc",
|
||||
isVehicle: this.entity.data.type === 'vehicle',
|
||||
isCharacter: this.actor.type === "character",
|
||||
isNPC: this.actor.type === "npc",
|
||||
isStarship: this.actor.type === "starship",
|
||||
isVehicle: this.actor.type === "vehicle",
|
||||
config: CONFIG.SW5E,
|
||||
rollData: this.actor.getRollData.bind(this.actor)
|
||||
};
|
||||
|
||||
// The Actor and its Items
|
||||
data.actor = duplicate(this.actor.data);
|
||||
data.items = this.actor.items.map(i => {
|
||||
i.data.labels = i.labels;
|
||||
return i.data;
|
||||
});
|
||||
// The Actor's data
|
||||
const actorData = this.actor.data.toObject(false);
|
||||
data.actor = actorData;
|
||||
data.data = actorData.data;
|
||||
|
||||
// Owned Items
|
||||
data.items = actorData.items;
|
||||
for (let i of data.items) {
|
||||
const item = this.actor.items.get(i._id);
|
||||
i.labels = item.labels;
|
||||
}
|
||||
data.items.sort((a, b) => (a.sort || 0) - (b.sort || 0));
|
||||
data.data = data.actor.data;
|
||||
|
||||
// Labels and filters
|
||||
data.labels = this.actor.labels || {};
|
||||
data.filters = this._filters;
|
||||
|
||||
// Ability Scores
|
||||
for ( let [a, abl] of Object.entries(data.actor.data.abilities)) {
|
||||
for (let [a, abl] of Object.entries(actorData.data.abilities)) {
|
||||
abl.icon = this._getProficiencyIcon(abl.proficient);
|
||||
abl.hover = CONFIG.SW5E.proficiencyLevels[abl.proficient];
|
||||
abl.label = CONFIG.SW5E.abilities[a];
|
||||
}
|
||||
|
||||
// Skills
|
||||
if (data.actor.data.skills) {
|
||||
for ( let [s, skl] of Object.entries(data.actor.data.skills)) {
|
||||
if (actorData.data.skills) {
|
||||
for (let [s, skl] of Object.entries(actorData.data.skills)) {
|
||||
skl.ability = CONFIG.SW5E.abilityAbbreviations[skl.ability];
|
||||
skl.icon = this._getProficiencyIcon(skl.value);
|
||||
skl.hover = CONFIG.SW5E.proficiencyLevels[skl.value];
|
||||
|
@ -98,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;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -132,31 +149,35 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
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) {
|
||||
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) {
|
||||
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(", ") : ""
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -166,7 +187,7 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
const senses = actorData.data.attributes.senses || {};
|
||||
const tags = {};
|
||||
for (let [k, label] of Object.entries(CONFIG.SW5E.senses)) {
|
||||
const v = senses[k] ?? 0
|
||||
const v = senses[k] ?? 0;
|
||||
if (v === 0) continue;
|
||||
tags[k] = `${game.i18n.localize(label)} ${v} ${senses.units}`;
|
||||
}
|
||||
|
@ -183,14 +204,14 @@ 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)) {
|
||||
const trait = traits[t];
|
||||
|
@ -206,7 +227,7 @@ 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());
|
||||
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
|
||||
|
@ -246,7 +267,7 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
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,
|
||||
|
@ -260,7 +281,7 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
const maxLevel = Array.fromRange(10).reduce((max, i) => {
|
||||
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);
|
||||
|
||||
|
@ -274,11 +295,14 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
}
|
||||
|
||||
// Pact magic users have cantrips and a pact magic section
|
||||
// TODO: Check if this is needed, we've removed pacts everywhere else
|
||||
if (levels.pact && levels.pact.max) {
|
||||
if (!powerbook["0"]) registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]);
|
||||
const l = levels.pact;
|
||||
const config = CONFIG.SW5E.powerPreparationModes.pact;
|
||||
registerSection("pact", sections.pact, config, {
|
||||
const level = game.i18n.localize(`SW5E.PowerLevel${levels.pact.level}`);
|
||||
const label = `${config} — ${level}`;
|
||||
registerSection("pact", sections.pact, label, {
|
||||
prepMode: "pact",
|
||||
value: l.value,
|
||||
max: l.max,
|
||||
|
@ -287,7 +311,7 @@ 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}`;
|
||||
|
@ -330,13 +354,13 @@ 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;
|
||||
if (data.activation && data.activation.type !== f) return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
||||
// 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
|
||||
|
@ -494,17 +515,25 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
_onConfigMenu(event) {
|
||||
event.preventDefault();
|
||||
const button = event.currentTarget;
|
||||
let app;
|
||||
switch (button.dataset.action) {
|
||||
case "hit-dice":
|
||||
app = new ActorHitDiceConfig(this.object);
|
||||
break;
|
||||
case "movement":
|
||||
new ActorMovementConfig(this.object).render(true);
|
||||
app = new ActorMovementConfig(this.object);
|
||||
break;
|
||||
case "flags":
|
||||
new ActorSheetFlags(this.object).render(true);
|
||||
app = new ActorSheetFlags(this.object);
|
||||
break;
|
||||
case "senses":
|
||||
new ActorSensesConfig(this.object).render(true);
|
||||
app = new ActorSensesConfig(this.object);
|
||||
break;
|
||||
case "type":
|
||||
new ActorTypeConfig(this.object).render(true);
|
||||
break;
|
||||
}
|
||||
app?.render(true);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -525,9 +554,9 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
|
||||
// Toggle next level - forward on click, backwards on right
|
||||
if (event.type === "click") {
|
||||
field.val(levels[(idx === levels.length - 1) ? 0 : idx + 1]);
|
||||
field.val(levels[idx === levels.length - 1 ? 0 : idx + 1]);
|
||||
} else if (event.type === "contextmenu") {
|
||||
field.val(levels[(idx === 0) ? levels.length - 1 : idx - 1]);
|
||||
field.val(levels[idx === 0 ? levels.length - 1 : idx - 1]);
|
||||
}
|
||||
|
||||
// Update the field value and save the form
|
||||
|
@ -538,13 +567,13 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
|
||||
/** @override */
|
||||
async _onDropActor(event, data) {
|
||||
const canPolymorph = game.user.isGM || (this.actor.owner && game.settings.get('sw5e', 'allowPolymorphing'));
|
||||
const canPolymorph = game.user.isGM || (this.actor.isOwner && game.settings.get("sw5e", "allowPolymorphing"));
|
||||
if (!canPolymorph) return false;
|
||||
|
||||
// Get the target actor
|
||||
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);
|
||||
|
@ -552,35 +581,37 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
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;
|
||||
}
|
||||
|
||||
// Ignore certain statuses
|
||||
if (itemData.data) {
|
||||
["attunement", "equipped", "proficient", "prepared"].forEach(k => delete itemData.data[k]);
|
||||
// Ignore certain statuses
|
||||
["equipped", "proficient", "prepared"].forEach((k) => delete itemData.data[k]);
|
||||
|
||||
// Downgrade ATTUNED to REQUIRED
|
||||
itemData.data.attunement = Math.min(itemData.data.attunement, CONFIG.SW5E.attunementTypes.REQUIRED);
|
||||
}
|
||||
|
||||
// Stack identical consumables
|
||||
if (itemData.type === "consumable" && itemData.flags.core?.sourceId) {
|
||||
const similarItem = this.actor.items.find((i) => {
|
||||
const sourceId = i.getFlag("core", "sourceId");
|
||||
return sourceId && sourceId === itemData.flags.core?.sourceId && i.type === "consumable";
|
||||
});
|
||||
if (similarItem) {
|
||||
return similarItem.update({
|
||||
"data.quantity": similarItem.data.data.quantity + Math.max(itemData.data.quantity, 1)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Create the owned item as normal
|
||||
|
@ -662,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,8 +764,8 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
_onItemSummary(event) {
|
||||
event.preventDefault();
|
||||
let li = $(event.currentTarget).parents(".item"),
|
||||
item = this.actor.getOwnedItem(li.data("item-id")),
|
||||
chatData = item.getChatData({secrets: this.actor.owner});
|
||||
item = this.actor.items.get(li.data("item-id")),
|
||||
chatData = item.getChatData({secrets: this.actor.isOwner});
|
||||
|
||||
// Toggle summary
|
||||
if (li.hasClass("expanded")) {
|
||||
|
@ -714,7 +774,7 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
} 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)});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -822,7 +883,7 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
const filter = li.dataset.filter;
|
||||
if (set.has(filter)) set.delete(filter);
|
||||
else set.add(filter);
|
||||
this.render();
|
||||
return this.render();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -838,7 +899,7 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
const label = a.parentElement.querySelector("label");
|
||||
const choices = CONFIG.SW5E[a.dataset.options];
|
||||
const options = {name: a.dataset.target, title: label.innerText, choices};
|
||||
new TraitSelector(this.actor, options).render(true)
|
||||
return new TraitSelector(this.actor, options).render(true);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -846,15 +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}
|
||||
|
@ -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,7 +62,6 @@ 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"}},
|
||||
|
@ -73,11 +73,23 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
|
|||
};
|
||||
|
||||
// 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,14 +104,19 @@ 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);
|
||||
|
@ -113,7 +130,9 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
|
|||
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);
|
||||
|
@ -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"} },
|
||||
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);
|
||||
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;
|
||||
|
@ -184,8 +256,7 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
|
|||
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));
|
||||
|
@ -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,10 +349,9 @@ 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);
|
||||
const cls = this.actor.itemTypes.class.find((c) => c.name === itemData.name);
|
||||
let priorLevel = cls?.data.data.levels ?? 0;
|
||||
if (!!cls) {
|
||||
const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level);
|
||||
|
@ -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"} },
|
||||
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));
|
||||
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);
|
||||
|
@ -58,8 +75,7 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
|
|||
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,8 +104,7 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
|
|||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_updateObject(event, formData) {
|
||||
|
||||
async _updateObject(event, formData) {
|
||||
// Format NPC Challenge Rating
|
||||
const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5};
|
||||
let crv = "data.details.cr";
|
||||
|
@ -96,7 +113,7 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
|
|||
if (cr) formData[crv] = cr < 1 ? cr : parseInt(cr);
|
||||
|
||||
// Parent ActorSheet update steps
|
||||
super._updateObject(event, formData);
|
||||
return super._updateObject(event, formData);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
|
|
@ -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;
|
||||
|
@ -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});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -39,12 +39,15 @@ export default class AbilityUseDialog extends Dialog {
|
|||
// Prepare dialog form data
|
||||
const data = {
|
||||
item: item.data,
|
||||
title: game.i18n.format("SW5E.AbilityUseHint", item.data),
|
||||
title: game.i18n.format("SW5E.AbilityUseHint", {
|
||||
type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`),
|
||||
name: item.name
|
||||
}),
|
||||
note: this._getAbilityUseNote(item.data, uses, recharge),
|
||||
consumePowerSlot: false,
|
||||
consumeRecharge: recharges,
|
||||
consumeResource: !!itemData.consume.target,
|
||||
consumeUses: uses.max,
|
||||
consumeUses: uses.per && uses.max > 0,
|
||||
canUse: recharges ? recharge.charged : sufficientUses,
|
||||
createTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget,
|
||||
errors: []
|
||||
|
@ -59,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,10 +90,9 @@ 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) {
|
||||
|
@ -106,70 +108,74 @@ export default class AbilityUseDialog extends Dialog {
|
|||
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
|
||||
let powerLevels;
|
||||
if (powerType === "force") {
|
||||
powerLevels = Array.fromRange(10).reduce((arr, i) => {
|
||||
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};
|
||||
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 && 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);
|
||||
} else if (powerType === "tech") {
|
||||
powerLevels = Array.fromRange(10).reduce((arr, i) => {
|
||||
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};
|
||||
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 && 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,7 +185,6 @@ 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");
|
||||
|
@ -187,8 +192,8 @@ export default class AbilityUseDialog extends Dialog {
|
|||
// Abilities which use Recharge
|
||||
if (!!recharge.value) {
|
||||
return game.i18n.format(recharge.charged ? "SW5E.AbilityUseChargedHint" : "SW5E.AbilityUseRechargeHint", {
|
||||
type: item.type,
|
||||
})
|
||||
type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`)
|
||||
});
|
||||
}
|
||||
|
||||
// Does not use any resource
|
||||
|
@ -201,7 +206,7 @@ export default class AbilityUseDialog extends Dialog {
|
|||
else if (item.data.quantity === 1 && uses.autoDestroy) str = "SW5E.AbilityUseConsumableDestroyHint";
|
||||
else if (item.data.quantity > 1) str = "SW5E.AbilityUseConsumableQuantityHint";
|
||||
return game.i18n.format(str, {
|
||||
type: item.data.consumableType,
|
||||
type: game.i18n.localize(`SW5E.Consumable${item.data.consumableType.capitalize()}`),
|
||||
value: uses.value,
|
||||
quantity: item.data.quantity,
|
||||
max: uses.max,
|
||||
|
@ -212,17 +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;
|
||||
const baseData = this.document.toJSON();
|
||||
for (let [k, v] of Object.entries(CONFIG.SW5E.characterFlags)) {
|
||||
if (!flags.hasOwnProperty(v.section)) flags[v.section] = {};
|
||||
let flag = duplicate(v);
|
||||
let flag = foundry.utils.deepClone(v);
|
||||
flag.type = v.type.name;
|
||||
flag.isCheckbox = v.type === Boolean;
|
||||
flag.isSelect = v.hasOwnProperty('choices');
|
||||
flag.isSelect = v.hasOwnProperty("choices");
|
||||
flag.value = getProperty(baseData.flags, `sw5e.${k}`);
|
||||
flags[v.section][`flags.sw5e.${k}`] = flag;
|
||||
}
|
||||
|
@ -97,7 +115,7 @@ 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
|
||||
updateData.flags.dnd5e = updateData.flags.sw5e;
|
||||
for (let [k, v] of Object.entries(flags)) {
|
||||
if ([undefined, null, "", false, 0].includes(v)) {
|
||||
delete flags[k];
|
||||
|
|
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();
|
||||
}
|
||||
}
|
|
@ -40,27 +40,25 @@ export default class LongRestDialog extends Dialog {
|
|||
static async longRestDialog({actor} = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const dlg = new this(actor, {
|
||||
title: "Long Rest",
|
||||
title: game.i18n.localize("SW5E.LongRest"),
|
||||
buttons: {
|
||||
rest: {
|
||||
icon: '<i class="fas fa-bed"></i>',
|
||||
label: "Rest",
|
||||
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,17 +17,18 @@ export default class ActorMovementConfig extends BaseEntitySheet {
|
|||
|
||||
/** @override */
|
||||
get title() {
|
||||
return `${game.i18n.localize("SW5E.MovementConfig")}: ${this.entity.name}`;
|
||||
return `${game.i18n.localize("SW5E.MovementConfig")}: ${this.document.name}`;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
getData(options) {
|
||||
const sourceMovement = foundry.utils.getProperty(this.document.data._source, "data.attributes.movement") || {};
|
||||
const data = {
|
||||
movement: duplicate(this.entity._data.data.attributes.movement),
|
||||
movement: foundry.utils.deepClone(sourceMovement),
|
||||
units: CONFIG.SW5E.movementUnits
|
||||
}
|
||||
};
|
||||
for (let [k, v] of Object.entries(data.movement)) {
|
||||
if (["units", "hover"].includes(k)) continue;
|
||||
data.movement[k] = Number.isNumeric(v) ? v.toNearest(0.1) : 0;
|
||||
|
|
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)) {
|
||||
const v = senses[name];
|
||||
data.senses[name] = {
|
||||
label: game.i18n.localize(label),
|
||||
value: Number.isNumeric(v) ? v.toNearest(0.1) : 0
|
||||
}
|
||||
};
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ export default class ShortRestDialog extends Dialog {
|
|||
// Determine Hit Dice
|
||||
data.availableHD = this.actor.data.items.reduce((hd, item) => {
|
||||
if (item.type === "class") {
|
||||
const d = item.data;
|
||||
const d = item.data.data;
|
||||
const denom = d.hitDice || "d6";
|
||||
const available = parseInt(d.levels || 1) - parseInt(d.hitDiceUsed || 0);
|
||||
hd[denom] = denom in hd ? hd[denom] + available : available;
|
||||
|
@ -59,7 +59,6 @@ export default class ShortRestDialog extends Dialog {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
|
||||
/** @override */
|
||||
activateListeners(html) {
|
||||
super.activateListeners(html);
|
||||
|
@ -93,12 +92,12 @@ export default class ShortRestDialog extends Dialog {
|
|||
static async shortRestDialog({actor} = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const dlg = new this(actor, {
|
||||
title: "Short Rest",
|
||||
title: game.i18n.localize("SW5E.ShortRest"),
|
||||
buttons: {
|
||||
rest: {
|
||||
icon: '<i class="fas fa-bed"></i>',
|
||||
label: "Rest",
|
||||
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
|
||||
}
|
||||
},
|
||||
|
@ -127,7 +126,9 @@ export default class ShortRestDialog extends Dialog {
|
|||
* @return {Promise}
|
||||
*/
|
||||
static async longRestDialog({actor} = {}) {
|
||||
console.warn("WARNING! ShortRestDialog.longRestDialog has been deprecated, use LongRestDialog.longRestDialog instead.");
|
||||
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);
|
||||
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
|
||||
|
|
|
@ -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) {
|
||||
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);
|
||||
CharacterImporter.addProfessions(sourceCharacter, actor);
|
||||
}
|
||||
|
||||
const profession = sourceCharacter.attribs.find(e => e.name == "class").current;
|
||||
let professionLevel = sourceCharacter.attribs.find(e => e.name == "class_display").current;
|
||||
professionLevel = parseInt( professionLevel.replace(/[^0-9]/g,'') ); //remove a-z, leaving only integers
|
||||
CharacterImporter.addClasses(profession, professionLevel, actor);
|
||||
// Parse all classes and add them to already created actor.
|
||||
// "class" is a reserved word, therefore I use profession where I can.
|
||||
static async addProfessions(sourceCharacter, actor) {
|
||||
let result = [];
|
||||
|
||||
// parse all class and multiclassX items
|
||||
// couldn't get Array.filter to work here for some reason
|
||||
// result = array of objects. each object is a separate class
|
||||
sourceCharacter.attribs.forEach((e) => {
|
||||
if (CharacterImporter.classOrMulticlass(e.name)) {
|
||||
var t = {
|
||||
profession: CharacterImporter.capitalize(e.current),
|
||||
type: CharacterImporter.baseOrMulti(e.name),
|
||||
level: CharacterImporter.getLevel(e, sourceCharacter)
|
||||
};
|
||||
result.push(t);
|
||||
}
|
||||
});
|
||||
|
||||
// pull classes directly from system compendium and add them to current actor
|
||||
const professionsPack = await game.packs.get("sw5e.classes").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').getContent();
|
||||
let assignedClass = classes.find( c => c.name === profession );
|
||||
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 classOrMulticlass(name) {
|
||||
return name === "class" || (name.includes("multiclass") && name.length <= 12);
|
||||
}
|
||||
|
||||
static baseOrMulti(name) {
|
||||
if (name === "class") {
|
||||
return "base_class";
|
||||
} else {
|
||||
return "multi_class";
|
||||
}
|
||||
}
|
||||
|
||||
static getLevel(item, sourceCharacter) {
|
||||
if (item.name === "class") {
|
||||
let result = sourceCharacter.attribs.find((e) => e.name === "base_level").current;
|
||||
return parseInt(result);
|
||||
} else {
|
||||
let result = sourceCharacter.attribs.find((e) => e.name === `${item.name}_lvl`).current;
|
||||
return parseInt(result);
|
||||
}
|
||||
}
|
||||
|
||||
static capitalize(str) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
static async addSpecies(race, actor) {
|
||||
const species = await game.packs.get("sw5e.species").getDocuments();
|
||||
const assignedSpecies = species.find((c) => c.name === race);
|
||||
const activeEffects = [...assignedSpecies.data.effects][0].data.changes;
|
||||
const actorData = {data: {abilities: {...actor.data.data.abilities}}};
|
||||
|
||||
activeEffects.map((effect) => {
|
||||
switch (effect.key) {
|
||||
case "data.abilities.str.value":
|
||||
actorData.data.abilities.str.value -= effect.value;
|
||||
break;
|
||||
|
||||
case "data.abilities.dex.value":
|
||||
actorData.data.abilities.dex.value -= effect.value;
|
||||
break;
|
||||
|
||||
case "data.abilities.con.value":
|
||||
actorData.data.abilities.con.value -= effect.value;
|
||||
break;
|
||||
|
||||
case "data.abilities.int.value":
|
||||
actorData.data.abilities.int.value -= effect.value;
|
||||
break;
|
||||
|
||||
case "data.abilities.wis.value":
|
||||
actorData.data.abilities.wis.value -= effect.value;
|
||||
break;
|
||||
|
||||
case "data.abilities.cha.value":
|
||||
actorData.data.abilities.cha.value -= effect.value;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
actor.update(actorData);
|
||||
|
||||
await actor.createEmbeddedDocuments("Item", [assignedSpecies.data], {displaySheet: false});
|
||||
}
|
||||
|
||||
static async addPowers(powers, actor) {
|
||||
const forcePowers = await game.packs.get("sw5e.forcepowers").getDocuments();
|
||||
const techPowers = await game.packs.get("sw5e.techpowers").getDocuments();
|
||||
|
||||
for (const power of powers) {
|
||||
const createdPower = forcePowers.find((c) => c.name === power) || techPowers.find((c) => c.name === power);
|
||||
|
||||
if (createdPower) {
|
||||
await actor.createEmbeddedDocuments("Item", [createdPower.data], {displaySheet: false});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static async addItems(items, actor) {
|
||||
const weapons = await game.packs.get("sw5e.weapons").getDocuments();
|
||||
const armors = await game.packs.get("sw5e.armor").getDocuments();
|
||||
const adventuringGear = await game.packs.get("sw5e.adventuringgear").getDocuments();
|
||||
|
||||
for (const item of items) {
|
||||
const createdItem =
|
||||
weapons.find((c) => c.name.toLowerCase() === item.name.toLowerCase()) ||
|
||||
armors.find((c) => c.name.toLowerCase() === item.name.toLowerCase()) ||
|
||||
adventuringGear.find((c) => c.name.toLowerCase() === item.name.toLowerCase());
|
||||
|
||||
if (createdItem) {
|
||||
if (item.quantity != 1) {
|
||||
createdItem.data.data.quantity = item.quantity;
|
||||
}
|
||||
|
||||
await actor.createEmbeddedDocuments("Item", [createdItem.data], {displaySheet: false});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static addImportButton(html) {
|
||||
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);
|
||||
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(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>';
|
||||
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,4 +1,3 @@
|
|||
|
||||
/**
|
||||
* Highlight critical success or failure on d20 rolls
|
||||
*/
|
||||
|
@ -11,9 +10,9 @@ export const highlightCriticalSuccessFailure = function(message, html, data) {
|
|||
const d = roll.dice[0];
|
||||
|
||||
// Ensure it is an un-modified d20 roll
|
||||
const isD20 = (d.faces === 20) && ( d.values.length === 1 );
|
||||
const isD20 = d.faces === 20 && d.values.length === 1;
|
||||
if (!isD20) return;
|
||||
const isModifiedRoll = ("success" in d.results[0]) || d.options.marginSuccess || d.options.marginFailure;
|
||||
const isModifiedRoll = "success" in d.results[0] || d.options.marginSuccess || d.options.marginFailure;
|
||||
if (isModifiedRoll) return;
|
||||
|
||||
// Highlight successes and failures
|
||||
|
@ -40,14 +39,14 @@ export const displayChatActionButtons = function(message, html, data) {
|
|||
|
||||
// If the user is the message author or the actor owner, proceed
|
||||
let actor = game.actors.get(data.message.speaker.actor);
|
||||
if ( actor && actor.owner ) return;
|
||||
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"
|
||||
btn.style.display = "none";
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -64,34 +63,34 @@ 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 => {
|
||||
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;
|
||||
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(" + ");
|
||||
return parts.filter((p) => p !== null).join(" + ");
|
||||
};
|
||||
|
|
1414
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`);
|
||||
}
|
21
module/effects.js
vendored
|
@ -10,13 +10,15 @@ export function onManageActiveEffect(event, owner) {
|
|||
const effect = li.dataset.effectId ? owner.effects.get(li.dataset.effectId) : null;
|
||||
switch (a.dataset.action) {
|
||||
case "create":
|
||||
return ActiveEffect.create({
|
||||
label: "New Effect",
|
||||
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,22 +34,21 @@ 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: []
|
||||
}
|
||||
};
|
||||
|
|
|
@ -18,9 +18,9 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
/** @inheritdoc */
|
||||
static get defaultOptions() {
|
||||
return mergeObject(super.defaultOptions, {
|
||||
return foundry.utils.mergeObject(super.defaultOptions, {
|
||||
width: 560,
|
||||
height: 400,
|
||||
classes: ["sw5e", "sheet", "item"],
|
||||
|
@ -32,7 +32,7 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
/** @inheritdoc */
|
||||
get template() {
|
||||
const path = "systems/sw5e/templates/items/";
|
||||
return `${path}/${this.item.data.type}.html`;
|
||||
|
@ -43,33 +43,39 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
/** @override */
|
||||
async getData(options) {
|
||||
const data = super.getData(options);
|
||||
const itemData = data.data;
|
||||
data.labels = this.item.labels;
|
||||
data.config = CONFIG.SW5E;
|
||||
|
||||
// Item Type, Status, and Details
|
||||
data.itemType = game.i18n.localize(`ITEM.Type${data.item.type.titleCase()}`);
|
||||
data.itemStatus = this._getItemStatus(data.item);
|
||||
data.itemProperties = this._getItemProperties(data.item);
|
||||
data.isPhysical = data.item.data.hasOwnProperty("quantity");
|
||||
data.itemStatus = this._getItemStatus(itemData);
|
||||
data.itemProperties = this._getItemProperties(itemData);
|
||||
data.isPhysical = itemData.data.hasOwnProperty("quantity");
|
||||
|
||||
// Potential consumption targets
|
||||
data.abilityConsumptionTargets = this._getItemConsumptionTargets(data.item);
|
||||
data.abilityConsumptionTargets = this._getItemConsumptionTargets(itemData);
|
||||
|
||||
// Action Detail
|
||||
// Action Details
|
||||
data.hasAttackRoll = this.item.hasAttack;
|
||||
data.isHealing = data.item.data.actionType === "heal";
|
||||
data.isFlatDC = getProperty(data.item.data, "save.scaling") === "flat";
|
||||
data.isLine = ["line", "wall"].includes(data.item.data.target?.type);
|
||||
data.isHealing = itemData.data.actionType === "heal";
|
||||
data.isFlatDC = getProperty(itemData, "data.save.scaling") === "flat";
|
||||
data.isLine = ["line", "wall"].includes(itemData.data.target?.type);
|
||||
|
||||
// Original maximum uses formula
|
||||
if (this.item._data.data?.uses?.max) data.data.uses.max = this.item._data.data.uses.max;
|
||||
const sourceMax = foundry.utils.getProperty(this.item.data._source, "data.uses.max");
|
||||
if (sourceMax) itemData.data.uses.max = sourceMax;
|
||||
|
||||
// Vehicles
|
||||
data.isCrewed = data.item.data.activation?.type === "crew";
|
||||
data.isMountable = this._isItemMountable(data.item);
|
||||
data.isCrewed = itemData.data.activation?.type === "crew";
|
||||
data.isMountable = this._isItemMountable(itemData);
|
||||
|
||||
// Prepare Active Effects
|
||||
data.effects = prepareActiveEffectCategories(this.entity.effects);
|
||||
data.effects = prepareActiveEffectCategories(this.item.effects);
|
||||
|
||||
// Re-define the template data references (backwards compatible)
|
||||
data.item = itemData;
|
||||
data.data = itemData.data;
|
||||
return data;
|
||||
}
|
||||
|
||||
|
@ -102,9 +108,11 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
|
||||
// Attributes
|
||||
else if (consume.type === "attribute") {
|
||||
const attributes = Object.values(CombatTrackerConfig.prototype.getAttributeChoices())[0]; // Bit of a hack
|
||||
return attributes.reduce((obj, a) => {
|
||||
obj[a] = a;
|
||||
const attributes = TokenDocument.getTrackedAttributes(actor.data.data);
|
||||
attributes.bar.forEach((a) => a.push("value"));
|
||||
return attributes.bar.concat(attributes.value).reduce((obj, a) => {
|
||||
let k = a.join(".");
|
||||
obj[k] = k;
|
||||
return obj;
|
||||
}, {});
|
||||
}
|
||||
|
@ -128,7 +136,10 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
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.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,7 +261,7 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
/* Form Submission */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
/** @inheritdoc */
|
||||
_getSubmitData(updateData = {}) {
|
||||
// Create the expanded update data object
|
||||
const fd = new FormDataExtended(this.form, {editors: this.editors});
|
||||
|
@ -264,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(
|
||||
|
@ -303,7 +318,7 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
if (a.classList.contains("delete-damage")) {
|
||||
await this._onSubmit(event); // Submit any unsaved changes
|
||||
const li = a.closest(".damage-part");
|
||||
const damage = duplicate(this.item.data.data.damage);
|
||||
const damage = foundry.utils.deepClone(this.item.data.data.damage);
|
||||
damage.parts.splice(Number(li.dataset.damagePart), 1);
|
||||
return this.item.update({"data.damage.parts": damage.parts});
|
||||
}
|
||||
|
@ -312,33 +327,42 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle spawning the TraitSelector application which allows a checkbox of multiple trait options
|
||||
* Handle spawning the TraitSelector application for selection various options.
|
||||
* @param {Event} event The click event which originated the selection
|
||||
* @private
|
||||
*/
|
||||
_onConfigureClassSkills(event) {
|
||||
_onConfigureTraits(event) {
|
||||
event.preventDefault();
|
||||
const skills = this.item.data.data.skills;
|
||||
const choices = skills.choices && skills.choices.length ? skills.choices : Object.keys(CONFIG.SW5E.skills);
|
||||
const a = event.currentTarget;
|
||||
const label = a.parentElement;
|
||||
|
||||
// Render the Trait Selector dialog
|
||||
new TraitSelector(this.item, {
|
||||
const options = {
|
||||
name: a.dataset.target,
|
||||
title: label.innerText,
|
||||
choices: Object.entries(CONFIG.SW5E.skills).reduce((obj, e) => {
|
||||
if (choices.includes(e[0])) obj[e[0]] = e[1];
|
||||
return obj;
|
||||
}, {}),
|
||||
minimum: skills.number,
|
||||
maximum: skills.number
|
||||
}).render(true);
|
||||
title: a.parentElement.innerText,
|
||||
choices: [],
|
||||
allowCustom: false
|
||||
};
|
||||
|
||||
switch (a.dataset.options) {
|
||||
case "saves":
|
||||
options.choices = CONFIG.SW5E.abilities;
|
||||
options.valueKey = null;
|
||||
break;
|
||||
case "skills":
|
||||
const skills = this.item.data.data.skills;
|
||||
const choiceSet =
|
||||
skills.choices && skills.choices.length ? skills.choices : Object.keys(CONFIG.SW5E.skills);
|
||||
options.choices = Object.fromEntries(
|
||||
Object.entries(CONFIG.SW5E.skills).filter((skill) => choiceSet.includes(skill[0]))
|
||||
);
|
||||
options.maximum = skills.number;
|
||||
break;
|
||||
}
|
||||
new TraitSelector(this.item, options).render(true);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
/** @inheritdoc */
|
||||
async _onSubmit(...args) {
|
||||
if (this._tabs[0].active === "details") this.position.height = "auto";
|
||||
await super._onSubmit(...args);
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
/* -------------------------------------------- */
|
||||
/* Hotbar Macros */
|
||||
/* -------------------------------------------- */
|
||||
|
@ -17,7 +16,7 @@ export async function create5eMacro(data, slot) {
|
|||
|
||||
// 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));
|
||||
let macro = game.macros.entities.find((m) => m.name === item.name && m.command === command);
|
||||
if (!macro) {
|
||||
macro = await Macro.create({
|
||||
name: item.name,
|
||||
|
@ -46,9 +45,11 @@ export function rollItemMacro(itemName) {
|
|||
if (!actor) actor = game.actors.get(speaker.actor);
|
||||
|
||||
// Get matching items
|
||||
const items = actor ? actor.items.filter(i => i.name === itemName) : [];
|
||||
const items = actor ? actor.items.filter((i) => i.name === itemName) : [];
|
||||
if (items.length > 1) {
|
||||
ui.notifications.warn(`Your controlled Actor ${actor.name} has more than one Item with name ${itemName}. The first matched item will be chosen.`);
|
||||
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}`);
|
||||
}
|
||||
|
|
|
@ -3,14 +3,17 @@
|
|||
* @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});
|
||||
ui.notifications.info(
|
||||
`Applying SW5e System Migration for version ${game.system.data.version}. Please be patient and do not close your game or shut down your server.`,
|
||||
{permanent: true}
|
||||
);
|
||||
|
||||
// Migrate World Actors
|
||||
for await ( let a of game.actors.entities ) {
|
||||
for await (let a of game.actors.contents) {
|
||||
try {
|
||||
console.log(`Checking Actor entity ${a.name} for migration needs`);
|
||||
const updateData = await migrateActorData(a.data);
|
||||
if ( !isObjectEmpty(updateData) ) {
|
||||
if (!foundry.utils.isObjectEmpty(updateData)) {
|
||||
console.log(`Migrating Actor entity ${a.name}`);
|
||||
await a.update(updateData, {enforceTypes: false});
|
||||
}
|
||||
|
@ -21,10 +24,10 @@ export const migrateWorld = async function() {
|
|||
}
|
||||
|
||||
// Migrate World Items
|
||||
for ( let i of game.items.entities ) {
|
||||
for (let i of game.items.contents) {
|
||||
try {
|
||||
const updateData = migrateItemData(i.data);
|
||||
if ( !isObjectEmpty(updateData) ) {
|
||||
const updateData = migrateItemData(i.toObject());
|
||||
if (!foundry.utils.isObjectEmpty(updateData)) {
|
||||
console.log(`Migrating Item entity ${i.name}`);
|
||||
await i.update(updateData, {enforceTypes: false});
|
||||
}
|
||||
|
@ -35,12 +38,15 @@ export const migrateWorld = async function() {
|
|||
}
|
||||
|
||||
// Migrate Actor Override Tokens
|
||||
for ( let s of game.scenes.entities ) {
|
||||
for (let s of game.scenes.contents) {
|
||||
try {
|
||||
const updateData = await migrateSceneData(s.data);
|
||||
if ( !isObjectEmpty(updateData) ) {
|
||||
if (!foundry.utils.isObjectEmpty(updateData)) {
|
||||
console.log(`Migrating Scene entity ${s.name}`);
|
||||
await s.update(updateData, {enforceTypes: false});
|
||||
// If we do not do this, then synthetic token actors remain in cache
|
||||
// with the un-updated actorData.
|
||||
s.tokens.contents.forEach((t) => (t._actor = null));
|
||||
}
|
||||
} catch (err) {
|
||||
err.message = `Failed sw5e system migration for Scene ${s.name}: ${err.message}`;
|
||||
|
@ -77,40 +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}`);
|
||||
};
|
||||
|
||||
|
@ -128,34 +131,40 @@ export const migrateActorData = async function(actor) {
|
|||
const updateData = {};
|
||||
|
||||
// Actor Data Updates
|
||||
if (actor.data) {
|
||||
_migrateActorMovement(actor, updateData);
|
||||
_migrateActorSenses(actor, updateData);
|
||||
_migrateActorType(actor, updateData);
|
||||
}
|
||||
|
||||
// Migrate Owned Items
|
||||
if (!!actor.items) {
|
||||
let hasItemUpdates = false;
|
||||
const items = await actor.items.reduce(async (memo, i) => {
|
||||
const results = await memo;
|
||||
|
||||
// Migrate the Owned Item
|
||||
let itemUpdate = await migrateActorItemData(i, actor);
|
||||
const itemData = i instanceof CONFIG.Item.documentClass ? i.toObject() : i;
|
||||
let itemUpdate = await migrateActorItemData(itemData, actor);
|
||||
|
||||
// Prepared, Equipped, and Proficient for NPC actors
|
||||
if (actor.type === "npc") {
|
||||
if (getProperty(i.data, "preparation.prepared") === false) itemUpdate["data.preparation.prepared"] = true;
|
||||
if (getProperty(i.data, "equipped") === false) itemUpdate["data.equipped"] = true;
|
||||
if (getProperty(i.data, "proficient") === false) itemUpdate["data.proficient"] = true;
|
||||
if (getProperty(itemData.data, "preparation.prepared") === false)
|
||||
itemUpdate["data.preparation.prepared"] = true;
|
||||
if (getProperty(itemData.data, "equipped") === false) itemUpdate["data.equipped"] = true;
|
||||
if (getProperty(itemData.data, "proficient") === false) itemUpdate["data.proficient"] = true;
|
||||
}
|
||||
|
||||
// Update the Owned Item
|
||||
if (!isObjectEmpty(itemUpdate)) {
|
||||
hasItemUpdates = true;
|
||||
itemUpdate._id = itemData._id;
|
||||
console.log(`Migrating Actor ${actor.name}'s ${i.name}`);
|
||||
return [...results, mergeObject(i, itemUpdate, {enforceTypes: false, inplace: false})];
|
||||
} else return [...results, i];
|
||||
results.push(expandObject(itemUpdate));
|
||||
}
|
||||
|
||||
return results;
|
||||
}, []);
|
||||
|
||||
if ( hasItemUpdates ) updateData.items = items;
|
||||
if (items.length > 0) updateData.items = items;
|
||||
}
|
||||
|
||||
// Update NPC data with new datamodel information
|
||||
|
@ -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);
|
||||
|
@ -196,12 +203,13 @@ 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) {
|
||||
const updateData = {};
|
||||
|
@ -234,24 +242,34 @@ export const migrateActorItemData = async function(item, actor) {
|
|||
* @return {Object} The updateData to apply
|
||||
*/
|
||||
export const migrateSceneData = async function (scene) {
|
||||
const tokens = duplicate(scene.tokens);
|
||||
return {
|
||||
tokens: await Promise.all(tokens.map(async (t) => {
|
||||
if (!t.actorId || t.actorLink || !t.actorData.data) {
|
||||
const tokens = await Promise.all(
|
||||
scene.tokens.map(async (token) => {
|
||||
const t = token.toJSON();
|
||||
if (!t.actorId || t.actorLink) {
|
||||
t.actorData = {};
|
||||
return t;
|
||||
}
|
||||
const token = new Token(t);
|
||||
if ( !token.actor ) {
|
||||
} else if (!game.actors.has(t.actorId)) {
|
||||
t.actorId = null;
|
||||
t.actorData = {};
|
||||
} else if (!t.actorLink) {
|
||||
const updateData = await migrateActorData(token.data.actorData);
|
||||
t.actorData = mergeObject(token.data.actorData, updateData);
|
||||
const actorData = duplicate(t.actorData);
|
||||
actorData.type = token.actor?.type;
|
||||
const update = migrateActorData(actorData);
|
||||
["items", "effects"].forEach((embeddedName) => {
|
||||
if (!update[embeddedName]?.length) return;
|
||||
const updates = new Map(update[embeddedName].map((u) => [u._id, u]));
|
||||
t.actorData[embeddedName].forEach((original) => {
|
||||
const update = updates.get(original._id);
|
||||
if (update) mergeObject(original, update);
|
||||
});
|
||||
delete update[embeddedName];
|
||||
});
|
||||
|
||||
mergeObject(t.actorData, update);
|
||||
}
|
||||
return t;
|
||||
}))
|
||||
};
|
||||
})
|
||||
);
|
||||
return {tokens};
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -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 => {
|
||||
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;
|
||||
|
@ -296,7 +320,9 @@ function _updateNPCData(actor) {
|
|||
const itemData = i.data;
|
||||
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 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;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -407,7 +431,7 @@ function _migrateActorPowers(actorData, updateData) {
|
|||
// Remove the Power DC Bonus
|
||||
updateData["data.bonuses.power.-=dc"] = null;
|
||||
|
||||
return updateData
|
||||
return updateData;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -420,6 +444,7 @@ function _migrateActorSenses(actor, updateData) {
|
|||
const ad = actor.data;
|
||||
if (ad?.traits?.senses === undefined) return;
|
||||
const original = ad.traits.senses || "";
|
||||
if (typeof original !== "string") return;
|
||||
|
||||
// Try to match old senses with the format like "Darkvision 60 ft, Blindsight 30 ft"
|
||||
const pattern = /([A-z]+)\s?([0-9]+)\s?([A-z]+)?/;
|
||||
|
@ -449,6 +474,86 @@ function _migrateActorSenses(actor, updateData) {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Migrate the actor details.type string to object
|
||||
* @private
|
||||
*/
|
||||
function _migrateActorType(actor, updateData) {
|
||||
const ad = actor.data;
|
||||
const original = ad.details?.type;
|
||||
if (typeof original !== "string") return;
|
||||
|
||||
// New default data structure
|
||||
let data = {
|
||||
value: "",
|
||||
subtype: "",
|
||||
swarm: "",
|
||||
custom: ""
|
||||
};
|
||||
|
||||
// Specifics
|
||||
// (Some of these have weird names, these need to be addressed individually)
|
||||
if (original === "force entity") {
|
||||
data.value = "force";
|
||||
data.subtype = "storm";
|
||||
} else if (original === "human") {
|
||||
data.value = "humanoid";
|
||||
data.subtype = "human";
|
||||
} else if (["humanoid (any)", "humanoid (Villainous"].includes(original)) {
|
||||
data.value = "humanoid";
|
||||
} else if (original === "tree") {
|
||||
data.value = "plant";
|
||||
data.subtype = "tree";
|
||||
} else if (original === "(humanoid) or Large (beast) force entity") {
|
||||
data.value = "force";
|
||||
} else if (original === "droid (appears human)") {
|
||||
data.value = "droid";
|
||||
} else {
|
||||
// Match the existing string
|
||||
const pattern = /^(?:swarm of (?<size>[\w\-]+) )?(?<type>[^(]+?)(?:\((?<subtype>[^)]+)\))?$/i;
|
||||
const match = original.trim().match(pattern);
|
||||
if (match) {
|
||||
// Match a known creature type
|
||||
const typeLc = match.groups.type.trim().toLowerCase();
|
||||
const typeMatch = Object.entries(CONFIG.SW5E.creatureTypes).find(([k, v]) => {
|
||||
return (
|
||||
typeLc === k ||
|
||||
typeLc === game.i18n.localize(v).toLowerCase() ||
|
||||
typeLc === game.i18n.localize(`${v}Pl`).toLowerCase()
|
||||
);
|
||||
});
|
||||
if (typeMatch) data.value = typeMatch[0];
|
||||
else {
|
||||
data.value = "custom";
|
||||
data.custom = match.groups.type.trim().titleCase();
|
||||
}
|
||||
data.subtype = match.groups.subtype?.trim().titleCase() || "";
|
||||
|
||||
// Match a swarm
|
||||
const isNamedSwarm = actor.name.startsWith(game.i18n.localize("SW5E.CreatureSwarm"));
|
||||
if (match.groups.size || isNamedSwarm) {
|
||||
const sizeLc = match.groups.size ? match.groups.size.trim().toLowerCase() : "tiny";
|
||||
const sizeMatch = Object.entries(CONFIG.SW5E.actorSizes).find(([k, v]) => {
|
||||
return sizeLc === k || sizeLc === game.i18n.localize(v).toLowerCase();
|
||||
});
|
||||
data.swarm = sizeMatch ? sizeMatch[0] : "tiny";
|
||||
} else data.swarm = "";
|
||||
}
|
||||
|
||||
// No match found
|
||||
else {
|
||||
data.value = "custom";
|
||||
data.custom = original;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the actor data
|
||||
updateData["data.details.type"] = data;
|
||||
return updateData;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
|
@ -456,19 +561,34 @@ function _migrateItemClassPowerCasting(item, updateData) {
|
|||
if (item.type === "class") {
|
||||
switch (item.name) {
|
||||
case "Consular":
|
||||
updateData["data.powercasting"] = "consular";
|
||||
updateData["data.powercasting"] = {
|
||||
progression: "consular",
|
||||
ability: ""
|
||||
};
|
||||
break;
|
||||
case "Engineer":
|
||||
updateData["data.powercasting"] = "engineer";
|
||||
updateData["data.powercasting"] = {
|
||||
progression: "engineer",
|
||||
ability: ""
|
||||
};
|
||||
break;
|
||||
case "Guardian":
|
||||
updateData["data.powercasting"] = "guardian";
|
||||
updateData["data.powercasting"] = {
|
||||
progression: "guardian",
|
||||
ability: ""
|
||||
};
|
||||
break;
|
||||
case "Scout":
|
||||
updateData["data.powercasting"] = "scout";
|
||||
updateData["data.powercasting"] = {
|
||||
progression: "scout",
|
||||
ability: ""
|
||||
};
|
||||
break;
|
||||
case "Sentinel":
|
||||
updateData["data.powercasting"] = "sentinel";
|
||||
updateData["data.powercasting"] = {
|
||||
progression: "sentinel",
|
||||
ability: ""
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -552,10 +679,10 @@ export async function purgeFlags(pack) {
|
|||
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 => {
|
||||
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,7 +692,6 @@ 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
|
||||
|
@ -577,8 +703,7 @@ export function removeDeprecatedObjects(data) {
|
|||
if (v._deprecated === true) {
|
||||
console.log(`Deleting deprecated object key ${k}`);
|
||||
delete data[k];
|
||||
}
|
||||
else removeDeprecatedObjects(v);
|
||||
} else removeDeprecatedObjects(v);
|
||||
}
|
||||
}
|
||||
return data;
|
||||
|
|
|
@ -5,7 +5,6 @@ import { SW5E } from "../config.js";
|
|||
* @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
|
||||
|
@ -19,7 +18,7 @@ export default class AbilityTemplate extends MeasuredTemplate {
|
|||
// Prepare template data
|
||||
const templateData = {
|
||||
t: templateShape,
|
||||
user: game.user._id,
|
||||
user: game.user.data._id,
|
||||
distance: target.value,
|
||||
direction: 0,
|
||||
x: 0,
|
||||
|
@ -45,10 +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;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -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;
|
||||
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 => {
|
||||
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 () {
|
||||
|
||||
/**
|
||||
* 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"
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
*/
|
||||
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);
|
||||
}
|
||||
}
|
12
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",
|
||||
|
@ -3068,9 +3068,9 @@
|
|||
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
|
||||
},
|
||||
"y18n": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
|
||||
"integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE="
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz",
|
||||
"integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ=="
|
||||
},
|
||||
"yargs": {
|
||||
"version": "7.1.1",
|
||||
|
|
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/Defensive Technique.webp
Normal file
After Width: | Height: | Size: 7.8 KiB |
BIN
packs/Icons/Force Powers/Kinetite.webp
Normal file
After Width: | Height: | Size: 8.7 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 |
BIN
packs/Icons/Martial Vibroweapons/Chained Dagger.webp
Normal file
After Width: | Height: | Size: 9.1 KiB |
BIN
packs/Icons/Martial Vibroweapons/Disguised Blade.webp
Normal file
After Width: | Height: | Size: 7.3 KiB |
BIN
packs/Icons/Martial Vibroweapons/Disruptorshiv.webp
Normal file
After Width: | Height: | Size: 8 KiB |
BIN
packs/Icons/Martial Vibroweapons/Echostaff.webp
Normal file
After Width: | Height: | Size: 6.8 KiB |
BIN
packs/Icons/Martial Vibroweapons/Electrobaton.webp
Normal file
After Width: | Height: | Size: 7.1 KiB |
BIN
packs/Icons/Martial Vibroweapons/Electrohammer.webp
Normal file
After Width: | Height: | Size: 8.3 KiB |