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.AbbreviationDC": "DC",
"SW5E.AbbreviationLR": "LR",
"SW5E.AbbreviationLevel": "Lvl.",
"SW5E.AbbreviationLbs": "lbs.",
"SW5E.AbbreviationSR": "SR",
"SW5E.Ability": "Ability",
"SW5E.AbilityCha": "Charisma",
"SW5E.AbilityCon": "Constitution",
"SW5E.AbilityDex": "Dexterity",
"SW5E.AbilityInt": "Intelligence",
"SW5E.AbilityModifier": "Ability Modifier",
"SW5E.AbilityStr": "Strength",
"SW5E.AbilityUseCantUse": "You are not currently able to use this ability!",
"SW5E.AbilityUseCharged": "charged",
"SW5E.AbilityUseConsume": "Consume Available Usage?",
"SW5E.AbilityUseDepleted": "depleted",
"SW5E.AbilityUseHint": "Configure how you would like to use the",
"SW5E.AbilityUseRechargeHint": "This ability uses a recharge mechanic and is currently",
"SW5E.AbilityUseWarnEnd": "available uses per",
"SW5E.AbilityUseWarnStart": "This ability has",
"SW5E.AbilityStrAbbr": "str",
"SW5E.AbilityCon": "Constitution",
"SW5E.AbilityConAbbr": "con",
"SW5E.AbilityDex": "Dexterity",
"SW5E.AbilityDexAbbr": "dex",
"SW5E.AbilityInt": "Intelligence",
"SW5E.AbilityIntAbbr": "int",
"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.ActionPl": "Actions",
"SW5E.ActionAbil": "Ability Check",
"SW5E.ActionHeal": "Healing",
"SW5E.ActionMPAK": "Melee Power Attack",
@ -33,6 +49,8 @@
"SW5E.ActionRWAK": "Ranged Weapon Attack",
"SW5E.ActionSave": "Saving Throw",
"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.Advantage": "Advantage",
"SW5E.Alignment": "Alignment",
@ -44,9 +62,10 @@
"SW5E.AlignmentLN": "Lawful Neutral",
"SW5E.AlignmentND": "Neutral Dark",
"SW5E.AlignmentNL": "Neutral Light",
"SW5E.AlignmentBN": "Balenced Neutral",
"SW5E.AlignmentBN": "Balanced Neutral",
"SW5E.Archetypes": "Archetypes",
"SW5E.ArmorClass": "Armor Class",
"SW5E.AC": "AC",
"SW5E.ArmorProperties": "Armor Properties",
"SW5E.ArmorProperAbsorptive": "Absorptive",
"SW5E.ArmorProperAgile": "Agile",
@ -76,6 +95,8 @@
"SW5E.ArmorProperSteadfast": "Steadfast",
"SW5E.ArmorProperVersatile": "Versatile",
"SW5E.Attack": "Attack",
"SW5E.AttackPl": "Attacks",
"SW5E.AttackRoll": "Attack Roll",
"SW5E.Attributes": "Attributes",
"SW5E.Attuned": "Attuned",
"SW5E.Background": "Background",
@ -84,12 +105,12 @@
"SW5E.BonusAbilitySave": "Global Saving Throw Bonus",
"SW5E.BonusAbilitySkill": "Global Skill Check Bonus",
"SW5E.BonusAction": "Bonus Action",
"SW5E.BonusMSAttack": "Melee Power Attack Bonus",
"SW5E.BonusMSDamage": "Melee Power Damage Bonus",
"SW5E.BonusMPAttack": "Melee Power Attack Bonus",
"SW5E.BonusMPDamage": "Melee Power Damage Bonus",
"SW5E.BonusMWAttack": "Melee Weapon Attack Bonus",
"SW5E.BonusMWDamage": "Melee Weapon Damage Bonus",
"SW5E.BonusRSAttack": "Ranged Power Attack Bonus",
"SW5E.BonusRSDamage": "Ranged Power Damage Bonus",
"SW5E.BonusRPAttack": "Ranged Power Attack Bonus",
"SW5E.BonusRPDamage": "Ranged Power Damage Bonus",
"SW5E.BonusRWAttack": "Ranged Weapon Attack Bonus",
"SW5E.BonusRWDamage": "Ranged Weapon Damage Bonus",
"SW5E.BonusSaveForm": "Update Bonuses",
@ -116,6 +137,7 @@
"SW5E.ConBlinded": "Blinded",
"SW5E.ConCharmed": "Charmed",
"SW5E.ConDeafened": "Deafened",
"SW5E.ConDiseased": "Diseased",
"SW5E.ConExhaustion": "Exhaustion",
"SW5E.ConFrightened": "Frightened",
"SW5E.ConGrappled": "Grappled",
@ -128,9 +150,22 @@
"SW5E.ConProne": "Prone",
"SW5E.ConRestrained": "Restrained",
"SW5E.ConShocked": "Shocked",
"SW5E.ConSlowed": "Slowed",
"SW5E.ConStunned": "Stunned",
"SW5E.ConUnconscious": "Unconscious",
"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.ConsumablePoison": "Poison",
"SW5E.ConsumableAdrenal": "Adrenal",
@ -139,8 +174,17 @@
"SW5E.ConsumableTrinket": "Trinket",
"SW5E.ConsumableTechnology": "Technology",
"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.CostGP": "Cost (GP)",
"SW5E.Cover": "Cover",
"SW5E.CoverHalf": "Half",
"SW5E.CoverThreeQuarters": "Three Quarters",
"SW5E.CoverTotal": "Total",
"SW5E.CostGP": "Cost (CR)",
"SW5E.Critical": "Critical",
"SW5E.CriticalHit": "Critical Hit",
"SW5E.Currency": "Currency",
@ -160,15 +204,22 @@
"SW5E.DamageKinetic": "Kinetic",
"SW5E.DamageLightning": "Lightning",
"SW5E.DamageNecrotic": "Necrotic",
"SW5E.DamagePhysical": "Physical",
"SW5E.DamagePoison": "Poison",
"SW5E.DamagePsychic": "Psychic",
"SW5E.DamageSonic": "Sonic",
"SW5E.Day": "Day",
"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.DefaultAbilityCheck": "Default Ability Check",
"SW5E.Description": "Description",
"SW5E.Details": "Details",
"SW5E.Dimensions": "Dimensions",
"SW5E.Disadvantage": "Disadvantage",
"SW5E.DistAny": "Any",
"SW5E.DistFt": "Feet",
@ -185,6 +236,7 @@
"SW5E.EquipmentShield": "Shield",
"SW5E.EquipmentShieldProficiency": "Shields",
"SW5E.EquipmentTrinket": "Trinket",
"SW5E.EquipmentVehicle": "Vehicle Equipment",
"SW5E.Equipped": "Equipped",
"SW5E.Exhaustion": "Exhaustion",
"SW5E.Expertise": "Expertise",
@ -208,6 +260,7 @@
"SW5E.ItemTypePowerPl": "Powers",
"SW5E.ItemTypeWeapon": "Weapon",
"SW5E.ItemTypeWeaponPl": "Weapons",
"SW5E.ItemNoUses": "{name} has no available uses remaining.",
"SW5E.FeatureActive": "Active Abilities",
"SW5E.FeatureAdd": "Create Feature",
@ -217,12 +270,37 @@
"SW5E.FeatureRechargeResult": "1d6 Result",
"SW5E.FeatureUsage": "Feature Usage",
"SW5E.Features": "Features",
"SW5E.FeetAbbr": "ft.",
"SW5E.Filter": "Filter",
"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.FlagsInstructions": "Configure character features and traits which fine-tune behaviors of the SW5e system.",
"SW5E.FlagsSave": "Update 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.Formula": "Formula",
"SW5E.GrantedAbilities": "Granted Abilities",
@ -230,9 +308,12 @@
"SW5E.Healing": "Healing",
"SW5E.HealingTemp": "Healing (Temporary)",
"SW5E.Health": "Health",
"SW5E.HealthConditions": "Health Conditions",
"SW5E.HealthFormula": "Health Formula",
"SW5E.HitDice": "Hit Dice",
"SW5E.HitDiceRoll": "Roll Hit Dice",
"SW5E.HitDiceUsed": "Hit Dice Used",
"SW5E.HitDiceWarn": "{name} has no available {formula} Hit Dice remaining!",
"SW5E.Identified": "Identified",
"SW5E.Initiative": "Initiative",
"SW5E.Inspiration": "Inspiration",
@ -242,12 +323,10 @@
"SW5E.ItemActivationCost": "Activation Cost",
"SW5E.ItemAttackBonus": "Attack Roll Bonus",
"SW5E.ItemConsumableActivation": "Consumable Activation",
"SW5E.ItemConsumableUsage": "Consumable Usage",
"SW5E.ItemConsumableDetails": "Consumable Details",
"SW5E.ItemConsumableStatus": "Consumable Status",
"SW5E.ItemConsumableType": "Consumable Type",
"SW5E.ItemConsumableUsage": "Consumable Usage",
"SW5E.ItemConsumeOnUse": "Consume on Use",
"SW5E.ItemContainerCapacity": "Capacity",
"SW5E.ItemContainerCapacityItems": "Items",
"SW5E.ItemContainerCapacityType": "Capacity Type",
@ -270,6 +349,10 @@
"SW5E.ItemEquipmentUsage": "Equipment Usage",
"SW5E.ItemName": "Item Name",
"SW5E.ItemNew": "New {type}",
"SW5E.ItemRechargeCheck": "{name} recharge check",
"SW5E.ItemRechargeFailure": "failure!",
"SW5E.ItemRechargeSuccess": "success!",
"SW5E.ItemRequiredStr": "Required Strength",
"SW5E.ItemToolProficiency": "Tool Proficiency",
@ -281,7 +364,8 @@
"SW5E.ItemWeaponType": "Weapon Type",
"SW5E.ItemWeaponUsage": "Weapon Usage",
"SW5E.JackOfAllTrades": "Jack of all Trades",
"SW5E.LairAct": "Lair Action",
"SW5E.LairAct": "Uses Lair Action",
"SW5E.LairActionLabel": "Lair Action",
"SW5E.Languages": "Languages",
"SW5E.LanguagesAleena": "Aleena",
"SW5E.LanguagesAntarian": "Antarian",
@ -383,12 +467,18 @@
"SW5E.LanguagesYevethan": "Yevethan",
"SW5E.LanguagesZabraki": "Zabraki",
"SW5E.LanguagesZygerrian": "Zygerrian",
"SW5E.LegAct": "Legd. Actions",
"SW5E.LegRes": "Legd. Resistance",
"SW5E.LegAct": "Legendary Actions",
"SW5E.LegendaryActionLabel": "Legendary Action",
"SW5E.LegRes": "Legendary Resistance",
"SW5E.Level": "Level",
"SW5E.LevelScaling": "Level Scaling",
"SW5E.LimitedUses": "Limited Uses",
"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.Modifier": "Modifier",
"SW5E.Name": "Character Name",
@ -397,6 +487,8 @@
"SW5E.Normal": "Normal",
"SW5E.NotProficient": "Not Proficient",
"SW5E.OtherFormula": "Other Formula",
"SW5E.PactMagic": "Pact Magic",
"SW5E.Passive": "Passive",
"SW5E.PlaceTemplate": "Place Measured Template",
"SW5E.Polymorph": "Polymorph",
"SW5E.PolymorphAcceptSettings": "Custom Settings",
@ -414,10 +506,12 @@
"SW5E.PolymorphMergeSkills": "Merge skill proficiencies (take the highest)",
"SW5E.PolymorphPromptTitle": "Transforming Actor",
"SW5E.PolymorphRestoreTransformation": "Restore Transformation",
"SW5E.PolymorphRevertWarn": "You do not have permission to revert this Actor's polymorphed state.",
"SW5E.PolymorphTmpClass": "Temporary Class",
"SW5E.PolymorphTokens": "Transform all linked tokens?",
"SW5E.PolymorphWarn": "You are not allowed to polymorph this actor!",
"SW5E.PolymorphWildShape": "Wild Shape",
"SW5E.Concentrated": "Concentrate",
"SW5E.Concentrate": "Concentrate",
"SW5E.Price": "Price",
"SW5E.Proficiency": "Proficiency",
"SW5E.Proficient": "Proficient",
@ -426,6 +520,8 @@
"SW5E.Range": "Range",
"SW5E.Rarity": "Rarity",
"SW5E.Reaction": "Reaction",
"SW5E.ReactionPl": "Reactions",
"SW5E.Recharge": "Recharge",
"SW5E.RequiredMaterials": "Required Materials",
"SW5E.Requirements": "Requirements",
"SW5E.ResourcePrimary": "Resource 1",
@ -438,7 +534,8 @@
"SW5E.RollExample": "e.g. +1d4",
"SW5E.RollMode": "Roll Mode",
"SW5E.RollSituationalBonus": "Situational Bonus?",
"SW5E.Save": "Save",
"SW5E.SavingThrow": "Saving Throw",
"SW5E.SavePromptTitle": "{ability} Saving Throw",
"SW5E.ScalingFormula": "Scaling Formula",
"SW5E.SchoolLgt": "Light",
"SW5E.SchoolUni": "Universal",
@ -451,8 +548,13 @@
"SW5E.SenseTS": "Tremorsense",
"SW5E.Senses": "Senses",
"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.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.Size": "Size",
"SW5E.SizeGargantuan": "Gargantuan",
@ -479,6 +581,7 @@
"SW5E.SkillSte": "Stealth",
"SW5E.SkillSur": "Survival",
"SW5E.SkillTec": "Technology",
"SW5E.SkillPromptTitle": "{skill} Skill Check",
"SW5E.Slots": "Slots",
"SW5E.Source": "Source",
"SW5E.Special": "Special",
@ -511,7 +614,8 @@
"SW5E.PowerLevel7": "7th Level",
"SW5E.PowerLevel8": "8th 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.PowerName": "Power Name",
"SW5E.PowerNone": "None",
@ -532,6 +636,7 @@
"SW5E.Powerbook": "Powerbook",
"SW5E.SpeciesDescription": "Description",
"SW5E.SpeciesTraits": "Species Traits",
"SW5E.StealthDisadvantage": "Stealth Disadvantage",
"SW5E.SubclassName": "Subclass Name",
"SW5E.Supply": "Supply",
"SW5E.Target": "Target",
@ -552,6 +657,7 @@
"SW5E.TargetWall": "Wall",
"SW5E.TargetWeapon": "Weapon",
"SW5E.Temp": "Temp",
"SW5E.Threshold": "Threshold",
"SW5E.TimeDay": "Days",
"SW5E.TimeHour": "Hours",
"SW5E.TimeInst": "Instantaneous",
@ -567,6 +673,7 @@
"SW5E.ToolArtist": "Artist's Tools",
"SW5E.ToolAstrotech": "Astrotech's Tools",
"SW5E.ToolBiotech": "Biotech's Tools",
"SW5E.ToolCheck": "Tool Check",
"SW5E.ToolConstructor": "Constructor's Tools",
"SW5E.ToolCybertech": "Cybertech's Tools",
"SW5E.ToolJeweler": "Jeweler's Tools",
@ -593,6 +700,7 @@
"SW5E.ToolMusicalInstrument": "Musical Instrument",
"SW5E.ToolVehicle": "Vehicle (Land or Water)",
"SW5E.TraitArmorProf": "Armor Proficiencies",
"SW5E.TraitSave": "Update",
"SW5E.TraitSelectorSpecial": "Special (Split with Semi-Colon)",
"SW5E.TraitToolProf": "Tool Proficiencies",
"SW5E.TraitWeaponProf": "Weapon Proficiencies",
@ -602,6 +710,20 @@
"SW5E.Usage": "Usage",
"SW5E.Use": "Use",
"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.VersatileDamage": "Versatile Damage",
"SW5E.VsDC": "vs DC.",
@ -648,7 +770,7 @@
"SW5E.WeaponSimpleLW": "Simple Lightweapon",
"SW5E.Weight": "Weight",
"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.power": "power",
"SETTINGS.5eAllowPolymorphingL": "Allow players to polymorph their own actors.",
@ -659,12 +781,19 @@
"SETTINGS.5eAutoPowerTemplateN": "Always place Power Template",
"SETTINGS.5eCurWtL": "Carried currency affects character encumbrance following the rules on PHB pg. 143.",
"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.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.5eInitTBN": "Initiative Dexterity Tiebreaker",
"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";
@actorNameHeight: 60px;
.sw5e.sheet.actor {
/* ----------------------------------------- */
/* Sheet Header */
/* ----------------------------------------- */
.sheet-header {
.charlevel {
// Portrait Artwork
img.profile {
flex: 0 0 160px;
height: 60px;
margin: 0;
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;
}
}
max-width: 160px;
height: 160px;
}
// Character Name
h1.charname {
flex: 1;
height: @actorNameHeight;
padding: 0;
input {
height: @actorNameHeight - 20px;
margin: 10px 0;
}
}
.experience {
width: 100%;
height: 20px;
padding-right: 5px;
font-size: 16px;
color: @colorOlive;
// Character Level
.header-exp {
flex: 0 0 150px;
margin-right: 3px;
height: @actorNameHeight;
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
h4.box-title {
height: 18px;
line-height: 16px;
margin: 4px 8px 2px;
.modesto();
font-size: 18px;
border-bottom: 1px solid @colorBeige;
color: @colorOlive;
border-bottom: 1px solid @colorFaint;
}
/* ----------------------------------------- */
/* Attributes */
/* ----------------------------------------- */
.tab.attributes {
overflow: hidden;
}
ul.attributes {
flex: 0 0 60px;
list-style: none;
margin: 5px 0 0;
margin: 0;
padding: 0;
li.attribute {
height: 70px;
margin: 0 5px;
height: 60px;
margin: 0 5px 0 0;
border: @borderGroove;
border-radius: 4px;
.nodesto();
text-align: center;
.attribute-name {
flex: 0 0 18px;
&:last-child {
margin: 0;
}
.attribute-value {
display: flex;
justify-content: center;
align-items: center;
height: 28px;
line-height: 28px;
.modesto();
input {
display: inline;
max-width: 80%;
height: 28px;
margin: 0;
> * {
font-weight: 400;
font-size: 24px;
}
span.sep {
display: inline;
position: relative;
top: 2px;
font-size: 28px;
color: @colorTan;
}
&.multiple {
input { max-width: 33%; }
&.multiple input {
flex: 0 0 33%;
}
}
@ -125,8 +140,9 @@
flex: 0 0 18px;
margin-top: -1px;
line-height: 18px;
font-family: "Signika", "Palatino Linotype", serif;
font-family: "Signika", sans-serif;
font-size: 12px;
font-weight: 400;
}
}
}
@ -136,22 +152,28 @@
/* ----------------------------------------- */
.ability-scores {
flex: 0 0 100%;
flex: 0 0 100px;
height: 440px;
list-style: none;
margin: 0;
padding: 0;
.nodesto();
.modesto();
border: @borderGroove;
border-radius: 3px;
.ability {
height: 70px;
margin: 0 5px;
text-align: center;
border: @borderGroove;
border-radius: 3px;
border-bottom: @borderGroove;
&:last-child {
border-bottom: none;
margin-bottom: -3px;
}
input.ability-score {
height: 30px;
width: 50px;
width: 36px;
margin: 0 auto;
line-height: 32px;
font-size: 24px;
@ -159,12 +181,14 @@
.ability-modifiers {
height: 24px;
margin: -10px 0 0;
margin: -8px 0 0;
span.ability-mod,
span.ability-save {
flex: 0 0 24px;
height: 24px;
height: 22px;
line-height: 22px;
font-size: 16px;
border-top: @borderGroove;
}
@ -180,12 +204,6 @@
border-left: @borderGroove;
}
}
/* Hide modifier box on hover */
input.ability-score:hover + .ability-modifiers {
visibility: hidden;
}
}
}
@ -209,16 +227,21 @@
/* ----------------------------------------- */
ul.skills-list {
flex: 0 0 192px;
flex: 0 0 180px;
height: 440px;
list-style: none;
margin: 5px 5px 0;
padding: 2px 2px 0;
margin: 0 5px 0;
padding: 3px 0 2px;
border: @borderGroove;
border-radius: 3px;
li.skill {
height: 22px;
padding: 3px 0;
height: 24px;
padding: 3px 2px;
&:nth-child(even) {
background: rgba(0, 0, 0, 0.05);
}
h4 {
flex: 1px;
@ -234,6 +257,7 @@
.skill-ability {
flex: 0 0 26px;
text-transform: capitalize;
}
.skill-mod {
@ -253,23 +277,25 @@
/* ----------------------------------------- */
.counters {
flex: 0 0 100%;
flex: none;
padding: 5px 0;
margin: 0;
border-bottom: @borderGroove;
margin-bottom: 5px;
.counter {
padding: 0 3px;
line-height: 32px;
height: 20px;
line-height: 20px;
h4 {
flex: auto;
margin: 0;
.nodesto();
font-size: 14px;
font-size: 13px;
font-weight: bold;
color: @colorOlive;
}
.counter-value {
flex: 0 0 50px;
flex: none;
text-align: right;
> * {
display: inline;
@ -286,12 +312,13 @@
input[type="checkbox"] {
position: relative;
width: 16px;
height: 16px;
margin: 0;
top: 6px;
top: 4px;
}
span.sep {
margin: 0 -2px;
font-size: 12px;
}
}
@ -301,28 +328,31 @@
/* Traits */
/* ----------------------------------------- */
.traits {
margin: 0 5px;
.center-pane {
height: 100%;
padding: 0 5px 0 3px;
overflow-y: auto;
scrollbar-width: thin;
}
.traits {
.form-group, .form-group-stacked {
margin: 0 0 4px 0;
margin: 0 0 3px 0;
justify-content: space-between;
}
.configure-flags {
flex: 1;
}
.actor-size {
flex: 0 0 150px;
}
label {
flex: 0 0 150px;
flex: none;
line-height: 20px;
font-weight: bold;
margin: 0;
margin: 0 10px 0 0;
}
select {
max-width: 200px;
}
input {
@ -360,18 +390,10 @@
.inventory-filters {
margin: 0 8px;
flex: 0 0 20px;
h3, .filter-title {
.nodesto();
color: @colorOlive;
font-size: 18px;
margin: 0;
}
&.powerbook-filters {
flex: 0 0 40px;
}
justify-content: flex-end;
.currency {
flex: 0 0 100%;
list-style: none;
margin: 4px 0 8px;
padding: 0;
@ -398,6 +420,7 @@
margin: 0;
padding: 0 5px;
overflow-y: auto;
scrollbar-width: thin;
// Inventory Item
.item {
@ -424,12 +447,12 @@
overflow-x: hidden;
}
&.rollable .item-image:hover {
background-image: url("/icons/svg/d20-black.svg") !important;
}
&.rollable:hover .item-image {
background-image: url("/icons/svg/d20-grey.svg") !important;
}
&.rollable .item-image:hover {
background-image: url("/icons/svg/d20-black.svg") !important;
}
i.attuned {
color: @colorTan;
@ -453,6 +476,7 @@
text-align: right;
font-size: 11px;
color: @colorTan;
white-space: nowrap;
}
}
@ -468,8 +492,8 @@
h3 {
margin: 0 -5px 0 0;
padding-left: 5px;
font-size: 13px;
font-weight: bold;
.modesto();
font-size: 16px;
}
.item-controls a.item-create {
@ -484,6 +508,10 @@
color: @colorTan;
text-align: center;
border-right: 1px solid @colorFaint;
word-break: break-word;
white-space: nowrap;
overflow: hidden;
&:last-child { border-right: none; }
&.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 */
/* ----------------------------------------- */
.powercasting-ability {
h3 {
flex: none;
margin-right: 5px;
}
flex: 0 0 240px;
margin: 0;
input, span {
flex: 0 0 32px;
text-align: center;
@ -549,7 +639,7 @@
.power-slots,
.power-comps {
flex: 0 0 72px;
flex: 0 0 75px;
padding-right: 5px;
text-align: right;
font-size: 12px;
@ -558,10 +648,13 @@
}
.power-slots {
input, span.sep {
input {
display: inline;
max-width: 20px;
text-align: center;
}
.sep {
font-size: 13px;
font-weight: normal;
}
}

View file

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

View file

@ -4,8 +4,8 @@
/* Basic Structure */
/* ----------------------------------------- */
.sw5e.sheet.actor.character {
min-width: 680px;
min-height: 736px;
min-width: 720px;
min-height: 680px;
/* ----------------------------------------- */
/* Sheet Header */
@ -13,93 +13,73 @@
.sheet-header {
// Character Profile image (larger than usual)
// Character Profile image (larger)
img.profile {
flex: 0 0 180px;
max-width: 180px;
height: 180px;
flex: 0 0 160px;
max-width: 160px;
height: 160px;
}
// Character level and experience bar
// Character Level
.charlevel {
flex: 0 0 180px;
padding: 0 5px 2px;
flex: 0 0 20px;
height: 20px;
font-size: 18px;
color: @colorTan;
white-space: nowrap;
}
.level {
height: 28px;
// Experience Tracking
.experience {
flex: 0 0 32px;
margin-bottom: -5px;
align-items: center;
font-size: 18px;
span.max {
color: @colorTan;
flex: none;
margin-left: 3px;
}
.experience {
input[type="text"] {
width: 100px;
}
}
.xpbar {
width: 100%;
flex: 0 0 8px;
background: #666;
}
.xpbar {
flex: 0 0 8px;
width: 100%;
margin-bottom: 5px;
background: @colorTan;
border: 1px solid #000;
border-radius: 3px;
.bar {
height: 4px;
margin: 1px;
display: block;
background: #afebff;
border: 1px solid #000;
border-radius: 3px;
.bar {
height: 4px;
margin: 1px;
display: block;
background: #afebff;
border: 1px solid #000;
border-radius: 2px;
}
border-radius: 2px;
}
}
// Character Summary
.summary {
border-bottom: @borderGroove;
}
// Primary Attributes
// Header 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 {
border: 1px solid @colorBeige;
border-radius: 2px;
background: rgba(0, 0, 0, 0.05);
padding: 1px 3px;
margin: 0 6px;
padding: 0 3px;
margin: 0 3px;
}
.hit-dice {
font-size: 24px;
}
.initiative .attribute-footer input {
width: 32px;
}
}
.summary .proficiency {
text-align: right;
padding-right: 5px;
}
}
@ -107,26 +87,24 @@
/* Sheet Body */
/* ----------------------------------------- */
.attributes {
.resource {
.attribute-name {
margin: 0 8px;
input[type="text"] {
height: 20px;
margin: 2px 0 -2px;
line-height: 24px;
}
}
label.checkbox {
margin: 0 3px;
input[type="checkbox"] {
transform: scale(1.2);
}
}
// Custom Resources
.resource .attribute-value {
input {
flex: 0 0 25%;
}
.initiative .attribute-footer input {
width: 32px;
label.recharge {
height: 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 {
flex: 0 0 180px;
text-align: right;
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 */
/* ----------------------------------------- */

View file

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

View file

@ -192,7 +192,26 @@
text-align: right;
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 {
margin: 0;
padding: 0;
@ -263,16 +282,6 @@
color: @colorTan;
}
.recharge {
span {
flex: 0 0 80px;
}
label.checkbox {
flex: 0 0 80px;
text-align: right;
}
}
/* ----------------------------------------- */
/* Item Actions */
/* ----------------------------------------- */

View file

@ -5,5 +5,37 @@
/* ----------------------------------------- */
.sw5e.sheet.actor.npc {
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 "character.less";
@import "npc.less";
@import "vehicle.less";

View file

@ -7,13 +7,41 @@
/* Fonts */
/* ----------------------------------------- */
/* russo-one-regular - latin */
@font-face {
font-family: "Nodesto";
src: url("fonts/NodestoCapsCondensed.otf");
font-family: 'Russo One';
font-style: normal;
font-weight: 400;
src: url('./fonts/RussoOne.ttf');
}
.nodesto {
font-family: "Nodesto", "Signika", "Palatino Linotype", serif;
.russoOne {
font-family: 'Russo One';
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 {ActorSheetFlags} from "../../apps/actor-flags.js";
import Item5e from "../../item/entity.js";
import TraitSelector from "../../apps/trait-selector.js";
import ActorSheetFlags from "../../apps/actor-flags.js";
import {SW5E} from '../../config.js';
/**
* Extend the basic ActorSheet class to do all the SW5e things!
* This sheet is an Abstract layer which is not used.
*
* @type {ActorSheet}
* @extends {ActorSheet}
*/
export class ActorSheet5e extends ActorSheet {
export default class ActorSheet5e extends ActorSheet {
constructor(...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",
isCharacter: this.entity.data.type === "character",
isNPC: this.entity.data.type === "npc",
isVehicle: this.entity.data.type === 'vehicle',
config: CONFIG.SW5E,
};
@ -75,11 +83,13 @@ export class ActorSheet5e extends ActorSheet {
}
// Update skill labels
for ( let [s, skl] of Object.entries(data.actor.data.skills)) {
skl.ability = data.actor.data.abilities[skl.ability].label.substring(0, 3);
skl.icon = this._getProficiencyIcon(skl.value);
skl.hover = CONFIG.SW5E.proficiencyLevels[skl.value];
skl.label = CONFIG.SW5E.skills[s];
if (data.actor.data.skills) {
for ( let [s, skl] of Object.entries(data.actor.data.skills)) {
skl.ability = CONFIG.SW5E.abilityAbbreviations[skl.ability];
skl.icon = this._getProficiencyIcon(skl.value);
skl.hover = CONFIG.SW5E.proficiencyLevels[skl.value];
skl.label = CONFIG.SW5E.skills[s];
}
}
// Update traits
@ -96,9 +106,9 @@ export class ActorSheet5e extends ActorSheet {
_prepareTraits(traits) {
const map = {
"dr": CONFIG.SW5E.damageTypes,
"di": CONFIG.SW5E.damageTypes,
"dv": CONFIG.SW5E.damageTypes,
"dr": CONFIG.SW5E.damageResistanceTypes,
"di": CONFIG.SW5E.damageResistanceTypes,
"dv": CONFIG.SW5E.damageResistanceTypes,
"ci": CONFIG.SW5E.conditionTypes,
"languages": CONFIG.SW5E.languages,
"armorProf": CONFIG.SW5E.armorProficiencies,
@ -200,7 +210,7 @@ export class ActorSheet5e extends ActorSheet {
if ( mode in sections ) {
s = sections[mode];
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
if ( filters.has("equipped") ) {
if (data.equipped && data.equipped !== true) return false;
if ( data.equipped !== true ) return false;
}
return true;
});
@ -295,8 +305,10 @@ export class ActorSheet5e extends ActorSheet {
// Editable Only Listeners
if ( this.isEditable ) {
// Relative updates for numeric fields
html.find('input[data-dtype="Number"]').change(this._onChangeInputDelta.bind(this));
// Input focus and update
const inputs = html.find("input");
inputs.focus(ev => ev.currentTarget.select());
inputs.addBack().find('[data-dtype="Number"]').change(this._onChangeInputDelta.bind(this));
// Ability Proficiency
html.find('.ability-proficiency').click(this._onToggleAbilityProficiency.bind(this));
@ -328,14 +340,6 @@ export class ActorSheet5e extends ActorSheet {
// Roll Skill Checks
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
html.find('.item .item-image').click(event => this._onItemRoll(event));
html.find('.item .item-recharge').click(event => this._onItemRecharge(event));
@ -424,37 +428,9 @@ export class ActorSheet5e extends ActorSheet {
/* -------------------------------------------- */
/** @override */
async _onDrop (event) {
event.preventDefault();
// 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) {
async _onDropActor(event, data) {
const canPolymorph = game.user.isGM || (this.actor.owner && game.settings.get('sw5e', 'allowPolymorphing'));
if ( !canPolymorph ) return false;
// Get the target actor
let sourceActor = null;
@ -521,6 +497,30 @@ export class ActorSheet5e extends ActorSheet {
}).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 type = header.dataset.type;
const itemData = {
name: `New ${type.capitalize()}`,
name: game.i18n.format("SW5E.ItemNew", {type: type.capitalize()}),
type: type,
data: duplicate(header.dataset)
};
@ -736,11 +736,8 @@ export class ActorSheet5e extends ActorSheet {
event.preventDefault();
const a = event.currentTarget;
const label = a.parentElement.querySelector("label");
const options = {
name: label.getAttribute("for"),
title: label.innerText,
choices: CONFIG.SW5E[a.dataset.options]
};
const choices = CONFIG.SW5E[a.dataset.options];
const options = { name: a.dataset.target, title: label.innerText, choices };
new TraitSelector(this.actor, options).render(true)
}
@ -760,4 +757,90 @@ export class ActorSheet5e extends ActorSheet {
});
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.
* Extends the base ActorSheet5e class.
* @type {ActorSheet5e}
*/
export class ActorSheet5eCharacter extends ActorSheet5e {
export default class ActorSheet5eCharacter extends ActorSheet5e {
/**
* Define default rendering options for the NPC sheet
@ -14,24 +14,11 @@ export class ActorSheet5eCharacter extends ActorSheet5e {
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
classes: ["sw5e", "sheet", "actor", "character"],
width: 672,
width: 720,
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
sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking");
sheetData["classLabels"] = this.actor.itemTypes.class.map(c => c.name).join(", ");
// Return data for rendering
return sheetData;
@ -80,13 +68,12 @@ export class ActorSheet5eCharacter extends ActorSheet5e {
loot: { label: "SW5E.ItemTypeLootPl", items: [], dataset: {type: "loot"} }
};
// Partition items by category
let [items, powers, feats, classes, species] = data.items.reduce((arr, item) => {
// Item details
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.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);
else if ( item.type === "feat" ) arr[2].push(item);
else if ( item.type === "class" ) arr[3].push(item);
else if ( item.type === "species" ) arr[4].push(item);
else if ( item.type === "species" ) arr[4].push(item);
else if ( Object.keys(inventory).includes(item.type ) ) arr[0].push(item);
return arr;
}, [[], [], [], [], []]);
@ -111,27 +98,24 @@ export class ActorSheet5eCharacter extends ActorSheet5e {
powers = this._filterItems(powers, this._filters.powerbook);
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...)
const powerbook = this._preparePowerbook(data, powers);
const nPrepared = powers.filter(s => {
return (s.data.level > 0) && (s.data.preparation.mode === "prepared") && s.data.preparation.prepared;
}).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
const features = {
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"} },
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);
features.classes.items = classes;
features.species.items = species;
features.species.items = species;
// Assign and return
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
/* -------------------------------------------- */

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.
* Extends the base ActorSheet5e class.
* @type {ActorSheet5e}
* @extends {ActorSheet5e}
*/
export class ActorSheet5eNPC extends ActorSheet5e {
export default class ActorSheet5eNPC extends ActorSheet5e {
/**
* Define default rendering options for the NPC sheet
* @return {Object}
*/
/** @override */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
classes: ["sw5e", "sheet", "actor", "npc"],
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
const features = {
weapons: { label: "Attacks", items: [] , hasActions: true, dataset: {type: "weapon", "weapon-type": "natural"} },
actions: { label: "Actions", items: [] , hasActions: true, dataset: {type: "feat", "activation.type": "action"} },
passive: { label: "Features", items: [], dataset: {type: "feat"} },
equipment: { label: "Inventory", items: [], dataset: {type: "loot"}}
weapons: { label: game.i18n.localize("SW5E.AttackPl"), items: [] , hasActions: true, dataset: {type: "weapon", "weapon-type": "natural"} },
actions: { label: game.i18n.localize("SW5E.ActionPl"), items: [] , hasActions: true, dataset: {type: "feat", "activation.type": "action"} },
passive: { label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"} },
equipment: { label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}}
};
// Start by classifying items into groups for rendering
let [powers, other] = data.items.reduce((arr, item) => {
item.img = item.img || DEFAULT_TOKEN;
item.isStack = 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.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));
@ -86,9 +70,7 @@ export class ActorSheet5eNPC extends ActorSheet5e {
/* -------------------------------------------- */
/**
* Add some extra data when rendering the sheet to reduce the amount of logic required within the template.
*/
/** @override */
getData() {
const data = super.getData();
@ -103,12 +85,7 @@ export class ActorSheet5eNPC extends ActorSheet5e {
/* Object Updates */
/* -------------------------------------------- */
/**
* 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
*/
/** @override */
_updateObject(event, formData) {
// Format NPC Challenge Rating
@ -126,14 +103,9 @@ export class ActorSheet5eNPC extends ActorSheet5e {
/* Event Listeners and Handlers */
/* -------------------------------------------- */
/**
* Activate event listeners using the prepared sheet HTML
* @param html {HTML} The prepared HTML object ready to be rendered into the DOM
*/
/** @override */
activateListeners(html) {
super.activateListeners(html);
// Rollable Health Formula
html.find(".health .rollable").click(this._onRollHealthFormula.bind(this));
}
@ -152,4 +124,4 @@ export class ActorSheet5eNPC extends ActorSheet5e {
AudioHelper.play({src: CONFIG.sounds.dice});
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
* @type {Dialog}
*/
export class AbilityUseDialog extends Dialog {
export default class AbilityUseDialog extends Dialog {
constructor(item, dialogData={}, options={}) {
super(dialogData, options);
this.options.classes = ["sw5e", "dialog"];
@ -25,40 +25,150 @@ export class AbilityUseDialog extends Dialog {
* @return {Promise}
*/
static async create(item) {
if ( !item.isOwned ) throw new Error("You cannot display an ability usage dialog for an unowned item");
const uses = item.data.data.uses;
const recharge = item.data.data.recharge;
// Prepare data
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;
// Render the ability usage template
const html = await renderTemplate("systems/sw5e/templates/apps/ability-use.html", {
// Prepare dialog form data
const data = {
item: item.data,
canUse: recharges ? recharge.charged : uses.value > 0,
consume: true,
uses: uses,
recharges: !!recharge.value,
isCharged: recharge.charged,
title: game.i18n.format("SW5E.AbilityUseHint", item.data),
note: this._getAbilityUseNote(item.data, uses, recharge),
hasLimitedUses: uses.max || recharges,
canUse: recharges ? recharge.charged : (quantity > 0 && !uses.value) || uses.value > 0,
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
const icon = data.hasPowerSlots ? "fa-magic" : "fa-fist-raised";
const label = game.i18n.localize("SW5E.AbilityUse" + (data.hasPowerSlots ? "Cast" : "Use"));
return new Promise((resolve) => {
let formData = null;
const dlg = new this(item, {
title: `${item.name}: Ability Configuration`,
title: `${item.name}: Usage Configuration`,
content: html,
buttons: {
use: {
icon: '<i class="fas fa-fist-raised"></i>',
label: "Use Ability",
callback: html => formData = new FormData(html[0].querySelector("#ability-use-form"))
icon: `<i class="fas ${icon}"></i>`,
label: label,
callback: html => resolve(new FormData(html[0].querySelector("form")))
}
},
default: "use",
close: () => resolve(formData)
close: () => resolve(null)
});
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;
return mergeObject(options, {
id: "actor-flags",
@ -68,10 +72,10 @@ export class ActorSheetFlags extends BaseEntitySheet {
{name: "data.bonuses.mwak.damage", label: "SW5E.BonusMWDamage"},
{name: "data.bonuses.rwak.attack", label: "SW5E.BonusRWAttack"},
{name: "data.bonuses.rwak.damage", label: "SW5E.BonusRWDamage"},
{name: "data.bonuses.mpak.attack", label: "SW5E.BonusMSAttack"},
{name: "data.bonuses.mpak.damage", label: "SW5E.BonusMSDamage"},
{name: "data.bonuses.rpak.attack", label: "SW5E.BonusRSAttack"},
{name: "data.bonuses.rpak.damage", label: "SW5E.BonusRSDamage"},
{name: "data.bonuses.mpak.attack", label: "SW5E.BonusMPAttack"},
{name: "data.bonuses.mpak.damage", label: "SW5E.BonusMPDamage"},
{name: "data.bonuses.rpak.attack", label: "SW5E.BonusRPAttack"},
{name: "data.bonuses.rpak.damage", label: "SW5E.BonusRPDamage"},
{name: "data.bonuses.abilities.check", label: "SW5E.BonusAbilityCheck"},
{name: "data.bonuses.abilities.save", label: "SW5E.BonusAbilitySave"},
{name: "data.bonuses.abilities.skill", label: "SW5E.BonusAbilitySkill"},
@ -91,7 +95,7 @@ export class ActorSheetFlags extends BaseEntitySheet {
*/
async _updateObject(event, formData) {
const actor = this.object;
const updateData = expandObject(formData);
let updateData = expandObject(formData);
// Unset any flags which are "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});
}
}

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
* @type {Dialog}
* @extends {Dialog}
*/
export class ShortRestDialog extends Dialog {
export default class ShortRestDialog extends Dialog {
constructor(actor, dialogData={}, options={}) {
super(dialogData, options);
@ -34,6 +36,8 @@ export class ShortRestDialog extends Dialog {
/** @override */
getData() {
const data = super.getData();
// Determine Hit Dice
data.availableHD = this.actor.data.items.reduce((hd, item) => {
if ( item.type === "class" ) {
const d = item.data;
@ -45,6 +49,11 @@ export class ShortRestDialog extends Dialog {
}, {});
data.canRoll = this.actor.data.data.attributes.hd > 0;
data.denomination = this._denom;
// Determine rest type
const variant = game.settings.get("sw5e", "restVariant");
data.promptNewDay = variant !== "epic"; // It's never a new day when only resting 1 minute
data.newDay = false; // It may be a new day, but not by default
return data;
}
@ -56,7 +65,6 @@ export class ShortRestDialog extends Dialog {
super.activateListeners(html);
let btn = html.find("#roll-hd");
btn.click(this._onRollHitDie.bind(this));
super.activateListeners(html);
}
/* -------------------------------------------- */
@ -83,21 +91,27 @@ export class ShortRestDialog extends Dialog {
* @return {Promise}
*/
static async shortRestDialog({actor}={}) {
return new Promise(resolve => {
return new Promise((resolve, reject) => {
const dlg = new this(actor, {
title: "Short Rest",
buttons: {
rest: {
icon: '<i class="fas fa-bed"></i>',
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: {
icon: '<i class="fas fa-times"></i>',
label: "Cancel",
callback: () => resolve(false)
callback: reject
}
}
},
close: reject
});
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
* workflow has been resolved.
* @deprecated
* @param {Actor5e} actor
* @return {Promise}
*/
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,
class resources, limited use item charges, and power slots.</p>`;
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);
});
console.warn("WARNING! ShortRestDialog.longRestDialog has been deprecated, use LongRestDialog.longRestDialog instead.");
return LongRestDialog.longRestDialog(...arguments);
}
}

View file

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

View file

@ -1,32 +1,40 @@
/**
* Measure the distance between two pixel coordinates
* See BaseGrid.measureDistance for more details
*
* @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));
/** @override */
export const measureDistances = function(segments, options={}) {
if ( !options.gridSpaces ) return BaseGrid.prototype.measureDistances.call(this, segments, options);
// Get the number of straight and diagonal moves
let nDiagonal = Math.min(nx, ny),
nStraight = Math.abs(ny - nx);
// Track the total number of diagonals
let nDiagonal = 0;
const rule = this.parent.diagonalRule;
const d = canvas.dimensions;
// Alternative DMG Movement
if ( this.parent.diagonalRule === "5105" ) {
let nd10 = Math.floor(nDiagonal / 2);
let spaces = (nd10 * 2) + (nDiagonal - nd10) + nStraight;
return spaces * canvas.dimensions.distance;
}
// Iterate over measured segments
return segments.map(s => {
let r = s.ray;
// Standard PHB Movement
else return (nStraight + nDiagonal) * canvas.scene.data.gridDistance;
// Determine the total distance traveled
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) {
const data = _TokenGetBarAttribute.bind(this)(...args);
if ( data && (data.attribute === "attributes.hp") ) {
data.value += parseInt(data['temp'] || 0);
data.max += parseInt(data['tempmax'] || 0);
data.value += parseInt(getProperty(this.actor.data, "data.attributes.hp.temp") || 0);
data.max += parseInt(getProperty(this.actor.data, "data.attributes.hp.tempmax") || 0);
}
return data;
};

View file

@ -1,18 +1,18 @@
import {Actor5e} from "./actor/entity.js";
/**
* Highlight critical success or failure on d20 rolls
*/
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
const roll = message.roll;
let d = roll.parts[0];
const isD20Roll = d instanceof Die && (d.faces === 20) && (d.results.length === 1);
if ( !isD20Roll ) return;
if ( !roll.dice.length ) return;
const d = roll.dice[0];
// 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;
if ( isModifiedRoll ) return;
@ -60,32 +60,55 @@ export const displayChatActionButtons = function(message, html, data) {
* @return {Array} The extended options Array including new context choices
*/
export const addChatMessageContextOptions = function(html, options) {
let canApply = li => 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(
{
name: game.i18n.localize("SW5E.ChatContextDamage"),
icon: '<i class="fas fa-user-minus"></i>',
condition: canApply,
callback: li => Actor5e.applyDamage(li, 1)
callback: li => applyChatCardDamage(li, 1)
},
{
name: game.i18n.localize("SW5E.ChatContextHealing"),
icon: '<i class="fas fa-user-plus"></i>',
condition: canApply,
callback: li => Actor5e.applyDamage(li, -1)
callback: li => applyChatCardDamage(li, -1)
},
{
name: game.i18n.localize("SW5E.ChatContextDoubleDamage"),
icon: '<i class="fas fa-user-injured"></i>',
condition: canApply,
callback: li => Actor5e.applyDamage(li, 2)
callback: li => applyChatCardDamage(li, 2)
},
{
name: game.i18n.localize("SW5E.ChatContextHalfDamage"),
icon: '<i class="fas fa-user-shield"></i>',
condition: canApply,
callback: li => Actor5e.applyDamage(li, 0.5)
callback: li => applyChatCardDamage(li, 0.5)
}
);
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 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 ( 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(" + ");
};
/* -------------------------------------------- */
/**
* 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 = {};
// ASCII Artwork
SW5E.ASCII = `_______________________________
SW5E.ASCII = `__________________________________________
_
| |
___| |_ __ _ _ ____ ____ _ _ __ ___
/ __| __/ _\ | |__\ \ /\ / / _\ | |__/ __|
\__ \ || (_) | | \ V V / (_) | | \__ \
|___/\__\__/_|_| \_/\_/ \__/_|_| |___/
_______________________________`;
__________________________________________`;
/**
@ -25,6 +25,15 @@ SW5E.abilities = {
"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 = {
"sim": "SW5E.WeaponSimpleProficiency",
"mar": "SW5E.WeaponMartialProficiency",
"mar": "SW5E.WeaponMartialProficiency"
};
SW5E.toolProficiencies = {
@ -119,9 +128,21 @@ SW5E.abilityActivationTypes = {
"day": SW5E.timePeriods.day,
"special": SW5E.timePeriods.spec,
"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
@ -196,7 +217,8 @@ SW5E.equipmentTypes = {
"natural": "SW5E.EquipmentNatural",
"shield": "SW5E.EquipmentShield",
"clothing": "SW5E.EquipmentClothing",
"trinket": "SW5E.EquipmentTrinket"
"trinket": "SW5E.EquipmentTrinket",
"vehicle": "SW5E.EquipmentVehicle"
};
@ -231,7 +253,6 @@ SW5E.consumableTypes = {
"trinket": "SW5E.ConsumableTrinket"
};
/* -------------------------------------------- */
/**
@ -261,10 +282,14 @@ SW5E.damageTypes = {
"sonic": "SW5E.DamageSonic"
};
// Damage Resistance Types
SW5E.damageResistanceTypes = mergeObject(duplicate(SW5E.damageTypes), {
"physical": "SW5E.DamagePhysical"
});
/* -------------------------------------------- */
// armor Types
SW5E.armorpropertiesTypes = {
SW5E.armorPropertiesTypes = {
"Absorptive": "SW5E.ArmorProperAbsorptive",
"Agile": "SW5E.ArmorProperAgile",
"Anchor": "SW5E.ArmorProperAnchor",
@ -315,7 +340,8 @@ SW5E.distanceUnits = {
*/
SW5E.encumbrance = {
currencyPerWeight: 50,
strMultiplier: 15
strMultiplier: 15,
vehicleWeightMultiplier: 2000 // 2000 lbs in a ton
};
/* -------------------------------------------- */
@ -435,7 +461,7 @@ SW5E.powerPreparationModes = {
"prepared": "SW5E.PowerPrepPrepared"
};
SW5E.powerUpcastModes = ["always"];
SW5E.powerUpcastModes = ["always", "pact", "prepared"];
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
SW5E.conditionTypes = {
"blinded": "SW5E.ConBlinded",
"charmed": "SW5E.ConCharmed",
"deafened": "SW5E.ConDeafened",
"diseased": "SW5E.ConDiseased",
"exhaustion": "SW5E.ConExhaustion",
"frightened": "SW5E.ConFrightened",
"grappled": "SW5E.ConGrappled",
@ -768,8 +810,8 @@ SW5E.characterFlags = {
type: Boolean
},
"powerfulBuild": {
name: "Powerful Build",
hint: "You count as one size larger when determining your carrying capacity and the weight you can push, drag, or lift.",
name: "SW5E.FlagsPowerfulBuild",
hint: "SW5E.FlagsPowerfulBuildHint",
section: "Racial Traits",
type: Boolean
},
@ -798,40 +840,40 @@ SW5E.characterFlags = {
type: Boolean
},
"initiativeAdv": {
name: "Advantage on Initiative",
hint: "Provided by feats or magical items.",
name: "SW5E.FlagsInitiativeAdv",
hint: "SW5E.FlagsInitiativeAdvHint",
section: "Feats",
type: Boolean
},
"initiativeAlert": {
name: "Alert Feat",
hint: "Provides +5 to Initiative.",
name: "SW5E.FlagsAlert",
hint: "SW5E.FlagsAlertHint",
section: "Feats",
type: Boolean
},
"jackOfAllTrades": {
name: "Jack of All Trades",
hint: "Half-Proficiency to Ability Checks in which you are not already Proficient.",
name: "SW5E.FlagsJOAT",
hint: "SW5E.FlagsJOATHint",
section: "Feats",
type: Boolean
},
"observantFeat": {
name: "Observant Feat",
hint: "Provides a +5 to passive Perception and Investigation.",
name: "SW5E.FlagsObservant",
hint: "SW5E.FlagsObservantHint",
skills: ['prc','inv'],
section: "Feats",
type: Boolean
},
"remarkableAthlete": {
name: "Remarkable Athlete.",
hint: "Half-Proficiency (rounded-up) to physical Ability Checks and Initiative.",
name: "SW5E.FlagsRemarkableAthlete",
hint: "SW5E.FlagsRemarkableAthleteHint",
abilities: ['str','dex','con'],
section: "Feats",
type: Boolean
},
"weaponCriticalThreshold": {
name: "Critical Hit Threshold",
hint: "Allow for expanded critical range; for example Improved or Superior Critical",
name: "SW5E.FlagsCritThreshold",
hint: "SW5E.FlagsCritThresholdHint",
section: "Feats",
type: Number,
placeholder: 20

View file

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

View file

@ -1,11 +1,11 @@
import { Dice5e } from "../dice.js";
import { AbilityUseDialog } from "../apps/ability-use-dialog.js";
import { AbilityTemplate } from "../pixi/ability-template.js";
import {d20Roll, damageRoll} from "../dice.js";
import AbilityUseDialog from "../apps/ability-use-dialog.js";
import AbilityTemplate from "../pixi/ability-template.js";
/**
* Override and extend the basic :class:`Item` implementation
*/
export class Item5e extends Item {
export default class Item5e extends Item {
/* -------------------------------------------- */
/* Item Properties */
@ -20,14 +20,34 @@ export class Item5e extends Item {
if (!("ability" in itemData)) return null;
// 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
else if ( this.actor ) {
else if (this.actor) {
const actorData = this.actor.data.data;
if ( this.data.type === "power" ) return actorData.attributes.powercasting || "int";
else if ( this.data.type === "tool" ) return "int";
else return "str";
// Powers - Use Actor powercasting modifier
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
@ -149,15 +169,16 @@ export class Item5e extends Item {
arr.push(c[0].titleCase().slice(0, 1));
return arr;
}, []);
labels.materials = data?.materials?.value ?? null;
}
// Feat Items
else if ( itemData.type === "feat" ) {
const act = data.activation;
if ( act && (act.type === C.abilityActivationTypes.legendary) ) labels.featType = "Legendary Action";
else if ( act && (act.type === C.abilityActivationTypes.lair) ) labels.featType = "Lair Action";
else if ( act && act.type ) labels.featType = data.damage.length ? "Attack" : "Action";
else labels.featType = "Passive";
if ( act && (act.type === C.abilityActivationTypes.legendary) ) labels.featType = game.i18n.localize("SW5E.LegendaryActionLabel");
else if ( act && (act.type === C.abilityActivationTypes.lair) ) labels.featType = game.i18n.localize("SW5E.LairActionLabel");
else if ( act && act.type ) labels.featType = game.i18n.localize(data.damage.length ? "SW5E.Attack" : "SW5E.Action");
else labels.featType = game.i18n.localize("SW5E.Passive");
}
// Species Items
@ -167,7 +188,7 @@ export class Item5e extends Item {
// Equipment Items
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
@ -201,7 +222,7 @@ export class Item5e extends Item {
// Recharge Label
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
@ -216,7 +237,7 @@ export class Item5e extends Item {
} else { // Un-owned items
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
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
* @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}
*/
async roll({configureDialog=true}={}) {
async roll({configureDialog=true, rollMode=null, createMessage=true}={}) {
// Basic template rendering data
const token = this.actor.token;
@ -259,10 +284,17 @@ export class Item5e extends Item {
if (this.data.type === "feat") {
let configured = await this._rollFeat(configureDialog);
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
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 html = await renderTemplate(template, templateData);
@ -279,12 +311,90 @@ export class Item5e extends Item {
};
// Toggle default roll mode
let rollMode = game.settings.get("core", "rollMode");
if ( ["gmroll", "blindroll"].includes(rollMode) ) chatData["whisper"] = ChatMessage.getWhisperIDs("GM");
rollMode = rollMode || game.settings.get("core", "rollMode");
if ( ["gmroll", "blindroll"].includes(rollMode) ) chatData["whisper"] = ChatMessage.getWhisperRecipients("GM");
if ( rollMode === "blindroll" ) chatData["blind"] = true;
// 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");
// 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;
let usesCharges = !!uses.per && (uses.max > 0);
let placeTemplate = false;
let consume = usesRecharge || usesCharges;
let consume = charge.value || usesCharges;
// Determine whether the feat uses charges
configureDialog = configureDialog && (consume || this.hasAreaTarget);
if ( configureDialog ) {
const usage = await AbilityUseDialog.create(this);
if ( usage === null ) return false;
consume = Boolean(usage.get("consume"));
consume = Boolean(usage.get("consumeUse"));
placeTemplate = Boolean(usage.get("placeTemplate"));
}
// Update Item data
const current = getProperty(this.data, "data.uses.value") || 0;
if ( consume && usesRecharge ) {
await this.update({"data.recharge.charged": false});
if ( consume && charge.value ) {
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 ) {
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)});
}
@ -340,14 +458,13 @@ export class Item5e extends Item {
* @param {Object} htmlOptions Options used by the TextEditor.enrichHTML function
* @return {Object} An object of chat data to render
*/
getChatData(htmlOptions) {
getChatData(htmlOptions={}) {
const data = duplicate(this.data.data);
const labels = this.labels;
// Rich text description
data.description.value = TextEditor.enrichHTML(data.description.value, htmlOptions);
// Item type specific properties
const props = [];
const fn = this[`_${this.data.type}ChatData`];
@ -356,16 +473,16 @@ export class Item5e extends Item {
// General equipment properties
if ( data.hasOwnProperty("equipped") && !["loot", "tool"].includes(this.data.type) ) {
props.push(
data.equipped ? "Equipped" : "Not Equipped",
data.proficient ? "Proficient": "Not Proficient",
game.i18n.localize(data.equipped ? "SW5E.Equipped" : "SW5E.Unequipped"),
game.i18n.localize(data.proficient ? "SW5E.Proficient" : "SW5E.NotProficient"),
);
}
// Ability activation properties
if ( data.hasOwnProperty("activation") ) {
props.push(
labels.activation + (data.activation?.condition ? ` (${data.activation.condition})` : ""),
labels.target,
labels.activation,
labels.range,
labels.duration
);
@ -386,7 +503,7 @@ export class Item5e extends Item {
props.push(
CONFIG.SW5E.equipmentTypes[data.armor.type],
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) {
props.push(
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;
}
@ -437,8 +554,8 @@ export class Item5e extends Item {
*/
_lootChatData(data, labels, props) {
props.push(
"Loot",
data.weight ? data.weight + " lbs." : null
game.i18n.localize("SW5E.ItemTypeLoot"),
data.weight ? data.weight + " " + game.i18n.localize("SW5E.AbbreviationLbs") : null
);
}
@ -452,7 +569,7 @@ export class Item5e extends Item {
_powerChatData(data, labels, props) {
props.push(
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)
* 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 actorData = this.actor.data.data;
const flags = this.actor.data.flags.sw5e || {};
if ( !this.hasAttack ) {
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();
// Define Roll bonuses
@ -492,26 +611,44 @@ export class Item5e extends Item {
}
// Attack Bonus
const actorBonus = actorData.bonuses[itemData.actionType] || {};
const actorBonus = actorData?.bonuses?.[itemData.actionType] || {};
if ( itemData.attackBonus || actorBonus.attack ) {
parts.push("@atk");
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
const rollConfig = {
event: options.event,
const rollConfig = mergeObject({
parts: parts,
actor: this.actor,
data: rollData,
title: `${this.name} - Attack Roll`,
title: title,
speaker: ChatMessage.getSpeaker({actor: this.actor}),
dialogOptions: {
width: 400,
top: options.event ? options.event.clientY - 80 : null,
left: window.innerWidth - 710
}
};
},
messageData: {"flags.sw5e.roll": {type: "attack", itemId: this.id }}
}, options);
rollConfig.event = options.event;
// Expanded weapon critical threshold
if (( this.data.type === "weapon" ) && flags.weaponCriticalThreshold) {
@ -529,16 +666,22 @@ export class Item5e extends Item {
if ( flags.halflingLucky ) rollConfig.halflingLucky = true;
// 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)
* 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}={}) {
const itemData = this.data.data;
@ -546,32 +689,54 @@ export class Item5e extends Item {
if ( !this.hasDamage ) {
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();
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
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 ( (itemData.scaling.mode === "atwill") ) {
const lvl = this.actor.data.type === "character" ? actorData.details.level : actorData.details.powerLevel;
this._scaleAtWillDamage(parts, lvl, itemData.scaling.formula );
} else if ( powerLevel && (itemData.scaling.mode === "level") && itemData.scaling.formula ) {
this._scalePowerDamage(parts, itemData.level, powerLevel, itemData.scaling.formula );
if ( (itemData.scaling.mode === "cantrip") ) {
const level = this.actor.data.type === "character" ? actorData.details.level : actorData.details.powerLevel;
this._scaleCantripDamage(parts, itemData.scaling.formula, level, rollData);
}
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
const actorBonus = actorData.bonuses[itemData.actionType] || {};
const actorBonus = getProperty(actorData, `bonuses.${itemData.actionType}`) || {};
if ( actorBonus.damage && parseInt(actorBonus.damage) !== 0 ) {
parts.push("@dmg");
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
const title = `${this.name} - Damage Roll`;
const flavor = this.labels.damageTypes.length ? `${title} (${this.labels.damageTypes})` : title;
return Dice5e.damageRoll({
return damageRoll({
event: event,
parts: parts,
actor: this.actor,
@ -583,7 +748,8 @@ export class Item5e extends Item {
width: 400,
top: event ? event.clientY - 80 : null,
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
* @private
*/
_scaleAtWillDamage(parts, level, scale) {
_scaleAtWillDamage(parts, scale, level, rollData) {
const add = Math.floor((level + 1) / 6);
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}`);
} else {
parts[0] = parts[0].replace(new RegExp(Roll.diceRgx, "g"), (match, nd, d) => `${parseInt(nd)+add}d${d}`);
// FUTURE SOLUTION - 0.7.0 AND LATER
if (isNewerVersion(game.data.version, "0.6.9")) {
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} powerLevel The casted power level
* @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
*/
_scalePowerDamage(parts, baseLevel, powerLevel, formula) {
_scalePowerDamage(parts, baseLevel, powerLevel, formula, rollData) {
const upcastLevels = Math.max(powerLevel - baseLevel, 0);
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;
}
@ -625,8 +850,8 @@ export class Item5e extends Item {
/**
* 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
*/
async rollFormula(options={}) {
@ -636,14 +861,16 @@ export class Item5e extends Item {
// Define Roll Data
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
const roll = new Roll(rollData.item.formula, rollData).roll();
roll.toMessage({
speaker: ChatMessage.getSpeaker({actor: this.actor}),
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;
}
@ -652,58 +879,77 @@ export class Item5e extends Item {
/**
* Use a consumable item, deducting from the quantity or charges of the item.
*
* @return {Promise.<Roll>} A Promise which resolves to the created Roll instance or null
* @param {boolean} configureDialog Whether to show a configuration dialog
* @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;
// Dispatch a damage roll
let roll = null;
if ( itemData.damage.parts.length ) {
roll = await this.rollDamage(options);
// Determine whether to deduct uses of the item
const uses = itemData.uses || {};
const autoDestroy = uses.autoDestroy;
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
if ( itemData.formula ) {
roll = await this.rollFormula(options);
}
// 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
// Update Item data
if ( consume ) {
const current = uses.value || 0;
const remaining = usesCharges ? Math.max(current - 1, 0) : current;
if ( usesRecharge ) await this.update({"data.recharge.charged": false});
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
* @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;
if ( !data.recharge.value ) return;
@ -713,7 +959,7 @@ export class Item5e extends Item {
// Display a Chat Message
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})
})];
@ -725,10 +971,9 @@ export class Item5e extends Item {
/* -------------------------------------------- */
/**
* Roll a Tool Check
* Rely upon the Dice5e.d20Roll logic for the core implementation
*
* @return {Promise.<Roll>} A Promise which resolves to the created Roll instance
* Roll a Tool Check. Rely upon the 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
*/
rollToolCheck(options={}) {
if ( this.type !== "tool" ) throw "Wrong item type!";
@ -736,24 +981,28 @@ export class Item5e extends Item {
// Prepare roll data
let rollData = this.getRollData();
const parts = [`@mod`, "@prof"];
const title = `${this.name} - Tool Check`;
const title = `${this.name} - ${game.i18n.localize("SW5E.ToolCheck")}`;
// Call the roll helper utility
return Dice5e.d20Roll({
event: options.event,
// Compose the roll data
const rollConfig = mergeObject({
parts: parts,
data: rollData,
template: "systems/sw5e/templates/chat/tool-roll-dialog.html",
title: title,
speaker: ChatMessage.getSpeaker({actor: this.actor}),
flavor: `${this.name} - Tool Check`,
flavor: `${this.name} - ${game.i18n.localize("SW5E.ToolCheck")}`,
dialogOptions: {
width: 400,
top: options.event ? options.event.clientY - 80 : null,
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
const item = actor.getOwnedItem(card.dataset.itemId);
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;
@ -828,7 +1077,7 @@ export class Item5e extends Item {
if ( isTargetted ) {
targets = this._getChatCardTargets(card);
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;
}
}
@ -837,18 +1086,16 @@ export class Item5e extends Item {
if ( action === "attack" ) await item.rollAttack({event});
else if ( action === "damage" ) await item.rollDamage({event, powerLevel});
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
else if ( action === "save" ) {
for ( let t of targets ) {
await t.rollAbilitySave(button.dataset.ability, {event});
for ( let a of targets ) {
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
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);
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
* @type {ItemSheet}
* Override and extend the core ItemSheet implementation to handle specific item types
* @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 */
static get defaultOptions() {
@ -13,7 +22,7 @@ export class ItemSheet5e extends ItemSheet {
width: 560,
height: 420,
classes: ["sw5e", "sheet", "item"],
resizable: false,
resizable: true,
scrollY: [".tab.details"],
tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}]
});
@ -43,14 +52,79 @@ export class ItemSheet5e extends ItemSheet {
data.itemProperties = this._getItemProperties(data.item);
data.isPhysical = data.item.data.hasOwnProperty("quantity");
// Potential consumption targets
data.abilityConsumptionTargets = this._getItemConsumptionTargets(data.item);
// Action Details
data.hasAttackRoll = this.item.hasAttack;
data.isHealing = data.item.data.actionType === "heal";
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;
}
/* -------------------------------------------- */
/**
* 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];
}
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" ) {
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(
labels.components,
labels.materials,
item.data.components.concentration ? "Concentration" : null,
item.data.components.ritual ? "Ritual" : null
item.data.components.concentration ? game.i18n.localize("SW5E.Concentration") : 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 */
setPosition(position={}) {
position.height = this._tabs[0].active === "details" ? "auto" : this.options.height;
@ -141,33 +231,12 @@ export class ItemSheet5e extends ItemSheet {
/** @override */
_updateObject(event, formData) {
// TODO: This can be removed once 0.7.x is release channel
if ( !formData.data ) formData = expandObject(formData);
// Handle Damage Array
let damage = Object.entries(formData).filter(e => e[0].startsWith("data.damage.parts"));
formData["data.damage.parts"] = damage.reduce((arr, entry) => {
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;
}, []);
const damage = formData.data?.damage;
if ( damage ) damage.parts = Object.values(damage?.parts || {}).map(d => [d[0] || "", d[1] || ""]);
// Update the Item
super._updateObject(event, formData);
@ -179,16 +248,14 @@ export class ItemSheet5e extends ItemSheet {
activateListeners(html) {
super.activateListeners(html);
html.find(".damage-control").click(this._onDamageControl.bind(this));
// Activate any Trait Selectors
html.find('.trait-selector.class-skills').click(this._onConfigureClassSkills.bind(this));
// Armor properties
html.find(".armorproperties-control").click(this._onarmorpropertiesControl.bind(this));
// Weapon properties
html.find(".weaponproperties-control").click(this._onweaponpropertiesControl.bind(this));
// Armor properties
html.find(".armorproperties-control").click(this._onarmorpropertiesControl.bind(this));
// Weapon properties
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
* @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
* @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
@ -37,8 +37,8 @@ export class AbilityTemplate extends MeasuredTemplate {
templateData.width = target.value;
templateData.direction = 45;
break;
case "ray": // 5e rays are most commonly 5ft wide
templateData.width = 5;
case "ray": // 5e rays are most commonly 1 square (5 ft) in width
templateData.width = canvas.dimensions.distance;
break;
default:
break;

View file

@ -11,6 +11,23 @@ export const registerSystemSettings = function() {
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
*/
@ -23,7 +40,8 @@ export const registerSystemSettings = function() {
type: String,
choices: {
"555": "SETTINGS.5eDiagPHB",
"5105": "SETTINGS.5eDiagDMG"
"5105": "SETTINGS.5eDiagDMG",
"EUCL": "SETTINGS.5eDiagEuclidean",
},
onChange: rule => canvas.grid.diagonalRule = rule
});
@ -31,21 +49,14 @@ export const registerSystemSettings = function() {
/**
* 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", {
name: "SETTINGS.5eInitTBN",
hint: "SETTINGS.5eInitTBL",
scope: "world",
config: true,
default: false,
type: Boolean,
onChange: enable => _set5eInitiative(enable)
type: Boolean
});
_set5eInitiative(game.settings.get("sw5e", "initiativeDexTiebreaker"));
/**
* Require Currency Carrying Weight

View file

@ -17,7 +17,8 @@ export const preloadHandlebarsTemplates = async function() {
// Item Sheet Partials
"systems/sw5e/templates/items/parts/item-action.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

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
* Author: Kakeman89
* 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
* 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 { preloadHandlebarsTemplates } from "./module/templates.js";
import { _getInitiativeFormula } from "./module/combat.js";
import { measureDistance, getBarAttribute } from "./module/canvas.js";
import { Actor5e } from "./module/actor/entity.js";
import { ActorSheet5eCharacter } from "./module/actor/sheets/character.js";
import { Item5e } from "./module/item/entity.js";
import { ItemSheet5e } from "./module/item/sheet.js";
import { ActorSheet5eNPC } from "./module/actor/sheets/npc.js";
import { Dice5e } from "./module/dice.js";
import { measureDistances, getBarAttribute } from "./module/canvas.js";
// Import Entities
import Actor5e from "./module/actor/entity.js";
import Item5e from "./module/item/entity.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 dice from "./module/dice.js";
import * as macros from "./module/macros.js";
import * as migrations from "./module/migration.js";
/* -------------------------------------------- */
@ -31,11 +44,28 @@ Hooks.once("init", function() {
// Create a SW5E namespace within the game global
game.sw5e = {
Actor5e,
Dice5e,
Item5e,
migrations,
rollItemMacro
applications: {
AbilityUseDialog,
ActorSheetFlags,
ActorSheet5eCharacter,
ActorSheet5eNPC,
ActorSheet5eVehicle,
ItemSheet5e,
ShortRestDialog,
TraitSelector
},
canvas: {
AbilityTemplate
},
config: SW5E,
dice: dice,
entities: {
Actor5e,
Item5e,
},
macros: macros,
migrations: migrations,
rollItemMacro: macros.rollItemMacro
};
// Record Configuration Values
@ -51,12 +81,14 @@ Hooks.once("init", function() {
registerSystemSettings();
// Patch Core Functions
CONFIG.Combat.initiative.formula = "1d20 + @attributes.init.mod + @attributes.init.prof + @attributes.init.bonus";
Combat.prototype._getInitiativeFormula = _getInitiativeFormula;
// Register sheet application classes
Actors.unregisterSheet("core", ActorSheet);
Actors.registerSheet("sw5e", ActorSheet5eCharacter, { types: ["character"], makeDefault: true });
Actors.registerSheet("sw5e", ActorSheet5eNPC, { types: ["npc"], makeDefault: true });
Actors.registerSheet('sw5e', ActorSheet5eVehicle, {types: ['vehicle'], makeDefault: true});
Items.unregisterSheet("core", ItemSheet);
Items.registerSheet("sw5e", ItemSheet5e, {makeDefault: true});
@ -76,14 +108,28 @@ Hooks.once("setup", function() {
// Localize CONFIG objects once up-front
const toLocalize = [
"abilities", "alignments", "conditionTypes", "consumableTypes", "currencies", "damageTypes", "distanceUnits", "equipmentTypes",
"healingTypes", "itemActionTypes", "limitedUsePeriods", "senses", "skills", "powerComponents", "powerLevels", "powerPreparationModes",
"powerSchools", "powerScalingModes", "targetTypes", "timePeriods", "weaponProperties", "weaponTypes", "languages", "polymorphSettings",
"armorProficiencies", "weaponProficiencies", "toolProficiencies", "abilityActivationTypes", "actorSizes", "proficiencyLevels", "armorpropertiesTypes"
"abilities", "abilityAbbreviations", "alignments", "conditionTypes", "consumableTypes", "currencies",
"damageTypes", "damageResistanceTypes", "distanceUnits", "equipmentTypes", "healingTypes", "itemActionTypes",
"limitedUsePeriods", "senses", "skills", "powerComponents", "powerLevels", "powerPreparationModes", "powerSchools",
"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 ) {
CONFIG.SW5E[o] = Object.entries(CONFIG.SW5E[o]).reduce((obj, e) => {
obj[e[0]] = game.i18n.localize(e[1]);
const localized = Object.entries(CONFIG.SW5E[o]).map(e => {
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;
}, {});
}
@ -101,7 +147,6 @@ Hooks.once("ready", function() {
const NEEDS_MIGRATION_VERSION = 0.84;
const COMPATIBLE_MIGRATION_VERSION = 0.80;
let needMigration = (currentVersion < NEEDS_MIGRATION_VERSION) || (currentVersion === null);
const canMigrate = currentVersion >= COMPATIBLE_MIGRATION_VERSION;
// Perform the migration
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
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
canvas.grid.diagonalRule = game.settings.get("sw5e", "diagonalMovement");
SquareGrid.prototype.measureDistance = measureDistance;
SquareGrid.prototype.measureDistances = measureDistances;
// Extend Token Resource Bars
Token.prototype.getBarAttribute = getBarAttribute;
@ -149,63 +194,6 @@ Hooks.on("getChatLogEntryContext", chat.addChatMessageContextOptions);
Hooks.on("renderChatLog", (app, html, data) => Item5e.chatListeners(html));
Hooks.on('getActorDirectoryEntryContext', Actor5e.addDirectoryContextOptions);
/* -------------------------------------------- */
/* 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();
}
Handlebars.registerHelper('getProperty', function (data, property) {
return getProperty(data, property);
});

View file

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

View file

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

View file

@ -4,31 +4,34 @@
<header class="sheet-header flexrow">
<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">
<input name="name" type="text" value="{{actor.name}}" placeholder="{{ localize 'SW5E.Name' }}"/>
</h1>
<div class="charlevel">
<div class="level {{#if disableExperience}}noxp{{/if}}">
<aside class="header-exp flexcol">
<div class="charlevel">
<label>{{ localize "SW5E.Level" }} {{data.details.level}}</label>
<span class="levels">{{classLabels}}</span>
</div>
{{#unless disableExperience}}
<div class="experience">
<div class="experience flexrow">
<input name="data.details.xp.value" type="text" value="{{data.details.xp.value}}"
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 class="xpbar">
<span class="bar" style="width: {{data.details.xp.pct}}%"></span>
</div>
{{/unless}}
</div>
</aside>
{{!-- Character Summary --}}
<ul class="summary">
<ul class="summary flexrow">
<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>
<input type="text" name="data.details.background" value="{{data.details.background}}" placeholder="{{ localize 'SW5E.Background' }}"/>
@ -36,6 +39,9 @@
<li>
<input type="text" name="data.details.alignment" value="{{data.details.alignment}}" placeholder="{{ localize 'SW5E.Alignment' }}"/>
</li>
<li class="proficiency">
<span>{{ localize "SW5E.Proficiency" }} {{numberFormat data.attributes.prof decimals=0 sign=true}}</span>
</li>
</ul>
{{!-- Header Attributes --}}
@ -57,6 +63,17 @@
</footer>
</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">
<h4 class="attribute-name box-title">{{ localize "SW5E.ArmorClass" }}</h4>
<div class="attribute-value">
@ -64,8 +81,7 @@
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>
<span class="power-dc">{{localize "SW5E.PowerDC"}} {{data.attributes.powerdc}}</span>
</footer>
</li>
@ -81,18 +97,19 @@
</footer>
</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}} / {{data.details.level}}</label>
<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">
<a class="rest short-rest">{{ localize "SW5E.RestS" }}</a>
<a class="rest long-rest">{{ localize "SW5E.RestL" }}</a>
<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>
</div>
</section>
</header>
{{!-- NPC Sheet Navigation --}}
@ -118,7 +135,7 @@
<div class="ability-modifiers flexrow">
<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"/>
<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>
</div>
</li>
@ -133,13 +150,13 @@
<a class="proficiency-toggle skill-proficiency" title="{{skill.hover}}">{{{skill.icon}}}</a>
<h4 class="skill-name rollable">{{skill.label}}</h4>
<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>
</li>
{{/each}}
</ul>
<section class="center-pane">
<section class="center-pane flexcol">
{{!-- Body Attributes --}}
<ul class="attributes flexrow">
@ -149,39 +166,27 @@
<input name="data.resources.{{res.name}}.label" type="text" value="{{res.label}}"
placeholder="{{res.placeholder}}" />
</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}}"
data-dtype="Number" placeholder="0"/>
<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"/>
</div>
<footer class="attribute-footer">
<label class="checkbox">
{{ localize "SW5E.AbbreviationSR" }} <input name="data.resources.{{res.name}}.sr" type="checkbox" {{checked res.sr}}/>
</label>
<label class="checkbox">
<label class="recharge checkbox">
{{ localize "SW5E.AbbreviationLR" }} <input name="data.resources.{{res.name}}.lr" type="checkbox" {{checked res.lr}}/>
</label>
</footer>
</div>
</li>
{{/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>
{{!-- Counters --}}
<div class="counters flexrow">
<div class="counters">
<div class="counter flexrow death-saves">
<h4 class="death-save rollable">{{ localize "SW5E.DeathSave" }}</h4>
<div class="counter-value">
@ -193,14 +198,14 @@
value="{{data.attributes.death.failure}}"/>
</div>
</div>
<div class="counter flexrow">
<div class="counter flexrow exhaustion">
<h4>{{ localize "SW5E.Exhaustion" }}</h4>
<div class="counter-value">
<input type="text" name="data.attributes.exhaustion" data-dtype="Number" placeholder="0"
value="{{data.attributes.exhaustion}}" />
</div>
</div>
<div class="counter flexrow">
<div class="counter flexrow inspiration">
<h4>{{ localize "SW5E.Inspiration" }}</h4>
<div class="counter-value">
<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 -->
<header class="sheet-header">
<h1 class="charname">
<input name="name" type="text" value="{{actor.name}}" placeholder="{{ localize 'SW5E.Name' }}"/>
</h1>
{{!-- Sheet Header --}}
<header class="sheet-header flexrow">
<img class="profile" src="{{actor.img}}" title="{{actor.name}}" data-edit="img"/>
<section class="header-details flexrow">
<h1 class="charname">
<input name="name" type="text" value="{{actor.name}}" placeholder="{{ localize 'SW5E.Name' }}"/>
</h1>
</section>
</header>
<!-- BODY -->
{{!-- Sheet Body --}}
<section class="sheet-body">
<div class="tab biography">
{{editor content=data.details.biography.value target="data.details.biography.value" button=true owner=owner editable=editable}}
</div>
</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>

View file

@ -4,33 +4,79 @@
<header class="sheet-header flexrow">
<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">
<input name="name" type="text" value="{{actor.name}}" placeholder="{{ localize 'SW5E.Name' }}"/>
</h1>
<div class="charlevel">
<div class="level">
<aside class="header-exp flexcol">
<div class="cr">
<label>{{ localize "SW5E.AbbreviationCR" }}</label>
<input name="data.details.cr" type="text" value="{{labels.cr}}" placeholder="1"/>
</div>
<div class="experience">
<span>{{data.details.xp.value}} XP</span>
</div>
</div>
</aside>
<ul class="summary">
{{!-- Character Summary --}}
<ul class="summary flexrow">
<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>
<input type="text" name="data.details.alignment" value="{{data.details.alignment}}" placeholder="{{ localize 'SW5E.Alignment' }}"/>
</li>
<li>
<input type="text" name="data.details.type" value="{{data.details.type}}" placeholder="{{ localize 'SW5E.Type' }}"/>
</li>
<li>
<input type="text" name="data.details.source" value="{{data.details.source}}" placeholder="{{ localize 'SW5E.Source' }}"/>
</li>
</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>
{{!-- NPC Sheet Navigation --}}
@ -54,7 +100,7 @@
<div class="ability-modifiers flexrow">
<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"/>
<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>
</div>
</li>
@ -69,58 +115,16 @@
<a class="proficiency-toggle skill-proficiency" title="{{skill.hover}}">{{{skill.icon}}}</a>
<h4 class="skill-name rollable">{{skill.label}}</h4>
<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>
</li>
{{/each}}
</ul>
<section class="center-pane">
{{!-- 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 class="center-pane flexcol">
{{!-- Legendary Actions --}}
<div class="counters flexrow">
<div class="counters">
<div class="counter flexrow legendary">
<h4>{{ localize "SW5E.LegAct" }}</h4>
<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">
<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="bonus">{{localize "SW5E.BonusAction"}}</li>
<li class="filter-item" data-filter="reaction">{{localize "SW5E.Reaction"}}</li>
</ul>
</div>
{{/unless}}
<ol class="inventory-list">
{{#each sections as |section sid|}}
@ -17,6 +18,12 @@
<div class="item-detail item-action">{{localize "SW5E.Usage"}}</div>
{{/if}}
{{#if section.columns}}
{{#each section.columns}}
<div class="item-detail {{css}}">{{label}}</div>
{{/each}}
{{/if}}
{{#if ../owner}}
<div class="item-controls">
<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">
{{item.data.subclass}}
</div>
<div class="item-detail">
<div class="item-detail item-action">
Level {{item.data.levels}}
</div>
{{/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}}
<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-delete" title="Delete Item"><i class="fas fa-trash"></i></a>
</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">
<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 data.attributes.powercasting}}
<option value="">{{localize "SW5E.None"}}</option>
@ -9,21 +15,15 @@
{{/each}}
{{/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>
<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="bonus">{{localize "SW5E.BonusAction"}}</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="ritual">{{localize "SW5E.Ritual"}}</li>
<li class="filter-item" data-filter="prepared">{{localize "SW5E.Prepared"}}{{#if preparedPowers}} ({{preparedPowers}}){{/if}}</li>
</ul>
</div>
@ -47,7 +47,7 @@
</a>
{{/if}}
{{ else }}
<span class="power-slots">{{{section.uses}}}</span>
<span>{{{section.uses}}}</span>
<span class="sep"> / </span>
<span class="power-max">{{{section.slots}}}</span>
{{/if}}
@ -72,7 +72,7 @@
<div class="item-name flexrow rollable">
<div class="item-image" style="background-image: url({{item.img}})"></div>
<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>
{{/if}}
</div>

View file

@ -10,95 +10,115 @@
</select>
</div>
{{#unless isVehicle}}
<div class="form-group {{#unless data.traits.senses}}inactive{{/unless}}">
<label>{{localize "SW5E.Senses"}}</label>
<input type="text" name="data.traits.senses" value="{{data.traits.senses}}" placeholder="{{ localize 'SW5E.None' }}"/>
</div>
<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">
{{#each data.traits.languages.selected as |v k|}}
<li class="tag {{k}}">{{v}}</li>
{{/each}}
</ul>
<a class="trait-selector" data-options="languages"><i class="fas fa-edit"></i></a>
</div>
{{/unless}}
<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">
{{#each data.traits.di.selected as |v k|}}
<li class="tag {{k}}">{{v}}</li>
{{/each}}
</ul>
<a class="trait-selector" data-options="damageTypes"><i class="fas fa-edit"></i></a>
</div>
<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">
{{#each data.traits.dr.selected as |v k|}}
<li class="tag {{k}}">{{v}}</li>
{{/each}}
</ul>
<a class="trait-selector" data-options="damageTypes"><i class="fas fa-edit"></i></a>
</div>
<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">
{{#each data.traits.dv.selected as |v k|}}
<li class="tag {{k}}">{{v}}</li>
{{/each}}
</ul>
<a class="trait-selector" data-options="damageTypes"><i class="fas fa-edit"></i></a>
</div>
<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">
{{#each data.traits.ci.selected as |v k|}}
<li class="tag {{k}}">{{v}}</li>
{{/each}}
</ul>
<a class="trait-selector" data-options="conditionTypes"><i class="fas fa-edit"></i></a>
</div>
{{#if isCharacter}}
<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">
{{#each data.traits.weaponProf.selected as |v k|}}
<li class="tag {{k}}">{{v}}</li>
{{/each}}
</ul>
<a class="trait-selector" data-options="weaponProficiencies"><i class="fas fa-edit"></i></a>
</div>
<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">
{{#each data.traits.armorProf.selected as |v k|}}
<li class="tag {{k}}">{{v}}</li>
{{/each}}
</ul>
<a class="trait-selector" data-options="armorProficiencies"><i class="fas fa-edit"></i></a>
</div>
<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">
{{#each data.traits.toolProf.selected as |v k|}}
<li class="tag {{k}}">{{v}}</li>
{{/each}}
</ul>
<a class="trait-selector" data-options="toolProficiencies"><i class="fas fa-edit"></i></a>
</div>
{{/if}}
{{#unless isVehicle}}
<div class="form-group ">
<label>{{localize "SW5E.SpecialTraits"}}</label>
<a class="configure-flags"><i class="fas fa-cog"></i></a>
</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">
<p>{{ localize "SW5E.AbilityUseHint" }} <strong>{{item.name}}</strong> {{ localize "SW5E.Ability" }}.</p>
<p type="note">
{{#if recharges}}
{{ localize "SW5E.AbilityUseRechargeHint" }} <strong>{{#if isCharged}}{{ localize "SW5E.AbilityUseCharged" }}{{else}}{{ localize "SW5E.AbilityUseDepleted" }}{{/if}}</strong>.</p>
{{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>
<p>{{ title }}</p>
<p class="notes">{{note}}</p>
{{#each errors}}
<p class="notification error">{{localize this}}</p>
{{/each}}
{{#if hasPowerSlots}}
<div class="form-group">
<label class="checkbox">{{ localize "SW5E.AbilityUseConsume" }} <input type="checkbox" name="consume" {{checked consume}}/></label>
{{#if hasPlaceableTemplate}}
<div class="form-group">
<label class="checkbox">{{ localize "SW5E.PlaceTemplate" }}
<input type="checkbox" name="placeTemplate" checked/>
</label>
<label>{{ localize "SW5E.PowerCastUpcast" }}</label>
<div class="form-fields">
<select name="level" {{#unless canUpcast}}disabled{{/unless}}>
{{#select item.data.level}}
{{#each powerLevels as |l|}}
<option value="{{l.level}}" {{#unless l.canCast}}disabled{{/unless}}>{{l.label}}</option>
{{/each}}
{{/select}}
</select>
</div>
{{/if}}
</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>

View file

@ -24,7 +24,7 @@
<input type="text" name="{{key}}" value="{{flag.value}}" placeholder="{{flag.placeholder}}" data-dtype="{{flag.type}}"/>
{{/if}}
<p class="notes">{{flag.hint}}</p>
<p class="notes">{{localize flag.hint}}</p>
</div>
{{/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>
{{/unless}}
</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">
{{#each buttons as |button id|}}
<button class="dialog-button" data-button="{{id}}">

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -23,6 +23,21 @@
</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 --}}
<div class="form-group input-select-select">
@ -99,4 +114,28 @@
</select>
</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>
</div>
<ul class="summary">
<ul class="summary flexrow">
<li>
{{labels.level}}
</li>

View file

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

View file

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