Merge branch 'master' into character-sheet-importer
24
.github/workflows/main.yml
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
name: Gulp build and commit updated stylesheets
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, Develop]
|
||||
pull_request:
|
||||
branches: [master, Develop]
|
||||
|
||||
jobs:
|
||||
gulp-build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
|
||||
- name: Compile with Gulp
|
||||
uses: elstudio/actions-js-build/build@v2
|
||||
|
||||
- name: Commit changes
|
||||
uses: elstudio/actions-js-build/commit@v3
|
||||
with:
|
||||
commitMessage: Regenerate css
|
|
@ -1,5 +1,7 @@
|
|||
# Foundry Virtual Tabletop - SW5e Game System
|
||||
|
||||
This unofficial implementation of the SW5e system for Foundry VTT is made by fans for fans and is not associated with SW5e, Disney, Wizards of the Coast, or their partners in any way.
|
||||
|
||||
This game system for [Foundry Virtual Tabletop](http://foundryvtt.com) provides character sheet and game system
|
||||
support for the SW5E roleplaying game.
|
||||
|
||||
|
|
BIN
fonts/EngliBesh-KG3W.ttf
Normal file
32
gulpfile.js
|
@ -1,31 +1,28 @@
|
|||
const gulp = require('gulp');
|
||||
const less = require('gulp-less');
|
||||
const gulp = require("gulp");
|
||||
const less = require("gulp-less");
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* Compile LESS
|
||||
/* ----------------------------------------- */
|
||||
|
||||
const SW5E_LESS = ["less/**/*.less"];
|
||||
|
||||
function compileLESS() {
|
||||
return gulp.src("less/original/sw5e.less")
|
||||
.pipe(less())
|
||||
.pipe(gulp.dest("./"))
|
||||
return gulp.src("less/original/sw5e.less").pipe(less()).pipe(gulp.dest("./"));
|
||||
}
|
||||
|
||||
function compileGlobalLess() {
|
||||
return gulp.src("less/update/sw5e-global.less")
|
||||
.pipe(less())
|
||||
.pipe(gulp.dest("./"))
|
||||
return gulp.src("less/update/sw5e-global.less").pipe(less()).pipe(gulp.dest("./"));
|
||||
}
|
||||
|
||||
function compileLightLess() {
|
||||
return gulp.src("less/update/sw5e-light.less")
|
||||
.pipe(less())
|
||||
.pipe(gulp.dest("./"))
|
||||
return gulp.src("less/update/sw5e-light.less").pipe(less()).pipe(gulp.dest("./"));
|
||||
}
|
||||
|
||||
function compileDarkLess() {
|
||||
return gulp.src("less/update/sw5e-dark.less")
|
||||
.pipe(less())
|
||||
.pipe(gulp.dest("./"))
|
||||
return gulp.src("less/update/sw5e-dark.less").pipe(less()).pipe(gulp.dest("./"));
|
||||
}
|
||||
|
||||
const css = gulp.series(compileLESS, compileGlobalLess, compileLightLess, compileDarkLess);
|
||||
|
||||
/* ----------------------------------------- */
|
||||
|
@ -40,8 +37,5 @@ function watchUpdates() {
|
|||
/* Export Tasks
|
||||
/* ----------------------------------------- */
|
||||
|
||||
exports.default = gulp.series(
|
||||
gulp.parallel(css),
|
||||
watchUpdates
|
||||
);
|
||||
exports.css = css;
|
||||
exports.default = css;
|
||||
gulp.parallel(css), (exports.watch = gulp.series(gulp.parallel(css), watchUpdates));
|
||||
|
|
35
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,7 +662,14 @@
|
|||
"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",
|
||||
|
@ -725,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",
|
||||
|
@ -941,4 +954,4 @@
|
|||
"SETTINGS.SWColorN": "Display Theme",
|
||||
"SETTINGS.SWColorLight": "Light Theme",
|
||||
"SETTINGS.SWColorDark": "Dark Theme"
|
||||
}
|
||||
}
|
|
@ -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,8 +487,8 @@
|
|||
overflow: hidden;
|
||||
&:last-child { border-right: none; }
|
||||
&.item-action {flex: 0 0 100px}
|
||||
&.attunement {flex: 0 0 24px}
|
||||
}
|
||||
|
||||
.item-weight {
|
||||
flex: 0 0 60px;
|
||||
border-left: 1px solid @colorFaint;
|
||||
|
@ -491,6 +508,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 +590,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 +623,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 +675,6 @@
|
|||
padding-right: 8px;
|
||||
margin-bottom: 4px;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
@ -398,11 +426,6 @@
|
|||
border: none;
|
||||
margin-right: 5px;
|
||||
}
|
||||
h4 {
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -419,32 +442,13 @@
|
|||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
.item-name {
|
||||
h3 {
|
||||
padding-left: 5px;
|
||||
//.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------------------------- */
|
||||
|
@ -482,4 +486,41 @@
|
|||
height: 24px;
|
||||
margin: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* HUD
|
||||
/* ----------------------------------------- */
|
||||
|
||||
.placeable-hud .control-icon {
|
||||
box-sizing: content-box;
|
||||
width: 40px;
|
||||
flex: 0 0 40px;
|
||||
margin: 8px 0;
|
||||
font-size: 28px;
|
||||
line-height: 40px;
|
||||
text-align: center;
|
||||
color: #FBF4F4;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
box-shadow: 0 0 15px #000;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
pointer-events: all;
|
||||
}
|
||||
#token-hud .status-effects {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
left: 50px;
|
||||
top: 0;
|
||||
display: grid;
|
||||
padding: 3px;
|
||||
box-sizing: content-box;
|
||||
width: 100px;
|
||||
color: #FBF4F4;
|
||||
grid-template-columns: 25px 25px 25px 25px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
box-shadow: 0 0 15px #000;
|
||||
border: 1px solid #333;
|
||||
border-radius: 4px;
|
||||
pointer-events: all;
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
/* Chat Cards
|
||||
/* ----------------------------------------- */
|
||||
|
||||
.sw5e.chat-card {
|
||||
.sw5e.chat-card,
|
||||
.midi-qol-item-card {
|
||||
font-style: normal;
|
||||
font-size: 12px;
|
||||
|
||||
|
@ -22,7 +23,7 @@
|
|||
flex: 1;
|
||||
margin: 0;
|
||||
line-height: 36px;
|
||||
.bungeeInline();
|
||||
.engli-Besh();
|
||||
color: @colorOlive;
|
||||
&:hover {
|
||||
color: #111;
|
||||
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -31,11 +31,4 @@
|
|||
.summary {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.powercasting-ability {
|
||||
label {
|
||||
flex: none;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -19,15 +19,15 @@
|
|||
font-size: @font-size;
|
||||
font-weight: 400;
|
||||
}
|
||||
/* bungee-inline-regular - latin */
|
||||
/* engli-besh */
|
||||
@font-face {
|
||||
font-family: 'Bungee Inline';
|
||||
font-family: 'Engli-Besh';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('./fonts/BungeeInline.ttf');
|
||||
src: url('./fonts/EngliBesh-KG3W.ttf');
|
||||
}
|
||||
.bungeeInline {
|
||||
font-family: 'Bungee Inline';
|
||||
.engli-Besh {
|
||||
font-family: 'Engli-Besh';
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
@blockquoteShadow: 0 0 20px rgba(@colorBlue, 0.8);
|
||||
|
||||
//forms
|
||||
@inputBackgroundColor: white;
|
||||
@inputBackgroundColor: @colorGray;
|
||||
@inputBorderNormal: @colorLightGray;
|
||||
@inputBorderHover: @colorGray;
|
||||
@inputBorderFocus: @colorRed;
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
.dropShadow1();
|
||||
}
|
||||
.sw5e.sheet.actor.character {
|
||||
min-width: 780px;
|
||||
min-width: 850px;
|
||||
min-height: 720px;
|
||||
}
|
||||
.sw5e.sheet .window-content {
|
||||
|
@ -54,7 +54,7 @@
|
|||
grid-template-rows: 1fr 26px auto;
|
||||
grid-template-columns: 128px 1fr;
|
||||
column-gap: 8px;
|
||||
row-gap: 8px;
|
||||
grid-row-gap: 8px;
|
||||
|
||||
img {
|
||||
grid-column-start: 1;
|
||||
|
@ -683,7 +683,7 @@
|
|||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-gap: 16px;
|
||||
row-gap: 8px;
|
||||
grid-row-gap: 8px;
|
||||
|
||||
input,
|
||||
select {
|
||||
|
@ -715,7 +715,7 @@
|
|||
}
|
||||
|
||||
.languages {
|
||||
grid-column-end: span 2;
|
||||
grid-column-end: span 1;
|
||||
label {
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
|
@ -745,7 +745,7 @@
|
|||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-gap: 4px;
|
||||
row-gap: 4px;
|
||||
grid-row-gap: 4px;
|
||||
|
||||
strong {
|
||||
font-size: 13px;
|
||||
|
@ -994,7 +994,7 @@
|
|||
}
|
||||
&.limited {
|
||||
grid-template-rows: 144px auto;
|
||||
row-gap: 8px;
|
||||
grid-row-gap: 8px;
|
||||
header {
|
||||
grid-template-rows: 1fr;
|
||||
}
|
||||
|
|
|
@ -54,7 +54,8 @@
|
|||
color: @colorBlue;
|
||||
}
|
||||
|
||||
.sw5e.chat-card {
|
||||
.sw5e.chat-card,
|
||||
.midi-qol-item-card {
|
||||
.card-header {
|
||||
padding: 0;
|
||||
border: none;
|
||||
|
|
|
@ -31,7 +31,8 @@
|
|||
color: @chatNotificationColor;
|
||||
}
|
||||
|
||||
.sw5e.chat-card {
|
||||
.sw5e.chat-card,
|
||||
.midi-qol-item-card {
|
||||
|
||||
.card-header {
|
||||
h3 {
|
||||
|
@ -94,7 +95,8 @@
|
|||
}
|
||||
#chat-controls {
|
||||
.roll-type-select {
|
||||
background: @inputBackgroundColor;
|
||||
background: #4f4f4f;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
label {
|
||||
color: @bodyFontColor;
|
||||
|
@ -102,7 +104,7 @@
|
|||
|
||||
}
|
||||
#chat-form textarea {
|
||||
background: @inputBackgroundColor;
|
||||
background: #4f4f4f;
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -51,7 +51,8 @@
|
|||
|
||||
}
|
||||
|
||||
.sw5e.chat-card {
|
||||
.sw5e.chat-card,
|
||||
.midi-qol-item-card {
|
||||
font-size: 13px;
|
||||
|
||||
.card-header {
|
||||
|
@ -168,6 +169,10 @@
|
|||
}
|
||||
}
|
||||
#chat-controls {
|
||||
&.roll-type-select {
|
||||
background: #4f4f4f;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
padding-top: 4px;
|
||||
label {
|
||||
color: @colorBlack;
|
||||
|
@ -175,7 +180,7 @@
|
|||
|
||||
}
|
||||
#chat-form textarea {
|
||||
background: white;
|
||||
background: #4f4f4f;
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
|
|
|
@ -48,6 +48,12 @@
|
|||
font-weight: 400;
|
||||
src: url('./fonts/Aurebesh.ttf');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Engli-Besh';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('./fonts/EngliBesh-KG3W.ttf');
|
||||
}
|
||||
@import "_variables.less";
|
||||
|
||||
html {
|
||||
|
@ -77,6 +83,9 @@ html {
|
|||
|
||||
body {
|
||||
.openSans(13px, 400);
|
||||
background-image: url('./ui/SW5e-logo.svg');
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
h1 {
|
||||
|
|
|
@ -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';
|
||||
|
||||
/**
|
||||
|
@ -94,8 +92,14 @@ export default class Actor5e extends Actor {
|
|||
init.total = init.mod + init.prof + init.bonus;
|
||||
|
||||
// Prepare power-casting data
|
||||
this._computePowercastingDC(this.data);
|
||||
data.attributes.powerdc = data.attributes.powercasting ? data.abilities[data.attributes.powercasting].dc : 10;
|
||||
this._computePowercastingProgression(this.data);
|
||||
|
||||
// Compute owned item attributes which depend on prepared Actor data
|
||||
this.items.forEach(item => {
|
||||
item.getSaveDC();
|
||||
item.getAttackToHit();
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -163,14 +167,17 @@ 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);
|
||||
}
|
||||
|
||||
// Load item data for all identified features
|
||||
const features = await Promise.all(ids.map(id => fromUuid(id)));
|
||||
const features = [];
|
||||
for ( let id of ids ) {
|
||||
features.push(await fromUuid(id));
|
||||
}
|
||||
|
||||
// Class powers should always be prepared
|
||||
for ( const feature of features ) {
|
||||
|
@ -207,7 +214,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
|
||||
}
|
||||
|
@ -314,19 +321,22 @@ export default class Actor5e extends Actor {
|
|||
const joat = flags.jackOfAllTrades;
|
||||
const observant = flags.observantFeat;
|
||||
const skillBonus = Number.isNumeric(bonuses.skill) ? parseInt(bonuses.skill) : 0;
|
||||
let round = Math.floor;
|
||||
for (let [id, skl] of Object.entries(data.skills)) {
|
||||
skl.value = parseFloat(skl.value || 0);
|
||||
skl.value = Math.clamped(Number(skl.value).toNearest(0.5), 0, 2) ?? 0;
|
||||
let round = Math.floor;
|
||||
|
||||
// Apply Remarkable Athlete or Jack of all Trades
|
||||
let multi = skl.value;
|
||||
if ( athlete && (skl.value === 0) && feats.remarkableAthlete.abilities.includes(skl.ability) ) {
|
||||
multi = 0.5;
|
||||
// Remarkable
|
||||
if ( athlete && (skl.value < 0.5) && feats.remarkableAthlete.abilities.includes(skl.ability) ) {
|
||||
skl.value = 0.5;
|
||||
round = Math.ceil;
|
||||
}
|
||||
if ( joat && (skl.value === 0 ) ) multi = 0.5;
|
||||
|
||||
// Retain the maximum skill proficiency when skill proficiencies are merged
|
||||
// Jack of All Trades
|
||||
if ( joat && (skl.value < 0.5) ) {
|
||||
skl.value = 0.5;
|
||||
}
|
||||
|
||||
// Polymorph Skill Proficiencies
|
||||
if ( originalSkills ) {
|
||||
skl.value = Math.max(skl.value, originalSkills[id].value);
|
||||
}
|
||||
|
@ -334,7 +344,7 @@ export default class Actor5e extends Actor {
|
|||
// Compute modifier
|
||||
skl.bonus = checkBonus + skillBonus;
|
||||
skl.mod = data.abilities[skl.ability].mod;
|
||||
skl.prof = round(multi * data.attributes.prof);
|
||||
skl.prof = round(skl.value * data.attributes.prof);
|
||||
skl.total = skl.mod + skl.prof + skl.bonus;
|
||||
|
||||
// Compute passive bonus
|
||||
|
@ -345,31 +355,6 @@ export default class Actor5e extends Actor {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Compute the powercasting DC for all item abilities which use power DC scaling
|
||||
* @param {object} actorData The actor data being prepared
|
||||
* @private
|
||||
*/
|
||||
_computePowercastingDC(actorData) {
|
||||
|
||||
// Compute the powercasting DC
|
||||
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});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Prepare data related to the power-casting capabilities of the Actor
|
||||
* @private
|
||||
|
@ -418,7 +403,7 @@ export default class Actor5e extends Actor {
|
|||
progression.slot = Math.ceil(caster.data.levels / denom);
|
||||
}
|
||||
|
||||
// EXCEPTION: NPC with an explicit powercaster level
|
||||
// EXCEPTION: NPC with an explicit power-caster level
|
||||
if (isNPC && actorData.data.details.powerLevel) {
|
||||
progression.slot = actorData.data.details.powerLevel;
|
||||
}
|
||||
|
@ -429,9 +414,9 @@ export default class Actor5e extends Actor {
|
|||
for ( let [n, lvl] of Object.entries(powers) ) {
|
||||
let i = parseInt(n.slice(-1));
|
||||
if ( Number.isNaN(i) ) continue;
|
||||
if ( Number.isNumeric(lvl.override) ) lvl.max = Math.max(parseInt(lvl.override), 1);
|
||||
if ( Number.isNumeric(lvl.override) ) lvl.max = Math.max(parseInt(lvl.override), 0);
|
||||
else lvl.max = slots[i-1] || 0;
|
||||
lvl.value = Math.min(parseInt(lvl.value), lvl.max);
|
||||
lvl.value = parseInt(lvl.value);
|
||||
}
|
||||
|
||||
// Determine the Actor's pact magic level (if any)
|
||||
|
@ -473,8 +458,8 @@ export default class Actor5e extends Actor {
|
|||
return weight + (q * w);
|
||||
}, 0);
|
||||
|
||||
// [Optional] add Currency Weight
|
||||
if ( game.settings.get("sw5e", "currencyWeight") ) {
|
||||
// [Optional] add Currency Weight (for non-transformed actors)
|
||||
if ( game.settings.get("sw5e", "currencyWeight") && actorData.data.currency ) {
|
||||
const currency = actorData.data.currency;
|
||||
const numCoins = Object.values(currency).reduce((val, denom) => val += Math.max(denom, 0), 0);
|
||||
weight += numCoins / CONFIG.SW5E.encumbrance.currencyPerWeight;
|
||||
|
@ -546,20 +531,82 @@ 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":
|
||||
if ( getProperty(itemData, "data.equipped") === undefined ) {
|
||||
initial["data.equipped"] = isNPC; // NPCs automatically equip weapons
|
||||
}
|
||||
if ( getProperty(itemData, "data.proficient") === undefined ) {
|
||||
if ( isNPC ) {
|
||||
initial["data.proficient"] = true; // NPCs automatically have equipment proficiency
|
||||
} else {
|
||||
const weaponProf = {
|
||||
"natural": true,
|
||||
"simpleVW": "sim",
|
||||
"simpleB": "sim",
|
||||
"simpleLW": "sim",
|
||||
"martialVW": "mar",
|
||||
"martialB": "mar",
|
||||
"martialLW": "mar"
|
||||
}[itemData.data?.weaponType]; // Player characters check proficiency
|
||||
const actorWeaponProfs = this.data.data.traits?.weaponProf?.value || [];
|
||||
const hasWeaponProf = (weaponProf === true) || actorWeaponProfs.includes(weaponProf);
|
||||
initial["data.proficient"] = hasWeaponProf;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "equipment":
|
||||
if ( getProperty(itemData, "data.equipped") === undefined ) {
|
||||
initial["data.equipped"] = isNPC; // NPCs automatically equip equipment
|
||||
}
|
||||
if ( getProperty(itemData, "data.proficient") === undefined ) {
|
||||
if ( isNPC ) {
|
||||
initial["data.proficient"] = true; // NPCs automatically have equipment proficiency
|
||||
} else {
|
||||
const armorProf = {
|
||||
"natural": true,
|
||||
"clothing": true,
|
||||
"light": "lgt",
|
||||
"medium": "med",
|
||||
"heavy": "hvy",
|
||||
"shield": "shl"
|
||||
}[itemData.data?.armor?.type]; // Player characters check proficiency
|
||||
const actorArmorProfs = this.data.data.traits?.armorProf?.value || [];
|
||||
const hasEquipmentProf = (armorProf === true) || actorArmorProfs.includes(armorProf);
|
||||
initial["data.proficient"] = hasEquipmentProf;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "power":
|
||||
initial["data.prepared"] = true; // automatically prepare powers for everyone
|
||||
break;
|
||||
}
|
||||
mergeObject(itemData, initial);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Gameplay Mechanics */
|
||||
|
@ -600,77 +647,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 +975,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;
|
||||
}
|
||||
|
@ -1130,8 +1116,7 @@ export default class Actor5e extends Actor {
|
|||
|
||||
// Recover power slots
|
||||
for ( let [k, v] of Object.entries(data.powers) ) {
|
||||
if ( !v.max && !v.override ) continue;
|
||||
updateData[`data.powers.${k}.value`] = v.override || v.max;
|
||||
updateData[`data.powers.${k}.value`] = Number.isNumeric(v.override) ? v.override : (v.max ?? 0);
|
||||
}
|
||||
|
||||
// Recover pact slots.
|
||||
|
@ -1238,10 +1223,10 @@ export default class Actor5e extends Actor {
|
|||
}
|
||||
|
||||
// Get the original Actor data and the new source data
|
||||
const o = duplicate(this.data);
|
||||
const o = duplicate(this.toJSON());
|
||||
o.flags.sw5e = o.flags.sw5e || {};
|
||||
o.flags.sw5e.transformOptions = {mergeSkills, mergeSaves};
|
||||
const source = duplicate(target.data);
|
||||
const source = duplicate(target.toJSON());
|
||||
|
||||
// Prepare new data to merge from the source
|
||||
const d = {
|
||||
|
@ -1249,6 +1234,7 @@ export default class Actor5e extends Actor {
|
|||
name: `${o.name} (${source.name})`, // Append the new shape to your old name
|
||||
data: source.data, // Get the data model of your new form
|
||||
items: source.items, // Get the items of your new form
|
||||
effects: o.effects.concat(source.effects), // Combine active effects from both forms
|
||||
token: source.token, // New token configuration
|
||||
img: source.img, // New appearance
|
||||
permission: o.permission, // Use the original actor permissions
|
||||
|
@ -1271,7 +1257,7 @@ export default class Actor5e extends Actor {
|
|||
// Handle wildcard
|
||||
if ( source.token.randomImg ) {
|
||||
const images = await target.getTokenImages();
|
||||
d.token.img = images[0];
|
||||
d.token.img = images[Math.floor(Math.random() * images.length)];
|
||||
}
|
||||
|
||||
// Keep Token configurations
|
||||
|
@ -1355,7 +1341,7 @@ export default class Actor5e extends Actor {
|
|||
newTokenData.actorId = newActor.id;
|
||||
return newTokenData;
|
||||
});
|
||||
return canvas.scene.updateEmbeddedEntity("Token", updates);
|
||||
return canvas.scene?.updateEmbeddedEntity("Token", updates);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -1437,4 +1423,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();
|
||||
}
|
||||
}
|
||||
|
|
860
module/actor/sheets/newSheet/base.js
Normal file
|
@ -0,0 +1,860 @@
|
|||
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 .group-list",
|
||||
".features .group-list",
|
||||
".powerbook .group-list",
|
||||
".effects .effects-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 The Actor data being prepared.
|
||||
* @param {boolean} [largestPrimary=false] Show the largest movement speed as "primary", otherwise show "walk"
|
||||
* @returns {{primary: string, special: string}}
|
||||
* @private
|
||||
*/
|
||||
_getMovementSpeed(actorData, largestPrimary=false) {
|
||||
const movement = actorData.data.attributes.movement || {};
|
||||
|
||||
// Prepare an array of available movement speeds
|
||||
let speeds = [
|
||||
[movement.burrow, `${game.i18n.localize("SW5E.MovementBurrow")} ${movement.burrow}`],
|
||||
[movement.climb, `${game.i18n.localize("SW5E.MovementClimb")} ${movement.climb}`],
|
||||
[movement.fly, `${game.i18n.localize("SW5E.MovementFly")} ${movement.fly}` + (movement.hover ? ` (${game.i18n.localize("SW5E.MovementHover")})` : "")],
|
||||
[movement.swim, `${game.i18n.localize("SW5E.MovementSwim")} ${movement.swim}`]
|
||||
]
|
||||
if ( largestPrimary ) {
|
||||
speeds.push([movement.walk, `${game.i18n.localize("SW5E.MovementWalk")} ${movement.walk}`]);
|
||||
}
|
||||
|
||||
// Filter and sort speeds on their values
|
||||
speeds = speeds.filter(s => !!s[0]).sort((a, b) => b[0] - a[0]);
|
||||
|
||||
// Case 1: Largest as primary
|
||||
if ( largestPrimary ) {
|
||||
let primary = speeds.shift();
|
||||
return {
|
||||
primary: `${primary ? primary[1] : "0"} ${movement.units}`,
|
||||
special: speeds.map(s => s[1]).join(", ")
|
||||
}
|
||||
}
|
||||
|
||||
// Case 2: Walk as primary
|
||||
else {
|
||||
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: '<i class="far fa-circle"></i>',
|
||||
0.5: '<i class="fas fa-adjust"></i>',
|
||||
1: '<i class="fas fa-check"></i>',
|
||||
2: '<i class="fas fa-check-double"></i>'
|
||||
};
|
||||
return icons[level] || icons[0];
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* 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: '<i class="fas fa-check"></i>',
|
||||
label: game.i18n.localize('SW5E.PolymorphAcceptSettings'),
|
||||
callback: html => this.actor.transformInto(sourceActor, rememberOptions(html))
|
||||
},
|
||||
wildshape: {
|
||||
icon: '<i class="fas fa-paw"></i>',
|
||||
label: game.i18n.localize('SW5E.PolymorphWildShape'),
|
||||
callback: html => this.actor.transformInto(sourceActor, {
|
||||
keepBio: true,
|
||||
keepClass: true,
|
||||
keepMental: true,
|
||||
mergeSaves: true,
|
||||
mergeSkills: true,
|
||||
transformTokens: rememberOptions(html).transformTokens
|
||||
})
|
||||
},
|
||||
polymorph: {
|
||||
icon: '<i class="fas fa-pastafarianism"></i>',
|
||||
label: game.i18n.localize('SW5E.Polymorph'),
|
||||
callback: html => this.actor.transformInto(sourceActor, {
|
||||
transformTokens: rememberOptions(html).transformTokens
|
||||
})
|
||||
},
|
||||
cancel: {
|
||||
icon: '<i class="fas fa-times"></i>',
|
||||
label: game.i18n.localize('Cancel')
|
||||
}
|
||||
}
|
||||
}, {
|
||||
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;
|
||||
}
|
||||
|
||||
// Ignore certain statuses
|
||||
if ( itemData.data ) {
|
||||
["attunement", "equipped", "proficient", "prepared"].forEach(k => delete itemData.data[k]);
|
||||
}
|
||||
|
||||
// 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 = $(`<div class="item-summary">${chatData.description.value}</div>`);
|
||||
let props = $(`<div class="item-properties"></div>`);
|
||||
chatData.properties.forEach(p => props.append(`<span class="tag">${p}</span>`));
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import ActorSheet5e from "../base.js";
|
||||
import ActorSheet5e from "./base.js";
|
||||
import Actor5e from "../../entity.js";
|
||||
|
||||
/**
|
||||
|
@ -57,6 +57,9 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
|
|||
// Experience Tracking
|
||||
sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking");
|
||||
sheetData["classLabels"] = this.actor.itemTypes.class.map(c => c.name).join(", ");
|
||||
sheetData["multiclassLabels"] = this.actor.itemTypes.class.map(c => {
|
||||
return [c.data.data.archetype, c.name, c.data.data.levels].filterJoin(' ')
|
||||
}).join(', ');
|
||||
|
||||
// Return data for rendering
|
||||
return sheetData;
|
||||
|
@ -79,13 +82,25 @@ 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;
|
||||
item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
|
||||
item.attunement = {
|
||||
[CONFIG.SW5E.attunementTypes.REQUIRED]: {
|
||||
icon: "fa-sun",
|
||||
cls: "not-attuned",
|
||||
title: "SW5E.AttunementRequired"
|
||||
},
|
||||
[CONFIG.SW5E.attunementTypes.ATTUNED]: {
|
||||
icon: "fa-sun",
|
||||
cls: "attuned",
|
||||
title: "SW5E.AttunementAttuned"
|
||||
}
|
||||
}[item.data.attunement];
|
||||
|
||||
// Item usage
|
||||
item.hasUses = item.data.uses && (item.data.uses.max > 0);
|
||||
|
@ -104,10 +119,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 +148,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 +164,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);
|
||||
|
@ -195,7 +216,7 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
|
|||
if ( !this.options.editable ) return;
|
||||
|
||||
// Inventory Functions
|
||||
html.find(".currency-convert").click(this._onConvertCurrency.bind(this));
|
||||
// html.find(".currency-convert").click(this._onConvertCurrency.bind(this));
|
||||
|
||||
// Item State Toggling
|
||||
html.find('.item-toggle').click(this._onToggleItem.bind(this));
|
||||
|
@ -204,8 +225,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 +292,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 +351,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: `<p>${game.i18n.localize("SW5E.CurrencyConvertHint")}</p>`,
|
||||
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).
|
||||
|
|
|
@ -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.
|
||||
|
@ -16,7 +16,6 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
|
|||
static get defaultOptions() {
|
||||
return mergeObject(super.defaultOptions, {
|
||||
classes: ["sw5e", "sheet", "actor", "npc"],
|
||||
width: 600,
|
||||
width: 800,
|
||||
tabs: [{
|
||||
navSelector: ".root-tabs",
|
||||
|
@ -116,7 +115,7 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
|
|||
/** @override */
|
||||
activateListeners(html) {
|
||||
super.activateListeners(html);
|
||||
html.find(".health .rollable").click(this._onRollHealthFormula.bind(this));
|
||||
html.find(".health .rollable").click(this._onRollHPFormula.bind(this));
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -126,7 +125,7 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
|
|||
* @param {Event} event The original click event
|
||||
* @private
|
||||
*/
|
||||
_onRollHealthFormula(event) {
|
||||
_onRollHPFormula(event) {
|
||||
event.preventDefault();
|
||||
const formula = this.actor.data.data.attributes.hp.formula;
|
||||
if ( !formula ) return;
|
||||
|
@ -135,3 +134,4 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
|
|||
this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
385
module/actor/sheets/newSheet/vehicle.js
Normal file
|
@ -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};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_getMovementSpeed(actorData, largestPrimary=true) {
|
||||
return super._getMovementSpeed(actorData, largestPrimary);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Prepare items that are mounted to a vehicle and require one or more crew
|
||||
* to operate.
|
||||
* @private
|
||||
*/
|
||||
_prepareCrewedItem(item) {
|
||||
|
||||
// Determine crewed status
|
||||
const isCrewed = item.data.crewed;
|
||||
item.toggleClass = isCrewed ? 'active' : '';
|
||||
item.toggleTitle = game.i18n.localize(`SW5E.${isCrewed ? 'Crewed' : 'Uncrewed'}`);
|
||||
|
||||
// Handle crew actions
|
||||
if (item.type === 'feat' && item.data.activation.type === 'crew') {
|
||||
item.crew = item.data.activation.cost;
|
||||
item.cover = game.i18n.localize(`SW5E.${item.data.cover ? 'CoverTotal' : 'None'}`);
|
||||
if (item.data.cover === .5) item.cover = '½';
|
||||
else if (item.data.cover === .75) item.cover = '¾';
|
||||
else if (item.data.cover === null) item.cover = '—';
|
||||
if (item.crew < 1 || item.crew === null) item.crew = '—';
|
||||
}
|
||||
|
||||
// Prepare vehicle weapons
|
||||
if (item.type === 'equipment' || item.type === 'weapon') {
|
||||
item.threshold = item.data.hp.dt ? item.data.hp.dt : '—';
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Organize Owned Items for rendering the Vehicle sheet.
|
||||
* @private
|
||||
*/
|
||||
_prepareItems(data) {
|
||||
const cargoColumns = [{
|
||||
label: game.i18n.localize('SW5E.Quantity'),
|
||||
css: 'item-qty',
|
||||
property: 'quantity',
|
||||
editable: 'Number'
|
||||
}];
|
||||
|
||||
const equipmentColumns = [{
|
||||
label: game.i18n.localize('SW5E.Quantity'),
|
||||
css: 'item-qty',
|
||||
property: 'data.quantity'
|
||||
}, {
|
||||
label: game.i18n.localize('SW5E.AC'),
|
||||
css: 'item-ac',
|
||||
property: 'data.armor.value'
|
||||
}, {
|
||||
label: game.i18n.localize('SW5E.HP'),
|
||||
css: 'item-hp',
|
||||
property: 'data.hp.value',
|
||||
editable: 'Number'
|
||||
}, {
|
||||
label: game.i18n.localize('SW5E.Threshold'),
|
||||
css: 'item-threshold',
|
||||
property: 'threshold'
|
||||
}];
|
||||
|
||||
const features = {
|
||||
actions: {
|
||||
label: game.i18n.localize('SW5E.ActionPl'),
|
||||
items: [],
|
||||
crewable: true,
|
||||
dataset: {type: 'feat', 'activation.type': 'crew'},
|
||||
columns: [{
|
||||
label: game.i18n.localize('SW5E.VehicleCrew'),
|
||||
css: 'item-crew',
|
||||
property: 'crew'
|
||||
}, {
|
||||
label: game.i18n.localize('SW5E.Cover'),
|
||||
css: 'item-cover',
|
||||
property: 'cover'
|
||||
}]
|
||||
},
|
||||
equipment: {
|
||||
label: game.i18n.localize('SW5E.ItemTypeEquipment'),
|
||||
items: [],
|
||||
crewable: true,
|
||||
dataset: {type: 'equipment', 'armor.type': 'vehicle'},
|
||||
columns: equipmentColumns
|
||||
},
|
||||
passive: {
|
||||
label: game.i18n.localize('SW5E.Features'),
|
||||
items: [],
|
||||
dataset: {type: 'feat'}
|
||||
},
|
||||
reactions: {
|
||||
label: game.i18n.localize('SW5E.ReactionPl'),
|
||||
items: [],
|
||||
dataset: {type: 'feat', 'activation.type': 'reaction'}
|
||||
},
|
||||
weapons: {
|
||||
label: game.i18n.localize('SW5E.ItemTypeWeaponPl'),
|
||||
items: [],
|
||||
crewable: true,
|
||||
dataset: {type: 'weapon', 'weapon-type': 'siege'},
|
||||
columns: equipmentColumns
|
||||
}
|
||||
};
|
||||
|
||||
const cargo = {
|
||||
crew: {
|
||||
label: game.i18n.localize('SW5E.VehicleCrew'),
|
||||
items: data.data.cargo.crew,
|
||||
css: 'cargo-row crew',
|
||||
editableName: true,
|
||||
dataset: {type: 'crew'},
|
||||
columns: cargoColumns
|
||||
},
|
||||
passengers: {
|
||||
label: game.i18n.localize('SW5E.VehiclePassengers'),
|
||||
items: data.data.cargo.passengers,
|
||||
css: 'cargo-row passengers',
|
||||
editableName: true,
|
||||
dataset: {type: 'passengers'},
|
||||
columns: cargoColumns
|
||||
},
|
||||
cargo: {
|
||||
label: game.i18n.localize('SW5E.VehicleCargo'),
|
||||
items: [],
|
||||
dataset: {type: 'loot'},
|
||||
columns: [{
|
||||
label: game.i18n.localize('SW5E.Quantity'),
|
||||
css: 'item-qty',
|
||||
property: 'data.quantity',
|
||||
editable: 'Number'
|
||||
}, {
|
||||
label: game.i18n.localize('SW5E.Price'),
|
||||
css: 'item-price',
|
||||
property: 'data.price',
|
||||
editable: 'Number'
|
||||
}, {
|
||||
label: game.i18n.localize('SW5E.Weight'),
|
||||
css: 'item-weight',
|
||||
property: 'data.weight',
|
||||
editable: 'Number'
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
let totalWeight = 0;
|
||||
for (const item of data.items) {
|
||||
this._prepareCrewedItem(item);
|
||||
if (item.type === 'weapon') features.weapons.items.push(item);
|
||||
else if (item.type === 'equipment') features.equipment.items.push(item);
|
||||
else if (item.type === 'loot') {
|
||||
totalWeight += (item.data.weight || 0) * item.data.quantity;
|
||||
cargo.cargo.items.push(item);
|
||||
}
|
||||
else if (item.type === 'feat') {
|
||||
if (!item.data.activation.type || item.data.activation.type === 'none') {
|
||||
features.passive.items.push(item);
|
||||
}
|
||||
else if (item.data.activation.type === 'reaction') features.reactions.items.push(item);
|
||||
else features.actions.items.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
data.features = Object.values(features);
|
||||
data.cargo = Object.values(cargo);
|
||||
data.data.attributes.encumbrance = this._computeEncumbrance(totalWeight, data);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Event Listeners and Handlers */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
activateListeners(html) {
|
||||
super.activateListeners(html);
|
||||
if (!this.options.editable) return;
|
||||
|
||||
html.find('.item-toggle').click(this._onToggleItem.bind(this));
|
||||
html.find('.item-hp input')
|
||||
.click(evt => evt.target.select())
|
||||
.change(this._onHPChange.bind(this));
|
||||
|
||||
html.find('.item:not(.cargo-row) input[data-property]')
|
||||
.click(evt => evt.target.select())
|
||||
.change(this._onEditInSheet.bind(this));
|
||||
|
||||
html.find('.cargo-row input')
|
||||
.click(evt => evt.target.select())
|
||||
.change(this._onCargoRowChange.bind(this));
|
||||
|
||||
if (this.actor.data.data.attributes.actions.stations) {
|
||||
html.find('.counter.actions, .counter.action-thresholds').hide();
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle saving a cargo row (i.e. crew or passenger) in-sheet.
|
||||
* @param event {Event}
|
||||
* @returns {Promise<Actor>|null}
|
||||
* @private
|
||||
*/
|
||||
_onCargoRowChange(event) {
|
||||
event.preventDefault();
|
||||
const target = event.currentTarget;
|
||||
const row = target.closest('.item');
|
||||
const idx = Number(row.dataset.itemId);
|
||||
const property = row.classList.contains('crew') ? 'crew' : 'passengers';
|
||||
|
||||
// Get the cargo entry
|
||||
const cargo = duplicate(this.actor.data.data.cargo[property]);
|
||||
const entry = cargo[idx];
|
||||
if (!entry) return null;
|
||||
|
||||
// Update the cargo value
|
||||
const key = target.dataset.property || 'name';
|
||||
const type = target.dataset.dtype;
|
||||
let value = target.value;
|
||||
if (type === 'Number') value = Number(value);
|
||||
entry[key] = value;
|
||||
|
||||
// Perform the Actor update
|
||||
return this.actor.update({[`data.cargo.${property}`]: cargo});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle editing certain values like quantity, price, and weight in-sheet.
|
||||
* @param event {Event}
|
||||
* @returns {Promise<Item>}
|
||||
* @private
|
||||
*/
|
||||
_onEditInSheet(event) {
|
||||
event.preventDefault();
|
||||
const itemID = event.currentTarget.closest('.item').dataset.itemId;
|
||||
const item = this.actor.items.get(itemID);
|
||||
const property = event.currentTarget.dataset.property;
|
||||
const type = event.currentTarget.dataset.dtype;
|
||||
let value = event.currentTarget.value;
|
||||
switch (type) {
|
||||
case 'Number': value = parseInt(value); break;
|
||||
case 'Boolean': value = value === 'true'; break;
|
||||
}
|
||||
return item.update({[`${property}`]: value});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle creating a new crew or passenger row.
|
||||
* @param event {Event}
|
||||
* @returns {Promise<Actor|Item>}
|
||||
* @private
|
||||
*/
|
||||
_onItemCreate(event) {
|
||||
event.preventDefault();
|
||||
const target = event.currentTarget;
|
||||
const type = target.dataset.type;
|
||||
if (type === 'crew' || type === 'passengers') {
|
||||
const cargo = duplicate(this.actor.data.data.cargo[type]);
|
||||
cargo.push(this.constructor.newCargo);
|
||||
return this.actor.update({[`data.cargo.${type}`]: cargo});
|
||||
}
|
||||
return super._onItemCreate(event);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle deleting a crew or passenger row.
|
||||
* @param event {Event}
|
||||
* @returns {Promise<Actor|Item>}
|
||||
* @private
|
||||
*/
|
||||
_onItemDelete(event) {
|
||||
event.preventDefault();
|
||||
const row = event.currentTarget.closest('.item');
|
||||
if (row.classList.contains('cargo-row')) {
|
||||
const idx = Number(row.dataset.itemId);
|
||||
const type = row.classList.contains('crew') ? 'crew' : 'passengers';
|
||||
const cargo = duplicate(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx);
|
||||
return this.actor.update({[`data.cargo.${type}`]: cargo});
|
||||
}
|
||||
|
||||
return super._onItemDelete(event);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Special handling for editing HP to clamp it within appropriate range.
|
||||
* @param event {Event}
|
||||
* @returns {Promise<Item>}
|
||||
* @private
|
||||
*/
|
||||
_onHPChange(event) {
|
||||
event.preventDefault();
|
||||
const itemID = event.currentTarget.closest('.item').dataset.itemId;
|
||||
const item = this.actor.items.get(itemID);
|
||||
const hp = Math.clamped(0, parseInt(event.currentTarget.value), item.data.data.hp.max);
|
||||
event.currentTarget.value = hp;
|
||||
return item.update({'data.hp.value': hp});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle toggling an item's crewed status.
|
||||
* @param event {Event}
|
||||
* @returns {Promise<Item>}
|
||||
* @private
|
||||
*/
|
||||
_onToggleItem(event) {
|
||||
event.preventDefault();
|
||||
const itemID = event.currentTarget.closest('.item').dataset.itemId;
|
||||
const item = this.actor.items.get(itemID);
|
||||
const crewed = !!item.data.data.crewed;
|
||||
return item.update({'data.crewed': !crewed});
|
||||
}
|
||||
};
|
|
@ -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);
|
||||
|
||||
|
@ -115,23 +119,59 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Prepare the display of movement speed data for the Actor
|
||||
* @param {object} actorData
|
||||
* Prepare the display of movement speed data for the Actor*
|
||||
* @param {object} actorData The Actor data being prepared.
|
||||
* @param {boolean} [largestPrimary=false] Show the largest movement speed as "primary", otherwise show "walk"
|
||||
* @returns {{primary: string, special: string}}
|
||||
* @private
|
||||
*/
|
||||
_getMovementSpeed(actorData) {
|
||||
const movement = actorData.data.attributes.movement;
|
||||
const speeds = [
|
||||
_getMovementSpeed(actorData, largestPrimary=false) {
|
||||
const movement = actorData.data.attributes.movement || {};
|
||||
|
||||
// Prepare an array of available movement speeds
|
||||
let speeds = [
|
||||
[movement.burrow, `${game.i18n.localize("SW5E.MovementBurrow")} ${movement.burrow}`],
|
||||
[movement.climb, `${game.i18n.localize("SW5E.MovementClimb")} ${movement.climb}`],
|
||||
[movement.fly, `${game.i18n.localize("SW5E.MovementFly")} ${movement.fly}` + (movement.hover ? ` (${game.i18n.localize("SW5E.MovementHover")})` : "")],
|
||||
[movement.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(", ") : ""
|
||||
]
|
||||
if ( largestPrimary ) {
|
||||
speeds.push([movement.walk, `${game.i18n.localize("SW5E.MovementWalk")} ${movement.walk}`]);
|
||||
}
|
||||
|
||||
// Filter and sort speeds on their values
|
||||
speeds = speeds.filter(s => !!s[0]).sort((a, b) => b[0] - a[0]);
|
||||
|
||||
// Case 1: Largest as primary
|
||||
if ( largestPrimary ) {
|
||||
let primary = speeds.shift();
|
||||
return {
|
||||
primary: `${primary ? primary[1] : "0"} ${movement.units}`,
|
||||
special: speeds.map(s => s[1]).join(", ")
|
||||
}
|
||||
}
|
||||
|
||||
// Case 2: Walk as primary
|
||||
else {
|
||||
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;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -334,7 +374,7 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
1: '<i class="fas fa-check"></i>',
|
||||
2: '<i class="fas fa-check-double"></i>'
|
||||
};
|
||||
return icons[level];
|
||||
return icons[level] || icons[0];
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -373,8 +413,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 +487,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 +581,8 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
icon: '<i class="fas fa-paw"></i>',
|
||||
label: game.i18n.localize('SW5E.PolymorphWildShape'),
|
||||
callback: html => this.actor.transformInto(sourceActor, {
|
||||
keepBio: true,
|
||||
keepClass: true,
|
||||
keepMental: true,
|
||||
mergeSaves: true,
|
||||
mergeSkills: true,
|
||||
|
@ -565,6 +619,11 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
itemData = scroll.data;
|
||||
}
|
||||
|
||||
// Ignore certain statuses
|
||||
if ( itemData.data ) {
|
||||
["attunement", "equipped", "proficient", "prepared"].forEach(k => delete itemData.data[k]);
|
||||
}
|
||||
|
||||
// Create the owned item as normal
|
||||
return super._onDropItemCreate(itemData);
|
||||
}
|
||||
|
@ -619,14 +678,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 +739,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 +843,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();
|
|
@ -1,4 +1,4 @@
|
|||
import ActorSheet5e from "../base.js";
|
||||
import ActorSheet5e from "./base.js";
|
||||
import Actor5e from "../../entity.js";
|
||||
|
||||
/**
|
||||
|
@ -46,6 +46,9 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
|
|||
// Experience Tracking
|
||||
sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking");
|
||||
sheetData["classLabels"] = this.actor.itemTypes.class.map(c => c.name).join(", ");
|
||||
sheetData["multiclassLabels"] = this.actor.itemTypes.class.map(c => {
|
||||
return [c.data.data.archetype, c.name, c.data.data.levels].filterJoin(' ')
|
||||
}).join(', ');
|
||||
|
||||
// Return data for rendering
|
||||
return sheetData;
|
||||
|
@ -75,6 +78,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 = {
|
||||
[CONFIG.SW5E.attunementTypes.REQUIRED]: {
|
||||
icon: "fa-sun",
|
||||
cls: "not-attuned",
|
||||
title: "SW5E.AttunementRequired"
|
||||
},
|
||||
[CONFIG.SW5E.attunementTypes.ATTUNED]: {
|
||||
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 +137,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 +218,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 +278,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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import ActorSheet5e from "../base.js";
|
||||
import ActorSheet5e from "./base.js";
|
||||
|
||||
/**
|
||||
* An Actor sheet for Vehicle type actors.
|
||||
|
@ -56,6 +56,13 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_getMovementSpeed(actorData, largestPrimary=true) {
|
||||
return super._getMovementSpeed(actorData, largestPrimary);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Prepare items that are mounted to a vehicle and require one or more crew
|
||||
* to operate.
|
||||
|
@ -86,13 +93,6 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_getMovementSpeed(actorData) {
|
||||
return {primary: "", special: ""};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Organize Owned Items for rendering the Vehicle sheet.
|
||||
* @private
|
||||
|
|
|
@ -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: `<i class="fas ${icon}"></i>`,
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -120,10 +127,13 @@ export default class AbilityUseDialog extends Dialog {
|
|||
});
|
||||
}
|
||||
const canCast = powerLevels.some(l => l.hasSlots);
|
||||
if ( !canCast ) data.errors.push(game.i18n.format("SW5E.PowerCastNoSlots", {
|
||||
level: CONFIG.SW5E.powerLevels[lvl],
|
||||
name: data.item.name
|
||||
}));
|
||||
|
||||
// Return merged data
|
||||
data = mergeObject(data, { isPower: true, canUpcast, powerLevels });
|
||||
if ( !canCast ) data.errors.push("SW5E.PowerCastNoSlots");
|
||||
// Merge power casting data
|
||||
return mergeObject(data, { isPower: true, consumePowerSlot, powerLevels });
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -158,6 +168,8 @@ export default class AbilityUseDialog extends Dialog {
|
|||
type: item.data.consumableType,
|
||||
value: uses.value,
|
||||
quantity: item.data.quantity,
|
||||
max: uses.max,
|
||||
per: CONFIG.SW5E.limitedUsePeriods[uses.per]
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
|
|
43
module/apps/senses-config.js
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
@ -49,7 +49,7 @@ export default class TraitSelector extends FormApplication {
|
|||
}
|
||||
|
||||
// Return data
|
||||
return {
|
||||
return {
|
||||
allowCustom: this.options.allowCustom,
|
||||
choices: choices,
|
||||
custom: attr ? attr.custom : ""
|
||||
|
|
|
@ -66,7 +66,7 @@ export const displayChatActionButtons = function(message, html, data) {
|
|||
export const addChatMessageContextOptions = function(html, options) {
|
||||
let canApply = li => {
|
||||
const message = game.messages.get(li.data("messageId"));
|
||||
return message.isRoll && message.isContentVisible && canvas.tokens.controlled.length;
|
||||
return message?.isRoll && message?.isContentVisible && canvas?.tokens.controlled.length;
|
||||
};
|
||||
options.push(
|
||||
{
|
||||
|
@ -103,15 +103,16 @@ export const addChatMessageContextOptions = function(html, options) {
|
|||
* Apply rolled dice damage to the token or tokens which are currently controlled.
|
||||
* This allows for damage to be scaled by a multiplier to account for healing, critical hits, or resistance
|
||||
*
|
||||
* @param {HTMLElement} roll The chat entry which contains the roll data
|
||||
* @param {HTMLElement} li The chat entry which contains the roll data
|
||||
* @param {Number} multiplier A damage multiplier to apply to the rolled damage.
|
||||
* @return {Promise}
|
||||
*/
|
||||
function applyChatCardDamage(roll, multiplier) {
|
||||
const amount = roll.find('.dice-total').text();
|
||||
function applyChatCardDamage(li, multiplier) {
|
||||
const message = game.messages.get(li.data("messageId"));
|
||||
const roll = message.roll;
|
||||
return Promise.all(canvas.tokens.controlled.map(t => {
|
||||
const a = t.actor;
|
||||
return a.applyDamage(amount, multiplier);
|
||||
return a.applyDamage(roll.total, multiplier);
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export const ClassFeatures = {
|
||||
|
||||
};
|
||||
|
||||
|
|
|
@ -12,8 +12,8 @@ export const _getInitiativeFormula = function(combatant) {
|
|||
|
||||
let nd = 1;
|
||||
let mods = "";
|
||||
|
||||
if (actor.getFlag("sw5e", "halflingLucky")) mods += "r=1";
|
||||
|
||||
if (actor.getFlag("sw5e", "halflingLucky")) mods += "r1=1";
|
||||
if (actor.getFlag("sw5e", "initiativeAdv")) {
|
||||
nd = 2;
|
||||
mods += "kh";
|
||||
|
@ -26,15 +26,3 @@ export const _getInitiativeFormula = function(combatant) {
|
|||
if ( tiebreaker ) parts.push(actor.data.data.abilities.dex.value / 100);
|
||||
return parts.filter(p => p !== null).join(" + ");
|
||||
};
|
||||
|
||||
/**
|
||||
* When the Combat encounter updates - re-render open Actor sheets for combatants in the encounter.
|
||||
*/
|
||||
Hooks.on("updateCombat", (combat, data, options, userId) => {
|
||||
const updateTurn = ("turn" in data) || ("round" in data);
|
||||
if ( !updateTurn ) return;
|
||||
for ( let t of combat.turns ) {
|
||||
const a = t.actor;
|
||||
if ( t.actor ) t.actor.sheet.render(false);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -4,14 +4,13 @@ import {ClassFeatures} from "./classFeatures.js"
|
|||
export const SW5E = {};
|
||||
|
||||
// ASCII Artwork
|
||||
SW5E.ASCII = `__________________________________________
|
||||
_
|
||||
| |
|
||||
___| |_ __ _ _ ____ ____ _ _ __ ___
|
||||
/ __| __/ _\ | |__\ \ /\ / / _\ | |__/ __|
|
||||
\__ \ || (_) | | \ V V / (_) | | \__ \
|
||||
|___/\__\__/_|_| \_/\_/ \__/_|_| |___/
|
||||
__________________________________________`;
|
||||
SW5E.ASCII = `
|
||||
___________ ___________
|
||||
/ _____/ \\ / \\ ____/ ____
|
||||
\\_____ \\\\ \\/\\/ /____ \\_/ __ \\
|
||||
/ \\\\ // \\ ___/
|
||||
\\______ / \\__/\\ //______ /\\__ >
|
||||
\\/ \\/ \\/ \\/ `;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -54,6 +53,30 @@ SW5E.alignments = {
|
|||
'cd': "SW5E.AlignmentCD"
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* An enumeration of item attunement types
|
||||
* @enum {number}
|
||||
*/
|
||||
SW5E.attunementTypes = {
|
||||
NONE: 0,
|
||||
REQUIRED: 1,
|
||||
ATTUNED: 2,
|
||||
}
|
||||
|
||||
/**
|
||||
* 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",
|
||||
|
@ -291,6 +314,7 @@ SW5E.damageResistanceTypes = duplicate(SW5E.damageTypes);
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
|
||||
// armor Types
|
||||
SW5E.armorPropertiesTypes = {
|
||||
"Absorptive": "SW5E.ArmorProperAbsorptive",
|
||||
|
@ -325,6 +349,19 @@ SW5E.armorPropertiesTypes = {
|
|||
"Versatile": "SW5E.ArmorProperVersatile"
|
||||
};
|
||||
|
||||
/**
|
||||
* The valid units of measure for movement distances in the game system.
|
||||
* By default this uses the imperial units of feet and miles.
|
||||
* @type {Object<string,string>}
|
||||
*/
|
||||
SW5E.movementTypes = {
|
||||
"burrow": "SW5E.MovementBurrow",
|
||||
"climb": "SW5E.MovementClimb",
|
||||
"fly": "SW5E.MovementFly",
|
||||
"swim": "SW5E.MovementSwim",
|
||||
"walk": "SW5E.MovementWalk",
|
||||
}
|
||||
|
||||
/**
|
||||
* The valid units of measure for movement distances in the game system.
|
||||
* By default this uses the imperial units of feet and miles.
|
||||
|
@ -433,17 +470,16 @@ 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 +1176,7 @@ SW5E.characterFlags = {
|
|||
section: "Feats",
|
||||
type: Boolean
|
||||
},
|
||||
"remarkableAthlete": {
|
||||
"remarkableAthlete": {
|
||||
name: "SW5E.FlagsRemarkableAthlete",
|
||||
hint: "SW5E.FlagsRemarkableAthleteHint",
|
||||
abilities: ['str','dex','con'],
|
||||
|
|
|
@ -1,3 +1,69 @@
|
|||
/**
|
||||
* A standardized helper function for simplifying the constant parts of a multipart roll formula
|
||||
*
|
||||
* @param {string} formula The original Roll formula
|
||||
* @param {Object} data Actor or item data against which to parse the roll
|
||||
* @param {Object} options Formatting options
|
||||
* @param {boolean} options.constantFirst Puts the constants before the dice terms in the resulting formula
|
||||
*
|
||||
* @return {string} The resulting simplified formula
|
||||
*/
|
||||
export function simplifyRollFormula(formula, data, {constantFirst = false} = {}) {
|
||||
const roll = new Roll(formula, data); // Parses the formula and replaces any @properties
|
||||
const terms = roll.terms;
|
||||
|
||||
// Some terms are "too complicated" for this algorithm to simplify
|
||||
// In this case, the original formula is returned.
|
||||
if (terms.some(_isUnsupportedTerm)) return roll.formula;
|
||||
|
||||
const rollableTerms = []; // Terms that are non-constant, and their associated operators
|
||||
const constantTerms = []; // Terms that are constant, and their associated operators
|
||||
let operators = []; // Temporary storage for operators before they are moved to one of the above
|
||||
|
||||
for (let term of terms) { // For each term
|
||||
if (["+", "-"].includes(term)) operators.push(term); // If the term is an addition/subtraction operator, push the term into the operators array
|
||||
else { // Otherwise the term is not an operator
|
||||
if (term instanceof DiceTerm) { // If the term is something rollable
|
||||
rollableTerms.push(...operators); // Place all the operators into the rollableTerms array
|
||||
rollableTerms.push(term); // Then place this rollable term into it as well
|
||||
} //
|
||||
else { // Otherwise, this must be a constant
|
||||
constantTerms.push(...operators); // Place the operators into the constantTerms array
|
||||
constantTerms.push(term); // Then also add this constant term to that array.
|
||||
} //
|
||||
operators = []; // Finally, the operators have now all been assigend to one of the arrays, so empty this before the next iteration.
|
||||
}
|
||||
}
|
||||
|
||||
const constantFormula = Roll.cleanFormula(constantTerms); // Cleans up the constant terms and produces a new formula string
|
||||
const rollableFormula = Roll.cleanFormula(rollableTerms); // Cleans up the non-constant terms and produces a new formula string
|
||||
|
||||
const constantPart = roll._safeEval(constantFormula); // Mathematically evaluate the constant formula to produce a single constant term
|
||||
|
||||
const parts = constantFirst ? // Order the rollable and constant terms, either constant first or second depending on the optional argumen
|
||||
[constantPart, rollableFormula] : [rollableFormula, constantPart];
|
||||
|
||||
// Join the parts with a + sign, pass them to `Roll` once again to clean up the formula
|
||||
return new Roll(parts.filterJoin(" + ")).formula;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Only some terms are supported by simplifyRollFormula, this method returns true when the term is not supported.
|
||||
* @param {*} term - A single Dice term to check support on
|
||||
* @return {Boolean} True when unsupported, false if supported
|
||||
*/
|
||||
function _isUnsupportedTerm(term) {
|
||||
const diceTerm = term instanceof DiceTerm;
|
||||
const operator = ["+", "-"].includes(term);
|
||||
const number = !isNaN(Number(term));
|
||||
|
||||
return !(diceTerm || operator || number);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A standardized helper function for managing core 5e "d20 rolls"
|
||||
*
|
||||
|
@ -44,8 +110,8 @@ export async function d20Roll({parts=[], data={}, event={}, rollMode=null, templ
|
|||
let adv = 0;
|
||||
fastForward = fastForward ?? (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
|
||||
if (fastForward) {
|
||||
if ( advantage || event.altKey ) adv = 1;
|
||||
else if ( disadvantage || event.ctrlKey || event.metaKey ) adv = -1;
|
||||
if ( advantage ?? event.altKey ) adv = 1;
|
||||
else if ( disadvantage ?? (event.ctrlKey || event.metaKey) ) adv = -1;
|
||||
}
|
||||
|
||||
// Define the inner roll function
|
||||
|
@ -53,7 +119,7 @@ export async function d20Roll({parts=[], data={}, event={}, rollMode=null, templ
|
|||
|
||||
// Determine the d20 roll and modifiers
|
||||
let nd = 1;
|
||||
let mods = halflingLucky ? "r=1" : "";
|
||||
let mods = halflingLucky ? "r1=1" : "";
|
||||
|
||||
// Handle advantage
|
||||
if (adv === 1) {
|
||||
|
@ -109,6 +175,8 @@ export async function d20Roll({parts=[], data={}, event={}, rollMode=null, templ
|
|||
if (d.faces === 20) {
|
||||
d.options.critical = critical;
|
||||
d.options.fumble = fumble;
|
||||
if ( adv === 1 ) d.options.advantage = true;
|
||||
else if ( adv === -1 ) d.options.disadvantage = true;
|
||||
if (targetValue) d.options.target = targetValue;
|
||||
}
|
||||
}
|
||||
|
@ -131,7 +199,6 @@ export async function d20Roll({parts=[], data={}, event={}, rollMode=null, templ
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
|
||||
/**
|
||||
* Present a Dialog form which creates a d20 roll once submitted
|
||||
* @return {Promise<Roll>}
|
||||
|
@ -175,7 +242,6 @@ async function _d20RollDialog({template, title, parts, data, rollMode, dialogOpt
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
|
@ -214,7 +280,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) {
|
||||
|
@ -242,7 +307,9 @@ export async function damageRoll({parts, actor, data, event={}, rollMode=null, t
|
|||
|
||||
// Execute the roll
|
||||
try {
|
||||
return roll.roll();
|
||||
roll.evaluate()
|
||||
if ( crit ) roll.dice.forEach(d => d.options.critical = true); // TODO workaround core bug which wipes Roll#options on roll
|
||||
return roll;
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
ui.notifications.error(`Dice roll evaluation failed: ${err.message}`);
|
||||
|
@ -251,7 +318,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
|
||||
});
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import {d20Roll, damageRoll} from "../dice.js";
|
||||
import {simplifyRollFormula, d20Roll, damageRoll} from "../dice.js";
|
||||
import AbilityUseDialog from "../apps/ability-use-dialog.js";
|
||||
import AbilityTemplate from "../pixi/ability-template.js";
|
||||
|
||||
/**
|
||||
* Override and extend the basic :class:`Item` implementation
|
||||
|
@ -101,7 +100,8 @@ export default class Item5e extends Item {
|
|||
* @type {boolean}
|
||||
*/
|
||||
get hasSave() {
|
||||
return !!(this.data.data.save && this.data.data.save.ability);
|
||||
const save = this.data.data?.save || {};
|
||||
return !!(save.ability && save.scaling);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -152,7 +152,7 @@ export default class Item5e extends Item {
|
|||
const itemData = this.data;
|
||||
const data = itemData.data;
|
||||
const C = CONFIG.SW5E;
|
||||
const labels = {};
|
||||
const labels = this.labels = {};
|
||||
|
||||
// Classes
|
||||
if ( itemData.type === "class" ) {
|
||||
|
@ -251,12 +251,13 @@ export default class Item5e extends Item {
|
|||
|
||||
// Item Actions
|
||||
if ( data.hasOwnProperty("actionType") ) {
|
||||
// if this item is owned, we populate the label and saving throw during actor init
|
||||
if (!this.isOwned) {
|
||||
// Saving throws
|
||||
this.getSaveDC();
|
||||
|
||||
// Saving throws for unowned items
|
||||
const save = data.save;
|
||||
if ( save?.ability && !this.isOwned ) {
|
||||
if ( save.scaling !== "flat" ) save.dc = null;
|
||||
labels.save = game.i18n.format("SW5E.SaveDC", {dc: save.dc || "", ability: C.abilities[save.ability]});
|
||||
// To Hit
|
||||
this.getAttackToHit();
|
||||
}
|
||||
|
||||
// Damage
|
||||
|
@ -265,10 +266,111 @@ export default class Item5e extends Item {
|
|||
labels.damage = dam.parts.map(d => d[0]).join(" + ").replace(/\+ -/g, "- ");
|
||||
labels.damageTypes = dam.parts.map(d => C.damageTypes[d[1]]).join(", ");
|
||||
}
|
||||
|
||||
// Limited Uses
|
||||
if ( this.isOwned && !!data.uses?.max ) {
|
||||
let max = data.uses.max;
|
||||
if ( !Number.isNumeric(max) ) {
|
||||
max = Roll.replaceFormulaData(max, this.actor.getRollData());
|
||||
if ( Roll.MATH_PROXY.safeEval ) max = Roll.MATH_PROXY.safeEval(max);
|
||||
}
|
||||
data.uses.max = Number(max);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Update the derived power DC for an item that requires a saving throw
|
||||
* @returns {number|null}
|
||||
*/
|
||||
getSaveDC() {
|
||||
if ( !this.hasSave ) return;
|
||||
const save = this.data.data?.save;
|
||||
|
||||
// Actor power-DC based scaling
|
||||
if ( save.scaling === "power" ) {
|
||||
save.dc = this.isOwned ? getProperty(this.actor.data, "data.attributes.powerdc") : null;
|
||||
}
|
||||
|
||||
// Assign labels
|
||||
this.labels = labels;
|
||||
// Ability-score based scaling
|
||||
else if ( save.scaling !== "flat" ) {
|
||||
save.dc = this.isOwned ? getProperty(this.actor.data, `data.abilities.${save.scaling}.dc`) : null;
|
||||
}
|
||||
|
||||
// Update labels
|
||||
const abl = CONFIG.SW5E.abilities[save.ability];
|
||||
this.labels.save = game.i18n.format("SW5E.SaveDC", {dc: save.dc || "", ability: abl});
|
||||
return save.dc;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Update a label to the Item detailing its total to hit bonus.
|
||||
* Sources:
|
||||
* - item entity's innate attack bonus
|
||||
* - item's actor's proficiency bonus if applicable
|
||||
* - item's actor's global bonuses to the given item type
|
||||
* - item's ammunition if applicable
|
||||
*
|
||||
* @returns {Object} returns `rollData` and `parts` to be used in the item's Attack roll
|
||||
*/
|
||||
getAttackToHit() {
|
||||
const itemData = this.data.data;
|
||||
if ( !this.hasAttack || !itemData ) return;
|
||||
const rollData = this.getRollData();
|
||||
|
||||
// Define Roll bonuses
|
||||
const parts = [];
|
||||
|
||||
// Include the item's innate attack bonus as the initial value and label
|
||||
if ( itemData.attackBonus ) {
|
||||
parts.push(itemData.attackBonus)
|
||||
this.labels.toHit = itemData.attackBonus;
|
||||
}
|
||||
|
||||
// Take no further action for un-owned items
|
||||
if ( !this.isOwned ) return {rollData, parts};
|
||||
|
||||
// Ability score modifier
|
||||
parts.push(`@mod`);
|
||||
|
||||
// Add proficiency bonus if an explicit proficiency flag is present or for non-item features
|
||||
if ( !["weapon", "consumable"].includes(this.data.type) || itemData.proficient ) {
|
||||
parts.push("@prof");
|
||||
}
|
||||
|
||||
// Actor-level global bonus to attack rolls
|
||||
const actorBonus = this.actor.data.data.bonuses?.[itemData.actionType] || {};
|
||||
if ( actorBonus.attack ) parts.push(actorBonus.attack);
|
||||
|
||||
// One-time bonus provided by consumed ammunition
|
||||
if ( (itemData.consume?.type === 'ammo') && !!this.actor.items ) {
|
||||
const ammoItemData = this.actor.items.get(itemData.consume.target)?.data;
|
||||
|
||||
if (ammoItemData) {
|
||||
const ammoItemQuantity = ammoItemData.data.quantity;
|
||||
const ammoCanBeConsumed = ammoItemQuantity && (ammoItemQuantity - (itemData.consume.amount ?? 0) >= 0);
|
||||
const ammoItemAttackBonus = ammoItemData.data.attackBonus;
|
||||
const ammoIsTypeConsumable = (ammoItemData.type === "consumable") && (ammoItemData.data.consumableType === "ammo")
|
||||
if ( ammoCanBeConsumed && ammoItemAttackBonus && ammoIsTypeConsumable ) {
|
||||
parts.push("@ammo");
|
||||
rollData["ammo"] = ammoItemAttackBonus;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Condense the resulting attack bonus formula into a simplified label
|
||||
let toHitLabel = simplifyRollFormula(parts.join('+'), rollData).trim();
|
||||
if (toHitLabel.charAt(0) !== '-') {
|
||||
toHitLabel = '+ ' + toHitLabel
|
||||
}
|
||||
this.labels.toHit = toHitLabel;
|
||||
|
||||
// Update labels and return the prepared roll data
|
||||
return {rollData, parts};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -279,9 +381,251 @@ export default class Item5e extends Item {
|
|||
* @param {string} [rollMode] The roll display mode with which to display (or not) the card
|
||||
* @param {boolean} [createMessage] Whether to automatically create a chat message (if true) or simply return
|
||||
* the prepared chat message data (if false).
|
||||
* @return {Promise}
|
||||
* @return {Promise<ChatMessage|object|void>}
|
||||
*/
|
||||
async roll({configureDialog=true, rollMode=null, createMessage=true}={}) {
|
||||
async roll({configureDialog=true, rollMode, createMessage=true}={}) {
|
||||
let item = this;
|
||||
const actor = this.actor;
|
||||
|
||||
// Reference aspects of the item data necessary for usage
|
||||
const id = this.data.data; // Item data
|
||||
const hasArea = this.hasAreaTarget; // Is the ability usage an AoE?
|
||||
const resource = id.consume || {}; // Resource consumption
|
||||
const recharge = id.recharge || {}; // Recharge mechanic
|
||||
const uses = id?.uses ?? {}; // Limited uses
|
||||
const isPower = this.type === "power"; // Does the item require a power slot?
|
||||
const requirePowerSlot = isPower && (id.level > 0) && CONFIG.SW5E.powerUpcastModes.includes(id.preparation.mode);
|
||||
|
||||
// Define follow-up actions resulting from the item usage
|
||||
let createMeasuredTemplate = hasArea; // Trigger a template creation
|
||||
let consumeRecharge = !!recharge.value; // Consume recharge
|
||||
let consumeResource = !!resource.target && (resource.type !== "ammo") // Consume a linked (non-ammo) resource
|
||||
let consumePowerSlot = requirePowerSlot; // Consume a power slot
|
||||
let consumeUsage = !!uses.per; // Consume limited uses
|
||||
let consumeQuantity = uses.autoDestroy; // Consume quantity of the item in lieu of uses
|
||||
|
||||
// Display a configuration dialog to customize the usage
|
||||
const needsConfiguration = createMeasuredTemplate || consumeRecharge || consumeResource || consumePowerSlot || consumeUsage;
|
||||
if (configureDialog && needsConfiguration) {
|
||||
const configuration = await AbilityUseDialog.create(this);
|
||||
if (!configuration) return;
|
||||
|
||||
// Determine consumption preferences
|
||||
createMeasuredTemplate = Boolean(configuration.placeTemplate);
|
||||
consumeUsage = Boolean(configuration.consumeUse);
|
||||
consumeRecharge = Boolean(configuration.consumeRecharge);
|
||||
consumeResource = Boolean(configuration.consumeResource);
|
||||
consumePowerSlot = Boolean(configuration.consumeSlot);
|
||||
|
||||
// Handle power upcasting
|
||||
if ( requirePowerSlot ) {
|
||||
const slotLevel = configuration.level;
|
||||
const powerLevel = slotLevel === "pact" ? actor.data.data.powers.pact.level : parseInt(slotLevel);
|
||||
if (powerLevel !== id.level) {
|
||||
const upcastData = mergeObject(this.data, {"data.level": powerLevel}, {inplace: false});
|
||||
item = this.constructor.createOwned(upcastData, actor); // Replace the item with an upcast version
|
||||
}
|
||||
if ( consumePowerSlot ) consumePowerSlot = slotLevel === "pact" ? "pact" : `power${powerLevel}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine whether the item can be used by testing for resource consumption
|
||||
const usage = item._getUsageUpdates({consumeRecharge, consumeResource, consumePowerSlot, consumeUsage, consumeQuantity});
|
||||
if ( !usage ) return;
|
||||
const {actorUpdates, itemUpdates, resourceUpdates} = usage;
|
||||
|
||||
// Commit pending data updates
|
||||
if ( !isObjectEmpty(itemUpdates) ) await item.update(itemUpdates);
|
||||
if ( consumeQuantity && (item.data.data.quantity === 0) ) await item.delete();
|
||||
if ( !isObjectEmpty(actorUpdates) ) await actor.update(actorUpdates);
|
||||
if ( !isObjectEmpty(resourceUpdates) ) {
|
||||
const resource = actor.items.get(id.consume?.target);
|
||||
if ( resource ) await resource.update(resourceUpdates);
|
||||
}
|
||||
|
||||
// Initiate measured template creation
|
||||
if ( createMeasuredTemplate ) {
|
||||
const template = game.sw5e.canvas.AbilityTemplate.fromItem(item);
|
||||
if ( template ) template.drawPreview();
|
||||
}
|
||||
|
||||
// Create or return the Chat Message data
|
||||
return item.displayCard({rollMode, createMessage});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Verify that the consumed resources used by an Item are available.
|
||||
* Otherwise display an error and return false.
|
||||
* @param {boolean} consumeQuantity Consume quantity of the item if other consumption modes are not available?
|
||||
* @param {boolean} consumeRecharge Whether the item consumes the recharge mechanic
|
||||
* @param {boolean} consumeResource Whether the item consumes a limited resource
|
||||
* @param {string|boolean} consumePowerSlot A level of power slot consumed, or false
|
||||
* @param {boolean} consumeUsage Whether the item consumes a limited usage
|
||||
* @returns {object|boolean} A set of data changes to apply when the item is used, or false
|
||||
* @private
|
||||
*/
|
||||
_getUsageUpdates({consumeQuantity=false, consumeRecharge=false, consumeResource=false, consumePowerSlot=false, consumeUsage=false}) {
|
||||
|
||||
// Reference item data
|
||||
const id = this.data.data;
|
||||
const actorUpdates = {};
|
||||
const itemUpdates = {};
|
||||
const resourceUpdates = {};
|
||||
|
||||
// Consume Recharge
|
||||
if ( consumeRecharge ) {
|
||||
const recharge = id.recharge || {};
|
||||
if ( recharge.charged === false ) {
|
||||
ui.notifications.warn(game.i18n.format("SW5E.ItemNoUses", {name: this.name}));
|
||||
return false;
|
||||
}
|
||||
itemUpdates["data.recharge.charged"] = false;
|
||||
}
|
||||
|
||||
// Consume Limited Resource
|
||||
if ( consumeResource ) {
|
||||
const canConsume = this._handleConsumeResource(itemUpdates, actorUpdates, resourceUpdates);
|
||||
if ( canConsume === false ) return false;
|
||||
}
|
||||
|
||||
// Consume Power Slots
|
||||
if ( consumePowerSlot ) {
|
||||
const level = this.actor?.data.data.powers[consumePowerSlot];
|
||||
const powers = Number(level?.value ?? 0);
|
||||
if ( powers === 0 ) {
|
||||
const label = game.i18n.localize(consumePowerSlot === "pact" ? "SW5E.PowerProgPact" : `SW5E.PowerLevel${id.level}`);
|
||||
ui.notifications.warn(game.i18n.format("SW5E.PowerCastNoSlots", {name: this.name, level: label}));
|
||||
return false;
|
||||
}
|
||||
actorUpdates[`data.powers.${consumePowerSlot}.value`] = Math.max(powers - 1, 0);
|
||||
}
|
||||
|
||||
// Consume Limited Usage
|
||||
if ( consumeUsage ) {
|
||||
const uses = id.uses || {};
|
||||
const available = Number(uses.value ?? 0);
|
||||
let used = false;
|
||||
|
||||
// Reduce usages
|
||||
const remaining = Math.max(available - 1, 0);
|
||||
if ( available >= 1 ) {
|
||||
used = true;
|
||||
itemUpdates["data.uses.value"] = remaining;
|
||||
}
|
||||
|
||||
// Reduce quantity if not reducing usages or if usages hit 0 and we are set to consumeQuantity
|
||||
if ( consumeQuantity && (!used || (remaining === 0)) ) {
|
||||
const q = Number(id.quantity ?? 1);
|
||||
if ( q >= 1 ) {
|
||||
used = true;
|
||||
itemUpdates["data.quantity"] = Math.max(q - 1, 0);
|
||||
itemUpdates["data.uses.value"] = uses.max ?? 1;
|
||||
}
|
||||
}
|
||||
|
||||
// If the item was not used, return a warning
|
||||
if ( !used ) {
|
||||
ui.notifications.warn(game.i18n.format("SW5E.ItemNoUses", {name: this.name}));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Return the configured usage
|
||||
return {itemUpdates, actorUpdates, resourceUpdates};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle update actions required when consuming an external resource
|
||||
* @param {object} itemUpdates An object of data updates applied to this item
|
||||
* @param {object} actorUpdates An object of data updates applied to the item owner (Actor)
|
||||
* @param {object} resourceUpdates An object of data updates applied to a different resource item (Item)
|
||||
* @return {boolean|void} Return false to block further progress, or return nothing to continue
|
||||
* @private
|
||||
*/
|
||||
_handleConsumeResource(itemUpdates, actorUpdates, resourceUpdates) {
|
||||
const actor = this.actor;
|
||||
const itemData = this.data.data;
|
||||
const consume = itemData.consume || {};
|
||||
if ( !consume.type ) return;
|
||||
|
||||
// No consumed target
|
||||
const typeLabel = CONFIG.SW5E.abilityConsumptionTypes[consume.type];
|
||||
if ( !consume.target ) {
|
||||
ui.notifications.warn(game.i18n.format("SW5E.ConsumeWarningNoResource", {name: this.name, type: typeLabel}));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Identify the consumed resource and its current quantity
|
||||
let resource = null;
|
||||
let amount = Number(consume.amount ?? 1);
|
||||
let quantity = 0;
|
||||
switch ( consume.type ) {
|
||||
case "attribute":
|
||||
resource = getProperty(actor.data.data, consume.target);
|
||||
quantity = resource || 0;
|
||||
break;
|
||||
case "ammo":
|
||||
case "material":
|
||||
resource = actor.items.get(consume.target);
|
||||
quantity = resource ? resource.data.data.quantity : 0;
|
||||
break;
|
||||
case "charges":
|
||||
resource = actor.items.get(consume.target);
|
||||
if ( !resource ) break;
|
||||
const uses = resource.data.data.uses;
|
||||
if ( uses.per && uses.max ) quantity = uses.value;
|
||||
else if ( resource.data.data.recharge?.value ) {
|
||||
quantity = resource.data.data.recharge.charged ? 1 : 0;
|
||||
amount = 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Verify that a consumed resource is available
|
||||
if ( !resource ) {
|
||||
ui.notifications.warn(game.i18n.format("SW5E.ConsumeWarningNoSource", {name: this.name, type: typeLabel}));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify that the required quantity is available
|
||||
let remaining = quantity - amount;
|
||||
if ( remaining < 0 ) {
|
||||
ui.notifications.warn(game.i18n.format("SW5E.ConsumeWarningNoQuantity", {name: this.name, type: typeLabel}));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Define updates to provided data objects
|
||||
switch ( consume.type ) {
|
||||
case "attribute":
|
||||
actorUpdates[`data.${consume.target}`] = remaining;
|
||||
break;
|
||||
case "ammo":
|
||||
case "material":
|
||||
resourceUpdates["data.quantity"] = remaining;
|
||||
break;
|
||||
case "charges":
|
||||
const uses = resource.data.data.uses || {};
|
||||
const recharge = resource.data.data.recharge || {};
|
||||
if ( uses.per && uses.max ) resourceUpdates["data.uses.value"] = remaining;
|
||||
else if ( recharge.value ) resourceUpdates["data.recharge.charged"] = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Display the chat card for an Item as a Chat Message
|
||||
* @param {object} options Options which configure the display of the item chat card
|
||||
* @param {string} rollMode The message visibility mode to apply to the created card
|
||||
* @param {boolean} createMessage Whether to automatically create a ChatMessage entity (if true), or only return
|
||||
* the prepared message data (if false)
|
||||
*/
|
||||
async displayCard({rollMode, createMessage=true}={}) {
|
||||
|
||||
// Basic template rendering data
|
||||
const token = this.actor.token;
|
||||
|
@ -300,190 +644,31 @@ export default class Item5e extends Item {
|
|||
hasAreaTarget: this.hasAreaTarget
|
||||
};
|
||||
|
||||
// For feature items, optionally show an ability usage dialog
|
||||
if (this.data.type === "feat") {
|
||||
let configured = await this._rollFeat(configureDialog);
|
||||
if ( configured === false ) return;
|
||||
} else if ( this.data.type === "consumable" ) {
|
||||
let configured = await this._rollConsumable(configureDialog);
|
||||
if ( configured === false ) return;
|
||||
}
|
||||
|
||||
// For items which consume a resource, handle that here
|
||||
const allowed = await this._handleResourceConsumption({isCard: true, isAttack: false});
|
||||
if ( allowed === false ) return;
|
||||
|
||||
// Render the chat card template
|
||||
const templateType = ["tool"].includes(this.data.type) ? this.data.type : "item";
|
||||
const template = `systems/sw5e/templates/chat/${templateType}-card.html`;
|
||||
const html = await renderTemplate(template, templateData);
|
||||
|
||||
// Basic chat message data
|
||||
// Create the ChatMessage data object
|
||||
const chatData = {
|
||||
user: game.user._id,
|
||||
type: CONST.CHAT_MESSAGE_TYPES.OTHER,
|
||||
content: html,
|
||||
flavor: this.data.data.chatFlavor || this.name,
|
||||
speaker: {
|
||||
actor: this.actor._id,
|
||||
token: this.actor.token,
|
||||
alias: this.actor.name
|
||||
},
|
||||
speaker: ChatMessage.getSpeaker({actor: this.actor, token}),
|
||||
flags: {"core.canPopout": true}
|
||||
};
|
||||
|
||||
// If the consumable was destroyed in the process - embed the item data in the surviving message
|
||||
// If the Item was destroyed in the process of displaying its card - embed the item data in the chat message
|
||||
if ( (this.data.type === "consumable") && !this.actor.items.has(this.id) ) {
|
||||
chatData.flags["sw5e.itemData"] = this.data;
|
||||
}
|
||||
|
||||
// Toggle default roll mode
|
||||
rollMode = rollMode || game.settings.get("core", "rollMode");
|
||||
if ( ["gmroll", "blindroll"].includes(rollMode) ) chatData["whisper"] = ChatMessage.getWhisperRecipients("GM");
|
||||
if ( rollMode === "blindroll" ) chatData["blind"] = true;
|
||||
// Apply the roll mode to adjust message visibility
|
||||
ChatMessage.applyRollMode(chatData, rollMode || game.settings.get("core", "rollMode"));
|
||||
|
||||
// Create the chat message
|
||||
if ( createMessage ) return ChatMessage.create(chatData);
|
||||
else return chatData;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* For items which consume a resource, handle the consumption of that resource when the item is used.
|
||||
* There are four types of ability consumptions which are handled:
|
||||
* 1. Ammunition (on attack rolls)
|
||||
* 2. Attributes (on card usage)
|
||||
* 3. Materials (on card usage)
|
||||
* 4. Item Charges (on card usage)
|
||||
*
|
||||
* @param {boolean} isCard Is the item card being played?
|
||||
* @param {boolean} isAttack Is an attack roll being made?
|
||||
* @return {Promise<boolean>} Can the item card or attack roll be allowed to proceed?
|
||||
* @private
|
||||
*/
|
||||
async _handleResourceConsumption({isCard=false, isAttack=false}={}) {
|
||||
const itemData = this.data.data;
|
||||
const consume = itemData.consume || {};
|
||||
if ( !consume.type ) return true;
|
||||
const actor = this.actor;
|
||||
const typeLabel = CONFIG.SW5E.abilityConsumptionTypes[consume.type];
|
||||
|
||||
// Only handle certain types for certain actions
|
||||
if ( ((consume.type === "ammo") && !isAttack ) || ((consume.type !== "ammo") && !isCard) ) return true;
|
||||
|
||||
// No consumed target set
|
||||
if ( !consume.target ) {
|
||||
ui.notifications.warn(game.i18n.format("SW5E.ConsumeWarningNoResource", {name: this.name, type: typeLabel}));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Identify the consumed resource and it's quantity
|
||||
let consumed = null;
|
||||
let amount = parseInt(consume.amount || 1);
|
||||
let quantity = 0;
|
||||
switch ( consume.type ) {
|
||||
case "attribute":
|
||||
consumed = getProperty(actor.data.data, consume.target);
|
||||
quantity = consumed || 0;
|
||||
break;
|
||||
case "ammo":
|
||||
case "material":
|
||||
consumed = actor.items.get(consume.target);
|
||||
quantity = consumed ? consumed.data.data.quantity : 0;
|
||||
break;
|
||||
case "charges":
|
||||
consumed = actor.items.get(consume.target);
|
||||
if ( !consumed ) break;
|
||||
const uses = consumed.data.data.uses;
|
||||
if ( uses.per && uses.max ) quantity = uses.value;
|
||||
else if ( consumed.data.data.recharge?.value ) {
|
||||
quantity = consumed.data.data.recharge.charged ? 1 : 0;
|
||||
amount = 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Verify that the consumed resource is available
|
||||
if ( [null, undefined].includes(consumed) ) {
|
||||
ui.notifications.warn(game.i18n.format("SW5E.ConsumeWarningNoSource", {name: this.name, type: typeLabel}));
|
||||
return false;
|
||||
}
|
||||
let remaining = quantity - amount;
|
||||
if ( remaining < 0) {
|
||||
ui.notifications.warn(game.i18n.format("SW5E.ConsumeWarningNoQuantity", {name: this.name, type: typeLabel}));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update the consumed resource
|
||||
switch ( consume.type ) {
|
||||
case "attribute":
|
||||
await this.actor.update({[`data.${consume.target}`]: remaining});
|
||||
break;
|
||||
case "ammo":
|
||||
case "material":
|
||||
await consumed.update({"data.quantity": remaining});
|
||||
break;
|
||||
case "charges":
|
||||
const uses = consumed.data.data.uses || {};
|
||||
const recharge = consumed.data.data.recharge || {};
|
||||
if ( uses.per && uses.max ) await consumed.update({"data.uses.value": remaining});
|
||||
else if ( recharge.value ) await consumed.update({"data.recharge.charged": false});
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Additional rolling steps when rolling a feat-type item
|
||||
* @private
|
||||
* @return {boolean} whether the roll should be prevented
|
||||
*/
|
||||
async _rollFeat(configureDialog) {
|
||||
if ( this.data.type !== "feat" ) throw new Error("Wrong Item type");
|
||||
|
||||
// Configure whether to consume a limited use or to place a template
|
||||
const charge = this.data.data.recharge;
|
||||
const uses = this.data.data.uses;
|
||||
let usesCharges = !!uses.per && !!uses.max;
|
||||
let placeTemplate = false;
|
||||
let consume = charge.value || usesCharges;
|
||||
|
||||
// Determine whether the feat uses charges
|
||||
configureDialog = configureDialog && (consume || this.hasAreaTarget);
|
||||
if ( configureDialog ) {
|
||||
const usage = await AbilityUseDialog.create(this);
|
||||
if ( usage === null ) return false;
|
||||
consume = Boolean(usage.get("consumeUse"));
|
||||
placeTemplate = Boolean(usage.get("placeTemplate"));
|
||||
}
|
||||
|
||||
// Update Item data
|
||||
const current = getProperty(this.data, "data.uses.value") || 0;
|
||||
if ( consume && charge.value ) {
|
||||
if ( !charge.charged ) {
|
||||
ui.notifications.warn(game.i18n.format("SW5E.ItemNoUses", {name: this.name}));
|
||||
return false;
|
||||
}
|
||||
else await this.update({"data.recharge.charged": false});
|
||||
}
|
||||
else if ( consume && usesCharges ) {
|
||||
if ( uses.value <= 0 ) {
|
||||
ui.notifications.warn(game.i18n.format("SW5E.ItemNoUses", {name: this.name}));
|
||||
return false;
|
||||
}
|
||||
await this.update({"data.uses.value": Math.max(current - 1, 0)});
|
||||
}
|
||||
|
||||
// Maybe initiate template placement workflow
|
||||
if ( this.hasAreaTarget && placeTemplate ) {
|
||||
const template = AbilityTemplate.fromItem(this);
|
||||
if ( template ) template.drawPreview();
|
||||
if ( this.owner && this.owner.sheet ) this.owner.sheet.minimize();
|
||||
}
|
||||
return true;
|
||||
// Create the Chat Message or return its data
|
||||
return createMessage ? ChatMessage.create(chatData) : chatData;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -507,8 +692,9 @@ export default class Item5e extends Item {
|
|||
const fn = this[`_${this.data.type}ChatData`];
|
||||
if ( fn ) fn.bind(this)(data, labels, props);
|
||||
|
||||
// General equipment properties
|
||||
// Equipment properties
|
||||
if ( data.hasOwnProperty("equipped") && !["loot", "tool"].includes(this.data.type) ) {
|
||||
if ( data.attunement === CONFIG.SW5E.attunementTypes.REQUIRED ) props.push(game.i18n.localize(CONFIG.SW5E.attunements[CONFIG.SW5E.attunementTypes.REQUIRED]));
|
||||
props.push(
|
||||
game.i18n.localize(data.equipped ? "SW5E.Equipped" : "SW5E.Unequipped"),
|
||||
game.i18n.localize(data.proficient ? "SW5E.Proficient" : "SW5E.NotProficient"),
|
||||
|
@ -633,43 +819,35 @@ export default class Item5e extends Item {
|
|||
*/
|
||||
async rollAttack(options={}) {
|
||||
const itemData = this.data.data;
|
||||
const actorData = this.actor.data.data;
|
||||
const flags = this.actor.data.flags.sw5e || {};
|
||||
if ( !this.hasAttack ) {
|
||||
throw new Error("You may not place an Attack Roll with this Item.");
|
||||
}
|
||||
let title = `${this.name} - ${game.i18n.localize("SW5E.AttackRoll")}`;
|
||||
const rollData = this.getRollData();
|
||||
|
||||
// Define Roll bonuses
|
||||
const parts = [`@mod`];
|
||||
if ( (this.data.type !== "weapon") || itemData.proficient ) {
|
||||
parts.push("@prof");
|
||||
}
|
||||
// get the parts and rollData for this item's attack
|
||||
const {parts, rollData} = this.getAttackToHit();
|
||||
|
||||
// Attack Bonus
|
||||
if ( itemData.attackBonus ) parts.push(itemData.attackBonus);
|
||||
const actorBonus = actorData?.bonuses?.[itemData.actionType] || {};
|
||||
if ( actorBonus.attack ) parts.push(actorBonus.attack);
|
||||
|
||||
// Ammunition Bonus
|
||||
// Handle ammunition consumption
|
||||
delete this._ammo;
|
||||
let ammo = null;
|
||||
let ammoUpdate = null;
|
||||
const consume = itemData.consume;
|
||||
if ( consume?.type === "ammo" ) {
|
||||
const ammo = this.actor.items.get(consume.target);
|
||||
if(ammo?.data){
|
||||
ammo = this.actor.items.get(consume.target);
|
||||
if (ammo?.data) {
|
||||
const q = ammo.data.data.quantity;
|
||||
const consumeAmount = consume.amount ?? 0;
|
||||
if ( q && (q - consumeAmount >= 0) ) {
|
||||
this._ammo = ammo;
|
||||
let ammoBonus = ammo.data.data.attackBonus;
|
||||
if ( ammoBonus ) {
|
||||
parts.push("@ammo");
|
||||
rollData["ammo"] = ammoBonus;
|
||||
title += ` [${ammo.name}]`;
|
||||
}
|
||||
title += ` [${ammo.name}]`;
|
||||
}
|
||||
}
|
||||
|
||||
// Get pending ammunition update
|
||||
const usage = this._getUsageUpdates({consumeResource: true});
|
||||
if ( usage === false ) return null;
|
||||
ammoUpdate = usage.resourceUpdates || {};
|
||||
}
|
||||
|
||||
// Compose roll options
|
||||
|
@ -710,9 +888,8 @@ export default class Item5e extends Item {
|
|||
const roll = await d20Roll(rollConfig);
|
||||
if ( roll === false ) return null;
|
||||
|
||||
// Handle resource consumption if the attack roll was made
|
||||
const allowed = await this._handleResourceConsumption({isCard: false, isAttack: true});
|
||||
if ( allowed === false ) return null;
|
||||
// Commit ammunition consumption on attack rolls resource consumption if the attack roll was made
|
||||
if ( ammo && !isObjectEmpty(ammoUpdate) ) await ammo.update(ammoUpdate);
|
||||
return roll;
|
||||
}
|
||||
|
||||
|
@ -722,12 +899,13 @@ export default class Item5e extends Item {
|
|||
* Place a damage roll using an item (weapon, feat, power, or equipment)
|
||||
* Rely upon the damageRoll logic for the core implementation.
|
||||
* @param {MouseEvent} [event] An event which triggered this roll, if any
|
||||
* @param {boolean} [critical] Should damage be rolled as a critical hit?
|
||||
* @param {number} [powerLevel] If the item is a power, override the level for damage scaling
|
||||
* @param {boolean} [versatile] If the item is a weapon, roll damage using the versatile formula
|
||||
* @param {object} [options] Additional options passed to the damageRoll function
|
||||
* @return {Promise<Roll>} A Promise which resolves to the created Roll instance
|
||||
*/
|
||||
rollDamage({event, powerLevel=null, versatile=false, options={}}={}) {
|
||||
rollDamage({critical=false, event=null, powerLevel=null, versatile=false, options={}}={}) {
|
||||
if ( !this.hasDamage ) throw new Error("You may not make a Damage Roll with this Item.");
|
||||
const itemData = this.data.data;
|
||||
const actorData = this.actor.data.data;
|
||||
|
@ -739,12 +917,15 @@ export default class Item5e extends Item {
|
|||
if ( powerLevel ) rollData.item.level = powerLevel;
|
||||
|
||||
// Configure the damage roll
|
||||
const title = `${this.name} - ${game.i18n.localize("SW5E.DamageRoll")}`;
|
||||
const actionFlavor = game.i18n.localize(itemData.actionType === "heal" ? "SW5E.Healing" : "SW5E.DamageRoll");
|
||||
const title = `${this.name} - ${actionFlavor}`;
|
||||
const rollConfig = {
|
||||
event: event,
|
||||
parts: parts,
|
||||
actor: this.actor,
|
||||
critical: critical ?? event?.altKey ?? false,
|
||||
data: rollData,
|
||||
event: event,
|
||||
fastForward: event ? event.shiftKey || event.altKey || event.ctrlKey || event.metaKey : false,
|
||||
parts: parts,
|
||||
title: title,
|
||||
flavor: this.labels.damageTypes.length ? `${title} (${this.labels.damageTypes})` : title,
|
||||
speaker: ChatMessage.getSpeaker({actor: this.actor}),
|
||||
|
@ -780,10 +961,13 @@ export default class Item5e extends Item {
|
|||
parts.push(actorBonus.damage);
|
||||
}
|
||||
|
||||
// Add ammunition damage
|
||||
if ( this._ammo ) {
|
||||
// Handle ammunition damage
|
||||
const ammoData = this._ammo?.data;
|
||||
|
||||
// only add the ammunition damage if the ammution is a consumable with type 'ammo'
|
||||
if ( this._ammo && (ammoData.type === "consumable") && (ammoData.data.consumableType === "ammo") ) {
|
||||
parts.push("@ammo");
|
||||
rollData["ammo"] = this._ammo.data.data.damage.parts.map(p => p[0]).join("+");
|
||||
rollData["ammo"] = ammoData.data.damage.parts.map(p => p[0]).join("+");
|
||||
rollConfig.flavor += ` [${this._ammo.name}]`;
|
||||
delete this._ammo;
|
||||
}
|
||||
|
@ -893,74 +1077,6 @@ export default class Item5e extends Item {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Use a consumable item, deducting from the quantity or charges of the item.
|
||||
* @param {boolean} configureDialog Whether to show a configuration dialog
|
||||
* @return {boolean} Whether further execution should be prevented
|
||||
* @private
|
||||
*/
|
||||
async _rollConsumable(configureDialog) {
|
||||
if ( this.data.type !== "consumable" ) throw new Error("Wrong Item type");
|
||||
const itemData = this.data.data;
|
||||
|
||||
// Determine whether to deduct uses of the item
|
||||
const uses = itemData.uses || {};
|
||||
const autoDestroy = uses.autoDestroy;
|
||||
let usesCharges = !!uses.per && (uses.max > 0);
|
||||
const recharge = itemData.recharge || {};
|
||||
const usesRecharge = !!recharge.value;
|
||||
|
||||
// Display a configuration dialog to confirm the usage
|
||||
let placeTemplate = false;
|
||||
let consume = uses.autoUse || true;
|
||||
if ( configureDialog ) {
|
||||
const usage = await AbilityUseDialog.create(this);
|
||||
if ( usage === null ) return false;
|
||||
consume = Boolean(usage.get("consumeUse"));
|
||||
placeTemplate = Boolean(usage.get("placeTemplate"));
|
||||
}
|
||||
|
||||
// Update Item data
|
||||
if ( consume ) {
|
||||
const current = uses.value || 0;
|
||||
const remaining = usesCharges ? Math.max(current - 1, 0) : current;
|
||||
if ( usesRecharge ) await this.update({"data.recharge.charged": false});
|
||||
else {
|
||||
const q = itemData.quantity;
|
||||
// Case 1, reduce charges
|
||||
if ( remaining ) {
|
||||
await this.update({"data.uses.value": remaining});
|
||||
}
|
||||
// Case 2, reduce quantity
|
||||
else if ( q > 1 ) {
|
||||
await this.update({"data.quantity": q - 1, "data.uses.value": uses.max || 0});
|
||||
}
|
||||
// Case 3, destroy the item
|
||||
else if ( (q <= 1) && autoDestroy ) {
|
||||
await this.actor.deleteOwnedItem(this.id);
|
||||
}
|
||||
// Case 4, reduce item to 0 quantity and 0 charges
|
||||
else if ( (q === 1) ) {
|
||||
await this.update({"data.quantity": q - 1, "data.uses.value": 0});
|
||||
}
|
||||
// Case 5, item unusable, display warning and do nothing
|
||||
else {
|
||||
ui.notifications.warn(game.i18n.format("SW5E.ItemNoUses", {name: this.name}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Maybe initiate template placement workflow
|
||||
if ( this.hasAreaTarget && placeTemplate ) {
|
||||
const template = AbilityTemplate.fromItem(this);
|
||||
if ( template ) template.drawPreview();
|
||||
if ( this.owner && this.owner.sheet ) this.owner.sheet.minimize();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Perform an ability recharge test for an item which uses the d6 recharge mechanic
|
||||
* @return {Promise<Roll>} A Promise which resolves to the created Roll instance
|
||||
|
@ -1013,6 +1129,7 @@ export default class Item5e extends Item {
|
|||
left: window.innerWidth - 710,
|
||||
},
|
||||
halflingLucky: this.actor.getFlag("sw5e", "halflingLucky" ) || false,
|
||||
reliableTalent: (this.data.data.proficient >= 1) && this.actor.getFlag("sw5e", "reliableTalent"),
|
||||
messageData: {"flags.sw5e.roll": {type: "tool", itemId: this.id }}
|
||||
}, options);
|
||||
rollConfig.event = options.event;
|
||||
|
@ -1094,9 +1211,14 @@ export default class Item5e extends Item {
|
|||
case "attack":
|
||||
await item.rollAttack({event}); break;
|
||||
case "damage":
|
||||
await item.rollDamage({event, powerLevel}); break;
|
||||
case "versatile":
|
||||
await item.rollDamage({event, powerLevel, versatile: true}); break;
|
||||
await item.rollDamage({
|
||||
critical: event.altKey,
|
||||
event: event,
|
||||
powerLevel: powerLevel,
|
||||
versatile: action === "versatile"
|
||||
});
|
||||
break;
|
||||
case "formula":
|
||||
await item.rollFormula({event, powerLevel}); break;
|
||||
case "save":
|
||||
|
@ -1109,7 +1231,7 @@ export default class Item5e extends Item {
|
|||
case "toolCheck":
|
||||
await item.rollToolCheck({event}); break;
|
||||
case "placeTemplate":
|
||||
const template = AbilityTemplate.fromItem(item);
|
||||
const template = game.sw5e.canvas.AbilityTemplate.fromItem(item);
|
||||
if ( template ) template.drawPreview();
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -61,6 +61,9 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
data.isFlatDC = getProperty(data.item.data, "save.scaling") === "flat";
|
||||
data.isLine = ["line", "wall"].includes(data.item.data.target?.type);
|
||||
|
||||
// Original maximum uses formula
|
||||
if ( this.item._data.data?.uses?.max ) data.data.uses.max = this.item._data.data.uses.max;
|
||||
|
||||
// Vehicles
|
||||
data.isCrewed = data.item.data.activation?.type === 'crew';
|
||||
data.isMountable = this._isItemMountable(data.item);
|
||||
|
@ -91,7 +94,7 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
ammo[i.id] = `${i.name} (${i.data.data.quantity})`;
|
||||
}
|
||||
return ammo;
|
||||
}, {});
|
||||
}, {[item._id]: `${item.name} (${item.data.quantity})`});
|
||||
}
|
||||
|
||||
// Attributes
|
||||
|
@ -335,7 +338,7 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
|
||||
// Render the Trait Selector dialog
|
||||
new TraitSelector(this.item, {
|
||||
name: a.dataset.edit,
|
||||
name: a.dataset.target,
|
||||
title: label.innerText,
|
||||
choices: Object.entries(CONFIG.SW5E.skills).reduce((obj, e) => {
|
||||
if ( choices.includes(e[0] ) ) obj[e[0]] = e[1];
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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});
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -120,15 +120,15 @@ export const migrateCompendium = async function(pack) {
|
|||
/**
|
||||
* Migrate a single Actor entity to incorporate latest data model changes
|
||||
* Return an Object of updateData to be applied
|
||||
* @param {Actor} actor The actor to Update
|
||||
* @return {Object} The updateData to apply
|
||||
* @param {object} actor The actor data object to update
|
||||
* @return {Object} The updateData to apply
|
||||
*/
|
||||
export const migrateActorData = function(actor) {
|
||||
const updateData = {};
|
||||
|
||||
// Actor Data Updates
|
||||
_migrateActorBonuses(actor, updateData);
|
||||
_migrateActorMovement(actor, updateData);
|
||||
_migrateActorSenses(actor, updateData);
|
||||
|
||||
// Migrate Owned Items
|
||||
if ( !actor.items ) return updateData;
|
||||
|
@ -191,6 +191,7 @@ function cleanActorData(actorData) {
|
|||
*/
|
||||
export const migrateItemData = function(item) {
|
||||
const updateData = {};
|
||||
_migrateItemAttunement(item, updateData);
|
||||
return updateData;
|
||||
};
|
||||
|
||||
|
@ -228,33 +229,83 @@ export const migrateSceneData = function(scene) {
|
|||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Migrate the actor bonuses object
|
||||
* Migrate the actor speed string to movement object
|
||||
* @private
|
||||
*/
|
||||
function _migrateActorBonuses(actor, updateData) {
|
||||
const b = game.system.model.Actor.character.bonuses;
|
||||
for ( let k of Object.keys(actor.data.bonuses || {}) ) {
|
||||
if ( k in b ) updateData[`data.bonuses.${k}`] = b[k];
|
||||
else updateData[`data.bonuses.-=${k}`] = null;
|
||||
function _migrateActorMovement(actorData, updateData) {
|
||||
const ad = actorData.data;
|
||||
|
||||
// Work is needed if old data is present
|
||||
const old = actorData.type === 'vehicle' ? ad?.attributes?.speed : ad?.attributes?.speed?.value;
|
||||
const hasOld = old !== undefined;
|
||||
if ( hasOld ) {
|
||||
|
||||
// If new data is not present, migrate the old data
|
||||
const hasNew = ad?.attributes?.movement?.walk !== undefined;
|
||||
if ( !hasNew && (typeof old === "string") ) {
|
||||
const s = (old || "").split(" ");
|
||||
if ( s.length > 0 ) updateData["data.attributes.movement.walk"] = Number.isNumeric(s[0]) ? parseInt(s[0]) : null;
|
||||
}
|
||||
|
||||
// Remove the old attribute
|
||||
updateData["data.attributes.-=speed"] = null;
|
||||
}
|
||||
return updateData
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Migrate the actor bonuses object
|
||||
* Migrate the actor traits.senses string to attributes.senses object
|
||||
* @private
|
||||
*/
|
||||
function _migrateActorMovement(actor, updateData) {
|
||||
if ( actor.data.attributes?.movement?.walk !== undefined ) return;
|
||||
const s = (actor.data.attributes?.speed?.value || "").split(" ");
|
||||
if ( s.length > 0 ) updateData["data.attributes.movement.walk"] = Number.isNumeric(s[0]) ? parseInt(s[0]) : null;
|
||||
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"] = CONFIG.SW5E.attunementTypes.NONE;
|
||||
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
|
||||
|
|
|
@ -29,8 +29,8 @@ export default class AbilityTemplate extends MeasuredTemplate {
|
|||
|
||||
// Additional type-specific data
|
||||
switch ( templateShape ) {
|
||||
case "cone": // 5e cone RAW should be 53.13 degrees
|
||||
templateData.angle = 53.13;
|
||||
case "cone":
|
||||
templateData.angle = CONFIG.MeasuredTemplate.defaults.angle;
|
||||
break;
|
||||
case "rect": // 5e rectangular AoEs are always cubes
|
||||
templateData.distance = Math.hypot(target.value, target.value);
|
||||
|
@ -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)
|
||||
|
|
|
@ -14,12 +14,13 @@ 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",
|
||||
"systems/sw5e/templates/actors/newActor/parts/swalt-powerbook.html",
|
||||
"systems/sw5e/templates/actors/newActor/parts/swalt-resources.html",
|
||||
"systems/sw5e/templates/actors/newActor/parts/swalt-traits.html",
|
||||
|
|
75
package-lock.json
generated
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"name": "sw5e",
|
||||
"requires": true,
|
||||
"lockfileVersion": 1,
|
||||
"dependencies": {
|
||||
|
@ -89,7 +90,6 @@
|
|||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz",
|
||||
"integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"micromatch": "^3.1.4",
|
||||
"normalize-path": "^2.1.1"
|
||||
|
@ -99,7 +99,6 @@
|
|||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
|
||||
"integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"remove-trailing-separator": "^1.0.1"
|
||||
}
|
||||
|
@ -272,7 +271,6 @@
|
|||
"version": "0.11.2",
|
||||
"resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz",
|
||||
"integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"cache-base": "^1.0.1",
|
||||
"class-utils": "^0.3.5",
|
||||
|
@ -287,7 +285,6 @@
|
|||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
|
||||
"integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-descriptor": "^1.0.0"
|
||||
}
|
||||
|
@ -296,7 +293,6 @@
|
|||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
|
||||
"integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"kind-of": "^6.0.0"
|
||||
}
|
||||
|
@ -305,7 +301,6 @@
|
|||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
|
||||
"integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"kind-of": "^6.0.0"
|
||||
}
|
||||
|
@ -314,7 +309,6 @@
|
|||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
|
||||
"integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-accessor-descriptor": "^1.0.0",
|
||||
"is-data-descriptor": "^1.0.0",
|
||||
|
@ -332,6 +326,7 @@
|
|||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"file-uri-to-path": "1.0.0"
|
||||
}
|
||||
|
@ -349,7 +344,6 @@
|
|||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
|
||||
"integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"arr-flatten": "^1.1.0",
|
||||
"array-unique": "^0.3.2",
|
||||
|
@ -367,7 +361,6 @@
|
|||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
|
||||
"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-extendable": "^0.1.0"
|
||||
}
|
||||
|
@ -388,7 +381,6 @@
|
|||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz",
|
||||
"integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"collection-visit": "^1.0.0",
|
||||
"component-emitter": "^1.2.1",
|
||||
|
@ -419,7 +411,6 @@
|
|||
"version": "2.1.8",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz",
|
||||
"integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"anymatch": "^2.0.0",
|
||||
"async-each": "^1.0.1",
|
||||
|
@ -697,6 +688,7 @@
|
|||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz",
|
||||
"integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"prr": "~1.0.1"
|
||||
}
|
||||
|
@ -782,7 +774,6 @@
|
|||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
|
||||
"integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"debug": "^2.3.3",
|
||||
"define-property": "^0.2.5",
|
||||
|
@ -797,7 +788,6 @@
|
|||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
|
||||
"integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-descriptor": "^0.1.0"
|
||||
}
|
||||
|
@ -806,7 +796,6 @@
|
|||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
|
||||
"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-extendable": "^0.1.0"
|
||||
}
|
||||
|
@ -864,7 +853,6 @@
|
|||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz",
|
||||
"integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"array-unique": "^0.3.2",
|
||||
"define-property": "^1.0.0",
|
||||
|
@ -880,7 +868,6 @@
|
|||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
|
||||
"integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-descriptor": "^1.0.0"
|
||||
}
|
||||
|
@ -889,7 +876,6 @@
|
|||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
|
||||
"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-extendable": "^0.1.0"
|
||||
}
|
||||
|
@ -898,7 +884,6 @@
|
|||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
|
||||
"integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"kind-of": "^6.0.0"
|
||||
}
|
||||
|
@ -907,7 +892,6 @@
|
|||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
|
||||
"integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"kind-of": "^6.0.0"
|
||||
}
|
||||
|
@ -916,7 +900,6 @@
|
|||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
|
||||
"integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-accessor-descriptor": "^1.0.0",
|
||||
"is-data-descriptor": "^1.0.0",
|
||||
|
@ -944,13 +927,13 @@
|
|||
"file-uri-to-path": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="
|
||||
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
|
||||
"optional": true
|
||||
},
|
||||
"fill-range": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
|
||||
"integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"extend-shallow": "^2.0.1",
|
||||
"is-number": "^3.0.0",
|
||||
|
@ -962,7 +945,6 @@
|
|||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
|
||||
"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-extendable": "^0.1.0"
|
||||
}
|
||||
|
@ -982,7 +964,6 @@
|
|||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz",
|
||||
"integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"detect-file": "^1.0.0",
|
||||
"is-glob": "^4.0.0",
|
||||
|
@ -1055,7 +1036,6 @@
|
|||
"version": "1.2.13",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz",
|
||||
"integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"bindings": "^1.5.0",
|
||||
|
@ -1130,7 +1110,6 @@
|
|||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-5.0.5.tgz",
|
||||
"integrity": "sha512-zOZgGGEHPklZNjZQaZ9f41i7F2YwE+tS5ZHrDhbBCk3stwahn5vQxnFmBJZHoYdusR6R1bLSXeGUy/BhctwKzw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"anymatch": "^2.0.0",
|
||||
"async-done": "^1.2.0",
|
||||
|
@ -1180,7 +1159,6 @@
|
|||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.2.tgz",
|
||||
"integrity": "sha512-dvEs27SCZt2ibF29xYgmnwwCYZxdxhQ/+LFWlbAW8y7jt68L/65402Lz3+CKy0Ov4rOs+NERmDq7YlZaDqUIfA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"glob-watcher": "^5.0.3",
|
||||
"gulp-cli": "^2.2.0",
|
||||
|
@ -1192,7 +1170,6 @@
|
|||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.3.0.tgz",
|
||||
"integrity": "sha512-zzGBl5fHo0EKSXsHzjspp3y5CONegCm8ErO5Qh0UzFzk2y4tMvzLWhoDokADbarfZRL2pGpRp7yt6gfJX4ph7A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-colors": "^1.0.1",
|
||||
"archy": "^1.0.0",
|
||||
|
@ -1220,7 +1197,6 @@
|
|||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/gulp-less/-/gulp-less-4.0.1.tgz",
|
||||
"integrity": "sha512-hmM2k0FfQp7Ptm3ZaqO2CkMX3hqpiIOn4OHtuSsCeFym63F7oWlEua5v6u1cIjVUKYsVIs9zPg9vbqTEb/udpA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"accord": "^0.29.0",
|
||||
"less": "2.6.x || ^3.7.1",
|
||||
|
@ -1256,7 +1232,6 @@
|
|||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz",
|
||||
"integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"get-value": "^2.0.6",
|
||||
"has-values": "^1.0.0",
|
||||
|
@ -1267,7 +1242,6 @@
|
|||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz",
|
||||
"integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-number": "^3.0.0",
|
||||
"kind-of": "^4.0.0"
|
||||
|
@ -1277,7 +1251,6 @@
|
|||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz",
|
||||
"integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-buffer": "^1.1.5"
|
||||
}
|
||||
|
@ -1300,7 +1273,8 @@
|
|||
"image-size": {
|
||||
"version": "0.5.5",
|
||||
"resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
|
||||
"integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w="
|
||||
"integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=",
|
||||
"optional": true
|
||||
},
|
||||
"indx": {
|
||||
"version": "0.2.3",
|
||||
|
@ -1322,9 +1296,9 @@
|
|||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
|
||||
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw=="
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
|
||||
},
|
||||
"interpret": {
|
||||
"version": "1.4.0",
|
||||
|
@ -1474,7 +1448,6 @@
|
|||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
|
||||
"integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"kind-of": "^3.0.2"
|
||||
},
|
||||
|
@ -1483,7 +1456,6 @@
|
|||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
|
||||
"integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-buffer": "^1.1.5"
|
||||
}
|
||||
|
@ -1640,7 +1612,6 @@
|
|||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz",
|
||||
"integrity": "sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"extend": "^3.0.0",
|
||||
"findup-sync": "^3.0.0",
|
||||
|
@ -1708,6 +1679,7 @@
|
|||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
|
||||
"integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"pify": "^4.0.1",
|
||||
"semver": "^5.6.0"
|
||||
|
@ -1716,7 +1688,8 @@
|
|||
"pify": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
|
||||
"integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="
|
||||
"integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -1745,7 +1718,6 @@
|
|||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz",
|
||||
"integrity": "sha1-xvNINKDY28OzfCfui7yyfHd1WC4=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"findup-sync": "^2.0.0",
|
||||
"micromatch": "^3.0.4",
|
||||
|
@ -1757,7 +1729,6 @@
|
|||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz",
|
||||
"integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"detect-file": "^1.0.0",
|
||||
"is-glob": "^3.1.0",
|
||||
|
@ -1769,7 +1740,6 @@
|
|||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
|
||||
"integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-extglob": "^2.1.0"
|
||||
}
|
||||
|
@ -1780,7 +1750,6 @@
|
|||
"version": "3.1.10",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
|
||||
"integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"arr-diff": "^4.0.0",
|
||||
"array-unique": "^0.3.2",
|
||||
|
@ -1800,7 +1769,8 @@
|
|||
"mime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
|
||||
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
||||
"optional": true
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.0.4",
|
||||
|
@ -1842,13 +1812,13 @@
|
|||
"nan": {
|
||||
"version": "2.14.2",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz",
|
||||
"integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ=="
|
||||
"integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==",
|
||||
"optional": true
|
||||
},
|
||||
"nanomatch": {
|
||||
"version": "1.2.13",
|
||||
"resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
|
||||
"integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"arr-diff": "^4.0.0",
|
||||
"array-unique": "^0.3.2",
|
||||
|
@ -2187,7 +2157,8 @@
|
|||
"prr": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
|
||||
"integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY="
|
||||
"integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=",
|
||||
"optional": true
|
||||
},
|
||||
"pump": {
|
||||
"version": "2.0.1",
|
||||
|
@ -2245,7 +2216,6 @@
|
|||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz",
|
||||
"integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"graceful-fs": "^4.1.11",
|
||||
"micromatch": "^3.1.10",
|
||||
|
@ -2428,7 +2398,6 @@
|
|||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
|
||||
"integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"base": "^0.11.1",
|
||||
"debug": "^2.2.0",
|
||||
|
@ -2444,7 +2413,6 @@
|
|||
"version": "0.2.5",
|
||||
"resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
|
||||
"integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-descriptor": "^0.1.0"
|
||||
}
|
||||
|
@ -2453,7 +2421,6 @@
|
|||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
|
||||
"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-extendable": "^0.1.0"
|
||||
}
|
||||
|
@ -2787,7 +2754,6 @@
|
|||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz",
|
||||
"integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-number": "^3.0.0",
|
||||
"repeat-string": "^1.6.1"
|
||||
|
@ -2857,7 +2823,8 @@
|
|||
"uglify-to-browserify": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz",
|
||||
"integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc="
|
||||
"integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=",
|
||||
"optional": true
|
||||
},
|
||||
"unc-path-regex": {
|
||||
"version": "0.1.2",
|
||||
|
|
9
package.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"name": "sw5e",
|
||||
"description": "This game system for [Foundry Virtual Tabletop](http://foundryvtt.com) provides character sheet and game system \r support for the SW5E roleplaying game.",
|
||||
"main": "sw5e.js",
|
||||
"dependencies": {
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-less": "^4.0.1"
|
||||
}
|
||||
}
|
BIN
packs/Icons/Archetypes/Archaeologist Pursuit.webp
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
packs/Icons/Archetypes/Construction Engineering.webp
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
packs/Icons/Archetypes/Kro Var Order.webp
Normal file
After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 11 KiB |
BIN
packs/Icons/Archetypes/Vonil-Ishu Form.webp
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
packs/Icons/Archetypes/Way of Tutelage.webp
Normal file
After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 770 B After Width: | Height: | Size: 770 B |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
BIN
packs/Icons/Feats/Adaptive Training.webp
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
packs/Icons/Feats/Augmented Cyborg.webp
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
packs/Icons/Feats/Blinding Agility.webp
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
packs/Icons/Feats/Climber.webp
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
packs/Icons/Feats/Close Quarters Caster.webp
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
packs/Icons/Feats/Combat Caster.webp
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
packs/Icons/Feats/Companion Keeper.webp
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
packs/Icons/Feats/Cunning Intellect.webp
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
packs/Icons/Feats/Customized Droid.webp
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
packs/Icons/Feats/Dual Focused Caster.webp
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
packs/Icons/Feats/Exalted Awareness.webp
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
packs/Icons/Feats/Expert Potency.webp
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
packs/Icons/Feats/Focused Vitality.webp
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
packs/Icons/Feats/Investigative Attunement.webp
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
packs/Icons/Feats/Kinetic Stoicism.webp
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
packs/Icons/Feats/Mariner.webp
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
packs/Icons/Feats/Martial Adept.webp
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
packs/Icons/Feats/Meditative Mindfulness.webp
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
packs/Icons/Feats/Mounted Caster.webp
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
packs/Icons/Feats/Overwhelming Presence.webp
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
packs/Icons/Feats/Precision Applications.webp
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
packs/Icons/Feats/Prone Combatant.webp
Normal file
After Width: | Height: | Size: 9.3 KiB |
BIN
packs/Icons/Feats/Quick Caster.webp
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
packs/Icons/Feats/Relentless Pursuer.webp
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
packs/Icons/Feats/Savage Shorty.webp
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
packs/Icons/Feats/Serene Resolve.webp
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
packs/Icons/Feats/Shard Modification.webp
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
packs/Icons/Feats/Sniping Caster.webp
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
packs/Icons/Feats/Tiny Terror.webp
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
packs/Icons/Feats/Tireless Outrider.webp
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
packs/Icons/Feats/Titan's Power.webp
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
packs/Icons/Feats/Unnatural Resilience.webp
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
packs/Icons/Feats/Versatile Design.webp
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
packs/Icons/Feats/War Caster.webp
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
packs/Icons/Lightsaber Forms/Vonil-Ishu Form.webp
Normal file
After Width: | Height: | Size: 6.6 KiB |
BIN
packs/Icons/Martial Blasters/Blaster Cannon.webp
Normal file
After Width: | Height: | Size: 9.5 KiB |
BIN
packs/Icons/Martial Blasters/Carbine Rifle.webp
Normal file
After Width: | Height: | Size: 8.2 KiB |
BIN
packs/Icons/Martial Blasters/Chaingun.webp
Normal file
After Width: | Height: | Size: 8.1 KiB |
BIN
packs/Icons/Martial Blasters/Cycler Rifle.webp
Normal file
After Width: | Height: | Size: 7.8 KiB |
BIN
packs/Icons/Martial Blasters/Heavy Bowcaster.webp
Normal file
After Width: | Height: | Size: 9.6 KiB |
BIN
packs/Icons/Martial Blasters/Heavy Repeater.webp
Normal file
After Width: | Height: | Size: 9.7 KiB |
BIN
packs/Icons/Martial Blasters/Heavy Shotgun.webp
Normal file
After Width: | Height: | Size: 9.2 KiB |
BIN
packs/Icons/Martial Blasters/Heavy Slugpistol.webp
Normal file
After Width: | Height: | Size: 8.6 KiB |
BIN
packs/Icons/Martial Blasters/Hunting Rifle.webp
Normal file
After Width: | Height: | Size: 6.9 KiB |