Super VJ Update

This commit is contained in:
Kakeman89 2020-09-11 17:11:11 -04:00
parent 442212bdea
commit 1983b74bde
59 changed files with 4640 additions and 2462 deletions

View file

@ -5,25 +5,41 @@
"SW5E.AbbreviationConc": "Conc.", "SW5E.AbbreviationConc": "Conc.",
"SW5E.AbbreviationDC": "DC", "SW5E.AbbreviationDC": "DC",
"SW5E.AbbreviationLR": "LR", "SW5E.AbbreviationLR": "LR",
"SW5E.AbbreviationLevel": "Lvl.",
"SW5E.AbbreviationLbs": "lbs.", "SW5E.AbbreviationLbs": "lbs.",
"SW5E.AbbreviationSR": "SR", "SW5E.AbbreviationSR": "SR",
"SW5E.Ability": "Ability", "SW5E.Ability": "Ability",
"SW5E.AbilityCha": "Charisma",
"SW5E.AbilityCon": "Constitution",
"SW5E.AbilityDex": "Dexterity",
"SW5E.AbilityInt": "Intelligence",
"SW5E.AbilityModifier": "Ability Modifier",
"SW5E.AbilityStr": "Strength", "SW5E.AbilityStr": "Strength",
"SW5E.AbilityUseCantUse": "You are not currently able to use this ability!", "SW5E.AbilityStrAbbr": "str",
"SW5E.AbilityUseCharged": "charged", "SW5E.AbilityCon": "Constitution",
"SW5E.AbilityUseConsume": "Consume Available Usage?", "SW5E.AbilityConAbbr": "con",
"SW5E.AbilityUseDepleted": "depleted", "SW5E.AbilityDex": "Dexterity",
"SW5E.AbilityUseHint": "Configure how you would like to use the", "SW5E.AbilityDexAbbr": "dex",
"SW5E.AbilityUseRechargeHint": "This ability uses a recharge mechanic and is currently", "SW5E.AbilityInt": "Intelligence",
"SW5E.AbilityUseWarnEnd": "available uses per", "SW5E.AbilityIntAbbr": "int",
"SW5E.AbilityUseWarnStart": "This ability has",
"SW5E.AbilityWis": "Wisdom", "SW5E.AbilityWis": "Wisdom",
"SW5E.AbilityWisAbbr": "wis",
"SW5E.AbilityCha": "Charisma",
"SW5E.AbilityChaAbbr": "cha",
"SW5E.AbilityModifier": "Ability Modifier",
"SW5E.AbilityPromptText": "What type of {ability} check?",
"SW5E.AbilityPromptTitle": "{ability} Ability Check",
"SW5E.AbilityUseHint": "Configure how you would like to use the {name} {type}.",
"SW5E.AbilityUseUnavailableHint": "There are no uses of this item remaining!",
"SW5E.AbilityUseChargedHint": "This {type} is charged and ready to use!",
"SW5E.AbilityUseRechargedHint": "This {type} is depleted and must be recharged!",
"SW5E.AbilityUseNormalHint": "This {type} has {value} of {max} uses per {per} remaining.",
"SW5E.AbilityUseConsumableChargeHint": "Using this {type} will consume 1 charge of {value} remaining.",
"SW5E.AbilityUseConsumableQuantityHint": "Using this {type} will consume 1 quantity of {quantity} remaining",
"SW5E.AbilityUseConsumableDestroyHint": "Using this {type} will consume its final charge and it will be destroyed.",
"SW5E.AbilityUseConsume": "Consume Available Usage?",
"SW5E.AbilityUseChargesLabel": "{value} Charges",
"SW5E.AbilityUseConsumableLabel": "{max} per {per}",
"SW5E.AbilityUseCast": "Cast Spell",
"SW5E.AbilityUseUse": "Use Ability",
"SW5E.Action": "Action", "SW5E.Action": "Action",
"SW5E.ActionPl": "Actions",
"SW5E.ActionAbil": "Ability Check", "SW5E.ActionAbil": "Ability Check",
"SW5E.ActionHeal": "Healing", "SW5E.ActionHeal": "Healing",
"SW5E.ActionMPAK": "Melee Power Attack", "SW5E.ActionMPAK": "Melee Power Attack",
@ -33,6 +49,8 @@
"SW5E.ActionRWAK": "Ranged Weapon Attack", "SW5E.ActionRWAK": "Ranged Weapon Attack",
"SW5E.ActionSave": "Saving Throw", "SW5E.ActionSave": "Saving Throw",
"SW5E.ActionUtil": "Utility", "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.Add": "Add", "SW5E.Add": "Add",
"SW5E.Advantage": "Advantage", "SW5E.Advantage": "Advantage",
"SW5E.Alignment": "Alignment", "SW5E.Alignment": "Alignment",
@ -44,9 +62,10 @@
"SW5E.AlignmentLN": "Lawful Neutral", "SW5E.AlignmentLN": "Lawful Neutral",
"SW5E.AlignmentND": "Neutral Dark", "SW5E.AlignmentND": "Neutral Dark",
"SW5E.AlignmentNL": "Neutral Light", "SW5E.AlignmentNL": "Neutral Light",
"SW5E.AlignmentBN": "Balenced Neutral", "SW5E.AlignmentBN": "Balanced Neutral",
"SW5E.Archetypes": "Archetypes", "SW5E.Archetypes": "Archetypes",
"SW5E.ArmorClass": "Armor Class", "SW5E.ArmorClass": "Armor Class",
"SW5E.AC": "AC",
"SW5E.ArmorProperties": "Armor Properties", "SW5E.ArmorProperties": "Armor Properties",
"SW5E.ArmorProperAbsorptive": "Absorptive", "SW5E.ArmorProperAbsorptive": "Absorptive",
"SW5E.ArmorProperAgile": "Agile", "SW5E.ArmorProperAgile": "Agile",
@ -76,6 +95,8 @@
"SW5E.ArmorProperSteadfast": "Steadfast", "SW5E.ArmorProperSteadfast": "Steadfast",
"SW5E.ArmorProperVersatile": "Versatile", "SW5E.ArmorProperVersatile": "Versatile",
"SW5E.Attack": "Attack", "SW5E.Attack": "Attack",
"SW5E.AttackPl": "Attacks",
"SW5E.AttackRoll": "Attack Roll",
"SW5E.Attributes": "Attributes", "SW5E.Attributes": "Attributes",
"SW5E.Attuned": "Attuned", "SW5E.Attuned": "Attuned",
"SW5E.Background": "Background", "SW5E.Background": "Background",
@ -84,12 +105,12 @@
"SW5E.BonusAbilitySave": "Global Saving Throw Bonus", "SW5E.BonusAbilitySave": "Global Saving Throw Bonus",
"SW5E.BonusAbilitySkill": "Global Skill Check Bonus", "SW5E.BonusAbilitySkill": "Global Skill Check Bonus",
"SW5E.BonusAction": "Bonus Action", "SW5E.BonusAction": "Bonus Action",
"SW5E.BonusMSAttack": "Melee Power Attack Bonus", "SW5E.BonusMPAttack": "Melee Power Attack Bonus",
"SW5E.BonusMSDamage": "Melee Power Damage Bonus", "SW5E.BonusMPDamage": "Melee Power Damage Bonus",
"SW5E.BonusMWAttack": "Melee Weapon Attack Bonus", "SW5E.BonusMWAttack": "Melee Weapon Attack Bonus",
"SW5E.BonusMWDamage": "Melee Weapon Damage Bonus", "SW5E.BonusMWDamage": "Melee Weapon Damage Bonus",
"SW5E.BonusRSAttack": "Ranged Power Attack Bonus", "SW5E.BonusRPAttack": "Ranged Power Attack Bonus",
"SW5E.BonusRSDamage": "Ranged Power Damage Bonus", "SW5E.BonusRPDamage": "Ranged Power Damage Bonus",
"SW5E.BonusRWAttack": "Ranged Weapon Attack Bonus", "SW5E.BonusRWAttack": "Ranged Weapon Attack Bonus",
"SW5E.BonusRWDamage": "Ranged Weapon Damage Bonus", "SW5E.BonusRWDamage": "Ranged Weapon Damage Bonus",
"SW5E.BonusSaveForm": "Update Bonuses", "SW5E.BonusSaveForm": "Update Bonuses",
@ -116,6 +137,7 @@
"SW5E.ConBlinded": "Blinded", "SW5E.ConBlinded": "Blinded",
"SW5E.ConCharmed": "Charmed", "SW5E.ConCharmed": "Charmed",
"SW5E.ConDeafened": "Deafened", "SW5E.ConDeafened": "Deafened",
"SW5E.ConDiseased": "Diseased",
"SW5E.ConExhaustion": "Exhaustion", "SW5E.ConExhaustion": "Exhaustion",
"SW5E.ConFrightened": "Frightened", "SW5E.ConFrightened": "Frightened",
"SW5E.ConGrappled": "Grappled", "SW5E.ConGrappled": "Grappled",
@ -128,9 +150,22 @@
"SW5E.ConProne": "Prone", "SW5E.ConProne": "Prone",
"SW5E.ConRestrained": "Restrained", "SW5E.ConRestrained": "Restrained",
"SW5E.ConShocked": "Shocked", "SW5E.ConShocked": "Shocked",
"SW5E.ConSlowed": "Slowed",
"SW5E.ConStunned": "Stunned", "SW5E.ConStunned": "Stunned",
"SW5E.ConUnconscious": "Unconscious", "SW5E.ConUnconscious": "Unconscious",
"SW5E.Concentration": "Concentration", "SW5E.Concentration": "Concentration",
"SW5E.ConsumeTitle": "Resource Consumption",
"SW5E.ConsumeAmmunition": "Ammunition",
"SW5E.ConsumeAttribute": "Attribute",
"SW5E.ConsumeMaterial": "Material",
"SW5E.ConsumeCharges": "Item Uses",
"SW5E.ConsumeWarningNoResource": "{name} is designated to consume {type} but no resource is specified!",
"SW5E.ConsumeWarningNoSource": "The designated {type} source that {name} consumes no longer exists!",
"SW5E.ConsumeWarningNoQuantity": "{name} has run out of its designated {type}!",
"SW5E.ConsumeWarningZeroAttribute": "{name} has run out of its designated attribute resource pool!",
"SW5E.ConsumableAmmunition": "Ammunition",
"SW5E.ConsumableFood": "Food", "SW5E.ConsumableFood": "Food",
"SW5E.ConsumablePoison": "Poison", "SW5E.ConsumablePoison": "Poison",
"SW5E.ConsumableAdrenal": "Adrenal", "SW5E.ConsumableAdrenal": "Adrenal",
@ -139,8 +174,17 @@
"SW5E.ConsumableTrinket": "Trinket", "SW5E.ConsumableTrinket": "Trinket",
"SW5E.ConsumableTechnology": "Technology", "SW5E.ConsumableTechnology": "Technology",
"SW5E.ConsumableAmmunition": "Ammunition", "SW5E.ConsumableAmmunition": "Ammunition",
"SW5E.ConsumableUseWarnStart": "This consumable has",
"SW5E.ConsumableUseWarnEnd": "of the current unit",
"SW5E.ConsumableUnitWarn": "units remaining",
"SW5E.ConsumableLastChargeWarn": "This is the last charge of this unit and consuming it will also reduce the item's quantity by 1",
"SW5E.ConsumableWithoutCharges": "available units to use",
"SW5E.Consumed": "Consumed", "SW5E.Consumed": "Consumed",
"SW5E.CostGP": "Cost (GP)", "SW5E.Cover": "Cover",
"SW5E.CoverHalf": "Half",
"SW5E.CoverThreeQuarters": "Three Quarters",
"SW5E.CoverTotal": "Total",
"SW5E.CostGP": "Cost (CR)",
"SW5E.Critical": "Critical", "SW5E.Critical": "Critical",
"SW5E.CriticalHit": "Critical Hit", "SW5E.CriticalHit": "Critical Hit",
"SW5E.Currency": "Currency", "SW5E.Currency": "Currency",
@ -160,15 +204,22 @@
"SW5E.DamageKinetic": "Kinetic", "SW5E.DamageKinetic": "Kinetic",
"SW5E.DamageLightning": "Lightning", "SW5E.DamageLightning": "Lightning",
"SW5E.DamageNecrotic": "Necrotic", "SW5E.DamageNecrotic": "Necrotic",
"SW5E.DamagePhysical": "Physical",
"SW5E.DamagePoison": "Poison", "SW5E.DamagePoison": "Poison",
"SW5E.DamagePsychic": "Psychic", "SW5E.DamagePsychic": "Psychic",
"SW5E.DamageSonic": "Sonic", "SW5E.DamageSonic": "Sonic",
"SW5E.Day": "Day", "SW5E.Day": "Day",
"SW5E.DeathSave": "Death Saves", "SW5E.DeathSave": "Death Saves",
"SW5E.DeathSaveCriticalSuccess": "{name} critically succeeded on a death saving throw and has regained 1 Hit Point!",
"SW5E.DeathSaveSuccess": "{name} has survived with 3 death save successes and is now stable!",
"SW5E.DeathSaveFailure": "{name} has died with 3 death save failures!",
"SW5E.DeathSavingThrow": "Death Saving Throw",
"SW5E.DeathSaveUnnecessary": "You do not need to roll death saves because you have a positive number of hit points or have already reached 3 successes or failures.",
"SW5E.Default": "Default", "SW5E.Default": "Default",
"SW5E.DefaultAbilityCheck": "Default Ability Check", "SW5E.DefaultAbilityCheck": "Default Ability Check",
"SW5E.Description": "Description", "SW5E.Description": "Description",
"SW5E.Details": "Details", "SW5E.Details": "Details",
"SW5E.Dimensions": "Dimensions",
"SW5E.Disadvantage": "Disadvantage", "SW5E.Disadvantage": "Disadvantage",
"SW5E.DistAny": "Any", "SW5E.DistAny": "Any",
"SW5E.DistFt": "Feet", "SW5E.DistFt": "Feet",
@ -185,6 +236,7 @@
"SW5E.EquipmentShield": "Shield", "SW5E.EquipmentShield": "Shield",
"SW5E.EquipmentShieldProficiency": "Shields", "SW5E.EquipmentShieldProficiency": "Shields",
"SW5E.EquipmentTrinket": "Trinket", "SW5E.EquipmentTrinket": "Trinket",
"SW5E.EquipmentVehicle": "Vehicle Equipment",
"SW5E.Equipped": "Equipped", "SW5E.Equipped": "Equipped",
"SW5E.Exhaustion": "Exhaustion", "SW5E.Exhaustion": "Exhaustion",
"SW5E.Expertise": "Expertise", "SW5E.Expertise": "Expertise",
@ -208,6 +260,7 @@
"SW5E.ItemTypePowerPl": "Powers", "SW5E.ItemTypePowerPl": "Powers",
"SW5E.ItemTypeWeapon": "Weapon", "SW5E.ItemTypeWeapon": "Weapon",
"SW5E.ItemTypeWeaponPl": "Weapons", "SW5E.ItemTypeWeaponPl": "Weapons",
"SW5E.ItemNoUses": "{name} has no available uses remaining.",
"SW5E.FeatureActive": "Active Abilities", "SW5E.FeatureActive": "Active Abilities",
"SW5E.FeatureAdd": "Create Feature", "SW5E.FeatureAdd": "Create Feature",
@ -217,12 +270,37 @@
"SW5E.FeatureRechargeResult": "1d6 Result", "SW5E.FeatureRechargeResult": "1d6 Result",
"SW5E.FeatureUsage": "Feature Usage", "SW5E.FeatureUsage": "Feature Usage",
"SW5E.Features": "Features", "SW5E.Features": "Features",
"SW5E.FeetAbbr": "ft.",
"SW5E.Filter": "Filter", "SW5E.Filter": "Filter",
"SW5E.FilterNoPowers": "No powers found for this set of filters.", "SW5E.FilterNoPowers": "No powers found for this set of filters.",
"SW5E.NoPowerLevels": "This character has no powercaster levels, but you may add powers manually.", "SW5E.NoPowerLevels": "This character has no powercaster levels, but you may add powers manually.",
"SW5E.FlagsInstructions": "Configure character features and traits which fine-tune behaviors of the SW5e system.", "SW5E.FlagsInstructions": "Configure character features and traits which fine-tune behaviors of the SW5e system.",
"SW5E.FlagsSave": "Update Special Traits", "SW5E.FlagsSave": "Update Special Traits",
"SW5E.FlagsTitle": "Configure Special Traits", "SW5E.FlagsTitle": "Configure Special Traits",
"SW5E.FlagsPowerfulBuild": "Powerful Build",
"SW5E.FlagsPowerfulBuildHint": "Provides increased carrying capacity.",
"SW5E.FlagsSavageAttacks": "Savage Attacks",
"SW5E.FlagsSavageAttacksHint": "Adds extra critical hit weapon dice.",
"SW5E.FlagsElvenAccuracy": "Elven Accuracy",
"SW5E.FlagsElvenAccuracyHint": "Roll an extra d20 with advantage to Dex, Int, Wis, or Cha.",
"SW5E.FlagsHalflingLucky": "Halfling Lucky",
"SW5E.FlagsHalflingLuckyHint": "Reroll ones when rolling d20 checks.",
"SW5E.FlagsInitiativeAdv": "Advantage on Initiative",
"SW5E.FlagsInitiativeAdvHint": "Provided by feats or magical items.",
"SW5E.FlagsAlert": "Alert Feat",
"SW5E.FlagsAlertHint": "Provides +5 to Initiative.",
"SW5E.FlagsJOAT": "Jack of All Trades",
"SW5E.FlagsJOATHint": "Half-Proficiency to Ability Checks in which you are not already Proficient.",
"SW5E.FlagsObservant": "Observant Feat",
"SW5E.FlagsObservantHint": "Provides a +5 to passive Perception and Investigation.",
"SW5E.FlagsReliableTalent": "Reliable Talent",
"SW5E.FlagsReliableTalentHint": "Rogues Reliable Talent Feature.",
"SW5E.FlagsRemarkableAthlete": "Remarkable Athlete.",
"SW5E.FlagsRemarkableAthleteHint": "Half-Proficiency (rounded-up) to physical Ability Checks and Initiative.",
"SW5E.FlagsCritThreshold": "Critical Hit Threshold",
"SW5E.FlagsCritThresholdHint": "Allow for expanded critical range; for example Improved or Superior Critical",
"SW5E.Flat": "Flat", "SW5E.Flat": "Flat",
"SW5E.Formula": "Formula", "SW5E.Formula": "Formula",
"SW5E.GrantedAbilities": "Granted Abilities", "SW5E.GrantedAbilities": "Granted Abilities",
@ -230,9 +308,12 @@
"SW5E.Healing": "Healing", "SW5E.Healing": "Healing",
"SW5E.HealingTemp": "Healing (Temporary)", "SW5E.HealingTemp": "Healing (Temporary)",
"SW5E.Health": "Health", "SW5E.Health": "Health",
"SW5E.HealthConditions": "Health Conditions",
"SW5E.HealthFormula": "Health Formula", "SW5E.HealthFormula": "Health Formula",
"SW5E.HitDice": "Hit Dice", "SW5E.HitDice": "Hit Dice",
"SW5E.HitDiceRoll": "Roll Hit Dice",
"SW5E.HitDiceUsed": "Hit Dice Used", "SW5E.HitDiceUsed": "Hit Dice Used",
"SW5E.HitDiceWarn": "{name} has no available {formula} Hit Dice remaining!",
"SW5E.Identified": "Identified", "SW5E.Identified": "Identified",
"SW5E.Initiative": "Initiative", "SW5E.Initiative": "Initiative",
"SW5E.Inspiration": "Inspiration", "SW5E.Inspiration": "Inspiration",
@ -242,12 +323,10 @@
"SW5E.ItemActivationCost": "Activation Cost", "SW5E.ItemActivationCost": "Activation Cost",
"SW5E.ItemAttackBonus": "Attack Roll Bonus", "SW5E.ItemAttackBonus": "Attack Roll Bonus",
"SW5E.ItemConsumableActivation": "Consumable Activation", "SW5E.ItemConsumableActivation": "Consumable Activation",
"SW5E.ItemConsumableUsage": "Consumable Usage",
"SW5E.ItemConsumableDetails": "Consumable Details", "SW5E.ItemConsumableDetails": "Consumable Details",
"SW5E.ItemConsumableStatus": "Consumable Status", "SW5E.ItemConsumableStatus": "Consumable Status",
"SW5E.ItemConsumableType": "Consumable Type", "SW5E.ItemConsumableType": "Consumable Type",
"SW5E.ItemConsumableUsage": "Consumable Usage",
"SW5E.ItemConsumeOnUse": "Consume on Use",
"SW5E.ItemContainerCapacity": "Capacity", "SW5E.ItemContainerCapacity": "Capacity",
"SW5E.ItemContainerCapacityItems": "Items", "SW5E.ItemContainerCapacityItems": "Items",
"SW5E.ItemContainerCapacityType": "Capacity Type", "SW5E.ItemContainerCapacityType": "Capacity Type",
@ -270,6 +349,10 @@
"SW5E.ItemEquipmentUsage": "Equipment Usage", "SW5E.ItemEquipmentUsage": "Equipment Usage",
"SW5E.ItemName": "Item Name", "SW5E.ItemName": "Item Name",
"SW5E.ItemNew": "New {type}",
"SW5E.ItemRechargeCheck": "{name} recharge check",
"SW5E.ItemRechargeFailure": "failure!",
"SW5E.ItemRechargeSuccess": "success!",
"SW5E.ItemRequiredStr": "Required Strength", "SW5E.ItemRequiredStr": "Required Strength",
"SW5E.ItemToolProficiency": "Tool Proficiency", "SW5E.ItemToolProficiency": "Tool Proficiency",
@ -281,7 +364,8 @@
"SW5E.ItemWeaponType": "Weapon Type", "SW5E.ItemWeaponType": "Weapon Type",
"SW5E.ItemWeaponUsage": "Weapon Usage", "SW5E.ItemWeaponUsage": "Weapon Usage",
"SW5E.JackOfAllTrades": "Jack of all Trades", "SW5E.JackOfAllTrades": "Jack of all Trades",
"SW5E.LairAct": "Lair Action", "SW5E.LairAct": "Uses Lair Action",
"SW5E.LairActionLabel": "Lair Action",
"SW5E.Languages": "Languages", "SW5E.Languages": "Languages",
"SW5E.LanguagesAleena": "Aleena", "SW5E.LanguagesAleena": "Aleena",
"SW5E.LanguagesAntarian": "Antarian", "SW5E.LanguagesAntarian": "Antarian",
@ -383,12 +467,18 @@
"SW5E.LanguagesYevethan": "Yevethan", "SW5E.LanguagesYevethan": "Yevethan",
"SW5E.LanguagesZabraki": "Zabraki", "SW5E.LanguagesZabraki": "Zabraki",
"SW5E.LanguagesZygerrian": "Zygerrian", "SW5E.LanguagesZygerrian": "Zygerrian",
"SW5E.LegAct": "Legd. Actions", "SW5E.LegAct": "Legendary Actions",
"SW5E.LegRes": "Legd. Resistance", "SW5E.LegendaryActionLabel": "Legendary Action",
"SW5E.LegRes": "Legendary Resistance",
"SW5E.Level": "Level", "SW5E.Level": "Level",
"SW5E.LevelScaling": "Level Scaling", "SW5E.LevelScaling": "Level Scaling",
"SW5E.LimitedUses": "Limited Uses", "SW5E.LimitedUses": "Limited Uses",
"SW5E.LongRest": "Long Rest", "SW5E.LongRest": "Long Rest",
"SW5E.LongRestNormal": "Long Rest (8 hours)",
"SW5E.LongRestGritty": "Long Rest (7 days)",
"SW5E.LongRestEpic": "Long Rest (1 hour)",
"SW5E.LongRestOvernight": "Long Rest (New Day)",
"SW5E.LongRestResult": "{name} takes a long rest and recovers {health} Hit Points and {dice} Hit Dice.",
"SW5E.Max": "Max", "SW5E.Max": "Max",
"SW5E.Modifier": "Modifier", "SW5E.Modifier": "Modifier",
"SW5E.Name": "Character Name", "SW5E.Name": "Character Name",
@ -397,6 +487,8 @@
"SW5E.Normal": "Normal", "SW5E.Normal": "Normal",
"SW5E.NotProficient": "Not Proficient", "SW5E.NotProficient": "Not Proficient",
"SW5E.OtherFormula": "Other Formula", "SW5E.OtherFormula": "Other Formula",
"SW5E.PactMagic": "Pact Magic",
"SW5E.Passive": "Passive",
"SW5E.PlaceTemplate": "Place Measured Template", "SW5E.PlaceTemplate": "Place Measured Template",
"SW5E.Polymorph": "Polymorph", "SW5E.Polymorph": "Polymorph",
"SW5E.PolymorphAcceptSettings": "Custom Settings", "SW5E.PolymorphAcceptSettings": "Custom Settings",
@ -414,10 +506,12 @@
"SW5E.PolymorphMergeSkills": "Merge skill proficiencies (take the highest)", "SW5E.PolymorphMergeSkills": "Merge skill proficiencies (take the highest)",
"SW5E.PolymorphPromptTitle": "Transforming Actor", "SW5E.PolymorphPromptTitle": "Transforming Actor",
"SW5E.PolymorphRestoreTransformation": "Restore Transformation", "SW5E.PolymorphRestoreTransformation": "Restore Transformation",
"SW5E.PolymorphRevertWarn": "You do not have permission to revert this Actor's polymorphed state.",
"SW5E.PolymorphTmpClass": "Temporary Class", "SW5E.PolymorphTmpClass": "Temporary Class",
"SW5E.PolymorphTokens": "Transform all linked tokens?", "SW5E.PolymorphTokens": "Transform all linked tokens?",
"SW5E.PolymorphWarn": "You are not allowed to polymorph this actor!",
"SW5E.PolymorphWildShape": "Wild Shape", "SW5E.PolymorphWildShape": "Wild Shape",
"SW5E.Concentrated": "Concentrate", "SW5E.Concentrate": "Concentrate",
"SW5E.Price": "Price", "SW5E.Price": "Price",
"SW5E.Proficiency": "Proficiency", "SW5E.Proficiency": "Proficiency",
"SW5E.Proficient": "Proficient", "SW5E.Proficient": "Proficient",
@ -426,6 +520,8 @@
"SW5E.Range": "Range", "SW5E.Range": "Range",
"SW5E.Rarity": "Rarity", "SW5E.Rarity": "Rarity",
"SW5E.Reaction": "Reaction", "SW5E.Reaction": "Reaction",
"SW5E.ReactionPl": "Reactions",
"SW5E.Recharge": "Recharge",
"SW5E.RequiredMaterials": "Required Materials", "SW5E.RequiredMaterials": "Required Materials",
"SW5E.Requirements": "Requirements", "SW5E.Requirements": "Requirements",
"SW5E.ResourcePrimary": "Resource 1", "SW5E.ResourcePrimary": "Resource 1",
@ -438,7 +534,8 @@
"SW5E.RollExample": "e.g. +1d4", "SW5E.RollExample": "e.g. +1d4",
"SW5E.RollMode": "Roll Mode", "SW5E.RollMode": "Roll Mode",
"SW5E.RollSituationalBonus": "Situational Bonus?", "SW5E.RollSituationalBonus": "Situational Bonus?",
"SW5E.Save": "Save", "SW5E.SavingThrow": "Saving Throw",
"SW5E.SavePromptTitle": "{ability} Saving Throw",
"SW5E.ScalingFormula": "Scaling Formula", "SW5E.ScalingFormula": "Scaling Formula",
"SW5E.SchoolLgt": "Light", "SW5E.SchoolLgt": "Light",
"SW5E.SchoolUni": "Universal", "SW5E.SchoolUni": "Universal",
@ -451,8 +548,13 @@
"SW5E.SenseTS": "Tremorsense", "SW5E.SenseTS": "Tremorsense",
"SW5E.Senses": "Senses", "SW5E.Senses": "Senses",
"SW5E.ShortRest": "Short Rest", "SW5E.ShortRest": "Short Rest",
"SW5E.ShortRestNormal": "Short Rest (1 hour)",
"SW5E.ShortRestGritty": "Short Rest (8 hours)",
"SW5E.ShortRestEpic": "Short Rest (5 minutes)",
"SW5E.ShortRestOvernight": "Short Rest (New Day)",
"SW5E.ShortRestHint": "Take a short rest? On a short rest you may spend remaining Hit Dice and recover primary or secondary resources.", "SW5E.ShortRestHint": "Take a short rest? On a short rest you may spend remaining Hit Dice and recover primary or secondary resources.",
"SW5E.ShortRestNoHD": "No Hit Dice remaining", "SW5E.ShortRestNoHD": "No Hit Dice remaining",
"SW5E.ShortRestResult": "{name} takes a short rest spending {dice} Hit Dice to recover {health} Hit Points.",
"SW5E.ShortRestSelect": "Select Dice to Roll", "SW5E.ShortRestSelect": "Select Dice to Roll",
"SW5E.Size": "Size", "SW5E.Size": "Size",
"SW5E.SizeGargantuan": "Gargantuan", "SW5E.SizeGargantuan": "Gargantuan",
@ -479,6 +581,7 @@
"SW5E.SkillSte": "Stealth", "SW5E.SkillSte": "Stealth",
"SW5E.SkillSur": "Survival", "SW5E.SkillSur": "Survival",
"SW5E.SkillTec": "Technology", "SW5E.SkillTec": "Technology",
"SW5E.SkillPromptTitle": "{skill} Skill Check",
"SW5E.Slots": "Slots", "SW5E.Slots": "Slots",
"SW5E.Source": "Source", "SW5E.Source": "Source",
"SW5E.Special": "Special", "SW5E.Special": "Special",
@ -511,7 +614,8 @@
"SW5E.PowerLevel7": "7th Level", "SW5E.PowerLevel7": "7th Level",
"SW5E.PowerLevel8": "8th Level", "SW5E.PowerLevel8": "8th Level",
"SW5E.PowerLevel9": "9th Level", "SW5E.PowerLevel9": "9th Level",
"SW5E.PowerLevelPact": "Pact Slot", "SW5E.PowerLevelSlot": "{level} ({n} Slots)",
"SW5E.PowerLevelPact": "Pact Slot [Level {level}] ({n} Slots)",
"SW5E.PowerMaterials": "Powercasting Materials", "SW5E.PowerMaterials": "Powercasting Materials",
"SW5E.PowerName": "Power Name", "SW5E.PowerName": "Power Name",
"SW5E.PowerNone": "None", "SW5E.PowerNone": "None",
@ -532,6 +636,7 @@
"SW5E.Powerbook": "Powerbook", "SW5E.Powerbook": "Powerbook",
"SW5E.SpeciesDescription": "Description", "SW5E.SpeciesDescription": "Description",
"SW5E.SpeciesTraits": "Species Traits", "SW5E.SpeciesTraits": "Species Traits",
"SW5E.StealthDisadvantage": "Stealth Disadvantage",
"SW5E.SubclassName": "Subclass Name", "SW5E.SubclassName": "Subclass Name",
"SW5E.Supply": "Supply", "SW5E.Supply": "Supply",
"SW5E.Target": "Target", "SW5E.Target": "Target",
@ -552,6 +657,7 @@
"SW5E.TargetWall": "Wall", "SW5E.TargetWall": "Wall",
"SW5E.TargetWeapon": "Weapon", "SW5E.TargetWeapon": "Weapon",
"SW5E.Temp": "Temp", "SW5E.Temp": "Temp",
"SW5E.Threshold": "Threshold",
"SW5E.TimeDay": "Days", "SW5E.TimeDay": "Days",
"SW5E.TimeHour": "Hours", "SW5E.TimeHour": "Hours",
"SW5E.TimeInst": "Instantaneous", "SW5E.TimeInst": "Instantaneous",
@ -567,6 +673,7 @@
"SW5E.ToolArtist": "Artist's Tools", "SW5E.ToolArtist": "Artist's Tools",
"SW5E.ToolAstrotech": "Astrotech's Tools", "SW5E.ToolAstrotech": "Astrotech's Tools",
"SW5E.ToolBiotech": "Biotech's Tools", "SW5E.ToolBiotech": "Biotech's Tools",
"SW5E.ToolCheck": "Tool Check",
"SW5E.ToolConstructor": "Constructor's Tools", "SW5E.ToolConstructor": "Constructor's Tools",
"SW5E.ToolCybertech": "Cybertech's Tools", "SW5E.ToolCybertech": "Cybertech's Tools",
"SW5E.ToolJeweler": "Jeweler's Tools", "SW5E.ToolJeweler": "Jeweler's Tools",
@ -593,6 +700,7 @@
"SW5E.ToolMusicalInstrument": "Musical Instrument", "SW5E.ToolMusicalInstrument": "Musical Instrument",
"SW5E.ToolVehicle": "Vehicle (Land or Water)", "SW5E.ToolVehicle": "Vehicle (Land or Water)",
"SW5E.TraitArmorProf": "Armor Proficiencies", "SW5E.TraitArmorProf": "Armor Proficiencies",
"SW5E.TraitSave": "Update",
"SW5E.TraitSelectorSpecial": "Special (Split with Semi-Colon)", "SW5E.TraitSelectorSpecial": "Special (Split with Semi-Colon)",
"SW5E.TraitToolProf": "Tool Proficiencies", "SW5E.TraitToolProf": "Tool Proficiencies",
"SW5E.TraitWeaponProf": "Weapon Proficiencies", "SW5E.TraitWeaponProf": "Weapon Proficiencies",
@ -602,6 +710,20 @@
"SW5E.Usage": "Usage", "SW5E.Usage": "Usage",
"SW5E.Use": "Use", "SW5E.Use": "Use",
"SW5E.Uses": "Uses", "SW5E.Uses": "Uses",
"SW5E.Vehicle": "Vehicle",
"SW5E.VehicleActionStations": "Action Stations",
"SW5E.VehicleActionThresholds": "Action Thresholds",
"SW5E.VehicleCargo": "Cargo",
"SW5E.VehicleCargoCapacity": "Cargo Capacity",
"SW5E.VehicleCargoCrew": "Cargo & Crew",
"SW5E.VehicleCreatureCapacity": "Creature Capacity",
"SW5E.VehicleCrew": "Crew",
"SW5E.VehicleCrewed": "Crewed",
"SW5E.VehicleCrewAction": "Crew Action",
"SW5E.VehicleEquipment": "Vehicle Equipment",
"SW5E.VehicleMishap": "Mishap",
"SW5E.VehiclePassengers": "Passengers",
"SW5E.VehicleUncrewed": "Uncrewed",
"SW5E.Versatile": "Versatile", "SW5E.Versatile": "Versatile",
"SW5E.VersatileDamage": "Versatile Damage", "SW5E.VersatileDamage": "Versatile Damage",
"SW5E.VsDC": "vs DC.", "SW5E.VsDC": "vs DC.",
@ -648,7 +770,7 @@
"SW5E.WeaponSimpleLW": "Simple Lightweapon", "SW5E.WeaponSimpleLW": "Simple Lightweapon",
"SW5E.Weight": "Weight", "SW5E.Weight": "Weight",
"SW5E.available": "available", "SW5E.available": "available",
"SW5E.description": "A comprehensive game system for running games of Dungeons & Dragons 5th Edition in the Foundry VTT environment.", "SW5E.description": "A comprehensive game system for running games of Star Wars 5th Edition in the Foundry VTT environment.",
"SW5E.of": "of", "SW5E.of": "of",
"SW5E.power": "power", "SW5E.power": "power",
"SETTINGS.5eAllowPolymorphingL": "Allow players to polymorph their own actors.", "SETTINGS.5eAllowPolymorphingL": "Allow players to polymorph their own actors.",
@ -659,12 +781,19 @@
"SETTINGS.5eAutoPowerTemplateN": "Always place Power Template", "SETTINGS.5eAutoPowerTemplateN": "Always place Power Template",
"SETTINGS.5eCurWtL": "Carried currency affects character encumbrance following the rules on PHB pg. 143.", "SETTINGS.5eCurWtL": "Carried currency affects character encumbrance following the rules on PHB pg. 143.",
"SETTINGS.5eCurWtN": "Apply Currency Weight", "SETTINGS.5eCurWtN": "Apply Currency Weight",
"SETTINGS.5eDiagDMG": "Dungeon Master's Guide (5/10/5)",
"SETTINGS.5eDiagEuclidean": "Euclidean (7.07 ft. Diagonal)",
"SETTINGS.5eDiagL": "Configure which diagonal movement rule should be used for games within this system.", "SETTINGS.5eDiagL": "Configure which diagonal movement rule should be used for games within this system.",
"SETTINGS.5eDiagN": "Diagonal Movement Rule", "SETTINGS.5eDiagN": "Diagonal Movement Rule",
"SETTINGS.5eDiagPHB": "Player's Handbook (5/5/5)", "SETTINGS.5eDiagPHB": "PHB: Equidistant (5/5/5)",
"SETTINGS.5eDiagDMG": "DMG: Alternating (5/10/5)",
"SETTINGS.5eInitTBL": "Append the raw Dexterity ability score to break ties in Initiative.", "SETTINGS.5eInitTBL": "Append the raw Dexterity ability score to break ties in Initiative.",
"SETTINGS.5eInitTBN": "Initiative Dexterity Tiebreaker", "SETTINGS.5eInitTBN": "Initiative Dexterity Tiebreaker",
"SETTINGS.5eNoExpL": "Remove experience bars from character sheets.", "SETTINGS.5eNoExpL": "Remove experience bars from character sheets.",
"SETTINGS.5eNoExpN": "Disable Experience Tracking" "SETTINGS.5eNoExpN": "Disable Experience Tracking",
"SETTINGS.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.5eRestGritty": "Gritty Realism (LR: 7 days, SR: 8 hours)",
"SETTINGS.5eRestEpic": "Epic Heroism (LR: 1 hour, SR: 1 min)"
} }

