Compare commits

..

No commits in common. "master" and "ExileofBrokenSky-ExileOfBrokenSky" have entirely different histories.

663 changed files with 17512 additions and 30688 deletions

BIN
.DS_Store vendored

Binary file not shown.

View file

@ -1,24 +0,0 @@
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

3
.gitignore vendored
View file

@ -22,9 +22,6 @@
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid* hs_err_pid*
# Mac-OS file
.DS_Store
# IDE Folders # IDE Folders
.idea/ .idea/
.vs/ .vs/

View file

@ -1,14 +0,0 @@
{
"printWidth": 120,
"tabWidth": 4,
"useTabs": false,
"semi": true,
"singleQuote": false,
"quoteProps": "consistent",
"jsxSingleQuote": false,
"trailingComma": "none",
"bracketSpacing": false,
"jsxBracketSameLine": false,
"arrowParens": "always",
"endOfLine": "lf"
}

View file

@ -1,3 +0,0 @@
{
"editor.formatOnSave": true
}

View file

@ -1,8 +0,0 @@
Rick Fisto
- [Fisto's Codex](https://www.gmbinder.com/share/-M-qA_FYgTwJjU8yFjjx)
Heresy
- [Heritic's Guide to the Galaxy](https://www.gmbinder.com/share/-M815p5BfQ0wbdKY7zqN)
Erikstormtrooper
- [Englibesh Font](http://www.erikstormtrooper.com/englibesh.htm)

View file

@ -1,7 +1,5 @@
# Foundry Virtual Tabletop - SW5e Game System # 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 This game system for [Foundry Virtual Tabletop](http://foundryvtt.com) provides character sheet and game system
support for the SW5E roleplaying game. support for the SW5E roleplaying game.
@ -27,10 +25,3 @@ may do this by cloning the repository or downloading a zip archive from the
Code and content contributions are accepted. Please feel free to submit issues to the issue tracker or submit merge Code and content contributions are accepted. Please feel free to submit issues to the issue tracker or submit merge
requests for code changes. Approval for such requests involves code and (if necessary) design review by The Dev Team. requests for code changes. Approval for such requests involves code and (if necessary) design review by The Dev Team.
Please reach out on the SW5E Foundry Dev Discord with any questions. Please reach out on the SW5E Foundry Dev Discord with any questions.
## Compatible Modules and Optimum Settings
- DAE (Dynamic Active Effects) is needed for many automatic features.
- **Please enable: "Include active effects in special traits display" in "Configure Game Settings> Module Settings> Dynamic Active Effects".**
- Midi QoL is compatible with great features
- Token Action Hud has compatibility

Binary file not shown.

View file

@ -1,28 +1,31 @@
const gulp = require("gulp"); const gulp = require('gulp');
const less = require("gulp-less"); const less = require('gulp-less');
/* ----------------------------------------- */ /* ----------------------------------------- */
/* Compile LESS /* Compile LESS
/* ----------------------------------------- */ /* ----------------------------------------- */
const SW5E_LESS = ["less/**/*.less"]; const SW5E_LESS = ["less/**/*.less"];
function compileLESS() { 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() { 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() { 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() { 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); const css = gulp.series(compileLESS, compileGlobalLess, compileLightLess, compileDarkLess);
/* ----------------------------------------- */ /* ----------------------------------------- */
@ -37,5 +40,8 @@ function watchUpdates() {
/* Export Tasks /* Export Tasks
/* ----------------------------------------- */ /* ----------------------------------------- */
exports.default = css; exports.default = gulp.series(
gulp.parallel(css), (exports.watch = gulp.series(gulp.parallel(css), watchUpdates)); gulp.parallel(css),
watchUpdates
);
exports.css = css;

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -69,31 +69,14 @@
line-height: 30px; line-height: 30px;
} }
} }
// Movement Configuration
.movement, .hit-dice {
h4.attribute-name {
position: relative;
}
.config-button {
position: absolute;
display: none;
right: 0;
top: 1px;
font-size: 12px;
font-weight: normal;
}
&:hover .config-button {
display: block;
} }
} }
// Temporary HP .attributes {
input.temphp { input.temphp {
width: 48%; width: 48%;
} }
} }
}
/* ----------------------------------------- */ /* ----------------------------------------- */
/* General Styles */ /* General Styles */
@ -358,7 +341,7 @@
margin: 0 0 3px 0; margin: 0 0 3px 0;
justify-content: space-between; justify-content: space-between;
} }
.config-button { .configure-flags {
flex: 1; flex: 1;
} }
@ -400,8 +383,7 @@
.tab.features, .tab.features,
.tab.inventory, .tab.inventory,
.tab.force-powerbook, .tab.powerbook {
.tab.tech-powerbook {
overflow-y: hidden; overflow-y: hidden;
} }
@ -446,8 +428,12 @@
&.rollable .item-image:hover { &.rollable .item-image:hover {
background-image: url("../../icons/svg/d20-black.svg") !important; background-image: url("../../icons/svg/d20-black.svg") !important;
} }
i.attuned { color: @colorTan; } i.attuned {
i.not-attuned { color: @colorCrimson; } color: @colorTan;
}
h4 {
font-size: 14px;
}
} }
// Item uses // Item uses
@ -488,7 +474,6 @@
overflow: hidden; overflow: hidden;
&:last-child { border-right: none; } &:last-child { border-right: none; }
&.item-action {flex: 0 0 100px} &.item-action {flex: 0 0 100px}
&.attunement {flex: 0 0 24px}
} }
.item-weight { .item-weight {
flex: 0 0 60px; flex: 0 0 60px;
@ -591,23 +576,26 @@
.powercasting-ability { .powercasting-ability {
flex: 0 0 240px; flex: 0 0 240px;
margin: 0; margin: 0;
label, span {
flex: none; input, span {
} flex: 0 0 32px;
input {
flex: 0 0 28px;
text-align: center; text-align: center;
} }
select { select {
margin: 0 5px; margin: 0 5px;
flex: 0 0 120px; flex: 0 0 150px;
}
h3.power-dc {
flex: 1;
text-align: right;
} }
} }
.power-slots, .power-slots,
.power-comps { .power-comps {
flex: none; flex: 0 0 75px;
padding: 0 5px; padding-right: 5px;
text-align: right;
font-size: 12px; font-size: 12px;
color: @colorTan; color: @colorTan;
border-right: 1px solid @colorFaint; border-right: 1px solid @colorFaint;
@ -624,10 +612,9 @@
} }
} }
.powerbook .power-uses { .power-uses {
padding-right: 5px; padding-right: 8px;
text-align: right; text-align: right !important;
color: @colorTan;
} }
.power-school, .power-action, .power-target { .power-school, .power-action, .power-target {
@ -655,15 +642,6 @@
// Empty powerbook controls // Empty powerbook controls
.powerbook-empty .item-controls { flex: 1; } .powerbook-empty .item-controls { flex: 1; }
/* ----------------------------------------- */
/* Features Tab */
/* ----------------------------------------- */
// Original class icon
.features i.original-class {
color: #4b4a44
}
/* ----------------------------------------- */ /* ----------------------------------------- */
/* TinyMCE */ /* TinyMCE */
/* ----------------------------------------- */ /* ----------------------------------------- */
@ -685,6 +663,5 @@
padding-right: 8px; padding-right: 8px;
margin-bottom: 4px; margin-bottom: 4px;
overflow-y: auto; overflow-y: auto;
scrollbar-width: thin;
} }
} }

View file

