forked from GitHub-Mirrors/foundry-sw5e
Compare commits
No commits in common. "master" and "magicjedi90" have entirely different histories.
master
...
magicjedi9
2770 changed files with 10218 additions and 45964 deletions
BIN
.DS_Store
vendored
BIN
.DS_Store
vendored
Binary file not shown.
24
.github/workflows/main.yml
vendored
24
.github/workflows/main.yml
vendored
|
@ -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
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -22,10 +22,6 @@
|
|||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||
hs_err_pid*
|
||||
|
||||
# Mac-OS file
|
||||
.DS_Store
|
||||
|
||||
# IDE Folders
|
||||
.idea/
|
||||
.vs/
|
||||
node_modules
|
||||
|
|
14
.prettierrc
14
.prettierrc
|
@ -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"
|
||||
}
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"editor.formatOnSave": true
|
||||
}
|
|
@ -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)
|
37
README.md
37
README.md
|
@ -1,36 +1,9 @@
|
|||
# Foundry Virtual Tabletop - SW5e Game System
|
||||
# sw5e
|
||||
SW5E Foundry VTT 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.
|
||||
For easy install
|
||||
|
||||
This game system for [Foundry Virtual Tabletop](http://foundryvtt.com) provides character sheet and game system
|
||||
support for the SW5E roleplaying game.
|
||||
|
||||
This system provides character sheet support for Actors and Items, mechanical support for dice and rules necessary to
|
||||
play games of SW5E, and compendium content for Monsters, Heroes, Items, Powers, Class Features, Monster
|
||||
Features, and more!
|
||||
|
||||
The software component of this system is distributed under the GNUv3 license.
|
||||
|
||||
## Installation Instructions
|
||||
|
||||
To install and use the SW5e system for Foundry Virtual Tabletop, simply paste the following URL into the
|
||||
**Install System** dialog on the Setup menu of the application.
|
||||
In the Foundry Configuration and Setup window go to the Game Systems tab, click on the "Install System" button, and
|
||||
place the following link into the Manifest URL at the bottom
|
||||
|
||||
https://raw.githubusercontent.com/unrealkakeman89/sw5e/master/system.json
|
||||
|
||||
If you wish to manually install the system, you must clone or extract it into the ``Data/systems/sw5e`` folder. You
|
||||
may do this by cloning the repository or downloading a zip archive from the
|
||||
[Releases Page](https://github.com/unrealkakeman89/sw5e).
|
||||
|
||||
## Community Contribution
|
||||
|
||||
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.
|
||||
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
33
gulpfile.js
33
gulpfile.js
|
@ -1,29 +1,17 @@
|
|||
const gulp = require("gulp");
|
||||
const less = require("gulp-less");
|
||||
const gulp = require('gulp');
|
||||
const less = require('gulp-less');
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* Compile LESS
|
||||
/* ----------------------------------------- */
|
||||
|
||||
const SW5E_LESS = ["less/**/*.less"];
|
||||
|
||||
const SW5E_LESS = ["less/*.less"];
|
||||
function compileLESS() {
|
||||
return gulp.src("less/original/sw5e.less").pipe(less()).pipe(gulp.dest("./"));
|
||||
return gulp.src("less/sw5e.less")
|
||||
.pipe(less())
|
||||
.pipe(gulp.dest("./"))
|
||||
}
|
||||
|
||||
function compileGlobalLess() {
|
||||
return gulp.src("less/update/sw5e-global.less").pipe(less()).pipe(gulp.dest("./"));
|
||||
}
|
||||
|
||||
function compileLightLess() {
|
||||
return gulp.src("less/update/sw5e-light.less").pipe(less()).pipe(gulp.dest("./"));
|
||||
}
|
||||
|
||||
function compileDarkLess() {
|
||||
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);
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* Watch Updates
|
||||
|
@ -37,5 +25,8 @@ function watchUpdates() {
|
|||
/* Export Tasks
|
||||
/* ----------------------------------------- */
|
||||
|
||||
exports.default = css;
|
||||
gulp.parallel(css), (exports.watch = gulp.series(gulp.parallel(css), watchUpdates));
|
||||
exports.default = gulp.series(
|
||||
gulp.parallel(css),
|
||||
watchUpdates
|
||||
);
|
||||
exports.css = css;
|
||||
|
|
1674
lang/en.json
1674
lang/en.json
File diff suppressed because it is too large
Load diff
1027
lang/fr.json
1027
lang/fr.json
File diff suppressed because it is too large
Load diff
1027
lang/it.json
1027
lang/it.json
File diff suppressed because it is too large
Load diff
|
@ -1,99 +1,67 @@
|
|||
@import "./variables.less";
|
||||
|
||||
@actorNameHeight: 60px;
|
||||
|
||||
.sw5e.sheet.actor {
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* Sheet Header */
|
||||
/* ----------------------------------------- */
|
||||
|
||||
.sheet-header {
|
||||
|
||||
// Portrait Artwork
|
||||
img.profile {
|
||||
.charlevel {
|
||||
flex: 0 0 160px;
|
||||
max-width: 160px;
|
||||
height: 160px;
|
||||
}
|
||||
height: 60px;
|
||||
margin: 0;
|
||||
padding: 5px;
|
||||
text-align: right;
|
||||
|
||||
// Character Name
|
||||
h1.charname {
|
||||
flex: 1;
|
||||
height: @actorNameHeight;
|
||||
padding: 0;
|
||||
input {
|
||||
height: @actorNameHeight - 20px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
}
|
||||
.level {
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
font-size: 20px;
|
||||
line-height: 30px;
|
||||
|
||||
// Character Level
|
||||
.header-exp {
|
||||
flex: 0 0 150px;
|
||||
margin-right: 3px;
|
||||
height: @actorNameHeight;
|
||||
justify-content: flex-end;
|
||||
label {
|
||||
display: inline;
|
||||
font-size: 24px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
// Character Summary
|
||||
.summary {
|
||||
height: 30px;
|
||||
border-bottom: @borderGroove;
|
||||
font-size: 18px;
|
||||
input, span {
|
||||
display: block;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
input {
|
||||
display: inline;
|
||||
width: 36px;
|
||||
font-size: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
// If the Experience Bar is disabled
|
||||
&.noxp {
|
||||
margin-top: 10px;
|
||||
> label {
|
||||
font-size: 32px;
|
||||
}
|
||||
> input {
|
||||
font-size: 32px;
|
||||
flex: 0 0 40px;
|
||||
height: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.experience {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
padding-right: 5px;
|
||||
font-size: 16px;
|
||||
color: @colorOlive;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Primary Attributes
|
||||
.attributes {
|
||||
flex: 0 0 100%;
|
||||
margin: 0;
|
||||
|
||||
.attribute {
|
||||
height: 70px;
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-right: @borderGroove;
|
||||
border-radius: 0;
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.attribute-value {
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
// Movement Configuration
|
||||
.movement, .hit-dice {
|
||||
h4.attribute-name {
|
||||
position: relative;
|
||||
}
|
||||
.config-button {
|
||||
position: absolute;
|
||||
display: none;
|
||||
right: 0;
|
||||
top: 1px;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
}
|
||||
&:hover .config-button {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
// Temporary HP
|
||||
input.temphp {
|
||||
width: 48%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* General Styles */
|
||||
|
@ -101,55 +69,55 @@
|
|||
|
||||
// Box Headers
|
||||
h4.box-title {
|
||||
height: 18px;
|
||||
line-height: 16px;
|
||||
margin: 4px 8px 2px;
|
||||
.russoOne(14px);
|
||||
color: @colorOlive;
|
||||
border-bottom: 1px solid @colorFaint;
|
||||
white-space: nowrap;
|
||||
font-size: 18px;
|
||||
border-bottom: 1px solid @colorBeige;
|
||||
}
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* Attributes */
|
||||
/* ----------------------------------------- */
|
||||
|
||||
.tab.attributes {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
ul.attributes {
|
||||
flex: 0 0 60px;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
margin: 5px 0 0;
|
||||
padding: 0;
|
||||
|
||||
li.attribute {
|
||||
height: 60px;
|
||||
margin: 0 5px 0 0;
|
||||
height: 70px;
|
||||
margin: 0 5px;
|
||||
border: @borderGroove;
|
||||
border-radius: 4px;
|
||||
.nodesto();
|
||||
text-align: center;
|
||||
|
||||
&:last-child {
|
||||
margin: 0;
|
||||
.attribute-name {
|
||||
flex: 0 0 18px;
|
||||
}
|
||||
|
||||
.attribute-value {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
.russoOne();
|
||||
|
||||
> * {
|
||||
font-weight: 400;
|
||||
input {
|
||||
display: inline;
|
||||
max-width: 80%;
|
||||
height: 28px;
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
&.multiple input {
|
||||
flex: 0 0 33%;
|
||||
span.sep {
|
||||
display: inline;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
font-size: 28px;
|
||||
color: @colorTan;
|
||||
}
|
||||
|
||||
&.multiple {
|
||||
input { max-width: 33%; }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -157,10 +125,8 @@
|
|||
flex: 0 0 18px;
|
||||
margin-top: -1px;
|
||||
line-height: 18px;
|
||||
font-family: "Signika", sans-serif;
|
||||
font-family: "Signika", "Palatino Linotype", serif;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -170,28 +136,22 @@
|
|||
/* ----------------------------------------- */
|
||||
|
||||
.ability-scores {
|
||||
flex: 0 0 100px;
|
||||
height: 440px;
|
||||
flex: 0 0 100%;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
.russoOne();
|
||||
border: @borderGroove;
|
||||
border-radius: 3px;
|
||||
.nodesto();
|
||||
|
||||
.ability {
|
||||
height: 70px;
|
||||
margin: 0 5px;
|
||||
text-align: center;
|
||||
border-bottom: @borderGroove;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: -3px;
|
||||
}
|
||||
border: @borderGroove;
|
||||
border-radius: 3px;
|
||||
|
||||
input.ability-score {
|
||||
height: 30px;
|
||||
width: 36px;
|
||||
width: 50px;
|
||||
margin: 0 auto;
|
||||
line-height: 32px;
|
||||
font-size: 24px;
|
||||
|
@ -199,14 +159,12 @@
|
|||
|
||||
.ability-modifiers {
|
||||
height: 24px;
|
||||
margin: -8px 0 0;
|
||||
margin: -10px 0 0;
|
||||
|
||||
span.ability-mod,
|
||||
span.ability-save {
|
||||
flex: 0 0 24px;
|
||||
height: 22px;
|
||||
line-height: 22px;
|
||||
font-size: 16px;
|
||||
height: 24px;
|
||||
border-top: @borderGroove;
|
||||
}
|
||||
|
||||
|
@ -222,6 +180,12 @@
|
|||
border-left: @borderGroove;
|
||||
}
|
||||
}
|
||||
|
||||
/* Hide modifier box on hover */
|
||||
|
||||
input.ability-score:hover + .ability-modifiers {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -245,22 +209,16 @@
|
|||
/* ----------------------------------------- */
|
||||
|
||||
ul.skills-list {
|
||||
flex: 0 0 180px;
|
||||
height: 440px;
|
||||
flex: 0 0 192px;
|
||||
list-style: none;
|
||||
margin: 0 5px 0;
|
||||
padding: 3px 0 2px;
|
||||
margin: 5px 5px 0;
|
||||
padding: 2px 2px 0;
|
||||
border: @borderGroove;
|
||||
border-radius: 3px;
|
||||
|
||||
li.skill {
|
||||
height: 24px;
|
||||
width: 225px;
|
||||
padding: 3px 2px;
|
||||
|
||||
&:nth-child(even) {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
height: 22px;
|
||||
padding: 3px 0;
|
||||
|
||||
h4 {
|
||||
flex: 1px;
|
||||
|
@ -276,7 +234,6 @@
|
|||
|
||||
.skill-ability {
|
||||
flex: 0 0 26px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.skill-mod {
|
||||
|
@ -296,47 +253,45 @@
|
|||
/* ----------------------------------------- */
|
||||
|
||||
.counters {
|
||||
flex: none;
|
||||
padding: 5px 0;
|
||||
margin: 0;
|
||||
flex: 0 0 100%;
|
||||
border-bottom: @borderGroove;
|
||||
margin-bottom: 5px;
|
||||
|
||||
.counter {
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
padding: 0 3px;
|
||||
line-height: 32px;
|
||||
|
||||
h4 {
|
||||
flex: auto;
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
color: @colorOlive;
|
||||
.nodesto();
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.counter-value {
|
||||
flex: none;
|
||||
flex: 0 0 50px;
|
||||
text-align: right;
|
||||
> * {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="number"] {
|
||||
input[type="text"] {
|
||||
height: 20px;
|
||||
max-width: 20px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
position: relative;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 0;
|
||||
top: 4px;
|
||||
top: 6px;
|
||||
}
|
||||
|
||||
span.sep {
|
||||
margin: 0 -2px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
@ -346,31 +301,28 @@
|
|||
/* Traits */
|
||||
/* ----------------------------------------- */
|
||||
|
||||
.center-pane {
|
||||
height: 100%;
|
||||
padding: 0 5px 0 3px;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.traits {
|
||||
margin: 0 5px;
|
||||
|
||||
.form-group, .form-group-stacked {
|
||||
margin: 0 0 3px 0;
|
||||
margin: 0 0 4px 0;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.config-button {
|
||||
|
||||
.configure-flags {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
label {
|
||||
flex: none;
|
||||
line-height: 20px;
|
||||
font-weight: bold;
|
||||
margin: 0 10px 0 0;
|
||||
|
||||
.actor-size {
|
||||
flex: 0 0 150px;
|
||||
}
|
||||
|
||||
select {
|
||||
max-width: 200px;
|
||||
label {
|
||||
flex: 0 0 150px;
|
||||
line-height: 20px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
input {
|
||||
|
@ -400,8 +352,7 @@
|
|||
|
||||
.tab.features,
|
||||
.tab.inventory,
|
||||
.tab.force-powerbook,
|
||||
.tab.tech-powerbook {
|
||||
.tab.powerbook {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
|
@ -409,10 +360,18 @@
|
|||
.inventory-filters {
|
||||
margin: 0 8px;
|
||||
flex: 0 0 20px;
|
||||
justify-content: flex-end;
|
||||
h3, .filter-title {
|
||||
.nodesto();
|
||||
color: @colorOlive;
|
||||
font-size: 18px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&.powerbook-filters {
|
||||
flex: 0 0 40px;
|
||||
}
|
||||
|
||||
.currency {
|
||||
flex: 0 0 100%;
|
||||
list-style: none;
|
||||
margin: 4px 0 8px;
|
||||
padding: 0;
|
||||
|
@ -434,20 +393,47 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Inventory item lists
|
||||
.inventory-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0 5px;
|
||||
overflow-y: auto;
|
||||
|
||||
// Inventory Item
|
||||
.item {
|
||||
line-height: 30px;
|
||||
padding: 0 2px; // to align with the header border
|
||||
border-bottom: 1px solid @colorFaint;
|
||||
&:last-child { border-bottom: none; }
|
||||
|
||||
// Item Header Name
|
||||
.item-name {
|
||||
cursor: pointer;
|
||||
&.rollable:hover .item-image {
|
||||
background-image: url("../../icons/svg/d20-grey.svg") !important;
|
||||
max-height: 30px;
|
||||
overflow: hidden;
|
||||
|
||||
.item-image {
|
||||
flex: 0 0 30px;
|
||||
background-size: 30px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
&.rollable .item-image:hover {
|
||||
background-image: url("../../icons/svg/d20-black.svg") !important;
|
||||
background-image: url("/icons/svg/d20-black.svg") !important;
|
||||
}
|
||||
&.rollable:hover .item-image {
|
||||
background-image: url("/icons/svg/d20-grey.svg") !important;
|
||||
}
|
||||
|
||||
i.attuned {
|
||||
color: @colorTan;
|
||||
}
|
||||
i.attuned { color: @colorTan; }
|
||||
i.not-attuned { color: @colorCrimson; }
|
||||
}
|
||||
|
||||
// Item uses
|
||||
|
@ -466,12 +452,26 @@
|
|||
flex: 0 0 80px;
|
||||
text-align: right;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
color: @colorTan;
|
||||
}
|
||||
}
|
||||
|
||||
// Inventory Header
|
||||
.inventory-header {
|
||||
margin: 2px 0;
|
||||
padding: 0;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border: @borderGroove;
|
||||
font-weight: bold;
|
||||
line-height: 24px;
|
||||
|
||||
h3 {
|
||||
margin: 0 -5px 0 0;
|
||||
padding-left: 5px;
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.item-controls a.item-create {
|
||||
flex: 0 0 100%;
|
||||
}
|
||||
|
@ -481,24 +481,37 @@
|
|||
.item-detail {
|
||||
flex: 0 0 70px;
|
||||
font-size: 12px;
|
||||
color: @colorTan;
|
||||
text-align: center;
|
||||
border-right: 1px solid @colorFaint;
|
||||
word-break: break-word;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
&:last-child { border-right: none; }
|
||||
&.item-action {flex: 0 0 100px}
|
||||
&.attunement {flex: 0 0 24px}
|
||||
}
|
||||
|
||||
.item-weight {
|
||||
flex: 0 0 60px;
|
||||
border-left: 1px solid @colorFaint;
|
||||
border-right: 1px solid @colorFaint;
|
||||
}
|
||||
|
||||
.item-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
// Item Control Buttons
|
||||
.item-controls {
|
||||
flex: 0 0 44px;
|
||||
.flexrow();
|
||||
justify-content: flex-end;
|
||||
|
||||
a {
|
||||
flex: 0 0 22px;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
color: @colorTan;
|
||||
}
|
||||
}
|
||||
|
||||
// Item Dropdown Summary
|
||||
|
@ -507,80 +520,7 @@
|
|||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
padding: 0.25em 0.5em;
|
||||
color: @colorDark;
|
||||
border-top: 1px solid @colorFaint;
|
||||
h2 {
|
||||
font-family: 'Russo One';
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
border-bottom: 2px solid rgb(13, 153, 204);
|
||||
color: #c40f0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Encumbrance Bar */
|
||||
.encumbrance {
|
||||
flex: 0 0 12px;
|
||||
background: @colorTan;
|
||||
margin: 1px 15px 0 1px;
|
||||
border: 1px solid @colorDark;
|
||||
border-radius: 3px;
|
||||
position: relative;
|
||||
|
||||
.encumbrance-bar {
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 1px;
|
||||
background: #6c8aa5;
|
||||
height: 8px;
|
||||
border: 1px solid #cde4ff;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.encumbrance-label {
|
||||
height: 10px;
|
||||
padding: 0 5px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
font-size: 13px;
|
||||
line-height: 12px;
|
||||
text-align: right;
|
||||
color: #EEE;
|
||||
text-shadow: 0 0 5px #000;
|
||||
}
|
||||
|
||||
.encumbrance-breakpoint {
|
||||
display: block;
|
||||
position: absolute;
|
||||
&.encumbrance-33 { left: 33% }
|
||||
&.encumbrance-66 { left: 66% }
|
||||
}
|
||||
|
||||
.arrow-up {
|
||||
bottom: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-bottom: 4px solid #666;
|
||||
}
|
||||
|
||||
.arrow-down {
|
||||
top: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-top: 4px solid #666;
|
||||
}
|
||||
|
||||
&.encumbered {
|
||||
.arrow-up { border-bottom: 4px solid #000; }
|
||||
.arrow-down { border-top: 4px solid #000; }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -589,45 +529,45 @@
|
|||
/* ----------------------------------------- */
|
||||
|
||||
.powercasting-ability {
|
||||
flex: 0 0 240px;
|
||||
margin: 0;
|
||||
label, span {
|
||||
h3 {
|
||||
flex: none;
|
||||
margin-right: 5px;
|
||||
}
|
||||
input {
|
||||
flex: 0 0 28px;
|
||||
input, span {
|
||||
flex: 0 0 32px;
|
||||
text-align: center;
|
||||
}
|
||||
select {
|
||||
margin: 0 5px;
|
||||
flex: 0 0 120px;
|
||||
flex: 0 0 150px;
|
||||
}
|
||||
h3.power-dc {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.power-slots,
|
||||
.power-comps {
|
||||
flex: none;
|
||||
padding: 0 5px;
|
||||
flex: 0 0 72px;
|
||||
padding-right: 5px;
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
color: @colorTan;
|
||||
border-right: 1px solid @colorFaint;
|
||||
}
|
||||
|
||||
.power-slots {
|
||||
input {
|
||||
input, span.sep {
|
||||
display: inline;
|
||||
max-width: 20px;
|
||||
}
|
||||
.sep {
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.powerbook .power-uses {
|
||||
padding-right: 5px;
|
||||
text-align: right;
|
||||
color: @colorTan;
|
||||
.power-uses {
|
||||
padding-right: 8px;
|
||||
text-align: right !important;
|
||||
}
|
||||
|
||||
.power-school, .power-action, .power-target {
|
||||
|
@ -655,15 +595,6 @@
|
|||
// Empty powerbook controls
|
||||
.powerbook-empty .item-controls { flex: 1; }
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* Features Tab */
|
||||
/* ----------------------------------------- */
|
||||
|
||||
// Original class icon
|
||||
.features i.original-class {
|
||||
color: #4b4a44
|
||||
}
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* TinyMCE */
|
||||
/* ----------------------------------------- */
|
||||
|
@ -672,19 +603,3 @@
|
|||
padding: 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
#actor-flags {
|
||||
.window-content {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
form {
|
||||
height: 100%;
|
||||
}
|
||||
.form-body {
|
||||
height: calc(100% - 40px);
|
||||
padding-right: 8px;
|
||||
margin-bottom: 4px;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
}
|
|
@ -5,12 +5,14 @@
|
|||
@detailsHeight: 40px;
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* All DnD5e Apps */
|
||||
/* All D&D5e Apps */
|
||||
/* ----------------------------------------- */
|
||||
|
||||
.sw5e {
|
||||
.window-content {
|
||||
background: @sheetBackground;
|
||||
font-size: 13px;
|
||||
color: @colorDark
|
||||
}
|
||||
|
||||
/* ----------------------------------------- */
|
||||
|
@ -18,9 +20,7 @@
|
|||
/* ----------------------------------------- */
|
||||
|
||||
// Item Sheet form fields
|
||||
input[type="text"],
|
||||
input[type="number"],
|
||||
select {
|
||||
input[type="text"], select {
|
||||
height: calc(100% - 2px);
|
||||
border: 1px solid @colorTan;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
|
@ -28,8 +28,7 @@
|
|||
}
|
||||
|
||||
// Hovered Fields
|
||||
input[type="text"],
|
||||
input[type="number"] {
|
||||
input[type="text"] {
|
||||
&:hover,
|
||||
&:focus {
|
||||
border: 1px solid #111;
|
||||
|
@ -42,8 +41,6 @@
|
|||
select:disabled,
|
||||
textarea:disabled {
|
||||
color: @colorOlive;
|
||||
border: 1px solid transparent !important;
|
||||
outline: none !important;
|
||||
&:hover,
|
||||
&:focus {
|
||||
box-shadow: none !important;
|
||||
|
@ -58,6 +55,21 @@
|
|||
border: @borderGroove;
|
||||
}
|
||||
|
||||
// Checkbox Labels
|
||||
label.checkbox {
|
||||
flex: auto;
|
||||
margin: 0;
|
||||
line-height: 20px;
|
||||
font-size: 11px;
|
||||
text-align: right;
|
||||
input[type="checkbox"] {
|
||||
height: auto;
|
||||
margin: 0 5px 0;
|
||||
position: relative;
|
||||
top: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Form Groups */
|
||||
.form-group {
|
||||
label {
|
||||
|
@ -76,12 +88,11 @@
|
|||
|
||||
// Stacked Groups
|
||||
.form-group.stacked {
|
||||
> label {
|
||||
label {
|
||||
flex: 0 0 100%;
|
||||
margin: 0;
|
||||
}
|
||||
label.checkbox,
|
||||
label.radio {
|
||||
label.checkbox {
|
||||
flex: auto;
|
||||
text-align: left;
|
||||
}
|
||||
|
@ -90,10 +101,11 @@
|
|||
// Form Headers
|
||||
.form-header {
|
||||
margin: 0 0 0.25em 0;
|
||||
padding: 2px 0;
|
||||
padding: 0 2px;
|
||||
.nodesto();
|
||||
font-size: 24px;
|
||||
border-top: @borderGroove;
|
||||
border-bottom: @borderGroove;
|
||||
.russoOne();
|
||||
}
|
||||
|
||||
/* Tags */
|
||||
|
@ -110,34 +122,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 */
|
||||
/* ----------------------------------------- */
|
||||
|
@ -167,8 +151,7 @@
|
|||
/* ----------------------------------------- */
|
||||
|
||||
// Input Fields
|
||||
input[type="text"],
|
||||
input[type="number"] {
|
||||
input[type="text"] {
|
||||
background: none;
|
||||
border: 1px solid transparent;
|
||||
&:hover,
|
||||
|
@ -185,24 +168,11 @@
|
|||
background: transparent;
|
||||
}
|
||||
|
||||
// Rollable Titles
|
||||
// Rollable Links
|
||||
.editable .rollable:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
.editable h4.rollable:hover,
|
||||
.editable .rollable:hover > h4 {
|
||||
color: #000;
|
||||
text-shadow: 0 0 10px red;
|
||||
}
|
||||
|
||||
// Separators
|
||||
span.sep {
|
||||
flex: none;
|
||||
margin: 0 1px;
|
||||
display: inline;
|
||||
position: relative;
|
||||
color: @colorTan;
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* ----------------------------------------- */
|
||||
|
@ -220,14 +190,10 @@
|
|||
/* ----------------------------------------- */
|
||||
/* Sheet Header */
|
||||
/* ----------------------------------------- */
|
||||
|
||||
.sheet-header {
|
||||
flex: 0 0 @headerHeight;
|
||||
border-bottom: @borderGroove;
|
||||
|
||||
.header-details {
|
||||
.russoOne();
|
||||
}
|
||||
.nodesto();
|
||||
|
||||
/* Character Name */
|
||||
h1 {
|
||||
|
@ -238,10 +204,8 @@
|
|||
padding: 5px;
|
||||
|
||||
input {
|
||||
display: block;
|
||||
height: 50px;
|
||||
font-size: 32px;
|
||||
margin: 0;
|
||||
font-size: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -250,7 +214,7 @@
|
|||
flex: 0 0 @headerHeight;
|
||||
max-width: @headerHeight;
|
||||
height: @headerHeight;
|
||||
object-fit: contain;
|
||||
object-fit: cover;
|
||||
border: none;
|
||||
border-right: @borderGroove;
|
||||
}
|
||||
|
@ -266,10 +230,11 @@
|
|||
border-bottom: none;
|
||||
|
||||
li {
|
||||
height: calc(100% - 6px);
|
||||
width: 33.33%;
|
||||
float: left;
|
||||
height: @detailsHeight - 6px;
|
||||
margin: 2px 0;
|
||||
padding: 0;
|
||||
padding: 0 3px;
|
||||
border-right: @borderGroove;
|
||||
line-height: 34px;
|
||||
color: @colorOlive;
|
||||
|
@ -288,7 +253,7 @@
|
|||
.sheet-navigation {
|
||||
flex: 0 0 @navHeight;
|
||||
margin-bottom: 5px;
|
||||
.russoOne(14px);
|
||||
.nodesto();
|
||||
|
||||
.item {
|
||||
height: 30px;
|
||||
|
@ -316,30 +281,22 @@
|
|||
/* ----------------------------------------- */
|
||||
|
||||
.filter-list {
|
||||
align-items: center;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: 16px;
|
||||
max-width: 70%;
|
||||
|
||||
.filter-icon {
|
||||
flex: none;
|
||||
font-size: 14px;
|
||||
color: @colorTan;
|
||||
.filter-title {
|
||||
flex: 3;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
margin: 0 6px 0 0;
|
||||
margin: 0 6px;
|
||||
border-bottom: 3px solid @colorBeige;
|
||||
white-space: nowrap;
|
||||
|
||||
&:last-child {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-shadow: 0 0 4px red;
|
||||
border-bottom: 3px solid @colorTan;
|
||||
|
@ -355,9 +312,6 @@
|
|||
/* Trait Lists */
|
||||
/* ----------------------------------------- */
|
||||
|
||||
.traits {
|
||||
margin: 5px 0 0;
|
||||
|
||||
.trait-selector {
|
||||
flex: 0 0 16px;
|
||||
padding: 2px 0;
|
||||
|
@ -366,114 +320,11 @@
|
|||
}
|
||||
|
||||
.traits-list {
|
||||
flex: 0 0 100%;
|
||||
line-height: 20px;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* Items Lists */
|
||||
/* ----------------------------------------- */
|
||||
|
||||
.items-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
color: @colorTan;
|
||||
|
||||
// Child lists
|
||||
.item-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
// Item Name
|
||||
.item-name {
|
||||
flex: 2;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
align-items: center;
|
||||
h3, h4 {
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
// Control Buttons
|
||||
.item-controls {
|
||||
flex: 0 0 60px;
|
||||
justify-content: space-between;
|
||||
a {
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
// Individual Item
|
||||
.item {
|
||||
align-items: center;
|
||||
padding: 0 2px; // to align with the header border
|
||||
border-bottom: 1px solid @colorFaint;
|
||||
&:last-child { border-bottom: none; }
|
||||
|
||||
.item-name {
|
||||
color: @colorDark;
|
||||
.item-image {
|
||||
flex: 0 0 30px;
|
||||
height: 30px;
|
||||
background-size: 30px;
|
||||
border: none;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Section Header
|
||||
.items-header {
|
||||
height: 28px;
|
||||
margin: 2px 0;
|
||||
padding: 0;
|
||||
align-items: center;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border: @borderGroove;
|
||||
font-weight: bold;
|
||||
> * {
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
h3 {
|
||||
padding-left: 5px;
|
||||
//.modesto();
|
||||
text-align: left;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* Active Effects */
|
||||
/* ----------------------------------------- */
|
||||
|
||||
.effects .item {
|
||||
.effect-source,
|
||||
.effect-duration,
|
||||
.effect-controls {
|
||||
text-align: center;
|
||||
border-left: 1px solid @colorFaint;
|
||||
border-right: 1px solid @colorFaint;
|
||||
font-size: 12px;
|
||||
}
|
||||
.effect-controls {
|
||||
border: none;
|
||||
}
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -482,7 +333,7 @@
|
|||
/* Trait Selector
|
||||
/* ----------------------------------------- */
|
||||
|
||||
.trait-selector {
|
||||
#trait-selector {
|
||||
.trait-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
|
@ -494,93 +345,3 @@
|
|||
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;
|
||||
}
|
250
less/character.less
Normal file
250
less/character.less
Normal file
|
@ -0,0 +1,250 @@
|
|||
@import "./variables.less";
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* Basic Structure */
|
||||
/* ----------------------------------------- */
|
||||
.sw5e.sheet.actor.character {
|
||||
min-width: 680px;
|
||||
min-height: 736px;
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* Sheet Header */
|
||||
/* ----------------------------------------- */
|
||||
|
||||
.sheet-header {
|
||||
|
||||
// Character Profile image (larger than usual)
|
||||
img.profile {
|
||||
flex: 0 0 180px;
|
||||
max-width: 180px;
|
||||
height: 180px;
|
||||
}
|
||||
|
||||
// Character level and experience bar
|
||||
.charlevel {
|
||||
flex: 0 0 180px;
|
||||
padding: 0 5px 2px;
|
||||
|
||||
.level {
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.experience {
|
||||
input[type="text"] {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.xpbar {
|
||||
width: 100%;
|
||||
flex: 0 0 8px;
|
||||
background: #666;
|
||||
border: 1px solid #000;
|
||||
border-radius: 3px;
|
||||
|
||||
.bar {
|
||||
height: 4px;
|
||||
margin: 1px;
|
||||
display: block;
|
||||
background: #afebff;
|
||||
border: 1px solid #000;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Character Summary
|
||||
.summary {
|
||||
border-bottom: @borderGroove;
|
||||
}
|
||||
|
||||
// Primary Attributes
|
||||
.attributes {
|
||||
height: 80px;
|
||||
margin: 0;
|
||||
|
||||
.attribute {
|
||||
height: 80px;
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-right: @borderGroove;
|
||||
border-radius: 0;
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.attribute-value {
|
||||
margin: 5px 0 0;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.attribute-name {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.attribute-footer {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
a.rest {
|
||||
border: 1px solid @colorBeige;
|
||||
border-radius: 2px;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
padding: 1px 3px;
|
||||
margin: 0 6px;
|
||||
}
|
||||
|
||||
.hit-dice {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* Sheet Body */
|
||||
/* ----------------------------------------- */
|
||||
|
||||
.attributes {
|
||||
.resource {
|
||||
.attribute-name {
|
||||
margin: 0 8px;
|
||||
input[type="text"] {
|
||||
height: 20px;
|
||||
margin: 2px 0 -2px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
label.checkbox {
|
||||
margin: 0 3px;
|
||||
input[type="checkbox"] {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.initiative .attribute-footer input {
|
||||
width: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
ul.skills-list {
|
||||
flex: 0 0 212px;
|
||||
|
||||
li.skill {
|
||||
padding: 3px;
|
||||
h4 {
|
||||
flex: 1px;
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.counters {
|
||||
.death-saves {
|
||||
flex: 2;
|
||||
.counter-value {
|
||||
flex: 0 0 90px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-detail.player-class {
|
||||
flex: 0 0 180px;
|
||||
text-align: right;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* Inventory */
|
||||
/* ----------------------------------------- */
|
||||
|
||||
/* Encumbrance Bar */
|
||||
.encumbrance {
|
||||
flex: 0 0 12px;
|
||||
background: @colorTan;
|
||||
margin: 1px 15px 0 1px;
|
||||
border: 1px solid @colorDark;
|
||||
border-radius: 3px;
|
||||
position: relative;
|
||||
|
||||
.encumbrance-bar {
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 1px;
|
||||
background: #6c8aa5;
|
||||
height: 8px;
|
||||
border: 1px solid #cde4ff;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.encumbrance-label {
|
||||
height: 10px;
|
||||
padding: 0 5px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
font-size: 13px;
|
||||
line-height: 12px;
|
||||
text-align: right;
|
||||
color: #EEE;
|
||||
text-shadow: 0 0 5px #000;
|
||||
}
|
||||
|
||||
.encumbrance-breakpoint {
|
||||
display: block;
|
||||
position: absolute;
|
||||
&.encumbrance-33 { left: 33% }
|
||||
&.encumbrance-66 { left: 66% }
|
||||
}
|
||||
|
||||
.arrow-up {
|
||||
bottom: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-bottom: 4px solid #666;
|
||||
}
|
||||
|
||||
.arrow-down {
|
||||
top: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-top: 4px solid #666;
|
||||
}
|
||||
|
||||
&.encumbered {
|
||||
.arrow-up { border-bottom: 4px solid #000; }
|
||||
.arrow-down { border-top: 4px solid #000; }
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* Item Controls */
|
||||
/* ----------------------------------------- */
|
||||
|
||||
.inventory,
|
||||
.powerbook {
|
||||
.item-controls {
|
||||
flex: 0 0 68px;
|
||||
.item-toggle {
|
||||
color: @colorBeige;
|
||||
&.active {
|
||||
color: @colorOlive;
|
||||
}
|
||||
&.fixed {
|
||||
color: @colorCrimson;
|
||||
&:hover {
|
||||
text-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,8 +4,7 @@
|
|||
/* Chat Cards
|
||||
/* ----------------------------------------- */
|
||||
|
||||
.sw5e.chat-card,
|
||||
.midi-qol-item-card {
|
||||
.sw5e.chat-card {
|
||||
font-style: normal;
|
||||
font-size: 12px;
|
||||
|
||||
|
@ -23,7 +22,7 @@
|
|||
flex: 1;
|
||||
margin: 0;
|
||||
line-height: 36px;
|
||||
.engli-Besh();
|
||||
.nodesto();
|
||||
color: @colorOlive;
|
||||
&:hover {
|
||||
color: #111;
|
||||
|
@ -73,7 +72,7 @@
|
|||
|
||||
span {
|
||||
border-right: 2px groove #FFF;
|
||||
padding: 0 3px 0 0;
|
||||
padding: 0 5px 0 0;
|
||||
font-size: 10px;
|
||||
|
||||
&:last-child {
|
291
less/items.less
Normal file
291
less/items.less
Normal file
|
@ -0,0 +1,291 @@
|
|||
@import "./variables.less";
|
||||
.sw5e.sheet.item {
|
||||
min-height: 420px;
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* Sheet Header */
|
||||
/* ----------------------------------------- */
|
||||
|
||||
.sheet-header {
|
||||
img.profile {
|
||||
border: 2px solid #000;
|
||||
}
|
||||
|
||||
.item-subtitle {
|
||||
flex: 0 0 80px;
|
||||
height: 60px;
|
||||
margin: 0;
|
||||
padding: 5px;
|
||||
text-align: right;
|
||||
color: @colorTan;
|
||||
|
||||
.item-type {
|
||||
font-size: 24px;
|
||||
line-height: 26px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.item-status {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sheet-navigation {
|
||||
margin-bottom: 5px;
|
||||
.item {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.sheet-body {
|
||||
overflow: hidden;
|
||||
|
||||
.tab {
|
||||
padding: 0 5px;
|
||||
overflow: hidden auto;
|
||||
}
|
||||
|
||||
.item-properties {
|
||||
flex: 0 0 120px;
|
||||
margin: 5px 5px 5px 0;
|
||||
padding-right: 5px;
|
||||
border-right: @borderGroove;
|
||||
|
||||
.form-group {
|
||||
margin: 0;
|
||||
label {
|
||||
line-height: 20px;
|
||||
}
|
||||
input {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.properties-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
margin: 3px 0;
|
||||
padding: 0 2px;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border: @borderGroove;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* Item Details Form */
|
||||
/* ----------------------------------------- */
|
||||
|
||||
.details {
|
||||
|
||||
// Item Sheet form fields
|
||||
input[type="text"], select {
|
||||
height: 24px;
|
||||
border: 1px solid @colorTan;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
span {
|
||||
text-align: center;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-group.input-select {
|
||||
select {
|
||||
flex: 1.8;
|
||||
}
|
||||
}
|
||||
|
||||
.form-group.input-select-select {
|
||||
select {
|
||||
flex: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.form-group.uses-per {
|
||||
input {
|
||||
flex: 1;
|
||||
}
|
||||
span {
|
||||
flex: 0 0 16px;
|
||||
}
|
||||
select {
|
||||
flex: 3;
|
||||
}
|
||||
}
|
||||
|
||||
span.sep {
|
||||
flex: 0 0 8px;
|
||||
}
|
||||
|
||||
.prepared {
|
||||
flex: 1.3 !important;
|
||||
text-align: right;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
// Power Materials
|
||||
.power-materials {
|
||||
flex: 0 0 100%;
|
||||
margin: 0.25em 0;
|
||||
justify-content: flex-end;
|
||||
label {
|
||||
flex: 0 0 64px;
|
||||
text-align: right;
|
||||
margin-right: 5px;
|
||||
font-size: 12px;
|
||||
line-height: 24px;
|
||||
}
|
||||
input[type="text"] {
|
||||
flex: 0 0 48px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* Item Actions */
|
||||
/* ----------------------------------------- */
|
||||
|
||||
h4.damage-header {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-weight: bold;
|
||||
line-height: 24px;
|
||||
color: @colorOlive;
|
||||
}
|
||||
|
||||
.damage-parts {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
.damage-part {
|
||||
flex: 0 0 100%;
|
||||
padding: 0;
|
||||
input {
|
||||
flex: 3;
|
||||
}
|
||||
select {
|
||||
margin-left: 5px;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.damage-control {
|
||||
width: 18px;
|
||||
flex: 0 0 18px;
|
||||
line-height: 24px;
|
||||
float: right;
|
||||
text-align: right;
|
||||
color: @colorTan;
|
||||
}
|
||||
|
||||
h4.armorproperties-header {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-weight: bold;
|
||||
line-height: 24px;
|
||||
color: @colorOlive;
|
||||
}
|
||||
|
||||
.armorproperties-parts {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
.armorproperties-part {
|
||||
flex: 0 0 100%;
|
||||
padding: 0;
|
||||
input {
|
||||
flex: 3;
|
||||
}
|
||||
select {
|
||||
margin-left: 5px;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.armorproperties-control {
|
||||
width: 18px;
|
||||
flex: 0 0 18px;
|
||||
line-height: 24px;
|
||||
float: right;
|
||||
text-align: right;
|
||||
color: @colorTan;
|
||||
}
|
||||
|
||||
h4.weaponproperties-header {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-weight: bold;
|
||||
line-height: 24px;
|
||||
color: @colorOlive;
|
||||
}
|
||||
|
||||
.weaponproperties-parts {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
.weaponproperties-part {
|
||||
flex: 0 0 100%;
|
||||
padding: 0;
|
||||
input {
|
||||
flex: 3;
|
||||
}
|
||||
select {
|
||||
margin-left: 5px;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.weaponproperties-control {
|
||||
width: 18px;
|
||||
flex: 0 0 18px;
|
||||
line-height: 24px;
|
||||
float: right;
|
||||
text-align: right;
|
||||
color: @colorTan;
|
||||
}
|
||||
|
||||
.recharge {
|
||||
span {
|
||||
flex: 0 0 80px;
|
||||
}
|
||||
label.checkbox {
|
||||
flex: 0 0 80px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* Item Actions */
|
||||
/* ----------------------------------------- */
|
||||
|
||||
.weapon-properties label.checkbox {
|
||||
flex: 0 0 98px;
|
||||
}
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* Loot Sheet (No Tabs) */
|
||||
/* ----------------------------------------- */
|
||||
|
||||
.loot-header {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
9
less/npc.less
Normal file
9
less/npc.less
Normal file
|
@ -0,0 +1,9 @@
|
|||
@import "./variables.less";
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* Basic Structure */
|
||||
/* ----------------------------------------- */
|
||||
.sw5e.sheet.actor.npc {
|
||||
min-width: 600px;
|
||||
min-height: 658px;
|
||||
}
|
|
@ -1,180 +0,0 @@
|
|||
@import "./variables.less";
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* Basic Structure */
|
||||
/* ----------------------------------------- */
|
||||
.sw5e.sheet.actor.character {
|
||||
min-width: 800px;
|
||||
min-height: 680px;
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* Sheet Header */
|
||||
/* ----------------------------------------- */
|
||||
|
||||
.sheet-header {
|
||||
|
||||
// Character Profile image (larger)
|
||||
img.profile {
|
||||
flex: 0 0 160px;
|
||||
max-width: 160px;
|
||||
height: 160px;
|
||||
}
|
||||
|
||||
// Character Level
|
||||
.charlevel {
|
||||
flex: 0 0 20px;
|
||||
height: 20px;
|
||||
font-size: 18px;
|
||||
color: @colorTan;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// Experience Tracking
|
||||
.experience {
|
||||
flex: 0 0 32px;
|
||||
margin-bottom: -5px;
|
||||
align-items: center;
|
||||
font-size: 18px;
|
||||
span.max {
|
||||
color: @colorTan;
|
||||
flex: none;
|
||||
margin-left: 3px;
|
||||
}
|
||||
}
|
||||
.xpbar {
|
||||
flex: 0 0 8px;
|
||||
width: 100%;
|
||||
margin-bottom: 5px;
|
||||
background: @colorTan;
|
||||
border: 1px solid #000;
|
||||
border-radius: 3px;
|
||||
.bar {
|
||||
height: 4px;
|
||||
margin: 1px;
|
||||
display: block;
|
||||
background: #afebff;
|
||||
border: 1px solid #000;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
// Header Attributes
|
||||
.attributes {
|
||||
a.rest {
|
||||
border: 1px solid @colorBeige;
|
||||
border-radius: 2px;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
padding: 0 3px;
|
||||
margin: 0 3px;
|
||||
}
|
||||
|
||||
.hit-dice {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.initiative .attribute-footer input {
|
||||
width: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.summary .proficiency {
|
||||
text-align: right;
|
||||
padding-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* Sheet Body */
|
||||
/* ----------------------------------------- */
|
||||
|
||||
// Custom Resources
|
||||
.resource .attribute-value {
|
||||
> input {
|
||||
flex: 0 0 25%;
|
||||
}
|
||||
label.recharge {
|
||||
height: 32px;
|
||||
position: relative;
|
||||
font-family: "Signika", sans-serif;
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
color: @colorOlive;
|
||||
align-items: center;
|
||||
input[type="checkbox"] {
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
margin: 0;
|
||||
top: -6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul.skills-list {
|
||||
flex: 0 0 212px;
|
||||
|
||||
li.skill {
|
||||
padding: 3px;
|
||||
h4 {
|
||||
flex: 1px;
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-detail.player-class {
|
||||
flex: 0 0 180px;
|
||||
text-align: right;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* Item Controls */
|
||||
/* ----------------------------------------- */
|
||||
|
||||
.inventory,
|
||||
.powerbook {
|
||||
.item-controls {
|
||||
flex: 0 0 68px;
|
||||
.item-toggle {
|
||||
color: @colorBeige;
|
||||
&.active {
|
||||
color: @colorOlive;
|
||||
}
|
||||
&.fixed {
|
||||
color: @colorCrimson;
|
||||
&:hover {
|
||||
text-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* Biography */
|
||||
/* ----------------------------------------- */
|
||||
|
||||
.characteristics {
|
||||
flex: 0 0 180px;
|
||||
height: 100%;
|
||||
padding: 0 3px 3px;
|
||||
label {
|
||||
flex: 0 0 20px;
|
||||
.russoOne();
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
line-height: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
textarea {
|
||||
.openSans();
|
||||
border: 1px solid @colorFaint;
|
||||
resize: none;
|
||||
}
|
||||
}
|
||||
|
||||
.biography {
|
||||
max-width: calc(100% - 180px);
|
||||
}
|
||||
}
|
|
@ -1,524 +0,0 @@
|
|||
@import "./variables.less";
|
||||
.sw5e.sheet.item {
|
||||
min-height: 660px;
|
||||
min-width: 680px;
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* Sheet Header */
|
||||
/* ----------------------------------------- */
|
||||
|
||||
.sheet-header {
|
||||
img.profile {
|
||||
border: 2px solid #000;
|
||||
}
|
||||
h1 {
|
||||
input {
|
||||
font-size: 26px;
|
||||
}
|
||||
}
|
||||
.header-details.flexrow {
|
||||
h1 {
|
||||
font-size: 26px;
|
||||
}
|
||||
.charname {
|
||||
font-size: 26px;
|
||||
}
|
||||
}
|
||||
.item-subtitle {
|
||||
flex: 0 0 80px;
|
||||
height: 60px;
|
||||
margin: 0;
|
||||
padding: 5px;
|
||||
text-align: right;
|
||||
color: @colorTan;
|
||||
|
||||
.item-type {
|
||||
font-size: 20px;
|
||||
line-height: 24px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.item-status {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.summary {
|
||||
li {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.sheet-navigation {
|
||||
margin-bottom: 5px;
|
||||
.item {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.sheet-body {
|
||||
overflow: hidden;
|
||||
|
||||
h1 {
|
||||
font-family: 'Russo One';
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
border-bottom: none;
|
||||
color: #c40f0f;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-family: 'Russo One';
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
border-bottom: 2px solid rgb(13, 153, 204);
|
||||
color: #c40f0f;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-family: 'Russo One';
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
border-bottom: none;
|
||||
color: #c40f0f;
|
||||
}
|
||||
.smalltable {
|
||||
table {
|
||||
border-top: none;
|
||||
border-bottom: none;
|
||||
width: 200px;
|
||||
}
|
||||
td {
|
||||
&:nth-child(odd) {
|
||||
width: 50px;
|
||||
margin: 0.5em 0.5em;
|
||||
text-align: center;
|
||||
}
|
||||
&:nth-child(even) {
|
||||
width: 150px;
|
||||
margin: 0.5em 0.5em;
|
||||
padding: 0 10px 0 10px;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
thead {
|
||||
border-bottom: 0;
|
||||
}
|
||||
th {
|
||||
color: #000000;
|
||||
text-shadow: none;
|
||||
border-bottom: 0;
|
||||
background-color: #bdc8cc;
|
||||
text-transform: none;
|
||||
font-weight: bold;
|
||||
font-family: 'Open Sans';
|
||||
&:nth-child(odd) {
|
||||
width: 50px;
|
||||
margin: 0.5em 0.5em;
|
||||
text-align: center;
|
||||
}
|
||||
&:nth-child(even) {
|
||||
width: 150px;
|
||||
margin: 0.5em 0.5em;
|
||||
padding: 0 10px 0 10px;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
.medtable {
|
||||
table {
|
||||
width: 500px;
|
||||
border: 0;
|
||||
margin: 0.5em 0.5em;
|
||||
}
|
||||
td {
|
||||
&:nth-child(odd) {
|
||||
width: 50px;
|
||||
margin: 0.5em 0.5em;
|
||||
text-align: center;
|
||||
}
|
||||
&:nth-child(even) {
|
||||
width: 450px;
|
||||
margin: 0.5em 0.5em;
|
||||
padding: 0 10px 0 0;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
thead {
|
||||
border-bottom: 0;
|
||||
}
|
||||
th {
|
||||
color: #000000;
|
||||
text-shadow: none;
|
||||
border-bottom: 0;
|
||||
background-color: #bdc8cc;
|
||||
text-transform: none;
|
||||
font-weight: bold;
|
||||
font-family: 'Open Sans';
|
||||
&:nth-child(odd) {
|
||||
text-align: center;
|
||||
}
|
||||
&:nth-child(even) {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
.classtable {
|
||||
blockquote {
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
background-color: #bdc8cc;
|
||||
width: 600px;
|
||||
h3 {
|
||||
color: #000000;
|
||||
text-transform: uppercase;
|
||||
font-family: 'Russo One';
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
border-top: 0;
|
||||
border-bottom: 0;
|
||||
margin: 0.5em 0;
|
||||
font-style: normal;
|
||||
text-shadow: none;
|
||||
}
|
||||
thead {
|
||||
color: #000000;
|
||||
text-shadow: none;
|
||||
border-bottom: 0;
|
||||
background-color: #bdc8cc;
|
||||
text-transform: none;
|
||||
font-style: normal;
|
||||
font-family: 'Open Sans';
|
||||
}
|
||||
th {
|
||||
color: #000000;
|
||||
text-shadow: none;
|
||||
border-bottom: 0;
|
||||
background-color: #bdc8cc;
|
||||
text-transform: none;
|
||||
font-style: normal;
|
||||
font-weight: bold;
|
||||
font-family: 'Open Sans';
|
||||
}
|
||||
tbody {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
.speciestable {
|
||||
blockquote {
|
||||
width: 620px;
|
||||
padding: 15px 10px;
|
||||
margin: 15px;
|
||||
line-height: 20px;
|
||||
background-color: #bdc8cc;
|
||||
border-top: 2px solid #0d99cc !important;
|
||||
border-bottom: 2px solid #0d99cc !important;
|
||||
border-left: 0 !important;
|
||||
border-right: 0 !important;
|
||||
-webkit-box-shadow: 0 0 1.5em rgba(13, 153, 204, .5) !important;
|
||||
box-shadow: 0 0 1.5em rgba(13, 153, 204, .5) !important;
|
||||
overflow-x: auto;
|
||||
h3 {
|
||||
color: #000000;
|
||||
font-size: 22px;
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
table {
|
||||
background-color: #bdc8cc;
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
line-height: 18px;
|
||||
margin-bottom: 15px;
|
||||
border: 0;
|
||||
border-bottom: none;
|
||||
overflow-x: auto;
|
||||
tbody {
|
||||
tr {
|
||||
&:nth-child(odd) {
|
||||
background-color: #c9d6db;
|
||||
}
|
||||
&:nth-child(even) {
|
||||
background-color: #bdc8cc;
|
||||
}
|
||||
}
|
||||
}
|
||||
td {
|
||||
&:nth-child(1) {
|
||||
padding-right: 5px;
|
||||
width: 100px;
|
||||
font-style: italic;
|
||||
font-weight: 800;
|
||||
}
|
||||
}
|
||||
h3 {
|
||||
font-family: 'Russo One';
|
||||
color: #000000;
|
||||
font-size: 15px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
thead {
|
||||
font-style: normal;
|
||||
font-size: 18px;
|
||||
background-color: #bdc8cc;
|
||||
text-shadow: none;
|
||||
text-align: left;
|
||||
line-height: 20px;
|
||||
border-top: 5px solid #0d99cc;
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.icon {
|
||||
&:before {
|
||||
display: inline-block;
|
||||
font-style: normal;
|
||||
font-variant: normal;
|
||||
text-rendering: auto;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
}
|
||||
a.entity-link {
|
||||
background: #DDD;
|
||||
padding: 1px 4px;
|
||||
border: 1px solid #4b4a44;
|
||||
border-radius: 2px;
|
||||
white-space: nowrap;
|
||||
word-break: break-all;
|
||||
i {
|
||||
&::before {
|
||||
content: url("ui/jedi-order.svg") !important;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
height: 15px;
|
||||
width: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
#species-description {
|
||||
h2 {
|
||||
font-family: 'Russo One';
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.5px;
|
||||
border-bottom: 2px solid rgb(13, 153, 204);
|
||||
color: #c40f0f;
|
||||
}
|
||||
}
|
||||
#Traits {
|
||||
h2 {
|
||||
font-family: 'Russo One';
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
border-bottom: 2px solid rgb(13, 153, 204);
|
||||
color: #c40f0f;
|
||||
}
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 0 5px;
|
||||
overflow: hidden auto;
|
||||
}
|
||||
|
||||
.item-properties {
|
||||
flex: 0 0 120px;
|
||||
margin: 5px 5px 5px 0;
|
||||
padding-right: 5px;
|
||||
border-right: @borderGroove;
|
||||
|
||||
.form-group {
|
||||
margin: 0;
|
||||
label {
|
||||
line-height: 20px;
|
||||
}
|
||||
input {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.properties-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
margin: 3px 0;
|
||||
padding: 0 2px;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border: @borderGroove;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* Item Details Form */
|
||||
/* ----------------------------------------- */
|
||||
|
||||
.details {
|
||||
|
||||
// Item Sheet form fields
|
||||
input[type="text"],
|
||||
input[type="number"],
|
||||
select {
|
||||
height: 24px;
|
||||
border: 1px solid @colorTan;
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
span {
|
||||
text-align: center;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-group.input-select {
|
||||
select {
|
||||
flex: 1.8;
|
||||
}
|
||||
}
|
||||
|
||||
.form-group.input-select-select {
|
||||
select {
|
||||
flex: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.form-group.uses-per {
|
||||
.form-fields {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
input {
|
||||
flex: 0 0 32px;
|
||||
}
|
||||
span {
|
||||
flex: 0 0 16px;
|
||||
margin: 0 4px 0 0;
|
||||
}
|
||||
}
|
||||
span.sep {
|
||||
flex: 0 0 8px;
|
||||
}
|
||||
|
||||
.prepared {
|
||||
flex: 1.3 !important;
|
||||
text-align: right;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
// Power Materials
|
||||
.power-materials {
|
||||
flex: 0 0 100%;
|
||||
margin: 0.25em 0;
|
||||
justify-content: flex-end;
|
||||
label {
|
||||
flex: 0 0 64px;
|
||||
text-align: right;
|
||||
margin-right: 5px;
|
||||
font-size: 12px;
|
||||
line-height: 24px;
|
||||
}
|
||||
input[type="text"] {
|
||||
flex: 0 0 48px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* Item Actions */
|
||||
/* ----------------------------------------- */
|
||||
|
||||
h4.damage-header {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-weight: bold;
|
||||
line-height: 24px;
|
||||
color: @colorOlive;
|
||||
}
|
||||
|
||||
.damage-parts {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
.damage-part {
|
||||
flex: 0 0 100%;
|
||||
padding: 0;
|
||||
input {
|
||||
flex: 3;
|
||||
}
|
||||
select {
|
||||
margin-left: 5px;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.damage-control {
|
||||
width: 18px;
|
||||
flex: 0 0 18px;
|
||||
line-height: 24px;
|
||||
float: right;
|
||||
text-align: right;
|
||||
color: @colorTan;
|
||||
}
|
||||
|
||||
.recharge.form-group {
|
||||
span {
|
||||
text-align: right;
|
||||
padding-right: 3px;
|
||||
}
|
||||
input[type="text"] {
|
||||
flex: 0 0 32px;
|
||||
text-align: center;
|
||||
}
|
||||
label.checkbox {
|
||||
flex: none;
|
||||
input {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
top: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* Item Actions */
|
||||
/* ----------------------------------------- */
|
||||
|
||||
.weapon-properties label.checkbox {
|
||||
flex: 0 0 98px;
|
||||
}
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* Loot Sheet (No Tabs) */
|
||||
/* ----------------------------------------- */
|
||||
|
||||
.loot-header {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
@import "./variables.less";
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* Basic Structure */
|
||||
/* ----------------------------------------- */
|
||||
.sw5e.sheet.actor.npc {
|
||||
min-width: 872px;
|
||||
min-height: 680px;
|
||||
|
||||
.header-exp {
|
||||
flex: 0 0 80px;
|
||||
justify-content: center;
|
||||
.cr {
|
||||
flex: 0 0 32px;
|
||||
line-height: 28px;
|
||||
margin-bottom: -5px;
|
||||
font-size: 24px;
|
||||
input {
|
||||
width: 32px;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
.experience {
|
||||
flex: 0 0 18px;
|
||||
color: @colorTan;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.summary {
|
||||
font-size: 18px;
|
||||
|
||||
li.creature-type {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 1em;
|
||||
padding: 0 3px;
|
||||
|
||||
span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.config-button {
|
||||
display: none;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
line-height: 2em;
|
||||
}
|
||||
&:hover .config-button {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
|
||||
/* Deprecated vars */
|
||||
@borderLight: #CCC;
|
||||
@borderDark: #666;
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* Fonts */
|
||||
/* ----------------------------------------- */
|
||||
|
||||
/* russo-one-regular - latin */
|
||||
@font-face {
|
||||
font-family: 'Russo One';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('./fonts/RussoOne.ttf');
|
||||
}
|
||||
.russoOne(@font-size: 20px) {
|
||||
font-family: 'Russo One';
|
||||
font-size: @font-size;
|
||||
font-weight: 400;
|
||||
}
|
||||
/* engli-besh */
|
||||
@font-face {
|
||||
font-family: 'Engli-Besh';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('./fonts/EngliBesh-KG3W.ttf');
|
||||
}
|
||||
.engli-Besh {
|
||||
font-family: 'Engli-Besh';
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
}
|
||||
/* open-sans-regular - latin */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('./fonts/OpenSans-Regular.ttf');
|
||||
}
|
||||
.openSans {
|
||||
font-family: 'Open Sans';
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* Sheet Styles */
|
||||
/* ----------------------------------------- */
|
||||
|
||||
@colorDark: #191813;
|
||||
@colorFaint: #c9c7b8;
|
||||
@colorBeige: #b5b3a4;
|
||||
@colorTan: #7a7971;
|
||||
@colorOlive: #4b4a44;
|
||||
@colorCrimson: #44191A;
|
||||
@borderGroove: 2px groove #eeede0;
|
||||
//@sheetBackground: url("ui/parchment.jpg") repeat;
|
||||
|
||||
|
||||
//SW5e Colors
|
||||
@colorBlack: #1C1C1C;
|
||||
@colorDarkGray: #363636;
|
||||
@colorGray: #4f4f4f;
|
||||
@colorLightGray: #828282;
|
||||
@colorPaleGray: #D6D6D6;
|
||||
@colorRed: #c40f0f;
|
||||
@colorPaleRed: #FBF4F4;
|
||||
@colorLightRed: #F6E1E1;
|
||||
@colorBlue: #0d99cc;
|
||||
@colorLightBlue: #7ed6f7;
|
||||
@colorPaleBlue: #afc6d6;
|
||||
|
||||
@sheetBackground: linear-gradient(90deg, @colorPaleBlue 0%, @colorPaleGray 30%, @colorPaleGray 70%, @colorPaleBlue); //url("ui/parchment.webp") repeat;
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* Flexbox */
|
||||
/* ----------------------------------------- */
|
||||
|
||||
.flexrow {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
|
||||
> * {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.flex1 { flex: 1; }
|
||||
.flex2 { flex: 2; }
|
||||
.flex3 { flex: 3; }
|
||||
.flex4 { flex: 4; }
|
||||
}
|
||||
|
||||
.flexcol {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
> * {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.flex1 { flex: 1; }
|
||||
.flex2 { flex: 2; }
|
||||
.flex3 { flex: 3; }
|
||||
.flex4 { flex: 4; }
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
.sw5e.sheet.actor.vehicle {
|
||||
.features {
|
||||
.item-controls {
|
||||
flex: 0 0 68px;
|
||||
.item-toggle {
|
||||
color: #b5b3a4;
|
||||
&.active {
|
||||
color: #4b4a44;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.counters {
|
||||
.counter.creature-cap {
|
||||
.counter-value {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
input {
|
||||
max-width: none;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.counter.cargo-cap {
|
||||
input {
|
||||
max-width: 40px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,4 +5,3 @@
|
|||
@import "chat.less";
|
||||
@import "character.less";
|
||||
@import "npc.less";
|
||||
@import "vehicle.less";
|
|
@ -1,146 +0,0 @@
|
|||
//override Primary Red
|
||||
@colorRed: #E81111;
|
||||
@colorDarkBg: #2b2b2b;
|
||||
//Background
|
||||
@primaryBackground: linear-gradient(90deg,#626262 0,#4d4d4d 30%,#4d4d4d 70%,#626262);
|
||||
|
||||
//Typography
|
||||
@headingColor: @colorRed;
|
||||
@headerBorderColor: @colorBlue;
|
||||
@bodyFontColor: white;
|
||||
@linkColor: @colorRed;
|
||||
@linkSecondaryColor: @colorPaleGray;
|
||||
|
||||
@blockquoteBackground: @colorPaleRed;
|
||||
@blockquoteBorder: @colorRed;
|
||||
@blockquoteShadow: 0 0 20px rgba(@colorRed, 0.8);
|
||||
|
||||
//forms
|
||||
@inputBackgroundColor: @colorDarkGray;
|
||||
@inputBorderNormal: @colorLightGray;
|
||||
@inputBorderHover: @colorGray;
|
||||
@inputBorderFocus: @colorRed;
|
||||
@inputTextColor: white;
|
||||
|
||||
@buttonBackground: @colorRed;
|
||||
@buttonTextColor: white;
|
||||
@buttonHoverBackground: lighten(@colorRed, 5);
|
||||
@buttonSecondaryBackground: @colorLightGray;
|
||||
@buttonSecondaryTextColor: white;
|
||||
@buttonSecondaryHoverBackground: lighten(@colorLightGray, 5);
|
||||
|
||||
//other bits
|
||||
@hrColor: @colorBlue;
|
||||
@tableTextColor: white;
|
||||
@tableHeaderTextColor: @colorPaleGray;
|
||||
@tableBackground: @colorGray;
|
||||
@tableRowHoverBackground: lighten(@colorLightGray, 10);
|
||||
@tableRowBorderColor: @colorLightGray;
|
||||
|
||||
//universalColors
|
||||
@windowHeaderBackground: @colorDarkBg;
|
||||
@windowHeaderLinkColor: @colorRed;
|
||||
|
||||
//Sidebar
|
||||
@sidebarTabBackground: @windowHeaderBackground;
|
||||
@sidebarTabLinkColor: @windowHeaderLinkColor;
|
||||
@sidebarTabLinkUnderline: @colorRed;
|
||||
|
||||
@chatBackground: @colorDarkGray;
|
||||
@chatHeaderColor: @colorRed;
|
||||
@chatHeaderBottomBorderColor: @colorBlue;
|
||||
@chatNotificationColor: @colorBlue;
|
||||
@cardButtonBorder: @colorLightGray;
|
||||
@cardFooterBorder: @colorLightBlue;
|
||||
@cardFooterSeparator: @colorPaleGray;
|
||||
|
||||
@diceFormulaBackground: @colorGray;
|
||||
@diceFormualColor: white;
|
||||
@diceTotalBackground: @colorPaleRed;
|
||||
@diceTotalBorder: @colorRed;
|
||||
@diceTotalShadow: @colorRed;
|
||||
@diceSuccessColor: @colorGreen;
|
||||
@diceFailureColor: @colorRed;
|
||||
@diceCriticalBackground: @colorPaleGreen;
|
||||
@diceCriticalColor: @colorGreen;
|
||||
@diceFumbleBackground: @colorPaleRed;
|
||||
@diceFumbleColor: @colorRed;
|
||||
|
||||
@altRowBackground: @colorGray;
|
||||
|
||||
@combatRoundColor: @colorRed;
|
||||
@combatRoundBorder: @colorBlue;
|
||||
@combatCombatantControlColor: @colorPaleGray;
|
||||
@combatCombatantControlColorActive: @colorRed;
|
||||
@combatActiveCombatantColor: @colorBlue;
|
||||
@combatTokenResourceColor: white;
|
||||
@combatTokenResouceBorder: @colorLightGray;
|
||||
@combatControlsBorder: @colorBlue;
|
||||
|
||||
@folderSearchIconColor: @colorBlue;
|
||||
@folderSubdirectoryBackground: @colorDarkBg;
|
||||
@folderSubdirectoryBorder: @colorLightGray;
|
||||
@directoryListItemBorder: @colorBlue;
|
||||
@folderHeaderBackground: @colorDarkBg;
|
||||
@folderHeaderColor: white;
|
||||
@folderIconColor: @colorBlue;
|
||||
|
||||
@entityBackgroundColor: @colorDarkBg;
|
||||
@entityNameColor: @colorBlack;
|
||||
|
||||
@sceneBorderColor: @colorBlue;
|
||||
@sceneBackgroundColor: @colorDarkBg;
|
||||
|
||||
@playlistBackgroundColor: @colorDarkBg;
|
||||
@playlistHeaderBorder: @colorBlue;
|
||||
@playlistSoundColor: @colorBlack;
|
||||
|
||||
@compendiumEntityBackground: @colorDarkBg;
|
||||
@compendiumStatusIcon: @colorLightGray;
|
||||
|
||||
@foundryNavBgColor: rgba(@colorLightBlue, 0.4);
|
||||
@foundryNavTextColor: white;
|
||||
@foundryNavBorderColor: @colorBlue;
|
||||
@foundryNavBgColorGM: @colorBlue;
|
||||
@foundryNavBorderColorGM: @colorPaleBlue;
|
||||
@foundryNavSceneLinkColor: white;
|
||||
@foundryNavActiveBgColor: rgba(@colorRed, 0.6);
|
||||
@foundryNavActiveBorderColor: lighten(@colorRed, 20);
|
||||
@foundryNavActiveGlow: darken(@colorRed, 20);
|
||||
@foundryNavContextShadow: darken(@colorBlue, 20);
|
||||
@foundryNavContextBorderColor: @colorBlue;
|
||||
|
||||
@foundryPlayersArrowColor: @colorLightGray;
|
||||
|
||||
@actorPanelBgColor: white;
|
||||
@actorNameColor: @colorRed;
|
||||
@actorXPBarBorder: @colorGray;
|
||||
@actorXPBarBackground: @colorPaleBlue;
|
||||
@actorXPBarColor: @colorBlue;
|
||||
@actorProficiencyTextColor: @colorGray;
|
||||
@actorAttributeInputColor: @colorGray;
|
||||
@actorSeparatorColor: @colorLightGray;
|
||||
@actorAttributeButtonBorder: @colorPaleGray;
|
||||
@actorAttributeButtonBorderHover: @colorRed;
|
||||
@actorNavigationTabsColor: @colorGray;
|
||||
@actorNavigationTabsActiveColor: @colorRed;
|
||||
@actorNavigationTabsHoverBgColor: rgba(@colorGray, 0.1);
|
||||
@actorNavigationTabsActiveHoverBgColor: rgba(@colorRed, 0.1);
|
||||
@actorFilterBorderColor: @colorLightGray;
|
||||
@actorFilterHoverColor: @colorRed;
|
||||
@actorFilterActiveColor: @colorRed;
|
||||
@actorGroupListHeaderBgColor: lighten(@colorPaleGray, 10);
|
||||
@actorGroupListTitleBorderColor: @colorBlue;
|
||||
@actorGroupListColumnBorderColor: @colorPaleGray;
|
||||
@actorGroupListAltRowColor: lighten(@colorPaleGray, 10);
|
||||
@actorItemRollableD20Color: @colorGray;
|
||||
@actorItemRollableD20HoverColor: @colorRed;
|
||||
@actorItemControlToggleColor: @colorLightGray;
|
||||
@actorAbilityScoreColor: @colorGray;
|
||||
@actorAbilityBorderColor: @colorPaleGray;
|
||||
@actorSkillsAltRowColor: lighten(@colorPaleGray, 10);
|
||||
@actorEncumbranceLabelBackground: @colorPaleGray;
|
||||
@actorEncumbranceTextColor: @colorBlack;
|
||||
@actorEncumbranceBorderColor: @colorBlack;
|
||||
@actorEncumbranceBarBgColor: @colorPaleBlue;
|
||||
@actorEncumbranceBarColor: @colorBlue;
|
|
@ -1,143 +0,0 @@
|
|||
//Background
|
||||
@primaryBackground: linear-gradient(90deg,#afc6d6 0,#d6d6d6 30%,#d6d6d6 70%,#afc6d6);// linear-gradient(90deg, @colorPaleBlue 0%, @colorPaleGray 30%, @colorPaleGray 70%, @colorPaleBlue);
|
||||
|
||||
//Typography
|
||||
@headingColor: @colorRed;
|
||||
@headerBorderColor: @colorBlue;
|
||||
@bodyFontColor: @colorBlack;
|
||||
@linkColor: @colorRed;
|
||||
@linkSecondaryColor: @colorGray;
|
||||
|
||||
@blockquoteBackground: @colorPaleBlue;
|
||||
@blockquoteBorder: @colorBlue;
|
||||
@blockquoteShadow: 0 0 20px rgba(@colorBlue, 0.8);
|
||||
|
||||
//forms
|
||||
@inputBackgroundColor: white;
|
||||
@inputBorderNormal: @colorLightGray;
|
||||
@inputBorderHover: @colorGray;
|
||||
@inputBorderFocus: @colorRed;
|
||||
@inputTextColor: @colorBlack;
|
||||
|
||||
@buttonBackground: @colorRed;
|
||||
@buttonTextColor: white;
|
||||
@buttonHoverBackground: lighten(@colorRed, 5);
|
||||
@buttonSecondaryBackground: @colorPaleGray;
|
||||
@buttonSecondaryTextColor: @colorBlack;
|
||||
@buttonSecondaryHoverBackground: lighten(@colorPaleGray, 5);
|
||||
|
||||
//other bits
|
||||
@hrColor: @colorBlue;
|
||||
@tableTextColor: @colorBlack;
|
||||
@tableHeaderTextColor: @colorLightGray;
|
||||
@tableBackground: white;
|
||||
@tableRowHoverBackground: lighten(@colorPaleGray, 10);
|
||||
@tableRowBorderColor: @colorPaleGray;
|
||||
|
||||
//universalColors
|
||||
@windowHeaderBackground: white;
|
||||
@windowHeaderLinkColor: @colorRed;
|
||||
|
||||
//Sidebar
|
||||
@sidebarTabBackground: @windowHeaderBackground;
|
||||
@sidebarTabLinkColor: @windowHeaderLinkColor;
|
||||
@sidebarTabLinkUnderline: @colorRed;
|
||||
|
||||
@chatBackground: white;
|
||||
@chatHeaderColor: @colorRed;
|
||||
@chatHeaderBottomBorderColor: @colorBlue;
|
||||
@chatNotificationColor: @colorBlue;
|
||||
@cardButtonBorder: @colorLightGray;
|
||||
@cardFooterBorder: @colorLightBlue;
|
||||
@cardFooterSeparator: @colorPaleGray;
|
||||
|
||||
@diceFormulaBackground: @colorPaleGray;
|
||||
@diceFormualColor: @colorBlack;
|
||||
@diceTotalBackground: @colorPaleBlue;
|
||||
@diceTotalBorder: @colorBlue;
|
||||
@diceTotalShadow: @colorBlue;
|
||||
@diceSuccessColor: @colorGreen;
|
||||
@diceFailureColor: @colorRed;
|
||||
@diceCriticalBackground: @colorPaleGreen;
|
||||
@diceCriticalColor: @colorGreen;
|
||||
@diceFumbleBackground: @colorPaleRed;
|
||||
@diceFumbleColor: @colorRed;
|
||||
|
||||
@altRowBackground: @colorPaleBlue;
|
||||
|
||||
@combatRoundColor: @colorRed;
|
||||
@combatRoundBorder: @colorBlue;
|
||||
@combatCombatantControlColor: @colorLightGray;
|
||||
@combatCombatantControlColorActive: @colorDarkGray;
|
||||
@combatActiveCombatantColor: @colorBlue;
|
||||
@combatTokenResourceColor: @colorGray;
|
||||
@combatTokenResouceBorder: @colorLightGray;
|
||||
@combatControlsBorder: @colorBlue;
|
||||
|
||||
@folderSearchIconColor: @colorBlue;
|
||||
@folderSubdirectoryBackground: white;
|
||||
@folderSubdirectoryBorder: @colorBlack;
|
||||
@directoryListItemBorder: @colorBlue;
|
||||
@folderHeaderBackground: white;
|
||||
@folderHeaderColor: @colorBlack;
|
||||
@folderIconColor: @colorBlue;
|
||||
|
||||
@entityBackgroundColor: white;
|
||||
@entityNameColor: @colorBlack;
|
||||
|
||||
@sceneBorderColor: @colorBlue;
|
||||
@sceneBackgroundColor: white;
|
||||
|
||||
@playlistBackgroundColor: white;
|
||||
@playlistHeaderBorder: @colorBlue;
|
||||
@playlistSoundColor: @colorBlack;
|
||||
|
||||
@compendiumEntityBackground: white;
|
||||
@compendiumStatusIcon: @colorLightGray;
|
||||
|
||||
@foundryNavBgColor: rgba(@colorLightBlue, 0.4);
|
||||
@foundryNavTextColor: white;
|
||||
@foundryNavBorderColor: @colorBlue;
|
||||
@foundryNavBgColorGM: @colorBlue;
|
||||
@foundryNavBorderColorGM: @colorPaleBlue;
|
||||
@foundryNavSceneLinkColor: white;
|
||||
@foundryNavActiveBgColor: rgba(@colorRed, 0.6);
|
||||
@foundryNavActiveBorderColor: lighten(@colorRed, 20);
|
||||
@foundryNavActiveGlow: darken(@colorRed, 20);
|
||||
@foundryNavContextShadow: darken(@colorBlue, 20);
|
||||
@foundryNavContextBorderColor: @colorBlue;
|
||||
|
||||
@foundryPlayersArrowColor: @colorLightGray;
|
||||
|
||||
@actorPanelBgColor: white;
|
||||
@actorNameColor: @colorRed;
|
||||
@actorXPBarBorder: @colorGray;
|
||||
@actorXPBarBackground: @colorPaleBlue;
|
||||
@actorXPBarColor: @colorBlue;
|
||||
@actorProficiencyTextColor: @colorGray;
|
||||
@actorAttributeInputColor: @colorGray;
|
||||
@actorSeparatorColor: @colorLightGray;
|
||||
@actorAttributeButtonBorder: @colorPaleGray;
|
||||
@actorAttributeButtonBorderHover: @colorRed;
|
||||
@actorNavigationTabsColor: @colorGray;
|
||||
@actorNavigationTabsActiveColor: @colorRed;
|
||||
@actorNavigationTabsHoverBgColor: rgba(@colorGray, 0.1);
|
||||
@actorNavigationTabsActiveHoverBgColor: rgba(@colorRed, 0.1);
|
||||
@actorFilterBorderColor: @colorLightGray;
|
||||
@actorFilterHoverColor: @colorRed;
|
||||
@actorFilterActiveColor: @colorRed;
|
||||
@actorGroupListHeaderBgColor: lighten(@colorPaleGray, 10);
|
||||
@actorGroupListTitleBorderColor: @colorBlue;
|
||||
@actorGroupListColumnBorderColor: @colorPaleGray;
|
||||
@actorGroupListAltRowColor: lighten(@colorPaleGray, 10);
|
||||
@actorItemRollableD20Color: @colorGray;
|
||||
@actorItemRollableD20HoverColor: @colorRed;
|
||||
@actorItemControlToggleColor: @colorLightGray;
|
||||
@actorAbilityScoreColor: @colorGray;
|
||||
@actorAbilityBorderColor: @colorPaleGray;
|
||||
@actorSkillsAltRowColor: lighten(@colorPaleGray, 10);
|
||||
@actorEncumbranceLabelBackground: @colorPaleGray;
|
||||
@actorEncumbranceTextColor: @colorBlack;
|
||||
@actorEncumbranceBorderColor: @colorBlack;
|
||||
@actorEncumbranceBarBgColor: @colorPaleBlue;
|
||||
@actorEncumbranceBarColor: @colorBlue;
|
|
@ -1,67 +0,0 @@
|
|||
|
||||
/* ----------------------------------------- */
|
||||
/* Fonts */
|
||||
/* ----------------------------------------- */
|
||||
.russoOne(@size: 20px) {
|
||||
font-family: 'Russo One';
|
||||
font-size: @size;
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.openSans(@size: 13px, @weight: 400) {
|
||||
font-family: 'Open Sans';
|
||||
font-size: @size;
|
||||
font-weight: @weight;
|
||||
}
|
||||
.fontAwesome() {
|
||||
font-family: "Font Awesome 5 Free";
|
||||
-webkit-font-smoothing: antialiased;
|
||||
display: inline-block;
|
||||
font-style: normal;
|
||||
font-variant: normal;
|
||||
text-rendering: auto;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* Sheet Styles */
|
||||
/* ----------------------------------------- */
|
||||
|
||||
@colorDark: #191813;
|
||||
@colorFaint: #c9c7b8;
|
||||
@colorBeige: #b5b3a4;
|
||||
@colorTan: #7a7971;
|
||||
@colorOlive: #4b4a44;
|
||||
@colorCrimson: #44191A;
|
||||
@borderGroove: 2px groove #eeede0;
|
||||
//@sheetBackground: url("ui/parchment.jpg") repeat;
|
||||
|
||||
|
||||
//SW5e Colors
|
||||
@colorBlack: #1C1C1C;
|
||||
@colorDarkGray: #363636;
|
||||
@colorGray: #4f4f4f;
|
||||
@colorLightGray: #828282;
|
||||
@colorPaleGray: #D6D6D6;
|
||||
@colorRed: #c40f0f;
|
||||
@colorPaleRed: #FBF4F4;
|
||||
@colorLightRed: #F6E1E1;
|
||||
@colorBlue: #0d99cc;
|
||||
@colorLightBlue: #7ed6f7;
|
||||
@colorPaleBlue: #afc6d6;
|
||||
@colorGreen: #0dce0d;
|
||||
@colorPaleGreen: #bcdcbe;
|
||||
|
||||
@sheetBackground: linear-gradient(90deg, @colorPaleBlue 0%, @colorPaleGray 30%, @colorPaleGray 70%, @colorPaleBlue);
|
||||
|
||||
|
||||
.dropShadow1(){
|
||||
box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 3px 1px -2px rgba(0,0,0,0.12), 0 1px 5px 0 rgba(0,0,0,0.2);
|
||||
}
|
||||
.dropShadow2() {
|
||||
box-shadow: 0 4px 5px 0 rgba(0,0,0,0.14), 0 1px 10px 0 rgba(0,0,0,0.12), 0 2px 4px -1px rgba(0,0,0,0.3);
|
||||
}
|
||||
.dropShadow3() {
|
||||
box-shadow: 0 8px 17px 2px rgba(0,0,0,0.14), 0 3px 14px 2px rgba(0,0,0,0.12), 0 5px 5px -3px rgba(0,0,0,0.2);
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -1,420 +0,0 @@
|
|||
.panel {
|
||||
background: @actorPanelBgColor;
|
||||
}
|
||||
|
||||
.sw5e.sheet .window-content {
|
||||
color: @colorBlack;
|
||||
background: linear-gradient(90deg,#afc6d6 0,#d6d6d6 30%,#d6d6d6 70%,#afc6d6);
|
||||
input,
|
||||
select {
|
||||
color: @colorBlack;
|
||||
&:hover {
|
||||
border-color: @inputBorderHover;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: @inputBorderFocus;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-color: @inputBorderFocus;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sw5e.sheet.actor {
|
||||
color: @colorBlack;
|
||||
input, select, textarea {
|
||||
&:hover {
|
||||
border-color: @inputBorderFocus;
|
||||
}
|
||||
&:focus {
|
||||
border-color: @inputBorderFocus;
|
||||
}
|
||||
}
|
||||
.swalt-sheet {
|
||||
section>h1 {
|
||||
border-bottom: 2px solid @colorBlue;
|
||||
}
|
||||
|
||||
header {
|
||||
|
||||
h1.character-name {
|
||||
color: @actorNameColor;
|
||||
|
||||
input[type="text"] {
|
||||
color: @actorNameColor;
|
||||
}
|
||||
}
|
||||
|
||||
.level-experience {
|
||||
|
||||
.xpbar {
|
||||
border: 1px solid @actorXPBarBorder;
|
||||
background-color: @actorXPBarBackground;
|
||||
|
||||
.bar {
|
||||
background-color: @actorXPBarColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.summary {
|
||||
|
||||
input,
|
||||
.proficiency {
|
||||
color: @actorProficiencyTextColor;
|
||||
}
|
||||
}
|
||||
|
||||
.attributes {
|
||||
|
||||
.attribute-value,
|
||||
.attribute-value input {
|
||||
color: @actorAttributeInputColor;
|
||||
}
|
||||
|
||||
.attribute-value {
|
||||
|
||||
.value-separator {
|
||||
color: @actorSeparatorColor;
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
button {
|
||||
border: 1px solid @actorAttributeButtonBorder;
|
||||
|
||||
&:hover {
|
||||
color: @actorAttributeButtonBorderHover;
|
||||
}
|
||||
}
|
||||
|
||||
&.hit-points,
|
||||
&.hit-dice,
|
||||
&.initiative {
|
||||
button {
|
||||
border: 1px solid @actorAttributeButtonBorder;
|
||||
color: @colorRed;
|
||||
|
||||
&:hover {
|
||||
border-color: @actorAttributeButtonBorderHover;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nav.sheet-navigation {
|
||||
.item {
|
||||
color: @actorNavigationTabsColor;
|
||||
|
||||
&.active {
|
||||
color: @actorNavigationTabsActiveColor;
|
||||
border-bottom-color: @actorNavigationTabsActiveColor;
|
||||
|
||||
&:hover {
|
||||
background: @actorNavigationTabsHoverBgColor;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: @actorNavigationTabsHoverBgColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab {
|
||||
|
||||
.filter-list {
|
||||
|
||||
.filter-item {
|
||||
border-bottom: 2px solid @actorFilterBorderColor;
|
||||
|
||||
&:hover {
|
||||
color: @actorFilterHoverColor;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: @actorFilterActiveColor;
|
||||
border-bottom-color: @actorFilterActiveColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.group-list-header {
|
||||
background: @actorGroupListHeaderBgColor;
|
||||
}
|
||||
|
||||
.group-list-title {
|
||||
border-bottom: 1px solid @actorGroupListTitleBorderColor;
|
||||
}
|
||||
|
||||
.group-list-header,
|
||||
.group-list {
|
||||
.item-detail {
|
||||
border-left: 1px solid @actorGroupListColumnBorderColor;
|
||||
}
|
||||
}
|
||||
|
||||
.group-list,
|
||||
.group-list ol {
|
||||
li.item {
|
||||
&:nth-child(even) {
|
||||
background-color: @actorGroupListAltRowColor;
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: @colorBlack;
|
||||
}
|
||||
|
||||
|
||||
.item-name {
|
||||
|
||||
.item-image {
|
||||
|
||||
&::before {
|
||||
color: @actorItemRollableD20Color;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
&.rollable:hover {
|
||||
|
||||
.item-image {
|
||||
&:hover {
|
||||
&::before {
|
||||
color: @actorItemRollableD20HoverColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-control {
|
||||
&:hover {
|
||||
color: @linkColor !important;
|
||||
}
|
||||
|
||||
&.item-toggle {
|
||||
color: @actorItemControlToggleColor;
|
||||
|
||||
&.active {
|
||||
color: @colorBlack;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
.tab.attributes {
|
||||
.abilities {
|
||||
|
||||
.scores {
|
||||
li {
|
||||
border: 1px solid @actorAbilityBorderColor;
|
||||
|
||||
h2 {
|
||||
&:hover {
|
||||
color: @linkColor;
|
||||
}
|
||||
}
|
||||
|
||||
.ability-score {
|
||||
color: @actorAbilityScoreColor;
|
||||
}
|
||||
|
||||
.ability-modifiers {
|
||||
|
||||
.ability-mod,
|
||||
.ability-save {
|
||||
border-color: @actorAbilityBorderColor;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.skills {
|
||||
li {
|
||||
&:nth-child(even) {
|
||||
background-color: @actorSkillsAltRowColor;
|
||||
}
|
||||
.proficiency-toggle {
|
||||
color: @colorBlack;
|
||||
}
|
||||
|
||||
.skill-name {
|
||||
&:hover {
|
||||
color: @linkColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.traits-resources {
|
||||
nav {
|
||||
button {
|
||||
color: @actorNavigationTabsColor;
|
||||
|
||||
&.active {
|
||||
color: @actorNavigationTabsActiveColor;
|
||||
border-bottom-color: @actorNavigationTabsActiveColor;
|
||||
|
||||
&:hover {
|
||||
background: @actorNavigationTabsActiveHoverBgColor;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: @actorNavigationTabsHoverBgColor;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
section.traits {
|
||||
.trait-selector {
|
||||
i.fas {
|
||||
color: @linkColor;
|
||||
}
|
||||
}
|
||||
|
||||
.languages {
|
||||
label {
|
||||
&:hover {
|
||||
color: @linkColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
section.resources {
|
||||
.resource-items {
|
||||
.resource {
|
||||
h1 {
|
||||
|
||||
input {
|
||||
color: @headingColor;
|
||||
border-bottom: 2px solid @headerBorderColor;
|
||||
}
|
||||
}
|
||||
|
||||
.attribute-value,
|
||||
.attribute-value input {
|
||||
color: @actorAttributeInputColor;
|
||||
}
|
||||
|
||||
.attribute-value {
|
||||
.value-separator {
|
||||
color: @actorSeparatorColor;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.counters {
|
||||
.counter {
|
||||
h4 {
|
||||
&.rollable {
|
||||
&:hover {
|
||||
color: @linkColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.death-success {
|
||||
i {
|
||||
color: @colorGreen;
|
||||
}
|
||||
}
|
||||
|
||||
.death-fail {
|
||||
i {
|
||||
color: @colorRed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab.inventory {
|
||||
.currency {
|
||||
color: @headingColor;
|
||||
}
|
||||
|
||||
.encumbrance-wrapper {
|
||||
.encumbrance-label {
|
||||
background: @actorEncumbranceLabelBackground;
|
||||
color: @actorEncumbranceTextColor;
|
||||
border: 1px solid @actorEncumbranceBorderColor;
|
||||
}
|
||||
|
||||
.encumbrance {
|
||||
background: @actorEncumbranceBarBgColor;
|
||||
.encumbrance-bar {
|
||||
background: @actorEncumbranceBarColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.tab.force-powerbook,
|
||||
.tab.tech-powerbook {
|
||||
.powercasting-ability {
|
||||
label,
|
||||
h3 {
|
||||
color: @headingColor;
|
||||
|
||||
span {
|
||||
color: @colorBlack;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab.notes {
|
||||
section {
|
||||
&>input {
|
||||
color: @headingColor;
|
||||
border-bottom: 2px solid @headerBorderColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&.npc {
|
||||
.swalt-sheet {
|
||||
header {
|
||||
div.creature-type:hover {
|
||||
border-color: @inputBorderFocus;
|
||||
}
|
||||
.experience {
|
||||
color: @actorProficiencyTextColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,105 +0,0 @@
|
|||
input[type="text"], input[type="number"], input[type="password"], input[type="date"], input[type="time"], select, textarea, .roundTransition {
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s;
|
||||
&:hover {
|
||||
box-shadow: none;
|
||||
}
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
input[type=range] {
|
||||
-webkit-appearance: none; /* Hides the slider so that custom slider can be made */
|
||||
width: 100%; /* Specific width is required for Firefox. */
|
||||
background: transparent; /* Otherwise white in Chrome */
|
||||
}
|
||||
|
||||
input[type=range]::-webkit-slider-thumb{
|
||||
-webkit-appearance: none;
|
||||
background: @colorRed;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 32px;
|
||||
cursor: pointer;
|
||||
box-shadow: none;
|
||||
}
|
||||
input[type=range]::-moz-range-thumb{
|
||||
-webkit-appearance: none;
|
||||
background: @colorRed;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 32px;
|
||||
cursor: pointer;
|
||||
box-shadow: none;
|
||||
}
|
||||
input[type=range]::-ms-thumb {
|
||||
-webkit-appearance: none;
|
||||
background: @colorRed;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 32px;
|
||||
cursor: pointer;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
input[type=range]::-webkit-slider-runnable-track {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
cursor: pointer;
|
||||
background: @colorLightBlue;
|
||||
border-radius: 4px;
|
||||
border: 1px solid @colorBlue;
|
||||
box-shadow: none;
|
||||
}
|
||||
input[type=range]:focus::-webkit-slider-runnable-track {
|
||||
background: @colorBlue;
|
||||
}
|
||||
input[type=range]::-moz-range-track {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
cursor: pointer;
|
||||
background: @colorLightBlue;
|
||||
border-radius: 4px;
|
||||
border: 1px solid @colorBlue;
|
||||
box-shadow: none;
|
||||
}
|
||||
input[type=range]::-ms-track {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
cursor: pointer;
|
||||
background: @colorLightBlue;
|
||||
border-radius: 4px;
|
||||
border: 1px solid @colorBlue;
|
||||
box-shadow: none;
|
||||
}
|
||||
input[type=range]:focus {
|
||||
outline: none; /* Removes the blue border. You should probably do some kind of focus styling for accessibility reasons though. */
|
||||
}
|
||||
|
||||
input[type=range]::-ms-track {
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
|
||||
/* Hides the slider so custom styles can be added */
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
button, input[type="button"], input[type="submit"], input[type="reset"] {
|
||||
.openSans(13px, 700);
|
||||
text-align: center;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
&:hover, &:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: default;
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
input[type="text"], input[type="number"], input[type="password"], input[type="date"], input[type="time"], select, textarea {
|
||||
border: 1px solid @inputBorderNormal;
|
||||
&:hover {
|
||||
border-color: @inputBorderHover;
|
||||
}
|
||||
&:focus {
|
||||
border-color: @inputBorderFocus;
|
||||
}
|
||||
&::placeholder {
|
||||
color: @inputTextColor;
|
||||
opacity: 0.5;
|
||||
}
|
||||
::-ms-input-placeholder { /* Microsoft Edge */
|
||||
color: @inputTextColor;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
button, input[type="button"], input[type="submit"], input[type="reset"] {
|
||||
background: @buttonBackground;
|
||||
color: @buttonTextColor;
|
||||
&:hover, &:focus {
|
||||
background: @buttonHoverBackground;
|
||||
}
|
||||
&:disabled {
|
||||
&:hover, &:focus {
|
||||
background: @buttonBackground;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
input[type="reset"], button.secondary, button[type="reset"], input[type="button"].secondary, input[type="submit"].secondary {
|
||||
background: @buttonSecondaryBackground;
|
||||
color: @buttonSecondaryTextColor;
|
||||
&:hover {
|
||||
background: @buttonSecondaryHoverBackground;
|
||||
}
|
||||
&:disabled {
|
||||
&:hover, &:focus {
|
||||
background: @buttonSecondaryBackground;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
button {
|
||||
border: none;
|
||||
}
|
||||
.notes, .hint {
|
||||
color: rgba(@bodyFontColor, 0.8);
|
||||
}
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
.window-app {
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
.dropShadow2();
|
||||
& > header {
|
||||
background: @windowHeaderBackground;
|
||||
border-radius: 4px 4px 0 0;
|
||||
border: none;
|
||||
.dropShadow1();
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.window-content {
|
||||
background: @primaryBackground;
|
||||
color: @bodyFontColor;
|
||||
footer {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
&.minimized {
|
||||
& > header, & > .window-header {
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#client-settings {
|
||||
nav.tabs {
|
||||
border: none;
|
||||
font-size: 17px;
|
||||
line-height: 1.6;
|
||||
a.item {
|
||||
border-bottom: 3px solid transparent;
|
||||
color: @bodyFontColor;
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
&.active {
|
||||
text-shadow: none;
|
||||
border-bottom-color: @sidebarTabLinkUnderline;
|
||||
}
|
||||
}
|
||||
}
|
||||
section.content {
|
||||
border: none;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-buttons {
|
||||
margin-top: 8px;
|
||||
button:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
button:not(.default) {
|
||||
border: 1px solid @buttonBackground;
|
||||
margin-right: 4px;
|
||||
background: @buttonSecondaryBackground;
|
||||
color: @buttonSecondaryTextColor;
|
||||
&:hover {
|
||||
background: @buttonSecondaryHoverBackground;
|
||||
}
|
||||
|
||||
}
|
||||
button.normal.default {
|
||||
border: none;
|
||||
background: @buttonBackground;
|
||||
color: @buttonTextColor;
|
||||
&:hover {
|
||||
background: @buttonHoverBackground;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,84 +0,0 @@
|
|||
#navigation {
|
||||
#nav-toggle {
|
||||
background: @foundryNavBgColor;
|
||||
color: @foundryNavTextColor;
|
||||
|
||||
transform: rotate(-90deg);
|
||||
|
||||
}
|
||||
.nav-item {
|
||||
border: 1px solid @foundryNavBorderColor;
|
||||
}
|
||||
#scene-list {
|
||||
.scene {
|
||||
border: 1px solid @foundryNavBorderColor;
|
||||
background: rgba(@foundryNavBgColor, 0.4);
|
||||
a {
|
||||
color: @foundryNavSceneLinkColor;
|
||||
}
|
||||
&.gm {
|
||||
border: 1px solid @foundryNavBorderColorGM;
|
||||
background: rgba(@foundryNavBgColorGM, 0.4);
|
||||
}
|
||||
&.view, &.context {
|
||||
box-shadow: 0 0 8px @foundryNavContextShadow;
|
||||
border-color: @foundryNavContextBorderColor;
|
||||
}
|
||||
&.active {
|
||||
border-color: @foundryNavActiveBorderColor;
|
||||
background: @foundryNavActiveBgColor;
|
||||
box-shadow: 0 0 8px @foundryNavActiveGlow;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#controls {
|
||||
.scene-control, .control-tool {
|
||||
background: @foundryNavBgColor;
|
||||
color: @foundryNavTextColor;
|
||||
border: 1px solid @foundryNavBorderColor;
|
||||
box-shadow: none;
|
||||
&:hover {
|
||||
background: @foundryNavBgColor;
|
||||
box-shadow: 0 0 8px @foundryNavContextShadow;
|
||||
}
|
||||
&.active {
|
||||
border-color: @foundryNavActiveBorderColor;
|
||||
background: @foundryNavActiveBgColor;
|
||||
box-shadow: 0 0 8px @foundryNavActiveGlow;
|
||||
}
|
||||
}
|
||||
}
|
||||
#players {
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
h3 {
|
||||
background: @sidebarTabBackground;
|
||||
border: none;
|
||||
margin: 0;
|
||||
padding: 0 8px;
|
||||
font-size: 17px;
|
||||
line-height: 30px;
|
||||
.dropShadow1();
|
||||
border-radius: 4px 4px 0 0;
|
||||
.players-mode {
|
||||
color: @foundryPlayersArrowColor;
|
||||
}
|
||||
}
|
||||
ol {
|
||||
margin: 4px 0;
|
||||
.player-name.self {
|
||||
color: inherit;
|
||||
font-weight: 700;
|
||||
}
|
||||
.player {
|
||||
color: @bodyFontColor;
|
||||
}
|
||||
.player-active {
|
||||
margin-top: 7px;
|
||||
&.active {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,388 +0,0 @@
|
|||
#sidebar {
|
||||
border: none; //1px solid @colorBlue;
|
||||
&.collapsed {
|
||||
#sidebar-tabs {
|
||||
min-height: 370px;
|
||||
justify-content: center;
|
||||
& > .item.active {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#sidebar-tabs {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
justify-content: space-between;
|
||||
.dropShadow1();
|
||||
|
||||
.item {
|
||||
font-size: 16px;
|
||||
}
|
||||
.item.active {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
background: none;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*-----------
|
||||
** Chat Tab
|
||||
-----------*/
|
||||
|
||||
#chat-log {
|
||||
.chat-message {
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 8px;
|
||||
.dropShadow1();
|
||||
& > header {
|
||||
color: @colorRed;
|
||||
border-bottom: 2px solid @colorBlue;
|
||||
margin-bottom: 4px;
|
||||
span {
|
||||
color: @colorBlack;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
.notification-pip {
|
||||
color: @colorBlue;
|
||||
}
|
||||
|
||||
.sw5e.chat-card,
|
||||
.midi-qol-item-card {
|
||||
.card-header {
|
||||
padding: 0;
|
||||
border: none;
|
||||
img {
|
||||
flex: 0 0 36px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
line-height: 36px;
|
||||
.russoOne(17px);
|
||||
border-bottom: none;
|
||||
&:hover {
|
||||
text-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-content {
|
||||
margin: 4px 0;
|
||||
|
||||
h3 {
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
> * {
|
||||
-webkit-user-select: text;
|
||||
-moz-user-select: text;
|
||||
-ms-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
}
|
||||
|
||||
.card-buttons {
|
||||
margin: 4px 0;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
line-height: 28px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button {
|
||||
.openSans(13px, 700);
|
||||
padding: 4px 0;
|
||||
height: auto;
|
||||
line-height: 1.6;
|
||||
margin: 4px 0;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
&:hover, &:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
padding: 4px 0 0;
|
||||
|
||||
span {
|
||||
padding: 0 4px 0 0;
|
||||
font-size: 10px;
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.dice-roll {
|
||||
.dice-formula {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.dice-total {
|
||||
border-radius: 0;
|
||||
padding: 4px 0;
|
||||
}
|
||||
}
|
||||
#chat-controls {
|
||||
padding-top: 4px;
|
||||
}
|
||||
#chat-form textarea {
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
/*-----------
|
||||
** Combat Tab
|
||||
-----------*/
|
||||
#combat {
|
||||
h3 {
|
||||
border: none;
|
||||
}
|
||||
|
||||
#combat-tracker {
|
||||
li.combatant {
|
||||
padding: 4px 0;
|
||||
background: none;
|
||||
.token-name {
|
||||
text-shadow: none;
|
||||
}
|
||||
.ce-image-wrapper {
|
||||
.token-image {
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
h4 {
|
||||
color: @colorBlack;
|
||||
}
|
||||
.roll {
|
||||
background: none;
|
||||
&::before {
|
||||
content: "\f6cf";
|
||||
.fontAwesome();
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
.initiative {
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
#combat-controls {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
** Folders
|
||||
*/
|
||||
.sidebar-tab {
|
||||
.directory-header {
|
||||
margin-bottom: 4px;
|
||||
.header-search {
|
||||
position: relative;
|
||||
i.fa-search {
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
}
|
||||
input {
|
||||
text-align: left;
|
||||
padding-left: 22px;
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.subdirectory {
|
||||
border: none;
|
||||
margin-left: 8px;
|
||||
min-height: 8px;
|
||||
|
||||
}
|
||||
.directory-list {
|
||||
padding-bottom: 4px;
|
||||
.folder {
|
||||
& > .folder-header {
|
||||
line-height: initial;
|
||||
padding: 0 0 0 8px;
|
||||
position: relative;
|
||||
border: none;
|
||||
h3 {
|
||||
padding: 8px 4px;
|
||||
.openSans(13px, 700);
|
||||
line-height: 1.6;
|
||||
& > i {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
a {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 4px;
|
||||
height: 100%;
|
||||
padding: 0 4px;
|
||||
i {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
&.create-folder {
|
||||
right: 28px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.directory-item img {
|
||||
flex: 0 0 32px;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
align-self: center;
|
||||
}
|
||||
.actor, .item, .journal, .table {
|
||||
border: none;
|
||||
.entity-name {
|
||||
.openSans(13px, 700);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
#scenes {
|
||||
.subdirectory {
|
||||
border-left: none;
|
||||
}
|
||||
.scene {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
position: relative;
|
||||
height: 128px;
|
||||
& + .scene {
|
||||
margin-top: 4px;
|
||||
}
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 99px;
|
||||
position: absolute;
|
||||
top: 28px;
|
||||
left: 0;
|
||||
}
|
||||
h3 {
|
||||
.openSans(13px, 700);
|
||||
text-align: left;
|
||||
text-shadow: none;
|
||||
padding: 4px 4px 4px 12px;
|
||||
line-height: 1.6;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#playlists {
|
||||
.directory-list {
|
||||
padding: 0 8px;
|
||||
li.playlist {
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 8px;
|
||||
border-top: inherit;
|
||||
.dropShadow1();
|
||||
.playlist-header {
|
||||
text-decoration: none;
|
||||
}
|
||||
li.sound {
|
||||
border: none;
|
||||
h4 {
|
||||
.openSans(13px, 400);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
#compendium {
|
||||
.compendium-entity {
|
||||
margin: 0 4px;
|
||||
padding: 8px;
|
||||
.dropShadow1();
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
&+ .compendium-entity {
|
||||
margin-top: 4px;
|
||||
}
|
||||
h3 {
|
||||
background: none;
|
||||
border: none;
|
||||
.russoOne(17px);
|
||||
padding: 0;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
ol.compendium-list {
|
||||
li.compendium-pack {
|
||||
margin: 0;
|
||||
padding: 4px;
|
||||
border: none;
|
||||
.pack-title {
|
||||
margin: 0;
|
||||
position: relative;
|
||||
a {
|
||||
.openSans(13px, 700);
|
||||
i {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.status-icons {
|
||||
top: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#settings {
|
||||
h2 {
|
||||
border: none;
|
||||
margin: 0 8px;
|
||||
padding: 0;
|
||||
background: none;
|
||||
}
|
||||
#game-details, #settings-game, #settings-documentation, #settings-access {
|
||||
padding: 0 8px;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
}
|
|
@ -1,298 +0,0 @@
|
|||
#sidebar-tabs {
|
||||
background: @sidebarTabBackground;
|
||||
& > .collapse {
|
||||
color: @sidebarTabLinkColor;
|
||||
}
|
||||
.item.active {
|
||||
color: @sidebarTabLinkColor;
|
||||
border-bottom: 3px solid @sidebarTabLinkUnderline;
|
||||
}
|
||||
}
|
||||
|
||||
/*-----------
|
||||
** Chat Tab
|
||||
-----------*/
|
||||
|
||||
#chat-log {
|
||||
.chat-message {
|
||||
background: @chatBackground;
|
||||
color: @bodyFontColor;
|
||||
& > header {
|
||||
color: @chatHeaderColor;
|
||||
border-bottom: 2px solid @chatHeaderBottomBorderColor;
|
||||
span {
|
||||
color: @bodyFontColor;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
.notification-pip {
|
||||
color: @chatNotificationColor;
|
||||
}
|
||||
|
||||
.sw5e.chat-card,
|
||||
.midi-qol-item-card {
|
||||
|
||||
.card-header {
|
||||
h3 {
|
||||
color: @bodyFontColor;
|
||||
&:hover {
|
||||
color: @bodyFontColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.card-buttons {
|
||||
span {
|
||||
border: 1px solid @cardButtonBorder;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
border-top: 1px solid @cardFooterBorder;
|
||||
|
||||
span {
|
||||
border-right: 1px solid @cardFooterSeparator;
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.dice-roll {
|
||||
|
||||
.dice-formula {
|
||||
background: @diceFormulaBackground;
|
||||
color: @diceFormualColor;
|
||||
box-shadow: none;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.dice-total {
|
||||
background: @diceTotalBackground;
|
||||
border: 1px solid @diceTotalBorder;
|
||||
box-shadow: 0 0 12px rgba(@diceTotalShadow,.8);
|
||||
&.success {
|
||||
color: @diceSuccessColor;
|
||||
}
|
||||
&.failure {
|
||||
color: @diceFailureColor;
|
||||
}
|
||||
&.critical {
|
||||
color: @diceCriticalColor;
|
||||
background: @diceCriticalBackground;
|
||||
box-shadow: 0 0 12px rgba(@diceCriticalColor,.5);
|
||||
}
|
||||
&.fumble {
|
||||
color: @diceFumbleColor;
|
||||
background: @diceFumbleBackground;
|
||||
box-shadow: 0 0 12px rgba(@diceFumbleColor,.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
#chat-controls {
|
||||
.roll-type-select {
|
||||
background: #a9a9a9;
|
||||
color: #1C1C1C;
|
||||
}
|
||||
label {
|
||||
color: @bodyFontColor;
|
||||
}
|
||||
|
||||
}
|
||||
#chat-form textarea {
|
||||
background: #a9a9a9;
|
||||
color: #1C1C1C;
|
||||
|
||||
}
|
||||
|
||||
/*-----------
|
||||
** Combat Tab
|
||||
-----------*/
|
||||
#combat {
|
||||
#combat-round {
|
||||
color: @combatRoundColor;
|
||||
border-bottom: 2px solid @combatRoundColor;
|
||||
.encounters {
|
||||
h4 {
|
||||
color: @combatRoundColor;
|
||||
}
|
||||
a {
|
||||
color: @linkSecondaryColor;
|
||||
&:hover {
|
||||
color: @linkColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#combat-tracker {
|
||||
//padding-top: 4px;
|
||||
li.combatant {
|
||||
color: @bodyFontColor;
|
||||
&:nth-child(even) {
|
||||
background: rgba(@altRowBackground, 0.5);
|
||||
}
|
||||
h4 {
|
||||
color: @bodyFontColor
|
||||
}
|
||||
.roll {
|
||||
color: @linkSecondaryColor;
|
||||
&:hover {
|
||||
color: @linkColor;
|
||||
}
|
||||
}
|
||||
.combatant-control {
|
||||
color: @combatCombatantControlColor;
|
||||
&.active {
|
||||
color: @combatCombatantControlColorActive;
|
||||
}
|
||||
}
|
||||
.token-resource {
|
||||
color: @combatTokenResourceColor;
|
||||
border-right: 1px solid @combatTokenResouceBorder;
|
||||
}
|
||||
&.active {
|
||||
color: @combatActiveCombatantColor;
|
||||
.initiative, h4 {
|
||||
color: @combatActiveCombatantColor;
|
||||
}
|
||||
}
|
||||
&.hidden {
|
||||
color: @bodyFontColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
#combat-controls {
|
||||
border-top: 1px solid @combatControlsBorder;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
** Folders
|
||||
*/
|
||||
.sidebar-tab {
|
||||
.directory-header {
|
||||
.header-search {
|
||||
i.fa-search {
|
||||
color: @folderSearchIconColor;
|
||||
}
|
||||
input {
|
||||
background: @inputBackgroundColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
.subdirectory {
|
||||
background: @folderSubdirectoryBackground;
|
||||
.folder {
|
||||
border-left: 2px solid rgba(@folderSubdirectoryBorder, 0.4);
|
||||
}
|
||||
}
|
||||
.directory-list {
|
||||
li + li {
|
||||
border-top: 1px solid @directoryListItemBorder;
|
||||
}
|
||||
.folder {
|
||||
& > .folder-header {
|
||||
background: @folderHeaderBackground;
|
||||
h3 {
|
||||
background: @folderHeaderBackground;
|
||||
color: @folderHeaderColor;
|
||||
& > i {
|
||||
color: @folderIconColor;
|
||||
}
|
||||
}
|
||||
a {
|
||||
color: @linkSecondaryColor;
|
||||
&:hover {
|
||||
color: @linkColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.collapsed > .folder-header {
|
||||
background: @folderHeaderBackground;
|
||||
}
|
||||
& + .entity {
|
||||
border-top: 1px solid @directoryListItemBorder;
|
||||
}
|
||||
}
|
||||
|
||||
.actor, .item, .journal, .table {
|
||||
background: @entityBackgroundColor;
|
||||
.entity-name {
|
||||
color: @entityNameColor;
|
||||
}
|
||||
&:nth-child(even) {
|
||||
background: rgba(@altRowBackground, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#scenes {
|
||||
.scene {
|
||||
border-top: 1px solid @sceneBorderColor;
|
||||
border-left: 4px solid @sceneBorderColor;
|
||||
&::after {
|
||||
box-shadow: 0 0 20px @sceneBorderColor inset;
|
||||
}
|
||||
h3 {
|
||||
background: @sceneBackgroundColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#playlists {
|
||||
.directory-list {
|
||||
li.playlist {
|
||||
background: @playlistBackgroundColor;
|
||||
.playlist-header {
|
||||
background: @playlistBackgroundColor;
|
||||
color: @colorRed;
|
||||
border-bottom: 2px solid @playlistHeaderBorder;
|
||||
}
|
||||
li.sound {
|
||||
color: @playlistSoundColor;
|
||||
|
||||
}
|
||||
a.sound-control {
|
||||
color: @linkColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
#compendium {
|
||||
.compendium-entity {
|
||||
background: @compendiumEntityBackground !important;
|
||||
h3 {
|
||||
border-bottom: 2px solid @headerBorderColor;
|
||||
}
|
||||
ol.compendium-list {
|
||||
li.compendium-pack {
|
||||
&:nth-child(even) {
|
||||
background: rgba(@altRowBackground, 0.3);
|
||||
}
|
||||
.pack-title {
|
||||
.status-icons {
|
||||
color: @compendiumStatusIcon;
|
||||
}
|
||||
}
|
||||
footer.compendium-footer {
|
||||
color: @bodyFontColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#settings {
|
||||
h2 {
|
||||
color: @headingColor;
|
||||
border-bottom: 2px solid @headerBorderColor;
|
||||
}
|
||||
#game-details, #settings-game, #settings-documentation, #settings-access {
|
||||
color: @bodyFontColor;
|
||||
}
|
||||
}
|
|
@ -1,500 +0,0 @@
|
|||
#sidebar {
|
||||
border: none; //1px solid @colorBlue;
|
||||
}
|
||||
|
||||
#sidebar-tabs {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
background: white;
|
||||
.dropShadow1();
|
||||
& > .collapse {
|
||||
color: @colorRed;
|
||||
}
|
||||
.item {
|
||||
font-size: 16px;
|
||||
}
|
||||
.item.active {
|
||||
color: @colorRed;
|
||||
border: none;
|
||||
border-bottom: 3px solid @colorRed;
|
||||
box-shadow: none;
|
||||
background: none;
|
||||
text-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
/*-----------
|
||||
** Chat Tab
|
||||
-----------*/
|
||||
|
||||
#chat-log {
|
||||
.chat-message {
|
||||
background: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 8px;
|
||||
.dropShadow1();
|
||||
& > header {
|
||||
color: @colorRed;
|
||||
border-bottom: 2px solid @colorBlue;
|
||||
margin-bottom: 4px;
|
||||
span {
|
||||
color: @colorBlack;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
.notification-pip {
|
||||
color: @colorBlue;
|
||||
text-shadow: none;
|
||||
|
||||
}
|
||||
|
||||
.sw5e.chat-card,
|
||||
.midi-qol-item-card {
|
||||
font-size: 13px;
|
||||
|
||||
.card-header {
|
||||
padding: 0;
|
||||
border: none;
|
||||
|
||||
img {
|
||||
flex: 0 0 36px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
line-height: 36px;
|
||||
.russoOne(17px);
|
||||
color: @colorBlack;
|
||||
&:hover {
|
||||
color: @colorBlack;
|
||||
text-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-content {
|
||||
margin: 4px 0;
|
||||
|
||||
h3 {
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
> * {
|
||||
-webkit-user-select: text;
|
||||
-moz-user-select: text;
|
||||
-ms-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
}
|
||||
|
||||
.card-buttons {
|
||||
margin: 4px 0;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
line-height: 28px;
|
||||
text-align: center;
|
||||
border: 1px solid @colorLightGray;
|
||||
}
|
||||
|
||||
button {
|
||||
.openSans(13px, 700);
|
||||
padding: 4px 0;
|
||||
height: auto;
|
||||
line-height: 1.6;
|
||||
margin: 4px 0;
|
||||
background: @colorRed;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
&:hover, &:focus {
|
||||
background-color: lighten(@colorRed, 5);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
padding: 4px 0 0;
|
||||
border-top: 1px solid @colorLightBlue;
|
||||
|
||||
span {
|
||||
border-right: 2px groove #FFF;
|
||||
padding: 0 4px 0 0;
|
||||
font-size: 10px;
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.dice-roll {
|
||||
|
||||
.dice-formula {
|
||||
background: none;
|
||||
border: none;
|
||||
}
|
||||
.dice-total {
|
||||
background: @colorPaleBlue;
|
||||
border: 1px solid @colorBlue;
|
||||
border-radius: 0;
|
||||
padding: 4px 0;
|
||||
box-shadow: 0 0 12px rgba(@colorBlue,.5);
|
||||
&.success {
|
||||
color: inherit;
|
||||
background: #c7d0c0;
|
||||
border: 1px solid #006c00;
|
||||
}
|
||||
&.failure {
|
||||
color: inherit;
|
||||
background: #ffdddd;
|
||||
border: 1px solid #6e0000;
|
||||
}
|
||||
&.critical {
|
||||
color: @colorGreen;
|
||||
background: @colorPaleGreen;
|
||||
box-shadow: 0 0 12px rgba(@colorGreen,.5);
|
||||
}
|
||||
&.fumble {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
}
|
||||
#chat-controls {
|
||||
&.roll-type-select {
|
||||
background: #4f4f4f;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
padding-top: 4px;
|
||||
label {
|
||||
color: @colorBlack;
|
||||
}
|
||||
|
||||
}
|
||||
#chat-form textarea {
|
||||
background: #4f4f4f;
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
/*-----------
|
||||
** Combat Tab
|
||||
-----------*/
|
||||
#combat {
|
||||
#combat-round {
|
||||
color: @colorRed;
|
||||
border-bottom: 2px solid @colorBlue;
|
||||
.encounters {
|
||||
h4 {
|
||||
color: @colorRed;
|
||||
}
|
||||
a {
|
||||
color: @colorGray;
|
||||
&:hover {
|
||||
color: @colorRed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#combat-tracker {
|
||||
//padding-top: 4px;
|
||||
li.combatant {
|
||||
padding: 4px 0;
|
||||
color: @colorBlack;
|
||||
background: none;
|
||||
&:nth-child(even) {
|
||||
background: rgba(@colorPaleBlue, 0.5);
|
||||
}
|
||||
h4 {
|
||||
color: @colorBlack;
|
||||
text-shadow: none;
|
||||
}
|
||||
.roll {
|
||||
background: none;
|
||||
color: @colorGray;
|
||||
&::before {
|
||||
content: "\f6cf";
|
||||
.fontAwesome();
|
||||
font-size: 28px;
|
||||
}
|
||||
&:hover {
|
||||
color: @colorRed;
|
||||
}
|
||||
}
|
||||
.combatant-control {
|
||||
color: @colorLightGray;
|
||||
text-shadow: none;
|
||||
&.active {
|
||||
color: @colorDarkGray;
|
||||
}
|
||||
}
|
||||
.token-resource {
|
||||
color: @colorGray;
|
||||
border-right: 1px solid @colorLightGray;
|
||||
}
|
||||
.initiative {
|
||||
text-shadow: none;
|
||||
}
|
||||
&.active {
|
||||
color: @colorBlue;
|
||||
.initiative, h4 {
|
||||
color: @colorBlue;
|
||||
}
|
||||
}
|
||||
&.hidden {
|
||||
opacity: 0.5;
|
||||
color: @colorBlack;
|
||||
}
|
||||
}
|
||||
}
|
||||
#combat-controls {
|
||||
padding-top: 0;
|
||||
border-top: 1px solid @colorBlue;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
** Folders
|
||||
*/
|
||||
.sidebar-tab {
|
||||
.directory-header {
|
||||
margin-bottom: 4px;
|
||||
.header-search {
|
||||
position: relative;
|
||||
i.fa-search {
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
color: @colorBlue;
|
||||
}
|
||||
input {
|
||||
text-align: left;
|
||||
padding-left: 22px;
|
||||
background: white;
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.subdirectory {
|
||||
border: none;
|
||||
margin-left: 8px;
|
||||
background: white;
|
||||
min-height: 8px;
|
||||
.folder {
|
||||
border-left: 2px solid rgba(@colorBlack, 0.4);
|
||||
}
|
||||
}
|
||||
.directory-list {
|
||||
padding-bottom: 4px;
|
||||
li + li {
|
||||
border-top: 1px solid @colorBlue;
|
||||
}
|
||||
.folder {
|
||||
& > .folder-header {
|
||||
line-height: initial;
|
||||
padding: 0 0 0 8px;
|
||||
position: relative;
|
||||
border: none;
|
||||
background: white;
|
||||
h3 {
|
||||
padding: 8px 4px;
|
||||
background: white;
|
||||
color: @colorBlack;
|
||||
.openSans(13px, 700);
|
||||
line-height: 1.6;
|
||||
& > i {
|
||||
margin-right: 4px;
|
||||
color: @colorBlue;
|
||||
}
|
||||
}
|
||||
a {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 4px;
|
||||
height: 100%;
|
||||
padding: 0 4px;
|
||||
color: @colorLightGray;
|
||||
&:hover {
|
||||
color: @colorRed;
|
||||
}
|
||||
i {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
&.create-folder {
|
||||
right: 28px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.collapsed > .folder-header {
|
||||
background: white;
|
||||
}
|
||||
& + .entity {
|
||||
border-top: 1px solid @colorBlue;
|
||||
}
|
||||
}
|
||||
.directory-item img {
|
||||
flex: 0 0 32px;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
align-self: center;
|
||||
}
|
||||
.actor, .item, .journal, .table {
|
||||
background: white;
|
||||
border: none;
|
||||
.entity-name {
|
||||
.openSans(13px, 700);
|
||||
color: @colorBlack;
|
||||
}
|
||||
&:nth-child(even) {
|
||||
background: rgba(@colorPaleBlue, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#scenes {
|
||||
.subdirectory {
|
||||
border-left: none;
|
||||
}
|
||||
.scene {
|
||||
border: none;
|
||||
border-top: 1px solid @colorBlue;
|
||||
border-left: 4px solid @colorBlue;
|
||||
box-shadow: none;
|
||||
position: relative;
|
||||
height: 128px;
|
||||
//margin-bottom: 4px;
|
||||
& + .scene {
|
||||
margin-top: 4px;
|
||||
}
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 99px;
|
||||
position: absolute;
|
||||
top: 28px;
|
||||
left: 0;
|
||||
box-shadow: 0 0 20px @colorBlue inset;
|
||||
}
|
||||
h3 {
|
||||
.openSans(13px, 700);
|
||||
text-align: left;
|
||||
text-shadow: none;
|
||||
padding: 4px 4px 4px 12px;
|
||||
background: white;
|
||||
line-height: 1.6;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#playlists {
|
||||
.directory-list {
|
||||
padding: 0 8px;
|
||||
li.playlist {
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
margin-bottom: 8px;
|
||||
border-top: inherit;
|
||||
.dropShadow1();
|
||||
.playlist-header {
|
||||
background: white;
|
||||
color: @colorRed;
|
||||
text-decoration: none;
|
||||
border-bottom: 2px solid @colorBlue;
|
||||
}
|
||||
li.sound {
|
||||
border: none;
|
||||
color: @colorBlack;
|
||||
h4 {
|
||||
.openSans(13px, 400);
|
||||
}
|
||||
|
||||
}
|
||||
a.sound-control {
|
||||
color: @colorRed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
#compendium {
|
||||
.compendium-entity {
|
||||
margin: 0 4px;
|
||||
padding: 8px;
|
||||
background: white !important;
|
||||
.dropShadow1();
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
&+ .compendium-entity {
|
||||
margin-top: 4px;
|
||||
}
|
||||
h3 {
|
||||
border: none;
|
||||
color: @colorRed;
|
||||
border-bottom: 2px solid @colorBlue;
|
||||
.russoOne(17px);
|
||||
padding: 0;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
ol.compendium-list {
|
||||
li.compendium-pack {
|
||||
margin: 0;
|
||||
padding: 4px;
|
||||
border: none;
|
||||
&:nth-child(even) {
|
||||
background: rgba(@colorPaleBlue, 0.3);
|
||||
}
|
||||
.pack-title {
|
||||
margin: 0;
|
||||
position: relative;
|
||||
a {
|
||||
.openSans(13px, 700);
|
||||
i {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.status-icons {
|
||||
top: 4px;
|
||||
color: @colorLightGray;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
footer.compendium-footer {
|
||||
color: @colorBlack;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#settings {
|
||||
h2 {
|
||||
color: @colorRed;
|
||||
border: none;
|
||||
border-bottom: 2px solid @colorBlue;
|
||||
margin: 0 8px;
|
||||
padding: 0;
|
||||
}
|
||||
#game-details, #settings-game, #settings-documentation, #settings-access {
|
||||
padding: 0 8px;
|
||||
margin: 0 0 8px;
|
||||
color: @colorBlack;
|
||||
}
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
@import "_variables.less";
|
||||
@import "_variables-dark.less";
|
||||
|
||||
body.dark-theme {
|
||||
.app {
|
||||
background: @primaryBackground;
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
color: @headingColor;
|
||||
}
|
||||
h3 {
|
||||
border-bottom: 2px solid @headerBorderColor;
|
||||
}
|
||||
|
||||
a {
|
||||
color: @linkColor;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
text-shadow: none;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
padding: 4px 8px;
|
||||
background-color: @blockquoteBackground;
|
||||
border: 1px solid @blockquoteBorder;
|
||||
box-shadow: @blockquoteShadow;
|
||||
}
|
||||
|
||||
hr {
|
||||
border-width: 0 0 1px 0;
|
||||
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/sidebar-themes.less";
|
||||
@import "components/foundry-nav-themes.less";
|
||||
@import "components/foundry-app-window-themes.less";
|
||||
@import "components/actor-themes.less";
|
||||
}
|
|
@ -1,194 +0,0 @@
|
|||
/* open-sans-regular - latin */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('./fonts/OpenSans-Regular.ttf');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: url('./fonts/OpenSans-Italic.ttf');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url('./fonts/OpenSans-Bold.ttf');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: url('./fonts/OpenSans-BoldItalic.ttf');
|
||||
}
|
||||
/* russo-one-regular - latin */
|
||||
@font-face {
|
||||
font-family: 'Russo One';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('./fonts/RussoOne.ttf');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Russo One';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: url('./fonts/RussoOne.ttf');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Russo One';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url('./fonts/RussoOne.ttf');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Aurebesh';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('./fonts/Aurebesh.ttf');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Engli-Besh';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('./fonts/EngliBesh-KG3W.ttf');
|
||||
}
|
||||
@import "_variables.less";
|
||||
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
*, *:before, *:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
// ::-webkit-scrollbar {
|
||||
// width: 6px;
|
||||
// height: 6px;
|
||||
// }
|
||||
::-webkit-scrollbar-track {
|
||||
border: 1px solid @colorBlue;
|
||||
border-radius: 4px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
outline: none;
|
||||
border-radius: 4px;
|
||||
background: @colorBlue;
|
||||
border: none;
|
||||
}
|
||||
:root {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: @colorBlue @colorPaleBlue;
|
||||
}
|
||||
|
||||
body {
|
||||
.openSans(13px, 400);
|
||||
background-image: url('./ui/SW5e-logo.svg');
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
h1 {
|
||||
.russoOne(34px);
|
||||
}
|
||||
h2 {
|
||||
.russoOne(27px);
|
||||
}
|
||||
h3 {
|
||||
.russoOne(21px);
|
||||
}
|
||||
h4 {
|
||||
.russoOne(17px);
|
||||
}
|
||||
h5, h6 {
|
||||
.russoOne(13px);
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
&:hover, &:active {
|
||||
text-shadow: none;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.app {
|
||||
border: none;// 1px solid @colorBlue;
|
||||
.dropShadow1();
|
||||
}
|
||||
#pause {
|
||||
img {display: none;}
|
||||
background: none;
|
||||
height: 128px;
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
margin-left: -64px;
|
||||
left: 50%;
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
background: url("ui/pause-inner.svg") no-repeat 50% 50%;
|
||||
animation-name: pause-spin;
|
||||
animation-duration: 10000ms;
|
||||
animation-iteration-count: infinite;
|
||||
animation-timing-function: linear;
|
||||
}
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
margin-left: -64px;
|
||||
left: 50%;
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
background: url("ui/pause-outer.svg") no-repeat 50% 50%;
|
||||
animation-name: pause-spin;
|
||||
animation-duration: 5000ms;
|
||||
animation-iteration-count: infinite;
|
||||
animation-timing-function: linear;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
h3 {
|
||||
border-bottom: 0;
|
||||
line-height: 1;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 256px;
|
||||
margin-left: -128px;
|
||||
margin-top: -13px;
|
||||
text-shadow: 0 0 24px @colorBlue;
|
||||
&::before, &::after {
|
||||
position: absolute;
|
||||
font-family: "Aurebesh", sans-serif;
|
||||
font-size: 13px;
|
||||
color: @colorGray;
|
||||
animation: none;
|
||||
opacity: 0.8;
|
||||
text-shadow: 0 0 8px @colorBlue;
|
||||
}
|
||||
&::before {
|
||||
content: "GAME";
|
||||
top: -13px;
|
||||
left: 42px;
|
||||
}
|
||||
&::after {
|
||||
content: "PAUSED";
|
||||
bottom: -13px;
|
||||
right: 42px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@import "components/forms-global.less";
|
||||
@import "components/sidebar-global.less";
|
||||
@import "components/actor-global.less";
|
||||
|
||||
@keyframes pause-spin {
|
||||
from {
|
||||
transform:rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
@import "_variables.less";
|
||||
@import "_variables-light.less";
|
||||
|
||||
body.light-theme {
|
||||
.app {
|
||||
background: @primaryBackground;
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
color: @headingColor;
|
||||
}
|
||||
h3 {
|
||||
border-bottom: 2px solid @headerBorderColor;
|
||||
}
|
||||
|
||||
a {
|
||||
color: @linkColor;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
text-shadow: none;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
padding: 4px 8px;
|
||||
background-color: @blockquoteBackground;
|
||||
border: 1px solid @blockquoteBorder;
|
||||
box-shadow: @blockquoteShadow;
|
||||
}
|
||||
|
||||
hr {
|
||||
border-width: 0 0 1px 0;
|
||||
border-bottom: 1px solid @hrColor;
|
||||
}
|
||||
@import "components/forms-themes.less";
|
||||
@import "components/sidebar-themes.less";
|
||||
@import "components/foundry-nav-themes.less";
|
||||
@import "components/foundry-app-window-themes.less";
|
||||
@import "components/actor-themes.less";
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
@import "variables.less";
|
||||
|
||||
|
||||
|
||||
a {
|
||||
color: @colorRed;
|
||||
text-decoration: none;
|
||||
&:hover, &:active {
|
||||
text-shadow: none;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.app {
|
||||
background: @sheetBackground;
|
||||
border: none;// 1px solid @colorBlue;
|
||||
.dropShadow1();
|
||||
}
|
||||
|
||||
#context-menu {
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
color: @colorBlack;
|
||||
padding: 0 8px;
|
||||
ol.context-items {
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
border: 1px solid @colorLightGray;
|
||||
.dropShadow2();
|
||||
li.context-item {
|
||||
&:first-child {
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
&:last-child {
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
i {
|
||||
color: @colorBlue;
|
||||
}
|
||||
&:hover {
|
||||
background: @colorRed;
|
||||
color: white;
|
||||
text-shadow: none;
|
||||
cursor: pointer;
|
||||
i {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
& + li {
|
||||
border-top: 1px solid @colorPaleGray;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@import "components/forms.less";
|
||||
@import "components/sidebar.less";
|
65
less/variables.less
Normal file
65
less/variables.less
Normal file
|
@ -0,0 +1,65 @@
|
|||
|
||||
/* Deprecated vars */
|
||||
@borderLight: #CCC;
|
||||
@borderDark: #666;
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* Fonts */
|
||||
/* ----------------------------------------- */
|
||||
|
||||
@font-face {
|
||||
font-family: "Nodesto";
|
||||
src: url("fonts/NodestoCapsCondensed.otf");
|
||||
}
|
||||
.nodesto {
|
||||
font-family: "Nodesto", "Signika", "Palatino Linotype", serif;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* Sheet Styles */
|
||||
/* ----------------------------------------- */
|
||||
|
||||
@colorDark: #191813;
|
||||
@colorFaint: #c9c7b8;
|
||||
@colorBeige: #b5b3a4;
|
||||
@colorTan: #7a7971;
|
||||
@colorOlive: #4b4a44;
|
||||
@colorCrimson: #44191A;
|
||||
@borderGroove: 2px groove #eeede0;
|
||||
@sheetBackground: url("ui/parchment.jpg") repeat;
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* Flexbox */
|
||||
/* ----------------------------------------- */
|
||||
|
||||
.flexrow {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
|
||||
> * {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.flex1 { flex: 1; }
|
||||
.flex2 { flex: 2; }
|
||||
.flex3 { flex: 3; }
|
||||
.flex4 { flex: 4; }
|
||||
}
|
||||
|
||||
.flexcol {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
> * {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.flex1 { flex: 1; }
|
||||
.flex2 { flex: 2; }
|
||||
.flex3 { flex: 3; }
|
||||
.flex4 { flex: 4; }
|
||||
}
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
763
module/actor/sheets/base.js
Normal file
763
module/actor/sheets/base.js
Normal file
|
@ -0,0 +1,763 @@
|
|||
import {TraitSelector} from "../../apps/trait-selector.js";
|
||||
import {ActorSheetFlags} from "../../apps/actor-flags.js";
|
||||
import {SW5E} from '../../config.js';
|
||||
|
||||
/**
|
||||
* Extend the basic ActorSheet class to do all the D&D5e things!
|
||||
* This sheet is an Abstract layer which is not used.
|
||||
*
|
||||
* @type {ActorSheet}
|
||||
*/
|
||||
export 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()
|
||||
};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
static get defaultOptions() {
|
||||
return mergeObject(super.defaultOptions, {
|
||||
scrollY: [
|
||||
".inventory .inventory-list",
|
||||
".features .inventory-list",
|
||||
".powerbook .inventory-list"
|
||||
],
|
||||
tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}]
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @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",
|
||||
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];
|
||||
}
|
||||
|
||||
// Update skill labels
|
||||
for ( let [s, skl] of Object.entries(data.actor.data.skills)) {
|
||||
skl.ability = data.actor.data.abilities[skl.ability].label.substring(0, 3);
|
||||
skl.icon = this._getProficiencyIcon(skl.value);
|
||||
skl.hover = CONFIG.SW5E.proficiencyLevels[skl.value];
|
||||
skl.label = CONFIG.SW5E.skills[s];
|
||||
}
|
||||
|
||||
// Update traits
|
||||
this._prepareTraits(data.actor.data.traits);
|
||||
|
||||
// Prepare owned items
|
||||
this._prepareItems(data);
|
||||
|
||||
// Return data to the sheet
|
||||
return data
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
_prepareTraits(traits) {
|
||||
const map = {
|
||||
"dr": CONFIG.SW5E.damageTypes,
|
||||
"di": CONFIG.SW5E.damageTypes,
|
||||
"dv": CONFIG.SW5E.damageTypes,
|
||||
"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, level={}) => {
|
||||
powerbook[i] = {
|
||||
order: i,
|
||||
label: label,
|
||||
usesSlots: i > 0,
|
||||
canCreate: owner && (i >= 1),
|
||||
canPrepare: (data.actor.type === "character") && (i >= 1),
|
||||
powers: [],
|
||||
uses: useLabels[i] || level.value || 0,
|
||||
slots: useLabels[i] || level.max || 0,
|
||||
override: level.override || 0,
|
||||
dataset: {"type": "power", "level": i},
|
||||
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);
|
||||
|
||||
// Structure the powerbook for every level up to the maximum which has a slot
|
||||
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]);
|
||||
}
|
||||
}
|
||||
if ( levels.pact && levels.pact.max ) {
|
||||
registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]);
|
||||
registerSection("pact", sections.pact, CONFIG.SW5E.powerPreparationModes.pact, levels.pact);
|
||||
}
|
||||
|
||||
// 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}`;
|
||||
|
||||
// Powercasting mode specific headings
|
||||
if ( mode in sections ) {
|
||||
s = sections[mode];
|
||||
if ( !powerbook[s] ){
|
||||
registerSection(sl, s, CONFIG.SW5E.powerPreparationModes[mode], levels[mode]);
|
||||
}
|
||||
}
|
||||
|
||||
// Higher-level power headings
|
||||
else if ( !powerbook[s] ) {
|
||||
registerSection(sl, s, CONFIG.SW5E.powerLevels[s], 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 && 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 h4').click(event => this._onItemSummary(event));
|
||||
|
||||
// Editable Only Listeners
|
||||
if ( this.isEditable ) {
|
||||
|
||||
// Relative updates for numeric fields
|
||||
html.find('input[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-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));
|
||||
}
|
||||
|
||||
// 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 Dragging
|
||||
let handler = ev => this._onDragItemStart(ev);
|
||||
html.find('li.item').each((i, li) => {
|
||||
if ( li.classList.contains("inventory-header") ) return;
|
||||
li.setAttribute("draggable", true);
|
||||
li.addEventListener("dragstart", handler, false);
|
||||
});
|
||||
|
||||
// Item Rolling
|
||||
html.find('.item .item-image').click(event => this._onItemRoll(event));
|
||||
html.find('.item .item-recharge').click(event => this._onItemRecharge(event));
|
||||
}
|
||||
|
||||
// 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 _onDrop (event) {
|
||||
event.preventDefault();
|
||||
|
||||
// Get dropped data
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(event.dataTransfer.getData('text/plain'));
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle a polymorph
|
||||
if (data && (data.type === "Actor")) {
|
||||
if (game.user.isGM || (game.settings.get('sw5e', 'allowPolymorphing') && this.actor.owner)) {
|
||||
return this._onDropPolymorph(event, data);
|
||||
}
|
||||
}
|
||||
|
||||
// Call parent on drop logic
|
||||
return super._onDrop(event);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle dropping an Actor on the sheet to trigger a Polymorph workflow
|
||||
* @param {DragEvent} event The drop event
|
||||
* @param {Object} data The data transfer
|
||||
* @private
|
||||
*/
|
||||
async _onDropPolymorph(event, data) {
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* 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: `New ${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 options = {
|
||||
name: label.getAttribute("for"),
|
||||
title: label.innerText,
|
||||
choices: CONFIG.SW5E[a.dataset.options]
|
||||
};
|
||||
new TraitSelector(this.actor, options).render(true)
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_getHeaderButtons() {
|
||||
let buttons = super._getHeaderButtons();
|
||||
|
||||
// Add button to revert polymorph
|
||||
if ( !this.actor.isPolymorphed || this.actor.isToken ) return buttons;
|
||||
buttons.unshift({
|
||||
label: 'SW5E.PolymorphRestoreTransformation',
|
||||
class: "restore-transformation",
|
||||
icon: "fas fa-backward",
|
||||
onclick: ev => this.actor.revertOriginalForm()
|
||||
});
|
||||
return buttons;
|
||||
}
|
||||
}
|
313
module/actor/sheets/character.js
Normal file
313
module/actor/sheets/character.js
Normal file
|
@ -0,0 +1,313 @@
|
|||
import { ActorSheet5e } from "./base.js";
|
||||
|
||||
/**
|
||||
* An Actor sheet for player character type actors in the D&D5E system.
|
||||
* Extends the base ActorSheet5e class.
|
||||
* @type {ActorSheet5e}
|
||||
*/
|
||||
export class ActorSheet5eCharacter extends ActorSheet5e {
|
||||
|
||||
/**
|
||||
* Define default rendering options for the NPC sheet
|
||||
* @return {Object}
|
||||
*/
|
||||
static get defaultOptions() {
|
||||
return mergeObject(super.defaultOptions, {
|
||||
classes: ["sw5e", "sheet", "actor", "character"],
|
||||
width: 672,
|
||||
height: 736
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Rendering */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Get the correct HTML template path to use for rendering this particular sheet
|
||||
* @type {String}
|
||||
*/
|
||||
get template() {
|
||||
if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/limited-sheet.html";
|
||||
return "systems/sw5e/templates/actors/character-sheet.html";
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Add some extra data when rendering the sheet to reduce the amount of logic required within the template.
|
||||
*/
|
||||
getData() {
|
||||
const sheetData = super.getData();
|
||||
|
||||
// Temporary HP
|
||||
let hp = sheetData.data.attributes.hp;
|
||||
if (hp.temp === 0) delete hp.temp;
|
||||
if (hp.tempmax === 0) delete hp.tempmax;
|
||||
|
||||
// Resources
|
||||
sheetData["resources"] = ["primary", "secondary", "tertiary"].reduce((arr, r) => {
|
||||
const res = sheetData.data.resources[r] || {};
|
||||
res.name = r;
|
||||
res.placeholder = game.i18n.localize("SW5E.Resource"+r.titleCase());
|
||||
if (res && res.value === 0) delete res.value;
|
||||
if (res && res.max === 0) delete res.max;
|
||||
return arr.concat([res]);
|
||||
}, []);
|
||||
|
||||
// Experience Tracking
|
||||
sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking");
|
||||
|
||||
// Return data for rendering
|
||||
return sheetData;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Organize and classify Owned Items for Character sheets
|
||||
* @private
|
||||
*/
|
||||
_prepareItems(data) {
|
||||
|
||||
// Categorize items as inventory, powerbook, features, and classes
|
||||
const inventory = {
|
||||
weapon: { label: "SW5E.ItemTypeWeaponPl", items: [], dataset: {type: "weapon"} },
|
||||
equipment: { label: "SW5E.ItemTypeEquipmentPl", items: [], dataset: {type: "equipment"} },
|
||||
consumable: { label: "SW5E.ItemTypeConsumablePl", items: [], dataset: {type: "consumable"} },
|
||||
tool: { label: "SW5E.ItemTypeToolPl", items: [], dataset: {type: "tool"} },
|
||||
backpack: { label: "SW5E.ItemTypeContainerPl", items: [], dataset: {type: "backpack"} },
|
||||
loot: { label: "SW5E.ItemTypeLootPl", items: [], dataset: {type: "loot"} }
|
||||
};
|
||||
|
||||
// Partition items by category
|
||||
let [items, powers, feats, classes] = data.items.reduce((arr, item) => {
|
||||
|
||||
// Item details
|
||||
item.img = item.img || DEFAULT_TOKEN;
|
||||
item.isStack = item.data.quantity ? item.data.quantity > 1 : false;
|
||||
|
||||
// Item usage
|
||||
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));
|
||||
|
||||
// Item toggle state
|
||||
this._prepareItemToggleState(item);
|
||||
|
||||
// Classify items into types
|
||||
if ( item.type === "power" ) arr[1].push(item);
|
||||
else if ( item.type === "feat" ) arr[2].push(item);
|
||||
else if ( item.type === "class" ) arr[3].push(item);
|
||||
else if ( Object.keys(inventory).includes(item.type ) ) arr[0].push(item);
|
||||
return arr;
|
||||
}, [[], [], [], []]);
|
||||
|
||||
// Apply active item filters
|
||||
items = this._filterItems(items, this._filters.inventory);
|
||||
powers = this._filterItems(powers, this._filters.powerbook);
|
||||
feats = this._filterItems(feats, this._filters.features);
|
||||
|
||||
// Organize Powerbook and count the number of prepared powers (excluding always, at will, etc...)
|
||||
const powerbook = this._preparePowerbook(data, powers);
|
||||
const nPrepared = powers.filter(s => {
|
||||
return (s.data.level > 0) && (s.data.preparation.mode === "prepared") && s.data.preparation.prepared;
|
||||
}).length;
|
||||
|
||||
// Organize Inventory
|
||||
let totalWeight = 0;
|
||||
for ( let i of items ) {
|
||||
i.data.quantity = i.data.quantity || 0;
|
||||
i.data.weight = i.data.weight || 0;
|
||||
i.totalWeight = Math.round(i.data.quantity * i.data.weight * 10) / 10;
|
||||
inventory[i.type].items.push(i);
|
||||
totalWeight += i.totalWeight;
|
||||
}
|
||||
data.data.attributes.encumbrance = this._computeEncumbrance(totalWeight, data);
|
||||
|
||||
// Organize Features
|
||||
const features = {
|
||||
classes: { label: "SW5E.ItemTypeClassPl", items: [], hasActions: false, dataset: {type: "class"}, isClass: true },
|
||||
active: { label: "SW5E.FeatureActive", items: [], hasActions: true, dataset: {type: "feat", "activation.type": "action"} },
|
||||
passive: { label: "SW5E.FeaturePassive", items: [], hasActions: false, dataset: {type: "feat"} }
|
||||
};
|
||||
for ( let f of feats ) {
|
||||
if ( f.data.activation.type ) features.active.items.push(f);
|
||||
else features.passive.items.push(f);
|
||||
}
|
||||
classes.sort((a, b) => b.levels - a.levels);
|
||||
features.classes.items = classes;
|
||||
|
||||
// Assign and return
|
||||
data.inventory = Object.values(inventory);
|
||||
data.powerbook = powerbook;
|
||||
data.preparedPowers = nPrepared;
|
||||
data.features = Object.values(features);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A helper method to establish the displayed preparation state for an item
|
||||
* @param {Item} item
|
||||
* @private
|
||||
*/
|
||||
_prepareItemToggleState(item) {
|
||||
if (item.type === "power") {
|
||||
const isAlways = getProperty(item.data, "preparation.mode") === "always";
|
||||
const isPrepared = getProperty(item.data, "preparation.prepared");
|
||||
item.toggleClass = isPrepared ? "active" : "";
|
||||
if ( isAlways ) item.toggleClass = "fixed";
|
||||
if ( isAlways ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.always;
|
||||
else if ( isPrepared ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.prepared;
|
||||
else item.toggleTitle = game.i18n.localize("SW5E.PowerUnprepared");
|
||||
}
|
||||
else {
|
||||
const isActive = getProperty(item.data, "equipped");
|
||||
item.toggleClass = isActive ? "active" : "";
|
||||
item.toggleTitle = game.i18n.localize(isActive ? "SW5E.Equipped" : "SW5E.Unequipped");
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Compute the level and percentage of encumbrance for an Actor.
|
||||
*
|
||||
* Optionally include the weight of carried currency across all denominations by applying the standard rule
|
||||
* from the PHB pg. 143
|
||||
*
|
||||
* @param {Number} totalWeight The cumulative item weight from inventory items
|
||||
* @param {Object} actorData The data object for the Actor being rendered
|
||||
* @return {Object} An object describing the character's encumbrance level
|
||||
* @private
|
||||
*/
|
||||
_computeEncumbrance(totalWeight, actorData) {
|
||||
|
||||
// Encumbrance classes
|
||||
let mod = {
|
||||
tiny: 0.5,
|
||||
sm: 1,
|
||||
med: 1,
|
||||
lg: 2,
|
||||
huge: 4,
|
||||
grg: 8
|
||||
}[actorData.data.traits.size] || 1;
|
||||
|
||||
// Apply Powerful Build feat
|
||||
if ( this.actor.getFlag("sw5e", "powerfulBuild") ) mod = Math.min(mod * 2, 8);
|
||||
|
||||
// Add Currency Weight
|
||||
if ( game.settings.get("sw5e", "currencyWeight") ) {
|
||||
const currency = actorData.data.currency;
|
||||
const numCoins = Object.values(currency).reduce((val, denom) => val += denom, 0);
|
||||
totalWeight += numCoins / CONFIG.SW5E.encumbrance.currencyPerWeight;
|
||||
}
|
||||
|
||||
// Compute Encumbrance percentage
|
||||
const enc = {
|
||||
max: actorData.data.abilities.str.value * CONFIG.SW5E.encumbrance.strMultiplier * mod,
|
||||
value: Math.round(totalWeight * 10) / 10,
|
||||
};
|
||||
enc.pct = Math.min(enc.value * 100 / enc.max, 99);
|
||||
enc.encumbered = enc.pct > (2/3);
|
||||
return enc;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Event Listeners and Handlers
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Activate event listeners using the prepared sheet HTML
|
||||
* @param html {HTML} The prepared HTML object ready to be rendered into the DOM
|
||||
*/
|
||||
activateListeners(html) {
|
||||
super.activateListeners(html);
|
||||
if ( !this.options.editable ) return;
|
||||
|
||||
// Inventory Functions
|
||||
html.find(".currency-convert").click(this._onConvertCurrency.bind(this));
|
||||
|
||||
// Item State Toggling
|
||||
html.find('.item-toggle').click(this._onToggleItem.bind(this));
|
||||
|
||||
// Short and Long Rest
|
||||
html.find('.short-rest').click(this._onShortRest.bind(this));
|
||||
html.find('.long-rest').click(this._onLongRest.bind(this));
|
||||
|
||||
// Death saving throws
|
||||
html.find('.death-save').click(this._onDeathSave.bind(this));
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle rolling a death saving throw for the Character
|
||||
* @param {MouseEvent} event The originating click event
|
||||
* @private
|
||||
*/
|
||||
_onDeathSave(event) {
|
||||
event.preventDefault();
|
||||
return this.actor.rollDeathSave({event: event});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
|
||||
/**
|
||||
* Handle toggling the state of an Owned Item within the Actor
|
||||
* @param {Event} event The triggering click event
|
||||
* @private
|
||||
*/
|
||||
_onToggleItem(event) {
|
||||
event.preventDefault();
|
||||
const itemId = event.currentTarget.closest(".item").dataset.itemId;
|
||||
const item = this.actor.getOwnedItem(itemId);
|
||||
const attr = item.data.type === "power" ? "data.preparation.prepared" : "data.equipped";
|
||||
return item.update({[attr]: !getProperty(item.data, attr)});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Take a short rest, calling the relevant function on the Actor instance
|
||||
* @param {Event} event The triggering click event
|
||||
* @private
|
||||
*/
|
||||
async _onShortRest(event) {
|
||||
event.preventDefault();
|
||||
await this._onSubmit(event);
|
||||
return this.actor.shortRest();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Take a long rest, calling the relevant function on the Actor instance
|
||||
* @param {Event} event The triggering click event
|
||||
* @private
|
||||
*/
|
||||
async _onLongRest(event) {
|
||||
event.preventDefault();
|
||||
await this._onSubmit(event);
|
||||
return this.actor.longRest();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* 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()
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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": "∞"
|
||||
};
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
|
@ -1,752 +0,0 @@
|
|||
import ActorSheet5e from "./base.js";
|
||||
import Actor5e from "../../entity.js";
|
||||
|
||||
/**
|
||||
* An Actor sheet for player character type actors in the SW5E system.
|
||||
* Extends the base ActorSheet5e class.
|
||||
* @type {ActorSheet5e}
|
||||
*/
|
||||
export default class ActorSheet5eCharacterNew extends ActorSheet5e {
|
||||
get template() {
|
||||
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";
|
||||
}
|
||||
/**
|
||||
* Define default rendering options for the NPC sheet
|
||||
* @return {Object}
|
||||
*/
|
||||
static get defaultOptions() {
|
||||
return mergeObject(super.defaultOptions, {
|
||||
classes: ["swalt", "sw5e", "sheet", "actor", "character"],
|
||||
blockFavTab: true,
|
||||
subTabs: null,
|
||||
width: 800,
|
||||
tabs: [
|
||||
{
|
||||
navSelector: ".root-tabs",
|
||||
contentSelector: ".sheet-body",
|
||||
initial: "attributes"
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Add some extra data when rendering the sheet to reduce the amount of logic required within the template.
|
||||
*/
|
||||
getData() {
|
||||
const sheetData = super.getData();
|
||||
|
||||
// Temporary HP
|
||||
let hp = sheetData.data.attributes.hp;
|
||||
if (hp.temp === 0) delete hp.temp;
|
||||
if (hp.tempmax === 0) delete hp.tempmax;
|
||||
|
||||
// Resources
|
||||
sheetData["resources"] = ["primary", "secondary", "tertiary"].reduce((arr, r) => {
|
||||
const res = sheetData.data.resources[r] || {};
|
||||
res.name = r;
|
||||
res.placeholder = game.i18n.localize("SW5E.Resource" + r.titleCase());
|
||||
if (res && res.value === 0) delete res.value;
|
||||
if (res && res.max === 0) delete res.max;
|
||||
return arr.concat([res]);
|
||||
}, []);
|
||||
|
||||
// Experience Tracking
|
||||
sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking");
|
||||
sheetData["classLabels"] = this.actor.itemTypes.class.map((c) => c.name).join(", ");
|
||||
sheetData["multiclassLabels"] = this.actor.itemTypes.class
|
||||
.map((c) => {
|
||||
return [c.data.data.archetype, c.name, c.data.data.levels].filterJoin(" ");
|
||||
})
|
||||
.join(", ");
|
||||
|
||||
// Return data for rendering
|
||||
return sheetData;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Organize and classify Owned Items for Character sheets
|
||||
* @private
|
||||
*/
|
||||
_prepareItems(data) {
|
||||
// Categorize items as inventory, powerbook, features, and classes
|
||||
const inventory = {
|
||||
weapon: {label: "SW5E.ItemTypeWeaponPl", items: [], dataset: {type: "weapon"}},
|
||||
equipment: {label: "SW5E.ItemTypeEquipmentPl", items: [], dataset: {type: "equipment"}},
|
||||
consumable: {label: "SW5E.ItemTypeConsumablePl", items: [], dataset: {type: "consumable"}},
|
||||
tool: {label: "SW5E.ItemTypeToolPl", items: [], dataset: {type: "tool"}},
|
||||
backpack: {label: "SW5E.ItemTypeContainerPl", items: [], dataset: {type: "backpack"}},
|
||||
loot: {label: "SW5E.ItemTypeLootPl", items: [], dataset: {type: "loot"}}
|
||||
};
|
||||
|
||||
// Partition items by category
|
||||
let [
|
||||
items,
|
||||
forcepowers,
|
||||
techpowers,
|
||||
feats,
|
||||
classes,
|
||||
deployments,
|
||||
deploymentfeatures,
|
||||
ventures,
|
||||
species,
|
||||
archetypes,
|
||||
classfeatures,
|
||||
backgrounds,
|
||||
fightingstyles,
|
||||
fightingmasteries,
|
||||
lightsaberforms
|
||||
] = data.items.reduce(
|
||||
(arr, item) => {
|
||||
// Item details
|
||||
item.img = item.img || CONST.DEFAULT_TOKEN;
|
||||
item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1;
|
||||
item.attunement = {
|
||||
[CONFIG.SW5E.attunementTypes.REQUIRED]: {
|
||||
icon: "fa-sun",
|
||||
cls: "not-attuned",
|
||||
title: "SW5E.AttunementRequired"
|
||||
},
|
||||
[CONFIG.SW5E.attunementTypes.ATTUNED]: {
|
||||
icon: "fa-sun",
|
||||
cls: "attuned",
|
||||
title: "SW5E.AttunementAttuned"
|
||||
}
|
||||
}[item.data.attunement];
|
||||
|
||||
// Item usage
|
||||
item.hasUses = item.data.uses && item.data.uses.max > 0;
|
||||
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);
|
||||
|
||||
// Item toggle state
|
||||
this._prepareItemToggleState(item);
|
||||
|
||||
// Primary Class
|
||||
if (item.type === "class")
|
||||
item.isOriginalClass = item._id === this.actor.data.data.details.originalClass;
|
||||
|
||||
// Classify items into types
|
||||
if (item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school)) arr[1].push(item);
|
||||
else if (item.type === "power" && ["tec"].includes(item.data.school)) arr[2].push(item);
|
||||
else if (item.type === "feat") arr[3].push(item);
|
||||
else if (item.type === "class") arr[4].push(item);
|
||||
else if (item.type === "deployment") arr[5].push(item);
|
||||
else if (item.type === "deploymentfeature") arr[6].push(item);
|
||||
else if (item.type === "venture") arr[7].push(item);
|
||||
else if (item.type === "species") arr[8].push(item);
|
||||
else if (item.type === "archetype") arr[9].push(item);
|
||||
else if (item.type === "classfeature") 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);
|
||||
return arr;
|
||||
},
|
||||
[[], [], [], [], [], [], [], [], [], [], [], [], [], [], []]
|
||||
);
|
||||
|
||||
// Apply active item filters
|
||||
items = this._filterItems(items, this._filters.inventory);
|
||||
forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook);
|
||||
techpowers = this._filterItems(techpowers, this._filters.techPowerbook);
|
||||
feats = this._filterItems(feats, this._filters.features);
|
||||
|
||||
// Organize items
|
||||
for (let i of items) {
|
||||
i.data.quantity = i.data.quantity || 0;
|
||||
i.data.weight = i.data.weight || 0;
|
||||
i.totalWeight = (i.data.quantity * i.data.weight).toNearest(0.1);
|
||||
inventory[i.type].items.push(i);
|
||||
}
|
||||
|
||||
// Organize Powerbook and count the number of prepared powers (excluding always, at will, etc...)
|
||||
const forcePowerbook = this._preparePowerbook(data, forcepowers, "uni");
|
||||
const techPowerbook = this._preparePowerbook(data, techpowers, "tec");
|
||||
|
||||
// Organize Features
|
||||
const features = {
|
||||
classes: {
|
||||
label: "SW5E.ItemTypeClassPl",
|
||||
items: [],
|
||||
hasActions: false,
|
||||
dataset: {type: "class"},
|
||||
isClass: true
|
||||
},
|
||||
classfeatures: {
|
||||
label: "SW5E.ItemTypeClassFeats",
|
||||
items: [],
|
||||
hasActions: true,
|
||||
dataset: {type: "classfeature"},
|
||||
isClassfeature: true
|
||||
},
|
||||
archetype: {
|
||||
label: "SW5E.ItemTypeArchetype",
|
||||
items: [],
|
||||
hasActions: false,
|
||||
dataset: {type: "archetype"},
|
||||
isArchetype: true
|
||||
},
|
||||
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"}}
|
||||
};
|
||||
for (let f of feats) {
|
||||
if (f.data.activation.type) features.active.items.push(f);
|
||||
else features.passive.items.push(f);
|
||||
}
|
||||
classes.sort((a, b) => b.data.levels - a.data.levels);
|
||||
features.classes.items = classes;
|
||||
features.classfeatures.items = classfeatures;
|
||||
features.archetype.items = archetypes;
|
||||
features.deployments.items = deployments;
|
||||
features.deploymentfeatures.items = deploymentfeatures;
|
||||
features.ventures.items = ventures;
|
||||
features.species.items = species;
|
||||
features.background.items = backgrounds;
|
||||
features.fightingstyles.items = fightingstyles;
|
||||
features.fightingmasteries.items = fightingmasteries;
|
||||
features.lightsaberforms.items = lightsaberforms;
|
||||
|
||||
// Assign and return
|
||||
data.inventory = Object.values(inventory);
|
||||
data.forcePowerbook = forcePowerbook;
|
||||
data.techPowerbook = techPowerbook;
|
||||
data.features = Object.values(features);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A helper method to establish the displayed preparation state for an item
|
||||
* @param {Item} item
|
||||
* @private
|
||||
*/
|
||||
_prepareItemToggleState(item) {
|
||||
if (item.type === "power") {
|
||||
const isAlways = getProperty(item.data, "preparation.mode") === "always";
|
||||
const isPrepared = getProperty(item.data, "preparation.prepared");
|
||||
item.toggleClass = isPrepared ? "active" : "";
|
||||
if (isAlways) item.toggleClass = "fixed";
|
||||
if (isAlways) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.always;
|
||||
else if (isPrepared) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.prepared;
|
||||
else item.toggleTitle = game.i18n.localize("SW5E.PowerUnprepared");
|
||||
} else {
|
||||
const isActive = getProperty(item.data, "equipped");
|
||||
item.toggleClass = isActive ? "active" : "";
|
||||
item.toggleTitle = game.i18n.localize(isActive ? "SW5E.Equipped" : "SW5E.Unequipped");
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Event Listeners and Handlers
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Activate event listeners using the prepared sheet HTML
|
||||
* @param html {jQuery} The prepared HTML object ready to be rendered into the DOM
|
||||
*/
|
||||
activateListeners(html) {
|
||||
super.activateListeners(html);
|
||||
if (!this.isEditable) return;
|
||||
|
||||
// Inventory Functions
|
||||
// html.find(".currency-convert").click(this._onConvertCurrency.bind(this));
|
||||
|
||||
// Item State Toggling
|
||||
html.find(".item-toggle").click(this._onToggleItem.bind(this));
|
||||
|
||||
// Short and Long Rest
|
||||
html.find(".short-rest").click(this._onShortRest.bind(this));
|
||||
html.find(".long-rest").click(this._onLongRest.bind(this));
|
||||
|
||||
// Rollable sheet actions
|
||||
html.find(".rollable[data-action]").click(this._onSheetAction.bind(this));
|
||||
|
||||
// Send Languages to Chat onClick
|
||||
html.find('[data-options="share-languages"]').click((event) => {
|
||||
event.preventDefault();
|
||||
let langs = this.actor.data.data.traits.languages.value
|
||||
.map((l) => CONFIG.SW5E.languages[l] || l)
|
||||
.join(", ");
|
||||
let custom = this.actor.data.data.traits.languages.custom;
|
||||
if (custom) langs += ", " + custom.replace(/;/g, ",");
|
||||
let content = `
|
||||
<div class="sw5e chat-card item-card" data-acor-id="${this.actor.data._id}">
|
||||
<header class="card-header flexrow">
|
||||
<img src="${this.actor.data.token.img}" title="" width="36" height="36" style="border: none;"/>
|
||||
<h3>Known Languages</h3>
|
||||
</header>
|
||||
<div class="card-content">${langs}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Send to Chat
|
||||
let rollBlind = false;
|
||||
let rollMode = game.settings.get("core", "rollMode");
|
||||
if (rollMode === "blindroll") rollBlind = true;
|
||||
let data = {
|
||||
user: game.user.data._id,
|
||||
content: content,
|
||||
blind: rollBlind,
|
||||
speaker: {
|
||||
actor: this.actor.data._id,
|
||||
token: this.actor.token,
|
||||
alias: this.actor.name
|
||||
},
|
||||
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
|
||||
html.find(".item-delete").off("click");
|
||||
html.find(".item-delete").click((event) => {
|
||||
let li = $(event.currentTarget).parents(".item");
|
||||
let itemId = li.attr("data-item-id");
|
||||
let item = this.actor.items.get(itemId);
|
||||
new Dialog({
|
||||
title: `Deleting ${item.data.name}`,
|
||||
content: `<p>Are you sure you want to delete ${item.data.name}?</p>`,
|
||||
buttons: {
|
||||
Yes: {
|
||||
icon: '<i class="fa fa-check"></i>',
|
||||
label: "Yes",
|
||||
callback: (dlg) => {
|
||||
this.actor.deleteOwnedItem(itemId);
|
||||
}
|
||||
},
|
||||
cancel: {
|
||||
icon: '<i class="fas fa-times"></i>',
|
||||
label: "No"
|
||||
}
|
||||
},
|
||||
default: "cancel"
|
||||
}).render(true);
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle mouse click events for character sheet actions
|
||||
* @param {MouseEvent} event The originating click event
|
||||
* @private
|
||||
*/
|
||||
_onSheetAction(event) {
|
||||
event.preventDefault();
|
||||
const button = event.currentTarget;
|
||||
switch (button.dataset.action) {
|
||||
case "rollDeathSave":
|
||||
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
|
||||
* @param {Event} event The triggering click event
|
||||
* @private
|
||||
*/
|
||||
_onToggleItem(event) {
|
||||
event.preventDefault();
|
||||
const itemId = event.currentTarget.closest(".item").dataset.itemId;
|
||||
const item = this.actor.items.get(itemId);
|
||||
const attr = item.data.type === "power" ? "data.preparation.prepared" : "data.equipped";
|
||||
return item.update({[attr]: !getProperty(item.data, attr)});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Take a short rest, calling the relevant function on the Actor instance
|
||||
* @param {Event} event The triggering click event
|
||||
* @private
|
||||
*/
|
||||
async _onShortRest(event) {
|
||||
event.preventDefault();
|
||||
await this._onSubmit(event);
|
||||
return this.actor.shortRest();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Take a long rest, calling the relevant function on the Actor instance
|
||||
* @param {Event} event The triggering click event
|
||||
* @private
|
||||
*/
|
||||
async _onLongRest(event) {
|
||||
event.preventDefault();
|
||||
await this._onSubmit(event);
|
||||
return this.actor.longRest();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
async _onDropItemCreate(itemData) {
|
||||
// Increment the number of class levels of a character instead of creating a new item
|
||||
if (itemData.type === "class") {
|
||||
const cls = this.actor.itemTypes.class.find((c) => c.name === itemData.name);
|
||||
let priorLevel = cls?.data.data.levels ?? 0;
|
||||
if (!!cls) {
|
||||
const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level);
|
||||
if (next > priorLevel) {
|
||||
itemData.levels = next;
|
||||
return cls.update({"data.levels": next});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Increment the number of deployment ranks of a character instead of creating a new item
|
||||
// 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) {
|
||||
// Thisfunction is adapted for the SwaltSheet from the Favorites Item
|
||||
// Tab Module created for Foundry VTT - by Felix Müller (Felix#6196 on Discord).
|
||||
// It is licensed under a Creative Commons Attribution 4.0 International License
|
||||
// and can be found at https://github.com/syl3r86/favtab.
|
||||
let favItems = [];
|
||||
let favFeats = [];
|
||||
let favPowers = {
|
||||
0: {
|
||||
isCantrip: true,
|
||||
powers: []
|
||||
},
|
||||
1: {
|
||||
powers: [],
|
||||
value: data.actor.data.powers.power1.value,
|
||||
max: data.actor.data.powers.power1.max
|
||||
},
|
||||
2: {
|
||||
powers: [],
|
||||
value: data.actor.data.powers.power2.value,
|
||||
max: data.actor.data.powers.power2.max
|
||||
},
|
||||
3: {
|
||||
powers: [],
|
||||
value: data.actor.data.powers.power3.value,
|
||||
max: data.actor.data.powers.power3.max
|
||||
},
|
||||
4: {
|
||||
powers: [],
|
||||
value: data.actor.data.powers.power4.value,
|
||||
max: data.actor.data.powers.power4.max
|
||||
},
|
||||
5: {
|
||||
powers: [],
|
||||
value: data.actor.data.powers.power5.value,
|
||||
max: data.actor.data.powers.power5.max
|
||||
},
|
||||
6: {
|
||||
powers: [],
|
||||
value: data.actor.data.powers.power6.value,
|
||||
max: data.actor.data.powers.power6.max
|
||||
},
|
||||
7: {
|
||||
powers: [],
|
||||
value: data.actor.data.powers.power7.value,
|
||||
max: data.actor.data.powers.power7.max
|
||||
},
|
||||
8: {
|
||||
powers: [],
|
||||
value: data.actor.data.powers.power8.value,
|
||||
max: data.actor.data.powers.power8.max
|
||||
},
|
||||
9: {
|
||||
powers: [],
|
||||
value: data.actor.data.powers.power9.value,
|
||||
max: data.actor.data.powers.power9.max
|
||||
}
|
||||
};
|
||||
|
||||
let powerCount = 0;
|
||||
let items = data.actor.items;
|
||||
for (let item of items) {
|
||||
if (item.type == "class") continue;
|
||||
if (item.flags.favtab === undefined || item.flags.favtab.isFavourite === undefined) {
|
||||
item.flags.favtab = {
|
||||
isFavourite: false
|
||||
};
|
||||
}
|
||||
let isFav = item.flags.favtab.isFavourite;
|
||||
if (app.options.editable) {
|
||||
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>`
|
||||
);
|
||||
favBtn.click((ev) => {
|
||||
app.actor.items.get(item.data._id).update({
|
||||
"flags.favtab.isFavourite": !item.flags.favtab.isFavourite
|
||||
});
|
||||
});
|
||||
html.find(`.item[data-item-id="${item.data._id}"]`).find(".item-controls").prepend(favBtn);
|
||||
}
|
||||
|
||||
if (isFav) {
|
||||
item.powerComps = "";
|
||||
if (item.data.components) {
|
||||
let comps = item.data.components;
|
||||
let v = comps.vocal ? "V" : "";
|
||||
let s = comps.somatic ? "S" : "";
|
||||
let m = comps.material ? "M" : "";
|
||||
let c = !!comps.concentration;
|
||||
let r = !!comps.ritual;
|
||||
item.powerComps = `${v}${s}${m}`;
|
||||
item.powerCon = c;
|
||||
item.powerRit = r;
|
||||
}
|
||||
|
||||
item.editable = app.options.editable;
|
||||
switch (item.type) {
|
||||
case "feat":
|
||||
if (item.flags.favtab.sort === undefined) {
|
||||
item.flags.favtab.sort = (favFeats.count + 1) * 100000; // initial sort key if not present
|
||||
}
|
||||
favFeats.push(item);
|
||||
break;
|
||||
case "power":
|
||||
if (item.data.preparation.mode) {
|
||||
item.powerPrepMode = ` (${CONFIG.SW5E.powerPreparationModes[item.data.preparation.mode]})`;
|
||||
}
|
||||
if (item.data.level) {
|
||||
favPowers[item.data.level].powers.push(item);
|
||||
} else {
|
||||
favPowers[0].powers.push(item);
|
||||
}
|
||||
powerCount++;
|
||||
break;
|
||||
default:
|
||||
if (item.flags.favtab.sort === undefined) {
|
||||
item.flags.favtab.sort = (favItems.count + 1) * 100000; // initial sort key if not present
|
||||
}
|
||||
favItems.push(item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Alter core CSS to fit new button
|
||||
// if (app.options.editable) {
|
||||
// html.find('.powerbook .item-controls').css('flex', '0 0 88px');
|
||||
// html.find('.inventory .item-controls, .features .item-controls').css('flex', '0 0 90px');
|
||||
// html.find('.favourite .item-controls').css('flex', '0 0 22px');
|
||||
// }
|
||||
|
||||
let tabContainer = html.find(".favtabtarget");
|
||||
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.favPowers = powerCount > 0 ? favPowers : false;
|
||||
data.editable = app.options.editable;
|
||||
|
||||
await loadTemplates(["systems/sw5e/templates/actors/newActor/item.hbs"]);
|
||||
let favtabHtml = $(await renderTemplate("systems/sw5e/templates/actors/newActor/template.hbs", data));
|
||||
favtabHtml.find(".item-name h4").click((event) => app._onItemSummary(event));
|
||||
|
||||
if (app.options.editable) {
|
||||
favtabHtml.find(".item-image").click((ev) => app._onItemRoll(ev));
|
||||
let handler = (ev) => app._onDragStart(ev);
|
||||
favtabHtml.find(".item").each((i, li) => {
|
||||
if (li.classList.contains("inventory-header")) return;
|
||||
li.setAttribute("draggable", true);
|
||||
li.addEventListener("dragstart", handler, false);
|
||||
});
|
||||
//favtabHtml.find('.item-toggle').click(event => app._onToggleItem(event));
|
||||
favtabHtml.find(".item-edit").click((ev) => {
|
||||
let itemId = $(ev.target).parents(".item")[0].dataset.itemId;
|
||||
app.actor.items.get(itemId).sheet.render(true);
|
||||
});
|
||||
favtabHtml.find(".item-fav").click((ev) => {
|
||||
let itemId = $(ev.target).parents(".item")[0].dataset.itemId;
|
||||
let val = !app.actor.items.get(itemId).data.flags.favtab.isFavourite;
|
||||
app.actor.items.get(itemId).update({
|
||||
"flags.favtab.isFavourite": val
|
||||
});
|
||||
});
|
||||
|
||||
// Sorting
|
||||
favtabHtml.find(".item").on("drop", (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
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) return;
|
||||
let list = null;
|
||||
if (dropData.data.type === "feat") list = favFeats;
|
||||
else list = favItems;
|
||||
let dragSource = list.find((i) => i.data._id === dropData.data._id);
|
||||
let siblings = list.filter((i) => i.data._id !== dropData.data._id);
|
||||
let targetId = ev.target.closest(".item").dataset.itemId;
|
||||
let dragTarget = siblings.find((s) => s.data._id === targetId);
|
||||
|
||||
if (dragTarget === undefined) return;
|
||||
const sortUpdates = SortingHelpers.performIntegerSort(dragSource, {
|
||||
target: dragTarget,
|
||||
siblings: siblings,
|
||||
sortKey: "flags.favtab.sort"
|
||||
});
|
||||
const updateData = sortUpdates.map((u) => {
|
||||
const update = u.update;
|
||||
update._id = u.target.data._id;
|
||||
return update;
|
||||
});
|
||||
app.actor.updateEmbeddedEntity("OwnedItem", updateData);
|
||||
});
|
||||
}
|
||||
tabContainer.append(favtabHtml);
|
||||
// if(app.options.editable) {
|
||||
// let handler = ev => app._onDragItemStart(ev);
|
||||
// tabContainer.find('.item').each((i, li) => {
|
||||
// if (li.classList.contains("inventory-header")) return;
|
||||
// li.setAttribute("draggable", true);
|
||||
// li.addEventListener("dragstart", handler, false);
|
||||
// });
|
||||
//}
|
||||
// try {
|
||||
// if (game.modules.get("betterrolls5e") && game.modules.get("betterrolls5e").active) BetterRolls.addItemContent(app.object, favtabHtml, ".item .item-name h4", ".item-properties", ".item > .rollable div");
|
||||
// }
|
||||
// catch (err) {
|
||||
// // Better Rolls not found!
|
||||
// }
|
||||
Hooks.callAll("renderedSwaltSheet", app, html, data);
|
||||
}
|
||||
async function addSubTabs(app, html, data) {
|
||||
if (data.options.subTabs == null) {
|
||||
//let subTabs = []; //{subgroup: '', target: '', active: false}
|
||||
data.options.subTabs = {};
|
||||
html.find("[data-subgroup-selection] [data-subgroup]").each((idx, el) => {
|
||||
let subgroup = el.getAttribute("data-subgroup");
|
||||
let target = el.getAttribute("data-target");
|
||||
let targetObj = {target: target, active: el.classList.contains("active")};
|
||||
if (data.options.subTabs.hasOwnProperty(subgroup)) {
|
||||
data.options.subTabs[subgroup].push(targetObj);
|
||||
} else {
|
||||
data.options.subTabs[subgroup] = [];
|
||||
data.options.subTabs[subgroup].push(targetObj);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const group in data.options.subTabs) {
|
||||
data.options.subTabs[group].forEach((tab) => {
|
||||
if (tab.active) {
|
||||
html.find(`[data-subgroup=${group}][data-target=${tab.target}]`).addClass("active");
|
||||
} else {
|
||||
html.find(`[data-subgroup=${group}][data-target=${tab.target}]`).removeClass("active");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
html.find("[data-subgroup-selection]")
|
||||
.children()
|
||||
.on("click", (event) => {
|
||||
let subgroup = event.target.closest("[data-subgroup]").getAttribute("data-subgroup");
|
||||
let target = event.target.closest("[data-target]").getAttribute("data-target");
|
||||
html.find(`[data-subgroup=${subgroup}]`).removeClass("active");
|
||||
html.find(`[data-subgroup=${subgroup}][data-target=${target}]`).addClass("active");
|
||||
let tabId = data.options.subTabs[subgroup].find((tab) => {
|
||||
return tab.target == target;
|
||||
});
|
||||
data.options.subTabs[subgroup].map((el) => {
|
||||
el.active = el.target == target;
|
||||
return el;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Hooks.on("renderActorSheet5eCharacterNew", (app, html, data) => {
|
||||
addFavorites(app, html, data);
|
||||
addSubTabs(app, html, data);
|
||||
});
|
|
@ -1,159 +0,0 @@
|
|||
import ActorSheet5e from "./base.js";
|
||||
|
||||
/**
|
||||
* An Actor sheet for NPC type characters in the SW5E system.
|
||||
* Extends the base ActorSheet5e class.
|
||||
* @extends {ActorSheet5e}
|
||||
*/
|
||||
export default class ActorSheet5eNPCNew extends ActorSheet5e {
|
||||
/** @override */
|
||||
get template() {
|
||||
if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/newActor/limited-sheet.html";
|
||||
return `systems/sw5e/templates/actors/newActor/npc-sheet.html`;
|
||||
}
|
||||
/** @override */
|
||||
static get defaultOptions() {
|
||||
return mergeObject(super.defaultOptions, {
|
||||
classes: ["sw5e", "sheet", "actor", "npc"],
|
||||
width: 800,
|
||||
tabs: [
|
||||
{
|
||||
navSelector: ".root-tabs",
|
||||
contentSelector: ".sheet-body",
|
||||
initial: "attributes"
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
static unsupportedItemTypes = new Set(["class"]);
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Organize Owned Items for rendering the NPC sheet
|
||||
* @private
|
||||
*/
|
||||
_prepareItems(data) {
|
||||
// Categorize Items as Features and Powers
|
||||
const features = {
|
||||
weapons: {
|
||||
label: game.i18n.localize("SW5E.AttackPl"),
|
||||
items: [],
|
||||
hasActions: true,
|
||||
dataset: {"type": "weapon", "weapon-type": "natural"}
|
||||
},
|
||||
actions: {
|
||||
label: game.i18n.localize("SW5E.ActionPl"),
|
||||
items: [],
|
||||
hasActions: true,
|
||||
dataset: {"type": "feat", "activation.type": "action"}
|
||||
},
|
||||
passive: {label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"}},
|
||||
equipment: {label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}}
|
||||
};
|
||||
|
||||
// Start by classifying items into groups for rendering
|
||||
let [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 features.equipment.items.push(item);
|
||||
}
|
||||
|
||||
// Assign and return
|
||||
data.features = Object.values(features);
|
||||
data.forcePowerbook = forcePowerbook;
|
||||
data.techPowerbook = techPowerbook;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
getData(options) {
|
||||
const data = super.getData(options);
|
||||
|
||||
// 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;
|
||||
|
||||
// Creature Type
|
||||
data.labels["type"] = this.actor.labels.creatureType;
|
||||
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});
|
||||
}
|
||||
}
|
|
@ -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});
|
||||
}
|
||||
}
|
|
@ -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});
|
||||
}
|
||||
}
|
155
module/actor/sheets/npc.js
Normal file
155
module/actor/sheets/npc.js
Normal file
|
@ -0,0 +1,155 @@
|
|||
import { ActorSheet5e } from "../sheets/base.js";
|
||||
|
||||
/**
|
||||
* An Actor sheet for NPC type characters in the D&D5E system.
|
||||
* Extends the base ActorSheet5e class.
|
||||
* @type {ActorSheet5e}
|
||||
*/
|
||||
export class ActorSheet5eNPC extends ActorSheet5e {
|
||||
|
||||
/**
|
||||
* Define default rendering options for the NPC sheet
|
||||
* @return {Object}
|
||||
*/
|
||||
static get defaultOptions() {
|
||||
return mergeObject(super.defaultOptions, {
|
||||
classes: ["sw5e", "sheet", "actor", "npc"],
|
||||
width: 600,
|
||||
height: 658
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Rendering */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Get the correct HTML template path to use for rendering this particular sheet
|
||||
* @type {String}
|
||||
*/
|
||||
get template() {
|
||||
if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/limited-sheet.html";
|
||||
return "systems/sw5e/templates/actors/npc-sheet.html";
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Organize Owned Items for rendering the NPC sheet
|
||||
* @private
|
||||
*/
|
||||
_prepareItems(data) {
|
||||
|
||||
// Categorize Items as Features and Powers
|
||||
const features = {
|
||||
weapons: { label: "Attacks", items: [] , hasActions: true, dataset: {type: "weapon", "weapon-type": "natural"} },
|
||||
actions: { label: "Actions", items: [] , hasActions: true, dataset: {type: "feat", "activation.type": "action"} },
|
||||
passive: { label: "Features", items: [], dataset: {type: "feat"} },
|
||||
equipment: { label: "Inventory", items: [], dataset: {type: "loot"}}
|
||||
};
|
||||
|
||||
// Start by classifying items into groups for rendering
|
||||
let [powers, other] = data.items.reduce((arr, item) => {
|
||||
item.img = item.img || DEFAULT_TOKEN;
|
||||
item.isStack = item.data.quantity ? item.data.quantity > 1 : false;
|
||||
item.hasUses = item.data.uses && (item.data.uses.max > 0);
|
||||
item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false);
|
||||
item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0));
|
||||
item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type));
|
||||
if ( item.type === "power" ) arr[0].push(item);
|
||||
else arr[1].push(item);
|
||||
return arr;
|
||||
}, [[], []]);
|
||||
|
||||
// Apply item filters
|
||||
powers = this._filterItems(powers, this._filters.powerbook);
|
||||
other = this._filterItems(other, this._filters.features);
|
||||
|
||||
// Organize Powerbook
|
||||
const powerbook = this._preparePowerbook(data, powers);
|
||||
|
||||
// Organize Features
|
||||
for ( let item of other ) {
|
||||
if ( item.type === "weapon" ) features.weapons.items.push(item);
|
||||
else if ( item.type === "feat" ) {
|
||||
if ( item.data.activation.type ) features.actions.items.push(item);
|
||||
else features.passive.items.push(item);
|
||||
}
|
||||
else features.equipment.items.push(item);
|
||||
}
|
||||
|
||||
// Assign and return
|
||||
data.features = Object.values(features);
|
||||
data.powerbook = powerbook;
|
||||
}
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Add some extra data when rendering the sheet to reduce the amount of logic required within the template.
|
||||
*/
|
||||
getData() {
|
||||
const data = super.getData();
|
||||
|
||||
// Challenge Rating
|
||||
const cr = parseFloat(data.data.details.cr || 0);
|
||||
const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"};
|
||||
data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1;
|
||||
return data;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Object Updates */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* This method is called upon form submission after form data is validated
|
||||
* @param event {Event} The initial triggering submission event
|
||||
* @param formData {Object} The object of validated form data with which to update the object
|
||||
* @private
|
||||
*/
|
||||
_updateObject(event, formData) {
|
||||
|
||||
// Format NPC Challenge Rating
|
||||
const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5};
|
||||
let crv = "data.details.cr";
|
||||
let cr = formData[crv];
|
||||
cr = crs[cr] || parseFloat(cr);
|
||||
if ( cr ) formData[crv] = cr < 1 ? cr : parseInt(cr);
|
||||
|
||||
// Parent ActorSheet update steps
|
||||
super._updateObject(event, formData);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Event Listeners and Handlers */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Activate event listeners using the prepared sheet HTML
|
||||
* @param html {HTML} The prepared HTML object ready to be rendered into the DOM
|
||||
*/
|
||||
activateListeners(html) {
|
||||
super.activateListeners(html);
|
||||
|
||||
// Rollable Health Formula
|
||||
html.find(".health .rollable").click(this._onRollHealthFormula.bind(this));
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle rolling NPC health values using the provided formula
|
||||
* @param {Event} event The original click event
|
||||
* @private
|
||||
*/
|
||||
_onRollHealthFormula(event) {
|
||||
event.preventDefault();
|
||||
const formula = this.actor.data.data.attributes.hp.formula;
|
||||
if ( !formula ) return;
|
||||
const hp = new Roll(formula).roll().total;
|
||||
AudioHelper.play({src: CONFIG.sounds.dice});
|
||||
this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp});
|
||||
}
|
||||
}
|
|
@ -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": "∞"
|
||||
};
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
|
@ -1,368 +0,0 @@
|
|||
import ActorSheet5e from "./base.js";
|
||||
import Actor5e from "../../entity.js";
|
||||
|
||||
/**
|
||||
* An Actor sheet for player character type actors in the SW5E system.
|
||||
* Extends the base ActorSheet5e class.
|
||||
* @type {ActorSheet5e}
|
||||
*/
|
||||
export default class ActorSheet5eCharacter extends ActorSheet5e {
|
||||
/**
|
||||
* Define default rendering options for the NPC sheet
|
||||
* @return {Object}
|
||||
*/
|
||||
static get defaultOptions() {
|
||||
return mergeObject(super.defaultOptions, {
|
||||
classes: ["sw5e", "sheet", "actor", "character"],
|
||||
width: 720,
|
||||
height: 736
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Add some extra data when rendering the sheet to reduce the amount of logic required within the template.
|
||||
*/
|
||||
getData() {
|
||||
const sheetData = super.getData();
|
||||
|
||||
// Temporary HP
|
||||
let hp = sheetData.data.attributes.hp;
|
||||
if (hp.temp === 0) delete hp.temp;
|
||||
if (hp.tempmax === 0) delete hp.tempmax;
|
||||
|
||||
// Resources
|
||||
sheetData["resources"] = ["primary", "secondary", "tertiary"].reduce((arr, r) => {
|
||||
const res = sheetData.data.resources[r] || {};
|
||||
res.name = r;
|
||||
res.placeholder = game.i18n.localize("SW5E.Resource" + r.titleCase());
|
||||
if (res && res.value === 0) delete res.value;
|
||||
if (res && res.max === 0) delete res.max;
|
||||
return arr.concat([res]);
|
||||
}, []);
|
||||
|
||||
// Experience Tracking
|
||||
sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking");
|
||||
sheetData["classLabels"] = this.actor.itemTypes.class.map((c) => c.name).join(", ");
|
||||
sheetData["multiclassLabels"] = this.actor.itemTypes.class
|
||||
.map((c) => {
|
||||
return [c.data.data.archetype, c.name, c.data.data.levels].filterJoin(" ");
|
||||
})
|
||||
.join(", ");
|
||||
|
||||
// Return data for rendering
|
||||
return sheetData;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Organize and classify Owned Items for Character sheets
|
||||
* @private
|
||||
*/
|
||||
_prepareItems(data) {
|
||||
// Categorize items as inventory, powerbook, features, and classes
|
||||
const inventory = {
|
||||
weapon: {label: "SW5E.ItemTypeWeaponPl", items: [], dataset: {type: "weapon"}},
|
||||
equipment: {label: "SW5E.ItemTypeEquipmentPl", items: [], dataset: {type: "equipment"}},
|
||||
consumable: {label: "SW5E.ItemTypeConsumablePl", items: [], dataset: {type: "consumable"}},
|
||||
tool: {label: "SW5E.ItemTypeToolPl", items: [], dataset: {type: "tool"}},
|
||||
backpack: {label: "SW5E.ItemTypeContainerPl", items: [], dataset: {type: "backpack"}},
|
||||
loot: {label: "SW5E.ItemTypeLootPl", items: [], dataset: {type: "loot"}}
|
||||
};
|
||||
|
||||
// Partition items by category
|
||||
let [
|
||||
items,
|
||||
powers,
|
||||
feats,
|
||||
classes,
|
||||
species,
|
||||
archetypes,
|
||||
classfeatures,
|
||||
backgrounds,
|
||||
fightingstyles,
|
||||
fightingmasteries,
|
||||
lightsaberforms
|
||||
] = data.items.reduce(
|
||||
(arr, item) => {
|
||||
// Item details
|
||||
item.img = item.img || CONST.DEFAULT_TOKEN;
|
||||
item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1;
|
||||
item.attunement = {
|
||||
[CONFIG.SW5E.attunementTypes.REQUIRED]: {
|
||||
icon: "fa-sun",
|
||||
cls: "not-attuned",
|
||||
title: "SW5E.AttunementRequired"
|
||||
},
|
||||
[CONFIG.SW5E.attunementTypes.ATTUNED]: {
|
||||
icon: "fa-sun",
|
||||
cls: "attuned",
|
||||
title: "SW5E.AttunementAttuned"
|
||||
}
|
||||
}[item.data.attunement];
|
||||
|
||||
// Item usage
|
||||
item.hasUses = item.data.uses && item.data.uses.max > 0;
|
||||
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);
|
||||
|
||||
// Item toggle state
|
||||
this._prepareItemToggleState(item);
|
||||
|
||||
// Primary Class
|
||||
if (item.type === "class")
|
||||
item.isOriginalClass = item._id === this.actor.data.data.details.originalClass;
|
||||
|
||||
// Classify items into types
|
||||
if (item.type === "power") arr[1].push(item);
|
||||
else if (item.type === "feat") arr[2].push(item);
|
||||
else if (item.type === "class") arr[3].push(item);
|
||||
else if (item.type === "species") arr[4].push(item);
|
||||
else if (item.type === "archetype") arr[5].push(item);
|
||||
else if (item.type === "classfeature") arr[6].push(item);
|
||||
else if (item.type === "background") arr[7].push(item);
|
||||
else if (item.type === "fightingstyle") arr[8].push(item);
|
||||
else if (item.type === "fightingmastery") arr[9].push(item);
|
||||
else if (item.type === "lightsaberform") arr[10].push(item);
|
||||
else if (Object.keys(inventory).includes(item.type)) arr[0].push(item);
|
||||
return arr;
|
||||
},
|
||||
[[], [], [], [], [], [], [], [], [], [], []]
|
||||
);
|
||||
|
||||
// Apply active item filters
|
||||
items = this._filterItems(items, this._filters.inventory);
|
||||
powers = this._filterItems(powers, this._filters.powerbook);
|
||||
feats = this._filterItems(feats, this._filters.features);
|
||||
|
||||
// Organize items
|
||||
for (let i of items) {
|
||||
i.data.quantity = i.data.quantity || 0;
|
||||
i.data.weight = i.data.weight || 0;
|
||||
i.totalWeight = (i.data.quantity * i.data.weight).toNearest(0.1);
|
||||
inventory[i.type].items.push(i);
|
||||
}
|
||||
|
||||
// Organize Powerbook and count the number of prepared powers (excluding always, at will, etc...)
|
||||
const powerbook = this._preparePowerbook(data, powers);
|
||||
const nPrepared = powers.filter((s) => {
|
||||
return s.data.level > 0 && s.data.preparation.mode === "prepared" && s.data.preparation.prepared;
|
||||
}).length;
|
||||
|
||||
// Organize Features
|
||||
const features = {
|
||||
classes: {
|
||||
label: "SW5E.ItemTypeClassPl",
|
||||
items: [],
|
||||
hasActions: false,
|
||||
dataset: {type: "class"},
|
||||
isClass: true
|
||||
},
|
||||
classfeatures: {
|
||||
label: "SW5E.ItemTypeClassFeats",
|
||||
items: [],
|
||||
hasActions: true,
|
||||
dataset: {type: "classfeature"},
|
||||
isClassfeature: true
|
||||
},
|
||||
archetype: {
|
||||
label: "SW5E.ItemTypeArchetype",
|
||||
items: [],
|
||||
hasActions: false,
|
||||
dataset: {type: "archetype"},
|
||||
isArchetype: true
|
||||
},
|
||||
species: {
|
||||
label: "SW5E.ItemTypeSpecies",
|
||||
items: [],
|
||||
hasActions: false,
|
||||
dataset: {type: "species"},
|
||||
isSpecies: true
|
||||
},
|
||||
background: {
|
||||
label: "SW5E.ItemTypeBackground",
|
||||
items: [],
|
||||
hasActions: false,
|
||||
dataset: {type: "background"},
|
||||
isBackground: true
|
||||
},
|
||||
fightingstyles: {
|
||||
label: "SW5E.ItemTypeFightingStylePl",
|
||||
items: [],
|
||||
hasActions: false,
|
||||
dataset: {type: "fightingstyle"},
|
||||
isFightingstyle: true
|
||||
},
|
||||
fightingmasteries: {
|
||||
label: "SW5E.ItemTypeFightingMasteryPl",
|
||||
items: [],
|
||||
hasActions: false,
|
||||
dataset: {type: "fightingmastery"},
|
||||
isFightingmastery: true
|
||||
},
|
||||
lightsaberforms: {
|
||||
label: "SW5E.ItemTypeLightsaberFormPl",
|
||||
items: [],
|
||||
hasActions: false,
|
||||
dataset: {type: "lightsaberform"},
|
||||
isLightsaberform: true
|
||||
},
|
||||
active: {
|
||||
label: "SW5E.FeatureActive",
|
||||
items: [],
|
||||
hasActions: true,
|
||||
dataset: {"type": "feat", "activation.type": "action"}
|
||||
},
|
||||
passive: {label: "SW5E.FeaturePassive", items: [], hasActions: false, dataset: {type: "feat"}}
|
||||
};
|
||||
for (let f of feats) {
|
||||
if (f.data.activation.type) features.active.items.push(f);
|
||||
else features.passive.items.push(f);
|
||||
}
|
||||
classes.sort((a, b) => b.data.levels - a.data.levels);
|
||||
features.classes.items = classes;
|
||||
features.classfeatures.items = classfeatures;
|
||||
features.archetype.items = archetypes;
|
||||
features.species.items = species;
|
||||
features.background.items = backgrounds;
|
||||
features.fightingstyles.items = fightingstyles;
|
||||
features.fightingmasteries.items = fightingmasteries;
|
||||
features.lightsaberforms.items = lightsaberforms;
|
||||
|
||||
// Assign and return
|
||||
data.inventory = Object.values(inventory);
|
||||
data.powerbook = powerbook;
|
||||
data.preparedPowers = nPrepared;
|
||||
data.features = Object.values(features);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A helper method to establish the displayed preparation state for an item
|
||||
* @param {Item} item
|
||||
* @private
|
||||
*/
|
||||
_prepareItemToggleState(item) {
|
||||
if (item.type === "power") {
|
||||
const isAlways = getProperty(item.data, "preparation.mode") === "always";
|
||||
const isPrepared = getProperty(item.data, "preparation.prepared");
|
||||
item.toggleClass = isPrepared ? "active" : "";
|
||||
if (isAlways) item.toggleClass = "fixed";
|
||||
if (isAlways) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.always;
|
||||
else if (isPrepared) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.prepared;
|
||||
else item.toggleTitle = game.i18n.localize("SW5E.PowerUnprepared");
|
||||
} else {
|
||||
const isActive = getProperty(item.data, "equipped");
|
||||
item.toggleClass = isActive ? "active" : "";
|
||||
item.toggleTitle = game.i18n.localize(isActive ? "SW5E.Equipped" : "SW5E.Unequipped");
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Event Listeners and Handlers
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Activate event listeners using the prepared sheet HTML
|
||||
* @param html {jQuery} The prepared HTML object ready to be rendered into the DOM
|
||||
*/
|
||||
activateListeners(html) {
|
||||
super.activateListeners(html);
|
||||
if (!this.isEditable) return;
|
||||
|
||||
// Item State Toggling
|
||||
html.find(".item-toggle").click(this._onToggleItem.bind(this));
|
||||
|
||||
// Short and Long Rest
|
||||
html.find(".short-rest").click(this._onShortRest.bind(this));
|
||||
html.find(".long-rest").click(this._onLongRest.bind(this));
|
||||
|
||||
// Rollable sheet actions
|
||||
html.find(".rollable[data-action]").click(this._onSheetAction.bind(this));
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle mouse click events for character sheet actions
|
||||
* @param {MouseEvent} event The originating click event
|
||||
* @private
|
||||
*/
|
||||
_onSheetAction(event) {
|
||||
event.preventDefault();
|
||||
const button = event.currentTarget;
|
||||
switch (button.dataset.action) {
|
||||
case "rollDeathSave":
|
||||
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
|
||||
* @param {Event} event The triggering click event
|
||||
* @private
|
||||
*/
|
||||
_onToggleItem(event) {
|
||||
event.preventDefault();
|
||||
const itemId = event.currentTarget.closest(".item").dataset.itemId;
|
||||
const item = this.actor.items.get(itemId);
|
||||
const attr = item.data.type === "power" ? "data.preparation.prepared" : "data.equipped";
|
||||
return item.update({[attr]: !getProperty(item.data, attr)});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Take a short rest, calling the relevant function on the Actor instance
|
||||
* @param {Event} event The triggering click event
|
||||
* @private
|
||||
*/
|
||||
async _onShortRest(event) {
|
||||
event.preventDefault();
|
||||
await this._onSubmit(event);
|
||||
return this.actor.shortRest();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Take a long rest, calling the relevant function on the Actor instance
|
||||
* @param {Event} event The triggering click event
|
||||
* @private
|
||||
*/
|
||||
async _onLongRest(event) {
|
||||
event.preventDefault();
|
||||
await this._onSubmit(event);
|
||||
return this.actor.longRest();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
async _onDropItemCreate(itemData) {
|
||||
// Increment the number of class levels a character instead of creating a new item
|
||||
if (itemData.type === "class") {
|
||||
const cls = this.actor.itemTypes.class.find((c) => c.name === itemData.name);
|
||||
let priorLevel = cls?.data.data.levels ?? 0;
|
||||
if (!!cls) {
|
||||
const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level);
|
||||
if (next > priorLevel) {
|
||||
itemData.levels = next;
|
||||
return cls.update({"data.levels": next});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default drop handling if levels were not added
|
||||
return super._onDropItemCreate(itemData);
|
||||
}
|
||||
}
|
|
@ -1,144 +0,0 @@
|
|||
import ActorSheet5e from "./base.js";
|
||||
|
||||
/**
|
||||
* An Actor sheet for NPC type characters in the SW5E system.
|
||||
* Extends the base ActorSheet5e class.
|
||||
* @extends {ActorSheet5e}
|
||||
*/
|
||||
export default class ActorSheet5eNPC extends ActorSheet5e {
|
||||
/** @override */
|
||||
static get defaultOptions() {
|
||||
return mergeObject(super.defaultOptions, {
|
||||
classes: ["sw5e", "sheet", "actor", "npc"],
|
||||
width: 600,
|
||||
height: 680
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
static unsupportedItemTypes = new Set(["class"]);
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Organize Owned Items for rendering the NPC sheet
|
||||
* @private
|
||||
*/
|
||||
_prepareItems(data) {
|
||||
// Categorize Items as Features and Powers
|
||||
const features = {
|
||||
weapons: {
|
||||
label: game.i18n.localize("SW5E.AttackPl"),
|
||||
items: [],
|
||||
hasActions: true,
|
||||
dataset: {"type": "weapon", "weapon-type": "natural"}
|
||||
},
|
||||
actions: {
|
||||
label: game.i18n.localize("SW5E.ActionPl"),
|
||||
items: [],
|
||||
hasActions: true,
|
||||
dataset: {"type": "feat", "activation.type": "action"}
|
||||
},
|
||||
passive: {label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"}},
|
||||
equipment: {label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}}
|
||||
};
|
||||
|
||||
// Start by classifying items into groups for rendering
|
||||
let [powers, other] = data.items.reduce(
|
||||
(arr, item) => {
|
||||
item.img = item.img || 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") arr[0].push(item);
|
||||
else arr[1].push(item);
|
||||
return arr;
|
||||
},
|
||||
[[], []]
|
||||
);
|
||||
|
||||
// Apply item filters
|
||||
powers = this._filterItems(powers, this._filters.powerbook);
|
||||
other = this._filterItems(other, this._filters.features);
|
||||
|
||||
// Organize Powerbook
|
||||
const powerbook = this._preparePowerbook(data, powers);
|
||||
|
||||
// Organize Features
|
||||
for (let item of other) {
|
||||
if (item.type === "weapon") features.weapons.items.push(item);
|
||||
else if (item.type === "feat") {
|
||||
if (item.data.activation.type) features.actions.items.push(item);
|
||||
else features.passive.items.push(item);
|
||||
} else features.equipment.items.push(item);
|
||||
}
|
||||
|
||||
// Assign and return
|
||||
data.features = Object.values(features);
|
||||
data.powerbook = powerbook;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
getData(options) {
|
||||
const data = super.getData(options);
|
||||
|
||||
// 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;
|
||||
|
||||
// Creature Type
|
||||
data.labels["type"] = this.actor.labels.creatureType;
|
||||
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});
|
||||
}
|
||||
}
|
|
@ -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});
|
||||
}
|
||||
}
|
|
@ -2,8 +2,8 @@
|
|||
* A specialized Dialog subclass for ability usage
|
||||
* @type {Dialog}
|
||||
*/
|
||||
export default class AbilityUseDialog extends Dialog {
|
||||
constructor(item, dialogData = {}, options = {}) {
|
||||
export class AbilityUseDialog extends Dialog {
|
||||
constructor(item, dialogData={}, options={}) {
|
||||
super(dialogData, options);
|
||||
this.options.classes = ["sw5e", "dialog"];
|
||||
|
||||
|
@ -25,203 +25,40 @@ export default class AbilityUseDialog extends Dialog {
|
|||
* @return {Promise}
|
||||
*/
|
||||
static async create(item) {
|
||||
if (!item.isOwned) throw new Error("You cannot display an ability usage dialog for an unowned item");
|
||||
|
||||
// Prepare data
|
||||
const actorData = item.actor.data.data;
|
||||
const itemData = item.data.data;
|
||||
const uses = itemData.uses || {};
|
||||
const quantity = itemData.quantity || 0;
|
||||
const recharge = itemData.recharge || {};
|
||||
const uses = item.data.data.uses;
|
||||
const recharge = item.data.data.recharge;
|
||||
const recharges = !!recharge.value;
|
||||
const sufficientUses = (quantity > 0 && !uses.value) || uses.value > 0;
|
||||
|
||||
// Prepare dialog form data
|
||||
const data = {
|
||||
item: item.data,
|
||||
title: game.i18n.format("SW5E.AbilityUseHint", {
|
||||
type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`),
|
||||
name: item.name
|
||||
}),
|
||||
note: this._getAbilityUseNote(item.data, uses, recharge),
|
||||
consumePowerSlot: false,
|
||||
consumeRecharge: recharges,
|
||||
consumeResource: !!itemData.consume.target,
|
||||
consumeUses: uses.per && uses.max > 0,
|
||||
canUse: recharges ? recharge.charged : sufficientUses,
|
||||
createTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget,
|
||||
errors: []
|
||||
};
|
||||
if (item.data.type === "power") this._getPowerData(actorData, itemData, data);
|
||||
|
||||
// Render the ability usage template
|
||||
const html = await renderTemplate("systems/sw5e/templates/apps/ability-use.html", data);
|
||||
const html = await renderTemplate("systems/sw5e/templates/apps/ability-use.html", {
|
||||
item: item.data,
|
||||
canUse: recharges ? recharge.charged : uses.value > 0,
|
||||
consume: true,
|
||||
uses: uses,
|
||||
recharges: !!recharge.value,
|
||||
isCharged: recharge.charged,
|
||||
hasPlaceableTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget,
|
||||
perLabel: CONFIG.SW5E.limitedUsePeriods[uses.per]
|
||||
});
|
||||
|
||||
// Create the Dialog and return data as a Promise
|
||||
const icon = data.isPower ? "fa-magic" : "fa-fist-raised";
|
||||
const label = game.i18n.localize("SW5E.AbilityUse" + (data.isPower ? "Cast" : "Use"));
|
||||
// Create the Dialog and return as a Promise
|
||||
return new Promise((resolve) => {
|
||||
let formData = null;
|
||||
const dlg = new this(item, {
|
||||
title: `${item.name}: ${game.i18n.localize("SW5E.AbilityUseConfig")}`,
|
||||
title: `${item.name}: Ability Configuration`,
|
||||
content: html,
|
||||
buttons: {
|
||||
use: {
|
||||
icon: `<i class="fas ${icon}"></i>`,
|
||||
label: label,
|
||||
callback: (html) => {
|
||||
const fd = new FormDataExtended(html[0].querySelector("form"));
|
||||
resolve(fd.toObject());
|
||||
}
|
||||
icon: '<i class="fas fa-fist-raised"></i>',
|
||||
label: "Use Ability",
|
||||
callback: html => formData = new FormData(html[0].querySelector("#ability-use-form"))
|
||||
}
|
||||
},
|
||||
default: "use",
|
||||
close: () => resolve(null)
|
||||
close: () => resolve(formData)
|
||||
});
|
||||
dlg.render(true);
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Helpers */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Get dialog data related to limited power slots
|
||||
* @private
|
||||
*/
|
||||
static _getPowerData(actorData, itemData, data) {
|
||||
// Determine whether the power may be up-cast
|
||||
const lvl = itemData.level;
|
||||
const consumePowerSlot = lvl > 0 && CONFIG.SW5E.powerUpcastModes.includes(itemData.preparation.mode);
|
||||
|
||||
// If can't upcast, return early and don't bother calculating available power slots
|
||||
if (!consumePowerSlot) {
|
||||
mergeObject(data, {isPower: true, consumePowerSlot});
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine the levels which are feasible
|
||||
let lmax = 0;
|
||||
let points;
|
||||
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;
|
||||
const label = CONFIG.SW5E.powerLevels[i];
|
||||
const l = actorData.powers["power" + i] || {fmax: 0, foverride: null};
|
||||
let max = parseInt(l.foverride || l.fmax || 0);
|
||||
let slots = Math.clamped(parseInt(l.fvalue || 0), 0, max);
|
||||
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;
|
||||
}, [])
|
||||
.filter((sl) => sl.level <= lmax);
|
||||
} else if (powerType === "tech") {
|
||||
powerLevels = Array.fromRange(10)
|
||||
.reduce((arr, i) => {
|
||||
if (i < lvl) return arr;
|
||||
const label = CONFIG.SW5E.powerLevels[i];
|
||||
const l = actorData.powers["power" + i] || {tmax: 0, toverride: null};
|
||||
let max = parseInt(l.override || l.tmax || 0);
|
||||
let slots = Math.clamped(parseInt(l.tvalue || 0), 0, max);
|
||||
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;
|
||||
}, [])
|
||||
.filter((sl) => sl.level <= lmax);
|
||||
}
|
||||
|
||||
const canCast = powerLevels.some((l) => l.hasSlots);
|
||||
if (!canCast)
|
||||
data.errors.push(
|
||||
game.i18n.format("SW5E.PowerCastNoSlots", {
|
||||
level: CONFIG.SW5E.powerLevels[lvl],
|
||||
name: data.item.name
|
||||
})
|
||||
);
|
||||
|
||||
// Merge power casting data
|
||||
return foundry.utils.mergeObject(data, {isPower: true, consumePowerSlot, powerLevels});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Get the ability usage note that is displayed
|
||||
* @private
|
||||
*/
|
||||
static _getAbilityUseNote(item, uses, recharge) {
|
||||
// Zero quantity
|
||||
const quantity = item.data.quantity;
|
||||
if (quantity <= 0) return game.i18n.localize("SW5E.AbilityUseUnavailableHint");
|
||||
|
||||
// Abilities which use Recharge
|
||||
if (!!recharge.value) {
|
||||
return game.i18n.format(recharge.charged ? "SW5E.AbilityUseChargedHint" : "SW5E.AbilityUseRechargeHint", {
|
||||
type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`)
|
||||
});
|
||||
}
|
||||
|
||||
// Does not use any resource
|
||||
if (!uses.per || !uses.max) return "";
|
||||
|
||||
// Consumables
|
||||
if (item.type === "consumable") {
|
||||
let str = "SW5E.AbilityUseNormalHint";
|
||||
if (uses.value > 1) str = "SW5E.AbilityUseConsumableChargeHint";
|
||||
else if (item.data.quantity === 1 && uses.autoDestroy) str = "SW5E.AbilityUseConsumableDestroyHint";
|
||||
else if (item.data.quantity > 1) str = "SW5E.AbilityUseConsumableQuantityHint";
|
||||
return game.i18n.format(str, {
|
||||
type: game.i18n.localize(`SW5E.Consumable${item.data.consumableType.capitalize()}`),
|
||||
value: uses.value,
|
||||
quantity: item.data.quantity,
|
||||
max: uses.max,
|
||||
per: CONFIG.SW5E.limitedUsePeriods[uses.per]
|
||||
});
|
||||
}
|
||||
|
||||
// Other Items
|
||||
else {
|
||||
return game.i18n.format("SW5E.AbilityUseNormalHint", {
|
||||
type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`),
|
||||
value: uses.value,
|
||||
max: uses.max,
|
||||
per: CONFIG.SW5E.limitedUsePeriods[uses.per]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
/**
|
||||
* An application class which provides advanced configuration for special character flags which modify an Actor
|
||||
* @implements {DocumentSheet}
|
||||
*/
|
||||
export default class ActorSheetFlags extends DocumentSheet {
|
||||
export class ActorSheetFlags extends BaseEntitySheet {
|
||||
static get defaultOptions() {
|
||||
return foundry.utils.mergeObject(super.defaultOptions, {
|
||||
const options = super.defaultOptions;
|
||||
return mergeObject(options, {
|
||||
id: "actor-flags",
|
||||
classes: ["sw5e"],
|
||||
template: "systems/sw5e/templates/apps/actor-flags.html",
|
||||
|
@ -15,18 +12,23 @@ export default class ActorSheetFlags extends DocumentSheet {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
/**
|
||||
* Configure the title of the special traits selection window to include the Actor name
|
||||
* @type {String}
|
||||
*/
|
||||
get title() {
|
||||
return `${game.i18n.localize("SW5E.FlagsTitle")}: ${this.object.name}`;
|
||||
return `${game.i18n.localize('SW5E.FlagsTitle')}: ${this.object.name}`;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
/**
|
||||
* Prepare data used to render the special Actor traits selection UI
|
||||
* @return {Object}
|
||||
*/
|
||||
getData() {
|
||||
const data = {};
|
||||
const data = super.getData();
|
||||
data.actor = this.object;
|
||||
data.classes = this._getClasses();
|
||||
data.flags = this._getFlags();
|
||||
data.bonuses = this._getBonuses();
|
||||
return data;
|
||||
|
@ -34,39 +36,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
|
||||
* Add some additional data for rendering
|
||||
* @return {object}
|
||||
* @private
|
||||
* @return {Object}
|
||||
*/
|
||||
_getFlags() {
|
||||
const flags = {};
|
||||
const baseData = this.document.toJSON();
|
||||
for (let [k, v] of Object.entries(CONFIG.SW5E.characterFlags)) {
|
||||
if (!flags.hasOwnProperty(v.section)) flags[v.section] = {};
|
||||
let flag = foundry.utils.deepClone(v);
|
||||
for ( let [k, v] of Object.entries(CONFIG.SW5E.characterFlags) ) {
|
||||
if ( !flags.hasOwnProperty(v.section) ) flags[v.section] = {};
|
||||
let flag = duplicate(v);
|
||||
flag.type = v.type.name;
|
||||
flag.isCheckbox = v.type === Boolean;
|
||||
flag.isSelect = v.hasOwnProperty("choices");
|
||||
flag.value = getProperty(baseData.flags, `sw5e.${k}`);
|
||||
flag.isSelect = v.hasOwnProperty('choices');
|
||||
flag.value = this.entity.getFlag("sw5e", k);
|
||||
flags[v.section][`flags.sw5e.${k}`] = flag;
|
||||
}
|
||||
return flags;
|
||||
|
@ -76,7 +59,7 @@ export default class ActorSheetFlags extends DocumentSheet {
|
|||
|
||||
/**
|
||||
* Get the bonuses fields and their localization strings
|
||||
* @return {Array<object>}
|
||||
* @return {Array}
|
||||
* @private
|
||||
*/
|
||||
_getBonuses() {
|
||||
|
@ -85,55 +68,45 @@ export default class ActorSheetFlags extends DocumentSheet {
|
|||
{name: "data.bonuses.mwak.damage", label: "SW5E.BonusMWDamage"},
|
||||
{name: "data.bonuses.rwak.attack", label: "SW5E.BonusRWAttack"},
|
||||
{name: "data.bonuses.rwak.damage", label: "SW5E.BonusRWDamage"},
|
||||
{name: "data.bonuses.mpak.attack", label: "SW5E.BonusMPAttack"},
|
||||
{name: "data.bonuses.mpak.damage", label: "SW5E.BonusMPDamage"},
|
||||
{name: "data.bonuses.rpak.attack", label: "SW5E.BonusRPAttack"},
|
||||
{name: "data.bonuses.rpak.damage", label: "SW5E.BonusRPDamage"},
|
||||
{name: "data.bonuses.msak.attack", label: "SW5E.BonusMSAttack"},
|
||||
{name: "data.bonuses.msak.damage", label: "SW5E.BonusMSDamage"},
|
||||
{name: "data.bonuses.rsak.attack", label: "SW5E.BonusRSAttack"},
|
||||
{name: "data.bonuses.rsak.damage", label: "SW5E.BonusRSDamage"},
|
||||
{name: "data.bonuses.abilities.check", label: "SW5E.BonusAbilityCheck"},
|
||||
{name: "data.bonuses.abilities.save", label: "SW5E.BonusAbilitySave"},
|
||||
{name: "data.bonuses.abilities.skill", label: "SW5E.BonusAbilitySkill"},
|
||||
{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"}
|
||||
{name: "data.bonuses.power.dc", label: "SW5E.BonusPowerDC"}
|
||||
];
|
||||
for (let b of bonuses) {
|
||||
b.value = getProperty(this.object._data, b.name) || "";
|
||||
for ( let b of bonuses ) {
|
||||
b.value = getProperty(this.object.data, b.name) || "";
|
||||
}
|
||||
return bonuses;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
/**
|
||||
* Update the Actor using the configured flags
|
||||
* Remove/unset any flags which are no longer configured
|
||||
*/
|
||||
async _updateObject(event, formData) {
|
||||
const actor = this.object;
|
||||
let updateData = expandObject(formData);
|
||||
const updateData = expandObject(formData);
|
||||
|
||||
// Unset any flags which are "false"
|
||||
let unset = false;
|
||||
const flags = updateData.flags.sw5e;
|
||||
//clone flags to dnd5e for module compatability
|
||||
updateData.flags.dnd5e = updateData.flags.sw5e;
|
||||
for (let [k, v] of Object.entries(flags)) {
|
||||
if ([undefined, null, "", false, 0].includes(v)) {
|
||||
for ( let [k, v] of Object.entries(flags) ) {
|
||||
if ( [undefined, null, "", false, 0].includes(v) ) {
|
||||
delete flags[k];
|
||||
if (hasProperty(actor._data.flags, `sw5e.${k}`)) {
|
||||
if ( hasProperty(actor.data.flags, `sw5e.${k}`) ) {
|
||||
unset = true;
|
||||
flags[`-=${k}`] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear any bonuses which are whitespace only
|
||||
for (let b of Object.values(updateData.data.bonuses)) {
|
||||
for (let [k, v] of Object.entries(b)) {
|
||||
b[k] = v.trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Diff the data against any applied overrides and apply
|
||||
// Apply the changes
|
||||
await actor.update(updateData, {diff: false});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
102
module/apps/cast-dialog.js
Normal file
102
module/apps/cast-dialog.js
Normal file
|
@ -0,0 +1,102 @@
|
|||
/**
|
||||
* A specialized Dialog subclass for casting a cast item at a certain level
|
||||
* @type {Dialog}
|
||||
*/
|
||||
export class CastDialog extends Dialog {
|
||||
constructor(actor, item, dialogData={}, options={}) {
|
||||
super(dialogData, options);
|
||||
this.options.classes = ["sw5e", "dialog"];
|
||||
|
||||
/**
|
||||
* Store a reference to the Actor entity which is casting the cast
|
||||
* @type {Actor5e}
|
||||
*/
|
||||
this.actor = actor;
|
||||
|
||||
/**
|
||||
* Store a reference to the Item entity which is the cast being cast
|
||||
* @type {Item5e}
|
||||
*/
|
||||
this.item = item;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Rendering */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A constructor function which displays the Cast Cast Dialog app for a given Actor and Item.
|
||||
* Returns a Promise which resolves to the dialog FormData once the workflow has been completed.
|
||||
* @param {Actor5e} actor
|
||||
* @param {Item5e} item
|
||||
* @return {Promise}
|
||||
*/
|
||||
static async create(actor, item) {
|
||||
const ad = actor.data.data;
|
||||
const id = item.data.data;
|
||||
|
||||
// Determine whether the cast may be upcast
|
||||
const lvl = id.level;
|
||||
const canUpcast = (lvl > 0) && CONFIG.SW5E.castUpcastModes.includes(id.preparation.mode);
|
||||
|
||||
// Determine the levels which are feasible
|
||||
let lmax = 0;
|
||||
const castLevels = Array.fromRange(10).reduce((arr, i) => {
|
||||
if ( i < lvl ) return arr;
|
||||
const l = ad.casts["cast"+i] || {max: 0, override: null};
|
||||
let max = parseInt(l.override || l.max || 0);
|
||||
let slots = Math.clamped(parseInt(l.value || 0), 0, max);
|
||||
if ( max > 0 ) lmax = i;
|
||||
arr.push({
|
||||
level: i,
|
||||
label: i > 0 ? `${CONFIG.SW5E.castLevels[i]} (${slots} Slots)` : CONFIG.SW5E.castLevels[i],
|
||||
canCast: canUpcast && (max > 0),
|
||||
hasSlots: slots > 0
|
||||
});
|
||||
return arr;
|
||||
}, []).filter(sl => sl.level <= lmax);
|
||||
|
||||
const pact = ad.casts.pact;
|
||||
if (pact.level >= lvl) {
|
||||
// If this character has pact slots, present them as an option for
|
||||
// casting the cast.
|
||||
castLevels.push({
|
||||
level: 'pact',
|
||||
label: game.i18n.localize('SW5E.CastLevelPact')
|
||||
+ ` (${game.i18n.localize('SW5E.Level')} ${pact.level}) `
|
||||
+ `(${pact.value} ${game.i18n.localize('SW5E.Slots')})`,
|
||||
canCast: canUpcast,
|
||||
hasSlots: pact.value > 0
|
||||
});
|
||||
}
|
||||
|
||||
const canCast = castLevels.some(l => l.hasSlots);
|
||||
|
||||
// Render the Cast casting template
|
||||
const html = await renderTemplate("systems/sw5e/templates/apps/cast-cast.html", {
|
||||
item: item.data,
|
||||
canCast: canCast,
|
||||
canUpcast: canUpcast,
|
||||
castLevels,
|
||||
hasPlaceableTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget
|
||||
});
|
||||
|
||||
// Create the Dialog and return as a Promise
|
||||
return new Promise((resolve, reject) => {
|
||||
const dlg = new this(actor, item, {
|
||||
title: `${item.name}: Cast Configuration`,
|
||||
content: html,
|
||||
buttons: {
|
||||
cast: {
|
||||
icon: '<i class="fas fa-magic"></i>',
|
||||
label: "Cast",
|
||||
callback: html => resolve(new FormData(html[0].querySelector("#cast-config-form")))
|
||||
}
|
||||
},
|
||||
default: "cast",
|
||||
close: reject
|
||||
});
|
||||
dlg.render(true);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
/**
|
||||
* A helper Dialog subclass for completing a long rest
|
||||
* @extends {Dialog}
|
||||
*/
|
||||
export default class LongRestDialog extends Dialog {
|
||||
constructor(actor, dialogData = {}, options = {}) {
|
||||
super(dialogData, options);
|
||||
this.actor = actor;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
static get defaultOptions() {
|
||||
return mergeObject(super.defaultOptions, {
|
||||
template: "systems/sw5e/templates/apps/long-rest.html",
|
||||
classes: ["sw5e", "dialog"]
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
getData() {
|
||||
const data = super.getData();
|
||||
const variant = game.settings.get("sw5e", "restVariant");
|
||||
data.promptNewDay = variant !== "gritty"; // It's always a new day when resting 1 week
|
||||
data.newDay = variant === "normal"; // It's probably a new day when resting normally (8 hours)
|
||||
return data;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A helper constructor function which displays the Long Rest confirmation dialog and returns a Promise once it's
|
||||
* workflow has been resolved.
|
||||
* @param {Actor5e} actor
|
||||
* @return {Promise}
|
||||
*/
|
||||
static async longRestDialog({actor} = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const dlg = new this(actor, {
|
||||
title: game.i18n.localize("SW5E.LongRest"),
|
||||
buttons: {
|
||||
rest: {
|
||||
icon: '<i class="fas fa-bed"></i>',
|
||||
label: game.i18n.localize("SW5E.Rest"),
|
||||
callback: (html) => {
|
||||
let newDay = true;
|
||||
if (game.settings.get("sw5e", "restVariant") !== "gritty")
|
||||
newDay = html.find('input[name="newDay"]')[0].checked;
|
||||
resolve(newDay);
|
||||
}
|
||||
},
|
||||
cancel: {
|
||||
icon: '<i class="fas fa-times"></i>',
|
||||
label: game.i18n.localize("Cancel"),
|
||||
callback: reject
|
||||
}
|
||||
},
|
||||
default: "rest",
|
||||
close: reject
|
||||
});
|
||||
dlg.render(true);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
/**
|
||||
* A simple form to set actor movement speeds
|
||||
* @extends {DocumentSheet}
|
||||
*/
|
||||
export default class ActorMovementConfig extends DocumentSheet {
|
||||
/** @override */
|
||||
static get defaultOptions() {
|
||||
return foundry.utils.mergeObject(super.defaultOptions, {
|
||||
classes: ["sw5e"],
|
||||
template: "systems/sw5e/templates/apps/movement-config.html",
|
||||
width: 300,
|
||||
height: "auto"
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
get title() {
|
||||
return `${game.i18n.localize("SW5E.MovementConfig")}: ${this.document.name}`;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
getData(options) {
|
||||
const sourceMovement = foundry.utils.getProperty(this.document.data._source, "data.attributes.movement") || {};
|
||||
const data = {
|
||||
movement: foundry.utils.deepClone(sourceMovement),
|
||||
units: CONFIG.SW5E.movementUnits
|
||||
};
|
||||
for (let [k, v] of Object.entries(data.movement)) {
|
||||
if (["units", "hover"].includes(k)) continue;
|
||||
data.movement[k] = Number.isNumeric(v) ? v.toNearest(0.1) : 0;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
102
module/apps/power-cast-dialog.js
Normal file
102
module/apps/power-cast-dialog.js
Normal file
|
@ -0,0 +1,102 @@
|
|||
/**
|
||||
* A specialized Dialog subclass for casting a power item at a certain level
|
||||
* @type {Dialog}
|
||||
*/
|
||||
export class PowerCastDialog extends Dialog {
|
||||
constructor(actor, item, dialogData={}, options={}) {
|
||||
super(dialogData, options);
|
||||
this.options.classes = ["sw5e", "dialog"];
|
||||
|
||||
/**
|
||||
* Store a reference to the Actor entity which is casting the power
|
||||
* @type {Actor5e}
|
||||
*/
|
||||
this.actor = actor;
|
||||
|
||||
/**
|
||||
* Store a reference to the Item entity which is the power being cast
|
||||
* @type {Item5e}
|
||||
*/
|
||||
this.item = item;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Rendering */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A constructor function which displays the Power Cast Dialog app for a given Actor and Item.
|
||||
* Returns a Promise which resolves to the dialog FormData once the workflow has been completed.
|
||||
* @param {Actor5e} actor
|
||||
* @param {Item5e} item
|
||||
* @return {Promise}
|
||||
*/
|
||||
static async create(actor, item) {
|
||||
const ad = actor.data.data;
|
||||
const id = item.data.data;
|
||||
|
||||
// Determine whether the power may be upcast
|
||||
const lvl = id.level;
|
||||
const canUpcast = (lvl > 0) && CONFIG.SW5E.powerUpcastModes.includes(id.preparation.mode);
|
||||
|
||||
// Determine the levels which are feasible
|
||||
let lmax = 0;
|
||||
const powerLevels = Array.fromRange(10).reduce((arr, i) => {
|
||||
if ( i < lvl ) return arr;
|
||||
const l = ad.powers["power"+i] || {max: 0, override: null};
|
||||
let max = parseInt(l.override || l.max || 0);
|
||||
let slots = Math.clamped(parseInt(l.value || 0), 0, max);
|
||||
if ( max > 0 ) lmax = i;
|
||||
arr.push({
|
||||
level: i,
|
||||
label: i > 0 ? `${CONFIG.SW5E.powerLevels[i]} (${slots} Slots)` : CONFIG.SW5E.powerLevels[i],
|
||||
canCast: canUpcast && (max > 0),
|
||||
hasSlots: slots > 0
|
||||
});
|
||||
return arr;
|
||||
}, []).filter(sl => sl.level <= lmax);
|
||||
|
||||
const pact = ad.powers.pact;
|
||||
if (pact.level >= lvl) {
|
||||
// If this character has pact slots, present them as an option for
|
||||
// casting the power.
|
||||
powerLevels.push({
|
||||
level: 'pact',
|
||||
label: game.i18n.localize('SW5E.PowerLevelPact')
|
||||
+ ` (${game.i18n.localize('SW5E.Level')} ${pact.level}) `
|
||||
+ `(${pact.value} ${game.i18n.localize('SW5E.Slots')})`,
|
||||
canCast: canUpcast,
|
||||
hasSlots: pact.value > 0
|
||||
});
|
||||
}
|
||||
|
||||
const canCast = powerLevels.some(l => l.hasSlots);
|
||||
|
||||
// Render the Power casting template
|
||||
const html = await renderTemplate("systems/sw5e/templates/apps/power-cast.html", {
|
||||
item: item.data,
|
||||
canCast: canCast,
|
||||
canUpcast: canUpcast,
|
||||
powerLevels,
|
||||
hasPlaceableTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget
|
||||
});
|
||||
|
||||
// Create the Dialog and return as a Promise
|
||||
return new Promise((resolve, reject) => {
|
||||
const dlg = new this(actor, item, {
|
||||
title: `${item.name}: Power Configuration`,
|
||||
content: html,
|
||||
buttons: {
|
||||
cast: {
|
||||
icon: '<i class="fas fa-magic"></i>',
|
||||
label: "Cast",
|
||||
callback: html => resolve(new FormData(html[0].querySelector("#power-config-form")))
|
||||
}
|
||||
},
|
||||
default: "cast",
|
||||
close: reject
|
||||
});
|
||||
dlg.render(true);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -1,11 +1,9 @@
|
|||
import LongRestDialog from "./long-rest.js";
|
||||
|
||||
/**
|
||||
* A helper Dialog subclass for rolling Hit Dice on short rest
|
||||
* @extends {Dialog}
|
||||
* @type {Dialog}
|
||||
*/
|
||||
export default class ShortRestDialog extends Dialog {
|
||||
constructor(actor, dialogData = {}, options = {}) {
|
||||
export class ShortRestDialog extends Dialog {
|
||||
constructor(actor, dialogData={}, options={}) {
|
||||
super(dialogData, options);
|
||||
|
||||
/**
|
||||
|
@ -36,11 +34,9 @@ export default class ShortRestDialog extends Dialog {
|
|||
/** @override */
|
||||
getData() {
|
||||
const data = super.getData();
|
||||
|
||||
// Determine Hit Dice
|
||||
data.availableHD = this.actor.data.items.reduce((hd, item) => {
|
||||
if (item.type === "class") {
|
||||
const d = item.data.data;
|
||||
if ( item.type === "class" ) {
|
||||
const d = item.data;
|
||||
const denom = d.hitDice || "d6";
|
||||
const available = parseInt(d.levels || 1) - parseInt(d.hitDiceUsed || 0);
|
||||
hd[denom] = denom in hd ? hd[denom] + available : available;
|
||||
|
@ -49,21 +45,18 @@ export default class ShortRestDialog extends Dialog {
|
|||
}, {});
|
||||
data.canRoll = this.actor.data.data.attributes.hd > 0;
|
||||
data.denomination = this._denom;
|
||||
|
||||
// Determine rest type
|
||||
const variant = game.settings.get("sw5e", "restVariant");
|
||||
data.promptNewDay = variant !== "epic"; // It's never a new day when only resting 1 minute
|
||||
data.newDay = false; // It may be a new day, but not by default
|
||||
return data;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
|
||||
/** @override */
|
||||
activateListeners(html) {
|
||||
super.activateListeners(html);
|
||||
let btn = html.find("#roll-hd");
|
||||
btn.click(this._onRollHitDie.bind(this));
|
||||
super.activateListeners(html);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -89,28 +82,22 @@ export default class ShortRestDialog extends Dialog {
|
|||
* @param {Actor5e} actor
|
||||
* @return {Promise}
|
||||
*/
|
||||
static async shortRestDialog({actor} = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
static async shortRestDialog({actor}={}) {
|
||||
return new Promise(resolve => {
|
||||
const dlg = new this(actor, {
|
||||
title: game.i18n.localize("SW5E.ShortRest"),
|
||||
title: "Short Rest",
|
||||
buttons: {
|
||||
rest: {
|
||||
icon: '<i class="fas fa-bed"></i>',
|
||||
label: game.i18n.localize("SW5E.Rest"),
|
||||
callback: (html) => {
|
||||
let newDay = false;
|
||||
if (game.settings.get("sw5e", "restVariant") === "gritty")
|
||||
newDay = html.find('input[name="newDay"]')[0].checked;
|
||||
resolve(newDay);
|
||||
}
|
||||
label: "Rest",
|
||||
callback: () => resolve(true)
|
||||
},
|
||||
cancel: {
|
||||
icon: '<i class="fas fa-times"></i>',
|
||||
label: game.i18n.localize("Cancel"),
|
||||
callback: reject
|
||||
label: "Cancel",
|
||||
callback: () => resolve(false)
|
||||
}
|
||||
}
|
||||
},
|
||||
close: reject
|
||||
});
|
||||
dlg.render(true);
|
||||
});
|
||||
|
@ -121,14 +108,31 @@ export default class ShortRestDialog extends Dialog {
|
|||
/**
|
||||
* A helper constructor function which displays the Long Rest confirmation dialog and returns a Promise once it's
|
||||
* workflow has been resolved.
|
||||
* @deprecated
|
||||
* @param {Actor5e} actor
|
||||
* @return {Promise}
|
||||
*/
|
||||
static async longRestDialog({actor} = {}) {
|
||||
console.warn(
|
||||
"WARNING! ShortRestDialog.longRestDialog has been deprecated, use LongRestDialog.longRestDialog instead."
|
||||
);
|
||||
return LongRestDialog.longRestDialog(...arguments);
|
||||
static async longRestDialog({actor}={}) {
|
||||
const content = `<p>Take a long rest?</p><p>On a long rest you will recover hit points, half your maximum hit dice,
|
||||
class resources, limited use item charges, and power slots.</p>`;
|
||||
return new Promise((resolve, reject) => {
|
||||
new Dialog({
|
||||
title: "Long Rest",
|
||||
content: content,
|
||||
buttons: {
|
||||
rest: {
|
||||
icon: '<i class="fas fa-bed"></i>',
|
||||
label: "Rest",
|
||||
callback: resolve
|
||||
},
|
||||
cancel: {
|
||||
icon: '<i class="fas fa-times"></i>',
|
||||
label: "Cancel",
|
||||
callback: reject
|
||||
},
|
||||
},
|
||||
default: 'rest',
|
||||
close: reject
|
||||
}, {classes: ["sw5e", "dialog"]}).render(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
/**
|
||||
* A specialized form used to select from a checklist of attributes, traits, or properties
|
||||
* @extends {DocumentSheet}
|
||||
* @extends {FormApplication}
|
||||
*/
|
||||
export default class TraitSelector extends DocumentSheet {
|
||||
/** @inheritdoc */
|
||||
export class TraitSelector extends FormApplication {
|
||||
|
||||
/** @override */
|
||||
static get defaultOptions() {
|
||||
return foundry.utils.mergeObject(super.defaultOptions, {
|
||||
return mergeObject(super.defaultOptions, {
|
||||
id: "trait-selector",
|
||||
classes: ["sw5e", "trait-selector", "subconfig"],
|
||||
classes: ["sw5e"],
|
||||
title: "Actor Trait Selection",
|
||||
template: "systems/sw5e/templates/apps/trait-selector.html",
|
||||
width: 320,
|
||||
|
@ -15,9 +16,7 @@ export default class TraitSelector extends DocumentSheet {
|
|||
choices: {},
|
||||
allowCustom: true,
|
||||
minimum: 0,
|
||||
maximum: null,
|
||||
valueKey: "value",
|
||||
customKey: "custom"
|
||||
maximum: null
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -25,7 +24,7 @@ export default class TraitSelector extends DocumentSheet {
|
|||
|
||||
/**
|
||||
* Return a reference to the target attribute
|
||||
* @type {string}
|
||||
* @type {String}
|
||||
*/
|
||||
get attribute() {
|
||||
return this.options.name;
|
||||
|
@ -35,50 +34,52 @@ export default class TraitSelector extends DocumentSheet {
|
|||
|
||||
/** @override */
|
||||
getData() {
|
||||
const attr = foundry.utils.getProperty(this.object.data, this.attribute);
|
||||
const o = this.options;
|
||||
const value = o.valueKey ? attr[o.valueKey] ?? [] : attr;
|
||||
const custom = o.customKey ? attr[o.customKey] ?? "" : "";
|
||||
|
||||
// Get current values
|
||||
let attr = getProperty(this.object.data, this.attribute) || {};
|
||||
attr.value = attr.value || [];
|
||||
|
||||
// Populate choices
|
||||
const choices = Object.entries(o.choices).reduce((obj, e) => {
|
||||
let [k, v] = e;
|
||||
obj[k] = {label: v, chosen: attr ? value.includes(k) : false};
|
||||
return obj;
|
||||
}, {});
|
||||
const choices = duplicate(this.options.choices);
|
||||
for ( let [k, v] of Object.entries(choices) ) {
|
||||
choices[k] = {
|
||||
label: v,
|
||||
chosen: attr ? attr.value.includes(k) : false
|
||||
}
|
||||
}
|
||||
|
||||
// Return data
|
||||
return {
|
||||
allowCustom: o.allowCustom,
|
||||
allowCustom: this.options.allowCustom,
|
||||
choices: choices,
|
||||
custom: custom
|
||||
};
|
||||
custom: attr ? attr.custom : ""
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
async _updateObject(event, formData) {
|
||||
const o = this.options;
|
||||
_updateObject(event, formData) {
|
||||
const updateData = {};
|
||||
|
||||
// Obtain choices
|
||||
const chosen = [];
|
||||
for (let [k, v] of Object.entries(formData)) {
|
||||
if (k !== "custom" && v) chosen.push(k);
|
||||
for ( let [k, v] of Object.entries(formData) ) {
|
||||
if ( (k !== "custom") && v ) chosen.push(k);
|
||||
}
|
||||
|
||||
// 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;
|
||||
updateData[`${this.attribute}.value`] = chosen;
|
||||
|
||||
// Validate the number chosen
|
||||
if (o.minimum && chosen.length < o.minimum) {
|
||||
return ui.notifications.error(`You must choose at least ${o.minimum} options`);
|
||||
if ( this.options.minimum && (chosen.length < this.options.minimum) ) {
|
||||
return ui.notifications.error(`You must choose at least ${this.options.minimum} options`);
|
||||
}
|
||||
if (o.maximum && chosen.length > o.maximum) {
|
||||
return ui.notifications.error(`You may choose no more than ${o.maximum} options`);
|
||||
if ( this.options.maximum && (chosen.length > this.options.maximum) ) {
|
||||
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
|
||||
|
|
|
@ -1,38 +1,46 @@
|
|||
/** @override */
|
||||
export const measureDistances = function (segments, options = {}) {
|
||||
if (!options.gridSpaces) return BaseGrid.prototype.measureDistances.call(this, segments, options);
|
||||
/**
|
||||
* Measure the distance between two pixel coordinates
|
||||
* See BaseGrid.measureDistance for more details
|
||||
*
|
||||
* @param {Object} p0 The origin coordinate {x, y}
|
||||
* @param {Object} p1 The destination coordinate {x, y}
|
||||
* @param {boolean} gridSpaces Enforce grid distance (if true) vs. direct point-to-point (if false)
|
||||
* @return {number} The distance between p1 and p0
|
||||
*/
|
||||
export const measureDistance = function(p0, p1, {gridSpaces=true}={}) {
|
||||
if ( !gridSpaces ) return BaseGrid.prototype.measureDistance.bind(this)(p0, p1, {gridSpaces});
|
||||
let gs = canvas.dimensions.size,
|
||||
ray = new Ray(p0, p1),
|
||||
nx = Math.abs(Math.ceil(ray.dx / gs)),
|
||||
ny = Math.abs(Math.ceil(ray.dy / gs));
|
||||
|
||||
// Track the total number of diagonals
|
||||
let nDiagonal = 0;
|
||||
const rule = this.parent.diagonalRule;
|
||||
const d = canvas.dimensions;
|
||||
|
||||
// Iterate over measured segments
|
||||
return segments.map((s) => {
|
||||
let r = s.ray;
|
||||
|
||||
// Determine the total distance traveled
|
||||
let nx = Math.abs(Math.ceil(r.dx / d.size));
|
||||
let ny = Math.abs(Math.ceil(r.dy / d.size));
|
||||
|
||||
// Determine the number of straight and diagonal moves
|
||||
let nd = Math.min(nx, ny);
|
||||
let ns = Math.abs(ny - nx);
|
||||
nDiagonal += nd;
|
||||
// Get the number of straight and diagonal moves
|
||||
let nDiagonal = Math.min(nx, ny),
|
||||
nStraight = Math.abs(ny - nx);
|
||||
|
||||
// Alternative DMG Movement
|
||||
if (rule === "5105") {
|
||||
let nd10 = Math.floor(nDiagonal / 2) - Math.floor((nDiagonal - nd) / 2);
|
||||
let spaces = nd10 * 2 + (nd - nd10) + ns;
|
||||
if ( this.parent.diagonalRule === "5105" ) {
|
||||
let nd10 = Math.floor(nDiagonal / 2);
|
||||
let spaces = (nd10 * 2) + (nDiagonal - nd10) + nStraight;
|
||||
return spaces * canvas.dimensions.distance;
|
||||
}
|
||||
|
||||
// Euclidean Measurement
|
||||
else if (rule === "EUCL") {
|
||||
return Math.round(Math.hypot(nx, ny) * canvas.scene.data.gridDistance);
|
||||
}
|
||||
|
||||
// Standard PHB Movement
|
||||
else return (ns + nd) * canvas.scene.data.gridDistance;
|
||||
});
|
||||
else return (nStraight + nDiagonal) * 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(data['temp'] || 0);
|
||||
data.max += parseInt(data['tempmax'] || 0);
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,27 +1,26 @@
|
|||
import {Actor5e} from "./actor/entity.js";
|
||||
|
||||
/**
|
||||
* Highlight critical success or failure on d20 rolls
|
||||
*/
|
||||
export const highlightCriticalSuccessFailure = function (message, html, data) {
|
||||
if (!message.isRoll || !message.isContentVisible) return;
|
||||
export const highlightCriticalSuccessFailure = function(message, html, data) {
|
||||
if ( !message.isRoll || !message.isRollVisible || !message.roll.parts.length ) return;
|
||||
|
||||
// Highlight rolls where the first part is a d20 roll
|
||||
const roll = message.roll;
|
||||
if (!roll.dice.length) return;
|
||||
const d = roll.dice[0];
|
||||
let d = roll.parts[0];
|
||||
const isD20Roll = d instanceof Die && (d.faces === 20) && (d.results.length === 1);
|
||||
if ( !isD20Roll ) return;
|
||||
|
||||
// Ensure it is an un-modified d20 roll
|
||||
const isD20 = d.faces === 20 && d.values.length === 1;
|
||||
if (!isD20) return;
|
||||
const isModifiedRoll = "success" in d.results[0] || d.options.marginSuccess || d.options.marginFailure;
|
||||
if (isModifiedRoll) return;
|
||||
// Ensure it is not a modified roll
|
||||
const isModifiedRoll = ("success" in d.rolls[0]) || d.options.marginSuccess || d.options.marginFailure;
|
||||
if ( isModifiedRoll ) return;
|
||||
|
||||
// Highlight successes and failures
|
||||
const critical = d.options.critical || 20;
|
||||
const fumble = d.options.fumble || 1;
|
||||
if (d.total >= critical) html.find(".dice-total").addClass("critical");
|
||||
else if (d.total <= fumble) html.find(".dice-total").addClass("fumble");
|
||||
else if (d.options.target) {
|
||||
if (roll.total >= d.options.target) html.find(".dice-total").addClass("success");
|
||||
if ( d.options.critical && (d.total >= d.options.critical) ) html.find(".dice-total").addClass("critical");
|
||||
else if ( d.options.fumble && (d.total <= d.options.fumble) ) html.find(".dice-total").addClass("fumble");
|
||||
else if ( d.options.target ) {
|
||||
if ( roll.total >= d.options.target ) html.find(".dice-total").addClass("success");
|
||||
else html.find(".dice-total").addClass("failure");
|
||||
}
|
||||
};
|
||||
|
@ -31,22 +30,20 @@ export const highlightCriticalSuccessFailure = function (message, html, data) {
|
|||
/**
|
||||
* Optionally hide the display of chat card action buttons which cannot be performed by the user
|
||||
*/
|
||||
export const displayChatActionButtons = function (message, html, data) {
|
||||
export const displayChatActionButtons = function(message, html, data) {
|
||||
const chatCard = html.find(".sw5e.chat-card");
|
||||
if (chatCard.length > 0) {
|
||||
const flavor = html.find(".flavor-text");
|
||||
if (flavor.text() === html.find(".item-name").text()) flavor.remove();
|
||||
if ( chatCard.length > 0 ) {
|
||||
|
||||
// If the user is the message author or the actor owner, proceed
|
||||
let actor = game.actors.get(data.message.speaker.actor);
|
||||
if (actor && actor.isOwner) return;
|
||||
else if (game.user.isGM || data.author.id === game.user.id) return;
|
||||
if ( actor && actor.owner ) return;
|
||||
else if ( game.user.isGM || (data.author.id === game.user.id)) return;
|
||||
|
||||
// Otherwise conceal action buttons except for saving throw
|
||||
const buttons = chatCard.find("button[data-action]");
|
||||
buttons.each((i, btn) => {
|
||||
if (btn.dataset.action === "save") return;
|
||||
btn.style.display = "none";
|
||||
if ( btn.dataset.action === "save" ) return;
|
||||
btn.style.display = "none"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -62,59 +59,33 @@ export const displayChatActionButtons = function (message, html, data) {
|
|||
*
|
||||
* @return {Array} The extended options Array including new context choices
|
||||
*/
|
||||
export const addChatMessageContextOptions = function (html, options) {
|
||||
let canApply = (li) => {
|
||||
const message = game.messages.get(li.data("messageId"));
|
||||
return message?.isRoll && message?.isContentVisible && canvas.tokens?.controlled.length;
|
||||
};
|
||||
export const addChatMessageContextOptions = function(html, options) {
|
||||
let canApply = li => canvas.tokens.controlledTokens.length && li.find(".dice-roll").length;
|
||||
options.push(
|
||||
{
|
||||
name: game.i18n.localize("SW5E.ChatContextDamage"),
|
||||
icon: '<i class="fas fa-user-minus"></i>',
|
||||
condition: canApply,
|
||||
callback: (li) => applyChatCardDamage(li, 1)
|
||||
callback: li => Actor5e.applyDamage(li, 1)
|
||||
},
|
||||
{
|
||||
name: game.i18n.localize("SW5E.ChatContextHealing"),
|
||||
icon: '<i class="fas fa-user-plus"></i>',
|
||||
condition: canApply,
|
||||
callback: (li) => applyChatCardDamage(li, -1)
|
||||
callback: li => Actor5e.applyDamage(li, -1)
|
||||
},
|
||||
{
|
||||
name: game.i18n.localize("SW5E.ChatContextDoubleDamage"),
|
||||
icon: '<i class="fas fa-user-injured"></i>',
|
||||
condition: canApply,
|
||||
callback: (li) => applyChatCardDamage(li, 2)
|
||||
callback: li => Actor5e.applyDamage(li, 2)
|
||||
},
|
||||
{
|
||||
name: game.i18n.localize("SW5E.ChatContextHalfDamage"),
|
||||
icon: '<i class="fas fa-user-shield"></i>',
|
||||
condition: canApply,
|
||||
callback: (li) => applyChatCardDamage(li, 0.5)
|
||||
callback: li => Actor5e.applyDamage(li, 0.5)
|
||||
}
|
||||
);
|
||||
return options;
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Apply rolled dice damage to the token or tokens which are currently controlled.
|
||||
* This allows for damage to be scaled by a multiplier to account for healing, critical hits, or resistance
|
||||
*
|
||||
* @param {HTMLElement} li The chat entry which contains the roll data
|
||||
* @param {Number} multiplier A damage multiplier to apply to the rolled damage.
|
||||
* @return {Promise}
|
||||
*/
|
||||
function applyChatCardDamage(li, multiplier) {
|
||||
const message = game.messages.get(li.data("messageId"));
|
||||
const roll = message.roll;
|
||||
return Promise.all(
|
||||
canvas.tokens.controlled.map((t) => {
|
||||
const a = t.actor;
|
||||
return a.applyDamage(roll.total, multiplier);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
export const ClassFeatures = {};
|
|
@ -1,31 +1,16 @@
|
|||
|
||||
/**
|
||||
* Override the default Initiative formula to customize special behaviors of the SW5e system.
|
||||
* Override the default Initiative formula to customize special behaviors of the D&D5e system.
|
||||
* Apply advantage, proficiency, or bonuses where appropriate
|
||||
* Apply the dexterity score as a decimal tiebreaker if requested
|
||||
* See Combat._getInitiativeFormula for more detail.
|
||||
*/
|
||||
export const _getInitiativeFormula = function () {
|
||||
const actor = this.actor;
|
||||
if (!actor) return "1d20";
|
||||
export const _getInitiativeFormula = function(combatant) {
|
||||
const actor = combatant.actor;
|
||||
if ( !actor ) return "1d20";
|
||||
const init = actor.data.data.attributes.init;
|
||||
|
||||
// Construct initiative formula parts
|
||||
let nd = 1;
|
||||
let mods = "";
|
||||
if (actor.getFlag("sw5e", "halflingLucky")) mods += "r1=1";
|
||||
if (actor.getFlag("sw5e", "initiativeAdv")) {
|
||||
nd = 2;
|
||||
mods += "kh";
|
||||
}
|
||||
const parts = [
|
||||
`${nd}d20${mods}`,
|
||||
init.mod,
|
||||
init.prof !== 0 ? init.prof : null,
|
||||
init.bonus !== 0 ? init.bonus : null
|
||||
];
|
||||
|
||||
// Optionally apply Dexterity tiebreaker
|
||||
const tiebreaker = game.settings.get("sw5e", "initiativeDexTiebreaker");
|
||||
if (tiebreaker) parts.push(actor.data.data.abilities.dex.value / 100);
|
||||
return parts.filter((p) => p !== null).join(" + ");
|
||||
const parts = ["1d20", init.mod, (init.prof !== 0) ? init.prof : null, (init.bonus !== 0) ? init.bonus : null];
|
||||
if ( actor.getFlag("sw5e", "initiativeAdv") ) parts[0] = "2d20kh";
|
||||
if ( CONFIG.Combat.initiative.tiebreaker ) parts.push(actor.data.data.abilities.dex.value / 100);
|
||||
return parts.filter(p => p !== null).join(" + ");
|
||||
};
|
||||
|
|
1832
module/config.js
1832
module/config.js
File diff suppressed because it is too large
Load diff
498
module/dice.js
498
module/dice.js
|
@ -1,313 +1,251 @@
|
|||
export {default as D20Roll} from "./dice/d20-roll.js";
|
||||
export {default as DamageRoll} from "./dice/damage-roll.js";
|
||||
export class Dice5e {
|
||||
|
||||
/**
|
||||
* 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".
|
||||
* 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 {object} data Actor or item data against which to parse the roll
|
||||
* @param {Array} parts The dice roll component parts, excluding the initial d20
|
||||
* @param {Object} data Actor or item data against which to parse the roll
|
||||
* @param {Event|object} event The triggering event which initiated the roll
|
||||
* @param {string} rollMode A specific roll mode to apply as the default for the resulting roll
|
||||
* @param {string|null} template The HTML template used to render the roll dialog
|
||||
* @param {string|null} title The dice roll UI window title
|
||||
* @param {Object} speaker The ChatMessage speaker to pass when creating the chat
|
||||
* @param {string|null} flavor Flavor text to use in the posted chat message
|
||||
* @param {Boolean} fastForward Allow fast-forward advantage selection
|
||||
* @param {Function} onClose Callback for actions to take when the dialog form is closed
|
||||
* @param {Object} dialogOptions Modal dialog options
|
||||
* @param {boolean} advantage Apply advantage to the roll (unless otherwise specified)
|
||||
* @param {boolean} disadvantage Apply disadvantage to the roll (unless otherwise specified)
|
||||
* @param {number} critical The value of d20 result which represents a critical success
|
||||
* @param {number} fumble The value of d20 result which represents a critical failure
|
||||
* @param {number} targetValue Assign a target value against which the result of this roll should be compared
|
||||
* @param {boolean} elvenAccuracy Allow Elven Accuracy to modify this roll?
|
||||
* @param {boolean} halflingLucky Allow Halfling Luck to modify this roll?
|
||||
*
|
||||
* @param {boolean} [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} [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
|
||||
* @return {Promise} A Promise which resolves once the roll workflow has completed
|
||||
*/
|
||||
export async function d20Roll({
|
||||
parts = [],
|
||||
data = {}, // Roll creation
|
||||
advantage,
|
||||
disadvantage,
|
||||
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
|
||||
} = {}) {
|
||||
static async d20Roll({parts=[], data={}, event={}, rollMode=null, template=null, title=null, speaker=null,
|
||||
flavor=null, fastForward=null, onClose, dialogOptions,
|
||||
advantage=null, disadvantage=null, critical=20, fumble=1, targetValue=null,
|
||||
elvenAccuracy=false, halflingLucky=false}={}) {
|
||||
|
||||
// Handle input arguments
|
||||
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";
|
||||
flavor = flavor || title;
|
||||
speaker = speaker || ChatMessage.getSpeaker();
|
||||
parts = parts.concat(["@bonus"]);
|
||||
rollMode = rollMode || game.settings.get("core", "rollMode");
|
||||
let rolled = false;
|
||||
|
||||
// Construct the D20Roll instance
|
||||
const roll = new CONFIG.Dice.D20Roll(formula, data, {
|
||||
flavor: flavor || title,
|
||||
advantageMode,
|
||||
defaultRollMode,
|
||||
critical,
|
||||
fumble,
|
||||
targetValue,
|
||||
elvenAccuracy,
|
||||
halflingLucky,
|
||||
reliableTalent
|
||||
});
|
||||
// Define inner roll function
|
||||
const _roll = function(parts, adv, form=null) {
|
||||
|
||||
// Prompt a Dialog to further configure the D20Roll
|
||||
if (!isFF) {
|
||||
const configured = await roll.configureDialog(
|
||||
{
|
||||
title,
|
||||
chooseModifier,
|
||||
defaultRollMode: defaultRollMode,
|
||||
defaultAction: advantageMode,
|
||||
defaultAbility: data?.item?.ability,
|
||||
template
|
||||
},
|
||||
dialogOptions
|
||||
);
|
||||
if (configured === null) return null;
|
||||
// Determine the d20 roll and modifiers
|
||||
let nd = 1;
|
||||
let mods = halflingLucky ? "r=1" : "";
|
||||
|
||||
// Handle advantage
|
||||
if ( adv === 1 ) {
|
||||
nd = elvenAccuracy ? 3 : 2;
|
||||
flavor += ` (${game.i18n.localize("SW5E.Advantage")})`;
|
||||
mods += "kh";
|
||||
}
|
||||
|
||||
// Evaluate the configured roll
|
||||
await roll.evaluate({async: true});
|
||||
|
||||
// Create a Chat Message
|
||||
if (speaker) {
|
||||
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;
|
||||
// Handle disadvantage
|
||||
else if ( adv === -1 ) {
|
||||
nd = 2;
|
||||
flavor += ` (${game.i18n.localize("SW5E.Disadvantage")})`;
|
||||
mods += "kl";
|
||||
}
|
||||
if (roll && chatMessage) await roll.toMessage(messageData);
|
||||
|
||||
// Include the d20 roll
|
||||
parts.unshift(`${nd}d20${mods}`);
|
||||
|
||||
// Optionally include a situational bonus
|
||||
if ( form !== null ) data['bonus'] = form.bonus.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;
|
||||
flavor += ` (${CONFIG.SW5E.abilities[data.ability]})`;
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the roll and flag critical thresholds on the d20
|
||||
let roll = new Roll(parts.join(" + "), data).roll();
|
||||
const d20 = roll.parts[0];
|
||||
d20.options.critical = critical;
|
||||
d20.options.fumble = fumble;
|
||||
if ( targetValue ) d20.options.target = targetValue;
|
||||
|
||||
// Convert the roll to a chat message and return the roll
|
||||
rollMode = form ? form.rollMode.value : rollMode;
|
||||
roll.toMessage({
|
||||
speaker: speaker,
|
||||
flavor: flavor
|
||||
}, { rollMode });
|
||||
rolled = true;
|
||||
return roll;
|
||||
}
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
// Determine whether the roll can be fast-forward
|
||||
if ( fastForward === null ) {
|
||||
fastForward = event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether this d20 roll should be fast-forwarded, and whether advantage or disadvantage should be applied
|
||||
* @returns {{isFF: boolean, advantageMode: number}} Whether the roll is fast-forward, and its advantage mode
|
||||
*/
|
||||
function _determineAdvantageMode({event, advantage = false, disadvantage = false, fastForward = false} = {}) {
|
||||
const isFF = fastForward || (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
|
||||
let advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.NORMAL;
|
||||
if (advantage || event?.altKey) advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.ADVANTAGE;
|
||||
else if (disadvantage || event?.ctrlKey || event?.metaKey)
|
||||
advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.DISADVANTAGE;
|
||||
return {isFF, advantageMode};
|
||||
}
|
||||
// Optionally allow fast-forwarding to specify advantage or disadvantage
|
||||
if ( fastForward ) {
|
||||
if ( advantage || event.altKey ) return _roll(parts, 1);
|
||||
else if ( disadvantage || event.ctrlKey || event.metaKey ) return _roll(parts, -1);
|
||||
else return _roll(parts, 0);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Damage Roll */
|
||||
/* -------------------------------------------- */
|
||||
// Render modal dialog
|
||||
template = template || "systems/sw5e/templates/chat/roll-dialog.html";
|
||||
let dialogData = {
|
||||
formula: parts.join(" + "),
|
||||
data: data,
|
||||
rollMode: rollMode,
|
||||
rollModes: CONFIG.rollModes,
|
||||
config: CONFIG.SW5E
|
||||
};
|
||||
const html = await renderTemplate(template, dialogData);
|
||||
|
||||
/**
|
||||
* A standardized helper function for managing core 5e damage rolls.
|
||||
// Create the Dialog window
|
||||
let roll;
|
||||
return new Promise(resolve => {
|
||||
new Dialog({
|
||||
title: title,
|
||||
content: html,
|
||||
buttons: {
|
||||
advantage: {
|
||||
label: game.i18n.localize("SW5E.Advantage"),
|
||||
callback: html => roll = _roll(parts, 1, html[0].children[0])
|
||||
},
|
||||
normal: {
|
||||
label: game.i18n.localize("SW5E.Normal"),
|
||||
callback: html => roll = _roll(parts, 0, html[0].children[0])
|
||||
},
|
||||
disadvantage: {
|
||||
label: game.i18n.localize("SW5E.Disadvantage"),
|
||||
callback: html => roll = _roll(parts, -1, html[0].children[0])
|
||||
}
|
||||
},
|
||||
default: "normal",
|
||||
close: html => {
|
||||
if (onClose) onClose(html, parts, data);
|
||||
resolve(rolled ? roll : false)
|
||||
}
|
||||
}, dialogOptions).render(true);
|
||||
})
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A standardized helper function for managing core 5e "d20 rolls"
|
||||
*
|
||||
* Holding SHIFT, ALT, or CTRL when the attack is rolled will "fast-forward".
|
||||
* This chooses the default options of a normal attack with no bonus, Critical, or no bonus respectively
|
||||
*
|
||||
* @param {string[]} parts The dice roll component parts, excluding the initial d20
|
||||
* @param {object} [data] Actor or item data against which to parse the roll
|
||||
* @param {Array} parts The dice roll component parts, excluding the initial d20
|
||||
* @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 {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} [critical=false] Flag this roll as a critical hit for the purposes of fast-forward or default dialog action
|
||||
* @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
|
||||
* @return {Promise} A Promise which resolves once the roll workflow has completed
|
||||
*/
|
||||
export async function damageRoll({
|
||||
parts = [],
|
||||
data, // Roll creation
|
||||
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
|
||||
} = {}) {
|
||||
static async damageRoll({parts, actor, data, event={}, rollMode=null, template, title, speaker, flavor,
|
||||
allowCritical=true, critical=false, fastForward=null, onClose, dialogOptions}) {
|
||||
|
||||
// Handle input arguments
|
||||
const defaultRollMode = rollMode || game.settings.get("core", "rollMode");
|
||||
flavor = flavor || title;
|
||||
speaker = speaker || ChatMessage.getSpeaker();
|
||||
rollMode = game.settings.get("core", "rollMode");
|
||||
let rolled = false;
|
||||
|
||||
// Construct the DamageRoll instance
|
||||
const formula = parts.join(" + ");
|
||||
const {isCritical, isFF} = _determineCriticalMode({critical, fastForward, event});
|
||||
const roll = new CONFIG.Dice.DamageRoll(formula, data, {
|
||||
flavor: flavor || title,
|
||||
critical: isCritical,
|
||||
criticalBonusDice,
|
||||
criticalMultiplier,
|
||||
multiplyNumeric,
|
||||
powerfulCritical
|
||||
});
|
||||
// Define inner roll function
|
||||
const _roll = function(parts, crit, form) {
|
||||
data['bonus'] = form ? form.bonus.value : 0;
|
||||
let roll = new Roll(parts.join("+"), data);
|
||||
|
||||
// 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;
|
||||
// Modify the damage formula for critical hits
|
||||
if ( crit === true ) {
|
||||
let add = (actor && actor.getFlag("sw5e", "savageAttacks")) ? 1 : 0;
|
||||
let mult = 2;
|
||||
roll.alter(add, mult);
|
||||
flavor = `${flavor} (${game.i18n.localize("SW5E.Critical")})`;
|
||||
}
|
||||
|
||||
// Evaluate the configured roll
|
||||
await roll.evaluate({async: true});
|
||||
|
||||
// Create a Chat Message
|
||||
if (speaker) {
|
||||
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);
|
||||
// Convert the roll to a chat message
|
||||
rollMode = form ? form.rollMode.value : rollMode;
|
||||
roll.toMessage({
|
||||
speaker: speaker,
|
||||
flavor: flavor
|
||||
}, { rollMode });
|
||||
rolled = true;
|
||||
return roll;
|
||||
}
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
// Determine whether the roll can be fast-forward
|
||||
if ( fastForward === null ) {
|
||||
fastForward = event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether this d20 roll should be fast-forwarded, and whether advantage or disadvantage should be applied
|
||||
* @returns {{isFF: boolean, isCritical: boolean}} Whether the roll is fast-forward, and whether it is a critical hit
|
||||
*/
|
||||
function _determineCriticalMode({event, critical = false, fastForward = false} = {}) {
|
||||
const isFF = fastForward || (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
|
||||
if (event?.altKey) critical = true;
|
||||
return {isFF, isCritical: critical};
|
||||
// Modify the roll and handle fast-forwarding
|
||||
if ( fastForward ) return _roll(parts, critical || event.altKey);
|
||||
else parts = parts.concat(["@bonus"]);
|
||||
|
||||
// Render modal dialog
|
||||
template = template || "systems/sw5e/templates/chat/roll-dialog.html";
|
||||
let dialogData = {
|
||||
formula: parts.join(" + "),
|
||||
data: data,
|
||||
rollMode: rollMode,
|
||||
rollModes: CONFIG.rollModes
|
||||
};
|
||||
const html = await renderTemplate(template, dialogData);
|
||||
|
||||
// Create the Dialog window
|
||||
let roll;
|
||||
return new Promise(resolve => {
|
||||
new Dialog({
|
||||
title: title,
|
||||
content: html,
|
||||
buttons: {
|
||||
critical: {
|
||||
condition: allowCritical,
|
||||
label: game.i18n.localize("SW5E.CriticalHit"),
|
||||
callback: html => roll = _roll(parts, true, html[0].children[0])
|
||||
},
|
||||
normal: {
|
||||
label: game.i18n.localize(allowCritical ? "SW5E.Normal" : "SW5E.Roll"),
|
||||
callback: html => roll = _roll(parts, false, html[0].children[0])
|
||||
},
|
||||
},
|
||||
default: "normal",
|
||||
close: html => {
|
||||
if (onClose) onClose(html, parts, data);
|
||||
resolve(rolled ? roll : false);
|
||||
}
|
||||
}, dialogOptions).render(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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`);
|
||||
}
|
64
module/effects.js
vendored
64
module/effects.js
vendored
|
@ -1,64 +0,0 @@
|
|||
/**
|
||||
* Manage Active Effect instances through the Actor Sheet via effect control buttons.
|
||||
* @param {MouseEvent} event The left-click event on the effect control
|
||||
* @param {Actor|Item} owner The owning entity which manages this effect
|
||||
*/
|
||||
export function onManageActiveEffect(event, owner) {
|
||||
event.preventDefault();
|
||||
const a = event.currentTarget;
|
||||
const li = a.closest("li");
|
||||
const effect = li.dataset.effectId ? owner.effects.get(li.dataset.effectId) : null;
|
||||
switch (a.dataset.action) {
|
||||
case "create":
|
||||
return owner.createEmbeddedDocuments("ActiveEffect", [
|
||||
{
|
||||
"label": game.i18n.localize("SW5E.EffectNew"),
|
||||
"icon": "icons/svg/aura.svg",
|
||||
"origin": owner.uuid,
|
||||
"duration.rounds": li.dataset.effectType === "temporary" ? 1 : undefined,
|
||||
"disabled": li.dataset.effectType === "inactive"
|
||||
}
|
||||
]);
|
||||
case "edit":
|
||||
return effect.sheet.render(true);
|
||||
case "delete":
|
||||
return effect.delete();
|
||||
case "toggle":
|
||||
return effect.update({disabled: !effect.data.disabled});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the data structure for Active Effects which are currently applied to an Actor or Item.
|
||||
* @param {ActiveEffect[]} effects The array of Active Effect instances to prepare sheet data for
|
||||
* @return {object} Data for rendering
|
||||
*/
|
||||
export function prepareActiveEffectCategories(effects) {
|
||||
// Define effect header categories
|
||||
const categories = {
|
||||
temporary: {
|
||||
type: "temporary",
|
||||
label: game.i18n.localize("SW5E.EffectTemporary"),
|
||||
effects: []
|
||||
},
|
||||
passive: {
|
||||
type: "passive",
|
||||
label: game.i18n.localize("SW5E.EffectPassive"),
|
||||
effects: []
|
||||
},
|
||||
inactive: {
|
||||
type: "inactive",
|
||||
label: game.i18n.localize("SW5E.EffectInactive"),
|
||||
effects: []
|
||||
}
|
||||
};
|
||||
|
||||
// Iterate over active effects, classifying them into categories
|
||||
for (let e of effects) {
|
||||
e._getSourceName(); // Trigger a lookup for the source name
|
||||
if (e.data.disabled) categories.inactive.effects.push(e);
|
||||
else if (e.isTemporary) categories.temporary.effects.push(e);
|
||||
else categories.passive.effects.push(e);
|
||||
}
|
||||
return categories;
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -1,30 +1,19 @@
|
|||
import TraitSelector from "../apps/trait-selector.js";
|
||||
import {onManageActiveEffect, prepareActiveEffectCategories} from "../effects.js";
|
||||
import { TraitSelector } from "../apps/trait-selector.js";
|
||||
|
||||
|
||||
/**
|
||||
* Override and extend the core ItemSheet implementation to handle specific item types
|
||||
* @extends {ItemSheet}
|
||||
* Override and extend the core ItemSheet implementation to handle D&D5E specific item types
|
||||
* @type {ItemSheet}
|
||||
*/
|
||||
export default class ItemSheet5e extends ItemSheet {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
export class ItemSheet5e extends ItemSheet {
|
||||
|
||||
// Expand the default size of the class sheet
|
||||
if (this.object.data.type === "class") {
|
||||
this.options.width = this.position.width = 600;
|
||||
this.options.height = this.position.height = 680;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
/** @override */
|
||||
static get defaultOptions() {
|
||||
return foundry.utils.mergeObject(super.defaultOptions, {
|
||||
return mergeObject(super.defaultOptions, {
|
||||
width: 560,
|
||||
height: 400,
|
||||
height: 420,
|
||||
classes: ["sw5e", "sheet", "item"],
|
||||
resizable: true,
|
||||
resizable: false,
|
||||
scrollY: [".tab.details"],
|
||||
tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}]
|
||||
});
|
||||
|
@ -32,7 +21,7 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
/** @override */
|
||||
get template() {
|
||||
const path = "systems/sw5e/templates/items/";
|
||||
return `${path}/${this.item.data.type}.html`;
|
||||
|
@ -41,130 +30,43 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
async getData(options) {
|
||||
const data = super.getData(options);
|
||||
const itemData = data.data;
|
||||
getData() {
|
||||
const data = super.getData();
|
||||
data.labels = this.item.labels;
|
||||
|
||||
// Include CONFIG values
|
||||
data.config = CONFIG.SW5E;
|
||||
|
||||
// Item Type, Status, and Details
|
||||
data.itemType = game.i18n.localize(`ITEM.Type${data.item.type.titleCase()}`);
|
||||
data.itemStatus = this._getItemStatus(itemData);
|
||||
data.itemProperties = this._getItemProperties(itemData);
|
||||
data.isPhysical = itemData.data.hasOwnProperty("quantity");
|
||||
|
||||
// Potential consumption targets
|
||||
data.abilityConsumptionTargets = this._getItemConsumptionTargets(itemData);
|
||||
data.itemType = data.item.type.titleCase();
|
||||
data.itemStatus = this._getItemStatus(data.item);
|
||||
data.itemProperties = this._getItemProperties(data.item);
|
||||
data.isPhysical = data.item.data.hasOwnProperty("quantity");
|
||||
|
||||
// Action Details
|
||||
data.hasAttackRoll = this.item.hasAttack;
|
||||
data.isHealing = itemData.data.actionType === "heal";
|
||||
data.isFlatDC = getProperty(itemData, "data.save.scaling") === "flat";
|
||||
data.isLine = ["line", "wall"].includes(itemData.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
|
||||
data.isCrewed = itemData.data.activation?.type === "crew";
|
||||
data.isMountable = this._isItemMountable(itemData);
|
||||
|
||||
// Prepare Active Effects
|
||||
data.effects = prepareActiveEffectCategories(this.item.effects);
|
||||
|
||||
// Re-define the template data references (backwards compatible)
|
||||
data.item = itemData;
|
||||
data.data = itemData.data;
|
||||
data.isHealing = data.item.data.actionType === "heal";
|
||||
data.isFlatDC = getProperty(data.item.data, "save.scaling") === "flat";
|
||||
data.isWeapon = data.item.type === "weapon";
|
||||
return data;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Get the valid item consumption targets which exist on the actor
|
||||
* @param {Object} item Item data for the item being displayed
|
||||
* @return {{string: string}} An object of potential consumption targets
|
||||
* @private
|
||||
*/
|
||||
_getItemConsumptionTargets(item) {
|
||||
const consume = item.data.consume || {};
|
||||
if (!consume.type) return [];
|
||||
const actor = this.item.actor;
|
||||
if (!actor) return {};
|
||||
|
||||
// Ammunition
|
||||
if (consume.type === "ammo") {
|
||||
return actor.itemTypes.consumable.reduce(
|
||||
(ammo, i) => {
|
||||
if (i.data.data.consumableType === "ammo") {
|
||||
ammo[i.id] = `${i.name} (${i.data.data.quantity})`;
|
||||
}
|
||||
return ammo;
|
||||
},
|
||||
{[item._id]: `${item.name} (${item.data.quantity})`}
|
||||
);
|
||||
}
|
||||
|
||||
// Attributes
|
||||
else if (consume.type === "attribute") {
|
||||
const attributes = TokenDocument.getTrackedAttributes(actor.data.data);
|
||||
attributes.bar.forEach((a) => a.push("value"));
|
||||
return attributes.bar.concat(attributes.value).reduce((obj, a) => {
|
||||
let k = a.join(".");
|
||||
obj[k] = k;
|
||||
return obj;
|
||||
}, {});
|
||||
}
|
||||
|
||||
// Materials
|
||||
else if (consume.type === "material") {
|
||||
return actor.items.reduce((obj, i) => {
|
||||
if (["consumable", "loot"].includes(i.data.type) && !i.data.data.activation) {
|
||||
obj[i.id] = `${i.name} (${i.data.data.quantity})`;
|
||||
}
|
||||
return obj;
|
||||
}, {});
|
||||
}
|
||||
|
||||
// Charges
|
||||
else if (consume.type === "charges") {
|
||||
return actor.items.reduce((obj, i) => {
|
||||
// Limited-use items
|
||||
const uses = i.data.data.uses || {};
|
||||
if (uses.per && uses.max) {
|
||||
const label =
|
||||
uses.per === "charges"
|
||||
? ` (${game.i18n.format("SW5E.AbilityUseChargesLabel", {value: uses.value})})`
|
||||
: ` (${game.i18n.format("SW5E.AbilityUseConsumableLabel", {
|
||||
max: uses.max,
|
||||
per: uses.per
|
||||
})})`;
|
||||
obj[i.id] = i.name + label;
|
||||
}
|
||||
|
||||
// Recharging items
|
||||
const recharge = i.data.data.recharge || {};
|
||||
if (recharge.value) obj[i.id] = `${i.name} (${game.i18n.format("SW5E.Recharge")})`;
|
||||
return obj;
|
||||
}, {});
|
||||
} else return {};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Get the text item status which is shown beneath the Item type in the top-right corner of the sheet
|
||||
* @return {string}
|
||||
* @private
|
||||
*/
|
||||
_getItemStatus(item) {
|
||||
if (item.type === "power") {
|
||||
if ( item.type === "power" ) {
|
||||
return CONFIG.SW5E.powerPreparationModes[item.data.preparation];
|
||||
} else if (["weapon", "equipment"].includes(item.type)) {
|
||||
return game.i18n.localize(item.data.equipped ? "SW5E.Equipped" : "SW5E.Unequipped");
|
||||
} else if (item.type === "tool") {
|
||||
return game.i18n.localize(item.data.proficient ? "SW5E.Proficient" : "SW5E.NotProficient");
|
||||
}
|
||||
else if ( ["weapon", "equipment"].includes(item.type) ) {
|
||||
return item.data.equipped ? "Equipped" : "Unequipped";
|
||||
}
|
||||
else if ( item.type === "tool" ) {
|
||||
return item.data.proficient ? "Proficient" : "Not Proficient";
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -179,81 +81,52 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
const props = [];
|
||||
const labels = this.item.labels;
|
||||
|
||||
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") {
|
||||
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(
|
||||
labels.components,
|
||||
labels.materials,
|
||||
item.data.components.concentration ? game.i18n.localize("SW5E.Concentration") : null,
|
||||
item.data.components.ritual ? game.i18n.localize("SW5E.Ritual") : null
|
||||
);
|
||||
} else if (item.type === "equipment") {
|
||||
item.data.components.concentration ? "Concentration" : null,
|
||||
item.data.components.ritual ? "Ritual" : null
|
||||
)
|
||||
}
|
||||
|
||||
else if ( item.type === "equipment" ) {
|
||||
props.push(CONFIG.SW5E.equipmentTypes[item.data.armor.type]);
|
||||
props.push(labels.armor);
|
||||
} else if (item.type === "feat") {
|
||||
}
|
||||
|
||||
else if ( item.type === "feat" ) {
|
||||
props.push(labels.featType);
|
||||
//TODO: Work out these
|
||||
} else if (item.type === "species") {
|
||||
//props.push(labels.species);
|
||||
} else if (item.type === "archetype") {
|
||||
//props.push(labels.archetype);
|
||||
} else if (item.type === "background") {
|
||||
//props.push(labels.background);
|
||||
} else if (item.type === "classfeature") {
|
||||
//props.push(labels.classfeature);
|
||||
} else if (item.type === "deployment") {
|
||||
//props.push(labels.deployment);
|
||||
} else if (item.type === "venture") {
|
||||
//props.push(labels.venture);
|
||||
} else if (item.type === "fightingmastery") {
|
||||
//props.push(labels.fightingmastery);
|
||||
} else if (item.type === "fightingstyle") {
|
||||
//props.push(labels.fightingstyle);
|
||||
} else if (item.type === "lightsaberform") {
|
||||
//props.push(labels.lightsaberform);
|
||||
}
|
||||
|
||||
// Action type
|
||||
if (item.data.actionType) {
|
||||
if ( item.data.actionType ) {
|
||||
props.push(CONFIG.SW5E.itemActionTypes[item.data.actionType]);
|
||||
}
|
||||
|
||||
// Action usage
|
||||
if (item.type !== "weapon" && item.data.activation && !isObjectEmpty(item.data.activation)) {
|
||||
props.push(labels.activation, labels.range, labels.target, labels.duration);
|
||||
if ( (item.type !== "weapon") && item.data.activation && !isObjectEmpty(item.data.activation) ) {
|
||||
props.push(
|
||||
labels.activation,
|
||||
labels.range,
|
||||
labels.target,
|
||||
labels.duration
|
||||
)
|
||||
}
|
||||
return props.filter((p) => !!p);
|
||||
return props.filter(p => !!p);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Is this item a separate large object like a siege engine or vehicle
|
||||
* component that is usually mounted on fixtures rather than equipped, and
|
||||
* has its own AC and HP.
|
||||
* @param item
|
||||
* @returns {boolean}
|
||||
* @private
|
||||
*/
|
||||
_isItemMountable(item) {
|
||||
const data = item.data;
|
||||
return (
|
||||
(item.type === "weapon" && data.weaponType === "siege") ||
|
||||
(item.type === "equipment" && data.armor.type === "vehicle")
|
||||
);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
setPosition(position = {}) {
|
||||
if (!(this._minimized || position.height)) {
|
||||
/** @override */
|
||||
setPosition(position={}) {
|
||||
position.height = this._tabs[0].active === "details" ? "auto" : this.options.height;
|
||||
}
|
||||
return super.setPosition(position);
|
||||
}
|
||||
|
||||
|
@ -261,38 +134,57 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
/* Form Submission */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
_getSubmitData(updateData = {}) {
|
||||
// Create the expanded update data object
|
||||
const fd = new FormDataExtended(this.form, {editors: this.editors});
|
||||
let data = fd.toObject();
|
||||
if (updateData) data = mergeObject(data, updateData);
|
||||
else data = expandObject(data);
|
||||
/** @override */
|
||||
_updateObject(event, formData) {
|
||||
|
||||
// Handle Damage array
|
||||
const damage = data.data?.damage;
|
||||
if (damage) damage.parts = Object.values(damage?.parts || {}).map((d) => [d[0] || "", d[1] || ""]);
|
||||
// Handle Damage Array
|
||||
let damage = Object.entries(formData).filter(e => e[0].startsWith("data.damage.parts"));
|
||||
formData["data.damage.parts"] = damage.reduce((arr, entry) => {
|
||||
let [i, j] = entry[0].split(".").slice(3);
|
||||
if ( !arr[i] ) arr[i] = [];
|
||||
arr[i][j] = entry[1];
|
||||
return arr;
|
||||
}, []);
|
||||
|
||||
// Return the flattened submission data
|
||||
return flattenObject(data);
|
||||
// Handle armorproperties Array
|
||||
let armorproperties = Object.entries(formData).filter(e => e[0].startsWith("data.armorproperties.parts"));
|
||||
formData["data.armorproperties.parts"] = armorproperties.reduce((arr, entry) => {
|
||||
let [i, j] = entry[0].split(".").slice(3);
|
||||
if ( !arr[i] ) arr[i] = [];
|
||||
arr[i][j] = entry[1];
|
||||
return arr;
|
||||
}, []);
|
||||
|
||||
// Handle weaponproperties Array
|
||||
let weaponproperties = Object.entries(formData).filter(e => e[0].startsWith("data.weaponproperties.parts"));
|
||||
formData["data.weaponproperties.parts"] = weaponproperties.reduce((arr, entry) => {
|
||||
let [i, j] = entry[0].split(".").slice(3);
|
||||
if ( !arr[i] ) arr[i] = [];
|
||||
arr[i][j] = entry[1];
|
||||
return arr;
|
||||
}, []);
|
||||
|
||||
|
||||
// Update the Item
|
||||
super._updateObject(event, formData);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
/** @override */
|
||||
activateListeners(html) {
|
||||
super.activateListeners(html);
|
||||
if (this.isEditable) {
|
||||
html.find(".damage-control").click(this._onDamageControl.bind(this));
|
||||
html.find(".trait-selector.class-skills").click(this._onConfigureTraits.bind(this));
|
||||
html.find(".effect-control").click((ev) => {
|
||||
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."
|
||||
);
|
||||
onManageActiveEffect(ev, this.item);
|
||||
});
|
||||
}
|
||||
|
||||
// Activate any Trait Selectors
|
||||
html.find('.trait-selector.class-skills').click(this._onConfigureClassSkills.bind(this));
|
||||
|
||||
// Armor properties
|
||||
html.find(".armorproperties-control").click(this._onarmorpropertiesControl.bind(this));
|
||||
|
||||
// Weapon properties
|
||||
html.find(".weaponproperties-control").click(this._onweaponpropertiesControl.bind(this));
|
||||
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -308,17 +200,17 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
const a = event.currentTarget;
|
||||
|
||||
// Add new damage component
|
||||
if (a.classList.contains("add-damage")) {
|
||||
if ( a.classList.contains("add-damage") ) {
|
||||
await this._onSubmit(event); // Submit any unsaved changes
|
||||
const damage = this.item.data.data.damage;
|
||||
return this.item.update({"data.damage.parts": damage.parts.concat([["", ""]])});
|
||||
}
|
||||
|
||||
// Remove a damage component
|
||||
if (a.classList.contains("delete-damage")) {
|
||||
if ( a.classList.contains("delete-damage") ) {
|
||||
await this._onSubmit(event); // Submit any unsaved changes
|
||||
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);
|
||||
return this.item.update({"data.damage.parts": damage.parts});
|
||||
}
|
||||
|
@ -326,45 +218,86 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle spawning the TraitSelector application for selection various options.
|
||||
* @param {Event} event The click event which originated the selection
|
||||
/**
|
||||
* Add or remove a armorproperties part from the armorproperties formula
|
||||
* @param {Event} event The original click event
|
||||
* @return {Promise}
|
||||
* @private
|
||||
*/
|
||||
_onConfigureTraits(event) {
|
||||
async _onarmorpropertiesControl(event) {
|
||||
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 choiceSet =
|
||||
skills.choices && skills.choices.length ? skills.choices : Object.keys(CONFIG.SW5E.skills);
|
||||
options.choices = Object.fromEntries(
|
||||
Object.entries(CONFIG.SW5E.skills).filter((skill) => choiceSet.includes(skill[0]))
|
||||
);
|
||||
options.maximum = skills.number;
|
||||
break;
|
||||
// Add new armorproperties component
|
||||
if ( a.classList.contains("add-armorproperties") ) {
|
||||
await this._onSubmit(event); // Submit any unsaved changes
|
||||
const armorproperties = this.item.data.data.armorproperties;
|
||||
return this.item.update({"data.armorproperties.parts": armorproperties.parts.concat([["", ""]])});
|
||||
}
|
||||
|
||||
// Remove a armorproperties component
|
||||
if ( a.classList.contains("delete-armorproperties") ) {
|
||||
await this._onSubmit(event); // Submit any unsaved changes
|
||||
const li = a.closest(".armorproperties-part");
|
||||
const armorproperties = duplicate(this.item.data.data.armorproperties);
|
||||
armorproperties.parts.splice(Number(li.dataset.armorpropertiesPart), 1);
|
||||
return this.item.update({"data.armorproperties.parts": armorproperties.parts});
|
||||
}
|
||||
new TraitSelector(this.item, options).render(true);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
async _onSubmit(...args) {
|
||||
if (this._tabs[0].active === "details") this.position.height = "auto";
|
||||
await super._onSubmit(...args);
|
||||
/**
|
||||
* Add or remove a weaponproperties part from the weaponproperties formula
|
||||
* @param {Event} event The original click event
|
||||
* @return {Promise}
|
||||
* @private
|
||||
*/
|
||||
async _onweaponpropertiesControl(event) {
|
||||
event.preventDefault();
|
||||
const a = event.currentTarget;
|
||||
|
||||
// Add new weaponproperties component
|
||||
if ( a.classList.contains("add-weaponproperties") ) {
|
||||
await this._onSubmit(event); // Submit any unsaved changes
|
||||
const weaponproperties = this.item.data.data.weaponproperties;
|
||||
return this.item.update({"data.weaponproperties.parts": weaponproperties.parts.concat([["", ""]])});
|
||||
}
|
||||
|
||||
// Remove a weaponproperties component
|
||||
if ( a.classList.contains("delete-weaponproperties") ) {
|
||||
await this._onSubmit(event); // Submit any unsaved changes
|
||||
const li = a.closest(".weaponproperties-part");
|
||||
const weaponproperties = duplicate(this.item.data.data.weaponproperties);
|
||||
weaponproperties.parts.splice(Number(li.dataset.weaponpropertiesPart), 1);
|
||||
return this.item.update({"data.weaponproperties.parts": weaponproperties.parts});
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle spawning the TraitSelector application which allows a checkbox of multiple trait options
|
||||
* @param {Event} event The click event which originated the selection
|
||||
* @private
|
||||
*/
|
||||
_onConfigureClassSkills(event) {
|
||||
event.preventDefault();
|
||||
const skills = this.item.data.data.skills;
|
||||
const choices = skills.choices && skills.choices.length ? skills.choices : Object.keys(CONFIG.SW5E.skills);
|
||||
const a = event.currentTarget;
|
||||
const label = a.parentElement;
|
||||
|
||||
// Render the Trait Selector dialog
|
||||
new TraitSelector(this.item, {
|
||||
name: a.dataset.edit,
|
||||
title: label.innerText,
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
/* -------------------------------------------- */
|
||||
/* Hotbar Macros */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create a Macro from an Item drop.
|
||||
* Get an existing item macro if one exists, otherwise create a new one.
|
||||
* @param {Object} data The dropped data
|
||||
* @param {number} slot The hotbar slot to use
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export async function create5eMacro(data, slot) {
|
||||
if (data.type !== "Item") return;
|
||||
if (!("data" in data)) return ui.notifications.warn("You can only create macro buttons for owned Items");
|
||||
const item = data.data;
|
||||
|
||||
// Create the macro command
|
||||
const command = `game.sw5e.rollItemMacro("${item.name}");`;
|
||||
let macro = game.macros.entities.find((m) => m.name === item.name && m.command === command);
|
||||
if (!macro) {
|
||||
macro = await Macro.create({
|
||||
name: item.name,
|
||||
type: "script",
|
||||
img: item.img,
|
||||
command: command,
|
||||
flags: {"sw5e.itemMacro": true}
|
||||
});
|
||||
}
|
||||
game.user.assignHotbarMacro(macro, slot);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create a Macro from an Item drop.
|
||||
* Get an existing item macro if one exists, otherwise create a new one.
|
||||
* @param {string} itemName
|
||||
* @return {Promise}
|
||||
*/
|
||||
export function rollItemMacro(itemName) {
|
||||
const speaker = ChatMessage.getSpeaker();
|
||||
let actor;
|
||||
if (speaker.token) actor = game.actors.tokens[speaker.token];
|
||||
if (!actor) actor = game.actors.get(speaker.actor);
|
||||
|
||||
// Get matching items
|
||||
const items = actor ? actor.items.filter((i) => i.name === itemName) : [];
|
||||
if (items.length > 1) {
|
||||
ui.notifications.warn(
|
||||
`Your controlled Actor ${actor.name} has more than one Item with name ${itemName}. The first matched item will be chosen.`
|
||||
);
|
||||
} else if (items.length === 0) {
|
||||
return ui.notifications.warn(`Your controlled Actor does not have an item named ${itemName}`);
|
||||
}
|
||||
const item = items[0];
|
||||
|
||||
// Trigger the item roll
|
||||
return item.roll();
|
||||
}
|
|
@ -2,68 +2,59 @@
|
|||
* Perform a system migration for the entire World, applying migrations for Actors, Items, and Compendium packs
|
||||
* @return {Promise} A Promise which resolves once the migration is completed
|
||||
*/
|
||||
export const migrateWorld = async function () {
|
||||
ui.notifications.info(
|
||||
`Applying SW5e System Migration for version ${game.system.data.version}. Please be patient and do not close your game or shut down your server.`,
|
||||
{permanent: true}
|
||||
);
|
||||
export const migrateWorld = async function() {
|
||||
ui.notifications.info(`Applying SW5E System Migration for version ${game.system.data.version}. Please be patient and do not close your game or shut down your server.`, {permanent: true});
|
||||
|
||||
// Migrate World Actors
|
||||
for await (let a of game.actors.contents) {
|
||||
for ( let a of game.actors.entities ) {
|
||||
try {
|
||||
console.log(`Checking Actor entity ${a.name} for migration needs`);
|
||||
const updateData = await migrateActorData(a.data);
|
||||
if (!foundry.utils.isObjectEmpty(updateData)) {
|
||||
const updateData = migrateActorData(a.data);
|
||||
if ( !isObjectEmpty(updateData) ) {
|
||||
console.log(`Migrating Actor entity ${a.name}`);
|
||||
await a.update(updateData, {enforceTypes: false});
|
||||
}
|
||||
} catch (err) {
|
||||
err.message = `Failed sw5e system migration for Actor ${a.name}: ${err.message}`;
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate World Items
|
||||
for (let i of game.items.contents) {
|
||||
for ( let i of game.items.entities ) {
|
||||
try {
|
||||
const updateData = migrateItemData(i.toObject());
|
||||
if (!foundry.utils.isObjectEmpty(updateData)) {
|
||||
const updateData = migrateItemData(i.data);
|
||||
if ( !isObjectEmpty(updateData) ) {
|
||||
console.log(`Migrating Item entity ${i.name}`);
|
||||
await i.update(updateData, {enforceTypes: false});
|
||||
}
|
||||
} catch (err) {
|
||||
err.message = `Failed sw5e system migration for Item ${i.name}: ${err.message}`;
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate Actor Override Tokens
|
||||
for (let s of game.scenes.contents) {
|
||||
for ( let s of game.scenes.entities ) {
|
||||
try {
|
||||
const updateData = await migrateSceneData(s.data);
|
||||
if (!foundry.utils.isObjectEmpty(updateData)) {
|
||||
const updateData = migrateSceneData(s.data);
|
||||
if ( !isObjectEmpty(updateData) ) {
|
||||
console.log(`Migrating Scene entity ${s.name}`);
|
||||
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) {
|
||||
err.message = `Failed sw5e system migration for Scene ${s.name}: ${err.message}`;
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate World Compendium Packs
|
||||
for (let p of game.packs) {
|
||||
if (p.metadata.package !== "world") continue;
|
||||
if (!["Actor", "Item", "Scene"].includes(p.metadata.entity)) continue;
|
||||
const packs = game.packs.filter(p => {
|
||||
return (p.metadata.package === "world") && ["Actor", "Item", "Scene"].includes(p.metadata.entity)
|
||||
});
|
||||
for ( let p of packs ) {
|
||||
await migrateCompendium(p);
|
||||
}
|
||||
|
||||
// Set the migration as complete
|
||||
game.settings.set("sw5e", "systemMigrationVersion", game.system.data.version);
|
||||
ui.notifications.info(`SW5e System Migration to version ${game.system.data.version} completed!`, {permanent: true});
|
||||
ui.notifications.info(`SW5E System Migration to version ${game.system.data.version} completed!`, {permanent: true});
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -73,47 +64,31 @@ export const migrateWorld = async function () {
|
|||
* @param pack
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const migrateCompendium = async function (pack) {
|
||||
export const migrateCompendium = async function(pack) {
|
||||
const entity = pack.metadata.entity;
|
||||
if (!["Actor", "Item", "Scene"].includes(entity)) return;
|
||||
|
||||
// Unlock the pack for editing
|
||||
const wasLocked = pack.locked;
|
||||
await pack.configure({locked: false});
|
||||
if ( !["Actor", "Item", "Scene"].includes(entity) ) return;
|
||||
|
||||
// Begin by requesting server-side data model migration and get the migrated content
|
||||
await pack.migrate();
|
||||
const documents = await pack.getDocuments();
|
||||
const content = await pack.getContent();
|
||||
|
||||
// Iterate over compendium entries - applying fine-tuned migration functions
|
||||
for await (let doc of documents) {
|
||||
let updateData = {};
|
||||
for ( let ent of content ) {
|
||||
try {
|
||||
switch (entity) {
|
||||
case "Actor":
|
||||
updateData = await migrateActorData(doc.data);
|
||||
break;
|
||||
case "Item":
|
||||
updateData = migrateItemData(doc.toObject());
|
||||
break;
|
||||
case "Scene":
|
||||
updateData = await migrateSceneData(doc.data);
|
||||
break;
|
||||
let updateData = null;
|
||||
if (entity === "Item") updateData = migrateItemData(ent.data);
|
||||
else if (entity === "Actor") updateData = migrateActorData(ent.data);
|
||||
else if ( entity === "Scene" ) updateData = migrateSceneData(ent.data);
|
||||
if (!isObjectEmpty(updateData)) {
|
||||
expandObject(updateData);
|
||||
updateData["_id"] = ent._id;
|
||||
await pack.updateEntity(updateData);
|
||||
console.log(`Migrated ${entity} entity ${ent.name} in Compendium ${pack.collection}`);
|
||||
}
|
||||
if (foundry.utils.isObjectEmpty(updateData)) continue;
|
||||
|
||||
// Save the entry, if data was changed
|
||||
await doc.update(updateData);
|
||||
console.log(`Migrated ${entity} entity ${doc.name} in Compendium ${pack.collection}`);
|
||||
} catch (err) {
|
||||
// Handle migration failures
|
||||
err.message = `Failed sw5e system migration for entity ${doc.name} in pack ${pack.collection}: ${err.message}`;
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the original locked status for the pack
|
||||
await pack.configure({locked: wasLocked});
|
||||
console.log(`Migrated all ${entity} entities from Compendium ${pack.collection}`);
|
||||
};
|
||||
|
||||
|
@ -124,112 +99,56 @@ export const migrateCompendium = async function (pack) {
|
|||
/**
|
||||
* Migrate a single Actor entity to incorporate latest data model changes
|
||||
* Return an Object of updateData to be applied
|
||||
* @param {object} actor The actor data object to update
|
||||
* @param {Actor} actor The actor to Update
|
||||
* @return {Object} The updateData to apply
|
||||
*/
|
||||
export const migrateActorData = async function (actor) {
|
||||
export const migrateActorData = function(actor) {
|
||||
const updateData = {};
|
||||
|
||||
// Actor Data Updates
|
||||
if (actor.data) {
|
||||
_migrateActorMovement(actor, updateData);
|
||||
_migrateActorSenses(actor, updateData);
|
||||
_migrateActorType(actor, updateData);
|
||||
}
|
||||
_migrateActorBonuses(actor, updateData);
|
||||
|
||||
// Remove deprecated fields
|
||||
_migrateRemoveDeprecated(actor, updateData);
|
||||
|
||||
// Migrate Owned Items
|
||||
if (!!actor.items) {
|
||||
const items = await actor.items.reduce(async (memo, i) => {
|
||||
const results = await memo;
|
||||
if ( !actor.items ) return updateData;
|
||||
let hasItemUpdates = false;
|
||||
const items = actor.items.map(i => {
|
||||
|
||||
// Migrate the Owned Item
|
||||
const itemData = i instanceof CONFIG.Item.documentClass ? i.toObject() : i;
|
||||
let itemUpdate = await migrateActorItemData(itemData, actor);
|
||||
let itemUpdate = migrateItemData(i);
|
||||
|
||||
// Prepared, Equipped, and Proficient for NPC actors
|
||||
if (actor.type === "npc") {
|
||||
if (getProperty(itemData.data, "preparation.prepared") === false)
|
||||
itemUpdate["data.preparation.prepared"] = true;
|
||||
if (getProperty(itemData.data, "equipped") === false) itemUpdate["data.equipped"] = true;
|
||||
if (getProperty(itemData.data, "proficient") === false) itemUpdate["data.proficient"] = true;
|
||||
if ( actor.type === "npc" ) {
|
||||
if (getProperty(i.data, "preparation.prepared") === false) itemUpdate["data.preparation.prepared"] = true;
|
||||
if (getProperty(i.data, "equipped") === false) itemUpdate["data.equipped"] = true;
|
||||
if (getProperty(i.data, "proficient") === false) itemUpdate["data.proficient"] = true;
|
||||
}
|
||||
|
||||
// Update the Owned Item
|
||||
if (!isObjectEmpty(itemUpdate)) {
|
||||
itemUpdate._id = itemData._id;
|
||||
console.log(`Migrating Actor ${actor.name}'s ${i.name}`);
|
||||
results.push(expandObject(itemUpdate));
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if ( !isObjectEmpty(itemUpdate) ) {
|
||||
hasItemUpdates = true;
|
||||
return mergeObject(i, itemUpdate, {enforceTypes: false, inplace: false});
|
||||
} else return i;
|
||||
});
|
||||
if ( hasItemUpdates ) updateData.items = items;
|
||||
return updateData;
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @return {Object} The scrubbed Actor data
|
||||
*/
|
||||
function cleanActorData(actorData) {
|
||||
// Scrub system data
|
||||
const model = game.system.model.Actor[actorData.type];
|
||||
actorData.data = filterObject(actorData.data, model);
|
||||
|
||||
// Scrub system flags
|
||||
const allowedFlags = CONFIG.SW5E.allowedActorFlags.reduce((obj, f) => {
|
||||
obj[f] = null;
|
||||
return obj;
|
||||
}, {});
|
||||
if (actorData.flags.sw5e) {
|
||||
actorData.flags.sw5e = filterObject(actorData.flags.sw5e, allowedFlags);
|
||||
}
|
||||
|
||||
// Return the scrubbed data
|
||||
return actorData;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Migrate a single Item entity to incorporate latest data model changes
|
||||
*
|
||||
* @param {object} item Item data to migrate
|
||||
* @return {object} The updateData to apply
|
||||
*/
|
||||
export const migrateItemData = function (item) {
|
||||
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) {
|
||||
export const migrateItemData = function(item) {
|
||||
const updateData = {};
|
||||
_migrateItemClassPowerCasting(item, updateData);
|
||||
_migrateItemAttunement(item, updateData);
|
||||
await _migrateItemPower(item, actor, updateData);
|
||||
|
||||
// Remove deprecated fields
|
||||
_migrateRemoveDeprecated(item, updateData);
|
||||
|
||||
// Return the migrated update data
|
||||
return updateData;
|
||||
};
|
||||
|
||||
|
@ -241,470 +160,65 @@ export const migrateActorItemData = async function (item, actor) {
|
|||
* @param {Object} scene The Scene data to Update
|
||||
* @return {Object} The updateData to apply
|
||||
*/
|
||||
export const migrateSceneData = async function (scene) {
|
||||
const tokens = await Promise.all(
|
||||
scene.tokens.map(async (token) => {
|
||||
const t = token.toJSON();
|
||||
if (!t.actorId || t.actorLink) {
|
||||
export const migrateSceneData = function(scene) {
|
||||
const tokens = duplicate(scene.tokens);
|
||||
return {
|
||||
tokens: tokens.map(t => {
|
||||
if (!t.actorId || t.actorLink || !t.actorData.data) {
|
||||
t.actorData = {};
|
||||
} else if (!game.actors.has(t.actorId)) {
|
||||
return t;
|
||||
}
|
||||
const token = new Token(t);
|
||||
if ( !token.actor ) {
|
||||
t.actorId = null;
|
||||
t.actorData = {};
|
||||
} else if (!t.actorLink) {
|
||||
const actorData = duplicate(t.actorData);
|
||||
actorData.type = token.actor?.type;
|
||||
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);
|
||||
} else if ( !t.actorLink ) {
|
||||
const updateData = migrateActorData(token.data.actorData);
|
||||
t.actorData = mergeObject(token.data.actorData, updateData);
|
||||
}
|
||||
return t;
|
||||
})
|
||||
);
|
||||
return {tokens};
|
||||
};
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Low level migration utilities
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Migrate the actor bonuses object
|
||||
* @private
|
||||
*/
|
||||
function _migrateActorBonuses(actor, updateData) {
|
||||
const b = game.system.model.Actor.character.bonuses;
|
||||
for ( let k of Object.keys(actor.data.bonuses || {}) ) {
|
||||
if ( k in b ) updateData[`data.bonuses.${k}`] = b[k];
|
||||
else updateData[`data.bonuses.-=${k}`] = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
|
||||
/**
|
||||
* Update an NPC Actor's data based on compendium
|
||||
* @param {Object} actor The data object for an Actor
|
||||
* @return {Object} The updated Actor
|
||||
* A general migration to remove all fields from the data model which are flagged with a _deprecated tag
|
||||
* @private
|
||||
*/
|
||||
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));
|
||||
const _migrateRemoveDeprecated = function(ent, updateData) {
|
||||
const flat = flattenObject(ent.data);
|
||||
|
||||
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");
|
||||
// Identify objects to deprecate
|
||||
const toDeprecate = Object.entries(flat).filter(e => e[0].endsWith("_deprecated") && (e[1] === true)).map(e => {
|
||||
let parent = e[0].split(".");
|
||||
parent.pop();
|
||||
return parent.join(".");
|
||||
});
|
||||
|
||||
// Remove them
|
||||
for ( let k of toDeprecate ) {
|
||||
let parts = k.split(".");
|
||||
parts[parts.length-1] = "-=" + parts[parts.length-1];
|
||||
updateData[`data.${parts.join(".")}`] = null;
|
||||
}
|
||||
|
||||
//merge object
|
||||
actorData = mergeObject(actorData, updateData);
|
||||
// Return the scrubbed data
|
||||
return actor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate the actor speed string to movement object
|
||||
* @private
|
||||
*/
|
||||
function _migrateActorMovement(actorData, updateData) {
|
||||
const ad = actorData.data;
|
||||
|
||||
// Work is needed if old data is present
|
||||
const old = actorData.type === "vehicle" ? ad?.attributes?.speed : ad?.attributes?.speed?.value;
|
||||
const hasOld = old !== undefined;
|
||||
if (hasOld) {
|
||||
// If new data is not present, migrate the old data
|
||||
const hasNew = ad?.attributes?.movement?.walk !== undefined;
|
||||
if (!hasNew && typeof old === "string") {
|
||||
const s = (old || "").split(" ");
|
||||
if (s.length > 0)
|
||||
updateData["data.attributes.movement.walk"] = Number.isNumeric(s[0]) ? parseInt(s[0]) : null;
|
||||
}
|
||||
|
||||
// Remove the old attribute
|
||||
updateData["data.attributes.-=speed"] = null;
|
||||
}
|
||||
return updateData;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Migrate the actor speed string to movement object
|
||||
* @private
|
||||
*/
|
||||
function _migrateActorPowers(actorData, updateData) {
|
||||
const ad = actorData.data;
|
||||
|
||||
// If new Force & Tech data is not present, create it
|
||||
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.
|
||||
* @param {Compendium} pack The compendium pack to clean
|
||||
* @private
|
||||
*/
|
||||
export async function purgeFlags(pack) {
|
||||
const cleanFlags = (flags) => {
|
||||
const flags5e = flags.sw5e || null;
|
||||
return flags5e ? {sw5e: flags5e} : {};
|
||||
};
|
||||
await pack.configure({locked: false});
|
||||
const content = await pack.getContent();
|
||||
for (let entity of content) {
|
||||
const update = {_id: entity.id, flags: cleanFlags(entity.data.flags)};
|
||||
if (pack.entity === "Actor") {
|
||||
update.items = entity.data.items.map((i) => {
|
||||
i.flags = cleanFlags(i.flags);
|
||||
return i;
|
||||
});
|
||||
}
|
||||
await pack.updateEntity(update, {recursive: false});
|
||||
console.log(`Purged flags from ${entity.name}`);
|
||||
}
|
||||
await pack.configure({locked: true});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Purge the data model of any inner objects which have been flagged as _deprecated.
|
||||
* @param {object} data The data to clean
|
||||
* @private
|
||||
*/
|
||||
export function removeDeprecatedObjects(data) {
|
||||
for (let [k, v] of Object.entries(data)) {
|
||||
if (getType(v) === "Object") {
|
||||
if (v._deprecated === true) {
|
||||
console.log(`Deleting deprecated object key ${k}`);
|
||||
delete data[k];
|
||||
} else removeDeprecatedObjects(v);
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import {SW5E} from "../config.js";
|
||||
import { SW5E } from "../config.js";
|
||||
|
||||
/**
|
||||
* A helper class for building MeasuredTemplates for 5e powers and abilities
|
||||
* @extends {MeasuredTemplate}
|
||||
*/
|
||||
export default class AbilityTemplate extends MeasuredTemplate {
|
||||
export class AbilityTemplate extends MeasuredTemplate {
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
@ -13,12 +14,12 @@ export default class AbilityTemplate extends MeasuredTemplate {
|
|||
static fromItem(item) {
|
||||
const target = getProperty(item.data, "data.target") || {};
|
||||
const templateShape = SW5E.areaTargetTypes[target.type];
|
||||
if (!templateShape) return null;
|
||||
if ( !templateShape ) return null;
|
||||
|
||||
// Prepare template data
|
||||
const templateData = {
|
||||
t: templateShape,
|
||||
user: game.user.data._id,
|
||||
user: game.user._id,
|
||||
distance: target.value,
|
||||
direction: 0,
|
||||
x: 0,
|
||||
|
@ -27,48 +28,37 @@ export default class AbilityTemplate extends MeasuredTemplate {
|
|||
};
|
||||
|
||||
// Additional type-specific data
|
||||
switch (templateShape) {
|
||||
case "cone":
|
||||
templateData.angle = CONFIG.MeasuredTemplate.defaults.angle;
|
||||
switch ( templateShape ) {
|
||||
case "cone": // 5e cone RAW should be 53.13 degrees
|
||||
templateData.angle = 53.13;
|
||||
break;
|
||||
case "rect": // 5e rectangular AoEs are always cubes
|
||||
templateData.distance = Math.hypot(target.value, target.value);
|
||||
templateData.width = target.value;
|
||||
templateData.direction = 45;
|
||||
break;
|
||||
case "ray": // 5e rays are most commonly 1 square (5 ft) in width
|
||||
templateData.width = target.width ?? canvas.dimensions.distance;
|
||||
case "ray": // 5e rays are most commonly 5ft wide
|
||||
templateData.width = 5;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// Return the template constructed from the item data
|
||||
const cls = CONFIG.MeasuredTemplate.documentClass;
|
||||
const template = new cls(templateData, {parent: canvas.scene});
|
||||
const object = new this(template);
|
||||
object.item = item;
|
||||
object.actorSheet = item.actor?.sheet || null;
|
||||
return object;
|
||||
return new this(templateData);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Creates a preview of the power template
|
||||
* @param {Event} event The initiating click event
|
||||
*/
|
||||
drawPreview() {
|
||||
drawPreview(event) {
|
||||
const initialLayer = canvas.activeLayer;
|
||||
|
||||
// Draw the template and switch to the template layer
|
||||
this.draw();
|
||||
this.layer.activate();
|
||||
this.layer.preview.addChild(this);
|
||||
|
||||
// Hide the sheet that originated the preview
|
||||
if (this.actorSheet) this.actorSheet.minimize();
|
||||
|
||||
// Activate interactivity
|
||||
this.activatePreviewListeners(initialLayer);
|
||||
}
|
||||
|
||||
|
@ -83,43 +73,48 @@ export default class AbilityTemplate extends MeasuredTemplate {
|
|||
let moveTime = 0;
|
||||
|
||||
// Update placement (mouse-move)
|
||||
handlers.mm = (event) => {
|
||||
handlers.mm = event => {
|
||||
event.stopPropagation();
|
||||
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 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();
|
||||
moveTime = now;
|
||||
};
|
||||
|
||||
// Cancel the workflow (right-click)
|
||||
handlers.rc = (event) => {
|
||||
handlers.rc = event => {
|
||||
this.layer.preview.removeChildren();
|
||||
canvas.stage.off("mousemove", handlers.mm);
|
||||
canvas.stage.off("mousedown", handlers.lc);
|
||||
canvas.app.view.oncontextmenu = null;
|
||||
canvas.app.view.onwheel = null;
|
||||
initialLayer.activate();
|
||||
this.actorSheet.maximize();
|
||||
};
|
||||
|
||||
// Confirm the workflow (left-click)
|
||||
handlers.lc = (event) => {
|
||||
handlers.lc = event => {
|
||||
handlers.rc(event);
|
||||
const destination = canvas.grid.getSnappedPosition(this.data.x, this.data.y, 2);
|
||||
this.data.update(destination);
|
||||
canvas.scene.createEmbeddedDocuments("MeasuredTemplate", [this.data]);
|
||||
|
||||
// Confirm final snapped position
|
||||
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)
|
||||
handlers.mw = (event) => {
|
||||
if (event.ctrlKey) event.preventDefault(); // Avoid zooming the browser window
|
||||
handlers.mw = event => {
|
||||
if ( event.ctrlKey ) event.preventDefault(); // Avoid zooming the browser window
|
||||
event.stopPropagation();
|
||||
let delta = canvas.grid.type > CONST.GRID_TYPES.SQUARE ? 30 : 15;
|
||||
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();
|
||||
};
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
export const registerSystemSettings = function () {
|
||||
export const registerSystemSettings = function() {
|
||||
|
||||
/**
|
||||
* Track the system version upon which point a migration was last applied
|
||||
*/
|
||||
|
@ -6,25 +7,8 @@ export const registerSystemSettings = function () {
|
|||
name: "System Migration Version",
|
||||
scope: "world",
|
||||
config: false,
|
||||
type: String,
|
||||
default: game.system.data.version
|
||||
});
|
||||
|
||||
/**
|
||||
* Register resting variants
|
||||
*/
|
||||
game.settings.register("sw5e", "restVariant", {
|
||||
name: "SETTINGS.5eRestN",
|
||||
hint: "SETTINGS.5eRestL",
|
||||
scope: "world",
|
||||
config: true,
|
||||
default: "normal",
|
||||
type: String,
|
||||
choices: {
|
||||
normal: "SETTINGS.5eRestPHB",
|
||||
gritty: "SETTINGS.5eRestGritty",
|
||||
epic: "SETTINGS.5eRestEpic"
|
||||
}
|
||||
type: Number,
|
||||
default: 0
|
||||
});
|
||||
|
||||
/**
|
||||
|
@ -38,24 +22,30 @@ export const registerSystemSettings = function () {
|
|||
default: "555",
|
||||
type: String,
|
||||
choices: {
|
||||
555: "SETTINGS.5eDiagPHB",
|
||||
5105: "SETTINGS.5eDiagDMG",
|
||||
EUCL: "SETTINGS.5eDiagEuclidean"
|
||||
"555": "SETTINGS.5eDiagPHB",
|
||||
"5105": "SETTINGS.5eDiagDMG"
|
||||
},
|
||||
onChange: (rule) => (canvas.grid.diagonalRule = rule)
|
||||
onChange: rule => canvas.grid.diagonalRule = rule
|
||||
});
|
||||
|
||||
/**
|
||||
* Register Initiative formula setting
|
||||
*/
|
||||
function _set5eInitiative(tiebreaker) {
|
||||
CONFIG.Combat.initiative.tiebreaker = tiebreaker;
|
||||
CONFIG.Combat.initiative.decimals = tiebreaker ? 2 : 0;
|
||||
if ( ui.combat && ui.combat._rendered ) ui.combat.render();
|
||||
}
|
||||
game.settings.register("sw5e", "initiativeDexTiebreaker", {
|
||||
name: "SETTINGS.5eInitTBN",
|
||||
hint: "SETTINGS.5eInitTBL",
|
||||
scope: "world",
|
||||
config: true,
|
||||
default: false,
|
||||
type: Boolean
|
||||
type: Boolean,
|
||||
onChange: enable => _set5eInitiative(enable)
|
||||
});
|
||||
_set5eInitiative(game.settings.get("sw5e", "initiativeDexTiebreaker"));
|
||||
|
||||
/**
|
||||
* Require Currency Carrying Weight
|
||||
|
@ -78,6 +68,18 @@ export const registerSystemSettings = function () {
|
|||
scope: "world",
|
||||
config: true,
|
||||
default: false,
|
||||
type: Boolean,
|
||||
});
|
||||
|
||||
/**
|
||||
* Option to automatically create Power Measured Template on roll
|
||||
*/
|
||||
game.settings.register("sw5e", "alwaysPlacePowerTemplate", {
|
||||
name: "SETTINGS.5eAutoPowerTemplateN",
|
||||
hint: "SETTINGS.5eAutoPowerTemplateL",
|
||||
scope: "client",
|
||||
config: true,
|
||||
default: false,
|
||||
type: Boolean
|
||||
});
|
||||
|
||||
|
@ -91,7 +93,7 @@ export const registerSystemSettings = function () {
|
|||
config: true,
|
||||
default: false,
|
||||
type: Boolean,
|
||||
onChange: (s) => {
|
||||
onChange: s => {
|
||||
ui.chat.render();
|
||||
}
|
||||
});
|
||||
|
@ -99,10 +101,10 @@ export const registerSystemSettings = function () {
|
|||
/**
|
||||
* Option to allow GMs to restrict polymorphing to GMs only.
|
||||
*/
|
||||
game.settings.register("sw5e", "allowPolymorphing", {
|
||||
name: "SETTINGS.5eAllowPolymorphingN",
|
||||
hint: "SETTINGS.5eAllowPolymorphingL",
|
||||
scope: "world",
|
||||
game.settings.register('sw5e', 'allowPolymorphing', {
|
||||
name: 'SETTINGS.5eAllowPolymorphingN',
|
||||
hint: 'SETTINGS.5eAllowPolymorphingL',
|
||||
scope: 'world',
|
||||
config: true,
|
||||
default: false,
|
||||
type: Boolean
|
||||
|
@ -111,8 +113,8 @@ export const registerSystemSettings = function () {
|
|||
/**
|
||||
* Remember last-used polymorph settings.
|
||||
*/
|
||||
game.settings.register("sw5e", "polymorphSettings", {
|
||||
scope: "client",
|
||||
game.settings.register('sw5e', 'polymorphSettings', {
|
||||
scope: 'client',
|
||||
default: {
|
||||
keepPhysical: false,
|
||||
keepMental: false,
|
||||
|
@ -129,16 +131,4 @@ export const registerSystemSettings = function () {
|
|||
transformTokens: true
|
||||
}
|
||||
});
|
||||
game.settings.register("sw5e", "colorTheme", {
|
||||
name: "SETTINGS.SWColorN",
|
||||
hint: "SETTINGS.SWColorL",
|
||||
scope: "world",
|
||||
config: true,
|
||||
default: "light",
|
||||
type: String,
|
||||
choices: {
|
||||
light: "SETTINGS.SWColorLight",
|
||||
dark: "SETTINGS.SWColorDark"
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -3,33 +3,23 @@
|
|||
* Pre-loaded templates are compiled and cached for fast access when rendering
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const preloadHandlebarsTemplates = async function () {
|
||||
return loadTemplates([
|
||||
// Shared Partials
|
||||
"systems/sw5e/templates/actors/parts/active-effects.html",
|
||||
export const preloadHandlebarsTemplates = async function() {
|
||||
|
||||
// Define template paths to load
|
||||
const templatePaths = [
|
||||
|
||||
// Actor Sheet Partials
|
||||
"systems/sw5e/templates/actors/oldActor/parts/actor-traits.html",
|
||||
"systems/sw5e/templates/actors/oldActor/parts/actor-inventory.html",
|
||||
"systems/sw5e/templates/actors/oldActor/parts/actor-features.html",
|
||||
"systems/sw5e/templates/actors/oldActor/parts/actor-powerbook.html",
|
||||
"systems/sw5e/templates/actors/oldActor/parts/actor-notes.html",
|
||||
|
||||
"systems/sw5e/templates/actors/newActor/parts/swalt-biography.html",
|
||||
"systems/sw5e/templates/actors/newActor/parts/swalt-core.html",
|
||||
"systems/sw5e/templates/actors/newActor/parts/swalt-crew.html",
|
||||
"systems/sw5e/templates/actors/newActor/parts/swalt-active-effects.html",
|
||||
"systems/sw5e/templates/actors/newActor/parts/swalt-features.html",
|
||||
"systems/sw5e/templates/actors/newActor/parts/swalt-inventory.html",
|
||||
"systems/sw5e/templates/actors/newActor/parts/swalt-force-powerbook.html",
|
||||
"systems/sw5e/templates/actors/newActor/parts/swalt-tech-powerbook.html",
|
||||
"systems/sw5e/templates/actors/newActor/parts/swalt-resources.html",
|
||||
"systems/sw5e/templates/actors/newActor/parts/swalt-traits.html",
|
||||
"systems/sw5e/templates/actors/parts/actor-traits.html",
|
||||
"systems/sw5e/templates/actors/parts/actor-inventory.html",
|
||||
"systems/sw5e/templates/actors/parts/actor-features.html",
|
||||
"systems/sw5e/templates/actors/parts/actor-powerbook.html",
|
||||
|
||||
// Item Sheet Partials
|
||||
"systems/sw5e/templates/items/parts/item-action.html",
|
||||
"systems/sw5e/templates/items/parts/item-activation.html",
|
||||
"systems/sw5e/templates/items/parts/item-description.html",
|
||||
"systems/sw5e/templates/items/parts/item-mountable.html"
|
||||
]);
|
||||
"systems/sw5e/templates/items/parts/item-description.html"
|
||||
];
|
||||
|
||||
// Load the template parts
|
||||
return loadTemplates(templatePaths);
|
||||
};
|
||||
|
|
102
module/token.js
102
module/token.js
|
@ -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);
|
||||
}
|
||||
}
|
3105
package-lock.json
generated
3105
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -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"
|
||||
}
|
||||
}
|
BIN
packs/Icons/Ammunition/Dart.png
Normal file
BIN
packs/Icons/Ammunition/Dart.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 39 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.1 KiB |
BIN
packs/Icons/Ammunition/Missile, Fragmentation.png
Normal file
BIN
packs/Icons/Ammunition/Missile, Fragmentation.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 40 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.3 KiB |
BIN
packs/Icons/Ammunition/Power Cell.png
Normal file
BIN
packs/Icons/Ammunition/Power Cell.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 39 KiB |
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue