From 5bb253d9c3deda9d49f77ff6f961565857d0b4fe Mon Sep 17 00:00:00 2001 From: supervj <64861570+supervj@users.noreply.github.com> Date: Fri, 6 Aug 2021 16:38:15 -0400 Subject: [PATCH] Update Core to 1.4.1 Update Core to 1.4.1 and internal Version to 1.4.1.R1-A8 --- lang/en.json | 41 +- less/original/actors.less | 1323 +++++----- less/original/apps.less | 1135 +++++---- less/original/npc.less | 100 +- less/original/vehicle.less | 53 +- less/update/components/actor-global.less | 2228 +++++++++-------- module/active-effect.js | 113 + module/actor/entity.js | 261 +- module/actor/sheets/newSheet/base.js | 203 +- module/actor/sheets/newSheet/character.js | 4 + module/actor/sheets/newSheet/npc.js | 19 + module/actor/sheets/newSheet/vehicle.js | 53 +- module/actor/sheets/oldSheets/base.js | 203 +- module/actor/sheets/oldSheets/character.js | 5 + module/actor/sheets/oldSheets/npc.js | 19 + module/actor/sheets/oldSheets/vehicle.js | 53 +- module/apps/actor-armor.js | 94 + module/apps/actor-flags.js | 4 +- module/apps/proficiency-selector.js | 120 + module/apps/property-attribution.js | 92 + module/apps/trait-selector.js | 11 +- module/config.js | 179 +- module/item/entity.js | 49 +- module/item/sheet.js | 13 +- module/migration.js | 84 +- module/settings.js | 12 + module/templates.js | 2 + sw5e.js | 25 +- system.json | 315 ++- template.json | 2042 ++++++++------- .../actors/newActor/character-sheet.html | 16 +- templates/actors/newActor/npc-sheet.html | 35 +- .../actors/newActor/parts/swalt-core.html | 4 +- .../newActor/parts/swalt-inventory.html | 2 +- .../actors/newActor/parts/swalt-traits.html | 6 +- .../actors/newActor/parts/swalt-warnings.html | 5 + templates/actors/newActor/vehicle-sheet.html | 18 +- .../actors/oldActor/character-sheet.html | 17 +- templates/actors/oldActor/npc-sheet.html | 25 +- .../actors/oldActor/parts/actor-features.html | 2 +- .../oldActor/parts/actor-inventory.html | 2 +- .../actors/oldActor/parts/actor-traits.html | 6 +- .../actors/oldActor/parts/actor-warnings.html | 5 + templates/actors/oldActor/vehicle-sheet.html | 18 +- templates/actors/parts/active-effects.html | 78 +- templates/apps/actor-armor.html | 21 + templates/apps/property-attribution.html | 16 + templates/apps/trait-selector.html | 36 +- templates/chat/item-card.html | 5 +- templates/items/backpack.html | 13 +- templates/items/class.html | 21 +- templates/items/consumable.html | 10 +- templates/items/equipment.html | 11 +- templates/items/loot.html | 4 +- templates/items/tool.html | 26 +- templates/items/weapon.html | 10 +- 56 files changed, 5440 insertions(+), 3827 deletions(-) create mode 100644 module/active-effect.js create mode 100644 module/apps/actor-armor.js create mode 100644 module/apps/proficiency-selector.js create mode 100644 module/apps/property-attribution.js create mode 100644 templates/actors/newActor/parts/swalt-warnings.html create mode 100644 templates/actors/oldActor/parts/actor-warnings.html create mode 100644 templates/apps/actor-armor.html create mode 100644 templates/apps/property-attribution.html diff --git a/lang/en.json b/lang/en.json index 1decdafc..fa5eb7f4 100644 --- a/lang/en.json +++ b/lang/en.json @@ -41,6 +41,8 @@ "SETTINGS.5eDiagPHB": "PHB: Equidistant (5/5/5)", "SETTINGS.5eInitTBL": "Append the raw Dexterity ability score to break ties in Initiative.", "SETTINGS.5eInitTBN": "Initiative Dexterity Tiebreaker", + "SETTINGS.5eMetricN": "Use Metric Weight Units", + "SETTINGS.5eMetricL": "Replaces all reference to lbs with kgs and updates the encumberance calculations to use metric weight units.", "SETTINGS.5eNoExpL": "Remove experience bars from character sheets.", "SETTINGS.5eNoExpN": "Disable Experience Tracking", "SETTINGS.5eReset": "Reset", @@ -58,6 +60,7 @@ "SW5E.AbbreviationConc": "Conc.", "SW5E.AbbreviationCR": "CR", "SW5E.AbbreviationDC": "DC", + "SW5E.AbbreviationKgs": "kgs", "SW5E.AbbreviationLbs": "lbs.", "SW5E.AbbreviationLevel": "Lvl.", "SW5E.AbbreviationLR": "LR", @@ -127,6 +130,19 @@ "SW5E.ArchetypeName": "Archetype Name", "SW5E.Archetypes": "Archetypes", "SW5E.ArmorClass": "Armor Class", + "SW5E.ArmorClassEquipment": "Equipped Armor", + "SW5E.ArmorClassFlat": "Flat", + "SW5E.ArmorClassUnarmored": "Unarmored", + "SW5E.ArmorClassNatural": "Natural Armor", + "SW5E.ArmorClassMage": "Mage Armor", + "SW5E.ArmorClassDraconic": "Draconic Resilience", + "SW5E.ArmorClassUnarmoredMonk": "Unarmored Defense (Monk)", + "SW5E.ArmorClassUnarmoredBarbarian": "Unarmored Defense (Barbarian)", + "SW5E.ArmorClassCustom": "Custom Formula", + "SW5E.ArmorConfig": "Configure Armor", + "SW5E.ArmorConfigHint": "Fill in the above box to override automatically calculated armor class.", + "SW5E.ArmorClassCalculation": "Calculation", + "SW5E.ArmorClassFormula": "Formula", "SW5E.ArmorProperAbsorptive": "Absorptive", "SW5E.ArmorProperAgile": "Agile", "SW5E.ArmorProperAnchor": "Anchor", @@ -171,6 +187,7 @@ "SW5E.Background": "Background", "SW5E.Biography": "Biography", "SW5E.Bonds": "Bonds", + "SW5E.Bonus": "Bonus", "SW5E.BonusAbilityCheck": "Global Ability Check Bonus", "SW5E.BonusAbilitySave": "Global Saving Throw Bonus", "SW5E.BonusAbilitySkill": "Global Skill Check Bonus", @@ -211,6 +228,7 @@ "SW5E.ClassOriginal": "Original Class", "SW5E.ClassSaves": "Saving Throws", "SW5E.ClassSkillsChosen": "Chosen Class Skills", + "SW5E.ClassSkillsEligible": "Eligible Class Skills", "SW5E.ClassSkillsNumber": "Number of Starting Skills", "SW5E.Collapse": "Collapse/Expand", "SW5E.ComponentMaterial": "Material", @@ -340,6 +358,8 @@ "SW5E.Disadvantage": "Disadvantage", "SW5E.DistAny": "Any", "SW5E.DistFt": "Feet", + "SW5E.DistKm": "Kilometers", + "SW5E.DistM": "Meters", "SW5E.DistMi": "Miles", "SW5E.DistSelf": "Self", "SW5E.DistTouch": "Touch", @@ -351,6 +371,8 @@ "SW5E.EffectInactive": "Inactive Effects", "SW5E.EffectNew": "New Effect", "SW5E.EffectPassive": "Passive Effects", + "SW5E.EffectUnavailable": "Unavailable Effects", + "SW5E.EffectUnavailableInfo": "Source item must be equipped or attuned to activate these", "SW5E.Effects": "Effects", "SW5E.EffectTemporary": "Temporary Effects", "SW5E.EffectsCategoryInactive": "Inactive Effects", @@ -581,11 +603,18 @@ "SW5E.ItemName": "Item Name", "SW5E.ItemNew": "New {type}", "SW5E.ItemNoUses": "{name} has no available uses remaining.", + "SW5E.ItemRarityCommon": "common", + "SW5E.ItemRarityUncommon": "uncommon", + "SW5E.ItemRarityRare": "rare", + "SW5E.ItemRarityVeryRare": "very rare", + "SW5E.ItemRarityLegendary": "legendary", + "SW5E.ItemRarityArtifact": "artifact", "SW5E.ItemRechargeCheck": "{name} recharge check", "SW5E.ItemRechargeFailure": "failure!", "SW5E.ItemRechargeSuccess": "success!", "SW5E.ItemRequiredStr": "Required Strength", "SW5E.ItemToolProficiency": "Tool Proficiency", + "SW5E.ItemToolType": "Tool Type", "SW5E.ItemTypeArchetype": "Archetype", "SW5E.ItemTypeBackground": "Background", "Sw5E.ItemTypeBackgroundPl": "Backgrounds", @@ -892,6 +921,8 @@ "SW5E.Price": "Price", "SW5E.Proficiency": "Proficiency", "SW5E.Proficient": "Proficient", + "SW5E.PropertyBase": "Base", + "SW5E.PropertyTotal": "Total", "SW5E.Quantity": "Quantity", "SW5E.Range": "Range", "SW5E.Rank": "Rank", @@ -1121,7 +1152,7 @@ "SW5E.ToolSurveyor": "Surveyor's Tools", "SW5E.ToolSynthweaver": "Synthweavers's Tools", "SW5E.ToolTinker": "Tinker's Tools", - "SW5E.ToolVehicle": "Vehicle (Land or Water)", + "SW5E.ToolVehicle": "Vehicles", "SW5E.TraitArmorProf": "Armor Proficiencies", "SW5E.TraitSave": "Update", "SW5E.TraitSelectorSpecial": "Special (Split with Semi-Colon)", @@ -1148,10 +1179,16 @@ "SW5E.VehicleEquipment": "Vehicle Equipment", "SW5E.VehicleMishap": "Mishap", "SW5E.VehiclePassengers": "Passengers", + "SW5E.VehicleTypeAir": "Air Vehicle", + "SW5E.VehicleTypeLand": "Land Vehicle", + "SW5E.VehicleTypeWater": "Water Vehicle", "SW5E.VehicleUncrewed": "Uncrewed", "SW5E.Versatile": "Versatile", "SW5E.VersatileDamage": "Versatile Damage", "SW5E.VsDC": "vs DC.", + "SW5E.WarnBadACFormula": "The provided AC formula could not be evaluated.", + "SW5E.WarnMultipleArmor": "More than one suit of armor equipped, AC calculation may be incorrect.", + "SW5E.WarnMultipleShields": "More than one shield equipped, AC calculation may be incorrect.", "SW5E.WeaponAmmo": "Ammunition", "SW5E.WeaponBlasterPistolProficiency": "Blaster Pistol", "SW5E.WeaponChakramProficiency": "Chakrams", @@ -1234,4 +1271,4 @@ "SW5E.WeaponVibrorapierProficiency": "Vibrorapier", "SW5E.WeaponVibrowhipProficiency": "Vibrowhip", "SW5E.Weight": "Weight" -} \ No newline at end of file +} diff --git a/less/original/actors.less b/less/original/actors.less index eef86c31..f3d6c9be 100644 --- a/less/original/actors.less +++ b/less/original/actors.less @@ -3,688 +3,733 @@ @actorNameHeight: 60px; .sw5e.sheet.actor { + /* ----------------------------------------- */ + /* Sheet Header */ + /* ----------------------------------------- */ + .sheet-header { + // Portrait Artwork + img.profile { + flex: 0 0 160px; + max-width: 160px; + height: 160px; + } - /* ----------------------------------------- */ - /* Sheet Header */ - /* ----------------------------------------- */ - .sheet-header { + // Character Name + h1.charname { + flex: 1; + height: @actorNameHeight; + padding: 0; + input { + height: @actorNameHeight - 20px; + margin: 10px 0; + } + } - // Portrait Artwork - img.profile { - flex: 0 0 160px; - max-width: 160px; - height: 160px; + // 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; + } + select { + position: relative; + top: -4px; + } + } + + // 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-name { + position: relative; + } + + .attribute-value { + height: 30px; + line-height: 30px; + } + + // Attribute Configuration + .config-button { + position: absolute; + display: none; + right: -6px; + top: -3px; + font-size: 10px; + font-weight: normal; + } + &:hover .config-button { + display: block; + } + + // Temporary HP + input.temphp { + width: 48%; + } + + // Attribution Tooltip + .property-attribution { + min-width: 150px; + top: 50px; + font-family: Signika, sans-serif; + font-size: 14px; + font-weight: normal; + line-height: 1rem; + } + } + } } - // Character Name - h1.charname { - flex: 1; - height: @actorNameHeight; - padding: 0; - input { - height: @actorNameHeight - 20px; - margin: 10px 0; - } - } + /* ----------------------------------------- */ + /* General Styles */ + /* ----------------------------------------- */ - // 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; - } - } - - // Movement Configuration - .movement, .hit-dice { - h4.attribute-name { - position: relative; - } - .config-button { - position: absolute; - display: none; - right: 0; - top: 1px; - font-size: 12px; - font-weight: normal; - } - &:hover .config-button { - display: block; - } - } - - // Temporary HP - input.temphp { - width: 48%; - } - } - } - - /* ----------------------------------------- */ - /* General Styles */ - /* ----------------------------------------- */ - - // Box Headers - h4.box-title { - height: 18px; - line-height: 16px; - margin: 4px 8px 2px; - .russoOne(14px); - color: @colorOlive; - border-bottom: 1px solid @colorFaint; - white-space: nowrap; - } - - /* ----------------------------------------- */ - /* Attributes */ - /* ----------------------------------------- */ - - .tab.attributes { - overflow: hidden; - } - - ul.attributes { - flex: 0 0 60px; - list-style: none; - margin: 0; - padding: 0; - - li.attribute { - height: 60px; - margin: 0 5px 0 0; - border: @borderGroove; - border-radius: 4px; - text-align: center; - - &:last-child { - margin: 0; - } - - .attribute-value { - display: flex; - justify-content: center; - align-items: center; - height: 28px; - line-height: 28px; - .russoOne(); - - > * { - font-weight: 400; - font-size: 24px; - } - - &.multiple input { - flex: 0 0 33%; - } - } - - .attribute-footer { - flex: 0 0 18px; - margin-top: -1px; - line-height: 18px; - font-family: "Signika", sans-serif; - font-size: 12px; - font-weight: 400; - white-space: nowrap; - } - } - } - - /* ----------------------------------------- */ - /* Ability Scores */ - /* ----------------------------------------- */ - - .ability-scores { - flex: 0 0 100px; - height: 440px; - list-style: none; - margin: 0; - padding: 0; - .russoOne(); - border: @borderGroove; - border-radius: 3px; - - .ability { - height: 70px; - text-align: center; - border-bottom: @borderGroove; - - &:last-child { - border-bottom: none; - margin-bottom: -3px; - } - - input.ability-score { - height: 30px; - width: 36px; - margin: 0 auto; - line-height: 32px; - font-size: 24px; - } - - .ability-modifiers { - height: 24px; - margin: -8px 0 0; - - span.ability-mod, - span.ability-save { - flex: 0 0 24px; - height: 22px; - line-height: 22px; - font-size: 16px; - border-top: @borderGroove; - } - - span.ability-mod { - border-right: @borderGroove; - } - - .ability-proficiency { - line-height: 30px; - } - - span.ability-save { - border-left: @borderGroove; - } - } - } - } - - // Proficiency Toggle Buttons - .proficiency-toggle { - color: @colorBeige; - font-size: 12px; - } - .proficient .proficiency-toggle { - color: @colorOlive; - } - - .locked .proficiency-toggle { - color: @colorBeige; - text-shadow: none; - cursor: default; - } - - /* ----------------------------------------- */ - /* Skills */ - /* ----------------------------------------- */ - - ul.skills-list { - flex: 0 0 180px; - height: 440px; - list-style: none; - margin: 0 5px 0; - padding: 3px 0 2px; - border: @borderGroove; - border-radius: 3px; - - li.skill { - height: 24px; - width: 225px; - padding: 3px 2px; - - &:nth-child(even) { - background: rgba(0, 0, 0, 0.05); - } - - h4 { - flex: 1px; - margin: 0; - font-size: 11px; - line-height: 18px; - } - - .skill-proficiency { - flex: 0 0 16px; - line-height: 18px; - } - - .skill-ability { - flex: 0 0 26px; - text-transform: capitalize; - } - - .skill-mod { - flex: 0 0 20px; - } - - .skill-passive { - flex: 0 0 26px; - text-align: center; - color: @colorTan; - } - } - } - - /* ----------------------------------------- */ - /* Statuses */ - /* ----------------------------------------- */ - - .counters { - flex: none; - padding: 5px 0; - margin: 0; - border-bottom: @borderGroove; - - .counter { - height: 20px; - line-height: 20px; - - h4 { - flex: auto; - margin: 0; - font-size: 13px; - font-weight: bold; + // Box Headers + h4.box-title { + height: 18px; + line-height: 16px; + margin: 4px 8px 2px; + .russoOne(14px); color: @colorOlive; - } + border-bottom: 1px solid @colorFaint; + white-space: nowrap; + } - .counter-value { - flex: none; - text-align: right; - > * { - display: inline; - } - } + /* ----------------------------------------- */ + /* Attributes */ + /* ----------------------------------------- */ - input[type="text"], - input[type="number"] { - height: 20px; - max-width: 20px; + .tab.attributes { + overflow: hidden; + } + + ul.attributes { + flex: 0 0 60px; + list-style: none; margin: 0; padding: 0; - text-align: center; - } - input[type="checkbox"] { + + li.attribute { + height: 60px; + margin: 0 5px 0 0; + border: @borderGroove; + border-radius: 4px; + text-align: center; + + &:last-child { + margin: 0; + } + + .attribute-value { + display: flex; + justify-content: center; + align-items: center; + height: 28px; + line-height: 28px; + .russoOne(); + + > * { + font-weight: 400; + font-size: 24px; + } + + &.multiple input { + flex: 0 0 33%; + } + } + + .attribute-footer { + flex: 0 0 18px; + margin-top: -1px; + line-height: 18px; + font-family: "Signika", sans-serif; + font-size: 12px; + font-weight: 400; + white-space: nowrap; + } + } + } + + .attributable { position: relative; - width: 16px; - height: 16px; + + &:hover .tooltip { + display: block; + } + } + + /* ----------------------------------------- */ + /* Ability Scores */ + /* ----------------------------------------- */ + + .ability-scores { + flex: 0 0 100px; + height: 440px; + list-style: none; margin: 0; - top: 4px; - } - span.sep { + padding: 0; + .russoOne(); + border: @borderGroove; + border-radius: 3px; + + .ability { + height: 70px; + text-align: center; + border-bottom: @borderGroove; + + &:last-child { + border-bottom: none; + margin-bottom: -3px; + } + + input.ability-score { + height: 30px; + width: 36px; + margin: 0 auto; + line-height: 32px; + font-size: 24px; + } + + .ability-modifiers { + height: 24px; + margin: -8px 0 0; + + span.ability-mod, + span.ability-save { + flex: 0 0 24px; + height: 22px; + line-height: 22px; + font-size: 16px; + border-top: @borderGroove; + } + + span.ability-mod { + border-right: @borderGroove; + } + + .ability-proficiency { + line-height: 30px; + } + + span.ability-save { + border-left: @borderGroove; + } + } + } + } + + // Proficiency Toggle Buttons + .proficiency-toggle { + color: @colorBeige; font-size: 12px; - } } - } - - /* ----------------------------------------- */ - /* Traits */ - /* ----------------------------------------- */ - - .center-pane { - height: 100%; - padding: 0 5px 0 3px; - overflow-y: auto; - scrollbar-width: thin; - } - - .traits { - .form-group, .form-group-stacked { - margin: 0 0 3px 0; - justify-content: space-between; - } - .config-button { - flex: 1; + .proficient .proficiency-toggle { + color: @colorOlive; } - label { - flex: none; - line-height: 20px; - font-weight: bold; - margin: 0 10px 0 0; + .locked .proficiency-toggle { + color: @colorBeige; + text-shadow: none; + cursor: default; } - select { - max-width: 200px; + /* ----------------------------------------- */ + /* Skills */ + /* ----------------------------------------- */ + + ul.skills-list { + flex: 0 0 180px; + height: 440px; + list-style: none; + margin: 0 5px 0; + padding: 3px 0 2px; + border: @borderGroove; + border-radius: 3px; + + li.skill { + height: 24px; + width: 225px; + padding: 3px 2px; + + &:nth-child(even) { + background: rgba(0, 0, 0, 0.05); + } + + h4 { + flex: 1px; + margin: 0; + font-size: 11px; + line-height: 18px; + } + + .skill-proficiency { + flex: 0 0 16px; + line-height: 18px; + } + + .skill-ability { + flex: 0 0 26px; + text-transform: capitalize; + } + + .skill-mod { + flex: 0 0 20px; + } + + .skill-passive { + flex: 0 0 26px; + text-align: center; + color: @colorTan; + } + } } - input { - text-align: right; - } + /* ----------------------------------------- */ + /* Statuses */ + /* ----------------------------------------- */ - i.fas { - float: right; - margin-right: 3px; - text-align: right; - color: #999; - - &:hover { - color: #111; - text-shadow: 0 0 10px red; - } - } - - .inactive { - color: @colorTan; - } - } - - /* ----------------------------------------- */ - /* Inventory Lists */ - /* ----------------------------------------- */ - - .tab.features, - .tab.inventory, - .tab.force-powerbook, - .tab.tech-powerbook { - overflow-y: hidden; - } - - /* Inventory List Filters */ - .inventory-filters { - margin: 0 8px; - flex: 0 0 20px; - justify-content: flex-end; - - .currency { - flex: 0 0 100%; - list-style: none; - margin: 4px 0 8px; - padding: 0; - font-size: 12px; - - label { - flex: 0; - margin-left: 8px; - text-align: right; - line-height: 20px; - color: @colorTan; - } - input[type="text"] { - flex: 0 0 48px; - text-align: center; - margin-left: 8px; + .counters { + flex: none; + padding: 5px 0; + margin: 0; border-bottom: @borderGroove; - } + + .counter { + height: 20px; + line-height: 20px; + + h4 { + flex: auto; + margin: 0; + font-size: 13px; + font-weight: bold; + color: @colorOlive; + } + + .counter-value { + flex: none; + text-align: right; + > * { + display: inline; + } + } + + input[type="text"], + input[type="number"] { + height: 20px; + max-width: 20px; + margin: 0; + padding: 0; + text-align: center; + } + input[type="checkbox"] { + position: relative; + width: 16px; + height: 16px; + margin: 0; + top: 4px; + } + span.sep { + font-size: 12px; + } + } } - } - // Inventory item lists - .inventory-list { - padding: 0 5px; - .item { - .item-name { - cursor: pointer; - &.rollable:hover .item-image { - background-image: url("../../icons/svg/d20-grey.svg") !important; + /* ----------------------------------------- */ + /* Traits */ + /* ----------------------------------------- */ + + .center-pane { + height: 100%; + padding: 0 5px 0 3px; + overflow-y: auto; + scrollbar-width: thin; + } + + .traits { + .form-group, + .form-group-stacked { + margin: 0 0 3px 0; + justify-content: space-between; } - &.rollable .item-image:hover { - background-image: url("../../icons/svg/d20-black.svg") !important; + .config-button { + flex: 1; } - i.attuned { color: @colorTan; } - i.not-attuned { color: @colorCrimson; } - } - // Item uses - .item-uses input { - width: 24px; - text-align: center; - } + label { + flex: none; + line-height: 20px; + font-weight: bold; + margin: 0 10px 0 0; + } - // Item Dropdown Properties - .item-properties { - margin-top: 3px; - } + select { + max-width: 200px; + } - // Charged - .item-recharge { - flex: 0 0 80px; + input { + text-align: right; + } + + i.fas { + float: right; + margin-right: 3px; + text-align: right; + color: #999; + + &:hover { + color: #111; + text-shadow: 0 0 10px red; + } + } + + .inactive { + color: @colorTan; + } + } + + /* ----------------------------------------- */ + /* Inventory Lists */ + /* ----------------------------------------- */ + + .tab.features, + .tab.inventory, + .tab.force-powerbook, + .tab.tech-powerbook { + overflow-y: hidden; + } + + /* Inventory List Filters */ + .inventory-filters { + margin: 0 8px; + flex: 0 0 20px; + justify-content: flex-end; + + .currency { + flex: 0 0 100%; + list-style: none; + margin: 4px 0 8px; + padding: 0; + font-size: 12px; + + label { + flex: 0; + margin-left: 8px; + text-align: right; + line-height: 20px; + color: @colorTan; + } + input[type="text"] { + flex: 0 0 48px; + text-align: center; + margin-left: 8px; + border-bottom: @borderGroove; + } + } + } + + // Inventory item lists + .inventory-list { + padding: 0 5px; + .item { + .item-name { + cursor: pointer; + &.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; + } + i.not-attuned { + color: @colorCrimson; + } + } + + // Item uses + .item-uses input { + width: 24px; + text-align: center; + } + + // Item Dropdown Properties + .item-properties { + margin-top: 3px; + } + + // Charged + .item-recharge { + flex: 0 0 80px; + text-align: right; + font-size: 11px; + white-space: nowrap; + } + } + + // Inventory Header + .inventory-header { + .item-controls a.item-create { + flex: 0 0 100%; + } + } + + // Item Detail Sections + .item-detail { + flex: 0 0 70px; + font-size: 12px; + 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; + } + &.attunement { + flex: 0 0 24px; + } + } + .item-weight { + flex: 0 0 60px; + border-left: 1px solid @colorFaint; + border-right: 1px solid @colorFaint; + } + + // Item Control Buttons + .item-controls { + flex: 0 0 44px; + } + + // Item Dropdown Summary + .item-summary { + flex: 0 0 100%; + font-size: 12px; + line-height: 16px; + padding: 0.25em 0.5em; + color: @colorDark; + border-top: 1px solid @colorFaint; + h2 { + font-family: "Russo One"; + font-size: 20px; + font-weight: 400; + text-transform: uppercase; + letter-spacing: 0.5px; + border-bottom: 2px solid rgb(13, 153, 204); + color: #c40f0f; + } + } + } + + /* 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 { + flex: 0 0 240px; + margin: 0; + label, + span { + flex: none; + } + input { + flex: 0 0 28px; + text-align: center; + } + select { + margin: 0 5px; + flex: 0 0 120px; + } + } + + .power-slots, + .power-comps { + flex: none; + padding: 0 5px; + font-size: 12px; + color: @colorTan; + border-right: 1px solid @colorFaint; + } + + .power-slots { + input { + display: inline; + max-width: 20px; + } + .sep { + font-size: 13px; + font-weight: normal; + } + } + + .powerbook .power-uses { + padding-right: 5px; text-align: right; - font-size: 11px; - white-space: nowrap; - } + color: @colorTan; } - // Inventory Header - .inventory-header { - .item-controls a.item-create { - flex: 0 0 100%; - } + .power-school, + .power-action, + .power-target { + flex: 0 0 100px; + font-size: 12px; + color: @colorTan; + text-align: center; + border-right: 1px solid @colorFaint; } - // Item Detail Sections - .item-detail { - flex: 0 0 70px; - font-size: 12px; - 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} - &.attunement {flex: 0 0 24px} - } - .item-weight { - flex: 0 0 60px; - border-left: 1px solid @colorFaint; - border-right: 1px solid @colorFaint; + .power-component { + line-height: 14px; + &.C, + &.R { + display: inline-block; + text-align: center; + padding-top: 1px; + width: 16px; + color: @colorFaint; + background: rgba(0, 0, 0, 0.4); + border: 1px solid transparent; + border-radius: 8px; + } } - // Item Control Buttons - .item-controls { - flex: 0 0 44px; + // Empty powerbook controls + .powerbook-empty .item-controls { + flex: 1; } - // Item Dropdown Summary - .item-summary { - flex: 0 0 100%; - font-size: 12px; - line-height: 16px; - padding: 0.25em 0.5em; - color: @colorDark; - border-top: 1px solid @colorFaint; - h2 { - font-family: 'Russo One'; - font-size: 20px; - font-weight: 400; - text-transform: uppercase; - letter-spacing: 0.5px; - border-bottom: 2px solid rgb(13, 153, 204); - color: #c40f0f; - } - } - } + /* ----------------------------------------- */ + /* Features Tab */ + /* ----------------------------------------- */ - /* 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; + // Original class icon + .features i.original-class { + color: #4b4a44; } - .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; + /* ----------------------------------------- */ + /* TinyMCE */ + /* ----------------------------------------- */ + + .editor { + padding: 0 8px; } - - .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 { - flex: 0 0 240px; - margin: 0; - label, span { - flex: none; - } - input { - flex: 0 0 28px; - text-align: center; - } - select { - margin: 0 5px; - flex: 0 0 120px; - } - } - - .power-slots, - .power-comps { - flex: none; - padding: 0 5px; - font-size: 12px; - color: @colorTan; - border-right: 1px solid @colorFaint; - } - - .power-slots { - input { - display: inline; - max-width: 20px; - } - .sep { - font-size: 13px; - font-weight: normal; - } - } - - .powerbook .power-uses { - padding-right: 5px; - text-align: right; - color: @colorTan; - } - - .power-school, .power-action, .power-target { - flex: 0 0 100px; - font-size: 12px; - color: @colorTan; - text-align: center; - border-right: 1px solid @colorFaint; - } - - .power-component { - line-height: 14px; - &.C, &.R { - display: inline-block; - text-align: center; - padding-top: 1px; - width: 16px; - color: @colorFaint; - background: rgba(0, 0, 0, 0.4); - border: 1px solid transparent; - border-radius: 8px; - } - } - - // Empty powerbook controls - .powerbook-empty .item-controls { flex: 1; } - - /* ----------------------------------------- */ - /* Features Tab */ - /* ----------------------------------------- */ - - // Original class icon - .features i.original-class { - color: #4b4a44 - } - - /* ----------------------------------------- */ - /* TinyMCE */ - /* ----------------------------------------- */ - - .editor { - padding: 0 8px; - } } #actor-flags { - .window-content { - overflow-y: hidden; - } - form { - height: 100%; - } - .form-body { - height: calc(100% - 40px); - padding-right: 8px; - margin-bottom: 4px; - overflow-y: auto; - scrollbar-width: thin; - } + .window-content { + overflow-y: hidden; + } + form { + height: 100%; + } + .form-body { + height: calc(100% - 40px); + padding-right: 8px; + margin-bottom: 4px; + overflow-y: auto; + scrollbar-width: thin; + } } diff --git a/less/original/apps.less b/less/original/apps.less index 67ca5cac..7b0c5612 100644 --- a/less/original/apps.less +++ b/less/original/apps.less @@ -5,137 +5,139 @@ @detailsHeight: 40px; /* ----------------------------------------- */ -/* All DnD5e Apps */ +/* All SW5e Apps */ /* ----------------------------------------- */ .sw5e { - .window-content { - font-size: 13px; - } - - /* ----------------------------------------- */ - /* Element Styles */ - /* ----------------------------------------- */ - - // Item Sheet form fields - input[type="text"], - input[type="number"], - select { - height: calc(100% - 2px); - border: 1px solid @colorTan; - background: rgba(0, 0, 0, 0.05); - color: @colorDark; - } - - // Hovered Fields - input[type="text"], - input[type="number"] { - &:hover, - &:focus { - border: 1px solid #111; - box-shadow: 0 0 8px red; + .window-content { + font-size: 13px; } - } - // Disabled Fields - input:disabled, - select:disabled, - textarea:disabled { - color: @colorOlive; - border: 1px solid transparent !important; - outline: none !important; - &:hover, - &:focus { - box-shadow: none !important; - border: 1px solid transparent !important; - outline: none !important; + /* ----------------------------------------- */ + /* Element Styles */ + /* ----------------------------------------- */ + + // Item Sheet form fields + input[type="text"], + input[type="number"], + select { + height: calc(100% - 2px); + border: 1px solid @colorTan; + background: rgba(0, 0, 0, 0.05); + color: @colorDark; } - } - // Buttons - button { - background: rgba(0, 0, 0, 0.10); - border: @borderGroove; - } - - /* Form Groups */ - .form-group { - label { - flex: 2; - color: @colorOlive; - font-weight: bold; + // Hovered Fields + input[type="text"], + input[type="number"] { + &:hover, + &:focus { + border: 1px solid #111; + box-shadow: 0 0 8px red; + } } - .form-fields { - .flexrow(); - > * { - margin: 0 3px 0 0; - &:last-child { margin-right: 0; } - } - } - } - // Stacked Groups - .form-group.stacked { - > label { - flex: 0 0 100%; - margin: 0; + // Disabled Fields + input:disabled, + select:disabled, + textarea:disabled { + color: @colorOlive; + border: 1px solid transparent !important; + outline: none !important; + &:hover, + &:focus { + box-shadow: none !important; + border: 1px solid transparent !important; + outline: none !important; + } } - label.checkbox, - label.radio { - flex: auto; - text-align: left; + + // Buttons + button { + background: rgba(0, 0, 0, 0.1); + border: @borderGroove; } - } - // Form Headers - .form-header { - margin: 0 0 0.25em 0; - padding: 2px 0; - border-top: @borderGroove; - border-bottom: @borderGroove; - .russoOne(); - } + /* Form Groups */ + .form-group { + label { + flex: 2; + color: @colorOlive; + font-weight: bold; + } + .form-fields { + .flexrow(); + > * { + margin: 0 3px 0 0; + &:last-child { + margin-right: 0; + } + } + } + } - /* Tags */ - .tag { - display: inline-block; - margin: 0 2px 0 0; - padding: 0 3px; - font-size: 10px; - line-height: 16px; - border: 1px solid #999; - border-radius: 3px; - background: rgba(0, 0, 0, 0.05); - } + // Stacked Groups + .form-group.stacked { + > label { + flex: 0 0 100%; + margin: 0; + } + label.checkbox, + label.radio { + flex: auto; + text-align: left; + } + } + + // Form Headers + .form-header { + margin: 0 0 0.25em 0; + padding: 2px 0; + border-top: @borderGroove; + border-bottom: @borderGroove; + .russoOne(); + } + + /* Tags */ + .tag { + display: inline-block; + margin: 0 2px 0 0; + padding: 0 3px; + font-size: 10px; + line-height: 16px; + border: 1px solid #999; + border-radius: 3px; + background: rgba(0, 0, 0, 0.05); + } } - /* ----------------------------------------- */ /* Hit Dice Config Sheet Specifically */ /* ----------------------------------------- */ .sw5e.hd-config { - .form-group { - button.increment, button.decrement { - flex: 0 0 1rem; - line-height: 1rem; - } + .form-group { + button.increment, + button.decrement { + flex: 0 0 1rem; + line-height: 1rem; + } - button.decrement { - margin-right: 0; - } + button.decrement { + margin-right: 0; + } - span.sep { - margin: 0; - } + span.sep { + margin: 0; + } - input { - flex: 0 0 2rem; - text-align: center; - margin-left: 2px; - margin-right: 2px; + input { + flex: 0 0 2rem; + text-align: center; + margin-left: 2px; + margin-right: 2px; + } } - } } /* ----------------------------------------- */ @@ -143,356 +145,459 @@ /* ----------------------------------------- */ .sw5e.sheet { - .window-content { - overflow-y: hidden; - padding: 5px; - background: @sheetBackground; - font-size: 13px; - color: @colorDark; - - form { - height: 100%; - overflow: hidden; - } - - .tab { - height: 100%; - overflow-y: auto; - align-content: flex-start; - } - } - - /* ----------------------------------------- */ - /* Element Styles */ - /* ----------------------------------------- */ - - // Input Fields - input[type="text"], - input[type="number"] { - background: none; - border: 1px solid transparent; - &:hover, - &:focus { - border: 1px solid #111; - } - } - - // Select Fields - select { - flex: 1; - font-size: 12px; - height: 22px; - background: transparent; - } - - // Rollable Titles - .editable .rollable:hover { - cursor: pointer; - } - .editable h4.rollable:hover, - .editable .rollable:hover > h4 { - color: #000; - text-shadow: 0 0 10px red; - } - - // Separators - span.sep { - flex: none; - margin: 0 1px; - display: inline; - position: relative; - color: @colorTan; - font-weight: normal; - } - - /* ----------------------------------------- */ - /* TinyMCE */ - /* ----------------------------------------- */ - - .editor { - height: 100%; - .tox-toolbar-overlord, - .tox-toolbar__primary { - background: none; - } - } - - /* ----------------------------------------- */ - /* Sheet Header */ - /* ----------------------------------------- */ - - .sheet-header { - flex: 0 0 @headerHeight; - border-bottom: @borderGroove; - - .header-details { - .russoOne(); - } - - /* Character Name */ - h1 { - flex: 1; - border-bottom: none; - height: 60px; - margin: 0; - padding: 5px; - - input { - display: block; - height: 50px; - font-size: 32px; - margin: 0; - } - } - - /* Profile Image */ - img.profile { - flex: 0 0 @headerHeight; - max-width: @headerHeight; - height: @headerHeight; - object-fit: contain; - border: none; - border-right: @borderGroove; - } - - /* Header Summary Details */ - .summary { - flex: 0 0 100%; - height: @detailsHeight; - margin: 0; - padding: 0; - list-style: none; - border-top: @borderGroove; - border-bottom: none; - - li { - height: calc(100% - 6px); - float: left; - margin: 2px 0; - padding: 0; - border-right: @borderGroove; - line-height: 34px; - color: @colorOlive; - - &:last-child { - border-right: none; - } - } - } - } - - /* ----------------------------------------- */ - /* Sheet Navigation */ - /* ----------------------------------------- */ - - .sheet-navigation { - flex: 0 0 @navHeight; - margin-bottom: 5px; - .russoOne(14px); - - .item { - height: 30px; - line-height: 32px; - margin: 0 24px; - border-bottom: 3px solid @colorBeige; - - &.active { - border-bottom: 3px solid @colorCrimson; - } - } - } - - /* ----------------------------------------- */ - /* Sheet Body */ - /* ----------------------------------------- */ - - .sheet-body { - flex: 1; - overflow: hidden; - } - - /* ----------------------------------------- */ - /* List Filters */ - /* ----------------------------------------- */ - - .filter-list { - align-items: center; - list-style: none; - margin: 0; - padding: 0; - line-height: 16px; - max-width: 70%; - - .filter-icon { - flex: none; - font-size: 14px; - color: @colorTan; - } - - .filter-item { - text-align: center; - font-size: 12px; - 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; - } - - &.active { - border-bottom: 3px solid @colorCrimson; - } - } - } - - /* ----------------------------------------- */ - /* Trait Lists */ - /* ----------------------------------------- */ - - .traits { - margin: 5px 0 0; - - .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; - } - } - - /* ----------------------------------------- */ - /* Items Lists */ - /* ----------------------------------------- */ - - .items-list { - list-style: none; - margin: 0; - padding: 0; - overflow-y: auto; - scrollbar-width: thin; - color: @colorTan; - - // Child lists - .item-list { - list-style: none; - margin: 0; - padding: 0; - } - - // Item Name - .item-name { - flex: 2; - margin: 0; - overflow: hidden; - font-size: 13px; - text-align: left; - align-items: center; - h3, h4 { - margin: 0; - white-space: nowrap; - overflow-x: hidden; - } - } - - // Control Buttons - .item-controls { - flex: 0 0 60px; - justify-content: space-between; - a { - font-size: 12px; - text-align: center; - } - } - // Individual Item - .item { - align-items: center; - padding: 0 2px; // to align with the header border - border-bottom: 1px solid @colorFaint; - &:last-child { border-bottom: none; } - - .item-name { + .window-content { + overflow-y: hidden; + padding: 5px; + background: @sheetBackground; + font-size: 13px; color: @colorDark; - .item-image { - flex: 0 0 30px; - height: 30px; - background-size: 30px; - border: none; - margin-right: 5px; + + form { + height: 100%; + overflow: hidden; + } + + .tab { + height: 100%; + overflow-y: auto; + align-content: flex-start; } - } } - // Section Header - .items-header { - height: 28px; - margin: 2px 0; - padding: 0; - align-items: center; - background: rgba(0, 0, 0, 0.05); - border: @borderGroove; - font-weight: bold; - > * { + /* ----------------------------------------- */ + /* Element Styles */ + /* ----------------------------------------- */ + + // Input Fields + input[type="text"], + input[type="number"] { + background: none; + border: 1px solid transparent; + &:hover, + &:focus { + border: 1px solid #111; + } + } + + // Select Fields + select { + flex: 1; font-size: 12px; - text-align: center; - } - h3 { - padding-left: 5px; - //.modesto(); - text-align: left; - font-size: 16px; - } + height: 22px; + background: transparent; } - } - /* ----------------------------------------- */ - /* Active Effects */ - /* ----------------------------------------- */ + // Rollable Titles + .editable .rollable:hover { + cursor: pointer; + } + .editable h4.rollable:hover, + .editable .rollable:hover > h4 { + color: #000; + text-shadow: 0 0 10px red; + } - .effects .item { - .effect-source, - .effect-duration, - .effect-controls { - text-align: center; - border-left: 1px solid @colorFaint; - border-right: 1px solid @colorFaint; - font-size: 12px; + // Separators + span.sep { + flex: none; + margin: 0 1px; + display: inline; + position: relative; + color: @colorTan; + font-weight: normal; } - .effect-controls { - border: none; + + /* ----------------------------------------- */ + /* TinyMCE */ + /* ----------------------------------------- */ + + .editor { + height: 100%; + .tox-toolbar-overlord, + .tox-toolbar__primary { + background: none; + } + } + + /* ----------------------------------------- */ + /* Notifications */ + /* ----------------------------------------- */ + + .warnings, + .info { + flex: 0 0 100%; + margin: 0; + padding: 0; + list-style: none; + + .notification { + font-family: "Signika", sans-serif; + font-weight: normal; + font-size: 13px; + box-shadow: none; + padding: 2px 8px; + margin-bottom: 2px; + } + } + + /* ----------------------------------------- */ + /* Sheet Header */ + /* ----------------------------------------- */ + + .sheet-header { + flex: 0 0 @headerHeight; + border-bottom: @borderGroove; + + .header-details { + .russoOne(); + + .summary select { + width: 100%; + height: 100%; + border: 0; + + .modesto(); + text-transform: capitalize; + font-weight: 100; + } + } + + /* Character Name */ + h1 { + flex: 1; + border-bottom: none; + height: 60px; + margin: 0; + padding: 5px; + + input { + display: block; + height: 50px; + font-size: 32px; + margin: 0; + } + } + + /* Profile Image */ + img.profile { + flex: 0 0 @headerHeight; + max-width: @headerHeight; + height: @headerHeight; + object-fit: contain; + border: none; + border-right: @borderGroove; + } + + /* Header Summary Details */ + .summary { + flex: 0 0 100%; + height: @detailsHeight; + margin: 0; + padding: 0; + list-style: none; + border-top: @borderGroove; + border-bottom: none; + + li { + height: calc(100% - 6px); + float: left; + margin: 2px 0; + padding: 0; + border-right: @borderGroove; + line-height: 34px; + color: @colorOlive; + + &:last-child { + border-right: none; + } + } + } + } + + /* ----------------------------------------- */ + /* Sheet Navigation */ + /* ----------------------------------------- */ + + .sheet-navigation { + flex: 0 0 @navHeight; + margin-bottom: 5px; + .russoOne(14px); + + .item { + height: 30px; + line-height: 32px; + margin: 0 24px; + border-bottom: 3px solid @colorBeige; + + &.active { + border-bottom: 3px solid @colorCrimson; + } + } + } + + /* ----------------------------------------- */ + /* Sheet Body */ + /* ----------------------------------------- */ + + .sheet-body { + flex: 1; + overflow: hidden; + } + + /* ----------------------------------------- */ + /* List Filters */ + /* ----------------------------------------- */ + + .filter-list { + align-items: center; + list-style: none; + margin: 0; + padding: 0; + line-height: 16px; + max-width: 70%; + + .filter-icon { + flex: none; + font-size: 14px; + color: @colorTan; + } + + .filter-item { + text-align: center; + font-size: 12px; + 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; + } + + &.active { + border-bottom: 3px solid @colorCrimson; + } + } + } + + /* ----------------------------------------- */ + /* Trait Lists */ + /* ----------------------------------------- */ + + .traits { + margin: 5px 0 0; + + .trait-selector, + .proficiency-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; + } + } + + /* ----------------------------------------- */ + /* Items Lists */ + /* ----------------------------------------- */ + + .items-list { + list-style: none; + margin: 0; + padding: 0; + overflow-y: auto; + scrollbar-width: thin; + color: @colorTan; + + // Child lists + .item-list { + list-style: none; + margin: 0; + padding: 0; + } + + // Item Name + .item-name { + flex: 2; + margin: 0; + overflow: hidden; + font-size: 13px; + text-align: left; + align-items: center; + h3, + h4 { + margin: 0; + white-space: nowrap; + overflow-x: hidden; + } + } + + // Control Buttons + .item-controls { + flex: 0 0 60px; + justify-content: space-between; + a { + font-size: 12px; + text-align: center; + } + } + // Individual Item + .item { + align-items: center; + padding: 0 2px; // to align with the header border + border-bottom: 1px solid @colorFaint; + &:last-child { + border-bottom: none; + } + + .item-name { + color: @colorDark; + .item-image { + flex: 0 0 30px; + height: 30px; + background-size: 30px; + border: none; + margin-right: 5px; + } + } + } + + // Section Header + .items-header { + height: 28px; + margin: 2px 0; + padding: 0; + align-items: center; + background: rgba(0, 0, 0, 0.05); + border: @borderGroove; + font-weight: bold; + > * { + font-size: 12px; + text-align: center; + } + h3 { + padding-left: 5px; + //.modesto(); + text-align: left; + font-size: 16px; + } + } + } + + /* ----------------------------------------- */ + /* Active Effects */ + /* ----------------------------------------- */ + + .effects .item { + .effect-source, + .effect-duration, + .effect-controls { + text-align: center; + border-left: 1px solid @colorFaint; + border-right: 1px solid @colorFaint; + font-size: 12px; + } + .effect-controls { + border: none; + } } - } } - /* ----------------------------------------- */ /* Trait Selector /* ----------------------------------------- */ .trait-selector { - .trait-list { - list-style: none; - margin: 0; - padding: 0; - } + .trait-list { + list-style: none; + margin: 0; + padding: 0; - input[type="text"] { - height: 24px; - margin: 2px; - } + li ol.trait-list { + margin-left: 1.5em; + } + } + + input[type="text"] { + height: 24px; + margin: 2px; + } +} + +/* ----------------------------------------- */ +/* Property Attribution */ +/* ----------------------------------------- */ + +.property-attribution { + display: none; + position: absolute; + padding: 3px; + border: 1px solid #000; + border-radius: 3px; + background-color: rgba(0, 0, 0, 0.8); + backdrop-filter: blur(4px); + color: @colorFaint; + text-align: left; + z-index: 1; + + table { + margin: 0; + border: 0; + tr { + background-color: inherit; + } + td { + padding: 3px; + } + tr.total > td { + font-weight: 600; + padding-top: 5px; + border-top: 1px solid @colorTan; + } + td.attribution-value { + width: 20%; + padding-right: 5px; + text-align: right; + font-weight: 600; + } + + // Arithmetic Operators + td::before { + opacity: 0.6; + } + td.mode-1::before { + // Multiply + content: "×"; + } + td.mode-2::before { + // Add + content: "+"; + } + td.mode-2.negative::before { + // Subtract + content: "−"; + margin-right: -1px; + } + td.mode-3::before { + // Downgrade + content: "↓"; + } + td.mode-4::before { + // Upgrade + content: "↑"; + } + } } /* ----------------------------------------- */ @@ -500,30 +605,41 @@ /* ----------------------------------------- */ .actor-type { - .trait-list { - display: flex; - flex-wrap: wrap; - li { - flex-basis: 50%; - flex-grow: 1; + .trait-list { + display: flex; + flex-wrap: wrap; + li { + flex-basis: 50%; + flex-grow: 1; + } + li.form-group { + flex-basis: 100%; + } } - li.form-group { - flex-basis: 100%; + label.radio { + display: flex; + flex: auto; + font-size: 12px; + line-height: 20px; + font-weight: normal; + > input[type="radio"] { + margin: 0 5px 0 0; + } } - } - label.radio { - display: flex; - flex: auto; - font-size: 12px; - line-height: 20px; - font-weight: normal; - > input[type="radio"] { - margin: 0 5px 0 0; + li.custom-type input[type="radio"] { + display: none; + } +} + +/* ----------------------------------------- */ +/* Armor Config Sheet Specifically */ +/* ----------------------------------------- */ + +.sw5e.actor-armor-config { + .ac-field input { + font-size: 3em; + text-align: center; } - } - li.custom-type input[type="radio"] { - display: none; - } } /* ----------------------------------------- */ @@ -531,56 +647,63 @@ /* ----------------------------------------- */ .sw5e.select-items-prompt { - .dialog-content { - margin-bottom: 1em; - } + .dialog-content { + margin-bottom: 1em; + } - .items-list { - margin-top: 0.5em; - } + .items-list { + margin-top: 0.5em; + } - .item-name > label, .item-image, input { - cursor: pointer; - } + .item-name > label, + .item-image, + input { + cursor: pointer; + } - .item-name > label { - align-items: center; - } -} + .item-name > label { + align-items: center; + } -/* ----------------------------------------- */ -/* HUD + .window-content { + max-height: 90vh; + overflow-y: auto; + } + + /* ----------------------------------------- */ + /* HUD /* ----------------------------------------- */ -.placeable-hud .control-icon { - box-sizing: content-box; - width: 40px; - flex: 0 0 40px; - margin: 8px 0; - font-size: 28px; - line-height: 40px; - text-align: center; - color: #FBF4F4; - background: rgba(0, 0, 0, 0.6); - box-shadow: 0 0 15px #000; - border: 1px solid #333; - border-radius: 8px; - pointer-events: all; + .placeable-hud .control-icon { + box-sizing: content-box; + width: 40px; + flex: 0 0 40px; + margin: 8px 0; + font-size: 28px; + line-height: 40px; + text-align: center; + color: #fbf4f4; + background: rgba(0, 0, 0, 0.6); + box-shadow: 0 0 15px #000; + border: 1px solid #333; + border-radius: 8px; + pointer-events: all; + } + #token-hud .status-effects { + visibility: hidden; + position: absolute; + left: 50px; + top: 0; + display: grid; + padding: 3px; + box-sizing: content-box; + width: 100px; + color: #fbf4f4; + grid-template-columns: 25px 25px 25px 25px; + background: rgba(0, 0, 0, 0.6); + box-shadow: 0 0 15px #000; + border: 1px solid #333; + border-radius: 4px; + pointer-events: all; + } } -#token-hud .status-effects { - visibility: hidden; - position: absolute; - left: 50px; - top: 0; - display: grid; - padding: 3px; - box-sizing: content-box; - width: 100px; - color: #FBF4F4; - grid-template-columns: 25px 25px 25px 25px; - background: rgba(0, 0, 0, 0.6); - box-shadow: 0 0 15px #000; - border: 1px solid #333; - border-radius: 4px; - pointer-events: all; -} \ No newline at end of file diff --git a/less/original/npc.less b/less/original/npc.less index 21cb7be1..6049e32d 100644 --- a/less/original/npc.less +++ b/less/original/npc.less @@ -4,54 +4,58 @@ /* Basic Structure */ /* ----------------------------------------- */ .sw5e.sheet.actor.npc { - min-width: 872px; - min-height: 680px; + min-width: 872px; + 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; - } + .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, + .proficiency { + flex: 0 0 18px; + color: @colorTan; + font-size: 16px; + } + .proficiency { + margin-top: -0.3em; + } } - .experience { - flex: 0 0 18px; - color: @colorTan; - font-size: 16px; + + .summary { + font-size: 18px; + + li.creature-type { + display: flex; + justify-content: space-between; + width: 1em; + padding: 0 3px; + + span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .config-button { + display: none; + font-size: 12px; + font-weight: normal; + line-height: 2em; + } + &:hover .config-button { + display: block; + } + } } - } - - .summary { - font-size: 18px; - - li.creature-type { - display: flex; - justify-content: space-between; - width: 1em; - padding: 0 3px; - - span { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - .config-button { - display: none; - font-size: 12px; - font-weight: normal; - line-height: 2em; - } - &:hover .config-button { - display: block; - } - } - } -} \ No newline at end of file +} diff --git a/less/original/vehicle.less b/less/original/vehicle.less index a9cf6062..4545846f 100644 --- a/less/original/vehicle.less +++ b/less/original/vehicle.less @@ -1,32 +1,35 @@ .sw5e.sheet.actor.vehicle { - .features { - .item-controls { - flex: 0 0 68px; - .item-toggle { - color: #b5b3a4; - &.active { - color: #4b4a44; + min-width: 720px; + min-height: 680px; + + .features { + .item-controls { + flex: 0 0 68px; + .item-toggle { + color: #b5b3a4; + &.active { + color: #4b4a44; + } + } } - } } - } - .counters { - .counter.creature-cap { - .counter-value { - flex: 1; - } + .counters { + .counter.creature-cap { + .counter-value { + flex: 1; + } - input { - max-width: none; - text-align: right; - } - } + input { + max-width: none; + text-align: right; + } + } - .counter.cargo-cap { - input { - max-width: 40px; - text-align: right; - } + .counter.cargo-cap { + input { + max-width: 40px; + text-align: right; + } + } } - } } diff --git a/less/update/components/actor-global.less b/less/update/components/actor-global.less index f32ecc4e..41bbaeb6 100644 --- a/less/update/components/actor-global.less +++ b/less/update/components/actor-global.less @@ -1,1126 +1,1170 @@ .panel { - padding: 8px; - border-radius: 4px; - .dropShadow1(); + padding: 8px; + border-radius: 4px; + .dropShadow1(); } .sw5e.sheet.actor.character { - min-width: 880px; - min-height: 720px; + min-width: 880px; + min-height: 720px; } .sw5e.sheet .window-content { - .openSans(12px); + .openSans(12px); - input, - select { - height: 24px; - line-height: 20px; - padding: 1px 4px; - &:hover { - box-shadow: none; + input, + select { + height: 24px; + line-height: 20px; + padding: 1px 4px; + &:hover { + box-shadow: none; + } + &:focus { + box-shadow: none; + } } - &:focus { - box-shadow: none; - } - } - button { - cursor: pointer; - &:hover, - &:focus { - box-shadow: none; + button { + cursor: pointer; + &:hover, + &:focus { + box-shadow: none; + } } - } } .sw5e.sheet.actor { - input, - select, - textarea { - border-color: transparent; - background: none; - } - .swalt-sheet { - display: grid; - grid-template-rows: 182px 36px auto; - - section > h1 { - .russoOne(17px); - text-align: left; - margin-bottom: 4px; - } - - header { - display: grid; - grid-template-rows: 1fr 26px auto; - grid-template-columns: 128px 1fr; - column-gap: 8px; - grid-row-gap: 8px; - - img { - grid-column-start: 1; - grid-row-start: 1; - grid-row-end: 4; - box-sizing: border-box; - border: none; - border-radius: 0; - max-width: 100%; - max-height: 100%; - } - - h1.character-name { - grid-row: 1; - grid-column: 2; - margin: 0; - border: none; - align-self: center; - height: auto; - - .russoOne(32px); - text-transform: uppercase; - height: auto; - - input[type="text"] { - .russoOne(32px); - text-transform: uppercase; - height: auto; - border: none; - background: none; - &:focus { - text-transform: none; - } - } - } - - .level-experience { - grid-row: 1; - grid-column: 3; - - .charlevel { - .russoOne(17px); - text-align: right; - input { - display: inline-block; - width: 42px; - height: auto; - } - } - - .experience { - .russoOne(17px); - text-align: right; - line-height: 26px; - input { - display: inline-block; - width: 120px; - text-align: right; - } - } - - .xpbar { - height: 8px; - - .bar { - display: block; - height: 100%; - } - } - } - - .summary { - grid-column-start: 2; - grid-row-start: 2; - grid-column-end: 4; - display: grid; - grid-template-rows: 1fr; - grid-template-columns: repeat(4, 1fr); - - input, - .proficiency { - display: inline; - height: auto; - .russoOne(17px); - line-height: 24px; - width: 100%; - } - - .proficiency { - line-height: 26px; - } - } - - .attributes { - grid-column-start: 2; - grid-row-start: 3; - grid-column-end: 4; - display: grid; - grid-template-columns: repeat(5, 1fr); - column-gap: 12px; - - h1 { - text-align: center; - } - - .attribute-value, - .attribute-value input { - .russoOne(22px); - text-align: center; - line-height: 1; - } - - .attribute-value { - &.multiple { - display: grid; - grid-template-columns: auto 14px auto; - - input { - width: 100%; - } - } - - input { - display: inline-block; - } - - .value-number { - display: inline-block; - text-align: right; - - padding: 0 3px; - - &:last-child { - text-align: left; - } - } - - span.value-number { - padding: 1px 4px; - } - - .initiative { - padding: 1px 4px; - display: block; - } - } - - footer { - button { - background: none; - padding: 1px 3px; - font-size: inherit; - line-height: inherit; - display: inline-block; - width: auto; - - &:hover { - font-weight: 400; - } - } - - &.hit-points, - &.hit-dice, - &.initiative { - display: grid; - grid-template-columns: 1fr 1fr; - column-gap: 8px; - margin-top: 0; - input, - button { - //border: 1px solid @colorPaleGray; - width: 100%; - text-align: center; - } - - button { - font-weight: 400; - margin-top: 2px; - } - - span { - display: block; - padding: 3px 4px; - } - } - - &.speed { - margin-top: 0; - input { - text-align: center; - } - } - } - } - } - - nav.sheet-navigation { - display: grid; - grid-template-columns: repeat(7, 1fr); - column-gap: 8px; - margin: 4px 0; - - .item { + input, + select, + textarea { + border-color: transparent; background: none; - border: none; - border-bottom: 3px solid transparent; - border-radius: 0; - margin: 0; - padding: 3px 0 0; - line-height: 1; - .russoOne(16px); - } - } - .editor { - position: static; - min-height: 32px; - padding: 0; - .editor-edit { - display: block; - font-size: 12px; - background: none; - border: none; - padding: 0; - box-shadow: none; - top: 0; - right: 0; - &:hover { - text-shadow: none; - } - } - .tox.tox-tinymce { - height: 250px !important; - } - } - .tab { - display: none; - - &.active { - display: block; - } - - .filter-list { - list-style: none; - margin: 0; - padding: 0 0 8px; - display: flex; - flex-direction: row; - justify-content: flex-end; - max-width: 100%; - - .filter-title { - display: none; - font-weight: bold; - width: 50px; - } - - .filter-item { - width: 100px; - text-align: center; - - & + .filter-item { - margin-left: 12px; - } - - &:hover { - text-shadow: none; - } - } - } - - .group-list-header { - display: grid; - padding-right: 6px; - } - - .group-list-title { - h3 { - .russoOne(17px); - margin: 4px 0 0; - padding: 0 4px; - display: inline; - border: none; - } - - .item-create { - font-size: 12px; - i { - font-size: 10px; - } - &:hover { - text-shadow: none; - } - } - } - - .group-list-header, - .group-list { - .item-detail { - text-align: left; - padding: 4px; - } - } - - .group-list { - height: 100%; - overflow-y: scroll; - & > li:first-child { - padding-top: 8px; - } - } - - .group-list, - .group-list ol { - list-style: none; - margin: 0 0 8px; - padding: 0; - - .item-uses { - input { - display: inline-block; - width: 32px; - margin-right: 0; - text-align: right; - } - span { - padding-left: 8px; - } - .slot-max-override { - margin-left: 5px; - &:hover { - text-shadow: none; - } - } - } - li.item { - display: grid; - - h4 { - .openSans(13px, 700); - letter-spacing: 0; - } - .item-name, - .item-detail { - padding: 4px; - line-height: 30px; - } - - .item-name { - display: flex; - - .item-image { - width: 30px; - height: 30px; - position: relative; - background-size: contain; - &::before { - font-family: "Font Awesome 5 Free"; - font-weight: 900; - content: "\f6cf"; - opacity: 0; - position: absolute; - top: 0; - left: 2px; - font-size: 26px; - } - } - - h4 { - line-height: 30px; - display: inline-block; - height: 30px; - padding-left: 8px; - margin: 0; - } - - &.rollable:hover { - text-shadow: none; - - .item-image { - background-image: none !important; - - &::before { - opacity: 1; - } - - &:hover { - background-image: none !important; - - &::before { - opacity: 1; - } - } - } - } - } - - .item-summary { - grid-column-start: 1; - grid-column-end: -1; - padding: 4px 4px 4px 38px; - } - - .item-controls { - display: flex; - flex-direction: row; - justify-content: space-evenly; - } - - .item-control { - &:hover { - text-shadow: none; - } - } - } - } - - .group-grid-inventory { - grid-template-columns: auto 60px 100px 100px 100px; - - &.group-list-title { - .item-controls { - grid-column-start: 5; - } - } - } - .group-grid-features { - grid-template-columns: auto 100px 100px 100px; - &.group-list-title { - display: grid; - } - .item-controls { - grid-column-start: 4; - } - } - .group-grid-powers { - grid-template-columns: auto repeat(5, 100px); - &.group-list-title { - display: grid; - align-items: end; - .item-detail { - padding: 0 4px; - } - } - } - .group-grid-fav-items { - grid-template-columns: auto 60px 30px 30px 50px; - &.group-list-title { - display: grid; - align-items: end; - .item-detail { - padding: 0 4px; - } - } - } } - .tab > .panel { - height: 100%; - overflow: hidden; - display: grid; - } + .attributable { + position: relative; - .tab.attributes { - &.active { - display: grid; - } - - grid-template-columns: 350px auto; - grid-template-rows: auto; - column-gap: 16px; - - .abilities { - display: grid; - grid-template-columns: 128px auto; - grid-template-rows: auto; - column-gap: 12px; - - ol { - list-style: none; - margin: 0; - padding: 0; - } - - .scores { - li { - border-radius: 0; - padding: 4px; - & + li { - border-top: 0 !important; - } - &:first-child { - border-radius: 4px 4px 0 0; - } - - &:last-child { - border-bottom-width: 1px; - border-radius: 0 0 4px 4px; - } - - h2 { - .russoOne(14px); - border: none; - text-align: center; - margin: 0; - - &:hover { - text-shadow: none; - } - } - - .ability-score { - .russoOne(22px); - text-align: center; - width: 48px; - margin: 0 auto; - height: 24px; - display: block; - } - - .ability-modifiers { - margin: 0 -4px -4px; - display: grid; - grid-template-columns: 28px auto 28px; - - .ability-mod, - .ability-save { - padding: 2px 4px; - display: block; - font-weight: bold; - font-size: 13px; - text-align: center; - border-style: solid; - } - - .ability-mod { - border-width: 1px 1px 0 0; - border-radius: 0 4px 0 0; - } - - .ability-save { - border-width: 1px 0 0 1px; - border-radius: 4px 0 0 0; - } - - .proficiency-toggle { - border: none; - background: none; - line-height: 1; - } - } - } - } - - .skills { - li { - display: grid; - grid-template-columns: 28px auto 18px 28px; - align-items: center; - - .proficiency-toggle { - border: none; - background: none; - height: 23px; - line-height: 23px; - padding: 0 4px; - } - - .skill-name { - &:hover { - text-shadow: none; - } - } - - .skill-ability { - text-transform: capitalize; - } - - .skill-mod { - text-align: right; - padding-right: 4px; - } - } - } - } - - .traits-resources { - grid-template-rows: 32px auto; - nav { - margin-bottom: 4px; - - button { - display: inline-block; - width: auto; - background: none; - border: none; - border-bottom: 3px solid transparent; - border-radius: 0; - margin: 0; - padding: 0 4px; - line-height: 1.6; - .russoOne(14px); - - & + button { - margin-left: 8px; - } - } - } - - section.traits { - display: grid; - grid-template-columns: 1fr 1fr; - grid-gap: 16px; - grid-row-gap: 8px; - - input, - select { + &:hover .tooltip { display: block; - width: 100%; - text-align: left; - } - - label { - font-size: 13px; - } - - .trait-selector { - background: none; - border: none; - display: inline; - width: auto; - &:hover { - text-shadow: none; - } - i.fas { - float: none; - &:hover { - text-shadow: none; - } - } - } - - .powercasting { - text-transform: capitalize; - } - - .languages { - grid-column-end: span 1; - label { - &:hover { - cursor: pointer; - } - } - } - - .traits-list { - li { - display: inline; - - &::after { - content: ","; - } - - &:last-child::after { - content: ""; - } - } - } - - ul.passives { - grid-column-end: span 2; - list-style: none; - padding: 0; - margin: 0; - display: grid; - grid-template-columns: 1fr 1fr; - grid-gap: 4px; - grid-row-gap: 4px; - - strong { - font-size: 13px; - } - } } - - section.resources { - .resource-items { - display: grid; - grid-template-columns: repeat(3, 1fr); - column-gap: 12px; - - .resource { - h1 { - border: none; - margin: 0; - - input { - font-family: "Russo One"; - font-size: 16px; - font-weight: 400; - text-align: center; - margin-bottom: 4px; - border-radius: 0; - } - } - - .attribute-value, - .attribute-value input { - .russoOne(22px); - text-align: center; - line-height: 1; - } - - .attribute-value { - display: grid; - grid-template-columns: auto 14px auto; - - input { - display: block; - width: 100%; - } - - .value-number { - display: block; - width: 100%; - text-align: right; - padding: 0 3px; - &:last-child { - text-align: left; - } - } - - span.value-number { - padding: 1px 4px; - } - } - - .attribute-footer { - margin: 0; - display: grid; - grid-template-columns: 1fr 1fr; - - label { - text-align: center; - } - } - } - } - - .counters { - border: none; - margin: 16px 0; - display: grid; - grid-template-columns: repeat(3, 1fr); - - .counter { - height: auto; - border: none; - text-align: center; - - h4 { - font-size: 13px; - margin: 0; - //display: inline; - &.rollable { - &:hover { - text-shadow: none; - } - } - } - - .counter-value { - display: inline; - text-align: left; - } - - input[type="text"] { - display: inline-block; - width: 10px; - } - - input[type="checkbox"] { - display: inline-block; - } - - .death-success, - .death-fail { - display: inline-block; - } - - .death-success { - margin-right: 8px; - } - } - } - } - } } - - .tab.inventory { - & > .panel { - grid-template-rows: 32px 32px 24px auto; - } - - .currency-encumbrance { - display: grid; - grid-template-columns: 200px auto; - margin-bottom: 8px; - align-items: center; - } - - .currency { - .russoOne(14px); - - input { - display: inline-block; - width: 128px; - .openSans(13px); - } - } - - .encumbrance-wrapper { - display: grid; - grid-template-columns: 400px 100px; - width: 500px; - justify-self: end; - - .encumbrance-label { - font-size: 12px; - line-height: 14px; - width: 100%; - text-shadow: none; - padding: 0; - margin: 0; - height: auto; - text-align: center; - margin-left: -2px; - border-radius: 0 4px 4px 0; - } - - .encumbrance { - position: relative; - border-radius: 4px; - height: 16px; - margin: 0; - width: 100%; - - .encumbrance-bar { - position: absolute; - top: 0; - left: 0; - height: 100%; - border-radius: 4px; - border: none; - } - } - } - } - .tab.features { - & > .panel { - grid-template-rows: 24px auto; - } - } - .tab.force-powerbook, - .tab.tech-powerbook { - .resource-items { - display: grid; - grid-template-columns: repeat(5, 1fr); - column-gap: 24px; - - .resource { - h1 { - border: none; - margin: 0; - font-family: "Russo One"; - font-size: 14px; - font-weight: 400; - text-align: center; - margin-bottom: 4px; - border-radius: 0; - } - - .attribute-value, - .attribute-value input { - .russoOne(22px); - text-align: center; - line-height: 1; - } - - .attribute-value { - display: grid; - grid-template-columns: auto 14px auto; - - input { - display: block; - width: 100%; - } - - .value-number { - display: block; - width: 100%; - text-align: right; - padding: 0 3px; - &:last-child { - text-align: left; - } - } - - span.value-number { - padding: 1px 4px; - } - } - - .attribute-footer { - margin: 0; - display: grid; - grid-template-columns: 1fr 1fr; - flex: 0 0 18px; - margin-top: -1px; - line-height: 18px; - font-family: "Signika", sans-serif; - font-size: 12px; - font-weight: 400; - white-space: nowrap; - - input { - text-align: center; - } - } - } - } - - & > .panel { - grid-template-rows: 64px 32px 24px auto; - } - h3.power-dc { - line-height: 24px; - } - .force-powercasting-ability { - display: grid; - grid-template-columns: 1fr 1fr 1fr 1fr; - label, - h3 { - .russoOne(13px); - border-bottom: none; - } - } - } - .tab.biography { - grid-template-columns: 1fr 2fr; - grid-template-rows: 100%; - column-gap: 16px; - padding-bottom: 8px; - max-width: 100%; - &.active { - display: grid; - } - & > .panel { - display: block; - overflow-y: auto; - } - section { - position: relative; - } - } - .tab.notes { - & > .panel { - display: block; - overflow-y: auto; - } - section { - position: relative; - & > input { - .russoOne(16px); - text-align: left; - margin-bottom: 4px; - } - .editor .editor-edit { - top: 3px; - } - } - } - &.limited { - grid-template-rows: 144px auto; - grid-row-gap: 8px; - header { - grid-template-rows: 1fr; - } - - .tab.biography { - grid-template-columns: 100%; - } - } - } - &.npc { .swalt-sheet { - header { - h1.character-name { - align-self: auto; - } - .npc-size, .creature-type { - .russoOne(18px); - line-height: 28px; + display: grid; + grid-template-rows: 182px 36px auto; + + section > h1 { + .russoOne(17px); + text-align: left; + margin-bottom: 4px; } - div.creature-type { - display: flex; - justify-content: space-between; - padding: 1px 4px; - border: 1px solid transparent; - overflow-x: auto; + header { + display: grid; + grid-template-rows: 1fr 26px auto; + grid-template-columns: 128px 1fr; + column-gap: 8px; + grid-row-gap: 8px; - span { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } + img { + grid-column-start: 1; + grid-row-start: 1; + grid-row-end: 4; + box-sizing: border-box; + border: none; + border-radius: 0; + max-width: 100%; + max-height: 100%; + } - .config-button { + h1.character-name { + grid-row: 1; + grid-column: 2; + margin: 0; + border: none; + align-self: center; + height: auto; + + .russoOne(32px); + text-transform: uppercase; + height: auto; + + input[type="text"] { + .russoOne(32px); + text-transform: uppercase; + height: auto; + border: none; + background: none; + &:focus { + text-transform: none; + } + } + } + + .level-experience { + grid-row: 1; + grid-column: 3; + + .charlevel { + .russoOne(17px); + text-align: right; + input { + display: inline-block; + width: 42px; + height: auto; + } + } + + .experience { + .russoOne(17px); + text-align: right; + line-height: 26px; + input { + display: inline-block; + width: 120px; + text-align: right; + } + } + + .xpbar { + height: 8px; + + .bar { + display: block; + height: 100%; + } + } + } + + .summary { + grid-column-start: 2; + grid-row-start: 2; + grid-column-end: 4; + display: grid; + grid-template-rows: 1fr; + grid-template-columns: repeat(4, 1fr); + + input, + .proficiency { + display: inline; + height: auto; + .russoOne(17px); + line-height: 24px; + width: 100%; + } + + .proficiency { + line-height: 26px; + } + select { + position: relative; + top: -4px; + } + } + + .attributes { + grid-column-start: 2; + grid-row-start: 3; + grid-column-end: 4; + display: grid; + grid-template-columns: repeat(5, 1fr); + column-gap: 12px; + + h1 { + text-align: center; + } + .attribute-name { + position: relative; + } + + .attribute-value, + .attribute-value input { + .russoOne(22px); + text-align: center; + line-height: 1; + } + + .attribute-value { + &.multiple { + display: grid; + grid-template-columns: auto 14px auto; + + input { + width: 100%; + } + } + + input { + display: inline-block; + } + + .value-number { + display: inline-block; + text-align: right; + + padding: 0 3px; + + &:last-child { + text-align: left; + } + } + + span.value-number { + padding: 1px 4px; + } + + .initiative { + padding: 1px 4px; + display: block; + } + } + + // Attribute Configuration + .config-button { + position: absolute; + display: none; + right: -6px; + top: -3px; + font-size: 10px; + font-weight: normal; + } + &:hover .config-button { + display: block; + } + + // Temporary HP + input.temphp { + width: 48%; + } + + // Attribution Tooltip + .property-attribution { + min-width: 150px; + top: 50px; + font-family: Signika, sans-serif; + font-size: 14px; + font-weight: normal; + line-height: 1rem; + } + + footer { + button { + background: none; + padding: 1px 3px; + font-size: inherit; + line-height: inherit; + display: inline-block; + width: auto; + + &:hover { + font-weight: 400; + } + } + + &.hit-points, + &.hit-dice, + &.initiative { + display: grid; + grid-template-columns: 1fr 1fr; + column-gap: 8px; + margin-top: 0; + input, + button { + //border: 1px solid @colorPaleGray; + width: 100%; + text-align: center; + } + + button { + font-weight: 400; + margin-top: 2px; + } + + span { + display: block; + padding: 3px 4px; + } + } + + &.speed { + margin-top: 0; + input { + text-align: center; + } + } + } + } + } + + nav.sheet-navigation { + display: grid; + grid-template-columns: repeat(7, 1fr); + column-gap: 8px; + margin: 4px 0; + + .item { + background: none; + border: none; + border-bottom: 3px solid transparent; + border-radius: 0; + margin: 0; + padding: 3px 0 0; + line-height: 1; + .russoOne(16px); + } + } + .editor { + position: static; + min-height: 32px; + padding: 0; + .editor-edit { + display: block; + font-size: 12px; + background: none; + border: none; + padding: 0; + box-shadow: none; + top: 0; + right: 0; + &:hover { + text-shadow: none; + } + } + .tox.tox-tinymce { + height: 250px !important; + } + } + .tab { display: none; - font-size: 12px; - font-weight: normal; - line-height: 2em; - } - &:hover .config-button { - display: block; - } + + &.active { + display: block; + } + + .filter-list { + list-style: none; + margin: 0; + padding: 0 0 8px; + display: flex; + flex-direction: row; + justify-content: flex-end; + max-width: 100%; + + .filter-title { + display: none; + font-weight: bold; + width: 50px; + } + + .filter-item { + width: 100px; + text-align: center; + + & + .filter-item { + margin-left: 12px; + } + + &:hover { + text-shadow: none; + } + } + } + + .group-list-header { + display: grid; + padding-right: 6px; + } + + .group-list-title { + h3 { + .russoOne(17px); + margin: 4px 0 0; + padding: 0 4px; + display: inline; + border: none; + } + + .item-create { + font-size: 12px; + i { + font-size: 10px; + } + &:hover { + text-shadow: none; + } + } + } + + .group-list-header, + .group-list { + .item-detail { + text-align: left; + padding: 4px; + } + } + + .group-list { + height: 100%; + overflow-y: scroll; + & > li:first-child { + padding-top: 8px; + } + } + + .group-list, + .group-list ol { + list-style: none; + margin: 0 0 8px; + padding: 0; + + .item-uses { + input { + display: inline-block; + width: 32px; + margin-right: 0; + text-align: right; + } + span { + padding-left: 8px; + } + .slot-max-override { + margin-left: 5px; + &:hover { + text-shadow: none; + } + } + } + li.item { + display: grid; + + h4 { + .openSans(13px, 700); + letter-spacing: 0; + } + .item-name, + .item-detail { + padding: 4px; + line-height: 30px; + } + + .item-name { + display: flex; + + .item-image { + width: 30px; + height: 30px; + position: relative; + background-size: contain; + &::before { + font-family: "Font Awesome 5 Free"; + font-weight: 900; + content: "\f6cf"; + opacity: 0; + position: absolute; + top: 0; + left: 2px; + font-size: 26px; + } + } + + h4 { + line-height: 30px; + display: inline-block; + height: 30px; + padding-left: 8px; + margin: 0; + } + + &.rollable:hover { + text-shadow: none; + + .item-image { + background-image: none !important; + + &::before { + opacity: 1; + } + + &:hover { + background-image: none !important; + + &::before { + opacity: 1; + } + } + } + } + } + + .item-summary { + grid-column-start: 1; + grid-column-end: -1; + padding: 4px 4px 4px 38px; + } + + .item-controls { + display: flex; + flex-direction: row; + justify-content: space-evenly; + } + + .item-control { + &:hover { + text-shadow: none; + } + } + } + } + + .group-grid-inventory { + grid-template-columns: auto 60px 100px 100px 100px; + + &.group-list-title { + .item-controls { + grid-column-start: 5; + } + } + } + .group-grid-features { + grid-template-columns: auto 100px 100px 100px; + &.group-list-title { + display: grid; + } + .item-controls { + grid-column-start: 4; + } + } + .group-grid-powers { + grid-template-columns: auto repeat(5, 100px); + &.group-list-title { + display: grid; + align-items: end; + .item-detail { + padding: 0 4px; + } + } + } + .group-grid-fav-items { + grid-template-columns: auto 60px 30px 30px 50px; + &.group-list-title { + display: grid; + align-items: end; + .item-detail { + padding: 0 4px; + } + } + } } - .attributes { - grid-template-columns: repeat(3, 1fr); - footer { - &.proficiency { - margin-top: 0; - line-height: 24px; - text-align: center; - } - &.hit-points { - display: block; - } - } + .tab > .panel { + height: 100%; + overflow: hidden; + display: grid; } - } - nav.sheet-navigation { - grid-template-columns: repeat(6, 1fr); - } - .tab.attributes { - .traits-resources { - display: block; - .counter { - display: flex; - .counter-value { - margin-left: auto; + .tab.attributes { + &.active { + display: grid; + } + + grid-template-columns: 350px auto; + grid-template-rows: auto; + column-gap: 16px; + + .abilities { + display: grid; + grid-template-columns: 128px auto; + grid-template-rows: auto; + column-gap: 12px; + + ol { + list-style: none; + margin: 0; + padding: 0; + } + + .scores { + li { + border-radius: 0; + padding: 4px; + & + li { + border-top: 0 !important; + } + &:first-child { + border-radius: 4px 4px 0 0; + } + + &:last-child { + border-bottom-width: 1px; + border-radius: 0 0 4px 4px; + } + + h2 { + .russoOne(14px); + border: none; + text-align: center; + margin: 0; + + &:hover { + text-shadow: none; + } + } + + .ability-score { + .russoOne(22px); + text-align: center; + width: 48px; + margin: 0 auto; + height: 24px; + display: block; + } + + .ability-modifiers { + margin: 0 -4px -4px; + display: grid; + grid-template-columns: 28px auto 28px; + + .ability-mod, + .ability-save { + padding: 2px 4px; + display: block; + font-weight: bold; + font-size: 13px; + text-align: center; + border-style: solid; + } + + .ability-mod { + border-width: 1px 1px 0 0; + border-radius: 0 4px 0 0; + } + + .ability-save { + border-width: 1px 0 0 1px; + border-radius: 4px 0 0 0; + } + + .proficiency-toggle { + border: none; + background: none; + line-height: 1; + } + } + } + } + + .skills { + li { + display: grid; + grid-template-columns: 28px auto 18px 28px; + align-items: center; + + .proficiency-toggle { + border: none; + background: none; + height: 23px; + line-height: 23px; + padding: 0 4px; + } + + .skill-name { + &:hover { + text-shadow: none; + } + } + + .skill-ability { + text-transform: capitalize; + } + + .skill-mod { + text-align: right; + padding-right: 4px; + } + } + } + } + + .traits-resources { + grid-template-rows: 32px auto; + nav { + margin-bottom: 4px; + + button { + display: inline-block; + width: auto; + background: none; + border: none; + border-bottom: 3px solid transparent; + border-radius: 0; + margin: 0; + padding: 0 4px; + line-height: 1.6; + .russoOne(14px); + + & + button { + margin-left: 8px; + } + } + } + + section.traits { + display: grid; + grid-template-columns: 1fr 1fr; + grid-gap: 16px; + grid-row-gap: 8px; + + input, + select { + display: block; + width: 100%; + text-align: left; + } + + label { + font-size: 13px; + } + + .trait-selector { + background: none; + border: none; + display: inline; + width: auto; + &:hover { + text-shadow: none; + } + i.fas { + float: none; + &:hover { + text-shadow: none; + } + } + } + + .powercasting { + text-transform: capitalize; + } + + .languages { + grid-column-end: span 1; + label { + &:hover { + cursor: pointer; + } + } + } + + .traits-list { + li { + display: inline; + + &::after { + content: ","; + } + + &:last-child::after { + content: ""; + } + } + } + + ul.passives { + grid-column-end: span 2; + list-style: none; + padding: 0; + margin: 0; + display: grid; + grid-template-columns: 1fr 1fr; + grid-gap: 4px; + grid-row-gap: 4px; + + strong { + font-size: 13px; + } + } + } + + section.resources { + .resource-items { + display: grid; + grid-template-columns: repeat(3, 1fr); + column-gap: 12px; + + .resource { + h1 { + border: none; + margin: 0; + + input { + font-family: "Russo One"; + font-size: 16px; + font-weight: 400; + text-align: center; + margin-bottom: 4px; + border-radius: 0; + } + } + + .attribute-value, + .attribute-value input { + .russoOne(22px); + text-align: center; + line-height: 1; + } + + .attribute-value { + display: grid; + grid-template-columns: auto 14px auto; + + input { + display: block; + width: 100%; + } + + .value-number { + display: block; + width: 100%; + text-align: right; + padding: 0 3px; + &:last-child { + text-align: left; + } + } + + span.value-number { + padding: 1px 4px; + } + } + + .attribute-footer { + margin: 0; + display: grid; + grid-template-columns: 1fr 1fr; + + label { + text-align: center; + } + } + } + } + + .counters { + border: none; + margin: 16px 0; + display: grid; + grid-template-columns: repeat(3, 1fr); + + .counter { + height: auto; + border: none; + text-align: center; + + h4 { + font-size: 13px; + margin: 0; + //display: inline; + &.rollable { + &:hover { + text-shadow: none; + } + } + } + + .counter-value { + display: inline; + text-align: left; + } + + input[type="text"] { + display: inline-block; + width: 10px; + } + + input[type="checkbox"] { + display: inline-block; + } + + .death-success, + .death-fail { + display: inline-block; + } + + .death-success { + margin-right: 8px; + } + } + } + } } - } } - } - .tab.force-powerbook, - .tab.tech-powerbook { - input.powercasting-level { - width: 48px; + + .tab.inventory { + & > .panel { + grid-template-rows: 32px 32px 24px auto; + } + + .currency-encumbrance { + display: grid; + grid-template-columns: 200px auto; + margin-bottom: 8px; + align-items: center; + } + + .currency { + .russoOne(14px); + + input { + display: inline-block; + width: 128px; + .openSans(13px); + } + } + + .encumbrance-wrapper { + display: grid; + grid-template-columns: 400px 100px; + width: 500px; + justify-self: end; + + .encumbrance-label { + font-size: 12px; + line-height: 14px; + width: 100%; + text-shadow: none; + padding: 0; + margin: 0; + height: auto; + text-align: center; + margin-left: -2px; + border-radius: 0 4px 4px 0; + } + + .encumbrance { + position: relative; + border-radius: 4px; + height: 16px; + margin: 0; + width: 100%; + + .encumbrance-bar { + position: absolute; + top: 0; + left: 0; + height: 100%; + border-radius: 4px; + border: none; + } + } + } + } + .tab.features { + & > .panel { + grid-template-rows: 24px auto; + } + } + .tab.force-powerbook, + .tab.tech-powerbook { + .resource-items { + display: grid; + grid-template-columns: repeat(5, 1fr); + column-gap: 24px; + + .resource { + h1 { + border: none; + margin: 0; + font-family: "Russo One"; + font-size: 14px; + font-weight: 400; + text-align: center; + margin-bottom: 4px; + border-radius: 0; + } + + .attribute-value, + .attribute-value input { + .russoOne(22px); + text-align: center; + line-height: 1; + } + + .attribute-value { + display: grid; + grid-template-columns: auto 14px auto; + + input { + display: block; + width: 100%; + } + + .value-number { + display: block; + width: 100%; + text-align: right; + padding: 0 3px; + &:last-child { + text-align: left; + } + } + + span.value-number { + padding: 1px 4px; + } + } + + .attribute-footer { + margin: 0; + display: grid; + grid-template-columns: 1fr 1fr; + flex: 0 0 18px; + margin-top: -1px; + line-height: 18px; + font-family: "Signika", sans-serif; + font-size: 12px; + font-weight: 400; + white-space: nowrap; + + input { + text-align: center; + } + } + } + } + + & > .panel { + grid-template-rows: 64px 32px 24px auto; + } + h3.power-dc { + line-height: 24px; + } + .force-powercasting-ability { + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr; + label, + h3 { + .russoOne(13px); + border-bottom: none; + } + } + } + .tab.biography { + grid-template-columns: 1fr 2fr; + grid-template-rows: 100%; + column-gap: 16px; + padding-bottom: 8px; + max-width: 100%; + &.active { + display: grid; + } + & > .panel { + display: block; + overflow-y: auto; + } + section { + position: relative; + } + } + .tab.notes { + & > .panel { + display: block; + overflow-y: auto; + } + section { + position: relative; + & > input { + .russoOne(16px); + text-align: left; + margin-bottom: 4px; + } + .editor .editor-edit { + top: 3px; + } + } + } + &.limited { + grid-template-rows: 144px auto; + grid-row-gap: 8px; + header { + grid-template-rows: 1fr; + } + + .tab.biography { + grid-template-columns: 100%; + } + } + } + &.npc { + .swalt-sheet { + header { + h1.character-name { + align-self: auto; + } + .npc-size, + .creature-type { + .russoOne(18px); + line-height: 28px; + } + + div.creature-type { + display: flex; + justify-content: space-between; + padding: 1px 4px; + border: 1px solid transparent; + overflow-x: auto; + + span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .config-button { + display: none; + font-size: 12px; + font-weight: normal; + line-height: 2em; + } + &:hover .config-button { + display: block; + } + } + + .attributes { + grid-template-columns: repeat(3, 1fr); + footer { + &.proficiency { + margin-top: 0; + line-height: 24px; + text-align: center; + } + &.hit-points { + display: block; + } + } + } + } + nav.sheet-navigation { + grid-template-columns: repeat(6, 1fr); + } + .tab.attributes { + .traits-resources { + display: block; + + .counter { + display: flex; + .counter-value { + margin-left: auto; + } + } + } + } + .tab.force-powerbook, + .tab.tech-powerbook { + input.powercasting-level { + width: 48px; + } + } + .tab.biography.active { + display: block; + } } - } - .tab.biography.active { - display: block; - } } - } } diff --git a/module/active-effect.js b/module/active-effect.js new file mode 100644 index 00000000..2ca55a21 --- /dev/null +++ b/module/active-effect.js @@ -0,0 +1,113 @@ +/** + * Extend the base ActiveEffect class to implement system-specific logic. + * @extends {ActiveEffect} + */ +export default class ActiveEffect5e extends ActiveEffect { + /** + * Is this active effect currently suppressed? + * @type {boolean} + */ + isSuppressed = false; + + /* --------------------------------------------- */ + + /** @inheritdoc */ + apply(actor, change) { + if ( this.isSuppressed ) return null; + return super.apply(actor, change); + } + + /* --------------------------------------------- */ + + /** + * Determine whether this Active Effect is suppressed or not. + */ + determineSuppression() { + this.isSuppressed = false; + if ( this.data.disabled || (this.parent.documentName !== "Actor") ) return; + const [parentType, parentId, documentType, documentId] = this.data.origin?.split(".") ?? []; + if ( (parentType !== "Actor") || (parentId !== this.parent.id) || (documentType !== "Item") ) return; + const item = this.parent.items.get(documentId); + if ( !item ) return; + const itemData = item.data.data; + // If an item is not equipped, or it is equipped but it requires attunement and is not attuned, then disable any + // Active Effects that might have originated from it. + this.isSuppressed = itemData.equipped === false || (itemData.attunement === CONFIG.SW5E.attunementTypes.REQUIRED); + } + + /* --------------------------------------------- */ + + /** + * Manage Active Effect instances through the Actor Sheet via effect control buttons. + * @param {MouseEvent} event The left-click event on the effect control + * @param {Actor|Item} owner The owning entity which manages this effect + */ + static onManageActiveEffect(event, owner) { + event.preventDefault(); + const a = event.currentTarget; + const li = a.closest("li"); + const effect = li.dataset.effectId ? owner.effects.get(li.dataset.effectId) : null; + switch ( a.dataset.action ) { + case "create": + return owner.createEmbeddedDocuments("ActiveEffect", [{ + label: game.i18n.localize("SW5E.EffectNew"), + icon: "icons/svg/aura.svg", + origin: owner.uuid, + "duration.rounds": li.dataset.effectType === "temporary" ? 1 : undefined, + disabled: li.dataset.effectType === "inactive" + }]); + case "edit": + return effect.sheet.render(true); + case "delete": + return effect.delete(); + case "toggle": + return effect.update({disabled: !effect.data.disabled}); + } + } + + /* --------------------------------------------- */ + + /** + * Prepare the data structure for Active Effects which are currently applied to an Actor or Item. + * @param {ActiveEffect[]} effects The array of Active Effect instances to prepare sheet data for + * @return {object} Data for rendering + */ + static prepareActiveEffectCategories(effects) { + // Define effect header categories + const categories = { + temporary: { + type: "temporary", + label: game.i18n.localize("SW5E.EffectTemporary"), + effects: [] + }, + passive: { + type: "passive", + label: game.i18n.localize("SW5E.EffectPassive"), + effects: [] + }, + inactive: { + type: "inactive", + label: game.i18n.localize("SW5E.EffectInactive"), + effects: [] + }, + suppressed: { + type: "suppressed", + label: game.i18n.localize("SW5E.EffectUnavailable"), + effects: [], + info: [game.i18n.localize("SW5E.EffectUnavailableInfo")] + } + }; + + // Iterate over active effects, classifying them into categories + for ( let e of effects ) { + e._getSourceName(); // Trigger a lookup for the source name + if ( e.isSuppressed ) categories.suppressed.effects.push(e); + else if ( e.data.disabled ) categories.inactive.effects.push(e); + else if ( e.isTemporary ) categories.temporary.effects.push(e); + else categories.passive.effects.push(e); + } + + categories.suppressed.hidden = !categories.suppressed.effects.length; + return categories; + } +} diff --git a/module/actor/entity.js b/module/actor/entity.js index 0fe01203..1c88eb5f 100644 --- a/module/actor/entity.js +++ b/module/actor/entity.js @@ -27,7 +27,7 @@ export default class Actor5e extends Actor { */ get classes() { if (this._classes !== undefined) return this._classes; - if (this.data.type !== "character") return (this._classes = {}); + if (!["character", "npc"].includes(this.data.type)) return (this._classes = {}); return (this._classes = this.items .filter((item) => item.type === "class") .reduce((obj, cls) => { @@ -52,6 +52,7 @@ export default class Actor5e extends Actor { /** @override */ prepareData() { + this._preparationWarnings = []; super.prepareData(); // iterate over owned items and recompute attributes that depend on prepared actor data @@ -62,6 +63,7 @@ export default class Actor5e extends Actor { /** @override */ prepareBaseData() { + this._prepareBaseArmorClass(this.data); switch (this.data.type) { case "character": return this._prepareCharacterData(this.data); @@ -74,6 +76,16 @@ export default class Actor5e extends Actor { } } + /* --------------------------------------------- */ + + /** @override */ + applyActiveEffects() { + // The Active Effects do not have access to their parent at preparation time so we wait until this stage to + // determine whether they are suppressed or not. + this.effects.forEach((e) => e.determineSuppression()); + return super.applyActiveEffects(); + } + /* -------------------------------------------- */ /** @override */ @@ -146,6 +158,12 @@ export default class Actor5e extends Actor { // Prepare power-casting data this._computeDerivedPowercasting(this.data); + + // Prepare armor class data + const ac = this._computeArmorClass(data); + this.armor = ac.equippedArmor || null; + this.shield = ac.equippedShield || null; + if (ac.warnings) this._preparationWarnings.push(...ac.warnings); } /* -------------------------------------------- */ @@ -425,6 +443,21 @@ export default class Actor5e extends Actor { /* -------------------------------------------- */ + /** + * Initialize derived AC fields for Active Effects to target. + * @param actorData + * @private + */ + _prepareBaseArmorClass(actorData) { + const ac = actorData.data.attributes.ac; + ac.base = 10; + ac.shield = ac.bonus = ac.cover = 0; + this.armor = null; + this.shield = null; + } + + /* -------------------------------------------- */ + /** * Prepare data related to the power-casting capabilities of the Actor * @private @@ -658,6 +691,103 @@ export default class Actor5e extends Actor { /* -------------------------------------------- */ + /** + * Determine a character's AC value from their equipped armor and shield. + * @param {object} data Note that this object will be mutated. + * @return {{ + * calc: string, + * value: number, + * base: number, + * shield: number, + * bonus: number, + * cover: number, + * flat: number, + * equippedArmor: Item5e, + * equippedShield: Item5e, + * warnings: string[] + * }} + * @private + */ + _computeArmorClass(data) { + // Get AC configuration and apply automatic migrations for older data structures + const ac = data.attributes.ac; + ac.warnings = []; + let cfg = CONFIG.SW5E.armorClasses[ac.calc]; + if (!cfg) { + ac.calc = "flat"; + if (Number.isNumeric(ac.value)) ac.flat = Number(ac.value); + cfg = CONFIG.SW5E.armorClasses.flat; + } + + // Identify Equipped Items + const armorTypes = new Set(Object.keys(CONFIG.SW5E.armorTypes)); + const {armors, shields} = this.itemTypes.equipment.reduce( + (obj, equip) => { + const armor = equip.data.data.armor; + if (!equip.data.data.equipped || !armorTypes.has(armor?.type)) return obj; + if (armor.type === "shield") obj.shields.push(equip); + else obj.armors.push(equip); + return obj; + }, + {armors: [], shields: []} + ); + + // Determine base AC + switch (ac.calc) { + // Flat AC (no additional bonuses) + case "flat": + ac.value = ac.flat; + return ac; + + // Natural AC (includes bonuses) + case "natural": + ac.base = ac.flat; + break; + + // Equipment-based AC + case "default": + if (armors.length) { + if (armors.length > 1) ac.warnings.push("SW5E.WarnMultipleArmor"); + const armorData = armors[0].data.data.armor; + const isHeavy = armorData.type === "heavy"; + ac.dex = isHeavy ? 0 : Math.min(armorData.dex ?? Infinity, data.abilities.dex.mod); + ac.base = (armorData.value ?? 0) + ac.dex; + ac.equippedArmor = armors[0]; + } else { + ac.dex = data.abilities.dex.mod; + ac.base = 10 + ac.dex; + } + break; + + // Formula-based AC + default: + let formula = ac.calc === "custom" ? ac.formula : cfg.formula; + const rollData = this.getRollData(); + try { + const replaced = Roll.replaceFormulaData(formula, rollData); + ac.base = Roll.safeEval(replaced); + } catch (err) { + ac.warnings.push("SW5E.WarnBadACFormula"); + const replaced = Roll.replaceFormulaData(CONFIG.SW5E.armorClasses.default.formula, rollData); + ac.base = Roll.safeEval(replaced); + } + break; + } + + // Equipped Shield + if (shields.length) { + if (shields.length > 1) ac.warnings.push("SW5E.WarnMultipleShields"); + ac.shield = shields[0].data.data.armor.value ?? 0; + ac.equippedShield = shields[0]; + } + + // Compute total AC and return + ac.value = ac.base + ac.shield + ac.bonus + ac.cover; + return ac; + } + + /* -------------------------------------------- */ + /** * Prepare data related to the power-casting capabilities of the Actor * @private @@ -712,7 +842,12 @@ export default class Actor5e extends Actor { if (game.settings.get("sw5e", "currencyWeight") && actorData.data.currency) { const currency = actorData.data.currency; const numCoins = Object.values(currency).reduce((val, denom) => (val += Math.max(denom, 0)), 0); - weight += numCoins / CONFIG.SW5E.encumbrance.currencyPerWeight; + + const currencyPerWeight = game.settings.get("sw5e", "metricWeightUnits") + ? CONFIG.SW5E.encumbrance.currencyPerWeight.metric + : CONFIG.SW5E.encumbrance.currencyPerWeight.imperial; + + weight += numCoins / currencyPerWeight; } // Determine the encumbrance size class @@ -729,9 +864,14 @@ export default class Actor5e extends Actor { // Compute Encumbrance percentage weight = weight.toNearest(0.1); - const max = actorData.data.abilities.str.value * CONFIG.SW5E.encumbrance.strMultiplier * mod; + + const strengthMultiplier = game.settings.get("sw5e", "metricWeightUnits") + ? CONFIG.SW5E.encumbrance.strMultiplier.metric + : CONFIG.SW5E.encumbrance.strMultiplier.imperial; + + const max = (actorData.data.abilities.str.value * strengthMultiplier * mod).toNearest(0.1); const pct = Math.clamped((weight * 100) / max, 0, 100); - return {value: weight.toNearest(0.1), max, pct, encumbered: pct > 2 / 3}; + return {value: weight.toNearest(0.1), max, pct, encumbered: pct > 200 / 3}; } /* -------------------------------------------- */ @@ -741,7 +881,10 @@ export default class Actor5e extends Actor { /** @inheritdoc */ async _preCreate(data, options, user) { await super._preCreate(data, options, user); + const sourceId = this.getFlag("core", "sourceId"); + if (sourceId?.startsWith("Compendium.")) return; + // Some sensible defaults for convenience // Token size category const s = CONFIG.SW5E.tokenSizes[this.data.data.traits.size || "med"]; this.data.token.update({width: s, height: s}); @@ -1044,6 +1187,7 @@ export default class Actor5e extends Actor { // Evaluate a global saving throw bonus const parts = []; const data = {}; + const speaker = options.speaker || ChatMessage.getSpeaker({actor: this}); // Include a global actor ability save bonus const bonuses = foundry.utils.getProperty(this.data.data, "bonuses.abilities") || {}; @@ -1060,7 +1204,7 @@ export default class Actor5e extends Actor { halflingLucky: this.getFlag("sw5e", "halflingLucky"), targetValue: 10, messageData: { - "speaker": options.speaker || ChatMessage.getSpeaker({actor: this}), + "speaker": speaker, "flags.sw5e.roll": {type: "death"} } }); @@ -1192,6 +1336,7 @@ export default class Actor5e extends Actor { * @property {number} dhd Hit dice recovered or spent during the rest. * @property {object} updateData Updates applied to the actor. * @property {Array.} updateItems Updates applied to actor's items. + * @property {boolean} longRest Whether the rest type was a long rest. * @property {boolean} newDay Whether a new day occurred during the rest. */ @@ -1316,7 +1461,8 @@ export default class Actor5e extends Actor { ...hitDiceUpdates, ...this._getRestItemUsesRecovery({recoverLongRestUses: longRest, recoverDailyUses: newDay}) ], - newDay: newDay + longRest, + newDay }; // Perform updates @@ -1326,6 +1472,9 @@ export default class Actor5e extends Actor { // Display a Chat Message summarizing the rest effects if (chat) await this._displayRestResultMessage(result, longRest); + // Call restCompleted hook so that modules can easily perform actions when actors finish a rest + Hooks.callAll("restCompleted", this, result); + // Return data summarizing the rest effects return result; } @@ -1364,13 +1513,13 @@ export default class Actor5e extends Actor { // Determine the chat message to display if (longRest) { message = "SW5E.LongRestResult"; - if (dhp !== 0) message += "HP"; + if (healthRestored) message += "HP"; if (dfp !== 0) message += "FP"; if (dtp !== 0) message += "TP"; - if (dhd !== 0) message += "HD"; + if (diceRestored) message += "HD"; } else { message = "SW5E.ShortRestResultShort"; - if (dhd !== 0 && dhp !== 0) { + if (diceRestored && healthRestored) { if (dtp !== 0) { message = "SW5E.ShortRestResultWithTech"; } else { @@ -1581,19 +1730,19 @@ export default class Actor5e extends Actor { /** * Transform this Actor into another one. * - * @param {Actor} target The target Actor. - * @param {boolean} [keepPhysical] Keep physical abilities (str, dex, con) - * @param {boolean} [keepMental] Keep mental abilities (int, wis, cha) - * @param {boolean} [keepSaves] Keep saving throw proficiencies - * @param {boolean} [keepSkills] Keep skill proficiencies - * @param {boolean} [mergeSaves] Take the maximum of the save proficiencies - * @param {boolean} [mergeSkills] Take the maximum of the skill proficiencies - * @param {boolean} [keepClass] Keep proficiency bonus - * @param {boolean} [keepFeats] Keep features - * @param {boolean} [keepPowers] Keep powers - * @param {boolean} [keepItems] Keep items - * @param {boolean} [keepBio] Keep biography - * @param {boolean} [keepVision] Keep vision + * @param {Actor5e} target The target Actor. + * @param {boolean} [keepPhysical] Keep physical abilities (str, dex, con) + * @param {boolean} [keepMental] Keep mental abilities (int, wis, cha) + * @param {boolean} [keepSaves] Keep saving throw proficiencies + * @param {boolean} [keepSkills] Keep skill proficiencies + * @param {boolean} [mergeSaves] Take the maximum of the save proficiencies + * @param {boolean} [mergeSkills] Take the maximum of the skill proficiencies + * @param {boolean} [keepClass] Keep proficiency bonus + * @param {boolean} [keepFeats] Keep features + * @param {boolean} [keepPowers] Keep powers + * @param {boolean} [keepItems] Keep items + * @param {boolean} [keepBio] Keep biography + * @param {boolean} [keepVision] Keep vision * @param {boolean} [transformTokens] Transform linked tokens too */ async transformInto( @@ -1649,16 +1798,16 @@ export default class Actor5e extends Actor { d.data.attributes.exhaustion = o.data.attributes.exhaustion; // Keep your prior exhaustion level d.data.attributes.inspiration = o.data.attributes.inspiration; // Keep inspiration d.data.powers = o.data.powers; // Keep power slots + d.data.attributes.ac.flat = target.data.data.attributes.ac.value; // Override AC // Token appearance updates d.token = {name: d.name}; for (let k of ["width", "height", "scale", "img", "mirrorX", "mirrorY", "tint", "alpha", "lockRotation"]) { d.token[k] = source.token[k]; } - if (!keepVision) { - for (let k of ["dimSight", "brightSight", "dimLight", "brightLight", "vision", "sightAngle"]) { - d.token[k] = source.token[k]; - } + const vision = keepVision ? o.token : source.token; + for (let k of ["dimSight", "brightSight", "dimLight", "brightLight", "vision", "sightAngle"]) { + d.token[k] = vision[k]; } if (source.token.randomImg) { const images = await target.getTokenImages(); @@ -1745,9 +1894,9 @@ export default class Actor5e extends Actor { const tokens = this.getActiveTokens(true); const updates = tokens.map((t) => { const newTokenData = foundry.utils.deepClone(d.token); - if (!t.data.actorLink) newTokenData.actorData = newActor.data; newTokenData._id = t.data._id; newTokenData.actorId = newActor.id; + newTokenData.actorLink = true; return newTokenData; }); return canvas.scene?.updateEmbeddedDocuments("Token", updates); @@ -1771,10 +1920,25 @@ export default class Actor5e extends Actor { const baseActor = game.actors.get(this.token.data.actorId); const prototypeTokenData = await baseActor.getTokenData(); const tokenUpdate = {actorData: {}}; - for (let k of ["width", "height", "scale", "img", "mirrorX", "mirrorY", "tint", "alpha", "lockRotation"]) { + for (let k of [ + "width", + "height", + "scale", + "img", + "mirrorX", + "mirrorY", + "tint", + "alpha", + "lockRotation", + "name" + ]) { tokenUpdate[k] = prototypeTokenData[k]; } - return this.token.update(tokenUpdate, {recursive: false}); + await this.token.update(tokenUpdate, {recursive: false}); + await this.sheet.close(); + const actor = this.token.getActor(); + actor.sheet.render(true); + return actor; } // Obtain a reference to the original actor @@ -1854,6 +2018,45 @@ export default class Actor5e extends Actor { return type; } + /* -------------------------------------------- */ + + /** + * Populate a proficiency object with a `selected` field containing a combination of + * localizable group & individual proficiencies from `value` and the contents of `custom`. + * + * @param {object} data Object containing proficiency data + * @param {Array.} data.value Array of standard proficiency keys + * @param {string} data.custom Semicolon-separated string of custom proficiencies + * @param {string} type "armor", "weapon", or "tool" + */ + static prepareProficiencies(data, type) { + const profs = CONFIG.SW5E[`${type}Proficiencies`]; + const itemTypes = CONFIG.SW5E[`${type}Ids`]; + + let values = []; + if (data.value) { + values = data.value instanceof Array ? data.value : [data.value]; + } + + data.selected = {}; + const pack = game.packs.get(CONFIG.SW5E.sourcePacks.ITEMS); + for (const key of values) { + if (profs[key]) { + data.selected[key] = profs[key]; + } else if (itemTypes && itemTypes[key]) { + const item = pack.index.get(itemTypes[key]); + data.selected[key] = item.name; + } else if (type === "tool" && CONFIG.SW5E.vehicleTypes[key]) { + data.selected[key] = CONFIG.SW5E.vehicleTypes[key]; + } + } + + // Add custom entries + if (data.custom) { + data.custom.split(";").forEach((c, i) => (data.selected[`custom${i + 1}`] = c.trim())); + } + } + /* -------------------------------------------- */ /* DEPRECATED METHODS */ /* -------------------------------------------- */ diff --git a/module/actor/sheets/newSheet/base.js b/module/actor/sheets/newSheet/base.js index 0b2136bc..a6a40142 100644 --- a/module/actor/sheets/newSheet/base.js +++ b/module/actor/sheets/newSheet/base.js @@ -1,12 +1,16 @@ +import Actor5e from "../../entity.js"; import Item5e from "../../../item/entity.js"; +import ProficiencySelector from "../../../apps/proficiency-selector.js"; +import PropertyAttribution from "../../../apps/property-attribution.js"; import TraitSelector from "../../../apps/trait-selector.js"; +import ActorArmorConfig from "../../../apps/actor-armor.js"; import ActorSheetFlags from "../../../apps/actor-flags.js"; import ActorHitDiceConfig from "../../../apps/hit-dice-config.js"; import ActorMovementConfig from "../../../apps/movement-config.js"; import ActorSensesConfig from "../../../apps/senses-config.js"; import ActorTypeConfig from "../../../apps/actor-type.js"; import {SW5E} from "../../../config.js"; -import {onManageActiveEffect, prepareActiveEffectCategories} from "../../../effects.js"; +import ActiveEffect5e from "../../../active-effect.js"; /** * Extend the basic ActorSheet class to suppose SW5e-specific logic and functionality. @@ -85,6 +89,7 @@ export default class ActorSheet5e extends ActorSheet { // The Actor's data const actorData = this.actor.data.toObject(false); + const source = this.actor.data._source.data; data.actor = actorData; data.data = actorData.data; @@ -105,6 +110,7 @@ export default class ActorSheet5e extends ActorSheet { abl.icon = this._getProficiencyIcon(abl.proficient); abl.hover = CONFIG.SW5E.proficiencyLevels[abl.proficient]; abl.label = CONFIG.SW5E.abilities[a]; + abl.baseProf = source.abilities[a].proficient; } // Skills @@ -117,6 +123,7 @@ export default class ActorSheet5e extends ActorSheet { skl.label = CONFIG.SW5E.starshipSkills[s]; } else { skl.label = CONFIG.SW5E.skills[s]; + skl.baseValue = source.skills[s].value; } } } @@ -134,7 +141,15 @@ export default class ActorSheet5e extends ActorSheet { this._prepareItems(data); // Prepare active effects - data.effects = prepareActiveEffectCategories(this.actor.effects); + data.effects = ActiveEffect5e.prepareActiveEffectCategories(this.actor.effects); + + // Prepare warnings + data.warnings = this.actor._preparationWarnings; + + // Prepare property attributions + this.attribution = { + "attributes.ac": this._prepareArmorClassAttribution(actorData.data) + }; // Return data to the sheet return data; @@ -204,6 +219,105 @@ export default class ActorSheet5e extends ActorSheet { /* -------------------------------------------- */ + /** + * Produce a list of armor class attribution objects. + * @param {object} data Actor data to determine the attributions from. + * @return {AttributionDescription[]} List of attribution descriptions. + */ + _prepareArmorClassAttribution(data) { + const ac = data.attributes.ac; + const cfg = CONFIG.SW5E.armorClasses[ac.calc]; + const attribution = []; + + // Base AC Attribution + switch (ac.calc) { + // Flat AC + case "flat": + return [ + { + label: game.i18n.localize("SW5E.ArmorClassFlat"), + mode: CONST.ACTIVE_EFFECT_MODES.OVERRIDE, + value: ac.flat + } + ]; + + // Natural armor + case "natural": + attribution.push({ + label: game.i18n.localize("SW5E.ArmorClassNatural"), + mode: CONST.ACTIVE_EFFECT_MODES.OVERRIDE, + value: ac.flat + }); + break; + + // Equipment-based AC + case "default": + const hasArmor = !!this.actor.armor; + attribution.push({ + label: hasArmor ? this.actor.armor.name : game.i18n.localize("SW5E.ArmorClassUnarmored"), + mode: CONST.ACTIVE_EFFECT_MODES.OVERRIDE, + value: hasArmor ? this.actor.armor.data.data.armor.value : 10 + }); + if (ac.dex !== 0) { + attribution.push({ + label: game.i18n.localize("SW5E.AbilityDex"), + mode: CONST.ACTIVE_EFFECT_MODES.ADD, + value: ac.dex + }); + } + break; + + // Other AC formula + default: + const formula = ac.calc === "custom" ? ac.formula : cfg.formula; + let base = ac.base; + const dataRgx = new RegExp(/@([a-z.0-9_\-]+)/gi); + for (const [match, term] of formula.matchAll(dataRgx)) { + const value = foundry.utils.getProperty(data, term); + if (term === "attributes.ac.base" || value === 0) continue; + if (Number.isNumeric(value)) base -= Number(value); + attribution.push({ + label: match, + mode: CONST.ACTIVE_EFFECT_MODES.ADD, + value: foundry.utils.getProperty(data, term) + }); + } + attribution.unshift({ + label: game.i18n.localize("SW5E.PropertyBase"), + mode: CONST.ACTIVE_EFFECT_MODES.OVERRIDE, + value: base + }); + break; + } + + // Shield + if (ac.shield !== 0) + attribution.push({ + label: this.actor.shield?.name ?? game.i18n.localize("SW5E.EquipmentShield"), + mode: CONST.ACTIVE_EFFECT_MODES.ADD, + value: ac.shield + }); + + // Bonus + if (ac.bonus !== 0) + attribution.push({ + label: game.i18n.localize("SW5E.Bonus"), + mode: CONST.ACTIVE_EFFECT_MODES.ADD, + value: ac.bonus + }); + + // Cover + if (ac.cover !== 0) + attribution.push({ + label: game.i18n.localize("SW5E.Cover"), + mode: CONST.ACTIVE_EFFECT_MODES.ADD, + value: ac.cover + }); + return attribution; + } + + /* -------------------------------------------- */ + /** * Prepare the data structure for traits data like languages, resistances & vulnerabilities, and proficiencies * @param {object} traits The raw traits data object from the actor data @@ -215,10 +329,7 @@ export default class ActorSheet5e extends ActorSheet { di: CONFIG.SW5E.damageResistanceTypes, dv: CONFIG.SW5E.damageResistanceTypes, ci: CONFIG.SW5E.conditionTypes, - languages: CONFIG.SW5E.languages, - armorProf: CONFIG.SW5E.armorProficiencies, - weaponProf: CONFIG.SW5E.weaponProficiencies, - toolProf: CONFIG.SW5E.toolProficiencies + languages: CONFIG.SW5E.languages }; for (let [t, choices] of Object.entries(map)) { const trait = traits[t]; @@ -238,6 +349,14 @@ export default class ActorSheet5e extends ActorSheet { } trait.cssClass = !isObjectEmpty(trait.selected) ? "" : "inactive"; } + + // Populate and localize proficiencies + for (const t of ["armor", "weapon", "tool"]) { + const trait = traits[`${t}Prof`]; + if (!trait) continue; + Actor5e.prepareProficiencies(trait, t); + trait.cssClass = !isObjectEmpty(trait.selected) ? "" : "inactive"; + } } /* -------------------------------------------- */ @@ -414,6 +533,8 @@ export default class ActorSheet5e extends ActorSheet { // View Item Sheets html.find(".item-edit").click(this._onItemEdit.bind(this)); + // Property attributions + html.find(".attributable").mouseover(this._onPropertyAttribution.bind(this)); // Editable Only Listeners if (this.isEditable) { @@ -429,6 +550,7 @@ export default class ActorSheet5e extends ActorSheet { html.find(".skill-proficiency").on("click contextmenu", this._onCycleSkillProficiency.bind(this)); // Trait Selector + html.find(".proficiency-selector").click(this._onProficiencySelector.bind(this)); html.find(".trait-selector").click(this._onTraitSelector.bind(this)); // Configure Special Flags @@ -446,7 +568,7 @@ export default class ActorSheet5e extends ActorSheet { html.find(".decrement-class-level").click(this._onDecrementClassLevel.bind(this)); // Active Effect management - html.find(".effect-control").click((ev) => onManageActiveEffect(ev, this.actor)); + html.find(".effect-control").click((ev) => ActiveEffect5e.onManageActiveEffect(ev, this.actor)); } // Owner Only Listeners @@ -474,7 +596,7 @@ export default class ActorSheet5e extends ActorSheet { /* -------------------------------------------- */ /** - * Iinitialize Item list filters by activating the set of filters which are currently applied + * Initialize Item list filters by activating the set of filters which are currently applied * @private */ _initializeFilterItemList(i, ul) { @@ -517,6 +639,9 @@ export default class ActorSheet5e extends ActorSheet { const button = event.currentTarget; let app; switch (button.dataset.action) { + case "armor": + app = new ActorArmorConfig(this.object); + break; case "hit-dice": app = new ActorHitDiceConfig(this.object); break; @@ -530,7 +655,7 @@ export default class ActorSheet5e extends ActorSheet { app = new ActorSensesConfig(this.object); break; case "type": - new ActorTypeConfig(this.object).render(true); + app = new ActorTypeConfig(this.object); break; } app?.render(true); @@ -545,22 +670,19 @@ export default class ActorSheet5e extends ActorSheet { */ _onCycleSkillProficiency(event) { event.preventDefault(); - const field = $(event.currentTarget).siblings('input[type="hidden"]'); + const field = event.currentTarget.previousElementSibling; + const skillName = field.parentElement.dataset.skill; + const source = this.actor.data._source.data.skills[skillName]; + if (!source) return; - // Get the current level and the array of levels - const level = parseFloat(field.val()); + // Cycle to the next or previous skill level const levels = [0, 1, 0.5, 2]; - let idx = levels.indexOf(level); - - // Toggle next level - forward on click, backwards on right - if (event.type === "click") { - field.val(levels[idx === levels.length - 1 ? 0 : idx + 1]); - } else if (event.type === "contextmenu") { - field.val(levels[idx === 0 ? levels.length - 1 : idx - 1]); - } + let idx = levels.indexOf(source.value); + const next = idx + (event.type === "click" ? 1 : 3); + field.value = levels[next % 4]; // Update the field value and save the form - this._onSubmit(event); + return this._onSubmit(event); } /* -------------------------------------------- */ @@ -674,7 +796,12 @@ export default class ActorSheet5e extends ActorSheet { if (itemData.type === "consumable" && itemData.flags.core?.sourceId) { const similarItem = this.actor.items.find((i) => { const sourceId = i.getFlag("core", "sourceId"); - return sourceId && sourceId === itemData.flags.core?.sourceId && i.type === "consumable"; + return ( + sourceId && + sourceId === itemData.flags.core?.sourceId && + i.type === "consumable" && + i.name === itemData.name + ); }); if (similarItem && itemData.name !== "Power Cell") { // Always create a new powercell instead of increasing quantity @@ -901,6 +1028,22 @@ export default class ActorSheet5e extends ActorSheet { /* -------------------------------------------- */ + /** + * Handle displaying the property attribution tooltip when a property is hovered over. + * @param {Event} event The originating mouse event. + * @private + */ + async _onPropertyAttribution(event) { + const existingTooltip = event.currentTarget.querySelector("div.tooltip"); + const property = event.currentTarget.dataset.property; + if (existingTooltip || !property || !this.attribution) return; + + let html = await new PropertyAttribution(this.object, this.attribution, property).renderTooltip(); + event.currentTarget.insertAdjacentElement("beforeend", html[0]); + } + + /* -------------------------------------------- */ + /** * Handle rolling an Ability check, either a test or a saving throw * @param {Event} event The originating click event @@ -957,6 +1100,22 @@ export default class ActorSheet5e extends ActorSheet { /* -------------------------------------------- */ + /** + * Handle spawning the ProficiencySelector application to configure armor, weapon, and tool proficiencies. + * @param {Event} event The click event which originated the selection + * @private + */ + _onProficiencySelector(event) { + event.preventDefault(); + const a = event.currentTarget; + const label = a.parentElement.querySelector("label"); + const options = {name: a.dataset.target, title: label.innerText, type: a.dataset.type}; + if (options.type === "tool") options.sortCategories = true; + return new ProficiencySelector(this.actor, options).render(true); + } + + /* -------------------------------------------- */ + /** * Handle spawning the TraitSelector application which allows a checkbox of multiple trait options * @param {Event} event The click event which originated the selection diff --git a/module/actor/sheets/newSheet/character.js b/module/actor/sheets/newSheet/character.js index 91c68b1d..73599b9d 100644 --- a/module/actor/sheets/newSheet/character.js +++ b/module/actor/sheets/newSheet/character.js @@ -62,6 +62,10 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e { return [c.data.data.archetype, c.name, c.data.data.levels].filterJoin(" "); }) .join(", "); + // Weight unit + sheetData["weightUnit"] = game.settings.get("sw5e", "metricWeightUnits") + ? game.i18n.localize("SW5E.AbbreviationKgs") + : game.i18n.localize("SW5E.AbbreviationLbs"); // Return data for rendering return sheetData; diff --git a/module/actor/sheets/newSheet/npc.js b/module/actor/sheets/newSheet/npc.js index 699005ea..2a858d5a 100644 --- a/module/actor/sheets/newSheet/npc.js +++ b/module/actor/sheets/newSheet/npc.js @@ -111,9 +111,28 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e { // Creature Type data.labels["type"] = this.actor.labels.creatureType; + + // Armor Type + data.labels["armorType"] = this.getArmorLabel(); + return data; } + /* -------------------------------------------- */ + + /** + * Format NPC armor information into a localized string. + * @return {string} Formatted armor label. + */ + getArmorLabel() { + const ac = this.actor.data.data.attributes.ac; + const label = []; + if (ac.calc === "default") label.push(this.actor.armor?.name || game.i18n.localize("SW5E.ArmorClassUnarmored")); + else label.push(game.i18n.localize(CONFIG.SW5E.armorClasses[ac.calc].label)); + if (this.actor.shield) label.push(this.actor.shield.name); + return label.filterJoin(", "); + } + /* -------------------------------------------- */ /* Object Updates */ /* -------------------------------------------- */ diff --git a/module/actor/sheets/newSheet/vehicle.js b/module/actor/sheets/newSheet/vehicle.js index a5c6e2ea..66d5bcd9 100644 --- a/module/actor/sheets/newSheet/vehicle.js +++ b/module/actor/sheets/newSheet/vehicle.js @@ -13,7 +13,7 @@ export default class ActorSheet5eVehicle extends ActorSheet5e { static get defaultOptions() { return mergeObject(super.defaultOptions, { classes: ["sw5e", "sheet", "actor", "vehicle"], - width: 605, + width: 720, height: 680 }); } @@ -47,10 +47,17 @@ export default class ActorSheet5eVehicle extends ActorSheet5e { _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; + + const currencyPerWeight = game.settings.get("sw5e", "metricWeightUnits") + ? CONFIG.SW5E.encumbrance.currencyPerWeight.metric + : CONFIG.SW5E.encumbrance.currencyPerWeight.imperial; + + totalWeight += totalCoins / currencyPerWeight; // Vehicle weights are an order of magnitude greater. - totalWeight /= CONFIG.SW5E.encumbrance.vehicleWeightMultiplier; + totalWeight /= game.settings.get("sw5e", "metricWeightUnits") + ? CONFIG.SW5E.encumbrance.vehicleWeightMultiplier.metric + : CONFIG.SW5E.encumbrance.vehicleWeightMultiplier.imperial; // Compute overall encumbrance const max = actorData.data.attributes.capacity.cargo; @@ -80,12 +87,10 @@ export default class ActorSheet5eVehicle extends ActorSheet5e { // 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 === 0.5) item.cover = "½"; else if (item.data.cover === 0.75) item.cover = "¾"; else if (item.data.cover === null) item.cover = "—"; - if (item.crew < 1 || item.crew === null) item.crew = "—"; } // Prepare vehicle weapons @@ -114,7 +119,8 @@ export default class ActorSheet5eVehicle extends ActorSheet5e { { label: game.i18n.localize("SW5E.Quantity"), css: "item-qty", - property: "data.quantity" + property: "data.quantity", + editable: "Number" }, { label: game.i18n.localize("SW5E.AC"), @@ -138,14 +144,10 @@ export default class ActorSheet5eVehicle extends ActorSheet5e { actions: { label: game.i18n.localize("SW5E.ActionPl"), items: [], + hasActions: true, 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", @@ -179,6 +181,13 @@ export default class ActorSheet5eVehicle extends ActorSheet5e { } }; + data.items.forEach((item) => { + 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; + }); + const cargo = { crew: { label: game.i18n.localize("SW5E.VehicleCrew"), @@ -284,6 +293,10 @@ export default class ActorSheet5eVehicle extends ActorSheet5e { .click((evt) => evt.target.select()) .change(this._onCargoRowChange.bind(this)); + html.find(".item-qty:not(.cargo) input") + .click((evt) => evt.target.select()) + .change(this._onQtyChange.bind(this)); + if (this.actor.data.data.attributes.actions.stations) { html.find(".counter.actions, .counter.action-thresholds").hide(); } @@ -416,6 +429,24 @@ export default class ActorSheet5eVehicle extends ActorSheet5e { /* -------------------------------------------- */ + /** + * Special handling for editing quantity value of equipment and weapons inside the features tab. + * @param event {Event} + * @returns {Promise} + * @private + */ + + _onQtyChange(event) { + event.preventDefault(); + const itemID = event.currentTarget.closest(".item").dataset.itemId; + const item = this.actor.items.get(itemID); + const qty = parseInt(event.currentTarget.value); + event.currentTarget.value = qty; + return item.update({"data.quantity": qty}); + } + + /* -------------------------------------------- */ + /** * Handle toggling an item's crewed status. * @param event {Event} diff --git a/module/actor/sheets/oldSheets/base.js b/module/actor/sheets/oldSheets/base.js index acab8b2a..62e9220c 100644 --- a/module/actor/sheets/oldSheets/base.js +++ b/module/actor/sheets/oldSheets/base.js @@ -1,4 +1,7 @@ +import Actor5e from "../../entity.js"; import Item5e from "../../../item/entity.js"; +import ProficiencySelector from "../../../apps/proficiency-selector.js"; +import PropertyAttribution from "../../../apps/property-attribution.js"; import TraitSelector from "../../../apps/trait-selector.js"; import ActorSheetFlags from "../../../apps/actor-flags.js"; import ActorHitDiceConfig from "../../../apps/hit-dice-config.js"; @@ -6,7 +9,7 @@ import ActorMovementConfig from "../../../apps/movement-config.js"; import ActorSensesConfig from "../../../apps/senses-config.js"; import ActorTypeConfig from "../../../apps/actor-type.js"; import {SW5E} from "../../../config.js"; -import {onManageActiveEffect, prepareActiveEffectCategories} from "../../../effects.js"; +import ActiveEffect5e from "../../../active-effect.js"; /** * Extend the basic ActorSheet class to suppose SW5e-specific logic and functionality. @@ -82,6 +85,7 @@ export default class ActorSheet5e extends ActorSheet { // The Actor's data const actorData = this.actor.data.toObject(false); + const source = this.actor.data._source.data; data.actor = actorData; data.data = actorData.data; @@ -102,6 +106,7 @@ export default class ActorSheet5e extends ActorSheet { abl.icon = this._getProficiencyIcon(abl.proficient); abl.hover = CONFIG.SW5E.proficiencyLevels[abl.proficient]; abl.label = CONFIG.SW5E.abilities[a]; + abl.baseProf = source.abilities[a].proficient; } // Skills @@ -111,6 +116,7 @@ export default class ActorSheet5e extends ActorSheet { skl.icon = this._getProficiencyIcon(skl.value); skl.hover = CONFIG.SW5E.proficiencyLevels[skl.value]; skl.label = CONFIG.SW5E.skills[s]; + skl.baseValue = source.skills[s].value; } } @@ -127,7 +133,15 @@ export default class ActorSheet5e extends ActorSheet { this._prepareItems(data); // Prepare active effects - data.effects = prepareActiveEffectCategories(this.actor.effects); + data.effects = ActiveEffect5e.prepareActiveEffectCategories(this.actor.effects); + + // Prepare warnings + data.warnings = this.actor._preparationWarnings; + + // Prepare property attributions + this.attribution = { + "attributes.ac": this._prepareArmorClassAttribution(actorData.data) + }; // Return data to the sheet return data; @@ -197,6 +211,105 @@ export default class ActorSheet5e extends ActorSheet { /* -------------------------------------------- */ + /** + * Produce a list of armor class attribution objects. + * @param {object} data Actor data to determine the attributions from. + * @return {AttributionDescription[]} List of attribution descriptions. + */ + _prepareArmorClassAttribution(data) { + const ac = data.attributes.ac; + const cfg = CONFIG.SW5E.armorClasses[ac.calc]; + const attribution = []; + + // Base AC Attribution + switch (ac.calc) { + // Flat AC + case "flat": + return [ + { + label: game.i18n.localize("SW5E.ArmorClassFlat"), + mode: CONST.ACTIVE_EFFECT_MODES.OVERRIDE, + value: ac.flat + } + ]; + + // Natural armor + case "natural": + attribution.push({ + label: game.i18n.localize("SW5E.ArmorClassNatural"), + mode: CONST.ACTIVE_EFFECT_MODES.OVERRIDE, + value: ac.flat + }); + break; + + // Equipment-based AC + case "default": + const hasArmor = !!this.actor.armor; + attribution.push({ + label: hasArmor ? this.actor.armor.name : game.i18n.localize("SW5E.ArmorClassUnarmored"), + mode: CONST.ACTIVE_EFFECT_MODES.OVERRIDE, + value: hasArmor ? this.actor.armor.data.data.armor.value : 10 + }); + if (ac.dex !== 0) { + attribution.push({ + label: game.i18n.localize("SW5E.AbilityDex"), + mode: CONST.ACTIVE_EFFECT_MODES.ADD, + value: ac.dex + }); + } + break; + + // Other AC formula + default: + const formula = ac.calc === "custom" ? ac.formula : cfg.formula; + let base = ac.base; + const dataRgx = new RegExp(/@([a-z.0-9_\-]+)/gi); + for (const [match, term] of formula.matchAll(dataRgx)) { + const value = foundry.utils.getProperty(data, term); + if (term === "attributes.ac.base" || value === 0) continue; + if (Number.isNumeric(value)) base -= Number(value); + attribution.push({ + label: match, + mode: CONST.ACTIVE_EFFECT_MODES.ADD, + value: foundry.utils.getProperty(data, term) + }); + } + attribution.unshift({ + label: game.i18n.localize("SW5E.PropertyBase"), + mode: CONST.ACTIVE_EFFECT_MODES.OVERRIDE, + value: base + }); + break; + } + + // Shield + if (ac.shield !== 0) + attribution.push({ + label: this.actor.shield?.name ?? game.i18n.localize("SW5E.EquipmentShield"), + mode: CONST.ACTIVE_EFFECT_MODES.ADD, + value: ac.shield + }); + + // Bonus + if (ac.bonus !== 0) + attribution.push({ + label: game.i18n.localize("SW5E.Bonus"), + mode: CONST.ACTIVE_EFFECT_MODES.ADD, + value: ac.bonus + }); + + // Cover + if (ac.cover !== 0) + attribution.push({ + label: game.i18n.localize("SW5E.Cover"), + mode: CONST.ACTIVE_EFFECT_MODES.ADD, + value: ac.cover + }); + return attribution; + } + + /* -------------------------------------------- */ + /** * Prepare the data structure for traits data like languages, resistances & vulnerabilities, and proficiencies * @param {object} traits The raw traits data object from the actor data @@ -208,10 +321,7 @@ export default class ActorSheet5e extends ActorSheet { di: CONFIG.SW5E.damageResistanceTypes, dv: CONFIG.SW5E.damageResistanceTypes, ci: CONFIG.SW5E.conditionTypes, - languages: CONFIG.SW5E.languages, - armorProf: CONFIG.SW5E.armorProficiencies, - weaponProf: CONFIG.SW5E.weaponProficiencies, - toolProf: CONFIG.SW5E.toolProficiencies + languages: CONFIG.SW5E.languages }; for (let [t, choices] of Object.entries(map)) { const trait = traits[t]; @@ -231,6 +341,14 @@ export default class ActorSheet5e extends ActorSheet { } trait.cssClass = !isObjectEmpty(trait.selected) ? "" : "inactive"; } + + // Populate and localize proficiencies + for (const t of ["armor", "weapon", "tool"]) { + const trait = traits[`${t}Prof`]; + if (!trait) continue; + Actor5e.prepareProficiencies(trait, t); + trait.cssClass = !isObjectEmpty(trait.selected) ? "" : "inactive"; + } } /* -------------------------------------------- */ @@ -418,6 +536,9 @@ export default class ActorSheet5e extends ActorSheet { // View Item Sheets html.find(".item-edit").click(this._onItemEdit.bind(this)); + // Property attributions + html.find(".attributable").mouseover(this._onPropertyAttribution.bind(this)); + // Editable Only Listeners if (this.isEditable) { // Input focus and update @@ -432,6 +553,7 @@ export default class ActorSheet5e extends ActorSheet { html.find(".skill-proficiency").on("click contextmenu", this._onCycleSkillProficiency.bind(this)); // Trait Selector + html.find(".proficiency-selector").click(this._onProficiencySelector.bind(this)); html.find(".trait-selector").click(this._onTraitSelector.bind(this)); // Configure Special Flags @@ -446,7 +568,7 @@ export default class ActorSheet5e extends ActorSheet { html.find(".slot-max-override").click(this._onPowerSlotOverride.bind(this)); // Active Effect management - html.find(".effect-control").click((ev) => onManageActiveEffect(ev, this.actor)); + html.find(".effect-control").click((ev) => ActiveEffect5e.onManageActiveEffect(ev, this.actor)); } // Owner Only Listeners @@ -474,7 +596,7 @@ export default class ActorSheet5e extends ActorSheet { /* -------------------------------------------- */ /** - * Iinitialize Item list filters by activating the set of filters which are currently applied + * Initialize Item list filters by activating the set of filters which are currently applied * @private */ _initializeFilterItemList(i, ul) { @@ -517,6 +639,9 @@ export default class ActorSheet5e extends ActorSheet { const button = event.currentTarget; let app; switch (button.dataset.action) { + case "armor": + app = new ActorArmorConfig(this.object); + break; case "hit-dice": app = new ActorHitDiceConfig(this.object); break; @@ -530,7 +655,7 @@ export default class ActorSheet5e extends ActorSheet { app = new ActorSensesConfig(this.object); break; case "type": - new ActorTypeConfig(this.object).render(true); + app = new ActorTypeConfig(this.object); break; } app?.render(true); @@ -545,22 +670,19 @@ export default class ActorSheet5e extends ActorSheet { */ _onCycleSkillProficiency(event) { event.preventDefault(); - const field = $(event.currentTarget).siblings('input[type="hidden"]'); + const field = event.currentTarget.previousElementSibling; + const skillName = field.parentElement.dataset.skill; + const source = this.actor.data._source.data.skills[skillName]; + if (!source) return; - // Get the current level and the array of levels - const level = parseFloat(field.val()); + // Cycle to the next or previous skill level const levels = [0, 1, 0.5, 2]; - let idx = levels.indexOf(level); - - // Toggle next level - forward on click, backwards on right - if (event.type === "click") { - field.val(levels[idx === levels.length - 1 ? 0 : idx + 1]); - } else if (event.type === "contextmenu") { - field.val(levels[idx === 0 ? levels.length - 1 : idx - 1]); - } + let idx = levels.indexOf(source.value); + const next = idx + (event.type === "click" ? 1 : 3); + field.value = levels[next % 4]; // Update the field value and save the form - this._onSubmit(event); + return this._onSubmit(event); } /* -------------------------------------------- */ @@ -675,7 +797,12 @@ export default class ActorSheet5e extends ActorSheet { if (itemData.type === "consumable" && itemData.flags.core?.sourceId) { const similarItem = this.actor.items.find((i) => { const sourceId = i.getFlag("core", "sourceId"); - return sourceId && sourceId === itemData.flags.core?.sourceId && i.type === "consumable"; + return ( + sourceId && + sourceId === itemData.flags.core?.sourceId && + i.type === "consumable" && + i.name === itemData.name + ); }); if (similarItem) { return similarItem.update({ @@ -832,6 +959,22 @@ export default class ActorSheet5e extends ActorSheet { /* -------------------------------------------- */ + /** + * Handle displaying the property attribution tooltip when a property is hovered over. + * @param {Event} event The originating mouse event. + * @private + */ + async _onPropertyAttribution(event) { + const existingTooltip = event.currentTarget.querySelector("div.tooltip"); + const property = event.currentTarget.dataset.property; + if (existingTooltip || !property || !this.attribution) return; + + let html = await new PropertyAttribution(this.object, this.attribution, property).renderTooltip(); + event.currentTarget.insertAdjacentElement("beforeend", html[0]); + } + + /* -------------------------------------------- */ + /** * Handle rolling an Ability check, either a test or a saving throw * @param {Event} event The originating click event @@ -888,6 +1031,22 @@ export default class ActorSheet5e extends ActorSheet { /* -------------------------------------------- */ + /** + * Handle spawning the ProficiencySelector application to configure armor, weapon, and tool proficiencies. + * @param {Event} event The click event which originated the selection + * @private + */ + _onProficiencySelector(event) { + event.preventDefault(); + const a = event.currentTarget; + const label = a.parentElement.querySelector("label"); + const options = {name: a.dataset.target, title: label.innerText, type: a.dataset.type}; + if (options.type === "tool") options.sortCategories = true; + return new ProficiencySelector(this.actor, options).render(true); + } + + /* -------------------------------------------- */ + /** * Handle spawning the TraitSelector application which allows a checkbox of multiple trait options * @param {Event} event The click event which originated the selection diff --git a/module/actor/sheets/oldSheets/character.js b/module/actor/sheets/oldSheets/character.js index 038a28b4..73795b6a 100644 --- a/module/actor/sheets/oldSheets/character.js +++ b/module/actor/sheets/oldSheets/character.js @@ -51,6 +51,11 @@ export default class ActorSheet5eCharacter extends ActorSheet5e { }) .join(", "); + // Weight unit + sheetData["weightUnit"] = game.settings.get("sw5e", "metricWeightUnits") + ? game.i18n.localize("SW5E.AbbreviationKgs") + : game.i18n.localize("SW5E.AbbreviationLbs"); + // Return data for rendering return sheetData; } diff --git a/module/actor/sheets/oldSheets/npc.js b/module/actor/sheets/oldSheets/npc.js index ce848b0b..bf20617c 100644 --- a/module/actor/sheets/oldSheets/npc.js +++ b/module/actor/sheets/oldSheets/npc.js @@ -96,9 +96,28 @@ export default class ActorSheet5eNPC extends ActorSheet5e { // Creature Type data.labels["type"] = this.actor.labels.creatureType; + + // Armor Type + data.labels["armorType"] = this.getArmorLabel(); + return data; } + /* -------------------------------------------- */ + + /** + * Format NPC armor information into a localized string. + * @return {string} Formatted armor label. + */ + getArmorLabel() { + const ac = this.actor.data.data.attributes.ac; + const label = []; + if (ac.calc === "default") label.push(this.actor.armor?.name || game.i18n.localize("SW5E.ArmorClassUnarmored")); + else label.push(game.i18n.localize(CONFIG.SW5E.armorClasses[ac.calc].label)); + if (this.actor.shield) label.push(this.actor.shield.name); + return label.filterJoin(", "); + } + /* -------------------------------------------- */ /* Object Updates */ /* -------------------------------------------- */ diff --git a/module/actor/sheets/oldSheets/vehicle.js b/module/actor/sheets/oldSheets/vehicle.js index a5c6e2ea..66d5bcd9 100644 --- a/module/actor/sheets/oldSheets/vehicle.js +++ b/module/actor/sheets/oldSheets/vehicle.js @@ -13,7 +13,7 @@ export default class ActorSheet5eVehicle extends ActorSheet5e { static get defaultOptions() { return mergeObject(super.defaultOptions, { classes: ["sw5e", "sheet", "actor", "vehicle"], - width: 605, + width: 720, height: 680 }); } @@ -47,10 +47,17 @@ export default class ActorSheet5eVehicle extends ActorSheet5e { _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; + + const currencyPerWeight = game.settings.get("sw5e", "metricWeightUnits") + ? CONFIG.SW5E.encumbrance.currencyPerWeight.metric + : CONFIG.SW5E.encumbrance.currencyPerWeight.imperial; + + totalWeight += totalCoins / currencyPerWeight; // Vehicle weights are an order of magnitude greater. - totalWeight /= CONFIG.SW5E.encumbrance.vehicleWeightMultiplier; + totalWeight /= game.settings.get("sw5e", "metricWeightUnits") + ? CONFIG.SW5E.encumbrance.vehicleWeightMultiplier.metric + : CONFIG.SW5E.encumbrance.vehicleWeightMultiplier.imperial; // Compute overall encumbrance const max = actorData.data.attributes.capacity.cargo; @@ -80,12 +87,10 @@ export default class ActorSheet5eVehicle extends ActorSheet5e { // 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 === 0.5) item.cover = "½"; else if (item.data.cover === 0.75) item.cover = "¾"; else if (item.data.cover === null) item.cover = "—"; - if (item.crew < 1 || item.crew === null) item.crew = "—"; } // Prepare vehicle weapons @@ -114,7 +119,8 @@ export default class ActorSheet5eVehicle extends ActorSheet5e { { label: game.i18n.localize("SW5E.Quantity"), css: "item-qty", - property: "data.quantity" + property: "data.quantity", + editable: "Number" }, { label: game.i18n.localize("SW5E.AC"), @@ -138,14 +144,10 @@ export default class ActorSheet5eVehicle extends ActorSheet5e { actions: { label: game.i18n.localize("SW5E.ActionPl"), items: [], + hasActions: true, 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", @@ -179,6 +181,13 @@ export default class ActorSheet5eVehicle extends ActorSheet5e { } }; + data.items.forEach((item) => { + 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; + }); + const cargo = { crew: { label: game.i18n.localize("SW5E.VehicleCrew"), @@ -284,6 +293,10 @@ export default class ActorSheet5eVehicle extends ActorSheet5e { .click((evt) => evt.target.select()) .change(this._onCargoRowChange.bind(this)); + html.find(".item-qty:not(.cargo) input") + .click((evt) => evt.target.select()) + .change(this._onQtyChange.bind(this)); + if (this.actor.data.data.attributes.actions.stations) { html.find(".counter.actions, .counter.action-thresholds").hide(); } @@ -416,6 +429,24 @@ export default class ActorSheet5eVehicle extends ActorSheet5e { /* -------------------------------------------- */ + /** + * Special handling for editing quantity value of equipment and weapons inside the features tab. + * @param event {Event} + * @returns {Promise} + * @private + */ + + _onQtyChange(event) { + event.preventDefault(); + const itemID = event.currentTarget.closest(".item").dataset.itemId; + const item = this.actor.items.get(itemID); + const qty = parseInt(event.currentTarget.value); + event.currentTarget.value = qty; + return item.update({"data.quantity": qty}); + } + + /* -------------------------------------------- */ + /** * Handle toggling an item's crewed status. * @param event {Event} diff --git a/module/apps/actor-armor.js b/module/apps/actor-armor.js new file mode 100644 index 00000000..45018027 --- /dev/null +++ b/module/apps/actor-armor.js @@ -0,0 +1,94 @@ +/** + * Interface for managing a character's armor calculation. + * @extends {DocumentSheet} + */ +export default class ActorArmorConfig extends DocumentSheet { + /** @inheritdoc */ + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + id: "actor-armor-config", + classes: ["sw5e", "actor-armor-config"], + template: "systems/sw5e/templates/apps/actor-armor.html", + width: 320, + height: "auto" + }); + } + + /* -------------------------------------------- */ + + /** @inheritdoc */ + get title() { + return `${game.i18n.localize("SW5E.ArmorConfig")}: ${this.document.name}`; + } + + /* -------------------------------------------- */ + + /** @inheritdoc */ + async getData() { + // Get actor AC data + const actorData = foundry.utils.deepClone(this.object.data.data); + const ac = foundry.utils.getProperty(actorData, "attributes.ac"); + + // Get configuration data for the calculation mode + let cfg = CONFIG.SW5E.armorClasses[ac.calc]; + if (!cfg) { + ac.calc = "flat"; + cfg = CONFIG.SW5E.armorClasses.flat; + } + + // Return context data + return { + ac: ac, + calculations: CONFIG.SW5E.armorClasses, + value: this.object._computeArmorClass(actorData).value, + valueDisabled: !["flat", "natural"].includes(ac.calc), + formula: ac.calc === "custom" ? ac.formula : cfg.formula, + formulaDisabled: ac.calc !== "custom" + }; + } + + /* -------------------------------------------- */ + + /** @inheritdoc */ + async _updateObject(event, formData) { + const ac = foundry.utils.expandObject(formData).ac; + return this.object.update({"data.attributes.ac": ac}); + } + + /* -------------------------------------------- */ + /* Event Listeners and Handlers */ + /* -------------------------------------------- */ + + /** @inheritdoc */ + async _onChangeInput(event) { + await super._onChangeInput(event); + + // Reference actor data + let actorData = this.object.toObject(false); + let ac = actorData.data.attributes.ac; + + // Reference form data + const calc = this.form["ac.calc"].value; + const cfg = CONFIG.SW5E.armorClasses[calc]; + const enableFlat = ["flat", "natural"].includes(calc); + + // Handle changes to the calculation mode specifically + let formula = this.form["ac.formula"].value; + let flat = this.form["ac.flat"].value; + if (event.currentTarget.name === "ac.calc") { + formula = calc === "custom" ? ac.formula : cfg.formula; + if (enableFlat) flat = ac.flat; + } + + // Recompute effective AC + actorData = foundry.utils.mergeObject(actorData, {"data.attributes.ac": {calc, formula}}); + if (enableFlat) actorData.data.attributes.ac.flat = flat; + ac = this.object._computeArmorClass(actorData.data); + + // Update fields + this.form["ac.formula"].value = ac.formula; + this.form["ac.formula"].disabled = calc !== "custom"; + this.form["ac.flat"].value = enableFlat ? ac.flat : ac.value; + this.form["ac.flat"].disabled = !enableFlat; + } +} diff --git a/module/apps/actor-flags.js b/module/apps/actor-flags.js index 8f5255a5..6f135d32 100644 --- a/module/apps/actor-flags.js +++ b/module/apps/actor-flags.js @@ -99,7 +99,7 @@ export default class ActorSheetFlags extends DocumentSheet { {name: "data.bonuses.power.techDC", label: "SW5E.BonusTechPowerDC"} ]; for (let b of bonuses) { - b.value = getProperty(this.object._data, b.name) || ""; + b.value = getProperty(this.object.data._source, b.name) || ""; } return bonuses; } @@ -119,7 +119,7 @@ export default class ActorSheetFlags extends DocumentSheet { for (let [k, v] of Object.entries(flags)) { if ([undefined, null, "", false, 0].includes(v)) { delete flags[k]; - if (hasProperty(actor._data.flags, `sw5e.${k}`)) { + if (hasProperty(actor.data._source.flags, `sw5e.${k}`)) { unset = true; flags[`-=${k}`] = null; } diff --git a/module/apps/proficiency-selector.js b/module/apps/proficiency-selector.js new file mode 100644 index 00000000..322326a6 --- /dev/null +++ b/module/apps/proficiency-selector.js @@ -0,0 +1,120 @@ +import TraitSelector from "./trait-selector.js"; + +/** + * An application for selecting proficiencies with categories that can contain children. + * + * @extends {TraitSelector} + */ +export default class ProficiencySelector extends TraitSelector { + /** @inheritdoc */ + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + title: "Actor Proficiency Selection", + type: "", + sortCategories: false + }); + } + + /* -------------------------------------------- */ + + /** @inheritdoc */ + async getData() { + const attr = foundry.utils.getProperty(this.object.data, this.attribute); + const value = this.options.valueKey ? foundry.utils.getProperty(attr, this.options.valueKey) ?? [] : attr; + + this.options.choices = CONFIG.SW5E[`${this.options.type}Proficiencies`]; + const data = super.getData(); + + const pack = game.packs.get(CONFIG.SW5E.sourcePacks.ITEMS); + const ids = CONFIG.SW5E[`${this.options.type}Ids`]; + const map = CONFIG.SW5E[`${this.options.type}ProficienciesMap`]; + if (ids !== undefined) { + const typeProperty = this.options.type !== "armor" ? `${this.options.type}Type` : `armor.type`; + for (const [key, id] of Object.entries(ids)) { + const item = await pack.getDocument(id); + let type = foundry.utils.getProperty(item.data.data, typeProperty); + if (map && map[type]) type = map[type]; + const entry = { + label: item.name, + chosen: attr ? value.includes(key) : false + }; + if (data.choices[type] === undefined) { + data.choices[key] = entry; + } else { + if (data.choices[type].children === undefined) { + data.choices[type].children = {}; + } + data.choices[type].children[key] = entry; + } + } + } + + if (this.options.type === "tool") { + data.choices["vehicle"].children = Object.entries(CONFIG.SW5E.vehicleTypes).reduce((obj, [key, label]) => { + obj[key] = { + label: label, + chosen: attr ? value.includes(key) : false + }; + return obj; + }, {}); + } + + if (this.options.sortCategories) data.choices = this._sortObject(data.choices); + for (const category of Object.values(data.choices)) { + if (!category.children) continue; + category.children = this._sortObject(category.children); + } + + return data; + } + + /* -------------------------------------------- */ + + /** + * Take the provided object and sort by the "label" property. + * + * @param {object} object Object to be sorted. + * @return {object} Sorted object. + * @private + */ + _sortObject(object) { + return Object.fromEntries(Object.entries(object).sort((lhs, rhs) => lhs[1].label.localeCompare(rhs[1].label))); + } + + /* -------------------------------------------- */ + + /** @inheritdoc */ + activateListeners(html) { + super.activateListeners(html); + + for (const checkbox of html[0].querySelectorAll("input[type='checkbox']")) { + if (checkbox.checked) this._onToggleCategory(checkbox); + } + } + + /* -------------------------------------------- */ + + /** @inheritdoc */ + async _onChangeInput(event) { + super._onChangeInput(event); + + if (event.target.tagName === "INPUT") this._onToggleCategory(event.target); + } + + /* -------------------------------------------- */ + + /** + * Enable/disable all children when a category is checked. + * + * @param {HTMLElement} checkbox Checkbox that was changed. + * @private + */ + _onToggleCategory(checkbox) { + const children = checkbox.closest("li")?.querySelector("ol"); + if (!children) return; + + for (const child of children.querySelectorAll("input[type='checkbox']")) { + child.checked = child.disabled = checkbox.checked; + } + } +} diff --git a/module/apps/property-attribution.js b/module/apps/property-attribution.js new file mode 100644 index 00000000..52278d9e --- /dev/null +++ b/module/apps/property-attribution.js @@ -0,0 +1,92 @@ +/** + * Description for a single part of a property attribution. + * + * @typedef {object} AttributionDescription + * @property {string} label Descriptive label that will be displayed. If the label is in the form + * of an @ property, the system will try to turn it into a human-readable label. + * @property {number} mode Application mode for this step as defined in + * [CONST.ACTIVE_EFFECT_MODES](https://foundryvtt.com/api/module-constants.html#.ACTIVE_EFFECT_MODES). + * @property {number} value Value of this step. + */ + +/** + * Interface for viewing what factors went into determining a specific property. + * + * @extends {DocumentSheet} + */ +export default class PropertyAttribution extends Application { + /** + * @param {Document} object - Object containing the property to be attributed. + * @param {object.} attribution - Object containing all of the attribution data. + * @param {string} property - Dot separated path to the property. + */ + constructor(object, attribution, property, options = {}) { + super(options); + this.object = object; + this.attribution = attribution; + this.property = property; + } + + /* -------------------------------------------- */ + + /** @inheritdoc */ + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + id: "property-attribution", + classes: ["sw5e", "property-attribution"], + template: "systems/sw5e/templates/apps/property-attribution.html", + width: 320, + height: "auto" + }); + } + + /* -------------------------------------------- */ + + /** + * Render this view as a tooltip rather than a whole window. + * @return {jQuery} HTML of the rendered tooltip. + */ + async renderTooltip() { + const data = this.getData(this.options); + let html = await this._renderInner(data); + html[0].classList.add("tooltip"); + return html; + } + + /* -------------------------------------------- */ + + getData() { + const property = foundry.utils.getProperty(this.object.data.data, this.property); + let total; + if (Number.isNumeric(property)) { + total = property; + } else if (typeof property === "object" && Number.isNumeric(property.value)) { + total = property.value; + } + + const sources = foundry.utils.duplicate(this.attribution[this.property]); + return { + sources: sources.map((entry) => { + if (entry.label.startsWith("@")) { + entry.label = this.getPropertyLabel(entry.label.slice(1)); + } + if (entry.mode === CONST.ACTIVE_EFFECT_MODES.ADD && entry.value < 0) { + entry.negative = true; + entry.value = entry.value * -1; + } + return entry; + }), + total: total + }; + } + + /* -------------------------------------------- */ + + getPropertyLabel(property) { + const parts = property.split("."); + if (parts[0] === "abilities" && parts[1]) { + return CONFIG.SW5E.abilities[parts[1]]; + } + return property; + } +} diff --git a/module/apps/trait-selector.js b/module/apps/trait-selector.js index 6c454cf5..260b8a53 100644 --- a/module/apps/trait-selector.js +++ b/module/apps/trait-selector.js @@ -23,6 +23,13 @@ export default class TraitSelector extends DocumentSheet { /* -------------------------------------------- */ + /** @inheritdoc */ + get title() { + return this.options.title || super.title; + } + + /* -------------------------------------------- */ + /** * Return a reference to the target attribute * @type {string} @@ -37,8 +44,8 @@ export default class TraitSelector extends DocumentSheet { getData() { const attr = foundry.utils.getProperty(this.object.data, this.attribute); const o = this.options; - const value = o.valueKey ? attr[o.valueKey] ?? [] : attr; - const custom = o.customKey ? attr[o.customKey] ?? "" : ""; + const value = o.valueKey ? foundry.utils.getProperty(attr, o.valueKey) ?? [] : attr; + const custom = o.customKey ? foundry.utils.getProperty(attr, o.customKey) ?? "" : ""; // Populate choices const choices = Object.entries(o.choices).reduce((obj, e) => { diff --git a/module/config.js b/module/config.js index 1770bcea..32137a35 100644 --- a/module/config.js +++ b/module/config.js @@ -166,11 +166,14 @@ SW5E.weaponIds = { "whip": "QKTyxoO0YDnAsbYe" }; - */ - /* -------------------------------------------- */ -SW5E.toolProficiencies = { +/** + * The categories into which Tool items can be grouped. + * + * @enum {string} + */ +SW5E.toolTypes = { armor: "SW5E.ToolArmormech", arms: "SW5E.ToolArmstech", arti: "SW5E.ToolArtificer", @@ -199,7 +202,16 @@ SW5E.toolProficiencies = { secur: "SW5E.ToolSecurityKit", slic: "SW5E.ToolSlicerKit", spice: "SW5E.ToolSpiceKit", - music: "SW5E.ToolMusicalInstrument", + music: "SW5E.ToolMusicalInstrument" +}; + +/** + * The categories of tool proficiencies that a character can gain. + * + * @enum {string} + */ +SW5E.toolProficiencies = { + ...SW5E.toolTypes, vehicle: "SW5E.ToolVehicle" }; @@ -211,41 +223,7 @@ SW5E.toolProficiencies = { * * @enum {string} SW5E.toolIds = { - "alchemist": "SztwZhbhZeCqyAes", - "bagpipes": "yxHi57T5mmVt0oDr", - "brewer": "Y9S75go1hLMXUD48", - "calligrapher": "jhjo20QoiD5exf09", - "card": "YwlHI3BVJapz4a3E", - "carpenter": "8NS6MSOdXtUqD7Ib", - "cartographer": "fC0lFK8P4RuhpfaU", - "cobbler": "hM84pZnpCqKfi8XH", - "cook": "Gflnp29aEv5Lc1ZM", - "dice": "iBuTM09KD9IoM5L8", - "disg": "IBhDAr7WkhWPYLVn", - "drum": "69Dpr25pf4BjkHKb", - "dulcimer": "NtdDkjmpdIMiX7I2", - "flute": "eJOrPcAz9EcquyRQ", - "forg": "cG3m4YlHfbQlLEOx", - "glassblower": "rTbVrNcwApnuTz5E", - "herb": "i89okN7GFTWHsvPy", - "horn": "aa9KuBy4dst7WIW9", - "jeweler": "YfBwELTgPFHmQdHh", - "leatherworker": "PUMfwyVUbtyxgYbD", - "lute": "qBydtUUIkv520DT7", - "lyre": "EwG1EtmbgR3bM68U", - "mason": "skUih6tBvcBbORzA", - "navg": "YHCmjsiXxZ9UdUhU", - "painter": "ccm5xlWhx74d6lsK", - "panflute": "G5m5gYIx9VAUWC3J", - "pois": "il2GNi8C0DvGLL9P", - "potter": "hJS8yEVkqgJjwfWa", - "shawm": "G3cqbejJpfB91VhP", - "smith": "KndVe2insuctjIaj", - "thief": "woWZ1sO5IUVGzo58", - "tinker": "0d08g1i5WXnNrCNA", - "viol": "baoe3U5BfMMMxhCU", - "weaver": "ap9prThUB2y9lDyj", - "woodcarver": "xKErqkLo4ASYr5EP", + }; */ @@ -372,6 +350,21 @@ SW5E.itemCapacityTypes = { /* -------------------------------------------- */ +/** + * List of various item rarities. + * @enum {String} + */ +SW5E.itemRarity = { + common: "SW5E.ItemRarityCommon", + uncommon: "SW5E.ItemRarityUncommon", + rare: "SW5E.ItemRarityRare", + veryRare: "SW5E.ItemRarityVeryRare", + legendary: "SW5E.ItemRarityLegendary", + artifact: "SW5E.ItemRarityArtifact" +}; + +/* -------------------------------------------- */ + /** * Enumerate the lengths of time over which an item can have limited use ability * @type {Object} @@ -387,25 +380,47 @@ SW5E.limitedUsePeriods = { /* -------------------------------------------- */ +/** + * Specific equipment types that modify base AC + * @type {object} + */ +SW5E.armorTypes = { + light: "SW5E.EquipmentLight", + medium: "SW5E.EquipmentMedium", + heavy: "SW5E.EquipmentHeavy", + natural: "SW5E.EquipmentNatural", + shield: "SW5E.EquipmentShield" +}; + +/* -------------------------------------------- */ + /** * The set of equipment types for armor, clothing, and other objects which can be worn by the character * @type {Object} */ SW5E.equipmentTypes = { - light: "SW5E.EquipmentLight", - medium: "SW5E.EquipmentMedium", - heavy: "SW5E.EquipmentHeavy", hyper: "SW5E.EquipmentHyperdrive", bonus: "SW5E.EquipmentBonus", - natural: "SW5E.EquipmentNatural", powerc: "SW5E.EquipmentPowerCoupling", reactor: "SW5E.EquipmentReactor", - shield: "SW5E.EquipmentShield", clothing: "SW5E.EquipmentClothing", trinket: "SW5E.EquipmentTrinket", ssarmor: "SW5E.EquipmentStarshipArmor", ssshield: "SW5E.EquipmentStarshipShield", - vehicle: "SW5E.EquipmentVehicle" + vehicle: "SW5E.EquipmentVehicle", + ...SW5E.armorTypes +}; + +/* -------------------------------------------- */ + +/** + * The various types of vehicles in which characters can be proficient. + * @enum {string} + */ +SW5E.vehicleTypes = { + air: "SW5E.VehicleTypeAir", + land: "SW5E.VehicleTypeLand", + water: "SW5E.VehicleTypeWater" }; /* -------------------------------------------- */ @@ -435,6 +450,61 @@ SW5E.armorProficienciesMap = { shield: "shl" }; +// TODO: Same as weapon IDs +/** + * The basic armor types in 5e. This enables specific armor proficiencies, + * automated AC calculation in NPCs, and starting equipment. + * + * @enum {string} + */ +SW5E.armorIds = {}; + +// TODO: Same as weapon IDs +/** + * The basic shield in 5e. + * + * @enum {string} + */ +SW5E.shieldIds = {}; + +/** + * Common armor class calculations. + * @enum {object} + */ +SW5E.armorClasses = { + flat: { + label: "SW5E.ArmorClassFlat", + formula: "@attributes.ac.flat" + }, + natural: { + label: "SW5E.ArmorClassNatural", + formula: "@attributes.ac.flat" + }, + default: { + label: "SW5E.ArmorClassEquipment", + formula: "@attributes.ac.base + @abilities.dex.mod" + }, + mage: { + label: "SW5E.ArmorClassMage", + formula: "13 + @abilities.dex.mod" + }, + draconic: { + label: "SW5E.ArmorClassDraconic", + formula: "13 + @abilities.dex.mod" + }, + unarmoredMonk: { + label: "SW5E.ArmorClassUnarmoredMonk", + formula: "10 + @abilities.dex.mod + @abilities.wis.mod" + }, + unarmoredBarb: { + label: "SW5E.ArmorClassUnarmoredBarbarian", + formula: "10 + @abilities.dex.mod + @abilities.con.mod" + }, + custom: { + label: "SW5E.ArmorClassCustom" + } +}; + /* -------------------------------------------- */ /** @@ -545,7 +615,9 @@ SW5E.movementTypes = { */ SW5E.movementUnits = { ft: "SW5E.DistFt", - mi: "SW5E.DistMi" + mi: "SW5E.DistMi", + m: "SW5E.DistM", + km: "SW5E.DistKm" }; /** @@ -571,9 +643,18 @@ for (let [k, v] of Object.entries(SW5E.movementUnits)) { * @type {Object} */ SW5E.encumbrance = { - currencyPerWeight: 50, - strMultiplier: 15, - vehicleWeightMultiplier: 2000 // 2000 lbs in a ton + currencyPerWeight: { + imperial: 50, + metric: 110 + }, + strMultiplier: { + imperial: 15, + metric: 6.8 + }, + vehicleWeightMultiplier: { + imperial: 2000, // 2000 lbs in an imperial ton + metric: 1000 // 1000 kg in a metric ton + } }; /* -------------------------------------------- */ diff --git a/module/item/entity.js b/module/item/entity.js index 088362b0..b60dd2d2 100644 --- a/module/item/entity.js +++ b/module/item/entity.js @@ -751,7 +751,7 @@ export default class Item5e extends Item { } // Verify that a consumed resource is available - if (!resource) { + if (resource === undefined) { ui.notifications.warn(game.i18n.format("SW5E.ConsumeWarningNoSource", {name: this.name, type: typeLabel})); return false; } @@ -796,7 +796,7 @@ export default class Item5e extends Item { // Render the chat card template const token = this.actor.token; const templateData = { - actor: this.actor, + actor: this.actor.data, tokenId: token?.uuid || null, item: this.data, data: this.getChatData(), @@ -1025,24 +1025,22 @@ export default class Item5e extends Item { } // Compose roll options - const rollConfig = mergeObject( - { - parts: parts, - actor: this.actor, - data: rollData, - title: title, - flavor: 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}} + let rollConfig = { + parts: parts, + actor: this.actor, + data: rollData, + title: title, + flavor: title, + dialogOptions: { + width: 400, + top: options.event ? options.event.clientY - 80 : null, + left: window.innerWidth - 710 }, - options - ); - rollConfig.event = options.event; + messageData: { + "flags.sw5e.roll": {type: "attack", itemId: this.id}, + "speaker": ChatMessage.getSpeaker({actor: this.actor}) + } + }; // Expanded critical hit thresholds if (this.data.type === "weapon" && flags.weaponCriticalThreshold) { @@ -1059,6 +1057,9 @@ export default class Item5e extends Item { // Apply Halfling Lucky if (flags.halflingLucky) rollConfig.halflingLucky = true; + // Compose calculated roll options with passed-in roll options + rollConfig = mergeObject(rollConfig, options); + // Invoke the d20 roll helper const roll = await d20Roll(rollConfig); if (!roll) return null; @@ -1121,8 +1122,10 @@ export default class Item5e extends Item { // Scale damage from up-casting powers if (this.data.type === "power") { if (itemData.scaling.mode === "atwill") { - const level = - this.actor.data.type === "character" ? actorData.details.level : actorData.details.powerLevel; + let level; + if (this.actor.type === "character") level = actorData.details.level; + else if (itemData.preparation.mode === "innate") level = Math.ceil(actorData.details.cr); + else level = actorData.details.powerLevel; this._scaleAtWillDamage(parts, itemData.scaling.formula, level, rollData); } else if (powerLevel && itemData.scaling.mode === "level" && itemData.scaling.formula) { const scaling = itemData.scaling.formula; @@ -1651,7 +1654,7 @@ export default class Item5e extends Item { */ static async createScrollFromPower(power) { // Get power data - const itemData = power instanceof Item5e ? power.data : power; + const itemData = power instanceof Item5e ? power.toObject() : power; const {actionType, description, source, activation, duration, target, range, damage, save, level} = itemData.data; @@ -1672,7 +1675,7 @@ export default class Item5e extends Item { const desc = `${scrollIntro}

${itemData.name} (Level ${level})


${description.value}

Scroll Details


${scrollDetails}`; // Create the power scroll data - const powerScrollData = mergeObject(scrollData, { + const powerScrollData = foundry.utils.mergeObject(scrollData, { name: `${game.i18n.localize("SW5E.PowerScroll")}: ${itemData.name}`, img: itemData.img, data: { diff --git a/module/item/sheet.js b/module/item/sheet.js index 0bb7f64b..c0143847 100644 --- a/module/item/sheet.js +++ b/module/item/sheet.js @@ -1,5 +1,5 @@ import TraitSelector from "../apps/trait-selector.js"; -import {onManageActiveEffect, prepareActiveEffectCategories} from "../effects.js"; +import ActiveEffect5e from "../active-effect.js"; /** * Override and extend the core ItemSheet implementation to handle specific item types @@ -71,7 +71,7 @@ export default class ItemSheet5e extends ItemSheet { data.isMountable = this._isItemMountable(itemData); // Prepare Active Effects - data.effects = prepareActiveEffectCategories(this.item.effects); + data.effects = ActiveEffect5e.prepareActiveEffectCategories(this.item.effects); // Re-define the template data references (backwards compatible) data.item = itemData; @@ -347,12 +347,15 @@ export default class ItemSheet5e extends ItemSheet { options.choices = CONFIG.SW5E.abilities; options.valueKey = null; break; + case "skills.choices": + options.choices = CONFIG.SW5E.skills; + options.valueKey = null; + break; case "skills": const skills = this.item.data.data.skills; - const choiceSet = - skills.choices && skills.choices.length ? skills.choices : Object.keys(CONFIG.SW5E.skills); + const choiceSet = skills.choices?.length ? skills.choices : Object.keys(CONFIG.SW5E.skills); options.choices = Object.fromEntries( - Object.entries(CONFIG.SW5E.skills).filter((skill) => choiceSet.includes(skill[0])) + Object.entries(CONFIG.SW5E.skills).filter(([skill]) => choiceSet.includes(skill)) ); options.maximum = skills.number; break; diff --git a/module/migration.js b/module/migration.js index ab6eb420..15b9ce13 100644 --- a/module/migration.js +++ b/module/migration.js @@ -12,7 +12,7 @@ export const migrateWorld = async function () { for await (let a of game.actors.contents) { try { console.log(`Checking Actor entity ${a.name} for migration needs`); - const updateData = await migrateActorData(a.data); + const updateData = await migrateActorData(a.toObject()); if (!foundry.utils.isObjectEmpty(updateData)) { console.log(`Migrating Actor entity ${a.name}`); await a.update(updateData, {enforceTypes: false}); @@ -91,7 +91,7 @@ export const migrateCompendium = async function (pack) { try { switch (entity) { case "Actor": - updateData = await migrateActorData(doc.data); + updateData = await migrateActorData(doc.toObject()); break; case "Item": updateData = migrateItemData(doc.toObject()); @@ -117,6 +117,49 @@ export const migrateCompendium = async function (pack) { console.log(`Migrated all ${entity} entities from Compendium ${pack.collection}`); }; +/** + * Apply 'smart' AC migration to a given Actor compendium. This will perform the normal AC migration but additionally + * check to see if the actor has armor already equipped, and opt to use that instead. + * @param pack + * @return {Promise} + */ +export const migrateArmorClass = async function (pack) { + if (typeof pack === "string") pack = game.packs.get(pack); + if (pack.metadata.entity !== "Actor") return; + const wasLocked = pack.locked; + await pack.configure({locked: false}); + const actors = await pack.getDocuments(); + const updates = []; + const armor = new Set(Object.keys(CONFIG.SW5E.armorTypes)); + + for (const actor of actors) { + try { + console.log(`Migrating ${actor.name}...`); + const src = actor.toObject(); + const update = {_id: actor.id}; + + // Perform the normal migration. + _migrateActorAC(src, update); + updates.push(update); + + // CASE 1: Armor is equipped + const hasArmorEquipped = actor.itemTypes.equipment.some((e) => { + return armor.has(e.data.data.armor?.type) && e.data.data.equipped; + }); + if (hasArmorEquipped) update["data.attributes.ac.calc"] = "default"; + // CASE 2: NPC Natural Armor + else if (src.type === "npc") update["data.attributes.ac.calc"] = "natural"; + } catch (e) { + console.warn(`Failed to migrate armor class for Actor ${actor.name}`, e); + } + } + + await Actor.implementation.updateDocuments(updates, {pack: pack.collection}); + await pack.getDocuments(); // Force a re-prepare of all actors. + await pack.configure({locked: wasLocked}); + console.log(`Migrated the AC of all Actors from Compendium ${pack.collection}`); +}; + /* -------------------------------------------- */ /* Entity Type Migration Helpers */ /* -------------------------------------------- */ @@ -135,6 +178,7 @@ export const migrateActorData = async function (actor) { _migrateActorMovement(actor, updateData); _migrateActorSenses(actor, updateData); _migrateActorType(actor, updateData); + _migrateActorAC(actor, updateData); } // Migrate Owned Items @@ -215,6 +259,7 @@ export const migrateItemData = function (item) { const updateData = {}; _migrateItemClassPowerCasting(item, updateData); _migrateItemAttunement(item, updateData); + _migrateItemRarity(item, updateData); return updateData; }; @@ -229,6 +274,7 @@ export const migrateActorItemData = async function (item, actor) { const updateData = {}; _migrateItemClassPowerCasting(item, updateData); _migrateItemAttunement(item, updateData); + _migrateItemRarity(item, updateData); await _migrateItemPower(item, actor, updateData); return updateData; }; @@ -597,6 +643,20 @@ function _migrateItemClassPowerCasting(item, updateData) { /* -------------------------------------------- */ +/** + * Migrate the actor attributes.ac.value to the new ac.flat override field. + * @private + */ +function _migrateActorAC(actorData, updateData) { + const ac = actorData.data?.attributes?.ac; + if (!Number.isNumeric(ac?.value)) return; + updateData["data.attributes.ac.flat"] = ac.value; + updateData["data.attributes.ac.-=value"] = null; + return updateData; +} + +/* -------------------------------------------- */ + /** * Update an Power Item's data based on compendium * @param {Object} item The data object for an item @@ -664,6 +724,26 @@ function _migrateItemAttunement(item, updateData) { /* -------------------------------------------- */ +/** + * Attempt to migrate item rarity from freeform string to enum value. + * + * @param {object} item Item data to migrate + * @param {object} updateData Existing update to expand upon + * @return {object} The updateData to apply + * @private + */ +function _migrateItemRarity(item, updateData) { + if (item.data?.rarity === undefined) return updateData; + const rarity = Object.keys(CONFIG.SW5E.itemRarity).find( + (key) => + CONFIG.SW5E.itemRarity[key].toLowerCase() === item.data.rarity.toLowerCase() || key === item.data.rarity + ); + updateData["data.rarity"] = rarity ?? ""; + return updateData; +} + +/* -------------------------------------------- */ + /** * A general tool to purge flags from all entities in a Compendium pack. * @param {Compendium} pack The compendium pack to clean diff --git a/module/settings.js b/module/settings.js index 3b765c69..f21b9521 100644 --- a/module/settings.js +++ b/module/settings.js @@ -141,4 +141,16 @@ export const registerSystemSettings = function () { dark: "SETTINGS.SWColorDark" } }); + + /** + * Option to replace imperial weight units with metric weight units. + */ + game.settings.register("sw5e", "metricWeightUnits", { + name: "SETTINGS.5eMetricN", + hint: "SETTINGS.5eMetricL", + scope: "world", + config: true, + type: Boolean, + default: false + }); }; diff --git a/module/templates.js b/module/templates.js index e810b6da..b02286b4 100644 --- a/module/templates.js +++ b/module/templates.js @@ -14,6 +14,7 @@ export const preloadHandlebarsTemplates = async function () { "systems/sw5e/templates/actors/oldActor/parts/actor-features.html", "systems/sw5e/templates/actors/oldActor/parts/actor-powerbook.html", "systems/sw5e/templates/actors/oldActor/parts/actor-notes.html", + "systems/sw5e/templates/actors/oldActor/parts/actor-warnings.html", "systems/sw5e/templates/actors/newActor/parts/swalt-biography.html", "systems/sw5e/templates/actors/newActor/parts/swalt-core.html", @@ -25,6 +26,7 @@ export const preloadHandlebarsTemplates = async function () { "systems/sw5e/templates/actors/newActor/parts/swalt-tech-powerbook.html", "systems/sw5e/templates/actors/newActor/parts/swalt-resources.html", "systems/sw5e/templates/actors/newActor/parts/swalt-traits.html", + "systems/sw5e/templates/actors/newActor/parts/swalt-warnings.html", // Item Sheet Partials "systems/sw5e/templates/items/parts/item-action.html", diff --git a/sw5e.js b/sw5e.js index 3a1f6439..fe2a9c71 100644 --- a/sw5e.js +++ b/sw5e.js @@ -41,6 +41,7 @@ import * as chat from "./module/chat.js"; import * as dice from "./module/dice.js"; import * as macros from "./module/macros.js"; import * as migrations from "./module/migration.js"; +import ActiveEffect5e from "./module/active-effect.js"; /* -------------------------------------------- */ /* Foundry VTT Initialization */ @@ -83,6 +84,7 @@ Hooks.once("init", function () { // Record Configuration Values CONFIG.SW5E = SW5E; + CONFIG.ActiveEffect.documentClass = ActiveEffect5e; CONFIG.Actor.documentClass = Actor5e; CONFIG.Item.documentClass = Item5e; CONFIG.Token.documentClass = TokenDocument5e; @@ -193,6 +195,7 @@ Hooks.once("setup", function () { "abilityConsumptionTypes", "actorSizes", "alignments", + "armorClasses", "armorProficiencies", "armorPropertiesTypes", "conditionTypes", @@ -205,6 +208,7 @@ Hooks.once("setup", function () { "equipmentTypes", "healingTypes", "itemActionTypes", + "itemRarity", "languages", "limitedUsePeriods", "movementTypes", @@ -227,6 +231,8 @@ Hooks.once("setup", function () { "targetTypes", "timePeriods", "toolProficiencies", + "toolTypes", + "vehicleTypes", "weaponProficiencies", "weaponProperties", "weaponSizes", @@ -237,24 +243,30 @@ Hooks.once("setup", function () { const noSort = [ "abilities", "alignments", + "armorClasses", + "armorProficiencies", "currencies", "distanceUnits", "movementUnits", "itemActionTypes", + "itemRarity", "proficiencyLevels", "limitedUsePeriods", "powerComponents", "powerLevels", "powerPreparationModes", + "weaponProficiencies", "weaponTypes" ]; // Localize and sort CONFIG objects for (let o of toLocalize) { - const localized = Object.entries(CONFIG.SW5E[o]).map((e) => { - return [e[0], game.i18n.localize(e[1])]; + const localized = Object.entries(CONFIG.SW5E[o]).map(([k, v]) => { + if (v.label) v.label = game.i18n.localize(v.label); + if (typeof v === "string") return [k, game.i18n.localize(v)]; + return [k, v]; }); - if (!noSort.includes(o)) localized.sort((a, b) => a[1].localeCompare(b[1])); + if (!noSort.includes(o)) localized.sort((a, b) => (a[1].label ?? a[1]).localeCompare(b[1].label ?? b[1])); CONFIG.SW5E[o] = localized.reduce((obj, e) => { obj[e[0]] = e[1]; return obj; @@ -278,10 +290,13 @@ Hooks.once("ready", function () { // Determine whether a system migration is required and feasible if (!game.user.isGM) return; const currentVersion = game.settings.get("sw5e", "systemMigrationVersion"); - const NEEDS_MIGRATION_VERSION = "1.3.5.R1-A6"; + const NEEDS_MIGRATION_VERSION = "1.4.1.R1-A8"; // Check for R1 SW5E versions - const SW5E_NEEDS_MIGRATION_VERSION = "R1-A6"; + const SW5E_NEEDS_MIGRATION_VERSION = "R1-A8"; const COMPATIBLE_MIGRATION_VERSION = 0.8; + const totalDocuments = game.actors.size + game.scenes.size + game.items.size; + if (!currentVersion && totalDocuments === 0) + return game.settings.set("sw5e", "systemMigrationVersion", game.system.data.version); const needsMigration = currentVersion && (isNewerVersion(SW5E_NEEDS_MIGRATION_VERSION, currentVersion) || diff --git a/system.json b/system.json index 3a0a82ca..fe4d0e0f 100644 --- a/system.json +++ b/system.json @@ -1,160 +1,159 @@ { - "name": "sw5e", - "title": "SW 5th Edition", - "description": "A comprehensive game system for running games of SW 5th Edition in the Foundry VTT environment.", - "version": "1.3.5.R1-A7", - "author": "Dev Team", - "scripts": [], - "esmodules": ["sw5e.js"], - "styles": ["sw5e.css", "sw5e-global.css", "sw5e-light.css", "sw5e-dark.css"], - "packs": [ - { - "name": "adventuringgear", - "label": "Adventuring Gear", - "path": "./packs/packs/adventuringgear.db", - "entity": "Item" - }, - { - "name": "archetypes", - "label": "Archetypes", - "path": "./packs/packs/archetypes.db", - "entity": "Item" - }, - { - "name": "armor", - "label": "Armor", - "path": "./packs/packs/armor.db", - "entity": "Item" - }, - { - "name": "backgrounds", - "label": "Backgrounds", - "path": "./packs/packs/backgrounds.db", - "entity": "Item" - }, - { - "name": "classes", - "label": "Classes", - "path": "./packs/packs/classes.db", - "entity": "Item" - }, - { - "name": "classfeatures", - "label": "Class Features", - "path": "./packs/packs/classfeatures.db", - "entity": "Item" - }, - { - "name": "conditions", - "label": "Conditions", - "path": "./packs/packs/conditions.db", - "entity": "JournalEntry" - }, - { - "name": "enhanceditems", - "label": "Enhanced Items", - "path": "./packs/packs/enhanceditems.db", - "entity": "Item" - }, - { - "name": "feats", - "label": "Feats", - "path": "./packs/packs/feats.db", - "entity": "Item" - }, - { - "name": "fightingstyles", - "label": "Fighting Styles", - "path": "./packs/packs/fightingstyles.db", - "entity": "Item" - }, - { - "name": "fightingmasteries", - "label": "Fighting Masteries", - "path": "./packs/packs/fightingmasteries.db", - "entity": "Item" - }, - { - "name": "forcepowers", - "label": "Force Powers", - "path": "./packs/packs/forcepowers.db", - "entity": "Item" - }, - { - "name": "gamingset", - "label": "Gaming Sets", - "path": "./packs/packs/gamingset.db", - "entity": "Item" - }, - { - "name": "lightsaberform", - "label": "Lightsaber Forms", - "path": "./packs/packs/lightsaberforms.db", - "entity": "Item" - }, - { - "name": "monsters", - "label": "Monsters", - "path": "./packs/packs/monsters.db", - "entity": "Actor" - }, - { - "name": "species", - "label": "Species", - "path": "./packs/packs/species.db", - "entity": "Item" - }, - { - "name": "speciestraits", - "label": "Species Traits", - "path": "./packs/packs/speciestraits.db", - "entity": "Item" - }, - { - "name": "tables", - "label": "Tables", - "path": "./packs/packs/tables.db", - "entity": "RollTable" - }, - { - "name": "techpowers", - "label": "Tech Powers", - "path": "./packs/packs/techpowers.db", - "entity": "Item" - }, - { - "name": "weapons", - "label": "Weapons", - "path": "./packs/packs/weapons.db", - "entity": "Item" - } - ], - "languages": [ - { - "lang": "en", - "name": "English", - "path": "lang/en.json" - }, - { - "lang": "fr", - "name": "French", - "path": "lang/fr.json" - }, - { - "lang": "it", - "name": "Italian", - "path": "lang/it.json" - } - ], - "socket": true, - "gridDistance": 5, - "gridUnits": "ft", - "primaryTokenAttribute": "attributes.hp", - "secondaryTokenAttribute": null, - "minimumCoreVersion": "0.8.2", - "compatibleCoreVersion": "0.8.8", - "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" - + "name": "sw5e", + "title": "SW 5th Edition", + "description": "A comprehensive game system for running games of SW 5th Edition in the Foundry VTT environment.", + "version": "1.4.1.R1-A8", + "author": "Dev Team", + "scripts": [], + "esmodules": ["sw5e.js"], + "styles": ["sw5e.css", "sw5e-global.css", "sw5e-light.css", "sw5e-dark.css"], + "packs": [ + { + "name": "adventuringgear", + "label": "Adventuring Gear", + "path": "./packs/packs/adventuringgear.db", + "entity": "Item" + }, + { + "name": "archetypes", + "label": "Archetypes", + "path": "./packs/packs/archetypes.db", + "entity": "Item" + }, + { + "name": "armor", + "label": "Armor", + "path": "./packs/packs/armor.db", + "entity": "Item" + }, + { + "name": "backgrounds", + "label": "Backgrounds", + "path": "./packs/packs/backgrounds.db", + "entity": "Item" + }, + { + "name": "classes", + "label": "Classes", + "path": "./packs/packs/classes.db", + "entity": "Item" + }, + { + "name": "classfeatures", + "label": "Class Features", + "path": "./packs/packs/classfeatures.db", + "entity": "Item" + }, + { + "name": "conditions", + "label": "Conditions", + "path": "./packs/packs/conditions.db", + "entity": "JournalEntry" + }, + { + "name": "enhanceditems", + "label": "Enhanced Items", + "path": "./packs/packs/enhanceditems.db", + "entity": "Item" + }, + { + "name": "feats", + "label": "Feats", + "path": "./packs/packs/feats.db", + "entity": "Item" + }, + { + "name": "fightingstyles", + "label": "Fighting Styles", + "path": "./packs/packs/fightingstyles.db", + "entity": "Item" + }, + { + "name": "fightingmasteries", + "label": "Fighting Masteries", + "path": "./packs/packs/fightingmasteries.db", + "entity": "Item" + }, + { + "name": "forcepowers", + "label": "Force Powers", + "path": "./packs/packs/forcepowers.db", + "entity": "Item" + }, + { + "name": "gamingset", + "label": "Gaming Sets", + "path": "./packs/packs/gamingset.db", + "entity": "Item" + }, + { + "name": "lightsaberform", + "label": "Lightsaber Forms", + "path": "./packs/packs/lightsaberforms.db", + "entity": "Item" + }, + { + "name": "monsters", + "label": "Monsters", + "path": "./packs/packs/monsters.db", + "entity": "Actor" + }, + { + "name": "species", + "label": "Species", + "path": "./packs/packs/species.db", + "entity": "Item" + }, + { + "name": "speciestraits", + "label": "Species Traits", + "path": "./packs/packs/speciestraits.db", + "entity": "Item" + }, + { + "name": "tables", + "label": "Tables", + "path": "./packs/packs/tables.db", + "entity": "RollTable" + }, + { + "name": "techpowers", + "label": "Tech Powers", + "path": "./packs/packs/techpowers.db", + "entity": "Item" + }, + { + "name": "weapons", + "label": "Weapons", + "path": "./packs/packs/weapons.db", + "entity": "Item" + } + ], + "languages": [ + { + "lang": "en", + "name": "English", + "path": "lang/en.json" + }, + { + "lang": "fr", + "name": "French", + "path": "lang/fr.json" + }, + { + "lang": "it", + "name": "Italian", + "path": "lang/it.json" + } + ], + "socket": true, + "gridDistance": 5, + "gridUnits": "ft", + "primaryTokenAttribute": "attributes.hp", + "secondaryTokenAttribute": null, + "minimumCoreVersion": "0.8.2", + "compatibleCoreVersion": "0.8.8", + "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" } diff --git a/template.json b/template.json index 1681d52e..71cc2b05 100644 --- a/template.json +++ b/template.json @@ -1,1063 +1,1051 @@ { - "Actor": { - "types": ["character", "npc", "vehicle"], - "templates": { - "common": { - "abilities": { - "str": { - "value": 10, - "proficient": 0 - }, - "dex": { - "value": 10, - "proficient": 0 - }, - "con": { - "value": 10, - "proficient": 0 - }, - "int": { - "value": 10, - "proficient": 0 - }, - "wis": { - "value": 10, - "proficient": 0 - }, - "cha": { - "value": 10, - "proficient": 0 - } - }, - "attributes": { - "ac": { - "value": 10 - }, - "hp": { - "value": 10, - "min": 0, - "max": 10, - "temp": 0, - "tempmax": 0 - }, - "init": { - "value": 0, - "bonus": 0 - }, - "movement": { - "burrow": 0, - "climb": 0, - "crawl": 0, - "fly": 0, - "roll": 0, - "space": 0, - "swim": 0, - "turn": 0, - "walk": 30, - "units": "ft", - "hover": false - } - }, - "details": { - "biography": { - "value": "", - "public": "" - } - }, - "traits": { - "size": "med", - "di": { - "value": [], - "custom": "" - }, - "dr": { - "value": [], - "custom": "" - }, - "dv": { - "value": [], - "custom": "" - }, - "ci": { - "value": [], - "custom": "" - } - }, - "currency": { - "gc": 0 - } - }, - "creature": { - "attributes": { - "senses": { - "darkvision": 0, - "blindsight": 0, - "tremorsense": 0, - "truesight": 0, - "units": "ft", - "special": "" - }, - "powercasting": "none", - "force": { - "known": { - "value": 0, - "max": 0 + "Actor": { + "types": ["character", "npc", "vehicle"], + "templates": { + "common": { + "abilities": { + "str": { + "value": 10, + "proficient": 0 + }, + "dex": { + "value": 10, + "proficient": 0 + }, + "con": { + "value": 10, + "proficient": 0 + }, + "int": { + "value": 10, + "proficient": 0 + }, + "wis": { + "value": 10, + "proficient": 0 + }, + "cha": { + "value": 10, + "proficient": 0 + } + }, + "attributes": { + "ac": { + "flat": null, + "calc": "default", + "formula": "" + }, + "hp": { + "value": 10, + "min": 0, + "max": 10, + "temp": 0, + "tempmax": 0 + }, + "init": { + "value": 0, + "bonus": 0 + }, + "movement": { + "burrow": 0, + "climb": 0, + "crawl": 0, + "fly": 0, + "roll": 0, + "space": 0, + "swim": 0, + "turn": 0, + "walk": 30, + "units": "ft", + "hover": false + } + }, + "details": { + "biography": { + "value": "", + "public": "" + } + }, + "traits": { + "size": "med", + "di": { + "value": [], + "custom": "" + }, + "dr": { + "value": [], + "custom": "" + }, + "dv": { + "value": [], + "custom": "" + }, + "ci": { + "value": [], + "custom": "" + } + }, + "currency": { + "gc": 0 + } }, - "points": { - "value": 0, - "min": 0, - "max": 0, - "temp": 0, - "tempmax": 0 + "creature": { + "attributes": { + "senses": { + "darkvision": 0, + "blindsight": 0, + "tremorsense": 0, + "truesight": 0, + "units": "ft", + "special": "" + }, + "powercasting": "none", + "force": { + "known": { + "value": 0, + "max": 0 + }, + "points": { + "value": 0, + "min": 0, + "max": 0, + "temp": 0, + "tempmax": 0 + }, + "level": 0 + }, + "tech": { + "known": { + "value": 0, + "max": 0 + }, + "points": { + "value": 0, + "min": 0, + "max": 0, + "temp": 0, + "tempmax": 0 + }, + "level": 0 + } + }, + "details": { + "alignment": "", + "species": "" + }, + "skills": { + "acr": { + "value": 0, + "ability": "dex" + }, + "ani": { + "value": 0, + "ability": "wis" + }, + "ath": { + "value": 0, + "ability": "str" + }, + "dec": { + "value": 0, + "ability": "cha" + }, + "ins": { + "value": 0, + "ability": "wis" + }, + "itm": { + "value": 0, + "ability": "cha" + }, + "inv": { + "value": 0, + "ability": "int" + }, + "lor": { + "value": 0, + "ability": "int" + }, + "med": { + "value": 0, + "ability": "wis" + }, + "nat": { + "value": 0, + "ability": "int" + }, + "pil": { + "value": 0, + "ability": "int" + }, + "prc": { + "value": 0, + "ability": "wis" + }, + "prf": { + "value": 0, + "ability": "cha" + }, + "per": { + "value": 0, + "ability": "cha" + }, + "slt": { + "value": 0, + "ability": "dex" + }, + "ste": { + "value": 0, + "ability": "dex" + }, + "sur": { + "value": 0, + "ability": "wis" + }, + "tec": { + "value": 0, + "ability": "int" + } + }, + "traits": { + "languages": { + "value": [], + "custom": "" + } + }, + "powers": { + "power1": { + "fvalue": 1000, + "fmax": 0, + "foverride": null, + "tvalue": 1000, + "tmax": 0, + "toverride": null + }, + "power2": { + "fvalue": 1000, + "fmax": 0, + "foverride": null, + "tvalue": 1000, + "tmax": 0, + "toverride": null + }, + "power3": { + "fvalue": 1000, + "fmax": 0, + "foverride": null, + "tvalue": 1000, + "tmax": 0, + "toverride": null + }, + "power4": { + "fvalue": 1000, + "fmax": 0, + "foverride": null, + "tvalue": 1000, + "tmax": 0, + "toverride": null + }, + "power5": { + "fvalue": 1000, + "fmax": 0, + "foverride": null, + "tvalue": 1000, + "tmax": 0, + "toverride": null + }, + "power6": { + "fvalue": 1, + "fmax": 0, + "foverride": null, + "tvalue": 1, + "tmax": 0, + "toverride": null + }, + "power7": { + "fvalue": 1, + "fmax": 0, + "foverride": null, + "tvalue": 1, + "tmax": 0, + "toverride": null + }, + "power8": { + "fvalue": 1, + "fmax": 0, + "foverride": null, + "tvalue": 1, + "tmax": 0, + "toverride": null + }, + "power9": { + "fvalue": 1, + "fmax": 0, + "foverride": null, + "tvalue": 1, + "tmax": 0, + "toverride": null + } + }, + "bonuses": { + "mwak": { + "attack": "", + "damage": "" + }, + "rwak": { + "attack": "", + "damage": "" + }, + "mpak": { + "attack": "", + "damage": "" + }, + "rpak": { + "attack": "", + "damage": "" + }, + "abilities": { + "check": "", + "save": "", + "skill": "" + }, + "power": { + "forceLightDC": "", + "forceUnivDC": "", + "forceDarkDC": "", + "techDC": "" + } + } + } + }, + "htmlFields": ["details.biography.value", "details.biography.public"], + "character": { + "templates": ["common", "creature"], + "attributes": { + "death": { + "success": 0, + "failure": 0 + }, + "encumbrance": { + "value": null, + "max": null + }, + "exhaustion": 0, + "inspiration": 0 }, - "level": 0 - }, - "tech": { - "known": { - "value": 0, - "max": 0 + "details": { + "background": "", + "originalClass": "", + "xp": { + "value": 0, + "min": 0, + "max": 300 + }, + "appearance": "", + "trait": "", + "ideal": "", + "bond": "", + "flaw": "" }, - "points": { - "value": 0, - "min": 0, - "max": 0, - "temp": 0, - "tempmax": 0 + "resources": { + "primary": { + "value": 0, + "max": 0, + "sr": 0, + "lr": 0 + }, + "secondary": { + "value": 0, + "max": 0, + "sr": 0, + "lr": 0 + }, + "tertiary": { + "value": 0, + "max": 0, + "sr": 0, + "lr": 0 + } }, - "level": 0 - } + "traits": { + "weaponProf": { + "value": [], + "custom": "" + }, + "armorProf": { + "value": [], + "custom": "" + }, + "toolProf": { + "value": [], + "custom": "" + } + } }, - "details": { - "alignment": "", - "species": "" + "npc": { + "templates": ["common", "creature"], + "details": { + "type": { + "value": "", + "subtype": "", + "swarm": "", + "custom": "" + }, + "environment": "", + "cr": 1, + "powerForceLevel": 0, + "powerTechLevel": 0, + "xp": { + "value": 10 + }, + "source": "" + }, + "resources": { + "legact": { + "value": 0, + "max": 0 + }, + "legres": { + "value": 0, + "max": 0 + }, + "lair": { + "value": 0, + "initiative": 0 + } + } }, - "skills": { - "acr": { - "value": 0, - "ability": "dex" - }, - "ani": { - "value": 0, - "ability": "wis" - }, - "ath": { - "value": 0, - "ability": "str" - }, - "dec": { - "value": 0, - "ability": "cha" - }, - "ins": { - "value": 0, - "ability": "wis" - }, - "itm": { - "value": 0, - "ability": "cha" - }, - "inv": { - "value": 0, - "ability": "int" - }, - "lor": { - "value": 0, - "ability": "int" - }, - "med": { - "value": 0, - "ability": "wis" - }, - "nat": { - "value": 0, - "ability": "int" - }, - "pil": { - "value": 0, - "ability": "int" - }, - "prc": { - "value": 0, - "ability": "wis" - }, - "prf": { - "value": 0, - "ability": "cha" - }, - "per": { - "value": 0, - "ability": "cha" - }, - "slt": { - "value": 0, - "ability": "dex" - }, - "ste": { - "value": 0, - "ability": "dex" - }, - "sur": { - "value": 0, - "ability": "wis" - }, - "tec": { - "value": 0, - "ability": "int" - } + "starship": { + "templates": ["common"], + "attributes": { + "cargcap": 0, + "crewcap": 0, + "cscap": 0, + "death": { + "failure": 0, + "success": 0 + }, + "dr": 0, + "engpow": 1, + "exhaustion": 0, + "hsm": 1, + "hull": { + "die": "", + "dice": 0, + "formula": "", + "value": null, + "max": null + }, + "mods": { + "open": 10, + "max": 10 + }, + "pwrdice": { + "pwrdie": "", + "recovery": 1, + "central": { + "value": 0, + "max": 0 + }, + "comms": { + "value": 0, + "max": 0 + }, + "engines": { + "value": 0, + "max": 0 + }, + "shields": { + "value": 0, + "max": 0 + }, + "sensors": { + "value": 0, + "max": 0 + }, + "weapons": { + "value": 0, + "max": 0 + } + }, + "shld": { + "die": "", + "dice": 0, + "formula": "", + "value": null, + "max": null + }, + "shieldpow": 1, + "sscap": 0, + "suites": { + "open": 0, + "max": 0 + }, + "weaponpow": 1 + }, + "details": { + "tier": 0, + "role": "", + "source": "" + }, + "skills": { + "ast": { + "value": 0, + "ability": "int" + }, + "bst": { + "value": 0, + "ability": "str" + }, + "dat": { + "value": 0, + "ability": "int" + }, + "hid": { + "value": 0, + "ability": "dex" + }, + "imp": { + "value": 0, + "ability": "cha" + }, + "int": { + "value": 0, + "ability": "cha" + }, + "man": { + "value": 0, + "ability": "dex" + }, + "men": { + "value": 0, + "ability": "cha" + }, + "pat": { + "value": 0, + "ability": "con" + }, + "prb": { + "value": 0, + "ability": "int" + }, + "ram": { + "value": 0, + "ability": "str" + }, + "reg": { + "value": 0, + "ability": "con" + }, + "scn": { + "value": 0, + "ability": "wis" + }, + "swn": { + "value": 0, + "ability": "cha" + } + }, + "traits": { + "size": "med" + } }, - "traits": { - "languages": { - "value": [], - "custom": "" - } - }, - "powers": { - "power1": { - "fvalue": 1000, - "fmax": 0, - "foverride": null, - "tvalue": 1000, - "tmax": 0, - "toverride": null - }, - "power2": { - "fvalue": 1000, - "fmax": 0, - "foverride": null, - "tvalue": 1000, - "tmax": 0, - "toverride": null - }, - "power3": { - "fvalue": 1000, - "fmax": 0, - "foverride": null, - "tvalue": 1000, - "tmax": 0, - "toverride": null - }, - "power4": { - "fvalue": 1000, - "fmax": 0, - "foverride": null, - "tvalue": 1000, - "tmax": 0, - "toverride": null - }, - "power5": { - "fvalue": 1000, - "fmax": 0, - "foverride": null, - "tvalue": 1000, - "tmax": 0, - "toverride": null - }, - "power6": { - "fvalue": 1, - "fmax": 0, - "foverride": null, - "tvalue": 1, - "tmax": 0, - "toverride": null - }, - "power7": { - "fvalue": 1, - "fmax": 0, - "foverride": null, - "tvalue": 1, - "tmax": 0, - "toverride": null - }, - "power8": { - "fvalue": 1, - "fmax": 0, - "foverride": null, - "tvalue": 1, - "tmax": 0, - "toverride": null - }, - "power9": { - "fvalue": 1, - "fmax": 0, - "foverride": null, - "tvalue": 1, - "tmax": 0, - "toverride": null - } - }, - "bonuses": { - "mwak": { - "attack": "", - "damage": "" - }, - "rwak": { - "attack": "", - "damage": "" - }, - "mpak": { - "attack": "", - "damage": "" - }, - "rpak": { - "attack": "", - "damage": "" - }, - "abilities": { - "check": "", - "save": "", - "skill": "" - }, - "power": { - "forceLightDC": "", - "forceUnivDC": "", - "forceDarkDC": "", - "techDC": "" - } + "vehicle": { + "templates": ["common"], + "vehicleType": "water", + "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": [] + } } - } }, - "htmlFields": ["details.biography.value", "details.biography.public"], - "character": { - "templates": ["common", "creature"], - "attributes": { - "death": { - "success": 0, - "failure": 0 + "Item": { + "types": [ + "archetype", + "background", + "backpack", + "class", + "classfeature", + "consumable", + "deployment", + "deploymentfeature", + "equipment", + "feat", + "fightingmastery", + "fightingstyle", + "lightsaberform", + "loot", + "power", + "species", + "starship", + "starshipfeature", + "starshipmod", + "tool", + "venture", + "weapon" + ], + "templates": { + "archetypeDescription": { + "description": "", + "source": "" + }, + "backgroundDescription": { + "flavorText": { + "value": "" + }, + "flavorName": { + "value": "" + }, + "flavorDescription": { + "value": "" + }, + "flavorOptions": { + "value": "" + }, + "skillProficiencies": { + "value": "" + }, + "toolProficiencies": { + "value": "" + }, + "languages": { + "value": "" + }, + "equipment": { + "value": "" + }, + "suggestedCharacteristics": { + "value": "" + }, + "featureName": { + "value": "" + }, + "featureText": { + "value": "" + }, + "featOptions": { + "value": "" + }, + "personalityTraitOptions": { + "value": "" + }, + "idealOptions": { + "value": "" + }, + "flawOptions": { + "value": "" + }, + "bondOptions": { + "value": "" + }, + "source": { + "value": "" + } + }, + "itemDescription": { + "description": { + "value": "", + "chat": "", + "unidentified": "" + }, + "requirements": "", + "source": "" + }, + "classDescription": { + "description": { + "value": "" + } + }, + "fightingmasteryDescription": { + "description": { + "value": "" + }, + "source": { + "value": "" + } + }, + "fightingstyleDescription": { + "description": { + "value": "" + }, + "source": { + "value": "" + } + }, + "lightsaberformDescription": { + "description": { + "value": "" + }, + "source": { + "value": "" + } + }, + "speciesDescription": { + "data": "$characteristics-table", + "description": { + "value": "", + "chat": "", + "unidentified": "" + }, + "source": "", + "traits": "" + }, + "ventureDescription": { + "description": { + "value": "" + }, + "prerequisites": [""], + "source": { + "value": "" + } + }, + "physicalItem": { + "quantity": 1, + "weight": 0, + "price": 0, + "attunement": 0, + "equipped": false, + "rarity": "", + "identified": true + }, + "activatedEffect": { + "activation": { + "type": "", + "cost": 0, + "condition": "" + }, + "duration": { + "value": null, + "units": "" + }, + "target": { + "value": null, + "width": null, + "units": "", + "type": "" + }, + "range": { + "value": null, + "long": null, + "units": "" + }, + "uses": { + "value": 0, + "max": 0, + "per": null + }, + "consume": { + "type": "", + "target": null, + "amount": null + } + }, + "action": { + "ability": null, + "actionType": null, + "attackBonus": 0, + "chatFlavor": "", + "critical": null, + "damage": { + "parts": [], + "versatile": "" + }, + "formula": "", + "save": { + "ability": "", + "dc": null, + "scaling": "power" + } + }, + "mountable": { + "armor": { + "value": 10 + }, + "hp": { + "value": 0, + "max": 0, + "dt": null, + "conditions": "" + } + }, + "starshipDescription": { + "description": { + "value": "" + } + }, + "starshipEquipment": { + "capx": { + "value": null + }, + "hpperhd": { + "value": null + }, + "regrateco": { + "value": null + }, + "cscap": { + "value": null + }, + "sscap": { + "value": null + }, + "fuelcostsmod": { + "value": null + }, + "powerdicerec": { + "value": null + }, + "hdclass": { + "value": null + } + } }, - "encumbrance": { - "value": null, - "max": null + "htmlFields": ["description.value", "description.chat", "description.unidentified"], + "archetype": { + "templates": ["archetypeDescription"], + "className": "", + "classCasterType": "" }, - "exhaustion": 0, - "inspiration": 0 - }, - "details": { - "background": "", - "originalClass": "", - "xp": { - "value": 0, - "min": 0, - "max": 300 + "background": { + "templates": ["backgroundDescription"] }, - "appearance": "", - "trait": "", - "ideal": "", - "bond": "", - "flaw": "" - }, - "resources": { - "primary": { - "value": 0, - "max": 0, - "sr": 0, - "lr": 0 + "backpack": { + "templates": ["itemDescription", "physicalItem"], + "capacity": { + "type": "weight", + "value": 0, + "weightless": false + }, + "currency": { + "gc": 0 + } }, - "secondary": { - "value": 0, - "max": 0, - "sr": 0, - "lr": 0 + "class": { + "templates": ["classDescription"], + "className": "", + "levels": 1, + "archetype": "", + "hitDice": "d6", + "hitDiceUsed": 0, + "saves": [], + "skills": { + "number": 2, + "choices": [], + "value": [] + }, + "source": "", + "powercasting": { + "progression": "none", + "ability": "" + } }, - "tertiary": { - "value": 0, - "max": 0, - "sr": 0, - "lr": 0 - } - }, - "traits": { - "weaponProf": { - "value": [], - "custom": "" + "classfeature": { + "templates": ["itemDescription", "activatedEffect", "action"], + "recharge": { + "charged": false, + "value": null + } }, - "armorProf": { - "value": [], - "custom": "" + "deployment": { + "templates": ["classDescription"], + "flavorText": "", + "source": "" }, - "toolProf": { - "value": [], - "custom": "" - } - } - }, - "npc": { - "templates": ["common", "creature"], - "details": { - "type": { - "value": "", - "subtype": "", - "swarm": "", - "custom": "" + "deploymentfeature": { + "templates": ["itemDescription", "activatedEffect", "action"], + "recharge": { + "charged": false, + "value": null + } }, - "environment": "", - "cr": 1, - "powerForceLevel": 0, - "powerTechLevel": 0, - "xp": { - "value": 10 + "fightingmastery": { + "templates": ["fightingmasteryDescription"] }, - "source": "" - }, - "resources": { - "legact": { - "value": 0, - "max": 0 + "fightingstyle": { + "templates": ["fightingstyleDescription"] }, - "legres": { - "value": 0, - "max": 0 + "lightsaberform": { + "templates": ["lightsaberformDescription"] }, - "lair": { - "value": 0, - "initiative": 0 - } - } - }, - "starship": { - "templates": ["common"], - "attributes": { - "cargcap": 0, - "crewcap": 0, - "cscap": 0, - "death": { - "failure": 0, - "success": 0 + "venture": { + "templates": ["ventureDescription"] }, - "dr": 0, - "engpow": 1, - "exhaustion": 0, - "hsm": 1, - "hull": { - "die": "", - "dice": 0, - "formula":"", - "value": null, - "max": null - }, - "mods": { - "open": 10, - "max": 10 - }, - "pwrdice": { - "pwrdie": "", - "recovery": 1, - "central": { - "value": 0, - "max": 0 - }, - "comms": { - "value": 0, - "max": 0 - }, - "engines": { - "value": 0, - "max": 0 - }, - "shields": { - "value": 0, - "max": 0 - }, - "sensors": { - "value": 0, - "max": 0 - }, - "weapons": { - "value": 0, - "max": 0 - } - }, - "shld": { - "die": "", - "dice": 0, - "formula":"", - "value": null, - "max": null - }, - "shieldpow": 1, - "sscap": 0, - "suites": { - "open": 0, - "max": 0 - }, - "weaponpow": 1 - }, - "details": { - "tier": 0, - "role": "", - "source": "" - }, - "skills": { - "ast": { - "value": 0, - "ability": "int" - }, - "bst": { - "value": 0, - "ability": "str" - }, - "dat": { - "value": 0, - "ability": "int" - }, - "hid": { - "value": 0, - "ability": "dex" - }, - "imp": { - "value": 0, - "ability": "cha" - }, - "int": { - "value": 0, - "ability": "cha" - }, - "man": { - "value": 0, - "ability": "dex" - }, - "men": { - "value": 0, - "ability": "cha" - }, - "pat": { - "value": 0, - "ability": "con" - }, - "prb": { - "value": 0, - "ability": "int" - }, - "ram": { - "value": 0, - "ability": "str" - }, - "reg": { - "value": 0, - "ability": "con" - }, - "scn": { - "value": 0, - "ability": "wis" - }, - "swn": { - "value": 0, - "ability": "cha" - } - }, - "traits": { - "size": "med" - } - }, - "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": { - "types": [ - "archetype", - "background", - "backpack", - "class", - "classfeature", - "consumable", - "deployment", - "deploymentfeature", - "equipment", - "feat", - "fightingmastery", - "fightingstyle", - "lightsaberform", - "loot", - "power", - "species", - "starship", - "starshipfeature", - "starshipmod", - "tool", - "venture", - "weapon" - ], - "templates": { - "archetypeDescription": { - "description": "", - "source": "" - }, - "backgroundDescription": { - "flavorText": { - "value": "" - }, - "flavorName": { - "value": "" - }, - "flavorDescription": { - "value": "" - }, - "flavorOptions": { - "value": "" - }, - "skillProficiencies": { - "value": "" - }, - "toolProficiencies": { - "value": "" - }, - "languages": { - "value": "" + "consumable": { + "templates": ["itemDescription", "physicalItem", "activatedEffect", "action"], + "consumableType": "potion", + "uses": { + "autoDestroy": false + } }, "equipment": { - "value": "" + "templates": [ + "itemDescription", + "physicalItem", + "activatedEffect", + "action", + "mountable", + "starshipEquipment" + ], + "armor": { + "type": "light", + "value": 10, + "dex": null + }, + "speed": { + "value": null, + "conditions": "" + }, + "strength": 0, + "stealth": false, + "proficient": true }, - "suggestedCharacteristics": { - "value": "" + "feat": { + "templates": ["itemDescription", "activatedEffect", "action"], + "requirements": "", + "recharge": { + "value": null, + "charged": false + } }, - "featureName": { - "value": "" + "loot": { + "templates": ["itemDescription", "physicalItem"] }, - "featureText": { - "value": "" + "species": { + "templates": ["speciesDescription"] }, - "featOptions": { - "value": "" + "tool": { + "templates": ["itemDescription", "physicalItem"], + "toolType": "", + "ability": "int", + "chatFlavor": "", + "proficient": 0 }, - "personalityTraitOptions": { - "value": "" + "power": { + "templates": ["itemDescription", "activatedEffect", "action"], + "level": 1, + "school": "", + "components": { + "value": "", + "vocal": false, + "somatic": false, + "material": false, + "ritual": false, + "concentration": false + }, + "materials": { + "value": "", + "consumed": false, + "cost": 0, + "supply": 0 + }, + "preparation": { + "mode": "prepared", + "prepared": true + }, + "scaling": { + "mode": "none", + "formula": null + } }, - "idealOptions": { - "value": "" + "starship": { + "templates": ["starshipDescription"], + "size": "", + "tier": 0, + "hullDice": "d6", + "hullDiceStart": 1, + "hullDiceUsed": 0, + "shldDice": "d6", + "shldDiceStart": 1, + "shldDiceUsed": 0, + "pwrDice": "1", + "source": "SotG" }, - "flawOptions": { - "value": "" + "starshipfeature": { + "templates": ["itemDescription", "activatedEffect", "action"], + "size": "med", + "tier": 0 }, - "bondOptions": { - "value": "" + "starshipmod": { + "templates": ["itemDescription", "activatedEffect", "action"], + "recharge": { + "charged": false, + "value": null + }, + "system": { + "value": "" + }, + "grade": { + "value": "" + }, + "basecost": { + "value": "" + }, + "prerequisites": { + "value": "" + } }, - "source": { - "value": "" + "weapon": { + "templates": ["itemDescription", "physicalItem", "activatedEffect", "action", "mountable"], + "weaponType": "simpleVW", + "properties": {}, + "proficient": true } - }, - "itemDescription": { - "description": { - "value": "", - "chat": "", - "unidentified": "" - }, - "requirements": "", - "source": "" - }, - "classDescription": { - "description": { - "value": "" - } - }, - "fightingmasteryDescription": { - "description": { - "value": "" - }, - "source": { - "value": "" - } - }, - "fightingstyleDescription": { - "description": { - "value": "" - }, - "source": { - "value": "" - } - }, - "lightsaberformDescription": { - "description": { - "value": "" - }, - "source": { - "value": "" - } - }, - "speciesDescription": { - "data": "$characteristics-table", - "description": { - "value": "", - "chat": "", - "unidentified": "" - }, - "source": "", - "traits": "" - }, - "ventureDescription": { - "description": { - "value": "" - }, - "prerequisites": [""], - "source": { - "value": "" - } - }, - "physicalItem": { - "quantity": 1, - "weight": 0, - "price": 0, - "attuned": false, - "attunement": 0, - "equipped": false, - "rarity": "", - "identified": true - }, - "activatedEffect": { - "activation": { - "type": "", - "cost": 0, - "condition": "" - }, - "duration": { - "value": null, - "units": "" - }, - "target": { - "value": null, - "width": null, - "units": "", - "type": "" - }, - "range": { - "value": null, - "long": null, - "units": "" - }, - "uses": { - "value": 0, - "max": 0, - "per": null - }, - "consume": { - "type": "", - "target": null, - "amount": null - } - }, - "action": { - "ability": null, - "actionType": null, - "attackBonus": 0, - "chatFlavor": "", - "critical": null, - "damage": { - "parts": [], - "versatile": "" - }, - "formula": "", - "save": { - "ability": "", - "dc": null, - "scaling": "power" - } - }, - "mountable": { - "armor": { - "value": 10 - }, - "hp": { - "value": 0, - "max": 0, - "dt": null, - "conditions": "" - } - }, - "starshipDescription": { - "description": { - "value": "" - } - }, - "starshipEquipment": { - "capx": { - "value": null - }, - "hpperhd": { - "value": null - }, - "regrateco": { - "value": null - }, - "cscap": { - "value": null - }, - "sscap": { - "value": null - }, - "fuelcostsmod": { - "value": null - }, - "powerdicerec": { - "value": null - }, - "hdclass": { - "value": null - } - } - }, - "htmlFields": [ - "description.value", - "description.chat", - "description.unidentified" - ], - "archetype": { - "templates": ["archetypeDescription"], - "className": "", - "classCasterType": "" - }, - "background": { - "templates": ["backgroundDescription"] - }, - "backpack": { - "templates": ["itemDescription", "physicalItem"], - "capacity": { - "type": "weight", - "value": 0, - "weightless": false - }, - "currency": { - "gc": 0 - } - }, - "class": { - "templates": ["classDescription"], - "className": "", - "levels": 1, - "archetype": "", - "hitDice": "d6", - "hitDiceUsed": 0, - "saves": [], - "skills": { - "number": 2, - "choices": [], - "value": [] - }, - "source": "", - "powercasting": { - "progression": "none", - "ability": "" - } - }, - "classfeature": { - "templates": ["itemDescription", "activatedEffect", "action"], - "recharge": { - "charged": false, - "value": null - } - }, - "deployment": { - "templates": ["classDescription"], - "flavorText": "", - "source": "" - }, - "deploymentfeature": { - "templates": ["itemDescription", "activatedEffect", "action"], - "recharge": { - "charged": false, - "value": null - } - }, - "fightingmastery": { - "templates": ["fightingmasteryDescription"] - }, - "fightingstyle": { - "templates": ["fightingstyleDescription"] - }, - "lightsaberform": { - "templates": ["lightsaberformDescription"] - }, - "venture": { - "templates": ["ventureDescription"] - }, - "consumable": { - "templates": [ - "itemDescription", - "physicalItem", - "activatedEffect", - "action" - ], - "consumableType": "potion", - "uses": { - "autoDestroy": false - } - }, - "equipment": { - "templates": [ - "itemDescription", - "physicalItem", - "activatedEffect", - "action", - "mountable", - "starshipEquipment" - ], - "armor": { - "type": "light", - "value": 10, - "dex": null - }, - "speed": { - "value": null, - "conditions": "" - }, - "strength": 0, - "stealth": false, - "proficient": true - }, - "feat": { - "templates": ["itemDescription", "activatedEffect", "action"], - "requirements": "", - "recharge": { - "value": null, - "charged": false - } - }, - "loot": { - "templates": ["itemDescription", "physicalItem"] - }, - "species": { - "templates": ["speciesDescription"] - }, - "tool": { - "templates": ["itemDescription", "physicalItem"], - "ability": "int", - "chatFlavor": "", - "proficient": 0 - }, - "power": { - "templates": ["itemDescription", "activatedEffect", "action"], - "level": 1, - "school": "", - "components": { - "value": "", - "vocal": false, - "somatic": false, - "material": false, - "ritual": false, - "concentration": false - }, - "materials": { - "value": "", - "consumed": false, - "cost": 0, - "supply": 0 - }, - "preparation": { - "mode": "prepared", - "prepared": true - }, - "scaling": { - "mode": "none", - "formula": null - } - }, - "starship": { - "templates": ["starshipDescription"], - "size": "", - "tier": 0, - "hullDice": "d6", - "hullDiceStart": 1, - "hullDiceUsed": 0, - "shldDice": "d6", - "shldDiceStart": 1, - "shldDiceUsed": 0, - "pwrDice": "1", - "source": "SotG" - }, - "starshipfeature": { - "templates": ["itemDescription", "activatedEffect", "action"], - "size": "med", - "tier": 0 - }, - "starshipmod": { - "templates": ["itemDescription", "activatedEffect", "action"], - "recharge": { - "charged": false, - "value": null - }, - "system": { - "value": "" - }, - "grade": { - "value": "" - }, - "basecost": { - "value": "" - }, - "prerequisites": { - "value": "" - } - }, - "weapon": { - "templates": [ - "itemDescription", - "physicalItem", - "activatedEffect", - "action", - "mountable" - ], - "weaponType": "simpleVW", - "properties": {}, - "proficient": true } - } } diff --git a/templates/actors/newActor/character-sheet.html b/templates/actors/newActor/character-sheet.html index 9983d562..92def5da 100644 --- a/templates/actors/newActor/character-sheet.html +++ b/templates/actors/newActor/character-sheet.html @@ -2,6 +2,7 @@ {{!-- Sheet Header --}}
+ {{> "systems/sw5e/templates/actors/newActor/parts/swalt-warnings.html"}}

@@ -36,11 +37,16 @@
{{!-- ARMOR CLASS --}}
-

{{ localize "SW5E.ArmorClass" }}

-
- -
+ +
  • +

    + {{ localize "SW5E.ArmorClass" }} + +

    +
    + {{data.attributes.ac.value}} +
    +
  • {{!-- HIT POINTS --}} diff --git a/templates/actors/newActor/npc-sheet.html b/templates/actors/newActor/npc-sheet.html index 463c6d57..64782ccb 100644 --- a/templates/actors/newActor/npc-sheet.html +++ b/templates/actors/newActor/npc-sheet.html @@ -3,6 +3,8 @@ {{!-- NPC Sheet Header --}}
    + {{> "systems/sw5e/templates/actors/newActor/parts/swalt-warnings.html"}} +

    @@ -15,8 +17,12 @@
    {{data.details.xp.value}} XP
    - -
    +
    + + {{ localize "SW5E.Proficiency" }} {{numberFormat data.attributes.prof decimals=0 sign=true}} + +
    +
    {{lookup config.actorSizes data.traits.size}} {{!-- ARMOR CLASS --}}
    -

    {{ localize "SW5E.ArmorClass" }}

    -
    - -
    -
    - {{ localize "SW5E.Proficiency" }} - {{numberFormat data.attributes.prof decimals=0 sign=true}} -
    +
  • +

    + {{ localize "SW5E.ArmorClass" }} + +

    +
    + {{data.attributes.ac.value}} +
    +
    + {{labels.armorType}} +
    +
  • {{!-- HIT POINTS --}} @@ -99,7 +108,7 @@ {{numberFormat ability.mod decimals=0 sign=true}} + value="{{ability.baseProf}}" data-dtype="Number" /> - diff --git a/templates/actors/newActor/parts/swalt-core.html b/templates/actors/newActor/parts/swalt-core.html index 0f8a1dfe..728bf3ed 100644 --- a/templates/actors/newActor/parts/swalt-core.html +++ b/templates/actors/newActor/parts/swalt-core.html @@ -12,7 +12,7 @@ {{numberFormat ability.mod decimals=0 sign=true}} + value="{{ability.baseProf}}" data-dtype="Number" /> - diff --git a/templates/actors/newActor/parts/swalt-inventory.html b/templates/actors/newActor/parts/swalt-inventory.html index b7dd1284..1aff56a4 100644 --- a/templates/actors/newActor/parts/swalt-inventory.html +++ b/templates/actors/newActor/parts/swalt-inventory.html @@ -74,7 +74,7 @@ {{#if ../../isCharacter}}
    {{#if item.totalWeight}} - {{ item.totalWeight }} {{localize "SW5E.AbbreviationLbs"}} + {{ item.totalWeight }} {{ @root.weightUnit }} {{/if}}
    {{/if}} diff --git a/templates/actors/newActor/parts/swalt-traits.html b/templates/actors/newActor/parts/swalt-traits.html index a1efa627..be44e8d2 100644 --- a/templates/actors/newActor/parts/swalt-traits.html +++ b/templates/actors/newActor/parts/swalt-traits.html @@ -112,7 +112,7 @@
    - +
      @@ -123,7 +123,7 @@
    - +
      @@ -134,7 +134,7 @@
    - +
      diff --git a/templates/actors/newActor/parts/swalt-warnings.html b/templates/actors/newActor/parts/swalt-warnings.html new file mode 100644 index 00000000..71212e6f --- /dev/null +++ b/templates/actors/newActor/parts/swalt-warnings.html @@ -0,0 +1,5 @@ +
        +{{#each warnings}} +
      1. {{localize this}}
      2. +{{/each}} +
      diff --git a/templates/actors/newActor/vehicle-sheet.html b/templates/actors/newActor/vehicle-sheet.html index c0214b6e..392e4fb9 100644 --- a/templates/actors/newActor/vehicle-sheet.html +++ b/templates/actors/newActor/vehicle-sheet.html @@ -3,7 +3,9 @@ {{actor.name}}
      -

      + {{> "systems/sw5e/templates/actors/parts/actor-warnings.html"}} + +

      @@ -12,8 +14,10 @@ {{lookup config.actorSizes data.traits.size}}
    • - {{localize 'SW5E.Vehicle'}} -
    • + +
    • {{localize 'SW5E.ArmorClass'}}

      - +
      {{> 'systems/sw5e/templates/actors/newActor/parts/swalt-inventory.html' sections=cargo}}
    - +
    + {{> "systems/sw5e/templates/actors/parts/active-effects.html"}} +
    {{editor content=data.details.biography.value target='data.details.biography.value' button=true owner=owner editable=editable rollData=rollData}} diff --git a/templates/actors/oldActor/character-sheet.html b/templates/actors/oldActor/character-sheet.html index 9bda3baa..aa58d6ce 100644 --- a/templates/actors/oldActor/character-sheet.html +++ b/templates/actors/oldActor/character-sheet.html @@ -5,6 +5,8 @@
    + {{> "systems/sw5e/templates/actors/oldActor/parts/actor-warnings.html"}} +

    @@ -74,10 +76,13 @@ -
  • -

    {{ localize "SW5E.ArmorClass" }}

    -
    - +
  • +

    + {{ localize "SW5E.ArmorClass" }} + +

    +
    + {{data.attributes.ac.value}}
    {{localize "SW5E.PowerDC"}} {{data.attributes.powerdc}} @@ -136,7 +141,7 @@
    {{numberFormat ability.mod decimals=0 sign=true}} - + {{{ability.icon}}} {{numberFormat ability.save decimals=0 sign=true}}
    @@ -149,7 +154,7 @@ {{#each config.skills as |label s|}} {{#with (lookup ../data.skills s) as |skill|}}
  • - + {{{skill.icon}}}

    {{label}}

    {{skill.ability}} diff --git a/templates/actors/oldActor/npc-sheet.html b/templates/actors/oldActor/npc-sheet.html index 7d99d4e5..d7479daa 100644 --- a/templates/actors/oldActor/npc-sheet.html +++ b/templates/actors/oldActor/npc-sheet.html @@ -5,6 +5,8 @@
    + {{> "systems/sw5e/templates/actors/oldActor/parts/actor-warnings.html"}} +

    @@ -17,6 +19,11 @@
    {{data.details.xp.value}} XP
    +
    + + {{ localize "SW5E.Proficiency" }} {{numberFormat data.attributes.prof decimals=0 sign=true}} + +
    {{!-- Character Summary --}} @@ -51,14 +58,16 @@
  • -
  • -

    {{ localize "SW5E.ArmorClass" }}

    -
    - +
  • +

    + {{ localize "SW5E.ArmorClass" }} + +

    +
    + {{data.attributes.ac.value}}
    - {{ localize "SW5E.Proficiency" }} - {{numberFormat data.attributes.prof decimals=0 sign=true}} + {{labels.armorType}}
  • @@ -99,7 +108,7 @@
    {{numberFormat ability.mod decimals=0 sign=true}} - + {{{ability.icon}}} {{numberFormat ability.save decimals=0 sign=true}}
    @@ -112,7 +121,7 @@ {{#each config.skills as |label s|}} {{#with (lookup ../data.skills s) as |skill|}}
  • - + {{{skill.icon}}}

    {{label}}

    {{skill.ability}} diff --git a/templates/actors/oldActor/parts/actor-features.html b/templates/actors/oldActor/parts/actor-features.html index 58796acf..11449033 100644 --- a/templates/actors/oldActor/parts/actor-features.html +++ b/templates/actors/oldActor/parts/actor-features.html @@ -108,7 +108,7 @@ {{#if section.columns}} {{#each section.columns}}
    - {{#with (lookup item property)}} + {{#with (getProperty item property)}} {{#if ../editable}} diff --git a/templates/actors/oldActor/parts/actor-inventory.html b/templates/actors/oldActor/parts/actor-inventory.html index 956bfdaa..e735dcd0 100644 --- a/templates/actors/oldActor/parts/actor-inventory.html +++ b/templates/actors/oldActor/parts/actor-inventory.html @@ -89,7 +89,7 @@
    {{#if item.totalWeight}}
    - {{ item.totalWeight }} {{localize "SW5E.AbbreviationLbs"}} + {{ item.totalWeight }} {{ @root.weightUnit }}
    {{/if}}
    diff --git a/templates/actors/oldActor/parts/actor-traits.html b/templates/actors/oldActor/parts/actor-traits.html index 498a75f5..de40720e 100644 --- a/templates/actors/oldActor/parts/actor-traits.html +++ b/templates/actors/oldActor/parts/actor-traits.html @@ -85,7 +85,7 @@ {{#if isCharacter}}
    - +
      @@ -97,7 +97,7 @@
      - +
        @@ -109,7 +109,7 @@
        - +
          diff --git a/templates/actors/oldActor/parts/actor-warnings.html b/templates/actors/oldActor/parts/actor-warnings.html new file mode 100644 index 00000000..71212e6f --- /dev/null +++ b/templates/actors/oldActor/parts/actor-warnings.html @@ -0,0 +1,5 @@ +
            +{{#each warnings}} +
          1. {{localize this}}
          2. +{{/each}} +
          diff --git a/templates/actors/oldActor/vehicle-sheet.html b/templates/actors/oldActor/vehicle-sheet.html index 6b33b41a..496ac203 100644 --- a/templates/actors/oldActor/vehicle-sheet.html +++ b/templates/actors/oldActor/vehicle-sheet.html @@ -3,7 +3,9 @@ {{actor.name}}
          -

          + {{> "systems/sw5e/templates/actors/parts/actor-warnings.html"}} + +

          @@ -12,8 +14,10 @@ {{lookup config.actorSizes data.traits.size}}
        • - {{localize 'SW5E.Vehicle'}} -
        • + +
        • {{localize 'SW5E.ArmorClass'}}

          - +
          {{> 'systems/sw5e/templates/actors/oldActor/parts/actor-inventory.html' sections=cargo}}
        - +
        + {{> "systems/sw5e/templates/actors/parts/active-effects.html"}} +
        {{editor content=data.details.biography.value target='data.details.biography.value' button=true owner=owner editable=editable rollData=rollData}} diff --git a/templates/actors/parts/active-effects.html b/templates/actors/parts/active-effects.html index 26ce515a..c2f51609 100644 --- a/templates/actors/parts/active-effects.html +++ b/templates/actors/parts/active-effects.html @@ -1,38 +1,48 @@
          -{{#each effects as |section sid|}} -
        1. -

          {{localize section.label}}

          -
          {{localize "SW5E.Source"}}
          -
          {{localize "SW5E.Duration"}}
          - -
        2. + {{#each effects as |section sid|}} + {{#unless section.hidden}} +
        3. +

          {{localize section.label}}

          +
          {{localize "SW5E.Source"}}
          +
          {{localize "SW5E.Duration"}}
          + +
        4. -
            - {{#each section.effects as |effect|}} -
          1. -
            - -

            {{effect.data.label}}

            -
            -
            {{effect.sourceName}}
            -
            {{effect.duration.label}}
            - -
          2. + {{#if section.info}} +
              + {{#each section.info}} +
            1. {{this}}
            2. + {{/each}} +
            + {{/if}} + +
              + {{#each section.effects as |effect|}} +
            1. +
              + +

              {{effect.data.label}}

              +
              +
              {{effect.sourceName}}
              +
              {{effect.duration.label}}
              + +
            2. + {{/each}} +
            + {{/unless}} {{/each}} -
          -{{/each}}
        diff --git a/templates/apps/actor-armor.html b/templates/apps/actor-armor.html new file mode 100644 index 00000000..0ec89983 --- /dev/null +++ b/templates/apps/actor-armor.html @@ -0,0 +1,21 @@ +
        +
        + +
        + +
        + + +
        + +
        + + +
        + + +
        diff --git a/templates/apps/property-attribution.html b/templates/apps/property-attribution.html new file mode 100644 index 00000000..59674939 --- /dev/null +++ b/templates/apps/property-attribution.html @@ -0,0 +1,16 @@ +
        + + {{#each sources as |source|}} + + + + + {{/each}} + + + + +
        + {{source.value}} + {{source.label}}
        {{total}}{{localize "SW5E.PropertyTotal"}}
        +
        diff --git a/templates/apps/trait-selector.html b/templates/apps/trait-selector.html index 903556aa..263a48fe 100644 --- a/templates/apps/trait-selector.html +++ b/templates/apps/trait-selector.html @@ -1,19 +1,27 @@
        -
          - {{#each choices as |choice key|}} -
        1. - -
        2. - {{/each}} -
        + + {{#*inline "traitList"}} +
          + {{#each choices as |choice key|}} +
        1. + + {{#if choice.children}} + {{> traitList choices=choice.children}} + {{/if}} +
        2. + {{/each}} +
        + {{/inline}} + + {{> traitList}} {{#if allowCustom}} -
        - - -
        +
        + + +
        {{/if}}
        diff --git a/templates/chat/item-card.html b/templates/chat/item-card.html index 1c2b3016..fc3574e9 100644 --- a/templates/chat/item-card.html +++ b/templates/chat/item-card.html @@ -1,4 +1,4 @@ -
        @@ -27,7 +27,7 @@ {{/if}} {{#if hasSave}} - {{/if}} @@ -40,7 +40,6 @@ {{/if}} - {{#if isTool}} {{/if}} diff --git a/templates/items/backpack.html b/templates/items/backpack.html index 24b586d1..58797cbe 100644 --- a/templates/items/backpack.html +++ b/templates/items/backpack.html @@ -15,10 +15,11 @@
          -
        • - -
        • + +
        • @@ -53,11 +54,7 @@
          diff --git a/templates/items/class.html b/templates/items/class.html index de0fe204..128b4dec 100644 --- a/templates/items/class.html +++ b/templates/items/class.html @@ -128,12 +128,31 @@
      +
      + +
      +
        + {{#each data.skills.choices}} +
      • {{lookup ../config.skills this}}
      • + {{/each}} +
      +
      +
      +
      diff --git a/templates/items/consumable.html b/templates/items/consumable.html index 06aff718..a3079c35 100644 --- a/templates/items/consumable.html +++ b/templates/items/consumable.html @@ -19,7 +19,9 @@ {{lookup config.consumableTypes data.consumableType }}
    • - +
    • @@ -49,11 +51,7 @@
      diff --git a/templates/items/equipment.html b/templates/items/equipment.html index 4e42f8ba..4c975223 100644 --- a/templates/items/equipment.html +++ b/templates/items/equipment.html @@ -19,7 +19,9 @@ {{lookup config.equipmentTypes data.armor.type }}
    • - +
    • @@ -49,12 +51,7 @@
      diff --git a/templates/items/loot.html b/templates/items/loot.html index 3a0249dd..19a3b619 100644 --- a/templates/items/loot.html +++ b/templates/items/loot.html @@ -16,7 +16,9 @@
      • - +
      • diff --git a/templates/items/tool.html b/templates/items/tool.html index 6a2c36f2..bc442d04 100644 --- a/templates/items/tool.html +++ b/templates/items/tool.html @@ -16,7 +16,9 @@
        • - +
        • @@ -40,27 +42,27 @@ {{!-- Details Tab --}}
          + {{!-- Tool Type --}} +
          + + +
          + {{!-- Tool Proficiency --}}
          + {{selectOptions config.proficiencyLevels selected=data.proficient}} +
          {{!-- Ability Check --}}
          diff --git a/templates/items/weapon.html b/templates/items/weapon.html index cd696c2b..9051e449 100644 --- a/templates/items/weapon.html +++ b/templates/items/weapon.html @@ -19,7 +19,9 @@ {{lookup config.weaponTypes data.weaponType }}
        • - +
        • @@ -49,11 +51,7 @@