From e6bff40e1bdcd8d1fe4bb6d4db4632fa643b7b62 Mon Sep 17 00:00:00 2001
From: supervj <64861570+supervj@users.noreply.github.com>
Date: Mon, 4 Jan 2021 15:23:30 -0500
Subject: [PATCH] Update Core to 1.2
Update Core to 1.2, pulled from dev 12/10/2020
---
lang/en.json | 36 +-
less/original/actors.less | 72 +-
less/original/apps.less | 57 +-
less/original/character.less | 4 +-
less/original/chat.less | 5 +-
less/original/items.less | 304 ++++-
less/original/npc.less | 7 -
less/original/sw5e.less | 30 -
less/update/components/actor-global.less | 67 +-
less/update/components/actor-themes.less | 9 +
less/update/components/sidebar-global.less | 3 +-
less/update/components/sidebar-themes.less | 3 +-
less/update/components/sidebar.less | 3 +-
module/actor/entity.js | 186 ++-
module/actor/sheets/newSheet/base.js | 833 +++++++++++++
module/actor/sheets/newSheet/character.js | 110 +-
module/actor/sheets/newSheet/npc.js | 138 +++
module/actor/sheets/newSheet/vehicle.js | 385 ++++++
module/actor/sheets/{ => oldSheets}/base.js | 79 +-
module/actor/sheets/oldSheets/character.js | 42 +-
module/actor/sheets/oldSheets/npc.js | 2 +-
module/actor/sheets/oldSheets/vehicle.js | 2 +-
module/apps/ability-use-dialog.js | 25 +-
module/apps/movement-config.js | 12 +-
module/apps/senses-config.js | 43 +
module/apps/trait-selector.js | 6 +-
module/classFeatures.js | 37 +-
module/config.js | 34 +-
module/dice.js | 4 +-
module/macros.js | 1 -
module/migration.js | 67 +-
module/pixi/ability-template.js | 13 +-
module/templates.js | 2 +
.../Archetypes/Archaeologist Pursuit.webp | Bin 0 -> 12356 bytes
packs/Icons/Archetypes/Vonil-Ishu Form.webp | Bin 0 -> 12306 bytes
packs/Icons/Archetypes/Way of Tutelage.webp | Bin 0 -> 13212 bytes
...Shield.webp => Light Physical Shield.webp} | Bin
...rator.webp => Light Shield Generator.webp} | Bin
...ult Armor.webp => Laminanium Assault.webp} | Bin
...m Mesh Armor.webp => Neutronium Mesh.webp} | Bin
...ite Armor.webp => Plastoid Composite.webp} | Bin
.../Lightsaber Forms/Vonil-Ishu Form.webp | Bin 0 -> 6796 bytes
.../Martial Blasters/Blaster Cannon.webp | Bin 0 -> 9682 bytes
.../Icons/Martial Blasters/Carbine Rifle.webp | Bin 0 -> 8354 bytes
packs/Icons/Martial Blasters/Chaingun.webp | Bin 0 -> 8308 bytes
.../Icons/Martial Blasters/Cycler Rifle.webp | Bin 0 -> 8006 bytes
.../Martial Blasters/Heavy Bowcaster.webp | Bin 0 -> 9844 bytes
.../Martial Blasters/Heavy Repeater.webp | Bin 0 -> 9934 bytes
.../Icons/Martial Blasters/Heavy Shotgun.webp | Bin 0 -> 9388 bytes
.../Martial Blasters/Heavy Slugpistol.webp | Bin 0 -> 8814 bytes
.../Icons/Martial Blasters/Hunting Rifle.webp | Bin 0 -> 7050 bytes
.../Martial Blasters/Repeating Blaster.webp | Bin 0 -> 8692 bytes
packs/Icons/Martial Blasters/Revolver.webp | Bin 0 -> 8570 bytes
packs/Icons/Martial Blasters/Subrepeater.webp | Bin 0 -> 8062 bytes
.../{Holdout Blaster.webp => Hold-Out.webp} | Bin
packs/Icons/Simple Blasters/Ion Carbine.webp | Bin 0 -> 8404 bytes
.../Icons/Simple Blasters/Light Repeater.webp | Bin 0 -> 7644 bytes
.../Simple Blasters/Light Slugpistol.webp | Bin 0 -> 7628 bytes
packs/Icons/Simple Blasters/Needler.webp | Bin 0 -> 8312 bytes
packs/Icons/Simple Blasters/Slugpistol.webp | Bin 0 -> 9110 bytes
.../Simple Blasters/Tranquilizer Rifle.webp | Bin 0 -> 7546 bytes
.../Icons/Simple Blasters/Wrist Blaster.webp | Bin 0 -> 8894 bytes
packs/Icons/Simple Blasters/Wristblaster.webp | Bin 0 -> 8894 bytes
packs/packs/archetypes.db | 134 +--
packs/packs/armor.db | 52 +-
packs/packs/backgrounds.db | 94 +-
packs/packs/classes.db | 13 +-
packs/packs/classfeatures.db | 1072 +++++++++++------
packs/packs/feats.db | 1 -
packs/packs/forcepowers.db | 74 +-
packs/packs/lightsaberforms.db | 1 +
packs/packs/species.db | 224 ++--
packs/packs/techpowers.db | 3 +
packs/packs/weapons.db | 120 +-
sw5e-dark.css | 21 +-
sw5e-global.css | 102 +-
sw5e-light.css | 21 +-
sw5e.css | 360 +++++-
sw5e.js | 24 +-
system.json | 4 +-
template.json | 20 +-
.../actors/newActor/character-sheet.html | 29 +-
templates/actors/newActor/npc-sheet.html | 195 +++
.../newActor/parts/swalt-active-effects.html | 40 +
.../newActor/parts/swalt-inventory.html | 2 +-
.../actors/newActor/parts/swalt-traits.html | 14 +-
templates/actors/newActor/vehicle-sheet.html | 158 +++
.../actors/oldActor/character-sheet.html | 11 +-
templates/actors/oldActor/npc-sheet.html | 5 +-
.../actors/oldActor/parts/actor-features.html | 4 +-
.../actors/oldActor/parts/actor-notes.html | 33 +
.../oldActor/parts/actor-powerbook.html | 52 +-
.../actors/oldActor/parts/actor-traits.html | 16 +-
templates/actors/parts/active-effects.html | 2 +-
templates/apps/ability-use.html | 20 +-
templates/apps/senses-config.html | 20 +
templates/items/archetype.html | 6 +-
templates/items/class.html | 2 +-
templates/items/consumable.html | 10 +-
templates/items/equipment.html | 10 +-
templates/items/feat.html | 1 -
templates/items/parts/item-action.html | 2 +-
templates/items/parts/item-activation.html | 2 +-
templates/items/species.html | 4 +-
templates/items/weapon.html | 10 +-
105 files changed, 4335 insertions(+), 1274 deletions(-)
create mode 100644 module/actor/sheets/newSheet/base.js
create mode 100644 module/actor/sheets/newSheet/npc.js
create mode 100644 module/actor/sheets/newSheet/vehicle.js
rename module/actor/sheets/{ => oldSheets}/base.js (94%)
create mode 100644 module/apps/senses-config.js
create mode 100644 packs/Icons/Archetypes/Archaeologist Pursuit.webp
create mode 100644 packs/Icons/Archetypes/Vonil-Ishu Form.webp
create mode 100644 packs/Icons/Archetypes/Way of Tutelage.webp
rename packs/Icons/Armor/PHB/{Small Physical Shield.webp => Light Physical Shield.webp} (100%)
rename packs/Icons/Armor/PHB/{Small Shield Generator.webp => Light Shield Generator.webp} (100%)
rename packs/Icons/Armor/WH/{Laminanium Assault Armor.webp => Laminanium Assault.webp} (100%)
rename packs/Icons/Armor/WH/{Neutronium Mesh Armor.webp => Neutronium Mesh.webp} (100%)
rename packs/Icons/Armor/WH/{Plastoid Composite Armor.webp => Plastoid Composite.webp} (100%)
create mode 100644 packs/Icons/Lightsaber Forms/Vonil-Ishu Form.webp
create mode 100644 packs/Icons/Martial Blasters/Blaster Cannon.webp
create mode 100644 packs/Icons/Martial Blasters/Carbine Rifle.webp
create mode 100644 packs/Icons/Martial Blasters/Chaingun.webp
create mode 100644 packs/Icons/Martial Blasters/Cycler Rifle.webp
create mode 100644 packs/Icons/Martial Blasters/Heavy Bowcaster.webp
create mode 100644 packs/Icons/Martial Blasters/Heavy Repeater.webp
create mode 100644 packs/Icons/Martial Blasters/Heavy Shotgun.webp
create mode 100644 packs/Icons/Martial Blasters/Heavy Slugpistol.webp
create mode 100644 packs/Icons/Martial Blasters/Hunting Rifle.webp
create mode 100644 packs/Icons/Martial Blasters/Repeating Blaster.webp
create mode 100644 packs/Icons/Martial Blasters/Revolver.webp
create mode 100644 packs/Icons/Martial Blasters/Subrepeater.webp
rename packs/Icons/Simple Blasters/{Holdout Blaster.webp => Hold-Out.webp} (100%)
create mode 100644 packs/Icons/Simple Blasters/Ion Carbine.webp
create mode 100644 packs/Icons/Simple Blasters/Light Repeater.webp
create mode 100644 packs/Icons/Simple Blasters/Light Slugpistol.webp
create mode 100644 packs/Icons/Simple Blasters/Needler.webp
create mode 100644 packs/Icons/Simple Blasters/Slugpistol.webp
create mode 100644 packs/Icons/Simple Blasters/Tranquilizer Rifle.webp
create mode 100644 packs/Icons/Simple Blasters/Wrist Blaster.webp
create mode 100644 packs/Icons/Simple Blasters/Wristblaster.webp
create mode 100644 templates/actors/newActor/npc-sheet.html
create mode 100644 templates/actors/newActor/parts/swalt-active-effects.html
create mode 100644 templates/actors/newActor/vehicle-sheet.html
create mode 100644 templates/actors/oldActor/parts/actor-notes.html
create mode 100644 templates/apps/senses-config.html
diff --git a/lang/en.json b/lang/en.json
index f0f71088..21b0c0c7 100644
--- a/lang/en.json
+++ b/lang/en.json
@@ -6,14 +6,14 @@
"ITEM.TypeClass": "Class",
"ITEM.TypeConsumable": "Consumable",
"ITEM.TypeEquipment": "Equipment",
-"ITEM.TypeFeat": "Feature",
+"ITEM.TypeFeat": "Feat",
"ITEM.TypeLoot": "Loot",
"ITEM.TypePower": "Power",
"ITEM.TypeTool": "Tool",
"ITEM.TypeWeapon": "Weapon",
"ITEM.TypeArchetype": "Archetype",
"ITEM.TypeBackground": "Background",
-"ITEM.TypeLightsaberForm": "Lightsaber Form",
+"ITEM.TypeLightsaberform": "Lightsaber Form",
"ITEM.TypeClassfeature": "Class Feature",
"ITEM.TypeFightingmastery": "FightingMastery",
"ITEM.TypeFightingstyle": "Fighting Style",
@@ -47,7 +47,7 @@
"SW5E.AbilityUseHint": "Configure how you would like to use the {name} {type}.",
"SW5E.AbilityUseUnavailableHint": "There are no uses of this item remaining!",
"SW5E.AbilityUseChargedHint": "This {type} is charged and ready to use!",
-"SW5E.AbilityUseRechargedHint": "This {type} is depleted and must be recharged!",
+"SW5E.AbilityUseRechargeHint": "This {type} is depleted and must be recharged!",
"SW5E.AbilityUseNormalHint": "This {type} has {value} of {max} uses per {per} remaining.",
"SW5E.AbilityUseConsumableChargeHint": "Using this {type} will consume 1 charge of {value} remaining.",
"SW5E.AbilityUseConsumableQuantityHint": "Using this {type} will consume 1 quantity of {quantity} remaining",
@@ -85,6 +85,11 @@
"SW5E.AlignmentBN": "Balanced Neutral",
"SW5E.Archetypes": "Archetypes",
"SW5E.Appearance": "Appearance",
+"SW5E.Attunement": "Attunement",
+"SW5E.AttunementNone": "Attunement Not Required",
+"SW5E.AttunementRequired": "Attunement Required",
+"SW5E.AttunementAttuned": "Attuned",
+"SW5E.Attuned": "Attuned",
"SW5E.ArmorClass": "Armor Class",
"SW5E.AC": "AC",
"SW5E.ArmorProperties": "Armor Properties",
@@ -122,7 +127,6 @@
"SW5E.AttackPl": "Attacks",
"SW5E.AttackRoll": "Attack Roll",
"SW5E.Attributes": "Attributes",
-"SW5E.Attuned": "Attuned",
"SW5E.Background": "Background",
"SW5E.Biography": "Biography",
"SW5E.Bonds": "Bonds",
@@ -189,7 +193,8 @@
"SW5E.ConsumeWarningNoSource": "The designated {type} source that {name} consumes no longer exists!",
"SW5E.ConsumeWarningNoQuantity": "{name} has run out of its designated {type}!",
"SW5E.ConsumeWarningZeroAttribute": "{name} has run out of its designated attribute resource pool!",
-
+"SW5E.ConsumeResource": "Consume Resource?",
+"SW5E.ConsumeRecharge": "Consume Recharge?",
"SW5E.ConsumableAmmunition": "Ammunition",
"SW5E.ConsumableFood": "Food",
"SW5E.ConsumablePoison": "Poison",
@@ -288,6 +293,8 @@
"SW5E.ItemTypeContainerPl": "Containers",
"SW5E.ItemTypeEquipment": "Equipment",
"SW5E.ItemTypeEquipmentPl": "Equipment",
+"SW5E.ItemTypeFeat": "Feat",
+"SW5E.ItemTypeFeatPl": "Feats",
"SW5E.ItemTypeFightingMastery": "Fighting Mastery",
"SW5E.ItemTypeFightingMasteryPl": "Fighting Masteries",
"SW5E.ItemTypeFightingStyle": "Fighting Style",
@@ -388,8 +395,6 @@
"SW5E.FlagsReliableTalentHint": "Rogues Reliable Talent Feature.",
"SW5E.FlagsRemarkableAthlete": "Remarkable Athlete.",
"SW5E.FlagsRemarkableAthleteHint": "Half-Proficiency (rounded-up) to physical Ability Checks and Initiative.",
-"SW5E.FlagsCritThreshold": "Critical Hit Threshold",
-"SW5E.FlagsCritThresholdHint": "Allow for expanded critical range; for example Improved or Superior Critical",
"SW5E.FlagsWeaponCritThreshold": "Weapon Critical Hit Threshold",
"SW5E.FlagsWeaponCritThresholdHint": "An expanded critical hit threshold for weapon attacks.",
"SW5E.FlagsPowerCritThreshold": "Power Critical Hit Threshold",
@@ -593,6 +598,7 @@
"SW5E.NoCharges": "No Charges",
"SW5E.None": "None",
"SW5E.Normal": "Normal",
+"SW5E.Notes": "Notes",
"SW5E.NotProficient": "Not Proficient",
"SW5E.OtherFormula": "Other Formula",
"SW5E.PactMagic": "Pact Magic",
@@ -646,7 +652,7 @@
"SW5E.RollMode": "Roll Mode",
"SW5E.RollSituationalBonus": "Situational Bonus?",
"SW5E.Save": "Save",
-
+"SW5E.Movement": "Movement",
"SW5E.MovementConfig": "Configure Movement Speed",
"SW5E.MovementConfigHint": "Configure the movement speed and special movement attributes of this creature.",
"SW5E.MovementWalk": "Walk",
@@ -656,10 +662,18 @@
"SW5E.MovementFly": "Fly",
"SW5E.MovementSwim": "Swim",
"SW5E.MovementUnits": "Units",
-
+"SW5E.Senses": "Senses",
+"SW5E.SensesConfig": "Configure Senses",
+"SW5E.SensesConfigHint": "Configure any special sensory perception abilities that this actor possesses.",
+"SW5E.SenseDarkvision": "Darkvision",
+"SW5E.SenseBlindsight": "Blindsight",
+"SW5E.SenseTremorsense": "Tremorsense",
+"SW5E.SenseTruesight": "Truesight",
+"SW5E.SenseSpecial": "Special Senses",
"SW5E.SheetClassCharacter": "Default Character Sheet",
"SW5E.SheetClassCharacterOld": "Old Character Sheet",
"SW5E.SheetClassNPC": "Default NPC Sheet",
+"SW5E.SheetClassNPCOld": "Old NPC Sheet",
"SW5E.SheetClassVehicle": "Default Vehicle Sheet",
"SW5E.SheetClassItem": "Default Item Sheet",
@@ -724,7 +738,7 @@
"SW5E.PowerAtWill": "At-Will",
"SW5E.PowerCastConsume": "Consume Power Slot?",
"SW5E.PowerCastHint": "Configure how you would like to cast the",
-"SW5E.PowerCastNoSlots": "You have no available power slots to cast this power",
+"SW5E.PowerCastNoSlots": "You have no available {level} power slots with which to cast {name}",
"SW5E.PowerCastUpcast": "Cast at Level",
"SW5E.PowercasterLevel": "Powercaster Level",
"SW5E.PowerCastingHeader": "Power Casting",
@@ -940,4 +954,4 @@
"SETTINGS.SWColorN": "Display Theme",
"SETTINGS.SWColorLight": "Light Theme",
"SETTINGS.SWColorDark": "Dark Theme"
-}
+}
\ No newline at end of file
diff --git a/less/original/actors.less b/less/original/actors.less
index 7831bd55..5edc2d98 100644
--- a/less/original/actors.less
+++ b/less/original/actors.less
@@ -69,12 +69,29 @@
line-height: 30px;
}
}
- }
- }
- .attributes {
- input.temphp {
- width: 48%;
+ // Movement Configuration
+ .movement {
+ 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%;
+ }
}
}
@@ -238,6 +255,7 @@
li.skill {
height: 24px;
+ width: 225px;
padding: 3px 2px;
&:nth-child(even) {
@@ -340,7 +358,7 @@
margin: 0 0 3px 0;
justify-content: space-between;
}
- .configure-flags {
+ .config-button {
flex: 1;
}
@@ -427,9 +445,8 @@
&.rollable .item-image:hover {
background-image: url("../../icons/svg/d20-black.svg") !important;
}
- i.attuned {
- color: @colorTan;
- }
+ i.attuned { color: @colorTan; }
+ i.not-attuned { color: @colorCrimson; }
}
// Item uses
@@ -470,6 +487,7 @@
overflow: hidden;
&:last-child { border-right: none; }
&.item-action {flex: 0 0 100px}
+ &.attunement {flex: 0 0 24px}
}
.item-weight {
@@ -491,6 +509,15 @@
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;
+ }
}
}
@@ -564,26 +591,23 @@
.powercasting-ability {
flex: 0 0 240px;
margin: 0;
-
- input, span {
- flex: 0 0 32px;
+ label, span {
+ flex: none;
+ }
+ input {
+ flex: 0 0 28px;
text-align: center;
}
select {
margin: 0 5px;
- flex: 0 0 150px;
- }
- h3.power-dc {
- flex: 1;
- text-align: right;
+ flex: 0 0 120px;
}
}
.power-slots,
.power-comps {
- flex: 0 0 75px;
- padding-right: 5px;
- text-align: right;
+ flex: none;
+ padding: 0 5px;
font-size: 12px;
color: @colorTan;
border-right: 1px solid @colorFaint;
@@ -600,9 +624,10 @@
}
}
- .power-uses {
- padding-right: 8px;
- text-align: right !important;
+ .powerbook .power-uses {
+ padding-right: 5px;
+ text-align: right;
+ color: @colorTan;
}
.power-school, .power-action, .power-target {
@@ -651,5 +676,6 @@
padding-right: 8px;
margin-bottom: 4px;
overflow-y: auto;
+ scrollbar-width: thin;
}
}
\ No newline at end of file
diff --git a/less/original/apps.less b/less/original/apps.less
index d123b6ec..53e37d78 100644
--- a/less/original/apps.less
+++ b/less/original/apps.less
@@ -178,11 +178,14 @@
background: transparent;
}
- // Rollable Links
+ // Rollable Titles
.editable .rollable:hover {
+ cursor: pointer;
+ }
+ .editable h4.rollable:hover,
+ .editable .rollable:hover > h4 {
color: #000;
text-shadow: 0 0 10px red;
- cursor: pointer;
}
// Separators
@@ -306,6 +309,7 @@
/* ----------------------------------------- */
.filter-list {
+ align-items: center;
list-style: none;
margin: 0;
padding: 0;
@@ -382,6 +386,30 @@
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;
@@ -419,32 +447,13 @@
font-size: 12px;
text-align: center;
}
- .item-name {
+ h3 {
padding-left: 5px;
- .modesto();
+ //.modesto();
+ text-align: left;
font-size: 16px;
}
}
-
- // Item Name
- .item-name {
- flex: 2;
- margin: 0;
- overflow: hidden;
- font-size: 13px;
- text-align: left;
- align-items: center;
- }
-
- // Control Buttons
- .item-controls {
- flex: 0 0 60px;
- justify-content: space-between;
- a {
- font-size: 12px;
- text-align: center;
- }
- }
}
/* ----------------------------------------- */
diff --git a/less/original/character.less b/less/original/character.less
index 908f97e6..358b1d4a 100644
--- a/less/original/character.less
+++ b/less/original/character.less
@@ -4,7 +4,7 @@
/* Basic Structure */
/* ----------------------------------------- */
.sw5e.sheet.actor.character {
- min-width: 720px;
+ min-width: 800px;
min-height: 680px;
/* ----------------------------------------- */
@@ -160,7 +160,7 @@
padding: 0 3px 3px;
label {
flex: 0 0 20px;
- .bungeeInline();
+ .russoOne();
font-size: 16px;
font-weight: normal;
line-height: 20px;
diff --git a/less/original/chat.less b/less/original/chat.less
index 9b161dc0..7bf83c2a 100644
--- a/less/original/chat.less
+++ b/less/original/chat.less
@@ -4,7 +4,8 @@
/* Chat Cards
/* ----------------------------------------- */
-.sw5e.chat-card {
+.sw5e.chat-card,
+.midi-qol-item-card {
font-style: normal;
font-size: 12px;
@@ -72,7 +73,7 @@
span {
border-right: 2px groove #FFF;
- padding: 0 5px 0 0;
+ padding: 0 3px 0 0;
font-size: 10px;
&:last-child {
diff --git a/less/original/items.less b/less/original/items.less
index 239903e9..18d91755 100644
--- a/less/original/items.less
+++ b/less/original/items.less
@@ -1,6 +1,7 @@
@import "./variables.less";
.sw5e.sheet.item {
- min-height: 420px;
+ min-height: 660px;
+ min-width: 680px;
/* ----------------------------------------- */
/* Sheet Header */
@@ -10,7 +11,19 @@
img.profile {
border: 2px solid #000;
}
-
+ h1 {
+ input {
+ font-size: 26px;
+ }
+ }
+ .header-details.flexrow {
+ h1 {
+ font-size: 26px;
+ }
+ .charname {
+ font-size: 26px;
+ }
+ }
.item-subtitle {
flex: 0 0 80px;
height: 60px;
@@ -20,8 +33,8 @@
color: @colorTan;
.item-type {
- font-size: 24px;
- line-height: 26px;
+ font-size: 20px;
+ line-height: 24px;
margin: 0;
}
@@ -29,19 +42,298 @@
font-size: 16px;
line-height: 24px;
}
+
+ .summary {
+ li {
+ font-size: 16px;
+ }
+ }
+
}
}
.sheet-navigation {
margin-bottom: 5px;
.item {
- font-size: 18px;
+ font-size: 16px;
}
}
.sheet-body {
overflow: hidden;
-
+
+ h1 {
+ font-family: 'Russo One';
+ font-size: 20px;
+ font-weight: 400;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ border-bottom: none;
+ color: #c40f0f;
+ }
+
+ h2 {
+ font-family: 'Russo One';
+ font-size: 18px;
+ font-weight: 400;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ border-bottom: 2px solid rgb(13, 153, 204);
+ color: #c40f0f;
+ }
+
+ h3 {
+ font-family: 'Russo One';
+ font-size: 16px;
+ font-weight: 400;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ border-bottom: none;
+ color: #c40f0f;
+ }
+ .smalltable {
+ table {
+ border-top: none;
+ border-bottom: none;
+ width: 200px;
+ }
+ td {
+ &:nth-child(odd) {
+ width: 50px;
+ margin: 0.5em 0.5em;
+ text-align: center;
+ }
+ &:nth-child(even) {
+ width: 150px;
+ margin: 0.5em 0.5em;
+ padding: 0px 10px 0px 10px;
+ text-align: left;
+ }
+ }
+ thead {
+ border-bottom: 0px;
+ }
+ th {
+ color: #000000;
+ text-shadow: none;
+ border-bottom: 0px;
+ background-color: #bdc8cc;
+ text-transform: none;
+ font-weight: bold;
+ font-family: 'Open Sans';
+ &:nth-child(odd) {
+ width: 50px;
+ margin: 0.5em 0.5em;
+ text-align: center;
+ }
+ &:nth-child(even) {
+ width: 150px;
+ margin: 0.5em 0.5em;
+ padding: 0px 10px 0px 10px;
+ text-align: left;
+ }
+ }
+ }
+ .medtable {
+ table {
+ width: 500px;
+ border: 0px;
+ margin: 0.5em 0.5em;
+ }
+ td {
+ &:nth-child(odd) {
+ width: 50px;
+ margin: 0.5em 0.5em;
+ text-align: center;
+ }
+ &:nth-child(even) {
+ width: 450px;
+ margin: 0.5em 0.5em;
+ padding: 0px 10px 0px 0px;
+ text-align: left;
+ }
+ }
+ thead {
+ border-bottom: 0px;
+ }
+ th {
+ color: #000000;
+ text-shadow: none;
+ border-bottom: 0px;
+ background-color: #bdc8cc;
+ text-transform: none;
+ font-weight: bold;
+ font-family: 'Open Sans';
+ &:nth-child(odd) {
+ text-align: center;
+ }
+ &:nth-child(even) {
+ text-align: left;
+ }
+ }
+ }
+ .classtable {
+ blockquote {
+ border-left: 0px;
+ border-right: 0px;
+ background-color: #bdc8cc;
+ width: 600px;
+ h3 {
+ color: #000000;
+ text-transform: uppercase;
+ font-family: 'Russo One';
+ font-size: 16px;
+ }
+ }
+ table {
+ width: 100%;
+ border-collapse: collapse;
+ background: rgba(0, 0, 0, 0.05);
+ border-left: 0px;
+ border-right: 0px;
+ border-top: 0;
+ border-bottom: 0;
+ margin: 0.5em 0;
+ font-style: normal;
+ text-shadow: none;
+ }
+ thead {
+ color: #000000;
+ text-shadow: none;
+ border-bottom: 0px;
+ background-color: #bdc8cc;
+ text-transform: none;
+ font-style: normal;
+ font-family: 'Open Sans';
+ }
+ th {
+ color: #000000;
+ text-shadow: none;
+ border-bottom: 0px;
+ background-color: #bdc8cc;
+ text-transform: none;
+ font-style: normal;
+ font-weight: bold;
+ font-family: 'Open Sans';
+ }
+ tbody {
+ text-align: center;
+ }
+ }
+ .speciestable {
+ blockquote {
+ width: 620px;
+ padding: 15px 10px;
+ margin: 15px;
+ line-height: 20px;
+ background-color: #bdc8cc;
+ border-top: 2px solid #0d99cc !important;
+ border-bottom: 2px solid #0d99cc !important;
+ border-left: 0 !important;
+ border-right: 0 !important;
+ -webkit-box-shadow: 0 0 1.5em rgba(13, 153, 204, .5) !important;
+ box-shadow: 0 0 1.5em rgba(13, 153, 204, .5) !important;
+ overflow-x: auto;
+ h3 {
+ color: #000000;
+ font-size: 22px;
+ border-bottom: none;
+ }
+ }
+ table {
+ background-color: #bdc8cc;
+ border-collapse: collapse;
+ width: 100%;
+ line-height: 18px;
+ margin-bottom: 15px;
+ border: 0 0 0 0;
+ border-bottom: none;
+ overflow-x: auto;
+ tbody {
+ tr {
+ &:nth-child(odd) {
+ background-color: #c9d6db;
+ }
+ &:nth-child(even) {
+ background-color: #bdc8cc;
+ }
+ }
+ }
+ td {
+ &:nth-child(1) {
+ padding-right: 5px;
+ width: 100px;
+ font-style: italic;
+ font-weight: 800;
+ }
+ }
+ h3 {
+ font-family: 'Russo One';
+ color: #000000;
+ font-size: 15px;
+ text-transform: uppercase;
+ }
+ thead {
+ font-style: normal;
+ font-size: 18px;
+ background-color: #bdc8cc;
+ text-shadow: none;
+ text-align: left;
+ line-height: 20px;
+ border-top: 5px solid #0d99cc;
+ border-bottom: 0;
+ }
+ }
+ }
+ .icon {
+ &:before {
+ display: inline-block;
+ font-style: normal;
+ font-variant: normal;
+ text-rendering: auto;
+ -webkit-font-smoothing: antialiased;
+ }
+ }
+ a.entity-link {
+ background: #DDD;
+ padding: 1px 4px;
+ border: 1px solid #4b4a44;
+ border-radius: 2px;
+ white-space: nowrap;
+ word-break: break-all;
+ i {
+ &::before {
+ content: url("ui/jedi-order.svg") !important;
+ display: inline-block;
+ position: relative;
+ top: 2px;
+ height: 15px;
+ width: 15px;
+ }
+ }
+ }
+ #species-description {
+ h2 {
+ font-family: 'Russo One';
+ font-size: 20px;
+ font-weight: 400;
+ letter-spacing: 0.5px;
+ border-bottom: 2px solid rgb(13, 153, 204);
+ color: #c40f0f;
+ }
+ }
+ #Traits {
+ 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;
+ }
+ }
+
.tab {
padding: 0 5px;
overflow: hidden auto;
diff --git a/less/original/npc.less b/less/original/npc.less
index 6374aa3d..dada8bad 100644
--- a/less/original/npc.less
+++ b/less/original/npc.less
@@ -31,11 +31,4 @@
.summary {
font-size: 18px;
}
-
- .powercasting-ability {
- label {
- flex: none;
- }
- }
-
}
\ No newline at end of file
diff --git a/less/original/sw5e.less b/less/original/sw5e.less
index 345dab87..29ee4bba 100644
--- a/less/original/sw5e.less
+++ b/less/original/sw5e.less
@@ -6,33 +6,3 @@
@import "character.less";
@import "npc.less";
@import "vehicle.less";
-
-// TODO: Remove number styling after 0.7.x
-input[type="number"] {
- width: calc(100% - 2px);
- min-width: 20px;
- height: 26px;
- background: rgba(0, 0, 0, 0.05);
- padding: 1px 3px;
- margin: 0;
- color: #191813;
- font-family: inherit;
- font-size: inherit;
- text-align: inherit;
- line-height: inherit;
- border: 1px solid #7a7971;
- border-radius: 3px;
- -webkit-user-select: text;
- -moz-user-select: text;
- -ms-user-select: text;
- user-select: text;
- -moz-appearance: textfield;
- &:focus {
- box-shadow: 0 0 5px red;
- }
- }
- input[type="number"]::-webkit-inner-spin-button,
- input[type="number"]::-webkit-outer-spin-button {
- -webkit-appearance: none;
- }
-
\ No newline at end of file
diff --git a/less/update/components/actor-global.less b/less/update/components/actor-global.less
index 878877bd..ddce5187 100644
--- a/less/update/components/actor-global.less
+++ b/less/update/components/actor-global.less
@@ -5,7 +5,7 @@
.dropShadow1();
}
.sw5e.sheet.actor.character {
- min-width: 780px;
+ min-width: 800px;
min-height: 720px;
}
.sw5e.sheet .window-content {
@@ -60,6 +60,11 @@
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 {
@@ -93,6 +98,11 @@
.charlevel {
.russoOne(17px);
text-align: right;
+ input {
+ display: inline-block;
+ width: 42px;
+ height: auto;
+ }
}
.experience {
@@ -932,6 +942,9 @@
&>.panel {
grid-template-rows: 32px 24px 24px auto;
}
+ h3.power-dc {
+ line-height: 24px;
+ }
.powercasting-ability {
display: grid;
grid-template-columns: 2fr 1fr 1fr;
@@ -991,4 +1004,56 @@
}
}
}
+ &.npc {
+ .swalt-sheet {
+ header {
+ h1.character-name {
+ align-self: auto;
+ }
+ .npc-size {
+ .russoOne(18px);
+ line-height: 28px;
+ }
+ .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(4, 1fr);
+ }
+ .tab.attributes {
+ .traits-resources {
+ display: block;
+
+ .counter {
+ display: flex;
+ .counter-value {
+ margin-left: auto;
+ }
+ }
+ // section.traits {
+ // display:block;
+ // }
+ }
+ }
+ .tab.powerbook {
+ input.powercasting-level {
+ width: 48px;
+ }
+ }
+ .tab.biography.active {
+ display: block;
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/less/update/components/actor-themes.less b/less/update/components/actor-themes.less
index aa1ed4d5..6e2a5f44 100644
--- a/less/update/components/actor-themes.less
+++ b/less/update/components/actor-themes.less
@@ -404,4 +404,13 @@
}
}
}
+ &.npc {
+ .swalt-sheet {
+ header {
+ .experience {
+ color: @actorProficiencyTextColor;
+ }
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/less/update/components/sidebar-global.less b/less/update/components/sidebar-global.less
index 9d669a4a..2f69aae7 100644
--- a/less/update/components/sidebar-global.less
+++ b/less/update/components/sidebar-global.less
@@ -54,7 +54,8 @@
color: @colorBlue;
}
-.sw5e.chat-card {
+.sw5e.chat-card,
+.midi-qol-item-card {
.card-header {
padding: 0;
border: none;
diff --git a/less/update/components/sidebar-themes.less b/less/update/components/sidebar-themes.less
index b7d53e7a..4a3f57c4 100644
--- a/less/update/components/sidebar-themes.less
+++ b/less/update/components/sidebar-themes.less
@@ -31,7 +31,8 @@
color: @chatNotificationColor;
}
-.sw5e.chat-card {
+.sw5e.chat-card,
+.midi-qol-item-card {
.card-header {
h3 {
diff --git a/less/update/components/sidebar.less b/less/update/components/sidebar.less
index 2cfb6737..5f0ae311 100644
--- a/less/update/components/sidebar.less
+++ b/less/update/components/sidebar.less
@@ -51,7 +51,8 @@
}
-.sw5e.chat-card {
+.sw5e.chat-card,
+.midi-qol-item-card {
font-size: 13px;
.card-header {
diff --git a/module/actor/entity.js b/module/actor/entity.js
index d8ae2479..bdbd385e 100644
--- a/module/actor/entity.js
+++ b/module/actor/entity.js
@@ -1,8 +1,6 @@
import { d20Roll, damageRoll } from "../dice.js";
import ShortRestDialog from "../apps/short-rest.js";
import LongRestDialog from "../apps/long-rest.js";
-import AbilityUseDialog from "../apps/ability-use-dialog.js";
-import AbilityTemplate from "../pixi/ability-template.js";
import {SW5E} from '../config.js';
/**
@@ -163,8 +161,8 @@ export default class Actor5e extends Actor {
}
// Acquire archetype features
- const subConfig = clsConfig.archetypes[archetypeName] || {};
- for ( let [l, f] of Object.entries(subConfig.features || {}) ) {
+ const archConfig = clsConfig.archetypes[archetypeName] || {};
+ for ( let [l, f] of Object.entries(archConfig.features || {}) ) {
l = parseInt(l);
if ( (l <= level) && (l > priorLevel) ) ids = ids.concat(f);
}
@@ -207,7 +205,7 @@ export default class Actor5e extends Actor {
const updateData = expandObject(u);
const config = {
className: updateData.name || item.data.name,
- archetypeName: updateData.data.archetype || item.data.data.archetype,
+ archetypeName: getProperty(updateData, "data.archetype") || item.data.data.archetype,
level: getProperty(updateData, "data.levels"),
priorLevel: item ? item.data.data.levels : 0
}
@@ -356,16 +354,8 @@ export default class Actor5e extends Actor {
const data = actorData.data;
data.attributes.powerdc = data.attributes.powercasting ? data.abilities[data.attributes.powercasting].dc : 10;
- // Apply powercasting DC to any power items which use it
- for ( let i of this.items ) {
- const save = i.data.data.save;
- if ( save?.ability ) {
- if ( save.scaling === "power" ) save.dc = data.attributes.powerdc;
- else if ( save.scaling !== "flat" ) save.dc = data.abilities[save.scaling]?.dc ?? 10;
- const ability = CONFIG.SW5E.abilities[save.ability];
- i.labels.save = game.i18n.format("SW5E.SaveDC", {dc: save.dc || "", ability});
- }
- }
+ // Compute ability save DCs that depend on the calling actor
+ this.items.forEach(i => i.getSaveDC());
}
/* -------------------------------------------- */
@@ -546,20 +536,69 @@ export default class Actor5e extends Actor {
/* -------------------------------------------- */
/** @override */
- async createOwnedItem(itemData, options) {
+ async createEmbeddedEntity(embeddedName, itemData, options={}) {
- // Assume NPCs are always proficient with weapons and always have powers prepared
- if ( !this.hasPlayerOwner ) {
- let t = itemData.type;
- let initial = {};
- if ( t === "weapon" ) initial["data.proficient"] = true;
- if ( ["weapon", "equipment"].includes(t) ) initial["data.equipped"] = true;
- if ( t === "power" ) initial["data.prepared"] = true;
- mergeObject(itemData, initial);
- }
- return super.createOwnedItem(itemData, options);
+ // Pre-creation steps for owned items
+ if ( embeddedName === "OwnedItem" ) this._preCreateOwnedItem(itemData, options);
+
+ // Standard embedded entity creation
+ return super.createEmbeddedEntity(embeddedName, itemData, options);
}
+ /* -------------------------------------------- */
+
+ /**
+ * A temporary shim function which will eventually (in core fvtt version 0.8.0+) be migrated to the new abstraction layer
+ * @param itemData
+ * @param options
+ * @private
+ */
+ _preCreateOwnedItem(itemData, options) {
+ if ( this.data.type === "vehicle" ) return;
+ const isNPC = this.data.type === 'npc';
+ let initial = {};
+ switch ( itemData.type ) {
+ case "weapon":
+ initial["data.equipped"] = isNPC; // NPCs automatically equip weapons
+ let hasWeaponProf = isNPC; // NPCs automatically have weapon proficiency
+ if ( !isNPC ) {
+ const weaponProf = {
+ "natural": true,
+ "simpleVW": "sim",
+ "simpleB": "sim",
+ "simpleLW": "sim",
+ "martialVW": "mar",
+ "martialB": "mar",
+ "martialLW": "mar"
+ }[itemData.data?.weaponType];
+ const actorWeaponProfs = this.data.data.traits?.weaponProf?.value || [];
+ hasWeaponProf = (weaponProf === true) || actorWeaponProfs.includes(weaponProf);
+ }
+ initial["data.proficient"] = hasWeaponProf;
+ break;
+ case "equipment":
+ initial["data.equipped"] = isNPC; // NPCs automatically equip equipment
+ let hasEquipmentProf = isNPC; // NPCs automatically have equipment proficiency
+ if ( !isNPC ) {
+ const armorProf = {
+ "natural": true,
+ "clothing": true,
+ "light": "lgt",
+ "medium": "med",
+ "heavy": "hvy",
+ "shield": "shl"
+ }[itemData.data?.armor?.type];
+ const actorArmorProfs = this.data.data.traits?.armorProf?.value || [];
+ hasEquipmentProf = (armorProf === true) || actorArmorProfs.includes(armorProf);
+ }
+ initial["data.proficient"] = hasEquipmentProf;
+ break;
+ case "power":
+ initial["data.prepared"] = true; // NPCs automatically prepare powers
+ break;
+ }
+ mergeObject(itemData, initial);
+ }
/* -------------------------------------------- */
/* Gameplay Mechanics */
@@ -600,77 +639,16 @@ export default class Actor5e extends Actor {
"data.attributes.hp.temp": tmp - dt,
"data.attributes.hp.value": dh
};
- return this.update(updates);
- }
- /* -------------------------------------------- */
-
- /**
- * Cast a Power, consuming a power slot of a certain level
- * @param {Item5e} item The power being cast by the actor
- * @param {Event} event The originating user interaction which triggered the cast
- */
- async usePower(item, {configureDialog=true}={}) {
- if ( item.data.type !== "power" ) throw new Error("Wrong Item type");
- const itemData = item.data.data;
-
- // Configure powercasting data
- let lvl = itemData.level;
- const usesSlots = (lvl > 0) && CONFIG.SW5E.powerUpcastModes.includes(itemData.preparation.mode);
- const limitedUses = !!itemData.uses.per;
- let consumeSlot = `power${lvl}`;
- let consumeUse = false;
- let placeTemplate = false;
-
- // Configure power slot consumption and measured template placement from the form
- if ( configureDialog && (usesSlots || item.hasAreaTarget || limitedUses) ) {
- const usage = await AbilityUseDialog.create(item);
- if ( usage === null ) return;
-
- // Determine consumption preferences
- consumeSlot = Boolean(usage.get("consumeSlot"));
- consumeUse = Boolean(usage.get("consumeUse"));
- placeTemplate = Boolean(usage.get("placeTemplate"));
-
- // Determine the cast power level
- const isPact = usage.get('level') === 'pact';
- const lvl = isPact ? this.data.data.powers.pact.level : parseInt(usage.get("level"));
- if ( lvl !== item.data.data.level ) {
- const upcastData = mergeObject(item.data, {"data.level": lvl}, {inplace: false});
- item = item.constructor.createOwned(upcastData, this);
- }
-
- // Denote the power slot being consumed
- if ( consumeSlot ) consumeSlot = isPact ? "pact" : `power${lvl}`;
- }
-
- // Update Actor data
- if ( usesSlots && consumeSlot && (lvl > 0) ) {
- const slots = parseInt(this.data.data.powers[consumeSlot]?.value);
- if ( slots === 0 || Number.isNaN(slots) ) {
- return ui.notifications.error(game.i18n.localize("SW5E.PowerCastNoSlots"));
- }
- await this.update({
- [`data.powers.${consumeSlot}.value`]: Math.max(slots - 1, 0)
- });
- }
-
- // Update Item data
- if ( limitedUses && consumeUse ) {
- const uses = parseInt(itemData.uses.value || 0);
- if ( uses <= 0 ) ui.notifications.warn(game.i18n.format("SW5E.ItemNoUses", {name: item.name}));
- await item.update({"data.uses.value": Math.max(parseInt(item.data.data.uses.value || 0) - 1, 0)})
- }
-
- // Initiate ability template placement workflow if selected
- if ( placeTemplate && item.hasAreaTarget ) {
- const template = AbilityTemplate.fromItem(item);
- if ( template ) template.drawPreview();
- if ( this.sheet.rendered ) this.sheet.minimize();
- }
-
- // Invoke the Item roll
- return item.roll();
+ // Delegate damage application to a hook
+ // TODO replace this in the future with a better modifyTokenAttribute function in the core
+ const allowed = Hooks.call("modifyTokenAttribute", {
+ attribute: "attributes.hp",
+ value: amount,
+ isDelta: false,
+ isBar: true
+ }, updates);
+ return allowed !== false ? this.update(updates) : this;
}
/* -------------------------------------------- */
@@ -989,7 +967,7 @@ export default class Actor5e extends Actor {
// Adjust actor data
await cls.update({"data.hitDiceUsed": cls.data.data.hitDiceUsed + 1});
const hp = this.data.data.attributes.hp;
- const dhp = Math.min(hp.max - hp.value, roll.total);
+ const dhp = Math.min(hp.max + (hp.tempmax ?? 0) - hp.value, roll.total);
await this.update({"data.attributes.hp.value": hp.value + dhp});
return roll;
}
@@ -1437,4 +1415,18 @@ export default class Actor5e extends Actor {
console.warn(`The Actor5e#getPowerDC(ability) method has been deprecated in favor of Actor5e#data.data.abilities[ability].dc`);
return this.data.data.abilities[ability]?.dc;
}
+
+ /* -------------------------------------------- */
+
+ /**
+ * Cast a Power, consuming a power slot of a certain level
+ * @param {Item5e} item The power being cast by the actor
+ * @param {Event} event The originating user interaction which triggered the cast
+ * @deprecated since sw5e 1.2.0
+ */
+ async usePower(item, {configureDialog=true}={}) {
+ console.warn(`The Actor5e#usePower method has been deprecated in favor of Item5e#roll`);
+ if ( item.data.type !== "power" ) throw new Error("Wrong Item type");
+ return item.roll();
+ }
}
diff --git a/module/actor/sheets/newSheet/base.js b/module/actor/sheets/newSheet/base.js
new file mode 100644
index 00000000..92a3da39
--- /dev/null
+++ b/module/actor/sheets/newSheet/base.js
@@ -0,0 +1,833 @@
+import Item5e from "../../../item/entity.js";
+import TraitSelector from "../../../apps/trait-selector.js";
+import ActorSheetFlags from "../../../apps/actor-flags.js";
+import ActorMovementConfig from "../../../apps/movement-config.js";
+import ActorSensesConfig from "../../../apps/senses-config.js";
+import {SW5E} from '../../../config.js';
+import {onManageActiveEffect, prepareActiveEffectCategories} from "../../../effects.js";
+
+/**
+ * Extend the basic ActorSheet class to suppose SW5e-specific logic and functionality.
+ * This sheet is an Abstract layer which is not used.
+ * @extends {ActorSheet}
+ */
+export default class ActorSheet5e extends ActorSheet {
+ constructor(...args) {
+ super(...args);
+
+ /**
+ * Track the set of item filters which are applied
+ * @type {Set}
+ */
+ this._filters = {
+ inventory: new Set(),
+ powerbook: new Set(),
+ features: new Set(),
+ effects: new Set()
+ };
+ }
+
+ /* -------------------------------------------- */
+
+ /** @override */
+ static get defaultOptions() {
+ return mergeObject(super.defaultOptions, {
+ scrollY: [
+ ".inventory .inventory-list",
+ ".features .inventory-list",
+ ".powerbook .inventory-list",
+ ".effects .inventory-list"
+ ],
+ tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}]
+ });
+ }
+
+ /* -------------------------------------------- */
+
+ /** @override */
+ get template() {
+ if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/newActor/expanded-limited-sheet.html";
+ return `systems/sw5e/templates/actors/newActor/${this.actor.data.type}-sheet.html`;
+ }
+
+ /* -------------------------------------------- */
+
+ /** @override */
+ getData() {
+
+ // Basic data
+ let isOwner = this.entity.owner;
+ const data = {
+ owner: isOwner,
+ limited: this.entity.limited,
+ options: this.options,
+ editable: this.isEditable,
+ cssClass: isOwner ? "editable" : "locked",
+ isCharacter: this.entity.data.type === "character",
+ isNPC: this.entity.data.type === "npc",
+ isVehicle: this.entity.data.type === 'vehicle',
+ config: CONFIG.SW5E,
+ };
+
+ // The Actor and its Items
+ data.actor = duplicate(this.actor.data);
+ data.items = this.actor.items.map(i => {
+ i.data.labels = i.labels;
+ return i.data;
+ });
+ data.items.sort((a, b) => (a.sort || 0) - (b.sort || 0));
+ data.data = data.actor.data;
+ data.labels = this.actor.labels || {};
+ data.filters = this._filters;
+
+ // Ability Scores
+ for ( let [a, abl] of Object.entries(data.actor.data.abilities)) {
+ abl.icon = this._getProficiencyIcon(abl.proficient);
+ abl.hover = CONFIG.SW5E.proficiencyLevels[abl.proficient];
+ abl.label = CONFIG.SW5E.abilities[a];
+ }
+
+ // Skills
+ if (data.actor.data.skills) {
+ for ( let [s, skl] of Object.entries(data.actor.data.skills)) {
+ skl.ability = CONFIG.SW5E.abilityAbbreviations[skl.ability];
+ skl.icon = this._getProficiencyIcon(skl.value);
+ skl.hover = CONFIG.SW5E.proficiencyLevels[skl.value];
+ skl.label = CONFIG.SW5E.skills[s];
+ }
+ }
+
+ // Movement speeds
+ data.movement = this._getMovementSpeed(data.actor);
+
+ // Senses
+ data.senses = this._getSenses(data.actor);
+
+ // Update traits
+ this._prepareTraits(data.actor.data.traits);
+
+ // Prepare owned items
+ this._prepareItems(data);
+
+ // Prepare active effects
+ data.effects = prepareActiveEffectCategories(this.entity.effects);
+
+ // Return data to the sheet
+ return data
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Prepare the display of movement speed data for the Actor
+ * @param {object} actorData
+ * @returns {{primary: string, special: string}}
+ * @private
+ */
+ _getMovementSpeed(actorData) {
+ const movement = actorData.data.attributes.movement || {};
+ const speeds = [
+ [movement.burrow, `${game.i18n.localize("SW5E.MovementBurrow")} ${movement.burrow}`],
+ [movement.climb, `${game.i18n.localize("SW5E.MovementClimb")} ${movement.climb}`],
+ [movement.fly, `${game.i18n.localize("SW5E.MovementFly")} ${movement.fly}` + (movement.hover ? ` (${game.i18n.localize("SW5E.MovementHover")})` : "")],
+ [movement.swim, `${game.i18n.localize("SW5E.MovementSwim")} ${movement.swim}`]
+ ].filter(s => !!s[0]).sort((a, b) => b[0] - a[0]);
+ return {
+ primary: `${movement.walk || 0} ${movement.units}`,
+ special: speeds.length ? speeds.map(s => s[1]).join(", ") : ""
+ }
+ }
+
+ /* -------------------------------------------- */
+
+ _getSenses(actorData) {
+ const senses = actorData.data.attributes.senses || {};
+ const tags = {};
+ for ( let [k, label] of Object.entries(CONFIG.SW5E.senses) ) {
+ const v = senses[k] ?? 0
+ if ( v === 0 ) continue;
+ tags[k] = `${game.i18n.localize(label)} ${v} ${senses.units}`;
+ }
+ if ( !!senses.special ) tags["special"] = senses.special;
+ return tags;
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * 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
+ * @private
+ */
+ _prepareTraits(traits) {
+ const map = {
+ "dr": CONFIG.SW5E.damageResistanceTypes,
+ "di": CONFIG.SW5E.damageResistanceTypes,
+ "dv": CONFIG.SW5E.damageResistanceTypes,
+ "ci": CONFIG.SW5E.conditionTypes,
+ "languages": CONFIG.SW5E.languages,
+ "armorProf": CONFIG.SW5E.armorProficiencies,
+ "weaponProf": CONFIG.SW5E.weaponProficiencies,
+ "toolProf": CONFIG.SW5E.toolProficiencies
+ };
+ for ( let [t, choices] of Object.entries(map) ) {
+ const trait = traits[t];
+ if ( !trait ) continue;
+ let values = [];
+ if ( trait.value ) {
+ values = trait.value instanceof Array ? trait.value : [trait.value];
+ }
+ trait.selected = values.reduce((obj, t) => {
+ obj[t] = choices[t];
+ return obj;
+ }, {});
+
+ // Add custom entry
+ if ( trait.custom ) {
+ trait.custom.split(";").forEach((c, i) => trait.selected[`custom${i+1}`] = c.trim());
+ }
+ trait.cssClass = !isObjectEmpty(trait.selected) ? "" : "inactive";
+ }
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Insert a power into the powerbook object when rendering the character sheet
+ * @param {Object} data The Actor data being prepared
+ * @param {Array} powers The power data being prepared
+ * @private
+ */
+ _preparePowerbook(data, powers) {
+ const owner = this.actor.owner;
+ const levels = data.data.powers;
+ const powerbook = {};
+
+ // Define some mappings
+ const sections = {
+ "atwill": -20,
+ "innate": -10,
+ "pact": 0.5
+ };
+
+ // Label power slot uses headers
+ const useLabels = {
+ "-20": "-",
+ "-10": "-",
+ "0": "∞"
+ };
+
+ // Format a powerbook entry for a certain indexed level
+ const registerSection = (sl, i, label, {prepMode="prepared", value, max, override}={}) => {
+ powerbook[i] = {
+ order: i,
+ label: label,
+ usesSlots: i > 0,
+ canCreate: owner,
+ canPrepare: (data.actor.type === "character") && (i >= 1),
+ powers: [],
+ uses: useLabels[i] || value || 0,
+ slots: useLabels[i] || max || 0,
+ override: override || 0,
+ dataset: {"type": "power", "level": prepMode in sections ? 1 : i, "preparation.mode": prepMode},
+ prop: sl
+ };
+ };
+
+ // Determine the maximum power level which has a slot
+ const maxLevel = Array.fromRange(10).reduce((max, i) => {
+ if ( i === 0 ) return max;
+ const level = levels[`power${i}`];
+ if ( (level.max || level.override ) && ( i > max ) ) max = i;
+ return max;
+ }, 0);
+
+ // Level-based powercasters have cantrips and leveled slots
+ if ( maxLevel > 0 ) {
+ registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]);
+ for (let lvl = 1; lvl <= maxLevel; lvl++) {
+ const sl = `power${lvl}`;
+ registerSection(sl, lvl, CONFIG.SW5E.powerLevels[lvl], levels[sl]);
+ }
+ }
+
+ // Pact magic users have cantrips and a pact magic section
+ if ( levels.pact && levels.pact.max ) {
+ if ( !powerbook["0"] ) registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]);
+ const l = levels.pact;
+ const config = CONFIG.SW5E.powerPreparationModes.pact;
+ registerSection("pact", sections.pact, config, {
+ prepMode: "pact",
+ value: l.value,
+ max: l.max,
+ override: l.override
+ });
+ }
+
+ // Iterate over every power item, adding powers to the powerbook by section
+ powers.forEach(power => {
+ const mode = power.data.preparation.mode || "prepared";
+ let s = power.data.level || 0;
+ const sl = `power${s}`;
+
+ // Specialized powercasting modes (if they exist)
+ if ( mode in sections ) {
+ s = sections[mode];
+ if ( !powerbook[s] ){
+ const l = levels[mode] || {};
+ const config = CONFIG.SW5E.powerPreparationModes[mode];
+ registerSection(mode, s, config, {
+ prepMode: mode,
+ value: l.value,
+ max: l.max,
+ override: l.override
+ });
+ }
+ }
+
+ // Sections for higher-level powers which the caster "should not" have, but power items exist for
+ else if ( !powerbook[s] ) {
+ registerSection(sl, s, CONFIG.SW5E.powerLevels[s], {levels: levels[sl]});
+ }
+
+ // Add the power to the relevant heading
+ powerbook[s].powers.push(power);
+ });
+
+ // Sort the powerbook by section level
+ const sorted = Object.values(powerbook);
+ sorted.sort((a, b) => a.order - b.order);
+ return sorted;
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Determine whether an Owned Item will be shown based on the current set of filters
+ * @return {boolean}
+ * @private
+ */
+ _filterItems(items, filters) {
+ return items.filter(item => {
+ const data = item.data;
+
+ // Action usage
+ for ( let f of ["action", "bonus", "reaction"] ) {
+ if ( filters.has(f) ) {
+ if ((data.activation && (data.activation.type !== f))) return false;
+ }
+ }
+
+ // Power-specific filters
+ if ( filters.has("ritual") ) {
+ if (data.components.ritual !== true) return false;
+ }
+ if ( filters.has("concentration") ) {
+ if (data.components.concentration !== true) return false;
+ }
+ if ( filters.has("prepared") ) {
+ if ( data.level === 0 || ["innate", "always"].includes(data.preparation.mode) ) return true;
+ if ( this.actor.data.type === "npc" ) return true;
+ return data.preparation.prepared;
+ }
+
+ // Equipment-specific filters
+ if ( filters.has("equipped") ) {
+ if ( data.equipped !== true ) return false;
+ }
+ return true;
+ });
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Get the font-awesome icon used to display a certain level of skill proficiency
+ * @private
+ */
+ _getProficiencyIcon(level) {
+ const icons = {
+ 0: '',
+ 0.5: '',
+ 1: '',
+ 2: ''
+ };
+ return icons[level];
+ }
+
+ /* -------------------------------------------- */
+ /* Event Listeners and Handlers
+ /* -------------------------------------------- */
+
+ /**
+ * Activate event listeners using the prepared sheet HTML
+ * @param html {HTML} The prepared HTML object ready to be rendered into the DOM
+ */
+ activateListeners(html) {
+
+ // Activate Item Filters
+ const filterLists = html.find(".filter-list");
+ filterLists.each(this._initializeFilterItemList.bind(this));
+ filterLists.on("click", ".filter-item", this._onToggleFilter.bind(this));
+
+ // Item summaries
+ html.find('.item .item-name.rollable h4').click(event => this._onItemSummary(event));
+
+ // Editable Only Listeners
+ if ( this.isEditable ) {
+
+ // Input focus and update
+ const inputs = html.find("input");
+ inputs.focus(ev => ev.currentTarget.select());
+ inputs.addBack().find('[data-dtype="Number"]').change(this._onChangeInputDelta.bind(this));
+
+ // Ability Proficiency
+ html.find('.ability-proficiency').click(this._onToggleAbilityProficiency.bind(this));
+
+ // Toggle Skill Proficiency
+ html.find('.skill-proficiency').on("click contextmenu", this._onCycleSkillProficiency.bind(this));
+
+ // Trait Selector
+ html.find('.trait-selector').click(this._onTraitSelector.bind(this));
+
+ // Configure Special Flags
+ html.find('.config-button').click(this._onConfigMenu.bind(this));
+
+ // Owned Item management
+ html.find('.item-create').click(this._onItemCreate.bind(this));
+ html.find('.item-edit').click(this._onItemEdit.bind(this));
+ html.find('.item-delete').click(this._onItemDelete.bind(this));
+ html.find('.item-uses input').click(ev => ev.target.select()).change(this._onUsesChange.bind(this));
+ html.find('.slot-max-override').click(this._onPowerSlotOverride.bind(this));
+
+ // Active Effect management
+ html.find(".effect-control").click(ev => onManageActiveEffect(ev, this.entity));
+ }
+
+ // Owner Only Listeners
+ if ( this.actor.owner ) {
+
+ // Ability Checks
+ html.find('.ability-name').click(this._onRollAbilityTest.bind(this));
+
+
+ // Roll Skill Checks
+ html.find('.skill-name').click(this._onRollSkillCheck.bind(this));
+
+ // Item Rolling
+ html.find('.item .item-image').click(event => this._onItemRoll(event));
+ html.find('.item .item-recharge').click(event => this._onItemRecharge(event));
+ }
+
+ // Otherwise remove rollable classes
+ else {
+ html.find(".rollable").each((i, el) => el.classList.remove("rollable"));
+ }
+
+ // Handle default listeners last so system listeners are triggered first
+ super.activateListeners(html);
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Iinitialize Item list filters by activating the set of filters which are currently applied
+ * @private
+ */
+ _initializeFilterItemList(i, ul) {
+ const set = this._filters[ul.dataset.filter];
+ const filters = ul.querySelectorAll(".filter-item");
+ for ( let li of filters ) {
+ if ( set.has(li.dataset.filter) ) li.classList.add("active");
+ }
+ }
+
+ /* -------------------------------------------- */
+ /* Event Listeners and Handlers */
+ /* -------------------------------------------- */
+
+ /**
+ * Handle input changes to numeric form fields, allowing them to accept delta-typed inputs
+ * @param event
+ * @private
+ */
+ _onChangeInputDelta(event) {
+ const input = event.target;
+ const value = input.value;
+ if ( ["+", "-"].includes(value[0]) ) {
+ let delta = parseFloat(value);
+ input.value = getProperty(this.actor.data, input.name) + delta;
+ } else if ( value[0] === "=" ) {
+ input.value = value.slice(1);
+ }
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle spawning the TraitSelector application which allows a checkbox of multiple trait options
+ * @param {Event} event The click event which originated the selection
+ * @private
+ */
+ _onConfigMenu(event) {
+ event.preventDefault();
+ const button = event.currentTarget;
+ switch ( button.dataset.action ) {
+ case "movement":
+ new ActorMovementConfig(this.object).render(true);
+ break;
+ case "flags":
+ new ActorSheetFlags(this.object).render(true);
+ break;
+ case "senses":
+ new ActorSensesConfig(this.object).render(true);
+ break;
+ }
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle cycling proficiency in a Skill
+ * @param {Event} event A click or contextmenu event which triggered the handler
+ * @private
+ */
+ _onCycleSkillProficiency(event) {
+ event.preventDefault();
+ const field = $(event.currentTarget).siblings('input[type="hidden"]');
+
+ // Get the current level and the array of levels
+ const level = parseFloat(field.val());
+ 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]);
+ }
+
+ // Update the field value and save the form
+ this._onSubmit(event);
+ }
+
+ /* -------------------------------------------- */
+
+ /** @override */
+ async _onDropActor(event, data) {
+ const canPolymorph = game.user.isGM || (this.actor.owner && game.settings.get('sw5e', 'allowPolymorphing'));
+ if ( !canPolymorph ) return false;
+
+ // Get the target actor
+ let sourceActor = null;
+ if (data.pack) {
+ const pack = game.packs.find(p => p.collection === data.pack);
+ sourceActor = await pack.getEntity(data.id);
+ } else {
+ sourceActor = game.actors.get(data.id);
+ }
+ if ( !sourceActor ) return;
+
+ // Define a function to record polymorph settings for future use
+ const rememberOptions = html => {
+ const options = {};
+ html.find('input').each((i, el) => {
+ options[el.name] = el.checked;
+ });
+ const settings = mergeObject(game.settings.get('sw5e', 'polymorphSettings') || {}, options);
+ game.settings.set('sw5e', 'polymorphSettings', settings);
+ return settings;
+ };
+
+ // Create and render the Dialog
+ return new Dialog({
+ title: game.i18n.localize('SW5E.PolymorphPromptTitle'),
+ content: {
+ options: game.settings.get('sw5e', 'polymorphSettings'),
+ i18n: SW5E.polymorphSettings,
+ isToken: this.actor.isToken
+ },
+ default: 'accept',
+ buttons: {
+ accept: {
+ icon: '',
+ label: game.i18n.localize('SW5E.PolymorphAcceptSettings'),
+ callback: html => this.actor.transformInto(sourceActor, rememberOptions(html))
+ },
+ wildshape: {
+ icon: '',
+ label: game.i18n.localize('SW5E.PolymorphWildShape'),
+ callback: html => this.actor.transformInto(sourceActor, {
+ keepBio: true,
+ keepClass: true,
+ keepMental: true,
+ mergeSaves: true,
+ mergeSkills: true,
+ transformTokens: rememberOptions(html).transformTokens
+ })
+ },
+ polymorph: {
+ icon: '',
+ label: game.i18n.localize('SW5E.Polymorph'),
+ callback: html => this.actor.transformInto(sourceActor, {
+ transformTokens: rememberOptions(html).transformTokens
+ })
+ },
+ cancel: {
+ icon: '',
+ label: game.i18n.localize('Cancel')
+ }
+ }
+ }, {
+ classes: ['dialog', 'sw5e'],
+ width: 600,
+ template: 'systems/sw5e/templates/apps/polymorph-prompt.html'
+ }).render(true);
+ }
+
+ /* -------------------------------------------- */
+
+ /** @override */
+ async _onDropItemCreate(itemData) {
+
+ // Create a Consumable power scroll on the Inventory tab
+ if ( (itemData.type === "power") && (this._tabs[0].active === "inventory") ) {
+ const scroll = await Item5e.createScrollFromPower(itemData);
+ itemData = scroll.data;
+ }
+
+ // Create the owned item as normal
+ return super._onDropItemCreate(itemData);
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle enabling editing for a power slot override value
+ * @param {MouseEvent} event The originating click event
+ * @private
+ */
+ async _onPowerSlotOverride (event) {
+ const span = event.currentTarget.parentElement;
+ const level = span.dataset.level;
+ const override = this.actor.data.data.powers[level].override || span.dataset.slots;
+ const input = document.createElement("INPUT");
+ input.type = "text";
+ input.name = `data.powers.${level}.override`;
+ input.value = override;
+ input.placeholder = span.dataset.slots;
+ input.dataset.dtype = "Number";
+
+ // Replace the HTML
+ const parent = span.parentElement;
+ parent.removeChild(span);
+ parent.appendChild(input);
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Change the uses amount of an Owned Item within the Actor
+ * @param {Event} event The triggering click event
+ * @private
+ */
+ async _onUsesChange(event) {
+ event.preventDefault();
+ const itemId = event.currentTarget.closest(".item").dataset.itemId;
+ const item = this.actor.getOwnedItem(itemId);
+ const uses = Math.clamped(0, parseInt(event.target.value), item.data.data.uses.max);
+ event.target.value = uses;
+ return item.update({ 'data.uses.value': uses });
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle rolling of an item from the Actor sheet, obtaining the Item instance and dispatching to it's roll method
+ * @private
+ */
+ _onItemRoll(event) {
+ event.preventDefault();
+ const itemId = event.currentTarget.closest(".item").dataset.itemId;
+ const item = this.actor.getOwnedItem(itemId);
+ return item.roll();
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle attempting to recharge an item usage by rolling a recharge check
+ * @param {Event} event The originating click event
+ * @private
+ */
+ _onItemRecharge(event) {
+ event.preventDefault();
+ const itemId = event.currentTarget.closest(".item").dataset.itemId;
+ const item = this.actor.getOwnedItem(itemId);
+ return item.rollRecharge();
+ };
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle rolling of an item from the Actor sheet, obtaining the Item instance and dispatching to it's roll method
+ * @private
+ */
+ _onItemSummary(event) {
+ event.preventDefault();
+ let li = $(event.currentTarget).parents(".item"),
+ item = this.actor.getOwnedItem(li.data("item-id")),
+ chatData = item.getChatData({secrets: this.actor.owner});
+
+ // Toggle summary
+ if ( li.hasClass("expanded") ) {
+ let summary = li.children(".item-summary");
+ summary.slideUp(200, () => summary.remove());
+ } else {
+ let div = $(`
${chatData.description.value}
`);
+ let props = $(``);
+ chatData.properties.forEach(p => props.append(`${p}`));
+ div.append(props);
+ li.append(div.hide());
+ div.slideDown(200);
+ }
+ li.toggleClass("expanded");
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle creating a new Owned Item for the actor using initial data defined in the HTML dataset
+ * @param {Event} event The originating click event
+ * @private
+ */
+ _onItemCreate(event) {
+ event.preventDefault();
+ const header = event.currentTarget;
+ const type = header.dataset.type;
+ const itemData = {
+ name: game.i18n.format("SW5E.ItemNew", {type: type.capitalize()}),
+ type: type,
+ data: duplicate(header.dataset)
+ };
+ delete itemData.data["type"];
+ return this.actor.createEmbeddedEntity("OwnedItem", itemData);
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle editing an existing Owned Item for the Actor
+ * @param {Event} event The originating click event
+ * @private
+ */
+ _onItemEdit(event) {
+ event.preventDefault();
+ const li = event.currentTarget.closest(".item");
+ const item = this.actor.getOwnedItem(li.dataset.itemId);
+ item.sheet.render(true);
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle deleting an existing Owned Item for the Actor
+ * @param {Event} event The originating click event
+ * @private
+ */
+ _onItemDelete(event) {
+ event.preventDefault();
+ const li = event.currentTarget.closest(".item");
+ this.actor.deleteOwnedItem(li.dataset.itemId);
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle rolling an Ability check, either a test or a saving throw
+ * @param {Event} event The originating click event
+ * @private
+ */
+ _onRollAbilityTest(event) {
+ event.preventDefault();
+ let ability = event.currentTarget.parentElement.dataset.ability;
+ this.actor.rollAbility(ability, {event: event});
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle rolling a Skill check
+ * @param {Event} event The originating click event
+ * @private
+ */
+ _onRollSkillCheck(event) {
+ event.preventDefault();
+ const skill = event.currentTarget.parentElement.dataset.skill;
+ this.actor.rollSkill(skill, {event: event});
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle toggling Ability score proficiency level
+ * @param {Event} event The originating click event
+ * @private
+ */
+ _onToggleAbilityProficiency(event) {
+ event.preventDefault();
+ const field = event.currentTarget.previousElementSibling;
+ this.actor.update({[field.name]: 1 - parseInt(field.value)});
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle toggling of filters to display a different set of owned items
+ * @param {Event} event The click event which triggered the toggle
+ * @private
+ */
+ _onToggleFilter(event) {
+ event.preventDefault();
+ const li = event.currentTarget;
+ const set = this._filters[li.parentElement.dataset.filter];
+ const filter = li.dataset.filter;
+ if ( set.has(filter) ) set.delete(filter);
+ else set.add(filter);
+ this.render();
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle spawning the TraitSelector application which allows a checkbox of multiple trait options
+ * @param {Event} event The click event which originated the selection
+ * @private
+ */
+ _onTraitSelector(event) {
+ event.preventDefault();
+ const a = event.currentTarget;
+ const label = a.parentElement.querySelector("label");
+ const choices = CONFIG.SW5E[a.dataset.options];
+ const options = { name: a.dataset.target, title: label.innerText, choices };
+ new TraitSelector(this.actor, options).render(true)
+ }
+
+ /* -------------------------------------------- */
+
+ /** @override */
+ _getHeaderButtons() {
+ let buttons = super._getHeaderButtons();
+
+ // Add button to revert polymorph
+ if ( !this.actor.isPolymorphed || this.actor.isToken ) return buttons;
+ buttons.unshift({
+ label: 'SW5E.PolymorphRestoreTransformation',
+ class: "restore-transformation",
+ icon: "fas fa-backward",
+ onclick: ev => this.actor.revertOriginalForm()
+ });
+ return buttons;
+ }
+}
\ No newline at end of file
diff --git a/module/actor/sheets/newSheet/character.js b/module/actor/sheets/newSheet/character.js
index 40ed4eaf..643d3757 100644
--- a/module/actor/sheets/newSheet/character.js
+++ b/module/actor/sheets/newSheet/character.js
@@ -1,4 +1,4 @@
-import ActorSheet5e from "../base.js";
+import ActorSheet5e from "./base.js";
import Actor5e from "../../entity.js";
/**
@@ -79,9 +79,9 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
backpack: { label: "SW5E.ItemTypeContainerPl", items: [], dataset: {type: "backpack"} },
loot: { label: "SW5E.ItemTypeLootPl", items: [], dataset: {type: "loot"} }
};
-
+
// Partition items by category
- let [items, powers, feats, classes, species, archetypes, classfeatures, backgrounds, lightsaberforms] = data.items.reduce((arr, item) => {
+ let [items, powers, feats, classes, species, archetypes, classfeatures, backgrounds, fightingstyles, fightingmasteries, lightsaberforms] = data.items.reduce((arr, item) => {
// Item details
item.img = item.img || DEFAULT_TOKEN;
@@ -104,10 +104,12 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
else if ( item.type === "archetype" ) arr[5].push(item);
else if ( item.type === "classfeature" ) arr[6].push(item);
else if ( item.type === "background" ) arr[7].push(item);
- else if ( item.type === "lightsaberform" ) arr[8].push(item);
+ else if ( item.type === "fightingstyle" ) arr[8].push(item);
+ else if ( item.type === "fightingmastery" ) arr[9].push(item);
+ else if ( item.type === "lightsaberform" ) arr[10].push(item);
else if ( Object.keys(inventory).includes(item.type ) ) arr[0].push(item);
return arr;
- }, [[], [], [], [], [], [], [], [], []]);
+ }, [[], [], [], [], [], [], [], [], [], [], []]);
// Apply active item filters
items = this._filterItems(items, this._filters.inventory);
@@ -131,11 +133,13 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
// Organize Features
const features = {
classes: { label: "SW5E.ItemTypeClassPl", items: [], hasActions: false, dataset: {type: "class"}, isClass: true },
- classfeatures: { label: "SW5E.ItemTypeClassFeats", items: [], hasActions: false, dataset: {type: "classfeature"}, isClassfeature: true },
- archetype: { label: "SW5E.ItemTypeArchetype", items: [], hasActions: false, dataset: {type: "archetype"}, isArchetype: true },
- species: { label: "SW5E.ItemTypeSpecies", items: [], hasActions: false, dataset: {type: "species"}, isSpecies: true },
- background: { label: "SW5E.ItemTypeBackground", items: [], hasActions: false, dataset: {type: "background"}, isBackground: true },
- lightsaberform: { label: "SW5E.ItemTypeLightsaberForm", items: [], hasActions: false, dataset: {type: "lightsaberform"}, isLightsaberform: true },
+ classfeatures: { label: "SW5E.ItemTypeClassFeats", items: [], hasActions: true, dataset: {type: "classfeature"}, isClassfeature: true },
+ archetype: { label: "SW5E.ItemTypeArchetype", items: [], hasActions: false, dataset: {type: "archetype"}, isArchetype: true },
+ species: { label: "SW5E.ItemTypeSpecies", items: [], hasActions: false, dataset: {type: "species"}, isSpecies: true },
+ background: { label: "SW5E.ItemTypeBackground", items: [], hasActions: false, dataset: {type: "background"}, isBackground: true },
+ fightingstyles: { label: "SW5E.ItemTypeFightingStylePl", items: [], hasActions: false, dataset: {type: "fightingstyle"}, isFightingstyle: true },
+ fightingmasteries: { label: "SW5E.ItemTypeFightingMasteryPl", items: [], hasActions: false, dataset: {type: "fightingmastery"}, isFightingmastery: true },
+ lightsaberforms: { label: "SW5E.ItemTypeLightsaberFormPl", items: [], hasActions: false, dataset: {type: "lightsaberform"}, isLightsaberform: true },
active: { label: "SW5E.FeatureActive", items: [], hasActions: true, dataset: {type: "feat", "activation.type": "action"} },
passive: { label: "SW5E.FeaturePassive", items: [], hasActions: false, dataset: {type: "feat"} }
};
@@ -145,11 +149,13 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
}
classes.sort((a, b) => b.levels - a.levels);
features.classes.items = classes;
- features.classfeatures.items = classfeatures;
- features.archetype.items = archetypes;
- features.species.items = species;
- features.background.items = backgrounds;
- features.lightsaberform.items = lightsaberforms;
+ features.classfeatures.items = classfeatures;
+ features.archetype.items = archetypes;
+ features.species.items = species;
+ features.background.items = backgrounds;
+ features.fightingstyles.items = fightingstyles;
+ features.fightingmasteries.items = fightingmasteries;
+ features.lightsaberforms.items = lightsaberforms;
// Assign and return
data.inventory = Object.values(inventory);
@@ -204,8 +210,8 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
html.find('.short-rest').click(this._onShortRest.bind(this));
html.find('.long-rest').click(this._onLongRest.bind(this));
- // Death saving throws
- html.find('.death-save').click(this._onDeathSave.bind(this));
+ // Rollable sheet actions
+ html.find(".rollable[data-action]").click(this._onSheetAction.bind(this));
// Send Languages to Chat onClick
html.find('[data-options="share-languages"]').click(event => {
@@ -271,13 +277,19 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
/* -------------------------------------------- */
/**
- * Handle rolling a death saving throw for the Character
+ * Handle mouse click events for character sheet actions
* @param {MouseEvent} event The originating click event
* @private
*/
- _onDeathSave(event) {
+ _onSheetAction(event) {
event.preventDefault();
- return this.actor.rollDeathSave({event: event});
+ const button = event.currentTarget;
+ switch( button.dataset.action ) {
+ case "rollDeathSave":
+ return this.actor.rollDeathSave({event: event});
+ case "rollInitiative":
+ return this.actor.rollInitiative({createCombatants: true});
+ }
}
/* -------------------------------------------- */
@@ -324,57 +336,26 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
/* -------------------------------------------- */
- /**
- * Handle mouse click events to convert currency to the highest possible denomination
- * @param {MouseEvent} event The originating click event
- * @private
- */
- async _onConvertCurrency(event) {
- event.preventDefault();
- return Dialog.confirm({
- title: `${game.i18n.localize("SW5E.CurrencyConvert")}`,
- content: `${game.i18n.localize("SW5E.CurrencyConvertHint")}
`,
- yes: () => this.actor.convertCurrency()
- });
- }
-
- /* -------------------------------------------- */
-
/** @override */
async _onDropItemCreate(itemData) {
- // Upgrade the number of class levels a character has and add features
+ // Increment the number of class levels a character instead of creating a new item
if ( itemData.type === "class" ) {
const cls = this.actor.itemTypes.class.find(c => c.name === itemData.name);
- const classWasAlreadyPresent = !!cls;
-
- // Add new features for class level
- if ( !classWasAlreadyPresent ) {
- Actor5e.getClassFeatures(itemData).then(features => {
- this.actor.createEmbeddedEntity("OwnedItem", features);
- });
- }
-
- // If the actor already has the class, increment the level instead of creating a new item
- // then add new features as long as level increases
- if ( classWasAlreadyPresent ) {
- const lvl = cls.data.data.levels;
- const newLvl = Math.min(lvl + 1, 20 + lvl - this.actor.data.data.details.level);
- if ( !(lvl === newLvl) ) {
- cls.update({"data.levels": newLvl});
- itemData.data.levels = newLvl;
- Actor5e.getClassFeatures(itemData).then(features => {
- this.actor.createEmbeddedEntity("OwnedItem", features);
- });
+ let priorLevel = cls?.data.data.levels ?? 0;
+ if ( !!cls ) {
+ const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level);
+ if ( next > priorLevel ) {
+ itemData.levels = next;
+ return cls.update({"data.levels": next});
}
- return
}
}
+ // Default drop handling if levels were not added
super._onDropItemCreate(itemData);
}
}
-
async function addFavorites(app, html, data) {
// Thisfunction is adapted for the SwaltSheet from the Favorites Item
// Tab Module created for Foundry VTT - by Felix Müller (Felix#6196 on Discord).
@@ -516,7 +497,7 @@ async function addFavorites(app, html, data) {
if (app.options.editable) {
favtabHtml.find('.item-image').click(ev => app._onItemRoll(ev));
- let handler = ev => app._onDragItemStart(ev);
+ let handler = ev => app._onDragStart(ev);
favtabHtml.find('.item').each((i, li) => {
if (li.classList.contains("inventory-header")) return;
li.setAttribute("draggable", true);
@@ -566,7 +547,14 @@ async function addFavorites(app, html, data) {
});
}
tabContainer.append(favtabHtml);
-
+ // if(app.options.editable) {
+ // let handler = ev => app._onDragItemStart(ev);
+ // tabContainer.find('.item').each((i, li) => {
+ // if (li.classList.contains("inventory-header")) return;
+ // li.setAttribute("draggable", true);
+ // li.addEventListener("dragstart", handler, false);
+ // });
+ //}
// try {
// if (game.modules.get("betterrolls5e") && game.modules.get("betterrolls5e").active) BetterRolls.addItemContent(app.object, favtabHtml, ".item .item-name h4", ".item-properties", ".item > .rollable div");
// }
diff --git a/module/actor/sheets/newSheet/npc.js b/module/actor/sheets/newSheet/npc.js
new file mode 100644
index 00000000..2ef953a5
--- /dev/null
+++ b/module/actor/sheets/newSheet/npc.js
@@ -0,0 +1,138 @@
+import ActorSheet5e from "./base.js";
+
+/**
+ * An Actor sheet for NPC type characters in the SW5E system.
+ * Extends the base ActorSheet5e class.
+ * @extends {ActorSheet5e}
+ */
+export default class ActorSheet5eNPCNew extends ActorSheet5e {
+
+ /** @override */
+ get template() {
+ if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/newActor/limited-sheet.html";
+ return `systems/sw5e/templates/actors/newActor/npc-sheet.html`;
+ }
+ /** @override */
+ static get defaultOptions() {
+ return mergeObject(super.defaultOptions, {
+ classes: ["sw5e", "sheet", "actor", "npc"],
+ width: 600,
+ width: 800,
+ tabs: [{
+ navSelector: ".root-tabs",
+ contentSelector: ".sheet-body",
+ initial: "attributes"
+ }],
+ });
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Organize Owned Items for rendering the NPC sheet
+ * @private
+ */
+ _prepareItems(data) {
+
+ // Categorize Items as Features and Powers
+ const features = {
+ weapons: { label: game.i18n.localize("SW5E.AttackPl"), items: [] , hasActions: true, dataset: {type: "weapon", "weapon-type": "natural"} },
+ actions: { label: game.i18n.localize("SW5E.ActionPl"), items: [] , hasActions: true, dataset: {type: "feat", "activation.type": "action"} },
+ passive: { label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"} },
+ equipment: { label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}}
+ };
+
+ // Start by classifying items into groups for rendering
+ let [powers, other] = data.items.reduce((arr, item) => {
+ item.img = item.img || DEFAULT_TOKEN;
+ item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
+ item.hasUses = item.data.uses && (item.data.uses.max > 0);
+ item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false);
+ item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0));
+ item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type));
+ if ( item.type === "power" ) arr[0].push(item);
+ else arr[1].push(item);
+ return arr;
+ }, [[], []]);
+
+ // Apply item filters
+ powers = this._filterItems(powers, this._filters.powerbook);
+ other = this._filterItems(other, this._filters.features);
+
+ // Organize Powerbook
+ const powerbook = this._preparePowerbook(data, powers);
+
+ // Organize Features
+ for ( let item of other ) {
+ if ( item.type === "weapon" ) features.weapons.items.push(item);
+ else if ( item.type === "feat" ) {
+ if ( item.data.activation.type ) features.actions.items.push(item);
+ else features.passive.items.push(item);
+ }
+ else features.equipment.items.push(item);
+ }
+
+ // Assign and return
+ data.features = Object.values(features);
+ data.powerbook = powerbook;
+ }
+
+
+ /* -------------------------------------------- */
+
+ /** @override */
+ getData() {
+ const data = super.getData();
+
+ // Challenge Rating
+ const cr = parseFloat(data.data.details.cr || 0);
+ const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"};
+ data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1;
+ return data;
+ }
+
+ /* -------------------------------------------- */
+ /* Object Updates */
+ /* -------------------------------------------- */
+
+ /** @override */
+ _updateObject(event, formData) {
+
+ // Format NPC Challenge Rating
+ const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5};
+ let crv = "data.details.cr";
+ let cr = formData[crv];
+ cr = crs[cr] || parseFloat(cr);
+ if ( cr ) formData[crv] = cr < 1 ? cr : parseInt(cr);
+
+ // Parent ActorSheet update steps
+ super._updateObject(event, formData);
+ }
+
+ /* -------------------------------------------- */
+ /* Event Listeners and Handlers */
+ /* -------------------------------------------- */
+
+ /** @override */
+ activateListeners(html) {
+ super.activateListeners(html);
+ html.find(".health .rollable").click(this._onRollHealthFormula.bind(this));
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle rolling NPC health values using the provided formula
+ * @param {Event} event The original click event
+ * @private
+ */
+ _onRollHealthFormula(event) {
+ event.preventDefault();
+ const formula = this.actor.data.data.attributes.hp.formula;
+ if ( !formula ) return;
+ const hp = new Roll(formula).roll().total;
+ AudioHelper.play({src: CONFIG.sounds.dice});
+ this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp});
+ }
+}
+
diff --git a/module/actor/sheets/newSheet/vehicle.js b/module/actor/sheets/newSheet/vehicle.js
new file mode 100644
index 00000000..e34026c0
--- /dev/null
+++ b/module/actor/sheets/newSheet/vehicle.js
@@ -0,0 +1,385 @@
+import ActorSheet5e from "./base.js";
+
+/**
+ * An Actor sheet for Vehicle type actors.
+ * Extends the base ActorSheet5e class.
+ * @type {ActorSheet5e}
+ */
+export default class ActorSheet5eVehicle extends ActorSheet5e {
+ /**
+ * Define default rendering options for the Vehicle sheet.
+ * @returns {Object}
+ */
+ static get defaultOptions() {
+ return mergeObject(super.defaultOptions, {
+ classes: ["sw5e", "sheet", "actor", "vehicle"],
+ width: 605,
+ height: 680
+ });
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Creates a new cargo entry for a vehicle Actor.
+ */
+ static get newCargo() {
+ return {
+ name: '',
+ quantity: 1
+ };
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Compute the total weight of the vehicle's cargo.
+ * @param {Number} totalWeight The cumulative item weight from inventory items
+ * @param {Object} actorData The data object for the Actor being rendered
+ * @returns {{max: number, value: number, pct: number}}
+ * @private
+ */
+ _computeEncumbrance(totalWeight, actorData) {
+
+ // Compute currency weight
+ const totalCoins = Object.values(actorData.data.currency).reduce((acc, denom) => acc + denom, 0);
+ totalWeight += totalCoins / CONFIG.SW5E.encumbrance.currencyPerWeight;
+
+ // Vehicle weights are an order of magnitude greater.
+ totalWeight /= CONFIG.SW5E.encumbrance.vehicleWeightMultiplier;
+
+ // Compute overall encumbrance
+ const max = actorData.data.attributes.capacity.cargo;
+ const pct = Math.clamped((totalWeight * 100) / max, 0, 100);
+ return {value: totalWeight.toNearest(0.1), max, pct};
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Prepare items that are mounted to a vehicle and require one or more crew
+ * to operate.
+ * @private
+ */
+ _prepareCrewedItem(item) {
+
+ // Determine crewed status
+ const isCrewed = item.data.crewed;
+ item.toggleClass = isCrewed ? 'active' : '';
+ item.toggleTitle = game.i18n.localize(`SW5E.${isCrewed ? 'Crewed' : 'Uncrewed'}`);
+
+ // Handle crew actions
+ if (item.type === 'feat' && item.data.activation.type === 'crew') {
+ item.crew = item.data.activation.cost;
+ item.cover = game.i18n.localize(`SW5E.${item.data.cover ? 'CoverTotal' : 'None'}`);
+ if (item.data.cover === .5) item.cover = '½';
+ else if (item.data.cover === .75) item.cover = '¾';
+ else if (item.data.cover === null) item.cover = '—';
+ if (item.crew < 1 || item.crew === null) item.crew = '—';
+ }
+
+ // Prepare vehicle weapons
+ if (item.type === 'equipment' || item.type === 'weapon') {
+ item.threshold = item.data.hp.dt ? item.data.hp.dt : '—';
+ }
+ }
+
+ /* -------------------------------------------- */
+
+ /** @override */
+ _getMovementSpeed(actorData) {
+ return {primary: "", special: ""};
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Organize Owned Items for rendering the Vehicle sheet.
+ * @private
+ */
+ _prepareItems(data) {
+ const cargoColumns = [{
+ label: game.i18n.localize('SW5E.Quantity'),
+ css: 'item-qty',
+ property: 'quantity',
+ editable: 'Number'
+ }];
+
+ const equipmentColumns = [{
+ label: game.i18n.localize('SW5E.Quantity'),
+ css: 'item-qty',
+ property: 'data.quantity'
+ }, {
+ label: game.i18n.localize('SW5E.AC'),
+ css: 'item-ac',
+ property: 'data.armor.value'
+ }, {
+ label: game.i18n.localize('SW5E.HP'),
+ css: 'item-hp',
+ property: 'data.hp.value',
+ editable: 'Number'
+ }, {
+ label: game.i18n.localize('SW5E.Threshold'),
+ css: 'item-threshold',
+ property: 'threshold'
+ }];
+
+ const features = {
+ actions: {
+ label: game.i18n.localize('SW5E.ActionPl'),
+ items: [],
+ crewable: true,
+ dataset: {type: 'feat', 'activation.type': 'crew'},
+ columns: [{
+ label: game.i18n.localize('SW5E.VehicleCrew'),
+ css: 'item-crew',
+ property: 'crew'
+ }, {
+ label: game.i18n.localize('SW5E.Cover'),
+ css: 'item-cover',
+ property: 'cover'
+ }]
+ },
+ equipment: {
+ label: game.i18n.localize('SW5E.ItemTypeEquipment'),
+ items: [],
+ crewable: true,
+ dataset: {type: 'equipment', 'armor.type': 'vehicle'},
+ columns: equipmentColumns
+ },
+ passive: {
+ label: game.i18n.localize('SW5E.Features'),
+ items: [],
+ dataset: {type: 'feat'}
+ },
+ reactions: {
+ label: game.i18n.localize('SW5E.ReactionPl'),
+ items: [],
+ dataset: {type: 'feat', 'activation.type': 'reaction'}
+ },
+ weapons: {
+ label: game.i18n.localize('SW5E.ItemTypeWeaponPl'),
+ items: [],
+ crewable: true,
+ dataset: {type: 'weapon', 'weapon-type': 'siege'},
+ columns: equipmentColumns
+ }
+ };
+
+ const cargo = {
+ crew: {
+ label: game.i18n.localize('SW5E.VehicleCrew'),
+ items: data.data.cargo.crew,
+ css: 'cargo-row crew',
+ editableName: true,
+ dataset: {type: 'crew'},
+ columns: cargoColumns
+ },
+ passengers: {
+ label: game.i18n.localize('SW5E.VehiclePassengers'),
+ items: data.data.cargo.passengers,
+ css: 'cargo-row passengers',
+ editableName: true,
+ dataset: {type: 'passengers'},
+ columns: cargoColumns
+ },
+ cargo: {
+ label: game.i18n.localize('SW5E.VehicleCargo'),
+ items: [],
+ dataset: {type: 'loot'},
+ columns: [{
+ label: game.i18n.localize('SW5E.Quantity'),
+ css: 'item-qty',
+ property: 'data.quantity',
+ editable: 'Number'
+ }, {
+ label: game.i18n.localize('SW5E.Price'),
+ css: 'item-price',
+ property: 'data.price',
+ editable: 'Number'
+ }, {
+ label: game.i18n.localize('SW5E.Weight'),
+ css: 'item-weight',
+ property: 'data.weight',
+ editable: 'Number'
+ }]
+ }
+ };
+
+ let totalWeight = 0;
+ for (const item of data.items) {
+ this._prepareCrewedItem(item);
+ if (item.type === 'weapon') features.weapons.items.push(item);
+ else if (item.type === 'equipment') features.equipment.items.push(item);
+ else if (item.type === 'loot') {
+ totalWeight += (item.data.weight || 0) * item.data.quantity;
+ cargo.cargo.items.push(item);
+ }
+ else if (item.type === 'feat') {
+ if (!item.data.activation.type || item.data.activation.type === 'none') {
+ features.passive.items.push(item);
+ }
+ else if (item.data.activation.type === 'reaction') features.reactions.items.push(item);
+ else features.actions.items.push(item);
+ }
+ }
+
+ data.features = Object.values(features);
+ data.cargo = Object.values(cargo);
+ data.data.attributes.encumbrance = this._computeEncumbrance(totalWeight, data);
+ }
+
+ /* -------------------------------------------- */
+ /* Event Listeners and Handlers */
+ /* -------------------------------------------- */
+
+ /** @override */
+ activateListeners(html) {
+ super.activateListeners(html);
+ if (!this.options.editable) return;
+
+ html.find('.item-toggle').click(this._onToggleItem.bind(this));
+ html.find('.item-hp input')
+ .click(evt => evt.target.select())
+ .change(this._onHPChange.bind(this));
+
+ html.find('.item:not(.cargo-row) input[data-property]')
+ .click(evt => evt.target.select())
+ .change(this._onEditInSheet.bind(this));
+
+ html.find('.cargo-row input')
+ .click(evt => evt.target.select())
+ .change(this._onCargoRowChange.bind(this));
+
+ if (this.actor.data.data.attributes.actions.stations) {
+ html.find('.counter.actions, .counter.action-thresholds').hide();
+ }
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle saving a cargo row (i.e. crew or passenger) in-sheet.
+ * @param event {Event}
+ * @returns {Promise|null}
+ * @private
+ */
+ _onCargoRowChange(event) {
+ event.preventDefault();
+ const target = event.currentTarget;
+ const row = target.closest('.item');
+ const idx = Number(row.dataset.itemId);
+ const property = row.classList.contains('crew') ? 'crew' : 'passengers';
+
+ // Get the cargo entry
+ const cargo = duplicate(this.actor.data.data.cargo[property]);
+ const entry = cargo[idx];
+ if (!entry) return null;
+
+ // Update the cargo value
+ const key = target.dataset.property || 'name';
+ const type = target.dataset.dtype;
+ let value = target.value;
+ if (type === 'Number') value = Number(value);
+ entry[key] = value;
+
+ // Perform the Actor update
+ return this.actor.update({[`data.cargo.${property}`]: cargo});
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle editing certain values like quantity, price, and weight in-sheet.
+ * @param event {Event}
+ * @returns {Promise- }
+ * @private
+ */
+ _onEditInSheet(event) {
+ event.preventDefault();
+ const itemID = event.currentTarget.closest('.item').dataset.itemId;
+ const item = this.actor.items.get(itemID);
+ const property = event.currentTarget.dataset.property;
+ const type = event.currentTarget.dataset.dtype;
+ let value = event.currentTarget.value;
+ switch (type) {
+ case 'Number': value = parseInt(value); break;
+ case 'Boolean': value = value === 'true'; break;
+ }
+ return item.update({[`${property}`]: value});
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle creating a new crew or passenger row.
+ * @param event {Event}
+ * @returns {Promise}
+ * @private
+ */
+ _onItemCreate(event) {
+ event.preventDefault();
+ const target = event.currentTarget;
+ const type = target.dataset.type;
+ if (type === 'crew' || type === 'passengers') {
+ const cargo = duplicate(this.actor.data.data.cargo[type]);
+ cargo.push(this.constructor.newCargo);
+ return this.actor.update({[`data.cargo.${type}`]: cargo});
+ }
+ return super._onItemCreate(event);
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle deleting a crew or passenger row.
+ * @param event {Event}
+ * @returns {Promise}
+ * @private
+ */
+ _onItemDelete(event) {
+ event.preventDefault();
+ const row = event.currentTarget.closest('.item');
+ if (row.classList.contains('cargo-row')) {
+ const idx = Number(row.dataset.itemId);
+ const type = row.classList.contains('crew') ? 'crew' : 'passengers';
+ const cargo = duplicate(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx);
+ return this.actor.update({[`data.cargo.${type}`]: cargo});
+ }
+
+ return super._onItemDelete(event);
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Special handling for editing HP to clamp it within appropriate range.
+ * @param event {Event}
+ * @returns {Promise
- }
+ * @private
+ */
+ _onHPChange(event) {
+ event.preventDefault();
+ const itemID = event.currentTarget.closest('.item').dataset.itemId;
+ const item = this.actor.items.get(itemID);
+ const hp = Math.clamped(0, parseInt(event.currentTarget.value), item.data.data.hp.max);
+ event.currentTarget.value = hp;
+ return item.update({'data.hp.value': hp});
+ }
+
+ /* -------------------------------------------- */
+
+ /**
+ * Handle toggling an item's crewed status.
+ * @param event {Event}
+ * @returns {Promise
- }
+ * @private
+ */
+ _onToggleItem(event) {
+ event.preventDefault();
+ const itemID = event.currentTarget.closest('.item').dataset.itemId;
+ const item = this.actor.items.get(itemID);
+ const crewed = !!item.data.data.crewed;
+ return item.update({'data.crewed': !crewed});
+ }
+};
diff --git a/module/actor/sheets/base.js b/module/actor/sheets/oldSheets/base.js
similarity index 94%
rename from module/actor/sheets/base.js
rename to module/actor/sheets/oldSheets/base.js
index b21dc715..961ba894 100644
--- a/module/actor/sheets/base.js
+++ b/module/actor/sheets/oldSheets/base.js
@@ -1,9 +1,10 @@
-import Item5e from "../../item/entity.js";
-import TraitSelector from "../../apps/trait-selector.js";
-import ActorSheetFlags from "../../apps/actor-flags.js";
-import MovementConfig from "../../apps/movement-config.js";
-import {SW5E} from '../../config.js';
-import {onManageActiveEffect, prepareActiveEffectCategories} from "../../effects.js";
+import Item5e from "../../../item/entity.js";
+import TraitSelector from "../../../apps/trait-selector.js";
+import ActorSheetFlags from "../../../apps/actor-flags.js";
+import ActorMovementConfig from "../../../apps/movement-config.js";
+import ActorSensesConfig from "../../../apps/senses-config.js";
+import {SW5E} from '../../../config.js';
+import {onManageActiveEffect, prepareActiveEffectCategories} from "../../../effects.js";
/**
* Extend the basic ActorSheet class to suppose SW5e-specific logic and functionality.
@@ -99,6 +100,9 @@ export default class ActorSheet5e extends ActorSheet {
// Movement speeds
data.movement = this._getMovementSpeed(data.actor);
+ // Senses
+ data.senses = this._getSenses(data.actor);
+
// Update traits
this._prepareTraits(data.actor.data.traits);
@@ -121,7 +125,7 @@ export default class ActorSheet5e extends ActorSheet {
* @private
*/
_getMovementSpeed(actorData) {
- const movement = actorData.data.attributes.movement;
+ const movement = actorData.data.attributes.movement || {};
const speeds = [
[movement.burrow, `${game.i18n.localize("SW5E.MovementBurrow")} ${movement.burrow}`],
[movement.climb, `${game.i18n.localize("SW5E.MovementClimb")} ${movement.climb}`],
@@ -136,6 +140,20 @@ export default class ActorSheet5e extends ActorSheet {
/* -------------------------------------------- */
+ _getSenses(actorData) {
+ const senses = actorData.data.attributes.senses || {};
+ const tags = {};
+ for ( let [k, label] of Object.entries(CONFIG.SW5E.senses) ) {
+ const v = senses[k] ?? 0
+ if ( v === 0 ) continue;
+ tags[k] = `${game.i18n.localize(label)} ${v} ${senses.units}`;
+ }
+ if ( !!senses.special ) tags["special"] = senses.special;
+ return tags;
+ }
+
+ /* -------------------------------------------- */
+
/**
* 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
@@ -373,8 +391,7 @@ export default class ActorSheet5e extends ActorSheet {
html.find('.trait-selector').click(this._onTraitSelector.bind(this));
// Configure Special Flags
- html.find('.configure-movement').click(this._onMovementConfig.bind(this));
- html.find('.configure-flags').click(this._onConfigureFlags.bind(this));
+ html.find('.config-button').click(this._onConfigMenu.bind(this));
// Owned Item management
html.find('.item-create').click(this._onItemCreate.bind(this));
@@ -448,11 +465,24 @@ export default class ActorSheet5e extends ActorSheet {
/* -------------------------------------------- */
/**
- * Handle click events for the Traits tab button to configure special Character Flags
+ * Handle spawning the TraitSelector application which allows a checkbox of multiple trait options
+ * @param {Event} event The click event which originated the selection
+ * @private
*/
- _onConfigureFlags(event) {
+ _onConfigMenu(event) {
event.preventDefault();
- new ActorSheetFlags(this.actor).render(true);
+ const button = event.currentTarget;
+ switch ( button.dataset.action ) {
+ case "movement":
+ new ActorMovementConfig(this.object).render(true);
+ break;
+ case "flags":
+ new ActorSheetFlags(this.object).render(true);
+ break;
+ case "senses":
+ new ActorSensesConfig(this.object).render(true);
+ break;
+ }
}
/* -------------------------------------------- */
@@ -529,6 +559,8 @@ export default class ActorSheet5e extends ActorSheet {
icon: '',
label: game.i18n.localize('SW5E.PolymorphWildShape'),
callback: html => this.actor.transformInto(sourceActor, {
+ keepBio: true,
+ keepClass: true,
keepMental: true,
mergeSaves: true,
mergeSkills: true,
@@ -619,14 +651,7 @@ export default class ActorSheet5e extends ActorSheet {
event.preventDefault();
const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.getOwnedItem(itemId);
-
- // Roll powers through the actor
- if ( item.data.type === "power" ) {
- return this.actor.usePower(item, {configureDialog: !event.shiftKey});
- }
-
- // Otherwise roll the Item directly
- else return item.roll();
+ return item.roll();
}
/* -------------------------------------------- */
@@ -687,7 +712,7 @@ export default class ActorSheet5e extends ActorSheet {
data: duplicate(header.dataset)
};
delete itemData.data["type"];
- return this.actor.createOwnedItem(itemData);
+ return this.actor.createEmbeddedEntity("OwnedItem", itemData);
}
/* -------------------------------------------- */
@@ -791,18 +816,6 @@ export default class ActorSheet5e extends ActorSheet {
/* -------------------------------------------- */
- /**
- * Handle spawning the TraitSelector application which allows a checkbox of multiple trait options
- * @param {Event} event The click event which originated the selection
- * @private
- */
- _onMovementConfig(event) {
- event.preventDefault();
- new MovementConfig(this.object).render(true);
- }
-
- /* -------------------------------------------- */
-
/** @override */
_getHeaderButtons() {
let buttons = super._getHeaderButtons();
diff --git a/module/actor/sheets/oldSheets/character.js b/module/actor/sheets/oldSheets/character.js
index c897e9a1..f9d50806 100644
--- a/module/actor/sheets/oldSheets/character.js
+++ b/module/actor/sheets/oldSheets/character.js
@@ -1,4 +1,4 @@
-import ActorSheet5e from "../base.js";
+import ActorSheet5e from "./base.js";
import Actor5e from "../../entity.js";
/**
@@ -75,6 +75,18 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
// Item details
item.img = item.img || DEFAULT_TOKEN;
item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
+ item.attunement = {
+ 1: {
+ icon: "fa-sun",
+ cls: "not-attuned",
+ title: "SW5E.AttunementRequired"
+ },
+ 2: {
+ icon: "fa-sun",
+ cls: "attuned",
+ title: "SW5E.AttunementAttuned"
+ }
+ }[item.data.attunement];
// Item usage
item.hasUses = item.data.uses && (item.data.uses.max > 0);
@@ -122,7 +134,7 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
// Organize Features
const features = {
classes: { label: "SW5E.ItemTypeClassPl", items: [], hasActions: false, dataset: {type: "class"}, isClass: true },
- classfeatures: { label: "SW5E.ItemTypeClassFeats", items: [], hasActions: false, dataset: {type: "classfeature"}, isClassfeature: true },
+ classfeatures: { label: "SW5E.ItemTypeClassFeats", items: [], hasActions: true, dataset: {type: "classfeature"}, isClassfeature: true },
archetype: { label: "SW5E.ItemTypeArchetype", items: [], hasActions: false, dataset: {type: "archetype"}, isArchetype: true },
species: { label: "SW5E.ItemTypeSpecies", items: [], hasActions: false, dataset: {type: "species"}, isSpecies: true },
background: { label: "SW5E.ItemTypeBackground", items: [], hasActions: false, dataset: {type: "background"}, isBackground: true },
@@ -203,7 +215,7 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
/* -------------------------------------------- */
/**
- * Handle rolling a death saving throw for the Character
+ * Handle mouse click events for character sheet actions
* @param {MouseEvent} event The originating click event
* @private
*/
@@ -263,37 +275,21 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
/** @override */
async _onDropItemCreate(itemData) {
- let addLevel = false;
- // Upgrade the number of class levels a character has and add features
+ // Increment the number of class levels a character instead of creating a new item
if ( itemData.type === "class" ) {
const cls = this.actor.itemTypes.class.find(c => c.name === itemData.name);
let priorLevel = cls?.data.data.levels ?? 0;
- const hasClass = !!cls;
-
- // Increment levels instead of creating a new item
- if ( hasClass ) {
+ if ( !!cls ) {
const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level);
if ( next > priorLevel ) {
itemData.levels = next;
- await cls.update({"data.levels": next});
- addLevel = true;
+ return cls.update({"data.levels": next});
}
}
-
- // Add class features
- if ( !hasClass || addLevel ) {
- const features = await Actor5e.getClassFeatures({
- className: itemData.name,
- archetypeName: itemData.data.archetype,
- level: itemData.levels,
- priorLevel: priorLevel
- });
- await this.actor.createEmbeddedEntity("OwnedItem", features);
- }
}
// Default drop handling if levels were not added
- if ( !addLevel ) super._onDropItemCreate(itemData);
+ super._onDropItemCreate(itemData);
}
}
diff --git a/module/actor/sheets/oldSheets/npc.js b/module/actor/sheets/oldSheets/npc.js
index 124fa8b2..366dc65e 100644
--- a/module/actor/sheets/oldSheets/npc.js
+++ b/module/actor/sheets/oldSheets/npc.js
@@ -1,4 +1,4 @@
-import ActorSheet5e from "../base.js";
+import ActorSheet5e from "./base.js";
/**
* An Actor sheet for NPC type characters in the SW5E system.
diff --git a/module/actor/sheets/oldSheets/vehicle.js b/module/actor/sheets/oldSheets/vehicle.js
index 602f984c..e34026c0 100644
--- a/module/actor/sheets/oldSheets/vehicle.js
+++ b/module/actor/sheets/oldSheets/vehicle.js
@@ -1,4 +1,4 @@
-import ActorSheet5e from "../base.js";
+import ActorSheet5e from "./base.js";
/**
* An Actor sheet for Vehicle type actors.
diff --git a/module/apps/ability-use-dialog.js b/module/apps/ability-use-dialog.js
index 8a914e71..079ea93d 100644
--- a/module/apps/ability-use-dialog.js
+++ b/module/apps/ability-use-dialog.js
@@ -34,15 +34,19 @@ export default class AbilityUseDialog extends Dialog {
const quantity = itemData.quantity || 0;
const recharge = itemData.recharge || {};
const recharges = !!recharge.value;
+ const sufficientUses = (quantity > 0 && !uses.value) || uses.value > 0;
// Prepare dialog form data
const data = {
item: item.data,
title: game.i18n.format("SW5E.AbilityUseHint", item.data),
note: this._getAbilityUseNote(item.data, uses, recharge),
- hasLimitedUses: uses.max || recharges,
- canUse: recharges ? recharge.charged : (quantity > 0 && !uses.value) || uses.value > 0,
- hasPlaceableTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget,
+ consumePowerSlot: false,
+ consumeRecharge: recharges,
+ consumeResource: !!itemData.consume.target,
+ consumeUses: uses.max,
+ canUse: recharges ? recharge.charged : sufficientUses,
+ createTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget,
errors: []
};
if ( item.data.type === "power" ) this._getPowerData(actorData, itemData, data);
@@ -50,7 +54,7 @@ export default class AbilityUseDialog extends Dialog {
// Render the ability usage template
const html = await renderTemplate("systems/sw5e/templates/apps/ability-use.html", data);
- // Create the Dialog and return as a Promise
+ // Create the Dialog and return data as a Promise
const icon = data.isPower ? "fa-magic" : "fa-fist-raised";
const label = game.i18n.localize("SW5E.AbilityUse" + (data.isPower ? "Cast" : "Use"));
return new Promise((resolve) => {
@@ -61,7 +65,10 @@ export default class AbilityUseDialog extends Dialog {
use: {
icon: ``,
label: label,
- callback: html => resolve(new FormData(html[0].querySelector("form")))
+ callback: html => {
+ const fd = new FormDataExtended(html[0].querySelector("form"));
+ resolve(fd.toObject());
+ }
}
},
default: "use",
@@ -83,11 +90,11 @@ export default class AbilityUseDialog extends Dialog {
// Determine whether the power may be up-cast
const lvl = itemData.level;
- const canUpcast = (lvl > 0) && CONFIG.SW5E.powerUpcastModes.includes(itemData.preparation.mode);
+ const consumePowerSlot = (lvl > 0) && CONFIG.SW5E.powerUpcastModes.includes(itemData.preparation.mode);
// If can't upcast, return early and don't bother calculating available power slots
- if (!canUpcast) {
- data = mergeObject(data, { isPower: true, canUpcast });
+ if (!consumePowerSlot) {
+ mergeObject(data, { isPower: true, consumePowerSlot });
return;
}
@@ -122,7 +129,7 @@ export default class AbilityUseDialog extends Dialog {
const canCast = powerLevels.some(l => l.hasSlots);
// Return merged data
- data = mergeObject(data, { isPower: true, canUpcast, powerLevels });
+ data = mergeObject(data, { isPower: true, consumePowerSlot, powerLevels });
if ( !canCast ) data.errors.push("SW5E.PowerCastNoSlots");
}
diff --git a/module/apps/movement-config.js b/module/apps/movement-config.js
index 3d60a582..a57de39c 100644
--- a/module/apps/movement-config.js
+++ b/module/apps/movement-config.js
@@ -2,21 +2,27 @@
* A simple form to set actor movement speeds
* @implements {BaseEntitySheet}
*/
-export default class MovementConfig extends BaseEntitySheet {
+export default class ActorMovementConfig extends BaseEntitySheet {
/** @override */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
- title: "SW5E.MovementConfig",
classes: ["sw5e"],
template: "systems/sw5e/templates/apps/movement-config.html",
- width: 240,
+ width: 300,
height: "auto"
});
}
/* -------------------------------------------- */
+ /** @override */
+ get title() {
+ return `${game.i18n.localize("SW5E.MovementConfig")}: ${this.entity.name}`;
+ }
+
+ /* -------------------------------------------- */
+
/** @override */
getData(options) {
const data = {
diff --git a/module/apps/senses-config.js b/module/apps/senses-config.js
new file mode 100644
index 00000000..71f99bf1
--- /dev/null
+++ b/module/apps/senses-config.js
@@ -0,0 +1,43 @@
+/**
+ * A simple form to set actor movement speeds
+ * @implements {BaseEntitySheet}
+ */
+export default class ActorSensesConfig extends BaseEntitySheet {
+
+ /** @override */
+ static get defaultOptions() {
+ return mergeObject(super.defaultOptions, {
+ classes: ["sw5e"],
+ template: "systems/sw5e/templates/apps/senses-config.html",
+ width: 300,
+ height: "auto"
+ });
+ }
+
+ /* -------------------------------------------- */
+
+ /** @override */
+ get title() {
+ return `${game.i18n.localize("SW5E.SensesConfig")}: ${this.entity.name}`;
+ }
+
+ /* -------------------------------------------- */
+
+ /** @override */
+ getData(options) {
+ const senses = this.entity._data.data.attributes?.senses ?? {};
+ const data = {
+ senses: {},
+ special: senses.special ?? "",
+ units: senses.units, movementUnits: CONFIG.SW5E.movementUnits
+ };
+ for ( let [name, label] of Object.entries(CONFIG.SW5E.senses) ) {
+ const v = senses[name];
+ data.senses[name] = {
+ label: game.i18n.localize(label),
+ value: Number.isNumeric(v) ? v.toNearest(0.1) : 0
+ }
+ }
+ return data;
+ }
+}
diff --git a/module/apps/trait-selector.js b/module/apps/trait-selector.js
index b3af60d0..5a9e48be 100644
--- a/module/apps/trait-selector.js
+++ b/module/apps/trait-selector.js
@@ -36,8 +36,8 @@ export default class TraitSelector extends FormApplication {
getData() {
// Get current values
- let attr = getProperty(this.object._data, this.attribute) || {};
- attr.value = attr.value || [];
+ let attr = getProperty(this.object._data, this.attribute);
+ if ( getType(attr) !== "Object" ) attr = {value: [], custom: ""};
// Populate choices
const choices = duplicate(this.options.choices);
@@ -85,4 +85,4 @@ export default class TraitSelector extends FormApplication {
// Update the object
this.object.update(updateData);
}
-}
+}
\ No newline at end of file
diff --git a/module/classFeatures.js b/module/classFeatures.js
index f5bdc02b..7946c252 100644
--- a/module/classFeatures.js
+++ b/module/classFeatures.js
@@ -1,35 +1,4 @@
export const ClassFeatures = {
- "berserker": {
- "archetypes": {
- "addicted-approach": {
- "label": "Addicted Approach",
- "source": "PHB",
- "features": {
- "3": ["Compendium.sw5e.archetypes.PCwepUZqHYlxr4T3", "Compendium.sw5e.classfeatures.efOA0nrvUqKJOOeP", "Compendium.sw5e.classfeatures.nT6AfpQXSZ4IeChO"],
- "6": ["Compendium.sw5e.classfeatures.GbJDWzoTKWL7sEpR"],
- "10": ["Compendium.sw5e.classfeatures.3jqPPd5qJBBnonPw"],
- "14": ["Compendium.sw5e.classfeatures.xzRNHB2M2HdOZzr7"]
- }
- }
- },
- "features": {
- "1": ["Compendium.sw5e.classfeatures.IDt6duVrBzL8euRc", "Compendium.sw5e.classfeatures.rPOLy96fW96N2UPg"],
- "2": ["Compendium.sw5e.classfeatures.DlYiCiG39R0goG9u", "Compendium.sw5e.classfeatures.FbSpxpXm1xONn0na", "Compendium.sw5e.classfeatures.KDiQ8O2evV2Z1YTo", "Compendium.sw5e.classfeatures.Q1JyHnVs9iIEBs91", "Compendium.sw5e.classfeatures.ROdICoWR82v6A2Rf", "Compendium.sw5e.classfeatures.cdCx5Hvq2rYRMzRj", "Compendium.sw5e.classfeatures.dTdbL8dypa6BAdnP", "Compendium.sw5e.classfeatures.h1uDhP1tEOuvjRw6", "Compendium.sw5e.classfeatures.hMiA075EKBBOL2cv", "Compendium.sw5e.classfeatures.sgJdISZMtwv08WPJ", "Compendium.sw5e.classfeatures.v4CZJ8LBMl5PYZCO"],
- "3": ["Compendium.sw5e.classfeatures.kzwSN9SabKgWZZvU"],
- "4": ["Compendium.sw5e.classfeatures.9oyy0MMqEws2qoil"],
- "5": ["Compendium.sw5e.classfeatures.dPWmHiWmpnhHTsgd"],
- "7": ["Compendium.sw5e.classfeatures.Cid5ujSdnooH0vMm", "Compendium.sw5e.classfeatures.WTBhKJgkArQI3Tgv", "Compendium.sw5e.classfeatures.oiT3TJxzRWPKAX9E", "Compendium.sw5e.classfeatures.pMEmIt3NWThbee8k", "Compendium.sw5e.classfeatures.qWV5YogZcpZ3Y3xj"],
- "9": ["Compendium.sw5e.classfeatures.bi8G8H5Ur9B3BAyM"],
- "11": ["Compendium.sw5e.classfeatures.eWbTifdXJvvXT4CV"],
- "13": ["Compendium.sw5e.classfeatures.Hg8zYh1iXL0DGUVq", "Compendium.sw5e.classfeatures.QRnYiJmRk18ekE9v", "Compendium.sw5e.classfeatures.sfEr8ZBFVddlfLeF", "Compendium.sw5e.classfeatures.yGC9VzT840qQWxca"],
- "15": ["Compendium.sw5e.classfeatures.YHPUv9lN3nCapAgP"],
- "18": ["Compendium.sw5e.classfeatures.fFKNqUAWh0ZOhvRc"],
- "20": ["Compendium.sw5e.classfeatures.IWTDawTUf79eWbEV"]
- }
- },
- "consular": {
- "features": {
- "20": ["Compendium.sw5e.classfeatures.gSGeitc98ItAwhfF"]
- }
- }
-};
\ No newline at end of file
+
+};
+
diff --git a/module/config.js b/module/config.js
index 82a36fee..3cf516e6 100644
--- a/module/config.js
+++ b/module/config.js
@@ -8,9 +8,9 @@ SW5E.ASCII = `__________________________________________
_
| |
___| |_ __ _ _ ____ ____ _ _ __ ___
-/ __| __/ _\ | |__\ \ /\ / / _\ | |__/ __|
-\__ \ || (_) | | \ V V / (_) | | \__ \
-|___/\__\__/_|_| \_/\_/ \__/_|_| |___/
+/ __| __/ _\\ | |__\\ \\ /\\ / / _\\ | |__/ __|
+\\__ \\ || (_) | | \\ \V \V / (_) | | \\__ \\
+|___/\\__\\__/_|_| \\_/\\_/ \\__/_|_| |___/
__________________________________________`;
@@ -54,6 +54,20 @@ SW5E.alignments = {
'cd': "SW5E.AlignmentCD"
};
+/* -------------------------------------------- */
+
+/**
+ * An enumeration of item attunement states
+ * @type {{"0": string, "1": string, "2": string}}
+ */
+SW5E.attunements = {
+ 0: "SW5E.AttunementNone",
+ 1: "SW5E.AttunementRequired",
+ 2: "SW5E.AttunementAttuned"
+};
+
+/* -------------------------------------------- */
+
SW5E.weaponProficiencies = {
"sim": "SW5E.WeaponSimpleProficiency",
@@ -433,14 +447,14 @@ SW5E.hitDieTypes = ["d4", "d6", "d8", "d10", "d12", "d20"];
/* -------------------------------------------- */
/**
- * Character senses options
- * @type {Object}
+ * The set of possible sensory perception types which an Actor may have
+ * @type {object}
*/
SW5E.senses = {
- "bs": "SW5E.SenseBS",
- "dv": "SW5E.SenseDV",
- "ts": "SW5E.SenseTS",
- "tr": "SW5E.SenseTR"
+ "blindsight": "SW5E.SenseBlindsight",
+ "darkvision": "SW5E.SenseDarkvision",
+ "tremorsense": "SW5E.SenseTremorsense",
+ "truesight": "SW5E.SenseTruesight"
};
@@ -1140,7 +1154,7 @@ SW5E.characterFlags = {
section: "Feats",
type: Boolean
},
- "remarkableAthlete": {
+ "remarkableAthlete": {
name: "SW5E.FlagsRemarkableAthlete",
hint: "SW5E.FlagsRemarkableAthleteHint",
abilities: ['str','dex','con'],
diff --git a/module/dice.js b/module/dice.js
index 5eff6768..3a28b827 100644
--- a/module/dice.js
+++ b/module/dice.js
@@ -214,7 +214,6 @@ export async function damageRoll({parts, actor, data, event={}, rollMode=null, t
messageData.speaker = speaker || ChatMessage.getSpeaker();
const messageOptions = {rollMode: rollMode || game.settings.get("core", "rollMode")};
parts = parts.concat(["@bonus"]);
- fastForward = fastForward ?? (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
// Define inner roll function
const _roll = function(parts, crit, form) {
@@ -236,6 +235,7 @@ export async function damageRoll({parts, actor, data, event={}, rollMode=null, t
roll.terms[0].alter(1, criticalBonusDice);
roll._formula = roll.formula;
}
+ roll.dice.forEach(d => d.options.critical = true);
messageData.flavor += ` (${game.i18n.localize("SW5E.Critical")})`;
if ( "flags.sw5e.roll" in messageData ) messageData["flags.sw5e.roll"].critical = true;
}
@@ -251,7 +251,7 @@ export async function damageRoll({parts, actor, data, event={}, rollMode=null, t
};
// Create the Roll instance
- const roll = fastForward ? _roll(parts, critical || event.altKey) : await _damageRollDialog({
+ const roll = fastForward ? _roll(parts, critical) : await _damageRollDialog({
template, title, parts, data, allowCritical, rollMode: messageOptions.rollMode, dialogOptions, roll: _roll
});
diff --git a/module/macros.js b/module/macros.js
index 8b9c91b2..96bb3419 100644
--- a/module/macros.js
+++ b/module/macros.js
@@ -55,6 +55,5 @@ export function rollItemMacro(itemName) {
const item = items[0];
// Trigger the item roll
- if ( item.data.type === "power" ) return actor.usePower(item);
return item.roll();
}
diff --git a/module/migration.js b/module/migration.js
index 32b2fa74..2e19c076 100644
--- a/module/migration.js
+++ b/module/migration.js
@@ -3,7 +3,7 @@
* @return {Promise} A Promise which resolves once the migration is completed
*/
export const migrateWorld = async function() {
- ui.notifications.info(`Applying SW5E System Migration for version ${game.system.data.version}. Please be patient and do not close your game or shut down your server.`, {permanent: true});
+ ui.notifications.info(`Applying SW5e System Migration for version ${game.system.data.version}. Please be patient and do not close your game or shut down your server.`, {permanent: true});
// Migrate World Actors
for ( let a of game.actors.entities ) {
@@ -56,7 +56,7 @@ export const migrateWorld = async function() {
// Set the migration as complete
game.settings.set("sw5e", "systemMigrationVersion", game.system.data.version);
- ui.notifications.info(`SW5E System Migration to version ${game.system.data.version} completed!`, {permanent: true});
+ ui.notifications.info(`SW5e System Migration to version ${game.system.data.version} completed!`, {permanent: true});
};
/* -------------------------------------------- */
@@ -129,6 +129,7 @@ export const migrateActorData = function(actor) {
// Actor Data Updates
_migrateActorBonuses(actor, updateData);
_migrateActorMovement(actor, updateData);
+ _migrateActorSenses(actor, updateData);
// Migrate Owned Items
if ( !actor.items ) return updateData;
@@ -191,6 +192,7 @@ function cleanActorData(actorData) {
*/
export const migrateItemData = function(item) {
const updateData = {};
+ _migrateItemAttunement(item, updateData);
return updateData;
};
@@ -242,19 +244,72 @@ function _migrateActorBonuses(actor, updateData) {
/* -------------------------------------------- */
/**
- * Migrate the actor bonuses object
+ * Migrate the actor speed string to movement object
* @private
*/
function _migrateActorMovement(actor, updateData) {
- if ( actor.data.attributes?.movement?.walk !== undefined ) return;
- const s = (actor.data.attributes?.speed?.value || "").split(" ");
+ const ad = actor.data;
+ const old = ad?.attributes?.speed?.value;
+ if ( old === undefined ) return;
+ const s = (old || "").split(" ");
if ( s.length > 0 ) updateData["data.attributes.movement.walk"] = Number.isNumeric(s[0]) ? parseInt(s[0]) : null;
+ updateData["data.attributes.-=speed"] = null;
+ return updateData
+}
+
+/* -------------------------------------------- */
+
+/**
+ * Migrate the actor traits.senses string to attributes.senses object
+ * @private
+ */
+function _migrateActorSenses(actor, updateData) {
+ const ad = actor.data;
+ if ( ad?.traits?.senses === undefined ) return;
+ const original = ad.traits.senses || "";
+
+ // Try to match old senses with the format like "Darkvision 60 ft, Blindsight 30 ft"
+ const pattern = /([A-z]+)\s?([0-9]+)\s?([A-z]+)?/
+ let wasMatched = false;
+
+ // Match each comma-separated term
+ for ( let s of original.split(",") ) {
+ s = s.trim();
+ const match = s.match(pattern);
+ if ( !match ) continue;
+ const type = match[1].toLowerCase();
+ if ( type in CONFIG.SW5E.senses ) {
+ updateData[`data.attributes.senses.${type}`] = Number(match[2]).toNearest(0.5);
+ wasMatched = true;
+ }
+ }
+
+ // If nothing was matched, but there was an old string - put the whole thing in "special"
+ if ( !wasMatched && !!original ) {
+ updateData["data.attributes.senses.special"] = original;
+ }
+
+ // Remove the old traits.senses string once the migration is complete
+ updateData["data.traits.-=senses"] = null;
+ return updateData;
+}
+
+/* -------------------------------------------- */
+
+/**
+ * Delete the old data.attuned boolean
+ * @private
+ */
+function _migrateItemAttunement(item, updateData) {
+ if ( item.data.attuned === undefined ) return;
+ updateData["data.attunement"] = 0;
+ updateData["data.-=attuned"] = null;
+ 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/pixi/ability-template.js b/module/pixi/ability-template.js
index a7bf6e53..e594319d 100644
--- a/module/pixi/ability-template.js
+++ b/module/pixi/ability-template.js
@@ -45,7 +45,10 @@ export default class AbilityTemplate extends MeasuredTemplate {
}
// Return the template constructed from the item data
- return new this(templateData);
+ const template = new this(templateData);
+ template.item = item;
+ template.actorSheet = item.actor?.sheet || null;
+ return template;
}
/* -------------------------------------------- */
@@ -55,9 +58,16 @@ export default class AbilityTemplate extends MeasuredTemplate {
*/
drawPreview() {
const initialLayer = canvas.activeLayer;
+
+ // Draw the template and switch to the template layer
this.draw();
this.layer.activate();
this.layer.preview.addChild(this);
+
+ // Hide the sheet that originated the preview
+ if ( this.actorSheet ) this.actorSheet.minimize();
+
+ // Activate interactivity
this.activatePreviewListeners(initialLayer);
}
@@ -92,6 +102,7 @@ export default class AbilityTemplate extends MeasuredTemplate {
canvas.app.view.oncontextmenu = null;
canvas.app.view.onwheel = null;
initialLayer.activate();
+ this.actorSheet.maximize();
};
// Confirm the workflow (left-click)
diff --git a/module/templates.js b/module/templates.js
index 450eaaae..f2104023 100644
--- a/module/templates.js
+++ b/module/templates.js
@@ -14,9 +14,11 @@ export const preloadHandlebarsTemplates = async function() {
"systems/sw5e/templates/actors/oldActor/parts/actor-inventory.html",
"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/newActor/parts/swalt-biography.html",
"systems/sw5e/templates/actors/newActor/parts/swalt-core.html",
+ "systems/sw5e/templates/actors/newActor/parts/swalt-active-effects.html",
"systems/sw5e/templates/actors/newActor/parts/swalt-features.html",
"systems/sw5e/templates/actors/newActor/parts/swalt-inventory.html",
"systems/sw5e/templates/actors/newActor/parts/swalt-notes.html",
diff --git a/packs/Icons/Archetypes/Archaeologist Pursuit.webp b/packs/Icons/Archetypes/Archaeologist Pursuit.webp
new file mode 100644
index 0000000000000000000000000000000000000000..77e061e9a1d3ad71b3e3fe9f3f1ed82b2f38e9af
GIT binary patch
literal 12356
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
literal 0
HcmV?d00001
diff --git a/packs/Icons/Archetypes/Vonil-Ishu Form.webp b/packs/Icons/Archetypes/Vonil-Ishu Form.webp
new file mode 100644
index 0000000000000000000000000000000000000000..287d37748dc497d5a462753a9398967e59cb5775
GIT binary patch
literal 12306
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%0AqSxTtm78$M5$r!&80i@xny)R8Z#Yho^V%gOD
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
O*sx(^4I4J3palQ}kbdI;
literal 0
HcmV?d00001
diff --git a/packs/Icons/Armor/PHB/Small Physical Shield.webp b/packs/Icons/Armor/PHB/Light Physical Shield.webp
similarity index 100%
rename from packs/Icons/Armor/PHB/Small Physical Shield.webp
rename to packs/Icons/Armor/PHB/Light Physical Shield.webp
diff --git a/packs/Icons/Armor/PHB/Small Shield Generator.webp b/packs/Icons/Armor/PHB/Light Shield Generator.webp
similarity index 100%
rename from packs/Icons/Armor/PHB/Small Shield Generator.webp
rename to packs/Icons/Armor/PHB/Light Shield Generator.webp
diff --git a/packs/Icons/Armor/WH/Laminanium Assault Armor.webp b/packs/Icons/Armor/WH/Laminanium Assault.webp
similarity index 100%
rename from packs/Icons/Armor/WH/Laminanium Assault Armor.webp
rename to packs/Icons/Armor/WH/Laminanium Assault.webp
diff --git a/packs/Icons/Armor/WH/Neutronium Mesh Armor.webp b/packs/Icons/Armor/WH/Neutronium Mesh.webp
similarity index 100%
rename from packs/Icons/Armor/WH/Neutronium Mesh Armor.webp
rename to packs/Icons/Armor/WH/Neutronium Mesh.webp
diff --git a/packs/Icons/Armor/WH/Plastoid Composite Armor.webp b/packs/Icons/Armor/WH/Plastoid Composite.webp
similarity index 100%
rename from packs/Icons/Armor/WH/Plastoid Composite Armor.webp
rename to packs/Icons/Armor/WH/Plastoid Composite.webp
diff --git a/packs/Icons/Lightsaber Forms/Vonil-Ishu Form.webp b/packs/Icons/Lightsaber Forms/Vonil-Ishu Form.webp
new file mode 100644
index 0000000000000000000000000000000000000000..17f245de7fa3c1cb2515e4f6d71dc708eb89f90f
GIT binary patch
literal 6796
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