@ -10,7 +10,9 @@
.sw5e { .sw5e {
.window-content { .window-content {
background: @sheetBackground;
font-size: 13px; font-size: 13px;
color: @colorDark;
} }
/* ----------------------------------------- */ /* ----------------------------------------- */
@ -42,8 +44,6 @@
select:disabled, select:disabled,
textarea:disabled { textarea:disabled {
color: @colorOlive; color: @colorOlive;
border: 1px solid transparent !important;
outline: none !important;
&:hover, &:hover,
&:focus { &:focus {
box-shadow: none !important; box-shadow: none !important;
@ -58,6 +58,28 @@
border: @borderGroove; border: @borderGroove;
} }
// Checkbox Labels
// TODO: THIS CAN BE MOSTLY REMOVED NOW THAT IT IS IN CORE, see core forms.less
label.checkbox {
flex: auto;
padding: 0;
margin: 0;
height: 22px;
line-height: 22px;
font-size: 11px;
> input[type="checkbox"] {
width: 16px;
height: 16px;
margin: 0 2px 0 0;
position: relative;
top: 4px;
}
&.right > input[type="checkbox"] {
margin: 0 0 0 2px;
}
}
/* Form Groups */ /* Form Groups */
.form-group { .form-group {
label { label {
@ -76,12 +98,11 @@
// Stacked Groups // Stacked Groups
.form-group.stacked { .form-group.stacked {
> label { label {
flex: 0 0 100%; flex: 0 0 100%;
margin: 0; margin: 0;
} }
label.checkbox, label.checkbox {
label.radio {
flex: auto; flex: auto;
text-align: left; text-align: left;
} }
@ -110,34 +131,6 @@
} }
/* ----------------------------------------- */
/* Hit Dice Config Sheet Specifically */
/* ----------------------------------------- */
.sw5e.hd-config {
.form-group {
button.increment, button.decrement {
flex: 0 0 1rem;
line-height: 1rem;
}
button.decrement {
margin-right: 0;
}
span.sep {
margin: 0;
}
input {
flex: 0 0 2rem;
text-align: center;
margin-left: 2px;
margin-right: 2px;
}
}
}
/* ----------------------------------------- */ /* ----------------------------------------- */
/* Entity Sheets Specifically */ /* Entity Sheets Specifically */
/* ----------------------------------------- */ /* ----------------------------------------- */
@ -185,14 +178,11 @@
background: transparent; background: transparent;
} }
// Rollable Titles // Rollable Links
.editable .rollable:hover { .editable .rollable:hover {
cursor: pointer;
}
.editable h4.rollable:hover,
.editable .rollable:hover > h4 {
color: #000; color: #000;
text-shadow: 0 0 10px red; text-shadow: 0 0 10px red;
cursor: pointer;
} }
// Separators // Separators
@ -316,7 +306,6 @@
/* ----------------------------------------- */ /* ----------------------------------------- */
.filter-list { .filter-list {
align-items: center;
list-style: none; list-style: none;
margin: 0; margin: 0;
padding: 0; padding: 0;
@ -393,30 +382,6 @@
padding: 0; padding: 0;
} }
// Item Name
.item-name {
flex: 2;
margin: 0;
overflow: hidden;
font-size: 13px;
text-align: left;
align-items: center;
h3, h4 {
margin: 0;
white-space: nowrap;
overflow-x: hidden;
}
}
// Control Buttons
.item-controls {
flex: 0 0 60px;
justify-content: space-between;
a {
font-size: 12px;
text-align: center;
}
}
// Individual Item // Individual Item
.item { .item {
align-items: center; align-items: center;
@ -433,6 +398,11 @@
border: none; border: none;
margin-right: 5px; margin-right: 5px;
} }
h4 {
margin: 0;
white-space: nowrap;
overflow-x: hidden;
}
} }
} }
@ -449,13 +419,32 @@
font-size: 12px; font-size: 12px;
text-align: center; text-align: center;
} }
h3 { .item-name {
padding-left: 5px; padding-left: 5px;
//.modesto(); //.modesto();
text-align: left;
font-size: 16px; 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,7 +471,7 @@
/* Trait Selector /* Trait Selector
/* ----------------------------------------- */ /* ----------------------------------------- */
.trait-selector { #trait-selector {
.trait-list { .trait-list {
list-style: none; list-style: none;
margin: 0; margin: 0;
@ -494,93 +483,3 @@
margin: 2px; margin: 2px;
} }
} }
/* ----------------------------------------- */
/* Actor Type Config Sheet Specifically */
/* ----------------------------------------- */
.actor-type {
.trait-list {
display: flex;
flex-wrap: wrap;
li {
flex-basis: 50%;
flex-grow: 1;
}
li.form-group {
flex-basis: 100%;
}
}
label.radio {
display: flex;
flex: auto;
font-size: 12px;
line-height: 20px;
font-weight: normal;
> input[type="radio"] {
margin: 0 5px 0 0;
}
}
li.custom-type input[type="radio"] {
display: none;
}
}
/* ----------------------------------------- */
/* Add Feature Prompt Specifically */
/* ----------------------------------------- */
.sw5e.select-items-prompt {
.dialog-content {
margin-bottom: 1em;
}
.items-list {
margin-top: 0.5em;
}
.item-name > label, .item-image, input {
cursor: pointer;
}
.item-name > label {
align-items: center;
}
}
/* ----------------------------------------- */
/* 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;
}

View file

@ -89,7 +89,7 @@
// Custom Resources // Custom Resources
.resource .attribute-value { .resource .attribute-value {
> input { input {
flex: 0 0 25%; flex: 0 0 25%;
} }
label.recharge { label.recharge {
@ -99,7 +99,6 @@
font-size: 11px; font-size: 11px;
text-align: center; text-align: center;
color: @colorOlive; color: @colorOlive;
align-items: center;
input[type="checkbox"] { input[type="checkbox"] {
height: 14px; height: 14px;
width: 14px; width: 14px;

View file

@ -23,7 +23,7 @@
flex: 1; flex: 1;
margin: 0; margin: 0;
line-height: 36px; line-height: 36px;
.engli-Besh(); .bungeeInline();
color: @colorOlive; color: @colorOlive;
&:hover { &:hover {
color: #111; color: #111;
@ -73,7 +73,7 @@
span { span {
border-right: 2px groove #FFF; border-right: 2px groove #FFF;
padding: 0 3px 0 0; padding: 0 5px 0 0;
font-size: 10px; font-size: 10px;
&:last-child { &:last-child {

View file

@ -106,17 +106,17 @@
&:nth-child(even) { &:nth-child(even) {
width: 150px; width: 150px;
margin: 0.5em 0.5em; margin: 0.5em 0.5em;
padding: 0 10px 0 10px; padding: 0px 10px 0px 10px;
text-align: left; text-align: left;
} }
} }
thead { thead {
border-bottom: 0; border-bottom: 0px;
} }
th { th {
color: #000000; color: #000000;
text-shadow: none; text-shadow: none;
border-bottom: 0; border-bottom: 0px;
background-color: #bdc8cc; background-color: #bdc8cc;
text-transform: none; text-transform: none;
font-weight: bold; font-weight: bold;
@ -129,7 +129,7 @@
&:nth-child(even) { &:nth-child(even) {
width: 150px; width: 150px;
margin: 0.5em 0.5em; margin: 0.5em 0.5em;
padding: 0 10px 0 10px; padding: 0px 10px 0px 10px;
text-align: left; text-align: left;
} }
} }
@ -137,7 +137,7 @@
.medtable { .medtable {
table { table {
width: 500px; width: 500px;
border: 0; border: 0px;
margin: 0.5em 0.5em; margin: 0.5em 0.5em;
} }
td { td {
@ -149,17 +149,17 @@
&:nth-child(even) { &:nth-child(even) {
width: 450px; width: 450px;
margin: 0.5em 0.5em; margin: 0.5em 0.5em;
padding: 0 10px 0 0; padding: 0px 10px 0px 0px;
text-align: left; text-align: left;
} }
} }
thead { thead {
border-bottom: 0; border-bottom: 0px;
} }
th { th {
color: #000000; color: #000000;
text-shadow: none; text-shadow: none;
border-bottom: 0; border-bottom: 0px;
background-color: #bdc8cc; background-color: #bdc8cc;
text-transform: none; text-transform: none;
font-weight: bold; font-weight: bold;
@ -174,8 +174,8 @@
} }
.classtable { .classtable {
blockquote { blockquote {
border-left: 0; border-left: 0px;
border-right: 0; border-right: 0px;
background-color: #bdc8cc; background-color: #bdc8cc;
width: 600px; width: 600px;
h3 { h3 {
@ -189,8 +189,8 @@
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
background: rgba(0, 0, 0, 0.05); background: rgba(0, 0, 0, 0.05);
border-left: 0; border-left: 0px;
border-right: 0; border-right: 0px;
border-top: 0; border-top: 0;
border-bottom: 0; border-bottom: 0;
margin: 0.5em 0; margin: 0.5em 0;
@ -200,7 +200,7 @@
thead { thead {
color: #000000; color: #000000;
text-shadow: none; text-shadow: none;
border-bottom: 0; border-bottom: 0px;
background-color: #bdc8cc; background-color: #bdc8cc;
text-transform: none; text-transform: none;
font-style: normal; font-style: normal;
@ -209,7 +209,7 @@
th { th {
color: #000000; color: #000000;
text-shadow: none; text-shadow: none;
border-bottom: 0; border-bottom: 0px;
background-color: #bdc8cc; background-color: #bdc8cc;
text-transform: none; text-transform: none;
font-style: normal; font-style: normal;
@ -246,7 +246,7 @@
width: 100%; width: 100%;
line-height: 18px; line-height: 18px;
margin-bottom: 15px; margin-bottom: 15px;
border: 0; border: 0 0 0 0;
border-bottom: none; border-bottom: none;
overflow-x: auto; overflow-x: auto;
tbody { tbody {

View file

@ -4,7 +4,7 @@
/* Basic Structure */ /* Basic Structure */
/* ----------------------------------------- */ /* ----------------------------------------- */
.sw5e.sheet.actor.npc { .sw5e.sheet.actor.npc {
min-width: 872px; min-width: 600px;
min-height: 680px; min-height: 680px;
.header-exp { .header-exp {
@ -30,28 +30,12 @@
.summary { .summary {
font-size: 18px; font-size: 18px;
li.creature-type {
display: flex;
justify-content: space-between;
width: 1em;
padding: 0 3px;
span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
.config-button { .powercasting-ability {
display: none; label {
font-size: 12px; flex: none;
font-weight: normal;
line-height: 2em;
}
&:hover .config-button {
display: block;
}
} }
} }
} }

View file

@ -6,3 +6,33 @@
@import "character.less"; @import "character.less";
@import "npc.less"; @import "npc.less";
@import "vehicle.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;
}

View file

@ -19,15 +19,15 @@
font-size: @font-size; font-size: @font-size;
font-weight: 400; font-weight: 400;
} }
/* engli-besh */ /* bungee-inline-regular - latin */
@font-face { @font-face {
font-family: 'Engli-Besh'; font-family: 'Bungee Inline';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: url('./fonts/EngliBesh-KG3W.ttf'); src: url('./fonts/BungeeInline.ttf');
} }
.engli-Besh { .bungeeInline {
font-family: 'Engli-Besh'; font-family: 'Bungee Inline';
font-size: 20px; font-size: 20px;
font-weight: 400; font-weight: 400;
} }

View file

@ -1,10 +1,11 @@
.panel { .panel {
padding: 8px; padding: 8px;
border-radius: 4px; border-radius: 4px;
.dropShadow1(); .dropShadow1();
} }
.sw5e.sheet.actor.character { .sw5e.sheet.actor.character {
min-width: 880px; min-width: 800px;
min-height: 720px; min-height: 720px;
} }
.sw5e.sheet .window-content { .sw5e.sheet .window-content {
@ -33,11 +34,10 @@
} }
.sw5e.sheet.actor { .sw5e.sheet.actor {
input, input, select, textarea {
select,
textarea {
border-color: transparent; border-color: transparent;
background: none; background: none;
} }
.swalt-sheet { .swalt-sheet {
display: grid; display: grid;
@ -54,7 +54,7 @@
grid-template-rows: 1fr 26px auto; grid-template-rows: 1fr 26px auto;
grid-template-columns: 128px 1fr; grid-template-columns: 128px 1fr;
column-gap: 8px; column-gap: 8px;
grid-row-gap: 8px; row-gap: 8px;
img { img {
grid-column-start: 1; grid-column-start: 1;
@ -140,7 +140,6 @@
height: auto; height: auto;
.russoOne(17px); .russoOne(17px);
line-height: 24px; line-height: 24px;
width: 100%;
} }
.proficiency { .proficiency {
@ -165,9 +164,11 @@
.russoOne(22px); .russoOne(22px);
text-align: center; text-align: center;
line-height: 1; line-height: 1;
} }
.attribute-value { .attribute-value {
&.multiple { &.multiple {
display: grid; display: grid;
grid-template-columns: auto 14px auto; grid-template-columns: auto 14px auto;
@ -185,7 +186,7 @@
display: inline-block; display: inline-block;
text-align: right; text-align: right;
padding: 0 3px; padding: 0px 3px;
&:last-child { &:last-child {
text-align: left; text-align: left;
@ -203,6 +204,8 @@
} }
footer { footer {
button { button {
background: none; background: none;
padding: 1px 3px; padding: 1px 3px;
@ -231,8 +234,10 @@
} }
button { button {
font-weight: 400; font-weight: 400;
margin-top: 2px; margin-top: 2px;
} }
span { span {
@ -253,8 +258,8 @@
nav.sheet-navigation { nav.sheet-navigation {
display: grid; display: grid;
grid-template-columns: repeat(7, 1fr); grid-template-columns: repeat(6, 1fr);
column-gap: 8px; column-gap: 16px;
margin: 4px 0; margin: 4px 0;
.item { .item {
@ -322,6 +327,7 @@
&:hover { &:hover {
text-shadow: none; text-shadow: none;
} }
} }
} }
@ -348,6 +354,7 @@
text-shadow: none; text-shadow: none;
} }
} }
} }
.group-list-header, .group-list-header,
@ -413,13 +420,14 @@
&::before { &::before {
font-family: "Font Awesome 5 Free"; font-family: "Font Awesome 5 Free";
font-weight: 900; font-weight: 900;
content: "\f6cf"; content: '\f6cf';
opacity: 0; opacity: 0;
position: absolute; position: absolute;
top: 0; top: 0;
left: 2px; left: 2px;
font-size: 26px; font-size: 26px;
} }
} }
h4 { h4 {
@ -467,7 +475,9 @@
&:hover { &:hover {
text-shadow: none; text-shadow: none;
} }
} }
} }
} }
@ -488,6 +498,7 @@
.item-controls { .item-controls {
grid-column-start: 4; grid-column-start: 4;
} }
} }
.group-grid-powers { .group-grid-powers {
grid-template-columns: auto repeat(5, 100px); grid-template-columns: auto repeat(5, 100px);
@ -498,6 +509,8 @@
padding: 0 4px; padding: 0 4px;
} }
} }
} }
.group-grid-fav-items { .group-grid-fav-items {
grid-template-columns: auto 60px 30px 30px 50px; grid-template-columns: auto 60px 30px 30px 50px;
@ -509,6 +522,7 @@
} }
} }
} }
} }
.tab > .panel { .tab > .panel {
@ -605,6 +619,7 @@
line-height: 1; line-height: 1;
} }
} }
} }
} }
@ -614,6 +629,7 @@
grid-template-columns: 28px auto 18px 28px; grid-template-columns: 28px auto 18px 28px;
align-items: center; align-items: center;
.proficiency-toggle { .proficiency-toggle {
border: none; border: none;
background: none; background: none;
@ -667,7 +683,7 @@
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
grid-gap: 16px; grid-gap: 16px;
grid-row-gap: 8px; row-gap: 8px;
input, input,
select { select {
@ -693,15 +709,13 @@
&:hover { &:hover {
text-shadow: none; text-shadow: none;
} }
}
} }
.powercasting {
text-transform: capitalize;
} }
.languages { .languages {
grid-column-end: span 1; grid-column-end: span 2;
label { label {
&:hover { &:hover {
cursor: pointer; cursor: pointer;
@ -714,11 +728,11 @@
display: inline; display: inline;
&::after { &::after {
content: ","; content: ',';
} }
&:last-child::after { &:last-child::after {
content: ""; content: '';
} }
} }
} }
@ -731,10 +745,11 @@
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
grid-gap: 4px; grid-gap: 4px;
grid-row-gap: 4px; row-gap: 4px;
strong { strong {
font-size: 13px; font-size: 13px;
} }
} }
} }
@ -746,12 +761,13 @@
column-gap: 12px; column-gap: 12px;
.resource { .resource {
h1 { h1 {
border: none; border: none;
margin: 0; margin: 0;
input { input {
font-family: "Russo One"; font-family: 'Russo One';
font-size: 16px; font-size: 16px;
font-weight: 400; font-weight: 400;
text-align: center; text-align: center;
@ -780,7 +796,7 @@
display: block; display: block;
width: 100%; width: 100%;
text-align: right; text-align: right;
padding: 0 3px; padding: 0px 3px;
&:last-child { &:last-child {
text-align: left; text-align: left;
} }
@ -789,6 +805,7 @@
span.value-number { span.value-number {
padding: 1px 4px; padding: 1px 4px;
} }
} }
.attribute-footer { .attribute-footer {
@ -799,6 +816,7 @@
label { label {
text-align: center; text-align: center;
} }
} }
} }
} }
@ -842,11 +860,14 @@
.death-success, .death-success,
.death-fail { .death-fail {
display: inline-block; display: inline-block;
} }
.death-success { .death-success {
margin-right: 8px; margin-right: 8px;
} }
} }
} }
} }
@ -917,89 +938,23 @@
grid-template-rows: 24px auto; grid-template-rows: 24px auto;
} }
} }
.tab.force-powerbook, .tab.powerbook {
.tab.tech-powerbook {
.resource-items {
display: grid;
grid-template-columns: repeat(5, 1fr);
column-gap: 24px;
.resource {
h1 {
border: none;
margin: 0;
font-family: "Russo One";
font-size: 14px;
font-weight: 400;
text-align: center;
margin-bottom: 4px;
border-radius: 0;
}
.attribute-value,
.attribute-value input {
.russoOne(22px);
text-align: center;
line-height: 1;
}
.attribute-value {
display: grid;
grid-template-columns: auto 14px auto;
input {
display: block;
width: 100%;
}
.value-number {
display: block;
width: 100%;
text-align: right;
padding: 0 3px;
&:last-child {
text-align: left;
}
}
span.value-number {
padding: 1px 4px;
}
}
.attribute-footer {
margin: 0;
display: grid;
grid-template-columns: 1fr 1fr;
flex: 0 0 18px;
margin-top: -1px;
line-height: 18px;
font-family: "Signika", sans-serif;
font-size: 12px;
font-weight: 400;
white-space: nowrap;
input {
text-align: center;
}
}
}
}
&>.panel { &>.panel {
grid-template-rows: 64px 32px 24px auto; grid-template-rows: 32px 24px 24px auto;
} }
h3.power-dc { h3.power-dc {
line-height: 24px; line-height: 24px;
} }
.force-powercasting-ability { .powercasting-ability {
display: grid; display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr; grid-template-columns: 2fr 1fr 1fr;
label, label, h3 {
h3 {
.russoOne(13px); .russoOne(13px);
border-bottom: none; border-bottom: none;
} }
.power-dc {
grid-column-start: 3;
}
} }
} }
.tab.biography { .tab.biography {
@ -1018,6 +973,7 @@
section { section {
position: relative; position: relative;
} }
} }
.tab.notes { .tab.notes {
&>.panel { &>.panel {
@ -1038,7 +994,7 @@
} }
&.limited { &.limited {
grid-template-rows: 144px auto; grid-template-rows: 144px auto;
grid-row-gap: 8px; row-gap: 8px;
header { header {
grid-template-rows: 1fr; grid-template-rows: 1fr;
} }
@ -1054,35 +1010,10 @@
h1.character-name { h1.character-name {
align-self: auto; align-self: auto;
} }
.npc-size, .creature-type { .npc-size {
.russoOne(18px); .russoOne(18px);
line-height: 28px; line-height: 28px;
} }
div.creature-type {
display: flex;
justify-content: space-between;
padding: 1px 4px;
border: 1px solid transparent;
overflow-x: auto;
span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.config-button {
display: none;
font-size: 12px;
font-weight: normal;
line-height: 2em;
}
&:hover .config-button {
display: block;
}
}
.attributes { .attributes {
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
footer { footer {
@ -1098,7 +1029,7 @@
} }
} }
nav.sheet-navigation { nav.sheet-navigation {
grid-template-columns: repeat(6, 1fr); grid-template-columns: repeat(4, 1fr);
} }
.tab.attributes { .tab.attributes {
.traits-resources { .traits-resources {
@ -1110,10 +1041,12 @@
margin-left: auto; margin-left: auto;
} }
} }
// section.traits {
// display:block;
// }
} }
} }
.tab.force-powerbook, .tab.powerbook {
.tab.tech-powerbook {
input.powercasting-level { input.powercasting-level {
width: 48px; width: 48px;
} }

View file

@ -382,8 +382,7 @@
} }
.tab.force-powerbook, .tab.powerbook {
.tab.tech-powerbook {
.powercasting-ability { .powercasting-ability {
label, label,
h3 { h3 {
@ -408,9 +407,6 @@
&.npc { &.npc {
.swalt-sheet { .swalt-sheet {
header { header {
div.creature-type:hover {
border-color: @inputBorderFocus;
}
.experience { .experience {
color: @actorProficiencyTextColor; color: @actorProficiencyTextColor;
} }

View file

@ -1,4 +1,4 @@
input[type="text"], input[type="number"], input[type="password"], input[type="date"], input[type="time"], select, textarea, .roundTransition { input[type="text"], input[type="number"], input[type="password"], input[type="date"], input[type="time"], select, textarea {
border-radius: 4px; border-radius: 4px;
transition: all 0.3s; transition: all 0.3s;
&:hover { &:hover {

View file

@ -1,5 +1,6 @@
input[type="text"], input[type="number"], input[type="password"], input[type="date"], input[type="time"], select, textarea { input[type="text"], input[type="number"], input[type="password"], input[type="date"], input[type="time"], select, textarea {
border: 1px solid @inputBorderNormal; border: 1px solid @inputBorderNormal;
color: @inputTextColor;
&:hover { &:hover {
border-color: @inputBorderHover; border-color: @inputBorderHover;
} }

View file

@ -166,12 +166,6 @@
.token-name { .token-name {
text-shadow: none; text-shadow: none;
} }
.ce-image-wrapper {
.token-image {
width: auto;
height: auto;
}
}
h4 { h4 {
color: @colorBlack; color: @colorBlack;
} }
@ -231,7 +225,7 @@
padding-bottom: 4px; padding-bottom: 4px;
.folder { .folder {
& > .folder-header { & > .folder-header {
line-height: initial; line-height: default;
padding: 0 0 0 8px; padding: 0 0 0 8px;
position: relative; position: relative;
border: none; border: none;

View file

@ -95,8 +95,7 @@
} }
#chat-controls { #chat-controls {
.roll-type-select { .roll-type-select {
background: #a9a9a9; background: @inputBackgroundColor;
color: #1C1C1C;
} }
label { label {
color: @bodyFontColor; color: @bodyFontColor;
@ -104,8 +103,7 @@
} }
#chat-form textarea { #chat-form textarea {
background: #a9a9a9; background: @inputBackgroundColor;
color: #1C1C1C;
} }

View file

@ -169,10 +169,6 @@
} }
} }
#chat-controls { #chat-controls {
&.roll-type-select {
background: #4f4f4f;
color: #FFFFFF;
}
padding-top: 4px; padding-top: 4px;
label { label {
color: @colorBlack; color: @colorBlack;
@ -180,7 +176,7 @@
} }
#chat-form textarea { #chat-form textarea {
background: #4f4f4f; background: white;
&:focus { &:focus {
box-shadow: none; box-shadow: none;
outline: none; outline: none;
@ -302,7 +298,7 @@
} }
.folder { .folder {
& > .folder-header { & > .folder-header {
line-height: initial; line-height: default;
padding: 0 0 0 8px; padding: 0 0 0 8px;
position: relative; position: relative;
border: none; border: none;

View file

@ -39,16 +39,6 @@ body.dark-theme {
border-width: 0 0 1px 0; border-width: 0 0 1px 0;
border-bottom: 1px solid @hrColor; border-bottom: 1px solid @hrColor;
} }
select {
color: white;
background-color: rgba(0, 0, 0, 0.5);
}
input[type="text"], input[type="number"], input[type="password"], input[type="date"], input[type="time"], select, textarea {
color: @inputTextColor;
}
@import "components/forms-themes.less"; @import "components/forms-themes.less";
@import "components/sidebar-themes.less"; @import "components/sidebar-themes.less";
@import "components/foundry-nav-themes.less"; @import "components/foundry-nav-themes.less";

View file

@ -48,12 +48,6 @@
font-weight: 400; font-weight: 400;
src: url('./fonts/Aurebesh.ttf'); 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"; @import "_variables.less";
html { html {
@ -83,9 +77,6 @@ html {
body { body {
.openSans(13px, 400); .openSans(13px, 400);
background-image: url('./ui/SW5e-logo.svg');
background-repeat: no-repeat;
background-size: cover;
} }
h1 { h1 {

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

820
module/actor/sheets/base.js Normal file
View file

@ -0,0 +1,820 @@
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";
/**
* Extend the basic ActorSheet class to suppose SW5e-specific logic and functionality.
* This sheet is an Abstract layer which is not used.
* @extends {ActorSheet}
*/
export default class ActorSheet5e extends ActorSheet {
constructor(...args) {
super(...args);
/**
* Track the set of item filters which are applied
* @type {Set}
*/
this._filters = {
inventory: new Set(),
powerbook: new Set(),
features: new Set(),
effects: new Set()
};
}
/* -------------------------------------------- */
/** @override */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
scrollY: [
".inventory .inventory-list",
".features .inventory-list",
".powerbook .inventory-list",
".effects .inventory-list"
],
tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}]
});
}
/* -------------------------------------------- */
/** @override */
get template() {
if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/oldActor/limited-sheet.html";
return `systems/sw5e/templates/actors/oldActor/${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);
// Update traits
this._prepareTraits(data.actor.data.traits);
// Prepare owned items
this._prepareItems(data);
// Prepare active effects
data.effects = prepareActiveEffectCategories(this.entity.effects);
// Return data to the sheet
return data
}
/* -------------------------------------------- */
/**
* Prepare the display of movement speed data for the Actor
* @param {object} actorData
* @returns {{primary: string, special: string}}
* @private
*/
_getMovementSpeed(actorData) {
const movement = actorData.data.attributes.movement;
const speeds = [
[movement.burrow, `${game.i18n.localize("SW5E.MovementBurrow")} ${movement.burrow}`],
[movement.climb, `${game.i18n.localize("SW5E.MovementClimb")} ${movement.climb}`],
[movement.fly, `${game.i18n.localize("SW5E.MovementFly")} ${movement.fly}` + (movement.hover ? ` (${game.i18n.localize("SW5E.MovementHover")})` : "")],
[movement.swim, `${game.i18n.localize("SW5E.MovementSwim")} ${movement.swim}`]
].filter(s => !!s[0]).sort((a, b) => b[0] - a[0]);
return {
primary: `${movement.walk || 0} ${movement.units}`,
special: speeds.length ? speeds.map(s => s[1]).join(", ") : ""
}
}
/* -------------------------------------------- */
/**
* 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];
}
/* -------------------------------------------- */
/* 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('.configure-movement').click(this._onMovementConfig.bind(this));
html.find('.configure-flags').click(this._onConfigureFlags.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 click events for the Traits tab button to configure special Character Flags
*/
_onConfigureFlags(event) {
event.preventDefault();
new ActorSheetFlags(this.actor).render(true);
}
/* -------------------------------------------- */
/**
* 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, {
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;
}
// 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);
// 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();
}
/* -------------------------------------------- */
/**
* 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.createOwnedItem(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)
}
/* -------------------------------------------- */
/**
* 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();
// 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;
}
}

View file

@ -1,989 +0,0 @@
import Item5e from "../../../item/entity.js";
import TraitSelector from "../../../apps/trait-selector.js";
import ActorSheetFlags from "../../../apps/actor-flags.js";
import ActorHitDiceConfig from "../../../apps/hit-dice-config.js";
import ActorMovementConfig from "../../../apps/movement-config.js";
import ActorSensesConfig from "../../../apps/senses-config.js";
import ActorTypeConfig from "../../../apps/actor-type.js";
import {SW5E} from "../../../config.js";
import {onManageActiveEffect, prepareActiveEffectCategories} from "../../../effects.js";
/**
* 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(),
forcePowerbook: new Set(),
techPowerbook: new Set(),
features: new Set(),
effects: new Set()
};
}
/* -------------------------------------------- */
/** @override */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
scrollY: [
".inventory .group-list",
".features .group-list",
".force-powerbook .group-list",
".tech-powerbook .group-list",
".effects .effects-list"
],
tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}]
});
}
/* -------------------------------------------- */
/**
* A set of item types that should be prevented from being dropped on this type of actor sheet.
* @type {Set<string>}
*/
static unsupportedItemTypes = new Set();
/* -------------------------------------------- */
/** @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(options) {
// Basic data
let isOwner = this.actor.isOwner;
const data = {
owner: isOwner,
limited: this.actor.limited,
options: this.options,
editable: this.isEditable,
cssClass: isOwner ? "editable" : "locked",
isCharacter: this.actor.type === "character",
isNPC: this.actor.type === "npc",
isStarship: this.actor.type === "starship",
isVehicle: this.actor.type === "vehicle",
config: CONFIG.SW5E,
rollData: this.actor.getRollData.bind(this.actor)
};
// The Actor's data
const actorData = this.actor.data.toObject(false);
data.actor = actorData;
data.data = actorData.data;
// Owned Items
data.items = actorData.items;
for (let i of data.items) {
const item = this.actor.items.get(i._id);
i.labels = item.labels;
}
data.items.sort((a, b) => (a.sort || 0) - (b.sort || 0));
// Labels and filters
data.labels = this.actor.labels || {};
data.filters = this._filters;
// Ability Scores
for (let [a, abl] of Object.entries(actorData.data.abilities)) {
abl.icon = this._getProficiencyIcon(abl.proficient);
abl.hover = CONFIG.SW5E.proficiencyLevels[abl.proficient];
abl.label = CONFIG.SW5E.abilities[a];
}
// Skills
if (actorData.data.skills) {
for (let [s, skl] of Object.entries(actorData.data.skills)) {
skl.ability = CONFIG.SW5E.abilityAbbreviations[skl.ability];
skl.icon = this._getProficiencyIcon(skl.value);
skl.hover = CONFIG.SW5E.proficiencyLevels[skl.value];
if (data.actor.type === "starship") {
skl.label = CONFIG.SW5E.starshipSkills[s];
} else {
skl.label = CONFIG.SW5E.skills[s];
}
}
}
// Movement speeds
data.movement = this._getMovementSpeed(actorData);
// Senses
data.senses = this._getSenses(actorData);
// Update traits
this._prepareTraits(actorData.data.traits);
// Prepare owned items
this._prepareItems(data);
// Prepare active effects
data.effects = prepareActiveEffectCategories(this.actor.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
* @param {string} school The school of the powerbook being prepared
* @private
*/
_preparePowerbook(data, powers, school) {
const owner = this.actor.isOwner;
const levels = data.data.powers;
const powerbook = {};
// Define some mappings
const sections = {
atwill: -20,
innate: -10
};
// Label power slot uses headers
const useLabels = {
"-20": "-",
"-10": "-",
"0": "&infin;"
};
// 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,
"school": school
},
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]);
}
}
// 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;
if (this.actor.data.type === "starship") 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
/* -------------------------------------------- */
/** @inheritdoc */
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));
// View Item Sheets
html.find(".item-edit").click(this._onItemEdit.bind(this));
// 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-delete").click(this._onItemDelete.bind(this));
html.find(".item-collapse").click(this._onItemCollapse.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));
html.find(".increment-class-level").click(this._onIncrementClassLevel.bind(this));
html.find(".decrement-class-level").click(this._onDecrementClassLevel.bind(this));
// Active Effect management
html.find(".effect-control").click((ev) => onManageActiveEffect(ev, this.actor));
}
// Owner Only Listeners
if (this.actor.isOwner) {
// 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;
let app;
switch (button.dataset.action) {
case "hit-dice":
app = new ActorHitDiceConfig(this.object);
break;
case "movement":
app = new ActorMovementConfig(this.object);
break;
case "flags":
app = new ActorSheetFlags(this.object);
break;
case "senses":
app = new ActorSensesConfig(this.object);
break;
case "type":
new ActorTypeConfig(this.object).render(true);
break;
}
app?.render(true);
}
/* -------------------------------------------- */
/**
* 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.isOwner && 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) {
// Check to make sure items of this type are allowed on this actor
if (this.constructor.unsupportedItemTypes.has(itemData.type)) {
return ui.notifications.warn(
game.i18n.format("SW5E.ActorWarningInvalidItem", {
itemType: game.i18n.localize(CONFIG.Item.typeLabels[itemData.type]),
actorType: game.i18n.localize(CONFIG.Actor.typeLabels[this.actor.type])
})
);
}
// 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;
}
if (itemData.data) {
// Ignore certain statuses
["equipped", "proficient", "prepared"].forEach((k) => delete itemData.data[k]);
// Downgrade ATTUNED to REQUIRED
itemData.data.attunement = Math.min(itemData.data.attunement, CONFIG.SW5E.attunementTypes.REQUIRED);
}
// Stack identical consumables
if (itemData.type === "consumable" && itemData.flags.core?.sourceId) {
const similarItem = this.actor.items.find((i) => {
const sourceId = i.getFlag("core", "sourceId");
return sourceId && sourceId === itemData.flags.core?.sourceId && i.type === "consumable";
});
if (similarItem && itemData.name !== "Power Cell") {
// Always create a new powercell instead of increasing quantity
return similarItem.update({
"data.quantity": similarItem.data.data.quantity + Math.max(itemData.data.quantity, 1)
});
}
}
// 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.items.get(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.items.get(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.items.get(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.items.get(li.data("item-id")),
chatData = item.getChatData({secrets: this.actor.isOwner});
// 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: game.i18n.localize(`SW5E.ItemType${type.capitalize()}`)}),
type: type,
data: foundry.utils.deepClone(header.dataset)
};
delete itemData.data["type"];
return this.actor.createEmbeddedDocuments("Item", [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.items.get(li.dataset.itemId);
return 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");
const item = this.actor.items.get(li.dataset.itemId);
if (item) return item.delete();
}
/**
* Handle collapsing a Feature row on the actor sheet
* @param {Event} event The originating click event
* @private
*/
_onItemCollapse(event) {
event.preventDefault();
event.currentTarget.classList.toggle("active");
const li = event.currentTarget.closest("li");
const content = li.querySelector(".content");
if (content.style.display === "none") {
content.style.display = "block";
} else {
content.style.display = "none";
}
}
/**
* Handle incrementing class level on the actor sheet
* @param {Event} event The originating click event
* @private
*/
_onIncrementClassLevel(event) {
event.preventDefault();
const div = event.currentTarget.closest(".character");
const li = event.currentTarget.closest("li");
const actorId = div.id.split("-")[1];
const itemId = li.dataset.itemId;
const actor = game.actors.get(actorId);
const item = actor.items.get(itemId);
let levels = item.data.data.levels;
const update = {_id: item.data._id, data: {levels: levels + 1}};
actor.updateEmbeddedDocuments("Item", [update]);
}
/**
* Handle decrementing class level on the actor sheet
* @param {Event} event The originating click event
* @private
*/
_onDecrementClassLevel(event) {
event.preventDefault();
const div = event.currentTarget.closest(".character");
const li = event.currentTarget.closest("li");
const actorId = div.id.split("-")[1];
const itemId = li.dataset.itemId;
const actor = game.actors.get(actorId);
const item = actor.items.get(itemId);
let levels = item.data.data.levels;
const update = {_id: item.data._id, data: {levels: levels - 1}};
actor.updateEmbeddedDocuments("Item", [update]);
}
/* -------------------------------------------- */
/**
* 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;
return 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;
return 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;
return 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);
return 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};
return new TraitSelector(this.actor, options).render(true);
}
/* -------------------------------------------- */
/** @override */
_getHeaderButtons() {
let buttons = super._getHeaderButtons();
if (this.actor.isPolymorphed) {
buttons.unshift({
label: "SW5E.PolymorphRestoreTransformation",
class: "restore-transformation",
icon: "fas fa-backward",
onclick: () => this.actor.revertOriginalForm()
});
}
return buttons;
}
}

View file

@ -1,4 +1,4 @@
import ActorSheet5e from "./base.js"; import ActorSheet5e from "../base.js";
import Actor5e from "../../entity.js"; import Actor5e from "../../entity.js";
/** /**
@ -7,6 +7,7 @@ import Actor5e from "../../entity.js";
* @type {ActorSheet5e} * @type {ActorSheet5e}
*/ */
export default class ActorSheet5eCharacterNew extends ActorSheet5e { export default class ActorSheet5eCharacterNew extends ActorSheet5e {
get template() { get template() {
if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/newActor/limited-sheet.html"; if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/newActor/limited-sheet.html";
return "systems/sw5e/templates/actors/newActor/character-sheet.html"; return "systems/sw5e/templates/actors/newActor/character-sheet.html";
@ -16,18 +17,17 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
* @return {Object} * @return {Object}
*/ */
static get defaultOptions() { static get defaultOptions() {
return mergeObject(super.defaultOptions, { return mergeObject(super.defaultOptions, {
classes: ["swalt", "sw5e", "sheet", "actor", "character"], classes: ["swalt", "sw5e", "sheet", "actor", "character"],
blockFavTab: true, blockFavTab: true,
subTabs: null, subTabs: null,
width: 800, width: 800,
tabs: [ tabs: [{
{
navSelector: ".root-tabs", navSelector: ".root-tabs",
contentSelector: ".sheet-body", contentSelector: ".sheet-body",
initial: "attributes" initial: "attributes"
} }],
]
}); });
} }
@ -56,12 +56,7 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
// Experience Tracking // Experience Tracking
sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking"); sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking");
sheetData["classLabels"] = this.actor.itemTypes.class.map((c) => c.name).join(", "); 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 data for rendering
return sheetData; return sheetData;
@ -74,6 +69,7 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
* @private * @private
*/ */
_prepareItems(data) { _prepareItems(data) {
// Categorize items as inventory, powerbook, features, and classes // Categorize items as inventory, powerbook, features, and classes
const inventory = { const inventory = {
weapon: { label: "SW5E.ItemTypeWeaponPl", items: [], dataset: {type: "weapon"} }, weapon: { label: "SW5E.ItemTypeWeaponPl", items: [], dataset: {type: "weapon"} },
@ -85,191 +81,76 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
}; };
// Partition items by category // Partition items by category
let [ let [items, powers, feats, classes, species, archetypes, classfeatures, backgrounds, fightingstyles, fightingmasteries, lightsaberforms] = data.items.reduce((arr, item) => {
items,
forcepowers,
techpowers,
feats,
classes,
deployments,
deploymentfeatures,
ventures,
species,
archetypes,
classfeatures,
backgrounds,
fightingstyles,
fightingmasteries,
lightsaberforms
] = data.items.reduce(
(arr, item) => {
// Item details // Item details
item.img = item.img || CONST.DEFAULT_TOKEN; item.img = item.img || DEFAULT_TOKEN;
item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1; 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 usage
item.hasUses = item.data.uses && item.data.uses.max > 0; item.hasUses = item.data.uses && (item.data.uses.max > 0);
item.isOnCooldown = item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false);
item.data.recharge && !!item.data.recharge.value && item.data.recharge.charged === false; item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0));
item.isDepleted = item.isOnCooldown && item.data.uses.per && item.data.uses.value > 0; item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type));
item.hasTarget = !!item.data.target && !["none", ""].includes(item.data.target.type);
// Item toggle state // Item toggle state
this._prepareItemToggleState(item); this._prepareItemToggleState(item);
// Primary Class
if (item.type === "class")
item.isOriginalClass = item._id === this.actor.data.data.details.originalClass;
// Classify items into types // Classify items into types
if (item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school)) arr[1].push(item); if ( item.type === "power" ) arr[1].push(item);
else if (item.type === "power" && ["tec"].includes(item.data.school)) arr[2].push(item); else if ( item.type === "feat" ) arr[2].push(item);
else if (item.type === "feat") arr[3].push(item); else if ( item.type === "class" ) arr[3].push(item);
else if (item.type === "class") arr[4].push(item); else if ( item.type === "species" ) arr[4].push(item);
else if (item.type === "deployment") arr[5].push(item); else if ( item.type === "archetype" ) arr[5].push(item);
else if (item.type === "deploymentfeature") arr[6].push(item); else if ( item.type === "classfeature" ) arr[6].push(item);
else if (item.type === "venture") arr[7].push(item); else if ( item.type === "background" ) arr[7].push(item);
else if (item.type === "species") arr[8].push(item); else if ( item.type === "fightingstyle" ) arr[8].push(item);
else if (item.type === "archetype") arr[9].push(item); else if ( item.type === "fightingmastery" ) arr[9].push(item);
else if (item.type === "classfeature") arr[10].push(item); else if ( item.type === "lightsaberform" ) arr[10].push(item);
else if (item.type === "background") arr[11].push(item);
else if (item.type === "fightingstyle") arr[12].push(item);
else if (item.type === "fightingmastery") arr[13].push(item);
else if (item.type === "lightsaberform") arr[14].push(item);
else if ( Object.keys(inventory).includes(item.type ) ) arr[0].push(item); else if ( Object.keys(inventory).includes(item.type ) ) arr[0].push(item);
return arr; return arr;
}, }, [[], [], [], [], [], [], [], [], [], [], []]);
[[], [], [], [], [], [], [], [], [], [], [], [], [], [], []]
);
// Apply active item filters // Apply active item filters
items = this._filterItems(items, this._filters.inventory); items = this._filterItems(items, this._filters.inventory);
forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook); powers = this._filterItems(powers, this._filters.powerbook);
techpowers = this._filterItems(techpowers, this._filters.techPowerbook);
feats = this._filterItems(feats, this._filters.features); feats = this._filterItems(feats, this._filters.features);
// Organize items // Organize items
for ( let i of items ) { for ( let i of items ) {
i.data.quantity = i.data.quantity || 0; i.data.quantity = i.data.quantity || 0;
i.data.weight = i.data.weight || 0; i.data.weight = i.data.weight || 0;
i.totalWeight = (i.data.quantity * i.data.weight).toNearest(0.1); i.totalWeight = Math.round(i.data.quantity * i.data.weight * 10) / 10;
inventory[i.type].items.push(i); inventory[i.type].items.push(i);
} }
// Organize Powerbook and count the number of prepared powers (excluding always, at will, etc...) // Organize Powerbook and count the number of prepared powers (excluding always, at will, etc...)
const forcePowerbook = this._preparePowerbook(data, forcepowers, "uni"); const powerbook = this._preparePowerbook(data, powers);
const techPowerbook = this._preparePowerbook(data, techpowers, "tec"); const nPrepared = powers.filter(s => {
return (s.data.level > 0) && (s.data.preparation.mode === "prepared") && s.data.preparation.prepared;
}).length;
// Organize Features // Organize Features
const features = { const features = {
classes: { classes: { label: "SW5E.ItemTypeClassPl", items: [], hasActions: false, dataset: {type: "class"}, isClass: true },
label: "SW5E.ItemTypeClassPl", classfeatures: { label: "SW5E.ItemTypeClassFeats", items: [], hasActions: true, dataset: {type: "classfeature"}, isClassfeature: true },
items: [], archetype: { label: "SW5E.ItemTypeArchetype", items: [], hasActions: false, dataset: {type: "archetype"}, isArchetype: true },
hasActions: false, species: { label: "SW5E.ItemTypeSpecies", items: [], hasActions: false, dataset: {type: "species"}, isSpecies: true },
dataset: {type: "class"}, background: { label: "SW5E.ItemTypeBackground", items: [], hasActions: false, dataset: {type: "background"}, isBackground: true },
isClass: 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 },
classfeatures: { lightsaberforms: { label: "SW5E.ItemTypeLightsaberFormPl", items: [], hasActions: false, dataset: {type: "lightsaberform"}, isLightsaberform: true },
label: "SW5E.ItemTypeClassFeats", active: { label: "SW5E.FeatureActive", items: [], hasActions: true, dataset: {type: "feat", "activation.type": "action"} },
items: [],
hasActions: true,
dataset: {type: "classfeature"},
isClassfeature: true
},
archetype: {
label: "SW5E.ItemTypeArchetype",
items: [],
hasActions: false,
dataset: {type: "archetype"},
isArchetype: true
},
deployments: {
label: "SW5E.ItemTypeDeploymentPl",
items: [],
hasActions: false,
dataset: {type: "deployment"},
isDeployment: true
},
deploymentfeatures: {
label: "SW5E.ItemTypeDeploymentFeaturePl",
items: [],
hasActions: true,
dataset: {type: "deploymentfeature"},
isDeploymentfeature: true
},
ventures: {
label: "SW5E.ItemTypeVenturePl",
items: [],
hasActions: false,
dataset: {type: "venture"},
isVenture: 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"} } passive: { label: "SW5E.FeaturePassive", items: [], hasActions: false, dataset: {type: "feat"} }
}; };
for ( let f of feats ) { for ( let f of feats ) {
if ( f.data.activation.type ) features.active.items.push(f); if ( f.data.activation.type ) features.active.items.push(f);
else features.passive.items.push(f); else features.passive.items.push(f);
} }
classes.sort((a, b) => b.data.levels - a.data.levels); classes.sort((a, b) => b.levels - a.levels);
features.classes.items = classes; features.classes.items = classes;
features.classfeatures.items = classfeatures; features.classfeatures.items = classfeatures;
features.archetype.items = archetypes; features.archetype.items = archetypes;
features.deployments.items = deployments;
features.deploymentfeatures.items = deploymentfeatures;
features.ventures.items = ventures;
features.species.items = species; features.species.items = species;
features.background.items = backgrounds; features.background.items = backgrounds;
features.fightingstyles.items = fightingstyles; features.fightingstyles.items = fightingstyles;
@ -278,8 +159,8 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
// Assign and return // Assign and return
data.inventory = Object.values(inventory); data.inventory = Object.values(inventory);
data.forcePowerbook = forcePowerbook; data.powerbook = powerbook;
data.techPowerbook = techPowerbook; data.preparedPowers = nPrepared;
data.features = Object.values(features); data.features = Object.values(features);
} }
@ -299,7 +180,8 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
if ( isAlways ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.always; if ( isAlways ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.always;
else if ( isPrepared ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.prepared; else if ( isPrepared ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.prepared;
else item.toggleTitle = game.i18n.localize("SW5E.PowerUnprepared"); else item.toggleTitle = game.i18n.localize("SW5E.PowerUnprepared");
} else { }
else {
const isActive = getProperty(item.data, "equipped"); const isActive = getProperty(item.data, "equipped");
item.toggleClass = isActive ? "active" : ""; item.toggleClass = isActive ? "active" : "";
item.toggleTitle = game.i18n.localize(isActive ? "SW5E.Equipped" : "SW5E.Unequipped"); item.toggleTitle = game.i18n.localize(isActive ? "SW5E.Equipped" : "SW5E.Unequipped");
@ -312,35 +194,33 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
/** /**
* Activate event listeners using the prepared sheet HTML * Activate event listeners using the prepared sheet HTML
* @param html {jQuery} The prepared HTML object ready to be rendered into the DOM * @param html {HTML} The prepared HTML object ready to be rendered into the DOM
*/ */
activateListeners(html) { activateListeners(html) {
super.activateListeners(html); super.activateListeners(html);
if (!this.isEditable) return; if ( !this.options.editable ) return;
// Inventory Functions // Inventory Functions
// html.find(".currency-convert").click(this._onConvertCurrency.bind(this)); html.find(".currency-convert").click(this._onConvertCurrency.bind(this));
// Item State Toggling // Item State Toggling
html.find(".item-toggle").click(this._onToggleItem.bind(this)); html.find('.item-toggle').click(this._onToggleItem.bind(this));
// Short and Long Rest // Short and Long Rest
html.find(".short-rest").click(this._onShortRest.bind(this)); html.find('.short-rest').click(this._onShortRest.bind(this));
html.find(".long-rest").click(this._onLongRest.bind(this)); html.find('.long-rest').click(this._onLongRest.bind(this));
// Rollable sheet actions // Death saving throws
html.find(".rollable[data-action]").click(this._onSheetAction.bind(this)); html.find('.death-save').click(this._onDeathSave.bind(this));
// Send Languages to Chat onClick // Send Languages to Chat onClick
html.find('[data-options="share-languages"]').click((event) => { html.find('[data-options="share-languages"]').click(event => {
event.preventDefault(); event.preventDefault();
let langs = this.actor.data.data.traits.languages.value let langs = this.actor.data.data.traits.languages.value.map(l => SW5E.languages[l] || l).join(", ");
.map((l) => CONFIG.SW5E.languages[l] || l)
.join(", ");
let custom = this.actor.data.data.traits.languages.custom; let custom = this.actor.data.data.traits.languages.custom;
if (custom) langs += ", " + custom.replace(/;/g, ","); if (custom) langs += ", " + custom.replace(/;/g, ",");
let content = ` let content = `
<div class="sw5e chat-card item-card" data-acor-id="${this.actor.data._id}"> <div class="sw5e chat-card item-card" data-acor-id="${this.actor._id}">
<header class="card-header flexrow"> <header class="card-header flexrow">
<img src="${this.actor.data.token.img}" title="" width="36" height="36" style="border: none;"/> <img src="${this.actor.data.token.img}" title="" width="36" height="36" style="border: none;"/>
<h3>Known Languages</h3> <h3>Known Languages</h3>
@ -350,50 +230,46 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
`; `;
// Send to Chat // Send to Chat
let rollWhisper = null;
let rollBlind = false; let rollBlind = false;
let rollMode = game.settings.get("core", "rollMode"); let rollMode = game.settings.get("core", "rollMode");
if (["gmroll", "blindroll"].includes(rollMode)) rollWhisper = ChatMessage.getWhisperIDs("GM");
if (rollMode === "blindroll") rollBlind = true; if (rollMode === "blindroll") rollBlind = true;
let data = { ChatMessage.create({
user: game.user.data._id, user: game.user._id,
content: content, content: content,
blind: rollBlind,
speaker: { speaker: {
actor: this.actor.data._id, actor: this.actor._id,
token: this.actor.token, token: this.actor.token,
alias: this.actor.name alias: this.actor.name
}, },
type: CONST.CHAT_MESSAGE_TYPES.OTHER type: CONST.CHAT_MESSAGE_TYPES.OTHER
}; });
if (["gmroll", "blindroll"].includes(rollMode)) data["whisper"] = ChatMessage.getWhisperRecipients("GM");
else if (rollMode === "selfroll") data["whisper"] = [game.users.get(game.user.data._id)];
ChatMessage.create(data);
}); });
// Item Delete Confirmation // Item Delete Confirmation
html.find(".item-delete").off("click"); html.find('.item-delete').off("click");
html.find(".item-delete").click((event) => { html.find('.item-delete').click(event => {
let li = $(event.currentTarget).parents(".item"); let li = $(event.currentTarget).parents('.item');
let itemId = li.attr("data-item-id"); let itemId = li.attr("data-item-id");
let item = this.actor.items.get(itemId); let item = this.actor.getOwnedItem(itemId);
new Dialog({ new Dialog({
title: `Deleting ${item.data.name}`, title: `Deleting ${item.data.name}`,
content: `<p>Are you sure you want to delete ${item.data.name}?</p>`, content: `<p>Are you sure you want to delete ${item.data.name}?</p>`,
buttons: { buttons: {
Yes: { Yes: {
icon: '<i class="fa fa-check"></i>', icon: '<i class="fa fa-check"></i>',
label: "Yes", label: 'Yes',
callback: (dlg) => { callback: dlg => {
this.actor.deleteOwnedItem(itemId); this.actor.deleteOwnedItem(itemId);
} }
}, },
cancel: { cancel: {
icon: '<i class="fas fa-times"></i>', icon: '<i class="fas fa-times"></i>',
label: "No" label: 'No'
}
}, },
default: "cancel" },
default: 'cancel'
}).render(true); }).render(true);
}); });
} }
@ -401,23 +277,18 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
* Handle mouse click events for character sheet actions * Handle rolling a death saving throw for the Character
* @param {MouseEvent} event The originating click event * @param {MouseEvent} event The originating click event
* @private * @private
*/ */
_onSheetAction(event) { _onDeathSave(event) {
event.preventDefault(); event.preventDefault();
const button = event.currentTarget;
switch (button.dataset.action) {
case "rollDeathSave":
return this.actor.rollDeathSave({event: event}); return this.actor.rollDeathSave({event: event});
case "rollInitiative":
return this.actor.rollInitiative({createCombatants: true});
}
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
* Handle toggling the state of an Owned Item within the Actor * Handle toggling the state of an Owned Item within the Actor
* @param {Event} event The triggering click event * @param {Event} event The triggering click event
@ -426,7 +297,7 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
_onToggleItem(event) { _onToggleItem(event) {
event.preventDefault(); event.preventDefault();
const itemId = event.currentTarget.closest(".item").dataset.itemId; const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.items.get(itemId); const item = this.actor.getOwnedItem(itemId);
const attr = item.data.type === "power" ? "data.preparation.prepared" : "data.equipped"; const attr = item.data.type === "power" ? "data.preparation.prepared" : "data.equipped";
return item.update({[attr]: !getProperty(item.data, attr)}); return item.update({[attr]: !getProperty(item.data, attr)});
} }
@ -459,38 +330,57 @@ 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 */ /** @override */
async _onDropItemCreate(itemData) { async _onDropItemCreate(itemData) {
// Increment the number of class levels of a character instead of creating a new item
// Upgrade the number of class levels a character has and add features
if ( itemData.type === "class" ) { if ( itemData.type === "class" ) {
const cls = this.actor.itemTypes.class.find((c) => c.name === itemData.name); const cls = this.actor.itemTypes.class.find(c => c.name === itemData.name);
let priorLevel = cls?.data.data.levels ?? 0; const classWasAlreadyPresent = !!cls;
if (!!cls) {
const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level); // Add new features for class level
if (next > priorLevel) { if ( !classWasAlreadyPresent ) {
itemData.levels = next; Actor5e.getClassFeatures(itemData).then(features => {
return cls.update({"data.levels": next}); 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);
});
}
return
} }
} }
// Increment the number of deployment ranks of a character instead of creating a new item super._onDropItemCreate(itemData);
// else if ( itemData.type === "deployment" ) { }
// const rnk = this.actor.itemTypes.deployment.find(c => c.name === itemData.name); }
// let priorRank = rnk?.data.data.ranks ?? 0;
// if ( !!rnk ) {
// const next = Math.min(priorLevel + 1, 5 + priorRank - this.actor.data.data.details.rank);
// if ( next > priorRank ) {
// itemData.ranks = next;
// return rnk.update({"data.ranks": next});
// }
// }
// }
// Default drop handling if levels were not added
return super._onDropItemCreate(itemData);
}
}
async function addFavorites(app, html, data) { async function addFavorites(app, html, data) {
// Thisfunction is adapted for the SwaltSheet from the Favorites Item // Thisfunction is adapted for the SwaltSheet from the Favorites Item
// Tab Module created for Foundry VTT - by Felix Müller (Felix#6196 on Discord). // Tab Module created for Foundry VTT - by Felix Müller (Felix#6196 on Discord).
@ -548,9 +438,9 @@ async function addFavorites(app, html, data) {
value: data.actor.data.powers.power9.value, value: data.actor.data.powers.power9.value,
max: data.actor.data.powers.power9.max max: data.actor.data.powers.power9.max
} }
}; }
let powerCount = 0; let powerCount = 0
let items = data.actor.items; let items = data.actor.items;
for (let item of items) { for (let item of items) {
if (item.type == "class") continue; if (item.type == "class") continue;
@ -561,28 +451,24 @@ async function addFavorites(app, html, data) {
} }
let isFav = item.flags.favtab.isFavourite; let isFav = item.flags.favtab.isFavourite;
if (app.options.editable) { if (app.options.editable) {
let favBtn = $( let favBtn = $(`<a class="item-control item-toggle item-fav ${isFav ? "active" : ""}" data-fav="${isFav}" title="${isFav ? "Remove from Favourites" : "Add to Favourites"}"><i class="fas fa-star"></i></a>`);
`<a class="item-control item-toggle item-fav ${isFav ? "active" : ""}" data-fav="${isFav}" title="${ favBtn.click(ev => {
isFav ? "Remove from Favourites" : "Add to Favourites" app.actor.getOwnedItem(item._id).update({
}"><i class="fas fa-star"></i></a>`
);
favBtn.click((ev) => {
app.actor.items.get(item.data._id).update({
"flags.favtab.isFavourite": !item.flags.favtab.isFavourite "flags.favtab.isFavourite": !item.flags.favtab.isFavourite
}); });
}); });
html.find(`.item[data-item-id="${item.data._id}"]`).find(".item-controls").prepend(favBtn); html.find(`.item[data-item-id="${item._id}"]`).find('.item-controls').prepend(favBtn);
} }
if (isFav) { if (isFav) {
item.powerComps = ""; item.powerComps = "";
if (item.data.components) { if (item.data.components) {
let comps = item.data.components; let comps = item.data.components;
let v = comps.vocal ? "V" : ""; let v = (comps.vocal) ? "V" : "";
let s = comps.somatic ? "S" : ""; let s = (comps.somatic) ? "S" : "";
let m = comps.material ? "M" : ""; let m = (comps.material) ? "M" : "";
let c = !!comps.concentration; let c = (comps.concentration) ? true : false;
let r = !!comps.ritual; let r = (comps.ritual) ? true : false;
item.powerComps = `${v}${s}${m}`; item.powerComps = `${v}${s}${m}`;
item.powerCon = c; item.powerCon = c;
item.powerRit = r; item.powerRit = r;
@ -590,15 +476,15 @@ async function addFavorites(app, html, data) {
item.editable = app.options.editable; item.editable = app.options.editable;
switch (item.type) { switch (item.type) {
case "feat": case 'feat':
if (item.flags.favtab.sort === undefined) { if (item.flags.favtab.sort === undefined) {
item.flags.favtab.sort = (favFeats.count + 1) * 100000; // initial sort key if not present item.flags.favtab.sort = (favFeats.count + 1) * 100000; // initial sort key if not present
} }
favFeats.push(item); favFeats.push(item);
break; break;
case "power": case 'power':
if (item.data.preparation.mode) { if (item.data.preparation.mode) {
item.powerPrepMode = ` (${CONFIG.SW5E.powerPreparationModes[item.data.preparation.mode]})`; item.powerPrepMode = ` (${CONFIG.SW5E.powerPreparationModes[item.data.preparation.mode]})`
} }
if (item.data.level) { if (item.data.level) {
favPowers[item.data.level].powers.push(item); favPowers[item.data.level].powers.push(item);
@ -624,62 +510,62 @@ async function addFavorites(app, html, data) {
// html.find('.favourite .item-controls').css('flex', '0 0 22px'); // html.find('.favourite .item-controls').css('flex', '0 0 22px');
// } // }
let tabContainer = html.find(".favtabtarget"); let tabContainer = html.find('.favtabtarget');
data.favItems = favItems.length > 0 ? favItems.sort((a, b) => a.flags.favtab.sort - b.flags.favtab.sort) : false; data.favItems = favItems.length > 0 ? favItems.sort((a, b) => (a.flags.favtab.sort) - (b.flags.favtab.sort)) : false;
data.favFeats = favFeats.length > 0 ? favFeats.sort((a, b) => a.flags.favtab.sort - b.flags.favtab.sort) : false; data.favFeats = favFeats.length > 0 ? favFeats.sort((a, b) => (a.flags.favtab.sort) - (b.flags.favtab.sort)) : false;
data.favPowers = powerCount > 0 ? favPowers : false; data.favPowers = powerCount > 0 ? favPowers : false;
data.editable = app.options.editable; data.editable = app.options.editable;
await loadTemplates(["systems/sw5e/templates/actors/newActor/item.hbs"]); await loadTemplates(['systems/sw5e/templates/actors/newActor/item.hbs']);
let favtabHtml = $(await renderTemplate("systems/sw5e/templates/actors/newActor/template.hbs", data)); let favtabHtml = $(await renderTemplate('systems/sw5e/templates/actors/newActor/template.hbs', data));
favtabHtml.find(".item-name h4").click((event) => app._onItemSummary(event)); favtabHtml.find('.item-name h4').click(event => app._onItemSummary(event));
if (app.options.editable) { if (app.options.editable) {
favtabHtml.find(".item-image").click((ev) => app._onItemRoll(ev)); favtabHtml.find('.item-image').click(ev => app._onItemRoll(ev));
let handler = (ev) => app._onDragStart(ev); let handler = ev => app._onDragStart(ev);
favtabHtml.find(".item").each((i, li) => { favtabHtml.find('.item').each((i, li) => {
if (li.classList.contains("inventory-header")) return; if (li.classList.contains("inventory-header")) return;
li.setAttribute("draggable", true); li.setAttribute("draggable", true);
li.addEventListener("dragstart", handler, false); li.addEventListener("dragstart", handler, false);
}); });
//favtabHtml.find('.item-toggle').click(event => app._onToggleItem(event)); //favtabHtml.find('.item-toggle').click(event => app._onToggleItem(event));
favtabHtml.find(".item-edit").click((ev) => { favtabHtml.find('.item-edit').click(ev => {
let itemId = $(ev.target).parents(".item")[0].dataset.itemId; let itemId = $(ev.target).parents('.item')[0].dataset.itemId;
app.actor.items.get(itemId).sheet.render(true); app.actor.getOwnedItem(itemId).sheet.render(true);
}); });
favtabHtml.find(".item-fav").click((ev) => { favtabHtml.find('.item-fav').click(ev => {
let itemId = $(ev.target).parents(".item")[0].dataset.itemId; let itemId = $(ev.target).parents('.item')[0].dataset.itemId;
let val = !app.actor.items.get(itemId).data.flags.favtab.isFavourite; let val = !app.actor.getOwnedItem(itemId).data.flags.favtab.isFavourite
app.actor.items.get(itemId).update({ app.actor.getOwnedItem(itemId).update({
"flags.favtab.isFavourite": val "flags.favtab.isFavourite": val
}); });
}); });
// Sorting // Sorting
favtabHtml.find(".item").on("drop", (ev) => { favtabHtml.find('.item').on('drop', ev => {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
let dropData = JSON.parse(ev.originalEvent.dataTransfer.getData("text/plain")); let dropData = JSON.parse(ev.originalEvent.dataTransfer.getData('text/plain'));
// if (dropData.actorId !== app.actor.id || dropData.data.type === 'power') return; // if (dropData.actorId !== app.actor.id || dropData.data.type === 'power') return;
if (dropData.actorId !== app.actor.id) return; if (dropData.actorId !== app.actor.id) return;
let list = null; let list = null;
if (dropData.data.type === "feat") list = favFeats; if (dropData.data.type === 'feat') list = favFeats;
else list = favItems; else list = favItems;
let dragSource = list.find((i) => i.data._id === dropData.data._id); let dragSource = list.find(i => i._id === dropData.data._id);
let siblings = list.filter((i) => i.data._id !== dropData.data._id); let siblings = list.filter(i => i._id !== dropData.data._id);
let targetId = ev.target.closest(".item").dataset.itemId; let targetId = ev.target.closest('.item').dataset.itemId;
let dragTarget = siblings.find((s) => s.data._id === targetId); let dragTarget = siblings.find(s => s._id === targetId);
if (dragTarget === undefined) return; if (dragTarget === undefined) return;
const sortUpdates = SortingHelpers.performIntegerSort(dragSource, { const sortUpdates = SortingHelpers.performIntegerSort(dragSource, {
target: dragTarget, target: dragTarget,
siblings: siblings, siblings: siblings,
sortKey: "flags.favtab.sort" sortKey: 'flags.favtab.sort'
}); });
const updateData = sortUpdates.map((u) => { const updateData = sortUpdates.map(u => {
const update = u.update; const update = u.update;
update._id = u.target.data._id; update._id = u.target._id;
return update; return update;
}); });
app.actor.updateEmbeddedEntity("OwnedItem", updateData); app.actor.updateEmbeddedEntity("OwnedItem", updateData);
@ -706,44 +592,50 @@ async function addSubTabs(app, html, data) {
if(data.options.subTabs == null) { if(data.options.subTabs == null) {
//let subTabs = []; //{subgroup: '', target: '', active: false} //let subTabs = []; //{subgroup: '', target: '', active: false}
data.options.subTabs = {}; data.options.subTabs = {};
html.find("[data-subgroup-selection] [data-subgroup]").each((idx, el) => { html.find('[data-subgroup-selection] [data-subgroup]').each((idx, el) => {
let subgroup = el.getAttribute("data-subgroup"); let subgroup = el.getAttribute('data-subgroup');
let target = el.getAttribute("data-target"); let target = el.getAttribute('data-target');
let targetObj = {target: target, active: el.classList.contains("active")}; let targetObj = {target: target, active: el.classList.contains("active")}
if(data.options.subTabs.hasOwnProperty(subgroup)) { if(data.options.subTabs.hasOwnProperty(subgroup)) {
data.options.subTabs[subgroup].push(targetObj); data.options.subTabs[subgroup].push(targetObj);
} else { } else {
data.options.subTabs[subgroup] = []; data.options.subTabs[subgroup] = [];
data.options.subTabs[subgroup].push(targetObj); data.options.subTabs[subgroup].push(targetObj);
} }
}); })
} }
for(const group in data.options.subTabs) { for(const group in data.options.subTabs) {
data.options.subTabs[group].forEach((tab) => { data.options.subTabs[group].forEach(tab => {
if(tab.active) { if(tab.active) {
html.find(`[data-subgroup=${group}][data-target=${tab.target}]`).addClass("active"); html.find(`[data-subgroup=${group}][data-target=${tab.target}]`).addClass('active');
} else { } else {
html.find(`[data-subgroup=${group}][data-target=${tab.target}]`).removeClass("active"); html.find(`[data-subgroup=${group}][data-target=${tab.target}]`).removeClass('active');
} }
}); })
} }
html.find("[data-subgroup-selection]") html.find('[data-subgroup-selection]').children().on('click', event => {
.children() let subgroup = event.target.closest('[data-subgroup]').getAttribute('data-subgroup');
.on("click", (event) => { let target = event.target.closest('[data-target]').getAttribute('data-target');
let subgroup = event.target.closest("[data-subgroup]").getAttribute("data-subgroup"); html.find(`[data-subgroup=${subgroup}]`).removeClass('active');
let target = event.target.closest("[data-target]").getAttribute("data-target"); html.find(`[data-subgroup=${subgroup}][data-target=${target}]`).addClass('active');
html.find(`[data-subgroup=${subgroup}]`).removeClass("active"); let tabId = data.options.subTabs[subgroup].find(tab => {
html.find(`[data-subgroup=${subgroup}][data-target=${target}]`).addClass("active"); return tab.target == target
let tabId = data.options.subTabs[subgroup].find((tab) => {
return tab.target == target;
}); });
data.options.subTabs[subgroup].map((el) => { data.options.subTabs[subgroup].map(el => {
el.active = el.target == target; if(el.target == target) {
el.active = true;
} else {
el.active = false;
}
return el; return el;
}); })
});
})
} }
Hooks.on("renderActorSheet5eCharacterNew", (app, html, data) => { Hooks.on("renderActorSheet5eCharacterNew", (app, html, data) => {

View file

@ -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. * An Actor sheet for NPC type characters in the SW5E system.
@ -6,6 +6,7 @@ import ActorSheet5e from "./base.js";
* @extends {ActorSheet5e} * @extends {ActorSheet5e}
*/ */
export default class ActorSheet5eNPCNew extends ActorSheet5e { export default class ActorSheet5eNPCNew extends ActorSheet5e {
/** @override */ /** @override */
get template() { get template() {
if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/newActor/limited-sheet.html"; if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/newActor/limited-sheet.html";
@ -15,73 +16,51 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
static get defaultOptions() { static get defaultOptions() {
return mergeObject(super.defaultOptions, { return mergeObject(super.defaultOptions, {
classes: ["sw5e", "sheet", "actor", "npc"], classes: ["sw5e", "sheet", "actor", "npc"],
width: 600,
width: 800, width: 800,
tabs: [ tabs: [{
{
navSelector: ".root-tabs", navSelector: ".root-tabs",
contentSelector: ".sheet-body", contentSelector: ".sheet-body",
initial: "attributes" initial: "attributes"
} }],
]
}); });
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */
static unsupportedItemTypes = new Set(["class"]);
/* -------------------------------------------- */
/** /**
* Organize Owned Items for rendering the NPC sheet * Organize Owned Items for rendering the NPC sheet
* @private * @private
*/ */
_prepareItems(data) { _prepareItems(data) {
// Categorize Items as Features and Powers // Categorize Items as Features and Powers
const features = { const features = {
weapons: { weapons: { label: game.i18n.localize("SW5E.AttackPl"), items: [] , hasActions: true, dataset: {type: "weapon", "weapon-type": "natural"} },
label: game.i18n.localize("SW5E.AttackPl"), actions: { label: game.i18n.localize("SW5E.ActionPl"), items: [] , hasActions: true, dataset: {type: "feat", "activation.type": "action"} },
items: [],
hasActions: true,
dataset: {"type": "weapon", "weapon-type": "natural"}
},
actions: {
label: game.i18n.localize("SW5E.ActionPl"),
items: [],
hasActions: true,
dataset: {"type": "feat", "activation.type": "action"}
},
passive: { label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"} }, passive: { label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"} },
equipment: { label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}} equipment: { label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}}
}; };
// Start by classifying items into groups for rendering // Start by classifying items into groups for rendering
let [forcepowers, techpowers, other] = data.items.reduce( let [powers, other] = data.items.reduce((arr, item) => {
(arr, item) => { item.img = item.img || DEFAULT_TOKEN;
item.img = item.img || CONST.DEFAULT_TOKEN; item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1; item.hasUses = item.data.uses && (item.data.uses.max > 0);
item.hasUses = item.data.uses && item.data.uses.max > 0; item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false);
item.isOnCooldown = item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0));
item.data.recharge && !!item.data.recharge.value && item.data.recharge.charged === false; item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type));
item.isDepleted = item.isOnCooldown && item.data.uses.per && item.data.uses.value > 0; if ( item.type === "power" ) arr[0].push(item);
item.hasTarget = !!item.data.target && !["none", ""].includes(item.data.target.type); else arr[1].push(item);
if (item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school)) arr[0].push(item);
else if (item.type === "power" && ["tec"].includes(item.data.school)) arr[1].push(item);
else arr[2].push(item);
return arr; return arr;
}, }, [[], []]);
[[], [], []]
);
// Apply item filters // Apply item filters
forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook); powers = this._filterItems(powers, this._filters.powerbook);
techpowers = this._filterItems(techpowers, this._filters.techPowerbook);
other = this._filterItems(other, this._filters.features); other = this._filterItems(other, this._filters.features);
// Organize Powerbook // Organize Powerbook
const forcePowerbook = this._preparePowerbook(data, forcepowers, "uni"); const powerbook = this._preparePowerbook(data, powers);
const techPowerbook = this._preparePowerbook(data, techpowers, "tec");
// Organize Features // Organize Features
for ( let item of other ) { for ( let item of other ) {
@ -89,28 +68,26 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
else if ( item.type === "feat" ) { else if ( item.type === "feat" ) {
if ( item.data.activation.type ) features.actions.items.push(item); if ( item.data.activation.type ) features.actions.items.push(item);
else features.passive.items.push(item); else features.passive.items.push(item);
} else features.equipment.items.push(item); }
else features.equipment.items.push(item);
} }
// Assign and return // Assign and return
data.features = Object.values(features); data.features = Object.values(features);
data.forcePowerbook = forcePowerbook; data.powerbook = powerbook;
data.techPowerbook = techPowerbook;
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @inheritdoc */ /** @override */
getData(options) { getData() {
const data = super.getData(options); const data = super.getData();
// Challenge Rating // Challenge Rating
const cr = parseFloat(data.data.details.cr || 0); const cr = parseFloat(data.data.details.cr || 0);
const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"}; const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"};
data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1; data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1;
// Creature Type
data.labels["type"] = this.actor.labels.creatureType;
return data; return data;
} }
@ -119,7 +96,8 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */ /** @override */
async _updateObject(event, formData) { _updateObject(event, formData) {
// Format NPC Challenge Rating // Format NPC Challenge Rating
const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5}; const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5};
let crv = "data.details.cr"; let crv = "data.details.cr";
@ -128,7 +106,7 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
if ( cr ) formData[crv] = cr < 1 ? cr : parseInt(cr); if ( cr ) formData[crv] = cr < 1 ? cr : parseInt(cr);
// Parent ActorSheet update steps // Parent ActorSheet update steps
return super._updateObject(event, formData); super._updateObject(event, formData);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -138,7 +116,7 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
/** @override */ /** @override */
activateListeners(html) { activateListeners(html) {
super.activateListeners(html); super.activateListeners(html);
html.find(".health .rollable").click(this._onRollHPFormula.bind(this)); html.find(".health .rollable").click(this._onRollHealthFormula.bind(this));
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -148,7 +126,7 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
* @param {Event} event The original click event * @param {Event} event The original click event
* @private * @private
*/ */
_onRollHPFormula(event) { _onRollHealthFormula(event) {
event.preventDefault(); event.preventDefault();
const formula = this.actor.data.data.attributes.hp.formula; const formula = this.actor.data.data.attributes.hp.formula;
if ( !formula ) return; if ( !formula ) return;
@ -157,3 +135,4 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp}); this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp});
} }
} }

View file

@ -1,169 +0,0 @@
import ActorSheet5e from "./base.js";
/**
* An Actor sheet for starships in the SW5E system.
* Extends the base ActorSheet5e class.
* @extends {ActorSheet5e}
*/
export default class ActorSheet5eStarship extends ActorSheet5e {
/** @override */
get template() {
if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/newActor/limited-sheet.html";
return `systems/sw5e/templates/actors/newActor/starship.html`;
}
/** @override */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
classes: ["sw5e", "sheet", "actor", "starship"],
width: 800,
tabs: [
{
navSelector: ".root-tabs",
contentSelector: ".sheet-body",
initial: "attributes"
}
]
});
}
/* -------------------------------------------- */
/**
* Organize Owned Items for rendering the starship sheet
* @private
*/
_prepareItems(data) {
// Categorize Items as Features and Powers
const features = {
weapons: {
label: game.i18n.localize("SW5E.ItemTypeWeaponPl"),
items: [],
hasActions: true,
dataset: {"type": "weapon", "weapon-type": "natural"}
},
passive: {label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"}},
equipment: {label: game.i18n.localize("SW5E.StarshipEquipment"), items: [], dataset: {type: "equipment"}},
starshipfeatures: {
label: game.i18n.localize("SW5E.StarshipfeaturePl"),
items: [],
hasActions: true,
dataset: {type: "starshipfeature"}
},
starshipmods: {
label: game.i18n.localize("SW5E.StarshipmodPl"),
items: [],
hasActions: false,
dataset: {type: "starshipmod"}
}
};
// Start by classifying items into groups for rendering
let [forcepowers, techpowers, other] = data.items.reduce(
(arr, item) => {
item.img = item.img || CONST.DEFAULT_TOKEN;
item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1;
item.hasUses = item.data.uses && item.data.uses.max > 0;
item.isOnCooldown =
item.data.recharge && !!item.data.recharge.value && item.data.recharge.charged === false;
item.isDepleted = item.isOnCooldown && item.data.uses.per && item.data.uses.value > 0;
item.hasTarget = !!item.data.target && !["none", ""].includes(item.data.target.type);
if (item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school)) arr[0].push(item);
else if (item.type === "power" && ["tec"].includes(item.data.school)) arr[1].push(item);
else arr[2].push(item);
return arr;
},
[[], [], []]
);
// Apply item filters
forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook);
techpowers = this._filterItems(techpowers, this._filters.techPowerbook);
other = this._filterItems(other, this._filters.features);
// Organize Powerbook
// const forcePowerbook = this._preparePowerbook(data, forcepowers, "uni");
// const techPowerbook = this._preparePowerbook(data, techpowers, "tec");
// Organize Features
for (let item of other) {
if (item.type === "weapon") features.weapons.items.push(item);
else if (item.type === "feat") {
if (item.data.activation.type) features.actions.items.push(item);
else features.passive.items.push(item);
} else if (item.type === "starshipfeature") {
features.starshipfeatures.items.push(item);
} else if (item.type === "starshipmod") {
features.starshipmods.items.push(item);
} else features.equipment.items.push(item);
}
// Assign and return
data.features = Object.values(features);
// data.forcePowerbook = forcePowerbook;
// data.techPowerbook = techPowerbook;
}
/* -------------------------------------------- */
/** @override */
getData(options) {
const data = super.getData(options);
// Add Size info
data.isTiny = data.actor.data.traits.size === "tiny";
data.isSmall = data.actor.data.traits.size === "sm";
data.isMedium = data.actor.data.traits.size === "med";
data.isLarge = data.actor.data.traits.size === "lg";
data.isHuge = data.actor.data.traits.size === "huge";
data.isGargantuan = data.actor.data.traits.size === "grg";
// Challenge Rating
const cr = parseFloat(data.data.details.cr || 0);
const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"};
data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1;
return data;
}
/* -------------------------------------------- */
/* Object Updates */
/* -------------------------------------------- */
/** @override */
async _updateObject(event, formData) {
// Format NPC Challenge Rating
const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5};
let crv = "data.details.cr";
let cr = formData[crv];
cr = crs[cr] || parseFloat(cr);
if (cr) formData[crv] = cr < 1 ? cr : parseInt(cr);
// Parent ActorSheet update steps
return super._updateObject(event, formData);
}
/* -------------------------------------------- */
/* Event Listeners and Handlers */
/* -------------------------------------------- */
/** @override */
activateListeners(html) {
super.activateListeners(html);
html.find(".health .rollable").click(this._onRollHPFormula.bind(this));
}
/* -------------------------------------------- */
/**
* Handle rolling NPC health values using the provided formula
* @param {Event} event The original click event
* @private
*/
_onRollHPFormula(event) {
event.preventDefault();
const formula = this.actor.data.data.attributes.hp.formula;
if (!formula) return;
const hp = new Roll(formula).roll().total;
AudioHelper.play({src: CONFIG.sounds.dice});
this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp});
}
}

View file

@ -1,432 +0,0 @@
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
});
}
/* -------------------------------------------- */
/** @override */
static unsupportedItemTypes = new Set(["class"]);
/* -------------------------------------------- */
/**
* 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 === 0.5) item.cover = "½";
else if (item.data.cover === 0.75) item.cover = "¾";
else if (item.data.cover === null) item.cover = "—";
if (item.crew < 1 || item.crew === null) item.crew = "—";
}
// Prepare vehicle weapons
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"
}
]
}
};
// Classify items owned by the vehicle and compute total cargo weight
let totalWeight = 0;
for (const item of data.items) {
this._prepareCrewedItem(item);
// Handle cargo explicitly
const isCargo = item.flags.sw5e?.vehicleCargo === true;
if (isCargo) {
totalWeight += (item.data.weight || 0) * item.data.quantity;
cargo.cargo.items.push(item);
continue;
}
// Handle non-cargo item types
switch (item.type) {
case "weapon":
features.weapons.items.push(item);
break;
case "equipment":
features.equipment.items.push(item);
break;
case "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);
break;
default:
totalWeight += (item.data.weight || 0) * item.data.quantity;
cargo.cargo.items.push(item);
}
}
// Update the rendering context data
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.isEditable) 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 = foundry.utils.deepClone(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 = foundry.utils.deepClone(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 = foundry.utils.deepClone(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx);
return this.actor.update({[`data.cargo.${type}`]: cargo});
}
return super._onItemDelete(event);
}
/* -------------------------------------------- */
/** @override */
async _onDropItemCreate(itemData) {
const cargoTypes = ["weapon", "equipment", "consumable", "tool", "loot", "backpack"];
const isCargo = cargoTypes.includes(itemData.type) && this._tabs[0].active === "cargo";
foundry.utils.setProperty(itemData, "flags.sw5e.vehicleCargo", isCargo);
return super._onDropItemCreate(itemData);
}
/* -------------------------------------------- */
/**
* Special handling for editing HP to clamp it within appropriate range.
* @param event {Event}
* @returns {Promise<Item>}
* @private
*/
_onHPChange(event) {
event.preventDefault();
const itemID = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.items.get(itemID);
const hp = Math.clamped(0, parseInt(event.currentTarget.value), item.data.data.hp.max);
event.currentTarget.value = hp;
return item.update({"data.hp.value": hp});
}
/* -------------------------------------------- */
/**
* Handle toggling an item's crewed status.
* @param event {Event}
* @returns {Promise<Item>}
* @private
*/
_onToggleItem(event) {
event.preventDefault();
const itemID = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.items.get(itemID);
const crewed = !!item.data.data.crewed;
return item.update({"data.crewed": !crewed});
}
}

View file

@ -1,920 +0,0 @@
import Item5e from "../../../item/entity.js";
import TraitSelector from "../../../apps/trait-selector.js";
import ActorSheetFlags from "../../../apps/actor-flags.js";
import ActorHitDiceConfig from "../../../apps/hit-dice-config.js";
import ActorMovementConfig from "../../../apps/movement-config.js";
import ActorSensesConfig from "../../../apps/senses-config.js";
import ActorTypeConfig from "../../../apps/actor-type.js";
import {SW5E} from "../../../config.js";
import {onManageActiveEffect, prepareActiveEffectCategories} from "../../../effects.js";
/**
* Extend the basic ActorSheet class to suppose SW5e-specific logic and functionality.
* This sheet is an Abstract layer which is not used.
* @extends {ActorSheet}
*/
export default class ActorSheet5e extends ActorSheet {
constructor(...args) {
super(...args);
/**
* Track the set of item filters which are applied
* @type {Set}
*/
this._filters = {
inventory: new Set(),
powerbook: new Set(),
features: new Set(),
effects: new Set()
};
}
/* -------------------------------------------- */
/** @override */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
scrollY: [
".inventory .inventory-list",
".features .inventory-list",
".powerbook .inventory-list",
".effects .inventory-list"
],
tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}]
});
}
/* -------------------------------------------- */
/**
* A set of item types that should be prevented from being dropped on this type of actor sheet.
* @type {Set<string>}
*/
static unsupportedItemTypes = new Set();
/* -------------------------------------------- */
/** @override */
get template() {
if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/oldActor/limited-sheet.html";
return `systems/sw5e/templates/actors/oldActor/${this.actor.data.type}-sheet.html`;
}
/* -------------------------------------------- */
/** @override */
getData(options) {
// Basic data
let isOwner = this.actor.isOwner;
const data = {
owner: isOwner,
limited: this.actor.limited,
options: this.options,
editable: this.isEditable,
cssClass: isOwner ? "editable" : "locked",
isCharacter: this.actor.type === "character",
isNPC: this.actor.type === "npc",
isStarship: this.actor.type === "starship",
isVehicle: this.actor.type === "vehicle",
config: CONFIG.SW5E,
rollData: this.actor.getRollData.bind(this.actor)
};
// The Actor's data
const actorData = this.actor.data.toObject(false);
data.actor = actorData;
data.data = actorData.data;
// Owned Items
data.items = actorData.items;
for (let i of data.items) {
const item = this.actor.items.get(i._id);
i.labels = item.labels;
}
data.items.sort((a, b) => (a.sort || 0) - (b.sort || 0));
// Labels and filters
data.labels = this.actor.labels || {};
data.filters = this._filters;
// Ability Scores
for (let [a, abl] of Object.entries(actorData.data.abilities)) {
abl.icon = this._getProficiencyIcon(abl.proficient);
abl.hover = CONFIG.SW5E.proficiencyLevels[abl.proficient];
abl.label = CONFIG.SW5E.abilities[a];
}
// Skills
if (actorData.data.skills) {
for (let [s, skl] of Object.entries(actorData.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(actorData);
// Senses
data.senses = this._getSenses(actorData);
// Update traits
this._prepareTraits(actorData.data.traits);
// Prepare owned items
this._prepareItems(data);
// Prepare active effects
data.effects = prepareActiveEffectCategories(this.actor.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.isOwner;
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": "&infin;"
};
// 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
// TODO: Check if this is needed, we've removed pacts everywhere else
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;
const level = game.i18n.localize(`SW5E.PowerLevel${levels.pact.level}`);
const label = `${config}${level}`;
registerSection("pact", sections.pact, label, {
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
/* -------------------------------------------- */
/** @inheritdoc */
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));
// View Item Sheets
html.find(".item-edit").click(this._onItemEdit.bind(this));
// 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-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.actor));
}
// Owner Only Listeners
if (this.actor.isOwner) {
// 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;
let app;
switch (button.dataset.action) {
case "hit-dice":
app = new ActorHitDiceConfig(this.object);
break;
case "movement":
app = new ActorMovementConfig(this.object);
break;
case "flags":
app = new ActorSheetFlags(this.object);
break;
case "senses":
app = new ActorSensesConfig(this.object);
break;
case "type":
new ActorTypeConfig(this.object).render(true);
break;
}
app?.render(true);
}
/* -------------------------------------------- */
/**
* 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.isOwner && 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) {
// Check to make sure items of this type are allowed on this actor
if (this.constructor.unsupportedItemTypes.has(itemData.type)) {
return ui.notifications.warn(
game.i18n.format("SW5E.ActorWarningInvalidItem", {
itemType: game.i18n.localize(CONFIG.Item.typeLabels[itemData.type]),
actorType: game.i18n.localize(CONFIG.Actor.typeLabels[this.actor.type])
})
);
}
// Create a Consumable power scroll on the Inventory tab
// TODO: This is pretty non functional as the base items for the scrolls, and the powers, are not defined, maybe consider using holocrons
if (itemData.type === "power" && this._tabs[0].active === "inventory") {
const scroll = await Item5e.createScrollFromPower(itemData);
itemData = scroll.data;
}
if (itemData.data) {
// Ignore certain statuses
["equipped", "proficient", "prepared"].forEach((k) => delete itemData.data[k]);
// Downgrade ATTUNED to REQUIRED
itemData.data.attunement = Math.min(itemData.data.attunement, CONFIG.SW5E.attunementTypes.REQUIRED);
}
// Stack identical consumables
if (itemData.type === "consumable" && itemData.flags.core?.sourceId) {
const similarItem = this.actor.items.find((i) => {
const sourceId = i.getFlag("core", "sourceId");
return sourceId && sourceId === itemData.flags.core?.sourceId && i.type === "consumable";
});
if (similarItem) {
return similarItem.update({
"data.quantity": similarItem.data.data.quantity + Math.max(itemData.data.quantity, 1)
});
}
}
// 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.items.get(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.items.get(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.items.get(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.items.get(li.data("item-id")),
chatData = item.getChatData({secrets: this.actor.isOwner});
// 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: game.i18n.localize(`SW5E.ItemType${type.capitalize()}`)}),
type: type,
data: foundry.utils.deepClone(header.dataset)
};
delete itemData.data["type"];
return this.actor.createEmbeddedDocuments("Item", [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.items.get(li.dataset.itemId);
return 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");
const item = this.actor.items.get(li.dataset.itemId);
if (item) return item.delete();
}
/* -------------------------------------------- */
/**
* 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;
return 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;
return 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;
return 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);
return 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};
return new TraitSelector(this.actor, options).render(true);
}
/* -------------------------------------------- */
/** @override */
_getHeaderButtons() {
let buttons = super._getHeaderButtons();
if (this.actor.isPolymorphed) {
buttons.unshift({
label: "SW5E.PolymorphRestoreTransformation",
class: "restore-transformation",
icon: "fas fa-backward",
onclick: () => this.actor.revertOriginalForm()
});
}
return buttons;
}
}

View file

@ -1,4 +1,4 @@
import ActorSheet5e from "./base.js"; import ActorSheet5e from "../base.js";
import Actor5e from "../../entity.js"; import Actor5e from "../../entity.js";
/** /**
@ -7,6 +7,7 @@ import Actor5e from "../../entity.js";
* @type {ActorSheet5e} * @type {ActorSheet5e}
*/ */
export default class ActorSheet5eCharacter extends ActorSheet5e { export default class ActorSheet5eCharacter extends ActorSheet5e {
/** /**
* Define default rendering options for the NPC sheet * Define default rendering options for the NPC sheet
* @return {Object} * @return {Object}
@ -44,12 +45,7 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
// Experience Tracking // Experience Tracking
sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking"); sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking");
sheetData["classLabels"] = this.actor.itemTypes.class.map((c) => c.name).join(", "); 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 data for rendering
return sheetData; return sheetData;
@ -62,6 +58,7 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
* @private * @private
*/ */
_prepareItems(data) { _prepareItems(data) {
// Categorize items as inventory, powerbook, features, and classes // Categorize items as inventory, powerbook, features, and classes
const inventory = { const inventory = {
weapon: { label: "SW5E.ItemTypeWeaponPl", items: [], dataset: {type: "weapon"} }, weapon: { label: "SW5E.ItemTypeWeaponPl", items: [], dataset: {type: "weapon"} },
@ -73,50 +70,21 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
}; };
// Partition items by category // Partition items by category
let [ let [items, powers, feats, classes, species, archetypes, classfeatures, backgrounds, fightingstyles, fightingmasteries, lightsaberforms] = data.items.reduce((arr, item) => {
items,
powers,
feats,
classes,
species,
archetypes,
classfeatures,
backgrounds,
fightingstyles,
fightingmasteries,
lightsaberforms
] = data.items.reduce(
(arr, item) => {
// Item details // Item details
item.img = item.img || CONST.DEFAULT_TOKEN; item.img = item.img || DEFAULT_TOKEN;
item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1; 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 usage
item.hasUses = item.data.uses && item.data.uses.max > 0; item.hasUses = item.data.uses && (item.data.uses.max > 0);
item.isOnCooldown = item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false);
item.data.recharge && !!item.data.recharge.value && item.data.recharge.charged === false; item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0));
item.isDepleted = item.isOnCooldown && item.data.uses.per && item.data.uses.value > 0; item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type));
item.hasTarget = !!item.data.target && !["none", ""].includes(item.data.target.type);
// Item toggle state // Item toggle state
this._prepareItemToggleState(item); this._prepareItemToggleState(item);
// Primary Class
if (item.type === "class")
item.isOriginalClass = item._id === this.actor.data.data.details.originalClass;
// Classify items into types // Classify items into types
if ( item.type === "power" ) arr[1].push(item); if ( item.type === "power" ) arr[1].push(item);
else if ( item.type === "feat" ) arr[2].push(item); else if ( item.type === "feat" ) arr[2].push(item);
@ -130,9 +98,7 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
else if ( item.type === "lightsaberform" ) arr[10].push(item); else if ( item.type === "lightsaberform" ) arr[10].push(item);
else if ( Object.keys(inventory).includes(item.type ) ) arr[0].push(item); else if ( Object.keys(inventory).includes(item.type ) ) arr[0].push(item);
return arr; return arr;
}, }, [[], [], [], [], [], [], [], [], [], [], []]);
[[], [], [], [], [], [], [], [], [], [], []]
);
// Apply active item filters // Apply active item filters
items = this._filterItems(items, this._filters.inventory); items = this._filterItems(items, this._filters.inventory);
@ -149,81 +115,28 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
// Organize Powerbook and count the number of prepared powers (excluding always, at will, etc...) // Organize Powerbook and count the number of prepared powers (excluding always, at will, etc...)
const powerbook = this._preparePowerbook(data, powers); const powerbook = this._preparePowerbook(data, powers);
const nPrepared = powers.filter((s) => { const nPrepared = powers.filter(s => {
return s.data.level > 0 && s.data.preparation.mode === "prepared" && s.data.preparation.prepared; return (s.data.level > 0) && (s.data.preparation.mode === "prepared") && s.data.preparation.prepared;
}).length; }).length;
// Organize Features // Organize Features
const features = { const features = {
classes: { classes: { label: "SW5E.ItemTypeClassPl", items: [], hasActions: false, dataset: {type: "class"}, isClass: true },
label: "SW5E.ItemTypeClassPl", classfeatures: { label: "SW5E.ItemTypeClassFeats", items: [], hasActions: true, dataset: {type: "classfeature"}, isClassfeature: true },
items: [], archetype: { label: "SW5E.ItemTypeArchetype", items: [], hasActions: false, dataset: {type: "archetype"}, isArchetype: true },
hasActions: false, species: { label: "SW5E.ItemTypeSpecies", items: [], hasActions: false, dataset: {type: "species"}, isSpecies: true },
dataset: {type: "class"}, background: { label: "SW5E.ItemTypeBackground", items: [], hasActions: false, dataset: {type: "background"}, isBackground: true },
isClass: 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 },
classfeatures: { lightsaberforms: { label: "SW5E.ItemTypeLightsaberFormPl", items: [], hasActions: false, dataset: {type: "lightsaberform"}, isLightsaberform: true },
label: "SW5E.ItemTypeClassFeats", active: { label: "SW5E.FeatureActive", items: [], hasActions: true, dataset: {type: "feat", "activation.type": "action"} },
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"} } passive: { label: "SW5E.FeaturePassive", items: [], hasActions: false, dataset: {type: "feat"} }
}; };
for ( let f of feats ) { for ( let f of feats ) {
if ( f.data.activation.type ) features.active.items.push(f); if ( f.data.activation.type ) features.active.items.push(f);
else features.passive.items.push(f); else features.passive.items.push(f);
} }
classes.sort((a, b) => b.data.levels - a.data.levels); classes.sort((a, b) => b.levels - a.levels);
features.classes.items = classes; features.classes.items = classes;
features.classfeatures.items = classfeatures; features.classfeatures.items = classfeatures;
features.archetype.items = archetypes; features.archetype.items = archetypes;
@ -256,7 +169,8 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
if ( isAlways ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.always; if ( isAlways ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.always;
else if ( isPrepared ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.prepared; else if ( isPrepared ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.prepared;
else item.toggleTitle = game.i18n.localize("SW5E.PowerUnprepared"); else item.toggleTitle = game.i18n.localize("SW5E.PowerUnprepared");
} else { }
else {
const isActive = getProperty(item.data, "equipped"); const isActive = getProperty(item.data, "equipped");
item.toggleClass = isActive ? "active" : ""; item.toggleClass = isActive ? "active" : "";
item.toggleTitle = game.i18n.localize(isActive ? "SW5E.Equipped" : "SW5E.Unequipped"); item.toggleTitle = game.i18n.localize(isActive ? "SW5E.Equipped" : "SW5E.Unequipped");
@ -269,18 +183,18 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
/** /**
* Activate event listeners using the prepared sheet HTML * Activate event listeners using the prepared sheet HTML
* @param html {jQuery} The prepared HTML object ready to be rendered into the DOM * @param html {HTML} The prepared HTML object ready to be rendered into the DOM
*/ */
activateListeners(html) { activateListeners(html) {
super.activateListeners(html); super.activateListeners(html);
if (!this.isEditable) return; if ( !this.options.editable ) return;
// Item State Toggling // Item State Toggling
html.find(".item-toggle").click(this._onToggleItem.bind(this)); html.find('.item-toggle').click(this._onToggleItem.bind(this));
// Short and Long Rest // Short and Long Rest
html.find(".short-rest").click(this._onShortRest.bind(this)); html.find('.short-rest').click(this._onShortRest.bind(this));
html.find(".long-rest").click(this._onLongRest.bind(this)); html.find('.long-rest').click(this._onLongRest.bind(this));
// Rollable sheet actions // Rollable sheet actions
html.find(".rollable[data-action]").click(this._onSheetAction.bind(this)); html.find(".rollable[data-action]").click(this._onSheetAction.bind(this));
@ -289,7 +203,7 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
* Handle mouse click events for character sheet actions * Handle rolling a death saving throw for the Character
* @param {MouseEvent} event The originating click event * @param {MouseEvent} event The originating click event
* @private * @private
*/ */
@ -314,7 +228,7 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
_onToggleItem(event) { _onToggleItem(event) {
event.preventDefault(); event.preventDefault();
const itemId = event.currentTarget.closest(".item").dataset.itemId; const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.items.get(itemId); const item = this.actor.getOwnedItem(itemId);
const attr = item.data.type === "power" ? "data.preparation.prepared" : "data.equipped"; const attr = item.data.type === "power" ? "data.preparation.prepared" : "data.equipped";
return item.update({[attr]: !getProperty(item.data, attr)}); return item.update({[attr]: !getProperty(item.data, attr)});
} }
@ -349,20 +263,37 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
/** @override */ /** @override */
async _onDropItemCreate(itemData) { async _onDropItemCreate(itemData) {
// Increment the number of class levels a character instead of creating a new item let addLevel = false;
// Upgrade the number of class levels a character has and add features
if ( itemData.type === "class" ) { if ( itemData.type === "class" ) {
const cls = this.actor.itemTypes.class.find((c) => c.name === itemData.name); const cls = this.actor.itemTypes.class.find(c => c.name === itemData.name);
let priorLevel = cls?.data.data.levels ?? 0; let priorLevel = cls?.data.data.levels ?? 0;
if (!!cls) { const hasClass = !!cls;
// Increment levels instead of creating a new item
if ( hasClass ) {
const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level); const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level);
if ( next > priorLevel ) { if ( next > priorLevel ) {
itemData.levels = next; itemData.levels = next;
return cls.update({"data.levels": next}); await cls.update({"data.levels": next});
addLevel = true;
} }
} }
// 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 // Default drop handling if levels were not added
return super._onDropItemCreate(itemData); if ( !addLevel ) super._onDropItemCreate(itemData);
} }
} }

View file

@ -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. * An Actor sheet for NPC type characters in the SW5E system.
@ -6,6 +6,7 @@ import ActorSheet5e from "./base.js";
* @extends {ActorSheet5e} * @extends {ActorSheet5e}
*/ */
export default class ActorSheet5eNPC extends ActorSheet5e { export default class ActorSheet5eNPC extends ActorSheet5e {
/** @override */ /** @override */
static get defaultOptions() { static get defaultOptions() {
return mergeObject(super.defaultOptions, { return mergeObject(super.defaultOptions, {
@ -17,50 +18,32 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */
static unsupportedItemTypes = new Set(["class"]);
/* -------------------------------------------- */
/** /**
* Organize Owned Items for rendering the NPC sheet * Organize Owned Items for rendering the NPC sheet
* @private * @private
*/ */
_prepareItems(data) { _prepareItems(data) {
// Categorize Items as Features and Powers // Categorize Items as Features and Powers
const features = { const features = {
weapons: { weapons: { label: game.i18n.localize("SW5E.AttackPl"), items: [] , hasActions: true, dataset: {type: "weapon", "weapon-type": "natural"} },
label: game.i18n.localize("SW5E.AttackPl"), actions: { label: game.i18n.localize("SW5E.ActionPl"), items: [] , hasActions: true, dataset: {type: "feat", "activation.type": "action"} },
items: [],
hasActions: true,
dataset: {"type": "weapon", "weapon-type": "natural"}
},
actions: {
label: game.i18n.localize("SW5E.ActionPl"),
items: [],
hasActions: true,
dataset: {"type": "feat", "activation.type": "action"}
},
passive: { label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"} }, passive: { label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"} },
equipment: { label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}} equipment: { label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}}
}; };
// Start by classifying items into groups for rendering // Start by classifying items into groups for rendering
let [powers, other] = data.items.reduce( let [powers, other] = data.items.reduce((arr, item) => {
(arr, item) => { item.img = item.img || DEFAULT_TOKEN;
item.img = item.img || CONST.DEFAULT_TOKEN; item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1; item.hasUses = item.data.uses && (item.data.uses.max > 0);
item.hasUses = item.data.uses && item.data.uses.max > 0; item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false);
item.isOnCooldown = item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0));
item.data.recharge && !!item.data.recharge.value && item.data.recharge.charged === false; item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type));
item.isDepleted = item.isOnCooldown && item.data.uses.per && item.data.uses.value > 0;
item.hasTarget = !!item.data.target && !["none", ""].includes(item.data.target.type);
if ( item.type === "power" ) arr[0].push(item); if ( item.type === "power" ) arr[0].push(item);
else arr[1].push(item); else arr[1].push(item);
return arr; return arr;
}, }, [[], []]);
[[], []]
);
// Apply item filters // Apply item filters
powers = this._filterItems(powers, this._filters.powerbook); powers = this._filterItems(powers, this._filters.powerbook);
@ -75,7 +58,8 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
else if ( item.type === "feat" ) { else if ( item.type === "feat" ) {
if ( item.data.activation.type ) features.actions.items.push(item); if ( item.data.activation.type ) features.actions.items.push(item);
else features.passive.items.push(item); else features.passive.items.push(item);
} else features.equipment.items.push(item); }
else features.equipment.items.push(item);
} }
// Assign and return // Assign and return
@ -83,19 +67,17 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
data.powerbook = powerbook; data.powerbook = powerbook;
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @inheritdoc */ /** @override */
getData(options) { getData() {
const data = super.getData(options); const data = super.getData();
// Challenge Rating // Challenge Rating
const cr = parseFloat(data.data.details.cr || 0); const cr = parseFloat(data.data.details.cr || 0);
const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"}; const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"};
data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1; data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1;
// Creature Type
data.labels["type"] = this.actor.labels.creatureType;
return data; return data;
} }
@ -104,7 +86,8 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */ /** @override */
async _updateObject(event, formData) { _updateObject(event, formData) {
// Format NPC Challenge Rating // Format NPC Challenge Rating
const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5}; const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5};
let crv = "data.details.cr"; let crv = "data.details.cr";
@ -113,7 +96,7 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
if ( cr ) formData[crv] = cr < 1 ? cr : parseInt(cr); if ( cr ) formData[crv] = cr < 1 ? cr : parseInt(cr);
// Parent ActorSheet update steps // Parent ActorSheet update steps
return super._updateObject(event, formData); super._updateObject(event, formData);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */

View file

@ -1,4 +1,4 @@
import ActorSheet5e from "./base.js"; import ActorSheet5e from "../base.js";
/** /**
* An Actor sheet for Vehicle type actors. * An Actor sheet for Vehicle type actors.
@ -20,17 +20,12 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */
static unsupportedItemTypes = new Set(["class"]);
/* -------------------------------------------- */
/** /**
* Creates a new cargo entry for a vehicle Actor. * Creates a new cargo entry for a vehicle Actor.
*/ */
static get newCargo() { static get newCargo() {
return { return {
name: "", name: '',
quantity: 1 quantity: 1
}; };
} }
@ -45,6 +40,7 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
* @private * @private
*/ */
_computeEncumbrance(totalWeight, actorData) { _computeEncumbrance(totalWeight, actorData) {
// Compute currency weight // Compute currency weight
const totalCoins = Object.values(actorData.data.currency).reduce((acc, denom) => acc + denom, 0); const totalCoins = Object.values(actorData.data.currency).reduce((acc, denom) => acc + denom, 0);
totalWeight += totalCoins / CONFIG.SW5E.encumbrance.currencyPerWeight; totalWeight += totalCoins / CONFIG.SW5E.encumbrance.currencyPerWeight;
@ -60,203 +56,174 @@ 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 * Prepare items that are mounted to a vehicle and require one or more crew
* to operate. * to operate.
* @private * @private
*/ */
_prepareCrewedItem(item) { _prepareCrewedItem(item) {
// Determine crewed status // Determine crewed status
const isCrewed = item.data.crewed; const isCrewed = item.data.crewed;
item.toggleClass = isCrewed ? "active" : ""; item.toggleClass = isCrewed ? 'active' : '';
item.toggleTitle = game.i18n.localize(`SW5E.${isCrewed ? "Crewed" : "Uncrewed"}`); item.toggleTitle = game.i18n.localize(`SW5E.${isCrewed ? 'Crewed' : 'Uncrewed'}`);
// Handle crew actions // Handle crew actions
if (item.type === "feat" && item.data.activation.type === "crew") { if (item.type === 'feat' && item.data.activation.type === 'crew') {
item.crew = item.data.activation.cost; item.crew = item.data.activation.cost;
item.cover = game.i18n.localize(`SW5E.${item.data.cover ? "CoverTotal" : "None"}`); item.cover = game.i18n.localize(`SW5E.${item.data.cover ? 'CoverTotal' : 'None'}`);
if (item.data.cover === 0.5) item.cover = "½"; if (item.data.cover === .5) item.cover = '½';
else if (item.data.cover === 0.75) item.cover = "¾"; else if (item.data.cover === .75) item.cover = '¾';
else if (item.data.cover === null) item.cover = "—"; else if (item.data.cover === null) item.cover = '—';
if (item.crew < 1 || item.crew === null) item.crew = "—"; if (item.crew < 1 || item.crew === null) item.crew = '—';
} }
// Prepare vehicle weapons // Prepare vehicle weapons
if (item.type === "equipment" || item.type === "weapon") { if (item.type === 'equipment' || item.type === 'weapon') {
item.threshold = item.data.hp.dt ? item.data.hp.dt : "—"; item.threshold = item.data.hp.dt ? item.data.hp.dt : '—';
} }
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */
_getMovementSpeed(actorData) {
return {primary: "", special: ""};
}
/* -------------------------------------------- */
/** /**
* Organize Owned Items for rendering the Vehicle sheet. * Organize Owned Items for rendering the Vehicle sheet.
* @private * @private
*/ */
_prepareItems(data) { _prepareItems(data) {
const cargoColumns = [ const cargoColumns = [{
{ label: game.i18n.localize('SW5E.Quantity'),
label: game.i18n.localize("SW5E.Quantity"), css: 'item-qty',
css: "item-qty", property: 'quantity',
property: "quantity", editable: 'Number'
editable: "Number" }];
}
];
const equipmentColumns = [ const equipmentColumns = [{
{ label: game.i18n.localize('SW5E.Quantity'),
label: game.i18n.localize("SW5E.Quantity"), css: 'item-qty',
css: "item-qty", property: 'data.quantity'
property: "data.quantity" }, {
}, label: game.i18n.localize('SW5E.AC'),
{ css: 'item-ac',
label: game.i18n.localize("SW5E.AC"), property: 'data.armor.value'
css: "item-ac", }, {
property: "data.armor.value" label: game.i18n.localize('SW5E.HP'),
}, css: 'item-hp',
{ property: 'data.hp.value',
label: game.i18n.localize("SW5E.HP"), editable: 'Number'
css: "item-hp", }, {
property: "data.hp.value", label: game.i18n.localize('SW5E.Threshold'),
editable: "Number" css: 'item-threshold',
}, property: 'threshold'
{ }];
label: game.i18n.localize("SW5E.Threshold"),
css: "item-threshold",
property: "threshold"
}
];
const features = { const features = {
actions: { actions: {
label: game.i18n.localize("SW5E.ActionPl"), label: game.i18n.localize('SW5E.ActionPl'),
items: [], items: [],
crewable: true, crewable: true,
dataset: {"type": "feat", "activation.type": "crew"}, dataset: {type: 'feat', 'activation.type': 'crew'},
columns: [ columns: [{
{ label: game.i18n.localize('SW5E.VehicleCrew'),
label: game.i18n.localize("SW5E.VehicleCrew"), css: 'item-crew',
css: "item-crew", property: 'crew'
property: "crew" }, {
}, label: game.i18n.localize('SW5E.Cover'),
{ css: 'item-cover',
label: game.i18n.localize("SW5E.Cover"), property: 'cover'
css: "item-cover", }]
property: "cover"
}
]
}, },
equipment: { equipment: {
label: game.i18n.localize("SW5E.ItemTypeEquipment"), label: game.i18n.localize('SW5E.ItemTypeEquipment'),
items: [], items: [],
crewable: true, crewable: true,
dataset: {"type": "equipment", "armor.type": "vehicle"}, dataset: {type: 'equipment', 'armor.type': 'vehicle'},
columns: equipmentColumns columns: equipmentColumns
}, },
passive: { passive: {
label: game.i18n.localize("SW5E.Features"), label: game.i18n.localize('SW5E.Features'),
items: [], items: [],
dataset: {type: "feat"} dataset: {type: 'feat'}
}, },
reactions: { reactions: {
label: game.i18n.localize("SW5E.ReactionPl"), label: game.i18n.localize('SW5E.ReactionPl'),
items: [], items: [],
dataset: {"type": "feat", "activation.type": "reaction"} dataset: {type: 'feat', 'activation.type': 'reaction'}
}, },
weapons: { weapons: {
label: game.i18n.localize("SW5E.ItemTypeWeaponPl"), label: game.i18n.localize('SW5E.ItemTypeWeaponPl'),
items: [], items: [],
crewable: true, crewable: true,
dataset: {"type": "weapon", "weapon-type": "siege"}, dataset: {type: 'weapon', 'weapon-type': 'siege'},
columns: equipmentColumns columns: equipmentColumns
} }
}; };
const cargo = { const cargo = {
crew: { crew: {
label: game.i18n.localize("SW5E.VehicleCrew"), label: game.i18n.localize('SW5E.VehicleCrew'),
items: data.data.cargo.crew, items: data.data.cargo.crew,
css: "cargo-row crew", css: 'cargo-row crew',
editableName: true, editableName: true,
dataset: {type: "crew"}, dataset: {type: 'crew'},
columns: cargoColumns columns: cargoColumns
}, },
passengers: { passengers: {
label: game.i18n.localize("SW5E.VehiclePassengers"), label: game.i18n.localize('SW5E.VehiclePassengers'),
items: data.data.cargo.passengers, items: data.data.cargo.passengers,
css: "cargo-row passengers", css: 'cargo-row passengers',
editableName: true, editableName: true,
dataset: {type: "passengers"}, dataset: {type: 'passengers'},
columns: cargoColumns columns: cargoColumns
}, },
cargo: { cargo: {
label: game.i18n.localize("SW5E.VehicleCargo"), label: game.i18n.localize('SW5E.VehicleCargo'),
items: [], items: [],
dataset: {type: "loot"}, dataset: {type: 'loot'},
columns: [ columns: [{
{ label: game.i18n.localize('SW5E.Quantity'),
label: game.i18n.localize("SW5E.Quantity"), css: 'item-qty',
css: "item-qty", property: 'data.quantity',
property: "data.quantity", editable: 'Number'
editable: "Number" }, {
}, label: game.i18n.localize('SW5E.Price'),
{ css: 'item-price',
label: game.i18n.localize("SW5E.Price"), property: 'data.price',
css: "item-price", editable: 'Number'
property: "data.price", }, {
editable: "Number" label: game.i18n.localize('SW5E.Weight'),
}, css: 'item-weight',
{ property: 'data.weight',
label: game.i18n.localize("SW5E.Weight"), editable: 'Number'
css: "item-weight", }]
property: "data.weight",
editable: "Number"
}
]
} }
}; };
// Classify items owned by the vehicle and compute total cargo weight
let totalWeight = 0; let totalWeight = 0;
for (const item of data.items) { for (const item of data.items) {
this._prepareCrewedItem(item); this._prepareCrewedItem(item);
if (item.type === 'weapon') features.weapons.items.push(item);
// Handle cargo explicitly else if (item.type === 'equipment') features.equipment.items.push(item);
const isCargo = item.flags.sw5e?.vehicleCargo === true; else if (item.type === 'loot') {
if (isCargo) {
totalWeight += (item.data.weight || 0) * item.data.quantity; totalWeight += (item.data.weight || 0) * item.data.quantity;
cargo.cargo.items.push(item); cargo.cargo.items.push(item);
continue;
} }
else if (item.type === 'feat') {
// Handle non-cargo item types if (!item.data.activation.type || item.data.activation.type === 'none') {
switch (item.type) {
case "weapon":
features.weapons.items.push(item);
break;
case "equipment":
features.equipment.items.push(item);
break;
case "feat":
if (!item.data.activation.type || item.data.activation.type === "none")
features.passive.items.push(item); features.passive.items.push(item);
else if (item.data.activation.type === "reaction") features.reactions.items.push(item); }
else if (item.data.activation.type === 'reaction') features.reactions.items.push(item);
else features.actions.items.push(item); else features.actions.items.push(item);
break;
default:
totalWeight += (item.data.weight || 0) * item.data.quantity;
cargo.cargo.items.push(item);
} }
} }
// Update the rendering context data
data.features = Object.values(features); data.features = Object.values(features);
data.cargo = Object.values(cargo); data.cargo = Object.values(cargo);
data.data.attributes.encumbrance = this._computeEncumbrance(totalWeight, data); data.data.attributes.encumbrance = this._computeEncumbrance(totalWeight, data);
@ -269,23 +236,23 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
/** @override */ /** @override */
activateListeners(html) { activateListeners(html) {
super.activateListeners(html); super.activateListeners(html);
if (!this.isEditable) return; if (!this.options.editable) return;
html.find(".item-toggle").click(this._onToggleItem.bind(this)); html.find('.item-toggle').click(this._onToggleItem.bind(this));
html.find(".item-hp input") html.find('.item-hp input')
.click((evt) => evt.target.select()) .click(evt => evt.target.select())
.change(this._onHPChange.bind(this)); .change(this._onHPChange.bind(this));
html.find(".item:not(.cargo-row) input[data-property]") html.find('.item:not(.cargo-row) input[data-property]')
.click((evt) => evt.target.select()) .click(evt => evt.target.select())
.change(this._onEditInSheet.bind(this)); .change(this._onEditInSheet.bind(this));
html.find(".cargo-row input") html.find('.cargo-row input')
.click((evt) => evt.target.select()) .click(evt => evt.target.select())
.change(this._onCargoRowChange.bind(this)); .change(this._onCargoRowChange.bind(this));
if (this.actor.data.data.attributes.actions.stations) { if (this.actor.data.data.attributes.actions.stations) {
html.find(".counter.actions, .counter.action-thresholds").hide(); html.find('.counter.actions, .counter.action-thresholds').hide();
} }
} }
@ -300,20 +267,20 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
_onCargoRowChange(event) { _onCargoRowChange(event) {
event.preventDefault(); event.preventDefault();
const target = event.currentTarget; const target = event.currentTarget;
const row = target.closest(".item"); const row = target.closest('.item');
const idx = Number(row.dataset.itemId); const idx = Number(row.dataset.itemId);
const property = row.classList.contains("crew") ? "crew" : "passengers"; const property = row.classList.contains('crew') ? 'crew' : 'passengers';
// Get the cargo entry // Get the cargo entry
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[property]); const cargo = duplicate(this.actor.data.data.cargo[property]);
const entry = cargo[idx]; const entry = cargo[idx];
if (!entry) return null; if (!entry) return null;
// Update the cargo value // Update the cargo value
const key = target.dataset.property || "name"; const key = target.dataset.property || 'name';
const type = target.dataset.dtype; const type = target.dataset.dtype;
let value = target.value; let value = target.value;
if (type === "Number") value = Number(value); if (type === 'Number') value = Number(value);
entry[key] = value; entry[key] = value;
// Perform the Actor update // Perform the Actor update
@ -330,18 +297,14 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
*/ */
_onEditInSheet(event) { _onEditInSheet(event) {
event.preventDefault(); event.preventDefault();
const itemID = event.currentTarget.closest(".item").dataset.itemId; const itemID = event.currentTarget.closest('.item').dataset.itemId;
const item = this.actor.items.get(itemID); const item = this.actor.items.get(itemID);
const property = event.currentTarget.dataset.property; const property = event.currentTarget.dataset.property;
const type = event.currentTarget.dataset.dtype; const type = event.currentTarget.dataset.dtype;
let value = event.currentTarget.value; let value = event.currentTarget.value;
switch (type) { switch (type) {
case "Number": case 'Number': value = parseInt(value); break;
value = parseInt(value); case 'Boolean': value = value === 'true'; break;
break;
case "Boolean":
value = value === "true";
break;
} }
return item.update({[`${property}`]: value}); return item.update({[`${property}`]: value});
} }
@ -358,8 +321,8 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
event.preventDefault(); event.preventDefault();
const target = event.currentTarget; const target = event.currentTarget;
const type = target.dataset.type; const type = target.dataset.type;
if (type === "crew" || type === "passengers") { if (type === 'crew' || type === 'passengers') {
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]); const cargo = duplicate(this.actor.data.data.cargo[type]);
cargo.push(this.constructor.newCargo); cargo.push(this.constructor.newCargo);
return this.actor.update({[`data.cargo.${type}`]: cargo}); return this.actor.update({[`data.cargo.${type}`]: cargo});
} }
@ -376,11 +339,11 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
*/ */
_onItemDelete(event) { _onItemDelete(event) {
event.preventDefault(); event.preventDefault();
const row = event.currentTarget.closest(".item"); const row = event.currentTarget.closest('.item');
if (row.classList.contains("cargo-row")) { if (row.classList.contains('cargo-row')) {
const idx = Number(row.dataset.itemId); const idx = Number(row.dataset.itemId);
const type = row.classList.contains("crew") ? "crew" : "passengers"; const type = row.classList.contains('crew') ? 'crew' : 'passengers';
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx); const cargo = duplicate(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx);
return this.actor.update({[`data.cargo.${type}`]: cargo}); return this.actor.update({[`data.cargo.${type}`]: cargo});
} }
@ -389,16 +352,6 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */
async _onDropItemCreate(itemData) {
const cargoTypes = ["weapon", "equipment", "consumable", "tool", "loot", "backpack"];
const isCargo = cargoTypes.includes(itemData.type) && this._tabs[0].active === "cargo";
foundry.utils.setProperty(itemData, "flags.sw5e.vehicleCargo", isCargo);
return super._onDropItemCreate(itemData);
}
/* -------------------------------------------- */
/** /**
* Special handling for editing HP to clamp it within appropriate range. * Special handling for editing HP to clamp it within appropriate range.
* @param event {Event} * @param event {Event}
@ -407,11 +360,11 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
*/ */
_onHPChange(event) { _onHPChange(event) {
event.preventDefault(); event.preventDefault();
const itemID = event.currentTarget.closest(".item").dataset.itemId; const itemID = event.currentTarget.closest('.item').dataset.itemId;
const item = this.actor.items.get(itemID); const item = this.actor.items.get(itemID);
const hp = Math.clamped(0, parseInt(event.currentTarget.value), item.data.data.hp.max); const hp = Math.clamped(0, parseInt(event.currentTarget.value), item.data.data.hp.max);
event.currentTarget.value = hp; event.currentTarget.value = hp;
return item.update({"data.hp.value": hp}); return item.update({'data.hp.value': hp});
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -424,9 +377,9 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
*/ */
_onToggleItem(event) { _onToggleItem(event) {
event.preventDefault(); event.preventDefault();
const itemID = event.currentTarget.closest(".item").dataset.itemId; const itemID = event.currentTarget.closest('.item').dataset.itemId;
const item = this.actor.items.get(itemID); const item = this.actor.items.get(itemID);
const crewed = !!item.data.data.crewed; const crewed = !!item.data.data.crewed;
return item.update({"data.crewed": !crewed}); return item.update({'data.crewed': !crewed});
}
} }
};

View file

@ -34,22 +34,15 @@ export default class AbilityUseDialog extends Dialog {
const quantity = itemData.quantity || 0; const quantity = itemData.quantity || 0;
const recharge = itemData.recharge || {}; const recharge = itemData.recharge || {};
const recharges = !!recharge.value; const recharges = !!recharge.value;
const sufficientUses = (quantity > 0 && !uses.value) || uses.value > 0;
// Prepare dialog form data // Prepare dialog form data
const data = { const data = {
item: item.data, item: item.data,
title: game.i18n.format("SW5E.AbilityUseHint", { title: game.i18n.format("SW5E.AbilityUseHint", item.data),
type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`),
name: item.name
}),
note: this._getAbilityUseNote(item.data, uses, recharge), note: this._getAbilityUseNote(item.data, uses, recharge),
consumePowerSlot: false, hasLimitedUses: uses.max || recharges,
consumeRecharge: recharges, canUse: recharges ? recharge.charged : (quantity > 0 && !uses.value) || uses.value > 0,
consumeResource: !!itemData.consume.target, hasPlaceableTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget,
consumeUses: uses.per && uses.max > 0,
canUse: recharges ? recharge.charged : sufficientUses,
createTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget,
errors: [] errors: []
}; };
if ( item.data.type === "power" ) this._getPowerData(actorData, itemData, data); if ( item.data.type === "power" ) this._getPowerData(actorData, itemData, data);
@ -57,21 +50,18 @@ export default class AbilityUseDialog extends Dialog {
// Render the ability usage template // Render the ability usage template
const html = await renderTemplate("systems/sw5e/templates/apps/ability-use.html", data); const html = await renderTemplate("systems/sw5e/templates/apps/ability-use.html", data);
// Create the Dialog and return data as a Promise // Create the Dialog and return as a Promise
const icon = data.isPower ? "fa-magic" : "fa-fist-raised"; const icon = data.isPower ? "fa-magic" : "fa-fist-raised";
const label = game.i18n.localize("SW5E.AbilityUse" + (data.isPower ? "Cast" : "Use")); const label = game.i18n.localize("SW5E.AbilityUse" + (data.isPower ? "Cast" : "Use"));
return new Promise((resolve) => { return new Promise((resolve) => {
const dlg = new this(item, { const dlg = new this(item, {
title: `${item.name}: ${game.i18n.localize("SW5E.AbilityUseConfig")}`, title: `${item.name}: Usage Configuration`,
content: html, content: html,
buttons: { buttons: {
use: { use: {
icon: `<i class="fas ${icon}"></i>`, icon: `<i class="fas ${icon}"></i>`,
label: label, label: label,
callback: (html) => { callback: html => resolve(new FormData(html[0].querySelector("form")))
const fd = new FormDataExtended(html[0].querySelector("form"));
resolve(fd.toObject());
}
} }
}, },
default: "use", default: "use",
@ -90,92 +80,50 @@ export default class AbilityUseDialog extends Dialog {
* @private * @private
*/ */
static _getPowerData(actorData, itemData, data) { static _getPowerData(actorData, itemData, data) {
// Determine whether the power may be up-cast // Determine whether the power may be up-cast
const lvl = itemData.level; const lvl = itemData.level;
const consumePowerSlot = lvl > 0 && CONFIG.SW5E.powerUpcastModes.includes(itemData.preparation.mode); const canUpcast = (lvl > 0) && CONFIG.SW5E.powerUpcastModes.includes(itemData.preparation.mode);
// If can't upcast, return early and don't bother calculating available power slots // If can't upcast, return early and don't bother calculating available power slots
if (!consumePowerSlot) { if (!canUpcast) {
mergeObject(data, {isPower: true, consumePowerSlot}); data = mergeObject(data, { isPower: true, canUpcast });
return; return;
} }
// Determine the levels which are feasible // Determine the levels which are feasible
let lmax = 0; let lmax = 0;
let points; const powerLevels = Array.fromRange(10).reduce((arr, i) => {
let powerType;
switch (itemData.school) {
case "lgt":
case "uni":
case "drk": {
powerType = "force";
points = actorData.attributes.force.points.value + actorData.attributes.force.points.temp;
break;
}
case "tec": {
powerType = "tech";
points = actorData.attributes.tech.points.value + actorData.attributes.tech.points.temp;
break;
}
}
// eliminate point usage for innate casters
if (actorData.attributes.powercasting === "innate") points = 999;
let powerLevels;
if (powerType === "force") {
powerLevels = Array.fromRange(10)
.reduce((arr, i) => {
if ( i < lvl ) return arr; if ( i < lvl ) return arr;
const label = CONFIG.SW5E.powerLevels[i]; const label = CONFIG.SW5E.powerLevels[i];
const l = actorData.powers["power" + i] || {fmax: 0, foverride: null}; const l = actorData.powers["power"+i] || {max: 0, override: null};
let max = parseInt(l.foverride || l.fmax || 0); let max = parseInt(l.override || l.max || 0);
let slots = Math.clamped(parseInt(l.fvalue || 0), 0, max); let slots = Math.clamped(parseInt(l.value || 0), 0, max);
if ( max > 0 ) lmax = i; if ( max > 0 ) lmax = i;
if (max > 0 && slots > 0 && points > i) {
arr.push({ arr.push({
level: i, level: i,
label: i > 0 ? game.i18n.format("SW5E.PowerLevelSlot", {level: label, n: slots}) : label, label: i > 0 ? game.i18n.format('SW5E.PowerLevelSlot', {level: label, n: slots}) : label,
canCast: max > 0, canCast: max > 0,
hasSlots: slots > 0 hasSlots: slots > 0
}); });
}
return arr; return arr;
}, []) }, []).filter(sl => sl.level <= lmax);
.filter((sl) => sl.level <= lmax);
} else if (powerType === "tech") { // If this character has pact slots, present them as an option for casting the power.
powerLevels = Array.fromRange(10) const pact = actorData.powers.pact;
.reduce((arr, i) => { if (pact.level >= lvl) {
if (i < lvl) return arr; powerLevels.push({
const label = CONFIG.SW5E.powerLevels[i]; level: 'pact',
const l = actorData.powers["power" + i] || {tmax: 0, toverride: null}; label: `${game.i18n.format('SW5E.PowerLevelPact', {level: pact.level, n: pact.value})}`,
let max = parseInt(l.override || l.tmax || 0); canCast: true,
let slots = Math.clamped(parseInt(l.tvalue || 0), 0, max); hasSlots: pact.value > 0
if (max > 0) lmax = i;
if (max > 0 && slots > 0 && points > i) {
arr.push({
level: i,
label: i > 0 ? game.i18n.format("SW5E.PowerLevelSlot", {level: label, n: slots}) : label,
canCast: max > 0,
hasSlots: slots > 0
}); });
} }
return arr; const canCast = powerLevels.some(l => l.hasSlots);
}, [])
.filter((sl) => sl.level <= lmax);
}
const canCast = powerLevels.some((l) => l.hasSlots); // Return merged data
if (!canCast) data = mergeObject(data, { isPower: true, canUpcast, powerLevels });
data.errors.push( if ( !canCast ) data.errors.push("SW5E.PowerCastNoSlots");
game.i18n.format("SW5E.PowerCastNoSlots", {
level: CONFIG.SW5E.powerLevels[lvl],
name: data.item.name
})
);
// Merge power casting data
return foundry.utils.mergeObject(data, {isPower: true, consumePowerSlot, powerLevels});
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -185,6 +133,7 @@ export default class AbilityUseDialog extends Dialog {
* @private * @private
*/ */
static _getAbilityUseNote(item, uses, recharge) { static _getAbilityUseNote(item, uses, recharge) {
// Zero quantity // Zero quantity
const quantity = item.data.quantity; const quantity = item.data.quantity;
if ( quantity <= 0 ) return game.i18n.localize("SW5E.AbilityUseUnavailableHint"); if ( quantity <= 0 ) return game.i18n.localize("SW5E.AbilityUseUnavailableHint");
@ -192,8 +141,8 @@ export default class AbilityUseDialog extends Dialog {
// Abilities which use Recharge // Abilities which use Recharge
if ( !!recharge.value ) { if ( !!recharge.value ) {
return game.i18n.format(recharge.charged ? "SW5E.AbilityUseChargedHint" : "SW5E.AbilityUseRechargeHint", { return game.i18n.format(recharge.charged ? "SW5E.AbilityUseChargedHint" : "SW5E.AbilityUseRechargeHint", {
type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`) type: item.type,
}); })
} }
// Does not use any resource // Does not use any resource
@ -206,22 +155,26 @@ export default class AbilityUseDialog extends Dialog {
else if ( item.data.quantity === 1 && uses.autoDestroy ) str = "SW5E.AbilityUseConsumableDestroyHint"; else if ( item.data.quantity === 1 && uses.autoDestroy ) str = "SW5E.AbilityUseConsumableDestroyHint";
else if ( item.data.quantity > 1 ) str = "SW5E.AbilityUseConsumableQuantityHint"; else if ( item.data.quantity > 1 ) str = "SW5E.AbilityUseConsumableQuantityHint";
return game.i18n.format(str, { return game.i18n.format(str, {
type: game.i18n.localize(`SW5E.Consumable${item.data.consumableType.capitalize()}`), type: item.data.consumableType,
value: uses.value, value: uses.value,
quantity: item.data.quantity, quantity: item.data.quantity,
max: uses.max,
per: CONFIG.SW5E.limitedUsePeriods[uses.per]
}); });
} }
// Other Items // Other Items
else { else {
return game.i18n.format("SW5E.AbilityUseNormalHint", { return game.i18n.format("SW5E.AbilityUseNormalHint", {
type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`), type: item.type,
value: uses.value, value: uses.value,
max: uses.max, max: uses.max,
per: CONFIG.SW5E.limitedUsePeriods[uses.per] per: CONFIG.SW5E.limitedUsePeriods[uses.per]
}); });
} }
} }
/* -------------------------------------------- */
static _handleSubmit(formData, item) {
}
} }

View file

@ -1,10 +1,11 @@
/** /**
* An application class which provides advanced configuration for special character flags which modify an Actor * An application class which provides advanced configuration for special character flags which modify an Actor
* @implements {DocumentSheet} * @implements {BaseEntitySheet}
*/ */
export default class ActorSheetFlags extends DocumentSheet { export default class ActorSheetFlags extends BaseEntitySheet {
static get defaultOptions() { static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, { const options = super.defaultOptions;
return mergeObject(options, {
id: "actor-flags", id: "actor-flags",
classes: ["sw5e"], classes: ["sw5e"],
template: "systems/sw5e/templates/apps/actor-flags.html", template: "systems/sw5e/templates/apps/actor-flags.html",
@ -17,7 +18,7 @@ export default class ActorSheetFlags extends DocumentSheet {
/** @override */ /** @override */
get title() { get title() {
return `${game.i18n.localize("SW5E.FlagsTitle")}: ${this.object.name}`; return `${game.i18n.localize('SW5E.FlagsTitle')}: ${this.object.name}`;
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -26,7 +27,6 @@ export default class ActorSheetFlags extends DocumentSheet {
getData() { getData() {
const data = {}; const data = {};
data.actor = this.object; data.actor = this.object;
data.classes = this._getClasses();
data.flags = this._getFlags(); data.flags = this._getFlags();
data.bonuses = this._getBonuses(); data.bonuses = this._getBonuses();
return data; return data;
@ -34,38 +34,20 @@ export default class ActorSheetFlags extends DocumentSheet {
/* -------------------------------------------- */ /* -------------------------------------------- */
/**
* Prepare an object of sorted classes.
* @return {object}
* @private
*/
_getClasses() {
const classes = this.object.items.filter((i) => i.type === "class");
return classes
.sort((a, b) => a.name.localeCompare(b.name))
.reduce((obj, i) => {
obj[i.id] = i.name;
return obj;
}, {});
}
/* -------------------------------------------- */
/** /**
* Prepare an object of flags data which groups flags by section * Prepare an object of flags data which groups flags by section
* Add some additional data for rendering * Add some additional data for rendering
* @return {object} * @return {object}
* @private
*/ */
_getFlags() { _getFlags() {
const flags = {}; const flags = {};
const baseData = this.document.toJSON(); const baseData = this.entity._data;
for ( let [k, v] of Object.entries(CONFIG.SW5E.characterFlags) ) { for ( let [k, v] of Object.entries(CONFIG.SW5E.characterFlags) ) {
if ( !flags.hasOwnProperty(v.section) ) flags[v.section] = {}; if ( !flags.hasOwnProperty(v.section) ) flags[v.section] = {};
let flag = foundry.utils.deepClone(v); let flag = duplicate(v);
flag.type = v.type.name; flag.type = v.type.name;
flag.isCheckbox = v.type === Boolean; flag.isCheckbox = v.type === Boolean;
flag.isSelect = v.hasOwnProperty("choices"); flag.isSelect = v.hasOwnProperty('choices');
flag.value = getProperty(baseData.flags, `sw5e.${k}`); flag.value = getProperty(baseData.flags, `sw5e.${k}`);
flags[v.section][`flags.sw5e.${k}`] = flag; flags[v.section][`flags.sw5e.${k}`] = flag;
} }
@ -92,11 +74,7 @@ export default class ActorSheetFlags extends DocumentSheet {
{name: "data.bonuses.abilities.check", label: "SW5E.BonusAbilityCheck"}, {name: "data.bonuses.abilities.check", label: "SW5E.BonusAbilityCheck"},
{name: "data.bonuses.abilities.save", label: "SW5E.BonusAbilitySave"}, {name: "data.bonuses.abilities.save", label: "SW5E.BonusAbilitySave"},
{name: "data.bonuses.abilities.skill", label: "SW5E.BonusAbilitySkill"}, {name: "data.bonuses.abilities.skill", label: "SW5E.BonusAbilitySkill"},
{name: "data.bonuses.power.dc", label: "SW5E.BonusPowerDC"}, {name: "data.bonuses.power.dc", label: "SW5E.BonusPowerDC"}
{name: "data.bonuses.power.forceLightDC", label: "SW5E.BonusForceLightPowerDC"},
{name: "data.bonuses.power.forceDarkDC", label: "SW5E.BonusForceDarkPowerDC"},
{name: "data.bonuses.power.forceUnivDC", label: "SW5E.BonusForceUnivPowerDC"},
{name: "data.bonuses.power.techDC", label: "SW5E.BonusTechPowerDC"}
]; ];
for ( let b of bonuses ) { for ( let b of bonuses ) {
b.value = getProperty(this.object._data, b.name) || ""; b.value = getProperty(this.object._data, b.name) || "";
@ -115,7 +93,7 @@ export default class ActorSheetFlags extends DocumentSheet {
let unset = false; let unset = false;
const flags = updateData.flags.sw5e; const flags = updateData.flags.sw5e;
//clone flags to dnd5e for module compatability //clone flags to dnd5e for module compatability
updateData.flags.dnd5e = updateData.flags.sw5e; updateData.flags.dnd5e = updateData.flags.sw5e
for ( let [k, v] of Object.entries(flags) ) { for ( let [k, v] of Object.entries(flags) ) {
if ( [undefined, null, "", false, 0].includes(v) ) { if ( [undefined, null, "", false, 0].includes(v) ) {
delete flags[k]; delete flags[k];

View file

@ -1,111 +0,0 @@
import Actor5e from "../actor/entity.js";
/**
* A specialized form used to select from a checklist of attributes, traits, or properties
* @extends {FormApplication}
*/
export default class ActorTypeConfig extends FormApplication {
/** @inheritdoc */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["sw5e", "actor-type", "trait-selector"],
template: "systems/sw5e/templates/apps/actor-type.html",
title: "SW5E.CreatureTypeTitle",
width: 280,
height: "auto",
choices: {},
allowCustom: true,
minimum: 0,
maximum: null
});
}
/* -------------------------------------------- */
/** @override */
get id() {
return `actor-type-${this.object.id}`;
}
/* -------------------------------------------- */
/** @override */
getData(options) {
// Get current value or new default
let attr = foundry.utils.getProperty(this.object.data.data, "details.type");
if (foundry.utils.getType(attr) !== "Object")
attr = {
value: attr in CONFIG.SW5E.creatureTypes ? attr : "humanoid",
subtype: "",
swarm: "",
custom: ""
};
// Populate choices
const types = {};
for (let [k, v] of Object.entries(CONFIG.SW5E.creatureTypes)) {
types[k] = {
label: game.i18n.localize(v),
chosen: attr.value === k
};
}
// Return data for rendering
return {
types: types,
custom: {
value: attr.custom,
label: game.i18n.localize("SW5E.CreatureTypeSelectorCustom"),
chosen: attr.value === "custom"
},
subtype: attr.subtype,
swarm: attr.swarm,
sizes: Array.from(Object.entries(CONFIG.SW5E.actorSizes))
.reverse()
.reduce((obj, e) => {
obj[e[0]] = e[1];
return obj;
}, {}),
preview: Actor5e.formatCreatureType(attr) || ""
};
}
/* -------------------------------------------- */
/** @override */
async _updateObject(event, formData) {
const typeObject = foundry.utils.expandObject(formData);
return this.object.update({"data.details.type": typeObject});
}
/* -------------------------------------------- */
/* Event Listeners and Handlers */
/* -------------------------------------------- */
/** @inheritdoc */
activateListeners(html) {
super.activateListeners(html);
html.find("input[name='custom']").focusin(this._onCustomFieldFocused.bind(this));
}
/* -------------------------------------------- */
/** @inheritdoc */
_onChangeInput(event) {
super._onChangeInput(event);
const typeObject = foundry.utils.expandObject(this._getSubmitData());
this.form["preview"].value = Actor5e.formatCreatureType(typeObject) || "—";
}
/* -------------------------------------------- */
/**
* Select the custom radio button when the custom text field is focused.
* @param {FocusEvent} event The original focusin event
* @private
*/
_onCustomFieldFocused(event) {
this.form.querySelector("input[name='value'][value='custom']").checked = true;
this._onChangeInput(event);
}
}

View file

@ -1,92 +0,0 @@
/**
* A simple form to set actor hit dice amounts
* @implements {DocumentSheet}
*/
export default class ActorHitDiceConfig extends DocumentSheet {
/** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["sw5e", "hd-config", "dialog"],
template: "systems/sw5e/templates/apps/hit-dice-config.html",
width: 360,
height: "auto"
});
}
/* -------------------------------------------- */
/** @override */
get title() {
return `${game.i18n.localize("SW5E.HitDiceConfig")}: ${this.object.name}`;
}
/* -------------------------------------------- */
/** @override */
getData(options) {
return {
classes: this.object.items
.reduce((classes, item) => {
if (item.data.type === "class") {
// Add the appropriate data only if this item is a "class"
classes.push({
classItemId: item.data._id,
name: item.data.name,
diceDenom: item.data.data.hitDice,
currentHitDice: item.data.data.levels - item.data.data.hitDiceUsed,
maxHitDice: item.data.data.levels,
canRoll: item.data.data.levels - item.data.data.hitDiceUsed > 0
});
}
return classes;
}, [])
.sort((a, b) => parseInt(b.diceDenom.slice(1)) - parseInt(a.diceDenom.slice(1)))
};
}
/* -------------------------------------------- */
/** @override */
activateListeners(html) {
super.activateListeners(html);
// Hook up -/+ buttons to adjust the current value in the form
html.find("button.increment,button.decrement").click((event) => {
const button = event.currentTarget;
const current = button.parentElement.querySelector(".current");
const max = button.parentElement.querySelector(".max");
const direction = button.classList.contains("increment") ? 1 : -1;
current.value = Math.clamped(parseInt(current.value) + direction, 0, parseInt(max.value));
});
html.find("button.roll-hd").click(this._onRollHitDie.bind(this));
}
/* -------------------------------------------- */
/** @override */
async _updateObject(event, formData) {
const actorItems = this.object.items;
const classUpdates = Object.entries(formData).map(([id, hd]) => ({
"_id": id,
"data.hitDiceUsed": actorItems.get(id).data.data.levels - hd
}));
return this.object.updateEmbeddedDocuments("Item", classUpdates);
}
/* -------------------------------------------- */
/**
* Rolls the hit die corresponding with the class row containing the event's target button.
* @param {MouseEvent} event
* @private
*/
async _onRollHitDie(event) {
event.preventDefault();
const button = event.currentTarget;
await this.object.rollHitDie(button.dataset.hdDenom);
// Re-render dialog to reflect changed hit dice quantities
this.render();
}
}

View file

@ -40,25 +40,27 @@ export default class LongRestDialog extends Dialog {
static async longRestDialog({ actor } = {}) { static async longRestDialog({ actor } = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const dlg = new this(actor, { const dlg = new this(actor, {
title: game.i18n.localize("SW5E.LongRest"), title: "Long Rest",
buttons: { buttons: {
rest: { rest: {
icon: '<i class="fas fa-bed"></i>', icon: '<i class="fas fa-bed"></i>',
label: game.i18n.localize("SW5E.Rest"), label: "Rest",
callback: (html) => { callback: html => {
let newDay = true; let newDay = false;
if (game.settings.get("sw5e", "restVariant") !== "gritty") if (game.settings.get("sw5e", "restVariant") === "normal")
newDay = html.find('input[name="newDay"]')[0].checked; newDay = html.find('input[name="newDay"]')[0].checked;
else if(game.settings.get("sw5e", "restVariant") === "gritty")
newDay = true;
resolve(newDay); resolve(newDay);
} }
}, },
cancel: { cancel: {
icon: '<i class="fas fa-times"></i>', icon: '<i class="fas fa-times"></i>',
label: game.i18n.localize("Cancel"), label: "Cancel",
callback: reject callback: reject
} }
}, },
default: "rest", default: 'rest',
close: reject close: reject
}); });
dlg.render(true); dlg.render(true);

View file

@ -1,34 +1,28 @@
/** /**
* A simple form to set actor movement speeds * A simple form to set actor movement speeds
* @extends {DocumentSheet} * @implements {BaseEntitySheet}
*/ */
export default class ActorMovementConfig extends DocumentSheet { export default class MovementConfig extends BaseEntitySheet {
/** @override */ /** @override */
static get defaultOptions() { static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, { return mergeObject(super.defaultOptions, {
title: "SW5E.MovementConfig",
classes: ["sw5e"], classes: ["sw5e"],
template: "systems/sw5e/templates/apps/movement-config.html", template: "systems/sw5e/templates/apps/movement-config.html",
width: 300, width: 240,
height: "auto" height: "auto"
}); });
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */
get title() {
return `${game.i18n.localize("SW5E.MovementConfig")}: ${this.document.name}`;
}
/* -------------------------------------------- */
/** @override */ /** @override */
getData(options) { getData(options) {
const sourceMovement = foundry.utils.getProperty(this.document.data._source, "data.attributes.movement") || {};
const data = { const data = {
movement: foundry.utils.deepClone(sourceMovement), movement: duplicate(this.entity._data.data.attributes.movement),
units: CONFIG.SW5E.movementUnits units: CONFIG.SW5E.movementUnits
}; }
for ( let [k, v] of Object.entries(data.movement) ) { for ( let [k, v] of Object.entries(data.movement) ) {
if ( ["units", "hover"].includes(k) ) continue; if ( ["units", "hover"].includes(k) ) continue;
data.movement[k] = Number.isNumeric(v) ? v.toNearest(0.1) : 0; data.movement[k] = Number.isNumeric(v) ? v.toNearest(0.1) : 0;

View file

@ -1,66 +0,0 @@
/**
* A Dialog to prompt the user to select from a list of items.
* @type {Dialog}
*/
export default class SelectItemsPrompt extends Dialog {
constructor(items, dialogData = {}, options = {}) {
super(dialogData, options);
this.options.classes = ["sw5e", "dialog", "select-items-prompt", "sheet"];
/**
* Store a reference to the Item entities being used
* @type {Array<Item5e>}
*/
this.items = items;
}
activateListeners(html) {
super.activateListeners(html);
// render the item's sheet if its image is clicked
html.on("click", ".item-image", (event) => {
const item = this.items.find((feature) => feature.id === event.currentTarget.dataset?.itemId);
item?.sheet.render(true);
});
}
/**
* A constructor function which displays the AddItemPrompt app for a given Actor and Item set.
* Returns a Promise which resolves to the dialog FormData once the workflow has been completed.
* @param {Array<Item5e>} items
* @param {Object} options
* @param {string} options.hint - Localized hint to display at the top of the prompt
* @return {Promise<string[]>} - list of item ids which the user has selected
*/
static async create(items, {hint}) {
// Render the ability usage template
const html = await renderTemplate("systems/sw5e/templates/apps/select-items-prompt.html", {items, hint});
return new Promise((resolve) => {
const dlg = new this(items, {
title: game.i18n.localize("SW5E.SelectItemsPromptTitle"),
content: html,
buttons: {
apply: {
icon: `<i class="fas fa-user-plus"></i>`,
label: game.i18n.localize("SW5E.Apply"),
callback: (html) => {
const fd = new FormDataExtended(html[0].querySelector("form")).toObject();
const selectedIds = Object.keys(fd).filter((itemId) => fd[itemId]);
resolve(selectedIds);
}
},
cancel: {
icon: '<i class="fas fa-forward"></i>',
label: game.i18n.localize("SW5E.Skip"),
callback: () => resolve([])
}
},
default: "apply",
close: () => resolve([])
});
dlg.render(true);
});
}
}

View file

@ -1,43 +0,0 @@
/**
* A simple form to set Actor movement speeds.
* @extends {DocumentSheet}
*/
export default class ActorSensesConfig extends DocumentSheet {
/** @inheritdoc */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["sw5e"],
template: "systems/sw5e/templates/apps/senses-config.html",
width: 300,
height: "auto"
});
}
/* -------------------------------------------- */
/** @inheritdoc */
get title() {
return `${game.i18n.localize("SW5E.SensesConfig")}: ${this.document.name}`;
}
/* -------------------------------------------- */
/** @inheritdoc */
getData(options) {
const senses = foundry.utils.getProperty(this.document.data._source, "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;
}
}

View file

@ -40,7 +40,7 @@ export default class ShortRestDialog extends Dialog {
// Determine Hit Dice // Determine Hit Dice
data.availableHD = this.actor.data.items.reduce((hd, item) => { data.availableHD = this.actor.data.items.reduce((hd, item) => {
if ( item.type === "class" ) { if ( item.type === "class" ) {
const d = item.data.data; const d = item.data;
const denom = d.hitDice || "d6"; const denom = d.hitDice || "d6";
const available = parseInt(d.levels || 1) - parseInt(d.hitDiceUsed || 0); const available = parseInt(d.levels || 1) - parseInt(d.hitDiceUsed || 0);
hd[denom] = denom in hd ? hd[denom] + available : available; hd[denom] = denom in hd ? hd[denom] + available : available;
@ -59,6 +59,7 @@ export default class ShortRestDialog extends Dialog {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */ /** @override */
activateListeners(html) { activateListeners(html) {
super.activateListeners(html); super.activateListeners(html);
@ -92,12 +93,12 @@ export default class ShortRestDialog extends Dialog {
static async shortRestDialog({actor}={}) { static async shortRestDialog({actor}={}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const dlg = new this(actor, { const dlg = new this(actor, {
title: game.i18n.localize("SW5E.ShortRest"), title: "Short Rest",
buttons: { buttons: {
rest: { rest: {
icon: '<i class="fas fa-bed"></i>', icon: '<i class="fas fa-bed"></i>',
label: game.i18n.localize("SW5E.Rest"), label: "Rest",
callback: (html) => { callback: html => {
let newDay = false; let newDay = false;
if (game.settings.get("sw5e", "restVariant") === "gritty") if (game.settings.get("sw5e", "restVariant") === "gritty")
newDay = html.find('input[name="newDay"]')[0].checked; newDay = html.find('input[name="newDay"]')[0].checked;
@ -106,7 +107,7 @@ export default class ShortRestDialog extends Dialog {
}, },
cancel: { cancel: {
icon: '<i class="fas fa-times"></i>', icon: '<i class="fas fa-times"></i>',
label: game.i18n.localize("Cancel"), label: "Cancel",
callback: reject callback: reject
} }
}, },
@ -126,9 +127,7 @@ export default class ShortRestDialog extends Dialog {
* @return {Promise} * @return {Promise}
*/ */
static async longRestDialog({actor}={}) { static async longRestDialog({actor}={}) {
console.warn( console.warn("WARNING! ShortRestDialog.longRestDialog has been deprecated, use LongRestDialog.longRestDialog instead.");
"WARNING! ShortRestDialog.longRestDialog has been deprecated, use LongRestDialog.longRestDialog instead."
);
return LongRestDialog.longRestDialog(...arguments); return LongRestDialog.longRestDialog(...arguments);
} }
} }

View file

@ -1,13 +1,14 @@
/** /**
* A specialized form used to select from a checklist of attributes, traits, or properties * A specialized form used to select from a checklist of attributes, traits, or properties
* @extends {DocumentSheet} * @implements {FormApplication}
*/ */
export default class TraitSelector extends DocumentSheet { export default class TraitSelector extends FormApplication {
/** @inheritdoc */
/** @override */
static get defaultOptions() { static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, { return mergeObject(super.defaultOptions, {
id: "trait-selector", id: "trait-selector",
classes: ["sw5e", "trait-selector", "subconfig"], classes: ["sw5e"],
title: "Actor Trait Selection", title: "Actor Trait Selection",
template: "systems/sw5e/templates/apps/trait-selector.html", template: "systems/sw5e/templates/apps/trait-selector.html",
width: 320, width: 320,
@ -15,9 +16,7 @@ export default class TraitSelector extends DocumentSheet {
choices: {}, choices: {},
allowCustom: true, allowCustom: true,
minimum: 0, minimum: 0,
maximum: null, maximum: null
valueKey: "value",
customKey: "custom"
}); });
} }
@ -25,7 +24,7 @@ export default class TraitSelector extends DocumentSheet {
/** /**
* Return a reference to the target attribute * Return a reference to the target attribute
* @type {string} * @type {String}
*/ */
get attribute() { get attribute() {
return this.options.name; return this.options.name;
@ -35,50 +34,52 @@ export default class TraitSelector extends DocumentSheet {
/** @override */ /** @override */
getData() { getData() {
const attr = foundry.utils.getProperty(this.object.data, this.attribute);
const o = this.options; // Get current values
const value = o.valueKey ? attr[o.valueKey] ?? [] : attr; let attr = getProperty(this.object._data, this.attribute) || {};
const custom = o.customKey ? attr[o.customKey] ?? "" : ""; attr.value = attr.value || [];
// Populate choices // Populate choices
const choices = Object.entries(o.choices).reduce((obj, e) => { const choices = duplicate(this.options.choices);
let [k, v] = e; for ( let [k, v] of Object.entries(choices) ) {
obj[k] = {label: v, chosen: attr ? value.includes(k) : false}; choices[k] = {
return obj; label: v,
}, {}); chosen: attr ? attr.value.includes(k) : false
}
}
// Return data // Return data
return { return {
allowCustom: o.allowCustom, allowCustom: this.options.allowCustom,
choices: choices, choices: choices,
custom: custom custom: attr ? attr.custom : ""
}; }
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */ /** @override */
async _updateObject(event, formData) { _updateObject(event, formData) {
const o = this.options; const updateData = {};
// Obtain choices // Obtain choices
const chosen = []; const chosen = [];
for ( let [k, v] of Object.entries(formData) ) { for ( let [k, v] of Object.entries(formData) ) {
if (k !== "custom" && v) chosen.push(k); if ( (k !== "custom") && v ) chosen.push(k);
} }
updateData[`${this.attribute}.value`] = chosen;
// Object including custom data
const updateData = {};
if (o.valueKey) updateData[`${this.attribute}.${o.valueKey}`] = chosen;
else updateData[this.attribute] = chosen;
if (o.allowCustom) updateData[`${this.attribute}.${o.customKey}`] = formData.custom;
// Validate the number chosen // Validate the number chosen
if (o.minimum && chosen.length < o.minimum) { if ( this.options.minimum && (chosen.length < this.options.minimum) ) {
return ui.notifications.error(`You must choose at least ${o.minimum} options`); return ui.notifications.error(`You must choose at least ${this.options.minimum} options`);
} }
if (o.maximum && chosen.length > o.maximum) { if ( this.options.maximum && (chosen.length > this.options.maximum) ) {
return ui.notifications.error(`You may choose no more than ${o.maximum} options`); return ui.notifications.error(`You may choose no more than ${this.options.maximum} options`);
}
// Include custom
if ( this.options.allowCustom ) {
updateData[`${this.attribute}.custom`] = formData.custom;
} }
// Update the object // Update the object

View file

@ -8,7 +8,7 @@ export const measureDistances = function (segments, options = {}) {
const d = canvas.dimensions; const d = canvas.dimensions;
// Iterate over measured segments // Iterate over measured segments
return segments.map((s) => { return segments.map(s => {
let r = s.ray; let r = s.ray;
// Determine the total distance traveled // Determine the total distance traveled
@ -23,7 +23,7 @@ export const measureDistances = function (segments, options = {}) {
// Alternative DMG Movement // Alternative DMG Movement
if (rule === "5105") { if (rule === "5105") {
let nd10 = Math.floor(nDiagonal / 2) - Math.floor((nDiagonal - nd) / 2); let nd10 = Math.floor(nDiagonal / 2) - Math.floor((nDiagonal - nd) / 2);
let spaces = nd10 * 2 + (nd - nd10) + ns; let spaces = (nd10 * 2) + (nd - nd10) + ns;
return spaces * canvas.dimensions.distance; return spaces * canvas.dimensions.distance;
} }
@ -36,3 +36,19 @@ export const measureDistances = function (segments, options = {}) {
else return (ns + nd) * canvas.scene.data.gridDistance; else return (ns + nd) * canvas.scene.data.gridDistance;
}); });
}; };
/* -------------------------------------------- */
/**
* Hijack Token health bar rendering to include temporary and temp-max health in the bar display
* TODO: This should probably be replaced with a formal Token class extension
*/
const _TokenGetBarAttribute = Token.prototype.getBarAttribute;
export const getBarAttribute = function(...args) {
const data = _TokenGetBarAttribute.bind(this)(...args);
if ( data && (data.attribute === "attributes.hp") ) {
data.value += parseInt(getProperty(this.actor.data, "data.attributes.hp.temp") || 0);
data.max += parseInt(getProperty(this.actor.data, "data.attributes.hp.tempmax") || 0);
}
return data;
};

View file

@ -1,326 +0,0 @@
export default class CharacterImporter {
// transform JSON from sw5e.com to Foundry friendly format
// and insert new actor
static async transform(rawCharacter) {
const sourceCharacter = JSON.parse(rawCharacter); //source character
const details = {
species: sourceCharacter.attribs.find((e) => e.name == "race").current,
background: sourceCharacter.attribs.find((e) => e.name == "background").current,
alignment: sourceCharacter.attribs.find((e) => e.name == "alignment").current
};
const hp = {
value: sourceCharacter.attribs.find((e) => e.name == "hp").current,
min: 0,
max: sourceCharacter.attribs.find((e) => e.name == "hp").current,
temp: sourceCharacter.attribs.find((e) => e.name == "hp_temp").current
};
const abilities = {
str: {
value: sourceCharacter.attribs.find((e) => e.name == "strength").current,
proficient: sourceCharacter.attribs.find((e) => e.name == "strength_save_prof").current ? 1 : 0
},
dex: {
value: sourceCharacter.attribs.find((e) => e.name == "dexterity").current,
proficient: sourceCharacter.attribs.find((e) => e.name == "dexterity_save_prof").current ? 1 : 0
},
con: {
value: sourceCharacter.attribs.find((e) => e.name == "constitution").current,
proficient: sourceCharacter.attribs.find((e) => e.name == "constitution_save_prof").current ? 1 : 0
},
int: {
value: sourceCharacter.attribs.find((e) => e.name == "intelligence").current,
proficient: sourceCharacter.attribs.find((e) => e.name == "intelligence_save_prof").current ? 1 : 0
},
wis: {
value: sourceCharacter.attribs.find((e) => e.name == "wisdom").current,
proficient: sourceCharacter.attribs.find((e) => e.name == "wisdom_save_prof").current ? 1 : 0
},
cha: {
value: sourceCharacter.attribs.find((e) => e.name == "charisma").current,
proficient: sourceCharacter.attribs.find((e) => e.name == "charisma_save_prof").current ? 1 : 0
}
};
/* ----------------------------------------------------------------- */
/* character.data.skills.<skill_name>.value is all that matters
/* values can be 0, 0.5, 1 or 2
/* 0 = regular
/* 0.5 = half-proficient
/* 1 = proficient
/* 2 = expertise
/* foundry takes care of calculating the rest
/* ----------------------------------------------------------------- */
const skills = {
acr: {
value: sourceCharacter.attribs.find((e) => e.name == "acrobatics_type").current
},
ani: {
value: sourceCharacter.attribs.find((e) => e.name == "animal_handling_type").current
},
ath: {
value: sourceCharacter.attribs.find((e) => e.name == "athletics_type").current
},
dec: {
value: sourceCharacter.attribs.find((e) => e.name == "deception_type").current
},
ins: {
value: sourceCharacter.attribs.find((e) => e.name == "insight_type").current
},
inv: {
value: sourceCharacter.attribs.find((e) => e.name == "investigation_type").current
},
itm: {
value: sourceCharacter.attribs.find((e) => e.name == "intimidation_type").current
},
lor: {
value: sourceCharacter.attribs.find((e) => e.name == "lore_type").current
},
med: {
value: sourceCharacter.attribs.find((e) => e.name == "medicine_type").current
},
nat: {
value: sourceCharacter.attribs.find((e) => e.name == "nature_type").current
},
per: {
value: sourceCharacter.attribs.find((e) => e.name == "persuasion_type").current
},
pil: {
value: sourceCharacter.attribs.find((e) => e.name == "piloting_type").current
},
prc: {
value: sourceCharacter.attribs.find((e) => e.name == "perception_type").current
},
prf: {
value: sourceCharacter.attribs.find((e) => e.name == "performance_type").current
},
slt: {
value: sourceCharacter.attribs.find((e) => e.name == "sleight_of_hand_type").current
},
ste: {
value: sourceCharacter.attribs.find((e) => e.name == "stealth_type").current
},
sur: {
value: sourceCharacter.attribs.find((e) => e.name == "survival_type").current
},
tec: {
value: sourceCharacter.attribs.find((e) => e.name == "technology_type").current
}
};
const targetCharacter = {
name: sourceCharacter.name,
type: "character",
data: {
abilities: abilities,
details: details,
skills: skills,
attributes: {
hp: hp
}
}
};
let actor = await Actor.create(targetCharacter);
CharacterImporter.addProfessions(sourceCharacter, actor);
}
// Parse all classes and add them to already created actor.
// "class" is a reserved word, therefore I use profession where I can.
static async addProfessions(sourceCharacter, actor) {
let result = [];
// parse all class and multiclassX items
// couldn't get Array.filter to work here for some reason
// result = array of objects. each object is a separate class
sourceCharacter.attribs.forEach((e) => {
if (CharacterImporter.classOrMulticlass(e.name)) {
var t = {
profession: CharacterImporter.capitalize(e.current),
type: CharacterImporter.baseOrMulti(e.name),
level: CharacterImporter.getLevel(e, sourceCharacter)
};
result.push(t);
}
});
// pull classes directly from system compendium and add them to current actor
const professionsPack = await game.packs.get("sw5e.classes").getDocuments();
result.forEach((prof) => {
let assignedProfession = professionsPack.find((o) => o.name === prof.profession);
assignedProfession.data.data.levels = prof.level;
actor.createEmbeddedDocuments("Item", [assignedProfession.data], {displaySheet: false});
});
this.addSpecies(sourceCharacter.attribs.find((e) => e.name == "race").current, actor);
this.addPowers(
sourceCharacter.attribs
.filter((e) => e.name.search(/repeating_power.+_powername/g) != -1)
.map((e) => e.current),
actor
);
const discoveredItems = sourceCharacter.attribs.filter(
(e) => e.name.search(/repeating_inventory.+_itemname/g) != -1
);
const items = discoveredItems.map((item) => {
const id = item.name.match(/-\w{19}/g);
return {
name: item.current,
quantity: sourceCharacter.attribs.find((e) => e.name === `repeating_inventory_${id}_itemcount`).current
};
});
this.addItems(items, actor);
}
static async addClasses(profession, level, actor) {
let classes = await game.packs.get("sw5e.classes").getDocuments();
let assignedClass = classes.find((c) => c.name === profession);
assignedClass.data.data.levels = level;
await actor.createEmbeddedDocuments("Item", [assignedClass.data], {displaySheet: false});
}
static classOrMulticlass(name) {
return name === "class" || (name.includes("multiclass") && name.length <= 12);
}
static baseOrMulti(name) {
if (name === "class") {
return "base_class";
} else {
return "multi_class";
}
}
static getLevel(item, sourceCharacter) {
if (item.name === "class") {
let result = sourceCharacter.attribs.find((e) => e.name === "base_level").current;
return parseInt(result);
} else {
let result = sourceCharacter.attribs.find((e) => e.name === `${item.name}_lvl`).current;
return parseInt(result);
}
}
static capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
static async addSpecies(race, actor) {
const species = await game.packs.get("sw5e.species").getDocuments();
const assignedSpecies = species.find((c) => c.name === race);
const activeEffects = [...assignedSpecies.data.effects][0].data.changes;
const actorData = {data: {abilities: {...actor.data.data.abilities}}};
activeEffects.map((effect) => {
switch (effect.key) {
case "data.abilities.str.value":
actorData.data.abilities.str.value -= effect.value;
break;
case "data.abilities.dex.value":
actorData.data.abilities.dex.value -= effect.value;
break;
case "data.abilities.con.value":
actorData.data.abilities.con.value -= effect.value;
break;
case "data.abilities.int.value":
actorData.data.abilities.int.value -= effect.value;
break;
case "data.abilities.wis.value":
actorData.data.abilities.wis.value -= effect.value;
break;
case "data.abilities.cha.value":
actorData.data.abilities.cha.value -= effect.value;
break;
default:
break;
}
});
actor.update(actorData);
await actor.createEmbeddedDocuments("Item", [assignedSpecies.data], {displaySheet: false});
}
static async addPowers(powers, actor) {
const forcePowers = await game.packs.get("sw5e.forcepowers").getDocuments();
const techPowers = await game.packs.get("sw5e.techpowers").getDocuments();
for (const power of powers) {
const createdPower = forcePowers.find((c) => c.name === power) || techPowers.find((c) => c.name === power);
if (createdPower) {
await actor.createEmbeddedDocuments("Item", [createdPower.data], {displaySheet: false});
}
}
}
static async addItems(items, actor) {
const weapons = await game.packs.get("sw5e.weapons").getDocuments();
const armors = await game.packs.get("sw5e.armor").getDocuments();
const adventuringGear = await game.packs.get("sw5e.adventuringgear").getDocuments();
for (const item of items) {
const createdItem =
weapons.find((c) => c.name.toLowerCase() === item.name.toLowerCase()) ||
armors.find((c) => c.name.toLowerCase() === item.name.toLowerCase()) ||
adventuringGear.find((c) => c.name.toLowerCase() === item.name.toLowerCase());
if (createdItem) {
if (item.quantity != 1) {
createdItem.data.data.quantity = item.quantity;
}
await actor.createEmbeddedDocuments("Item", [createdItem.data], {displaySheet: false});
}
}
}
static addImportButton(html) {
const actionButtons = html.find(".header-actions");
actionButtons[0].insertAdjacentHTML(
"afterend",
`<div class="header-actions action-buttons flexrow"><button class="create-entity cs-import-button"><i class="fas fa-upload"></i> Import Character</button></div>`
);
let characterImportButton = $(".cs-import-button");
characterImportButton.click(() => {
let content = `<h1>Saved Character JSON Import</h1>
<label for="character-json">Paste character JSON here:</label>
</br>
<textarea id="character-json" name="character-json" rows="10" cols="50"></textarea>`;
let importDialog = new Dialog({
title: "Import Character from SW5e.com",
content: content,
buttons: {
Import: {
icon: `<i class="fas fa-file-import"></i>`,
label: "Import Character",
callback: () => {
let characterData = $("#character-json").val();
console.log("Parsing Character JSON");
CharacterImporter.transform(characterData);
}
},
Cancel: {
icon: `<i class="fas fa-times-circle"></i>`,
label: "Cancel",
callback: () => {}
}
}
});
importDialog.render(true);
});
}
}

View file

@ -1,3 +1,4 @@
/** /**
* Highlight critical success or failure on d20 rolls * Highlight critical success or failure on d20 rolls
*/ */
@ -10,9 +11,9 @@ export const highlightCriticalSuccessFailure = function (message, html, data) {
const d = roll.dice[0]; const d = roll.dice[0];
// Ensure it is an un-modified d20 roll // Ensure it is an un-modified d20 roll
const isD20 = d.faces === 20 && d.values.length === 1; const isD20 = (d.faces === 20) && ( d.values.length === 1 );
if ( !isD20 ) return; if ( !isD20 ) return;
const isModifiedRoll = "success" in d.results[0] || d.options.marginSuccess || d.options.marginFailure; const isModifiedRoll = ("success" in d.results[0]) || d.options.marginSuccess || d.options.marginFailure;
if ( isModifiedRoll ) return; if ( isModifiedRoll ) return;
// Highlight successes and failures // Highlight successes and failures
@ -39,14 +40,14 @@ export const displayChatActionButtons = function (message, html, data) {
// If the user is the message author or the actor owner, proceed // If the user is the message author or the actor owner, proceed
let actor = game.actors.get(data.message.speaker.actor); let actor = game.actors.get(data.message.speaker.actor);
if (actor && actor.isOwner) return; if ( actor && actor.owner ) return;
else if (game.user.isGM || data.author.id === game.user.id) return; else if ( game.user.isGM || (data.author.id === game.user.id)) return;
// Otherwise conceal action buttons except for saving throw // Otherwise conceal action buttons except for saving throw
const buttons = chatCard.find("button[data-action]"); const buttons = chatCard.find("button[data-action]");
buttons.each((i, btn) => { buttons.each((i, btn) => {
if ( btn.dataset.action === "save" ) return; if ( btn.dataset.action === "save" ) return;
btn.style.display = "none"; btn.style.display = "none"
}); });
} }
}; };
@ -63,34 +64,34 @@ export const displayChatActionButtons = function (message, html, data) {
* @return {Array} The extended options Array including new context choices * @return {Array} The extended options Array including new context choices
*/ */
export const addChatMessageContextOptions = function(html, options) { export const addChatMessageContextOptions = function(html, options) {
let canApply = (li) => { let canApply = li => {
const message = game.messages.get(li.data("messageId")); 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( options.push(
{ {
name: game.i18n.localize("SW5E.ChatContextDamage"), name: game.i18n.localize("SW5E.ChatContextDamage"),
icon: '<i class="fas fa-user-minus"></i>', icon: '<i class="fas fa-user-minus"></i>',
condition: canApply, condition: canApply,
callback: (li) => applyChatCardDamage(li, 1) callback: li => applyChatCardDamage(li, 1)
}, },
{ {
name: game.i18n.localize("SW5E.ChatContextHealing"), name: game.i18n.localize("SW5E.ChatContextHealing"),
icon: '<i class="fas fa-user-plus"></i>', icon: '<i class="fas fa-user-plus"></i>',
condition: canApply, condition: canApply,
callback: (li) => applyChatCardDamage(li, -1) callback: li => applyChatCardDamage(li, -1)
}, },
{ {
name: game.i18n.localize("SW5E.ChatContextDoubleDamage"), name: game.i18n.localize("SW5E.ChatContextDoubleDamage"),
icon: '<i class="fas fa-user-injured"></i>', icon: '<i class="fas fa-user-injured"></i>',
condition: canApply, condition: canApply,
callback: (li) => applyChatCardDamage(li, 2) callback: li => applyChatCardDamage(li, 2)
}, },
{ {
name: game.i18n.localize("SW5E.ChatContextHalfDamage"), name: game.i18n.localize("SW5E.ChatContextHalfDamage"),
icon: '<i class="fas fa-user-shield"></i>', icon: '<i class="fas fa-user-shield"></i>',
condition: canApply, condition: canApply,
callback: (li) => applyChatCardDamage(li, 0.5) callback: li => applyChatCardDamage(li, 0.5)
} }
); );
return options; return options;
@ -102,19 +103,16 @@ export const addChatMessageContextOptions = function (html, options) {
* Apply rolled dice damage to the token or tokens which are currently controlled. * 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 * This allows for damage to be scaled by a multiplier to account for healing, critical hits, or resistance
* *
* @param {HTMLElement} li The chat entry which contains the roll data * @param {HTMLElement} roll The chat entry which contains the roll data
* @param {Number} multiplier A damage multiplier to apply to the rolled damage. * @param {Number} multiplier A damage multiplier to apply to the rolled damage.
* @return {Promise} * @return {Promise}
*/ */
function applyChatCardDamage(li, multiplier) { function applyChatCardDamage(roll, multiplier) {
const message = game.messages.get(li.data("messageId")); const amount = roll.find('.dice-total').text();
const roll = message.roll; return Promise.all(canvas.tokens.controlled.map(t => {
return Promise.all(
canvas.tokens.controlled.map((t) => {
const a = t.actor; const a = t.actor;
return a.applyDamage(roll.total, multiplier); return a.applyDamage(amount, multiplier);
}) }));
);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */

View file

@ -1 +1,4 @@
export const ClassFeatures = {}; export const ClassFeatures = {
};

View file

@ -1,31 +1,40 @@
/** /**
* Override the default Initiative formula to customize special behaviors of the SW5e system. * Override the default Initiative formula to customize special behaviors of the SW5e system.
* Apply advantage, proficiency, or bonuses where appropriate * Apply advantage, proficiency, or bonuses where appropriate
* Apply the dexterity score as a decimal tiebreaker if requested * Apply the dexterity score as a decimal tiebreaker if requested
* See Combat._getInitiativeFormula for more detail. * See Combat._getInitiativeFormula for more detail.
*/ */
export const _getInitiativeFormula = function () { export const _getInitiativeFormula = function(combatant) {
const actor = this.actor; const actor = combatant.actor;
if ( !actor ) return "1d20"; if ( !actor ) return "1d20";
const init = actor.data.data.attributes.init; const init = actor.data.data.attributes.init;
// Construct initiative formula parts
let nd = 1; let nd = 1;
let mods = ""; let mods = "";
if (actor.getFlag("sw5e", "halflingLucky")) mods += "r1=1";
if (actor.getFlag("sw5e", "halflingLucky")) mods += "r=1";
if (actor.getFlag("sw5e", "initiativeAdv")) { if (actor.getFlag("sw5e", "initiativeAdv")) {
nd = 2; nd = 2;
mods += "kh"; mods += "kh";
} }
const parts = [
`${nd}d20${mods}`, const parts = [`${nd}d20${mods}`, init.mod, (init.prof !== 0) ? init.prof : null, (init.bonus !== 0) ? init.bonus : null];
init.mod,
init.prof !== 0 ? init.prof : null,
init.bonus !== 0 ? init.bonus : null
];
// Optionally apply Dexterity tiebreaker // Optionally apply Dexterity tiebreaker
const tiebreaker = game.settings.get("sw5e", "initiativeDexTiebreaker"); const tiebreaker = game.settings.get("sw5e", "initiativeDexTiebreaker");
if ( tiebreaker ) parts.push(actor.data.data.abilities.dex.value / 100); if ( tiebreaker ) parts.push(actor.data.data.abilities.dex.value / 100);
return parts.filter((p) => p !== null).join(" + "); return parts.filter(p => p !== null).join(" + ");
}; };
/**
* 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);
}
});

File diff suppressed because it is too large Load diff

View file

@ -1,313 +1,303 @@
export {default as D20Roll} from "./dice/d20-roll.js";
export {default as DamageRoll} from "./dice/damage-roll.js";
/** /**
* A standardized helper function for simplifying the constant parts of a multipart roll formula * A standardized helper function for managing core 5e "d20 rolls"
* *
* @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 (term instanceof OperatorTerm) 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.getFormula(constantTerms); // Cleans up the constant terms and produces a new formula string
const rollableFormula = Roll.getFormula(rollableTerms); // Cleans up the non-constant terms and produces a new formula string
// Mathematically evaluate the constant formula to produce a single constant term
let constantPart = undefined;
if (constantFormula) {
try {
constantPart = Roll.safeEval(constantFormula);
} catch (err) {
console.warn(`Unable to evaluate constant term ${constantFormula} in simplifyRollFormula`);
}
}
// Order the rollable and constant terms, either constant first or second depending on the optional argument
const parts = constantFirst ? [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 = term instanceof OperatorTerm && ["+", "-"].includes(term.operator);
const number = term instanceof NumericTerm;
return !(diceTerm || operator || number);
}
/* -------------------------------------------- */
/* D20 Roll */
/* -------------------------------------------- */
/**
* A standardized helper function for managing core 5e d20 rolls.
* Holding SHIFT, ALT, or CTRL when the attack is rolled will "fast-forward". * Holding SHIFT, ALT, or CTRL when the attack is rolled will "fast-forward".
* This chooses the default options of a normal attack with no bonus, Advantage, or Disadvantage respectively * This chooses the default options of a normal attack with no bonus, Advantage, or Disadvantage respectively
* *
* @param {string[]} parts The dice roll component parts, excluding the initial d20 * @param {Array} parts The dice roll component parts, excluding the initial d20
* @param {object} data Actor or item data against which to parse the roll * @param {Object} data Actor or item data against which to parse the roll
* @param {Event|object} event The triggering event which initiated the roll
* @param {string} rollMode A specific roll mode to apply as the default for the resulting roll
* @param {string|null} template The HTML template used to render the roll dialog
* @param {string|null} title The dice roll UI window title
* @param {Object} speaker The ChatMessage speaker to pass when creating the chat
* @param {string|null} flavor Flavor text to use in the posted chat message
* @param {Boolean} fastForward Allow fast-forward advantage selection
* @param {Function} onClose Callback for actions to take when the dialog form is closed
* @param {Object} dialogOptions Modal dialog options
* @param {boolean} advantage Apply advantage to the roll (unless otherwise specified)
* @param {boolean} disadvantage Apply disadvantage to the roll (unless otherwise specified)
* @param {number} critical The value of d20 result which represents a critical success
* @param {number} fumble The value of d20 result which represents a critical failure
* @param {number} targetValue Assign a target value against which the result of this roll should be compared
* @param {boolean} elvenAccuracy Allow Elven Accuracy to modify this roll?
* @param {boolean} halflingLucky Allow Halfling Luck to modify this roll?
* @param {boolean} reliableTalent Allow Reliable Talent to modify this roll?
* @param {boolean} chatMessage Automatically create a Chat Message for the result of this roll
* @param {object} messageData Additional data which is applied to the created Chat Message, if any
* *
* @param {boolean} [advantage] Apply advantage to the roll (unless otherwise specified) * @return {Promise} A Promise which resolves once the roll workflow has completed
* @param {boolean} [disadvantage] Apply disadvantage to the roll (unless otherwise specified)
* @param {number} [critical] The value of d20 result which represents a critical success
* @param {number} [fumble] The value of d20 result which represents a critical failure
* @param {number} [targetValue] Assign a target value against which the result of this roll should be compared
* @param {boolean} [elvenAccuracy] Allow Elven Accuracy to modify this roll?
* @param {boolean} [halflingLucky] Allow Halfling Luck to modify this roll?
* @param {boolean} [reliableTalent] Allow Reliable Talent to modify this roll?
* @param {boolean} [chooseModifier=false] Choose the ability modifier that should be used when the roll is made
* @param {boolean} [fastForward=false] Allow fast-forward advantage selection
* @param {Event} [event] The triggering event which initiated the roll
* @param {string} [rollMode] A specific roll mode to apply as the default for the resulting roll
* @param {string} [template] The HTML template used to render the roll dialog
* @param {string} [title] The dialog window title
* @param {Object} [dialogOptions] Modal dialog options
*
* @param {boolean} [chatMessage=true] Automatically create a Chat Message for the result of this roll
* @param {object} [messageData={}] Additional data which is applied to the created Chat Message, if any
* @param {string} [rollMode] A specific roll mode to apply as the default for the resulting roll
* @param {object} [speaker] The ChatMessage speaker to pass when creating the chat
* @param {string} [flavor] Flavor text to use in the posted chat message
*
* @return {Promise<D20Roll|null>} The evaluated D20Roll, or null if the workflow was cancelled
*/ */
export async function d20Roll({ export async function d20Roll({parts=[], data={}, event={}, rollMode=null, template=null, title=null, speaker=null,
parts = [], flavor=null, fastForward=null, dialogOptions,
data = {}, // Roll creation advantage=null, disadvantage=null, critical=20, fumble=1, targetValue=null,
advantage, elvenAccuracy=false, halflingLucky=false, reliableTalent=false,
disadvantage, chatMessage=true, messageData={}}={}) {
fumble = 1,
critical = 20,
targetValue,
elvenAccuracy,
halflingLucky,
reliableTalent, // Roll customization
chooseModifier = false,
fastForward = false,
event,
template,
title,
dialogOptions, // Dialog configuration
chatMessage = true,
messageData = {},
rollMode,
speaker,
flavor // Chat Message customization
} = {}) {
// Handle input arguments
const formula = ["1d20"].concat(parts).join(" + ");
const {advantageMode, isFF} = _determineAdvantageMode({advantage, disadvantage, fastForward, event});
const defaultRollMode = rollMode || game.settings.get("core", "rollMode");
if (chooseModifier && !isFF) data["mod"] = "@mod";
// Construct the D20Roll instance // Prepare Message Data
const roll = new CONFIG.Dice.D20Roll(formula, data, { messageData.flavor = flavor || title;
flavor: flavor || title, messageData.speaker = speaker || ChatMessage.getSpeaker();
advantageMode, const messageOptions = {rollMode: rollMode || game.settings.get("core", "rollMode")};
defaultRollMode, parts = parts.concat(["@bonus"]);
critical,
fumble,
targetValue,
elvenAccuracy,
halflingLucky,
reliableTalent
});
// Prompt a Dialog to further configure the D20Roll // Handle fast-forward events
if (!isFF) { let adv = 0;
const configured = await roll.configureDialog( fastForward = fastForward ?? (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
{ if (fastForward) {
title, if ( advantage || event.altKey ) adv = 1;
chooseModifier, else if ( disadvantage || event.ctrlKey || event.metaKey ) adv = -1;
defaultRollMode: defaultRollMode,
defaultAction: advantageMode,
defaultAbility: data?.item?.ability,
template
},
dialogOptions
);
if (configured === null) return null;
} }
// Evaluate the configured roll // Define the inner roll function
await roll.evaluate({async: true}); const _roll = (parts, adv, form) => {
// Determine the d20 roll and modifiers
let nd = 1;
let mods = halflingLucky ? "r=1" : "";
// Handle advantage
if (adv === 1) {
nd = elvenAccuracy ? 3 : 2;
messageData.flavor += ` (${game.i18n.localize("SW5E.Advantage")})`;
if ( "flags.sw5e.roll" in messageData ) messageData["flags.sw5e.roll"].advantage = true;
mods += "kh";
}
// Handle disadvantage
else if (adv === -1) {
nd = 2;
messageData.flavor += ` (${game.i18n.localize("SW5E.Disadvantage")})`;
if ( "flags.sw5e.roll" in messageData ) messageData["flags.sw5e.roll"].disadvantage = true;
mods += "kl";
}
// Prepend the d20 roll
let formula = `${nd}d20${mods}`;
if (reliableTalent) formula = `{${nd}d20${mods},10}kh`;
parts.unshift(formula);
// Optionally include a situational bonus
if ( form ) {
data['bonus'] = form.bonus.value;
messageOptions.rollMode = form.rollMode.value;
}
if (!data["bonus"]) parts.pop();
// Optionally include an ability score selection (used for tool checks)
const ability = form ? form.ability : null;
if (ability && ability.value) {
data.ability = ability.value;
const abl = data.abilities[data.ability];
if (abl) {
data.mod = abl.mod;
messageData.flavor += ` (${CONFIG.SW5E.abilities[data.ability]})`;
}
}
// Execute the roll
let roll = new Roll(parts.join(" + "), data);
try {
roll.roll();
} catch (err) {
console.error(err);
ui.notifications.error(`Dice roll evaluation failed: ${err.message}`);
return null;
}
// Flag d20 options for any 20-sided dice in the roll
for (let d of roll.dice) {
if (d.faces === 20) {
d.options.critical = critical;
d.options.fumble = fumble;
if (targetValue) d.options.target = targetValue;
}
}
// If reliable talent was applied, add it to the flavor text
if (reliableTalent && roll.dice[0].total < 10) {
messageData.flavor += ` (${game.i18n.localize("SW5E.FlagsReliableTalent")})`;
}
return roll;
};
// Create the Roll instance
const roll = fastForward ? _roll(parts, adv) :
await _d20RollDialog({template, title, parts, data, rollMode: messageOptions.rollMode, dialogOptions, roll: _roll});
// Create a Chat Message // Create a Chat Message
if (speaker) { if ( roll && chatMessage ) roll.toMessage(messageData, messageOptions);
console.warn(
`You are passing the speaker argument to the d20Roll function directly which should instead be passed as an internal key of messageData`
);
messageData.speaker = speaker;
}
if (roll && chatMessage) await roll.toMessage(messageData);
return roll; return roll;
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
* Determines whether this d20 roll should be fast-forwarded, and whether advantage or disadvantage should be applied * Present a Dialog form which creates a d20 roll once submitted
* @returns {{isFF: boolean, advantageMode: number}} Whether the roll is fast-forward, and its advantage mode * @return {Promise<Roll>}
* @private
*/ */
function _determineAdvantageMode({event, advantage = false, disadvantage = false, fastForward = false} = {}) { async function _d20RollDialog({template, title, parts, data, rollMode, dialogOptions, roll}={}) {
const isFF = fastForward || (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
let advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.NORMAL; // Render modal dialog
if (advantage || event?.altKey) advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.ADVANTAGE; template = template || "systems/sw5e/templates/chat/roll-dialog.html";
else if (disadvantage || event?.ctrlKey || event?.metaKey) let dialogData = {
advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.DISADVANTAGE; formula: parts.join(" + "),
return {isFF, advantageMode}; data: data,
rollMode: rollMode,
rollModes: CONFIG.Dice.rollModes,
config: CONFIG.SW5E
};
const html = await renderTemplate(template, dialogData);
// Create the Dialog window
return new Promise(resolve => {
new Dialog({
title: title,
content: html,
buttons: {
advantage: {
label: game.i18n.localize("SW5E.Advantage"),
callback: html => resolve(roll(parts, 1, html[0].querySelector("form")))
},
normal: {
label: game.i18n.localize("SW5E.Normal"),
callback: html => resolve(roll(parts, 0, html[0].querySelector("form")))
},
disadvantage: {
label: game.i18n.localize("SW5E.Disadvantage"),
callback: html => resolve(roll(parts, -1, html[0].querySelector("form")))
}
},
default: "normal",
close: () => resolve(null)
}, dialogOptions).render(true);
});
} }
/* -------------------------------------------- */
/* Damage Roll */
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
* A standardized helper function for managing core 5e damage rolls. * A standardized helper function for managing core 5e "d20 rolls"
* *
* Holding SHIFT, ALT, or CTRL when the attack is rolled will "fast-forward". * Holding SHIFT, ALT, or CTRL when the attack is rolled will "fast-forward".
* This chooses the default options of a normal attack with no bonus, Critical, or no bonus respectively * This chooses the default options of a normal attack with no bonus, Critical, or no bonus respectively
* *
* @param {string[]} parts The dice roll component parts, excluding the initial d20 * @param {Array} parts The dice roll component parts, excluding the initial d20
* @param {object} [data] Actor or item data against which to parse the roll * @param {Actor} actor The Actor making the damage roll
* @param {Object} data Actor or item data against which to parse the roll
* @param {Event|object}[event The triggering event which initiated the roll
* @param {string} rollMode A specific roll mode to apply as the default for the resulting roll
* @param {String} template The HTML template used to render the roll dialog
* @param {String} title The dice roll UI window title
* @param {Object} speaker The ChatMessage speaker to pass when creating the chat
* @param {string} flavor Flavor text to use in the posted chat message
* @param {boolean} allowCritical Allow the opportunity for a critical hit to be rolled
* @param {Boolean} critical Flag this roll as a critical hit for the purposes of fast-forward rolls
* @param {number} criticalBonusDice A number of bonus damage dice that are added for critical hits
* @param {number} criticalMultiplier A critical hit multiplier which is applied to critical hits
* @param {Boolean} fastForward Allow fast-forward advantage selection
* @param {Function} onClose Callback for actions to take when the dialog form is closed
* @param {Object} dialogOptions Modal dialog options
* @param {boolean} chatMessage Automatically create a Chat Message for the result of this roll
* @param {object} messageData Additional data which is applied to the created Chat Message, if any
* *
* @param {boolean} [critical=false] Flag this roll as a critical hit for the purposes of fast-forward or default dialog action * @return {Promise} A Promise which resolves once the roll workflow has completed
* @param {number} [criticalBonusDice=0] A number of bonus damage dice that are added for critical hits
* @param {number} [criticalMultiplier=2] A critical hit multiplier which is applied to critical hits
* @param {boolean} [multiplyNumeric=false] Multiply numeric terms by the critical multiplier
* @param {boolean} [powerfulCritical=false] Apply the "powerful criticals" house rule to critical hits
* @param {boolean} [fastForward=false] Allow fast-forward advantage selection
* @param {Event}[event] The triggering event which initiated the roll
* @param {boolean} [allowCritical=true] Allow the opportunity for a critical hit to be rolled
* @param {string} [template] The HTML template used to render the roll dialog
* @param {string} [title] The dice roll UI window title
* @param {object} [dialogOptions] Configuration dialog options
*
* @param {boolean} [chatMessage=true] Automatically create a Chat Message for the result of this roll
* @param {object} [messageData={}] Additional data which is applied to the created Chat Message, if any
* @param {string} [rollMode] A specific roll mode to apply as the default for the resulting roll
* @param {object} [speaker] The ChatMessage speaker to pass when creating the chat
* @param {string} [flavor] Flavor text to use in the posted chat message
*
* @return {Promise<DamageRoll|null>} The evaluated DamageRoll, or null if the workflow was canceled
*/ */
export async function damageRoll({ export async function damageRoll({parts, actor, data, event={}, rollMode=null, template, title, speaker, flavor,
parts = [], allowCritical=true, critical=false, criticalBonusDice=0, criticalMultiplier=2, fastForward=null,
data, // Roll creation dialogOptions={}, chatMessage=true, messageData={}}={}) {
critical = false,
criticalBonusDice,
criticalMultiplier,
multiplyNumeric,
powerfulCritical, // Damage customization
fastForward = false,
event,
allowCritical = true,
template,
title,
dialogOptions, // Dialog configuration
chatMessage = true,
messageData = {},
rollMode,
speaker,
flavor // Chat Message customization
} = {}) {
// Handle input arguments
const defaultRollMode = rollMode || game.settings.get("core", "rollMode");
// Construct the DamageRoll instance // Prepare Message Data
const formula = parts.join(" + "); messageData.flavor = flavor || title;
const {isCritical, isFF} = _determineCriticalMode({critical, fastForward, event}); messageData.speaker = speaker || ChatMessage.getSpeaker();
const roll = new CONFIG.Dice.DamageRoll(formula, data, { const messageOptions = {rollMode: rollMode || game.settings.get("core", "rollMode")};
flavor: flavor || title, parts = parts.concat(["@bonus"]);
critical: isCritical, fastForward = fastForward ?? (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
criticalBonusDice,
criticalMultiplier, // Define inner roll function
multiplyNumeric, const _roll = function(parts, crit, form) {
powerfulCritical
// Optionally include a situational bonus
if ( form ) {
data['bonus'] = form.bonus.value;
messageOptions.rollMode = form.rollMode.value;
}
if (!data["bonus"]) parts.pop();
// Create the damage roll
let roll = new Roll(parts.join("+"), data);
// Modify the damage formula for critical hits
if ( crit === true ) {
roll.alter(criticalMultiplier, 0); // Multiply all dice
if ( roll.terms[0] instanceof Die ) { // Add bonus dice for only the main dice term
roll.terms[0].alter(1, criticalBonusDice);
roll._formula = roll.formula;
}
messageData.flavor += ` (${game.i18n.localize("SW5E.Critical")})`;
if ( "flags.sw5e.roll" in messageData ) messageData["flags.sw5e.roll"].critical = true;
}
// Execute the roll
try {
return roll.roll();
} catch(err) {
console.error(err);
ui.notifications.error(`Dice roll evaluation failed: ${err.message}`);
return null;
}
};
// Create the Roll instance
const roll = fastForward ? _roll(parts, critical || event.altKey) : await _damageRollDialog({
template, title, parts, data, allowCritical, rollMode: messageOptions.rollMode, dialogOptions, roll: _roll
}); });
// Prompt a Dialog to further configure the DamageRoll
if (!isFF) {
const configured = await roll.configureDialog(
{
title,
defaultRollMode: defaultRollMode,
defaultCritical: isCritical,
template,
allowCritical
},
dialogOptions
);
if (configured === null) return null;
}
// Evaluate the configured roll
await roll.evaluate({async: true});
// Create a Chat Message // Create a Chat Message
if (speaker) { if ( roll && chatMessage ) roll.toMessage(messageData, messageOptions);
console.warn(
`You are passing the speaker argument to the damageRoll function directly which should instead be passed as an internal key of messageData`
);
messageData.speaker = speaker;
}
if (roll && chatMessage) await roll.toMessage(messageData);
return roll; return roll;
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
* Determines whether this d20 roll should be fast-forwarded, and whether advantage or disadvantage should be applied * Present a Dialog form which creates a damage roll once submitted
* @returns {{isFF: boolean, isCritical: boolean}} Whether the roll is fast-forward, and whether it is a critical hit * @return {Promise<Roll>}
* @private
*/ */
function _determineCriticalMode({event, critical = false, fastForward = false} = {}) { async function _damageRollDialog({template, title, parts, data, allowCritical, rollMode, dialogOptions, roll}={}) {
const isFF = fastForward || (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
if (event?.altKey) critical = true; // Render modal dialog
return {isFF, isCritical: critical}; template = template || "systems/sw5e/templates/chat/roll-dialog.html";
let dialogData = {
formula: parts.join(" + "),
data: data,
rollMode: rollMode,
rollModes: CONFIG.Dice.rollModes
};
const html = await renderTemplate(template, dialogData);
// Create the Dialog window
return new Promise(resolve => {
new Dialog({
title: title,
content: html,
buttons: {
critical: {
condition: allowCritical,
label: game.i18n.localize("SW5E.CriticalHit"),
callback: html => resolve(roll(parts, true, html[0].querySelector("form")))
},
normal: {
label: game.i18n.localize(allowCritical ? "SW5E.Normal" : "SW5E.Roll"),
callback: html => resolve(roll(parts, false, html[0].querySelector("form")))
},
},
default: "normal",
close: () => resolve(null)
}, dialogOptions).render(true);
});
} }

View file

@ -1,230 +0,0 @@
/**
* A type of Roll specific to a d20-based check, save, or attack roll in the 5e system.
* @param {string} formula The string formula to parse
* @param {object} data The data object against which to parse attributes within the formula
* @param {object} [options={}] Extra optional arguments which describe or modify the D20Roll
* @param {number} [options.advantageMode] What advantage modifier to apply to the roll (none, advantage, disadvantage)
* @param {number} [options.critical] The value of d20 result which represents a critical success
* @param {number} [options.fumble] The value of d20 result which represents a critical failure
* @param {(number)} [options.targetValue] Assign a target value against which the result of this roll should be compared
* @param {boolean} [options.elvenAccuracy=false] Allow Elven Accuracy to modify this roll?
* @param {boolean} [options.halflingLucky=false] Allow Halfling Luck to modify this roll?
* @param {boolean} [options.reliableTalent=false] Allow Reliable Talent to modify this roll?
*/
// TODO: Check elven accuracy, halfling lucky, and reliable talent are required
// Elven Accuracy is Supreme accuracy feat, Reliable Talent is operative's Reliable Talent Class Feat
export default class D20Roll extends Roll {
constructor(formula, data, options) {
super(formula, data, options);
if (!(this.terms[0] instanceof Die && this.terms[0].faces === 20)) {
throw new Error(`Invalid D20Roll formula provided ${this._formula}`);
}
this.configureModifiers();
}
/* -------------------------------------------- */
/**
* Advantage mode of a 5e d20 roll
* @enum {number}
*/
static ADV_MODE = {
NORMAL: 0,
ADVANTAGE: 1,
DISADVANTAGE: -1
};
/**
* The HTML template path used to configure evaluation of this Roll
* @type {string}
*/
static EVALUATION_TEMPLATE = "systems/sw5e/templates/chat/roll-dialog.html";
/* -------------------------------------------- */
/**
* A convenience reference for whether this D20Roll has advantage
* @type {boolean}
*/
get hasAdvantage() {
return this.options.advantageMode === D20Roll.ADV_MODE.ADVANTAGE;
}
/**
* A convenience reference for whether this D20Roll has disadvantage
* @type {boolean}
*/
get hasDisadvantage() {
return this.options.advantageMode === D20Roll.ADV_MODE.DISADVANTAGE;
}
/* -------------------------------------------- */
/* D20 Roll Methods */
/* -------------------------------------------- */
/**
* Apply optional modifiers which customize the behavior of the d20term
* @private
*/
configureModifiers() {
const d20 = this.terms[0];
d20.modifiers = [];
// Halfling Lucky
if (this.options.halflingLucky) d20.modifiers.push("r1=1");
// Reliable Talent
if (this.options.reliableTalent) d20.modifiers.push("min10");
// Handle Advantage or Disadvantage
if (this.hasAdvantage) {
d20.number = this.options.elvenAccuracy ? 3 : 2;
d20.modifiers.push("kh");
d20.options.advantage = true;
} else if (this.hasDisadvantage) {
d20.number = 2;
d20.modifiers.push("kl");
d20.options.disadvantage = true;
} else d20.number = 1;
// Assign critical and fumble thresholds
if (this.options.critical) d20.options.critical = this.options.critical;
if (this.options.fumble) d20.options.fumble = this.options.fumble;
if (this.options.targetValue) d20.options.target = this.options.targetValue;
// Re-compile the underlying formula
this._formula = this.constructor.getFormula(this.terms);
}
/* -------------------------------------------- */
/** @inheritdoc */
async toMessage(messageData = {}, options = {}) {
// Evaluate the roll now so we have the results available to determine whether reliable talent came into play
if (!this._evaluated) await this.evaluate({async: true});
// Add appropriate advantage mode message flavor and sw5e roll flags
messageData.flavor = messageData.flavor || this.options.flavor;
if (this.hasAdvantage) messageData.flavor += ` (${game.i18n.localize("SW5E.Advantage")})`;
else if (this.hasDisadvantage) messageData.flavor += ` (${game.i18n.localize("SW5E.Disadvantage")})`;
// Add reliable talent to the d20-term flavor text if it applied
if (this.options.reliableTalent) {
const d20 = this.dice[0];
const isRT = d20.results.every((r) => !r.active || r.result < 10);
const label = `(${game.i18n.localize("SW5E.FlagsReliableTalent")})`;
if (isRT) d20.options.flavor = d20.options.flavor ? `${d20.options.flavor} (${label})` : label;
}
// Record the preferred rollMode
options.rollMode = options.rollMode ?? this.options.rollMode;
return super.toMessage(messageData, options);
}
/* -------------------------------------------- */
/* Configuration Dialog */
/* -------------------------------------------- */
/**
* Create a Dialog prompt used to configure evaluation of an existing D20Roll instance.
* @param {object} data Dialog configuration data
* @param {string} [data.title] The title of the shown dialog window
* @param {number} [data.defaultRollMode] The roll mode that the roll mode select element should default to
* @param {number} [data.defaultAction] The button marked as default
* @param {boolean} [data.chooseModifier] Choose which ability modifier should be applied to the roll?
* @param {string} [data.defaultAbility] For tool rolls, the default ability modifier applied to the roll
* @param {string} [data.template] A custom path to an HTML template to use instead of the default
* @param {object} options Additional Dialog customization options
* @returns {Promise<D20Roll|null>} A resulting D20Roll object constructed with the dialog, or null if the dialog was closed
*/
async configureDialog(
{
title,
defaultRollMode,
defaultAction = D20Roll.ADV_MODE.NORMAL,
chooseModifier = false,
defaultAbility,
template
} = {},
options = {}
) {
// Render the Dialog inner HTML
const content = await renderTemplate(template ?? this.constructor.EVALUATION_TEMPLATE, {
formula: `${this.formula} + @bonus`,
defaultRollMode,
rollModes: CONFIG.Dice.rollModes,
chooseModifier,
defaultAbility,
abilities: CONFIG.SW5E.abilities
});
let defaultButton = "normal";
switch (defaultAction) {
case D20Roll.ADV_MODE.ADVANTAGE:
defaultButton = "advantage";
break;
case D20Roll.ADV_MODE.DISADVANTAGE:
defaultButton = "disadvantage";
break;
}
// Create the Dialog window and await submission of the form
return new Promise((resolve) => {
new Dialog(
{
title,
content,
buttons: {
advantage: {
label: game.i18n.localize("SW5E.Advantage"),
callback: (html) => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.ADVANTAGE))
},
normal: {
label: game.i18n.localize("SW5E.Normal"),
callback: (html) => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.NORMAL))
},
disadvantage: {
label: game.i18n.localize("SW5E.Disadvantage"),
callback: (html) => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.DISADVANTAGE))
}
},
default: defaultButton,
close: () => resolve(null)
},
options
).render(true);
});
}
/* -------------------------------------------- */
/**
* Handle submission of the Roll evaluation configuration Dialog
* @param {jQuery} html The submitted dialog content
* @param {number} advantageMode The chosen advantage mode
* @private
*/
_onDialogSubmit(html, advantageMode) {
const form = html[0].querySelector("form");
// Append a situational bonus term
if (form.bonus.value) {
const bonus = new Roll(form.bonus.value, this.data);
if (!(bonus.terms[0] instanceof OperatorTerm)) this.terms.push(new OperatorTerm({operator: "+"}));
this.terms = this.terms.concat(bonus.terms);
}
// Customize the modifier
if (form.ability?.value) {
const abl = this.data.abilities[form.ability.value];
this.terms.findSplice((t) => t.term === "@mod", new NumericTerm({number: abl.mod}));
this.options.flavor += ` (${CONFIG.SW5E.abilities[form.ability.value]})`;
}
// Apply advantage or disadvantage
this.options.advantageMode = advantageMode;
this.options.rollMode = form.rollMode.value;
this.configureModifiers();
return this;
}
}

View file

@ -1,186 +0,0 @@
/**
* A type of Roll specific to a damage (or healing) roll in the 5e system.
* @param {string} formula The string formula to parse
* @param {object} data The data object against which to parse attributes within the formula
* @param {object} [options={}] Extra optional arguments which describe or modify the DamageRoll
* @param {number} [options.criticalBonusDice=0] A number of bonus damage dice that are added for critical hits
* @param {number} [options.criticalMultiplier=2] A critical hit multiplier which is applied to critical hits
* @param {boolean} [options.multiplyNumeric=false] Multiply numeric terms by the critical multiplier
* @param {boolean} [options.powerfulCritical=false] Apply the "powerful criticals" house rule to critical hits
*
*/
export default class DamageRoll extends Roll {
constructor(formula, data, options) {
super(formula, data, options);
// For backwards compatibility, skip rolls which do not have the "critical" option defined
if (this.options.critical !== undefined) this.configureDamage();
}
/**
* The HTML template path used to configure evaluation of this Roll
* @type {string}
*/
static EVALUATION_TEMPLATE = "systems/sw5e/templates/chat/roll-dialog.html";
/* -------------------------------------------- */
/**
* A convenience reference for whether this DamageRoll is a critical hit
* @type {boolean}
*/
get isCritical() {
return this.options.critical;
}
/* -------------------------------------------- */
/* Damage Roll Methods */
/* -------------------------------------------- */
/**
* Apply optional modifiers which customize the behavior of the d20term
* @private
*/
configureDamage() {
let flatBonus = 0;
for (let [i, term] of this.terms.entries()) {
// Multiply dice terms
if (term instanceof DiceTerm) {
term.options.baseNumber = term.options.baseNumber ?? term.number; // Reset back
term.number = term.options.baseNumber;
if (this.isCritical) {
let cm = this.options.criticalMultiplier ?? 2;
// Powerful critical - maximize damage and reduce the multiplier by 1
if (this.options.powerfulCritical) {
flatBonus += term.number * term.faces;
cm = Math.max(1, cm - 1);
}
// Alter the damage term
let cb = this.options.criticalBonusDice && i === 0 ? this.options.criticalBonusDice : 0;
term.alter(cm, cb);
term.options.critical = true;
}
}
// Multiply numeric terms
else if (this.options.multiplyNumeric && term instanceof NumericTerm) {
term.options.baseNumber = term.options.baseNumber ?? term.number; // Reset back
term.number = term.options.baseNumber;
if (this.isCritical) {
term.number *= this.options.criticalMultiplier ?? 2;
term.options.critical = true;
}
}
}
// Add powerful critical bonus
if (this.options.powerfulCritical && flatBonus > 0) {
this.terms.push(new OperatorTerm({operator: "+"}));
this.terms.push(
new NumericTerm({number: flatBonus}, {flavor: game.i18n.localize("SW5E.PowerfulCritical")})
);
}
// Re-compile the underlying formula
this._formula = this.constructor.getFormula(this.terms);
}
/* -------------------------------------------- */
/** @inheritdoc */
toMessage(messageData = {}, options = {}) {
messageData.flavor = messageData.flavor || this.options.flavor;
if (this.isCritical) {
const label = game.i18n.localize("SW5E.CriticalHit");
messageData.flavor = messageData.flavor ? `${messageData.flavor} (${label})` : label;
}
options.rollMode = options.rollMode ?? this.options.rollMode;
return super.toMessage(messageData, options);
}
/* -------------------------------------------- */
/* Configuration Dialog */
/* -------------------------------------------- */
/**
* Create a Dialog prompt used to configure evaluation of an existing D20Roll instance.
* @param {object} data Dialog configuration data
* @param {string} [data.title] The title of the shown dialog window
* @param {number} [data.defaultRollMode] The roll mode that the roll mode select element should default to
* @param {string} [data.defaultCritical] Should critical be selected as default
* @param {string} [data.template] A custom path to an HTML template to use instead of the default
* @param {boolean} [data.allowCritical=true] Allow critical hit to be chosen as a possible damage mode
* @param {object} options Additional Dialog customization options
* @returns {Promise<D20Roll|null>} A resulting D20Roll object constructed with the dialog, or null if the dialog was closed
*/
async configureDialog(
{title, defaultRollMode, defaultCritical = false, template, allowCritical = true} = {},
options = {}
) {
// Render the Dialog inner HTML
const content = await renderTemplate(template ?? this.constructor.EVALUATION_TEMPLATE, {
formula: `${this.formula} + @bonus`,
defaultRollMode,
rollModes: CONFIG.Dice.rollModes
});
// Create the Dialog window and await submission of the form
return new Promise((resolve) => {
new Dialog(
{
title,
content,
buttons: {
critical: {
condition: allowCritical,
label: game.i18n.localize("SW5E.CriticalHit"),
callback: (html) => resolve(this._onDialogSubmit(html, true))
},
normal: {
label: game.i18n.localize(allowCritical ? "SW5E.Normal" : "SW5E.Roll"),
callback: (html) => resolve(this._onDialogSubmit(html, false))
}
},
default: defaultCritical ? "critical" : "normal",
close: () => resolve(null)
},
options
).render(true);
});
}
/* -------------------------------------------- */
/**
* Handle submission of the Roll evaluation configuration Dialog
* @param {jQuery} html The submitted dialog content
* @param {boolean} isCritical Is the damage a critical hit?
* @private
*/
_onDialogSubmit(html, isCritical) {
const form = html[0].querySelector("form");
// Append a situational bonus term
if (form.bonus.value) {
const bonus = new Roll(form.bonus.value, this.data);
if (!(bonus.terms[0] instanceof OperatorTerm)) this.terms.push(new OperatorTerm({operator: "+"}));
this.terms = this.terms.concat(bonus.terms);
}
// Apply advantage or disadvantage
this.options.critical = isCritical;
this.options.rollMode = form.rollMode.value;
this.configureDamage();
return this;
}
/* -------------------------------------------- */
/** @inheritdoc */
static fromData(data) {
const roll = super.fromData(data);
roll._formula = this.getFormula(roll.terms);
return roll;
}
}

View file

@ -1,15 +0,0 @@
/**
* @deprecated since 1.3.0
* @ignore
*/
async function d20Dialog(data, options) {
throw new Error(`The d20Dialog helper method is deprecated in favor of D20Roll#configureDialog`);
}
/**
* @deprecated since 1.3.0
* @ignore
*/
async function damageDialog(data, options) {
throw new Error(`The damageDialog helper method is deprecated in favor of DamageRoll#configureDialog`);
}

21
module/effects.js vendored
View file

@ -10,15 +10,13 @@ export function onManageActiveEffect(event, owner) {
const effect = li.dataset.effectId ? owner.effects.get(li.dataset.effectId) : null; const effect = li.dataset.effectId ? owner.effects.get(li.dataset.effectId) : null;
switch ( a.dataset.action ) { switch ( a.dataset.action ) {
case "create": case "create":
return owner.createEmbeddedDocuments("ActiveEffect", [ return ActiveEffect.create({
{ label: "New Effect",
"label": game.i18n.localize("SW5E.EffectNew"), icon: "icons/svg/aura.svg",
"icon": "icons/svg/aura.svg", origin: owner.uuid,
"origin": owner.uuid,
"duration.rounds": li.dataset.effectType === "temporary" ? 1 : undefined, "duration.rounds": li.dataset.effectType === "temporary" ? 1 : undefined,
"disabled": li.dataset.effectType === "inactive" disabled: li.dataset.effectType === "inactive"
} }, owner).create();
]);
case "edit": case "edit":
return effect.sheet.render(true); return effect.sheet.render(true);
case "delete": case "delete":
@ -34,21 +32,22 @@ export function onManageActiveEffect(event, owner) {
* @return {object} Data for rendering * @return {object} Data for rendering
*/ */
export function prepareActiveEffectCategories(effects) { export function prepareActiveEffectCategories(effects) {
// Define effect header categories // Define effect header categories
const categories = { const categories = {
temporary: { temporary: {
type: "temporary", type: "temporary",
label: game.i18n.localize("SW5E.EffectTemporary"), label: "Temporary Effects",
effects: [] effects: []
}, },
passive: { passive: {
type: "passive", type: "passive",
label: game.i18n.localize("SW5E.EffectPassive"), label: "Passive Effects",
effects: [] effects: []
}, },
inactive: { inactive: {
type: "inactive", type: "inactive",
label: game.i18n.localize("SW5E.EffectInactive"), label: "Inactive Effects",
effects: [] effects: []
} }
}; };

File diff suppressed because it is too large Load diff

View file

@ -18,9 +18,9 @@ export default class ItemSheet5e extends ItemSheet {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @inheritdoc */ /** @override */
static get defaultOptions() { static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, { return mergeObject(super.defaultOptions, {
width: 560, width: 560,
height: 400, height: 400,
classes: ["sw5e", "sheet", "item"], classes: ["sw5e", "sheet", "item"],
@ -32,7 +32,7 @@ export default class ItemSheet5e extends ItemSheet {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @inheritdoc */ /** @override */
get template() { get template() {
const path = "systems/sw5e/templates/items/"; const path = "systems/sw5e/templates/items/";
return `${path}/${this.item.data.type}.html`; return `${path}/${this.item.data.type}.html`;
@ -43,39 +43,30 @@ export default class ItemSheet5e extends ItemSheet {
/** @override */ /** @override */
async getData(options) { async getData(options) {
const data = super.getData(options); const data = super.getData(options);
const itemData = data.data;
data.labels = this.item.labels; data.labels = this.item.labels;
data.config = CONFIG.SW5E; data.config = CONFIG.SW5E;
// Item Type, Status, and Details // Item Type, Status, and Details
data.itemType = game.i18n.localize(`ITEM.Type${data.item.type.titleCase()}`); data.itemType = game.i18n.localize(`ITEM.Type${data.item.type.titleCase()}`);
data.itemStatus = this._getItemStatus(itemData); data.itemStatus = this._getItemStatus(data.item);
data.itemProperties = this._getItemProperties(itemData); data.itemProperties = this._getItemProperties(data.item);
data.isPhysical = itemData.data.hasOwnProperty("quantity"); data.isPhysical = data.item.data.hasOwnProperty("quantity");
// Potential consumption targets // Potential consumption targets
data.abilityConsumptionTargets = this._getItemConsumptionTargets(itemData); data.abilityConsumptionTargets = this._getItemConsumptionTargets(data.item);
// Action Details // Action Details
data.hasAttackRoll = this.item.hasAttack; data.hasAttackRoll = this.item.hasAttack;
data.isHealing = itemData.data.actionType === "heal"; data.isHealing = data.item.data.actionType === "heal";
data.isFlatDC = getProperty(itemData, "data.save.scaling") === "flat"; data.isFlatDC = getProperty(data.item.data, "save.scaling") === "flat";
data.isLine = ["line", "wall"].includes(itemData.data.target?.type); data.isLine = ["line", "wall"].includes(data.item.data.target?.type);
// Original maximum uses formula
const sourceMax = foundry.utils.getProperty(this.item.data._source, "data.uses.max");
if (sourceMax) itemData.data.uses.max = sourceMax;
// Vehicles // Vehicles
data.isCrewed = itemData.data.activation?.type === "crew"; data.isCrewed = data.item.data.activation?.type === 'crew';
data.isMountable = this._isItemMountable(itemData); data.isMountable = this._isItemMountable(data.item);
// Prepare Active Effects // Prepare Active Effects
data.effects = prepareActiveEffectCategories(this.item.effects); data.effects = prepareActiveEffectCategories(this.entity.effects);
// Re-define the template data references (backwards compatible)
data.item = itemData;
data.data = itemData.data;
return data; return data;
} }
@ -95,24 +86,19 @@ export default class ItemSheet5e extends ItemSheet {
// Ammunition // Ammunition
if ( consume.type === "ammo" ) { if ( consume.type === "ammo" ) {
return actor.itemTypes.consumable.reduce( return actor.itemTypes.consumable.reduce((ammo, i) => {
(ammo, i) => {
if ( i.data.data.consumableType === "ammo" ) { if ( i.data.data.consumableType === "ammo" ) {
ammo[i.id] = `${i.name} (${i.data.data.quantity})`; ammo[i.id] = `${i.name} (${i.data.data.quantity})`;
} }
return ammo; return ammo;
}, }, {});
{[item._id]: `${item.name} (${item.data.quantity})`}
);
} }
// Attributes // Attributes
else if ( consume.type === "attribute" ) { else if ( consume.type === "attribute" ) {
const attributes = TokenDocument.getTrackedAttributes(actor.data.data); const attributes = Object.values(CombatTrackerConfig.prototype.getAttributeChoices())[0]; // Bit of a hack
attributes.bar.forEach((a) => a.push("value")); return attributes.reduce((obj, a) => {
return attributes.bar.concat(attributes.value).reduce((obj, a) => { obj[a] = a;
let k = a.join(".");
obj[k] = k;
return obj; return obj;
}, {}); }, {});
} }
@ -130,16 +116,13 @@ export default class ItemSheet5e extends ItemSheet {
// Charges // Charges
else if ( consume.type === "charges" ) { else if ( consume.type === "charges" ) {
return actor.items.reduce((obj, i) => { return actor.items.reduce((obj, i) => {
// Limited-use items // Limited-use items
const uses = i.data.data.uses || {}; const uses = i.data.data.uses || {};
if ( uses.per && uses.max ) { if ( uses.per && uses.max ) {
const label = const label = uses.per === "charges" ?
uses.per === "charges" ` (${game.i18n.format("SW5E.AbilityUseChargesLabel", {value: uses.value})})` :
? ` (${game.i18n.format("SW5E.AbilityUseChargesLabel", {value: uses.value})})` ` (${game.i18n.format("SW5E.AbilityUseConsumableLabel", {max: uses.max, per: uses.per})})`;
: ` (${game.i18n.format("SW5E.AbilityUseConsumableLabel", {
max: uses.max,
per: uses.per
})})`;
obj[i.id] = i.name + label; obj[i.id] = i.name + label;
} }
@ -147,8 +130,9 @@ export default class ItemSheet5e extends ItemSheet {
const recharge = i.data.data.recharge || {}; const recharge = i.data.data.recharge || {};
if ( recharge.value ) obj[i.id] = `${i.name} (${game.i18n.format("SW5E.Recharge")})`; if ( recharge.value ) obj[i.id] = `${i.name} (${game.i18n.format("SW5E.Recharge")})`;
return obj; return obj;
}, {}); }, {})
} else return {}; }
else return {};
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -161,9 +145,11 @@ export default class ItemSheet5e extends ItemSheet {
_getItemStatus(item) { _getItemStatus(item) {
if ( item.type === "power" ) { if ( item.type === "power" ) {
return CONFIG.SW5E.powerPreparationModes[item.data.preparation]; return CONFIG.SW5E.powerPreparationModes[item.data.preparation];
} else if (["weapon", "equipment"].includes(item.type)) { }
else if ( ["weapon", "equipment"].includes(item.type) ) {
return game.i18n.localize(item.data.equipped ? "SW5E.Equipped" : "SW5E.Unequipped"); return game.i18n.localize(item.data.equipped ? "SW5E.Equipped" : "SW5E.Unequipped");
} else if (item.type === "tool") { }
else if ( item.type === "tool" ) {
return game.i18n.localize(item.data.proficient ? "SW5E.Proficient" : "SW5E.NotProficient"); return game.i18n.localize(item.data.proficient ? "SW5E.Proficient" : "SW5E.NotProficient");
} }
} }
@ -180,40 +166,48 @@ export default class ItemSheet5e extends ItemSheet {
const labels = this.item.labels; const labels = this.item.labels;
if ( item.type === "weapon" ) { if ( item.type === "weapon" ) {
props.push(...Object.entries(item.data.properties)
.filter(e => e[1] === true)
.map(e => CONFIG.SW5E.weaponProperties[e[0]]));
}
else if ( item.type === "power" ) {
props.push( props.push(
...Object.entries(item.data.properties) labels.components,
.filter((e) => e[1] === true)
.map((e) => CONFIG.SW5E.weaponProperties[e[0]])
);
} else if (item.type === "power") {
props.push(
labels.materials, labels.materials,
item.data.components.concentration ? game.i18n.localize("SW5E.Concentration") : null, item.data.components.concentration ? game.i18n.localize("SW5E.Concentration") : null,
item.data.components.ritual ? game.i18n.localize("SW5E.Ritual") : null item.data.components.ritual ? game.i18n.localize("SW5E.Ritual") : null
); )
} else if (item.type === "equipment") { }
else if ( item.type === "equipment" ) {
props.push(CONFIG.SW5E.equipmentTypes[item.data.armor.type]); props.push(CONFIG.SW5E.equipmentTypes[item.data.armor.type]);
props.push(labels.armor); props.push(labels.armor);
} else if (item.type === "feat") { }
else if ( item.type === "feat" ) {
props.push(labels.featType); props.push(labels.featType);
//TODO: Work out these }
} else if (item.type === "species") {
else if ( item.type === "species" ) {
//props.push(labels.species); //props.push(labels.species);
} else if (item.type === "archetype") { }
else if ( item.type === "archetype" ) {
//props.push(labels.archetype); //props.push(labels.archetype);
} else if (item.type === "background") { }
else if ( item.type === "background" ) {
//props.push(labels.background); //props.push(labels.background);
} else if (item.type === "classfeature") { }
else if ( item.type === "classfeature" ) {
//props.push(labels.classfeature); //props.push(labels.classfeature);
} else if (item.type === "deployment") { }
//props.push(labels.deployment); else if ( item.type === "fightingmastery" ) {
} else if (item.type === "venture") {
//props.push(labels.venture);
} else if (item.type === "fightingmastery") {
//props.push(labels.fightingmastery); //props.push(labels.fightingmastery);
} else if (item.type === "fightingstyle") { }
else if ( item.type === "fightingstyle" ) {
//props.push(labels.fightingstyle); //props.push(labels.fightingstyle);
} else if (item.type === "lightsaberform") { }
else if ( item.type === "lightsaberform" ) {
//props.push(labels.lightsaberform); //props.push(labels.lightsaberform);
} }
@ -223,10 +217,15 @@ export default class ItemSheet5e extends ItemSheet {
} }
// Action usage // Action usage
if (item.type !== "weapon" && item.data.activation && !isObjectEmpty(item.data.activation)) { if ( (item.type !== "weapon") && item.data.activation && !isObjectEmpty(item.data.activation) ) {
props.push(labels.activation, labels.range, labels.target, labels.duration); props.push(
labels.activation,
labels.range,
labels.target,
labels.duration
)
} }
return props.filter((p) => !!p); return props.filter(p => !!p);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -241,18 +240,16 @@ export default class ItemSheet5e extends ItemSheet {
*/ */
_isItemMountable(item) { _isItemMountable(item) {
const data = item.data; const data = item.data;
return ( return (item.type === 'weapon' && data.weaponType === 'siege')
(item.type === "weapon" && data.weaponType === "siege") || || (item.type === 'equipment' && data.armor.type === 'vehicle');
(item.type === "equipment" && data.armor.type === "vehicle")
);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @inheritdoc */ /** @override */
setPosition(position={}) { setPosition(position={}) {
if ( !(this._minimized || position.height) ) { if ( !(this._minimized || position.height) ) {
position.height = this._tabs[0].active === "details" ? "auto" : this.options.height; position.height = (this._tabs[0].active === "details") ? "auto" : this.options.height;
} }
return super.setPosition(position); return super.setPosition(position);
} }
@ -261,8 +258,9 @@ export default class ItemSheet5e extends ItemSheet {
/* Form Submission */ /* Form Submission */
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @inheritdoc */ /** @override */
_getSubmitData(updateData={}) { _getSubmitData(updateData={}) {
// Create the expanded update data object // Create the expanded update data object
const fd = new FormDataExtended(this.form, {editors: this.editors}); const fd = new FormDataExtended(this.form, {editors: this.editors});
let data = fd.toObject(); let data = fd.toObject();
@ -271,7 +269,7 @@ export default class ItemSheet5e extends ItemSheet {
// Handle Damage array // Handle Damage array
const damage = data.data?.damage; const damage = data.data?.damage;
if (damage) damage.parts = Object.values(damage?.parts || {}).map((d) => [d[0] || "", d[1] || ""]); if ( damage ) damage.parts = Object.values(damage?.parts || {}).map(d => [d[0] || "", d[1] || ""]);
// Return the flattened submission data // Return the flattened submission data
return flattenObject(data); return flattenObject(data);
@ -279,18 +277,15 @@ export default class ItemSheet5e extends ItemSheet {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @inheritdoc */ /** @override */
activateListeners(html) { activateListeners(html) {
super.activateListeners(html); super.activateListeners(html);
if ( this.isEditable ) { if ( this.isEditable ) {
html.find(".damage-control").click(this._onDamageControl.bind(this)); html.find(".damage-control").click(this._onDamageControl.bind(this));
html.find(".trait-selector.class-skills").click(this._onConfigureTraits.bind(this)); html.find('.trait-selector.class-skills').click(this._onConfigureClassSkills.bind(this));
html.find(".effect-control").click((ev) => { html.find(".effect-control").click(ev => {
if (this.item.isOwned) if ( this.item.isOwned ) return ui.notifications.warn("Managing Active Effects within an Owned Item is not currently supported and will be added in a subsequent update.")
return ui.notifications.warn( onManageActiveEffect(ev, this.item)
"Managing Active Effects within an Owned Item is not currently supported and will be added in a subsequent update."
);
onManageActiveEffect(ev, this.item);
}); });
} }
} }
@ -318,7 +313,7 @@ export default class ItemSheet5e extends ItemSheet {
if ( a.classList.contains("delete-damage") ) { if ( a.classList.contains("delete-damage") ) {
await this._onSubmit(event); // Submit any unsaved changes await this._onSubmit(event); // Submit any unsaved changes
const li = a.closest(".damage-part"); const li = a.closest(".damage-part");
const damage = foundry.utils.deepClone(this.item.data.data.damage); const damage = duplicate(this.item.data.data.damage);
damage.parts.splice(Number(li.dataset.damagePart), 1); damage.parts.splice(Number(li.dataset.damagePart), 1);
return this.item.update({"data.damage.parts": damage.parts}); return this.item.update({"data.damage.parts": damage.parts});
} }
@ -327,42 +322,33 @@ export default class ItemSheet5e extends ItemSheet {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
* Handle spawning the TraitSelector application for selection various options. * Handle spawning the TraitSelector application which allows a checkbox of multiple trait options
* @param {Event} event The click event which originated the selection * @param {Event} event The click event which originated the selection
* @private * @private
*/ */
_onConfigureTraits(event) { _onConfigureClassSkills(event) {
event.preventDefault(); event.preventDefault();
const a = event.currentTarget;
const options = {
name: a.dataset.target,
title: a.parentElement.innerText,
choices: [],
allowCustom: false
};
switch (a.dataset.options) {
case "saves":
options.choices = CONFIG.SW5E.abilities;
options.valueKey = null;
break;
case "skills":
const skills = this.item.data.data.skills; const skills = this.item.data.data.skills;
const choiceSet = const choices = skills.choices && skills.choices.length ? skills.choices : Object.keys(CONFIG.SW5E.skills);
skills.choices && skills.choices.length ? skills.choices : Object.keys(CONFIG.SW5E.skills); const a = event.currentTarget;
options.choices = Object.fromEntries( const label = a.parentElement;
Object.entries(CONFIG.SW5E.skills).filter((skill) => choiceSet.includes(skill[0]))
); // Render the Trait Selector dialog
options.maximum = skills.number; new TraitSelector(this.item, {
break; name: a.dataset.edit,
} title: label.innerText,
new TraitSelector(this.item, options).render(true); choices: Object.entries(CONFIG.SW5E.skills).reduce((obj, e) => {
if ( choices.includes(e[0] ) ) obj[e[0]] = e[1];
return obj;
}, {}),
minimum: skills.number,
maximum: skills.number
}).render(true)
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @inheritdoc */ /** @override */
async _onSubmit(...args) { async _onSubmit(...args) {
if ( this._tabs[0].active === "details" ) this.position.height = "auto"; if ( this._tabs[0].active === "details" ) this.position.height = "auto";
await super._onSubmit(...args); await super._onSubmit(...args);

View file

@ -1,3 +1,4 @@
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Hotbar Macros */ /* Hotbar Macros */
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -16,7 +17,7 @@ export async function create5eMacro(data, slot) {
// Create the macro command // Create the macro command
const command = `game.sw5e.rollItemMacro("${item.name}");`; const command = `game.sw5e.rollItemMacro("${item.name}");`;
let macro = game.macros.entities.find((m) => m.name === item.name && m.command === command); let macro = game.macros.entities.find(m => (m.name === item.name) && (m.command === command));
if ( !macro ) { if ( !macro ) {
macro = await Macro.create({ macro = await Macro.create({
name: item.name, name: item.name,
@ -45,16 +46,15 @@ export function rollItemMacro(itemName) {
if ( !actor ) actor = game.actors.get(speaker.actor); if ( !actor ) actor = game.actors.get(speaker.actor);
// Get matching items // Get matching items
const items = actor ? actor.items.filter((i) => i.name === itemName) : []; const items = actor ? actor.items.filter(i => i.name === itemName) : [];
if ( items.length > 1 ) { if ( items.length > 1 ) {
ui.notifications.warn( ui.notifications.warn(`Your controlled Actor ${actor.name} has more than one Item with name ${itemName}. The first matched item will be chosen.`);
`Your controlled Actor ${actor.name} has more than one Item with name ${itemName}. The first matched item will be chosen.`
);
} else if ( items.length === 0 ) { } else if ( items.length === 0 ) {
return ui.notifications.warn(`Your controlled Actor does not have an item named ${itemName}`); return ui.notifications.warn(`Your controlled Actor does not have an item named ${itemName}`);
} }
const item = items[0]; const item = items[0];
// Trigger the item roll // Trigger the item roll
if ( item.data.type === "power" ) return actor.usePower(item);
return item.roll(); return item.roll();
} }

View file

@ -3,17 +3,13 @@
* @return {Promise} A Promise which resolves once the migration is completed * @return {Promise} A Promise which resolves once the migration is completed
*/ */
export const migrateWorld = async function() { export const migrateWorld = async function() {
ui.notifications.info( 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});
`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 // Migrate World Actors
for await (let a of game.actors.contents) { for ( let a of game.actors.entities ) {
try { try {
console.log(`Checking Actor entity ${a.name} for migration needs`); const updateData = migrateActorData(a.data);
const updateData = await migrateActorData(a.data); if ( !isObjectEmpty(updateData) ) {
if (!foundry.utils.isObjectEmpty(updateData)) {
console.log(`Migrating Actor entity ${a.name}`); console.log(`Migrating Actor entity ${a.name}`);
await a.update(updateData, {enforceTypes: false}); await a.update(updateData, {enforceTypes: false});
} }
@ -24,10 +20,10 @@ export const migrateWorld = async function () {
} }
// Migrate World Items // Migrate World Items
for (let i of game.items.contents) { for ( let i of game.items.entities ) {
try { try {
const updateData = migrateItemData(i.toObject()); const updateData = migrateItemData(i.data);
if (!foundry.utils.isObjectEmpty(updateData)) { if ( !isObjectEmpty(updateData) ) {
console.log(`Migrating Item entity ${i.name}`); console.log(`Migrating Item entity ${i.name}`);
await i.update(updateData, {enforceTypes: false}); await i.update(updateData, {enforceTypes: false});
} }
@ -38,15 +34,12 @@ export const migrateWorld = async function () {
} }
// Migrate Actor Override Tokens // Migrate Actor Override Tokens
for (let s of game.scenes.contents) { for ( let s of game.scenes.entities ) {
try { try {
const updateData = await migrateSceneData(s.data); const updateData = migrateSceneData(s.data);
if (!foundry.utils.isObjectEmpty(updateData)) { if ( !isObjectEmpty(updateData) ) {
console.log(`Migrating Scene entity ${s.name}`); console.log(`Migrating Scene entity ${s.name}`);
await s.update(updateData, {enforceTypes: false}); await s.update(updateData, {enforceTypes: false});
// If we do not do this, then synthetic token actors remain in cache
// with the un-updated actorData.
s.tokens.contents.forEach((t) => (t._actor = null));
} }
} catch(err) { } catch(err) {
err.message = `Failed sw5e system migration for Scene ${s.name}: ${err.message}`; err.message = `Failed sw5e system migration for Scene ${s.name}: ${err.message}`;
@ -63,7 +56,7 @@ export const migrateWorld = async function () {
// Set the migration as complete // Set the migration as complete
game.settings.set("sw5e", "systemMigrationVersion", game.system.data.version); 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});
}; };
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -83,37 +76,40 @@ export const migrateCompendium = async function (pack) {
// Begin by requesting server-side data model migration and get the migrated content // Begin by requesting server-side data model migration and get the migrated content
await pack.migrate(); await pack.migrate();
const documents = await pack.getDocuments(); const content = await pack.getContent();
// Iterate over compendium entries - applying fine-tuned migration functions // Iterate over compendium entries - applying fine-tuned migration functions
for await (let doc of documents) { for ( let ent of content ) {
let updateData = {}; let updateData = {};
try { try {
switch (entity) { switch (entity) {
case "Actor": case "Actor":
updateData = await migrateActorData(doc.data); updateData = migrateActorData(ent.data);
break; break;
case "Item": case "Item":
updateData = migrateItemData(doc.toObject()); updateData = migrateItemData(ent.data);
break; break;
case "Scene": case "Scene":
updateData = await migrateSceneData(doc.data); updateData = migrateSceneData(ent.data);
break; break;
} }
if (foundry.utils.isObjectEmpty(updateData)) continue; if ( isObjectEmpty(updateData) ) continue;
// Save the entry, if data was changed // Save the entry, if data was changed
await doc.update(updateData); updateData["_id"] = ent._id;
console.log(`Migrated ${entity} entity ${doc.name} in Compendium ${pack.collection}`); await pack.updateEntity(updateData);
} catch (err) { console.log(`Migrated ${entity} entity ${ent.name} in Compendium ${pack.collection}`);
}
// Handle migration failures // Handle migration failures
err.message = `Failed sw5e system migration for entity ${doc.name} in pack ${pack.collection}: ${err.message}`; catch(err) {
err.message = `Failed sw5e system migration for entity ${ent.name} in pack ${pack.collection}: ${err.message}`;
console.error(err); console.error(err);
} }
} }
// Apply the original locked status for the pack // Apply the original locked status for the pack
await pack.configure({locked: wasLocked}); pack.configure({locked: wasLocked});
console.log(`Migrated all ${entity} entities from Compendium ${pack.collection}`); console.log(`Migrated all ${entity} entities from Compendium ${pack.collection}`);
}; };
@ -124,68 +120,51 @@ export const migrateCompendium = async function (pack) {
/** /**
* Migrate a single Actor entity to incorporate latest data model changes * Migrate a single Actor entity to incorporate latest data model changes
* Return an Object of updateData to be applied * Return an Object of updateData to be applied
* @param {object} actor The actor data object to update * @param {Actor} actor The actor to Update
* @return {Object} The updateData to apply * @return {Object} The updateData to apply
*/ */
export const migrateActorData = async function (actor) { export const migrateActorData = function(actor) {
const updateData = {}; const updateData = {};
// Actor Data Updates // Actor Data Updates
if (actor.data) { _migrateActorBonuses(actor, updateData);
_migrateActorMovement(actor, updateData); _migrateActorMovement(actor, updateData);
_migrateActorSenses(actor, updateData);
_migrateActorType(actor, updateData);
}
// Migrate Owned Items // Migrate Owned Items
if (!!actor.items) { if ( !actor.items ) return updateData;
const items = await actor.items.reduce(async (memo, i) => { let hasItemUpdates = false;
const results = await memo; const items = actor.items.map(i => {
// Migrate the Owned Item // Migrate the Owned Item
const itemData = i instanceof CONFIG.Item.documentClass ? i.toObject() : i; let itemUpdate = migrateItemData(i);
let itemUpdate = await migrateActorItemData(itemData, actor);
// Prepared, Equipped, and Proficient for NPC actors // Prepared, Equipped, and Proficient for NPC actors
if ( actor.type === "npc" ) { if ( actor.type === "npc" ) {
if (getProperty(itemData.data, "preparation.prepared") === false) if (getProperty(i.data, "preparation.prepared") === false) itemUpdate["data.preparation.prepared"] = true;
itemUpdate["data.preparation.prepared"] = true; if (getProperty(i.data, "equipped") === false) itemUpdate["data.equipped"] = true;
if (getProperty(itemData.data, "equipped") === false) itemUpdate["data.equipped"] = true; if (getProperty(i.data, "proficient") === false) itemUpdate["data.proficient"] = true;
if (getProperty(itemData.data, "proficient") === false) itemUpdate["data.proficient"] = true;
} }
// Update the Owned Item // Update the Owned Item
if ( !isObjectEmpty(itemUpdate) ) { if ( !isObjectEmpty(itemUpdate) ) {
itemUpdate._id = itemData._id; hasItemUpdates = true;
console.log(`Migrating Actor ${actor.name}'s ${i.name}`); return mergeObject(i, itemUpdate, {enforceTypes: false, inplace: false});
results.push(expandObject(itemUpdate)); } else return i;
} });
if ( hasItemUpdates ) updateData.items = items;
return results;
}, []);
if (items.length > 0) updateData.items = items;
}
// Update NPC data with new datamodel information
if (actor.type === "npc") {
_updateNPCData(actor);
}
// migrate powers last since it relies on item classes being migrated first.
_migrateActorPowers(actor, updateData);
return updateData; return updateData;
}; };
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
* Scrub an Actor's system data, removing all keys which are not explicitly defined in the system template * Scrub an Actor's system data, removing all keys which are not explicitly defined in the system template
* @param {Object} actorData The data object for an Actor * @param {Object} actorData The data object for an Actor
* @return {Object} The scrubbed Actor data * @return {Object} The scrubbed Actor data
*/ */
function cleanActorData(actorData) { function cleanActorData(actorData) {
// Scrub system data // Scrub system data
const model = game.system.model.Actor[actorData.type]; const model = game.system.model.Actor[actorData.type];
actorData.data = filterObject(actorData.data, model); actorData.data = filterObject(actorData.data, model);
@ -203,33 +182,15 @@ function cleanActorData(actorData) {
return actorData; return actorData;
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
* Migrate a single Item entity to incorporate latest data model changes * Migrate a single Item entity to incorporate latest data model changes
* * @param item
* @param {object} item Item data to migrate
* @return {object} The updateData to apply
*/ */
export const migrateItemData = function(item) { export const migrateItemData = function(item) {
const updateData = {}; const updateData = {};
_migrateItemClassPowerCasting(item, updateData);
_migrateItemAttunement(item, updateData);
return updateData;
};
/* -------------------------------------------- */
/**
* Migrate a single owned actor Item entity to incorporate latest data model changes
* @param item
* @param actor
*/
export const migrateActorItemData = async function (item, actor) {
const updateData = {};
_migrateItemClassPowerCasting(item, updateData);
_migrateItemAttunement(item, updateData);
await _migrateItemPower(item, actor, updateData);
return updateData; return updateData;
}; };
@ -241,428 +202,58 @@ export const migrateActorItemData = async function (item, actor) {
* @param {Object} scene The Scene data to Update * @param {Object} scene The Scene data to Update
* @return {Object} The updateData to apply * @return {Object} The updateData to apply
*/ */
export const migrateSceneData = async function (scene) { export const migrateSceneData = function(scene) {
const tokens = await Promise.all( const tokens = duplicate(scene.tokens);
scene.tokens.map(async (token) => { return {
const t = token.toJSON(); tokens: tokens.map(t => {
if (!t.actorId || t.actorLink) { if (!t.actorId || t.actorLink || !t.actorData.data) {
t.actorData = {}; t.actorData = {};
} else if (!game.actors.has(t.actorId)) { return t;
}
const token = new Token(t);
if ( !token.actor ) {
t.actorId = null; t.actorId = null;
t.actorData = {}; t.actorData = {};
} else if ( !t.actorLink ) { } else if ( !t.actorLink ) {
const actorData = duplicate(t.actorData); const updateData = migrateActorData(token.data.actorData);
actorData.type = token.actor?.type; t.actorData = mergeObject(token.data.actorData, updateData);
const update = migrateActorData(actorData);
["items", "effects"].forEach((embeddedName) => {
if (!update[embeddedName]?.length) return;
const updates = new Map(update[embeddedName].map((u) => [u._id, u]));
t.actorData[embeddedName].forEach((original) => {
const update = updates.get(original._id);
if (update) mergeObject(original, update);
});
delete update[embeddedName];
});
mergeObject(t.actorData, update);
} }
return t; return t;
}) })
); };
return {tokens};
}; };
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Low level migration utilities /* Low level migration utilities
/* -------------------------------------------- */ /* -------------------------------------------- */
/* -------------------------------------------- */
/** /**
* Update an NPC Actor's data based on compendium * Migrate the actor bonuses object
* @param {Object} actor The data object for an Actor
* @return {Object} The updated Actor
*/
function _updateNPCData(actor) {
let actorData = actor.data;
const updateData = {};
// check for flag.core, if not there is no compendium monster so exit
const hasSource = actor?.flags?.core?.sourceId !== undefined;
if (!hasSource) return actor;
// shortcut out if dataVersion flag is set to 1.2.4 or higher
const hasDataVersion = actor?.flags?.sw5e?.dataVersion !== undefined;
if (
hasDataVersion &&
(actor.flags.sw5e.dataVersion === "1.2.4" || isNewerVersion("1.2.4", actor.flags.sw5e.dataVersion))
)
return actor;
// Check to see what the source of NPC is
const sourceId = actor.flags.core.sourceId;
const coreSource = sourceId.substr(0, sourceId.length - 17);
const core_id = sourceId.substr(sourceId.length - 16, 16);
if (coreSource === "Compendium.sw5e.monsters") {
game.packs
.get("sw5e.monsters")
.getEntity(core_id)
.then((monster) => {
const monsterData = monster.data.data;
// copy movement[], senses[], powercasting, force[], tech[], powerForceLevel, powerTechLevel
updateData["data.attributes.movement"] = monsterData.attributes.movement;
updateData["data.attributes.senses"] = monsterData.attributes.senses;
updateData["data.attributes.powercasting"] = monsterData.attributes.powercasting;
updateData["data.attributes.force"] = monsterData.attributes.force;
updateData["data.attributes.tech"] = monsterData.attributes.tech;
updateData["data.details.powerForceLevel"] = monsterData.details.powerForceLevel;
updateData["data.details.powerTechLevel"] = monsterData.details.powerTechLevel;
// push missing powers onto actor
let newPowers = [];
for (let i of monster.items) {
const itemData = i.data;
if (itemData.type === "power") {
const itemCompendium_id = itemData.flags?.core?.sourceId.split(".").slice(-1)[0];
let hasPower = !!actor.items.find(
(item) => item.flags?.core?.sourceId.split(".").slice(-1)[0] === itemCompendium_id
);
if (!hasPower) {
// Clone power to new object. Don't know if it is technically needed, but seems to prevent some weirdness.
const newPower = JSON.parse(JSON.stringify(itemData));
newPowers.push(newPower);
}
}
}
// get actor to create new powers
const liveActor = game.actors.get(actor._id);
// create the powers on the actor
liveActor.createEmbeddedEntity("OwnedItem", newPowers);
// set flag to check to see if migration has been done so we don't do it again.
liveActor.setFlag("sw5e", "dataVersion", "1.2.4");
});
}
//merge object
actorData = mergeObject(actorData, updateData);
// Return the scrubbed data
return actor;
}
/**
* Migrate the actor speed string to movement object
* @private * @private
*/ */
function _migrateActorMovement(actorData, updateData) { function _migrateActorBonuses(actor, updateData) {
const ad = actorData.data; const b = game.system.model.Actor.character.bonuses;
for ( let k of Object.keys(actor.data.bonuses || {}) ) {
// Work is needed if old data is present if ( k in b ) updateData[`data.bonuses.${k}`] = b[k];
const old = actorData.type === "vehicle" ? ad?.attributes?.speed : ad?.attributes?.speed?.value; else updateData[`data.bonuses.-=${k}`] = null;
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 speed string to movement object * Migrate the actor bonuses object
* @private * @private
*/ */
function _migrateActorPowers(actorData, updateData) { function _migrateActorMovement(actor, updateData) {
const ad = actorData.data; if ( actor.data.attributes?.movement?.walk !== undefined ) return;
const s = (actor.data.attributes?.speed?.value || "").split(" ");
// If new Force & Tech data is not present, create it if ( s.length > 0 ) updateData["data.attributes.movement.walk"] = Number.isNumeric(s[0]) ? parseInt(s[0]) : null;
let hasNewAttrib = ad?.attributes?.force?.level !== undefined;
if (!hasNewAttrib) {
updateData["data.attributes.force.known.value"] = 0;
updateData["data.attributes.force.known.max"] = 0;
updateData["data.attributes.force.points.value"] = 0;
updateData["data.attributes.force.points.min"] = 0;
updateData["data.attributes.force.points.max"] = 0;
updateData["data.attributes.force.points.temp"] = null;
updateData["data.attributes.force.points.tempmax"] = null;
updateData["data.attributes.force.level"] = 0;
updateData["data.attributes.tech.known.value"] = 0;
updateData["data.attributes.tech.known.max"] = 0;
updateData["data.attributes.tech.points.value"] = 0;
updateData["data.attributes.tech.points.min"] = 0;
updateData["data.attributes.tech.points.max"] = 0;
updateData["data.attributes.tech.points.temp"] = null;
updateData["data.attributes.tech.points.tempmax"] = null;
updateData["data.attributes.tech.level"] = 0;
} }
// If new Power F/T split data is not present, create it
const hasNewLimit = ad?.powers?.power1?.foverride !== undefined;
if (!hasNewLimit) {
for (let i = 1; i <= 9; i++) {
// add new
updateData["data.powers.power" + i + ".fvalue"] = getProperty(ad.powers, "power" + i + ".value");
updateData["data.powers.power" + i + ".fmax"] = getProperty(ad.powers, "power" + i + ".max");
updateData["data.powers.power" + i + ".foverride"] = null;
updateData["data.powers.power" + i + ".tvalue"] = getProperty(ad.powers, "power" + i + ".value");
updateData["data.powers.power" + i + ".tmax"] = getProperty(ad.powers, "power" + i + ".max");
updateData["data.powers.power" + i + ".toverride"] = null;
//remove old
updateData["data.powers.power" + i + ".-=value"] = null;
updateData["data.powers.power" + i + ".-=override"] = null;
}
}
// If new Bonus Power DC data is not present, create it
const hasNewBonus = ad?.bonuses?.power?.forceLightDC !== undefined;
if (!hasNewBonus) {
updateData["data.bonuses.power.forceLightDC"] = "";
updateData["data.bonuses.power.forceDarkDC"] = "";
updateData["data.bonuses.power.forceUnivDC"] = "";
updateData["data.bonuses.power.techDC"] = "";
}
// Remove the Power DC Bonus
updateData["data.bonuses.power.-=dc"] = null;
return updateData;
}
/* -------------------------------------------- */ /* -------------------------------------------- */
/**
* Migrate the actor traits.senses string to attributes.senses object
* @private
*/
function _migrateActorSenses(actor, updateData) {
const ad = actor.data;
if (ad?.traits?.senses === undefined) return;
const original = ad.traits.senses || "";
if (typeof original !== "string") return;
// 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;
}
/* -------------------------------------------- */
/**
* Migrate the actor details.type string to object
* @private
*/
function _migrateActorType(actor, updateData) {
const ad = actor.data;
const original = ad.details?.type;
if (typeof original !== "string") return;
// New default data structure
let data = {
value: "",
subtype: "",
swarm: "",
custom: ""
};
// Specifics
// (Some of these have weird names, these need to be addressed individually)
if (original === "force entity") {
data.value = "force";
data.subtype = "storm";
} else if (original === "human") {
data.value = "humanoid";
data.subtype = "human";
} else if (["humanoid (any)", "humanoid (Villainous"].includes(original)) {
data.value = "humanoid";
} else if (original === "tree") {
data.value = "plant";
data.subtype = "tree";
} else if (original === "(humanoid) or Large (beast) force entity") {
data.value = "force";
} else if (original === "droid (appears human)") {
data.value = "droid";
} else {
// Match the existing string
const pattern = /^(?:swarm of (?<size>[\w\-]+) )?(?<type>[^(]+?)(?:\((?<subtype>[^)]+)\))?$/i;
const match = original.trim().match(pattern);
if (match) {
// Match a known creature type
const typeLc = match.groups.type.trim().toLowerCase();
const typeMatch = Object.entries(CONFIG.SW5E.creatureTypes).find(([k, v]) => {
return (
typeLc === k ||
typeLc === game.i18n.localize(v).toLowerCase() ||
typeLc === game.i18n.localize(`${v}Pl`).toLowerCase()
);
});
if (typeMatch) data.value = typeMatch[0];
else {
data.value = "custom";
data.custom = match.groups.type.trim().titleCase();
}
data.subtype = match.groups.subtype?.trim().titleCase() || "";
// Match a swarm
const isNamedSwarm = actor.name.startsWith(game.i18n.localize("SW5E.CreatureSwarm"));
if (match.groups.size || isNamedSwarm) {
const sizeLc = match.groups.size ? match.groups.size.trim().toLowerCase() : "tiny";
const sizeMatch = Object.entries(CONFIG.SW5E.actorSizes).find(([k, v]) => {
return sizeLc === k || sizeLc === game.i18n.localize(v).toLowerCase();
});
data.swarm = sizeMatch ? sizeMatch[0] : "tiny";
} else data.swarm = "";
}
// No match found
else {
data.value = "custom";
data.custom = original;
}
}
// Update the actor data
updateData["data.details.type"] = data;
return updateData;
}
/* -------------------------------------------- */
/**
* @private
*/
function _migrateItemClassPowerCasting(item, updateData) {
if (item.type === "class") {
switch (item.name) {
case "Consular":
updateData["data.powercasting"] = {
progression: "consular",
ability: ""
};
break;
case "Engineer":
updateData["data.powercasting"] = {
progression: "engineer",
ability: ""
};
break;
case "Guardian":
updateData["data.powercasting"] = {
progression: "guardian",
ability: ""
};
break;
case "Scout":
updateData["data.powercasting"] = {
progression: "scout",
ability: ""
};
break;
case "Sentinel":
updateData["data.powercasting"] = {
progression: "sentinel",
ability: ""
};
break;
}
}
return updateData;
}
/* -------------------------------------------- */
/**
* Update an Power Item's data based on compendium
* @param {Object} item The data object for an item
* @param {Object} actor The data object for the actor owning the item
* @private
*/
async function _migrateItemPower(item, actor, updateData) {
// if item is not a power shortcut out
if (item.type !== "power") return updateData;
console.log(`Checking Actor ${actor.name}'s ${item.name} for migration needs`);
// check for flag.core, if not there is no compendium power so exit
const hasSource = item?.flags?.core?.sourceId !== undefined;
if (!hasSource) return updateData;
// shortcut out if dataVersion flag is set to 1.2.4 or higher
const hasDataVersion = item?.flags?.sw5e?.dataVersion !== undefined;
if (
hasDataVersion &&
(item.flags.sw5e.dataVersion === "1.2.4" || isNewerVersion("1.2.4", item.flags.sw5e.dataVersion))
)
return updateData;
// Check to see what the source of Power is
const sourceId = item.flags.core.sourceId;
const coreSource = sourceId.substr(0, sourceId.length - 17);
const core_id = sourceId.substr(sourceId.length - 16, 16);
//if power type is not force or tech exit out
let powerType = "none";
if (coreSource === "Compendium.sw5e.forcepowers") powerType = "sw5e.forcepowers";
if (coreSource === "Compendium.sw5e.techpowers") powerType = "sw5e.techpowers";
if (powerType === "none") return updateData;
const corePower = duplicate(await game.packs.get(powerType).getEntity(core_id));
console.log(`Updating Actor ${actor.name}'s ${item.name} from compendium`);
const corePowerData = corePower.data;
// copy Core Power Data over original Power
updateData["data"] = corePowerData;
updateData["flags"] = {sw5e: {dataVersion: "1.2.4"}};
return updateData;
//game.packs.get(powerType).getEntity(core_id).then(corePower => {
//})
}
/* -------------------------------------------- */
/**
* Delete the old data.attuned boolean
*
* @param {object} item Item data to migrate
* @param {object} updateData Existing update to expand upon
* @return {object} The updateData to apply
* @private
*/
function _migrateItemAttunement(item, updateData) {
if (item.data?.attuned === undefined) return updateData;
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. * A general tool to purge flags from all entities in a Compendium pack.
@ -679,10 +270,10 @@ export async function purgeFlags(pack) {
for ( let entity of content ) { for ( let entity of content ) {
const update = {_id: entity.id, flags: cleanFlags(entity.data.flags)}; const update = {_id: entity.id, flags: cleanFlags(entity.data.flags)};
if ( pack.entity === "Actor" ) { if ( pack.entity === "Actor" ) {
update.items = entity.data.items.map((i) => { update.items = entity.data.items.map(i => {
i.flags = cleanFlags(i.flags); i.flags = cleanFlags(i.flags);
return i; return i;
}); })
} }
await pack.updateEntity(update, {recursive: false}); await pack.updateEntity(update, {recursive: false});
console.log(`Purged flags from ${entity.name}`); console.log(`Purged flags from ${entity.name}`);
@ -692,6 +283,7 @@ export async function purgeFlags(pack) {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**
* Purge the data model of any inner objects which have been flagged as _deprecated. * Purge the data model of any inner objects which have been flagged as _deprecated.
* @param {object} data The data to clean * @param {object} data The data to clean
@ -703,7 +295,8 @@ export function removeDeprecatedObjects(data) {
if (v._deprecated === true) { if (v._deprecated === true) {
console.log(`Deleting deprecated object key ${k}`); console.log(`Deleting deprecated object key ${k}`);
delete data[k]; delete data[k];
} else removeDeprecatedObjects(v); }
else removeDeprecatedObjects(v);
} }
} }
return data; return data;

View file

@ -5,6 +5,7 @@ import {SW5E} from "../config.js";
* @extends {MeasuredTemplate} * @extends {MeasuredTemplate}
*/ */
export default class AbilityTemplate extends MeasuredTemplate { export default class AbilityTemplate extends MeasuredTemplate {
/** /**
* A factory method to create an AbilityTemplate instance using provided data from an Item5e instance * A factory method to create an AbilityTemplate instance using provided data from an Item5e instance
* @param {Item5e} item The Item object for which to construct the template * @param {Item5e} item The Item object for which to construct the template
@ -18,7 +19,7 @@ export default class AbilityTemplate extends MeasuredTemplate {
// Prepare template data // Prepare template data
const templateData = { const templateData = {
t: templateShape, t: templateShape,
user: game.user.data._id, user: game.user._id,
distance: target.value, distance: target.value,
direction: 0, direction: 0,
x: 0, x: 0,
@ -28,8 +29,8 @@ export default class AbilityTemplate extends MeasuredTemplate {
// Additional type-specific data // Additional type-specific data
switch ( templateShape ) { switch ( templateShape ) {
case "cone": case "cone": // 5e cone RAW should be 53.13 degrees
templateData.angle = CONFIG.MeasuredTemplate.defaults.angle; templateData.angle = 53.13;
break; break;
case "rect": // 5e rectangular AoEs are always cubes case "rect": // 5e rectangular AoEs are always cubes
templateData.distance = Math.hypot(target.value, target.value); templateData.distance = Math.hypot(target.value, target.value);
@ -44,12 +45,7 @@ export default class AbilityTemplate extends MeasuredTemplate {
} }
// Return the template constructed from the item data // Return the template constructed from the item data
const cls = CONFIG.MeasuredTemplate.documentClass; return new this(templateData);
const template = new cls(templateData, {parent: canvas.scene});
const object = new this(template);
object.item = item;
object.actorSheet = item.actor?.sheet || null;
return object;
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -59,16 +55,9 @@ export default class AbilityTemplate extends MeasuredTemplate {
*/ */
drawPreview() { drawPreview() {
const initialLayer = canvas.activeLayer; const initialLayer = canvas.activeLayer;
// Draw the template and switch to the template layer
this.draw(); this.draw();
this.layer.activate(); this.layer.activate();
this.layer.preview.addChild(this); this.layer.preview.addChild(this);
// Hide the sheet that originated the preview
if (this.actorSheet) this.actorSheet.minimize();
// Activate interactivity
this.activatePreviewListeners(initialLayer); this.activatePreviewListeners(initialLayer);
} }
@ -83,43 +72,48 @@ export default class AbilityTemplate extends MeasuredTemplate {
let moveTime = 0; let moveTime = 0;
// Update placement (mouse-move) // Update placement (mouse-move)
handlers.mm = (event) => { handlers.mm = event => {
event.stopPropagation(); event.stopPropagation();
let now = Date.now(); // Apply a 20ms throttle let now = Date.now(); // Apply a 20ms throttle
if ( now - moveTime <= 20 ) return; if ( now - moveTime <= 20 ) return;
const center = event.data.getLocalPosition(this.layer); const center = event.data.getLocalPosition(this.layer);
const snapped = canvas.grid.getSnappedPosition(center.x, center.y, 2); const snapped = canvas.grid.getSnappedPosition(center.x, center.y, 2);
this.data.update({x: snapped.x, y: snapped.y}); this.data.x = snapped.x;
this.data.y = snapped.y;
this.refresh(); this.refresh();
moveTime = now; moveTime = now;
}; };
// Cancel the workflow (right-click) // Cancel the workflow (right-click)
handlers.rc = (event) => { handlers.rc = event => {
this.layer.preview.removeChildren(); this.layer.preview.removeChildren();
canvas.stage.off("mousemove", handlers.mm); canvas.stage.off("mousemove", handlers.mm);
canvas.stage.off("mousedown", handlers.lc); canvas.stage.off("mousedown", handlers.lc);
canvas.app.view.oncontextmenu = null; canvas.app.view.oncontextmenu = null;
canvas.app.view.onwheel = null; canvas.app.view.onwheel = null;
initialLayer.activate(); initialLayer.activate();
this.actorSheet.maximize();
}; };
// Confirm the workflow (left-click) // Confirm the workflow (left-click)
handlers.lc = (event) => { handlers.lc = event => {
handlers.rc(event); handlers.rc(event);
const destination = canvas.grid.getSnappedPosition(this.data.x, this.data.y, 2);
this.data.update(destination); // Confirm final snapped position
canvas.scene.createEmbeddedDocuments("MeasuredTemplate", [this.data]); const destination = canvas.grid.getSnappedPosition(this.x, this.y, 2);
this.data.x = destination.x;
this.data.y = destination.y;
// Create the template
canvas.scene.createEmbeddedEntity("MeasuredTemplate", this.data);
}; };
// Rotate the template by 3 degree increments (mouse-wheel) // Rotate the template by 3 degree increments (mouse-wheel)
handlers.mw = (event) => { handlers.mw = event => {
if ( event.ctrlKey ) event.preventDefault(); // Avoid zooming the browser window if ( event.ctrlKey ) event.preventDefault(); // Avoid zooming the browser window
event.stopPropagation(); event.stopPropagation();
let delta = canvas.grid.type > CONST.GRID_TYPES.SQUARE ? 30 : 15; let delta = canvas.grid.type > CONST.GRID_TYPES.SQUARE ? 30 : 15;
let snap = event.shiftKey ? delta : 5; let snap = event.shiftKey ? delta : 5;
this.data.update({direction: this.data.direction + snap * Math.sign(event.deltaY)}); this.data.direction += (snap * Math.sign(event.deltaY));
this.refresh(); this.refresh();
}; };

View file

@ -1,4 +1,5 @@
export const registerSystemSettings = function() { export const registerSystemSettings = function() {
/** /**
* Track the system version upon which point a migration was last applied * Track the system version upon which point a migration was last applied
*/ */
@ -7,7 +8,7 @@ export const registerSystemSettings = function () {
scope: "world", scope: "world",
config: false, config: false,
type: String, type: String,
default: game.system.data.version default: ""
}); });
/** /**
@ -21,9 +22,9 @@ export const registerSystemSettings = function () {
default: "normal", default: "normal",
type: String, type: String,
choices: { choices: {
normal: "SETTINGS.5eRestPHB", "normal": "SETTINGS.5eRestPHB",
gritty: "SETTINGS.5eRestGritty", "gritty": "SETTINGS.5eRestGritty",
epic: "SETTINGS.5eRestEpic" "epic": "SETTINGS.5eRestEpic",
} }
}); });
@ -38,11 +39,11 @@ export const registerSystemSettings = function () {
default: "555", default: "555",
type: String, type: String,
choices: { choices: {
555: "SETTINGS.5eDiagPHB", "555": "SETTINGS.5eDiagPHB",
5105: "SETTINGS.5eDiagDMG", "5105": "SETTINGS.5eDiagDMG",
EUCL: "SETTINGS.5eDiagEuclidean" "EUCL": "SETTINGS.5eDiagEuclidean",
}, },
onChange: (rule) => (canvas.grid.diagonalRule = rule) onChange: rule => canvas.grid.diagonalRule = rule
}); });
/** /**
@ -78,7 +79,7 @@ export const registerSystemSettings = function () {
scope: "world", scope: "world",
config: true, config: true,
default: false, default: false,
type: Boolean type: Boolean,
}); });
/** /**
@ -91,7 +92,7 @@ export const registerSystemSettings = function () {
config: true, config: true,
default: false, default: false,
type: Boolean, type: Boolean,
onChange: (s) => { onChange: s => {
ui.chat.render(); ui.chat.render();
} }
}); });
@ -99,10 +100,10 @@ export const registerSystemSettings = function () {
/** /**
* Option to allow GMs to restrict polymorphing to GMs only. * Option to allow GMs to restrict polymorphing to GMs only.
*/ */
game.settings.register("sw5e", "allowPolymorphing", { game.settings.register('sw5e', 'allowPolymorphing', {
name: "SETTINGS.5eAllowPolymorphingN", name: 'SETTINGS.5eAllowPolymorphingN',
hint: "SETTINGS.5eAllowPolymorphingL", hint: 'SETTINGS.5eAllowPolymorphingL',
scope: "world", scope: 'world',
config: true, config: true,
default: false, default: false,
type: Boolean type: Boolean
@ -111,8 +112,8 @@ export const registerSystemSettings = function () {
/** /**
* Remember last-used polymorph settings. * Remember last-used polymorph settings.
*/ */
game.settings.register("sw5e", "polymorphSettings", { game.settings.register('sw5e', 'polymorphSettings', {
scope: "client", scope: 'client',
default: { default: {
keepPhysical: false, keepPhysical: false,
keepMental: false, keepMental: false,
@ -137,8 +138,8 @@ export const registerSystemSettings = function () {
default: "light", default: "light",
type: String, type: String,
choices: { choices: {
light: "SETTINGS.SWColorLight", "light": "SETTINGS.SWColorLight",
dark: "SETTINGS.SWColorDark" "dark": "SETTINGS.SWColorDark"
} }
}); });
}; };

View file

@ -5,6 +5,7 @@
*/ */
export const preloadHandlebarsTemplates = async function() { export const preloadHandlebarsTemplates = async function() {
return loadTemplates([ return loadTemplates([
// Shared Partials // Shared Partials
"systems/sw5e/templates/actors/parts/active-effects.html", "systems/sw5e/templates/actors/parts/active-effects.html",
@ -17,12 +18,11 @@ export const preloadHandlebarsTemplates = async function () {
"systems/sw5e/templates/actors/newActor/parts/swalt-biography.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-core.html",
"systems/sw5e/templates/actors/newActor/parts/swalt-crew.html",
"systems/sw5e/templates/actors/newActor/parts/swalt-active-effects.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-features.html",
"systems/sw5e/templates/actors/newActor/parts/swalt-inventory.html", "systems/sw5e/templates/actors/newActor/parts/swalt-inventory.html",
"systems/sw5e/templates/actors/newActor/parts/swalt-force-powerbook.html", "systems/sw5e/templates/actors/newActor/parts/swalt-notes.html",
"systems/sw5e/templates/actors/newActor/parts/swalt-tech-powerbook.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-resources.html",
"systems/sw5e/templates/actors/newActor/parts/swalt-traits.html", "systems/sw5e/templates/actors/newActor/parts/swalt-traits.html",

View file

@ -1,102 +0,0 @@
/**
* Extend the base TokenDocument class to implement system-specific HP bar logic.
* @extends {TokenDocument}
*/
export class TokenDocument5e extends TokenDocument {
/** @inheritdoc */
getBarAttribute(...args) {
const data = super.getBarAttribute(...args);
if (data && data.attribute === "attributes.hp") {
data.value += parseInt(getProperty(this.actor.data, "data.attributes.hp.temp") || 0);
data.max += parseInt(getProperty(this.actor.data, "data.attributes.hp.tempmax") || 0);
}
return data;
}
}
/* -------------------------------------------- */
/**
* Extend the base Token class to implement additional system-specific logic.
* @extends {Token}
*/
export class Token5e extends Token {
/** @inheritdoc */
_drawBar(number, bar, data) {
if (data.attribute === "attributes.hp") return this._drawHPBar(number, bar, data);
return super._drawBar(number, bar, data);
}
/* -------------------------------------------- */
/**
* Specialized drawing function for HP bars.
* @param {number} number The Bar number
* @param {PIXI.Graphics} bar The Bar container
* @param {object} data Resource data for this bar
* @private
*/
_drawHPBar(number, bar, data) {
// Extract health data
let {value, max, temp, tempmax} = this.document.actor.data.data.attributes.hp;
temp = Number(temp || 0);
tempmax = Number(tempmax || 0);
// Differentiate between effective maximum and displayed maximum
const effectiveMax = Math.max(0, max + tempmax);
let displayMax = max + (tempmax > 0 ? tempmax : 0);
// Allocate percentages of the total
const tempPct = Math.clamped(temp, 0, displayMax) / displayMax;
const valuePct = Math.clamped(value, 0, effectiveMax) / displayMax;
const colorPct = Math.clamped(value, 0, effectiveMax) / displayMax;
// Determine colors to use
const blk = 0x000000;
const hpColor = PIXI.utils.rgb2hex([1 - colorPct / 2, colorPct, 0]);
const c = CONFIG.SW5E.tokenHPColors;
// Determine the container size (logic borrowed from core)
const w = this.w;
let h = Math.max(canvas.dimensions.size / 12, 8);
if (this.data.height >= 2) h *= 1.6;
const bs = Math.clamped(h / 8, 1, 2);
const bs1 = bs + 1;
// Overall bar container
bar.clear();
bar.beginFill(blk, 0.5).lineStyle(bs, blk, 1.0).drawRoundedRect(0, 0, w, h, 3);
// Temporary maximum HP
if (tempmax > 0) {
const pct = max / effectiveMax;
bar.beginFill(c.tempmax, 1.0)
.lineStyle(1, blk, 1.0)
.drawRoundedRect(pct * w, 0, (1 - pct) * w, h, 2);
}
// Maximum HP penalty
else if (tempmax < 0) {
const pct = (max + tempmax) / max;
bar.beginFill(c.negmax, 1.0)
.lineStyle(1, blk, 1.0)
.drawRoundedRect(pct * w, 0, (1 - pct) * w, h, 2);
}
// Health bar
bar.beginFill(hpColor, 1.0)
.lineStyle(bs, blk, 1.0)
.drawRoundedRect(0, 0, valuePct * w, h, 2);
// Temporary hit points
if (temp > 0) {
bar.beginFill(c.temp, 1.0)
.lineStyle(0)
.drawRoundedRect(bs1, bs1, tempPct * w - 2 * bs1, h - 2 * bs1, 1);
}
// Set position
let posY = number === 0 ? this.h - h : 0;
bar.position.set(0, posY);
}
}

87
package-lock.json generated
View file

@ -1,5 +1,4 @@
{ {
"name": "sw5e",
"requires": true, "requires": true,
"lockfileVersion": 1, "lockfileVersion": 1,
"dependencies": { "dependencies": {
@ -90,6 +89,7 @@
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz",
"integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==",
"dev": true,
"requires": { "requires": {
"micromatch": "^3.1.4", "micromatch": "^3.1.4",
"normalize-path": "^2.1.1" "normalize-path": "^2.1.1"
@ -99,6 +99,7 @@
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
"integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
"dev": true,
"requires": { "requires": {
"remove-trailing-separator": "^1.0.1" "remove-trailing-separator": "^1.0.1"
} }
@ -271,6 +272,7 @@
"version": "0.11.2", "version": "0.11.2",
"resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz",
"integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==",
"dev": true,
"requires": { "requires": {
"cache-base": "^1.0.1", "cache-base": "^1.0.1",
"class-utils": "^0.3.5", "class-utils": "^0.3.5",
@ -285,6 +287,7 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
"integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
"dev": true,
"requires": { "requires": {
"is-descriptor": "^1.0.0" "is-descriptor": "^1.0.0"
} }
@ -293,6 +296,7 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
"integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
"dev": true,
"requires": { "requires": {
"kind-of": "^6.0.0" "kind-of": "^6.0.0"
} }
@ -301,6 +305,7 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
"integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
"dev": true,
"requires": { "requires": {
"kind-of": "^6.0.0" "kind-of": "^6.0.0"
} }
@ -309,6 +314,7 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
"integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
"dev": true,
"requires": { "requires": {
"is-accessor-descriptor": "^1.0.0", "is-accessor-descriptor": "^1.0.0",
"is-data-descriptor": "^1.0.0", "is-data-descriptor": "^1.0.0",
@ -326,7 +332,6 @@
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"optional": true,
"requires": { "requires": {
"file-uri-to-path": "1.0.0" "file-uri-to-path": "1.0.0"
} }
@ -344,6 +349,7 @@
"version": "2.3.2", "version": "2.3.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
"integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
"dev": true,
"requires": { "requires": {
"arr-flatten": "^1.1.0", "arr-flatten": "^1.1.0",
"array-unique": "^0.3.2", "array-unique": "^0.3.2",
@ -361,6 +367,7 @@
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
"dev": true,
"requires": { "requires": {
"is-extendable": "^0.1.0" "is-extendable": "^0.1.0"
} }
@ -381,6 +388,7 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz",
"integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==",
"dev": true,
"requires": { "requires": {
"collection-visit": "^1.0.0", "collection-visit": "^1.0.0",
"component-emitter": "^1.2.1", "component-emitter": "^1.2.1",
@ -411,6 +419,7 @@
"version": "2.1.8", "version": "2.1.8",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz",
"integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==",
"dev": true,
"requires": { "requires": {
"anymatch": "^2.0.0", "anymatch": "^2.0.0",
"async-each": "^1.0.1", "async-each": "^1.0.1",
@ -688,7 +697,6 @@
"version": "0.1.7", "version": "0.1.7",
"resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz",
"integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==",
"optional": true,
"requires": { "requires": {
"prr": "~1.0.1" "prr": "~1.0.1"
} }
@ -774,6 +782,7 @@
"version": "2.1.4", "version": "2.1.4",
"resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
"integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=",
"dev": true,
"requires": { "requires": {
"debug": "^2.3.3", "debug": "^2.3.3",
"define-property": "^0.2.5", "define-property": "^0.2.5",
@ -788,6 +797,7 @@
"version": "0.2.5", "version": "0.2.5",
"resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
"integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
"dev": true,
"requires": { "requires": {
"is-descriptor": "^0.1.0" "is-descriptor": "^0.1.0"
} }
@ -796,6 +806,7 @@
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
"dev": true,
"requires": { "requires": {
"is-extendable": "^0.1.0" "is-extendable": "^0.1.0"
} }
@ -853,6 +864,7 @@
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz",
"integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==",
"dev": true,
"requires": { "requires": {
"array-unique": "^0.3.2", "array-unique": "^0.3.2",
"define-property": "^1.0.0", "define-property": "^1.0.0",
@ -868,6 +880,7 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
"integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
"dev": true,
"requires": { "requires": {
"is-descriptor": "^1.0.0" "is-descriptor": "^1.0.0"
} }
@ -876,6 +889,7 @@
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
"dev": true,
"requires": { "requires": {
"is-extendable": "^0.1.0" "is-extendable": "^0.1.0"
} }
@ -884,6 +898,7 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
"integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
"dev": true,
"requires": { "requires": {
"kind-of": "^6.0.0" "kind-of": "^6.0.0"
} }
@ -892,6 +907,7 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
"integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
"dev": true,
"requires": { "requires": {
"kind-of": "^6.0.0" "kind-of": "^6.0.0"
} }
@ -900,6 +916,7 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
"integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
"dev": true,
"requires": { "requires": {
"is-accessor-descriptor": "^1.0.0", "is-accessor-descriptor": "^1.0.0",
"is-data-descriptor": "^1.0.0", "is-data-descriptor": "^1.0.0",
@ -927,13 +944,13 @@
"file-uri-to-path": { "file-uri-to-path": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", "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": { "fill-range": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
"integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
"dev": true,
"requires": { "requires": {
"extend-shallow": "^2.0.1", "extend-shallow": "^2.0.1",
"is-number": "^3.0.0", "is-number": "^3.0.0",
@ -945,6 +962,7 @@
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
"dev": true,
"requires": { "requires": {
"is-extendable": "^0.1.0" "is-extendable": "^0.1.0"
} }
@ -964,6 +982,7 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz",
"integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==",
"dev": true,
"requires": { "requires": {
"detect-file": "^1.0.0", "detect-file": "^1.0.0",
"is-glob": "^4.0.0", "is-glob": "^4.0.0",
@ -1036,6 +1055,7 @@
"version": "1.2.13", "version": "1.2.13",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz",
"integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
"dev": true,
"optional": true, "optional": true,
"requires": { "requires": {
"bindings": "^1.5.0", "bindings": "^1.5.0",
@ -1110,6 +1130,7 @@
"version": "5.0.5", "version": "5.0.5",
"resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-5.0.5.tgz", "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-5.0.5.tgz",
"integrity": "sha512-zOZgGGEHPklZNjZQaZ9f41i7F2YwE+tS5ZHrDhbBCk3stwahn5vQxnFmBJZHoYdusR6R1bLSXeGUy/BhctwKzw==", "integrity": "sha512-zOZgGGEHPklZNjZQaZ9f41i7F2YwE+tS5ZHrDhbBCk3stwahn5vQxnFmBJZHoYdusR6R1bLSXeGUy/BhctwKzw==",
"dev": true,
"requires": { "requires": {
"anymatch": "^2.0.0", "anymatch": "^2.0.0",
"async-done": "^1.2.0", "async-done": "^1.2.0",
@ -1159,6 +1180,7 @@
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.2.tgz", "resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.2.tgz",
"integrity": "sha512-dvEs27SCZt2ibF29xYgmnwwCYZxdxhQ/+LFWlbAW8y7jt68L/65402Lz3+CKy0Ov4rOs+NERmDq7YlZaDqUIfA==", "integrity": "sha512-dvEs27SCZt2ibF29xYgmnwwCYZxdxhQ/+LFWlbAW8y7jt68L/65402Lz3+CKy0Ov4rOs+NERmDq7YlZaDqUIfA==",
"dev": true,
"requires": { "requires": {
"glob-watcher": "^5.0.3", "glob-watcher": "^5.0.3",
"gulp-cli": "^2.2.0", "gulp-cli": "^2.2.0",
@ -1170,6 +1192,7 @@
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.3.0.tgz", "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.3.0.tgz",
"integrity": "sha512-zzGBl5fHo0EKSXsHzjspp3y5CONegCm8ErO5Qh0UzFzk2y4tMvzLWhoDokADbarfZRL2pGpRp7yt6gfJX4ph7A==", "integrity": "sha512-zzGBl5fHo0EKSXsHzjspp3y5CONegCm8ErO5Qh0UzFzk2y4tMvzLWhoDokADbarfZRL2pGpRp7yt6gfJX4ph7A==",
"dev": true,
"requires": { "requires": {
"ansi-colors": "^1.0.1", "ansi-colors": "^1.0.1",
"archy": "^1.0.0", "archy": "^1.0.0",
@ -1197,6 +1220,7 @@
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/gulp-less/-/gulp-less-4.0.1.tgz", "resolved": "https://registry.npmjs.org/gulp-less/-/gulp-less-4.0.1.tgz",
"integrity": "sha512-hmM2k0FfQp7Ptm3ZaqO2CkMX3hqpiIOn4OHtuSsCeFym63F7oWlEua5v6u1cIjVUKYsVIs9zPg9vbqTEb/udpA==", "integrity": "sha512-hmM2k0FfQp7Ptm3ZaqO2CkMX3hqpiIOn4OHtuSsCeFym63F7oWlEua5v6u1cIjVUKYsVIs9zPg9vbqTEb/udpA==",
"dev": true,
"requires": { "requires": {
"accord": "^0.29.0", "accord": "^0.29.0",
"less": "2.6.x || ^3.7.1", "less": "2.6.x || ^3.7.1",
@ -1232,6 +1256,7 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz",
"integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=",
"dev": true,
"requires": { "requires": {
"get-value": "^2.0.6", "get-value": "^2.0.6",
"has-values": "^1.0.0", "has-values": "^1.0.0",
@ -1242,6 +1267,7 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz",
"integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=",
"dev": true,
"requires": { "requires": {
"is-number": "^3.0.0", "is-number": "^3.0.0",
"kind-of": "^4.0.0" "kind-of": "^4.0.0"
@ -1251,6 +1277,7 @@
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz",
"integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=",
"dev": true,
"requires": { "requires": {
"is-buffer": "^1.1.5" "is-buffer": "^1.1.5"
} }
@ -1266,15 +1293,14 @@
} }
}, },
"hosted-git-info": { "hosted-git-info": {
"version": "2.8.9", "version": "2.8.8",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg=="
}, },
"image-size": { "image-size": {
"version": "0.5.5", "version": "0.5.5",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", "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": { "indx": {
"version": "0.2.3", "version": "0.2.3",
@ -1296,9 +1322,9 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
}, },
"ini": { "ini": {
"version": "1.3.8", "version": "1.3.5",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw=="
}, },
"interpret": { "interpret": {
"version": "1.4.0", "version": "1.4.0",
@ -1448,6 +1474,7 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
"integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=",
"dev": true,
"requires": { "requires": {
"kind-of": "^3.0.2" "kind-of": "^3.0.2"
}, },
@ -1456,6 +1483,7 @@
"version": "3.2.2", "version": "3.2.2",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
"integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
"dev": true,
"requires": { "requires": {
"is-buffer": "^1.1.5" "is-buffer": "^1.1.5"
} }
@ -1612,6 +1640,7 @@
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz",
"integrity": "sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog==", "integrity": "sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog==",
"dev": true,
"requires": { "requires": {
"extend": "^3.0.0", "extend": "^3.0.0",
"findup-sync": "^3.0.0", "findup-sync": "^3.0.0",
@ -1679,7 +1708,6 @@
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
"integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
"optional": true,
"requires": { "requires": {
"pify": "^4.0.1", "pify": "^4.0.1",
"semver": "^5.6.0" "semver": "^5.6.0"
@ -1688,8 +1716,7 @@
"pify": { "pify": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
"integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="
"optional": true
} }
} }
}, },
@ -1718,6 +1745,7 @@
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz", "resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz",
"integrity": "sha1-xvNINKDY28OzfCfui7yyfHd1WC4=", "integrity": "sha1-xvNINKDY28OzfCfui7yyfHd1WC4=",
"dev": true,
"requires": { "requires": {
"findup-sync": "^2.0.0", "findup-sync": "^2.0.0",
"micromatch": "^3.0.4", "micromatch": "^3.0.4",
@ -1729,6 +1757,7 @@
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz", "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz",
"integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=", "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=",
"dev": true,
"requires": { "requires": {
"detect-file": "^1.0.0", "detect-file": "^1.0.0",
"is-glob": "^3.1.0", "is-glob": "^3.1.0",
@ -1740,6 +1769,7 @@
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
"integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
"dev": true,
"requires": { "requires": {
"is-extglob": "^2.1.0" "is-extglob": "^2.1.0"
} }
@ -1750,6 +1780,7 @@
"version": "3.1.10", "version": "3.1.10",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz",
"integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==",
"dev": true,
"requires": { "requires": {
"arr-diff": "^4.0.0", "arr-diff": "^4.0.0",
"array-unique": "^0.3.2", "array-unique": "^0.3.2",
@ -1769,8 +1800,7 @@
"mime": { "mime": {
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
"optional": true
}, },
"minimatch": { "minimatch": {
"version": "3.0.4", "version": "3.0.4",
@ -1812,13 +1842,13 @@
"nan": { "nan": {
"version": "2.14.2", "version": "2.14.2",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz",
"integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ=="
"optional": true
}, },
"nanomatch": { "nanomatch": {
"version": "1.2.13", "version": "1.2.13",
"resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
"integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==",
"dev": true,
"requires": { "requires": {
"arr-diff": "^4.0.0", "arr-diff": "^4.0.0",
"array-unique": "^0.3.2", "array-unique": "^0.3.2",
@ -2157,8 +2187,7 @@
"prr": { "prr": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
"integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY="
"optional": true
}, },
"pump": { "pump": {
"version": "2.0.1", "version": "2.0.1",
@ -2216,6 +2245,7 @@
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz",
"integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==",
"dev": true,
"requires": { "requires": {
"graceful-fs": "^4.1.11", "graceful-fs": "^4.1.11",
"micromatch": "^3.1.10", "micromatch": "^3.1.10",
@ -2398,6 +2428,7 @@
"version": "0.8.2", "version": "0.8.2",
"resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
"integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==",
"dev": true,
"requires": { "requires": {
"base": "^0.11.1", "base": "^0.11.1",
"debug": "^2.2.0", "debug": "^2.2.0",
@ -2413,6 +2444,7 @@
"version": "0.2.5", "version": "0.2.5",
"resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
"integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
"dev": true,
"requires": { "requires": {
"is-descriptor": "^0.1.0" "is-descriptor": "^0.1.0"
} }
@ -2421,6 +2453,7 @@
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
"dev": true,
"requires": { "requires": {
"is-extendable": "^0.1.0" "is-extendable": "^0.1.0"
} }
@ -2754,6 +2787,7 @@
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz",
"integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=",
"dev": true,
"requires": { "requires": {
"is-number": "^3.0.0", "is-number": "^3.0.0",
"repeat-string": "^1.6.1" "repeat-string": "^1.6.1"
@ -2823,8 +2857,7 @@
"uglify-to-browserify": { "uglify-to-browserify": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", "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": { "unc-path-regex": {
"version": "0.1.2", "version": "0.1.2",
@ -3068,9 +3101,9 @@
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
}, },
"y18n": { "y18n": {
"version": "3.2.2", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz", "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
"integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==" "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE="
}, },
"yargs": { "yargs": {
"version": "7.1.1", "version": "7.1.1",

View file

@ -1,9 +0,0 @@
{
"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"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Some files were not shown because too many files have changed in this diff Show more