View file

@ -1,58 +1,73 @@
@import "./variables.less"; @import "./variables.less";
@actorNameHeight: 60px;
.sw5e.sheet.actor { .sw5e.sheet.actor {
/* ----------------------------------------- */ /* ----------------------------------------- */
/* Sheet Header */ /* Sheet Header */
/* ----------------------------------------- */ /* ----------------------------------------- */
.sheet-header { .sheet-header {
.charlevel {
// Portrait Artwork
img.profile {
flex: 0 0 160px; flex: 0 0 160px;
height: 60px; max-width: 160px;
margin: 0; height: 160px;
padding: 5px; }
text-align: right;
.level {
width: 100%;
height: 30px;
font-size: 20px;
line-height: 30px;
label {
display: inline;
font-size: 24px;
text-align: right;
}
input {
display: inline;
width: 36px;
font-size: 24px;
text-align: center;
}
// If the Experience Bar is disabled
&.noxp {
margin-top: 10px;
> label {
font-size: 32px;
}
> input {
font-size: 32px;
flex: 0 0 40px;
height: 44px;
}
}
// Character Name
h1.charname {
flex: 1;
height: @actorNameHeight;
padding: 0;
input {
height: @actorNameHeight - 20px;
margin: 10px 0;
} }
}
.experience { // Character Level
width: 100%; .header-exp {
height: 20px; flex: 0 0 150px;
padding-right: 5px; margin-right: 3px;
font-size: 16px; height: @actorNameHeight;
color: @colorOlive; justify-content: flex-end;
text-align: right;
}
// Character Summary
.summary {
height: 30px;
border-bottom: @borderGroove;
font-size: 18px;
input, span {
display: block;
height: 24px;
line-height: 24px;
}
}
// Primary Attributes
.attributes {
flex: 0 0 100%;
margin: 0;
.attribute {
height: 70px;
margin: 0;
border: none;
border-right: @borderGroove;
border-radius: 0;
&:last-child {
border-right: none;
}
.attribute-value {
height: 30px;
line-height: 30px;
}
} }
} }
} }
@ -69,55 +84,55 @@
// Box Headers // Box Headers
h4.box-title { h4.box-title {
height: 18px;
line-height: 16px; line-height: 16px;
margin: 4px 8px 2px; margin: 4px 8px 2px;
.modesto();
font-size: 18px; font-size: 18px;
border-bottom: 1px solid @colorBeige; color: @colorOlive;
border-bottom: 1px solid @colorFaint;
} }
/* ----------------------------------------- */ /* ----------------------------------------- */
/* Attributes */ /* Attributes */
/* ----------------------------------------- */ /* ----------------------------------------- */
.tab.attributes {
overflow: hidden;
}
ul.attributes { ul.attributes {
flex: 0 0 60px;
list-style: none; list-style: none;
margin: 5px 0 0; margin: 0;
padding: 0; padding: 0;
li.attribute { li.attribute {
height: 70px; height: 60px;
margin: 0 5px; margin: 0 5px 0 0;
border: @borderGroove; border: @borderGroove;
border-radius: 4px; border-radius: 4px;
.nodesto();
text-align: center; text-align: center;
.attribute-name { &:last-child {
flex: 0 0 18px; margin: 0;
} }
.attribute-value { .attribute-value {
display: flex;
justify-content: center;
align-items: center;
height: 28px; height: 28px;
line-height: 28px; line-height: 28px;
.modesto();
input { > * {
display: inline; font-weight: 400;
max-width: 80%;
height: 28px;
margin: 0;
font-size: 24px; font-size: 24px;
} }
span.sep { &.multiple input {
display: inline; flex: 0 0 33%;
position: relative;
top: 2px;
font-size: 28px;
color: @colorTan;
}
&.multiple {
input { max-width: 33%; }
} }
} }
@ -125,8 +140,9 @@
flex: 0 0 18px; flex: 0 0 18px;
margin-top: -1px; margin-top: -1px;
line-height: 18px; line-height: 18px;
font-family: "Signika", "Palatino Linotype", serif; font-family: "Signika", sans-serif;
font-size: 12px; font-size: 12px;
font-weight: 400;
} }
} }
} }
@ -136,22 +152,28 @@
/* ----------------------------------------- */ /* ----------------------------------------- */
.ability-scores { .ability-scores {
flex: 0 0 100%; flex: 0 0 100px;
height: 440px;
list-style: none; list-style: none;
margin: 0; margin: 0;
padding: 0; padding: 0;
.nodesto(); .modesto();
border: @borderGroove;
border-radius: 3px;
.ability { .ability {
height: 70px; height: 70px;
margin: 0 5px;
text-align: center; text-align: center;
border: @borderGroove; border-bottom: @borderGroove;
border-radius: 3px;
&:last-child {
border-bottom: none;
margin-bottom: -3px;
}
input.ability-score { input.ability-score {
height: 30px; height: 30px;
width: 50px; width: 36px;
margin: 0 auto; margin: 0 auto;
line-height: 32px; line-height: 32px;
font-size: 24px; font-size: 24px;
@ -159,12 +181,14 @@
.ability-modifiers { .ability-modifiers {
height: 24px; height: 24px;
margin: -10px 0 0; margin: -8px 0 0;
span.ability-mod, span.ability-mod,
span.ability-save { span.ability-save {
flex: 0 0 24px; flex: 0 0 24px;
height: 24px; height: 22px;
line-height: 22px;
font-size: 16px;
border-top: @borderGroove; border-top: @borderGroove;
} }
@ -180,12 +204,6 @@
border-left: @borderGroove; border-left: @borderGroove;
} }
} }
/* Hide modifier box on hover */
input.ability-score:hover + .ability-modifiers {
visibility: hidden;
}
} }
} }
@ -209,16 +227,21 @@
/* ----------------------------------------- */ /* ----------------------------------------- */
ul.skills-list { ul.skills-list {
flex: 0 0 192px; flex: 0 0 180px;
height: 440px;
list-style: none; list-style: none;
margin: 5px 5px 0; margin: 0 5px 0;
padding: 2px 2px 0; padding: 3px 0 2px;
border: @borderGroove; border: @borderGroove;
border-radius: 3px; border-radius: 3px;
li.skill { li.skill {
height: 22px; height: 24px;
padding: 3px 0; padding: 3px 2px;
&:nth-child(even) {
background: rgba(0, 0, 0, 0.05);
}
h4 { h4 {
flex: 1px; flex: 1px;
@ -234,6 +257,7 @@
.skill-ability { .skill-ability {
flex: 0 0 26px; flex: 0 0 26px;
text-transform: capitalize;
} }
.skill-mod { .skill-mod {
@ -253,23 +277,25 @@
/* ----------------------------------------- */ /* ----------------------------------------- */
.counters { .counters {
flex: 0 0 100%; flex: none;
padding: 5px 0;
margin: 0;
border-bottom: @borderGroove; border-bottom: @borderGroove;
margin-bottom: 5px;
.counter { .counter {
padding: 0 3px; height: 20px;
line-height: 32px; line-height: 20px;
h4 { h4 {
flex: auto; flex: auto;
margin: 0; margin: 0;
.nodesto(); font-size: 13px;
font-size: 14px; font-weight: bold;
color: @colorOlive;
} }
.counter-value { .counter-value {
flex: 0 0 50px; flex: none;
text-align: right; text-align: right;
> * { > * {
display: inline; display: inline;
@ -286,12 +312,13 @@
input[type="checkbox"] { input[type="checkbox"] {
position: relative; position: relative;
width: 16px;
height: 16px;
margin: 0; margin: 0;
top: 6px; top: 4px;
} }
span.sep { span.sep {
margin: 0 -2px;
font-size: 12px; font-size: 12px;
} }
} }
@ -301,28 +328,31 @@
/* Traits */ /* Traits */
/* ----------------------------------------- */ /* ----------------------------------------- */
.traits { .center-pane {
margin: 0 5px; height: 100%;
padding: 0 5px 0 3px;
overflow-y: auto;
scrollbar-width: thin;
}
.traits {
.form-group, .form-group-stacked { .form-group, .form-group-stacked {
margin: 0 0 4px 0; margin: 0 0 3px 0;
justify-content: space-between; justify-content: space-between;
} }
.configure-flags { .configure-flags {
flex: 1; flex: 1;
} }
.actor-size {
flex: 0 0 150px;
}
label { label {
flex: 0 0 150px; flex: none;
line-height: 20px; line-height: 20px;
font-weight: bold; font-weight: bold;
margin: 0; margin: 0 10px 0 0;
}
select {
max-width: 200px;
} }
input { input {
@ -360,18 +390,10 @@
.inventory-filters { .inventory-filters {
margin: 0 8px; margin: 0 8px;
flex: 0 0 20px; flex: 0 0 20px;
h3, .filter-title { justify-content: flex-end;
.nodesto();
color: @colorOlive;
font-size: 18px;
margin: 0;
}
&.powerbook-filters {
flex: 0 0 40px;
}
.currency { .currency {
flex: 0 0 100%;
list-style: none; list-style: none;
margin: 4px 0 8px; margin: 4px 0 8px;
padding: 0; padding: 0;
@ -398,6 +420,7 @@
margin: 0; margin: 0;
padding: 0 5px; padding: 0 5px;
overflow-y: auto; overflow-y: auto;
scrollbar-width: thin;
// Inventory Item // Inventory Item
.item { .item {
@ -424,12 +447,12 @@
overflow-x: hidden; overflow-x: hidden;
} }
&.rollable .item-image:hover {
background-image: url("/icons/svg/d20-black.svg") !important;
}
&.rollable:hover .item-image { &.rollable:hover .item-image {
background-image: url("/icons/svg/d20-grey.svg") !important; background-image: url("/icons/svg/d20-grey.svg") !important;
} }
&.rollable .item-image:hover {
background-image: url("/icons/svg/d20-black.svg") !important;
}
i.attuned { i.attuned {
color: @colorTan; color: @colorTan;
@ -453,6 +476,7 @@
text-align: right; text-align: right;
font-size: 11px; font-size: 11px;
color: @colorTan; color: @colorTan;
white-space: nowrap;
} }
} }
@ -468,8 +492,8 @@
h3 { h3 {
margin: 0 -5px 0 0; margin: 0 -5px 0 0;
padding-left: 5px; padding-left: 5px;
font-size: 13px; .modesto();
font-weight: bold; font-size: 16px;
} }
.item-controls a.item-create { .item-controls a.item-create {
@ -484,6 +508,10 @@
color: @colorTan; color: @colorTan;
text-align: center; text-align: center;
border-right: 1px solid @colorFaint; border-right: 1px solid @colorFaint;
word-break: break-word;
white-space: nowrap;
overflow: hidden;
&:last-child { border-right: none; } &:last-child { border-right: none; }
&.item-action {flex: 0 0 100px} &.item-action {flex: 0 0 100px}
} }
@ -524,15 +552,77 @@
} }
} }
/* Encumbrance Bar */
.encumbrance {
flex: 0 0 12px;
background: @colorTan;
margin: 1px 15px 0 1px;
border: 1px solid @colorDark;
border-radius: 3px;
position: relative;
.encumbrance-bar {
position: absolute;
top: 1px;
left: 1px;
background: #6c8aa5;
height: 8px;
border: 1px solid #cde4ff;
border-radius: 2px;
}
.encumbrance-label {
height: 10px;
padding: 0 5px;
position: absolute;
top: 0;
right: 0;
font-size: 13px;
line-height: 12px;
text-align: right;
color: #EEE;
text-shadow: 0 0 5px #000;
}
.encumbrance-breakpoint {
display: block;
position: absolute;
&.encumbrance-33 { left: 33% }
&.encumbrance-66 { left: 66% }
}
.arrow-up {
bottom: 0;
width: 0;
height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-bottom: 4px solid #666;
}
.arrow-down {
top: 0;
width: 0;
height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 4px solid #666;
}
&.encumbered {
.arrow-up { border-bottom: 4px solid #000; }
.arrow-down { border-top: 4px solid #000; }
}
}
/* ----------------------------------------- */ /* ----------------------------------------- */
/* Powerbook */ /* Powerbook */
/* ----------------------------------------- */ /* ----------------------------------------- */
.powercasting-ability { .powercasting-ability {
h3 { flex: 0 0 240px;
flex: none; margin: 0;
margin-right: 5px;
}
input, span { input, span {
flex: 0 0 32px; flex: 0 0 32px;
text-align: center; text-align: center;
@ -549,7 +639,7 @@
.power-slots, .power-slots,
.power-comps { .power-comps {
flex: 0 0 72px; flex: 0 0 75px;
padding-right: 5px; padding-right: 5px;
text-align: right; text-align: right;
font-size: 12px; font-size: 12px;
@ -558,10 +648,13 @@
} }
.power-slots { .power-slots {
input, span.sep { input {
display: inline; display: inline;
max-width: 20px; max-width: 20px;
text-align: center; }
.sep {
font-size: 13px;
font-weight: normal;
} }
} }

View file

@ -5,14 +5,14 @@
@detailsHeight: 40px; @detailsHeight: 40px;
/* ----------------------------------------- */ /* ----------------------------------------- */
/* All SW5e Apps */ /* All DnD5e Apps */
/* ----------------------------------------- */ /* ----------------------------------------- */
.sw5e { .sw5e {
.window-content { .window-content {
background: @sheetBackground; background: @sheetBackground;
font-size: 13px; font-size: 13px;
color: @colorDark color: @colorDark;
} }
/* ----------------------------------------- */ /* ----------------------------------------- */
@ -56,20 +56,27 @@
} }
// Checkbox Labels // Checkbox Labels
// TODO: THIS CAN BE MOSTLY REMOVED NOW THAT IT IS IN CORE, see core forms.less
label.checkbox { label.checkbox {
flex: auto; flex: auto;
padding: 0;
margin: 0; margin: 0;
line-height: 20px; height: 22px;
line-height: 22px;
font-size: 11px; font-size: 11px;
text-align: right; > input[type="checkbox"] {
input[type="checkbox"] { width: 16px;
height: auto; height: 16px;
margin: 0 5px 0; margin: 0 2px 0 0;
position: relative; position: relative;
top: 3px; top: 4px;
}
&.right > input[type="checkbox"] {
margin: 0 0 0 2px;
} }
} }
/* Form Groups */ /* Form Groups */
.form-group { .form-group {
label { label {
@ -101,11 +108,10 @@
// Form Headers // Form Headers
.form-header { .form-header {
margin: 0 0 0.25em 0; margin: 0 0 0.25em 0;
padding: 0 2px; padding: 2px 0;
.nodesto();
font-size: 24px;
border-top: @borderGroove; border-top: @borderGroove;
border-bottom: @borderGroove; border-bottom: @borderGroove;
.modesto();
} }
/* Tags */ /* Tags */
@ -175,6 +181,16 @@
cursor: pointer; cursor: pointer;
} }
// Separators
span.sep {
flex: none;
margin: 0 1px;
display: inline;
position: relative;
color: @colorTan;
font-weight: normal;
}
/* ----------------------------------------- */ /* ----------------------------------------- */
/* TinyMCE */ /* TinyMCE */
/* ----------------------------------------- */ /* ----------------------------------------- */
@ -190,10 +206,14 @@
/* ----------------------------------------- */ /* ----------------------------------------- */
/* Sheet Header */ /* Sheet Header */
/* ----------------------------------------- */ /* ----------------------------------------- */
.sheet-header { .sheet-header {
flex: 0 0 @headerHeight; flex: 0 0 @headerHeight;
border-bottom: @borderGroove; border-bottom: @borderGroove;
.nodesto();
.header-details {
.modesto();
}
/* Character Name */ /* Character Name */
h1 { h1 {
@ -204,8 +224,10 @@
padding: 5px; padding: 5px;
input { input {
display: block;
height: 50px; height: 50px;
font-size: 36px; font-size: 32px;
margin: 0;
} }
} }
@ -214,7 +236,7 @@
flex: 0 0 @headerHeight; flex: 0 0 @headerHeight;
max-width: @headerHeight; max-width: @headerHeight;
height: @headerHeight; height: @headerHeight;
object-fit: cover; object-fit: contain;
border: none; border: none;
border-right: @borderGroove; border-right: @borderGroove;
} }
@ -230,11 +252,10 @@
border-bottom: none; border-bottom: none;
li { li {
width: 33.33%; height: calc(100% - 6px);
float: left; float: left;
height: @detailsHeight - 6px;
margin: 2px 0; margin: 2px 0;
padding: 0 3px; padding: 0;
border-right: @borderGroove; border-right: @borderGroove;
line-height: 34px; line-height: 34px;
color: @colorOlive; color: @colorOlive;
@ -253,7 +274,7 @@
.sheet-navigation { .sheet-navigation {
flex: 0 0 @navHeight; flex: 0 0 @navHeight;
margin-bottom: 5px; margin-bottom: 5px;
.nodesto(); .modesto();
.item { .item {
height: 30px; height: 30px;
@ -285,18 +306,25 @@
margin: 0; margin: 0;
padding: 0; padding: 0;
line-height: 16px; line-height: 16px;
max-width: 70%;
.filter-title { .filter-icon {
flex: 3; flex: none;
font-size: 14px;
color: @colorTan;
} }
.filter-item { .filter-item {
text-align: center; text-align: center;
font-size: 12px; font-size: 12px;
margin: 0 6px; margin: 0 6px 0 0;
border-bottom: 3px solid @colorBeige; border-bottom: 3px solid @colorBeige;
white-space: nowrap; white-space: nowrap;
&:last-child {
margin: 0;
}
&:hover { &:hover {
text-shadow: 0 0 4px red; text-shadow: 0 0 4px red;
border-bottom: 3px solid @colorTan; border-bottom: 3px solid @colorTan;
@ -312,19 +340,23 @@
/* Trait Lists */ /* Trait Lists */
/* ----------------------------------------- */ /* ----------------------------------------- */
.trait-selector { .traits {
flex: 0 0 16px; margin: 5px 0 0;
padding: 2px 0;
color: #999;
font-size: 10px;
}
.traits-list { .trait-selector {
line-height: 20px; flex: 0 0 16px;
list-style: none; padding: 2px 0;
margin: 0; color: #999;
padding: 0; font-size: 10px;
text-align: right; }
.traits-list {
flex: 0 0 100%;
line-height: 20px;
list-style: none;
margin: 0;
padding: 0;
}
} }
} }

View file

@ -4,8 +4,8 @@
/* Basic Structure */ /* Basic Structure */
/* ----------------------------------------- */ /* ----------------------------------------- */
.sw5e.sheet.actor.character { .sw5e.sheet.actor.character {
min-width: 680px; min-width: 720px;
min-height: 736px; min-height: 680px;
/* ----------------------------------------- */ /* ----------------------------------------- */
/* Sheet Header */ /* Sheet Header */
@ -13,93 +13,73 @@
.sheet-header { .sheet-header {
// Character Profile image (larger than usual) // Character Profile image (larger)
img.profile { img.profile {
flex: 0 0 180px; flex: 0 0 160px;
max-width: 180px; max-width: 160px;
height: 180px; height: 160px;
} }
// Character level and experience bar // Character Level
.charlevel { .charlevel {
flex: 0 0 180px; flex: 0 0 20px;
padding: 0 5px 2px; height: 20px;
font-size: 18px;
color: @colorTan;
white-space: nowrap;
}
.level { // Experience Tracking
height: 28px; .experience {
flex: 0 0 32px;
margin-bottom: -5px;
align-items: center;
font-size: 18px;
span.max {
color: @colorTan;
flex: none;
margin-left: 3px;
} }
}
.experience { .xpbar {
input[type="text"] { flex: 0 0 8px;
width: 100px; width: 100%;
} margin-bottom: 5px;
} background: @colorTan;
border: 1px solid #000;
.xpbar { border-radius: 3px;
width: 100%; .bar {
flex: 0 0 8px; height: 4px;
background: #666; margin: 1px;
display: block;
background: #afebff;
border: 1px solid #000; border: 1px solid #000;
border-radius: 3px; border-radius: 2px;
.bar {
height: 4px;
margin: 1px;
display: block;
background: #afebff;
border: 1px solid #000;
border-radius: 2px;
}
} }
} }
// Character Summary // Header Attributes
.summary {
border-bottom: @borderGroove;
}
// Primary Attributes
.attributes { .attributes {
height: 80px;
margin: 0;
.attribute {
height: 80px;
margin: 0;
border: none;
border-right: @borderGroove;
border-radius: 0;
&:last-child {
border-right: none;
}
.attribute-value {
margin: 5px 0 0;
height: 32px;
line-height: 32px;
}
.attribute-name {
margin-top: 6px;
}
.attribute-footer {
margin-bottom: 2px;
}
}
a.rest { a.rest {
border: 1px solid @colorBeige; border: 1px solid @colorBeige;
border-radius: 2px; border-radius: 2px;
background: rgba(0, 0, 0, 0.05); background: rgba(0, 0, 0, 0.05);
padding: 1px 3px; padding: 0 3px;
margin: 0 6px; margin: 0 3px;
} }
.hit-dice { .hit-dice {
font-size: 24px; font-size: 24px;
} }
.initiative .attribute-footer input {
width: 32px;
}
}
.summary .proficiency {
text-align: right;
padding-right: 5px;
} }
} }
@ -107,26 +87,24 @@
/* Sheet Body */ /* Sheet Body */
/* ----------------------------------------- */ /* ----------------------------------------- */
.attributes { // Custom Resources
.resource { .resource .attribute-value {
.attribute-name { input {
margin: 0 8px; flex: 0 0 25%;
input[type="text"] {
height: 20px;
margin: 2px 0 -2px;
line-height: 24px;
}
}
label.checkbox {
margin: 0 3px;
input[type="checkbox"] {
transform: scale(1.2);
}
}
} }
label.recharge {
.initiative .attribute-footer input { height: 32px;
width: 32px; position: relative;
font-family: "Signika", sans-serif;
font-size: 11px;
text-align: center;
color: @colorOlive;
input[type="checkbox"] {
height: 14px;
width: 14px;
margin: 0;
top: -6px;
}
} }
} }
@ -143,88 +121,12 @@
} }
} }
.counters {
.death-saves {
flex: 2;
.counter-value {
flex: 0 0 90px;
}
}
}
.item-detail.player-class { .item-detail.player-class {
flex: 0 0 180px; flex: 0 0 180px;
text-align: right; text-align: right;
padding-right: 10px; padding-right: 10px;
} }
/* ----------------------------------------- */
/* Inventory */
/* ----------------------------------------- */
/* Encumbrance Bar */
.encumbrance {
flex: 0 0 12px;
background: @colorTan;
margin: 1px 15px 0 1px;
border: 1px solid @colorDark;
border-radius: 3px;
position: relative;
.encumbrance-bar {
position: absolute;
top: 1px;
left: 1px;
background: #6c8aa5;
height: 8px;
border: 1px solid #cde4ff;
border-radius: 2px;
}
.encumbrance-label {
height: 10px;
padding: 0 5px;
position: absolute;
top: 0;
right: 0;
font-size: 13px;
line-height: 12px;
text-align: right;
color: #EEE;
text-shadow: 0 0 5px #000;
}
.encumbrance-breakpoint {
display: block;
position: absolute;
&.encumbrance-33 { left: 33% }
&.encumbrance-66 { left: 66% }
}
.arrow-up {
bottom: 0;
width: 0;
height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-bottom: 4px solid #666;
}
.arrow-down {
top: 0;
width: 0;
height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 4px solid #666;
}
&.encumbered {
.arrow-up { border-bottom: 4px solid #000; }
.arrow-down { border-top: 4px solid #000; }
}
}
/* ----------------------------------------- */ /* ----------------------------------------- */
/* Item Controls */ /* Item Controls */
/* ----------------------------------------- */ /* ----------------------------------------- */

View file

@ -22,7 +22,7 @@
flex: 1; flex: 1;
margin: 0; margin: 0;
line-height: 36px; line-height: 36px;
.nodesto(); .bungeeInline();
color: @colorOlive; color: @colorOlive;
&:hover { &:hover {
color: #111; color: #111;

View file

@ -192,7 +192,26 @@
text-align: right; text-align: right;
color: @colorTan; color: @colorTan;
} }
.recharge.form-group {
span {
text-align: right;
padding-right: 3px;
}
input[type="text"] {
flex: 0 0 32px;
text-align: center;
}
label.checkbox {
flex: none;
input {
width: 16px;
height: 16px;
top: 4px;
}
}
}
h4.armorproperties-header { h4.armorproperties-header {
margin: 0; margin: 0;
padding: 0; padding: 0;
@ -263,16 +282,6 @@
color: @colorTan; color: @colorTan;
} }
.recharge {
span {
flex: 0 0 80px;
}
label.checkbox {
flex: 0 0 80px;
text-align: right;
}
}
/* ----------------------------------------- */ /* ----------------------------------------- */
/* Item Actions */ /* Item Actions */
/* ----------------------------------------- */ /* ----------------------------------------- */

View file

@ -5,5 +5,37 @@
/* ----------------------------------------- */ /* ----------------------------------------- */
.sw5e.sheet.actor.npc { .sw5e.sheet.actor.npc {
min-width: 600px; min-width: 600px;
min-height: 658px; min-height: 680px;
.header-exp {
flex: 0 0 80px;
justify-content: center;
.cr {
flex: 0 0 32px;
line-height: 28px;
margin-bottom: -5px;
font-size: 24px;
input {
width: 32px;
padding: 0;
text-align: center;
}
}
.experience {
flex: 0 0 18px;
color: @colorTan;
font-size: 16px;
}
}
.summary {
font-size: 18px;
}
.powercasting-ability {
label {
flex: none;
}
}
} }

View file

@ -5,3 +5,4 @@
@import "chat.less"; @import "chat.less";
@import "character.less"; @import "character.less";
@import "npc.less"; @import "npc.less";
@import "vehicle.less";

View file

@ -7,13 +7,41 @@
/* Fonts */ /* Fonts */
/* ----------------------------------------- */ /* ----------------------------------------- */
/* russo-one-regular - latin */
@font-face { @font-face {
font-family: "Nodesto"; font-family: 'Russo One';
src: url("fonts/NodestoCapsCondensed.otf"); font-style: normal;
font-weight: 400;
src: url('./fonts/RussoOne.ttf');
} }
.nodesto { .russoOne {
font-family: "Nodesto", "Signika", "Palatino Linotype", serif; font-family: 'Russo One';
font-size: 20px; font-size: 20px;
font-weight: 400;
}
/* bungee-inline-regular - latin */
@font-face {
font-family: 'Bungee Inline';
font-style: normal;
font-weight: 400;
src: url('./fonts/BungeeInline.ttf');
}
.bungeeInline {
font-family: 'Bungee Inline';
font-size: 20px;
font-weight: 400;
}
/* open-sans-regular - latin */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
src: url('./fonts/OpenSans-Regular.ttf');
}
.openSans {
font-family: 'Open Sans';
font-size: 20px;
font-weight: 400;
} }
/* ----------------------------------------- */ /* ----------------------------------------- */

32
less/vehicle.less Normal file
View file

@ -0,0 +1,32 @@
.sw5e.sheet.actor.vehicle {
.features {
.item-controls {
flex: 0 0 68px;
.item-toggle {
color: #b5b3a4;
&.active {
color: #4b4a44;
}
}
}
}
.counters {
.counter.creature-cap {
.counter-value {
flex: 1;
}
input {
max-width: none;
text-align: right;
}
}
.counter.cargo-cap {
input {
max-width: 40px;
text-align: right;
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,14 +1,14 @@
import {TraitSelector} from "../../apps/trait-selector.js"; import Item5e from "../../item/entity.js";
import {ActorSheetFlags} from "../../apps/actor-flags.js"; import TraitSelector from "../../apps/trait-selector.js";
import ActorSheetFlags from "../../apps/actor-flags.js";
import {SW5E} from '../../config.js'; import {SW5E} from '../../config.js';
/** /**
* Extend the basic ActorSheet class to do all the SW5e things! * Extend the basic ActorSheet class to do all the SW5e things!
* This sheet is an Abstract layer which is not used. * This sheet is an Abstract layer which is not used.
* * @extends {ActorSheet}
* @type {ActorSheet}
*/ */
export class ActorSheet5e extends ActorSheet { export default class ActorSheet5e extends ActorSheet {
constructor(...args) { constructor(...args) {
super(...args); super(...args);
@ -37,6 +37,13 @@ export class ActorSheet5e extends ActorSheet {
}); });
} }
/* -------------------------------------------- */
/** @override */
get template() {
if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/limited-sheet.html";
return `systems/sw5e/templates/actors/${this.actor.data.type}-sheet.html`;
}
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -53,6 +60,7 @@ export class ActorSheet5e extends ActorSheet {
cssClass: isOwner ? "editable" : "locked", cssClass: isOwner ? "editable" : "locked",
isCharacter: this.entity.data.type === "character", isCharacter: this.entity.data.type === "character",
isNPC: this.entity.data.type === "npc", isNPC: this.entity.data.type === "npc",
isVehicle: this.entity.data.type === 'vehicle',
config: CONFIG.SW5E, config: CONFIG.SW5E,
}; };
@ -75,11 +83,13 @@ export class ActorSheet5e extends ActorSheet {
} }
// Update skill labels // Update skill labels
for ( let [s, skl] of Object.entries(data.actor.data.skills)) { if (data.actor.data.skills) {
skl.ability = data.actor.data.abilities[skl.ability].label.substring(0, 3); for ( let [s, skl] of Object.entries(data.actor.data.skills)) {
skl.icon = this._getProficiencyIcon(skl.value); skl.ability = CONFIG.SW5E.abilityAbbreviations[skl.ability];
skl.hover = CONFIG.SW5E.proficiencyLevels[skl.value]; skl.icon = this._getProficiencyIcon(skl.value);
skl.label = CONFIG.SW5E.skills[s]; skl.hover = CONFIG.SW5E.proficiencyLevels[skl.value];
skl.label = CONFIG.SW5E.skills[s];
}
} }
// Update traits // Update traits
@ -96,9 +106,9 @@ export class ActorSheet5e extends ActorSheet {
_prepareTraits(traits) { _prepareTraits(traits) {
const map = { const map = {
"dr": CONFIG.SW5E.damageTypes, "dr": CONFIG.SW5E.damageResistanceTypes,
"di": CONFIG.SW5E.damageTypes, "di": CONFIG.SW5E.damageResistanceTypes,
"dv": CONFIG.SW5E.damageTypes, "dv": CONFIG.SW5E.damageResistanceTypes,
"ci": CONFIG.SW5E.conditionTypes, "ci": CONFIG.SW5E.conditionTypes,
"languages": CONFIG.SW5E.languages, "languages": CONFIG.SW5E.languages,
"armorProf": CONFIG.SW5E.armorProficiencies, "armorProf": CONFIG.SW5E.armorProficiencies,
@ -200,7 +210,7 @@ export class ActorSheet5e extends ActorSheet {
if ( mode in sections ) { if ( mode in sections ) {
s = sections[mode]; s = sections[mode];
if ( !powerbook[s] ){ if ( !powerbook[s] ){
registerSection(sl, s, CONFIG.SW5E.powerPreparationModes[mode], levels[mode]); registerSection(mode, s, CONFIG.SW5E.powerPreparationModes[mode], levels[mode]);
} }
} }
@ -252,7 +262,7 @@ export class ActorSheet5e extends ActorSheet {
// Equipment-specific filters // Equipment-specific filters
if ( filters.has("equipped") ) { if ( filters.has("equipped") ) {
if (data.equipped && data.equipped !== true) return false; if ( data.equipped !== true ) return false;
} }
return true; return true;
}); });
@ -295,8 +305,10 @@ export class ActorSheet5e extends ActorSheet {
// Editable Only Listeners // Editable Only Listeners
if ( this.isEditable ) { if ( this.isEditable ) {
// Relative updates for numeric fields // Input focus and update
html.find('input[data-dtype="Number"]').change(this._onChangeInputDelta.bind(this)); const inputs = html.find("input");
inputs.focus(ev => ev.currentTarget.select());
inputs.addBack().find('[data-dtype="Number"]').change(this._onChangeInputDelta.bind(this));
// Ability Proficiency // Ability Proficiency
html.find('.ability-proficiency').click(this._onToggleAbilityProficiency.bind(this)); html.find('.ability-proficiency').click(this._onToggleAbilityProficiency.bind(this));
@ -328,14 +340,6 @@ export class ActorSheet5e extends ActorSheet {
// Roll Skill Checks // Roll Skill Checks
html.find('.skill-name').click(this._onRollSkillCheck.bind(this)); html.find('.skill-name').click(this._onRollSkillCheck.bind(this));
// Item Dragging
let handler = ev => this._onDragItemStart(ev);
html.find('li.item').each((i, li) => {
if ( li.classList.contains("inventory-header") ) return;
li.setAttribute("draggable", true);
li.addEventListener("dragstart", handler, false);
});
// Item Rolling // Item Rolling
html.find('.item .item-image').click(event => this._onItemRoll(event)); html.find('.item .item-image').click(event => this._onItemRoll(event));
html.find('.item .item-recharge').click(event => this._onItemRecharge(event)); html.find('.item .item-recharge').click(event => this._onItemRecharge(event));
@ -424,37 +428,9 @@ export class ActorSheet5e extends ActorSheet {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */ /** @override */
async _onDrop (event) { async _onDropActor(event, data) {
event.preventDefault(); const canPolymorph = game.user.isGM || (this.actor.owner && game.settings.get('sw5e', 'allowPolymorphing'));
if ( !canPolymorph ) return false;
// Get dropped data
let data;
try {
data = JSON.parse(event.dataTransfer.getData('text/plain'));
} catch (err) {
return false;
}
// Handle a polymorph
if (data && (data.type === "Actor")) {
if (game.user.isGM || (game.settings.get('sw5e', 'allowPolymorphing') && this.actor.owner)) {
return this._onDropPolymorph(event, data);
}
}
// Call parent on drop logic
return super._onDrop(event);
}
/* -------------------------------------------- */
/**
* Handle dropping an Actor on the sheet to trigger a Polymorph workflow
* @param {DragEvent} event The drop event
* @param {Object} data The data transfer
* @private
*/
async _onDropPolymorph(event, data) {
// Get the target actor // Get the target actor
let sourceActor = null; let sourceActor = null;
@ -521,6 +497,30 @@ export class ActorSheet5e extends ActorSheet {
}).render(true); }).render(true);
} }
/* -------------------------------------------- */
/** @override */
async _onDropItemCreate(itemData) {
// Create a Consumable power scroll on the Inventory tab
if ( (itemData.type === "power") && (this._tabs[0].active === "inventory") ) {
const scroll = await Item5e.createScrollFromPower(itemData);
itemData = scroll.data;
}
// Upgrade the number of class levels a character has
if ( (itemData.type === "class") && ( this.actor.itemTypes.class.find(c => c.name === itemData.name)) ) {
const cls = this.actor.itemTypes.class.find(c => c.name === itemData.name);
const lvl = cls.data.data.levels;
return cls.update({"data.levels": Math.min(lvl + 1, 20 + lvl - this.actor.data.data.details.level)})
}
// Create the owned item as normal
// TODO remove conditional logic in 0.7.x
if (isNewerVersion(game.data.version, "0.6.9")) return super._onDropItemCreate(itemData);
else return this.actor.createEmbeddedEntity("OwnedItem", itemData);
}
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
@ -634,7 +634,7 @@ export class ActorSheet5e extends ActorSheet {
const header = event.currentTarget; const header = event.currentTarget;
const type = header.dataset.type; const type = header.dataset.type;
const itemData = { const itemData = {
name: `New ${type.capitalize()}`, name: game.i18n.format("SW5E.ItemNew", {type: type.capitalize()}),
type: type, type: type,
data: duplicate(header.dataset) data: duplicate(header.dataset)
}; };
@ -736,11 +736,8 @@ export class ActorSheet5e extends ActorSheet {
event.preventDefault(); event.preventDefault();
const a = event.currentTarget; const a = event.currentTarget;
const label = a.parentElement.querySelector("label"); const label = a.parentElement.querySelector("label");
const options = { const choices = CONFIG.SW5E[a.dataset.options];
name: label.getAttribute("for"), const options = { name: a.dataset.target, title: label.innerText, choices };
title: label.innerText,
choices: CONFIG.SW5E[a.dataset.options]
};
new TraitSelector(this.actor, options).render(true) new TraitSelector(this.actor, options).render(true)
} }
@ -760,4 +757,90 @@ export class ActorSheet5e extends ActorSheet {
}); });
return buttons; return buttons;
} }
/* -------------------------------------------- */
/* DEPRECATED */
/* -------------------------------------------- */
/**
* TODO: Remove once 0.7.x is release
* @deprecated since 0.7.0
*/
async _onDrop (event) {
event.preventDefault();
// Get dropped data
let data;
try {
data = JSON.parse(event.dataTransfer.getData('text/plain'));
} catch (err) {
return false;
}
if ( !data ) return false;
// Handle the drop with a Hooked function
const allowed = Hooks.call("dropActorSheetData", this.actor, this, data);
if ( allowed === false ) return;
// Case 1 - Dropped Item
if ( data.type === "Item" ) {
return this._onDropItem(event, data);
}
// Case 2 - Dropped Actor
if ( data.type === "Actor" ) {
return this._onDropActor(event, data);
}
}
/* -------------------------------------------- */
/**
* TODO: Remove once 0.7.x is release
* @deprecated since 0.7.0
*/
async _onDropItem(event, data) {
if ( !this.actor.owner ) return false;
let itemData = await this._getItemDropData(event, data);
// Handle item sorting within the same Actor
const actor = this.actor;
let sameActor = (data.actorId === actor._id) || (actor.isToken && (data.tokenId === actor.token.id));
if (sameActor) return this._onSortItem(event, itemData);
// Create a new item
this._onDropItemCreate(itemData);
}
/* -------------------------------------------- */
/**
* TODO: Remove once 0.7.x is release
* @deprecated since 0.7.0
*/
async _getItemDropData(event, data) {
let itemData = null;
// Case 1 - Import from a Compendium pack
if (data.pack) {
const pack = game.packs.get(data.pack);
if (pack.metadata.entity !== "Item") return;
itemData = await pack.getEntry(data.id);
}
// Case 2 - Data explicitly provided
else if (data.data) {
itemData = data.data;
}
// Case 3 - Import from World entity
else {
let item = game.items.get(data.id);
if (!item) return;
itemData = item.data;
}
// Return a copy of the extracted data
return duplicate(itemData);
}
} }

View file

@ -1,11 +1,11 @@
import { ActorSheet5e } from "./base.js"; import ActorSheet5e from "./base.js";
/** /**
* An Actor sheet for player character type actors in the SW5E system. * An Actor sheet for player character type actors in the SW5E system.
* Extends the base ActorSheet5e class. * Extends the base ActorSheet5e class.
* @type {ActorSheet5e} * @type {ActorSheet5e}
*/ */
export class ActorSheet5eCharacter extends ActorSheet5e { export default class ActorSheet5eCharacter extends ActorSheet5e {
/** /**
* Define default rendering options for the NPC sheet * Define default rendering options for the NPC sheet
@ -14,24 +14,11 @@ export class ActorSheet5eCharacter extends ActorSheet5e {
static get defaultOptions() { static get defaultOptions() {
return mergeObject(super.defaultOptions, { return mergeObject(super.defaultOptions, {
classes: ["sw5e", "sheet", "actor", "character"], classes: ["sw5e", "sheet", "actor", "character"],
width: 672, width: 720,
height: 736 height: 736
}); });
} }
/* -------------------------------------------- */
/* Rendering */
/* -------------------------------------------- */
/**
* Get the correct HTML template path to use for rendering this particular sheet
* @type {String}
*/
get template() {
if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/limited-sheet.html";
return "systems/sw5e/templates/actors/character-sheet.html";
}
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
@ -57,6 +44,7 @@ export class ActorSheet5eCharacter extends ActorSheet5e {
// Experience Tracking // Experience Tracking
sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking"); sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking");
sheetData["classLabels"] = this.actor.itemTypes.class.map(c => c.name).join(", ");
// Return data for rendering // Return data for rendering
return sheetData; return sheetData;
@ -80,13 +68,12 @@ export class ActorSheet5eCharacter extends ActorSheet5e {
loot: { label: "SW5E.ItemTypeLootPl", items: [], dataset: {type: "loot"} } loot: { label: "SW5E.ItemTypeLootPl", items: [], dataset: {type: "loot"} }
}; };
// Partition items by category // Partition items by category
let [items, powers, feats, classes, species] = data.items.reduce((arr, item) => { let [items, powers, feats, classes, species] = data.items.reduce((arr, item) => {
// Item details // Item details
item.img = item.img || DEFAULT_TOKEN; item.img = item.img || DEFAULT_TOKEN;
item.isStack = item.data.quantity ? item.data.quantity > 1 : false; item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
// Item usage // Item usage
item.hasUses = item.data.uses && (item.data.uses.max > 0); item.hasUses = item.data.uses && (item.data.uses.max > 0);
@ -101,7 +88,7 @@ export class ActorSheet5eCharacter extends ActorSheet5e {
if ( item.type === "power" ) arr[1].push(item); if ( item.type === "power" ) arr[1].push(item);
else if ( item.type === "feat" ) arr[2].push(item); else if ( item.type === "feat" ) arr[2].push(item);
else if ( item.type === "class" ) arr[3].push(item); else if ( item.type === "class" ) arr[3].push(item);
else if ( item.type === "species" ) arr[4].push(item); else if ( item.type === "species" ) arr[4].push(item);
else if ( Object.keys(inventory).includes(item.type ) ) arr[0].push(item); else if ( Object.keys(inventory).includes(item.type ) ) arr[0].push(item);
return arr; return arr;
}, [[], [], [], [], []]); }, [[], [], [], [], []]);
@ -111,27 +98,24 @@ export class ActorSheet5eCharacter extends ActorSheet5e {
powers = this._filterItems(powers, this._filters.powerbook); powers = this._filterItems(powers, this._filters.powerbook);
feats = this._filterItems(feats, this._filters.features); feats = this._filterItems(feats, this._filters.features);
// Organize items
for ( let i of items ) {
i.data.quantity = i.data.quantity || 0;
i.data.weight = i.data.weight || 0;
i.totalWeight = Math.round(i.data.quantity * i.data.weight * 10) / 10;
inventory[i.type].items.push(i);
}
// Organize Powerbook and count the number of prepared powers (excluding always, at will, etc...) // Organize Powerbook and count the number of prepared powers (excluding always, at will, etc...)
const powerbook = this._preparePowerbook(data, powers); const powerbook = this._preparePowerbook(data, powers);
const nPrepared = powers.filter(s => { const nPrepared = powers.filter(s => {
return (s.data.level > 0) && (s.data.preparation.mode === "prepared") && s.data.preparation.prepared; return (s.data.level > 0) && (s.data.preparation.mode === "prepared") && s.data.preparation.prepared;
}).length; }).length;
// Organize Inventory
let totalWeight = 0;
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;
inventory[i.type].items.push(i);
totalWeight += i.totalWeight;
}
data.data.attributes.encumbrance = this._computeEncumbrance(totalWeight, data);
// Organize Features // Organize Features
const features = { const features = {
classes: { label: "SW5E.ItemTypeClassPl", items: [], hasActions: false, dataset: {type: "class"}, isClass: true }, classes: { label: "SW5E.ItemTypeClassPl", items: [], hasActions: false, dataset: {type: "class"}, isClass: true },
species: { label: "SW5E.ItemTypeSpecies", items: [], hasActions: false, dataset: {type: "species"}, isSpecies: true}, species: { label: "SW5E.ItemTypeSpecies", items: [], hasActions: false, dataset: {type: "species"}, isSpecies: true},
active: { label: "SW5E.FeatureActive", items: [], hasActions: true, dataset: {type: "feat", "activation.type": "action"} }, active: { label: "SW5E.FeatureActive", items: [], hasActions: true, dataset: {type: "feat", "activation.type": "action"} },
passive: { label: "SW5E.FeaturePassive", items: [], hasActions: false, dataset: {type: "feat"} } passive: { label: "SW5E.FeaturePassive", items: [], hasActions: false, dataset: {type: "feat"} }
}; };
@ -141,7 +125,7 @@ export class ActorSheet5eCharacter extends ActorSheet5e {
} }
classes.sort((a, b) => b.levels - a.levels); classes.sort((a, b) => b.levels - a.levels);
features.classes.items = classes; features.classes.items = classes;
features.species.items = species; features.species.items = species;
// Assign and return // Assign and return
data.inventory = Object.values(inventory); data.inventory = Object.values(inventory);
@ -174,51 +158,6 @@ export class ActorSheet5eCharacter extends ActorSheet5e {
} }
} }
/* -------------------------------------------- */
/**
* Compute the level and percentage of encumbrance for an Actor.
*
* Optionally include the weight of carried currency across all denominations by applying the standard rule
* from the PHB pg. 143
*
* @param {Number} totalWeight The cumulative item weight from inventory items
* @param {Object} actorData The data object for the Actor being rendered
* @return {Object} An object describing the character's encumbrance level
* @private
*/
_computeEncumbrance(totalWeight, actorData) {
// Encumbrance classes
let mod = {
tiny: 0.5,
sm: 1,
med: 1,
lg: 2,
huge: 4,
grg: 8
}[actorData.data.traits.size] || 1;
// Apply Powerful Build feat
if ( this.actor.getFlag("sw5e", "powerfulBuild") ) mod = Math.min(mod * 2, 8);
// Add Currency Weight
if ( game.settings.get("sw5e", "currencyWeight") ) {
const currency = actorData.data.currency;
const numCoins = Object.values(currency).reduce((val, denom) => val += denom, 0);
totalWeight += numCoins / CONFIG.SW5E.encumbrance.currencyPerWeight;
}
// Compute Encumbrance percentage
const enc = {
max: actorData.data.abilities.str.value * CONFIG.SW5E.encumbrance.strMultiplier * mod,
value: Math.round(totalWeight * 10) / 10,
};
enc.pct = Math.min(enc.value * 100 / enc.max, 99);
enc.encumbered = enc.pct > (2/3);
return enc;
}
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Event Listeners and Handlers /* Event Listeners and Handlers
/* -------------------------------------------- */ /* -------------------------------------------- */

View file

@ -1,37 +1,21 @@
import { ActorSheet5e } from "../sheets/base.js"; import ActorSheet5e from "../sheets/base.js";
/** /**
* An Actor sheet for NPC type characters in the SW5E system. * An Actor sheet for NPC type characters in the SW5E system.
* Extends the base ActorSheet5e class. * Extends the base ActorSheet5e class.
* @type {ActorSheet5e} * @extends {ActorSheet5e}
*/ */
export class ActorSheet5eNPC extends ActorSheet5e { export default class ActorSheet5eNPC extends ActorSheet5e {
/** /** @override */
* Define default rendering options for the NPC sheet
* @return {Object}
*/
static get defaultOptions() { static get defaultOptions() {
return mergeObject(super.defaultOptions, { return mergeObject(super.defaultOptions, {
classes: ["sw5e", "sheet", "actor", "npc"], classes: ["sw5e", "sheet", "actor", "npc"],
width: 600, width: 600,
height: 658 height: 680
}); });
} }
/* -------------------------------------------- */
/* Rendering */
/* -------------------------------------------- */
/**
* Get the correct HTML template path to use for rendering this particular sheet
* @type {String}
*/
get template() {
if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/limited-sheet.html";
return "systems/sw5e/templates/actors/npc-sheet.html";
}
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
@ -42,16 +26,16 @@ export class ActorSheet5eNPC extends ActorSheet5e {
// Categorize Items as Features and Powers // Categorize Items as Features and Powers
const features = { const features = {
weapons: { label: "Attacks", items: [] , hasActions: true, dataset: {type: "weapon", "weapon-type": "natural"} }, weapons: { label: game.i18n.localize("SW5E.AttackPl"), items: [] , hasActions: true, dataset: {type: "weapon", "weapon-type": "natural"} },
actions: { label: "Actions", items: [] , hasActions: true, dataset: {type: "feat", "activation.type": "action"} }, actions: { label: game.i18n.localize("SW5E.ActionPl"), items: [] , hasActions: true, dataset: {type: "feat", "activation.type": "action"} },
passive: { label: "Features", items: [], dataset: {type: "feat"} }, passive: { label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"} },
equipment: { label: "Inventory", items: [], dataset: {type: "loot"}} equipment: { label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}}
}; };
// Start by classifying items into groups for rendering // Start by classifying items into groups for rendering
let [powers, other] = data.items.reduce((arr, item) => { let [powers, other] = data.items.reduce((arr, item) => {
item.img = item.img || DEFAULT_TOKEN; item.img = item.img || DEFAULT_TOKEN;
item.isStack = item.data.quantity ? item.data.quantity > 1 : false; item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
item.hasUses = item.data.uses && (item.data.uses.max > 0); item.hasUses = item.data.uses && (item.data.uses.max > 0);
item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false); item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false);
item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0)); item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0));
@ -86,9 +70,7 @@ export class ActorSheet5eNPC extends ActorSheet5e {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /** @override */
* Add some extra data when rendering the sheet to reduce the amount of logic required within the template.
*/
getData() { getData() {
const data = super.getData(); const data = super.getData();
@ -103,12 +85,7 @@ export class ActorSheet5eNPC extends ActorSheet5e {
/* Object Updates */ /* Object Updates */
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /** @override */
* This method is called upon form submission after form data is validated
* @param event {Event} The initial triggering submission event
* @param formData {Object} The object of validated form data with which to update the object
* @private
*/
_updateObject(event, formData) { _updateObject(event, formData) {
// Format NPC Challenge Rating // Format NPC Challenge Rating
@ -126,14 +103,9 @@ export class ActorSheet5eNPC extends ActorSheet5e {
/* Event Listeners and Handlers */ /* Event Listeners and Handlers */
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /** @override */
* Activate event listeners using the prepared sheet HTML
* @param html {HTML} The prepared HTML object ready to be rendered into the DOM
*/
activateListeners(html) { activateListeners(html) {
super.activateListeners(html); super.activateListeners(html);
// Rollable Health Formula
html.find(".health .rollable").click(this._onRollHealthFormula.bind(this)); html.find(".health .rollable").click(this._onRollHealthFormula.bind(this));
} }
@ -152,4 +124,4 @@ export class ActorSheet5eNPC extends ActorSheet5e {
AudioHelper.play({src: CONFIG.sounds.dice}); AudioHelper.play({src: CONFIG.sounds.dice});
this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp}); this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp});
} }
} }

View file

@ -0,0 +1,381 @@
import ActorSheet5e from "./base.js";
/**
* An Actor sheet for Vehicle type actors.
* Extends the base ActorSheet5e class.
* @type {ActorSheet5e}
*/
export default class ActorSheet5eVehicle extends ActorSheet5e {
/**
* Define default rendering options for the Vehicle sheet.
* @returns {Object}
*/
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
classes: ["sw5e", "sheet", "actor", "vehicle"],
width: 605,
height: 680
});
}
/* -------------------------------------------- */
/**
* Creates a new cargo entry for a vehicle Actor.
*/
static get newCargo() {
return {
name: '',
quantity: 1
};
}
/* -------------------------------------------- */
/**
* Compute the total weight of the vehicle's cargo.
* @param {Number} totalWeight The cumulative item weight from inventory items
* @param {Object} actorData The data object for the Actor being rendered
* @returns {{max: number, value: number, pct: number}}
* @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;
// Vehicle weights are an order of magnitude greater.
totalWeight /= CONFIG.SW5E.encumbrance.vehicleWeightMultiplier;
// Compute overall encumbrance
const enc = {
max: actorData.data.attributes.capacity.cargo,
value: Math.round(totalWeight * 10) / 10
};
enc.pct = Math.min(enc.value * 100 / enc.max, 99);
return enc;
}
/* -------------------------------------------- */
/**
* Prepare items that are mounted to a vehicle and require one or more crew
* to operate.
* @private
*/
_prepareCrewedItem(item) {
// Determine crewed status
const isCrewed = item.data.crewed;
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') {
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 = '—';
}
// Prepare vehicle weapons
if (item.type === 'equipment' || item.type === 'weapon') {
item.threshold = item.data.hp.dt ? item.data.hp.dt : '—';
}
}
/* -------------------------------------------- */
/**
* Organize Owned Items for rendering the Vehicle sheet.
* @private
*/
_prepareItems(data) {
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 features = {
actions: {
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'
}]
},
equipment: {
label: game.i18n.localize('SW5E.ItemTypeEquipment'),
items: [],
crewable: true,
dataset: {type: 'equipment', 'armor.type': 'vehicle'},
columns: equipmentColumns
},
passive: {
label: game.i18n.localize('SW5E.Features'),
items: [],
dataset: {type: 'feat'}
},
reactions: {
label: game.i18n.localize('SW5E.ReactionPl'),
items: [],
dataset: {type: 'feat', 'activation.type': 'reaction'}
},
weapons: {
label: game.i18n.localize('SW5E.ItemTypeWeaponPl'),
items: [],
crewable: true,
dataset: {type: 'weapon', 'weapon-type': 'siege'},
columns: equipmentColumns
}
};
const cargo = {
crew: {
label: game.i18n.localize('SW5E.VehicleCrew'),
items: data.data.cargo.crew,
css: 'cargo-row crew',
editableName: true,
dataset: {type: 'crew'},
columns: cargoColumns
},
passengers: {
label: game.i18n.localize('SW5E.VehiclePassengers'),
items: data.data.cargo.passengers,
css: 'cargo-row passengers',
editableName: true,
dataset: {type: 'passengers'},
columns: cargoColumns
},
cargo: {
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'
}]
}
};
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') {
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);
}
}
data.features = Object.values(features);
data.cargo = Object.values(cargo);
data.data.attributes.encumbrance = this._computeEncumbrance(totalWeight, data);
}
/* -------------------------------------------- */
/* Event Listeners and Handlers */
/* -------------------------------------------- */
/** @override */
activateListeners(html) {
super.activateListeners(html);
if (!this.options.editable) return;
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())
.change(this._onEditInSheet.bind(this));
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();
}
}
/* -------------------------------------------- */
/**
* Handle saving a cargo row (i.e. crew or passenger) in-sheet.
* @param event {Event}
* @returns {Promise<Actor>|null}
* @private
*/
_onCargoRowChange(event) {
event.preventDefault();
const target = event.currentTarget;
const row = target.closest('.item');
const idx = Number(row.dataset.itemId);
const property = row.classList.contains('crew') ? 'crew' : 'passengers';
// Get the cargo entry
const cargo = duplicate(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 type = target.dataset.dtype;
let value = target.value;
if (type === 'Number') value = Number(value);
entry[key] = value;
// Perform the Actor update
return this.actor.update({[`data.cargo.${property}`]: cargo});
}
/* -------------------------------------------- */
/**
* Handle editing certain values like quantity, price, and weight in-sheet.
* @param event {Event}
* @returns {Promise<Item>}
* @private
*/
_onEditInSheet(event) {
event.preventDefault();
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;
}
return item.update({[`${property}`]: value});
}
/* -------------------------------------------- */
/**
* Handle creating a new crew or passenger row.
* @param event {Event}
* @returns {Promise<Actor|Item>}
* @private
*/
_onItemCreate(event) {
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]);
cargo.push(this.constructor.newCargo);
return this.actor.update({[`data.cargo.${type}`]: cargo});
}
return super._onItemCreate(event);
}
/* -------------------------------------------- */
/**
* Handle deleting a crew or passenger row.
* @param event {Event}
* @returns {Promise<Actor|Item>}
* @private
*/
_onItemDelete(event) {
event.preventDefault();
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);
return this.actor.update({[`data.cargo.${type}`]: cargo});
}
return super._onItemDelete(event);
}
/* -------------------------------------------- */
/**
* Special handling for editing HP to clamp it within appropriate range.
* @param event {Event}
* @returns {Promise<Item>}
* @private
*/
_onHPChange(event) {
event.preventDefault();
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});
}
/* -------------------------------------------- */
/**
* Handle toggling an item's crewed status.
* @param event {Event}
* @returns {Promise<Item>}
* @private
*/
_onToggleItem(event) {
event.preventDefault();
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});
}
};

View file

@ -2,7 +2,7 @@
* A specialized Dialog subclass for ability usage * A specialized Dialog subclass for ability usage
* @type {Dialog} * @type {Dialog}
*/ */
export class AbilityUseDialog extends Dialog { export default class AbilityUseDialog extends Dialog {
constructor(item, dialogData={}, options={}) { constructor(item, dialogData={}, options={}) {
super(dialogData, options); super(dialogData, options);
this.options.classes = ["sw5e", "dialog"]; this.options.classes = ["sw5e", "dialog"];
@ -25,40 +25,150 @@ export class AbilityUseDialog extends Dialog {
* @return {Promise} * @return {Promise}
*/ */
static async create(item) { static async create(item) {
if ( !item.isOwned ) throw new Error("You cannot display an ability usage dialog for an unowned item");
const uses = item.data.data.uses; // Prepare data
const recharge = item.data.data.recharge; const actorData = item.actor.data.data;
const itemData = item.data.data;
const uses = itemData.uses || {};
const quantity = itemData.quantity || 0;
const recharge = itemData.recharge || {};
const recharges = !!recharge.value; const recharges = !!recharge.value;
// Render the ability usage template // Prepare dialog form data
const html = await renderTemplate("systems/sw5e/templates/apps/ability-use.html", { const data = {
item: item.data, item: item.data,
canUse: recharges ? recharge.charged : uses.value > 0, title: game.i18n.format("SW5E.AbilityUseHint", item.data),
consume: true, note: this._getAbilityUseNote(item.data, uses, recharge),
uses: uses, hasLimitedUses: uses.max || recharges,
recharges: !!recharge.value, canUse: recharges ? recharge.charged : (quantity > 0 && !uses.value) || uses.value > 0,
isCharged: recharge.charged,
hasPlaceableTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget, hasPlaceableTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget,
perLabel: CONFIG.SW5E.limitedUsePeriods[uses.per] errors: []
}); };
if ( item.data.type === "power" ) this._getPowerData(actorData, itemData, data);
// Render the ability usage template
const html = await renderTemplate("systems/sw5e/templates/apps/ability-use.html", data);
// Create the Dialog and return as a Promise // Create the Dialog and return as a Promise
const icon = data.hasPowerSlots ? "fa-magic" : "fa-fist-raised";
const label = game.i18n.localize("SW5E.AbilityUse" + (data.hasPowerSlots ? "Cast" : "Use"));
return new Promise((resolve) => { return new Promise((resolve) => {
let formData = null;
const dlg = new this(item, { const dlg = new this(item, {
title: `${item.name}: Ability Configuration`, title: `${item.name}: Usage Configuration`,
content: html, content: html,
buttons: { buttons: {
use: { use: {
icon: '<i class="fas fa-fist-raised"></i>', icon: `<i class="fas ${icon}"></i>`,
label: "Use Ability", label: label,
callback: html => formData = new FormData(html[0].querySelector("#ability-use-form")) callback: html => resolve(new FormData(html[0].querySelector("form")))
} }
}, },
default: "use", default: "use",
close: () => resolve(formData) close: () => resolve(null)
}); });
dlg.render(true); dlg.render(true);
}); });
} }
/* -------------------------------------------- */
/* Helpers */
/* -------------------------------------------- */
/**
* Get dialog data related to limited power slots
* @private
*/
static _getPowerData(actorData, itemData, data) {
// Determine whether the power may be up-cast
const lvl = itemData.level;
const canUpcast = (lvl > 0) && CONFIG.SW5E.powerUpcastModes.includes(itemData.preparation.mode);
// Determine the levels which are feasible
let lmax = 0;
const powerLevels = Array.fromRange(10).reduce((arr, i) => {
if ( i < lvl ) return arr;
const label = CONFIG.SW5E.powerLevels[i];
const l = actorData.powers["power"+i] || {max: 0, override: null};
let max = parseInt(l.override || l.max || 0);
let slots = Math.clamped(parseInt(l.value || 0), 0, max);
if ( max > 0 ) lmax = i;
arr.push({
level: i,
label: i > 0 ? game.i18n.format('SW5E.PowerLevelSlot', {level: label, n: slots}) : label,
canCast: canUpcast && (max > 0),
hasSlots: slots > 0
});
return arr;
}, []).filter(sl => sl.level <= lmax);
// If this character has pact slots, present them as an option for casting the power.
const pact = actorData.powers.pact;
if (pact.level >= lvl) {
powerLevels.push({
level: 'pact',
label: `${game.i18n.format('SW5E.PowerLevelPact', {level: pact.level, n: pact.value})}`,
canCast: canUpcast,
hasSlots: pact.value > 0
});
}
const canCast = powerLevels.some(l => l.hasSlots);
// Return merged data
data = mergeObject(data, { hasPowerSlots: true, canUpcast, powerLevels });
if ( !canCast ) data.errors.push("SW5E.PowerCastNoSlots");
}
/* -------------------------------------------- */
/**
* Get the ability usage note that is displayed
* @private
*/
static _getAbilityUseNote(item, uses, recharge) {
// Zero quantity
const quantity = item.data.quantity;
if ( quantity <= 0 ) return game.i18n.localize("SW5E.AbilityUseUnavailableHint");
// Abilities which use Recharge
if ( !!recharge.value ) {
return game.i18n.format(recharge.charged ? "SW5E.AbilityUseChargedHint" : "SW5E.AbilityUseRechargeHint", {
type: item.type,
})
}
// Does not use any resource
if ( !uses.per || !uses.max ) return "";
// Consumables
if ( item.type === "consumable" ) {
let str = "SW5E.AbilityUseNormalHint";
if ( uses.value > 1 ) str = "SW5E.AbilityUseConsumableChargeHint";
else if ( item.data.quantity === 1 && uses.autoDestroy ) str = "SW5E.AbilityUseConsumableDestroyHint";
else if ( item.data.quantity > 1 ) str = "SW5E.AbilityUseConsumableQuantityHint";
return game.i18n.format(str, {
type: item.data.consumableType,
value: uses.value,
quantity: item.data.quantity,
});
}
// Other Items
else {
return game.i18n.format("SW5E.AbilityUseNormalHint", {
type: item.type,
value: uses.value,
max: uses.max,
per: CONFIG.SW5E.limitedUsePeriods[uses.per]
});
}
}
/* -------------------------------------------- */
static _handleSubmit(formData, item) {
}
} }

View file

@ -1,5 +1,9 @@
export class ActorSheetFlags extends BaseEntitySheet { /**
static get defaultOptions() { * An application class which provides advanced configuration for special character flags which modify an Actor
* @extends {BaseEntitySheet}
*/
export default class ActorSheetFlags extends BaseEntitySheet {
static get defaultOptions() {
const options = super.defaultOptions; const options = super.defaultOptions;
return mergeObject(options, { return mergeObject(options, {
id: "actor-flags", id: "actor-flags",
@ -68,10 +72,10 @@ export class ActorSheetFlags extends BaseEntitySheet {
{name: "data.bonuses.mwak.damage", label: "SW5E.BonusMWDamage"}, {name: "data.bonuses.mwak.damage", label: "SW5E.BonusMWDamage"},
{name: "data.bonuses.rwak.attack", label: "SW5E.BonusRWAttack"}, {name: "data.bonuses.rwak.attack", label: "SW5E.BonusRWAttack"},
{name: "data.bonuses.rwak.damage", label: "SW5E.BonusRWDamage"}, {name: "data.bonuses.rwak.damage", label: "SW5E.BonusRWDamage"},
{name: "data.bonuses.mpak.attack", label: "SW5E.BonusMSAttack"}, {name: "data.bonuses.mpak.attack", label: "SW5E.BonusMPAttack"},
{name: "data.bonuses.mpak.damage", label: "SW5E.BonusMSDamage"}, {name: "data.bonuses.mpak.damage", label: "SW5E.BonusMPDamage"},
{name: "data.bonuses.rpak.attack", label: "SW5E.BonusRSAttack"}, {name: "data.bonuses.rpak.attack", label: "SW5E.BonusRPAttack"},
{name: "data.bonuses.rpak.damage", label: "SW5E.BonusRSDamage"}, {name: "data.bonuses.rpak.damage", label: "SW5E.BonusRPDamage"},
{name: "data.bonuses.abilities.check", label: "SW5E.BonusAbilityCheck"}, {name: "data.bonuses.abilities.check", label: "SW5E.BonusAbilityCheck"},
{name: "data.bonuses.abilities.save", label: "SW5E.BonusAbilitySave"}, {name: "data.bonuses.abilities.save", label: "SW5E.BonusAbilitySave"},
{name: "data.bonuses.abilities.skill", label: "SW5E.BonusAbilitySkill"}, {name: "data.bonuses.abilities.skill", label: "SW5E.BonusAbilitySkill"},
@ -91,7 +95,7 @@ export class ActorSheetFlags extends BaseEntitySheet {
*/ */
async _updateObject(event, formData) { async _updateObject(event, formData) {
const actor = this.object; const actor = this.object;
const updateData = expandObject(formData); let updateData = expandObject(formData);
// Unset any flags which are "false" // Unset any flags which are "false"
let unset = false; let unset = false;
@ -106,7 +110,18 @@ export class ActorSheetFlags extends BaseEntitySheet {
} }
} }
// Apply the changes // Clear any bonuses which are whitespace only
for ( let b of Object.values(updateData.data.bonuses ) ) {
for ( let [k, v] of Object.entries(b) ) {
b[k] = v.trim();
}
}
// Diff the data against any applied overrides and apply
// TODO: Remove this logical gate once 0.7.x is release channel
if ( !isNewerVersion("0.7.1", game.data.version) ){
updateData.data = diffObject(this.object.overrides, updateData.data);
}
await actor.update(updateData, {diff: false}); await actor.update(updateData, {diff: false});
} }
} }

69
module/apps/long-rest.js Normal file
View file

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

View file

@ -1,8 +1,10 @@
import LongRestDialog from "./long-rest.js";
/** /**
* A helper Dialog subclass for rolling Hit Dice on short rest * A helper Dialog subclass for rolling Hit Dice on short rest
* @type {Dialog} * @extends {Dialog}
*/ */
export class ShortRestDialog extends Dialog { export default class ShortRestDialog extends Dialog {
constructor(actor, dialogData={}, options={}) { constructor(actor, dialogData={}, options={}) {
super(dialogData, options); super(dialogData, options);
@ -34,6 +36,8 @@ export class ShortRestDialog extends Dialog {
/** @override */ /** @override */
getData() { getData() {
const data = super.getData(); const data = super.getData();
// Determine Hit Dice
data.availableHD = this.actor.data.items.reduce((hd, item) => { data.availableHD = this.actor.data.items.reduce((hd, item) => {
if ( item.type === "class" ) { if ( item.type === "class" ) {
const d = item.data; const d = item.data;
@ -45,6 +49,11 @@ export class ShortRestDialog extends Dialog {
}, {}); }, {});
data.canRoll = this.actor.data.data.attributes.hd > 0; data.canRoll = this.actor.data.data.attributes.hd > 0;
data.denomination = this._denom; data.denomination = this._denom;
// Determine rest type
const variant = game.settings.get("sw5e", "restVariant");
data.promptNewDay = variant !== "epic"; // It's never a new day when only resting 1 minute
data.newDay = false; // It may be a new day, but not by default
return data; return data;
} }
@ -56,7 +65,6 @@ export class ShortRestDialog extends Dialog {
super.activateListeners(html); super.activateListeners(html);
let btn = html.find("#roll-hd"); let btn = html.find("#roll-hd");
btn.click(this._onRollHitDie.bind(this)); btn.click(this._onRollHitDie.bind(this));
super.activateListeners(html);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -83,21 +91,27 @@ export class ShortRestDialog extends Dialog {
* @return {Promise} * @return {Promise}
*/ */
static async shortRestDialog({actor}={}) { static async shortRestDialog({actor}={}) {
return new Promise(resolve => { return new Promise((resolve, reject) => {
const dlg = new this(actor, { const dlg = new this(actor, {
title: "Short Rest", title: "Short Rest",
buttons: { buttons: {
rest: { rest: {
icon: '<i class="fas fa-bed"></i>', icon: '<i class="fas fa-bed"></i>',
label: "Rest", label: "Rest",
callback: () => resolve(true) callback: html => {
let newDay = false;
if (game.settings.get("sw5e", "restVariant") === "gritty")
newDay = html.find('input[name="newDay"]')[0].checked;
resolve(newDay);
}
}, },
cancel: { cancel: {
icon: '<i class="fas fa-times"></i>', icon: '<i class="fas fa-times"></i>',
label: "Cancel", label: "Cancel",
callback: () => resolve(false) callback: reject
} }
} },
close: reject
}); });
dlg.render(true); dlg.render(true);
}); });
@ -108,31 +122,12 @@ export class ShortRestDialog extends Dialog {
/** /**
* A helper constructor function which displays the Long Rest confirmation dialog and returns a Promise once it's * A helper constructor function which displays the Long Rest confirmation dialog and returns a Promise once it's
* workflow has been resolved. * workflow has been resolved.
* @deprecated
* @param {Actor5e} actor * @param {Actor5e} actor
* @return {Promise} * @return {Promise}
*/ */
static async longRestDialog({actor}={}) { static async longRestDialog({actor}={}) {
const content = `<p>Take a long rest?</p><p>On a long rest you will recover hit points, half your maximum hit dice, console.warn("WARNING! ShortRestDialog.longRestDialog has been deprecated, use LongRestDialog.longRestDialog instead.");
class resources, limited use item charges, and power slots.</p>`; return LongRestDialog.longRestDialog(...arguments);
return new Promise((resolve, reject) => {
new Dialog({
title: "Long Rest",
content: content,
buttons: {
rest: {
icon: '<i class="fas fa-bed"></i>',
label: "Rest",
callback: resolve
},
cancel: {
icon: '<i class="fas fa-times"></i>',
label: "Cancel",
callback: reject
},
},
default: 'rest',
close: reject
}, {classes: ["sw5e", "dialog"]}).render(true);
});
} }
} }

View file

@ -2,7 +2,7 @@
* A specialized form used to select from a checklist of attributes, traits, or properties * A specialized form used to select from a checklist of attributes, traits, or properties
* @extends {FormApplication} * @extends {FormApplication}
*/ */
export class TraitSelector extends FormApplication { export default class TraitSelector extends FormApplication {
/** @override */ /** @override */
static get defaultOptions() { static get defaultOptions() {

View file

@ -1,32 +1,40 @@
/** /** @override */
* Measure the distance between two pixel coordinates export const measureDistances = function(segments, options={}) {
* See BaseGrid.measureDistance for more details if ( !options.gridSpaces ) return BaseGrid.prototype.measureDistances.call(this, segments, options);
*
* @param {Object} p0 The origin coordinate {x, y}
* @param {Object} p1 The destination coordinate {x, y}
* @param {boolean} gridSpaces Enforce grid distance (if true) vs. direct point-to-point (if false)
* @return {number} The distance between p1 and p0
*/
export const measureDistance = function(p0, p1, {gridSpaces=true}={}) {
if ( !gridSpaces ) return BaseGrid.prototype.measureDistance.bind(this)(p0, p1, {gridSpaces});
let gs = canvas.dimensions.size,
ray = new Ray(p0, p1),
nx = Math.abs(Math.ceil(ray.dx / gs)),
ny = Math.abs(Math.ceil(ray.dy / gs));
// Get the number of straight and diagonal moves // Track the total number of diagonals
let nDiagonal = Math.min(nx, ny), let nDiagonal = 0;
nStraight = Math.abs(ny - nx); const rule = this.parent.diagonalRule;
const d = canvas.dimensions;
// Alternative DMG Movement // Iterate over measured segments
if ( this.parent.diagonalRule === "5105" ) { return segments.map(s => {
let nd10 = Math.floor(nDiagonal / 2); let r = s.ray;
let spaces = (nd10 * 2) + (nDiagonal - nd10) + nStraight;
return spaces * canvas.dimensions.distance;
}
// Standard PHB Movement // Determine the total distance traveled
else return (nStraight + nDiagonal) * canvas.scene.data.gridDistance; let nx = Math.abs(Math.ceil(r.dx / d.size));
let ny = Math.abs(Math.ceil(r.dy / d.size));
// Determine the number of straight and diagonal moves
let nd = Math.min(nx, ny);
let ns = Math.abs(ny - nx);
nDiagonal += nd;
// Alternative DMG Movement
if (rule === "5105") {
let nd10 = Math.floor(nDiagonal / 2) - Math.floor((nDiagonal - nd) / 2);
let spaces = (nd10 * 2) + (nd - nd10) + ns;
return spaces * canvas.dimensions.distance;
}
// Euclidean Measurement
else if (rule === "EUCL") {
return Math.round(Math.hypot(nx, ny) * canvas.scene.data.gridDistance);
}
// Standard PHB Movement
else return (ns + nd) * canvas.scene.data.gridDistance;
});
}; };
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -39,8 +47,8 @@ const _TokenGetBarAttribute = Token.prototype.getBarAttribute;
export const getBarAttribute = function(...args) { export const getBarAttribute = function(...args) {
const data = _TokenGetBarAttribute.bind(this)(...args); const data = _TokenGetBarAttribute.bind(this)(...args);
if ( data && (data.attribute === "attributes.hp") ) { if ( data && (data.attribute === "attributes.hp") ) {
data.value += parseInt(data['temp'] || 0); data.value += parseInt(getProperty(this.actor.data, "data.attributes.hp.temp") || 0);
data.max += parseInt(data['tempmax'] || 0); data.max += parseInt(getProperty(this.actor.data, "data.attributes.hp.tempmax") || 0);
} }
return data; return data;
}; };

View file

@ -1,18 +1,18 @@
import {Actor5e} from "./actor/entity.js";
/** /**
* Highlight critical success or failure on d20 rolls * Highlight critical success or failure on d20 rolls
*/ */
export const highlightCriticalSuccessFailure = function(message, html, data) { export const highlightCriticalSuccessFailure = function(message, html, data) {
if ( !message.isRoll || !message.isRollVisible || !message.roll.parts.length ) return; if ( !message.isRoll || !message.isContentVisible ) return;
// Highlight rolls where the first part is a d20 roll // Highlight rolls where the first part is a d20 roll
const roll = message.roll; const roll = message.roll;
let d = roll.parts[0]; if ( !roll.dice.length ) return;
const isD20Roll = d instanceof Die && (d.faces === 20) && (d.results.length === 1); const d = roll.dice[0];
if ( !isD20Roll ) return;
// Ensure it is not a modified roll // Ensure it is an un-modified d20 roll
const isD20 = (d.faces === 20) && ( d.results.length === 1 );
if ( !isD20 ) return;
const isModifiedRoll = ("success" in d.rolls[0]) || d.options.marginSuccess || d.options.marginFailure; const isModifiedRoll = ("success" in d.rolls[0]) || d.options.marginSuccess || d.options.marginFailure;
if ( isModifiedRoll ) return; if ( isModifiedRoll ) return;
@ -60,32 +60,55 @@ export const displayChatActionButtons = function(message, html, data) {
* @return {Array} The extended options Array including new context choices * @return {Array} The extended options Array including new context choices
*/ */
export const addChatMessageContextOptions = function(html, options) { export const addChatMessageContextOptions = function(html, options) {
let canApply = li => canvas.tokens.controlledTokens.length && li.find(".dice-roll").length; let canApply = li => {
const message = game.messages.get(li.data("messageId"));
return message.isRoll && message.isContentVisible && canvas.tokens.controlled.length;
};
options.push( options.push(
{ {
name: game.i18n.localize("SW5E.ChatContextDamage"), name: game.i18n.localize("SW5E.ChatContextDamage"),
icon: '<i class="fas fa-user-minus"></i>', icon: '<i class="fas fa-user-minus"></i>',
condition: canApply, condition: canApply,
callback: li => Actor5e.applyDamage(li, 1) callback: li => applyChatCardDamage(li, 1)
}, },
{ {
name: game.i18n.localize("SW5E.ChatContextHealing"), name: game.i18n.localize("SW5E.ChatContextHealing"),
icon: '<i class="fas fa-user-plus"></i>', icon: '<i class="fas fa-user-plus"></i>',
condition: canApply, condition: canApply,
callback: li => Actor5e.applyDamage(li, -1) callback: li => applyChatCardDamage(li, -1)
}, },
{ {
name: game.i18n.localize("SW5E.ChatContextDoubleDamage"), name: game.i18n.localize("SW5E.ChatContextDoubleDamage"),
icon: '<i class="fas fa-user-injured"></i>', icon: '<i class="fas fa-user-injured"></i>',
condition: canApply, condition: canApply,
callback: li => Actor5e.applyDamage(li, 2) callback: li => applyChatCardDamage(li, 2)
}, },
{ {
name: game.i18n.localize("SW5E.ChatContextHalfDamage"), name: game.i18n.localize("SW5E.ChatContextHalfDamage"),
icon: '<i class="fas fa-user-shield"></i>', icon: '<i class="fas fa-user-shield"></i>',
condition: canApply, condition: canApply,
callback: li => Actor5e.applyDamage(li, 0.5) callback: li => applyChatCardDamage(li, 0.5)
} }
); );
return options; return options;
}; };
/* -------------------------------------------- */
/**
* Apply rolled dice damage to the token or tokens which are currently controlled.
* This allows for damage to be scaled by a multiplier to account for healing, critical hits, or resistance
*
* @param {HTMLElement} roll The chat entry which contains the roll data
* @param {Number} multiplier A damage multiplier to apply to the rolled damage.
* @return {Promise}
*/
function applyChatCardDamage(roll, multiplier) {
const amount = roll.find('.dice-total').text();
return Promise.all(canvas.tokens.controlled.map(t => {
const a = t.actor;
return a.applyDamage(amount, multiplier);
}));
}
/* -------------------------------------------- */

View file

@ -11,6 +11,48 @@ export const _getInitiativeFormula = function(combatant) {
const init = actor.data.data.attributes.init; const init = actor.data.data.attributes.init;
const parts = ["1d20", init.mod, (init.prof !== 0) ? init.prof : null, (init.bonus !== 0) ? init.bonus : null]; const parts = ["1d20", init.mod, (init.prof !== 0) ? init.prof : null, (init.bonus !== 0) ? init.bonus : null];
if ( actor.getFlag("sw5e", "initiativeAdv") ) parts[0] = "2d20kh"; if ( actor.getFlag("sw5e", "initiativeAdv") ) parts[0] = "2d20kh";
if ( CONFIG.Combat.initiative.tiebreaker ) parts.push(actor.data.data.abilities.dex.value / 100);
// 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(" + ");
}; };
/* -------------------------------------------- */
/**
* TODO: A temporary shim until 0.7.x becomes stable
* @override
*/
TokenConfig.getTrackedAttributes = function(data, _path=[]) {
// Track the path and record found attributes
const attributes = {
"bar": [],
"value": []
};
// Recursively explore the object
for ( let [k, v] of Object.entries(data) ) {
let p = _path.concat([k]);
// Check objects for both a "value" and a "max"
if ( v instanceof Object ) {
const isBar = ("value" in v) && ("max" in v);
if ( isBar ) attributes.bar.push(p);
else {
const inner = this.getTrackedAttributes(data[k], p);
attributes.bar.push(...inner.bar);
attributes.value.push(...inner.value);
}
}
// Otherwise identify values which are numeric or null
else if ( Number.isNumeric(v) || (v === null) ) {
attributes.value.push(p);
}
}
return attributes;
};

View file

@ -2,14 +2,14 @@
export const SW5E = {}; export const SW5E = {};
// ASCII Artwork // ASCII Artwork
SW5E.ASCII = `_______________________________ SW5E.ASCII = `__________________________________________
_ _
| | | |
___| |_ __ _ _ ____ ____ _ _ __ ___ ___| |_ __ _ _ ____ ____ _ _ __ ___
/ __| __/ _\ | |__\ \ /\ / / _\ | |__/ __| / __| __/ _\ | |__\ \ /\ / / _\ | |__/ __|
\__ \ || (_) | | \ V V / (_) | | \__ \ \__ \ || (_) | | \ V V / (_) | | \__ \
|___/\__\__/_|_| \_/\_/ \__/_|_| |___/ |___/\__\__/_|_| \_/\_/ \__/_|_| |___/
_______________________________`; __________________________________________`;
/** /**
@ -25,6 +25,15 @@ SW5E.abilities = {
"cha": "SW5E.AbilityCha" "cha": "SW5E.AbilityCha"
}; };
SW5E.abilityAbbreviations = {
"str": "SW5E.AbilityStrAbbr",
"dex": "SW5E.AbilityDexAbbr",
"con": "SW5E.AbilityConAbbr",
"int": "SW5E.AbilityIntAbbr",
"wis": "SW5E.AbilityWisAbbr",
"cha": "SW5E.AbilityChaAbbr"
};
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
@ -46,7 +55,7 @@ SW5E.alignments = {
SW5E.weaponProficiencies = { SW5E.weaponProficiencies = {
"sim": "SW5E.WeaponSimpleProficiency", "sim": "SW5E.WeaponSimpleProficiency",
"mar": "SW5E.WeaponMartialProficiency", "mar": "SW5E.WeaponMartialProficiency"
}; };
SW5E.toolProficiencies = { SW5E.toolProficiencies = {
@ -119,9 +128,21 @@ SW5E.abilityActivationTypes = {
"day": SW5E.timePeriods.day, "day": SW5E.timePeriods.day,
"special": SW5E.timePeriods.spec, "special": SW5E.timePeriods.spec,
"legendary": "SW5E.LegAct", "legendary": "SW5E.LegAct",
"lair": "SW5E.LairAct" "lair": "SW5E.LairAct",
"crew": "SW5E.VehicleCrewAction"
}; };
/* -------------------------------------------- */
SW5E.abilityConsumptionTypes = {
"ammo": "SW5E.ConsumeAmmunition",
"attribute": "SW5E.ConsumeAttribute",
"material": "SW5E.ConsumeMaterial",
"charges": "SW5E.ConsumeCharges"
};
/* -------------------------------------------- */ /* -------------------------------------------- */
// Creature Sizes // Creature Sizes
@ -196,7 +217,8 @@ SW5E.equipmentTypes = {
"natural": "SW5E.EquipmentNatural", "natural": "SW5E.EquipmentNatural",
"shield": "SW5E.EquipmentShield", "shield": "SW5E.EquipmentShield",
"clothing": "SW5E.EquipmentClothing", "clothing": "SW5E.EquipmentClothing",
"trinket": "SW5E.EquipmentTrinket" "trinket": "SW5E.EquipmentTrinket",
"vehicle": "SW5E.EquipmentVehicle"
}; };
@ -231,7 +253,6 @@ SW5E.consumableTypes = {
"trinket": "SW5E.ConsumableTrinket" "trinket": "SW5E.ConsumableTrinket"
}; };
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
@ -261,10 +282,14 @@ SW5E.damageTypes = {
"sonic": "SW5E.DamageSonic" "sonic": "SW5E.DamageSonic"
}; };
// Damage Resistance Types
SW5E.damageResistanceTypes = mergeObject(duplicate(SW5E.damageTypes), {
"physical": "SW5E.DamagePhysical"
});
/* -------------------------------------------- */ /* -------------------------------------------- */
// armor Types // armor Types
SW5E.armorpropertiesTypes = { SW5E.armorPropertiesTypes = {
"Absorptive": "SW5E.ArmorProperAbsorptive", "Absorptive": "SW5E.ArmorProperAbsorptive",
"Agile": "SW5E.ArmorProperAgile", "Agile": "SW5E.ArmorProperAgile",
"Anchor": "SW5E.ArmorProperAnchor", "Anchor": "SW5E.ArmorProperAnchor",
@ -315,7 +340,8 @@ SW5E.distanceUnits = {
*/ */
SW5E.encumbrance = { SW5E.encumbrance = {
currencyPerWeight: 50, currencyPerWeight: 50,
strMultiplier: 15 strMultiplier: 15,
vehicleWeightMultiplier: 2000 // 2000 lbs in a ton
}; };
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -435,7 +461,7 @@ SW5E.powerPreparationModes = {
"prepared": "SW5E.PowerPrepPrepared" "prepared": "SW5E.PowerPrepPrepared"
}; };
SW5E.powerUpcastModes = ["always"]; SW5E.powerUpcastModes = ["always", "pact", "prepared"];
SW5E.powerProgression = { SW5E.powerProgression = {
@ -604,12 +630,28 @@ SW5E.proficiencyLevels = {
/* -------------------------------------------- */ /* -------------------------------------------- */
/**
* The amount of cover provided by an object.
* In cases where multiple pieces of cover are
* in play, we take the highest value.
*/
SW5E.cover = {
0: 'SW5E.None',
.5: 'SW5E.CoverHalf',
.75: 'SW5E.CoverThreeQuarters',
1: 'SW5E.CoverTotal'
};
/* -------------------------------------------- */
// Condition Types // Condition Types
SW5E.conditionTypes = { SW5E.conditionTypes = {
"blinded": "SW5E.ConBlinded", "blinded": "SW5E.ConBlinded",
"charmed": "SW5E.ConCharmed", "charmed": "SW5E.ConCharmed",
"deafened": "SW5E.ConDeafened", "deafened": "SW5E.ConDeafened",
"diseased": "SW5E.ConDiseased",
"exhaustion": "SW5E.ConExhaustion", "exhaustion": "SW5E.ConExhaustion",
"frightened": "SW5E.ConFrightened", "frightened": "SW5E.ConFrightened",
"grappled": "SW5E.ConGrappled", "grappled": "SW5E.ConGrappled",
@ -768,8 +810,8 @@ SW5E.characterFlags = {
type: Boolean type: Boolean
}, },
"powerfulBuild": { "powerfulBuild": {
name: "Powerful Build", name: "SW5E.FlagsPowerfulBuild",
hint: "You count as one size larger when determining your carrying capacity and the weight you can push, drag, or lift.", hint: "SW5E.FlagsPowerfulBuildHint",
section: "Racial Traits", section: "Racial Traits",
type: Boolean type: Boolean
}, },
@ -798,40 +840,40 @@ SW5E.characterFlags = {
type: Boolean type: Boolean
}, },
"initiativeAdv": { "initiativeAdv": {
name: "Advantage on Initiative", name: "SW5E.FlagsInitiativeAdv",
hint: "Provided by feats or magical items.", hint: "SW5E.FlagsInitiativeAdvHint",
section: "Feats", section: "Feats",
type: Boolean type: Boolean
}, },
"initiativeAlert": { "initiativeAlert": {
name: "Alert Feat", name: "SW5E.FlagsAlert",
hint: "Provides +5 to Initiative.", hint: "SW5E.FlagsAlertHint",
section: "Feats", section: "Feats",
type: Boolean type: Boolean
}, },
"jackOfAllTrades": { "jackOfAllTrades": {
name: "Jack of All Trades", name: "SW5E.FlagsJOAT",
hint: "Half-Proficiency to Ability Checks in which you are not already Proficient.", hint: "SW5E.FlagsJOATHint",
section: "Feats", section: "Feats",
type: Boolean type: Boolean
}, },
"observantFeat": { "observantFeat": {
name: "Observant Feat", name: "SW5E.FlagsObservant",
hint: "Provides a +5 to passive Perception and Investigation.", hint: "SW5E.FlagsObservantHint",
skills: ['prc','inv'], skills: ['prc','inv'],
section: "Feats", section: "Feats",
type: Boolean type: Boolean
}, },
"remarkableAthlete": { "remarkableAthlete": {
name: "Remarkable Athlete.", name: "SW5E.FlagsRemarkableAthlete",
hint: "Half-Proficiency (rounded-up) to physical Ability Checks and Initiative.", hint: "SW5E.FlagsRemarkableAthleteHint",
abilities: ['str','dex','con'], abilities: ['str','dex','con'],
section: "Feats", section: "Feats",
type: Boolean type: Boolean
}, },
"weaponCriticalThreshold": { "weaponCriticalThreshold": {
name: "Critical Hit Threshold", name: "SW5E.FlagsCritThreshold",
hint: "Allow for expanded critical range; for example Improved or Superior Critical", hint: "SW5E.FlagsCritThresholdHint",
section: "Feats", section: "Feats",
type: Number, type: Number,
placeholder: 20 placeholder: 20

View file

@ -1,112 +1,144 @@
export class Dice5e {
/** /**
* A standardized helper function for managing core 5e "d20 rolls" * A standardized helper function for managing core 5e "d20 rolls"
* *
* Holding SHIFT, ALT, or CTRL when the attack is rolled will "fast-forward". * Holding SHIFT, ALT, or CTRL when the attack is rolled will "fast-forward".
* This chooses the default options of a normal attack with no bonus, Advantage, or Disadvantage respectively * This chooses the default options of a normal attack with no bonus, Advantage, or Disadvantage respectively
* *
* @param {Array} parts The dice roll component parts, excluding the initial d20 * @param {Array} parts The dice roll component parts, excluding the initial d20
* @param {Object} data Actor or item data against which to parse the roll * @param {Object} data Actor or item data against which to parse the roll
* @param {Event|object} event The triggering event which initiated the roll * @param {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} 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} template The HTML template used to render the roll dialog
* @param {string|null} title The dice roll UI window title * @param {string|null} title The dice roll UI window title
* @param {Object} speaker The ChatMessage speaker to pass when creating the chat * @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 {string|null} flavor Flavor text to use in the posted chat message
* @param {Boolean} fastForward Allow fast-forward advantage selection * @param {Boolean} fastForward Allow fast-forward advantage selection
* @param {Function} onClose Callback for actions to take when the dialog form is closed * @param {Function} onClose Callback for actions to take when the dialog form is closed
* @param {Object} dialogOptions Modal dialog options * @param {Object} dialogOptions Modal dialog options
* @param {boolean} advantage Apply advantage to the roll (unless otherwise specified) * @param {boolean} advantage Apply advantage to the roll (unless otherwise specified)
* @param {boolean} disadvantage Apply disadvantage 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} 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} 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 {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} elvenAccuracy Allow Elven Accuracy to modify this roll?
* @param {boolean} halflingLucky Allow Halfling Luck to modify this roll? * @param {boolean} halflingLucky Allow Halfling Luck to modify this roll?
* * @param {boolean} reliableTalent Allow Reliable Talent to modify this roll?
* @return {Promise} A Promise which resolves once the roll workflow has completed * @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
*
* @return {Promise} A Promise which resolves once the roll workflow has completed
*/ */
static async d20Roll({parts=[], data={}, event={}, rollMode=null, template=null, title=null, speaker=null, export async function d20Roll({parts=[], data={}, event={}, rollMode=null, template=null, title=null, speaker=null,
flavor=null, fastForward=null, onClose, dialogOptions, flavor=null, fastForward=null, dialogOptions,
advantage=null, disadvantage=null, critical=20, fumble=1, targetValue=null, advantage=null, disadvantage=null, critical=20, fumble=1, targetValue=null,
elvenAccuracy=false, halflingLucky=false}={}) { elvenAccuracy=false, halflingLucky=false, reliableTalent=false,
chatMessage=true, messageData={}}={}) {
// Handle input arguments
flavor = flavor || title; // Prepare Message Data
speaker = speaker || ChatMessage.getSpeaker(); messageData.flavor = flavor || title;
messageData.speaker = speaker || ChatMessage.getSpeaker();
const messageOptions = {rollMode: rollMode || game.settings.get("core", "rollMode")};
parts = parts.concat(["@bonus"]); parts = parts.concat(["@bonus"]);
rollMode = rollMode || game.settings.get("core", "rollMode");
let rolled = false; // Handle fast-forward events
let adv = 0;
// Define inner roll function fastForward = fastForward ?? (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
const _roll = function(parts, adv, form=null) { if (fastForward) {
if ( advantage || event.altKey ) adv = 1;
// Determine the d20 roll and modifiers else if ( disadvantage || event.ctrlKey || event.metaKey ) adv = -1;
}
// Define the inner roll function
const _roll = (parts, adv, form) => {
// Determine the d20 roll and modifiers
let nd = 1; let nd = 1;
let mods = halflingLucky ? "r=1" : ""; let mods = halflingLucky ? "r=1" : "";
// Handle advantage // Handle advantage
if ( adv === 1 ) { if (adv === 1) {
nd = elvenAccuracy ? 3 : 2; nd = elvenAccuracy ? 3 : 2;
flavor += ` (${game.i18n.localize("SW5E.Advantage")})`; messageData.flavor += ` (${game.i18n.localize("SW5E.Advantage")})`;
mods += "kh"; if ( "flags.sw5e.roll" in messageData ) messageData["flags.sw5e.roll"].advantage = true;
mods += "kh";
} }
// Handle disadvantage // Handle disadvantage
else if ( adv === -1 ) { else if (adv === -1) {
nd = 2; nd = 2;
flavor += ` (${game.i18n.localize("SW5E.Disadvantage")})`; messageData.flavor += ` (${game.i18n.localize("SW5E.Disadvantage")})`;
mods += "kl"; if ( "flags.sw5e.roll" in messageData ) messageData["flags.sw5e.roll"].disadvantage = true;
mods += "kl";
} }
// Include the d20 roll // Prepend the d20 roll
parts.unshift(`${nd}d20${mods}`); let formula = `${nd}d20${mods}`;
if (reliableTalent) formula = `{${nd}d20${mods},10}kh`;
parts.unshift(formula);
// Optionally include a situational bonus // Optionally include a situational bonus
if ( form !== null ) data['bonus'] = form.bonus.value; if ( form ) {
if ( !data["bonus"] ) parts.pop(); 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) // Optionally include an ability score selection (used for tool checks)
const ability = form ? form.ability : null; const ability = form ? form.ability : null;
if ( ability && ability.value ) { if (ability && ability.value) {
data.ability = ability.value; data.ability = ability.value;
const abl = data.abilities[data.ability]; const abl = data.abilities[data.ability];
if ( abl ) { if (abl) {
data.mod = abl.mod; data.mod = abl.mod;
flavor += ` (${CONFIG.SW5E.abilities[data.ability]})`; messageData.flavor += ` (${CONFIG.SW5E.abilities[data.ability]})`;
} }
} }
// Execute the roll and flag critical thresholds on the d20 // Execute the roll
let roll = new Roll(parts.join(" + "), data).roll(); let roll = new Roll(parts.join(" + "), data);
const d20 = roll.parts[0]; try {
d20.options.critical = critical; roll.roll();
d20.options.fumble = fumble; } catch (err) {
if ( targetValue ) d20.options.target = targetValue; console.error(err);
ui.notifications.error(`Dice roll evaluation failed: ${err.message}`);
// Convert the roll to a chat message and return the roll return null;
rollMode = form ? form.rollMode.value : rollMode;
roll.toMessage({
speaker: speaker,
flavor: flavor
}, { rollMode });
rolled = true;
return roll;
};
// Determine whether the roll can be fast-forward
if ( fastForward === null ) {
fastForward = event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey);
} }
// Optionally allow fast-forwarding to specify advantage or disadvantage // Flag d20 options for any 20-sided dice in the roll
if ( fastForward ) { for (let d of roll.dice) {
if ( advantage || event.altKey ) return _roll(parts, 1); if (d.faces === 20) {
else if ( disadvantage || event.ctrlKey || event.metaKey ) return _roll(parts, -1); d.options.critical = critical;
else return _roll(parts, 0); d.options.fumble = fumble;
if (targetValue) d.options.target = targetValue;
}
} }
// If reliable talent was applied, add it to the flavor text
if (reliableTalent && roll.dice[0].total < 10) {
messageData.flavor += ` (${game.i18n.localize("SW5E.FlagsReliableTalent")})`;
}
return roll;
};
// Create the Roll instance
const roll = fastForward ? _roll(parts, adv) :
await _d20RollDialog({template, title, parts, data, rollMode: messageOptions.rollMode, dialogOptions, roll: _roll});
// Create a Chat Message
if ( roll && chatMessage ) roll.toMessage(messageData, messageOptions);
return roll;
}
/* -------------------------------------------- */
/**
* Present a Dialog form which creates a d20 roll once submitted
* @return {Promise<Roll>}
* @private
*/
async function _d20RollDialog({template, title, parts, data, rollMode, dialogOptions, roll}={}) {
// Render modal dialog // Render modal dialog
template = template || "systems/sw5e/templates/chat/roll-dialog.html"; template = template || "systems/sw5e/templates/chat/roll-dialog.html";
let dialogData = { let dialogData = {
@ -119,7 +151,6 @@ export class Dice5e {
const html = await renderTemplate(template, dialogData); const html = await renderTemplate(template, dialogData);
// Create the Dialog window // Create the Dialog window
let roll;
return new Promise(resolve => { return new Promise(resolve => {
new Dialog({ new Dialog({
title: title, title: title,
@ -127,26 +158,24 @@ export class Dice5e {
buttons: { buttons: {
advantage: { advantage: {
label: game.i18n.localize("SW5E.Advantage"), label: game.i18n.localize("SW5E.Advantage"),
callback: html => roll = _roll(parts, 1, html[0].children[0]) callback: html => resolve(roll(parts, 1, html[0].querySelector("form")))
}, },
normal: { normal: {
label: game.i18n.localize("SW5E.Normal"), label: game.i18n.localize("SW5E.Normal"),
callback: html => roll = _roll(parts, 0, html[0].children[0]) callback: html => resolve(roll(parts, 0, html[0].querySelector("form")))
}, },
disadvantage: { disadvantage: {
label: game.i18n.localize("SW5E.Disadvantage"), label: game.i18n.localize("SW5E.Disadvantage"),
callback: html => roll = _roll(parts, -1, html[0].children[0]) callback: html => resolve(roll(parts, -1, html[0].querySelector("form")))
} }
}, },
default: "normal", default: "normal",
close: html => { close: () => resolve(null)
if (onClose) onClose(html, parts, data);
resolve(rolled ? roll : false)
}
}, dialogOptions).render(true); }, dialogOptions).render(true);
}) });
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
@ -169,83 +198,103 @@ export class Dice5e {
* @param {Boolean} fastForward Allow fast-forward advantage selection * @param {Boolean} fastForward Allow fast-forward advantage selection
* @param {Function} onClose Callback for actions to take when the dialog form is closed * @param {Function} onClose Callback for actions to take when the dialog form is closed
* @param {Object} dialogOptions Modal dialog options * @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
* *
* @return {Promise} A Promise which resolves once the roll workflow has completed * @return {Promise} A Promise which resolves once the roll workflow has completed
*/ */
static async damageRoll({parts, actor, data, event={}, rollMode=null, template, title, speaker, flavor, export async function damageRoll({parts, actor, data, event={}, rollMode=null, template, title, speaker, flavor,
allowCritical=true, critical=false, fastForward=null, onClose, dialogOptions}) { allowCritical=true, critical=false, fastForward=null, dialogOptions, chatMessage=true, messageData={}}={}) {
// Handle input arguments // Prepare Message Data
flavor = flavor || title; messageData.flavor = flavor || title;
speaker = speaker || ChatMessage.getSpeaker(); messageData.speaker = speaker || ChatMessage.getSpeaker();
rollMode = game.settings.get("core", "rollMode"); const messageOptions = {rollMode: rollMode || game.settings.get("core", "rollMode")};
let rolled = false; parts = parts.concat(["@bonus"]);
fastForward = fastForward ?? (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
// Define inner roll function // Define inner roll function
const _roll = function(parts, crit, form) { const _roll = function(parts, crit, form) {
data['bonus'] = form ? form.bonus.value : 0;
// 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); let roll = new Roll(parts.join("+"), data);
// Modify the damage formula for critical hits // Modify the damage formula for critical hits
if ( crit === true ) { if ( crit === true ) {
let add = (actor && actor.getFlag("sw5e", "savageAttacks")) ? 1 : 0; let add = (actor && actor.getFlag("sw5e", "savageAttacks")) ? 1 : 0;
let mult = 2; let mult = 2;
roll.alter(add, mult); // TODO Backwards compatibility - REMOVE LATER
flavor = `${flavor} (${game.i18n.localize("SW5E.Critical")})`; if (isNewerVersion(game.data.version, "0.6.9")) roll.alter(mult, add);
} else roll.alter(add, mult);
messageData.flavor += ` (${game.i18n.localize("SW5E.Critical")})`;
// Convert the roll to a chat message if ( "flags.sw5e.roll" in messageData ) messageData["flags.sw5e.roll"].critical = true;
rollMode = form ? form.rollMode.value : rollMode;
roll.toMessage({
speaker: speaker,
flavor: flavor
}, { rollMode });
rolled = true;
return roll;
};
// Determine whether the roll can be fast-forward
if ( fastForward === null ) {
fastForward = event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey);
} }
// Modify the roll and handle fast-forwarding // Execute the roll
if ( fastForward ) return _roll(parts, critical || event.altKey); try {
else parts = parts.concat(["@bonus"]); return roll.roll();
} catch(err) {
console.error(err);
ui.notifications.error(`Dice roll evaluation failed: ${err.message}`);
return null;
}
};
// Render modal dialog // Create the Roll instance
template = template || "systems/sw5e/templates/chat/roll-dialog.html"; const roll = fastForward ? _roll(parts, critical || event.altKey) : await _damageRollDialog({
let dialogData = { template, title, parts, data, allowCritical, rollMode: messageOptions.rollMode, dialogOptions, roll: _roll
formula: parts.join(" + "), });
data: data,
rollMode: rollMode, // Create a Chat Message
rollModes: CONFIG.Dice.rollModes if ( roll && chatMessage ) roll.toMessage(messageData, messageOptions);
}; return roll;
const html = await renderTemplate(template, dialogData);
// Create the Dialog window }
let roll;
return new Promise(resolve => { /* -------------------------------------------- */
new Dialog({
title: title, /**
content: html, * Present a Dialog form which creates a damage roll once submitted
buttons: { * @return {Promise<Roll>}
critical: { * @private
condition: allowCritical, */
label: game.i18n.localize("SW5E.CriticalHit"), async function _damageRollDialog({template, title, parts, data, allowCritical, rollMode, dialogOptions, roll}={}) {
callback: html => roll = _roll(parts, true, html[0].children[0])
}, // Render modal dialog
normal: { template = template || "systems/sw5e/templates/chat/roll-dialog.html";
label: game.i18n.localize(allowCritical ? "SW5E.Normal" : "SW5E.Roll"), let dialogData = {
callback: html => roll = _roll(parts, false, html[0].children[0]) formula: parts.join(" + "),
}, data: data,
}, rollMode: rollMode,
default: "normal", rollModes: CONFIG.Dice.rollModes
close: html => { };
if (onClose) onClose(html, parts, data); const html = await renderTemplate(template, dialogData);
resolve(rolled ? roll : false);
} // Create the Dialog window
}, dialogOptions).render(true); return new Promise(resolve => {
}); new Dialog({
} title: title,
content: html,
buttons: {
critical: {
condition: allowCritical,
label: game.i18n.localize("SW5E.CriticalHit"),
callback: html => resolve(roll(parts, true, html[0].querySelector("form")))
},
normal: {
label: game.i18n.localize(allowCritical ? "SW5E.Normal" : "SW5E.Roll"),
callback: html => resolve(roll(parts, false, html[0].querySelector("form")))
},
},
default: "normal",
close: () => resolve(null)
}, dialogOptions).render(true);
});
} }

View file

@ -1,11 +1,11 @@
import { Dice5e } from "../dice.js"; import {d20Roll, damageRoll} from "../dice.js";
import { AbilityUseDialog } from "../apps/ability-use-dialog.js"; import AbilityUseDialog from "../apps/ability-use-dialog.js";
import { AbilityTemplate } from "../pixi/ability-template.js"; import AbilityTemplate from "../pixi/ability-template.js";
/** /**
* Override and extend the basic :class:`Item` implementation * Override and extend the basic :class:`Item` implementation
*/ */
export class Item5e extends Item { export default class Item5e extends Item {
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Item Properties */ /* Item Properties */
@ -20,14 +20,34 @@ export class Item5e extends Item {
if (!("ability" in itemData)) return null; if (!("ability" in itemData)) return null;
// Case 1 - defined directly by the item // Case 1 - defined directly by the item
if ( itemData.ability ) return itemData.ability; if (itemData.ability) return itemData.ability;
// Case 2 - inferred from a parent actor // Case 2 - inferred from a parent actor
else if ( this.actor ) { else if (this.actor) {
const actorData = this.actor.data.data; const actorData = this.actor.data.data;
if ( this.data.type === "power" ) return actorData.attributes.powercasting || "int";
else if ( this.data.type === "tool" ) return "int"; // Powers - Use Actor powercasting modifier
else return "str"; if (this.data.type === "power") return actorData.attributes.powercasting || "int";
// Tools - default to Intelligence
else if (this.data.type === "tool") return "int";
// Weapons
else if (this.data.type === "weapon") {
const wt = itemData.weaponType;
// Melee weapons - Str or Dex if Finesse (PHB pg. 147)
if ( ["simpleVW", "martialVW", "simpleLW", "martialLW"].includes(wt) ) {
if (itemData.properties.fin === true) { // Finesse weapons
return (actorData.abilities["dex"].mod >= actorData.abilities["str"].mod) ? "dex" : "str";
}
return "str";
}
// Ranged weapons - Dex (PH p.194)
else if ( ["simpleB", "martialB"].includes(wt) ) return "dex";
}
return "str";
} }
// Case 3 - unknown // Case 3 - unknown
@ -149,15 +169,16 @@ export class Item5e extends Item {
arr.push(c[0].titleCase().slice(0, 1)); arr.push(c[0].titleCase().slice(0, 1));
return arr; return arr;
}, []); }, []);
labels.materials = data?.materials?.value ?? null;
} }
// Feat Items // Feat Items
else if ( itemData.type === "feat" ) { else if ( itemData.type === "feat" ) {
const act = data.activation; const act = data.activation;
if ( act && (act.type === C.abilityActivationTypes.legendary) ) labels.featType = "Legendary Action"; if ( act && (act.type === C.abilityActivationTypes.legendary) ) labels.featType = game.i18n.localize("SW5E.LegendaryActionLabel");
else if ( act && (act.type === C.abilityActivationTypes.lair) ) labels.featType = "Lair Action"; else if ( act && (act.type === C.abilityActivationTypes.lair) ) labels.featType = game.i18n.localize("SW5E.LairActionLabel");
else if ( act && act.type ) labels.featType = data.damage.length ? "Attack" : "Action"; else if ( act && act.type ) labels.featType = game.i18n.localize(data.damage.length ? "SW5E.Attack" : "SW5E.Action");
else labels.featType = "Passive"; else labels.featType = game.i18n.localize("SW5E.Passive");
} }
// Species Items // Species Items
@ -167,7 +188,7 @@ export class Item5e extends Item {
// Equipment Items // Equipment Items
else if ( itemData.type === "equipment" ) { else if ( itemData.type === "equipment" ) {
labels.armor = data.armor.value ? `${data.armor.value} AC` : ""; labels.armor = data.armor.value ? `${data.armor.value} ${game.i18n.localize("SW5E.AC")}` : "";
} }
// Activated Items // Activated Items
@ -201,7 +222,7 @@ export class Item5e extends Item {
// Recharge Label // Recharge Label
let chg = data.recharge || {}; let chg = data.recharge || {};
labels.recharge = `Recharge [${chg.value}${parseInt(chg.value) < 6 ? "+" : ""}]`; labels.recharge = `${game.i18n.localize("SW5E.Recharge")} [${chg.value}${parseInt(chg.value) < 6 ? "+" : ""}]`;
} }
// Item Actions // Item Actions
@ -216,7 +237,7 @@ export class Item5e extends Item {
} else { // Un-owned items } else { // Un-owned items
if ( save.scaling !== "flat" ) save.dc = null; if ( save.scaling !== "flat" ) save.dc = null;
} }
labels.save = save.ability ? `DC ${save.dc || ""} ${C.abilities[save.ability]}` : ""; labels.save = save.ability ? `${game.i18n.localize("SW5E.AbbreviationDC")} ${save.dc || ""} ${C.abilities[save.ability]}` : "";
// Damage // Damage
let dam = data.damage || {}; let dam = data.damage || {};
@ -234,9 +255,13 @@ export class Item5e extends Item {
/** /**
* Roll the item to Chat, creating a chat card which contains follow up attack or damage roll options * Roll the item to Chat, creating a chat card which contains follow up attack or damage roll options
* @param {boolean} [configureDialog] Display a configuration dialog for the item roll, if applicable?
* @param {string} [rollMode] The roll display mode with which to display (or not) the card
* @param {boolean} [createMessage] Whether to automatically create a chat message (if true) or simply return
* the prepared chat message data (if false).
* @return {Promise} * @return {Promise}
*/ */
async roll({configureDialog=true}={}) { async roll({configureDialog=true, rollMode=null, createMessage=true}={}) {
// Basic template rendering data // Basic template rendering data
const token = this.actor.token; const token = this.actor.token;
@ -259,10 +284,17 @@ export class Item5e extends Item {
if (this.data.type === "feat") { if (this.data.type === "feat") {
let configured = await this._rollFeat(configureDialog); let configured = await this._rollFeat(configureDialog);
if ( configured === false ) return; if ( configured === false ) return;
} else if ( this.data.type === "consumable" ) {
let configured = await this._rollConsumable(configureDialog);
if ( configured === false ) return;
} }
// For items which consume a resource, handle that here
const allowed = await this._handleResourceConsumption({isCard: true, isAttack: false});
if ( allowed === false ) return;
// Render the chat card template // Render the chat card template
const templateType = ["tool", "consumable"].includes(this.data.type) ? this.data.type : "item"; const templateType = ["tool"].includes(this.data.type) ? this.data.type : "item";
const template = `systems/sw5e/templates/chat/${templateType}-card.html`; const template = `systems/sw5e/templates/chat/${templateType}-card.html`;
const html = await renderTemplate(template, templateData); const html = await renderTemplate(template, templateData);
@ -279,12 +311,90 @@ export class Item5e extends Item {
}; };
// Toggle default roll mode // Toggle default roll mode
let rollMode = game.settings.get("core", "rollMode"); rollMode = rollMode || game.settings.get("core", "rollMode");
if ( ["gmroll", "blindroll"].includes(rollMode) ) chatData["whisper"] = ChatMessage.getWhisperIDs("GM"); if ( ["gmroll", "blindroll"].includes(rollMode) ) chatData["whisper"] = ChatMessage.getWhisperRecipients("GM");
if ( rollMode === "blindroll" ) chatData["blind"] = true; if ( rollMode === "blindroll" ) chatData["blind"] = true;
// Create the chat message // Create the chat message
return ChatMessage.create(chatData); if ( createMessage ) return ChatMessage.create(chatData);
else return chatData;
}
/* -------------------------------------------- */
/**
* For items which consume a resource, handle the consumption of that resource when the item is used.
* There are four types of ability consumptions which are handled:
* 1. Ammunition (on attack rolls)
* 2. Attributes (on card usage)
* 3. Materials (on card usage)
* 4. Item Charges (on card usage)
*
* @param {boolean} isCard Is the item card being played?
* @param {boolean} isAttack Is an attack roll being made?
* @return {Promise<boolean>} Can the item card or attack roll be allowed to proceed?
* @private
*/
async _handleResourceConsumption({isCard=false, isAttack=false}={}) {
const itemData = this.data.data;
const consume = itemData.consume || {};
if ( !consume.type ) return true;
const actor = this.actor;
const typeLabel = CONFIG.SW5E.abilityConsumptionTypes[consume.type];
const amount = parseInt(consume.amount || 1);
// Only handle certain types for certain actions
if ( ((consume.type === "ammo") && !isAttack ) || ((consume.type !== "ammo") && !isCard) ) return true;
// No consumed target set
if ( !consume.target ) {
ui.notifications.warn(game.i18n.format("SW5E.ConsumeWarningNoResource", {name: this.name, type: typeLabel}));
return false;
}
// Identify the consumed resource and it's quantity
let consumed = null;
let quantity = 0;
switch ( consume.type ) {
case "attribute":
consumed = getProperty(actor.data.data, consume.target);
quantity = consumed || 0;
break;
case "ammo":
case "material":
consumed = actor.items.get(consume.target);
quantity = consumed ? consumed.data.data.quantity : 0;
break;
case "charges":
consumed = actor.items.get(consume.target);
quantity = consumed ? consumed.data.data.uses.value : 0;
break;
}
// Verify that the consumed resource is available
if ( [null, undefined].includes(consumed) ) {
ui.notifications.warn(game.i18n.format("SW5E.ConsumeWarningNoSource", {name: this.name, type: typeLabel}));
return false;
}
let remaining = quantity - amount;
if ( remaining < 0) {
ui.notifications.warn(game.i18n.format("SW5E.ConsumeWarningNoQuantity", {name: this.name, type: typeLabel}));
return false;
}
// Update the consumed resource
switch ( consume.type ) {
case "attribute":
await this.actor.update({[`data.${consume.target}`]: remaining});
break;
case "ammo":
case "material":
await consumed.update({"data.quantity": remaining});
break;
case "charges":
await consumed.update({"data.uses.value": remaining});
}
return true;
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -298,27 +408,35 @@ export class Item5e extends Item {
if ( this.data.type !== "feat" ) throw new Error("Wrong Item type"); if ( this.data.type !== "feat" ) throw new Error("Wrong Item type");
// Configure whether to consume a limited use or to place a template // Configure whether to consume a limited use or to place a template
const usesRecharge = !!this.data.data.recharge.value; const charge = this.data.data.recharge;
const uses = this.data.data.uses; const uses = this.data.data.uses;
let usesCharges = !!uses.per && (uses.max > 0); let usesCharges = !!uses.per && (uses.max > 0);
let placeTemplate = false; let placeTemplate = false;
let consume = usesRecharge || usesCharges; let consume = charge.value || usesCharges;
// Determine whether the feat uses charges // Determine whether the feat uses charges
configureDialog = configureDialog && (consume || this.hasAreaTarget); configureDialog = configureDialog && (consume || this.hasAreaTarget);
if ( configureDialog ) { if ( configureDialog ) {
const usage = await AbilityUseDialog.create(this); const usage = await AbilityUseDialog.create(this);
if ( usage === null ) return false; if ( usage === null ) return false;
consume = Boolean(usage.get("consume")); consume = Boolean(usage.get("consumeUse"));
placeTemplate = Boolean(usage.get("placeTemplate")); placeTemplate = Boolean(usage.get("placeTemplate"));
} }
// Update Item data // Update Item data
const current = getProperty(this.data, "data.uses.value") || 0; const current = getProperty(this.data, "data.uses.value") || 0;
if ( consume && usesRecharge ) { if ( consume && charge.value ) {
await this.update({"data.recharge.charged": false}); if ( !charge.charged ) {
ui.notifications.warn(game.i18n.format("SW5E.ItemNoUses", {name: this.name}));
return false;
}
else await this.update({"data.recharge.charged": false});
} }
else if ( consume && usesCharges ) { else if ( consume && usesCharges ) {
if ( uses.value <= 0 ) {
ui.notifications.warn(game.i18n.format("SW5E.ItemNoUses", {name: this.name}));
return false;
}
await this.update({"data.uses.value": Math.max(current - 1, 0)}); await this.update({"data.uses.value": Math.max(current - 1, 0)});
} }
@ -340,14 +458,13 @@ export class Item5e extends Item {
* @param {Object} htmlOptions Options used by the TextEditor.enrichHTML function * @param {Object} htmlOptions Options used by the TextEditor.enrichHTML function
* @return {Object} An object of chat data to render * @return {Object} An object of chat data to render
*/ */
getChatData(htmlOptions) { getChatData(htmlOptions={}) {
const data = duplicate(this.data.data); const data = duplicate(this.data.data);
const labels = this.labels; const labels = this.labels;
// Rich text description // Rich text description
data.description.value = TextEditor.enrichHTML(data.description.value, htmlOptions); data.description.value = TextEditor.enrichHTML(data.description.value, htmlOptions);
// Item type specific properties // Item type specific properties
const props = []; const props = [];
const fn = this[`_${this.data.type}ChatData`]; const fn = this[`_${this.data.type}ChatData`];
@ -356,16 +473,16 @@ export class Item5e extends Item {
// General equipment properties // General equipment properties
if ( data.hasOwnProperty("equipped") && !["loot", "tool"].includes(this.data.type) ) { if ( data.hasOwnProperty("equipped") && !["loot", "tool"].includes(this.data.type) ) {
props.push( props.push(
data.equipped ? "Equipped" : "Not Equipped", game.i18n.localize(data.equipped ? "SW5E.Equipped" : "SW5E.Unequipped"),
data.proficient ? "Proficient": "Not Proficient", game.i18n.localize(data.proficient ? "SW5E.Proficient" : "SW5E.NotProficient"),
); );
} }
// Ability activation properties // Ability activation properties
if ( data.hasOwnProperty("activation") ) { if ( data.hasOwnProperty("activation") ) {
props.push( props.push(
labels.activation + (data.activation?.condition ? ` (${data.activation.condition})` : ""),
labels.target, labels.target,
labels.activation,
labels.range, labels.range,
labels.duration labels.duration
); );
@ -386,7 +503,7 @@ export class Item5e extends Item {
props.push( props.push(
CONFIG.SW5E.equipmentTypes[data.armor.type], CONFIG.SW5E.equipmentTypes[data.armor.type],
labels.armor || null, labels.armor || null,
data.stealth.value ? "Stealth Disadvantage" : null, data.stealth.value ? game.i18n.localize("SW5E.StealthDisadvantage") : null
); );
} }
@ -411,7 +528,7 @@ export class Item5e extends Item {
_consumableChatData(data, labels, props) { _consumableChatData(data, labels, props) {
props.push( props.push(
CONFIG.SW5E.consumableTypes[data.consumableType], CONFIG.SW5E.consumableTypes[data.consumableType],
data.uses.value + "/" + data.uses.max + " Charges" data.uses.value + "/" + data.uses.max + " " + game.i18n.localize("SW5E.Charges")
); );
data.hasCharges = data.uses.value >= 0; data.hasCharges = data.uses.value >= 0;
} }
@ -437,8 +554,8 @@ export class Item5e extends Item {
*/ */
_lootChatData(data, labels, props) { _lootChatData(data, labels, props) {
props.push( props.push(
"Loot", game.i18n.localize("SW5E.ItemTypeLoot"),
data.weight ? data.weight + " lbs." : null data.weight ? data.weight + " " + game.i18n.localize("SW5E.AbbreviationLbs") : null
); );
} }
@ -452,7 +569,7 @@ export class Item5e extends Item {
_powerChatData(data, labels, props) { _powerChatData(data, labels, props) {
props.push( props.push(
labels.level, labels.level,
labels.components, labels.components + (labels.materials ? ` (${labels.materials})` : "")
); );
} }
@ -472,17 +589,19 @@ export class Item5e extends Item {
/** /**
* Place an attack roll using an item (weapon, feat, power, or equipment) * Place an attack roll using an item (weapon, feat, power, or equipment)
* Rely upon the Dice5e.d20Roll logic for the core implementation * Rely upon the d20Roll logic for the core implementation
* *
* @return {Promise.<Roll>} A Promise which resolves to the created Roll instance * @param {object} options Roll options which are configured and provided to the d20Roll function
* @return {Promise<Roll|null>} A Promise which resolves to the created Roll instance
*/ */
rollAttack(options={}) { async rollAttack(options={}) {
const itemData = this.data.data; const itemData = this.data.data;
const actorData = this.actor.data.data; const actorData = this.actor.data.data;
const flags = this.actor.data.flags.sw5e || {}; const flags = this.actor.data.flags.sw5e || {};
if ( !this.hasAttack ) { if ( !this.hasAttack ) {
throw new Error("You may not place an Attack Roll with this Item."); throw new Error("You may not place an Attack Roll with this Item.");
} }
let title = `${this.name} - ${game.i18n.localize("SW5E.AttackRoll")}`;
const rollData = this.getRollData(); const rollData = this.getRollData();
// Define Roll bonuses // Define Roll bonuses
@ -492,26 +611,44 @@ export class Item5e extends Item {
} }
// Attack Bonus // Attack Bonus
const actorBonus = actorData.bonuses[itemData.actionType] || {}; const actorBonus = actorData?.bonuses?.[itemData.actionType] || {};
if ( itemData.attackBonus || actorBonus.attack ) { if ( itemData.attackBonus || actorBonus.attack ) {
parts.push("@atk"); parts.push("@atk");
rollData["atk"] = [itemData.attackBonus, actorBonus.attack].filterJoin(" + "); rollData["atk"] = [itemData.attackBonus, actorBonus.attack].filterJoin(" + ");
} }
// Ammunition Bonus
delete this._ammo;
const consume = itemData.consume;
if ( consume?.type === "ammo" ) {
const ammo = this.actor.items.get(consume.target);
const q = ammo.data.data.quantity;
if ( q && (q - consume.amount >= 0) ) {
let ammoBonus = ammo.data.data.attackBonus;
if ( ammoBonus ) {
parts.push("@ammo");
rollData["ammo"] = ammoBonus;
title += ` [${ammo.name}]`;
this._ammo = ammo;
}
}
}
// Compose roll options // Compose roll options
const rollConfig = { const rollConfig = mergeObject({
event: options.event,
parts: parts, parts: parts,
actor: this.actor, actor: this.actor,
data: rollData, data: rollData,
title: `${this.name} - Attack Roll`, title: title,
speaker: ChatMessage.getSpeaker({actor: this.actor}), speaker: ChatMessage.getSpeaker({actor: this.actor}),
dialogOptions: { dialogOptions: {
width: 400, width: 400,
top: options.event ? options.event.clientY - 80 : null, top: options.event ? options.event.clientY - 80 : null,
left: window.innerWidth - 710 left: window.innerWidth - 710
} },
}; messageData: {"flags.sw5e.roll": {type: "attack", itemId: this.id }}
}, options);
rollConfig.event = options.event;
// Expanded weapon critical threshold // Expanded weapon critical threshold
if (( this.data.type === "weapon" ) && flags.weaponCriticalThreshold) { if (( this.data.type === "weapon" ) && flags.weaponCriticalThreshold) {
@ -529,16 +666,22 @@ export class Item5e extends Item {
if ( flags.halflingLucky ) rollConfig.halflingLucky = true; if ( flags.halflingLucky ) rollConfig.halflingLucky = true;
// Invoke the d20 roll helper // Invoke the d20 roll helper
return Dice5e.d20Roll(rollConfig); const roll = await d20Roll(rollConfig);
if ( roll === false ) return null;
// Handle resource consumption if the attack roll was made
const allowed = await this._handleResourceConsumption({isCard: false, isAttack: true});
if ( allowed === false ) return null;
return roll;
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
* Place a damage roll using an item (weapon, feat, power, or equipment) * Place a damage roll using an item (weapon, feat, power, or equipment)
* Rely upon the Dice5e.damageRoll logic for the core implementation * Rely upon the damageRoll logic for the core implementation
* *
* @return {Promise.<Roll>} A Promise which resolves to the created Roll instance * @return {Promise<Roll>} A Promise which resolves to the created Roll instance
*/ */
rollDamage({event, powerLevel=null, versatile=false}={}) { rollDamage({event, powerLevel=null, versatile=false}={}) {
const itemData = this.data.data; const itemData = this.data.data;
@ -546,32 +689,54 @@ export class Item5e extends Item {
if ( !this.hasDamage ) { if ( !this.hasDamage ) {
throw new Error("You may not make a Damage Roll with this Item."); throw new Error("You may not make a Damage Roll with this Item.");
} }
const messageData = {"flags.sw5e.roll": {type: "damage", itemId: this.id }};
// Get roll data
const rollData = this.getRollData(); const rollData = this.getRollData();
if ( powerLevel ) rollData.item.level = powerLevel; if ( powerLevel ) rollData.item.level = powerLevel;
// Get message labels
const title = `${this.name} - ${game.i18n.localize("SW5E.DamageRoll")}`;
let flavor = this.labels.damageTypes.length ? `${title} (${this.labels.damageTypes})` : title;
// Define Roll parts // Define Roll parts
const parts = itemData.damage.parts.map(d => d[0]); const parts = itemData.damage.parts.map(d => d[0]);
if ( versatile && itemData.damage.versatile ) parts[0] = itemData.damage.versatile;
// Adjust damage from versatile usage
if ( versatile && itemData.damage.versatile ) {
parts[0] = itemData.damage.versatile;
messageData["flags.sw5e.roll"].versatile = true;
}
// Scale damage from up-casting powers
if ( (this.data.type === "power") ) { if ( (this.data.type === "power") ) {
if ( (itemData.scaling.mode === "atwill") ) { if ( (itemData.scaling.mode === "cantrip") ) {
const lvl = this.actor.data.type === "character" ? actorData.details.level : actorData.details.powerLevel; const level = this.actor.data.type === "character" ? actorData.details.level : actorData.details.powerLevel;
this._scaleAtWillDamage(parts, lvl, itemData.scaling.formula ); this._scaleCantripDamage(parts, itemData.scaling.formula, level, rollData);
} else if ( powerLevel && (itemData.scaling.mode === "level") && itemData.scaling.formula ) { }
this._scalePowerDamage(parts, itemData.level, powerLevel, itemData.scaling.formula ); else if ( powerLevel && (itemData.scaling.mode === "level") && itemData.scaling.formula ) {
const scaling = itemData.scaling.formula;
this._scalePowerDamage(parts, itemData.level, powerLevel, scaling, rollData);
} }
} }
// Define Roll Data // Define Roll Data
const actorBonus = actorData.bonuses[itemData.actionType] || {}; const actorBonus = getProperty(actorData, `bonuses.${itemData.actionType}`) || {};
if ( actorBonus.damage && parseInt(actorBonus.damage) !== 0 ) { if ( actorBonus.damage && parseInt(actorBonus.damage) !== 0 ) {
parts.push("@dmg"); parts.push("@dmg");
rollData["dmg"] = actorBonus.damage; rollData["dmg"] = actorBonus.damage;
} }
// Ammunition Damage
if ( this._ammo ) {
parts.push("@ammo");
rollData["ammo"] = this._ammo.data.data.damage.parts.map(p => p[0]).join("+");
flavor += ` [${this._ammo.name}]`;
delete this._ammo;
}
// Call the roll helper utility // Call the roll helper utility
const title = `${this.name} - Damage Roll`; return damageRoll({
const flavor = this.labels.damageTypes.length ? `${title} (${this.labels.damageTypes})` : title;
return Dice5e.damageRoll({
event: event, event: event,
parts: parts, parts: parts,
actor: this.actor, actor: this.actor,
@ -583,7 +748,8 @@ export class Item5e extends Item {
width: 400, width: 400,
top: event ? event.clientY - 80 : null, top: event ? event.clientY - 80 : null,
left: window.innerWidth - 710 left: window.innerWidth - 710
} },
messageData
}); });
} }
@ -593,13 +759,24 @@ export class Item5e extends Item {
* Adjust an at-will damage formula to scale it for higher level characters and monsters * Adjust an at-will damage formula to scale it for higher level characters and monsters
* @private * @private
*/ */
_scaleAtWillDamage(parts, level, scale) { _scaleAtWillDamage(parts, scale, level, rollData) {
const add = Math.floor((level + 1) / 6); const add = Math.floor((level + 1) / 6);
if ( add === 0 ) return; if ( add === 0 ) return;
if ( scale && (scale !== parts[0]) ) {
parts[0] = parts[0] + " + " + scale.replace(new RegExp(Roll.diceRgx, "g"), (match, nd, d) => `${add}d${d}`); // FUTURE SOLUTION - 0.7.0 AND LATER
} else { if (isNewerVersion(game.data.version, "0.6.9")) {
parts[0] = parts[0].replace(new RegExp(Roll.diceRgx, "g"), (match, nd, d) => `${parseInt(nd)+add}d${d}`); this._scaleDamage(parts, scale || parts.join(" + "), add, rollData)
}
// LEGACY SOLUTION - 0.6.x AND OLDER
// TODO: Deprecate the legacy solution one FVTT 0.7.x is RELEASE
else {
if ( scale && (scale !== parts[0]) ) {
parts[0] = parts[0] + " + " + scale.replace(new RegExp(Roll.diceRgx, "g"), (match, nd, d) => `${add}d${d}`);
} else {
parts[0] = parts[0].replace(new RegExp(Roll.diceRgx, "g"), (match, nd, d) => `${parseInt(nd)+add}d${d}`);
}
} }
} }
@ -611,13 +788,61 @@ export class Item5e extends Item {
* @param {number} baseLevel The default power level * @param {number} baseLevel The default power level
* @param {number} powerLevel The casted power level * @param {number} powerLevel The casted power level
* @param {string} formula The scaling formula * @param {string} formula The scaling formula
* @param {object} rollData A data object that should be applied to the scaled damage roll
* @return {string[]} The scaled roll parts
* @private * @private
*/ */
_scalePowerDamage(parts, baseLevel, powerLevel, formula) { _scalePowerDamage(parts, baseLevel, powerLevel, formula, rollData) {
const upcastLevels = Math.max(powerLevel - baseLevel, 0); const upcastLevels = Math.max(powerLevel - baseLevel, 0);
if ( upcastLevels === 0 ) return parts; if ( upcastLevels === 0 ) return parts;
const bonus = new Roll(formula).alter(0, upcastLevels);
parts.push(bonus.formula); // FUTURE SOLUTION - 0.7.0 AND LATER
if (isNewerVersion(game.data.version, "0.6.9")) {
this._scaleDamage(parts, formula, upcastLevels, rollData);
}
// LEGACY SOLUTION - 0.6.x AND OLDER
// TODO: Deprecate the legacy solution one FVTT 0.7.x is RELEASE
else {
const bonus = new Roll(formula);
bonus.alter(0, upcastLevels);
parts.push(bonus.formula);
}
return parts;
}
/* -------------------------------------------- */
/**
* Scale an array of damage parts according to a provided scaling formula and scaling multiplier
* @param {string[]} parts Initial roll parts
* @param {string} scaling A scaling formula
* @param {number} times A number of times to apply the scaling formula
* @param {object} rollData A data object that should be applied to the scaled damage roll
* @return {string[]} The scaled roll parts
* @private
*/
_scaleDamage(parts, scaling, times, rollData) {
if ( times <= 0 ) return parts;
const p0 = new Roll(parts[0], rollData);
const s = new Roll(scaling, rollData).alter(times);
// Attempt to simplify by combining like dice terms
let simplified = false;
if ( (s.terms[0] instanceof Die) && (s.terms.length === 1) ) {
const d0 = p0.terms[0];
const s0 = s.terms[0];
if ( (d0 instanceof Die) && (d0.faces === s0.faces) && d0.modifiers.equals(s0.modifiers) ) {
d0.number += s0.number;
parts[0] = p0.formula;
simplified = true;
}
}
// Otherwise add to the first part
if ( !simplified ) {
parts[0] = `${parts[0]} + ${s.formula}`;
}
return parts; return parts;
} }
@ -625,8 +850,8 @@ export class Item5e extends Item {
/** /**
* Place an attack roll using an item (weapon, feat, power, or equipment) * Place an attack roll using an item (weapon, feat, power, or equipment)
* Rely upon the Dice5e.d20Roll logic for the core implementation * Rely upon the d20Roll logic for the core implementation
* *
* @return {Promise.<Roll>} A Promise which resolves to the created Roll instance * @return {Promise.<Roll>} A Promise which resolves to the created Roll instance
*/ */
async rollFormula(options={}) { async rollFormula(options={}) {
@ -636,14 +861,16 @@ export class Item5e extends Item {
// Define Roll Data // Define Roll Data
const rollData = this.getRollData(); const rollData = this.getRollData();
const title = `${this.name} - Other Formula`; if ( options.powerLevel ) rollData.item.level = options.powerLevel;
const title = `${this.name} - ${game.i18n.localize("SW5E.OtherFormula")}`;
// Invoke the roll and submit it to chat // Invoke the roll and submit it to chat
const roll = new Roll(rollData.item.formula, rollData).roll(); const roll = new Roll(rollData.item.formula, rollData).roll();
roll.toMessage({ roll.toMessage({
speaker: ChatMessage.getSpeaker({actor: this.actor}), speaker: ChatMessage.getSpeaker({actor: this.actor}),
flavor: this.data.data.chatFlavor || title, flavor: this.data.data.chatFlavor || title,
rollMode: game.settings.get("core", "rollMode") rollMode: game.settings.get("core", "rollMode"),
messageData: {"flags.sw5e.roll": {type: "other", itemId: this.id }}
}); });
return roll; return roll;
} }
@ -652,58 +879,77 @@ export class Item5e extends Item {
/** /**
* Use a consumable item, deducting from the quantity or charges of the item. * Use a consumable item, deducting from the quantity or charges of the item.
* * @param {boolean} configureDialog Whether to show a configuration dialog
* @return {Promise.<Roll>} A Promise which resolves to the created Roll instance or null * @return {boolean} Whether further execution should be prevented
* @private
*/ */
async rollConsumable(options={}) { async _rollConsumable(configureDialog) {
if ( this.data.type !== "consumable" ) throw new Error("Wrong Item type");
const itemData = this.data.data; const itemData = this.data.data;
// Dispatch a damage roll // Determine whether to deduct uses of the item
let roll = null; const uses = itemData.uses || {};
if ( itemData.damage.parts.length ) { const autoDestroy = uses.autoDestroy;
roll = await this.rollDamage(options); let usesCharges = !!uses.per && (uses.max > 0);
const recharge = itemData.recharge || {};
const usesRecharge = !!recharge.value;
// Display a configuration dialog to confirm the usage
let placeTemplate = false;
let consume = uses.autoUse || true;
if ( configureDialog ) {
const usage = await AbilityUseDialog.create(this);
if ( usage === null ) return false;
consume = Boolean(usage.get("consumeUse"));
placeTemplate = Boolean(usage.get("placeTemplate"));
} }
// Dispatch an other formula // Update Item data
if ( itemData.formula ) { if ( consume ) {
roll = await this.rollFormula(options); const current = uses.value || 0;
} const remaining = usesCharges ? Math.max(current - 1, 0) : current;
if ( usesRecharge ) await this.update({"data.recharge.charged": false});
// Deduct consumed charges from the item
if ( itemData.uses.autoUse ) {
let q = itemData.quantity;
let c = itemData.uses.value;
// Deduct an item quantity
if ( c <= 1 && q > 1 ) {
await this.update({
'data.quantity': Math.max(q - 1, 0),
'data.uses.value': itemData.uses.max
});
}
// Optionally destroy the item
else if ( c <= 1 && q <= 1 && itemData.uses.autoDestroy ) {
await this.actor.deleteOwnedItem(this.id);
}
// Deduct the remaining charges
else { else {
await this.update({'data.uses.value': Math.max(c - 1, 0)}); const q = itemData.quantity;
// Case 1, reduce charges
if ( remaining ) {
await this.update({"data.uses.value": remaining});
}
// Case 2, reduce quantity
else if ( q > 1 ) {
await this.update({"data.quantity": q - 1, "data.uses.value": uses.max || 0});
}
// Case 3, destroy the item
else if ( (q <= 1) && autoDestroy ) {
await this.actor.deleteOwnedItem(this.id);
}
// Case 4, reduce item to 0 quantity and 0 charges
else if ( (q === 1) ) {
await this.update({"data.quantity": q - 1, "data.uses.value": 0});
}
// Case 5, item unusable, display warning and do nothing
else {
ui.notifications.warn(game.i18n.format("SW5E.ItemNoUses", {name: this.name}));
}
} }
} }
return roll;
} // Maybe initiate template placement workflow
if ( this.hasAreaTarget && placeTemplate ) {
const template = AbilityTemplate.fromItem(this);
if ( template ) template.drawPreview(event);
if ( this.owner && this.owner.sheet ) this.owner.sheet.minimize();
}
return true;
}
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
* Perform an ability recharge test for an item which uses the d6 recharge mechanic * Perform an ability recharge test for an item which uses the d6 recharge mechanic
* @prarm {Object} options * @return {Promise<Roll>} A Promise which resolves to the created Roll instance
*
* @return {Promise.<Roll>} A Promise which resolves to the created Roll instance
*/ */
async rollRecharge(options={}) { async rollRecharge() {
const data = this.data.data; const data = this.data.data;
if ( !data.recharge.value ) return; if ( !data.recharge.value ) return;
@ -713,7 +959,7 @@ export class Item5e extends Item {
// Display a Chat Message // Display a Chat Message
const promises = [roll.toMessage({ const promises = [roll.toMessage({
flavor: `${this.name} recharge check - ${success ? "success!" : "failure!"}`, flavor: `${game.i18n.format("SW5E.ItemRechargeCheck", {name: this.name})} - ${game.i18n.localize(success ? "SW5E.ItemRechargeSuccess" : "SW5E.ItemRechargeFailure")}`,
speaker: ChatMessage.getSpeaker({actor: this.actor, token: this.actor.token}) speaker: ChatMessage.getSpeaker({actor: this.actor, token: this.actor.token})
})]; })];
@ -725,10 +971,9 @@ export class Item5e extends Item {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
* Roll a Tool Check * Roll a Tool Check. Rely upon the d20Roll logic for the core implementation
* Rely upon the Dice5e.d20Roll logic for the core implementation * @prarm {Object} options Roll configuration options provided to the d20Roll function
* * @return {Promise<Roll>} A Promise which resolves to the created Roll instance
* @return {Promise.<Roll>} A Promise which resolves to the created Roll instance
*/ */
rollToolCheck(options={}) { rollToolCheck(options={}) {
if ( this.type !== "tool" ) throw "Wrong item type!"; if ( this.type !== "tool" ) throw "Wrong item type!";
@ -736,24 +981,28 @@ export class Item5e extends Item {
// Prepare roll data // Prepare roll data
let rollData = this.getRollData(); let rollData = this.getRollData();
const parts = [`@mod`, "@prof"]; const parts = [`@mod`, "@prof"];
const title = `${this.name} - Tool Check`; const title = `${this.name} - ${game.i18n.localize("SW5E.ToolCheck")}`;
// Call the roll helper utility // Compose the roll data
return Dice5e.d20Roll({ const rollConfig = mergeObject({
event: options.event,
parts: parts, parts: parts,
data: rollData, data: rollData,
template: "systems/sw5e/templates/chat/tool-roll-dialog.html", template: "systems/sw5e/templates/chat/tool-roll-dialog.html",
title: title, title: title,
speaker: ChatMessage.getSpeaker({actor: this.actor}), speaker: ChatMessage.getSpeaker({actor: this.actor}),
flavor: `${this.name} - Tool Check`, flavor: `${this.name} - ${game.i18n.localize("SW5E.ToolCheck")}`,
dialogOptions: { dialogOptions: {
width: 400, width: 400,
top: options.event ? options.event.clientY - 80 : null, top: options.event ? options.event.clientY - 80 : null,
left: window.innerWidth - 710, left: window.innerWidth - 710,
}, },
halflingLucky: this.actor.getFlag("sw5e", "halflingLucky" ) || false halflingLucky: this.actor.getFlag("sw5e", "halflingLucky" ) || false,
}); messageData: {"flags.sw5e.roll": {type: "tool", itemId: this.id }}
}, options);
rollConfig.event = options.event;
// Call the roll helper utility
return d20Roll(rollConfig);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -819,7 +1068,7 @@ export class Item5e extends Item {
// Get the Item // Get the Item
const item = actor.getOwnedItem(card.dataset.itemId); const item = actor.getOwnedItem(card.dataset.itemId);
if ( !item ) { if ( !item ) {
return ui.notifications.error(`The requested item ${card.dataset.itemId} no longer exists on Actor ${actor.name}`) return ui.notifications.error(game.i18n.format("SW5E.ActionWarningNoItem", {item: card.dataset.itemId, name: actor.name}))
} }
const powerLevel = parseInt(card.dataset.powerLevel) || null; const powerLevel = parseInt(card.dataset.powerLevel) || null;
@ -828,7 +1077,7 @@ export class Item5e extends Item {
if ( isTargetted ) { if ( isTargetted ) {
targets = this._getChatCardTargets(card); targets = this._getChatCardTargets(card);
if ( !targets.length ) { if ( !targets.length ) {
ui.notifications.warn(`You must have one or more controlled Tokens in order to use this option.`); ui.notifications.warn(game.i18n.localize("SW5E.ActionWarningNoToken"));
return button.disabled = false; return button.disabled = false;
} }
} }
@ -837,18 +1086,16 @@ export class Item5e extends Item {
if ( action === "attack" ) await item.rollAttack({event}); if ( action === "attack" ) await item.rollAttack({event});
else if ( action === "damage" ) await item.rollDamage({event, powerLevel}); else if ( action === "damage" ) await item.rollDamage({event, powerLevel});
else if ( action === "versatile" ) await item.rollDamage({event, powerLevel, versatile: true}); else if ( action === "versatile" ) await item.rollDamage({event, powerLevel, versatile: true});
else if ( action === "formula" ) await item.rollFormula({event}); else if ( action === "formula" ) await item.rollFormula({event, powerLevel});
// Saving Throws for card targets // Saving Throws for card targets
else if ( action === "save" ) { else if ( action === "save" ) {
for ( let t of targets ) { for ( let a of targets ) {
await t.rollAbilitySave(button.dataset.ability, {event}); const speaker = ChatMessage.getSpeaker({scene: canvas.scene, token: a.token});
await a.rollAbilitySave(button.dataset.ability, { event, speaker });
} }
} }
// Consumable usage
else if ( action === "consume" ) await item.rollConsumable({event});
// Tool usage // Tool usage
else if ( action === "toolCheck" ) await item.rollToolCheck({event}); else if ( action === "toolCheck" ) await item.rollToolCheck({event});
@ -919,4 +1166,56 @@ export class Item5e extends Item {
if ( character && (controlled.length === 0) ) targets.push(character); if ( character && (controlled.length === 0) ) targets.push(character);
return targets; return targets;
} }
/* -------------------------------------------- */
/* Factory Methods */
/* -------------------------------------------- */
/**
* Create a consumable power scroll Item from a power Item.
* @param {Item5e} power The power to be made into a scroll
* @return {Item5e} The created scroll consumable item
* @private
*/
static async createScrollFromPower(power) {
// Get power data
const itemData = power instanceof Item5e ? power.data : power;
const {actionType, description, source, activation, duration, target, range, damage, save, level} = itemData.data;
// Get scroll data
const scrollUuid = CONFIG.SW5E.powerScrollIds[level];
const scrollItem = await fromUuid(scrollUuid);
const scrollData = scrollItem.data;
delete scrollData._id;
// Split the scroll description into an intro paragraph and the remaining details
const scrollDescription = scrollData.data.description.value;
const pdel = '</p>';
const scrollIntroEnd = scrollDescription.indexOf(pdel);
const scrollIntro = scrollDescription.slice(0, scrollIntroEnd + pdel.length);
const scrollDetails = scrollDescription.slice(scrollIntroEnd + pdel.length);
// Create a composite description from the scroll description and the power details
const desc = `${scrollIntro}<hr/><h3>${itemData.name} (Level ${level})</h3><hr/>${description.value}<hr/><h3>Scroll Details</h3><hr/>${scrollDetails}`;
// Create the power scroll data
const powerScrollData = mergeObject(scrollData, {
name: `${game.i18n.localize("SW5E.PowerScroll")}: ${itemData.name}`,
img: itemData.img,
data: {
"description.value": desc.trim(),
source,
actionType,
activation,
duration,
target,
range,
damage,
save,
level
}
});
return new this(powerScrollData);
}
} }

View file

@ -1,11 +1,20 @@
import { TraitSelector } from "../apps/trait-selector.js"; import TraitSelector from "../apps/trait-selector.js";
/** /**
* Override and extend the core ItemSheet implementation to handle SW5E specific item types * Override and extend the core ItemSheet implementation to handle specific item types
* @type {ItemSheet} * @extends {ItemSheet}
*/ */
export class ItemSheet5e extends ItemSheet { export default class ItemSheet5e extends ItemSheet {
constructor(...args) {
super(...args);
if ( this.object.data.type === "class" ) {
this.options.resizable = true;
this.options.width = 600;
this.options.height = 640;
}
}
/* -------------------------------------------- */
/** @override */ /** @override */
static get defaultOptions() { static get defaultOptions() {
@ -13,7 +22,7 @@ export class ItemSheet5e extends ItemSheet {
width: 560, width: 560,
height: 420, height: 420,
classes: ["sw5e", "sheet", "item"], classes: ["sw5e", "sheet", "item"],
resizable: false, resizable: true,
scrollY: [".tab.details"], scrollY: [".tab.details"],
tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}] tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}]
}); });
@ -43,14 +52,79 @@ export class ItemSheet5e extends ItemSheet {
data.itemProperties = this._getItemProperties(data.item); data.itemProperties = this._getItemProperties(data.item);
data.isPhysical = data.item.data.hasOwnProperty("quantity"); data.isPhysical = data.item.data.hasOwnProperty("quantity");
// Potential consumption targets
data.abilityConsumptionTargets = this._getItemConsumptionTargets(data.item);
// Action Details // Action Details
data.hasAttackRoll = this.item.hasAttack; data.hasAttackRoll = this.item.hasAttack;
data.isHealing = data.item.data.actionType === "heal"; data.isHealing = data.item.data.actionType === "heal";
data.isFlatDC = getProperty(data.item.data, "save.scaling") === "flat"; data.isFlatDC = getProperty(data.item.data, "save.scaling") === "flat";
data.isWeapon = data.item.type === "weapon";
// Vehicles
data.isCrewed = data.item.data.activation?.type === 'crew';
data.isMountable = this._isItemMountable(data.item);
return data; return data;
} }
/* -------------------------------------------- */
/**
* Get the valid item consumption targets which exist on the actor
* @param {Object} item Item data for the item being displayed
* @return {{string: string}} An object of potential consumption targets
* @private
*/
_getItemConsumptionTargets(item) {
const consume = item.data.consume || {};
if ( !consume.type ) return [];
const actor = this.item.actor;
if ( !actor ) return {};
// Ammunition
if ( consume.type === "ammo" ) {
return actor.itemTypes.consumable.reduce((ammo, i) => {
if ( i.data.data.consumableType === "ammo" ) {
ammo[i.id] = `${i.name} (${i.data.data.quantity})`;
}
return ammo;
}, {});
}
// 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;
return obj;
}, {});
}
// Materials
else if ( consume.type === "material" ) {
return actor.items.reduce((obj, i) => {
if ( ["consumable", "loot"].includes(i.data.type) && !i.data.data.activation ) {
obj[i.id] = `${i.name} (${i.data.data.quantity})`;
}
return obj;
}, {});
}
// Charges
else if ( consume.type === "charges" ) {
return actor.items.reduce((obj, i) => {
const uses = i.data.data.uses || {};
if ( uses.per && uses.max ) {
const label = uses.per === "charges" ?
` (${game.i18n.format("SW5E.AbilityUseChargesLabel", {value: uses.value})})` :
` (${game.i18n.format("SW5E.AbilityUseConsumableLabel", {max: uses.max, per: uses.per})})`;
obj[i.id] = i.name + label;
}
return obj;
}, {})
}
else return {};
}
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
@ -63,10 +137,10 @@ export class ItemSheet5e extends ItemSheet {
return CONFIG.SW5E.powerPreparationModes[item.data.preparation]; return CONFIG.SW5E.powerPreparationModes[item.data.preparation];
} }
else if ( ["weapon", "equipment"].includes(item.type) ) { else if ( ["weapon", "equipment"].includes(item.type) ) {
return item.data.equipped ? "Equipped" : "Unequipped"; return game.i18n.localize(item.data.equipped ? "SW5E.Equipped" : "SW5E.Unequipped");
} }
else if ( item.type === "tool" ) { else if ( item.type === "tool" ) {
return item.data.proficient ? "Proficient" : "Not Proficient"; return game.i18n.localize(item.data.proficient ? "SW5E.Proficient" : "SW5E.NotProficient");
} }
} }
@ -91,8 +165,8 @@ export class ItemSheet5e extends ItemSheet {
props.push( props.push(
labels.components, labels.components,
labels.materials, labels.materials,
item.data.components.concentration ? "Concentration" : null, item.data.components.concentration ? game.i18n.localize("SW5E.Concentration") : null,
item.data.components.ritual ? "Ritual" : null item.data.components.ritual ? game.i18n.localize("SW5E.Ritual") : null
) )
} }
@ -128,6 +202,22 @@ export class ItemSheet5e extends ItemSheet {
/* -------------------------------------------- */ /* -------------------------------------------- */
/**
* Is this item a separate large object like a siege engine or vehicle
* component that is usually mounted on fixtures rather than equipped, and
* has its own AC and HP.
* @param item
* @returns {boolean}
* @private
*/
_isItemMountable(item) {
const data = item.data;
return (item.type === 'weapon' && data.weaponType === 'siege')
|| (item.type === 'equipment' && data.armor.type === 'vehicle');
}
/* -------------------------------------------- */
/** @override */ /** @override */
setPosition(position={}) { setPosition(position={}) {
position.height = this._tabs[0].active === "details" ? "auto" : this.options.height; position.height = this._tabs[0].active === "details" ? "auto" : this.options.height;
@ -141,33 +231,12 @@ export class ItemSheet5e extends ItemSheet {
/** @override */ /** @override */
_updateObject(event, formData) { _updateObject(event, formData) {
// TODO: This can be removed once 0.7.x is release channel
if ( !formData.data ) formData = expandObject(formData);
// Handle Damage Array // Handle Damage Array
let damage = Object.entries(formData).filter(e => e[0].startsWith("data.damage.parts")); const damage = formData.data?.damage;
formData["data.damage.parts"] = damage.reduce((arr, entry) => { if ( damage ) damage.parts = Object.values(damage?.parts || {}).map(d => [d[0] || "", d[1] || ""]);
let [i, j] = entry[0].split(".").slice(3);
if ( !arr[i] ) arr[i] = [];
arr[i][j] = entry[1];
return arr;
}, []);
// Handle armorproperties Array
let armorproperties = Object.entries(formData).filter(e => e[0].startsWith("data.armorproperties.parts"));
formData["data.armorproperties.parts"] = armorproperties.reduce((arr, entry) => {
let [i, j] = entry[0].split(".").slice(3);
if ( !arr[i] ) arr[i] = [];
arr[i][j] = entry[1];
return arr;
}, []);
// Handle weaponproperties Array
let weaponproperties = Object.entries(formData).filter(e => e[0].startsWith("data.weaponproperties.parts"));
formData["data.weaponproperties.parts"] = weaponproperties.reduce((arr, entry) => {
let [i, j] = entry[0].split(".").slice(3);
if ( !arr[i] ) arr[i] = [];
arr[i][j] = entry[1];
return arr;
}, []);
// Update the Item // Update the Item
super._updateObject(event, formData); super._updateObject(event, formData);
@ -179,16 +248,14 @@ export class ItemSheet5e extends ItemSheet {
activateListeners(html) { activateListeners(html) {
super.activateListeners(html); super.activateListeners(html);
html.find(".damage-control").click(this._onDamageControl.bind(this)); html.find(".damage-control").click(this._onDamageControl.bind(this));
// Activate any Trait Selectors
html.find('.trait-selector.class-skills').click(this._onConfigureClassSkills.bind(this)); html.find('.trait-selector.class-skills').click(this._onConfigureClassSkills.bind(this));
// Armor properties // Armor properties
html.find(".armorproperties-control").click(this._onarmorpropertiesControl.bind(this)); html.find(".armorproperties-control").click(this._onarmorpropertiesControl.bind(this));
// Weapon properties // Weapon properties
html.find(".weaponproperties-control").click(this._onweaponpropertiesControl.bind(this)); html.find(".weaponproperties-control").click(this._onweaponpropertiesControl.bind(this));
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -222,64 +289,6 @@ export class ItemSheet5e extends ItemSheet {
/* -------------------------------------------- */ /* -------------------------------------------- */
/**
* Add or remove a armorproperties part from the armorproperties formula
* @param {Event} event The original click event
* @return {Promise}
* @private
*/
async _onarmorpropertiesControl(event) {
event.preventDefault();
const a = event.currentTarget;
// Add new armorproperties component
if ( a.classList.contains("add-armorproperties") ) {
await this._onSubmit(event); // Submit any unsaved changes
const armorproperties = this.item.data.data.armorproperties;
return this.item.update({"data.armorproperties.parts": armorproperties.parts.concat([["", ""]])});
}
// Remove a armorproperties component
if ( a.classList.contains("delete-armorproperties") ) {
await this._onSubmit(event); // Submit any unsaved changes
const li = a.closest(".armorproperties-part");
const armorproperties = duplicate(this.item.data.data.armorproperties);
armorproperties.parts.splice(Number(li.dataset.armorpropertiesPart), 1);
return this.item.update({"data.armorproperties.parts": armorproperties.parts});
}
}
/* -------------------------------------------- */
/**
* Add or remove a weaponproperties part from the weaponproperties formula
* @param {Event} event The original click event
* @return {Promise}
* @private
*/
async _onweaponpropertiesControl(event) {
event.preventDefault();
const a = event.currentTarget;
// Add new weaponproperties component
if ( a.classList.contains("add-weaponproperties") ) {
await this._onSubmit(event); // Submit any unsaved changes
const weaponproperties = this.item.data.data.weaponproperties;
return this.item.update({"data.weaponproperties.parts": weaponproperties.parts.concat([["", ""]])});
}
// Remove a weaponproperties component
if ( a.classList.contains("delete-weaponproperties") ) {
await this._onSubmit(event); // Submit any unsaved changes
const li = a.closest(".weaponproperties-part");
const weaponproperties = duplicate(this.item.data.data.weaponproperties);
weaponproperties.parts.splice(Number(li.dataset.weaponpropertiesPart), 1);
return this.item.update({"data.weaponproperties.parts": weaponproperties.parts});
}
}
/* -------------------------------------------- */
/** /**
* Handle spawning the TraitSelector application which allows a checkbox of multiple trait options * Handle spawning the TraitSelector application which allows a checkbox of multiple trait options
* @param {Event} event The click event which originated the selection * @param {Event} event The click event which originated the selection

60
module/macros.js Normal file
View file

@ -0,0 +1,60 @@
/* -------------------------------------------- */
/* Hotbar Macros */
/* -------------------------------------------- */
/**
* Create a Macro from an Item drop.
* Get an existing item macro if one exists, otherwise create a new one.
* @param {Object} data The dropped data
* @param {number} slot The hotbar slot to use
* @returns {Promise}
*/
export async function create5eMacro(data, slot) {
if ( data.type !== "Item" ) return;
if (!( "data" in data ) ) return ui.notifications.warn("You can only create macro buttons for owned Items");
const item = data.data;
// Create the macro command
const command = `game.sw5e.rollItemMacro("${item.name}");`;
let macro = game.macros.entities.find(m => (m.name === item.name) && (m.command === command));
if ( !macro ) {
macro = await Macro.create({
name: item.name,
type: "script",
img: item.img,
command: command,
flags: {"sw5e.itemMacro": true}
});
}
game.user.assignHotbarMacro(macro, slot);
return false;
}
/* -------------------------------------------- */
/**
* Create a Macro from an Item drop.
* Get an existing item macro if one exists, otherwise create a new one.
* @param {string} itemName
* @return {Promise}
*/
export function rollItemMacro(itemName) {
const speaker = ChatMessage.getSpeaker();
let actor;
if ( speaker.token ) actor = game.actors.tokens[speaker.token];
if ( !actor ) actor = game.actors.get(speaker.actor);
// Get matching items
const items = actor ? actor.items.filter(i => i.name === itemName) : [];
if ( items.length > 1 ) {
ui.notifications.warn(`Your controlled Actor ${actor.name} has more than one Item with name ${itemName}. The first matched item will be chosen.`);
} else if ( items.length === 0 ) {
return ui.notifications.warn(`Your controlled Actor does not have an item named ${itemName}`);
}
const item = items[0];
// Trigger the item roll
if ( item.data.type === "power" ) return actor.usePower(item);
return item.roll();
}

View file

@ -4,7 +4,7 @@ import { SW5E } from "../config.js";
* A helper class for building MeasuredTemplates for 5e powers and abilities * A helper class for building MeasuredTemplates for 5e powers and abilities
* @extends {MeasuredTemplate} * @extends {MeasuredTemplate}
*/ */
export class AbilityTemplate extends MeasuredTemplate { export default class AbilityTemplate extends MeasuredTemplate {
/** /**
* A factory method to create an AbilityTemplate instance using provided data from an Item5e instance * A factory method to create an AbilityTemplate instance using provided data from an Item5e instance
@ -37,8 +37,8 @@ export class AbilityTemplate extends MeasuredTemplate {
templateData.width = target.value; templateData.width = target.value;
templateData.direction = 45; templateData.direction = 45;
break; break;
case "ray": // 5e rays are most commonly 5ft wide case "ray": // 5e rays are most commonly 1 square (5 ft) in width
templateData.width = 5; templateData.width = canvas.dimensions.distance;
break; break;
default: default:
break; break;

View file

@ -11,6 +11,23 @@ export const registerSystemSettings = function() {
default: 0 default: 0
}); });
/**
* Register resting variants
*/
game.settings.register("sw5e", "restVariant", {
name: "SETTINGS.5eRestN",
hint: "SETTINGS.5eRestL",
scope: "world",
config: true,
default: "normal",
type: String,
choices: {
"normal": "SETTINGS.5eRestPHB",
"gritty": "SETTINGS.5eRestGritty",
"epic": "SETTINGS.5eRestEpic",
}
});
/** /**
* Register diagonal movement rule setting * Register diagonal movement rule setting
*/ */
@ -23,7 +40,8 @@ export const registerSystemSettings = function() {
type: String, type: String,
choices: { choices: {
"555": "SETTINGS.5eDiagPHB", "555": "SETTINGS.5eDiagPHB",
"5105": "SETTINGS.5eDiagDMG" "5105": "SETTINGS.5eDiagDMG",
"EUCL": "SETTINGS.5eDiagEuclidean",
}, },
onChange: rule => canvas.grid.diagonalRule = rule onChange: rule => canvas.grid.diagonalRule = rule
}); });
@ -31,21 +49,14 @@ export const registerSystemSettings = function() {
/** /**
* Register Initiative formula setting * Register Initiative formula setting
*/ */
function _set5eInitiative(tiebreaker) {
CONFIG.Combat.initiative.tiebreaker = tiebreaker;
CONFIG.Combat.initiative.decimals = tiebreaker ? 2 : 0;
if ( ui.combat && ui.combat._rendered ) ui.combat.render();
}
game.settings.register("sw5e", "initiativeDexTiebreaker", { game.settings.register("sw5e", "initiativeDexTiebreaker", {
name: "SETTINGS.5eInitTBN", name: "SETTINGS.5eInitTBN",
hint: "SETTINGS.5eInitTBL", hint: "SETTINGS.5eInitTBL",
scope: "world", scope: "world",
config: true, config: true,
default: false, default: false,
type: Boolean, type: Boolean
onChange: enable => _set5eInitiative(enable)
}); });
_set5eInitiative(game.settings.get("sw5e", "initiativeDexTiebreaker"));
/** /**
* Require Currency Carrying Weight * Require Currency Carrying Weight

View file

@ -17,7 +17,8 @@ export const preloadHandlebarsTemplates = async function() {
// Item Sheet Partials // Item Sheet Partials
"systems/sw5e/templates/items/parts/item-action.html", "systems/sw5e/templates/items/parts/item-action.html",
"systems/sw5e/templates/items/parts/item-activation.html", "systems/sw5e/templates/items/parts/item-activation.html",
"systems/sw5e/templates/items/parts/item-description.html" "systems/sw5e/templates/items/parts/item-description.html",
"systems/sw5e/templates/items/parts/item-mountable.html"
]; ];
// Load the template parts // Load the template parts

File diff suppressed because one or more lines are too long

669
sw5e.css

File diff suppressed because it is too large Load diff

152
sw5e.js
View file

@ -2,7 +2,7 @@
* The Star Wars 5th Edition game system for Foundry Virtual Tabletop * The Star Wars 5th Edition game system for Foundry Virtual Tabletop
* Author: Kakeman89 * Author: Kakeman89
* Software License: GNU GPLv3 * Software License: GNU GPLv3
* Content License: https://media.wizards.com/2016/downloads/SW5ERD-OGL_V5.1.pdf * Content License: https://media.wizards.com/2016/downloads/SW5E/SRD-OGL_V5.1.pdf
* Repository: https://gitlab.com/foundrynet/sw5e * Repository: https://gitlab.com/foundrynet/sw5e
* Issue Tracker: https://gitlab.com/foundrynet/sw5e/issues * Issue Tracker: https://gitlab.com/foundrynet/sw5e/issues
*/ */
@ -12,14 +12,27 @@ import { SW5E } from "./module/config.js";
import { registerSystemSettings } from "./module/settings.js"; import { registerSystemSettings } from "./module/settings.js";
import { preloadHandlebarsTemplates } from "./module/templates.js"; import { preloadHandlebarsTemplates } from "./module/templates.js";
import { _getInitiativeFormula } from "./module/combat.js"; import { _getInitiativeFormula } from "./module/combat.js";
import { measureDistance, getBarAttribute } from "./module/canvas.js"; import { measureDistances, getBarAttribute } from "./module/canvas.js";
import { Actor5e } from "./module/actor/entity.js";
import { ActorSheet5eCharacter } from "./module/actor/sheets/character.js"; // Import Entities
import { Item5e } from "./module/item/entity.js"; import Actor5e from "./module/actor/entity.js";
import { ItemSheet5e } from "./module/item/sheet.js"; import Item5e from "./module/item/entity.js";
import { ActorSheet5eNPC } from "./module/actor/sheets/npc.js";
import { Dice5e } from "./module/dice.js"; // Import Applications
import AbilityTemplate from "./module/pixi/ability-template.js";
import AbilityUseDialog from "./module/apps/ability-use-dialog.js";
import ActorSheetFlags from "./module/apps/actor-flags.js";
import ActorSheet5eCharacter from "./module/actor/sheets/character.js";
import ActorSheet5eNPC from "./module/actor/sheets/npc.js";
import ActorSheet5eVehicle from "./module/actor/sheets/vehicle.js";
import ItemSheet5e from "./module/item/sheet.js";
import ShortRestDialog from "./module/apps/short-rest.js";
import TraitSelector from "./module/apps/trait-selector.js";
// Import Helpers
import * as chat from "./module/chat.js"; import * as chat from "./module/chat.js";
import * as dice from "./module/dice.js";
import * as macros from "./module/macros.js";
import * as migrations from "./module/migration.js"; import * as migrations from "./module/migration.js";
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -31,11 +44,28 @@ Hooks.once("init", function() {
// Create a SW5E namespace within the game global // Create a SW5E namespace within the game global
game.sw5e = { game.sw5e = {
Actor5e, applications: {
Dice5e, AbilityUseDialog,
Item5e, ActorSheetFlags,
migrations, ActorSheet5eCharacter,
rollItemMacro ActorSheet5eNPC,
ActorSheet5eVehicle,
ItemSheet5e,
ShortRestDialog,
TraitSelector
},
canvas: {
AbilityTemplate
},
config: SW5E,
dice: dice,
entities: {
Actor5e,
Item5e,
},
macros: macros,
migrations: migrations,
rollItemMacro: macros.rollItemMacro
}; };
// Record Configuration Values // Record Configuration Values
@ -51,12 +81,14 @@ Hooks.once("init", function() {
registerSystemSettings(); registerSystemSettings();
// Patch Core Functions // Patch Core Functions
CONFIG.Combat.initiative.formula = "1d20 + @attributes.init.mod + @attributes.init.prof + @attributes.init.bonus";
Combat.prototype._getInitiativeFormula = _getInitiativeFormula; Combat.prototype._getInitiativeFormula = _getInitiativeFormula;
// Register sheet application classes // Register sheet application classes
Actors.unregisterSheet("core", ActorSheet); Actors.unregisterSheet("core", ActorSheet);
Actors.registerSheet("sw5e", ActorSheet5eCharacter, { types: ["character"], makeDefault: true }); Actors.registerSheet("sw5e", ActorSheet5eCharacter, { types: ["character"], makeDefault: true });
Actors.registerSheet("sw5e", ActorSheet5eNPC, { types: ["npc"], makeDefault: true }); Actors.registerSheet("sw5e", ActorSheet5eNPC, { types: ["npc"], makeDefault: true });
Actors.registerSheet('sw5e', ActorSheet5eVehicle, {types: ['vehicle'], makeDefault: true});
Items.unregisterSheet("core", ItemSheet); Items.unregisterSheet("core", ItemSheet);
Items.registerSheet("sw5e", ItemSheet5e, {makeDefault: true}); Items.registerSheet("sw5e", ItemSheet5e, {makeDefault: true});
@ -76,14 +108,28 @@ Hooks.once("setup", function() {
// Localize CONFIG objects once up-front // Localize CONFIG objects once up-front
const toLocalize = [ const toLocalize = [
"abilities", "alignments", "conditionTypes", "consumableTypes", "currencies", "damageTypes", "distanceUnits", "equipmentTypes", "abilities", "abilityAbbreviations", "alignments", "conditionTypes", "consumableTypes", "currencies",
"healingTypes", "itemActionTypes", "limitedUsePeriods", "senses", "skills", "powerComponents", "powerLevels", "powerPreparationModes", "damageTypes", "damageResistanceTypes", "distanceUnits", "equipmentTypes", "healingTypes", "itemActionTypes",
"powerSchools", "powerScalingModes", "targetTypes", "timePeriods", "weaponProperties", "weaponTypes", "languages", "polymorphSettings", "limitedUsePeriods", "senses", "skills", "powerComponents", "powerLevels", "powerPreparationModes", "powerSchools",
"armorProficiencies", "weaponProficiencies", "toolProficiencies", "abilityActivationTypes", "actorSizes", "proficiencyLevels", "armorpropertiesTypes" "powerScalingModes", "targetTypes", "timePeriods", "weaponProperties", "weaponTypes", "languages",
"polymorphSettings", "armorProficiencies", "weaponProficiencies", "toolProficiencies", "abilityActivationTypes",
"abilityConsumptionTypes", "actorSizes", "proficiencyLevels", "armorPropertiesTypes", "cover"
]; ];
// Exclude some from sorting where the default order matters
const noSort = [
"abilities", "alignments", "currencies", "distanceUnits", "itemActionTypes", "proficiencyLevels",
"limitedUsePeriods", "powerComponents", "powerLevels", "weaponTypes"
];
// Localize and sort CONFIG objects
for ( let o of toLocalize ) { for ( let o of toLocalize ) {
CONFIG.SW5E[o] = Object.entries(CONFIG.SW5E[o]).reduce((obj, e) => { const localized = Object.entries(CONFIG.SW5E[o]).map(e => {
obj[e[0]] = game.i18n.localize(e[1]); return [e[0], game.i18n.localize(e[1])];
});
if ( !noSort.includes(o) ) localized.sort((a, b) => a[1].localeCompare(b[1]));
CONFIG.SW5E[o] = localized.reduce((obj, e) => {
obj[e[0]] = e[1];
return obj; return obj;
}, {}); }, {});
} }
@ -101,7 +147,6 @@ Hooks.once("ready", function() {
const NEEDS_MIGRATION_VERSION = 0.84; const NEEDS_MIGRATION_VERSION = 0.84;
const COMPATIBLE_MIGRATION_VERSION = 0.80; const COMPATIBLE_MIGRATION_VERSION = 0.80;
let needMigration = (currentVersion < NEEDS_MIGRATION_VERSION) || (currentVersion === null); let needMigration = (currentVersion < NEEDS_MIGRATION_VERSION) || (currentVersion === null);
const canMigrate = currentVersion >= COMPATIBLE_MIGRATION_VERSION;
// Perform the migration // Perform the migration
if ( needMigration && game.user.isGM ) { if ( needMigration && game.user.isGM ) {
@ -112,7 +157,7 @@ Hooks.once("ready", function() {
} }
// Wait to register hotbar drop hook on ready so that modules could register earlier if they want to // Wait to register hotbar drop hook on ready so that modules could register earlier if they want to
Hooks.on("hotbarDrop", (bar, data, slot) => create5eMacro(data, slot)); Hooks.on("hotbarDrop", (bar, data, slot) => macros.create5eMacro(data, slot));
}); });
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -123,7 +168,7 @@ Hooks.on("canvasInit", function() {
// Extend Diagonal Measurement // Extend Diagonal Measurement
canvas.grid.diagonalRule = game.settings.get("sw5e", "diagonalMovement"); canvas.grid.diagonalRule = game.settings.get("sw5e", "diagonalMovement");
SquareGrid.prototype.measureDistance = measureDistance; SquareGrid.prototype.measureDistances = measureDistances;
// Extend Token Resource Bars // Extend Token Resource Bars
Token.prototype.getBarAttribute = getBarAttribute; Token.prototype.getBarAttribute = getBarAttribute;
@ -149,63 +194,6 @@ Hooks.on("getChatLogEntryContext", chat.addChatMessageContextOptions);
Hooks.on("renderChatLog", (app, html, data) => Item5e.chatListeners(html)); Hooks.on("renderChatLog", (app, html, data) => Item5e.chatListeners(html));
Hooks.on('getActorDirectoryEntryContext', Actor5e.addDirectoryContextOptions); Hooks.on('getActorDirectoryEntryContext', Actor5e.addDirectoryContextOptions);
Handlebars.registerHelper('getProperty', function (data, property) {
/* -------------------------------------------- */ return getProperty(data, property);
/* Hotbar Macros */ });
/* -------------------------------------------- */
/**
* Create a Macro from an Item drop.
* Get an existing item macro if one exists, otherwise create a new one.
* @param {Object} data The dropped data
* @param {number} slot The hotbar slot to use
* @returns {Promise}
*/
async function create5eMacro(data, slot) {
if ( data.type !== "Item" ) return;
if (!( "data" in data ) ) return ui.notifications.warn("You can only create macro buttons for owned Items");
const item = data.data;
// Create the macro command
const command = `game.sw5e.rollItemMacro("${item.name}");`;
let macro = game.macros.entities.find(m => (m.name === item.name) && (m.command === command));
if ( !macro ) {
macro = await Macro.create({
name: item.name,
type: "script",
img: item.img,
command: command,
flags: {"sw5e.itemMacro": true}
});
}
game.user.assignHotbarMacro(macro, slot);
return false;
}
/* -------------------------------------------- */
/**
* Create a Macro from an Item drop.
* Get an existing item macro if one exists, otherwise create a new one.
* @param {string} itemName
* @return {Promise}
*/
function rollItemMacro(itemName) {
const speaker = ChatMessage.getSpeaker();
let actor;
if ( speaker.token ) actor = game.actors.tokens[speaker.token];
if ( !actor ) actor = game.actors.get(speaker.actor);
// Get matching items
const items = actor ? actor.items.filter(i => i.name === itemName) : [];
if ( items.length > 1 ) {
ui.notifications.warn(`Your controlled Actor ${actor.name} has more than one Item with name ${itemName}. The first matched item will be chosen.`);
} else if ( items.length === 0 ) {
return ui.notifications.warn(`Your controlled Actor does not have an item named ${itemName}`);
}
const item = items[0];
// Trigger the item roll
if ( item.data.type === "power" ) return actor.usePower(item);
return item.roll();
}

View file

@ -95,13 +95,12 @@
} }
], ],
"socket": true, "socket": true,
"initiative": "1d20 + @abilities.dex.mod + @attributes.init.value + (@abilities.dex.value / 100)",
"gridDistance": 5, "gridDistance": 5,
"gridUnits": "ft", "gridUnits": "ft",
"primaryTokenAttribute": "attributes.hp", "primaryTokenAttribute": "attributes.hp",
"secondaryTokenAttribute": null, "secondaryTokenAttribute": null,
"minimumCoreVersion": "0.5.2", "minimumCoreVersion": "0.5.6",
"compatibleCoreVersion": "0.6.8", "compatibleCoreVersion": "0.7.2",
"url": "https://github.com/unrealkakeman89/sw5e", "url": "https://github.com/unrealkakeman89/sw5e",
"manifest": "https://raw.githubusercontent.com/unrealkakeman89/sw5e/master/system.json", "manifest": "https://raw.githubusercontent.com/unrealkakeman89/sw5e/master/system.json",
"download": "https://github.com/unrealkakeman89/sw5e/archive/master.zip" "download": "https://github.com/unrealkakeman89/sw5e/archive/master.zip"

View file

@ -1,6 +1,6 @@
{ {
"Actor": { "Actor": {
"types": ["character", "npc"], "types": ["character", "npc", "vehicle"],
"templates": { "templates": {
"common": { "common": {
"abilities": { "abilities": {
@ -31,7 +31,7 @@
}, },
"attributes": { "attributes": {
"ac": { "ac": {
"min": 0 "value": 10
}, },
"hp": { "hp": {
"value": 10, "value": 10,
@ -43,19 +43,47 @@
"init": { "init": {
"value": 0, "value": 0,
"bonus": 0 "bonus": 0
}, }
"speed": {
"value": "30 ft",
"special": ""
},
"powercasting": "int"
}, },
"details": { "details": {
"alignment": "",
"biography": { "biography": {
"value": "", "value": "",
"public": "" "public": ""
}
},
"traits": {
"size": "med",
"di": {
"value": [],
"custom": ""
}, },
"dr": {
"value": [],
"custom": ""
},
"dv": {
"value": [],
"custom": ""
},
"ci": {
"value": [],
"custom": ""
}
},
"currency": {
"gc": 0
}
},
"creature": {
"attributes": {
"powercasting": "int",
"speed": {
"value": "30 ft",
"special": ""
}
},
"details": {
"alignment": "",
"species": "" "species": ""
}, },
"skills": { "skills": {
@ -133,32 +161,12 @@
} }
}, },
"traits": { "traits": {
"size": "med",
"senses": "", "senses": "",
"languages": { "languages": {
"value": [], "value": [],
"custom": "" "custom": ""
},
"di": {
"value": [],
"custom": ""
},
"dr": {
"value": [],
"custom": ""
},
"dv": {
"value": [],
"custom": ""
},
"ci": {
"value": [],
"custom": ""
} }
}, },
"currency": {
"gc": 0
},
"powers": { "powers": {
"power1": { "power1": {
"value": 0, "value": 0,
@ -240,12 +248,16 @@
} }
}, },
"character": { "character": {
"templates": ["common"], "templates": ["common", "creature"],
"attributes": { "attributes": {
"death": { "death": {
"success": 0, "success": 0,
"failure": 0 "failure": 0
}, },
"encumbrance": {
"value": null,
"max": null
},
"exhaustion": 0, "exhaustion": 0,
"inspiration": 0 "inspiration": 0
}, },
@ -297,7 +309,7 @@
} }
}, },
"npc": { "npc": {
"templates": ["common"], "templates": ["common", "creature"],
"details": { "details": {
"type": "", "type": "",
"environment": "", "environment": "",
@ -322,6 +334,61 @@
"initiative": 0 "initiative": 0
} }
} }
},
"vehicle": {
"templates": ["common"],
"abilities": {
"int": {
"value": 0
},
"wis": {
"value": 0
},
"cha": {
"value": 0
}
},
"attributes": {
"ac": {
"value": null,
"motionless": ""
},
"actions": {
"stations": false,
"value": 0,
"thresholds": {
"2": null,
"1": null,
"0": null
}
},
"hp": {
"value": null,
"max": null,
"dt": null,
"mt": null
},
"capacity": {
"creature": "",
"cargo": 0
},
"speed": ""
},
"traits": {
"size": "lg",
"dimensions": "",
"di": {
"value": ["poison", "psychic"]
},
"ci": {
"value": ["blinded", "charmed", "deafened", "frightened", "paralyzed", "petrified", "poisoned", "stunned",
"unconscious"]
}
},
"cargo": {
"crew": [],
"passengers": []
}
} }
}, },
"Item": { "Item": {
@ -335,15 +402,15 @@
}, },
"source": "" "source": ""
}, },
"speciesDescription": { "speciesDescription": {
"data": "$characteristics-table", "data": "$characteristics-table",
"description": { "description": {
"value": "", "value": "",
"chat": "", "chat": "",
"unidentified": "" "unidentified": ""
}, },
"traits": "" "traits": ""
}, },
"physicalItem": { "physicalItem": {
"quantity": 1, "quantity": 1,
"weight": 0, "weight": 0,
@ -377,6 +444,11 @@
"value": 0, "value": 0,
"max": 0, "max": 0,
"per": null "per": null
},
"consume": {
"type": "",
"target": null,
"amount": null
} }
}, },
"action": { "action": {
@ -395,6 +467,17 @@
"dc": null, "dc": null,
"scaling": "power" "scaling": "power"
} }
},
"mountable": {
"armor": {
"value": 10
},
"hp": {
"value": 0,
"max": 0,
"dt": null,
"conditions": ""
}
} }
}, },
"backpack": { "backpack": {
@ -425,20 +508,20 @@
"templates": ["itemDescription", "physicalItem", "activatedEffect", "action"], "templates": ["itemDescription", "physicalItem", "activatedEffect", "action"],
"consumableType": "potion", "consumableType": "potion",
"uses": { "uses": {
"value": 0, "autoDestroy": false
"max": 0,
"per": null,
"autoUse": true,
"autoDestroy": true
} }
}, },
"equipment": { "equipment": {
"templates": ["itemDescription", "physicalItem", "activatedEffect", "action"], "templates": ["itemDescription", "physicalItem", "activatedEffect", "action", "mountable"],
"armor": { "armor": {
"type": "light", "type": "light",
"value": 10, "value": 10,
"dex": null "dex": null
}, },
"speed": {
"value": null,
"conditions": ""
},
"strength": 0, "strength": 0,
"stealth": false, "stealth": false,
"proficient": true "proficient": true
@ -456,8 +539,7 @@
}, },
"species": { "species": {
"templates": ["speciesDescription"] "templates": ["speciesDescription"]
},
},
"tool": { "tool": {
"templates": ["itemDescription", "physicalItem"], "templates": ["itemDescription", "physicalItem"],
"ability": "int", "ability": "int",
@ -487,13 +569,13 @@
"prepared": false "prepared": false
}, },
"scaling": { "scaling": {
"mode": null, "mode": "none",
"formula": null "formula": null
} }
}, },
"weapon": { "weapon": {
"templates": ["itemDescription", "physicalItem" ,"activatedEffect", "action"], "templates": ["itemDescription", "physicalItem" ,"activatedEffect", "action", "mountable"],
"weaponType": "simpleM", "weaponType": "simpleVW",
"properties": {}, "properties": {},
"proficient": true "proficient": true
} }

View file

@ -4,31 +4,34 @@
<header class="sheet-header flexrow"> <header class="sheet-header flexrow">
<img class="profile" src="{{actor.img}}" title="{{actor.name}}" data-edit="img"/> <img class="profile" src="{{actor.img}}" title="{{actor.name}}" data-edit="img"/>
<div class="header-details flexrow"> <section class="header-details flexrow">
<h1 class="charname"> <h1 class="charname">
<input name="name" type="text" value="{{actor.name}}" placeholder="{{ localize 'SW5E.Name' }}"/> <input name="name" type="text" value="{{actor.name}}" placeholder="{{ localize 'SW5E.Name' }}"/>
</h1> </h1>
<div class="charlevel"> <aside class="header-exp flexcol">
<div class="level {{#if disableExperience}}noxp{{/if}}"> <div class="charlevel">
<label>{{ localize "SW5E.Level" }} {{data.details.level}}</label> <label>{{ localize "SW5E.Level" }} {{data.details.level}}</label>
<span class="levels">{{classLabels}}</span>
</div> </div>
{{#unless disableExperience}} {{#unless disableExperience}}
<div class="experience"> <div class="experience flexrow">
<input name="data.details.xp.value" type="text" value="{{data.details.xp.value}}" <input name="data.details.xp.value" type="text" value="{{data.details.xp.value}}"
data-dtype="Number" placeholder="0"/> data-dtype="Number" placeholder="0"/>
<span class="max"> / {{data.details.xp.max}}</span> <span class="sep">/</span>
<span class="max">{{data.details.xp.max}}</span>
</div> </div>
<div class="xpbar"> <div class="xpbar">
<span class="bar" style="width: {{data.details.xp.pct}}%"></span> <span class="bar" style="width: {{data.details.xp.pct}}%"></span>
</div> </div>
{{/unless}} {{/unless}}
</div> </aside>
{{!-- Character Summary --}} {{!-- Character Summary --}}
<ul class="summary"> <ul class="summary flexrow">
<li> <li>
<input type="text" name="data.details.race" value="{{data.details.race}}" placeholder="{{ localize 'SW5E.Species' }}"/> <input type="text" name="data.details.species" value="{{data.details.species}}" placeholder="{{ localize 'SW5E.Species' }}"/>
</li> </li>
<li> <li>
<input type="text" name="data.details.background" value="{{data.details.background}}" placeholder="{{ localize 'SW5E.Background' }}"/> <input type="text" name="data.details.background" value="{{data.details.background}}" placeholder="{{ localize 'SW5E.Background' }}"/>
@ -36,6 +39,9 @@
<li> <li>
<input type="text" name="data.details.alignment" value="{{data.details.alignment}}" placeholder="{{ localize 'SW5E.Alignment' }}"/> <input type="text" name="data.details.alignment" value="{{data.details.alignment}}" placeholder="{{ localize 'SW5E.Alignment' }}"/>
</li> </li>
<li class="proficiency">
<span>{{ localize "SW5E.Proficiency" }} {{numberFormat data.attributes.prof decimals=0 sign=true}}</span>
</li>
</ul> </ul>
{{!-- Header Attributes --}} {{!-- Header Attributes --}}
@ -57,6 +63,17 @@
</footer> </footer>
</li> </li>
<li class="attribute">
<h4 class="attribute-name box-title">{{ localize "SW5E.HitDice" }}</h4>
<div class="attribute-value multiple">
<label class="hit-dice">{{data.attributes.hd}} <span class="sep"> / </span> {{data.details.level}}</label>
</div>
<footer class="attribute-footer">
<a class="rest short-rest">{{ localize "SW5E.RestS" }}</a>
<a class="rest long-rest">{{ localize "SW5E.RestL" }}</a>
</footer>
</li>
<li class="attribute"> <li class="attribute">
<h4 class="attribute-name box-title">{{ localize "SW5E.ArmorClass" }}</h4> <h4 class="attribute-name box-title">{{ localize "SW5E.ArmorClass" }}</h4>
<div class="attribute-value"> <div class="attribute-value">
@ -64,8 +81,7 @@
data-dtype="Number" placeholder="10"/> data-dtype="Number" placeholder="10"/>
</div> </div>
<footer class="attribute-footer"> <footer class="attribute-footer">
<span>{{ localize "SW5E.Proficiency" }}</span> <span class="power-dc">{{localize "SW5E.PowerDC"}} {{data.attributes.powerdc}}</span>
<span>{{numberFormat data.attributes.prof decimals=0 sign=true}}</span>
</footer> </footer>
</li> </li>
@ -81,18 +97,19 @@
</footer> </footer>
</li> </li>
<li class="attribute"> <li class="attribute initiative">
<h4 class="attribute-name box-title">{{ localize "SW5E.HitDice" }}</h4> <h4 class="attribute-name box-title">{{ localize "SW5E.Initiative" }}</h4>
<div class="attribute-value multiple"> <div class="attribute-value">
<label class="hit-dice">{{data.attributes.hd}} / {{data.details.level}}</label> <span>{{numberFormat data.attributes.init.total decimals=0 sign=true}}</span>
</div> </div>
<footer class="attribute-footer"> <footer class="attribute-footer">
<a class="rest short-rest">{{ localize "SW5E.RestS" }}</a> <span>{{ localize "SW5E.Modifier" }}</span>
<a class="rest long-rest">{{ localize "SW5E.RestL" }}</a> <input name="data.attributes.init.value" type="text" placeholder="0" data-dtype="Number"
value="{{numberFormat data.attributes.init.value decimals=0 sign=true}}"/>
</footer> </footer>
</li> </li>
</ul> </ul>
</div> </section>
</header> </header>
{{!-- NPC Sheet Navigation --}} {{!-- NPC Sheet Navigation --}}
@ -118,7 +135,7 @@
<div class="ability-modifiers flexrow"> <div class="ability-modifiers flexrow">
<span class="ability-mod" title="Modifier">{{numberFormat ability.mod decimals=0 sign=true}}</span> <span class="ability-mod" title="Modifier">{{numberFormat ability.mod decimals=0 sign=true}}</span>
<input type="hidden" name="data.abilities.{{id}}.proficient" value="{{ability.proficient}}" data-dtype="Number"/> <input type="hidden" name="data.abilities.{{id}}.proficient" value="{{ability.proficient}}" data-dtype="Number"/>
<a class="proficiency-toggle ability-proficiency" title="Proficiency">{{{ability.icon}}}</a> <a class="proficiency-toggle ability-proficiency" title="{{ localize 'SW5E.Proficiency' }}">{{{ability.icon}}}</a>
<span class="ability-save" title="Saving Throw">{{numberFormat ability.save decimals=0 sign=true}}</span> <span class="ability-save" title="Saving Throw">{{numberFormat ability.save decimals=0 sign=true}}</span>
</div> </div>
</li> </li>
@ -133,13 +150,13 @@
<a class="proficiency-toggle skill-proficiency" title="{{skill.hover}}">{{{skill.icon}}}</a> <a class="proficiency-toggle skill-proficiency" title="{{skill.hover}}">{{{skill.icon}}}</a>
<h4 class="skill-name rollable">{{skill.label}}</h4> <h4 class="skill-name rollable">{{skill.label}}</h4>
<span class="skill-ability">{{skill.ability}}</span> <span class="skill-ability">{{skill.ability}}</span>
<span class="skill-mod">{{numberFormat skill.mod decimals=0 sign=true}}</span> <span class="skill-mod">{{numberFormat skill.total decimals=0 sign=true}}</span>
<span class="skill-passive">({{skill.passive}})</span> <span class="skill-passive">({{skill.passive}})</span>
</li> </li>
{{/each}} {{/each}}
</ul> </ul>
<section class="center-pane"> <section class="center-pane flexcol">
{{!-- Body Attributes --}} {{!-- Body Attributes --}}
<ul class="attributes flexrow"> <ul class="attributes flexrow">
@ -149,39 +166,27 @@
<input name="data.resources.{{res.name}}.label" type="text" value="{{res.label}}" <input name="data.resources.{{res.name}}.label" type="text" value="{{res.label}}"
placeholder="{{res.placeholder}}" /> placeholder="{{res.placeholder}}" />
</h4> </h4>
<div class="attribute-value multiple"> <div class="attribute-value">
<label class="recharge checkbox">
{{ localize "SW5E.AbbreviationSR" }} <input name="data.resources.{{res.name}}.sr" type="checkbox" {{checked res.sr}}/>
</label>
<input name="data.resources.{{res.name}}.value" type="text" value="{{res.value}}" <input name="data.resources.{{res.name}}.value" type="text" value="{{res.value}}"
data-dtype="Number" placeholder="0"/> data-dtype="Number" placeholder="0"/>
<span class="sep"> / </span> <span class="sep"> / </span>
<input name="data.resources.{{res.name}}.max" type="text" value="{{res.max}}" <input name="data.resources.{{res.name}}.max" type="text" value="{{res.max}}"
data-dtype="Number" placeholder="0"/> data-dtype="Number" placeholder="0"/>
</div>
<footer class="attribute-footer"> <label class="recharge checkbox">
<label class="checkbox">
{{ localize "SW5E.AbbreviationSR" }} <input name="data.resources.{{res.name}}.sr" type="checkbox" {{checked res.sr}}/>
</label>
<label class="checkbox">
{{ localize "SW5E.AbbreviationLR" }} <input name="data.resources.{{res.name}}.lr" type="checkbox" {{checked res.lr}}/> {{ localize "SW5E.AbbreviationLR" }} <input name="data.resources.{{res.name}}.lr" type="checkbox" {{checked res.lr}}/>
</label> </label>
</footer> </div>
</li> </li>
{{/each}} {{/each}}
<li class="attribute initiative">
<h4 class="attribute-name box-title">{{ localize "SW5E.Initiative" }}</h4>
<div class="attribute-value">
<span>{{numberFormat data.attributes.init.total decimals=0 sign=true}}</span>
</div>
<footer class="attribute-footer">
<span>{{ localize "SW5E.Modifier" }}</span>
<input name="data.attributes.init.value" type="text" placeholder="0" data-dtype="Number"
value="{{numberFormat data.attributes.init.value decimals=0 sign=true}}"/>
</footer>
</li>
</ul> </ul>
{{!-- Counters --}} {{!-- Counters --}}
<div class="counters flexrow"> <div class="counters">
<div class="counter flexrow death-saves"> <div class="counter flexrow death-saves">
<h4 class="death-save rollable">{{ localize "SW5E.DeathSave" }}</h4> <h4 class="death-save rollable">{{ localize "SW5E.DeathSave" }}</h4>
<div class="counter-value"> <div class="counter-value">
@ -193,14 +198,14 @@
value="{{data.attributes.death.failure}}"/> value="{{data.attributes.death.failure}}"/>
</div> </div>
</div> </div>
<div class="counter flexrow"> <div class="counter flexrow exhaustion">
<h4>{{ localize "SW5E.Exhaustion" }}</h4> <h4>{{ localize "SW5E.Exhaustion" }}</h4>
<div class="counter-value"> <div class="counter-value">
<input type="text" name="data.attributes.exhaustion" data-dtype="Number" placeholder="0" <input type="text" name="data.attributes.exhaustion" data-dtype="Number" placeholder="0"
value="{{data.attributes.exhaustion}}" /> value="{{data.attributes.exhaustion}}" />
</div> </div>
</div> </div>
<div class="counter flexrow"> <div class="counter flexrow inspiration">
<h4>{{ localize "SW5E.Inspiration" }}</h4> <h4>{{ localize "SW5E.Inspiration" }}</h4>
<div class="counter-value"> <div class="counter-value">
<input type="checkbox" name="data.attributes.inspiration" data-dtype="Boolean" <input type="checkbox" name="data.attributes.inspiration" data-dtype="Boolean"

View file

@ -1,21 +1,20 @@
<form class="{{cssClass}}" autocomplete="off"> <form class="{{cssClass}} flexcol limited" autocomplete="off">
<!-- HEADER --> {{!-- Sheet Header --}}
<header class="sheet-header"> <header class="sheet-header flexrow">
<h1 class="charname"> <img class="profile" src="{{actor.img}}" title="{{actor.name}}" data-edit="img"/>
<input name="name" type="text" value="{{actor.name}}" placeholder="{{ localize 'SW5E.Name' }}"/>
</h1> <section class="header-details flexrow">
<h1 class="charname">
<input name="name" type="text" value="{{actor.name}}" placeholder="{{ localize 'SW5E.Name' }}"/>
</h1>
</section>
</header> </header>
<!-- BODY --> {{!-- Sheet Body --}}
<section class="sheet-body"> <section class="sheet-body">
<div class="tab biography"> <div class="tab biography">
{{editor content=data.details.biography.value target="data.details.biography.value" button=true owner=owner editable=editable}} {{editor content=data.details.biography.value target="data.details.biography.value" button=true owner=owner editable=editable}}
</div> </div>
</section> </section>
<!-- SIDEBAR -->
<section class="sheet-sidebar">
<img class="sheet-profile" src="{{actor.img}}" title="{{actor.name}}" height="220" width="220" data-edit="img"/>
</section>
</form> </form>

View file

@ -4,33 +4,79 @@
<header class="sheet-header flexrow"> <header class="sheet-header flexrow">
<img class="profile" src="{{actor.img}}" title="{{actor.name}}" data-edit="img"/> <img class="profile" src="{{actor.img}}" title="{{actor.name}}" data-edit="img"/>
<div class="header-details flexrow"> <section class="header-details flexrow">
<h1 class="charname"> <h1 class="charname">
<input name="name" type="text" value="{{actor.name}}" placeholder="{{ localize 'SW5E.Name' }}"/> <input name="name" type="text" value="{{actor.name}}" placeholder="{{ localize 'SW5E.Name' }}"/>
</h1> </h1>
<div class="charlevel"> <aside class="header-exp flexcol">
<div class="level"> <div class="cr">
<label>{{ localize "SW5E.AbbreviationCR" }}</label> <label>{{ localize "SW5E.AbbreviationCR" }}</label>
<input name="data.details.cr" type="text" value="{{labels.cr}}" placeholder="1"/> <input name="data.details.cr" type="text" value="{{labels.cr}}" placeholder="1"/>
</div> </div>
<div class="experience"> <div class="experience">
<span>{{data.details.xp.value}} XP</span> <span>{{data.details.xp.value}} XP</span>
</div> </div>
</div> </aside>
<ul class="summary"> {{!-- Character Summary --}}
<ul class="summary flexrow">
<li> <li>
<input type="text" name="data.details.type" value="{{data.details.type}}" placeholder="{{ localize 'SW5E.Type' }}"/> <span>{{lookup config.actorSizes data.traits.size}}</span>
</li> </li>
<li> <li>
<input type="text" name="data.details.alignment" value="{{data.details.alignment}}" placeholder="{{ localize 'SW5E.Alignment' }}"/> <input type="text" name="data.details.alignment" value="{{data.details.alignment}}" placeholder="{{ localize 'SW5E.Alignment' }}"/>
</li> </li>
<li>
<input type="text" name="data.details.type" value="{{data.details.type}}" placeholder="{{ localize 'SW5E.Type' }}"/>
</li>
<li> <li>
<input type="text" name="data.details.source" value="{{data.details.source}}" placeholder="{{ localize 'SW5E.Source' }}"/> <input type="text" name="data.details.source" value="{{data.details.source}}" placeholder="{{ localize 'SW5E.Source' }}"/>
</li> </li>
</ul> </ul>
</div>
{{!-- Header Attributes --}}
<ul class="attributes flexrow">
<li class="attribute health">
<h4 class="attribute-name box-title rollable">{{ localize "SW5E.Health" }}</h4>
<div class="attribute-value multiple">
<input name="data.attributes.hp.value" type="text" value="{{data.attributes.hp.value}}"
data-dtype="Number" placeholder="10"/>
<span class="sep"> / </span>
<input name="data.attributes.hp.max" type="text" value="{{data.attributes.hp.max}}"
data-dtype="Number" placeholder="10"/>
</div>
<footer class="attribute-footer">
<input name="data.attributes.hp.formula" class="hpformula" type="text" placeholder="{{ localize 'SW5E.HealthFormula' }}"
value="{{data.attributes.hp.formula}}"/>
</footer>
</li>
<li class="attribute">
<h4 class="attribute-name box-title">{{ localize "SW5E.ArmorClass" }}</h4>
<div class="attribute-value">
<input name="data.attributes.ac.value" type="text" value="{{data.attributes.ac.value}}"
data-dtype="Number" placeholder="10"/>
</div>
<footer class="attribute-footer">
<span>{{ localize "SW5E.Proficiency" }}</span>
<span>{{numberFormat data.attributes.prof decimals=0 sign=true}}</span>
</footer>
</li>
<li class="attribute">
<h4 class="attribute-name box-title">{{ localize "SW5E.Speed" }}</h4>
<div class="attribute-value">
<input name="data.attributes.speed.value" type="text"
value="{{data.attributes.speed.value}}" placeholder="0"/>
</div>
<footer class="attribute-footer">
<input type="text" class="speed" name="data.attributes.speed.special"
value="{{data.attributes.speed.special}}" placeholder="{{ localize 'SW5E.SpeedSpecial' }}"/>
</footer>
</li>
</ul>
</section>
</header> </header>
{{!-- NPC Sheet Navigation --}} {{!-- NPC Sheet Navigation --}}
@ -54,7 +100,7 @@
<div class="ability-modifiers flexrow"> <div class="ability-modifiers flexrow">
<span class="ability-mod" title="Modifier">{{numberFormat ability.mod decimals=0 sign=true}}</span> <span class="ability-mod" title="Modifier">{{numberFormat ability.mod decimals=0 sign=true}}</span>
<input type="hidden" name="data.abilities.{{id}}.proficient" value="{{ability.proficient}}" data-dtype="Number"/> <input type="hidden" name="data.abilities.{{id}}.proficient" value="{{ability.proficient}}" data-dtype="Number"/>
<a class="proficiency-toggle ability-proficiency" title="Proficiency">{{{ability.icon}}}</a> <a class="proficiency-toggle ability-proficiency" title="{{ localize 'SW5E.Proficiency' }}">{{{ability.icon}}}</a>
<span class="ability-save" title="Saving Throw">{{numberFormat ability.save decimals=0 sign=true}}</span> <span class="ability-save" title="Saving Throw">{{numberFormat ability.save decimals=0 sign=true}}</span>
</div> </div>
</li> </li>
@ -69,58 +115,16 @@
<a class="proficiency-toggle skill-proficiency" title="{{skill.hover}}">{{{skill.icon}}}</a> <a class="proficiency-toggle skill-proficiency" title="{{skill.hover}}">{{{skill.icon}}}</a>
<h4 class="skill-name rollable">{{skill.label}}</h4> <h4 class="skill-name rollable">{{skill.label}}</h4>
<span class="skill-ability">{{skill.ability}}</span> <span class="skill-ability">{{skill.ability}}</span>
<span class="skill-mod">{{numberFormat skill.mod decimals=0 sign=true}}</span> <span class="skill-mod">{{numberFormat skill.total decimals=0 sign=true}}</span>
<span class="skill-passive">({{skill.passive}})</span> <span class="skill-passive">({{skill.passive}})</span>
</li> </li>
{{/each}} {{/each}}
</ul> </ul>
<section class="center-pane"> <section class="center-pane flexcol">
{{!-- Attributes --}}
<ul class="attributes flexrow">
<li class="attribute health">
<h4 class="attribute-name box-title rollable">{{ localize "SW5E.Health" }}</h4>
<div class="attribute-value multiple">
<input name="data.attributes.hp.value" type="text" value="{{data.attributes.hp.value}}"
data-dtype="Number" placeholder="10"/>
<span class="sep"> / </span>
<input name="data.attributes.hp.max" type="text" value="{{data.attributes.hp.max}}"
data-dtype="Number" placeholder="10"/>
</div>
<footer class="attribute-footer">
<input name="data.attributes.hp.formula" class="hpformula" type="text" placeholder="{{ localize 'SW5E.HealthFormula' }}"
value="{{data.attributes.hp.formula}}"/>
</footer>
</li>
<li class="attribute">
<h4 class="attribute-name box-title">{{ localize "SW5E.ArmorClass" }}</h4>
<div class="attribute-value">
<input name="data.attributes.ac.value" type="text" value="{{data.attributes.ac.value}}"
data-dtype="Number" placeholder="10"/>
</div>
<footer class="attribute-footer">
<span>{{ localize "SW5E.Proficiency" }}</span>
<span>{{numberFormat data.attributes.prof decimals=0 sign=true}}</span>
</footer>
</li>
<li class="attribute">
<h4 class="attribute-name box-title">{{ localize "SW5E.Speed" }}</h4>
<div class="attribute-value">
<input name="data.attributes.speed.value" type="text"
value="{{data.attributes.speed.value}}" placeholder="0"/>
</div>
<footer class="attribute-footer">
<input type="text" class="speed" name="data.attributes.speed.special"
value="{{data.attributes.speed.special}}" placeholder="{{ localize 'SW5E.SpeedSpecial' }}"/>
</footer>
</li>
</ul>
{{!-- Legendary Actions --}} {{!-- Legendary Actions --}}
<div class="counters flexrow"> <div class="counters">
<div class="counter flexrow legendary"> <div class="counter flexrow legendary">
<h4>{{ localize "SW5E.LegAct" }}</h4> <h4>{{ localize "SW5E.LegAct" }}</h4>
<div class="counter-value"> <div class="counter-value">

View file

@ -1,11 +1,12 @@
<div class="inventory-filters"> {{#unless isVehicle}}
<div class="inventory-filters flexrow">
<ul class="filter-list flexrow" data-filter="features"> <ul class="filter-list flexrow" data-filter="features">
<li class="filter-title">{{localize "SW5E.Filter"}}</li>
<li class="filter-item" data-filter="action">{{localize "SW5E.Action"}}</li> <li class="filter-item" data-filter="action">{{localize "SW5E.Action"}}</li>
<li class="filter-item" data-filter="bonus">{{localize "SW5E.BonusAction"}}</li> <li class="filter-item" data-filter="bonus">{{localize "SW5E.BonusAction"}}</li>
<li class="filter-item" data-filter="reaction">{{localize "SW5E.Reaction"}}</li> <li class="filter-item" data-filter="reaction">{{localize "SW5E.Reaction"}}</li>
</ul> </ul>
</div> </div>
{{/unless}}
<ol class="inventory-list"> <ol class="inventory-list">
{{#each sections as |section sid|}} {{#each sections as |section sid|}}
@ -17,6 +18,12 @@
<div class="item-detail item-action">{{localize "SW5E.Usage"}}</div> <div class="item-detail item-action">{{localize "SW5E.Usage"}}</div>
{{/if}} {{/if}}
{{#if section.columns}}
{{#each section.columns}}
<div class="item-detail {{css}}">{{label}}</div>
{{/each}}
{{/if}}
{{#if ../owner}} {{#if ../owner}}
<div class="item-controls"> <div class="item-controls">
<a class="item-control item-create" title="{{localize 'SW5E.FeatureAdd'}}" {{#each section.dataset as |v k|}}data-{{k}}="{{v}}"{{/each}}> <a class="item-control item-create" title="{{localize 'SW5E.FeatureAdd'}}" {{#each section.dataset as |v k|}}data-{{k}}="{{v}}"{{/each}}>
@ -56,13 +63,34 @@
<div class="item-detail player-class"> <div class="item-detail player-class">
{{item.data.subclass}} {{item.data.subclass}}
</div> </div>
<div class="item-detail"> <div class="item-detail item-action">
Level {{item.data.levels}} Level {{item.data.levels}}
</div> </div>
{{/if}} {{/if}}
{{#if section.columns}}
{{#each section.columns}}
<div class="item-detail {{css}}">
{{#with (getProperty item property)}}
{{#if ../editable}}
<input type="text" value="{{this}}" placeholder="&mdash;"
data-dtype="{{../editable}}">
{{else}}
{{this}}
{{/if}}
{{/with}}
</div>
{{/each}}
{{/if}}
{{#if ../../owner}} {{#if ../../owner}}
<div class="item-controls"> <div class="item-controls">
{{#if section.crewable}}
<a class="item-control item-toggle {{item.toggleClass}}"
title="{{item.toggleTitle}}">
<i class="fas fa-sun"></i>
</a>
{{/if}}
<a class="item-control item-edit" title="Edit Item"><i class="fas fa-edit"></i></a> <a class="item-control item-edit" title="Edit Item"><i class="fas fa-edit"></i></a>
<a class="item-control item-delete" title="Delete Item"><i class="fas fa-trash"></i></a> <a class="item-control item-delete" title="Delete Item"><i class="fas fa-trash"></i></a>
</div> </div>

View file

@ -1,6 +1,12 @@
<div class="inventory-filters powerbook-filters"> <div class="inventory-filters powerbook-filters flexrow">
<div class="form-group powercasting-ability"> <div class="form-group powercasting-ability">
<h3>{{localize "SW5E.PowerAbility"}}</h3> {{#unless isNPC}}
<label>{{localize "SW5E.PowerAbility"}}</label>
{{else}}
<label>{{localize "SW5E.Level"}}</label>
<input class="powercasting-level" type="text" name="data.details.powerLevel"
value="{{data.details.powerLevel}}" data-dtype="Number" placeholder="0"/>
{{/unless}}
<select name="data.attributes.powercasting" data-type="String"> <select name="data.attributes.powercasting" data-type="String">
{{#select data.attributes.powercasting}} {{#select data.attributes.powercasting}}
<option value="">{{localize "SW5E.None"}}</option> <option value="">{{localize "SW5E.None"}}</option>
@ -9,21 +15,15 @@
{{/each}} {{/each}}
{{/select}} {{/select}}
</select> </select>
{{#if isNPC}}
<h3>{{localize "SW5E.PowercasterLevel"}}</h3>
<input class="powercasting-level" type="text" name="data.details.powerLevel"
value="{{data.details.powerLevel}}" data-dtype="Number" placeholder="0"/>
{{/if}}
<h3 class="power-dc">{{localize "SW5E.PowerDC"}} {{data.attributes.powerdc}}</h3>
</div> </div>
<ul class="filter-list flexrow" data-filter="powerbook"> <ul class="filter-list flexrow" data-filter="powerbook">
<li class="filter-title">{{localize "SW5E.Filter"}}</li>
<li class="filter-item" data-filter="action">{{localize "SW5E.Action"}}</li> <li class="filter-item" data-filter="action">{{localize "SW5E.Action"}}</li>
<li class="filter-item" data-filter="bonus">{{localize "SW5E.BonusAction"}}</li> <li class="filter-item" data-filter="bonus">{{localize "SW5E.BonusAction"}}</li>
<li class="filter-item" data-filter="reaction">{{localize "SW5E.Reaction"}}</li> <li class="filter-item" data-filter="reaction">{{localize "SW5E.Reaction"}}</li>
<li class="filter-item" data-filter="concentration">{{localize "SW5E.AbbreviationConc"}}</li> <li class="filter-item" data-filter="concentration">{{localize "SW5E.AbbreviationConc"}}</li>
<li class="filter-item" data-filter="ritual">{{localize "SW5E.Ritual"}}</li> <li class="filter-item" data-filter="ritual">{{localize "SW5E.Ritual"}}</li>
<li class="filter-item" data-filter="prepared">{{localize "SW5E.Prepared"}}{{#if preparedPowers}} ({{preparedPowers}}){{/if}}</li>
</ul> </ul>
</div> </div>
@ -47,7 +47,7 @@
</a> </a>
{{/if}} {{/if}}
{{ else }} {{ else }}
<span class="power-slots">{{{section.uses}}}</span> <span>{{{section.uses}}}</span>
<span class="sep"> / </span> <span class="sep"> / </span>
<span class="power-max">{{{section.slots}}}</span> <span class="power-max">{{{section.slots}}}</span>
{{/if}} {{/if}}
@ -72,7 +72,7 @@
<div class="item-name flexrow rollable"> <div class="item-name flexrow rollable">
<div class="item-image" style="background-image: url({{item.img}})"></div> <div class="item-image" style="background-image: url({{item.img}})"></div>
<h4>{{item.name}}</h4> <h4>{{item.name}}</h4>
{{#if item.data.uses.value }} {{#if item.data.uses.per }}
<div class="item-detail power-uses">Uses {{item.data.uses.value}} / {{item.data.uses.max}}</div> <div class="item-detail power-uses">Uses {{item.data.uses.value}} / {{item.data.uses.max}}</div>
{{/if}} {{/if}}
</div> </div>

View file

@ -10,95 +10,115 @@
</select> </select>
</div> </div>
{{#unless isVehicle}}
<div class="form-group {{#unless data.traits.senses}}inactive{{/unless}}"> <div class="form-group {{#unless data.traits.senses}}inactive{{/unless}}">
<label>{{localize "SW5E.Senses"}}</label> <label>{{localize "SW5E.Senses"}}</label>
<input type="text" name="data.traits.senses" value="{{data.traits.senses}}" placeholder="{{ localize 'SW5E.None' }}"/> <input type="text" name="data.traits.senses" value="{{data.traits.senses}}" placeholder="{{ localize 'SW5E.None' }}"/>
</div> </div>
<div class="form-group {{data.traits.languages.cssClass}}"> <div class="form-group {{data.traits.languages.cssClass}}">
<label for="data.traits.languages">{{localize "SW5E.Languages"}}</label> <label>{{localize "SW5E.Languages"}}</label>
<a class="trait-selector" data-options="languages" data-target="data.traits.languages">
<i class="fas fa-edit"></i>
</a>
<ul class="traits-list"> <ul class="traits-list">
{{#each data.traits.languages.selected as |v k|}} {{#each data.traits.languages.selected as |v k|}}
<li class="tag {{k}}">{{v}}</li> <li class="tag {{k}}">{{v}}</li>
{{/each}} {{/each}}
</ul> </ul>
<a class="trait-selector" data-options="languages"><i class="fas fa-edit"></i></a>
</div> </div>
{{/unless}}
<div class="form-group {{data.traits.di.cssClass}}"> <div class="form-group {{data.traits.di.cssClass}}">
<label for="data.traits.di">{{localize "SW5E.DamImm"}}</label> <label>{{localize "SW5E.DamImm"}}</label>
<a class="trait-selector" data-options="damageResistanceTypes" data-target="data.traits.di">
<i class="fas fa-edit"></i>
</a>
<ul class="traits-list"> <ul class="traits-list">
{{#each data.traits.di.selected as |v k|}} {{#each data.traits.di.selected as |v k|}}
<li class="tag {{k}}">{{v}}</li> <li class="tag {{k}}">{{v}}</li>
{{/each}} {{/each}}
</ul> </ul>
<a class="trait-selector" data-options="damageTypes"><i class="fas fa-edit"></i></a>
</div> </div>
<div class="form-group {{data.traits.dr.cssClass}}"> <div class="form-group {{data.traits.dr.cssClass}}">
<label for="data.traits.dr">{{localize "SW5E.DamRes"}}</label> <label>{{localize "SW5E.DamRes"}}</label>
<a class="trait-selector" data-options="damageResistanceTypes" data-target="data.traits.dr">
<i class="fas fa-edit"></i>
</a>
<ul class="traits-list"> <ul class="traits-list">
{{#each data.traits.dr.selected as |v k|}} {{#each data.traits.dr.selected as |v k|}}
<li class="tag {{k}}">{{v}}</li> <li class="tag {{k}}">{{v}}</li>
{{/each}} {{/each}}
</ul> </ul>
<a class="trait-selector" data-options="damageTypes"><i class="fas fa-edit"></i></a>
</div> </div>
<div class="form-group {{data.traits.dv.cssClass}}"> <div class="form-group {{data.traits.dv.cssClass}}">
<label for="data.traits.dv">{{localize "SW5E.DamVuln"}}</label> <label>{{localize "SW5E.DamVuln"}}</label>
<a class="trait-selector" data-options="damageResistanceTypes" data-target="data.traits.dv">
<i class="fas fa-edit"></i>
</a>
<ul class="traits-list"> <ul class="traits-list">
{{#each data.traits.dv.selected as |v k|}} {{#each data.traits.dv.selected as |v k|}}
<li class="tag {{k}}">{{v}}</li> <li class="tag {{k}}">{{v}}</li>
{{/each}} {{/each}}
</ul> </ul>
<a class="trait-selector" data-options="damageTypes"><i class="fas fa-edit"></i></a>
</div> </div>
<div class="form-group {{data.traits.ci.cssClass}}"> <div class="form-group {{data.traits.ci.cssClass}}">
<label for="data.traits.ci">{{localize "SW5E.ConImm"}}</label> <label>{{localize "SW5E.ConImm"}}</label>
<a class="trait-selector" data-options="conditionTypes" data-target="data.traits.ci">
<i class="fas fa-edit"></i>
</a>
<ul class="traits-list"> <ul class="traits-list">
{{#each data.traits.ci.selected as |v k|}} {{#each data.traits.ci.selected as |v k|}}
<li class="tag {{k}}">{{v}}</li> <li class="tag {{k}}">{{v}}</li>
{{/each}} {{/each}}
</ul> </ul>
<a class="trait-selector" data-options="conditionTypes"><i class="fas fa-edit"></i></a>
</div> </div>
{{#if isCharacter}} {{#if isCharacter}}
<div class="form-group {{data.traits.weaponProf.cssClass}}"> <div class="form-group {{data.traits.weaponProf.cssClass}}">
<label for="data.traits.weaponProf">{{localize "SW5E.TraitWeaponProf"}}</label> <label>{{localize "SW5E.TraitWeaponProf"}}</label>
<a class="trait-selector" data-options="weaponProficiencies" data-target="data.traits.weaponProf">
<i class="fas fa-edit"></i>
</a>
<ul class="traits-list"> <ul class="traits-list">
{{#each data.traits.weaponProf.selected as |v k|}} {{#each data.traits.weaponProf.selected as |v k|}}
<li class="tag {{k}}">{{v}}</li> <li class="tag {{k}}">{{v}}</li>
{{/each}} {{/each}}
</ul> </ul>
<a class="trait-selector" data-options="weaponProficiencies"><i class="fas fa-edit"></i></a>
</div> </div>
<div class="form-group {{data.traits.armorProf.cssClass}}"> <div class="form-group {{data.traits.armorProf.cssClass}}">
<label for="data.traits.armorProf">{{localize "SW5E.TraitArmorProf"}}</label> <label>{{localize "SW5E.TraitArmorProf"}}</label>
<a class="trait-selector" data-options="armorProficiencies" data-target="data.traits.armorProf">
<i class="fas fa-edit"></i>
</a>
<ul class="traits-list"> <ul class="traits-list">
{{#each data.traits.armorProf.selected as |v k|}} {{#each data.traits.armorProf.selected as |v k|}}
<li class="tag {{k}}">{{v}}</li> <li class="tag {{k}}">{{v}}</li>
{{/each}} {{/each}}
</ul> </ul>
<a class="trait-selector" data-options="armorProficiencies"><i class="fas fa-edit"></i></a>
</div> </div>
<div class="form-group {{data.traits.toolProf.cssClass}}"> <div class="form-group {{data.traits.toolProf.cssClass}}">
<label for="data.traits.toolProf">{{localize "SW5E.TraitToolProf"}}</label> <label>{{localize "SW5E.TraitToolProf"}}</label>
<a class="trait-selector" data-options="toolProficiencies" data-target="data.traits.toolProf">
<i class="fas fa-edit"></i>
</a>
<ul class="traits-list"> <ul class="traits-list">
{{#each data.traits.toolProf.selected as |v k|}} {{#each data.traits.toolProf.selected as |v k|}}
<li class="tag {{k}}">{{v}}</li> <li class="tag {{k}}">{{v}}</li>
{{/each}} {{/each}}
</ul> </ul>
<a class="trait-selector" data-options="toolProficiencies"><i class="fas fa-edit"></i></a>
</div> </div>
{{/if}} {{/if}}
{{#unless isVehicle}}
<div class="form-group "> <div class="form-group ">
<label>{{localize "SW5E.SpecialTraits"}}</label> <label>{{localize "SW5E.SpecialTraits"}}</label>
<a class="configure-flags"><i class="fas fa-cog"></i></a> <a class="configure-flags"><i class="fas fa-cog"></i></a>
</div> </div>
</div> {{/unless}}
</div>

View file

@ -0,0 +1,159 @@
<form class="{{cssClass}} flexcol" autocomplete="off">
<header class="sheet-header flexrow">
<img class="profile" src="{{actor.img}}" title="{{actor.name}}" alt="{{actor.name}}"
data-edit="img">
<section class="header-details flexrow">
<h1 class="charnam">
<input name="name" type="text" value="{{actor.name}}"
placeholder="{{localize 'SW5E.Name'}}">
</h1>
<ul class="summary flexrow">
<li>
<span>{{lookup config.actorSizes data.traits.size}}</span>
</li>
<li>
<span>{{localize 'SW5E.Vehicle'}}</span>
</li>
<li>
<input type="text" name="data.traits.dimensions"
value="{{data.traits.dimensions}}"
placeholder="{{localize 'SW5E.Dimensions'}}">
</li>
<li>
<input type="text" name="data.details.source"
value="{{data.details.source}}"
placeholder="{{localize 'SW5E.Source'}}">
</li>
</ul>
<ul class="attributes flexrow">
<li class="attribute health">
<h4 class="attribute-name box-title">{{localize 'SW5E.health'}}</h4>
<div class="attribute-value multiple">
<input name="data.attributes.hp.value" type="text" placeholder="&mdash;"
value="{{data.attributes.hp.value}}" data-dtype="Number">
<span class="sep"> &sol; </span>
<input name="data.attributes.hp.max" type="text" placeholder="&mdash;"
value="{{data.attributes.hp.max}}" data-dtype="Number">
</div>
<footer class="attribute-footer">
<input name="data.attributes.hp.dt" type="text" class="temphp"
placeholder="{{localize 'SW5E.Threshold'}}"
value="{{data.attributes.hp.dt}}" data-dtype="Number">
<input name="data.attributes.hp.mt" type="text" class="temphp"
placeholder="{{localize 'SW5E.VehicleMishap'}}"
value="{{data.attributes.hp.mt}}" data-dtype="Number">
</footer>
</li>
<li class="attribute">
<h4 class="attribute-name box-title">{{localize 'SW5E.ArmorClass'}}</h4>
<div class="attribute-value">
<input name="data.attributes.ac.value" type="text" placeholder="&mdash;"
value="{{data.attributes.ac.value}}" data-dtype="Number">
</div>
<footer class="attribute-footer">
<input type="text" name="data.attributes.ac.motionless"
placeholder="&mdash;" value="{{data.attributes.ac.motionless}}">
</footer>
</li>
<li class="attribute">
<h4 class="attribute-name box-title">{{localize 'SW5E.Speed'}}</h4>
<div class="attribute-value">
<input name="data.attributes.speed.value" type="text" placeholder="&mdash;"
value="{{data.attributes.speed.value}}">
</div>
</li>
</ul>
</section>
</header>
<nav class="sheet-navigation tabs" data-group="primary">
<a class="item active" data-tab="attributes">{{localize 'SW5E.Attributes'}}</a>
<a class="item" data-tab="features">{{localize 'SW5E.Features'}}</a>
<a class="item" data-tab="cargo">{{localize 'SW5E.VehicleCargoCrew'}}</a>
<a class="item" data-tab="biography">{{localize 'SW5E.Description'}}</a>
</nav>
<section class="sheet-body">
<div class="tab attributes flexrow" data-group="primary" data-tab="attributes">
<ul class="ability-scores flexrow">
{{#each data.abilities as |ability id|}}
<li class="ability" data-ability="{{id}}">
<h4 class="ability-name box-title rollable">{{ability.label}}</h4>
<input class="ability-score" name="data.abilities.{{id}}.value" type="text"
value="{{ability.value}}" data-dtype="Number" placeholder="0">
<div class="ability-modifiers flexrow">
<span class="ability-mod" title="{{localize 'SW5E.Modifier'}}">
{{numberFormat ability.mod decimals=0 sign=true}}
</span>
</div>
</li>
{{/each}}
</ul>
<section class="center-pane flexcol">
<div class="counters">
<div class="counter flexrow creature-cap">
<h4>{{localize 'SW5E.VehicleCreatureCapacity'}}</h4>
<div class="counter-value">
<input type="text" placeholder="&mdash;"
name="data.attributes.capacity.creature"
value="{{data.attributes.capacity.creature}}">
</div>
</div>
<div class="counter flexrow cargo-cap">
<h4>{{localize 'SW5E.VehicleCargoCapacity'}}</h4>
<div class="counter-value">
<input type="text" name="data.attributes.capacity.cargo" placeholder="0"
data-dtype="Number" value="{{data.attributes.capacity.cargo}}">
</div>
</div>
<div class="counter flexrow stations">
<h4>{{localize 'SW5E.VehicleActionStations'}}</h4>
<div class="counter-value">
<input name="data.attributes.actions.stations" type="checkbox"
data-dtype="Boolean" value="{{data.attributes.actions.stations}}"
{{checked data.attributes.actions.stations}}>
</div>
</div>
<div class="counter flexrow actions">
<h4>{{localize 'SW5E.ActionPl'}}</h4>
<div class="counter-value">
<input type="text" name="data.attributes.actions.value" placeholder="0"
data-dtype="Number" value="{{data.attributes.actions.value}}">
</div>
</div>
<div class="counter flexrow action-thresholds">
<h4>{{localize 'SW5E.VehicleActionThresholds'}}</h4>
<div class="counter-value">
<span class="sep">&lt;</span>
<input type="text" placeholder="&mdash;" data-dtype="Number"
value="{{data.attributes.actions.thresholds.[2]}}"
name="data.attributes.actions.thresholds.2">
<span class="sep">&lt;</span>
<input type="text" placeholder="&mdash;" data-dtype="Number"
value="{{data.attributes.actions.thresholds.[1]}}"
name="data.attributes.actions.thresholds.1">
<span class="sep">&lt;</span>
<input type="text" placeholder="&mdash;" data-dtype="Number"
value="{{data.attributes.actions.thresholds.[0]}}"
name="data.attributes.actions.thresholds.0">
</div>
</div>
</div>
{{> 'systems/sw5e/templates/actors/parts/actor-traits.html'}}
</section>
</div>
<div class="tab features flexcol" data-group="primary" data-tab="features">
{{> 'systems/sw5e/templates/actors/parts/actor-features.html' sections=features}}
</div>
<div class="tab cargo flexcol" data-group="primary" data-tab="cargo">
{{> 'systems/sw5e/templates/actors/parts/actor-inventory.html' sections=cargo}}
</div>
<div class="tab biography flexcol" data-group="primary" data-tab="biography">
{{editor content=data.details.biography.value target='data.details.biography.value'
button=true owner=owner editable=editable}}
</div>
</section>
</form>

View file

@ -1,26 +1,43 @@
<form id="ability-use-form"> <form id="ability-use-form">
<p>{{ localize "SW5E.AbilityUseHint" }} <strong>{{item.name}}</strong> {{ localize "SW5E.Ability" }}.</p> <p>{{ title }}</p>
<p class="notes">{{note}}</p>
<p type="note"> {{#each errors}}
{{#if recharges}} <p class="notification error">{{localize this}}</p>
{{ localize "SW5E.AbilityUseRechargeHint" }} <strong>{{#if isCharged}}{{ localize "SW5E.AbilityUseCharged" }}{{else}}{{ localize "SW5E.AbilityUseDepleted" }}{{/if}}</strong>.</p> {{/each}}
{{else}}
{{ localize "SW5E.AbilityUseWarnStart" }} <strong>{{uses.value}} {{ localize "SW5E.of" }} {{uses.max}}</strong> {{ localize "SW5E.AbilityUseWarnEnd" }} {{perLabel}}.
{{/if}}
{{#unless canUse}}
{{ localize "SW5E.AbilityUseCantUse" }}
{{/unless}}
</p>
{{#if hasPowerSlots}}
<div class="form-group"> <div class="form-group">
<label class="checkbox">{{ localize "SW5E.AbilityUseConsume" }} <input type="checkbox" name="consume" {{checked consume}}/></label> <label>{{ localize "SW5E.PowerCastUpcast" }}</label>
<div class="form-fields">
{{#if hasPlaceableTemplate}} <select name="level" {{#unless canUpcast}}disabled{{/unless}}>
<div class="form-group"> {{#select item.data.level}}
<label class="checkbox">{{ localize "SW5E.PlaceTemplate" }} {{#each powerLevels as |l|}}
<input type="checkbox" name="placeTemplate" checked/> <option value="{{l.level}}" {{#unless l.canCast}}disabled{{/unless}}>{{l.label}}</option>
</label> {{/each}}
{{/select}}
</select>
</div> </div>
{{/if}}
</div> </div>
{{#if canUpcast}}
<div class="form-group">
<label class="checkbox"><input type="checkbox" name="consumeSlot" checked/>{{ localize "SW5E.PowerCastConsume" }}</label>
</div>
{{/if}}
{{/if}}
{{#if hasLimitedUses}}
<div class="form-group">
<label class="checkbox"><input type="checkbox" name="consumeUse" checked/>{{ localize "SW5E.AbilityUseConsume" }}</label>
</div>
{{/if}}
{{#if hasPlaceableTemplate}}
<div class="form-group">
<label class="checkbox">
<input type="checkbox" name="placeTemplate" checked/>
{{ localize "SW5E.PlaceTemplate" }}
</label>
</div>
{{/if}}
</form> </form>

View file

@ -24,7 +24,7 @@
<input type="text" name="{{key}}" value="{{flag.value}}" placeholder="{{flag.placeholder}}" data-dtype="{{flag.type}}"/> <input type="text" name="{{key}}" value="{{flag.value}}" placeholder="{{flag.placeholder}}" data-dtype="{{flag.type}}"/>
{{/if}} {{/if}}
<p class="notes">{{flag.hint}}</p> <p class="notes">{{localize flag.hint}}</p>
</div> </div>
{{/each}} {{/each}}
{{/each}} {{/each}}

View file

@ -0,0 +1,20 @@
<form id="long-rest" class="dialog-content" onsubmit="event.preventDefault();">
<p>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 spell slots.</p>
{{#if promptNewDay}}
<div class="form-group">
<label>Is New Day?</label>
<input type="checkbox" name="newDay" {{checked newDay}}/>
<p class="hint">Recover limited use abilities which recharge "per day"?</p>
</div>
{{/if}}
<div class="dialog-buttons">
{{#each buttons as |button id|}}
<button class="dialog-button" data-button="{{id}}">
{{{button.icon}}}
{{{button.label}}}
</button>
{{/each}}
</div>
</form>

View file

@ -18,6 +18,15 @@
<p class="notes">{{ localize "SW5E.ShortRestNoHD" }}</p> <p class="notes">{{ localize "SW5E.ShortRestNoHD" }}</p>
{{/unless}} {{/unless}}
</div> </div>
{{#if promptNewDay}}
<div class="form-group">
<label>Is New Day?</label>
<input type="checkbox" name="newDay" {{checked newDay}}/>
<p class="hint">Recover limited use abilities which recharge "per day"?</p>
</div>
{{/if}}
<div class="dialog-buttons"> <div class="dialog-buttons">
{{#each buttons as |button id|}} {{#each buttons as |button id|}}
<button class="dialog-button" data-button="{{id}}"> <button class="dialog-button" data-button="{{id}}">

View file

@ -9,9 +9,11 @@
</li> </li>
{{/each}} {{/each}}
</ol> </ol>
{{#if allowCustom}}
<div class="form-group stacked"> <div class="form-group stacked">
<label>{{ localize "SW5E.TraitSelectorSpecial" }}</label> <label>{{ localize "SW5E.TraitSelectorSpecial" }}</label>
<input type="text" name="custom" value="{{custom}}" data-dtype="String"/> <input type="text" name="custom" value="{{custom}}" data-dtype="String"/>
</div> </div>
{{/if}}
<button type="submit" name="submit" value="1"><i class="far fa-save"></i> {{ localize "SW5E.Save"}}</button> <button type="submit" name="submit" value="1"><i class="far fa-save"></i> {{ localize "SW5E.Save"}}</button>
</form> </form>

View file

@ -14,7 +14,7 @@
<span class="item-status">{{itemStatus}}</span> <span class="item-status">{{itemStatus}}</span>
</div> </div>
<ul class="summary"> <ul class="summary flexrow">
<li></li> <li></li>
<li> <li>
<input type="text" name="data.rarity" value="{{data.rarity}}" placeholder="{{ localize 'SW5E.Rarity' }}"/> <input type="text" name="data.rarity" value="{{data.rarity}}" placeholder="{{ localize 'SW5E.Rarity' }}"/>

View file

@ -14,7 +14,7 @@
<span class="item-status">{{itemStatus}}</span> <span class="item-status">{{itemStatus}}</span>
</div> </div>
<ul class="summary"> <ul class="summary flexrow">
<li> <li>
<input type="text" name="data.source" value="{{data.source}}" placeholder="{{ localize 'SW5E.Source' }}"/> <input type="text" name="data.source" value="{{data.source}}" placeholder="{{ localize 'SW5E.Source' }}"/>
</li> </li>
@ -113,10 +113,6 @@
</ul> </ul>
</div> </div>
</div> </div>
{{!-- Granted Abilities (TODO) --}}
<h3 class="form-header">{{ localize "SW5E.GrantedAbilities" }}</h3>
<p class="notification warning">This is still to-do</p>
</div> </div>
</section> </section>
</form> </form>

View file

@ -14,7 +14,7 @@
<span class="item-status">{{itemStatus}}</span> <span class="item-status">{{itemStatus}}</span>
</div> </div>
<ul class="summary"> <ul class="summary flexrow">
<li> <li>
{{lookup config.consumableTypes data.consumableType }} {{lookup config.consumableTypes data.consumableType }}
</li> </li>
@ -73,14 +73,8 @@
{{!-- Item Activation Template --}} {{!-- Item Activation Template --}}
{{> "systems/sw5e/templates/items/parts/item-activation.html"}} {{> "systems/sw5e/templates/items/parts/item-activation.html"}}
{{!-- Consumable Usage --}}
{{#if data.activation.type}} {{#if data.activation.type}}
<div class="form-group"> <div class="form-group">
<label>{{ localize "SW5E.ItemConsumableUsage" }}</label>
<label class="checkbox">
<input type="checkbox" name="data.uses.autoUse" {{checked data.uses.autoUse}}/> {{ localize "SW5E.ItemConsumeOnUse" }}
</label>
<label class="checkbox"> <label class="checkbox">
<input type="checkbox" name="data.uses.autoDestroy" {{checked data.uses.autoDestroy}}/> {{ localize "SW5E.ItemDestroyEmpty" }} <input type="checkbox" name="data.uses.autoDestroy" {{checked data.uses.autoDestroy}}/> {{ localize "SW5E.ItemDestroyEmpty" }}
</label> </label>

View file

@ -14,7 +14,7 @@
<span class="item-status">{{itemStatus}}</span> <span class="item-status">{{itemStatus}}</span>
</div> </div>
<ul class="summary"> <ul class="summary flexrow">
<li> <li>
{{lookup config.equipmentTypes data.armor.type }} {{lookup config.equipmentTypes data.armor.type }}
</li> </li>
@ -57,6 +57,7 @@
</select> </select>
</div> </div>
{{#unless isMountable}}
{{!-- Equipment Status --}} {{!-- Equipment Status --}}
<div class="form-group stacked"> <div class="form-group stacked">
<label>{{ localize "SW5E.ItemEquipmentStatus" }}</label> <label>{{ localize "SW5E.ItemEquipmentStatus" }}</label>
@ -73,33 +74,34 @@
<input type="checkbox" name="data.attuned" {{checked data.attuned}}/> {{ localize "SW5E.Attuned" }} <input type="checkbox" name="data.attuned" {{checked data.attuned}}/> {{ localize "SW5E.Attuned" }}
</label> </label>
</div> </div>
{{/unless}}
{{#unless isWeapon }}
{{!-- ArmorProperties Formula --}} {{#unless isWeapon }}
<div class="form-group stacked weapon-properties"> {{!-- ArmorProperties Formula --}}
<h4 class="armorproperties-header"> <div class="form-group stacked weapon-properties">
{{#unless isWeapon }}{{ localize "SW5E.ArmorProperties" }}{{ else }}{{ localize "SW5E.ItemWeaponProperties" }}{{/unless}} <h4 class="armorproperties-header">
<a class="armorproperties-control add-armorproperties"><i class="fas fa-plus"></i></a> {{#unless isWeapon }}{{ localize "SW5E.ArmorProperties" }}{{ else }}{{ localize "SW5E.ItemWeaponProperties" }}{{/unless}}
</h4> <a class="armorproperties-control add-armorproperties"><i class="fas fa-plus"></i></a>
<ol class="armorproperties-parts form-group"> </h4>
{{#each data.armorproperties.parts as |part i| }} <ol class="armorproperties-parts form-group">
<li class="armorproperties-part flexrow" data-armorproperties-part="{{i}}"> {{#each data.armorproperties.parts as |part i| }}
<li class="armorproperties-part flexrow" data-armorproperties-part="{{i}}">
<select name="data.armorproperties.parts.{{i}}.1">
{{#select (lookup this "1") }} <select name="data.armorproperties.parts.{{i}}.1">
<option value="">{{ localize "SW5E.None" }}</option> {{#select (lookup this "1") }}
{{#each ../config.armorpropertiesTypes as |name type|}} <option value="">{{ localize "SW5E.None" }}</option>
<option value="{{type}}">{{name}}</option> {{#each ../config.armorpropertiesTypes as |name type|}}
{{/each}} <option value="{{type}}">{{name}}</option>
{{/select}} {{/each}}
</select> {{/select}}
<input type="text" name="data.armorproperties.parts.{{i}}.0" value="{{lookup this "0"}}"/> </select>
<a class="armorproperties-control delete-armorproperties"><i class="fas fa-minus"></i></a> <input type="text" name="data.armorproperties.parts.{{i}}.0" value="{{lookup this "0"}}"/>
</li> <a class="armorproperties-control delete-armorproperties"><i class="fas fa-minus"></i></a>
{{/each}} </li>
</ol> {{/each}}
</div> </ol>
{{/unless}} </div>
{{/unless}}
{{!-- Armor Class --}} {{!-- Armor Class --}}
<div class="form-group"> <div class="form-group">
@ -109,6 +111,7 @@
</div> </div>
</div> </div>
{{#unless isMountable}}
{{!-- Dexterity Modifier --}} {{!-- Dexterity Modifier --}}
<div class="form-group"> <div class="form-group">
<label>{{ localize "SW5E.ItemEquipmentDexMod" }}</label> <label>{{ localize "SW5E.ItemEquipmentDexMod" }}</label>
@ -130,6 +133,21 @@
<label>{{ localize "SW5E.ItemEquipmentStealthDisav" }}</label> <label>{{ localize "SW5E.ItemEquipmentStealthDisav" }}</label>
<input type="checkbox" name="data.stealth" value="1" {{checked data.stealth}}/> <input type="checkbox" name="data.stealth" value="1" {{checked data.stealth}}/>
</div> </div>
{{/unless}}
{{#if isMountable}}
{{> 'systems/sw5e/templates/items/parts/item-mountable.html'}}
<div class="form-group">
<label>{{localize 'SW5E.Speed'}}</label>
<div class="form-fields">
<input type="text" name="data.speed.value" value="{{data.speed.value}}"
placeholder="0" data-dtype="Number">
<span class="sep">{{localize 'SW5E.FeetAbbr'}}</span>
<input type="text" name="data.speed.conditions"
value="{{data.speed.conditions}}">
</div>
</div>
{{/if}}
<h3 class="form-header">{{ localize "SW5E.ItemEquipmentUsage" }}</h3> <h3 class="form-header">{{ localize "SW5E.ItemEquipmentUsage" }}</h3>

View file

@ -14,7 +14,7 @@
<span class="item-status">{{itemStatus}}</span> <span class="item-status">{{itemStatus}}</span>
</div> </div>
<ul class="summary"> <ul class="summary flexrow">
<li> <li>
{{labels.featType}} {{labels.featType}}
</li> </li>

View file

@ -14,7 +14,7 @@
<span class="item-status">{{itemStatus}}</span> <span class="item-status">{{itemStatus}}</span>
</div> </div>
<ul class="summary"> <ul class="summary flexrow">
<li> <li>
<input type="text" name="data.rarity" value="{{data.rarity}}" placeholder="{{ localize 'SW5E.Rarity' }}"/> <input type="text" name="data.rarity" value="{{data.rarity}}" placeholder="{{ localize 'SW5E.Rarity' }}"/>
</li> </li>

View file

@ -23,6 +23,21 @@
</div> </div>
</div> </div>
{{#if isCrewed}}
<div class="form-group">
<label>{{localize 'SW5E.Cover'}}</label>
<div class="form-fields">
<select name="data.cover" data-dtype="Number">
{{#select data.cover}}
<option value="">&mdash;</option>
{{#each config.cover as |v k|}}
<option value="{{k}}">{{v}}</option>
{{/each}}
{{/select}}
</select>
</div>
</div>
{{/if}}
{{!-- Ability Target --}} {{!-- Ability Target --}}
<div class="form-group input-select-select"> <div class="form-group input-select-select">
@ -99,4 +114,28 @@
</select> </select>
</div> </div>
</div> </div>
{{/if}}
{{!-- Consumption --}}
<div class="form-group uses-per">
<label>{{ localize "SW5E.ConsumeTitle" }}</label>
<div class="form-fields">
<select name="data.consume.type">
{{#select data.consume.type}}
<option value=""></option>
{{#each config.abilityConsumptionTypes as |name key|}}
<option value="{{key}}">{{name}}</option>
{{/each}}
{{/select}}
</select>
<select name="data.consume.target">
{{#select data.consume.target}}
<option value=""></option>
{{#each abilityConsumptionTargets as |name key|}}
<option value="{{key}}">{{name}}</option>
{{/each}}
{{/select}}
</select>
<input type="text" name="data.consume.amount" value="{{data.consume.amount}}" data-dtype="Number"/>
</div>
</div>
{{/if}}

View file

@ -0,0 +1,19 @@
<div class="form-group">
<label>{{localize 'SW5E.Health'}}</label>
<div class="form-fields">
<input type="text" name="data.hp.value" value="{{data.hp.value}}"
placeholder="0" data-dtype="Number">
<span class="sep">&sol;</span>
<input type="text" name="data.hp.max" value="{{data.hp.max}}" placeholder="0"
data-dtype="Number">
<input type="text" name="data.hp.dt" value="{{data.hp.dt}}" data-dtype="Number"
placeholder="{{localize 'SW5E.Threshold'}}">
</div>
</div>
<div class="form-group">
<label>{{localize 'SW5E.HealthConditions'}}</label>
<div class="form-fields">
<input type="text" name="data.hp.conditions" value="{{data.hp.conditions}}">
</div>
</div>

View file

@ -14,7 +14,7 @@
<span class="item-status">{{itemStatus}}</span> <span class="item-status">{{itemStatus}}</span>
</div> </div>
<ul class="summary"> <ul class="summary flexrow">
<li> <li>
{{labels.level}} {{labels.level}}
</li> </li>

View file

@ -14,7 +14,7 @@
<span class="item-status">{{itemStatus}}</span> <span class="item-status">{{itemStatus}}</span>
</div> </div>
<ul class="summary"> <ul class="summary flexrow">
<li> <li>
<input type="text" name="data.rarity" value="{{data.rarity}}" placeholder="{{ localize 'SW5E.Rarity' }}"/> <input type="text" name="data.rarity" value="{{data.rarity}}" placeholder="{{ localize 'SW5E.Rarity' }}"/>
</li> </li>

View file

@ -14,7 +14,7 @@
<span class="item-status">{{itemStatus}}</span> <span class="item-status">{{itemStatus}}</span>
</div> </div>
<ul class="summary"> <ul class="summary flexrow">
<li> <li>
{{lookup config.weaponTypes data.weaponType }} {{lookup config.weaponTypes data.weaponType }}
</li> </li>
@ -56,22 +56,26 @@
</select> </select>
</div> </div>
{{#unless isMountable}}
{{!-- Weapon Status --}} {{!-- Weapon Status --}}
<div class="form-group stacked"> <div class="form-group stacked">
<label>{{ localize "SW5E.ItemWeaponStatus" }}</label> <label>{{ localize "SW5E.ItemWeaponStatus" }}</label>
<label class="checkbox"> <div class="form-fields">
<input type="checkbox" name="data.proficient" {{checked data.proficient}}/> {{ localize "SW5E.Proficient" }} <label class="checkbox">
</label> <input type="checkbox" name="data.proficient" {{checked data.proficient}}/> {{ localize "SW5E.Proficient" }}
<label class="checkbox"> </label>
<input type="checkbox" name="data.equipped" {{checked data.equipped}}/> {{ localize "SW5E.Equipped" }} <label class="checkbox">
</label> <input type="checkbox" name="data.equipped" {{checked data.equipped}}/> {{ localize "SW5E.Equipped" }}
<label class="checkbox"> </label>
<input type="checkbox" name="data.identified" {{checked data.identified}}/> {{ localize "SW5E.Identified" }} <label class="checkbox">
</label> <input type="checkbox" name="data.identified" {{checked data.identified}}/> {{ localize "SW5E.Identified" }}
<label class="checkbox"> </label>
<input type="checkbox" name="data.attuned" {{checked data.attuned}}/> {{ localize "SW5E.Attuned" }} <label class="checkbox">
</label> <input type="checkbox" name="data.attuned" {{checked data.attuned}}/> {{ localize "SW5E.Attuned" }}
</label>
</div>
</div> </div>
{{/unless}}
{{#if sss}} {{#if sss}}
{{!-- Weapon Properties --}} {{!-- Weapon Properties --}}
@ -85,33 +89,44 @@
</div> </div>
{{/if}} {{/if}}
{{#if isWeapon }} {{#if isWeapon }}
<div class="form-group stacked weapon-properties"> <div class="form-group stacked weapon-properties">
{{!-- weaponproperties Formula --}} {{!-- weaponproperties Formula --}}
<h4 class="weaponproperties-header"> <h4 class="weaponproperties-header">
{{#unless isWeapon }}{{ localize "SW5E.ArmorProperties" }}{{ else }}{{ localize "SW5E.ItemWeaponProperties" }}{{/unless}} {{#unless isWeapon }}{{ localize "SW5E.ArmorProperties" }}{{ else }}{{ localize "SW5E.ItemWeaponProperties" }}{{/unless}}
<a class="weaponproperties-control add-weaponproperties"><i class="fas fa-plus"></i></a> <a class="weaponproperties-control add-weaponproperties"><i class="fas fa-plus"></i></a>
</h4> </h4>
<ol class="weaponproperties-parts form-group"> <ol class="weaponproperties-parts form-group">
{{#each data.weaponproperties.parts as |part i| }} {{#each data.weaponproperties.parts as |part i| }}
<li class="weaponproperties-part flexrow" data-weaponproperties-part="{{i}}"> <li class="weaponproperties-part flexrow" data-weaponproperties-part="{{i}}">
<select name="data.weaponproperties.parts.{{i}}.1"> <select name="data.weaponproperties.parts.{{i}}.1">
{{#select (lookup this "1") }} {{#select (lookup this "1") }}
<option value="">{{ localize "SW5E.None" }}</option> <option value="">{{ localize "SW5E.None" }}</option>
{{#each ../config.weaponProperties as |name type|}} {{#each ../config.weaponProperties as |name type|}}
<option value="{{type}}">{{name}}</option> <option value="{{type}}">{{name}}</option>
{{/each}} {{/each}}
{{/select}} {{/select}}
</select> </select>
<input type="text" name="data.weaponproperties.parts.{{i}}.0" value="{{lookup this "0"}}"/> <input type="text" name="data.weaponproperties.parts.{{i}}.0" value="{{lookup this "0"}}"/>
<a class="weaponproperties-control delete-weaponproperties"><i class="fas fa-minus"></i></a> <a class="weaponproperties-control delete-weaponproperties"><i class="fas fa-minus"></i></a>
</li> </li>
{{/each}} {{/each}}
</ol> </ol>
{{/if}} </div>
{{/if}}
</div> {{#if isMountable}}
<div class="form-group">
<label>{{localize 'SW5E.ArmorClass'}}</label>
<div class="form-fields">
<input type="text" name="data.armor.value" value="{{data.armor.value}}"
data-dtype="Number">
</div>
</div>
{{> 'systems/dnd5e/templates/items/parts/item-mountable.html'}}
{{/if}}
<h3 class="form-header">{{ localize "SW5E.ItemWeaponUsage" }}</h3> <h3 class="form-header">{{ localize "SW5E.ItemWeaponUsage" }}</h3>