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 + + + +Created with Fabric.js 5.2.4 + + + + + + + + + + + + + + + + \ 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 = ` +
+
+
${game.i18n.localize('sta.roll.positiveinfluences')}
+ +
+
+
${game.i18n.localize('sta.roll.negativeinfluences')}
+ +
+
+ `; + + 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 = ` -
-
-
-
- -
-
-
- -
-
-
`; +
+
+
${game.i18n.localize('sta.roll.positiveinfluences')}
+ +
+
+
${game.i18n.localize('sta.roll.negativeinfluences')}
+ +
+
+ `; 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 = ` -
    -

    ${game.i18n.localize('sta.roll.npccrew')}

    -
    -
    -
    -
    - -
    -
    - - -
    -

    ${game.i18n.localize('sta.roll.npcship')}

    -
    - -
    -
    - - -
    -
    - - - - - -
    -

    ${game.i18n.localize('sta.actor.starship.system.name')}

    -
    -
    -

    ${game.i18n.localize('sta.actor.starship.department.name')}

    -
    -
    -
    -
    - - -
    -
    `; +
    +

    ${game.i18n.localize('sta.roll.npccrew')}

    +
    +
    +
    +
    + +
    +
    + + +
    +

    ${game.i18n.localize('sta.roll.npcship')}

    +
    + +
    +
    + + +
    +
    + + + + + +
    +

    ${game.i18n.localize('sta.actor.starship.system.title')}

    +
    +
    +

    ${game.i18n.localize('sta.actor.starship.department.title')}

    +
    +
    +
    +
    + + +
    +
    +`; 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('#shipSystems').html(systemsHtml); const departmentsHtml = ` -
    - -
    - `; +
    + +
    + `; html.find('#shipDepartments').html(departmentsHtml); - + return; } - + const actor = token.actor; - + // Populate ship systems let systemsHtml = ''; for (const [key, system] of Object.entries(actor.system.systems)) { const systemLabel = game.i18n.localize(system.label); - + systemsHtml += ` -
    - - - ${system.value} - -
    - `; +
    + + + ${system.value} + +
    + `; } html.find('#shipSystems').html(systemsHtml); - + // Populate ship departments let departmentsHtml = ''; for (const [key, department] of Object.entries(actor.system.departments)) { const departmentLabel = game.i18n.localize(department.label); - + departmentsHtml += ` -
    - - - ${department.value} - -
    - `; +
    + + + ${department.value} + +
    + `; } 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 = '

    ' + localisedPluralisation.replace('|#|', complication) + '

    '; - } else { - complicationText = '

    ' + game.i18n.format('sta.roll.complication') + '

    '; - } - } else { - complicationText = ''; - } - - // Set the flavour to "[Attribute] [Discipline] Attribute Test". This shows the chat what type of test occured. - let flavor = ''; - switch (speaker.type) { - case 'character': - flavor = game.i18n.format('sta.actor.character.attribute.' + selectedAttribute) + ' ' + game.i18n.format('sta.actor.character.discipline.' + selectedDiscipline) + ' ' + game.i18n.format('sta.roll.task.name'); - break; - case 'starship': - flavor = game.i18n.format('sta.actor.starship.system.' + selectedAttribute) + ' ' + game.i18n.format('sta.actor.starship.department.' + selectedDiscipline) + ' ' + game.i18n.format('sta.roll.task.name'); - break; - case 'sidebar': - flavor = game.i18n.format('sta.apps.staroller') + ' ' + game.i18n.format('sta.roll.task.name'); - break; - case 'npccharacter': - flavor = game.i18n.format('sta.roll.npccrew' + selectedAttribute) + ' ' + game.i18n.format('sta.roll.npccrew') + ' ' + game.i18n.format('sta.roll.task.name'); - } - - const chatData = { - speakerId: speaker.id, - tokenId: speaker.token ? speaker.token.uuid : null, - dicePool, - checkTarget, - complicationMinimumValue: complicationMinimumValue + '+', - diceHtml: diceString, - complicationHtml: complicationText, - successText, - selectedAttribute, - selectedAttributeValue, - selectedDiscipline, - selectedDisciplineValue, - }; - const html = await renderTemplate('systems/sta/templates/chat/attribute-test.hbs', chatData); - - // 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(r, game.user, true).then((displayed) => { - this.sendToChat(speaker, html, undefined, r, flavor, ''); - }); - } else { - this.sendToChat(speaker, html, undefined, r, flavor, 'sounds/dice.wav'); - }; - } - - async performChallengeRoll(dicePool, challengeName, speaker = null) { - // Foundry will soon make rolling async only, setting it up as such now avoids a warning. - const rolledChallenge = await new Roll( dicePool + 'd6' ).evaluate( {}); - - const flavor = challengeName + ' ' + game.i18n.format('sta.roll.challenge.name'); - const successes = getSuccessesChallengeRoll( rolledChallenge ); - const effects = getEffectsFromChallengeRoll( rolledChallenge ); - const diceString = getDiceImageListFromChallengeRoll( rolledChallenge ); - - // pluralize success string - let successText = ''; - successText = successes + ' ' + i18nPluralize( successes, 'sta.roll.success' ); - - // pluralize effect string - let effectText = ''; - if (effects >= 1) { - effectText = '

    ' + i18nPluralize( effects, 'sta.roll.effect' ) + '

    '; - } - - const chatData = { - speakerId: speaker && speaker.id, - tokenId: speaker && speaker.token ? speaker.token.uuid : null, - dicePool, - diceHtml: diceString, - successText, - effectHtml: effectText, - }; - const html = - `
    - ${await renderTemplate('systems/sta/templates/chat/parts/challenge-roll.hbs', chatData)} -
    `; - - // 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 = `
    `+variablePrompt.replace('|#|', item.system.quantity)+`
    `; - - // Send the divs to populate a HTML template and sends to chat. - this.genericItemTemplate(item, speaker, variable) - .then((html)=>this.sendToChat(speaker, html, item)); - } - - async performTalentRoll(item, speaker) { - // Send the divs to populate a HTML template and sends to chat. - this.genericItemTemplate(item, speaker) - .then((html)=>this.sendToChat(speaker, html, item)); - } - - async performFocusRoll(item, speaker) { - // Send the divs to populate a HTML template and sends to chat. - this.genericItemTemplate(item, speaker) - .then((html)=>this.sendToChat(speaker, html, item)); - } - - async performValueRoll(item, speaker) { - // Send the divs to populate a HTML template and sends to chat. - this.genericItemTemplate(item, speaker) - .then((html)=>this.sendToChat(speaker, html, item)); - } - - async performInjuryRoll(item, speaker) { - // Send the divs to populate a HTML template and sends to chat. - this.genericItemTemplate(item, speaker) - .then((html)=>this.sendToChat(speaker, html, item)); - } - - async performWeaponRoll(item, speaker) { - let actorSecurity = 0; - if ( speaker.system.disciplines ) { - actorSecurity = parseInt( speaker.system.disciplines.security.value ); - } else if ( speaker.system.departments ) { - actorSecurity = parseInt( speaker.system.departments.security.value ); - } - let scaleDamage = 0; - if ( item.system.includescale && speaker.system.scale ) scaleDamage = parseInt( speaker.system.scale ); - const calculatedDamage = item.system.damage + actorSecurity + scaleDamage; - // Create variable div and populate it with localisation to use in the HTML. - let variablePrompt = game.i18n.format('sta.roll.weapon.damagePlural'); - if ( calculatedDamage == 1 ) { - variablePrompt = game.i18n.format('sta.roll.weapon.damage'); - } - const variable = `
    `+variablePrompt.replace('|#|', calculatedDamage)+`
    `; - - const tags = item.type === 'characterweapon' ? - this._assembleCharacterWeaponTags(item) : - this._assembleShipWeaponsTags(item); - - const damageRoll = await new Roll( calculatedDamage + 'd6' ).evaluate( {}); - const successes = getSuccessesChallengeRoll( damageRoll ); - const effects = getEffectsFromChallengeRoll( damageRoll ); - const diceString = getDiceImageListFromChallengeRoll( damageRoll ); - - // pluralize success string - let successText = ''; - successText = successes + ' ' + i18nPluralize( successes, 'sta.roll.success' ); - - // pluralize effect string - let effectText = ''; - if (effects >= 1) { - effectText = '

    ' + i18nPluralize( effects, 'sta.roll.effect' ) + '

    '; - } - - const rolls = { - challenge: { - diceHtml: diceString, - effectHtml: effectText, - successText, - } - }; - - const flags = { - sta: { - itemData: item.toObject(), - } - }; - - // Send the divs to populate a HTML template and sends to chat. - // 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. - this.genericItemTemplate(item, speaker, variable, tags, rolls).then( ( genericItemHTML ) => { - const finalHTML = genericItemHTML; - if (game.dice3d) { - game.dice3d.showForRoll(damageRoll, game.user, true).then( ()=> { - this.sendToChat( speaker, finalHTML, item, damageRoll, item.name, ''); - }); - } else { - this.sendToChat( speaker, finalHTML, item, damageRoll, item.name, 'sounds/dice.wav'); - } - }); - // if (game.dice3d) { - // game.dice3d.showForRoll(damageRoll).then((displayed) => { - // this.genericItemTemplate(item.img, item.name, item.system.description, variable, tags) - // .then((html)=>this.sendToChat(speaker, html, damageRoll, item.name, 'sounds/dice.wav')); - // }); - // } else { - // this.genericItemTemplate(item.img, item.name, item.system.description, variable, tags) - // .then((html)=>this.sendToChat(speaker, html, damageRoll, item.name, 'sounds/dice.wav')); - // } - } - - /** - * Parse out tag strings appropriate for a characterweapon Chat Card. - * - * @param {Item} item - * - * @return {string[]} - * @private - */ - _assembleCharacterWeaponTags(item) { - const LABELS = Object.freeze({ - melee: 'sta.actor.belonging.weapon.melee', - ranged: 'sta.actor.belonging.weapon.ranged', - area: 'sta.actor.belonging.weapon.area', - intense: 'sta.actor.belonging.weapon.intense', - knockdown: 'sta.actor.belonging.weapon.knockdown', - accurate: 'sta.actor.belonging.weapon.accurate', - charge: 'sta.actor.belonging.weapon.charge', - cumbersome: 'sta.actor.belonging.weapon.cumbersome', - deadly: 'sta.actor.belonging.weapon.deadly', - debilitating: 'sta.actor.belonging.weapon.debilitating', - grenade: 'sta.actor.belonging.weapon.grenade', - inaccurate: 'sta.actor.belonging.weapon.inaccurate', - nonlethal: 'sta.actor.belonging.weapon.nonlethal', - hiddenx: 'sta.actor.belonging.weapon.hiddenx', - persistentx: 'sta.actor.belonging.weapon.persistentx', - piercingx: 'sta.actor.belonging.weapon.piercingx', - viciousx: 'sta.actor.belonging.weapon.viciousx', - severity: 'sta.item.genericitem.severity', - stun: 'sta.actor.belonging.weapon.stun', - // 2E update introduced these duplicate Escalation and Opportunity qualities to this system, so we're doing those tags here. - escalation: 'sta.item.genericitem.escalation', - opportunity: 'sta.item.genericitem.opportunity', - }); - - const tags = []; - 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); - } - - // Hands are a special case. - if (item.system.hands) { - tags.push(`${item.system.hands} ${game.i18n.localize('sta.item.genericitem.handed')}`); - } - - return tags; - } - - async performWeaponRoll2e(item, speaker) { - // Create variable div and populate it with localisation to use in the HTML. - const variablePrompt = game.i18n.format('sta.roll.weapon.damage2e'); - const variable = `
    `+variablePrompt.replace('|#|', item.system.damage)+`
    `; - - const tags = this._assembleCharacterWeaponTags(item); - - // Send the divs to populate a HTML template and sends to chat. - this.genericItemTemplate(item, speaker, variable, tags) - .then((html)=>this.sendToChat(speaker, html, item)); - } - - async performStarshipWeaponRoll2e(item, speaker) { - let actorWeapons = 0; - if (speaker.system.systems.weapons.value > 6) actorWeapons = 1; - if (speaker.system.systems.weapons.value > 8) actorWeapons = 2; - if (speaker.system.systems.weapons.value > 10) actorWeapons = 3; - if (speaker.system.systems.weapons.value > 12) actorWeapons = 4; - - let scaleDamage = 0; - if (item.system.includescale == 'energy') scaleDamage = parseInt( speaker.system.scale ); - - const calculatedDamage = item.system.damage + actorWeapons + scaleDamage; - - const variablePrompt = game.i18n.format('sta.roll.weapon.damage2e'); - const variable = `
    `+variablePrompt.replace('|#|', calculatedDamage)+`
    `; - - const tags = this._assembleShipWeaponsTags(item); - - const flags = { - sta: { - itemData: item.toObject(), - } - }; - - // Send the divs to populate a HTML template and sends to chat. - this.genericItemTemplate(item, speaker, variable, tags) - .then((html)=>this.sendToChat(speaker, html, item)); - } - - async performArmorRoll(item, speaker) { - // Create variable div and populate it with localisation to use in the HTML. - const variablePrompt = game.i18n.format('sta.roll.armor.protect'); - const variable = `
    `+variablePrompt.replace('|#|', item.system.protection)+`
    `; - - // 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 = '

    ' + localisedPluralisation.replace('|#|', complication) + '

    '; + } else { + complicationText = '

    ' + game.i18n.format('sta.roll.complication') + '

    '; + } + } else { + complicationText = ''; + } + + // Set the flavour to "[Attribute] [Discipline] Attribute Test". This shows the chat what type of test occured. + let flavor = ''; + switch (speaker.type) { + case 'character': + flavor = game.i18n.format('sta.actor.character.attribute.' + selectedAttribute) + ' ' + game.i18n.format('sta.actor.character.discipline.' + selectedDiscipline) + ' ' + game.i18n.format('sta.roll.task.name'); + break; + case 'starship': + flavor = game.i18n.format('sta.actor.starship.system.' + selectedAttribute) + ' ' + game.i18n.format('sta.actor.starship.department.' + selectedDiscipline) + ' ' + game.i18n.format('sta.roll.task.name'); + break; + case 'sidebar': + flavor = game.i18n.format('sta.apps.staroller') + ' ' + game.i18n.format('sta.roll.task.name'); + break; + case 'npccharacter': + flavor = game.i18n.format('sta.roll.npccrew' + selectedAttribute) + ' ' + game.i18n.format('sta.roll.npccrew') + ' ' + game.i18n.format('sta.roll.task.name'); + } + + const chatData = { + speakerId: speaker.id, + tokenId: speaker.token ? speaker.token.uuid : null, + dicePool, + checkTarget, + complicationMinimumValue: complicationMinimumValue + '+', + diceHtml: diceString, + complicationHtml: complicationText, + successText, + selectedAttribute, + selectedAttributeValue, + selectedDiscipline, + selectedDisciplineValue, + }; + const html = await renderTemplate('systems/sta/templates/chat/attribute-test.hbs', chatData); + + // 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(r, game.user, true).then((displayed) => { + this.sendToChat(speaker, html, undefined, r, flavor, ''); + }); + } else { + this.sendToChat(speaker, html, undefined, r, flavor, 'sounds/dice.wav'); + }; + } + + async performChallengeRoll(dicePool, challengeName, speaker = null) { + // Foundry will soon make rolling async only, setting it up as such now avoids a warning. + const rolledChallenge = await new Roll( dicePool + 'd6' ).evaluate( {}); + + const flavor = challengeName + ' ' + game.i18n.format('sta.roll.challenge.name'); + const successes = getSuccessesChallengeRoll( rolledChallenge ); + const effects = getEffectsFromChallengeRoll( rolledChallenge ); + const diceString = getDiceImageListFromChallengeRoll( rolledChallenge ); + + // pluralize success string + let successText = ''; + successText = successes + ' ' + i18nPluralize( successes, 'sta.roll.success' ); + + // pluralize effect string + let effectText = ''; + if (effects >= 1) { + effectText = '

    ' + i18nPluralize( effects, 'sta.roll.effect' ) + '

    '; + } + + const chatData = { + speakerId: speaker && speaker.id, + tokenId: speaker && speaker.token ? speaker.token.uuid : null, + dicePool, + diceHtml: diceString, + successText, + effectHtml: effectText, + }; + const html = + `
    + ${await renderTemplate('systems/sta/templates/chat/challenge-roll.hbs', chatData)} +
    `; + + // 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 = `
    `+variablePrompt.replace('|#|', item.system.quantity)+`
    `; + + // Send the divs to populate a HTML template and sends to chat. + this.genericItemTemplate(item, speaker, variable) + .then((html)=>this.sendToChat(speaker, html, item)); + } + + async performTalentRoll(item, speaker) { + // Send the divs to populate a HTML template and sends to chat. + this.genericItemTemplate(item, speaker) + .then((html)=>this.sendToChat(speaker, html, item)); + } + + async performFocusRoll(item, speaker) { + // Send the divs to populate a HTML template and sends to chat. + this.genericItemTemplate(item, speaker) + .then((html)=>this.sendToChat(speaker, html, item)); + } + + async performValueRoll(item, speaker) { + // Send the divs to populate a HTML template and sends to chat. + this.genericItemTemplate(item, speaker) + .then((html)=>this.sendToChat(speaker, html, item)); + } + + async performInjuryRoll(item, speaker) { + // Send the divs to populate a HTML template and sends to chat. + this.genericItemTemplate(item, speaker) + .then((html)=>this.sendToChat(speaker, html, item)); + } + + async performTraitRoll(item, speaker) { + // Send the divs to populate a HTML template and sends to chat. + this.genericItemTemplate(item, speaker) + .then((html)=>this.sendToChat(speaker, html, item)); + } + + async performMilestoneRoll(item, speaker) { + // Send the divs to populate a HTML template and sends to chat. + this.genericItemTemplate(item, speaker) + .then((html)=>this.sendToChat(speaker, html, item)); + } + + async performWeaponRoll(item, speaker) { + let actorSecurity = 0; + if ( speaker.system.disciplines ) { + actorSecurity = parseInt( speaker.system.disciplines.security.value ); + } else if ( speaker.system.departments ) { + actorSecurity = parseInt( speaker.system.departments.security.value ); + } + let scaleDamage = 0; + if ( item.system.includescale && speaker.system.scale ) scaleDamage = parseInt( speaker.system.scale ); + const calculatedDamage = item.system.damage + actorSecurity + scaleDamage; + // Create variable div and populate it with localisation to use in the HTML. + let variablePrompt = game.i18n.format('sta.roll.weapon.damagePlural'); + if ( calculatedDamage == 1 ) { + variablePrompt = game.i18n.format('sta.roll.weapon.damage'); + } + const variable = `
    `+variablePrompt.replace('|#|', calculatedDamage)+`
    `; + + const tags = item.type === 'characterweapon' ? + this._assembleCharacterWeaponTags(item) : + this._assembleShipWeaponsTags(item); + + const damageRoll = await new Roll( calculatedDamage + 'd6' ).evaluate( {}); + const successes = getSuccessesChallengeRoll( damageRoll ); + const effects = getEffectsFromChallengeRoll( damageRoll ); + const diceString = getDiceImageListFromChallengeRoll( damageRoll ); + + // pluralize success string + let successText = ''; + successText = successes + ' ' + i18nPluralize( successes, 'sta.roll.success' ); + + // pluralize effect string + let effectText = ''; + if (effects >= 1) { + effectText = '

    ' + i18nPluralize( effects, 'sta.roll.effect' ) + '

    '; + } + + const rolls = { + challenge: { + diceHtml: diceString, + effectHtml: effectText, + successText, + } + }; + + const flags = { + sta: { + itemData: item.toObject(), + } + }; + + // Send the divs to populate a HTML template and sends to chat. + // 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. + this.genericItemTemplate(item, speaker, variable, tags, rolls).then( ( genericItemHTML ) => { + const finalHTML = genericItemHTML; + if (game.dice3d) { + game.dice3d.showForRoll(damageRoll, game.user, true).then( ()=> { + this.sendToChat( speaker, finalHTML, item, damageRoll, item.name, ''); + }); + } else { + this.sendToChat( speaker, finalHTML, item, damageRoll, item.name, 'sounds/dice.wav'); + } + }); + // if (game.dice3d) { + // game.dice3d.showForRoll(damageRoll).then((displayed) => { + // this.genericItemTemplate(item.img, item.name, item.system.description, variable, tags) + // .then((html)=>this.sendToChat(speaker, html, damageRoll, item.name, 'sounds/dice.wav')); + // }); + // } else { + // this.genericItemTemplate(item.img, item.name, item.system.description, variable, tags) + // .then((html)=>this.sendToChat(speaker, html, damageRoll, item.name, 'sounds/dice.wav')); + // } + } + + /** + * Parse out tag strings appropriate for a characterweapon Chat Card. + * + * @param {Item} item + * + * @return {string[]} + * @private + */ + _assembleCharacterWeaponTags(item) { + const LABELS = Object.freeze({ + melee: 'sta.actor.belonging.weapon.melee', + ranged: 'sta.actor.belonging.weapon.ranged', + area: 'sta.actor.belonging.weapon.area', + intense: 'sta.actor.belonging.weapon.intense', + knockdown: 'sta.actor.belonging.weapon.knockdown', + accurate: 'sta.actor.belonging.weapon.accurate', + charge: 'sta.actor.belonging.weapon.charge', + cumbersome: 'sta.actor.belonging.weapon.cumbersome', + deadly: 'sta.actor.belonging.weapon.deadly', + debilitating: 'sta.actor.belonging.weapon.debilitating', + grenade: 'sta.actor.belonging.weapon.grenade', + inaccurate: 'sta.actor.belonging.weapon.inaccurate', + nonlethal: 'sta.actor.belonging.weapon.nonlethal', + hiddenx: 'sta.actor.belonging.weapon.hiddenx', + persistentx: 'sta.actor.belonging.weapon.persistentx', + piercingx: 'sta.actor.belonging.weapon.piercingx', + viciousx: 'sta.actor.belonging.weapon.viciousx', + severity: 'sta.item.genericitem.severity', + stun: 'sta.actor.belonging.weapon.stun', + // 2E update introduced these duplicate Escalation and Opportunity qualities to this system, so we're doing those tags here. + escalation: 'sta.item.genericitem.escalation', + opportunity: 'sta.item.genericitem.opportunity', + }); + + const tags = []; + 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); + } + + // Hands are a special case. + if (item.system.hands) { + tags.push(`${item.system.hands} ${game.i18n.localize('sta.item.genericitem.handed')}`); + } + + return tags; + } + + async performWeaponRoll2e(item, speaker) { + // Create variable div and populate it with localisation to use in the HTML. + const variablePrompt = game.i18n.format('sta.roll.weapon.damage2e'); + const variable = `
    `+variablePrompt.replace('|#|', item.system.damage)+`
    `; + + const tags = this._assembleCharacterWeaponTags(item); + + // Send the divs to populate a HTML template and sends to chat. + this.genericItemTemplate(item, speaker, variable, tags) + .then((html)=>this.sendToChat(speaker, html, item)); + } + + async performStarshipWeaponRoll2e(item, speaker) { + let actorWeapons = 0; + if (speaker.system.systems.weapons.value > 6) actorWeapons = 1; + if (speaker.system.systems.weapons.value > 8) actorWeapons = 2; + if (speaker.system.systems.weapons.value > 10) actorWeapons = 3; + if (speaker.system.systems.weapons.value > 12) actorWeapons = 4; + + let scaleDamage = 0; + if (item.system.includescale == 'energy') scaleDamage = parseInt( speaker.system.scale ); + + const calculatedDamage = item.system.damage + actorWeapons + scaleDamage; + + const variablePrompt = game.i18n.format('sta.roll.weapon.damage2e'); + const variable = `
    `+variablePrompt.replace('|#|', calculatedDamage)+`
    `; + + const tags = this._assembleShipWeaponsTags(item); + + const flags = { + sta: { + itemData: item.toObject(), + } + }; + + // Send the divs to populate a HTML template and sends to chat. + this.genericItemTemplate(item, speaker, variable, tags) + .then((html)=>this.sendToChat(speaker, html, item)); + } + + async performArmorRoll(item, speaker) { + // Create variable div and populate it with localisation to use in the HTML. + const variablePrompt = game.i18n.format('sta.roll.armor.protect'); + const variable = `
    `+variablePrompt.replace('|#|', item.system.protection)+`
    `; + + // 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); +} diff --git a/src/module/apps/tracker.js b/src/module/apps/tracker.js index 40557ee..801003e 100644 --- a/src/module/apps/tracker.js +++ b/src/module/apps/tracker.js @@ -1,11 +1,5 @@ -/** TODO: Reformat this file so that it aligns with ES2020 constraints or update - the ESLint config to allow for ES2022. Currently cannot be parsed by ES2020. - - If code changes are being here made, temporarily switching to ES2022 within - the eslint.config.mjs on your local machine is advised to catch mistakes. - */ - export class STATracker extends Application { + constructor(options = {}) { super(options); } @@ -24,156 +18,59 @@ export class STATracker extends Application { async _render(force = false, options = {}) { await super._render(force, options); - // This is happening in _render() as opposed to render() because this has to happen after elements have been generated. this._updateRenderedPosition(); } _updateRenderedPosition() { - /** @type HTMLElement */ const tracker = this.element[0]; if (!tracker) return; - /** @type CameraViews */ const cameraViews = ui.webrtc; const CSS_CLASSES = { - DOCK_RIGHT: 'av-right', DOCK_BOTTOM: 'av-bottom', - DOCK_COLLAPSE: 'av-collapse', }; - /** @type HTMLElement */ const element = cameraViews.element[0]; - // If A/V is disabled, it is currently using an unrendered