diff --git a/eslint.config.mjs b/eslint.config.mjs
index 4808e2f..86ac485 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -84,7 +84,7 @@ export default [
rules: {
'arrow-body-style': 'off',
'comma-dangle': 'off',
- 'linebreak-style': ['error', 'windows'],
+ 'linebreak-style': ['error', 'unix'],
'indent': ['error', 2],
'import/extensions': 'off',
'max-len': ['error', {'code': 110, 'ignoreComments': true, 'ignoreStrings': true, 'ignoreTemplateLiterals': true}],
diff --git a/gulpfile.js b/gulpfile.js
index 9ade2fd..e0a7f80 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -1,402 +1,402 @@
-const gulp = require('gulp');
-const sass = require('gulp-sass')(require('sass'));
-const eslint = require('gulp-eslint');
-const fs = require('fs-extra');
-const path = require('path');
-const git = require('gulp-git');
-const archiver = require('archiver');
-const stringify = require('json-stringify-pretty-compact');
-const argv = require('yargs').argv;
-const chalk = require('chalk');
-
-gulp.task('sass', function(cb) {
- gulp
- .src('src/styles/*.scss')
- .pipe(sass())
- .pipe(
- gulp.dest(function(f) {
- return f.base;
- })
- );
- cb();
-});
-
-gulp.task(
- 'default',
- gulp.series('sass', function(cb) {
- gulp.watch('styles/*.scss', gulp.series('sass'));
- cb();
- })
-);
-
-/* Get Configuration File */
-function getConfig() {
- const configPath = path.resolve(process.cwd(), 'foundryconfig.json');
- let config;
-
- if (fs.existsSync(configPath)) {
- config = fs.readJSONSync(configPath);
- return config;
- } else {
- return;
- }
-}
-
-/* Get Manifest */
-function getManifest() {
- const json = {};
-
- if (fs.existsSync('src')) {
- json.root = 'src';
- } else {
- json.root = 'dist';
- }
-
- const systemPath = path.join(json.root, 'system.json');
-
- if (fs.existsSync(systemPath)) {
- json.file = fs.readJSONSync(systemPath);
- json.name = 'system.json';
- } else {
- return;
- }
-
- return json;
-}
-
-/* Build Sass */
-function buildSASS() {
- return gulp
- .src('src/styles/*.scss')
- .pipe(sass().on('error', sass.logError))
- .pipe(gulp.dest('dist')).pipe(gulp.dest('src'));
-}
-
-/* Build packs for system.json */
-// async function replaceTokenSystemJson() {
-// return gulp
-// .src(src.)
-// }
-
-/* Copy Files */
-async function copyFiles() {
- const statics = [
- 'lang',
- 'assets',
- 'module',
- 'templates',
- 'system.json',
- 'template.json',
- ];
- try {
- for (const file of statics) {
- if (fs.existsSync(path.join('src', file))) {
- await fs.copy(path.join('src', file), path.join('dist', file));
- }
- }
- return Promise.resolve();
- } catch (err) {
- Promise.reject(err);
- }
-}
-
-/* Build Watch */
-function buildWatch() {
- gulp.watch('src/**/*.scss', {ignoreInitial: false}, buildSASS);
- gulp.watch(
- ['src/lang', 'src/templates', 'src/*.json'],
- {ignoreInitial: false},
- copyFiles
- );
-}
-
-/* Clean */
-async function clean() {
- const name = path.basename(path.resolve('.'));
- const files = [];
- console.log(path.join('src', 'styles', `${name}.scss`));
-
- files.push(
- 'lang',
- 'templates',
- 'assets',
- 'packs',
- 'module',
- `${name}.js`,
- 'system.json',
- 'template.json'
- );
-
-
- // If the project uses SASS push SASS
- if (fs.existsSync(path.join('src', 'styles', `${name}.scss`))) {
- files.push(`${name}.css`);
- }
-
- console.log(' ', chalk.yellow('Files to clean:'));
- console.log(' ', chalk.blueBright(files.join('\n ')));
-
- // Attempt to remove the files
- try {
- for (const filePath of files) {
- await fs.remove(path.join('dist', filePath));
- }
- return Promise.resolve();
- } catch (err) {
- Promise.reject(err);
- }
-}
-
-function defaultDataPath() {
- switch (process.platform) {
- case 'win32':
- return path.resolve(process.env.localappdata, 'FoundryVTT');
- case 'linux':
- return path.resolve(process.env.HOME, '.local', 'share', 'FoundryVTT');
- case 'darwin':
- return path.resolve(process.env.HOME, 'Library', 'Application Support', 'FoundryVTT');
- default:
- throw Error('No known default for platform ${process.platform}');
- }
-}
-
-// Copy files to test location
-async function copyUserData() {
- const name = path.basename(path.resolve('.'));
- const config = fs.readJSONSync('foundryconfig.json');
-
- let destDir;
-
- try {
- if (fs.existsSync(path.resolve('.', 'dist', 'system.json')) ||
- fs.existsSync(path.resolve('.', 'src', 'system.json'))) {
- destDir = 'systems';
- } else {
- throw Error(
- `Could not find ${chalk.blueBright('system.json')}`
- );
- }
-
- let linkDir;
-
- if (!config.dataPath) {
- config.dataPath = defaultDataPath();
- }
-
- if (config.dataPath) {
- if (!fs.existsSync(path.join(config.dataPath, 'Data'))) {
- throw Error('User Data path invalid, no Data directory found');
- }
-
- linkDir = path.join(config.dataPath, 'Data', destDir, name);
- }
-
- if (argv.clean || argv.c) {
- console.log(
- chalk.yellow(`Removing build in ${chalk.blueBright(linkDir)}`)
- );
- await fs.remove(linkDir);
- } else {
- console.log(
- chalk.green(`Copying build to ${chalk.blueBright(linkDir)}`)
- );
-
- await fs.emptyDir(linkDir);
- await fs.copy('dist', linkDir);
- }
- return Promise.resolve();
- } catch (err) {
- console.log(err);
- Promise.reject(err);
- }
-}
-
-// Package build
-async function packageBuild() {
- const manifest = getManifest();
-
- return new Promise((resolve, reject) => {
- try {
- // Remove the package dir without doing anything else
- if (argv.clean || argv.c) {
- console.log(chalk.yellow('Removing all packaged files'));
- fs.removeSync('package');
- return;
- }
-
- // Ensure there is a directory to hold all the packaged versions
- fs.ensureDirSync('package');
-
- // Initialize the zip file
- const zipName = `${manifest.file.id}-v${manifest.file.version}.zip`;
- const zipFile = fs.createWriteStream(path.join('package', zipName));
- const zip = archiver('zip', {zlib: {level: 9}});
-
- zipFile.on('close', () => {
- console.log(chalk.green(zip.pointer() + ' total bytes'));
- console.log(
- chalk.green(`Zip file ${zipName} has been written`)
- );
- return resolve();
- });
-
- zip.on('error', (err) => {
- throw err;
- });
-
- zip.pipe(zipFile);
-
- // Add the directory with the final code
- zip.directory('dist/', manifest.file.id);
-
- zip.finalize();
- } catch (err) {
- return reject(err);
- }
- });
-}
-
-// Update version and URLs in the manifest JSON
-function updateManifest(cb) {
- const packageJson = fs.readJSONSync('package.json');
- const config = getConfig();
- const manifest = getManifest();
- const rawURL = config.rawURL;
- const repoURL = config.repository;
- const manifestRoot = manifest.root;
-
- if (!config) cb(Error(chalk.red('foundryconfig.json not found')));
- if (!manifest) cb(Error(chalk.red('Manifest JSON not found')));
- if (!rawURL || !repoURL) {
- cb(
- Error(
- chalk.red(
- 'Repository URLs not configured in foundryconfig.json'
- )
- )
- );
- }
-
- try {
- const version = argv.update || argv.u;
-
- /* Update version */
-
- const versionMatch = /^(\d{1,}).(\d{1,}).(\d{1,})$/;
- const currentVersion = manifest.file.version;
- let targetVersion = '';
-
- if (!version) {
- cb(Error('Missing version number'));
- }
-
- if (versionMatch.test(version)) {
- targetVersion = version;
- } else {
- targetVersion = currentVersion.replace(
- versionMatch,
- (substring, major, minor, patch) => {
- console.log(
- substring,
- Number(major) + 1,
- Number(minor) + 1,
- Number(patch) + 1
- );
- if (version === 'major') {
- return `${Number(major) + 1}.0.0`;
- } else if (version === 'minor') {
- return `${major}.${Number(minor) + 1}.0`;
- } else if (version === 'patch') {
- return `${major}.${minor}.${Number(patch) + 1}`;
- } else {
- return '';
- }
- }
- );
- }
-
- if (targetVersion === '') {
- return cb(Error(chalk.red('Error: Incorrect version arguments.')));
- }
-
- if (targetVersion === currentVersion) {
- return cb(
- Error(
- chalk.red(
- 'Error: Target version is identical to current version.'
- )
- )
- );
- }
- console.log(`Updating version number to '${targetVersion}'`);
-
- packageJson.version = targetVersion;
- manifest.file.version = targetVersion;
-
- /* Update URLs */
-
- const result = `${rawURL}/v${manifest.file.version}/package/${manifest.file.id}-v${manifest.file.version}.zip`;
-
- manifest.file.url = repoURL;
- manifest.file.manifest = `${rawURL}/master/${manifestRoot}/${manifest.name}`;
- manifest.file.download = result;
-
- const prettyProjectJson = stringify(manifest.file, {
- maxLength: 35,
- indent: '\t',
- });
-
- fs.writeJSONSync('package.json', packageJson, {spaces: '\t'});
- fs.writeFileSync(
- path.join(manifest.root, manifest.name),
- prettyProjectJson,
- 'utf8'
- );
-
- return cb();
- } catch (err) {
- cb(err);
- }
-}
-
-function gitAdd() {
- return gulp.src('package').pipe(git.add({args: '--no-all'}));
-}
-
-function gitCommit() {
- return gulp.src('./*').pipe(
- git.commit(`v${getManifest().file.version}`, {
- args: '-a',
- disableAppendPaths: true,
- })
- );
-}
-
-function gitTag() {
- const manifest = getManifest();
- return git.tag(
- `v${manifest.file.version}`,
- `Updated to ${manifest.file.version}`,
- (err) => {
- if (err) throw err;
- }
- );
-}
-
-const execGit = gulp.series(gitAdd, gitCommit, gitTag);
-
-const execBuild = gulp.parallel(buildSASS, copyFiles);
-
-exports.build = gulp.series(clean, execBuild);
-exports.watch = buildWatch;
-exports.clean = clean;
-exports.copy = copyUserData;
-exports.package = packageBuild;
-exports.update = updateManifest;
-exports.publish = gulp.series(
- clean,
- updateManifest,
- execBuild,
- packageBuild,
- execGit
-);
+const gulp = require('gulp');
+const sass = require('gulp-sass')(require('sass'));
+const eslint = require('gulp-eslint');
+const fs = require('fs-extra');
+const path = require('path');
+const git = require('gulp-git');
+const archiver = require('archiver');
+const stringify = require('json-stringify-pretty-compact');
+const argv = require('yargs').argv;
+const chalk = require('chalk');
+
+gulp.task('sass', function(cb) {
+ gulp
+ .src('src/styles/*.scss')
+ .pipe(sass())
+ .pipe(
+ gulp.dest(function(f) {
+ return f.base;
+ })
+ );
+ cb();
+});
+
+gulp.task(
+ 'default',
+ gulp.series('sass', function(cb) {
+ gulp.watch('styles/*.scss', gulp.series('sass'));
+ cb();
+ })
+);
+
+/* Get Configuration File */
+function getConfig() {
+ const configPath = path.resolve(process.cwd(), 'foundryconfig.json');
+ let config;
+
+ if (fs.existsSync(configPath)) {
+ config = fs.readJSONSync(configPath);
+ return config;
+ } else {
+ return;
+ }
+}
+
+/* Get Manifest */
+function getManifest() {
+ const json = {};
+
+ if (fs.existsSync('src')) {
+ json.root = 'src';
+ } else {
+ json.root = 'dist';
+ }
+
+ const systemPath = path.join(json.root, 'system.json');
+
+ if (fs.existsSync(systemPath)) {
+ json.file = fs.readJSONSync(systemPath);
+ json.name = 'system.json';
+ } else {
+ return;
+ }
+
+ return json;
+}
+
+/* Build Sass */
+function buildSASS() {
+ return gulp
+ .src('src/styles/*.scss')
+ .pipe(sass().on('error', sass.logError))
+ .pipe(gulp.dest('dist')).pipe(gulp.dest('src'));
+}
+
+/* Build packs for system.json */
+// async function replaceTokenSystemJson() {
+// return gulp
+// .src(src.)
+// }
+
+/* Copy Files */
+async function copyFiles() {
+ const statics = [
+ 'lang',
+ 'assets',
+ 'module',
+ 'templates',
+ 'system.json',
+ 'template.json',
+ ];
+ try {
+ for (const file of statics) {
+ if (fs.existsSync(path.join('src', file))) {
+ await fs.copy(path.join('src', file), path.join('dist', file));
+ }
+ }
+ return Promise.resolve();
+ } catch (err) {
+ Promise.reject(err);
+ }
+}
+
+/* Build Watch */
+function buildWatch() {
+ gulp.watch('src/**/*.scss', {ignoreInitial: false}, buildSASS);
+ gulp.watch(
+ ['src/lang', 'src/templates', 'src/*.json'],
+ {ignoreInitial: false},
+ copyFiles
+ );
+}
+
+/* Clean */
+async function clean() {
+ const name = path.basename(path.resolve('.'));
+ const files = [];
+ console.log(path.join('src', 'styles', `${name}.scss`));
+
+ files.push(
+ 'lang',
+ 'templates',
+ 'assets',
+ 'packs',
+ 'module',
+ `${name}.js`,
+ 'system.json',
+ 'template.json'
+ );
+
+
+ // If the project uses SASS push SASS
+ if (fs.existsSync(path.join('src', 'styles', `${name}.scss`))) {
+ files.push(`${name}.css`);
+ }
+
+ console.log(' ', chalk.yellow('Files to clean:'));
+ console.log(' ', chalk.blueBright(files.join('\n ')));
+
+ // Attempt to remove the files
+ try {
+ for (const filePath of files) {
+ await fs.remove(path.join('dist', filePath));
+ }
+ return Promise.resolve();
+ } catch (err) {
+ Promise.reject(err);
+ }
+}
+
+function defaultDataPath() {
+ switch (process.platform) {
+ case 'win32':
+ return path.resolve(process.env.localappdata, 'FoundryVTT');
+ case 'linux':
+ return path.resolve(process.env.HOME, '.local', 'share', 'FoundryVTT');
+ case 'darwin':
+ return path.resolve(process.env.HOME, 'Library', 'Application Support', 'FoundryVTT');
+ default:
+ throw Error('No known default for platform ${process.platform}');
+ }
+}
+
+// Copy files to test location
+async function copyUserData() {
+ const name = path.basename(path.resolve('.'));
+ const config = fs.readJSONSync('foundryconfig.json');
+
+ let destDir;
+
+ try {
+ if (fs.existsSync(path.resolve('.', 'dist', 'system.json')) ||
+ fs.existsSync(path.resolve('.', 'src', 'system.json'))) {
+ destDir = 'systems';
+ } else {
+ throw Error(
+ `Could not find ${chalk.blueBright('system.json')}`
+ );
+ }
+
+ let linkDir;
+
+ if (!config.dataPath) {
+ config.dataPath = defaultDataPath();
+ }
+
+ if (config.dataPath) {
+ if (!fs.existsSync(path.join(config.dataPath, 'Data'))) {
+ throw Error('User Data path invalid, no Data directory found');
+ }
+
+ linkDir = path.join(config.dataPath, 'Data', destDir, name);
+ }
+
+ if (argv.clean || argv.c) {
+ console.log(
+ chalk.yellow(`Removing build in ${chalk.blueBright(linkDir)}`)
+ );
+ await fs.remove(linkDir);
+ } else {
+ console.log(
+ chalk.green(`Copying build to ${chalk.blueBright(linkDir)}`)
+ );
+
+ await fs.emptyDir(linkDir);
+ await fs.copy('dist', linkDir);
+ }
+ return Promise.resolve();
+ } catch (err) {
+ console.log(err);
+ Promise.reject(err);
+ }
+}
+
+// Package build
+async function packageBuild() {
+ const manifest = getManifest();
+
+ return new Promise((resolve, reject) => {
+ try {
+ // Remove the package dir without doing anything else
+ if (argv.clean || argv.c) {
+ console.log(chalk.yellow('Removing all packaged files'));
+ fs.removeSync('package');
+ return;
+ }
+
+ // Ensure there is a directory to hold all the packaged versions
+ fs.ensureDirSync('package');
+
+ // Initialize the zip file
+ const zipName = `${manifest.file.id}-v${manifest.file.version}.zip`;
+ const zipFile = fs.createWriteStream(path.join('package', zipName));
+ const zip = archiver('zip', {zlib: {level: 9}});
+
+ zipFile.on('close', () => {
+ console.log(chalk.green(zip.pointer() + ' total bytes'));
+ console.log(
+ chalk.green(`Zip file ${zipName} has been written`)
+ );
+ return resolve();
+ });
+
+ zip.on('error', (err) => {
+ throw err;
+ });
+
+ zip.pipe(zipFile);
+
+ // Add the directory with the final code
+ zip.directory('dist/', manifest.file.id);
+
+ zip.finalize();
+ } catch (err) {
+ return reject(err);
+ }
+ });
+}
+
+// Update version and URLs in the manifest JSON
+function updateManifest(cb) {
+ const packageJson = fs.readJSONSync('package.json');
+ const config = getConfig();
+ const manifest = getManifest();
+ const rawURL = config.rawURL;
+ const repoURL = config.repository;
+ const manifestRoot = manifest.root;
+
+ if (!config) cb(Error(chalk.red('foundryconfig.json not found')));
+ if (!manifest) cb(Error(chalk.red('Manifest JSON not found')));
+ if (!rawURL || !repoURL) {
+ cb(
+ Error(
+ chalk.red(
+ 'Repository URLs not configured in foundryconfig.json'
+ )
+ )
+ );
+ }
+
+ try {
+ const version = argv.update || argv.u;
+
+ /* Update version */
+
+ const versionMatch = /^(\d{1,}).(\d{1,}).(\d{1,})$/;
+ const currentVersion = manifest.file.version;
+ let targetVersion = '';
+
+ if (!version) {
+ cb(Error('Missing version number'));
+ }
+
+ if (versionMatch.test(version)) {
+ targetVersion = version;
+ } else {
+ targetVersion = currentVersion.replace(
+ versionMatch,
+ (substring, major, minor, patch) => {
+ console.log(
+ substring,
+ Number(major) + 1,
+ Number(minor) + 1,
+ Number(patch) + 1
+ );
+ if (version === 'major') {
+ return `${Number(major) + 1}.0.0`;
+ } else if (version === 'minor') {
+ return `${major}.${Number(minor) + 1}.0`;
+ } else if (version === 'patch') {
+ return `${major}.${minor}.${Number(patch) + 1}`;
+ } else {
+ return '';
+ }
+ }
+ );
+ }
+
+ if (targetVersion === '') {
+ return cb(Error(chalk.red('Error: Incorrect version arguments.')));
+ }
+
+ if (targetVersion === currentVersion) {
+ return cb(
+ Error(
+ chalk.red(
+ 'Error: Target version is identical to current version.'
+ )
+ )
+ );
+ }
+ console.log(`Updating version number to '${targetVersion}'`);
+
+ packageJson.version = targetVersion;
+ manifest.file.version = targetVersion;
+
+ /* Update URLs */
+
+ const result = `${rawURL}/v${manifest.file.version}/package/${manifest.file.id}-v${manifest.file.version}.zip`;
+
+ manifest.file.url = repoURL;
+ manifest.file.manifest = `${rawURL}/master/${manifestRoot}/${manifest.name}`;
+ manifest.file.download = result;
+
+ const prettyProjectJson = stringify(manifest.file, {
+ maxLength: 35,
+ indent: '\t',
+ });
+
+ fs.writeJSONSync('package.json', packageJson, {spaces: '\t'});
+ fs.writeFileSync(
+ path.join(manifest.root, manifest.name),
+ prettyProjectJson,
+ 'utf8'
+ );
+
+ return cb();
+ } catch (err) {
+ cb(err);
+ }
+}
+
+function gitAdd() {
+ return gulp.src('package').pipe(git.add({args: '--no-all'}));
+}
+
+function gitCommit() {
+ return gulp.src('./*').pipe(
+ git.commit(`v${getManifest().file.version}`, {
+ args: '-a',
+ disableAppendPaths: true,
+ })
+ );
+}
+
+function gitTag() {
+ const manifest = getManifest();
+ return git.tag(
+ `v${manifest.file.version}`,
+ `Updated to ${manifest.file.version}`,
+ (err) => {
+ if (err) throw err;
+ }
+ );
+}
+
+const execGit = gulp.series(gitAdd, gitCommit, gitTag);
+
+const execBuild = gulp.parallel(buildSASS, copyFiles);
+
+exports.build = gulp.series(clean, execBuild);
+exports.watch = buildWatch;
+exports.clean = clean;
+exports.copy = copyUserData;
+exports.package = packageBuild;
+exports.update = updateManifest;
+exports.publish = gulp.series(
+ clean,
+ updateManifest,
+ execBuild,
+ packageBuild,
+ execGit
+);
diff --git a/src/assets/icons/chat-bubble.svg b/src/assets/icons/chat-bubble.svg
index 8dfc049..b4b13a5 100644
--- a/src/assets/icons/chat-bubble.svg
+++ b/src/assets/icons/chat-bubble.svg
@@ -1 +1,20 @@
-
\ No newline at end of file
+
+
+
\ No newline at end of file
diff --git a/src/lang/de.json b/src/lang/de.json
index 6882505..ab19a4e 100644
--- a/src/lang/de.json
+++ b/src/lang/de.json
@@ -66,6 +66,7 @@
"sta.actor.belonging.starshipweapon2e.title": "Raumschiff-Waffe",
"sta.actor.belonging.talent.title": "Talente",
"sta.actor.belonging.value.title": "Überzeugungen",
+ "sta.actor.belonging.trait.title": "Trait",
"sta.actor.belonging.weapon.includescale": "Inkl. Schiffsgröße?",
"sta.actor.belonging.weapon.title": "Waffen",
"sta.actor.belonging.weapon.dmg": "Schaden",
@@ -169,7 +170,8 @@
"sta.actor.starship.shieldmod": "Schild Modifikator",
"sta.actor.starship.shaken": "Erschüttert",
"sta.actor.starship.reservepower": "Reserve Energie",
- "sta.actor.starship.shieldcrewmod": "Schild/Crew Mod",
+ "sta.actor.starship.shieldmod": "Schildmodifikator",
+ "sta.actor.starship.crewmod": "Besatzungsmodifikator",
"sta.actor.smallcraft.parent": "Mutterschiff",
"sta.actor.smallcraft.child": "Shuttletyp",
@@ -218,6 +220,10 @@
"sta.apps.removemomentum": "removed {0} momentum from the pool",
"sta.apps.addthreat": "adde{0} Bedrohung zum Pool hinzugefügtd {0} threat to the pool",
"sta.apps.removethreat": "{0} Bedrohung aus dem Pool entfernt",
+ "sta.apps.deleteitem": "Element löschen",
+ "sta.apps.deleteconfirm": "Sind Sie sicher, dass Sie dieses Element löschen möchten?",
+ "sta.apps.yes": "Ja",
+ "sta.apps.no": "Nein",
"sta.roll.success": "Erfolg",
"sta.roll.successPlural": "Erfolge",
diff --git a/src/lang/en.json b/src/lang/en.json
index 2e63cdb..12e2559 100644
--- a/src/lang/en.json
+++ b/src/lang/en.json
@@ -66,6 +66,7 @@
"sta.actor.belonging.starshipweapon2e.title": "Starship Weapon",
"sta.actor.belonging.talent.title": "Talent",
"sta.actor.belonging.value.title": "Value",
+ "sta.actor.belonging.trait.title": "Trait",
"sta.actor.belonging.weapon.includescale": "Include Scale?",
"sta.actor.belonging.weapon.title": "Weapons",
"sta.actor.belonging.weapon.dmg": "Dmg",
@@ -169,7 +170,8 @@
"sta.actor.starship.shieldmod": "Shield Modifier",
"sta.actor.starship.shaken": "Shaken",
"sta.actor.starship.reservepower": "Reserve Power",
- "sta.actor.starship.shieldcrewmod": "Shield/Crew Mod",
+ "sta.actor.starship.shieldmod": "Shield Modifier",
+ "sta.actor.starship.crewmod": "Crew Modifier",
"sta.actor.smallcraft.parent": "Parent Ship",
"sta.actor.smallcraft.child": "Small Craft Type",
@@ -218,6 +220,10 @@
"sta.apps.removemomentum": "removed {0} momentum from the pool",
"sta.apps.addthreat": "added {0} threat to the pool",
"sta.apps.removethreat": "removed {0} threat from the pool",
+ "sta.apps.deleteitem": "Delete Item",
+ "sta.apps.deleteconfirm": "Are you sure you want to delete this item?",
+ "sta.apps.yes": "Yes",
+ "sta.apps.no": "No",
"sta.roll.success": "Success",
"sta.roll.successPlural": "Successes",
diff --git a/src/lang/es.json b/src/lang/es.json
index db647dd..69d8b5b 100644
--- a/src/lang/es.json
+++ b/src/lang/es.json
@@ -66,6 +66,7 @@
"sta.actor.belonging.starshipweapon2e.title": "Arma estelar",
"sta.actor.belonging.talent.title": "Talento",
"sta.actor.belonging.value.title": "Valor",
+ "sta.actor.belonging.trait.title": "Trait",
"sta.actor.belonging.weapon.includescale": "¿Incluir Escala?",
"sta.actor.belonging.weapon.title": "Armamento",
"sta.actor.belonging.weapon.dmg": "Daño",
@@ -169,7 +170,8 @@
"sta.actor.starship.shieldmod": "Modificador de Escudos",
"sta.actor.starship.shaken": "Sacudida",
"sta.actor.starship.reservepower": "Energía de Reserva",
- "sta.actor.starship.shieldcrewmod": "Mod. Escudos/Tripulación",
+ "sta.actor.starship.shieldmod": "Modificador de Escudo",
+ "sta.actor.starship.crewmod": "Modificador de Tripulación",
"sta.actor.smallcraft.parent": "Nave nodriza",
"sta.actor.smallcraft.child": "Tipo vehículo pequeño",
@@ -218,6 +220,10 @@
"sta.apps.removemomentum": "Retiró {0} puntos de Inercia al fondo común",
"sta.apps.addthreat": "Añadió {0} puntos de Amenaza a la reserva",
"sta.apps.removethreat": "Retiró {0} puntos de Amenaza a la reserva",
+ "sta.apps.deleteitem": "Eliminar Elemento",
+ "sta.apps.deleteconfirm": "¿Está seguro de que desea eliminar este elemento?",
+ "sta.apps.yes": "Sí",
+ "sta.apps.no": "No",
"sta.roll.success": "Éxito",
"sta.roll.successPlural": "Éxitos",
@@ -269,4 +275,4 @@
"sta.dice.dsn.ufp.red": "UFP Rojo",
"sta.dice.dsn.ufp.theme.black": "Star Trek Adventures UFP (Negro)",
"sta.dice.dsn.ufp.theme.white": "Star Trek Adventures UFP (Blanco)"
-}
+}
\ No newline at end of file
diff --git a/src/lang/fr.json b/src/lang/fr.json
index 9d61668..cddba25 100644
--- a/src/lang/fr.json
+++ b/src/lang/fr.json
@@ -66,6 +66,7 @@
"sta.actor.belonging.starshipweapon2e.title": "Armement du Vaisseau",
"sta.actor.belonging.talent.title": "Talent",
"sta.actor.belonging.value.title": "Idéaux",
+ "sta.actor.belonging.trait.title": "Trait",
"sta.actor.belonging.weapon.includescale": "Inclure l'échelle?",
"sta.actor.belonging.weapon.title": "Armes",
"sta.actor.belonging.weapon.dmg": "Dmg",
@@ -169,7 +170,8 @@
"sta.actor.starship.shieldmod": "Modificateur de Bouclier",
"sta.actor.starship.shaken": "Secoué",
"sta.actor.starship.reservepower": "Puissance de Réserve",
- "sta.actor.starship.shieldcrewmod": "Mod Bouclier/Équipage",
+ "sta.actor.starship.shieldmod": "Modificateur de Bouclier",
+ "sta.actor.starship.crewmod": "Modificateur d'Équipage",
"sta.actor.smallcraft.parent": "Vaisseau mère",
"sta.actor.smallcraft.child": "Type de vaisseau Enfant",
@@ -218,6 +220,10 @@
"sta.apps.removemomentum": "retiré {0} momentum du groupe",
"sta.apps.addthreat": "ajouté {0} menace au groupe",
"sta.apps.removethreat": "retiré {0} menace du groupe",
+ "sta.apps.deleteitem": "Supprimer l'Élément",
+ "sta.apps.deleteconfirm": "Êtes-vous sûr de vouloir supprimer cet élément?",
+ "sta.apps.yes": "Oui",
+ "sta.apps.no": "Non",
"sta.roll.success": "Succès",
"sta.roll.successPlural": "Succès",
diff --git a/src/lang/pt.json b/src/lang/pt.json
index 0985ad7..3f0a180 100644
--- a/src/lang/pt.json
+++ b/src/lang/pt.json
@@ -66,6 +66,7 @@
"sta.actor.belonging.starshipweapon2e.title": "Arma Estelar",
"sta.actor.belonging.talent.title": "Talento",
"sta.actor.belonging.value.title": "Valor",
+ "sta.actor.belonging.trait.title": "Trait",
"sta.actor.belonging.weapon.includescale": "Incluir escala?",
"sta.actor.belonging.weapon.title": "Armas",
"sta.actor.belonging.weapon.dmg": "Dan",
@@ -169,7 +170,8 @@
"sta.actor.starship.shieldmod": "Modificador de Escudos",
"sta.actor.starship.shaken": "Abalada",
"sta.actor.starship.reservepower": "Reserva de Energia",
- "sta.actor.starship.shieldcrewmod": "Mod de Escudo/Tripulação",
+ "sta.actor.starship.shieldmod": "Modificador de Escudo",
+ "sta.actor.starship.crewmod": "Modificador de Tripulação",
"sta.actor.smallcraft.parent": "Nave Pai",
"sta.actor.smallcraft.child": "Tipo Veículo Pequeno",
@@ -218,6 +220,10 @@
"sta.apps.removemomentum": "removeu {0} de Ímpeto da reserva",
"sta.apps.addthreat": "adicionou {0} de Ameaça à reserva",
"sta.apps.removethreat": "removeu {0} de Ameaça da reserva",
+ "sta.apps.deleteitem": "Excluir Item",
+ "sta.apps.deleteconfirm": "Tem certeza de que deseja excluir este item?",
+ "sta.apps.yes": "Sim",
+ "sta.apps.no": "Não",
"sta.roll.success": "Sucesso",
"sta.roll.successPlural": "Sucessos",
@@ -269,4 +275,4 @@
"sta.dice.dsn.ufp.red": "UFP Vermelho",
"sta.dice.dsn.ufp.theme.black": "Star Trek Adventures UFP (Preto)",
"sta.dice.dsn.ufp.theme.white": "Star Trek Adventures UFP (Branco)"
-}
+}
\ No newline at end of file
diff --git a/src/module/actors/actor.js b/src/module/actors/actor.js
index 4c53067..f48f510 100644
--- a/src/module/actors/actor.js
+++ b/src/module/actors/actor.js
@@ -1,224 +1,254 @@
-import {
- STARollDialog
-} from '../apps/roll-dialog.js';
-import {
- STARoll
-} from '../roll.js';
-
-export class STAActor extends Actor {
- prepareData() {
- if (!this.img) this.img = game.sta.defaultImage;
-
- super.prepareData();
- }
-}
-
-/** Shared functions for actors **/
-export class STASharedActorFunctions {
- // This function renders all the tracks. This will be used every time the character sheet is loaded. It is a vital element as such it runs before most other code!
- staRenderTracks(html, stressTrackMax, determinationPointsMax,
- repPointsMax, shieldsTrackMax, powerTrackMax, crewTrackMax) {
- let i;
- // Checks if details for the Stress Track was included, this should happen for all Characters!
- if (stressTrackMax) {
- for (i = 0; i < stressTrackMax; i++) {
- html.find('[id^="stress"]')[i].classList.add('stress');
- if (i + 1 <= html.find('#total-stress')[0].value) {
- html.find('[id^="stress"]')[i].setAttribute('data-selected', 'true');
- html.find('[id^="stress"]')[i].classList.add('selected');
- } else {
- html.find('[id^="stress"]')[i].removeAttribute('data-selected');
- html.find('[id^="stress"]')[i].classList.remove('selected');
- }
- }
- }
- // Checks if details for the Determination Track was included, this should happen for all Characters!
- if (determinationPointsMax) {
- for (i = 0; i < determinationPointsMax; i++) {
- html.find('[id^="determination"]')[i].classList.add('determination');
- if (i + 1 <= html.find('#total-determination')[0].value) {
- html.find('[id^="determination"]')[i].setAttribute('data-selected', 'true');
- html.find('[id^="determination"]')[i].classList.add('selected');
- } else {
- html.find('[id^="determination"]')[i].removeAttribute('data-selected');
- html.find('[id^="determination"]')[i].classList.remove('selected');
- }
- }
- }
- // Checks if details for the Reputation Track was included, this should happen for all Characters!
- if (repPointsMax) {
- for (i = 0; i < repPointsMax; i++) {
- html.find('[id^="rep"]')[i].classList.add('rep');
- if (i + 1 <= html.find('#total-rep')[0].value) {
- html.find('[id^="rep"]')[i].setAttribute('data-selected', 'true');
- html.find('[id^="rep"]')[i].classList.add('selected');
- } else {
- html.find('[id^="rep"]')[i].removeAttribute('data-selected');
- html.find('[id^="rep"]')[i].classList.remove('selected');
- }
- }
- }
- // if this is a starship, it will have shields instead of stress, but will be handled very similarly
- if (shieldsTrackMax) {
- for (i = 0; i < shieldsTrackMax; i++) {
- html.find('[id^="shields"]')[i].classList.add('shields');
- if (i + 1 <= html.find('#total-shields').val()) {
- html.find('[id^="shields"]')[i].setAttribute('data-selected', 'true');
- html.find('[id^="shields"]')[i].classList.add('selected');
- } else {
- html.find('[id^="shields"]')[i].removeAttribute('data-selected');
- html.find('[id^="shields"]')[i].classList.remove('selected');
- }
- }
- }
- // if this is a starship, it will have power instead of determination, but will be handled very similarly
- if (powerTrackMax) {
- for (i = 0; i < powerTrackMax; i++) {
- html.find('[id^="power"]')[i].classList.add('power');
- if (i + 1 <= html.find('#total-power').val()) {
- html.find('[id^="power"]')[i].setAttribute('data-selected', 'true');
- html.find('[id^="power"]')[i].classList.add('selected');
- } else {
- html.find('[id^="power"]')[i].removeAttribute('data-selected');
- html.find('[id^="power"]')[i].classList.remove('selected');
- }
- }
- }
- // if this is a starship, it will also have crew support level instead of determination, but will be handled very similarly
- if (crewTrackMax) {
- for (i = 0; i < crewTrackMax; i++) {
- html.find('[id^="crew"]')[i].classList.add('crew');
- if (i + 1 <= html.find('#total-crew').val()) {
- html.find('[id^="crew"]')[i].setAttribute('data-selected', 'true');
- html.find('[id^="crew"]')[i].classList.add('selected');
- } else {
- html.find('[id^="crew"]')[i].removeAttribute('data-selected');
- html.find('[id^="crew"]')[i].classList.remove('selected');
- }
- }
- }
- }
-
- // This handles performing an attribute test using the "Perform Check" button.
- async rollAttributeTest(event, selectedAttribute, selectedAttributeValue,
- selectedDiscipline, selectedDisciplineValue, defaultValue, speaker) {
- event.preventDefault();
- if (!defaultValue) defaultValue = 2;
- // This creates a dialog to gather details regarding the roll and waits for a response
- const rolldialog = await STARollDialog.create(true, defaultValue);
- if (rolldialog) {
- const dicePool = rolldialog.get('dicePoolSlider');
- const usingFocus = rolldialog.get('usingFocus') == null ? false : true;
- const usingDedicatedFocus = rolldialog.get('usingDedicatedFocus') == null ? false : true;
- const usingDetermination = rolldialog.get('usingDetermination') == null ? false : true;
- const complicationRange = parseInt(rolldialog.get('complicationRange'));
- // Once the response has been collected it then sends it to be rolled.
- const staRoll = new STARoll();
- staRoll.performAttributeTest(dicePool, usingFocus, usingDedicatedFocus, usingDetermination,
- selectedAttribute, selectedAttributeValue, selectedDiscipline,
- selectedDisciplineValue, complicationRange, speaker);
- }
- }
-
- // This handles performing an challenge roll using the "Perform Challenge Roll" button.
- async rollChallengeRoll(event, weaponName, defaultValue, speaker = null) {
- event.preventDefault();
- // This creates a dialog to gather details regarding the roll and waits for a response
- const rolldialog = await STARollDialog.create(false, defaultValue);
- if (rolldialog) {
- const dicePool = rolldialog.get('dicePoolValue');
- // Once the response has been collected it then sends it to be rolled.
- const staRoll = new STARoll();
- staRoll.performChallengeRoll(dicePool, weaponName, speaker);
- }
- }
-
- // This handles performing an "item" roll by clicking the item's image.
- async rollGenericItem(event, type, id, speaker) {
- event.preventDefault();
- const item = speaker.items.get(id);
- const staRoll = new STARoll();
- // It will send it to a different method depending what item type was sent to it.
- switch (type) {
- case 'item':
- staRoll.performItemRoll(item, speaker);
- break;
- case 'focus':
- staRoll.performFocusRoll(item, speaker);
- break;
- case 'value':
- staRoll.performValueRoll(item, speaker);
- break;
- case 'characterweapon':
- case 'starshipweapon':
- staRoll.performWeaponRoll(item, speaker);
- break;
- case 'characterweapon2e':
- staRoll.performWeaponRoll2e(item, speaker);
- break;
- case 'starshipweapon2e':
- staRoll.performStarshipWeaponRoll2e(item, speaker);
- break;
- case 'armor':
- staRoll.performArmorRoll(item, speaker);
- break;
- case 'talent':
- staRoll.performTalentRoll(item, speaker);
- break;
- case 'injury':
- staRoll.performInjuryRoll(item, speaker);
- break;
- }
- }
-
- /**
- * Create the "Are you sure?" delete dialog used for sheets' item delete behavior.
- *
- * @param {string} itemName - The item name to display.
- * @param {function} yesCb - The callback to handle a "yes" click.
- * @param {function} closeCb - The callback handling a "close" event.
- *
- * @return {Dialog}
- */
- deleteConfirmDialog(itemName, yesCb, closeCb) {
- // Dialog uses Simple Worldbuilding System Code.
- return new Dialog({
- title: 'Confirm Item Deletion',
- content: 'Are you sure you want to delete ' + itemName + '?',
- buttons: {
- yes: {
- icon: '',
- label: game.i18n.localize('Yes'),
- callback: yesCb,
- },
- no: {
- icon: '',
- label: game.i18n.localize('No'),
- }
- },
- default: 'no',
- close: closeCb,
- });
- }
-}
-// Add unarmed strike to new characters
-Hooks.on('createActor', async (actor, options, userId) => {
- if (game.user.id !== userId) return;
-
- if (actor.type === 'character') {
- const compendium2e = await game.packs.get('sta.equipment-crew');
- const item1 = await compendium2e.getDocument('cxIi0Ltb1sUCFnzp');
-
- const compendium1e = await game.packs.get('sta.personal-weapons-core');
- const item2 = await compendium1e.getDocument('3PTFLawY0tCva3gG');
-
- if (item1 && item2) {
- await actor.createEmbeddedDocuments('Item', [
- item1.toObject(),
- item2.toObject()
- ]);
- } else {
- console.error('One or both items were not found in the compendiums.');
- }
- }
-});
+import {
+ STARollDialog
+} from '../apps/roll-dialog.js';
+import {
+ STARoll
+} from '../apps/roll.js';
+
+export class STAActor extends Actor {
+ prepareData() {
+ if (!this.img) this.img = game.sta.defaultImage;
+
+ super.prepareData();
+ }
+}
+
+/** Shared functions for actors **/
+export class STASharedActorFunctions {
+ // This function renders all the tracks. This will be used every time the character sheet is loaded. It is a vital element as such it runs before most other code!
+ staRenderTracks(html, stressTrackMax, determinationPointsMax,
+ repPointsMax, shieldsTrackMax, powerTrackMax, crewTrackMax) {
+ let i;
+ // Checks if details for the Stress Track was included, this should happen for all Characters!
+ if (stressTrackMax) {
+ for (i = 0; i < stressTrackMax; i++) {
+ html.find('[id^="stress"]')[i].classList.add('stress');
+ if (i + 1 <= html.find('#total-stress')[0].value) {
+ html.find('[id^="stress"]')[i].setAttribute('data-selected', 'true');
+ html.find('[id^="stress"]')[i].classList.add('selected');
+ } else {
+ html.find('[id^="stress"]')[i].removeAttribute('data-selected');
+ html.find('[id^="stress"]')[i].classList.remove('selected');
+ }
+ }
+ }
+ // Checks if details for the Determination Track was included, this should happen for all Characters!
+ if (determinationPointsMax) {
+ for (i = 0; i < determinationPointsMax; i++) {
+ html.find('[id^="determination"]')[i].classList.add('determination');
+ if (i + 1 <= html.find('#total-determination')[0].value) {
+ html.find('[id^="determination"]')[i].setAttribute('data-selected', 'true');
+ html.find('[id^="determination"]')[i].classList.add('selected');
+ } else {
+ html.find('[id^="determination"]')[i].removeAttribute('data-selected');
+ html.find('[id^="determination"]')[i].classList.remove('selected');
+ }
+ }
+ }
+ // Checks if details for the Reputation Track was included, this should happen for all Characters!
+ if (repPointsMax) {
+ for (i = 0; i < repPointsMax; i++) {
+ html.find('[id^="rep"]')[i].classList.add('rep');
+ if (i + 1 <= html.find('#total-rep')[0].value) {
+ html.find('[id^="rep"]')[i].setAttribute('data-selected', 'true');
+ html.find('[id^="rep"]')[i].classList.add('selected');
+ } else {
+ html.find('[id^="rep"]')[i].removeAttribute('data-selected');
+ html.find('[id^="rep"]')[i].classList.remove('selected');
+ }
+ }
+ }
+ // if this is a starship, it will have shields instead of stress, but will be handled very similarly
+ if (shieldsTrackMax) {
+ for (i = 0; i < shieldsTrackMax; i++) {
+ html.find('[id^="shields"]')[i].classList.add('shields');
+ if (i + 1 <= html.find('#total-shields').val()) {
+ html.find('[id^="shields"]')[i].setAttribute('data-selected', 'true');
+ html.find('[id^="shields"]')[i].classList.add('selected');
+ } else {
+ html.find('[id^="shields"]')[i].removeAttribute('data-selected');
+ html.find('[id^="shields"]')[i].classList.remove('selected');
+ }
+ }
+ }
+ // if this is a starship, it will have power instead of determination, but will be handled very similarly
+ if (powerTrackMax) {
+ for (i = 0; i < powerTrackMax; i++) {
+ html.find('[id^="power"]')[i].classList.add('power');
+ if (i + 1 <= html.find('#total-power').val()) {
+ html.find('[id^="power"]')[i].setAttribute('data-selected', 'true');
+ html.find('[id^="power"]')[i].classList.add('selected');
+ } else {
+ html.find('[id^="power"]')[i].removeAttribute('data-selected');
+ html.find('[id^="power"]')[i].classList.remove('selected');
+ }
+ }
+ }
+ // if this is a starship, it will also have crew support level instead of determination, but will be handled very similarly
+ if (crewTrackMax) {
+ for (i = 0; i < crewTrackMax; i++) {
+ html.find('[id^="crew"]')[i].classList.add('crew');
+ if (i + 1 <= html.find('#total-crew').val()) {
+ html.find('[id^="crew"]')[i].setAttribute('data-selected', 'true');
+ html.find('[id^="crew"]')[i].classList.add('selected');
+ } else {
+ html.find('[id^="crew"]')[i].removeAttribute('data-selected');
+ html.find('[id^="crew"]')[i].classList.remove('selected');
+ }
+ }
+ }
+ }
+
+ // This handles performing an attribute test using the "Perform Check" button.
+ async rollAttributeTest(event, selectedAttribute, selectedAttributeValue,
+ selectedDiscipline, selectedDisciplineValue, defaultValue, speaker) {
+ event.preventDefault();
+ if (!defaultValue) defaultValue = 2;
+ // This creates a dialog to gather details regarding the roll and waits for a response
+ const rolldialog = await STARollDialog.create(true, defaultValue);
+ if (rolldialog) {
+ const dicePool = rolldialog.get('dicePoolSlider');
+ const usingFocus = rolldialog.get('usingFocus') == null ? false : true;
+ const usingDedicatedFocus = rolldialog.get('usingDedicatedFocus') == null ? false : true;
+ const usingDetermination = rolldialog.get('usingDetermination') == null ? false : true;
+ const complicationRange = parseInt(rolldialog.get('complicationRange'));
+ // Once the response has been collected it then sends it to be rolled.
+ const staRoll = new STARoll();
+ staRoll.performAttributeTest(dicePool, usingFocus, usingDedicatedFocus, usingDetermination,
+ selectedAttribute, selectedAttributeValue, selectedDiscipline,
+ selectedDisciplineValue, complicationRange, speaker);
+ }
+ }
+
+ // This handles performing an challenge roll using the "Perform Challenge Roll" button.
+ async rollChallengeRoll(event, weaponName, defaultValue, speaker = null) {
+ event.preventDefault();
+ // This creates a dialog to gather details regarding the roll and waits for a response
+ const rolldialog = await STARollDialog.create(false, defaultValue);
+ if (rolldialog) {
+ const dicePool = rolldialog.get('dicePoolValue');
+ // Once the response has been collected it then sends it to be rolled.
+ const staRoll = new STARoll();
+ staRoll.performChallengeRoll(dicePool, weaponName, speaker);
+ }
+ }
+
+ // This handles performing an "item" roll by clicking the item's image.
+ async rollGenericItem(event, type, id, speaker) {
+ event.preventDefault();
+ const item = speaker.items.get(id);
+ const staRoll = new STARoll();
+ // It will send it to a different method depending what item type was sent to it.
+ switch (type) {
+ case 'item':
+ staRoll.performItemRoll(item, speaker);
+ break;
+ case 'focus':
+ staRoll.performFocusRoll(item, speaker);
+ break;
+ case 'value':
+ staRoll.performValueRoll(item, speaker);
+ break;
+ case 'characterweapon':
+ case 'starshipweapon':
+ staRoll.performWeaponRoll(item, speaker);
+ break;
+ case 'characterweapon2e':
+ staRoll.performWeaponRoll2e(item, speaker);
+ break;
+ case 'starshipweapon2e':
+ staRoll.performStarshipWeaponRoll2e(item, speaker);
+ break;
+ case 'armor':
+ staRoll.performArmorRoll(item, speaker);
+ break;
+ case 'talent':
+ staRoll.performTalentRoll(item, speaker);
+ break;
+ case 'injury':
+ staRoll.performInjuryRoll(item, speaker);
+ break;
+ case 'trait':
+ staRoll.performTraitRoll(item, speaker);
+ break;
+ case 'milestone':
+ staRoll.performMilestoneRoll(item, speaker);
+ break;
+ }
+ }
+
+ /**
+ * Create the "Are you sure?" delete dialog used for sheets' item delete behavior.
+ *
+ * @param {string} itemName - The item name to display.
+ * @param {function} yesCb - The callback to handle a "yes" click.
+ * @param {function} closeCb - The callback handling a "close" event.
+ *
+ * @return {Dialog}
+ */
+ deleteConfirmDialog(itemName, yesCb, closeCb) {
+ // Dialog uses Simple Worldbuilding System Code.
+ return new Dialog({
+ title: 'Confirm Item Deletion',
+ content: 'Are you sure you want to delete ' + itemName + '?',
+ buttons: {
+ yes: {
+ icon: '',
+ label: game.i18n.localize('Yes'),
+ callback: yesCb,
+ },
+ no: {
+ icon: '',
+ label: game.i18n.localize('No'),
+ }
+ },
+ default: 'no',
+ close: closeCb,
+ });
+ }
+}
+// Add unarmed strike to new characters
+Hooks.on('createActor', async (actor, options, userId) => {
+ if (game.user.id !== userId) return;
+
+ if (actor.type === 'character') {
+ const compendium2e = await game.packs.get('sta.equipment-crew');
+ const item1 = await compendium2e.getDocument('cxIi0Ltb1sUCFnzp');
+
+ const compendium1e = await game.packs.get('sta.personal-weapons-core');
+ const item2 = await compendium1e.getDocument('3PTFLawY0tCva3gG');
+
+ if (item1 && item2) {
+ await actor.createEmbeddedDocuments('Item', [
+ item1.toObject(),
+ item2.toObject()
+ ]);
+ } else {
+ console.error('One or both items were not found in the compendiums.');
+ }
+ }
+});
+
+Hooks.on('renderActorSheet', async (actorSheet, html, data) => {
+ const actor = actorSheet.object;
+
+ if (actor.system.traits && actor.system.traits.trim()) {
+ const traitName = actor.system.traits.trim();
+
+ const existingTrait = actor.items.find((item) => item.name === traitName && item.type === 'trait');
+
+ if (!existingTrait) {
+ const traitItemData = {
+ name: traitName,
+ type: 'trait',
+ };
+
+ try {
+ await actor.createEmbeddedDocuments('Item', [traitItemData]);
+ await actor.update({'system.traits': ''});
+ } catch (err) {
+ console.error(`Error creating trait item for actor ${actor.name}:`, err);
+ }
+ }
+ }
+});
diff --git a/src/module/actors/sheets/character-sheet.js b/src/module/actors/sheets/character-sheet.js
index 6f71c03..0e8c292 100644
--- a/src/module/actors/sheets/character-sheet.js
+++ b/src/module/actors/sheets/character-sheet.js
@@ -1,635 +1,630 @@
-import {STASharedActorFunctions} from '../actor.js';
-
-export class STACharacterSheet extends ActorSheet {
- /** @override */
- static get defaultOptions() {
- return foundry.utils.mergeObject(super.defaultOptions, {
- classes: ['sta', 'sheet', 'actor', 'character'],
- width: 850,
- height: 910,
- dragDrop: [{
- dragSelector: '.item-list .item',
- dropSelector: null
- }],
- tabs: [
- {
- navSelector: '.character-tabs',
- contentSelector: '.character-header',
- initial: 'biography',
- }
- ]
- });
- }
-
- /* -------------------------------------------- */
-
- // If the player is not a GM and has limited permissions - send them to the limited sheet, otherwise, continue as usual.
- /** @override */
- get template() {
- const versionInfo = game.world.coreVersion;
- if ( !game.user.isGM && this.actor.limited) return 'systems/sta/templates/actors/limited-sheet.hbs';
- if (!foundry.utils.isNewerVersion(versionInfo, '0.8.-1')) return 'systems/sta/templates/actors/character-sheet-legacy.hbs';
- return `systems/sta/templates/actors/character-sheet.hbs`;
- }
-
- /* -------------------------------------------- */
-
- /** @override */
- getData() {
- const sheetData = this.object;
- sheetData.dtypes = ['String', 'Number', 'Boolean'];
-
- // Temporary fix I'm leaving in place until I deprecate in a future version
- const overrideMinAttributeTags = ['[Minor]', '[Notable]', '[Major]', '[NPC]', '[Child]'];
- const overrideMinAttribute = overrideMinAttributeTags.some(
- (tag) => sheetData.name.toLowerCase().indexOf(tag.toLowerCase()) !== -1
- );
-
-
- // Ensure attribute and discipline values aren't over the max/min.
- let minAttribute = overrideMinAttribute ? 0 : 7;
- let maxAttribute = 12;
- const overrideAttributeLimitSetting = game.settings.get('sta', 'characterAttributeLimitIgnore');
- if (overrideAttributeLimitSetting) {
- minAttribute = 0;
- maxAttribute = 99;
- }
- $.each(sheetData.system.attributes, (key, attribute) => {
- if (attribute.value > maxAttribute) attribute.value = maxAttribute;
- if (attribute.value < minAttribute) attribute.value = minAttribute;
- });
- const minDiscipline = 0;
- let maxDiscipline = 5;
- const overrideDisciplineLimitSetting = game.settings.get('sta', 'characterDisciplineLimitIgnore');
- if (overrideDisciplineLimitSetting) {
- maxDiscipline = 99;
- }
- $.each(sheetData.system.disciplines, (key, discipline) => {
- if (discipline.value > maxDiscipline) discipline.value = maxDiscipline;
- if (discipline.value < minDiscipline) discipline.value = minDiscipline;
- });
-
- // Check stress max/min
- if (!(sheetData.system.stress)) {
- sheetData.system.stress = {};
- }
- if (sheetData.system.stress.value > sheetData.system.stress.max) {
- sheetData.system.stress.value = sheetData.system.stress.max;
- }
- if (sheetData.system.stress.value < 0) {
- sheetData.system.stress.value = 0;
- }
-
- // Check determination max/min
- if (!(sheetData.system.determination)) {
- sheetData.system.determination = {};
- }
- if (sheetData.system.determination.value > 3) {
- sheetData.system.determination.value = 3;
- }
- if (sheetData.system.determination.value < 0) {
- sheetData.system.determination.value = 0;
- }
-
- // Check reputation max/min
- if (!(sheetData.system.reputation)) {
- sheetData.system.reputation = {};
- }
- if (sheetData.system.reputation.value > 20) {
- sheetData.system.reputation.value = 20;
- }
- if (sheetData.system.reputation < 0) {
- sheetData.system.reputation = 0;
- }
-
- return sheetData;
- }
-
- /* -------------------------------------------- */
-
- /** @override */
- activateListeners(html) {
- super.activateListeners(html);
-
- // Allows checking version easily
- const versionInfo = game.world.coreVersion;
-
- // Opens the class STASharedActorFunctions for access at various stages.
- const staActor = new STASharedActorFunctions();
-
- // If the player has limited access to the actor, there is nothing to see here. Return.
- if ( !game.user.isGM && this.actor.limited) return;
-
- // We use i a lot in for loops. Best to assign it now for use later in multiple places.
- let i;
-
- // TODO: This is not really doing anything yet
- // Here we are checking if there is armor equipped.
- // The player can only have one armor. As such, we will use this later.
- let armorNumber = 0;
- let stressTrackMax = 0;
- function armorCount(currentActor) {
- armorNumber = 0;
- currentActor.actor.items.forEach((values) => {
- if (values.type == 'armor') {
- if (values.equipped == true) armorNumber+= 1;
- }
- });
- }
- armorCount(this);
-
- // This creates a dynamic Determination Point tracker. It sets max determination to 3 (it is dynamic in Dishonored) and
- // creates a new div for each and places it under a child called "bar-determination-renderer"
- const determinationPointsMax = 3;
- for (i = 1; i <= determinationPointsMax; i++) {
- const detDiv = document.createElement('DIV');
- detDiv.className = 'box';
- detDiv.id = 'determination-' + i;
- detDiv.innerHTML = i;
- detDiv.style = 'width: calc(100% / 3);';
- html.find('#bar-determination-renderer')[0].appendChild(detDiv);
- }
-
- // This creates a dynamic Stress tracker. It polls for the value of the fitness attribute, security discipline, and checks for Resolute talent.
- // With the total value, creates a new div for each and places it under a child called "bar-stress-renderer".
- function stressTrackUpdate() {
- const localizedValues = {
- 'resolute': game.i18n.localize('sta.actor.character.talents.resolute')
- };
-
- stressTrackMax = parseInt(html.find('#fitness')[0].value) + parseInt(html.find('#security')[0].value);
- if (html.find(`[data-talent-name*="${localizedValues.resolute}"]`).length > 0) {
- stressTrackMax += 3;
- }
- stressTrackMax += parseInt(html.find('#strmod')[0].value);
- // This checks that the max-stress hidden field is equal to the calculated Max Stress value, if not it makes it so.
- if (html.find('#max-stress')[0].value != stressTrackMax) {
- html.find('#max-stress')[0].value = stressTrackMax;
- }
- html.find('#bar-stress-renderer').empty();
- for (let i = 1; i <= stressTrackMax; i++) {
- const stressDiv = document.createElement('DIV');
- stressDiv.className = 'box';
- stressDiv.id = 'stress-' + i;
- stressDiv.innerHTML = i;
- stressDiv.style = 'width: calc(100% / ' + html.find('#max-stress')[0].value + ');';
- html.find('#bar-stress-renderer')[0].appendChild(stressDiv);
- }
- }
- stressTrackUpdate();
-
- // This creates a dynamic Reputation tracker. For this it uses a max value of 30. This can be configured here.
- // It creates a new div for each and places it under a child called "bar-rep-renderer"
- const repPointsMax = game.settings.get('sta', 'maxNumberOfReputation');
- for (let i = 1; i <= repPointsMax; i++) {
- const repDiv = document.createElement('DIV');
- repDiv.className = 'box';
- repDiv.id = 'rep-' + i;
- repDiv.innerHTML = i;
- repDiv.style = 'width: calc(100% / ' + repPointsMax + ');';
- html.find('#bar-rep-renderer')[0].appendChild(repDiv);
- }
-
- // Fires the function staRenderTracks as soon as the parameters exist to do so.
- // staActor.staRenderTracks(html, stressTrackMax, determinationPointsMax, repPointsMax);
- staActor.staRenderTracks(html, stressTrackMax,
- determinationPointsMax, repPointsMax);
-
- // This allows for each item-edit image to link open an item sheet. This uses Simple Worldbuilding System Code.
- html.find('.control .edit').click((ev) => {
- const li = $(ev.currentTarget).parents('.entry');
- const item = this.actor.items.get(li.data('itemId'));
- item.sheet.render(true);
- });
-
- // This if statement checks if the form is editable, if not it hides control used by the owner, then aborts any more of the script.
- if (!this.options.editable) {
- // This hides the ability to Perform an Attribute Test for the character.
- for (i = 0; i < html.find('.check-button').length; i++) {
- html.find('.check-button')[i].style.display = 'none';
- }
- // This hides all toggle, add, and delete item images.
- for (i = 0; i < html.find('.control.create').length; i++) {
- html.find('.control.create')[i].style.display = 'none';
- }
- for (i = 0; i < html.find('.control .delete').length; i++) {
- html.find('.control .delete')[i].style.display = 'none';
- }
- for (i = 0; i < html.find('.control.toggle').length; i++) {
- html.find('.control.delete')[i].style.display = 'none';
- }
- // This hides all attribute and discipline check boxes (and titles)
- for (i = 0; i < html.find('.selector').length; i++) {
- html.find('.selector')[i].style.display = 'none';
- }
- for (i = 0; i < html.find('.selector').length; i++) {
- html.find('.selector')[i].style.display = 'none';
- }
- // Remove hover CSS from clickables that are no longer clickable.
- for (i = 0; i < html.find('.box').length; i++) {
- html.find('.box')[i].classList.add('unset-clickables');
- }
- for (i = 0; i < html.find('.rollable').length; i++) {
- html.find('.rollable')[i].classList.add('unset-clickables');
- }
-
- return;
- };
-
- // This toggles whether the value is used or not.
- html.find('.control.toggle').click((ev) => {
- const itemId = ev.currentTarget.closest('.entry').dataset.itemId;
- const item = this.actor.items.get(itemId);
- const state = item.system.used;
- if (state) {
- item.system.used = false;
- $(ev.currentTarget).children()[0].classList.remove('fa-toggle-on');
- $(ev.currentTarget).children()[0].classList.add('fa-toggle-off');
- $(ev.currentTarget).parents('.entry')[0].setAttribute('data-item-used', 'false');
- $(ev.currentTarget).parents('.entry')[0].style.textDecoration = 'none';
- } else {
- item.system.used = true;
- $(ev.currentTarget).children()[0].classList.remove('fa-toggle-off');
- $(ev.currentTarget).children()[0].classList.add('fa-toggle-on');
- $(ev.currentTarget).parents('.entry')[0].setAttribute('data-item-used', 'true');
- $(ev.currentTarget).parents('.entry')[0].style.textDecoration = 'line-through';
- }
- return this.actor.items.get(itemId).update({['system.used']: getProperty(item.system, 'used')});
- });
-
- // This allows for all items to be rolled, it gets the current targets type and id and sends it to the rollGenericItem function.
- html.find('.chat,.rollable').click((ev) =>{
- const itemType = $(ev.currentTarget).parents('.entry')[0].getAttribute('data-item-type');
- const itemId = $(ev.currentTarget).parents('.entry')[0].getAttribute('data-item-id');
- staActor.rollGenericItem(ev, itemType, itemId, this.actor);
- });
-
- // Allows item-create images to create an item of a type defined individually by each button. This uses code found via the Foundry VTT System Tutorial.
- html.find('.control.create').click((ev) => {
- ev.preventDefault();
- const header = ev.currentTarget;
- const type = header.dataset.type;
- const data = foundry.utils.duplicate(header.dataset);
- const name = `New ${type.capitalize()}`;
- if (type == 'armor' && armorNumber >= 1) {
- ui.notifications.info('The current actor has an equipped armor already. Adding unequipped.');
- data.equipped = false;
- }
- const itemData = {
- name: name,
- type: type,
- data: data,
- img: game.sta.defaultImage
- };
- delete itemData.data['type'];
- if (foundry.utils.isNewerVersion(versionInfo, '0.8.-1')) {
- return this.actor.createEmbeddedDocuments('Item', [(itemData)]);
- } else {
- return this.actor.createOwnedItem(itemData);
- }
- });
-
- // Allows item-delete images to allow deletion of the selected item.
- html.find('.control .delete').click((ev) => {
- const li = $(ev.currentTarget).parents('.entry');
- this.activeDialog = staActor.deleteConfirmDialog(
- li[0].getAttribute('data-item-value'),
- () => {
- if ( foundry.utils.isNewerVersion( versionInfo, '0.8.-1' )) {
- this.actor.deleteEmbeddedDocuments( 'Item', [li.data('itemId')] );
- } else {
- this.actor.deleteOwnedItem( li.data( 'itemId' ));
- }
- },
- () => this.activeDialog = null
- );
- this.activeDialog.render(true);
- });
-
- // Reads if a reputation track box has been clicked, and if it has will either: set the value to the clicked box, or reduce the value by one.
- // This check is dependent on various requirements, see comments in code.
- html.find('[id^="rep"]').click((ev) => {
- let total = '';
- const newTotalObject = $(ev.currentTarget)[0];
- const newTotal = newTotalObject.id.replace(/\D/g, '');
- // data-selected stores whether the track box is currently activated or not. This checks that the box is activated
- if (newTotalObject.getAttribute('data-selected') === 'true') {
- // Now we check that the "next" track box is not activated.
- // If there isn't one, or it isn't activated, we only want to decrease the value by 1 rather than setting the value.
- const nextCheck = 'rep-' + (parseInt(newTotal) + 1);
- if (!html.find('#'+nextCheck)[0] || html.find('#'+nextCheck)[0].getAttribute('data-selected') != 'true') {
- html.find('#total-rep')[0].value = html.find('#total-rep')[0].value - 1;
- this.submit();
- // If it isn't caught by the if, the next box is likely activated. If something happened, its safer to set the value anyway.
- } else {
- total = html.find('#total-rep')[0].value;
- if (total != newTotal) {
- html.find('#total-rep')[0].value = newTotal;
- this.submit();
- }
- }
- // If the clicked box wasn't activated, we need to activate it now.
- } else {
- total = html.find('#total-rep')[0].value;
- if (total != newTotal) {
- html.find('#total-rep')[0].value = newTotal;
- this.submit();
- }
- }
- });
-
- // Reads if a stress track box has been clicked, and if it has will either: set the value to the clicked box, or reduce the value by one.
- // See line 186-220 for a more detailed break down on the context of each scenario. Stress uses the same logic.
- html.find('[id^="stress"]').click((ev) => {
- let total = '';
- const newTotalObject = $(ev.currentTarget)[0];
- const newTotal = newTotalObject.id.substring(7);
- if (newTotalObject.getAttribute('data-selected') === 'true') {
- const nextCheck = 'stress-' + (parseInt(newTotal) + 1);
- if (!html.find('#'+nextCheck)[0] || html.find('#'+nextCheck)[0].getAttribute('data-selected') != 'true') {
- html.find('#total-stress')[0].value = html.find('#total-stress')[0].value - 1;
- this.submit();
- // If it isn't caught by the if, the next box is likely activated. If something happened, its safer to set the value anyway.
- } else {
- total = html.find('#total-stress')[0].value;
- if (total != newTotal) {
- html.find('#total-stress')[0].value = newTotal;
- this.submit();
- }
- }
- // If the clicked box wasn't activated, we need to activate it now.
- } else {
- total = html.find('#total-stress')[0].value;
- if (total != newTotal) {
- html.find('#total-stress')[0].value = newTotal;
- this.submit();
- }
- }
- });
-
- // Reads if a determination track box has been clicked, and if it has will either: set the value to the clicked box, or reduce the value by one.
- // See line 186-220 for a more detailed break down on the context of each scenario. Determination uses the same logic.
- html.find('[id^="determination"]').click((ev) => {
- let total = '';
- const newTotalObject = $(ev.currentTarget)[0];
- const newTotal = newTotalObject.id.replace(/\D/g, '');
- if (newTotalObject.getAttribute('data-selected') === 'true') {
- const nextCheck = 'determination-' + (parseInt(newTotal) + 1);
- if (!html.find('#'+nextCheck)[0] || html.find('#'+nextCheck)[0].getAttribute('data-selected') != 'true') {
- html.find('#total-determination')[0].value = html.find('#total-determination')[0].value - 1;
- this.submit();
- // If it isn't caught by the if, the next box is likely activated. If something happened, its safer to set the value anyway.
- } else {
- total = html.find('#total-determination')[0].value;
- if (total != newTotal) {
- html.find('#total-determination')[0].value = newTotal;
- this.submit();
- }
- }
- // If the clicked box wasn't activated, we need to activate it now.
- } else {
- total = html.find('#total-determination')[0].value;
- if (total != newTotal) {
- html.find('#total-determination')[0].value = newTotal;
- this.submit();
- }
- }
- });
-
- // This is used to clean up all the HTML that comes from displaying outputs from the text editor boxes. There's probably a better way to do this but the quick and dirty worked this time.
- $.each($('[id^=talent-tooltip-text-]'), function(index, value) {
- const beforeDescription = value.innerHTML;
- const decoded = TextEditor.decodeHTML(beforeDescription);
- const prettifiedDescription = TextEditor.previewHTML(decoded, 1000);
- $('#' + value.id).html(prettifiedDescription);
- });
-
-
- html.find('.talent-tooltip-clickable').click((ev) => {
- const talentId = $(ev.currentTarget)[0].id.substring('talent-tooltip-clickable-'.length);
- const currentShowingTalentId = $('.talent-tooltip-container:not(.hide)')[0] ? $('.talent-tooltip-container:not(.hide)')[0].id.substring('talent-tooltip-container-'.length) : null;
-
- if (talentId == currentShowingTalentId) {
- $('#talent-tooltip-container-' + talentId).addClass('hide').removeAttr('style');
- } else {
- $('.talent-tooltip-container').addClass('hide').removeAttr('style');
- $('#talent-tooltip-container-' + talentId).removeClass('hide').height($('#talent-tooltip-text-' + talentId)[0].scrollHeight + 5);
- }
- });
-
- $.each($('[id^=injury-tooltip-text-]'), function(index, value) {
- const beforeDescription = value.innerHTML;
- const decoded = TextEditor.decodeHTML(beforeDescription);
- const prettifiedDescription = TextEditor.previewHTML(decoded, 1000);
- $('#' + value.id).html(prettifiedDescription);
- });
-
-
- html.find('.injury-tooltip-clickable').click((ev) => {
- const injuryId = $(ev.currentTarget)[0].id.substring('injury-tooltip-clickable-'.length);
- const currentShowinginjuryId = $('.injury-tooltip-container:not(.hide)')[0] ? $('.injury-tooltip-container:not(.hide)')[0].id.substring('injury-tooltip-container-'.length) : null;
-
- if (injuryId == currentShowinginjuryId) {
- $('#injury-tooltip-container-' + injuryId).addClass('hide').removeAttr('style');
- } else {
- $('.injury-tooltip-container').addClass('hide').removeAttr('style');
- $('#injury-tooltip-container-' + injuryId).removeClass('hide').height($('#injury-tooltip-text-' + injuryId)[0].scrollHeight + 5);
- }
- });
-
- $.each($('[id^=focus-tooltip-text-]'), function(index, value) {
- const beforeDescription = value.innerHTML;
- const decoded = TextEditor.decodeHTML(beforeDescription);
- const prettifiedDescription = TextEditor.previewHTML(decoded, 1000);
- $('#' + value.id).html(prettifiedDescription);
- });
-
-
- html.find('.focus-tooltip-clickable').click((ev) => {
- const focusId = $(ev.currentTarget)[0].id.substring('focus-tooltip-clickable-'.length);
- const currentShowingfocusId = $('.focus-tooltip-container:not(.hide)')[0] ? $('.focus-tooltip-container:not(.hide)')[0].id.substring('focus-tooltip-container-'.length) : null;
-
- if (focusId == currentShowingfocusId) {
- $('#focus-tooltip-container-' + focusId).addClass('hide').removeAttr('style');
- } else {
- $('.focus-tooltip-container').addClass('hide').removeAttr('style');
- $('#focus-tooltip-container-' + focusId).removeClass('hide').height($('#focus-tooltip-text-' + focusId)[0].scrollHeight + 5);
- }
- });
-
- $.each($('[id^=value-tooltip-text-]'), function(index, value) {
- const beforeDescription = value.innerHTML;
- const decoded = TextEditor.decodeHTML(beforeDescription);
- const prettifiedDescription = TextEditor.previewHTML(decoded, 1000);
- $('#' + value.id).html(prettifiedDescription);
- });
-
- // Turns the Attribute checkboxes into essentially a radio button. It removes any other ticks, and then checks the new attribute.
- // Finally a submit is required as data has changed.
- html.find('.selector.attribute').click((ev) => {
- for (i = 0; i <= 5; i++) {
- html.find('.selector.attribute')[i].checked = false;
- }
- $(ev.currentTarget)[0].checked = true;
- this.submit();
- });
-
- // Turns the Discipline checkboxes into essentially a radio button. It removes any other ticks, and then checks the new discipline.
- // Finally a submit is required as data has changed.
- html.find('.selector.discipline').click((ev) => {
- for (i = 0; i <= 5; i++) {
- html.find('.selector.discipline')[i].checked = false;
- }
- $(ev.currentTarget)[0].checked = true;
- this.submit();
- });
-
- // If the check-button is clicked it performs the acclaim or reprimand calculation.
- html.find('.check-button.acclaim').click(async (ev) => {
- const dialogContent = `
-
- `;
-
- new Dialog({
- title: `${game.i18n.localize('sta.roll.acclaim')}`,
- content: dialogContent,
- buttons: {
- roll: {
- label: `${game.i18n.localize('sta.roll.acclaim')}`,
- callback: async (html) => {
- const PositiveInfluences = parseInt(html.find('#positiveInfluences').val()) || 1;
- const NegativeInfluences = parseInt(html.find('#negativeInfluences').val()) || 0;
-
- const selectedDisciplineValue = parseInt(document.querySelector('#total-rep')?.value) || 0;
- const existingReprimand = parseInt(document.querySelector('#reprimand')?.value) || 0;
- const targetNumber = selectedDisciplineValue + 7;
- const complicationThreshold = 20 - Math.min(existingReprimand, 5);
- const diceRollFormula = `${PositiveInfluences}d20`;
- const roll = new Roll(diceRollFormula);
-
- await roll.evaluate();
-
- let totalSuccesses = 0;
- let complications = 0;
- let acclaim = 0;
- let reprimand = 0;
- const diceResults = [];
-
- roll.terms[0].results.forEach((die) => {
- let coloredDieResult;
-
- if (die.result >= complicationThreshold) {
- coloredDieResult = `${die.result}`; // Red for complications
- complications += 1;
- } else if (die.result <= selectedDisciplineValue) {
- coloredDieResult = `${die.result}`; // Green for double successes
- totalSuccesses += 2;
- } else if (die.result <= targetNumber && die.result > selectedDisciplineValue) {
- coloredDieResult = `${die.result}`; // Blue for single successes
- totalSuccesses += 1;
- } else {
- coloredDieResult = `${die.result}`; // Default for other results
- }
- diceResults.push(coloredDieResult);
- });
-
- let chatContent = `${game.i18n.format('sta.roll.dicerolls')} ${diceResults.join(', ')} `;
-
- if (totalSuccesses > NegativeInfluences) {
- acclaim = totalSuccesses - NegativeInfluences;
- chatContent += `${game.i18n.format('sta.roll.gainacclaim', {0: acclaim})}`;
- } else if (totalSuccesses < NegativeInfluences) {
- reprimand = (NegativeInfluences - totalSuccesses) + complications;
- chatContent += `${game.i18n.format('sta.roll.gainreprimand', {0: reprimand})}`;
- } else if (totalSuccesses === NegativeInfluences) {
- chatContent += `${game.i18n.localize('sta.roll.nochange')}`;
- }
-
- ChatMessage.create({
- speaker: ChatMessage.getSpeaker(),
- content: chatContent
- });
- }
- }
- },
- render: (html) => {
- html.find('button').addClass('dialog-button roll default');
- }
- }).render(true);
- });
-
- // If the check-button is clicked it grabs the selected attribute and the selected discipline and fires the method rollAttributeTest. See actor.js for further info.
- html.find('.check-button.attribute').click((ev) => {
- let selectedAttribute = '';
- let selectedAttributeValue = '';
- let selectedDiscipline = '';
- let selectedDisciplineValue = '';
- for (i = 0; i <= 5; i++) {
- if (html.find('.selector.attribute')[i].checked === true) {
- selectedAttribute = html.find('.selector.attribute')[i].id;
- selectedAttribute = selectedAttribute.slice(0, -9);
- selectedAttributeValue = html.find('#'+selectedAttribute)[0].value;
- }
- }
- for (i = 0; i <= 5; i++) {
- if (html.find('.selector.discipline')[i].checked === true) {
- selectedDiscipline = html.find('.selector.discipline')[i].id;
- selectedDiscipline = selectedDiscipline.slice(0, -9);
- selectedDisciplineValue = html.find('#'+selectedDiscipline)[0].value;
- }
- }
-
- staActor.rollAttributeTest(ev, selectedAttribute,
- parseInt(selectedAttributeValue), selectedDiscipline,
- parseInt(selectedDisciplineValue), 2, this.actor);
- });
-
- // If the check-button is clicked it fires the method challenge roll method. See actor.js for further info.
- html.find('.check-button.challenge').click((ev) => {
- staActor.rollChallengeRoll(ev, 'Generic', 0, this.actor);
- });
-
- html.find('.reroll-result').click((ev) => {
- let selectedAttribute = '';
- let selectedAttributeValue = '';
- let selectedDiscipline = '';
- let selectedDisciplineValue = '';
- for (i = 0; i <= 5; i++) {
- if (html.find('.selector.attribute')[i].checked === true) {
- selectedAttribute = html.find('.selector.attribute')[i].id;
- selectedAttribute = selectedAttribute.slice(0, -9);
- selectedAttributeValue = html.find('#'+selectedAttribute)[0].value;
- }
- }
- for (i = 0; i <= 5; i++) {
- if (html.find('.selector.discipline')[i].checked === true) {
- selectedDiscipline = html.find('.selector.discipline')[i].id;
- selectedDiscipline = selectedDiscipline.slice(0, -9);
- selectedDisciplineValue = html.find('#'+selectedDiscipline)[0].value;
- }
- }
-
- staActor.rollAttributeTest(ev, selectedAttribute,
- parseInt(selectedAttributeValue), selectedDiscipline,
- parseInt(selectedDisciplineValue), null, this.actor);
- });
-
- $(html).find('[id^=character-weapon-]').each( function( _, value ) {
- const weaponDamage = parseInt(value.dataset.itemDamage);
- const securityValue = parseInt(html.find('#security')[0].value);
- const attackDamageValue = weaponDamage + securityValue;
- value.getElementsByClassName('damage')[0].innerText = attackDamageValue;
- });
- }
-}
+import {STASharedActorFunctions} from '../actor.js';
+
+export class STACharacterSheet extends ActorSheet {
+ /** @override */
+ static get defaultOptions() {
+ return foundry.utils.mergeObject(super.defaultOptions, {
+ width: 850,
+ height: 910,
+ dragDrop: [{
+ dragSelector: '.item-list .item',
+ dropSelector: null
+ }],
+ tabs: [
+ {
+ navSelector: '.sheet-tabs',
+ contentSelector: '.sheet-body',
+ initial: 'tab1',
+ }
+ ]
+ });
+ }
+
+ /* -------------------------------------------- */
+
+ // If the player is not a GM and has limited permissions - send them to the limited sheet, otherwise, continue as usual.
+ /** @override */
+ get template() {
+ const versionInfo = game.world.coreVersion;
+ if ( !game.user.isGM && this.actor.limited) return 'systems/sta/templates/actors/limited-sheet.hbs';
+ return `systems/sta/templates/actors/character-sheet.hbs`;
+ }
+ render(force = false, options = {}) {
+ if (!game.user.isGM && this.actor.limited) {
+ options = foundry.utils.mergeObject(options, {height: 250});
+ }
+ return super.render(force, options);
+ }
+
+ /* -------------------------------------------- */
+
+ /** @override */
+ getData() {
+ const sheetData = this.object;
+ sheetData.dtypes = ['String', 'Number', 'Boolean'];
+
+ // Temporary fix I'm leaving in place until I deprecate in a future version
+ const overrideMinAttributeTags = ['[Minor]', '[Notable]', '[Major]', '[NPC]', '[Child]'];
+ const overrideMinAttribute = overrideMinAttributeTags.some((tag) =>
+ sheetData.name.toLowerCase().indexOf(tag.toLowerCase()) !== -1
+ );
+
+ // Ensure attribute and discipline values aren't over the max/min.
+ let minAttribute = overrideMinAttribute ? 0 : 7;
+ let maxAttribute = 12;
+ const overrideAttributeLimitSetting = game.settings.get('sta', 'characterAttributeLimitIgnore');
+ if (overrideAttributeLimitSetting) {
+ minAttribute = 0;
+ maxAttribute = 99;
+ }
+ $.each(sheetData.system.attributes, (key, attribute) => {
+ if (attribute.value > maxAttribute) attribute.value = maxAttribute;
+ if (attribute.value < minAttribute) attribute.value = minAttribute;
+ });
+ const minDiscipline = 0;
+ let maxDiscipline = 5;
+ const overrideDisciplineLimitSetting = game.settings.get('sta', 'characterDisciplineLimitIgnore');
+ if (overrideDisciplineLimitSetting) {
+ maxDiscipline = 99;
+ }
+ $.each(sheetData.system.disciplines, (key, discipline) => {
+ if (discipline.value > maxDiscipline) discipline.value = maxDiscipline;
+ if (discipline.value < minDiscipline) discipline.value = minDiscipline;
+ });
+
+ // Check stress max/min
+ if (!(sheetData.system.stress)) {
+ sheetData.system.stress = {};
+ }
+ if (sheetData.system.stress.value > sheetData.system.stress.max) {
+ sheetData.system.stress.value = sheetData.system.stress.max;
+ }
+ if (sheetData.system.stress.value < 0) {
+ sheetData.system.stress.value = 0;
+ }
+
+ // Check determination max/min
+ if (!(sheetData.system.determination)) {
+ sheetData.system.determination = {};
+ }
+ if (sheetData.system.determination.value > 3) {
+ sheetData.system.determination.value = 3;
+ }
+ if (sheetData.system.determination.value < 0) {
+ sheetData.system.determination.value = 0;
+ }
+
+ // Check reputation max/min
+ if (!(sheetData.system.reputation)) {
+ sheetData.system.reputation = {};
+ }
+ if (sheetData.system.reputation.value > 20) {
+ sheetData.system.reputation.value = 20;
+ }
+ if (sheetData.system.reputation < 0) {
+ sheetData.system.reputation = 0;
+ }
+
+ return sheetData;
+ }
+
+ /* -------------------------------------------- */
+
+ /** @override */
+ activateListeners(html) {
+ super.activateListeners(html);
+
+ // Allows checking version easily
+ const versionInfo = game.world.coreVersion;
+
+ // Opens the class STASharedActorFunctions for access at various stages.
+ const staActor = new STASharedActorFunctions();
+
+ // If the player has limited access to the actor, there is nothing to see here. Return.
+ if ( !game.user.isGM && this.actor.limited) return;
+
+ // We use i a lot in for loops. Best to assign it now for use later in multiple places.
+ let i;
+
+ // TODO: This is not really doing anything yet
+ // Here we are checking if there is armor equipped.
+ // The player can only have one armor. As such, we will use this later.
+ let armorNumber = 0;
+ let stressTrackMax = 0;
+ function armorCount(currentActor) {
+ armorNumber = 0;
+ currentActor.actor.items.forEach((values) => {
+ if (values.type == 'armor') {
+ if (values.equipped == true) armorNumber+= 1;
+ }
+ });
+ }
+ armorCount(this);
+
+ // This creates a dynamic Determination Point tracker. It sets max determination to 3 (it is dynamic in Dishonored) and
+ // creates a new div for each and places it under a child called "bar-determination-renderer"
+ const determinationPointsMax = 3;
+ for (i = 1; i <= determinationPointsMax; i++) {
+ const detDiv = document.createElement('DIV');
+ detDiv.className = 'box';
+ detDiv.id = 'determination-' + i;
+ detDiv.innerHTML = i;
+ detDiv.style = 'width: calc(100% / 3);';
+ html.find('#bar-determination-renderer')[0].appendChild(detDiv);
+ }
+
+ // This creates a dynamic Stress tracker. It polls for the value of the fitness attribute, security discipline, and checks for Resolute talent.
+ // With the total value, creates a new div for each and places it under a child called "bar-stress-renderer".
+ function stressTrackUpdate() {
+ const localizedValues = {
+ 'resolute': game.i18n.localize('sta.actor.character.talents.resolute')
+ };
+
+ stressTrackMax = parseInt(html.find('#fitness')[0].value) + parseInt(html.find('#security')[0].value);
+ if (html.find(`[data-talent-name*="${localizedValues.resolute}"]`).length > 0) {
+ stressTrackMax += 3;
+ }
+ stressTrackMax += parseInt(html.find('#strmod')[0].value);
+ // This checks that the max-stress hidden field is equal to the calculated Max Stress value, if not it makes it so.
+ if (html.find('#max-stress')[0].value != stressTrackMax) {
+ html.find('#max-stress')[0].value = stressTrackMax;
+ }
+ html.find('#bar-stress-renderer').empty();
+ for (let i = 1; i <= stressTrackMax; i++) {
+ const stressDiv = document.createElement('DIV');
+ stressDiv.className = 'box';
+ stressDiv.id = 'stress-' + i;
+ stressDiv.innerHTML = i;
+ stressDiv.style = 'width: calc(100% / ' + html.find('#max-stress')[0].value + ');';
+ html.find('#bar-stress-renderer')[0].appendChild(stressDiv);
+ }
+ }
+ stressTrackUpdate();
+
+ // This creates a dynamic Reputation tracker. For this it uses a max value of 30. This can be configured here.
+ // It creates a new div for each and places it under a child called "bar-rep-renderer"
+ const repPointsMax = game.settings.get('sta', 'maxNumberOfReputation');
+ for (let i = 1; i <= repPointsMax; i++) {
+ const repDiv = document.createElement('DIV');
+ repDiv.className = 'box';
+ repDiv.id = 'rep-' + i;
+ repDiv.innerHTML = i;
+ repDiv.style = 'width: calc(100% / ' + repPointsMax + ');';
+ html.find('#bar-rep-renderer')[0].appendChild(repDiv);
+ }
+
+ // Fires the function staRenderTracks as soon as the parameters exist to do so.
+ // staActor.staRenderTracks(html, stressTrackMax, determinationPointsMax, repPointsMax);
+ staActor.staRenderTracks(html, stressTrackMax,
+ determinationPointsMax, repPointsMax);
+
+ // This if statement checks if the form is editable, if not it hides control used by the owner, then aborts any more of the script.
+ if (!this.options.editable) {
+ // This hides the ability to Perform an Attribute Test for the character.
+ for (i = 0; i < html.find('.check-button').length; i++) {
+ html.find('.check-button')[i].style.display = 'none';
+ }
+ // This hides all toggle, add, and delete item images.
+ for (i = 0; i < html.find('.control.create').length; i++) {
+ html.find('.control.create')[i].style.display = 'none';
+ }
+ for (i = 0; i < html.find('.control .delete').length; i++) {
+ html.find('.control .delete')[i].style.display = 'none';
+ }
+ for (i = 0; i < html.find('.control.toggle').length; i++) {
+ html.find('.control.delete')[i].style.display = 'none';
+ }
+ // This hides all attribute and discipline check boxes (and titles)
+ for (i = 0; i < html.find('.selector').length; i++) {
+ html.find('.selector')[i].style.display = 'none';
+ }
+ for (i = 0; i < html.find('.selector').length; i++) {
+ html.find('.selector')[i].style.display = 'none';
+ }
+ // Remove hover CSS from clickables that are no longer clickable.
+ for (i = 0; i < html.find('.box').length; i++) {
+ html.find('.box')[i].classList.add('unset-clickables');
+ }
+ for (i = 0; i < html.find('.rollable').length; i++) {
+ html.find('.rollable')[i].classList.add('unset-clickables');
+ }
+
+ return;
+ };
+
+ // This toggles whether the value is used or not.
+ html.find('.control.toggle').click((ev) => {
+ const itemId = ev.currentTarget.closest('.entry').dataset.itemId;
+ const item = this.actor.items.get(itemId);
+ const state = item.system.used;
+ if (state) {
+ item.system.used = false;
+ $(ev.currentTarget).children()[0].classList.remove('fa-toggle-on');
+ $(ev.currentTarget).children()[0].classList.add('fa-toggle-off');
+ $(ev.currentTarget).parents('.entry')[0].setAttribute('data-item-used', 'false');
+ $(ev.currentTarget).parents('.entry')[0].style.textDecoration = 'none';
+ } else {
+ item.system.used = true;
+ $(ev.currentTarget).children()[0].classList.remove('fa-toggle-off');
+ $(ev.currentTarget).children()[0].classList.add('fa-toggle-on');
+ $(ev.currentTarget).parents('.entry')[0].setAttribute('data-item-used', 'true');
+ $(ev.currentTarget).parents('.entry')[0].style.textDecoration = 'line-through';
+ }
+ return this.actor.items.get(itemId).update({['system.used']: getProperty(item.system, 'used')});
+ });
+
+ // This allows for all items to be rolled, it gets the current targets type and id and sends it to the rollGenericItem function.
+ html.find('.chat,.rollable').click((ev) =>{
+ const itemType = $(ev.currentTarget).parents('.entry')[0].getAttribute('data-item-type');
+ const itemId = $(ev.currentTarget).parents('.entry')[0].getAttribute('data-item-id');
+ staActor.rollGenericItem(ev, itemType, itemId, this.actor);
+ });
+
+ // Listen for changes in the item name input field
+ html.find('.item-name').on('change', (event) => {
+ const input = event.currentTarget;
+ const itemId = input.dataset.itemId;
+ const item = this.actor.items.get(itemId);
+ const newName = input.value.trim();
+
+ if (item && newName) {
+ item.update({name: newName});
+ }
+ });
+
+ // Create new items
+ html.find('.control.create').click(async (ev) => {
+ ev.preventDefault();
+ const header = ev.currentTarget;
+ const type = header.dataset.type;
+ const data = Object.assign({}, header.dataset);
+ const name = `New ${type.capitalize()}`;
+
+ const itemData = {
+ name: name,
+ type: type,
+ data: data,
+ };
+ delete itemData.data['type'];
+
+ const newItem = await this.actor.createEmbeddedDocuments('Item', [itemData]);
+ });
+
+ // Edit items
+ html.find('.control .edit').click((ev) => {
+ const li = $(ev.currentTarget).parents('.entry');
+ const item = this.actor.items.get(li.data('itemId'));
+ item.sheet.render(true);
+ });
+
+ // Delete items with confirmation dialog
+ html.find('.control .delete').click((ev) => {
+ const li = $(ev.currentTarget).parents('.entry');
+ const itemId = li.data('itemId');
+
+ new Dialog({
+ title: `${game.i18n.localize('sta.apps.deleteitem')}`,
+ content: `
${game.i18n.localize('sta.apps.deleteconfirm')}
`,
+ buttons: {
+ yes: {
+ icon: '',
+ label: `${game.i18n.localize('sta.apps.yes')}`,
+ callback: () => this.actor.deleteEmbeddedDocuments('Item', [itemId])
+ },
+ no: {
+ icon: '',
+ label: `${game.i18n.localize('sta.apps.no')}`
+ }
+ },
+ default: 'no'
+ }).render(true);
+ });
+
+ // Item popout tooltip of description
+ html.find('.item-name').on('mouseover', (event) => {
+ const input = event.currentTarget;
+ const itemId = input.dataset.itemId;
+ const item = this.actor.items.get(itemId);
+
+ if (item) {
+ const description = item.system.description?.trim().replace(/\n/g, ' ');
+
+ if (description) {
+ input._tooltipTimeout = setTimeout(() => {
+ let tooltip = document.querySelector('.item-tooltip');
+ if (!tooltip) {
+ tooltip = document.createElement('div');
+ tooltip.classList.add('item-tooltip');
+ document.body.appendChild(tooltip);
+ }
+
+ tooltip.innerHTML = `${description}`;
+
+ const {clientX: mouseX, clientY: mouseY} = event;
+ tooltip.style.left = `${mouseX + 10}px`;
+ tooltip.style.top = `${mouseY + 10}px`;
+
+ document.body.appendChild(tooltip);
+ const tooltipRect = tooltip.getBoundingClientRect();
+
+ if (tooltipRect.bottom > window.innerHeight) {
+ tooltip.style.top = `${window.innerHeight - tooltipRect.height - 20}px`;
+ }
+
+ input._tooltip = tooltip;
+ }, 1000);
+ }
+ }
+ });
+
+ html.find('.item-name').on('mouseout', (event) => {
+ const input = event.currentTarget;
+
+ if (input._tooltipTimeout) {
+ clearTimeout(input._tooltipTimeout);
+ delete input._tooltipTimeout;
+ }
+
+ if (input._tooltip) {
+ document.body.removeChild(input._tooltip);
+ delete input._tooltip;
+ }
+ });
+
+ // Reads if a reputation track box has been clicked, and if it has will either: set the value to the clicked box, or reduce the value by one.
+ // This check is dependent on various requirements, see comments in code.
+ html.find('[id^="rep"]').click((ev) => {
+ let total = '';
+ const newTotalObject = $(ev.currentTarget)[0];
+ const newTotal = newTotalObject.id.replace(/\D/g, '');
+ // data-selected stores whether the track box is currently activated or not. This checks that the box is activated
+ if (newTotalObject.getAttribute('data-selected') === 'true') {
+ // Now we check that the "next" track box is not activated.
+ // If there isn't one, or it isn't activated, we only want to decrease the value by 1 rather than setting the value.
+ const nextCheck = 'rep-' + (parseInt(newTotal) + 1);
+ if (!html.find('#'+nextCheck)[0] || html.find('#'+nextCheck)[0].getAttribute('data-selected') != 'true') {
+ html.find('#total-rep')[0].value = html.find('#total-rep')[0].value - 1;
+ this.submit();
+ // If it isn't caught by the if, the next box is likely activated. If something happened, its safer to set the value anyway.
+ } else {
+ total = html.find('#total-rep')[0].value;
+ if (total != newTotal) {
+ html.find('#total-rep')[0].value = newTotal;
+ this.submit();
+ }
+ }
+ // If the clicked box wasn't activated, we need to activate it now.
+ } else {
+ total = html.find('#total-rep')[0].value;
+ if (total != newTotal) {
+ html.find('#total-rep')[0].value = newTotal;
+ this.submit();
+ }
+ }
+ });
+
+ // Reads if a stress track box has been clicked, and if it has will either: set the value to the clicked box, or reduce the value by one.
+ // See line 186-220 for a more detailed break down on the context of each scenario. Stress uses the same logic.
+ html.find('[id^="stress"]').click((ev) => {
+ let total = '';
+ const newTotalObject = $(ev.currentTarget)[0];
+ const newTotal = newTotalObject.id.substring(7);
+ if (newTotalObject.getAttribute('data-selected') === 'true') {
+ const nextCheck = 'stress-' + (parseInt(newTotal) + 1);
+ if (!html.find('#'+nextCheck)[0] || html.find('#'+nextCheck)[0].getAttribute('data-selected') != 'true') {
+ html.find('#total-stress')[0].value = html.find('#total-stress')[0].value - 1;
+ this.submit();
+ // If it isn't caught by the if, the next box is likely activated. If something happened, its safer to set the value anyway.
+ } else {
+ total = html.find('#total-stress')[0].value;
+ if (total != newTotal) {
+ html.find('#total-stress')[0].value = newTotal;
+ this.submit();
+ }
+ }
+ // If the clicked box wasn't activated, we need to activate it now.
+ } else {
+ total = html.find('#total-stress')[0].value;
+ if (total != newTotal) {
+ html.find('#total-stress')[0].value = newTotal;
+ this.submit();
+ }
+ }
+ });
+
+ // Reads if a determination track box has been clicked, and if it has will either: set the value to the clicked box, or reduce the value by one.
+ // See line 186-220 for a more detailed break down on the context of each scenario. Determination uses the same logic.
+ html.find('[id^="determination"]').click((ev) => {
+ let total = '';
+ const newTotalObject = $(ev.currentTarget)[0];
+ const newTotal = newTotalObject.id.replace(/\D/g, '');
+ if (newTotalObject.getAttribute('data-selected') === 'true') {
+ const nextCheck = 'determination-' + (parseInt(newTotal) + 1);
+ if (!html.find('#'+nextCheck)[0] || html.find('#'+nextCheck)[0].getAttribute('data-selected') != 'true') {
+ html.find('#total-determination')[0].value = html.find('#total-determination')[0].value - 1;
+ this.submit();
+ // If it isn't caught by the if, the next box is likely activated. If something happened, its safer to set the value anyway.
+ } else {
+ total = html.find('#total-determination')[0].value;
+ if (total != newTotal) {
+ html.find('#total-determination')[0].value = newTotal;
+ this.submit();
+ }
+ }
+ // If the clicked box wasn't activated, we need to activate it now.
+ } else {
+ total = html.find('#total-determination')[0].value;
+ if (total != newTotal) {
+ html.find('#total-determination')[0].value = newTotal;
+ this.submit();
+ }
+ }
+ });
+
+ // Turns the Attribute checkboxes into essentially a radio button. It removes any other ticks, and then checks the new attribute.
+ // Finally a submit is required as data has changed.
+ html.find('.selector.attribute').click((ev) => {
+ for (i = 0; i <= 5; i++) {
+ html.find('.selector.attribute')[i].checked = false;
+ }
+ $(ev.currentTarget)[0].checked = true;
+ this.submit();
+ });
+
+ // Turns the Discipline checkboxes into essentially a radio button. It removes any other ticks, and then checks the new discipline.
+ // Finally a submit is required as data has changed.
+ html.find('.selector.discipline').click((ev) => {
+ for (i = 0; i <= 5; i++) {
+ html.find('.selector.discipline')[i].checked = false;
+ }
+ $(ev.currentTarget)[0].checked = true;
+ this.submit();
+ });
+
+ // If the check-button is clicked it performs the acclaim or reprimand calculation.
+ html.find('.check-button.acclaim').click(async (ev) => {
+ const dialogContent = `
+
+ `;
+
+ new Dialog({
+ title: `${game.i18n.localize('sta.roll.acclaim')}`,
+ content: dialogContent,
+ buttons: {
+ roll: {
+ label: `${game.i18n.localize('sta.roll.acclaim')}`,
+ callback: async (html) => {
+ const PositiveInfluences = parseInt(html.find('#positiveInfluences').val()) || 1;
+ const NegativeInfluences = parseInt(html.find('#negativeInfluences').val()) || 0;
+
+ const selectedDisciplineValue = parseInt(document.querySelector('#total-rep')?.value) || 0;
+ const existingReprimand = parseInt(document.querySelector('#reprimand')?.value) || 0;
+ const targetNumber = selectedDisciplineValue + 7;
+ const complicationThreshold = 20 - Math.min(existingReprimand, 5);
+ const diceRollFormula = `${PositiveInfluences}d20`;
+ const roll = new Roll(diceRollFormula);
+
+ await roll.evaluate();
+
+ let totalSuccesses = 0;
+ let complications = 0;
+ let acclaim = 0;
+ let reprimand = 0;
+ const diceResults = [];
+
+ roll.terms[0].results.forEach((die) => {
+ let coloredDieResult;
+
+ if (die.result >= complicationThreshold) {
+ coloredDieResult = `${die.result}`; // Red for complications
+ complications += 1;
+ } else if (die.result <= selectedDisciplineValue) {
+ coloredDieResult = `${die.result}`; // Green for double successes
+ totalSuccesses += 2;
+ } else if (die.result <= targetNumber && die.result > selectedDisciplineValue) {
+ coloredDieResult = `${die.result}`; // Blue for single successes
+ totalSuccesses += 1;
+ } else {
+ coloredDieResult = `${die.result}`; // Default for other results
+ }
+ diceResults.push(coloredDieResult);
+ });
+
+ let chatContent = `${game.i18n.format('sta.roll.dicerolls')} ${diceResults.join(', ')} `;
+
+ if (totalSuccesses > NegativeInfluences) {
+ acclaim = totalSuccesses - NegativeInfluences;
+ chatContent += `${game.i18n.format('sta.roll.gainacclaim', {0: acclaim})}`;
+ } else if (totalSuccesses < NegativeInfluences) {
+ reprimand = (NegativeInfluences - totalSuccesses) + complications;
+ chatContent += `${game.i18n.format('sta.roll.gainreprimand', {0: reprimand})}`;
+ } else if (totalSuccesses === NegativeInfluences) {
+ chatContent += `${game.i18n.localize('sta.roll.nochange')}`;
+ }
+
+ ChatMessage.create({
+ speaker: ChatMessage.getSpeaker(),
+ content: chatContent
+ });
+ }
+ }
+ },
+ render: (html) => {
+ html.find('button').addClass('dialog-button roll default');
+ }
+ }).render(true);
+ });
+
+ // If the check-button is clicked it grabs the selected attribute and the selected discipline and fires the method rollAttributeTest. See actor.js for further info.
+ html.find('.check-button.attribute').click((ev) => {
+ let selectedAttribute = '';
+ let selectedAttributeValue = '';
+ let selectedDiscipline = '';
+ let selectedDisciplineValue = '';
+ for (i = 0; i <= 5; i++) {
+ if (html.find('.selector.attribute')[i].checked === true) {
+ selectedAttribute = html.find('.selector.attribute')[i].id;
+ selectedAttribute = selectedAttribute.slice(0, -9);
+ selectedAttributeValue = html.find('#'+selectedAttribute)[0].value;
+ }
+ }
+ for (i = 0; i <= 5; i++) {
+ if (html.find('.selector.discipline')[i].checked === true) {
+ selectedDiscipline = html.find('.selector.discipline')[i].id;
+ selectedDiscipline = selectedDiscipline.slice(0, -9);
+ selectedDisciplineValue = html.find('#'+selectedDiscipline)[0].value;
+ }
+ }
+
+ staActor.rollAttributeTest(ev, selectedAttribute,
+ parseInt(selectedAttributeValue), selectedDiscipline,
+ parseInt(selectedDisciplineValue), 2, this.actor);
+ });
+
+ // If the check-button is clicked it fires the method challenge roll method. See actor.js for further info.
+ html.find('.check-button.challenge').click((ev) => {
+ staActor.rollChallengeRoll(ev, 'Generic', 0, this.actor);
+ });
+
+ html.find('.reroll-result').click((ev) => {
+ let selectedAttribute = '';
+ let selectedAttributeValue = '';
+ let selectedDiscipline = '';
+ let selectedDisciplineValue = '';
+ for (i = 0; i <= 5; i++) {
+ if (html.find('.selector.attribute')[i].checked === true) {
+ selectedAttribute = html.find('.selector.attribute')[i].id;
+ selectedAttribute = selectedAttribute.slice(0, -9);
+ selectedAttributeValue = html.find('#'+selectedAttribute)[0].value;
+ }
+ }
+ for (i = 0; i <= 5; i++) {
+ if (html.find('.selector.discipline')[i].checked === true) {
+ selectedDiscipline = html.find('.selector.discipline')[i].id;
+ selectedDiscipline = selectedDiscipline.slice(0, -9);
+ selectedDisciplineValue = html.find('#'+selectedDiscipline)[0].value;
+ }
+ }
+
+ staActor.rollAttributeTest(ev, selectedAttribute,
+ parseInt(selectedAttributeValue), selectedDiscipline,
+ parseInt(selectedDisciplineValue), null, this.actor);
+ });
+
+ $(html).find('[id^=character-weapon-]').each( function( _, value ) {
+ const weaponDamage = parseInt(value.dataset.itemDamage);
+ const securityValue = parseInt(html.find('#security')[0].value);
+ const attackDamageValue = weaponDamage + securityValue;
+ value.getElementsByClassName('damage')[0].innerText = attackDamageValue;
+ });
+ }
+}
diff --git a/src/module/actors/sheets/character-sheet2e.js b/src/module/actors/sheets/character-sheet2e.js
index ae2b528..069f13e 100644
--- a/src/module/actors/sheets/character-sheet2e.js
+++ b/src/module/actors/sheets/character-sheet2e.js
@@ -4,7 +4,6 @@ export class STACharacterSheet2e extends ActorSheet {
/** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
- classes: ['sta', 'sheet', 'actor', 'character'],
width: 850,
height: 910,
dragDrop: [{
@@ -13,9 +12,9 @@ export class STACharacterSheet2e extends ActorSheet {
}],
tabs: [
{
- navSelector: '.character-tabs',
- contentSelector: '.character-header',
- initial: 'biography',
+ navSelector: '.sheet-tabs',
+ contentSelector: '.sheet-body',
+ initial: 'tab1',
}
]
});
@@ -28,9 +27,14 @@ export class STACharacterSheet2e extends ActorSheet {
get template() {
const versionInfo = game.world.coreVersion;
if ( !game.user.isGM && this.actor.limited) return 'systems/sta/templates/actors/limited-sheet.hbs';
- if (!foundry.utils.isNewerVersion(versionInfo, '0.8.-1')) return 'systems/sta/templates/actors/character-sheet-legacy.hbs';
return `systems/sta/templates/actors/character-sheet2e.hbs`;
}
+ render(force = false, options = {}) {
+ if (!game.user.isGM && this.actor.limited) {
+ options = foundry.utils.mergeObject(options, {height: 250});
+ }
+ return super.render(force, options);
+ }
/* -------------------------------------------- */
@@ -41,8 +45,8 @@ export class STACharacterSheet2e extends ActorSheet {
// Temporary fix I'm leaving in place until I deprecate in a future version
const overrideMinAttributeTags = ['[Minor]', '[Notable]', '[Major]', '[NPC]', '[Child]'];
- const overrideMinAttribute = overrideMinAttributeTags.some(
- (tag) => sheetData.name.toLowerCase().indexOf(tag.toLowerCase()) !== -1
+ const overrideMinAttribute = overrideMinAttributeTags.some((tag) =>
+ sheetData.name.toLowerCase().indexOf(tag.toLowerCase()) !== -1
);
@@ -203,13 +207,6 @@ export class STACharacterSheet2e extends ActorSheet {
staActor.staRenderTracks(html, stressTrackMax,
determinationPointsMax, repPointsMax);
- // This allows for each item-edit image to link open an item sheet. This uses Simple Worldbuilding System Code.
- html.find('.control .edit').click((ev) => {
- const li = $(ev.currentTarget).parents('.entry');
- const item = this.actor.items.get(li.data('itemId'));
- item.sheet.render(true);
- });
-
// This if statement checks if the form is editable, if not it hides control used by the owner, then aborts any more of the script.
if (!this.options.editable) {
// This hides the ability to Perform an Attribute Test for the character.
@@ -272,46 +269,115 @@ export class STACharacterSheet2e extends ActorSheet {
staActor.rollGenericItem(ev, itemType, itemId, this.actor);
});
- // Allows item-create images to create an item of a type defined individually by each button. This uses code found via the Foundry VTT System Tutorial.
- html.find('.control.create').click((ev) => {
+ // Listen for changes in the item name input field
+ html.find('.item-name').on('change', (event) => {
+ const input = event.currentTarget;
+ const itemId = input.dataset.itemId;
+ const item = this.actor.items.get(itemId);
+ const newName = input.value.trim();
+
+ if (item && newName) {
+ item.update({name: newName});
+ }
+ });
+
+ // Create new items
+ html.find('.control.create').click(async (ev) => {
ev.preventDefault();
const header = ev.currentTarget;
const type = header.dataset.type;
- const data = foundry.utils.duplicate(header.dataset);
+ const data = Object.assign({}, header.dataset);
const name = `New ${type.capitalize()}`;
- if (type == 'armor' && armorNumber >= 1) {
- ui.notifications.info('The current actor has an equipped armor already. Adding unequipped.');
- data.equipped = false;
- }
+
const itemData = {
name: name,
type: type,
data: data,
- img: game.sta.defaultImage
};
delete itemData.data['type'];
- if (foundry.utils.isNewerVersion(versionInfo, '0.8.-1')) {
- return this.actor.createEmbeddedDocuments('Item', [(itemData)]);
- } else {
- return this.actor.createOwnedItem(itemData);
- }
+
+ const newItem = await this.actor.createEmbeddedDocuments('Item', [itemData]);
+ });
+
+ // Edit items
+ html.find('.control .edit').click((ev) => {
+ const li = $(ev.currentTarget).parents('.entry');
+ const item = this.actor.items.get(li.data('itemId'));
+ item.sheet.render(true);
});
- // Allows item-delete images to allow deletion of the selected item.
+ // Delete items with confirmation dialog
html.find('.control .delete').click((ev) => {
const li = $(ev.currentTarget).parents('.entry');
- this.activeDialog = staActor.deleteConfirmDialog(
- li[0].getAttribute('data-item-value'),
- () => {
- if ( foundry.utils.isNewerVersion( versionInfo, '0.8.-1' )) {
- this.actor.deleteEmbeddedDocuments( 'Item', [li.data('itemId')] );
- } else {
- this.actor.deleteOwnedItem( li.data( 'itemId' ));
+ const itemId = li.data('itemId');
+
+ new Dialog({
+ title: `${game.i18n.localize('sta.apps.deleteitem')}`,
+ content: `
${game.i18n.localize('sta.apps.deleteconfirm')}
`,
+ buttons: {
+ yes: {
+ icon: '',
+ label: `${game.i18n.localize('sta.apps.yes')}`,
+ callback: () => this.actor.deleteEmbeddedDocuments('Item', [itemId])
+ },
+ no: {
+ icon: '',
+ label: `${game.i18n.localize('sta.apps.no')}`
}
},
- () => this.activeDialog = null
- );
- this.activeDialog.render(true);
+ default: 'no'
+ }).render(true);
+ });
+
+ // Item popout tooltip of description
+ html.find('.item-name').on('mouseover', (event) => {
+ const input = event.currentTarget;
+ const itemId = input.dataset.itemId;
+ const item = this.actor.items.get(itemId);
+
+ if (item) {
+ const description = item.system.description?.trim().replace(/\n/g, ' ');
+
+ if (description) {
+ input._tooltipTimeout = setTimeout(() => {
+ let tooltip = document.querySelector('.item-tooltip');
+ if (!tooltip) {
+ tooltip = document.createElement('div');
+ tooltip.classList.add('item-tooltip');
+ document.body.appendChild(tooltip);
+ }
+
+ tooltip.innerHTML = `${description}`;
+
+ const {clientX: mouseX, clientY: mouseY} = event;
+ tooltip.style.left = `${mouseX + 10}px`;
+ tooltip.style.top = `${mouseY + 10}px`;
+
+ document.body.appendChild(tooltip);
+ const tooltipRect = tooltip.getBoundingClientRect();
+
+ if (tooltipRect.bottom > window.innerHeight) {
+ tooltip.style.top = `${window.innerHeight - tooltipRect.height - 20}px`;
+ }
+
+ input._tooltip = tooltip;
+ }, 1000);
+ }
+ }
+ });
+
+ html.find('.item-name').on('mouseout', (event) => {
+ const input = event.currentTarget;
+
+ if (input._tooltipTimeout) {
+ clearTimeout(input._tooltipTimeout);
+ delete input._tooltipTimeout;
+ }
+
+ if (input._tooltip) {
+ document.body.removeChild(input._tooltip);
+ delete input._tooltip;
+ }
});
// Reads if a reputation track box has been clicked, and if it has will either: set the value to the clicked box, or reduce the value by one.
@@ -404,74 +470,6 @@ export class STACharacterSheet2e extends ActorSheet {
}
});
- // This is used to clean up all the HTML that comes from displaying outputs from the text editor boxes. There's probably a better way to do this but the quick and dirty worked this time.
- $.each($('[id^=talent-tooltip-text-]'), function(index, value) {
- const beforeDescription = value.innerHTML;
- const decoded = TextEditor.decodeHTML(beforeDescription);
- const prettifiedDescription = TextEditor.previewHTML(decoded, 1000);
- $('#' + value.id).html(prettifiedDescription);
- });
-
-
- html.find('.talent-tooltip-clickable').click((ev) => {
- const talentId = $(ev.currentTarget)[0].id.substring('talent-tooltip-clickable-'.length);
- const currentShowingTalentId = $('.talent-tooltip-container:not(.hide)')[0] ? $('.talent-tooltip-container:not(.hide)')[0].id.substring('talent-tooltip-container-'.length) : null;
-
- if (talentId == currentShowingTalentId) {
- $('#talent-tooltip-container-' + talentId).addClass('hide').removeAttr('style');
- } else {
- $('.talent-tooltip-container').addClass('hide').removeAttr('style');
- $('#talent-tooltip-container-' + talentId).removeClass('hide').height($('#talent-tooltip-text-' + talentId)[0].scrollHeight + 5);
- }
- });
-
- $.each($('[id^=injury-tooltip-text-]'), function(index, value) {
- const beforeDescription = value.innerHTML;
- const decoded = TextEditor.decodeHTML(beforeDescription);
- const prettifiedDescription = TextEditor.previewHTML(decoded, 1000);
- $('#' + value.id).html(prettifiedDescription);
- });
-
-
- html.find('.injury-tooltip-clickable').click((ev) => {
- const injuryId = $(ev.currentTarget)[0].id.substring('injury-tooltip-clickable-'.length);
- const currentShowinginjuryId = $('.injury-tooltip-container:not(.hide)')[0] ? $('.injury-tooltip-container:not(.hide)')[0].id.substring('injury-tooltip-container-'.length) : null;
-
- if (injuryId == currentShowinginjuryId) {
- $('#injury-tooltip-container-' + injuryId).addClass('hide').removeAttr('style');
- } else {
- $('.injury-tooltip-container').addClass('hide').removeAttr('style');
- $('#injury-tooltip-container-' + injuryId).removeClass('hide').height($('#injury-tooltip-text-' + injuryId)[0].scrollHeight + 5);
- }
- });
-
- $.each($('[id^=focus-tooltip-text-]'), function(index, value) {
- const beforeDescription = value.innerHTML;
- const decoded = TextEditor.decodeHTML(beforeDescription);
- const prettifiedDescription = TextEditor.previewHTML(decoded, 1000);
- $('#' + value.id).html(prettifiedDescription);
- });
-
-
- html.find('.focus-tooltip-clickable').click((ev) => {
- const focusId = $(ev.currentTarget)[0].id.substring('focus-tooltip-clickable-'.length);
- const currentShowingfocusId = $('.focus-tooltip-container:not(.hide)')[0] ? $('.focus-tooltip-container:not(.hide)')[0].id.substring('focus-tooltip-container-'.length) : null;
-
- if (focusId == currentShowingfocusId) {
- $('#focus-tooltip-container-' + focusId).addClass('hide').removeAttr('style');
- } else {
- $('.focus-tooltip-container').addClass('hide').removeAttr('style');
- $('#focus-tooltip-container-' + focusId).removeClass('hide').height($('#focus-tooltip-text-' + focusId)[0].scrollHeight + 5);
- }
- });
-
- $.each($('[id^=value-tooltip-text-]'), function(index, value) {
- const beforeDescription = value.innerHTML;
- const decoded = TextEditor.decodeHTML(beforeDescription);
- const prettifiedDescription = TextEditor.previewHTML(decoded, 1000);
- $('#' + value.id).html(prettifiedDescription);
- });
-
// Turns the Attribute checkboxes into essentially a radio button. It removes any other ticks, and then checks the new attribute.
// Finally a submit is required as data has changed.
html.find('.selector.attribute').click((ev) => {
@@ -496,18 +494,17 @@ export class STACharacterSheet2e extends ActorSheet {
// If the check-button is clicked it performs the acclaim or reprimand calculation.
html.find('.check-button.acclaim').click(async (ev) => {
const dialogContent = `
- `;
+
+ `;
new Dialog({
title: `${game.i18n.localize('sta.roll.acclaim')}`,
@@ -518,7 +515,7 @@ export class STACharacterSheet2e extends ActorSheet {
callback: async (html) => {
const PositiveInfluences = parseInt(html.find('#positiveInfluences').val()) || 1;
const NegativeInfluences = parseInt(html.find('#negativeInfluences').val()) || 0;
-
+
const selectedDisciplineValue = parseInt(document.querySelector('#total-rep')?.value) || 0;
const existingReprimand = parseInt(document.querySelector('#reprimand')?.value) || 0;
const targetNumber = selectedDisciplineValue + 7;
@@ -541,10 +538,10 @@ export class STACharacterSheet2e extends ActorSheet {
coloredDieResult = `${die.result}`; // Red for complications
complications += 1;
} else if (die.result <= selectedDisciplineValue) {
- coloredDieResult = `${die.result}`; // Green for double successes
+ coloredDieResult = `${die.result}`; // Green for double successes
totalSuccesses += 2;
} else if (die.result <= targetNumber && die.result > selectedDisciplineValue) {
- coloredDieResult = `${die.result}`; // Blue for single successes
+ coloredDieResult = `${die.result}`; // Blue for single successes
totalSuccesses += 1;
} else {
coloredDieResult = `${die.result}`; // Default for other results
diff --git a/src/module/actors/sheets/extended-task-sheet.js b/src/module/actors/sheets/extended-task-sheet.js
index 8617ab9..bac6cc5 100644
--- a/src/module/actors/sheets/extended-task-sheet.js
+++ b/src/module/actors/sheets/extended-task-sheet.js
@@ -6,9 +6,8 @@ export class STAExtendedTaskSheet extends ActorSheet {
/** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
- classes: ['sta', 'sheet', 'actor', 'extendedtask'],
width: 500,
- height: 600
+ height: 500
});
}
diff --git a/src/module/actors/sheets/scenetraits-sheet.js b/src/module/actors/sheets/scenetraits-sheet.js
new file mode 100644
index 0000000..02fa0ce
--- /dev/null
+++ b/src/module/actors/sheets/scenetraits-sheet.js
@@ -0,0 +1,144 @@
+import {
+ STASharedActorFunctions
+} from '../actor.js';
+
+export class STASceneTraits extends ActorSheet {
+ static get defaultOptions() {
+ return foundry.utils.mergeObject(super.defaultOptions, {
+ width: 300,
+ height: 300
+ });
+ }
+
+ get template() {
+ if ( !game.user.isGM && this.actor.limited) {
+ ui.notifications.warn('You do not have permission to view this sheet!');
+ return false;
+ }
+ return `systems/sta/templates/actors/scenetraits-sheet.hbs`;
+ }
+
+ activateListeners(html) {
+ super.activateListeners(html);
+
+ const staActor = new STASharedActorFunctions();
+
+ // set up click handler for items to send to the actor rollGenericItem
+ html.find('.chat,.rollable').click( (ev) => {
+ const itemType = $(ev.currentTarget).parents('.entry')[0].getAttribute('data-item-type');
+ const itemId = $(ev.currentTarget).parents('.entry')[0].getAttribute('data-item-id');
+ staActor.rollGenericItem(ev, itemType, itemId, this.actor);
+ });
+
+ // Listen for changes in the item name input field
+ html.find('.item-name').on('change', (event) => {
+ const input = event.currentTarget;
+ const itemId = input.dataset.itemId;
+ const item = this.actor.items.get(itemId);
+ const newName = input.value.trim();
+
+ if (item && newName) {
+ item.update({name: newName});
+ }
+ });
+
+ // Create new items
+ html.find('.control.create').click(async (ev) => {
+ ev.preventDefault();
+ const header = ev.currentTarget;
+ const type = header.dataset.type;
+ const data = Object.assign({}, header.dataset);
+ const name = `New ${type.capitalize()}`;
+
+ const itemData = {
+ name: name,
+ type: type,
+ data: data,
+ };
+ delete itemData.data['type'];
+
+ const newItem = await this.actor.createEmbeddedDocuments('Item', [itemData]);
+ });
+
+ // Edit items
+ html.find('.control .edit').click((ev) => {
+ const li = $(ev.currentTarget).parents('.entry');
+ const item = this.actor.items.get(li.data('itemId'));
+ item.sheet.render(true);
+ });
+
+ // Delete items with confirmation dialog
+ html.find('.control .delete').click((ev) => {
+ const li = $(ev.currentTarget).parents('.entry');
+ const itemId = li.data('itemId');
+
+ new Dialog({
+ title: `${game.i18n.localize('sta.apps.deleteitem')}`,
+ content: `
${game.i18n.localize('sta.apps.deleteconfirm')}
`,
+ buttons: {
+ yes: {
+ icon: '',
+ label: `${game.i18n.localize('sta.apps.yes')}`,
+ callback: () => this.actor.deleteEmbeddedDocuments('Item', [itemId])
+ },
+ no: {
+ icon: '',
+ label: `${game.i18n.localize('sta.apps.no')}`
+ }
+ },
+ default: 'no'
+ }).render(true);
+ });
+
+ // Item popout tooltip of description
+ html.find('.item-name').on('mouseover', (event) => {
+ const input = event.currentTarget;
+ const itemId = input.dataset.itemId;
+ const item = this.actor.items.get(itemId);
+
+ if (item) {
+ const description = item.system.description?.trim().replace(/\n/g, ' ');
+
+ if (description) {
+ input._tooltipTimeout = setTimeout(() => {
+ let tooltip = document.querySelector('.item-tooltip');
+ if (!tooltip) {
+ tooltip = document.createElement('div');
+ tooltip.classList.add('item-tooltip');
+ document.body.appendChild(tooltip);
+ }
+
+ tooltip.innerHTML = `${description}`;
+
+ const {clientX: mouseX, clientY: mouseY} = event;
+ tooltip.style.left = `${mouseX + 10}px`;
+ tooltip.style.top = `${mouseY + 10}px`;
+
+ document.body.appendChild(tooltip);
+ const tooltipRect = tooltip.getBoundingClientRect();
+
+ if (tooltipRect.bottom > window.innerHeight) {
+ tooltip.style.top = `${window.innerHeight - tooltipRect.height - 20}px`;
+ }
+
+ input._tooltip = tooltip;
+ }, 1000);
+ }
+ }
+ });
+
+ html.find('.item-name').on('mouseout', (event) => {
+ const input = event.currentTarget;
+
+ if (input._tooltipTimeout) {
+ clearTimeout(input._tooltipTimeout);
+ delete input._tooltipTimeout;
+ }
+
+ if (input._tooltip) {
+ document.body.removeChild(input._tooltip);
+ delete input._tooltip;
+ }
+ });
+ }
+}
diff --git a/src/module/actors/sheets/smallcraft-sheet.js b/src/module/actors/sheets/smallcraft-sheet.js
index e5c0005..a14e4a2 100644
--- a/src/module/actors/sheets/smallcraft-sheet.js
+++ b/src/module/actors/sheets/smallcraft-sheet.js
@@ -1,447 +1,472 @@
-import {
- STASharedActorFunctions
-} from '../actor.js';
-
-export class STASmallCraftSheet extends ActorSheet {
- /** @override */
- static get defaultOptions() {
- return foundry.utils.mergeObject(super.defaultOptions, {
- classes: ['sta', 'sheet', 'actor', 'smallcraft'],
- width: 900,
- height: 735,
- dragDrop: [{
- dragSelector: '.item-list .item',
- dropSelector: null
- }]
- });
- }
-
- /* -------------------------------------------- */
- // If the player is not a GM and has limited permissions - send them to the limited sheet, otherwise, continue as usual.
- /** @override */
- get template() {
- const versionInfo = game.world.coreVersion;
- if ( !game.user.isGM && this.actor.limited) return 'systems/sta/templates/actors/limited-sheet.hbs';
- if (!foundry.utils.isNewerVersion(versionInfo, '0.8.-1')) return 'systems/sta/templates/actors/smallcraft-sheet-legacy.hbs';
- return `systems/sta/templates/actors/smallcraft-sheet.hbs`;
- }
-
-
- /* -------------------------------------------- */
-
- /** @override */
- getData() {
- const sheetData = this.object;
- sheetData.dtypes = ['String', 'Number', 'Boolean'];
-
- // Ensure department values don't weigh over the max.
- $.each(sheetData.system.departments, (key, department) => {
- if (department.value > 5) department.value = 5;
- });
-
- // Checks if shields is larger than its max, if so, set to max.
- if (sheetData.system.shields.value > sheetData.system.shields.max) {
- sheetData.system.shields.value = sheetData.system.shields.max;
- }
- if (sheetData.system.power.value > sheetData.system.power.max) {
- sheetData.system.power.value = sheetData.system.power.max;
- }
-
- // Ensure system and department values aren't lower than their minimums.
- $.each(sheetData.system.systems, (key, system) => {
- if (system.value < 0) system.value = 0;
- });
-
- $.each(sheetData.system.departments, (key, department) => {
- if (department.value < 0) department.value = 0;
- });
-
- // Checks if shields is below 0, if so - set it to 0.
- if (sheetData.system.shields.value < 0) {
- sheetData.system.shields.value = 0;
- }
- if (sheetData.system.power.value < 0) {
- sheetData.system.power.value = 0;
- }
-
- return sheetData;
- }
-
- /* -------------------------------------------- */
-
- /** @override */
- activateListeners(html) {
- super.activateListeners(html);
-
- // Allows checking version easily
- const versionInfo = game.world.coreVersion;
-
- // Opens the class STASharedActorFunctions for access at various stages.
- const staActor = new STASharedActorFunctions();
-
- // If the player has limited access to the actor, there is nothing to see here. Return.
- if ( !game.user.isGM && this.actor.limited) {
- return;
- }
-
- // We use i alot in for loops. Best to assign it now for use later in multiple places.
- let i;
- let shieldsTrackMax = 0;
- let powerTrackMax = 0;
-
- // This creates a dynamic Shields tracker. It polls for the value of the structure system and security department.
- // With the total value divided by 2, creates a new div for each and places it under a child called "bar-shields-renderer".
- function shieldsTrackUpdate() {
- const localizedValues = {
- 'advancedshields': game.i18n.localize('sta.actor.starship.talents.advancedshields')
- };
-
- shieldsTrackMax = Math.floor((parseInt(html.find('#structure')[0].value) + parseInt(html.find('#security')[0].value))/2) + parseInt(html.find('#shieldmod')[0].value);
- if (html.find(`[data-talent-name*="${localizedValues.advancedshields}"]`).length > 0) {
- shieldsTrackMax += 5;
- }
- // This checks that the max-shields hidden field is equal to the calculated Max Shields value, if not it makes it so.
- if (html.find('#max-shields')[0].value != shieldsTrackMax) {
- html.find('#max-shields')[0].value = shieldsTrackMax;
- }
- html.find('#bar-shields-renderer').empty();
- for (i = 1; i <= shieldsTrackMax; i++) {
- const div = document.createElement('DIV');
- div.className = 'box';
- div.id = 'shields-' + i;
- div.innerHTML = i;
- div.style = 'width: calc(100% / ' + html.find('#max-shields')[0].value + ');';
- html.find('#bar-shields-renderer')[0].appendChild(div);
- }
- }
- shieldsTrackUpdate();
-
- // This creates a dynamic Power tracker. It polls for the value of the engines system.
- // With the value, creates a new div for each and places it under a child called "bar-power-renderer".
- function powerTrackUpdate() {
- powerTrackMax = Math.ceil(parseInt(html.find('#engines')[0].value)/2);
- if (html.find('[data-talent-name*="Secondary Reactors"]').length > 0) {
- powerTrackMax += 5;
- }
- // This checks that the max-power hidden field is equal to the calculated Max Power value, if not it makes it so.
- if (html.find('#max-power')[0].value != powerTrackMax) {
- html.find('#max-power')[0].value = powerTrackMax;
- }
- html.find('#bar-power-renderer').empty();
- for (i = 1; i <= powerTrackMax; i++) {
- const div = document.createElement('DIV');
- div.className = 'box';
- div.id = 'power-' + i;
- div.innerHTML = i;
- div.style = 'width: calc(100% / ' + html.find('#max-power')[0].value + ');';
- html.find('#bar-power-renderer')[0].appendChild(div);
- }
- }
- powerTrackUpdate();
-
- // Fires the function staRenderTracks as soon as the parameters exist to do so.
- staActor.staRenderTracks(html, null, null, null,
- shieldsTrackMax, powerTrackMax, null);
-
- // This allows for each item-edit image to link open an item sheet. This uses Simple Worldbuilding System Code.
- html.find('.control .edit').click( (ev) => {
- const li = $(ev.currentTarget).parents( '.entry' );
- const item = this.actor.items.get( li.data( 'itemId' ) );
- item.sheet.render(true);
- });
-
- // This if statement checks if the form is editable, if not it hides controls used by the owner, then aborts any more of the script.
- if (!this.options.editable) {
- // This hides the ability to Perform an System Test for the character
- for (i = 0; i < html.find('.check-button').length; i++) {
- html.find('.check-button')[i].style.display = 'none';
- }
- // This hides all add and delete item images.
- for (i = 0; i < html.find('.control.create').length; i++) {
- html.find('.control.create')[i].style.display = 'none';
- }
- for (i = 0; i < html.find('.control .delete').length; i++) {
- html.find('.control .delete')[i].style.display = 'none';
- }
- // This hides all system and department check boxes (and titles)
- for (i = 0; i < html.find('.selector').length; i++) {
- html.find('.selector')[i].style.display = 'none';
- }
- // Remove hover CSS from clickables that are no longer clickable.
- for (i = 0; i < html.find('.box').length; i++) {
- html.find('.box')[i].classList.add('unset-clickables');
- }
- for (i = 0; i < html.find('.rollable').length; i++) {
- html.find('.rollable')[i].classList.add('unset-clickables');
- }
- return;
- };
-
- // set up click handler for items to send to the actor rollGenericItem
- html.find('.chat,.rollable').click( (ev) => {
- const itemType = $(ev.currentTarget).parents('.entry')[0].getAttribute('data-item-type');
- const itemId = $(ev.currentTarget).parents('.entry')[0].getAttribute('data-item-id');
- staActor.rollGenericItem(ev, itemType, itemId, this.actor);
- });
-
- // Allows item-create images to create an item of a type defined individually by each button. This uses code found via the Foundry VTT System Tutorial.
- html.find('.control.create').click((ev) => {
- ev.preventDefault();
- const header = ev.currentTarget;
- const type = header.dataset.type;
- const data = foundry.utils.duplicate(header.dataset);
- const name = `New ${type.capitalize()}`;
- const itemData = {
- name: name,
- type: type,
- data: data,
- img: game.sta.defaultImage
- };
- delete itemData.data['type'];
- if ( foundry.utils.isNewerVersion( versionInfo, '0.8.-1' )) {
- return this.actor.createEmbeddedDocuments( 'Item', [(itemData)] );
- } else {
- return this.actor.createOwnedItem( itemData );
- }
- });
-
- // Allows item-delete images to allow deletion of the selected item.
- html.find('.control .delete').click( (ev) => {
- // Cleaning up previous dialogs is nice, and also possibly avoids bugs from invalid popups.
- if (this.activeDialog) this.activeDialog.close();
-
- const li = $(ev.currentTarget).parents('.entry');
- this.activeDialog = staActor.deleteConfirmDialog(
- li[0].getAttribute('data-item-value'),
- () => {
- if ( foundry.utils.isNewerVersion( versionInfo, '0.8.-1' )) {
- this.actor.deleteEmbeddedDocuments( 'Item', [li.data('itemId')] );
- } else {
- this.actor.deleteOwnedItem( li.data( 'itemId' ));
- }
- },
- () => this.activeDialog = null
- );
- this.activeDialog.render(true);
- });
-
- // Reads if a shields track box has been clicked, and if it has will either: set the value to the clicked box, or reduce the value by one.
- // This check is dependent on various requirements, see comments in code.
- html.find('[id^="shields"]').click((ev) => {
- let total = '';
- const newTotalObject = $(ev.currentTarget)[0];
- const newTotal = newTotalObject.id.substring('shields-'.length);
- // data-selected stores whether the track box is currently activated or not. This checks that the box is activated
- if (newTotalObject.getAttribute('data-selected') === 'true') {
- // Now we check that the "next" track box is not activated.
- // If there isn't one, or it isn't activated, we only want to decrease the value by 1 rather than setting the value.
- const nextCheck = 'shields-' + (parseInt(newTotal) + 1);
- if (!html.find('#'+nextCheck)[0] || html.find('#'+nextCheck)[0].getAttribute('data-selected') != 'true') {
- html.find('#total-shields')[0].value = html.find('#total-shields')[0].value - 1;
- this.submit();
- // If it isn't caught by the if, the next box is likely activated. If something happened, its safer to set the value anyway.
- } else {
- total = html.find('#total-shields')[0].value;
- if (total != newTotal) {
- html.find('#total-shields')[0].value = newTotal;
- this.submit();
- }
- }
- // If the clicked box wasn't activated, we need to activate it now.
- } else {
- total = html.find('#total-shields')[0].value;
- if (total != newTotal) {
- html.find('#total-shields')[0].value = newTotal;
- this.submit();
- }
- }
- });
-
- // Reads if a power track box has been clicked, and if it has will either: set the value to the clicked box, or reduce the value by one.
- // This check is dependent on various requirements, see comments in code.
- html.find('[id^="power"]').click((ev) => {
- let total = '';
- const newTotalObject = $(ev.currentTarget)[0];
- const newTotal = newTotalObject.id.substring('power-'.length);
- // data-selected stores whether the track box is currently activated or not. This checks that the box is activated
- if (newTotalObject.getAttribute('data-selected') === 'true') {
- // Now we check that the "next" track box is not activated.
- // If there isn't one, or it isn't activated, we only want to decrease the value by 1 rather than setting the value.
- const nextCheck = 'power-' + (parseInt(newTotal) + 1);
- if (!html.find('#'+nextCheck)[0] || html.find('#'+nextCheck)[0].getAttribute('data-selected') != 'true') {
- html.find('#total-power')[0].value = html.find('#total-power')[0].value - 1;
- this.submit();
- // If it isn't caught by the if, the next box is likely activated. If something happened, its safer to set the value anyway.
- } else {
- total = html.find('#total-power')[0].value;
- if (total != newTotal) {
- html.find('#total-power')[0].value = newTotal;
- this.submit();
- }
- }
- // If the clicked box wasn't activated, we need to activate it now.
- } else {
- total = html.find('#total-power')[0].value;
- if (total != newTotal) {
- html.find('#total-power')[0].value = newTotal;
- this.submit();
- }
- }
- });
-
- // This is used to clean up all the HTML that comes from displaying outputs from the text editor boxes. There's probably a better way to do this but the quick and dirty worked this time.
- $.each($('[id^=talent-tooltip-text-]'), function(index, value) {
- const beforeDescription = value.innerHTML;
- const decoded = TextEditor.decodeHTML(beforeDescription);
- const prettifiedDescription = TextEditor.previewHTML(decoded, 1000);
- $('#' + value.id).html(prettifiedDescription);
- });
-
-
- html.find('.talent-tooltip-clickable').click((ev) => {
- const talentId = $(ev.currentTarget)[0].id.substring('talent-tooltip-clickable-'.length);
- const currentShowingTalentId = $('.talent-tooltip-container:not(.hide)')[0] ? $('.talent-tooltip-container:not(.hide)')[0].id.substring('talent-tooltip-container-'.length) : null;
-
- if (talentId == currentShowingTalentId) {
- $('#talent-tooltip-container-' + talentId).addClass('hide').removeAttr('style');
- } else {
- $('.talent-tooltip-container').addClass('hide').removeAttr('style');
- $('#talent-tooltip-container-' + talentId).removeClass('hide').height($('#talent-tooltip-text-' + talentId)[0].scrollHeight + 5);
- }
- });
-
- $.each($('[id^=injury-tooltip-text-]'), function(index, value) {
- const beforeDescription = value.innerHTML;
- const decoded = TextEditor.decodeHTML(beforeDescription);
- const prettifiedDescription = TextEditor.previewHTML(decoded, 1000);
- $('#' + value.id).html(prettifiedDescription);
- });
-
-
- html.find('.injury-tooltip-clickable').click((ev) => {
- const injuryId = $(ev.currentTarget)[0].id.substring('injury-tooltip-clickable-'.length);
- const currentShowinginjuryId = $('.injury-tooltip-container:not(.hide)')[0] ? $('.injury-tooltip-container:not(.hide)')[0].id.substring('injury-tooltip-container-'.length) : null;
-
- if (injuryId == currentShowinginjuryId) {
- $('#injury-tooltip-container-' + injuryId).addClass('hide').removeAttr('style');
- } else {
- $('.injury-tooltip-container').addClass('hide').removeAttr('style');
- $('#injury-tooltip-container-' + injuryId).removeClass('hide').height($('#injury-tooltip-text-' + injuryId)[0].scrollHeight + 5);
- }
- });
-
- // Turns the System checkboxes into essentially a radio button. It removes any other ticks, and then checks the new system.
- // Finally a submit is required as data has changed.
- html.find('.selector.system').click((ev) => {
- for (i = 0; i <= 5; i++) {
- html.find('.selector.system')[i].checked = false;
- }
- $(ev.currentTarget)[0].checked = true;
- this.submit();
- });
-
- // Turns the Department checkboxes into essentially a radio button. It removes any other ticks, and then checks the new department.
- // Finally a submit is required as data has changed.
- html.find('.selector.department').click((ev) => {
- for (i = 0; i <= 5; i++) {
- html.find('.selector.department')[i].checked = false;
- }
- $(ev.currentTarget)[0].checked = true;
- this.submit();
- });
-
- // If the check-button is clicked it grabs the selected system and the selected department and fires the method rollSystemTest. See actor.js for further info.
- html.find('.check-button.attribute').click((ev) => {
- let selectedSystem = '';
- let selectedSystemValue = '';
- let selectedDepartment = '';
- let selectedDepartmentValue = '';
- for (i = 0; i <= 5; i++) {
- if (html.find('.selector.system')[i].checked === true) {
- selectedSystem = html.find('.selector.system')[i].id;
- selectedSystem = selectedSystem.slice(0, -9);
- selectedSystemValue = html.find('#'+selectedSystem)[0].value;
- }
- }
- for (i = 0; i <= 5; i++) {
- if (html.find('.selector.department')[i].checked === true) {
- selectedDepartment = html.find('.selector.department')[i].id;
- selectedDepartment = selectedDepartment.slice(0, -9);
- selectedDepartmentValue = html.find('#'+selectedDepartment)[0].value;
- }
- }
-
- staActor.rollAttributeTest(ev, selectedSystem,
- parseInt(selectedSystemValue), selectedDepartment,
- parseInt(selectedDepartmentValue), 2, this.actor);
- });
-
- // If the check-button is clicked it fires the method challenge roll method. See actor.js for further info.
- html.find('.check-button.challenge').click( (ev) => {
- staActor.rollChallengeRoll(ev, null, null, this.actor);
- });
-
- html.find('.reroll-result').click((ev) => {
- let selectedSystem = '';
- let selectedSystemValue = '';
- let selectedDepartment = '';
- let selectedDepartmentValue = '';
- for (i = 0; i <= 5; i++) {
- if (html.find('.selector.system')[i].checked === true) {
- selectedSystem = html.find('.selector.system')[i].id;
- selectedSystem = selectedSystem.slice(0, -9);
- selectedSystemValue = html.find('#'+selectedSystem)[0].value;
- }
- }
- for (i = 0; i <= 5; i++) {
- if (html.find('.selector.department')[i].checked === true) {
- selectedDepartment = html.find('.selector.department')[i].id;
- selectedDepartment = selectedDepartment.slice(0, -9);
- selectedDepartmentValue = html.find('#'+selectedDepartment)[0].value;
- }
- }
-
- staActor.rollAttributeTest(ev, selectedSystem,
- parseInt(selectedSystemValue), selectedDepartment,
- parseInt(selectedDepartmentValue), null, this.actor);
- });
-
- $(html).find('[id^=smallcraft-weapon-]').each(function(_, value) {
- const weaponDamage = parseInt(value.dataset.itemDamage);
- const securityValue = parseInt(html.find('#security')[0].value);
- let scaleDamage = 0;
- if (value.dataset.itemIncludescale == 'true') scaleDamage = parseInt(html.find('#scale')[0].value);
- const attackDamageValue = weaponDamage + securityValue + scaleDamage;
- value.getElementsByClassName('damage')[0].innerText = attackDamageValue;
- });
-
- html.find('.selector.system').each(function(index, value) {
- const $systemCheckbox = $(value);
- const $systemBreach = $systemCheckbox.siblings('.breaches');
- const $systemDestroyed = $systemCheckbox.siblings('.system-destroyed');
-
- const shipScaleValue = Number.parseInt(html.find('#scale').attr('value'));
- const breachValue = Number.parseInt($systemBreach.attr('value'));
-
- const isSystemDamaged = breachValue >= (Math.ceil(shipScaleValue / 2)) ? true : false;
- const isSystemDisabled = breachValue >= shipScaleValue ? true : false;
- const isSystemDestroyed = breachValue >= (Math.ceil(shipScaleValue + 1)) ? true : false;
-
- if (isSystemDamaged && !isSystemDisabled && !isSystemDestroyed) {
- $systemBreach.addClass('highlight-damaged');
- $systemBreach.removeClass('highlight-disabled');
- $systemBreach.removeClass('highlight-destroyed');
- } else if (isSystemDisabled && !isSystemDestroyed) {
- $systemBreach.addClass('highlight-disabled');
- $systemBreach.removeClass('highlight-destroyed');
- $systemBreach.removeClass('highlight-damaged');
- } else if (isSystemDestroyed) {
- $systemBreach.addClass('highlight-destroyed');
- $systemBreach.removeClass('highlight-disabled');
- $systemBreach.removeClass('highlight-damaged');
- } else {
- $systemBreach.removeClass('highlight-damaged highlight-disabled highlight-destroyed');
- }
- });
- }
-}
+import {
+ STASharedActorFunctions
+} from '../actor.js';
+
+export class STASmallCraftSheet extends ActorSheet {
+ /** @override */
+ static get defaultOptions() {
+ return foundry.utils.mergeObject(super.defaultOptions, {
+ width: 850,
+ height: 850,
+ dragDrop: [{
+ dragSelector: '.item-list .item',
+ dropSelector: null
+ }]
+ });
+ }
+
+ /* -------------------------------------------- */
+ // If the player is not a GM and has limited permissions - send them to the limited sheet, otherwise, continue as usual.
+ /** @override */
+ get template() {
+ const versionInfo = game.world.coreVersion;
+ if ( !game.user.isGM && this.actor.limited) return 'systems/sta/templates/actors/limited-sheet.hbs';
+ return `systems/sta/templates/actors/smallcraft-sheet.hbs`;
+ }
+ render(force = false, options = {}) {
+ if (!game.user.isGM && this.actor.limited) {
+ options = foundry.utils.mergeObject(options, {height: 250});
+ }
+ return super.render(force, options);
+ }
+
+ /* -------------------------------------------- */
+
+ /** @override */
+ getData() {
+ const sheetData = this.object;
+ sheetData.dtypes = ['String', 'Number', 'Boolean'];
+
+ // Ensure department values don't weigh over the max.
+ $.each(sheetData.system.departments, (key, department) => {
+ if (department.value > 5) department.value = 5;
+ });
+
+ // Checks if shields is larger than its max, if so, set to max.
+ if (sheetData.system.shields.value > sheetData.system.shields.max) {
+ sheetData.system.shields.value = sheetData.system.shields.max;
+ }
+ if (sheetData.system.power.value > sheetData.system.power.max) {
+ sheetData.system.power.value = sheetData.system.power.max;
+ }
+
+ // Ensure system and department values aren't lower than their minimums.
+ $.each(sheetData.system.systems, (key, system) => {
+ if (system.value < 0) system.value = 0;
+ });
+
+ $.each(sheetData.system.departments, (key, department) => {
+ if (department.value < 0) department.value = 0;
+ });
+
+ // Checks if shields is below 0, if so - set it to 0.
+ if (sheetData.system.shields.value < 0) {
+ sheetData.system.shields.value = 0;
+ }
+ if (sheetData.system.power.value < 0) {
+ sheetData.system.power.value = 0;
+ }
+
+ return sheetData;
+ }
+
+ /* -------------------------------------------- */
+
+ /** @override */
+ activateListeners(html) {
+ super.activateListeners(html);
+
+ // Allows checking version easily
+ const versionInfo = game.world.coreVersion;
+
+ // Opens the class STASharedActorFunctions for access at various stages.
+ const staActor = new STASharedActorFunctions();
+
+ // If the player has limited access to the actor, there is nothing to see here. Return.
+ if ( !game.user.isGM && this.actor.limited) {
+ return;
+ }
+
+ // We use i alot in for loops. Best to assign it now for use later in multiple places.
+ let i;
+ let shieldsTrackMax = 0;
+ let powerTrackMax = 0;
+
+ // This creates a dynamic Shields tracker. It polls for the value of the structure system and security department.
+ // With the total value divided by 2, creates a new div for each and places it under a child called "bar-shields-renderer".
+ function shieldsTrackUpdate() {
+ const localizedValues = {
+ 'advancedshields': game.i18n.localize('sta.actor.starship.talents.advancedshields')
+ };
+
+ shieldsTrackMax = Math.floor((parseInt(html.find('#structure')[0].value) + parseInt(html.find('#security')[0].value))/2) + parseInt(html.find('#shieldmod')[0].value);
+ if (html.find(`[data-talent-name*="${localizedValues.advancedshields}"]`).length > 0) {
+ shieldsTrackMax += 5;
+ }
+ // This checks that the max-shields hidden field is equal to the calculated Max Shields value, if not it makes it so.
+ if (html.find('#max-shields')[0].value != shieldsTrackMax) {
+ html.find('#max-shields')[0].value = shieldsTrackMax;
+ }
+ html.find('#bar-shields-renderer').empty();
+ for (i = 1; i <= shieldsTrackMax; i++) {
+ const div = document.createElement('DIV');
+ div.className = 'box';
+ div.id = 'shields-' + i;
+ div.innerHTML = i;
+ div.style = 'width: calc(100% / ' + html.find('#max-shields')[0].value + ');';
+ html.find('#bar-shields-renderer')[0].appendChild(div);
+ }
+ }
+ shieldsTrackUpdate();
+
+ // This creates a dynamic Power tracker. It polls for the value of the engines system.
+ // With the value, creates a new div for each and places it under a child called "bar-power-renderer".
+ function powerTrackUpdate() {
+ powerTrackMax = Math.ceil(parseInt(html.find('#engines')[0].value)/2);
+ if (html.find('[data-talent-name*="Secondary Reactors"]').length > 0) {
+ powerTrackMax += 5;
+ }
+ // This checks that the max-power hidden field is equal to the calculated Max Power value, if not it makes it so.
+ if (html.find('#max-power')[0].value != powerTrackMax) {
+ html.find('#max-power')[0].value = powerTrackMax;
+ }
+ html.find('#bar-power-renderer').empty();
+ for (i = 1; i <= powerTrackMax; i++) {
+ const div = document.createElement('DIV');
+ div.className = 'box';
+ div.id = 'power-' + i;
+ div.innerHTML = i;
+ div.style = 'width: calc(100% / ' + html.find('#max-power')[0].value + ');';
+ html.find('#bar-power-renderer')[0].appendChild(div);
+ }
+ }
+ powerTrackUpdate();
+
+ // Fires the function staRenderTracks as soon as the parameters exist to do so.
+ staActor.staRenderTracks(html, null, null, null,
+ shieldsTrackMax, powerTrackMax, null);
+
+ // This if statement checks if the form is editable, if not it hides controls used by the owner, then aborts any more of the script.
+ if (!this.options.editable) {
+ // This hides the ability to Perform an System Test for the character
+ for (i = 0; i < html.find('.check-button').length; i++) {
+ html.find('.check-button')[i].style.display = 'none';
+ }
+ // This hides all add and delete item images.
+ for (i = 0; i < html.find('.control.create').length; i++) {
+ html.find('.control.create')[i].style.display = 'none';
+ }
+ for (i = 0; i < html.find('.control .delete').length; i++) {
+ html.find('.control .delete')[i].style.display = 'none';
+ }
+ // This hides all system and department check boxes (and titles)
+ for (i = 0; i < html.find('.selector').length; i++) {
+ html.find('.selector')[i].style.display = 'none';
+ }
+ // Remove hover CSS from clickables that are no longer clickable.
+ for (i = 0; i < html.find('.box').length; i++) {
+ html.find('.box')[i].classList.add('unset-clickables');
+ }
+ for (i = 0; i < html.find('.rollable').length; i++) {
+ html.find('.rollable')[i].classList.add('unset-clickables');
+ }
+ return;
+ };
+
+ // set up click handler for items to send to the actor rollGenericItem
+ html.find('.chat,.rollable').click( (ev) => {
+ const itemType = $(ev.currentTarget).parents('.entry')[0].getAttribute('data-item-type');
+ const itemId = $(ev.currentTarget).parents('.entry')[0].getAttribute('data-item-id');
+ staActor.rollGenericItem(ev, itemType, itemId, this.actor);
+ });
+
+ // Listen for changes in the item name input field
+ html.find('.item-name').on('change', (event) => {
+ const input = event.currentTarget;
+ const itemId = input.dataset.itemId;
+ const item = this.actor.items.get(itemId);
+ const newName = input.value.trim();
+
+ if (item && newName) {
+ item.update({name: newName});
+ }
+ });
+
+ // Create new items
+ html.find('.control.create').click(async (ev) => {
+ ev.preventDefault();
+ const header = ev.currentTarget;
+ const type = header.dataset.type;
+ const data = Object.assign({}, header.dataset);
+ const name = `New ${type.capitalize()}`;
+
+ const itemData = {
+ name: name,
+ type: type,
+ data: data,
+ };
+ delete itemData.data['type'];
+
+ const newItem = await this.actor.createEmbeddedDocuments('Item', [itemData]);
+ });
+
+ // Edit items
+ html.find('.control .edit').click((ev) => {
+ const li = $(ev.currentTarget).parents('.entry');
+ const item = this.actor.items.get(li.data('itemId'));
+ item.sheet.render(true);
+ });
+
+ // Delete items with confirmation dialog
+ html.find('.control .delete').click((ev) => {
+ const li = $(ev.currentTarget).parents('.entry');
+ const itemId = li.data('itemId');
+
+ new Dialog({
+ title: `${game.i18n.localize('sta.apps.deleteitem')}`,
+ content: `
${game.i18n.localize('sta.apps.deleteconfirm')}
`,
+ buttons: {
+ yes: {
+ icon: '',
+ label: `${game.i18n.localize('sta.apps.yes')}`,
+ callback: () => this.actor.deleteEmbeddedDocuments('Item', [itemId])
+ },
+ no: {
+ icon: '',
+ label: `${game.i18n.localize('sta.apps.no')}`
+ }
+ },
+ default: 'no'
+ }).render(true);
+ });
+
+ // Item popout tooltip of description
+ html.find('.item-name').on('mouseover', (event) => {
+ const input = event.currentTarget;
+ const itemId = input.dataset.itemId;
+ const item = this.actor.items.get(itemId);
+
+ if (item) {
+ const description = item.system.description?.trim().replace(/\n/g, ' ');
+
+ if (description) {
+ input._tooltipTimeout = setTimeout(() => {
+ let tooltip = document.querySelector('.item-tooltip');
+ if (!tooltip) {
+ tooltip = document.createElement('div');
+ tooltip.classList.add('item-tooltip');
+ document.body.appendChild(tooltip);
+ }
+
+ tooltip.innerHTML = `${description}`;
+
+ const {clientX: mouseX, clientY: mouseY} = event;
+ tooltip.style.left = `${mouseX + 10}px`;
+ tooltip.style.top = `${mouseY + 10}px`;
+
+ document.body.appendChild(tooltip);
+ const tooltipRect = tooltip.getBoundingClientRect();
+
+ if (tooltipRect.bottom > window.innerHeight) {
+ tooltip.style.top = `${window.innerHeight - tooltipRect.height - 20}px`;
+ }
+
+ input._tooltip = tooltip;
+ }, 1000);
+ }
+ }
+ });
+
+ html.find('.item-name').on('mouseout', (event) => {
+ const input = event.currentTarget;
+
+ if (input._tooltipTimeout) {
+ clearTimeout(input._tooltipTimeout);
+ delete input._tooltipTimeout;
+ }
+
+ if (input._tooltip) {
+ document.body.removeChild(input._tooltip);
+ delete input._tooltip;
+ }
+ });
+
+ // Reads if a shields track box has been clicked, and if it has will either: set the value to the clicked box, or reduce the value by one.
+ // This check is dependent on various requirements, see comments in code.
+ html.find('[id^="shields"]').click((ev) => {
+ let total = '';
+ const newTotalObject = $(ev.currentTarget)[0];
+ const newTotal = newTotalObject.id.substring('shields-'.length);
+ // data-selected stores whether the track box is currently activated or not. This checks that the box is activated
+ if (newTotalObject.getAttribute('data-selected') === 'true') {
+ // Now we check that the "next" track box is not activated.
+ // If there isn't one, or it isn't activated, we only want to decrease the value by 1 rather than setting the value.
+ const nextCheck = 'shields-' + (parseInt(newTotal) + 1);
+ if (!html.find('#'+nextCheck)[0] || html.find('#'+nextCheck)[0].getAttribute('data-selected') != 'true') {
+ html.find('#total-shields')[0].value = html.find('#total-shields')[0].value - 1;
+ this.submit();
+ // If it isn't caught by the if, the next box is likely activated. If something happened, its safer to set the value anyway.
+ } else {
+ total = html.find('#total-shields')[0].value;
+ if (total != newTotal) {
+ html.find('#total-shields')[0].value = newTotal;
+ this.submit();
+ }
+ }
+ // If the clicked box wasn't activated, we need to activate it now.
+ } else {
+ total = html.find('#total-shields')[0].value;
+ if (total != newTotal) {
+ html.find('#total-shields')[0].value = newTotal;
+ this.submit();
+ }
+ }
+ });
+
+ // Reads if a power track box has been clicked, and if it has will either: set the value to the clicked box, or reduce the value by one.
+ // This check is dependent on various requirements, see comments in code.
+ html.find('[id^="power"]').click((ev) => {
+ let total = '';
+ const newTotalObject = $(ev.currentTarget)[0];
+ const newTotal = newTotalObject.id.substring('power-'.length);
+ // data-selected stores whether the track box is currently activated or not. This checks that the box is activated
+ if (newTotalObject.getAttribute('data-selected') === 'true') {
+ // Now we check that the "next" track box is not activated.
+ // If there isn't one, or it isn't activated, we only want to decrease the value by 1 rather than setting the value.
+ const nextCheck = 'power-' + (parseInt(newTotal) + 1);
+ if (!html.find('#'+nextCheck)[0] || html.find('#'+nextCheck)[0].getAttribute('data-selected') != 'true') {
+ html.find('#total-power')[0].value = html.find('#total-power')[0].value - 1;
+ this.submit();
+ // If it isn't caught by the if, the next box is likely activated. If something happened, its safer to set the value anyway.
+ } else {
+ total = html.find('#total-power')[0].value;
+ if (total != newTotal) {
+ html.find('#total-power')[0].value = newTotal;
+ this.submit();
+ }
+ }
+ // If the clicked box wasn't activated, we need to activate it now.
+ } else {
+ total = html.find('#total-power')[0].value;
+ if (total != newTotal) {
+ html.find('#total-power')[0].value = newTotal;
+ this.submit();
+ }
+ }
+ });
+
+ // Turns the System checkboxes into essentially a radio button. It removes any other ticks, and then checks the new system.
+ // Finally a submit is required as data has changed.
+ html.find('.selector.system').click((ev) => {
+ for (i = 0; i <= 5; i++) {
+ html.find('.selector.system')[i].checked = false;
+ }
+ $(ev.currentTarget)[0].checked = true;
+ this.submit();
+ });
+
+ // Turns the Department checkboxes into essentially a radio button. It removes any other ticks, and then checks the new department.
+ // Finally a submit is required as data has changed.
+ html.find('.selector.department').click((ev) => {
+ for (i = 0; i <= 5; i++) {
+ html.find('.selector.department')[i].checked = false;
+ }
+ $(ev.currentTarget)[0].checked = true;
+ this.submit();
+ });
+
+ // If the check-button is clicked it grabs the selected system and the selected department and fires the method rollSystemTest. See actor.js for further info.
+ html.find('.check-button.attribute').click((ev) => {
+ let selectedSystem = '';
+ let selectedSystemValue = '';
+ let selectedDepartment = '';
+ let selectedDepartmentValue = '';
+ for (i = 0; i <= 5; i++) {
+ if (html.find('.selector.system')[i].checked === true) {
+ selectedSystem = html.find('.selector.system')[i].id;
+ selectedSystem = selectedSystem.slice(0, -9);
+ selectedSystemValue = html.find('#'+selectedSystem)[0].value;
+ }
+ }
+ for (i = 0; i <= 5; i++) {
+ if (html.find('.selector.department')[i].checked === true) {
+ selectedDepartment = html.find('.selector.department')[i].id;
+ selectedDepartment = selectedDepartment.slice(0, -9);
+ selectedDepartmentValue = html.find('#'+selectedDepartment)[0].value;
+ }
+ }
+
+ staActor.rollAttributeTest(ev, selectedSystem,
+ parseInt(selectedSystemValue), selectedDepartment,
+ parseInt(selectedDepartmentValue), 2, this.actor);
+ });
+
+ // If the check-button is clicked it fires the method challenge roll method. See actor.js for further info.
+ html.find('.check-button.challenge').click( (ev) => {
+ staActor.rollChallengeRoll(ev, null, null, this.actor);
+ });
+
+ html.find('.reroll-result').click((ev) => {
+ let selectedSystem = '';
+ let selectedSystemValue = '';
+ let selectedDepartment = '';
+ let selectedDepartmentValue = '';
+ for (i = 0; i <= 5; i++) {
+ if (html.find('.selector.system')[i].checked === true) {
+ selectedSystem = html.find('.selector.system')[i].id;
+ selectedSystem = selectedSystem.slice(0, -9);
+ selectedSystemValue = html.find('#'+selectedSystem)[0].value;
+ }
+ }
+ for (i = 0; i <= 5; i++) {
+ if (html.find('.selector.department')[i].checked === true) {
+ selectedDepartment = html.find('.selector.department')[i].id;
+ selectedDepartment = selectedDepartment.slice(0, -9);
+ selectedDepartmentValue = html.find('#'+selectedDepartment)[0].value;
+ }
+ }
+
+ staActor.rollAttributeTest(ev, selectedSystem,
+ parseInt(selectedSystemValue), selectedDepartment,
+ parseInt(selectedDepartmentValue), null, this.actor);
+ });
+
+ $(html).find('[id^=smallcraft-weapon-]').each(function(_, value) {
+ const weaponDamage = parseInt(value.dataset.itemDamage);
+ const securityValue = parseInt(html.find('#security')[0].value);
+ let scaleDamage = 0;
+ if (value.dataset.itemIncludescale == 'true') scaleDamage = parseInt(html.find('#scale')[0].value);
+ const attackDamageValue = weaponDamage + securityValue + scaleDamage;
+ value.getElementsByClassName('damage')[0].innerText = attackDamageValue;
+ });
+
+ html.find('.selector.system').each(function(index, value) {
+ const $systemCheckbox = $(value);
+ const $systemBreach = $systemCheckbox.siblings('.breaches');
+ const $systemDestroyed = $systemCheckbox.siblings('.system-destroyed');
+
+ const shipScaleValue = Number.parseInt(html.find('#scale').attr('value'));
+ const breachValue = Number.parseInt($systemBreach.attr('value'));
+
+ const isSystemDamaged = breachValue >= (Math.ceil(shipScaleValue / 2)) ? true : false;
+ const isSystemDisabled = breachValue >= shipScaleValue ? true : false;
+ const isSystemDestroyed = breachValue >= (Math.ceil(shipScaleValue + 1)) ? true : false;
+
+ if (isSystemDamaged && !isSystemDisabled && !isSystemDestroyed) {
+ $systemBreach.addClass('highlight-damaged');
+ $systemBreach.removeClass('highlight-disabled');
+ $systemBreach.removeClass('highlight-destroyed');
+ } else if (isSystemDisabled && !isSystemDestroyed) {
+ $systemBreach.addClass('highlight-disabled');
+ $systemBreach.removeClass('highlight-destroyed');
+ $systemBreach.removeClass('highlight-damaged');
+ } else if (isSystemDestroyed) {
+ $systemBreach.addClass('highlight-destroyed');
+ $systemBreach.removeClass('highlight-disabled');
+ $systemBreach.removeClass('highlight-damaged');
+ } else {
+ $systemBreach.removeClass('highlight-damaged highlight-disabled highlight-destroyed');
+ }
+ });
+ }
+}
diff --git a/src/module/actors/sheets/smallcraft-sheet2e.js b/src/module/actors/sheets/smallcraft-sheet2e.js
index fee5e40..4683812 100644
--- a/src/module/actors/sheets/smallcraft-sheet2e.js
+++ b/src/module/actors/sheets/smallcraft-sheet2e.js
@@ -6,9 +6,8 @@ export class STASmallCraftSheet2e extends ActorSheet {
/** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
- classes: ['sta', 'sheet', 'actor', 'smallcraft2e'],
- width: 900,
- height: 735,
+ width: 850,
+ height: 850,
dragDrop: [{
dragSelector: '.item-list .item',
dropSelector: null
@@ -22,10 +21,14 @@ export class STASmallCraftSheet2e extends ActorSheet {
get template() {
const versionInfo = game.world.coreVersion;
if ( !game.user.isGM && this.actor.limited) return 'systems/sta/templates/actors/limited-sheet.hbs';
- if (!foundry.utils.isNewerVersion(versionInfo, '0.8.-1')) return 'systems/sta/templates/actors/smallcraft-sheet-legacy.hbs';
return `systems/sta/templates/actors/smallcraft-sheet2e.hbs`;
}
-
+ render(force = false, options = {}) {
+ if (!game.user.isGM && this.actor.limited) {
+ options = foundry.utils.mergeObject(options, {height: 250});
+ }
+ return super.render(force, options);
+ }
/* -------------------------------------------- */
@@ -124,10 +127,7 @@ export class STASmallCraftSheet2e extends ActorSheet {
// With the value, creates a new div for each and places it under a child called "bar-power-renderer".
function powerTrackUpdate() {
powerTrackMax = 0;
- // powerTrackMax = Math.ceil(parseInt(html.find('#engines')[0].value)/2);
- // if (html.find('[data-talent-name*="Secondary Reactors"]').length > 0) {
- // powerTrackMax += 5;
- // }
+
// This checks that the max-power hidden field is equal to the calculated Max Power value, if not it makes it so.
if (html.find('#max-power')[0].value != powerTrackMax) {
html.find('#max-power')[0].value = powerTrackMax;
@@ -148,13 +148,6 @@ export class STASmallCraftSheet2e extends ActorSheet {
staActor.staRenderTracks(html, null, null, null,
shieldsTrackMax, powerTrackMax, null);
- // This allows for each item-edit image to link open an item sheet. This uses Simple Worldbuilding System Code.
- html.find('.control .edit').click( (ev) => {
- const li = $(ev.currentTarget).parents( '.entry' );
- const item = this.actor.items.get( li.data( 'itemId' ) );
- item.sheet.render(true);
- });
-
// This if statement checks if the form is editable, if not it hides controls used by the owner, then aborts any more of the script.
if (!this.options.editable) {
// This hides the ability to Perform an System Test for the character
@@ -189,45 +182,115 @@ export class STASmallCraftSheet2e extends ActorSheet {
staActor.rollGenericItem(ev, itemType, itemId, this.actor);
});
- // Allows item-create images to create an item of a type defined individually by each button. This uses code found via the Foundry VTT System Tutorial.
- html.find('.control.create').click((ev) => {
+ // Listen for changes in the item name input field
+ html.find('.item-name').on('change', (event) => {
+ const input = event.currentTarget;
+ const itemId = input.dataset.itemId;
+ const item = this.actor.items.get(itemId);
+ const newName = input.value.trim();
+
+ if (item && newName) {
+ item.update({name: newName});
+ }
+ });
+
+ // Create new items
+ html.find('.control.create').click(async (ev) => {
ev.preventDefault();
const header = ev.currentTarget;
const type = header.dataset.type;
- const data = foundry.utils.duplicate(header.dataset);
+ const data = Object.assign({}, header.dataset);
const name = `New ${type.capitalize()}`;
+
const itemData = {
name: name,
type: type,
data: data,
- img: game.sta.defaultImage
};
delete itemData.data['type'];
- if ( foundry.utils.isNewerVersion( versionInfo, '0.8.-1' )) {
- return this.actor.createEmbeddedDocuments( 'Item', [(itemData)] );
- } else {
- return this.actor.createOwnedItem( itemData );
- }
+
+ const newItem = await this.actor.createEmbeddedDocuments('Item', [itemData]);
});
- // Allows item-delete images to allow deletion of the selected item.
- html.find('.control .delete').click( (ev) => {
- // Cleaning up previous dialogs is nice, and also possibly avoids bugs from invalid popups.
- if (this.activeDialog) this.activeDialog.close();
+ // Edit items
+ html.find('.control .edit').click((ev) => {
+ const li = $(ev.currentTarget).parents('.entry');
+ const item = this.actor.items.get(li.data('itemId'));
+ item.sheet.render(true);
+ });
+ // Delete items with confirmation dialog
+ html.find('.control .delete').click((ev) => {
const li = $(ev.currentTarget).parents('.entry');
- this.activeDialog = staActor.deleteConfirmDialog(
- li[0].getAttribute('data-item-value'),
- () => {
- if ( foundry.utils.isNewerVersion( versionInfo, '0.8.-1' )) {
- this.actor.deleteEmbeddedDocuments( 'Item', [li.data('itemId')] );
- } else {
- this.actor.deleteOwnedItem( li.data( 'itemId' ));
+ const itemId = li.data('itemId');
+
+ new Dialog({
+ title: `${game.i18n.localize('sta.apps.deleteitem')}`,
+ content: `
${game.i18n.localize('sta.apps.deleteconfirm')}
`,
+ buttons: {
+ yes: {
+ icon: '',
+ label: `${game.i18n.localize('sta.apps.yes')}`,
+ callback: () => this.actor.deleteEmbeddedDocuments('Item', [itemId])
+ },
+ no: {
+ icon: '',
+ label: `${game.i18n.localize('sta.apps.no')}`
}
},
- () => this.activeDialog = null
- );
- this.activeDialog.render(true);
+ default: 'no'
+ }).render(true);
+ });
+
+ // Item popout tooltip of description
+ html.find('.item-name').on('mouseover', (event) => {
+ const input = event.currentTarget;
+ const itemId = input.dataset.itemId;
+ const item = this.actor.items.get(itemId);
+
+ if (item) {
+ const description = item.system.description?.trim().replace(/\n/g, ' ');
+
+ if (description) {
+ input._tooltipTimeout = setTimeout(() => {
+ let tooltip = document.querySelector('.item-tooltip');
+ if (!tooltip) {
+ tooltip = document.createElement('div');
+ tooltip.classList.add('item-tooltip');
+ document.body.appendChild(tooltip);
+ }
+
+ tooltip.innerHTML = `${description}`;
+
+ const {clientX: mouseX, clientY: mouseY} = event;
+ tooltip.style.left = `${mouseX + 10}px`;
+ tooltip.style.top = `${mouseY + 10}px`;
+
+ document.body.appendChild(tooltip);
+ const tooltipRect = tooltip.getBoundingClientRect();
+
+ if (tooltipRect.bottom > window.innerHeight) {
+ tooltip.style.top = `${window.innerHeight - tooltipRect.height - 20}px`;
+ }
+
+ input._tooltip = tooltip;
+ }, 1000);
+ }
+ }
+ });
+
+ html.find('.item-name').on('mouseout', (event) => {
+ const input = event.currentTarget;
+
+ if (input._tooltipTimeout) {
+ clearTimeout(input._tooltipTimeout);
+ delete input._tooltipTimeout;
+ }
+
+ if (input._tooltip) {
+ document.body.removeChild(input._tooltip);
+ delete input._tooltip;
+ }
});
// Reads if a shields track box has been clicked, and if it has will either: set the value to the clicked box, or reduce the value by one.
@@ -294,47 +357,6 @@ export class STASmallCraftSheet2e extends ActorSheet {
}
});
- // This is used to clean up all the HTML that comes from displaying outputs from the text editor boxes. There's probably a better way to do this but the quick and dirty worked this time.
- $.each($('[id^=talent-tooltip-text-]'), function(index, value) {
- const beforeDescription = value.innerHTML;
- const decoded = TextEditor.decodeHTML(beforeDescription);
- const prettifiedDescription = TextEditor.previewHTML(decoded, 1000);
- $('#' + value.id).html(prettifiedDescription);
- });
-
-
- html.find('.talent-tooltip-clickable').click((ev) => {
- const talentId = $(ev.currentTarget)[0].id.substring('talent-tooltip-clickable-'.length);
- const currentShowingTalentId = $('.talent-tooltip-container:not(.hide)')[0] ? $('.talent-tooltip-container:not(.hide)')[0].id.substring('talent-tooltip-container-'.length) : null;
-
- if (talentId == currentShowingTalentId) {
- $('#talent-tooltip-container-' + talentId).addClass('hide').removeAttr('style');
- } else {
- $('.talent-tooltip-container').addClass('hide').removeAttr('style');
- $('#talent-tooltip-container-' + talentId).removeClass('hide').height($('#talent-tooltip-text-' + talentId)[0].scrollHeight + 5);
- }
- });
-
- $.each($('[id^=injury-tooltip-text-]'), function(index, value) {
- const beforeDescription = value.innerHTML;
- const decoded = TextEditor.decodeHTML(beforeDescription);
- const prettifiedDescription = TextEditor.previewHTML(decoded, 1000);
- $('#' + value.id).html(prettifiedDescription);
- });
-
-
- html.find('.injury-tooltip-clickable').click((ev) => {
- const injuryId = $(ev.currentTarget)[0].id.substring('injury-tooltip-clickable-'.length);
- const currentShowinginjuryId = $('.injury-tooltip-container:not(.hide)')[0] ? $('.injury-tooltip-container:not(.hide)')[0].id.substring('injury-tooltip-container-'.length) : null;
-
- if (injuryId == currentShowinginjuryId) {
- $('#injury-tooltip-container-' + injuryId).addClass('hide').removeAttr('style');
- } else {
- $('.injury-tooltip-container').addClass('hide').removeAttr('style');
- $('#injury-tooltip-container-' + injuryId).removeClass('hide').height($('#injury-tooltip-text-' + injuryId)[0].scrollHeight + 5);
- }
- });
-
// Turns the System checkboxes into essentially a radio button. It removes any other ticks, and then checks the new system.
// Finally a submit is required as data has changed.
html.find('.selector.system').click((ev) => {
diff --git a/src/module/actors/sheets/starship-sheet.js b/src/module/actors/sheets/starship-sheet.js
index 672655c..2331180 100644
--- a/src/module/actors/sheets/starship-sheet.js
+++ b/src/module/actors/sheets/starship-sheet.js
@@ -1,521 +1,548 @@
-import {
- STASharedActorFunctions
-} from '../actor.js';
-
-export class STAStarshipSheet extends ActorSheet {
- constructor(object, options={}) {
- super(object, options);
-
- /**
- * An actively open dialog that should be closed before a new one is opened.
- * @type {Dialog}
- */
- this.activeDialog = null;
- }
-
- /** @override */
- static get defaultOptions() {
- return foundry.utils.mergeObject(super.defaultOptions, {
- classes: ['sta', 'sheet', 'actor', 'starship'],
- width: 800,
- height: 735,
- dragDrop: [{
- dragSelector: '.item-list .item',
- dropSelector: null
- }]
- });
- }
-
- /* -------------------------------------------- */
- // If the player is not a GM and has limited permissions - send them to the limited sheet, otherwise, continue as usual.
- /** @override */
- get template() {
- const versionInfo = game.world.coreVersion;
- if ( !game.user.isGM && this.actor.limited) return 'systems/sta/templates/actors/limited-sheet.hbs';
- if (!foundry.utils.isNewerVersion(versionInfo, '0.8.-1')) return 'systems/sta/templates/actors/starship-sheet-legacy.hbs';
- return `systems/sta/templates/actors/starship-sheet.hbs`;
- }
-
- /* -------------------------------------------- */
-
- /** @override */
- getData() {
- const sheetData = this.object;
- sheetData.dtypes = ['String', 'Number', 'Boolean'];
-
- // Ensure department values don't weigh over the max.
- const overrideDepartmentLimitSetting = game.settings.get('sta', 'shipDepartmentLimitIgnore');
- let maxDepartment = 5;
- if (overrideDepartmentLimitSetting) maxDepartment = 99;
- $.each(sheetData.system.departments, (key, department) => {
- if (department.value > maxDepartment) department.value = maxDepartment;
- });
-
- // Checks if shields is larger than its max, if so, set to max.
- if (sheetData.system.shields.value > sheetData.system.shields.max) {
- sheetData.system.shields.value = sheetData.system.shields.max;
- }
- if (sheetData.system.power.value > sheetData.system.power.max) {
- sheetData.system.power.value = sheetData.system.power.max;
- }
- if (sheetData.system.crew.value > sheetData.system.crew.max) {
- sheetData.system.crew.value = sheetData.system.crew.max;
- }
-
- // Ensure system and department values aren't lower than their minimums.
- $.each(sheetData.system.systems, (key, system) => {
- if (system.value < 0) system.value = 0;
- });
-
- $.each(sheetData.system.departments, (key, department) => {
- if (department.value < 0) department.value = 0;
- });
-
- // Checks if shields is below 0, if so - set it to 0.
- if (sheetData.system.shields.value < 0) {
- sheetData.system.shields.value = 0;
- }
- if (sheetData.system.power.value < 0) {
- sheetData.system.power.value = 0;
- }
- if (sheetData.system.crew.value < 0) {
- sheetData.system.crew.value = 0;
- }
-
- return sheetData;
- }
-
- /* -------------------------------------------- */
-
- /** @override */
- activateListeners(html) {
- super.activateListeners(html);
-
- // Allows checking version easily
- const versionInfo = game.world.coreVersion;
-
- // Opens the class STASharedActorFunctions for access at various stages.
- const staActor = new STASharedActorFunctions();
-
- // If the player has limited access to the actor, there is nothing to see here. Return.
- if ( !game.user.isGM && this.actor.limited) return;
-
- // We use i alot in for loops. Best to assign it now for use later in multiple places.
- let i;
- let shieldsTrackMax = 0;
- let powerTrackMax = 0;
- let crewTrackMax = 0;
-
- // This creates a dynamic Shields tracker. It polls for the value of the structure system and security department.
- // With the total value, creates a new div for each and places it under a child called "bar-shields-renderer".
- function shieldsTrackUpdate() {
- const localizedValues = {
- 'advancedshields': game.i18n.localize('sta.actor.starship.talents.advancedshields')
- };
-
- shieldsTrackMax = parseInt(html.find('#structure')[0].value) + parseInt(html.find('#security')[0].value) + parseInt(html.find('#shieldmod')[0].value);
- if (html.find(`[data-talent-name*="${localizedValues.advancedshields}"]`).length > 0) {
- shieldsTrackMax += 5;
- }
- // This checks that the max-shields hidden field is equal to the calculated Max Shields value, if not it makes it so.
- if (html.find('#max-shields')[0].value != shieldsTrackMax) {
- html.find('#max-shields')[0].value = shieldsTrackMax;
- }
- html.find('#bar-shields-renderer').empty();
- for (i = 1; i <= shieldsTrackMax; i++) {
- const div = document.createElement('DIV');
- div.className = 'box';
- div.id = 'shields-' + i;
- div.innerHTML = i;
- div.style = 'width: calc(100% / ' + html.find('#max-shields')[0].value + ');';
- html.find('#bar-shields-renderer')[0].appendChild(div);
- }
- }
- shieldsTrackUpdate();
-
- // This creates a dynamic Power tracker. It polls for the value of the engines system.
- // With the value, creates a new div for each and places it under a child called "bar-power-renderer".
- function powerTrackUpdate() {
- powerTrackMax = parseInt(html.find('#engines')[0].value);
- if (html.find('[data-talent-name*="Secondary Reactors"]').length > 0) {
- powerTrackMax += 5;
- }
- // This checks that the max-power hidden field is equal to the calculated Max Power value, if not it makes it so.
- if (html.find('#max-power')[0].value != powerTrackMax) {
- html.find('#max-power')[0].value = powerTrackMax;
- }
- html.find('#bar-power-renderer').empty();
- for (i = 1; i <= powerTrackMax; i++) {
- const div = document.createElement('DIV');
- div.className = 'box';
- div.id = 'power-' + i;
- div.innerHTML = i;
- div.style = 'width: calc(100% / ' + html.find('#max-power')[0].value + ');';
- html.find('#bar-power-renderer')[0].appendChild(div);
- }
- }
- powerTrackUpdate();
-
- // This creates a dynamic Crew Support tracker. It polls for the value of the ships's scale.
- // With the value, creates a new div for each and places it under a child called "bar-crew-renderer".
- function crewTrackUpdate() {
- crewTrackMax = parseInt(html.find('#scale')[0].value);
- // This checks that the max-crew hidden field is equal to the calculated Max Crew Support value, if not it makes it so.
- if (html.find('#max-crew')[0].value != crewTrackMax) {
- html.find('#max-crew')[0].value = crewTrackMax;
- }
- html.find('#bar-crew-renderer').empty();
- for (i = 1; i <= crewTrackMax; i++) {
- const div = document.createElement('DIV');
- div.className = 'box';
- div.id = 'crew-' + i;
- div.innerHTML = i;
- div.style = 'width: calc(100% / ' + html.find('#max-crew')[0].value + ');';
- html.find('#bar-crew-renderer')[0].appendChild(div);
- }
- }
- crewTrackUpdate();
-
- // Fires the function staRenderTracks as soon as the parameters exist to do so.
- staActor.staRenderTracks(html, null, null, null,
- shieldsTrackMax, powerTrackMax, crewTrackMax);
-
- // This allows for each item-edit image to link open an item sheet. This uses Simple Worldbuilding System Code.
- html.find('.control .edit').click((ev) => {
- const li = $(ev.currentTarget).parents('.entry');
- const item = this.actor.items.get(li.data('itemId'));
- item.sheet.render(true);
- });
-
- html.find('.click-to-nav').click((ev) => {
- const childId = $(ev.currentTarget).parents('.entry').data('itemChildId');
- const childShip = game.actors.find((target) => target.id === childId);
- childShip.sheet.render(true);
- });
-
- // This if statement checks if the form is editable, if not it hides controls used by the owner, then aborts any more of the script.
- if (!this.options.editable) {
- // This hides the ability to Perform an System Test for the character
- for (i = 0; i < html.find('.check-button').length; i++) {
- html.find('.check-button')[i].style.display = 'none';
- }
- // This hides all add and delete item images.
- for (i = 0; i < html.find('.control.create').length; i++) {
- html.find('.control.create')[i].style.display = 'none';
- }
- for (i = 0; i < html.find('.control .delete').length; i++) {
- html.find('.control .delete')[i].style.display = 'none';
- }
- // This hides all system and department check boxes (and titles)
- for (i = 0; i < html.find('.selector').length; i++) {
- html.find('.selector')[i].style.display = 'none';
- }
- // Remove hover CSS from clickables that are no longer clickable.
- for (i = 0; i < html.find('.box').length; i++) {
- html.find('.box')[i].classList.add('unset-clickables');
- }
- for (i = 0; i < html.find('.rollable').length; i++) {
- html.find('.rollable')[i].classList.add('unset-clickables');
- }
- return;
- }
-
- // set up click handler for items to send to the actor rollGenericItem
- html.find('.chat,.rollable').click( (ev) => {
- const itemType = $(ev.currentTarget).parents('.entry')[0].getAttribute('data-item-type');
- const itemId = $(ev.currentTarget).parents('.entry')[0].getAttribute('data-item-id');
- staActor.rollGenericItem(ev, itemType, itemId, this.actor);
- });
-
- // Allows item-create images to create an item of a type defined individually by each button. This uses code found via the Foundry VTT System Tutorial.
- html.find('.control.create').click( (ev) => {
- ev.preventDefault();
- const header = ev.currentTarget;
- const type = header.dataset.type;
- const data = foundry.utils.duplicate(header.dataset);
- const name = `New ${type.capitalize()}`;
- const itemData = {
- name: name,
- type: type,
- data: data,
- img: game.sta.defaultImage
- };
- delete itemData.data['type'];
- if ( foundry.utils.isNewerVersion( versionInfo, '0.8.-1' )) {
- return this.actor.createEmbeddedDocuments( 'Item', [(itemData)] );
- } else {
- return this.actor.createOwnedItem(itemData);
- }
- });
-
- // Allows item-delete images to allow deletion of the selected item.
- html.find('.control .delete').click((ev) => {
- // Cleaning up previous dialogs is nice, and also possibly avoids bugs from invalid popups.
- if (this.activeDialog) this.activeDialog.close();
-
- const li = $(ev.currentTarget).parents('.entry');
- this.activeDialog = staActor.deleteConfirmDialog(
- li[0].getAttribute('data-item-value'),
- () => {
- if ( foundry.utils.isNewerVersion( versionInfo, '0.8.-1' )) {
- this.actor.deleteEmbeddedDocuments( 'Item', [li.data('itemId')] );
- } else {
- this.actor.deleteOwnedItem( li.data( 'itemId' ));
- }
- },
- () => this.activeDialog = null
- );
- this.activeDialog.render(true);
- });
-
- // Reads if a shields track box has been clicked, and if it has will either: set the value to the clicked box, or reduce the value by one.
- // This check is dependent on various requirements, see comments in code.
- html.find('[id^="shields"]').click((ev) => {
- let total = '';
- const newTotalObject = $(ev.currentTarget)[0];
- const newTotal = newTotalObject.id.substring('shields-'.length);
- // data-selected stores whether the track box is currently activated or not. This checks that the box is activated
- if (newTotalObject.getAttribute('data-selected') === 'true') {
- // Now we check that the "next" track box is not activated.
- // If there isn't one, or it isn't activated, we only want to decrease the value by 1 rather than setting the value.
- const nextCheck = 'shields-' + (parseInt(newTotal) + 1);
- if (!html.find('#'+nextCheck)[0] || html.find('#'+nextCheck)[0].getAttribute('data-selected') != 'true') {
- html.find('#total-shields')[0].value = html.find('#total-shields')[0].value - 1;
- this.submit();
- // If it isn't caught by the if, the next box is likely activated. If something happened, its safer to set the value anyway.
- } else {
- total = html.find('#total-shields')[0].value;
- if (total != newTotal) {
- html.find('#total-shields')[0].value = newTotal;
- this.submit();
- }
- }
- // If the clicked box wasn't activated, we need to activate it now.
- } else {
- total = html.find('#total-shields')[0].value;
- if (total != newTotal) {
- html.find('#total-shields')[0].value = newTotal;
- this.submit();
- }
- }
- });
-
- // Reads if a power track box has been clicked, and if it has will either: set the value to the clicked box, or reduce the value by one.
- // This check is dependent on various requirements, see comments in code.
- html.find('[id^="power"]').click((ev) => {
- let total = '';
- const newTotalObject = $(ev.currentTarget)[0];
- const newTotal = newTotalObject.id.substring('power-'.length);
- // data-selected stores whether the track box is currently activated or not. This checks that the box is activated
- if (newTotalObject.getAttribute('data-selected') === 'true') {
- // Now we check that the "next" track box is not activated.
- // If there isn't one, or it isn't activated, we only want to decrease the value by 1 rather than setting the value.
- const nextCheck = 'power-' + (parseInt(newTotal) + 1);
- if (!html.find('#'+nextCheck)[0] || html.find('#'+nextCheck)[0].getAttribute('data-selected') != 'true') {
- html.find('#total-power')[0].value = html.find('#total-power')[0].value - 1;
- this.submit();
- // If it isn't caught by the if, the next box is likely activated. If something happened, its safer to set the value anyway.
- } else {
- total = html.find('#total-power')[0].value;
- if (total != newTotal) {
- html.find('#total-power')[0].value = newTotal;
- this.submit();
- }
- }
- // If the clicked box wasn't activated, we need to activate it now.
- } else {
- total = html.find('#total-power')[0].value;
- if (total != newTotal) {
- html.find('#total-power')[0].value = newTotal;
- this.submit();
- }
- }
- });
-
- // Reads if a crew track box has been clicked, and if it has will either: set the value to the clicked box, or reduce the value by one.
- // This check is dependent on various requirements, see comments in code.
- html.find('[id^="crew"]').click((ev) => {
- let total = '';
- const newTotalObject = $(ev.currentTarget)[0];
- const newTotal = newTotalObject.id.substring('crew-'.length);
- // data-selected stores whether the track box is currently activated or not. This checks that the box is activated
- if (newTotalObject.getAttribute('data-selected') === 'true') {
- // Now we check that the "next" track box is not activated.
- // If there isn't one, or it isn't activated, we only want to decrease the value by 1 rather than setting the value.
- const nextCheck = 'crew-' + (parseInt(newTotal) + 1);
- if (!html.find('#'+nextCheck)[0] || html.find('#'+nextCheck)[0].getAttribute('data-selected') != 'true') {
- html.find('#total-crew')[0].value = html.find('#total-crew')[0].value - 1;
- this.submit();
- // If it isn't caught by the if, the next box is likely activated. If something happened, its safer to set the value anyway.
- } else {
- total = html.find('#total-crew')[0].value;
- if (total != newTotal) {
- html.find('#total-crew')[0].value = newTotal;
- this.submit();
- }
- }
- // If the clicked box wasn't activated, we need to activate it now.
- } else {
- total = html.find('#total-crew')[0].value;
- if (total != newTotal) {
- html.find('#total-crew')[0].value = newTotal;
- this.submit();
- }
- }
- });
-
- // This is used to clean up all the HTML that comes from displaying outputs from the text editor boxes. There's probably a better way to do this but the quick and dirty worked this time.
- $.each($('[id^=talent-tooltip-text-]'), function(index, value) {
- const beforeDescription = value.innerHTML;
- const decoded = TextEditor.decodeHTML(beforeDescription);
- const prettifiedDescription = TextEditor.previewHTML(decoded, 1000);
- $('#' + value.id).html(prettifiedDescription);
- });
-
-
- html.find('.talent-tooltip-clickable').click((ev) => {
- const talentId = $(ev.currentTarget)[0].id.substring('talent-tooltip-clickable-'.length);
- const currentShowingTalentId = $('.talent-tooltip-container:not(.hide)')[0] ? $('.talent-tooltip-container:not(.hide)')[0].id.substring('talent-tooltip-container-'.length) : null;
-
- if (talentId == currentShowingTalentId) {
- $('#talent-tooltip-container-' + talentId).addClass('hide').removeAttr('style');
- } else {
- $('.talent-tooltip-container').addClass('hide').removeAttr('style');
- $('#talent-tooltip-container-' + talentId).removeClass('hide').height($('#talent-tooltip-text-' + talentId)[0].scrollHeight + 5);
- }
- });
-
- $.each($('[id^=injury-tooltip-text-]'), function(index, value) {
- const beforeDescription = value.innerHTML;
- const decoded = TextEditor.decodeHTML(beforeDescription);
- const prettifiedDescription = TextEditor.previewHTML(decoded, 1000);
- $('#' + value.id).html(prettifiedDescription);
- });
-
- html.find('.injury-tooltip-clickable').click((ev) => {
- const injuryId = $(ev.currentTarget)[0].id.substring('injury-tooltip-clickable-'.length);
- const currentShowinginjuryId = $('.injury-tooltip-container:not(.hide)')[0] ? $('.injury-tooltip-container:not(.hide)')[0].id.substring('injury-tooltip-container-'.length) : null;
-
- if (injuryId == currentShowinginjuryId) {
- $('#injury-tooltip-container-' + injuryId).addClass('hide').removeAttr('style');
- } else {
- $('.injury-tooltip-container').addClass('hide').removeAttr('style');
- $('#injury-tooltip-container-' + injuryId).removeClass('hide').height($('#injury-tooltip-text-' + injuryId)[0].scrollHeight + 5);
- }
- });
-
- // Turns the System checkboxes into essentially a radio button. It removes any other ticks, and then checks the new system.
- // Finally a submit is required as data has changed.
- html.find('.selector.system').click((ev) => {
- for (i = 0; i <= 5; i++) {
- html.find('.selector.system')[i].checked = false;
- }
- $(ev.currentTarget)[0].checked = true;
- this.submit();
- });
-
- // Turns the Department checkboxes into essentially a radio button. It removes any other ticks, and then checks the new department.
- // Finally a submit is required as data has changed.
- html.find('.selector.department').click((ev) => {
- for (i = 0; i <= 5; i++) {
- html.find('.selector.department')[i].checked = false;
- }
- $(ev.currentTarget)[0].checked = true;
- this.submit();
- });
-
- // If the check-button is clicked it grabs the selected system and the selected department and fires the method rollSystemTest. See actor.js for further info.
- html.find('.check-button.attribute').click((ev) => {
- let selectedSystem = '';
- let selectedSystemValue = '';
- let selectedDepartment = '';
- let selectedDepartmentValue = '';
- for (i = 0; i <= 5; i++) {
- if (html.find('.selector.system')[i].checked === true) {
- selectedSystem = html.find('.selector.system')[i].id;
- selectedSystem = selectedSystem.slice(0, -9);
- selectedSystemValue = html.find('#'+selectedSystem)[0].value;
- }
- }
- for (i = 0; i <= 5; i++) {
- if (html.find('.selector.department')[i].checked === true) {
- selectedDepartment = html.find('.selector.department')[i].id;
- selectedDepartment = selectedDepartment.slice(0, -9);
- selectedDepartmentValue = html.find('#'+selectedDepartment)[0].value;
- }
- }
-
- staActor.rollAttributeTest(ev, selectedSystem,
- parseInt(selectedSystemValue), selectedDepartment,
- parseInt(selectedDepartmentValue), 2, this.actor);
- });
-
- // If the check-button is clicked it fires the method challenge roll method. See actor.js for further info.
- html.find('.check-button.challenge').click((ev) => {
- staActor.rollChallengeRoll(ev, null, null, this.actor);
- });
-
- html.find('.reroll-result').click((ev) => {
- let selectedSystem = '';
- let selectedSystemValue = '';
- let selectedDepartment = '';
- let selectedDepartmentValue = '';
- for (i = 0; i <= 5; i++) {
- if (html.find('.selector.system')[i].checked === true) {
- selectedSystem = html.find('.selector.system')[i].id;
- selectedSystem = selectedSystem.slice(0, -9);
- selectedSystemValue = html.find('#'+selectedSystem)[0].value;
- }
- }
- for (i = 0; i <= 5; i++) {
- if (html.find('.selector.department')[i].checked === true) {
- selectedDepartment = html.find('.selector.department')[i].id;
- selectedDepartment = selectedDepartment.slice(0, -9);
- selectedDepartmentValue = html.find('#'+selectedDepartment)[0].value;
- }
- }
-
- staActor.rollAttributeTest(ev, selectedSystem,
- parseInt(selectedSystemValue), selectedDepartment,
- parseInt(selectedDepartmentValue), null, this.actor);
- });
-
- $(html).find('[id^=starship-weapon-]').each( function( _, value ) {
- const weaponDamage = parseInt(value.dataset.itemDamage);
- const securityValue = parseInt(html.find('#security')[0].value);
- let scaleDamage = 0;
- if (value.dataset.itemIncludescale == 'true') scaleDamage = parseInt(html.find('#scale')[0].value);
- const attackDamageValue = weaponDamage + securityValue + scaleDamage;
- value.getElementsByClassName('damage')[0].innerText = attackDamageValue;
- });
-
- html.find('.selector.system').each(function(index, value) {
- const $systemCheckbox = $(value);
- const $systemBreach = $systemCheckbox.siblings('.breaches');
- const $systemDestroyed = $systemCheckbox.siblings('.system-destroyed');
-
- const shipScaleValue = Number.parseInt(html.find('#scale').attr('value'));
- const breachValue = Number.parseInt($systemBreach.attr('value'));
-
- const isSystemDamaged = breachValue >= (Math.ceil(shipScaleValue / 2)) ? true : false;
- const isSystemDisabled = breachValue >= shipScaleValue ? true : false;
- const isSystemDestroyed = breachValue >= (Math.ceil(shipScaleValue + 1)) ? true : false;
-
- if (isSystemDamaged && !isSystemDisabled && !isSystemDestroyed) {
- $systemBreach.addClass('highlight-damaged');
- $systemBreach.removeClass('highlight-disabled');
- $systemBreach.removeClass('highlight-destroyed');
- } else if (isSystemDisabled && !isSystemDestroyed) {
- $systemBreach.addClass('highlight-disabled');
- $systemBreach.removeClass('highlight-destroyed');
- $systemBreach.removeClass('highlight-damaged');
- } else if (isSystemDestroyed) {
- $systemBreach.addClass('highlight-destroyed');
- $systemBreach.removeClass('highlight-disabled');
- $systemBreach.removeClass('highlight-damaged');
- } else {
- $systemBreach.removeClass('highlight-damaged highlight-disabled highlight-destroyed');
- }
- });
- }
-}
+import {
+ STASharedActorFunctions
+} from '../actor.js';
+
+export class STAStarshipSheet extends ActorSheet {
+ constructor(object, options={}) {
+ super(object, options);
+
+ /**
+ * An actively open dialog that should be closed before a new one is opened.
+ * @type {Dialog}
+ */
+ this.activeDialog = null;
+ }
+
+ /** @override */
+ static get defaultOptions() {
+ return foundry.utils.mergeObject(super.defaultOptions, {
+ width: 850,
+ height: 850,
+ dragDrop: [{
+ dragSelector: '.item-list .item',
+ dropSelector: null
+ }]
+ });
+ }
+
+ /* -------------------------------------------- */
+ // If the player is not a GM and has limited permissions - send them to the limited sheet, otherwise, continue as usual.
+ /** @override */
+ get template() {
+ const versionInfo = game.world.coreVersion;
+ if ( !game.user.isGM && this.actor.limited) return 'systems/sta/templates/actors/limited-sheet.hbs';
+ return `systems/sta/templates/actors/starship-sheet.hbs`;
+ }
+ render(force = false, options = {}) {
+ if (!game.user.isGM && this.actor.limited) {
+ options = foundry.utils.mergeObject(options, {height: 250});
+ }
+ return super.render(force, options);
+ }
+
+ /* -------------------------------------------- */
+
+ /** @override */
+ getData() {
+ const sheetData = this.object;
+ sheetData.dtypes = ['String', 'Number', 'Boolean'];
+
+ // Ensure department values don't weigh over the max.
+ const overrideDepartmentLimitSetting = game.settings.get('sta', 'shipDepartmentLimitIgnore');
+ let maxDepartment = 5;
+ if (overrideDepartmentLimitSetting) maxDepartment = 99;
+ $.each(sheetData.system.departments, (key, department) => {
+ if (department.value > maxDepartment) department.value = maxDepartment;
+ });
+
+ // Checks if shields is larger than its max, if so, set to max.
+ if (sheetData.system.shields.value > sheetData.system.shields.max) {
+ sheetData.system.shields.value = sheetData.system.shields.max;
+ }
+ if (sheetData.system.power.value > sheetData.system.power.max) {
+ sheetData.system.power.value = sheetData.system.power.max;
+ }
+ if (sheetData.system.crew.value > sheetData.system.crew.max) {
+ sheetData.system.crew.value = sheetData.system.crew.max;
+ }
+
+ // Ensure system and department values aren't lower than their minimums.
+ $.each(sheetData.system.systems, (key, system) => {
+ if (system.value < 0) system.value = 0;
+ });
+
+ $.each(sheetData.system.departments, (key, department) => {
+ if (department.value < 0) department.value = 0;
+ });
+
+ // Checks if shields is below 0, if so - set it to 0.
+ if (sheetData.system.shields.value < 0) {
+ sheetData.system.shields.value = 0;
+ }
+ if (sheetData.system.power.value < 0) {
+ sheetData.system.power.value = 0;
+ }
+ if (sheetData.system.crew.value < 0) {
+ sheetData.system.crew.value = 0;
+ }
+
+ return sheetData;
+ }
+
+ /* -------------------------------------------- */
+
+ /** @override */
+ activateListeners(html) {
+ super.activateListeners(html);
+
+ // Allows checking version easily
+ const versionInfo = game.world.coreVersion;
+
+ // Opens the class STASharedActorFunctions for access at various stages.
+ const staActor = new STASharedActorFunctions();
+
+ // If the player has limited access to the actor, there is nothing to see here. Return.
+ if ( !game.user.isGM && this.actor.limited) return;
+
+ // We use i alot in for loops. Best to assign it now for use later in multiple places.
+ let i;
+ let shieldsTrackMax = 0;
+ let powerTrackMax = 0;
+ let crewTrackMax = 0;
+
+ // This creates a dynamic Shields tracker. It polls for the value of the structure system and security department.
+ // With the total value, creates a new div for each and places it under a child called "bar-shields-renderer".
+ function shieldsTrackUpdate() {
+ const localizedValues = {
+ 'advancedshields': game.i18n.localize('sta.actor.starship.talents.advancedshields')
+ };
+
+ shieldsTrackMax = parseInt(html.find('#structure')[0].value) + parseInt(html.find('#security')[0].value) + parseInt(html.find('#shieldmod')[0].value);
+ if (html.find(`[data-talent-name*="${localizedValues.advancedshields}"]`).length > 0) {
+ shieldsTrackMax += 5;
+ }
+ // This checks that the max-shields hidden field is equal to the calculated Max Shields value, if not it makes it so.
+ if (html.find('#max-shields')[0].value != shieldsTrackMax) {
+ html.find('#max-shields')[0].value = shieldsTrackMax;
+ }
+ html.find('#bar-shields-renderer').empty();
+ for (i = 1; i <= shieldsTrackMax; i++) {
+ const div = document.createElement('DIV');
+ div.className = 'box';
+ div.id = 'shields-' + i;
+ div.innerHTML = i;
+ div.style = 'width: calc(100% / ' + html.find('#max-shields')[0].value + ');';
+ html.find('#bar-shields-renderer')[0].appendChild(div);
+ }
+ }
+ shieldsTrackUpdate();
+
+ // This creates a dynamic Power tracker. It polls for the value of the engines system.
+ // With the value, creates a new div for each and places it under a child called "bar-power-renderer".
+ function powerTrackUpdate() {
+ powerTrackMax = parseInt(html.find('#engines')[0].value);
+ if (html.find('[data-talent-name*="Secondary Reactors"]').length > 0) {
+ powerTrackMax += 5;
+ }
+ // This checks that the max-power hidden field is equal to the calculated Max Power value, if not it makes it so.
+ if (html.find('#max-power')[0].value != powerTrackMax) {
+ html.find('#max-power')[0].value = powerTrackMax;
+ }
+ html.find('#bar-power-renderer').empty();
+ for (i = 1; i <= powerTrackMax; i++) {
+ const div = document.createElement('DIV');
+ div.className = 'box';
+ div.id = 'power-' + i;
+ div.innerHTML = i;
+ div.style = 'width: calc(100% / ' + html.find('#max-power')[0].value + ');';
+ html.find('#bar-power-renderer')[0].appendChild(div);
+ }
+ }
+ powerTrackUpdate();
+
+ // This creates a dynamic Crew Support tracker. It polls for the value of the ships's scale.
+ // With the value, creates a new div for each and places it under a child called "bar-crew-renderer".
+ function crewTrackUpdate() {
+ crewTrackMax = parseInt(html.find('#scale')[0].value);
+ // This checks that the max-crew hidden field is equal to the calculated Max Crew Support value, if not it makes it so.
+ if (html.find('#max-crew')[0].value != crewTrackMax) {
+ html.find('#max-crew')[0].value = crewTrackMax;
+ }
+ html.find('#bar-crew-renderer').empty();
+ for (i = 1; i <= crewTrackMax; i++) {
+ const div = document.createElement('DIV');
+ div.className = 'box';
+ div.id = 'crew-' + i;
+ div.innerHTML = i;
+ div.style = 'width: calc(100% / ' + html.find('#max-crew')[0].value + ');';
+ html.find('#bar-crew-renderer')[0].appendChild(div);
+ }
+ }
+ crewTrackUpdate();
+
+ // Fires the function staRenderTracks as soon as the parameters exist to do so.
+ staActor.staRenderTracks(html, null, null, null,
+ shieldsTrackMax, powerTrackMax, crewTrackMax);
+
+ html.find('.click-to-nav').click((ev) => {
+ const childId = $(ev.currentTarget).parents('.entry').data('itemChildId');
+ const childShip = game.actors.find((target) => target.id === childId);
+ childShip.sheet.render(true);
+ });
+
+ // This if statement checks if the form is editable, if not it hides controls used by the owner, then aborts any more of the script.
+ if (!this.options.editable) {
+ // This hides the ability to Perform an System Test for the character
+ for (i = 0; i < html.find('.check-button').length; i++) {
+ html.find('.check-button')[i].style.display = 'none';
+ }
+ // This hides all add and delete item images.
+ for (i = 0; i < html.find('.control.create').length; i++) {
+ html.find('.control.create')[i].style.display = 'none';
+ }
+ for (i = 0; i < html.find('.control .delete').length; i++) {
+ html.find('.control .delete')[i].style.display = 'none';
+ }
+ // This hides all system and department check boxes (and titles)
+ for (i = 0; i < html.find('.selector').length; i++) {
+ html.find('.selector')[i].style.display = 'none';
+ }
+ // Remove hover CSS from clickables that are no longer clickable.
+ for (i = 0; i < html.find('.box').length; i++) {
+ html.find('.box')[i].classList.add('unset-clickables');
+ }
+ for (i = 0; i < html.find('.rollable').length; i++) {
+ html.find('.rollable')[i].classList.add('unset-clickables');
+ }
+ return;
+ }
+
+ // set up click handler for items to send to the actor rollGenericItem
+ html.find('.chat,.rollable').click( (ev) => {
+ const itemType = $(ev.currentTarget).parents('.entry')[0].getAttribute('data-item-type');
+ const itemId = $(ev.currentTarget).parents('.entry')[0].getAttribute('data-item-id');
+ staActor.rollGenericItem(ev, itemType, itemId, this.actor);
+ });
+
+ // Listen for changes in the item name input field
+ html.find('.item-name').on('change', (event) => {
+ const input = event.currentTarget;
+ const itemId = input.dataset.itemId;
+ const item = this.actor.items.get(itemId);
+ const newName = input.value.trim();
+
+ if (item && newName) {
+ item.update({name: newName});
+ }
+ });
+
+ // Create new items
+ html.find('.control.create').click(async (ev) => {
+ ev.preventDefault();
+ const header = ev.currentTarget;
+ const type = header.dataset.type;
+ const data = Object.assign({}, header.dataset);
+ const name = `New ${type.capitalize()}`;
+
+ const itemData = {
+ name: name,
+ type: type,
+ data: data,
+ };
+ delete itemData.data['type'];
+
+ const newItem = await this.actor.createEmbeddedDocuments('Item', [itemData]);
+ });
+
+ // Edit items
+ html.find('.control .edit').click((ev) => {
+ const li = $(ev.currentTarget).parents('.entry');
+ const item = this.actor.items.get(li.data('itemId'));
+ item.sheet.render(true);
+ });
+
+ // Delete items with confirmation dialog
+ html.find('.control .delete').click((ev) => {
+ const li = $(ev.currentTarget).parents('.entry');
+ const itemId = li.data('itemId');
+
+ new Dialog({
+ title: `${game.i18n.localize('sta.apps.deleteitem')}`,
+ content: `
${game.i18n.localize('sta.apps.deleteconfirm')}
`,
+ buttons: {
+ yes: {
+ icon: '',
+ label: `${game.i18n.localize('sta.apps.yes')}`,
+ callback: () => this.actor.deleteEmbeddedDocuments('Item', [itemId])
+ },
+ no: {
+ icon: '',
+ label: `${game.i18n.localize('sta.apps.no')}`
+ }
+ },
+ default: 'no'
+ }).render(true);
+ });
+
+ // Item popout tooltip of description
+ html.find('.item-name').on('mouseover', (event) => {
+ const input = event.currentTarget;
+ const itemId = input.dataset.itemId;
+ const item = this.actor.items.get(itemId);
+
+ if (item) {
+ const description = item.system.description?.trim().replace(/\n/g, ' ');
+
+ if (description) {
+ input._tooltipTimeout = setTimeout(() => {
+ let tooltip = document.querySelector('.item-tooltip');
+ if (!tooltip) {
+ tooltip = document.createElement('div');
+ tooltip.classList.add('item-tooltip');
+ document.body.appendChild(tooltip);
+ }
+
+ tooltip.innerHTML = `${description}`;
+
+ const {clientX: mouseX, clientY: mouseY} = event;
+ tooltip.style.left = `${mouseX + 10}px`;
+ tooltip.style.top = `${mouseY + 10}px`;
+
+ document.body.appendChild(tooltip);
+ const tooltipRect = tooltip.getBoundingClientRect();
+
+ if (tooltipRect.bottom > window.innerHeight) {
+ tooltip.style.top = `${window.innerHeight - tooltipRect.height - 20}px`;
+ }
+
+ input._tooltip = tooltip;
+ }, 1000);
+ }
+ }
+ });
+
+ html.find('.item-name').on('mouseout', (event) => {
+ const input = event.currentTarget;
+
+ if (input._tooltipTimeout) {
+ clearTimeout(input._tooltipTimeout);
+ delete input._tooltipTimeout;
+ }
+
+ if (input._tooltip) {
+ document.body.removeChild(input._tooltip);
+ delete input._tooltip;
+ }
+ });
+
+ // Reads if a shields track box has been clicked, and if it has will either: set the value to the clicked box, or reduce the value by one.
+ // This check is dependent on various requirements, see comments in code.
+ html.find('[id^="shields"]').click((ev) => {
+ let total = '';
+ const newTotalObject = $(ev.currentTarget)[0];
+ const newTotal = newTotalObject.id.substring('shields-'.length);
+ // data-selected stores whether the track box is currently activated or not. This checks that the box is activated
+ if (newTotalObject.getAttribute('data-selected') === 'true') {
+ // Now we check that the "next" track box is not activated.
+ // If there isn't one, or it isn't activated, we only want to decrease the value by 1 rather than setting the value.
+ const nextCheck = 'shields-' + (parseInt(newTotal) + 1);
+ if (!html.find('#'+nextCheck)[0] || html.find('#'+nextCheck)[0].getAttribute('data-selected') != 'true') {
+ html.find('#total-shields')[0].value = html.find('#total-shields')[0].value - 1;
+ this.submit();
+ // If it isn't caught by the if, the next box is likely activated. If something happened, its safer to set the value anyway.
+ } else {
+ total = html.find('#total-shields')[0].value;
+ if (total != newTotal) {
+ html.find('#total-shields')[0].value = newTotal;
+ this.submit();
+ }
+ }
+ // If the clicked box wasn't activated, we need to activate it now.
+ } else {
+ total = html.find('#total-shields')[0].value;
+ if (total != newTotal) {
+ html.find('#total-shields')[0].value = newTotal;
+ this.submit();
+ }
+ }
+ });
+
+ // Reads if a power track box has been clicked, and if it has will either: set the value to the clicked box, or reduce the value by one.
+ // This check is dependent on various requirements, see comments in code.
+ html.find('[id^="power"]').click((ev) => {
+ let total = '';
+ const newTotalObject = $(ev.currentTarget)[0];
+ const newTotal = newTotalObject.id.substring('power-'.length);
+ // data-selected stores whether the track box is currently activated or not. This checks that the box is activated
+ if (newTotalObject.getAttribute('data-selected') === 'true') {
+ // Now we check that the "next" track box is not activated.
+ // If there isn't one, or it isn't activated, we only want to decrease the value by 1 rather than setting the value.
+ const nextCheck = 'power-' + (parseInt(newTotal) + 1);
+ if (!html.find('#'+nextCheck)[0] || html.find('#'+nextCheck)[0].getAttribute('data-selected') != 'true') {
+ html.find('#total-power')[0].value = html.find('#total-power')[0].value - 1;
+ this.submit();
+ // If it isn't caught by the if, the next box is likely activated. If something happened, its safer to set the value anyway.
+ } else {
+ total = html.find('#total-power')[0].value;
+ if (total != newTotal) {
+ html.find('#total-power')[0].value = newTotal;
+ this.submit();
+ }
+ }
+ // If the clicked box wasn't activated, we need to activate it now.
+ } else {
+ total = html.find('#total-power')[0].value;
+ if (total != newTotal) {
+ html.find('#total-power')[0].value = newTotal;
+ this.submit();
+ }
+ }
+ });
+
+ // Reads if a crew track box has been clicked, and if it has will either: set the value to the clicked box, or reduce the value by one.
+ // This check is dependent on various requirements, see comments in code.
+ html.find('[id^="crew"]').click((ev) => {
+ let total = '';
+ const newTotalObject = $(ev.currentTarget)[0];
+ const newTotal = newTotalObject.id.substring('crew-'.length);
+ // data-selected stores whether the track box is currently activated or not. This checks that the box is activated
+ if (newTotalObject.getAttribute('data-selected') === 'true') {
+ // Now we check that the "next" track box is not activated.
+ // If there isn't one, or it isn't activated, we only want to decrease the value by 1 rather than setting the value.
+ const nextCheck = 'crew-' + (parseInt(newTotal) + 1);
+ if (!html.find('#'+nextCheck)[0] || html.find('#'+nextCheck)[0].getAttribute('data-selected') != 'true') {
+ html.find('#total-crew')[0].value = html.find('#total-crew')[0].value - 1;
+ this.submit();
+ // If it isn't caught by the if, the next box is likely activated. If something happened, its safer to set the value anyway.
+ } else {
+ total = html.find('#total-crew')[0].value;
+ if (total != newTotal) {
+ html.find('#total-crew')[0].value = newTotal;
+ this.submit();
+ }
+ }
+ // If the clicked box wasn't activated, we need to activate it now.
+ } else {
+ total = html.find('#total-crew')[0].value;
+ if (total != newTotal) {
+ html.find('#total-crew')[0].value = newTotal;
+ this.submit();
+ }
+ }
+ });
+
+ // Turns the System checkboxes into essentially a radio button. It removes any other ticks, and then checks the new system.
+ // Finally a submit is required as data has changed.
+ html.find('.selector.system').click((ev) => {
+ for (i = 0; i <= 5; i++) {
+ html.find('.selector.system')[i].checked = false;
+ }
+ $(ev.currentTarget)[0].checked = true;
+ this.submit();
+ });
+
+ // Turns the Department checkboxes into essentially a radio button. It removes any other ticks, and then checks the new department.
+ // Finally a submit is required as data has changed.
+ html.find('.selector.department').click((ev) => {
+ for (i = 0; i <= 5; i++) {
+ html.find('.selector.department')[i].checked = false;
+ }
+ $(ev.currentTarget)[0].checked = true;
+ this.submit();
+ });
+
+ // If the check-button is clicked it grabs the selected system and the selected department and fires the method rollSystemTest. See actor.js for further info.
+ html.find('.check-button.attribute').click((ev) => {
+ let selectedSystem = '';
+ let selectedSystemValue = '';
+ let selectedDepartment = '';
+ let selectedDepartmentValue = '';
+ for (i = 0; i <= 5; i++) {
+ if (html.find('.selector.system')[i].checked === true) {
+ selectedSystem = html.find('.selector.system')[i].id;
+ selectedSystem = selectedSystem.slice(0, -9);
+ selectedSystemValue = html.find('#'+selectedSystem)[0].value;
+ }
+ }
+ for (i = 0; i <= 5; i++) {
+ if (html.find('.selector.department')[i].checked === true) {
+ selectedDepartment = html.find('.selector.department')[i].id;
+ selectedDepartment = selectedDepartment.slice(0, -9);
+ selectedDepartmentValue = html.find('#'+selectedDepartment)[0].value;
+ }
+ }
+
+ staActor.rollAttributeTest(ev, selectedSystem,
+ parseInt(selectedSystemValue), selectedDepartment,
+ parseInt(selectedDepartmentValue), 2, this.actor);
+ });
+
+ // If the check-button is clicked it fires the method challenge roll method. See actor.js for further info.
+ html.find('.check-button.challenge').click((ev) => {
+ staActor.rollChallengeRoll(ev, null, null, this.actor);
+ });
+
+ html.find('.reroll-result').click((ev) => {
+ let selectedSystem = '';
+ let selectedSystemValue = '';
+ let selectedDepartment = '';
+ let selectedDepartmentValue = '';
+ for (i = 0; i <= 5; i++) {
+ if (html.find('.selector.system')[i].checked === true) {
+ selectedSystem = html.find('.selector.system')[i].id;
+ selectedSystem = selectedSystem.slice(0, -9);
+ selectedSystemValue = html.find('#'+selectedSystem)[0].value;
+ }
+ }
+ for (i = 0; i <= 5; i++) {
+ if (html.find('.selector.department')[i].checked === true) {
+ selectedDepartment = html.find('.selector.department')[i].id;
+ selectedDepartment = selectedDepartment.slice(0, -9);
+ selectedDepartmentValue = html.find('#'+selectedDepartment)[0].value;
+ }
+ }
+
+ staActor.rollAttributeTest(ev, selectedSystem,
+ parseInt(selectedSystemValue), selectedDepartment,
+ parseInt(selectedDepartmentValue), null, this.actor);
+ });
+
+ $(html).find('[id^=starship-weapon-]').each( function( _, value ) {
+ const weaponDamage = parseInt(value.dataset.itemDamage);
+ const securityValue = parseInt(html.find('#security')[0].value);
+ let scaleDamage = 0;
+ if (value.dataset.itemIncludescale == 'true') scaleDamage = parseInt(html.find('#scale')[0].value);
+ const attackDamageValue = weaponDamage + securityValue + scaleDamage;
+ value.getElementsByClassName('damage')[0].innerText = attackDamageValue;
+ });
+
+ html.find('.selector.system').each(function(index, value) {
+ const $systemCheckbox = $(value);
+ const $systemBreach = $systemCheckbox.siblings('.breaches');
+ const $systemDestroyed = $systemCheckbox.siblings('.system-destroyed');
+
+ const shipScaleValue = Number.parseInt(html.find('#scale').attr('value'));
+ const breachValue = Number.parseInt($systemBreach.attr('value'));
+
+ const isSystemDamaged = breachValue >= (Math.ceil(shipScaleValue / 2)) ? true : false;
+ const isSystemDisabled = breachValue >= shipScaleValue ? true : false;
+ const isSystemDestroyed = breachValue >= (Math.ceil(shipScaleValue + 1)) ? true : false;
+
+ if (isSystemDamaged && !isSystemDisabled && !isSystemDestroyed) {
+ $systemBreach.addClass('highlight-damaged');
+ $systemBreach.removeClass('highlight-disabled');
+ $systemBreach.removeClass('highlight-destroyed');
+ } else if (isSystemDisabled && !isSystemDestroyed) {
+ $systemBreach.addClass('highlight-disabled');
+ $systemBreach.removeClass('highlight-destroyed');
+ $systemBreach.removeClass('highlight-damaged');
+ } else if (isSystemDestroyed) {
+ $systemBreach.addClass('highlight-destroyed');
+ $systemBreach.removeClass('highlight-disabled');
+ $systemBreach.removeClass('highlight-damaged');
+ } else {
+ $systemBreach.removeClass('highlight-damaged highlight-disabled highlight-destroyed');
+ }
+ });
+ }
+}
diff --git a/src/module/actors/sheets/starship-sheet2e.js b/src/module/actors/sheets/starship-sheet2e.js
index b6c2580..501797d 100644
--- a/src/module/actors/sheets/starship-sheet2e.js
+++ b/src/module/actors/sheets/starship-sheet2e.js
@@ -16,9 +16,8 @@ export class STAStarshipSheet2e extends ActorSheet {
/** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
- classes: ['sta', 'sheet', 'actor', 'starship'],
- width: 800,
- height: 735,
+ width: 850,
+ height: 850,
dragDrop: [{
dragSelector: '.item-list .item',
dropSelector: null
@@ -32,9 +31,14 @@ export class STAStarshipSheet2e extends ActorSheet {
get template() {
const versionInfo = game.world.coreVersion;
if ( !game.user.isGM && this.actor.limited) return 'systems/sta/templates/actors/limited-sheet.hbs';
- if (!foundry.utils.isNewerVersion(versionInfo, '0.8.-1')) return 'systems/sta/templates/actors/starship-sheet-legacy.hbs';
return `systems/sta/templates/actors/starship-sheet2e.hbs`;
}
+ render(force = false, options = {}) {
+ if (!game.user.isGM && this.actor.limited) {
+ options = foundry.utils.mergeObject(options, {height: 250});
+ }
+ return super.render(force, options);
+ }
/* -------------------------------------------- */
@@ -92,7 +96,7 @@ export class STAStarshipSheet2e extends ActorSheet {
super.activateListeners(html);
// Allows checking version easily
- const versionInfo = game.world.coreVersion;
+ const versionInfo = game.world.coreVersion;
// Opens the class STASharedActorFunctions for access at various stages.
const staActor = new STASharedActorFunctions();
@@ -140,11 +144,8 @@ export class STAStarshipSheet2e extends ActorSheet {
// This creates a dynamic Power tracker. It polls for the value of the engines system.
// With the value, creates a new div for each and places it under a child called "bar-power-renderer".
function powerTrackUpdate() {
- // powerTrackMax = parseInt(html.find('#engines')[0].value);
powerTrackMax = 0;
- // if (html.find('[data-talent-name*="Secondary Reactors"]').length > 0) {
- // powerTrackMax += 5;
- // }
+
// This checks that the max-power hidden field is equal to the calculated Max Power value, if not it makes it so.
if (html.find('#max-power')[0].value != powerTrackMax) {
html.find('#max-power')[0].value = powerTrackMax;
@@ -179,7 +180,7 @@ export class STAStarshipSheet2e extends ActorSheet {
}
if (html.find(`[data-talent-name*="${localizedValues.abundantpersonnel}"]`).length > 0) {
crewTrackMax += crewTrackMax;
- }
+ }
// This checks that the max-crew hidden field is equal to the calculated Max Crew Support value, if not it makes it so.
if (html.find('#max-crew')[0].value != crewTrackMax) {
html.find('#max-crew')[0].value = crewTrackMax;
@@ -200,13 +201,6 @@ export class STAStarshipSheet2e extends ActorSheet {
staActor.staRenderTracks(html, null, null, null,
shieldsTrackMax, powerTrackMax, crewTrackMax);
- // This allows for each item-edit image to link open an item sheet. This uses Simple Worldbuilding System Code.
- html.find('.control .edit').click((ev) => {
- const li = $(ev.currentTarget).parents('.entry');
- const item = this.actor.items.get(li.data('itemId'));
- item.sheet.render(true);
- });
-
html.find('.click-to-nav').click((ev) => {
const childId = $(ev.currentTarget).parents('.entry').data('itemChildId');
const childShip = game.actors.find((target) => target.id === childId);
@@ -247,45 +241,115 @@ export class STAStarshipSheet2e extends ActorSheet {
staActor.rollGenericItem(ev, itemType, itemId, this.actor);
});
- // Allows item-create images to create an item of a type defined individually by each button. This uses code found via the Foundry VTT System Tutorial.
- html.find('.control.create').click( (ev) => {
+ // Listen for changes in the item name input field
+ html.find('.item-name').on('change', (event) => {
+ const input = event.currentTarget;
+ const itemId = input.dataset.itemId;
+ const item = this.actor.items.get(itemId);
+ const newName = input.value.trim();
+
+ if (item && newName) {
+ item.update({name: newName});
+ }
+ });
+
+ // Create new items
+ html.find('.control.create').click(async (ev) => {
ev.preventDefault();
const header = ev.currentTarget;
const type = header.dataset.type;
- const data = foundry.utils.duplicate(header.dataset);
+ const data = Object.assign({}, header.dataset);
const name = `New ${type.capitalize()}`;
+
const itemData = {
name: name,
type: type,
data: data,
- img: game.sta.defaultImage
};
delete itemData.data['type'];
- if ( foundry.utils.isNewerVersion( versionInfo, '0.8.-1' )) {
- return this.actor.createEmbeddedDocuments( 'Item', [(itemData)] );
- } else {
- return this.actor.createOwnedItem(itemData);
- }
+
+ const newItem = await this.actor.createEmbeddedDocuments('Item', [itemData]);
});
- // Allows item-delete images to allow deletion of the selected item.
- html.find('.control .delete').click((ev) => {
- // Cleaning up previous dialogs is nice, and also possibly avoids bugs from invalid popups.
- if (this.activeDialog) this.activeDialog.close();
+ // Edit items
+ html.find('.control .edit').click((ev) => {
+ const li = $(ev.currentTarget).parents('.entry');
+ const item = this.actor.items.get(li.data('itemId'));
+ item.sheet.render(true);
+ });
+ // Delete items with confirmation dialog
+ html.find('.control .delete').click((ev) => {
const li = $(ev.currentTarget).parents('.entry');
- this.activeDialog = staActor.deleteConfirmDialog(
- li[0].getAttribute('data-item-value'),
- () => {
- if ( foundry.utils.isNewerVersion( versionInfo, '0.8.-1' )) {
- this.actor.deleteEmbeddedDocuments( 'Item', [li.data('itemId')] );
- } else {
- this.actor.deleteOwnedItem( li.data( 'itemId' ));
+ const itemId = li.data('itemId');
+
+ new Dialog({
+ title: `${game.i18n.localize('sta.apps.deleteitem')}`,
+ content: `
${game.i18n.localize('sta.apps.deleteconfirm')}
`,
+ buttons: {
+ yes: {
+ icon: '',
+ label: `${game.i18n.localize('sta.apps.yes')}`,
+ callback: () => this.actor.deleteEmbeddedDocuments('Item', [itemId])
+ },
+ no: {
+ icon: '',
+ label: `${game.i18n.localize('sta.apps.no')}`
}
},
- () => this.activeDialog = null
- );
- this.activeDialog.render(true);
+ default: 'no'
+ }).render(true);
+ });
+
+ // Item popout tooltip of description
+ html.find('.item-name').on('mouseover', (event) => {
+ const input = event.currentTarget;
+ const itemId = input.dataset.itemId;
+ const item = this.actor.items.get(itemId);
+
+ if (item) {
+ const description = item.system.description?.trim().replace(/\n/g, ' ');
+
+ if (description) {
+ input._tooltipTimeout = setTimeout(() => {
+ let tooltip = document.querySelector('.item-tooltip');
+ if (!tooltip) {
+ tooltip = document.createElement('div');
+ tooltip.classList.add('item-tooltip');
+ document.body.appendChild(tooltip);
+ }
+
+ tooltip.innerHTML = `${description}`;
+
+ const {clientX: mouseX, clientY: mouseY} = event;
+ tooltip.style.left = `${mouseX + 10}px`;
+ tooltip.style.top = `${mouseY + 10}px`;
+
+ document.body.appendChild(tooltip);
+ const tooltipRect = tooltip.getBoundingClientRect();
+
+ if (tooltipRect.bottom > window.innerHeight) {
+ tooltip.style.top = `${window.innerHeight - tooltipRect.height - 20}px`;
+ }
+
+ input._tooltip = tooltip;
+ }, 1000);
+ }
+ }
+ });
+
+ html.find('.item-name').on('mouseout', (event) => {
+ const input = event.currentTarget;
+
+ if (input._tooltipTimeout) {
+ clearTimeout(input._tooltipTimeout);
+ delete input._tooltipTimeout;
+ }
+
+ if (input._tooltip) {
+ document.body.removeChild(input._tooltip);
+ delete input._tooltip;
+ }
});
// Reads if a shields track box has been clicked, and if it has will either: set the value to the clicked box, or reduce the value by one.
@@ -384,47 +448,6 @@ export class STAStarshipSheet2e extends ActorSheet {
}
});
- // This is used to clean up all the HTML that comes from displaying outputs from the text editor boxes. There's probably a better way to do this but the quick and dirty worked this time.
- $.each($('[id^=talent-tooltip-text-]'), function(index, value) {
- const beforeDescription = value.innerHTML;
- const decoded = TextEditor.decodeHTML(beforeDescription);
- const prettifiedDescription = TextEditor.previewHTML(decoded, 1000);
- $('#' + value.id).html(prettifiedDescription);
- });
-
-
- html.find('.talent-tooltip-clickable').click((ev) => {
- const talentId = $(ev.currentTarget)[0].id.substring('talent-tooltip-clickable-'.length);
- const currentShowingTalentId = $('.talent-tooltip-container:not(.hide)')[0] ? $('.talent-tooltip-container:not(.hide)')[0].id.substring('talent-tooltip-container-'.length) : null;
-
- if (talentId == currentShowingTalentId) {
- $('#talent-tooltip-container-' + talentId).addClass('hide').removeAttr('style');
- } else {
- $('.talent-tooltip-container').addClass('hide').removeAttr('style');
- $('#talent-tooltip-container-' + talentId).removeClass('hide').height($('#talent-tooltip-text-' + talentId)[0].scrollHeight + 5);
- }
- });
-
- $.each($('[id^=injury-tooltip-text-]'), function(index, value) {
- const beforeDescription = value.innerHTML;
- const decoded = TextEditor.decodeHTML(beforeDescription);
- const prettifiedDescription = TextEditor.previewHTML(decoded, 1000);
- $('#' + value.id).html(prettifiedDescription);
- });
-
-
- html.find('.injury-tooltip-clickable').click((ev) => {
- const injuryId = $(ev.currentTarget)[0].id.substring('injury-tooltip-clickable-'.length);
- const currentShowinginjuryId = $('.injury-tooltip-container:not(.hide)')[0] ? $('.injury-tooltip-container:not(.hide)')[0].id.substring('injury-tooltip-container-'.length) : null;
-
- if (injuryId == currentShowinginjuryId) {
- $('#injury-tooltip-container-' + injuryId).addClass('hide').removeAttr('style');
- } else {
- $('.injury-tooltip-container').addClass('hide').removeAttr('style');
- $('#injury-tooltip-container-' + injuryId).removeClass('hide').height($('#injury-tooltip-text-' + injuryId)[0].scrollHeight + 5);
- }
- });
-
// Turns the System checkboxes into essentially a radio button. It removes any other ticks, and then checks the new system.
// Finally a submit is required as data has changed.
html.find('.selector.system').click((ev) => {
@@ -519,7 +542,7 @@ export class STAStarshipSheet2e extends ActorSheet {
Hooks.on('renderSTAStarshipSheet2e', (app, html, data) => {
const sheetId = app.id;
- const sheetElement = $(`#${sheetId} .main`);
+ const sheetElement = $(`#${sheetId} .title`);
const shipScaleValue = Number.parseInt(html.find('#scale').attr('value'));
let totalBreaches = 0;
diff --git a/src/module/chat/Collapsible.js b/src/module/apps/Collapsible.js
similarity index 100%
rename from src/module/chat/Collapsible.js
rename to src/module/apps/Collapsible.js
diff --git a/src/module/dice/STARoller.js b/src/module/apps/STARoller.js
similarity index 52%
rename from src/module/dice/STARoller.js
rename to src/module/apps/STARoller.js
index bbfa1a7..0f35392 100644
--- a/src/module/dice/STARoller.js
+++ b/src/module/apps/STARoller.js
@@ -1,69 +1,24 @@
-import {
- STARoll
-} from '../roll.js';
+$(document).on('click', '#sta-roll-task-button', (event) => {
+ console.log('Roll Task button clicked');
+ STARoller.rollTaskRoll(event);
+});
+$(document).on('click', '#sta-roll-challenge-button', (event) => {
+ console.log('Roll Challenge button clicked');
+ STARoller.rollChallengeRoll(event);
+});
+$(document).on('click', '#sta-roll-npc-button', (event) => {
+ console.log('Roll NPC button clicked');
+ STARoller.rollnpcssroll(event);
+});
+
import {
STARollDialog
} from '../apps/roll-dialog.js';
-
+import {
+ STARoll
+} from '../apps/roll.js';
export class STARoller {
- static async Init(controls, html) {
- // Create the main dice roll button
- const diceRollbtn = $(`
-
-
-
-
-
- `);
-
- // Create the task roll button
- const taskrollbtn = $(`
-
-
-
- `);
-
- // Create the challenge roll button
- const challengerollbtn = $(`
-
-
-
- `);
-
- // Create the NPC Starship & Crew roll button
- const npcssrollbtn = $(`
-
-
-
- `);
-
- // Append the nested buttons to the main button's container
- diceRollbtn.find('.nested-buttons').append(taskrollbtn).append(challengerollbtn).append(npcssrollbtn);
-
- // Append the main button to the main controls
- html.find('.main-controls').append(diceRollbtn);
-
- // Add event listener to the main button to toggle the visibility of nested buttons
- diceRollbtn.on('click', (ev) => {
- const nestedButtons = diceRollbtn.find('.nested-buttons');
- diceRollbtn.toggleClass('active');
- diceRollbtn.find('.sub-controls').toggleClass('active');
- nestedButtons.toggle();
- });
-
- taskrollbtn.on('click', (ev) => {
- this.rollTaskRoll(ev);
- });
-
- challengerollbtn.on('click', (ev) => {
- this.rollChallengeRoll(ev);
- });
-
- npcssrollbtn.on('click', (ev) => {
- this.rollnpcssroll(ev);
- });
- }
static async rollTaskRoll(event) {
const selectedAttribute = 'STARoller';
const selectedDiscipline = 'STARoller';
@@ -110,45 +65,46 @@ export class STARoller {
static async rollnpcssroll(event) {
const dialogContent = `
- `;
+
+`;
new Dialog({
title: `${game.i18n.localize('sta.roll.npcshipandcrewroll')}`,
@@ -162,21 +118,20 @@ export class STARoller {
const selectedSystem = html.find('.selector.system:checked').val();
let selectedSystemLabel = html.find(`#${selectedSystem}-selector`).siblings('.original-system-label').val();
if (selectedSystemLabel) {
- selectedSystemLabel = selectedSystemLabel.substring(26);
+ selectedSystemLabel = selectedSystemLabel.substring(26);
}
let selectedSystemValue = html.find(`#${selectedSystem}`).text();
-
+
const selectedDepartment = html.find('.selector.department:checked').val();
let selectedDepartmentLabel = html.find(`#${selectedDepartment}-selector`).siblings('.original-department-label').val();
if (selectedDepartmentLabel) {
- selectedDepartmentLabel = selectedDepartmentLabel.substring(30);
+ selectedDepartmentLabel = selectedDepartmentLabel.substring(30);
}
let selectedDepartmentValue = html.find(`#${selectedDepartment}`).text();
-
+
const numDice = parseInt(html.find('#numDice').val());
const skillLevel = html.find('input[name="skillLevel"]:checked').val();
- let attributes;
- let departments;
+ let attributes; let departments;
switch (skillLevel) {
case 'basic':
attributes = 8;
@@ -197,14 +152,14 @@ export class STARoller {
}
const complicationRange = parseInt(html.find('#complication').val());
const shipNumDice = parseInt(html.find('#shipNumDice').val());
-
+
const speakerNPC = {
type: 'npccharacter',
};
let speakerstarship = {
type: 'starship',
};
-
+
const token = canvas.tokens.controlled[0];
if (!token || (token.actor.type !== 'starship' && token.actor.type !== 'smallcraft')) {
selectedSystemLabel = 'STARoller';
@@ -217,12 +172,12 @@ export class STARoller {
type: 'sidebar',
};
}
-
+
const staRoll = new STARoll();
staRoll.performAttributeTest(numDice, true, false, false,
skillLevel, attributes, skillLevel,
departments, complicationRange, speakerNPC);
-
+
if (html.find('#shipAssist').is(':checked')) {
staRoll.performAttributeTest(shipNumDice, true, false, false,
selectedSystemLabel, selectedSystemValue, selectedDepartmentLabel,
@@ -235,64 +190,60 @@ export class STARoller {
render: (html) => {
html.find('button').addClass('dialog-button roll default');
const token = canvas.tokens.controlled[0];
-
+
// Fallback to input box in case no token is selected
if (!token || (token.actor.type !== 'starship' && token.actor.type !== 'smallcraft')) {
const systemsHtml = `
-
+ `;
}
html.find('#shipDepartments').html(departmentsHtml);
}
}).render(true);
}
}
-Hooks.on('renderSceneControls', (controls, html) => {
- /* eslint-disable-next-line new-cap */
- STARoller.Init(controls, html);
-});
diff --git a/src/module/dice/dice-so-nice.js b/src/module/apps/dice-so-nice.js
similarity index 100%
rename from src/module/dice/dice-so-nice.js
rename to src/module/apps/dice-so-nice.js
diff --git a/src/module/apps/roll-dialog.js b/src/module/apps/roll-dialog.js
index c49605d..ed96ec3 100644
--- a/src/module/apps/roll-dialog.js
+++ b/src/module/apps/roll-dialog.js
@@ -1,40 +1,40 @@
-export class STARollDialog {
- static async create(isAttribute, defaultValue, selectedAttribute) {
- let html = '';
- if (isAttribute) {
- // Grab the RollDialog HTML file/
- if (selectedAttribute === 'STARoller') {
- html = await renderTemplate('systems/sta/templates/apps/STARoller-attribute.hbs', {'defaultValue': defaultValue});
- } else {
- html = await renderTemplate('systems/sta/templates/apps/dicepool-attribute.hbs', {'defaultValue': defaultValue});
- }
- } else {
- html = await renderTemplate('systems/sta/templates/apps/dicepool-challenge.hbs', {'defaultValue': defaultValue});
- }
-
- // Create a new promise for the HTML above.
- return new Promise((resolve) => {
- let formData = null;
-
- // Create a new dialog.
- const dlg = new Dialog({
- title: game.i18n.localize('sta.apps.dicepoolwindow'),
- content: html,
- buttons: {
- roll: {
- label: game.i18n.localize('sta.apps.rolldice'),
- callback: (html) => {
- formData = new FormData(html[0].querySelector('#dice-pool-form'));
- return resolve(formData);
- }
- }
- },
- default: 'roll',
- close: () => {}
- });
-
- // Render the dialog
- dlg.render(true);
- });
- }
-}
+export class STARollDialog {
+ static async create(isAttribute, defaultValue, selectedAttribute) {
+ let html = '';
+ if (isAttribute) {
+ // Grab the RollDialog HTML file/
+ if (selectedAttribute === 'STARoller') {
+ html = await renderTemplate('systems/sta/templates/apps/STARoller-attribute.hbs', {'defaultValue': defaultValue});
+ } else {
+ html = await renderTemplate('systems/sta/templates/apps/dicepool-attribute.hbs', {'defaultValue': defaultValue});
+ }
+ } else {
+ html = await renderTemplate('systems/sta/templates/apps/dicepool-challenge.hbs', {'defaultValue': defaultValue});
+ }
+
+ // Create a new promise for the HTML above.
+ return new Promise((resolve) => {
+ let formData = null;
+
+ // Create a new dialog.
+ const dlg = new Dialog({
+ title: game.i18n.localize('sta.apps.dicepoolwindow'),
+ content: html,
+ buttons: {
+ roll: {
+ label: game.i18n.localize('sta.apps.rolldice'),
+ callback: (html) => {
+ formData = new FormData(html[0].querySelector('#dice-pool-form'));
+ return resolve(formData);
+ }
+ }
+ },
+ default: 'roll',
+ close: () => {}
+ });
+
+ // Render the dialog
+ dlg.render(true);
+ });
+ }
+}
diff --git a/src/module/roll.js b/src/module/apps/roll.js
similarity index 97%
rename from src/module/roll.js
rename to src/module/apps/roll.js
index ac930b8..0326bad 100644
--- a/src/module/roll.js
+++ b/src/module/apps/roll.js
@@ -1,566 +1,578 @@
-export class STARoll {
- async performAttributeTest(dicePool, usingFocus, usingDedicatedFocus, usingDetermination,
- selectedAttribute, selectedAttributeValue, selectedDiscipline,
- selectedDisciplineValue, complicationRange, speaker) {
- // Define some variables that we will be using later.
-
- let i;
- let result = 0;
- let diceString = '';
- let success = 0;
- let complication = 0;
- const checkTarget =
- parseInt(selectedAttributeValue) + parseInt(selectedDisciplineValue);
- const complicationMinimumValue = 20 - (complicationRange - 1);
- const doubledetermination = parseInt(selectedDisciplineValue) + parseInt(selectedDisciplineValue);
-
- // Foundry will soon make rolling async only, setting it up as such now avoids a warning.
- const r = await new Roll( dicePool + 'd20' ).evaluate( {});
-
- // Now for each dice in the dice pool we want to check what the individual result was.
- for (i = 0; i < dicePool; i++) {
- result = r.terms[0].results[i].result;
- // If the result is less than or equal to the focus, that counts as 2 successes and we want to show the dice as green.
- if ((usingFocus && result <= selectedDisciplineValue) || result == 1) {
- diceString += '
' + result + '
';
- success += 2;
- } else if ((usingDedicatedFocus && result <= doubledetermination) || result == 1) {
- diceString += '
' + result + '
';
- success += 2;
- // If the result is less than or equal to the target (the discipline and attribute added together), that counts as 1 success but we want to show the dice as normal.
- } else if (result <= checkTarget) {
- diceString += '
' + result + '
';
- success += 1;
- // If the result is greater than or equal to the complication range, then we want to count it as a complication. We also want to show it as red!
- } else if (result >= complicationMinimumValue) {
- diceString += '
' + result + '
';
- complication += 1;
- // If none of the above is true, the dice failed to do anything and is treated as normal.
- } else {
- diceString += '
' + result + '
';
- }
- }
-
- // If using a Value and Determination, automatically add in an extra critical roll
- if (usingDetermination) {
- diceString += '
' + 1 + '
';
- success += 2;
- }
-
- // Here we want to check if the success was exactly one (as "1 Successes" doesn't make grammatical sense). We create a string for the Successes.
- let successText = '';
- if (success == 1) {
- successText = success + ' ' + game.i18n.format('sta.roll.success');
- } else {
- successText = success + ' ' + game.i18n.format('sta.roll.successPlural');
- }
-
- // Check if we allow multiple complications, or if only one complication ever happens.
- const multipleComplicationsAllowed = game.settings.get('sta', 'multipleComplications');
-
- // If there is any complications, we want to crate a string for this. If we allow multiple complications and they exist, we want to pluralise this also.
- // If no complications exist then we don't even show this box.
- let complicationText = '';
- if (complication >= 1) {
- if (complication > 1 && multipleComplicationsAllowed === true) {
- const localisedPluralisation = game.i18n.format('sta.roll.complicationPlural');
- complicationText = '
`;
-
- // Check if the dice3d module exists (Dice So Nice). If it does, post a roll in that and then send to chat after the roll has finished. If not just send to chat.
- if (game.dice3d) {
- game.dice3d.showForRoll(rolledChallenge, game.user, true).then((displayed) => {
- this.sendToChat(speaker, html, undefined, rolledChallenge, flavor, '');
- });
- } else {
- this.sendToChat(speaker, html, undefined, rolledChallenge, flavor, 'sounds/dice.wav');
- };
- }
-
- async performItemRoll(item, speaker) {
- // Create variable div and populate it with localisation to use in the HTML.
- const variablePrompt = game.i18n.format('sta.roll.item.quantity');
- const variable = `
`;
-
- // Send the divs to populate a HTML template and sends to chat.
- this.genericItemTemplate(item, speaker, variable)
- .then((html)=>this.sendToChat(speaker, html, item));
- }
-
- /**
- * Parse out tag strings appropriate for a shipweapon Chat Card.
- *
- * @param {Item} item
- *
- * @return {string[]}
- * @private
- */
- _assembleShipWeaponsTags(item) {
- const LABELS = Object.freeze({
- area: 'sta.actor.belonging.weapon.area',
- calibration: 'sta.actor.belonging.weapon.calibration',
- cumbersome: 'sta.actor.belonging.weapon.cumbersome',
- dampening: 'sta.actor.belonging.weapon.dampening',
- depleting: 'sta.actor.belonging.weapon.depleting',
- devastating: 'sta.actor.belonging.weapon.devastating',
- hiddenx: 'sta.actor.belonging.weapon.hiddenx',
- highyield: 'sta.actor.belonging.weapon.highyield',
- intense: 'sta.actor.belonging.weapon.intense',
- jamming: 'sta.actor.belonging.weapon.jamming',
- persistent: 'sta.actor.belonging.weapon.persistentx',
- persistentx: 'sta.actor.belonging.weapon.persistentx',
- piercing: 'sta.actor.belonging.weapon.piercingx',
- piercingx: 'sta.actor.belonging.weapon.piercingx',
- slowing: 'sta.actor.belonging.weapon.slowing',
- spread: 'sta.actor.belonging.weapon.spread',
- versatilex: 'sta.actor.belonging.weapon.versatilex',
- viciousx: 'sta.actor.belonging.weapon.viciousx',
- });
- const tags = [];
-
- if (item.system.range) {
- tags.push(game.i18n.localize(`sta.actor.belonging.weapon.${item.system.range}`));
- }
- if (item.system.includescale) {
- tags.push(game.i18n.localize(`sta.actor.belonging.weapon.${item.system.includescale}`));
- }
-
- const qualities = item.system.qualities;
- for (const property in qualities) {
- if (!Object.hasOwn(LABELS, property) || !qualities[property]) continue;
-
- // Some qualities have tiers/ranks/numbers.
- const label = game.i18n.localize(LABELS[property]);
- const tag = Number.isInteger(qualities[property]) ? `${label} ${qualities[property]}` : label;
-
- tags.push(tag);
- }
-
- return tags;
- }
-
- /**
- * Render a generic item card.
- *
- * @param {Item} item
- * @param {Actor} speaker
- * @param {string=} variable
- * @param {Array=} tags
- * @param {object=} rolls
- *
- * @return {Promise}
- */
- async genericItemTemplate(item, speaker, variable = '', tags = [], rolls) {
- // Checks if the following are empty/undefined. If so sets to blank.
- const descField = item.system.description ? item.system.description : '';
-
- const cardData = {
- speakerId: speaker.id,
- tokenId: speaker.token ? speaker.token.uuid : null,
- itemId: item.id,
- img: item.img,
- type: game.i18n.localize(`sta.actor.belonging.${item.type}.title`),
- name: item.name,
- descFieldHtml: descField,
- tags: tags.concat(this._assembleGenericTags(item)),
- varFieldHtml: variable,
- rolls: rolls,
- };
-
- // Returns it for the sendToChat to utilise.
- return await renderTemplate('systems/sta/templates/chat/generic-item.hbs', cardData);
- }
-
- /**
- * Parse out tag strings appropriate for a general Item Chat Card.
- *
- * @param {Item} item
- *
- * @return {string[]}
- * @private
- */
- _assembleGenericTags(item) {
- const LABELS = Object.freeze({
- escalation: 'sta.item.genericitem.escalation',
- opportunity: 'sta.item.genericitem.opportunity',
- });
- const tags = [];
- for (const property in item.system) {
- if (!Object.hasOwn(LABELS, property) || !item.system[property]) continue;
-
- // Some qualities have tiers/ranks/numbers.
- const label = game.i18n.localize(LABELS[property]);
- const tag = Number.isInteger(item.system[property]) ? `${label} ${item.system[property]}` : label;
- tags.push(tag);
- }
- return tags;
- }
-
- async sendToChat(speaker, content, item, roll, flavor, sound) {
- const rollMode = game.settings.get('core', 'rollMode');
- const messageProps = {
- user: game.user.id,
- speaker: ChatMessage.getSpeaker({actor: speaker}),
- content: content,
- sound: sound,
- flags: {},
- };
-
- if (typeof item != 'undefined') {
- messageProps.flags.sta = {
- itemData: item.toObject(),
- };
- }
-
- if (typeof roll != 'undefined') {
- messageProps.roll = roll;
- }
- if (typeof flavor != 'undefined') {
- messageProps.flavor = flavor;
- }
- // Apply the roll mode to automatically adjust visibility settings
- ChatMessage.applyRollMode(messageProps, rollMode);
-
- // Send the chat message
- return await ChatMessage.create(messageProps);
- }
-}
-
-/*
- Returns the number of successes in a d6 challenge die roll
-*/
-function getSuccessesChallengeRoll( roll ) {
- let dice = roll.terms[0].results.map( ( die ) => die.result);
- dice = dice.map( ( die ) => {
- if ( die == 2 ) {
- return 2;
- } else if (die == 1 || die == 5 || die == 6) {
- return 1;
- }
- return 0;
- });
- return dice.reduce( ( a, b ) => a + b, 0);
-}
-
-/*
- Returns the number of effects in a d6 challenge die roll
-*/
-function getEffectsFromChallengeRoll( roll ) {
- let dice = roll.terms[0].results.map( ( die ) => die.result);
- dice = dice.map( ( die ) => {
- if (die>=5) {
- return 1;
- }
- return 0;
- });
- return dice.reduce( ( a, b ) => a + b, 0);
-}
-
-/*
- Creates an HTML list of die face images from the results of a challenge roll
-*/
-function getDiceImageListFromChallengeRoll( roll ) {
- let diceString = '';
- const diceFaceTable = [
- '',
- '',
- '',
- '',
- '',
- ''
- ];
- diceString = roll.terms[0].results.map( ( die ) => die.result).map( ( result ) => diceFaceTable[result - 1]).join( ' ' );
- return diceString;
-}
-
-/*
- grabs the nationalized local reference, switching to the plural form if count > 1, also, replaces |#| with count, then returns the resulting string.
-*/
-function i18nPluralize( count, localizationReference ) {
- if ( count > 1 ) {
- return game.i18n.format( localizationReference + 'Plural' ).replace('|#|', count);
- }
- return game.i18n.format( localizationReference ).replace('|#|', count);
-}
+export class STARoll {
+ async performAttributeTest(dicePool, usingFocus, usingDedicatedFocus, usingDetermination,
+ selectedAttribute, selectedAttributeValue, selectedDiscipline,
+ selectedDisciplineValue, complicationRange, speaker) {
+ // Define some variables that we will be using later.
+
+ let i;
+ let result = 0;
+ let diceString = '';
+ let success = 0;
+ let complication = 0;
+ const checkTarget =
+ parseInt(selectedAttributeValue) + parseInt(selectedDisciplineValue);
+ const complicationMinimumValue = 20 - (complicationRange - 1);
+ const doubledetermination = parseInt(selectedDisciplineValue) + parseInt(selectedDisciplineValue);
+
+ // Foundry will soon make rolling async only, setting it up as such now avoids a warning.
+ const r = await new Roll( dicePool + 'd20' ).evaluate( {});
+
+ // Now for each dice in the dice pool we want to check what the individual result was.
+ for (i = 0; i < dicePool; i++) {
+ result = r.terms[0].results[i].result;
+ // If the result is less than or equal to the focus, that counts as 2 successes and we want to show the dice as green.
+ if ((usingFocus && result <= selectedDisciplineValue) || result == 1) {
+ diceString += '
' + result + '
';
+ success += 2;
+ } else if ((usingDedicatedFocus && result <= doubledetermination) || result == 1) {
+ diceString += '
' + result + '
';
+ success += 2;
+ // If the result is less than or equal to the target (the discipline and attribute added together), that counts as 1 success but we want to show the dice as normal.
+ } else if (result <= checkTarget) {
+ diceString += '
' + result + '
';
+ success += 1;
+ // If the result is greater than or equal to the complication range, then we want to count it as a complication. We also want to show it as red!
+ } else if (result >= complicationMinimumValue) {
+ diceString += '
' + result + '
';
+ complication += 1;
+ // If none of the above is true, the dice failed to do anything and is treated as normal.
+ } else {
+ diceString += '
' + result + '
';
+ }
+ }
+
+ // If using a Value and Determination, automatically add in an extra critical roll
+ if (usingDetermination) {
+ diceString += '
' + 1 + '
';
+ success += 2;
+ }
+
+ // Here we want to check if the success was exactly one (as "1 Successes" doesn't make grammatical sense). We create a string for the Successes.
+ let successText = '';
+ if (success == 1) {
+ successText = success + ' ' + game.i18n.format('sta.roll.success');
+ } else {
+ successText = success + ' ' + game.i18n.format('sta.roll.successPlural');
+ }
+
+ // Check if we allow multiple complications, or if only one complication ever happens.
+ const multipleComplicationsAllowed = game.settings.get('sta', 'multipleComplications');
+
+ // If there is any complications, we want to crate a string for this. If we allow multiple complications and they exist, we want to pluralise this also.
+ // If no complications exist then we don't even show this box.
+ let complicationText = '';
+ if (complication >= 1) {
+ if (complication > 1 && multipleComplicationsAllowed === true) {
+ const localisedPluralisation = game.i18n.format('sta.roll.complicationPlural');
+ complicationText = '
`;
+
+ // Check if the dice3d module exists (Dice So Nice). If it does, post a roll in that and then send to chat after the roll has finished. If not just send to chat.
+ if (game.dice3d) {
+ game.dice3d.showForRoll(rolledChallenge, game.user, true).then((displayed) => {
+ this.sendToChat(speaker, html, undefined, rolledChallenge, flavor, '');
+ });
+ } else {
+ this.sendToChat(speaker, html, undefined, rolledChallenge, flavor, 'sounds/dice.wav');
+ };
+ }
+
+ async performItemRoll(item, speaker) {
+ // Create variable div and populate it with localisation to use in the HTML.
+ const variablePrompt = game.i18n.format('sta.roll.item.quantity');
+ const variable = `