diff --git a/.prettierrc b/.prettierrc index 592d302e..4ca0a625 100644 --- a/.prettierrc +++ b/.prettierrc @@ -11,4 +11,4 @@ "jsxBracketSameLine": false, "arrowParens": "always", "endOfLine": "lf" -} +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 7a73a41b..cac0e10e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,2 +1,3 @@ { + "editor.formatOnSave": true } \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js index dabc0757..d18fc7d3 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -8,19 +8,19 @@ const less = require("gulp-less"); const SW5E_LESS = ["less/**/*.less"]; function compileLESS() { - return gulp.src("less/original/sw5e.less").pipe(less()).pipe(gulp.dest("./")); + return gulp.src("less/original/sw5e.less").pipe(less()).pipe(gulp.dest("./")); } function compileGlobalLess() { - return gulp.src("less/update/sw5e-global.less").pipe(less()).pipe(gulp.dest("./")); + return gulp.src("less/update/sw5e-global.less").pipe(less()).pipe(gulp.dest("./")); } function compileLightLess() { - return gulp.src("less/update/sw5e-light.less").pipe(less()).pipe(gulp.dest("./")); + return gulp.src("less/update/sw5e-light.less").pipe(less()).pipe(gulp.dest("./")); } function compileDarkLess() { - return gulp.src("less/update/sw5e-dark.less").pipe(less()).pipe(gulp.dest("./")); + return gulp.src("less/update/sw5e-dark.less").pipe(less()).pipe(gulp.dest("./")); } const css = gulp.series(compileLESS, compileGlobalLess, compileLightLess, compileDarkLess); @@ -30,7 +30,7 @@ const css = gulp.series(compileLESS, compileGlobalLess, compileLightLess, compil /* ----------------------------------------- */ function watchUpdates() { - gulp.watch(SW5E_LESS, css); + gulp.watch(SW5E_LESS, css); } /* ----------------------------------------- */ diff --git a/lang/en.json b/lang/en.json index e3242844..1decdafc 100644 --- a/lang/en.json +++ b/lang/en.json @@ -19,11 +19,10 @@ "ITEM.TypeLoot": "Loot", "ITEM.TypePower": "Power", "ITEM.TypeSpecies": "Species", - "ITEM.TypeStarship": "Starship", - "ITEM.TypeStarshipfeature": "Starship Feature", - "ITEM.TypeStarshipfeaturePl": "Starship Features", - "ITEM.TypeStarshipmod": "Starship Modification", - "ITEM.TypeStarshipmodPl": "Starship Modifications", + "ITEM.TypeStarshipfeature": "Starship Feature", + "ITEM.TypeStarshipfeaturePl": "Starship Features", + "ITEM.TypeStarshipmod": "Starship Modification", + "ITEM.TypeStarshipmodPl": "Starship Modifications", "ITEM.TypeTool": "Tool", "ITEM.TypeVenture": "Venture", "ITEM.TypeWeapon": "Weapon", @@ -194,7 +193,7 @@ "SW5E.BonusSaveForm": "Update Bonuses", "SW5E.BonusTechPowerDC": "Global Tech Power DC Bonus", "SW5E.BonusTitle": "Configure Actor Bonuses", - "SW5E.BurnFuel": "Burn", + "SW5E.BurnFuel": "Burn", "SW5E.CapacityMultiplier": "Capacity Multiplier", "SW5E.CentStorageCapacity": "Central Storage Capacity", "SW5E.ChallengeRating": "Challenge Rating", @@ -331,18 +330,8 @@ "SW5E.DeathSavingThrow": "Death Saving Throw", "SW5E.Default": "Default", "SW5E.DefaultAbilityCheck": "Default Ability Check", - "SW5E.Deployment": "Deployment", - "SW5E.DeploymentAcceptSettings": "Deploy Actor", - "SW5E.DeploymentPl": "Deployments", - "SW5E.DeploymentPromptTitle": "Deploying Actor", - "SW5E.DeploymentTypeCoordinator": "Coordinator", - "SW5E.DeploymentTypeCrew": "Crew", - "SW5E.DeploymentTypeGunner": "Gunner", - "SW5E.DeploymentTypeMechanic": "Mechanic", - "SW5E.DeploymentTypeOperator": "Operator", - "SW5E.DeploymentTypePilot": "Pilot", - "SW5E.DeploymentTypePassenger" : "Passenger", - "SW5E.DeploymentTypeTechnician": "Technician", + "SW5E.Deployment": "Deployment", + "SW5E.DeploymentPl": "Deployments", "SW5E.description": "A comprehensive game system for running games of Star Wars 5th Edition in the Foundry VTT environment.", "SW5E.Description": "Description", "SW5E.DestructionSave": "Destruction Saves", @@ -532,13 +521,12 @@ "SW5E.Flaws": "Flaws", "SW5E.ForcePowerbook": "Force Powers", "SW5E.Formula": "Formula", - "SW5E.FuelCapacity": "Fuel Capacity", + "SW5E.FuelCapacity": "Fuel Capacity", + "SW5E.FuelCostPerUnit": "Fuel Cost per Unit", "SW5E.FuelCostsMod": "Fuel Costs Modifier", - "SW5E.FuelCostPerUnit": "Fuel Cost per Unit", "SW5E.GrantedAbilities": "Granted Abilities", "SW5E.HalfProficient": "Half Proficient", "SW5E.HardpointSizeMod": "Hardpoint Size Modifier", - "SW5E.HardpointsPerRound": "Max Hardpoints Fired Per Round", "SW5E.Healing": "Healing", "SW5E.HealingTemp": "Healing (Temporary)", "SW5E.Health": "Health", @@ -554,12 +542,9 @@ "SW5E.HitDiceWarn": "{name} has no available {formula} Hit Dice remaining!", "SW5E.HP": "Health", "SW5E.HPFormula": "Health Formula", - "SW5E.HullDice": "Hull Dice", - "SW5E.HullDiceRoll": "Roll Hull Dice", - "SW5E.HullDiceUsed": "Hull Dice Used", - "SW5E.HullDiceWarn": "{name} has no available {formula} Hull Dice remaining!", - "SW5E.HullPoints": "Hull Points", - "SW5E.HullPointsFormula": "Hull Points Formula", + "SW5E.HullDice": "Hull Dice", + "SW5E.HullPoints": "Hull Points", + "SW5E.HullPointsFormula": "Hull Points Formula", "SW5E.HyperdriveClass": "Hyperdrive Class", "SW5E.Ideals": "Ideals", "SW5E.Identified": "Identified", @@ -633,9 +618,8 @@ "SW5E.ItemTypePowerPl": "Powers", "SW5E.ItemTypeSpecies": "Species", "SW5E.ItemTypeSpeciesPl": "Species", - "SW5E.ItemTypeStarship": "Starship", - "SW5E.ItemTypeStarshipMod": "Starship Modification", - "SW5E.ItemTypeStarshipModPl": "Starship Modifications", + "SW5E.ItemTypeStarshipMod": "Starship Modification", + "SW5E.ItemTypeStarshipModPl": "Starship Modifications", "SW5E.ItemTypeTool": "Tool", "SW5E.ItemTypeToolPl": "Tools", "SW5E.ItemTypeVenture": "Venture", @@ -802,8 +786,8 @@ "SW5E.MovementCrawl": "Crawl", "SW5E.MovementFly": "Fly", "SW5E.MovementHover": "Hover", - "SW5E.MovementRoll": "Roll", - "SW5E.MovementSpace": "Space Flight", + "SW5E.MovementRoll": "Roll", + "SW5E.MovementSpace": "Space Flight", "SW5E.MovementSwim": "Swim", "SW5E.MovementTurn": "Turning", "SW5E.MovementUnits": "Units", @@ -863,10 +847,10 @@ "SW5E.PowerCreate": "Create Power", "SW5E.PowerDC": "Power DC", "SW5E.PowerDetails": "Power Details", - "SW5E.PowerDie": "Power Die", - "SW5E.PowerDiePl": "Power Dice", - "SW5E.PowerDieAlloc": "Power Die Allocation", "SW5E.PowerDiceRecovery": "Power Dice Recovery", + "SW5E.PowerDie": "Power Die", + "SW5E.PowerDieAlloc": "Power Die Allocation", + "SW5E.PowerDiePl": "Power Dice", "SW5E.PowerEffects": "Power Effects", "SW5E.PowerfulCritical": "Powerful Critical", "SW5E.PowerLevel": "Power Level", @@ -916,23 +900,7 @@ "SW5E.Reaction": "Reaction", "SW5E.ReactionPl": "Reactions", "SW5E.Recharge": "Recharge", - "SW5E.RechargeRestHint": "Take a recharge rest? On a recharge rest you may spend remaining Hull Dice and recover Shields.", - "SW5E.RechargetRestNoHD": "No Hull Dice remaining", - "SW5E.RechargeRestNormal": "Recharge Rest (1 hour)", - "SW5E.RechargeRestOvernight": "Recharge Rest (New Day)", - "SW5E.RechargeRestResult": "{name} takes a recharge rest spending {dice} Hull Dice to recover {health} Hull Points.", - "SW5E.RechargeRestResultShort": "{name} takes a recharge rest.", - "SW5E.RechargeRestSelect": "Select Dice to Roll", - "SW5E.Refitting": "Refitting", - "SW5E.RefittingRest": "Refitting Rest", - "SW5E.RefittingRestEpic": "Refitting Rest (1 hour)", - "SW5E.RefittingRestGritty": "Refitting Rest (7 days)", - "SW5E.RefittingRestNormal": "Refitting Rest (8 hours)", - "SW5E.RefittingRestOvernight": "Refitting Rest (New Day)", - "SW5E.RefittingRestResult": "{name} takes a refitting rest.", - "SW5E.RefittingRestResultHD": "{name} takes a refitting rest and recovers {dice} Hull Dice.", - "SW5E.RefittingRestResultHP": "{name} takes a refitting rest and recovers {health} Hull Points.", - "SW5E.RefittingRestResultHPHD": "{name} takes a refitting rest and recovers {health} Hull Points and {dice} Hull Dice.", + "SW5E.Refitting": "Refitting", "SW5E.Refuel": "Refuel", "SW5E.RegenerationRateCoefficient": "Regeneration Rate Coefficient", "SW5E.RequiredMaterials": "Required Materials", @@ -980,13 +948,10 @@ "SW5E.SheetClassNPC": "Default NPC Sheet", "SW5E.SheetClassNPCOld": "Old NPC Sheet", "SW5E.SheetClassVehicle": "Default Vehicle Sheet", - "SW5E.ShieldDice": "Shield Dice", - "SW5E.ShieldDiceRoll": "Roll Shield Dice", - "SW5E.ShieldDiceUsed": "Shield Dice Used", - "SW5E.ShieldDiceWarn": "{name} has no available {formula} Shield Dice remaining!", - "SW5E.ShieldPoints": "Shield Points", - "SW5E.ShieldPointsFormula": "Shield Points Formula", - "SW5E.ShieldRegen": "Regen", + "SW5E.ShieldDice": "Shield Dice", + "SW5E.ShieldPoints": "Shield Points", + "SW5E.ShieldPointsFormula": "Shield Points Formula", + "SW5E.ShieldRegen": "Regen", "SW5E.ShortRest": "Short Rest", "SW5E.ShortRestEpic": "Short Rest (5 minutes)", "SW5E.ShortRestGritty": "Short Rest (8 hours)", @@ -1074,7 +1039,7 @@ "SW5E.StarshipSkillDat": "Data", "SW5E.StarshipSkillHid": "Hide", "SW5E.StarshipSkillImp": "Impress", - "SW5E.StarshipSkillInf": "Interfere", + "SW5E.StarshipSkillInt": "Interfere", "SW5E.StarshipSkillMan": "Maneuvering", "SW5E.StarshipSkillMen": "Menace", "SW5E.StarshipSkillPat": "Patch", diff --git a/less/original/actors.less b/less/original/actors.less index b716fa74..eef86c31 100644 --- a/less/original/actors.less +++ b/less/original/actors.less @@ -671,77 +671,6 @@ .editor { padding: 0 8px; } - .fuel { - flex: 0 0 12px; - background: #7a7971; - margin: 1px 15px 0 1px; - border: 1px solid #191813; - border-radius: 3px; - position: relative; - .fuel-bar { - position: absolute; - top: 1px; - left: 1px; - background: #6c8aa5; - height: 8px; - border: 1px solid #cde4ff; - border-radius: 2px; - } - .fuel-label { - height: 10px; - padding: 0 5px; - position: absolute; - top: 0; - right: 0; - font-size: 13px; - line-height: 12px; - text-align: right; - color: #EEE; - text-shadow: 0 0 5px #000; - } - .fuel-breakpoint { - display: block; - position: absolute; - } - .fuel-breakpoint.fuel-20 { - left: 20%; - } - .fuel-breakpoint.fuel-40 { - left: 40%; - } - .fuel-breakpoint.fuel-60 { - left: 60%; - } - .fuel-breakpoint.fuel-80 { - left: 80%; - } - .arrow-up { - bottom: 0; - width: 0; - height: 0; - border-left: 4px solid transparent; - border-right: 4px solid transparent; - border-bottom: 4px solid #000; - } - .arrow-down { - top: 0; - width: 0; - height: 0; - border-left: 4px solid transparent; - border-right: 4px solid transparent; - border-top: 4px solid #000; - } - } - .fuel.fueled { - .arrow-up { - border-bottom: 4px solid #fff; - border-bottom: 4px solid #000; - } - .arrow-down { - border-top: 4px solid #fff; - border-top: 4px solid #000; - } - } } #actor-flags { diff --git a/less/update/components/actor-global.less b/less/update/components/actor-global.less index 49279ff8..f32ecc4e 100644 --- a/less/update/components/actor-global.less +++ b/less/update/components/actor-global.less @@ -250,45 +250,7 @@ } } } - - .panel.resources { - .traits { - .fuel-wrapper { - display: grid; - grid-template-columns: 300px 100px; - width: 400px; - justify-self: end; - .fuel-label { - font-size: 12px; - line-height: 14px; - width: 100%; - text-shadow: none; - padding: 0; - margin: 0; - height: auto; - text-align: center; - margin-left: -2px; - border-radius: 0 4px 4px 0; - } - .fuel { - position: relative; - border-radius: 4px; - height: 16px; - margin: 0; - width: 100%; - .fuel-bar { - position: absolute; - top: 0; - left: 0; - height: 100%; - border-radius: 4px; - border: none; - } - } - } - } - } - + nav.sheet-navigation { display: grid; grid-template-columns: repeat(7, 1fr); @@ -1161,44 +1123,4 @@ } } } - input[type=range][orient=vertical] { - -webkit-appearance: slider-vertical; - width: 10px; - height: 60px !important; - padding: 0 0 !important; - background-color: #c40f0f !important; - box-sizing: border-box; - &::-webkit-slider-runnable-track { - -webkit-appearance: slider-vertical !important; - height: 60px !important; - width: 10px !important; - line-height: 60px !important; - padding-top: 0 !important; - padding-bottom: 0 !important; - margin-top: 0 0 !important; - border-radius: 3px !important; - background: linear-gradient( to top, #c40f0f 50%, #0dce0d 50% ); - } - &::-webkit-slider-thumb { - -webkit-appearance: none !important; - background-color: #c40f0f !important; - margin-right: -4px !important; - margin-top: 0px !important; - cursor: grab !important; - border-radius: 0 0 0 0 !important; - width: 10px !important; - height: 5px !important; - font-size: 10px; - } - } - output { - display: block; - margin: 5px auto; - font-size: 1.75em; - } - input { - .vertslider { - height: 60px; - } - } } diff --git a/less/update/sw5e-dark.less b/less/update/sw5e-dark.less index 3d96ef34..1dc380db 100644 --- a/less/update/sw5e-dark.less +++ b/less/update/sw5e-dark.less @@ -34,28 +34,6 @@ body.dark-theme { border: 1px solid @blockquoteBorder; box-shadow: @blockquoteShadow; } - - .sw5e.sheet.actor { - .swalt-sheet { - .panel.resources { - .traits { - .fuel-wrapper { - .fuel-label { - background: #D6D6D6; - color: #1C1C1C; - border: 1px solid #1C1C1C; - } - .fuel { - background: #c40f0f; - .fuel-bar { - background: #0dce0d; - } - } - } - } - } - } - } hr { border-width: 0 0 1px 0; diff --git a/less/update/sw5e-light.less b/less/update/sw5e-light.less index 9a9b5f9e..0f898c80 100644 --- a/less/update/sw5e-light.less +++ b/less/update/sw5e-light.less @@ -34,28 +34,6 @@ body.light-theme { border: 1px solid @blockquoteBorder; box-shadow: @blockquoteShadow; } - - .sw5e.sheet.actor { - .swalt-sheet { - .panel.resources { - .traits { - .fuel-wrapper { - .fuel-label { - background: #D6D6D6; - color: #1C1C1C; - border: 1px solid #1C1C1C; - } - .fuel { - background: #c40f0f; - .fuel-bar { - background: #0dce0d; - } - } - } - } - } - } - } hr { border-width: 0 0 1px 0; diff --git a/module/actor/entity.js b/module/actor/entity.js index 83ab6142..0fe01203 100644 --- a/module/actor/entity.js +++ b/module/actor/entity.js @@ -120,9 +120,6 @@ export default class Actor5e extends Actor { // Inventory encumbrance data.attributes.encumbrance = this._computeEncumbrance(actorData); - // Prepare Starship Data - if (actorData.type === "starship") this._computeStarshipData(actorData, data); - // Prepare skills this._prepareSkills(actorData, bonuses, checkBonus, originalSkills); @@ -228,11 +225,7 @@ export default class Actor5e extends Actor { */ async getClassFeatures({className, archetypeName, level} = {}) { const existing = new Set(this.items.map((i) => i.name)); - const features = await Actor5e.loadClassFeatures({ - className, - archetypeName, - level - }); + const features = await Actor5e.loadClassFeatures({className, archetypeName, level}); return features.filter((f) => !existing.has(f.name)) || []; } @@ -365,87 +358,15 @@ export default class Actor5e extends Actor { */ _prepareStarshipData(actorData) { const data = actorData.data; - data.attributes.prof = 0; - // Determine Starship size-based properties based on owned Starship item - const size = actorData.items.filter((i) => i.type === "starship"); - if (size.length !== 0) { - const sizeData = size[0].data.data; - const tiers = parseInt(sizeData.tier) || 0; - data.traits.size = sizeData.size; // needs to be the short code - data.details.tier = tiers; - data.attributes.ac.value = 10 + Math.max(tiers - 1, 0); - data.attributes.hull.die = sizeData.hullDice; - data.attributes.hull.dicemax = sizeData.hullDiceStart + tiers; - data.attributes.hull.dice = sizeData.hullDiceStart + tiers - (parseInt(sizeData.hullDiceUsed) || 0); - data.attributes.shld.die = sizeData.shldDice; - data.attributes.shld.dicemax = sizeData.shldDiceStart + tiers; - data.attributes.shld.dice = sizeData.shldDiceStart + tiers - (parseInt(sizeData.shldDiceUsed) || 0); - sizeData.pwrDice = SW5E.powerDieTypes[tiers]; - data.attributes.power.die = sizeData.pwrDice; - data.attributes.cost.baseBuild = sizeData.buildBaseCost; - data.attributes.workforce.minBuild = sizeData.buildMinWorkforce; - data.attributes.workforce.max = data.attributes.workforce.minBuild * 5; - data.attributes.cost.baseUpgrade = SW5E.baseUpgradeCost[tiers]; - data.attributes.cost.multUpgrade = sizeData.upgrdCostMult; - data.attributes.workforce.minUpgrade = sizeData.upgrdMinWorkforce; - data.attributes.equip.size.crewMinWorkforce = parseInt(sizeData.crewMinWorkforce) || 1; - data.attributes.mods.capLimit = sizeData.modBaseCap; - data.attributes.mods.suites.cap = sizeData.modMaxSuiteCap; - data.attributes.cost.multModification = sizeData.modCostMult; - data.attributes.workforce.minModification = sizeData.modMinWorkforce; - data.attributes.cost.multEquip = sizeData.equipCostMult; - data.attributes.workforce.minEquip = sizeData.equipMinWorkforce; - data.attributes.equip.size.cargoCap = sizeData.cargoCap; - data.attributes.fuel.cost = sizeData.fuelCost; - data.attributes.fuel.cap = sizeData.fuelCap; - data.attributes.equip.size.foodCap = sizeData.foodCap; - } - // Determine Starship armor-based properties based on owned Starship item - const armor = actorData.items.filter((i) => i.type === "equipment" && i.data.armor.type === "ssarmor"); // && (i.data.equipped === true))); - if (armor.length !== 0) { - const armorData = armor[0].data; - data.attributes.equip.armor.dr = parseInt(armorData.dmgred.value) || 0; - data.attributes.equip.armor.maxDex = armorData.armor.dex; - data.attributes.equip.armor.stealthDisadv = armorData.stealth; - } + // Proficiency + data.attributes.prof = Math.floor((Math.max(data.details.tier, 1) + 7) / 4); - // Determine Starship hyperdrive-based properties based on owned Starship item - const hyperdrive = actorData.items.filter((i) => i.type === "equipment" && i.data.armor.type === "hyper"); // && (i.data.equipped === true))); - if (hyperdrive.length !== 0) { - const hdData = hyperdrive[0].data; - data.attributes.equip.hyperdrive.class = parseFloat(hdData.hdclass.value) || null; - } - - // Determine Starship power coupling-based properties based on owned Starship item - const pwrcpl = actorData.items.filter((i) => i.type === "equipment" && i.data.armor.type === "powerc"); // && (i.data.equipped === true))); - if (pwrcpl.length !== 0) { - const pwrcplData = pwrcpl[0].data; - data.attributes.equip.powerCoupling.centralCap = parseInt(pwrcplData.cscap.value) || 0; - data.attributes.equip.powerCoupling.systemCap = parseInt(pwrcplData.sscap.value) || 0; - data.attributes.power.central.max = 0; - data.attributes.power.comms.max = 0; - data.attributes.power.engines.max = 0; - data.attributes.power.shields.max = 0; - data.attributes.power.sensors.max = 0; - data.attributes.power.weapons.max = 0; - } - - // Determine Starship reactor-based properties based on owned Starship item - const reactor = actorData.items.filter((i) => i.type === "equipment" && i.data.armor.type === "reactor"); // && (i.data.equipped === true))); - if (reactor.length !== 0) { - const reactorData = reactor[0].data; - data.attributes.equip.reactor.fuelMult = parseFloat(reactorData.fuelcostsmod.value) || 0; - data.attributes.equip.reactor.powerRecDie = reactorData.powdicerec.value; - } - - // Determine Starship shield-based properties based on owned Starship item - const shields = actorData.items.filter((i) => i.type === "equipment" && i.data.armor.type === "ssshield"); // && (i.data.equipped === true))); - if (shields.length !== 0) { - const shieldsData = shields[0].data; - data.attributes.equip.shields.capMult = parseFloat(shieldsData.capx.value) || 1; - data.attributes.equip.shields.regenRateMult = parseFloat(shieldsData.regrateco.value) || 1; - } + // Link hull to hp and shields to temp hp + data.attributes.hull.value = data.attributes.hp.value; + data.attributes.hull.max = data.attributes.hp.max; + data.attributes.shld.value = data.attributes.hp.temp; + data.attributes.shld.max = data.attributes.hp.tempmax; } /* -------------------------------------------- */ @@ -813,65 +734,10 @@ export default class Actor5e extends Actor { return {value: weight.toNearest(0.1), max, pct, encumbered: pct > 2 / 3}; } - _computeStarshipData(actorData, data) { - // Calculate AC - data.attributes.ac.value += Math.min(data.abilities.dex.mod, data.attributes.equip.armor.maxDex); - - // Set Power Die Storage - data.attributes.power.central.max += data.attributes.equip.powerCoupling.centralCap; - data.attributes.power.comms.max += data.attributes.equip.powerCoupling.systemCap; - data.attributes.power.engines.max += data.attributes.equip.powerCoupling.systemCap; - data.attributes.power.shields.max += data.attributes.equip.powerCoupling.systemCap; - data.attributes.power.sensors.max += data.attributes.equip.powerCoupling.systemCap; - data.attributes.power.weapons.max += data.attributes.equip.powerCoupling.systemCap; - - // Find Size info of Starship - const size = actorData.items.filter((i) => i.type === "starship"); - if (size.length === 0) return; - const sizeData = size[0].data.data; - - // Prepare Hull Points - data.attributes.hp.max = - sizeData.hullDiceRolled.reduce((a, b) => a + b, 0) + data.abilities.con.mod * data.attributes.hull.dicemax; - if (data.attributes.hp.value === null) data.attributes.hp.value = data.attributes.hp.max; - - // Prepare Shield Points - data.attributes.hp.tempmax = - (sizeData.shldDiceRolled.reduce((a, b) => a + b, 0) + - data.abilities.str.mod * data.attributes.shld.dicemax) * - data.attributes.equip.shields.capMult; - if (data.attributes.hp.temp === null) data.attributes.hp.temp = data.attributes.hp.tempmax; - - // Prepare Speeds - data.attributes.movement.space = - sizeData.baseSpaceSpeed + 50 * (data.abilities.str.mod - data.abilities.con.mod); - data.attributes.movement.turn = Math.min( - data.attributes.movement.space, - Math.max(50, sizeData.baseTurnSpeed - 50 * (data.abilities.dex.mod - data.abilities.con.mod)) - ); - - // Prepare Max Suites - data.attributes.mods.suites.max = - sizeData.modMaxSuitesBase + sizeData.modMaxSuitesMult * data.abilities.con.mod; - - // Prepare Hardpoints - data.attributes.mods.hardpoints.max = sizeData.hardpointMult * Math.max(1, data.abilities.str.mod); - - //Prepare Fuel - data.attributes.fuel = this._computeFuel(actorData); - } - /* -------------------------------------------- */ /* Event Handlers */ /* -------------------------------------------- */ - _computeFuel(actorData) { - const fuel = actorData.data.attributes.fuel; - // Compute Fuel percentage - const pct = Math.clamped((fuel.value.toNearest(0.1) * 100) / fuel.cap, 0, 100); - return {...fuel, pct, fueled: pct > 0}; - } - /** @inheritdoc */ async _preCreate(data, options, user) { await super._preCreate(data, options, user); @@ -1044,9 +910,7 @@ export default class Actor5e extends Actor { const label = CONFIG.SW5E.abilities[abilityId]; new Dialog({ title: game.i18n.format("SW5E.AbilityPromptTitle", {ability: label}), - content: `

${game.i18n.format("SW5E.AbilityPromptText", { - ability: label - })}

`, + content: `

${game.i18n.format("SW5E.AbilityPromptText", {ability: label})}

`, buttons: { test: { label: game.i18n.localize("SW5E.ActionAbil"), @@ -1233,18 +1097,13 @@ export default class Actor5e extends Actor { } // Increment successes - else - await this.update({ - "data.attributes.death.success": Math.clamped(successes, 0, 3) - }); + else await this.update({"data.attributes.death.success": Math.clamped(successes, 0, 3)}); } // Save failure else { let failures = (death.failure || 0) + (d20 === 1 ? 2 : 1); - await this.update({ - "data.attributes.death.failure": Math.clamped(failures, 0, 3) - }); + await this.update({"data.attributes.death.failure": Math.clamped(failures, 0, 3)}); if (failures >= 3) { // 3 Failures = death chatString = "SW5E.DeathSaveFailure"; @@ -1253,10 +1112,7 @@ export default class Actor5e extends Actor { // Display success/failure chat message if (chatString) { - let chatData = { - content: game.i18n.format(chatString, {name: this.name}), - speaker - }; + let chatData = {content: game.i18n.format(chatString, {name: this.name}), speaker}; ChatMessage.applyRollMode(chatData, roll.options.rollMode); await ChatMessage.create(chatData); } @@ -1293,12 +1149,7 @@ export default class Actor5e extends Actor { // If no class is available, display an error notification if (!cls) { - ui.notifications.error( - game.i18n.format("SW5E.HitDiceWarn", { - name: this.name, - formula: denomination - }) - ); + ui.notifications.error(game.i18n.format("SW5E.HitDiceWarn", {name: this.name, formula: denomination})); return null; } @@ -1333,218 +1184,6 @@ export default class Actor5e extends Actor { /* -------------------------------------------- */ - /** - * Roll a hull die of the appropriate type, gaining hull points equal to the die roll plus your CON modifier - * @param {string} [denomination] The hit denomination of hull die to roll. Example "d8". - * If no denomination is provided, the first available HD will be used - * @param {string} [numDice] How many damage dice to roll? - * @param {string} [keep] Which dice to keep? Example "kh1". - * @param {boolean} [dialog] Show a dialog prompt for configuring the hull die roll? - * @return {Promise} The created Roll instance, or null if no hull die was rolled - */ - async rollHullDie(denomination, numDice = "1", keep = "", {dialog = true} = {}) { - // If no denomination was provided, choose the first available - let sship = null; - if (!denomination) { - sship = this.itemTypes.class.find( - (s) => s.data.data.hullDiceUsed < s.data.data.tier + s.data.data.hullDiceStart - ); - if (!sship) return null; - denomination = sship.data.data.hullDice; - } - - // Otherwise locate a starship (if any) which has an available hit die of the requested denomination - else { - sship = this.items.find((i) => { - const d = i.data.data; - return d.hullDice === denomination && (d.hitDiceUsed || 0) < (d.tier || 0) + d.hullDiceStart; - }); - } - - // If no class is available, display an error notification - if (!sship) { - ui.notifications.error( - game.i18n.format("SW5E.HullDiceWarn", { - name: this.name, - formula: denomination - }) - ); - return null; - } - - // Prepare roll data - const parts = [`${numDice}${denomination}${keep}`, "@abilities.con.mod"]; - const title = game.i18n.localize("SW5E.HullDiceRoll"); - const rollData = duplicate(this.data.data); - - // Call the roll helper utility - const roll = await damageRoll({ - event: new Event("hitDie"), - parts: parts, - data: rollData, - title: title, - speaker: ChatMessage.getSpeaker({actor: this}), - allowcritical: false, - fastForward: !dialog, - dialogOptions: {width: 350}, - messageData: {"flags.sw5e.roll": {type: "hullDie"}} - }); - if (!roll) return null; - - // Adjust actor data - await sship.update({ - "data.hullDiceUsed": sship.data.data.hullDiceUsed + 1 - }); - const hp = this.data.data.attributes.hp; - const dhp = Math.min(hp.max - hp.value, roll.total); - await this.update({"data.attributes.hp.value": hp.value + dhp}); - return roll; - } - - /* -------------------------------------------- */ - - /** - * Roll a hull die of the appropriate type, gaining hull points equal to the die roll plus your CON modifier - * @return {Promise} The created Roll instance, or null if no hull die was rolled - */ - async rollHullDieCheck() { - // If no denomination was provided, choose the first available - let sship = null; - if (!denomination) { - sship = this.itemTypes.class.find( - (s) => s.data.data.hullDiceUsed < s.data.data.tier + s.data.data.hullDiceStart - ); - if (!sship) return null; - denomination = sship.data.data.hullDice; - } - - // Otherwise locate a starship (if any) which has an available hit die of the requested denomination - else { - sship = this.items.find((i) => { - const d = i.data.data; - return d.hullDice === denomination && (d.hitDiceUsed || 0) < (d.tier || 0) + d.hullDiceStart; - }); - } - - // If no class is available, display an error notification - if (!sship) { - ui.notifications.error( - game.i18n.format("SW5E.HullDiceWarn", { - name: this.name, - formula: denomination - }) - ); - return null; - } - - // Prepare roll data - const parts = [`${numDice}${denomination}${keep}`, "@abilities.con.mod"]; - const title = game.i18n.localize("SW5E.HullDiceRoll"); - const rollData = duplicate(this.data.data); - - // Call the roll helper utility - const roll = await damageRoll({ - event: new Event("hitDie"), - parts: parts, - data: rollData, - title: title, - speaker: ChatMessage.getSpeaker({actor: this}), - allowcritical: false, - fastForward: !dialog, - dialogOptions: {width: 350}, - messageData: {"flags.sw5e.roll": {type: "hullDie"}} - }); - if (!roll) return null; - - // Adjust actor data - await sship.update({ - "data.hullDiceUsed": sship.data.data.hullDiceUsed + 1 - }); - const hp = this.data.data.attributes.hp; - const dhp = Math.min(hp.max - hp.value, roll.total); - await this.update({"data.attributes.hp.value": hp.value + dhp}); - return roll; - } - - /* -------------------------------------------- */ - - /** - * Roll a shield die of the appropriate type, gaining shield points equal to the die roll - * multiplied by the shield regeneration coefficient - * @param {string} [denomination] The denomination of shield die to roll. Example "d8". - * If no denomination is provided, the first available SD will be used - * @param {boolean} [natural] Natural ship shield regeneration (true) or user action (false)? - * @param {string} [numDice] How many damage dice to roll? - * @param {string} [keep] Which dice to keep? Example "kh1". - * @param {boolean} [dialog] Show a dialog prompt for configuring the shield die roll? - * @return {Promise} The created Roll instance, or null if no shield die was rolled - */ - async rollShieldDie(denomination, natural = false, numDice = "1", keep = "", {dialog = true} = {}) { - // If no denomination was provided, choose the first available - let sship = null; - if (!denomination) { - sship = this.itemTypes.class.find( - (s) => s.data.data.shldDiceUsed < s.data.data.tier + s.data.data.shldDiceStart - ); - if (!sship) return null; - denomination = sship.data.data.shldDice; - } - - // Otherwise locate a starship (if any) which has an available hit die of the requested denomination - else { - sship = this.items.find((i) => { - const d = i.data.data; - return d.shldDice === denomination && (d.shldDiceUsed || 0) < (d.tier || 0) + d.shldDiceStart; - }); - } - - // If no starship is available, display an error notification - if (!sship) { - ui.notifications.error( - game.i18n.format("SW5E.ShldDiceWarn", { - name: this.name, - formula: denomination - }) - ); - return null; - } - - // if natural regeneration roll max - if (natural) { - numdice = denomination.substring(1); - denomination = ""; - keep = ""; - } - - // Prepare roll data - const parts = [`${numDice}${denomination}${keep} * @attributes.regenRate`]; - const title = game.i18n.localize("SW5E.ShieldDiceRoll"); - const rollData = duplicate(this.data.data); - - // Call the roll helper utility - roll = await damageRoll({ - event: new Event("shldDie"), - parts: parts, - data: rollData, - title: title, - speaker: ChatMessage.getSpeaker({actor: this}), - allowcritical: false, - fastForward: !dialog, - dialogOptions: {width: 350}, - messageData: {"flags.sw5e.roll": {type: "shldDie"}} - }); - if (!roll) return null; - - // Adjust actor data - await sship.update({ - "data.shldDiceUsed": sship.data.data.shldDiceUsed + 1 - }); - const hp = this.data.data.attributes.hp; - const dhp = Math.min(hp.tempmax - hp.temp, roll.total); - await this.update({"data.attributes.hp.temp": hp.temp + dhp}); - return roll; - } - /** * Results from a rest operation. * @@ -1578,10 +1217,7 @@ export default class Actor5e extends Actor { // Display a Dialog for rolling hit dice if (dialog) { try { - newDay = await ShortRestDialog.shortRestDialog({ - actor: this, - canRoll: hd0 > 0 - }); + newDay = await ShortRestDialog.shortRestDialog({actor: this, canRoll: hd0 > 0}); } catch (err) { return; } @@ -1678,10 +1314,7 @@ export default class Actor5e extends Actor { }, updateItems: [ ...hitDiceUpdates, - ...this._getRestItemUsesRecovery({ - recoverLongRestUses: longRest, - recoverDailyUses: newDay - }) + ...this._getRestItemUsesRecovery({recoverLongRestUses: longRest, recoverDailyUses: newDay}) ], newDay: newDay }; @@ -1904,10 +1537,7 @@ export default class Actor5e extends Actor { if (hitDiceRecovered < maxHitDice && d.hitDiceUsed > 0) { let delta = Math.min(d.hitDiceUsed || 0, maxHitDice - hitDiceRecovered); hitDiceRecovered += delta; - updates.push({ - "_id": item.id, - "data.hitDiceUsed": d.hitDiceUsed - delta - }); + updates.push({"_id": item.id, "data.hitDiceUsed": d.hitDiceUsed - delta}); } } @@ -1948,96 +1578,6 @@ export default class Actor5e extends Actor { /* -------------------------------------------- */ - /** - * Deploy an Actor into this one. - * - * @param {Actor} target The Actor to be deployed. - * @param {boolean} [coord] Deploy as Coordinator - * @param {boolean} [gunner] Deploy as Gunner - * @param {boolean} [mech] Deploy as Mechanic - * @param {boolean} [oper] Deploy as Operator - * @param {boolean} [pilot] Deploy as Pilot - * @param {boolean} [tech] Deploy as Technician - * @param {boolean} [crew] Deploy as Crew - * @param {boolean} [pass] Deploy as Passenger - */ - async deployInto( - target, - { - coord = false, - gunner = false, - mech = false, - oper = false, - pilot = false, - tech = false, - crew = false, - pass = false - } = {} - ) { - // Get the starship Actor data and the new char data - const sship = duplicate(this.toJSON()); - const ssDeploy = sship.data.attributes.deployment; - const char = target; - const charUUID = char.uuid; - const charName = char.data.name; - const charRank = char.data.data.attributes.rank; - let charProf = 0; - if (charRank === undefined || charRank.total > 0) { - charProf = char.data.data.attributes.prof; - } - - if (coord) { - ssDeploy.coord.uuid = charUUID; - ssDeploy.coord.name = charName; - ssDeploy.coord.rank = charRank ? charRank.coord : 0; - ssDeploy.coord.prof = charProf; - } - - if (gunner) { - ssDeploy.gunner.uuid = charUUID; - ssDeploy.gunner.name = charName; - ssDeploy.gunner.rank = charRank ? charRank.gunner : 0; - ssDeploy.gunner.prof = charProf; - } - - if (mech) { - ssDeploy.mechanic.uuid = charUUID; - ssDeploy.mechanic.name = charName; - ssDeploy.mechanic.rank = charRank ? charRank.mechanic : 0; - ssDeploy.mechanic.prof = charProf; - } - - if (oper) { - ssDeploy.operator.uuid = charUUID; - ssDeploy.operator.name = charName; - ssDeploy.operator.rank = charRank ? charRank.operator : 0; - ssDeploy.operator.prof = charProf; - } - - if (pilot) { - ssDeploy.pilot.uuid = charUUID; - ssDeploy.pilot.name = charName; - ssDeploy.pilot.rank = charRank ? charRank.pilot : 0; - ssDeploy.pilot.prof = charProf; - } - - if (tech) { - ssDeploy.technician.uuid = charUUID; - ssDeploy.technician.name = charName; - ssDeploy.technician.rank = charRank ? charRank.technician : 0; - ssDeploy.technician.prof = charProf; - } - - if (crew) { - ssDeploy.crew.push({uuid: charUUID, name: charName, rank: charRank, prof: charProf}); - } - - if (pass) { - ssDeploy.passenger.push({uuid: charUUID, name: charName, rank: charRank, prof: charProf}); - } - this.update({"data.attributes.deployment": ssDeploy}); - } - /** * Transform this Actor into another one. * diff --git a/module/actor/old_entity.js b/module/actor/old_entity.js new file mode 100644 index 00000000..50b9b895 --- /dev/null +++ b/module/actor/old_entity.js @@ -0,0 +1,2126 @@ +import {d20Roll, damageRoll} from "../dice.js"; +import ShortRestDialog from "../apps/short-rest.js"; +import LongRestDialog from "../apps/long-rest.js"; +import {SW5E} from "../config.js"; + +/** + * Extend the base Actor class to implement additional system-specific logic for SW5e. + */ +export default class Actor5e extends Actor { + /** + * Is this Actor currently polymorphed into some other creature? + * @return {boolean} + */ + get isPolymorphed() { + return this.getFlag("sw5e", "isPolymorphed") || false; + } + + /* -------------------------------------------- */ + + /** @override */ + prepareBaseData() { + switch (this.data.type) { + case "character": + return this._prepareCharacterData(this.data); + case "npc": + return this._prepareNPCData(this.data); + case "starship": + return this._prepareStarshipData(this.data); + case "vehicle": + return this._prepareVehicleData(this.data); + } + } + + /* -------------------------------------------- */ + + /** @override */ + prepareDerivedData() { + const actorData = this.data; + const data = actorData.data; + const flags = actorData.flags.sw5e || {}; + const bonuses = getProperty(data, "bonuses.abilities") || {}; + + // Retrieve data for polymorphed actors + let originalSaves = null; + let originalSkills = null; + if (this.isPolymorphed) { + const transformOptions = this.getFlag("sw5e", "transformOptions"); + const original = game.actors?.get(this.getFlag("sw5e", "originalActor")); + if (original) { + if (transformOptions.mergeSaves) { + originalSaves = original.data.data.abilities; + } + if (transformOptions.mergeSkills) { + originalSkills = original.data.data.skills; + } + } + } + + // Ability modifiers and saves + const dcBonus = Number.isNumeric(data.bonuses?.power?.dc) ? parseInt(data.bonuses.power.dc) : 0; + const saveBonus = Number.isNumeric(bonuses.save) ? parseInt(bonuses.save) : 0; + const checkBonus = Number.isNumeric(bonuses.check) ? parseInt(bonuses.check) : 0; + for (let [id, abl] of Object.entries(data.abilities)) { + abl.mod = Math.floor((abl.value - 10) / 2); + abl.prof = (abl.proficient || 0) * data.attributes.prof; + abl.saveBonus = saveBonus; + abl.checkBonus = checkBonus; + abl.save = abl.mod + abl.prof + abl.saveBonus; + abl.dc = 8 + abl.mod + data.attributes.prof + dcBonus; + + // If we merged saves when transforming, take the highest bonus here. + if (originalSaves && abl.proficient) { + abl.save = Math.max(abl.save, originalSaves[id].save); + } + } + + // Inventory encumbrance + data.attributes.encumbrance = this._computeEncumbrance(actorData); + + if (actorData.type === "starship") { + // Calculate AC + data.attributes.ac.value += Math.min(data.abilities.dex.mod, data.attributes.equip.armor.maxDex); + + // Set Power Die Storage + data.attributes.power.central.max += data.attributes.equip.powerCoupling.centralCap; + data.attributes.power.comms.max += data.attributes.equip.powerCoupling.systemCap; + data.attributes.power.engines.max += data.attributes.equip.powerCoupling.systemCap; + data.attributes.power.shields.max += data.attributes.equip.powerCoupling.systemCap; + data.attributes.power.sensors.max += data.attributes.equip.powerCoupling.systemCap; + data.attributes.power.weapons.max += data.attributes.equip.powerCoupling.systemCap; + + // Find Size info of Starship + const size = actorData.items.filter((i) => i.type === "starship"); + if (size.length === 0) return; + const sizeData = size[0].data; + + // Prepare Hull Points + data.attributes.hp.max = + sizeData.hullDiceRolled.reduce((a, b) => a + b, 0) + + data.abilities.con.mod * data.attributes.hull.dicemax; + if (data.attributes.hp.value === null) data.attributes.hp.value = data.attributes.hp.max; + + // Prepare Shield Points + data.attributes.hp.tempmax = + (sizeData.shldDiceRolled.reduce((a, b) => a + b, 0) + + data.abilities.str.mod * data.attributes.shld.dicemax) * + data.attributes.equip.shields.capMult; + if (data.attributes.hp.temp === null) data.attributes.hp.temp = data.attributes.hp.tempmax; + + // Prepare Speeds + data.attributes.movement.space = + sizeData.baseSpaceSpeed + 50 * (data.abilities.str.mod - data.abilities.con.mod); + data.attributes.movement.turn = Math.min( + data.attributes.movement.space, + Math.max(50, sizeData.baseTurnSpeed - 50 * (data.abilities.dex.mod - data.abilities.con.mod)) + ); + + // Prepare Max Suites + data.attributes.mods.suites.max = + sizeData.modMaxSuitesBase + sizeData.modMaxSuitesMult * data.abilities.con.mod; + + // Prepare Hardpoints + data.attributes.mods.hardpoints.max = sizeData.hardpointMult * Math.max(1, data.abilities.str.mod); + + //Prepare Fuel + data.attributes.fuel = this._computeFuel(actorData); + } + + // Prepare skills + this._prepareSkills(actorData, bonuses, checkBonus, originalSkills); + + // Determine Initiative Modifier + const init = data.attributes.init; + const athlete = flags.remarkableAthlete; + const joat = flags.jackOfAllTrades; + init.mod = data.abilities.dex.mod; + if (joat) init.prof = Math.floor(0.5 * data.attributes.prof); + else if (athlete) init.prof = Math.ceil(0.5 * data.attributes.prof); + else init.prof = 0; + init.value = init.value ?? 0; + init.bonus = init.value + (flags.initiativeAlert ? 5 : 0); + init.total = init.mod + init.prof + init.bonus; + + // Prepare power-casting data + data.attributes.powerForceLightDC = 8 + data.abilities.wis.mod + data.attributes.prof ?? 10; + data.attributes.powerForceDarkDC = 8 + data.abilities.cha.mod + data.attributes.prof ?? 10; + data.attributes.powerForceUnivDC = + Math.max(data.attributes.powerForceLightDC, data.attributes.powerForceDarkDC) ?? 10; + data.attributes.powerTechDC = 8 + data.abilities.int.mod + data.attributes.prof ?? 10; + this._computeDerivedPowercasting(this.data); + + // Compute owned item attributes which depend on prepared Actor data + this.items.forEach((item) => { + item.getSaveDC(); + item.getAttackToHit(); + }); + } + + /* -------------------------------------------- */ + + /** + * Return the amount of experience required to gain a certain character level. + * @param level {Number} The desired level + * @return {Number} The XP required + */ + getLevelExp(level) { + const levels = CONFIG.SW5E.CHARACTER_EXP_LEVELS; + return levels[Math.min(level, levels.length - 1)]; + } + + /* -------------------------------------------- */ + + /** + * Return the amount of experience granted by killing a creature of a certain CR. + * @param cr {Number} The creature's challenge rating + * @return {Number} The amount of experience granted per kill + */ + getCRExp(cr) { + if (cr < 1.0) return Math.max(200 * cr, 10); + return CONFIG.SW5E.CR_EXP_LEVELS[cr]; + } + + /* -------------------------------------------- */ + + /** @override */ + getRollData() { + const data = super.getRollData(); + data.classes = this.data.items.reduce((obj, i) => { + if (i.type === "class") { + obj[i.name.slugify({strict: true})] = i.data; + } + return obj; + }, {}); + data.prof = this.data.data.attributes.prof || 0; + return data; + } + + /* -------------------------------------------- */ + + /** + * Return the features which a character is awarded for each class level + * @param {string} className The class name being added + * @param {string} archetypeName The archetype of the class being added, if any + * @param {number} level The number of levels in the added class + * @param {number} priorLevel The previous level of the added class + * @return {Promise} Array of Item5e entities + */ + static async getClassFeatures({className = "", archetypeName = "", level = 1, priorLevel = 0} = {}) { + className = className.toLowerCase(); + archetypeName = archetypeName.slugify(); + + // Get the configuration of features which may be added + const clsConfig = CONFIG.SW5E.classFeatures[className]; + if (!clsConfig) return []; + + // Acquire class features + let ids = []; + for (let [l, f] of Object.entries(clsConfig.features || {})) { + l = parseInt(l); + if (l <= level && l > priorLevel) ids = ids.concat(f); + } + + // Acquire archetype features + const archConfig = clsConfig.archetypes[archetypeName] || {}; + for (let [l, f] of Object.entries(archConfig.features || {})) { + l = parseInt(l); + if (l <= level && l > priorLevel) ids = ids.concat(f); + } + + // Load item data for all identified features + const features = []; + for (let id of ids) { + features.push(await fromUuid(id)); + } + + // Class powers should always be prepared + for (const feature of features) { + if (feature.type === "power") { + const preparation = feature.data.data.preparation; + preparation.mode = "always"; + preparation.prepared = true; + } + } + return features; + } + + /* -------------------------------------------- */ + + /** @override */ + async updateEmbeddedEntity(embeddedName, data, options = {}) { + const createItems = embeddedName === "OwnedItem" ? await this._createClassFeatures(data) : []; + let updated = await super.updateEmbeddedEntity(embeddedName, data, options); + if (createItems.length) await this.createEmbeddedEntity("OwnedItem", createItems); + return updated; + } + + /* -------------------------------------------- */ + + /** + * Create additional class features in the Actor when a class item is updated. + * @private + */ + async _createClassFeatures(updated) { + let toCreate = []; + for (let u of updated instanceof Array ? updated : [updated]) { + const item = this.items.get(u._id); + if (!item || item.data.type !== "class") continue; + const updateData = expandObject(u); + const config = { + className: updateData.name || item.data.name, + archetypeName: getProperty(updateData, "data.archetype") || item.data.data.archetype, + level: getProperty(updateData, "data.levels"), + priorLevel: item ? item.data.data.levels : 0 + }; + + // Get and create features for an increased class level + let changed = false; + if (config.level && config.level > config.priorLevel) changed = true; + if (config.archetypeName !== item.data.data.archetype) changed = true; + + // Get features to create + if (changed) { + const existing = new Set(this.items.map((i) => i.name)); + const features = await Actor5e.getClassFeatures(config); + for (let f of features) { + if (!existing.has(f.name)) toCreate.push(f); + } + } + } + return toCreate; + } + + /* -------------------------------------------- */ + /* Data Preparation Helpers */ + /* -------------------------------------------- */ + + /** + * Prepare Character type specific data + */ + _prepareCharacterData(actorData) { + const data = actorData.data; + + // Determine character level and available hit dice based on owned Class items + const [level, hd] = actorData.items.reduce( + (arr, item) => { + if (item.type === "class") { + const classLevels = parseInt(item.data.levels) || 1; + arr[0] += classLevels; + arr[1] += classLevels - (parseInt(item.data.hitDiceUsed) || 0); + } + return arr; + }, + [0, 0] + ); + data.details.level = level; + data.attributes.hd = hd; + + // Character proficiency bonus + data.attributes.prof = Math.floor((level + 7) / 4); + + // Experience required for next level + const xp = data.details.xp; + xp.max = this.getLevelExp(level || 1); + const prior = this.getLevelExp(level - 1 || 0); + const required = xp.max - prior; + const pct = Math.round(((xp.value - prior) * 100) / required); + xp.pct = Math.clamped(pct, 0, 100); + + // Add base Powercasting attributes + this._computeBasePowercasting(actorData); + } + + /* -------------------------------------------- */ + + /** + * Prepare NPC type specific data + */ + _prepareNPCData(actorData) { + const data = actorData.data; + + // Kill Experience + data.details.xp.value = this.getCRExp(data.details.cr); + + // Proficiency + data.attributes.prof = Math.floor((Math.max(data.details.cr, 1) + 7) / 4); + + this._computeBasePowercasting(actorData); + + // Powercaster Level + if (data.attributes.powercasting && !Number.isNumeric(data.details.powerLevel)) { + data.details.powerLevel = Math.max(data.details.cr, 1); + } + } + + /* -------------------------------------------- */ + + /** + * Prepare vehicle type-specific data + * @param actorData + * @private + */ + _prepareVehicleData(actorData) {} + + /* -------------------------------------------- */ + + /* -------------------------------------------- */ + + /** + * Prepare starship type-specific data + * @param actorData + * @private + */ + _prepareStarshipData(actorData) { + const data = actorData.data; + data.attributes.prof = 0; + // Determine Starship size-based properties based on owned Starship item + const size = actorData.items.filter((i) => i.type === "starship"); + if (size.length !== 0) { + const sizeData = size[0].data; + const tiers = parseInt(sizeData.tier) || 0; + data.traits.size = sizeData.size; // needs to be the short code + data.details.tier = tiers; + data.attributes.ac.value = 10 + Math.max(tiers - 1, 0); + data.attributes.hull.die = sizeData.hullDice; + data.attributes.hull.dicemax = sizeData.hullDiceStart + tiers; + data.attributes.hull.dice = sizeData.hullDiceStart + tiers - (parseInt(sizeData.hullDiceUsed) || 0); + data.attributes.shld.die = sizeData.shldDice; + data.attributes.shld.dicemax = sizeData.shldDiceStart + tiers; + data.attributes.shld.dice = sizeData.shldDiceStart + tiers - (parseInt(sizeData.shldDiceUsed) || 0); + sizeData.pwrDice = SW5E.powerDieTypes[tiers]; + data.attributes.power.die = sizeData.pwrDice; + data.attributes.cost.baseBuild = sizeData.buildBaseCost; + data.attributes.workforce.minBuild = sizeData.buildMinWorkforce; + data.attributes.workforce.max = data.attributes.workforce.minBuild * 5; + data.attributes.cost.baseUpgrade = SW5E.baseUpgradeCost[tiers]; + data.attributes.cost.multUpgrade = sizeData.upgrdCostMult; + data.attributes.workforce.minUpgrade = sizeData.upgrdMinWorkforce; + data.attributes.equip.size.crewMinWorkforce = parseInt(sizeData.crewMinWorkforce) || 1; + data.attributes.mods.capLimit = sizeData.modBaseCap; + data.attributes.mods.suites.cap = sizeData.modMaxSuiteCap; + data.attributes.cost.multModification = sizeData.modCostMult; + data.attributes.workforce.minModification = sizeData.modMinWorkforce; + data.attributes.cost.multEquip = sizeData.equipCostMult; + data.attributes.workforce.minEquip = sizeData.equipMinWorkforce; + data.attributes.equip.size.cargoCap = sizeData.cargoCap; + data.attributes.fuel.cost = sizeData.fuelCost; + data.attributes.fuel.cap = sizeData.fuelCap; + data.attributes.equip.size.foodCap = sizeData.foodCap; + } + + // Determine Starship armor-based properties based on owned Starship item + const armor = actorData.items.filter((i) => i.type === "equipment" && i.data.armor.type === "ssarmor"); // && (i.data.equipped === true))); + if (armor.length !== 0) { + const armorData = armor[0].data; + data.attributes.equip.armor.dr = parseInt(armorData.dmgred.value) || 0; + data.attributes.equip.armor.maxDex = armorData.armor.dex; + data.attributes.equip.armor.stealthDisadv = armorData.stealth; + } + + // Determine Starship hyperdrive-based properties based on owned Starship item + const hyperdrive = actorData.items.filter((i) => i.type === "equipment" && i.data.armor.type === "hyper"); // && (i.data.equipped === true))); + if (hyperdrive.length !== 0) { + const hdData = hyperdrive[0].data; + data.attributes.equip.hyperdrive.class = parseFloat(hdData.hdclass.value) || null; + } + + // Determine Starship power coupling-based properties based on owned Starship item + const pwrcpl = actorData.items.filter((i) => i.type === "equipment" && i.data.armor.type === "powerc"); // && (i.data.equipped === true))); + if (pwrcpl.length !== 0) { + const pwrcplData = pwrcpl[0].data; + data.attributes.equip.powerCoupling.centralCap = parseInt(pwrcplData.cscap.value) || 0; + data.attributes.equip.powerCoupling.systemCap = parseInt(pwrcplData.sscap.value) || 0; + data.attributes.power.central.max = 0; + data.attributes.power.comms.max = 0; + data.attributes.power.engines.max = 0; + data.attributes.power.shields.max = 0; + data.attributes.power.sensors.max = 0; + data.attributes.power.weapons.max = 0; + } + + // Determine Starship reactor-based properties based on owned Starship item + const reactor = actorData.items.filter((i) => i.type === "equipment" && i.data.armor.type === "reactor"); // && (i.data.equipped === true))); + if (reactor.length !== 0) { + const reactorData = reactor[0].data; + data.attributes.equip.reactor.fuelMult = parseFloat(reactorData.fuelcostsmod.value) || 0; + data.attributes.equip.reactor.powerRecDie = reactorData.powdicerec.value; + } + + // Determine Starship shield-based properties based on owned Starship item + const shields = actorData.items.filter((i) => i.type === "equipment" && i.data.armor.type === "ssshield"); // && (i.data.equipped === true))); + if (shields.length !== 0) { + const shieldsData = shields[0].data; + data.attributes.equip.shields.capMult = parseFloat(shieldsData.capx.value) || 1; + data.attributes.equip.shields.regenRateMult = parseFloat(shieldsData.regrateco.value) || 1; + } + } + + /* -------------------------------------------- */ + + /** + * Prepare skill checks. + * @param actorData + * @param bonuses Global bonus data. + * @param checkBonus Ability check specific bonus. + * @param originalSkills A transformed actor's original actor's skills. + * @private + */ + _prepareSkills(actorData, bonuses, checkBonus, originalSkills) { + if (actorData.type === "vehicle") return; + + const data = actorData.data; + const flags = actorData.flags.sw5e || {}; + + // Skill modifiers + const feats = SW5E.characterFlags; + const athlete = flags.remarkableAthlete; + const joat = flags.jackOfAllTrades; + const observant = flags.observantFeat; + const skillBonus = Number.isNumeric(bonuses.skill) ? parseInt(bonuses.skill) : 0; + for (let [id, skl] of Object.entries(data.skills)) { + skl.value = Math.clamped(Number(skl.value).toNearest(0.5), 0, 2) ?? 0; + let round = Math.floor; + + // Remarkable + if (athlete && skl.value < 0.5 && feats.remarkableAthlete.abilities.includes(skl.ability)) { + skl.value = 0.5; + round = Math.ceil; + } + + // Jack of All Trades + if (joat && skl.value < 0.5) { + skl.value = 0.5; + } + + // Polymorph Skill Proficiencies + if (originalSkills) { + skl.value = Math.max(skl.value, originalSkills[id].value); + } + + // Compute modifier + skl.bonus = checkBonus + skillBonus; + skl.mod = data.abilities[skl.ability].mod; + skl.prof = round(skl.value * data.attributes.prof); + skl.total = skl.mod + skl.prof + skl.bonus; + + // Compute passive bonus + const passive = observant && feats.observantFeat.skills.includes(id) ? 5 : 0; + skl.passive = 10 + skl.total + passive; + } + } + + /* -------------------------------------------- */ + + /** + * Prepare data related to the power-casting capabilities of the Actor + * @private + */ + _computeBasePowercasting(actorData) { + if (actorData.type === "vehicle" || actorData.type === "starship") return; + const powers = actorData.data.powers; + const isNPC = actorData.type === "npc"; + + // Translate the list of classes into force and tech power-casting progression + const forceProgression = { + classes: 0, + levels: 0, + multi: 0, + maxClass: "none", + maxClassPriority: 0, + maxClassLevels: 0, + maxClassPowerLevel: 0, + powersKnown: 0, + points: 0 + }; + const techProgression = { + classes: 0, + levels: 0, + multi: 0, + maxClass: "none", + maxClassPriority: 0, + maxClassLevels: 0, + maxClassPowerLevel: 0, + powersKnown: 0, + points: 0 + }; + + // Tabulate the total power-casting progression + const classes = this.data.items.filter((i) => i.type === "class"); + let priority = 0; + for (let cls of classes) { + const d = cls.data; + if (d.powercasting === "none") continue; + const levels = d.levels; + const prog = d.powercasting; + + switch (prog) { + case "consular": + priority = 3; + forceProgression.levels += levels; + forceProgression.multi += (SW5E.powerMaxLevel["consular"][19] / 9) * levels; + forceProgression.classes++; + // see if class controls high level forcecasting + if (levels >= forceProgression.maxClassLevels && priority > forceProgression.maxClassPriority) { + forceProgression.maxClass = "consular"; + forceProgression.maxClassLevels = levels; + forceProgression.maxClassPriority = priority; + forceProgression.maxClassPowerLevel = + SW5E.powerMaxLevel["consular"][Math.clamped(levels - 1, 0, 20)]; + } + // calculate points and powers known + forceProgression.powersKnown += SW5E.powersKnown["consular"][Math.clamped(levels - 1, 0, 20)]; + forceProgression.points += SW5E.powerPoints["consular"][Math.clamped(levels - 1, 0, 20)]; + break; + case "engineer": + priority = 2; + techProgression.levels += levels; + techProgression.multi += (SW5E.powerMaxLevel["engineer"][19] / 9) * levels; + techProgression.classes++; + // see if class controls high level techcasting + if (levels >= techProgression.maxClassLevels && priority > techProgression.maxClassPriority) { + techProgression.maxClass = "engineer"; + techProgression.maxClassLevels = levels; + techProgression.maxClassPriority = priority; + techProgression.maxClassPowerLevel = + SW5E.powerMaxLevel["engineer"][Math.clamped(levels - 1, 0, 20)]; + } + techProgression.powersKnown += SW5E.powersKnown["engineer"][Math.clamped(levels - 1, 0, 20)]; + techProgression.points += SW5E.powerPoints["engineer"][Math.clamped(levels - 1, 0, 20)]; + break; + case "guardian": + priority = 1; + forceProgression.levels += levels; + forceProgression.multi += (SW5E.powerMaxLevel["guardian"][19] / 9) * levels; + forceProgression.classes++; + // see if class controls high level forcecasting + if (levels >= forceProgression.maxClassLevels && priority > forceProgression.maxClassPriority) { + forceProgression.maxClass = "guardian"; + forceProgression.maxClassLevels = levels; + forceProgression.maxClassPriority = priority; + forceProgression.maxClassPowerLevel = + SW5E.powerMaxLevel["guardian"][Math.clamped(levels - 1, 0, 20)]; + } + forceProgression.powersKnown += SW5E.powersKnown["guardian"][Math.clamped(levels - 1, 0, 20)]; + forceProgression.points += SW5E.powerPoints["guardian"][Math.clamped(levels - 1, 0, 20)]; + break; + case "scout": + priority = 1; + techProgression.levels += levels; + techProgression.multi += (SW5E.powerMaxLevel["scout"][19] / 9) * levels; + techProgression.classes++; + // see if class controls high level techcasting + if (levels >= techProgression.maxClassLevels && priority > techProgression.maxClassPriority) { + techProgression.maxClass = "scout"; + techProgression.maxClassLevels = levels; + techProgression.maxClassPriority = priority; + techProgression.maxClassPowerLevel = + SW5E.powerMaxLevel["scout"][Math.clamped(levels - 1, 0, 20)]; + } + techProgression.powersKnown += SW5E.powersKnown["scout"][Math.clamped(levels - 1, 0, 20)]; + techProgression.points += SW5E.powerPoints["scout"][Math.clamped(levels - 1, 0, 20)]; + break; + case "sentinel": + priority = 2; + forceProgression.levels += levels; + forceProgression.multi += (SW5E.powerMaxLevel["sentinel"][19] / 9) * levels; + forceProgression.classes++; + // see if class controls high level forcecasting + if (levels >= forceProgression.maxClassLevels && priority > forceProgression.maxClassPriority) { + forceProgression.maxClass = "sentinel"; + forceProgression.maxClassLevels = levels; + forceProgression.maxClassPriority = priority; + forceProgression.maxClassPowerLevel = + SW5E.powerMaxLevel["sentinel"][Math.clamped(levels - 1, 0, 20)]; + } + forceProgression.powersKnown += SW5E.powersKnown["sentinel"][Math.clamped(levels - 1, 0, 20)]; + forceProgression.points += SW5E.powerPoints["sentinel"][Math.clamped(levels - 1, 0, 20)]; + break; + } + } + + // EXCEPTION: multi-classed progression uses multi rounded down rather than levels + if (!isNPC && forceProgression.classes > 1) { + forceProgression.levels = Math.floor(forceProgression.multi); + forceProgression.maxClassPowerLevel = SW5E.powerMaxLevel["multi"][forceProgression.levels - 1]; + } + if (!isNPC && techProgression.classes > 1) { + techProgression.levels = Math.floor(techProgression.multi); + techProgression.maxClassPowerLevel = SW5E.powerMaxLevel["multi"][techProgression.levels - 1]; + } + + // EXCEPTION: NPC with an explicit power-caster level + if (isNPC && actorData.data.details.powerForceLevel) { + forceProgression.levels = actorData.data.details.powerForceLevel; + actorData.data.attributes.force.level = forceProgression.levels; + forceProgression.maxClass = actorData.data.attributes.powercasting; + forceProgression.maxClassPowerLevel = + SW5E.powerMaxLevel[forceProgression.maxClass][Math.clamped(forceProgression.levels - 1, 0, 20)]; + } + if (isNPC && actorData.data.details.powerTechLevel) { + techProgression.levels = actorData.data.details.powerTechLevel; + actorData.data.attributes.tech.level = techProgression.levels; + techProgression.maxClass = actorData.data.attributes.powercasting; + techProgression.maxClassPowerLevel = + SW5E.powerMaxLevel[techProgression.maxClass][Math.clamped(techProgression.levels - 1, 0, 20)]; + } + + // Look up the number of slots per level from the powerLimit table + let forcePowerLimit = Array.from(SW5E.powerLimit["none"]); + for (let i = 0; i < forceProgression.maxClassPowerLevel; i++) { + forcePowerLimit[i] = SW5E.powerLimit[forceProgression.maxClass][i]; + } + + for (let [n, lvl] of Object.entries(powers)) { + let i = parseInt(n.slice(-1)); + if (Number.isNaN(i)) continue; + if (Number.isNumeric(lvl.foverride)) lvl.fmax = Math.max(parseInt(lvl.foverride), 0); + else lvl.fmax = forcePowerLimit[i - 1] || 0; + if (isNPC) { + lvl.fvalue = lvl.fmax; + } else { + lvl.fvalue = Math.min(parseInt(lvl.fvalue || lvl.value || lvl.fmax), lvl.fmax); + } + } + + let techPowerLimit = Array.from(SW5E.powerLimit["none"]); + for (let i = 0; i < techProgression.maxClassPowerLevel; i++) { + techPowerLimit[i] = SW5E.powerLimit[techProgression.maxClass][i]; + } + + for (let [n, lvl] of Object.entries(powers)) { + let i = parseInt(n.slice(-1)); + if (Number.isNaN(i)) continue; + if (Number.isNumeric(lvl.toverride)) lvl.tmax = Math.max(parseInt(lvl.toverride), 0); + else lvl.tmax = techPowerLimit[i - 1] || 0; + if (isNPC) { + lvl.tvalue = lvl.tmax; + } else { + lvl.tvalue = Math.min(parseInt(lvl.tvalue || lvl.value || lvl.tmax), lvl.tmax); + } + } + + // Set Force and tech power for PC Actors + if (!isNPC && forceProgression.levels) { + actorData.data.attributes.force.known.max = forceProgression.powersKnown; + actorData.data.attributes.force.points.max = forceProgression.points; // + Math.max(actorData.data.abilities.wis.mod,actorData.data.abilities.cha.mod); + actorData.data.attributes.force.level = forceProgression.levels; + } + if (!isNPC && techProgression.levels) { + actorData.data.attributes.tech.known.max = techProgression.powersKnown; + actorData.data.attributes.tech.points.max = techProgression.points; // + actorData.data.abilities.int.mod; + actorData.data.attributes.tech.level = techProgression.levels; + } + + // Tally Powers Known and check for migration first to avoid errors + let hasKnownPowers = actorData?.data?.attributes?.force?.known?.value !== undefined; + if (hasKnownPowers) { + const knownPowers = this.data.items.filter((i) => i.type === "power"); + let knownForcePowers = 0; + let knownTechPowers = 0; + for (let knownPower of knownPowers) { + const d = knownPower.data; + switch (knownPower.data.school) { + case "lgt": + case "uni": + case "drk": { + knownForcePowers++; + break; + } + case "tec": { + knownTechPowers++; + break; + } + } + continue; + } + actorData.data.attributes.force.known.value = knownForcePowers; + actorData.data.attributes.tech.known.value = knownTechPowers; + } + } + + /* -------------------------------------------- */ + + /** + * Prepare data related to the power-casting capabilities of the Actor + * @private + */ + _computeDerivedPowercasting(actorData) { + if (actorData.type !== "actor") return; + + // Set Force and tech power for PC Actors + if (!!actorData.data.attributes.force.level) { + actorData.data.attributes.force.points.max += Math.max( + actorData.data.abilities.wis.mod, + actorData.data.abilities.cha.mod + ); + } + if (!!actorData.data.attributes.tech.level) { + actorData.data.attributes.tech.points.max += actorData.data.abilities.int.mod; + } + } + + /* -------------------------------------------- */ + + /** + * Compute the level and percentage of encumbrance for an Actor. + * + * Optionally include the weight of carried currency across all denominations by applying the standard rule + * from the PHB pg. 143 + * @param {Object} actorData The data object for the Actor being rendered + * @returns {{max: number, value: number, pct: number}} An object describing the character's encumbrance level + * @private + */ + _computeEncumbrance(actorData) { + // Get the total weight from items + const physicalItems = ["weapon", "equipment", "consumable", "tool", "backpack", "loot"]; + let weight = actorData.items.reduce((weight, i) => { + if (!physicalItems.includes(i.type)) return weight; + const q = i.data.quantity || 0; + const w = i.data.weight || 0; + return weight + q * w; + }, 0); + + // [Optional] add Currency Weight (for non-transformed actors) + if (game.settings.get("sw5e", "currencyWeight") && actorData.data.currency) { + const currency = actorData.data.currency; + const numCoins = Object.values(currency).reduce((val, denom) => (val += Math.max(denom, 0)), 0); + weight += numCoins / CONFIG.SW5E.encumbrance.currencyPerWeight; + } + + // Determine the encumbrance size class + let mod = + { + tiny: 0.5, + sm: 1, + med: 1, + lg: 2, + huge: 4, + grg: 8 + }[actorData.data.traits.size] || 1; + if (this.getFlag("sw5e", "powerfulBuild")) mod = Math.min(mod * 2, 8); + + // Compute Encumbrance percentage + weight = weight.toNearest(0.1); + const max = actorData.data.abilities.str.value * CONFIG.SW5E.encumbrance.strMultiplier * mod; + const pct = Math.clamped((weight * 100) / max, 0, 100); + return {value: weight.toNearest(0.1), max, pct, encumbered: pct > 2 / 3}; + } + + _computeFuel(actorData) { + const fuel = actorData.data.attributes.fuel; + // Compute Fuel percentage + const pct = Math.clamped((fuel.value.toNearest(0.1) * 100) / fuel.cap, 0, 100); + return {...fuel, pct, fueled: pct > 0}; + } + + /* -------------------------------------------- */ + /* Socket Listeners and Handlers + /* -------------------------------------------- */ + + /** @override */ + static async create(data, options = {}) { + data.token = data.token || {}; + if (data.type === "character") { + mergeObject( + data.token, + { + vision: true, + dimSight: 30, + brightSight: 0, + actorLink: true, + disposition: 1 + }, + {overwrite: false} + ); + } + return super.create(data, options); + } + + /* -------------------------------------------- */ + + /** @override */ + async update(data, options = {}) { + // Apply changes in Actor size to Token width/height + const newSize = getProperty(data, "data.traits.size"); + if (newSize && newSize !== getProperty(this.data, "data.traits.size")) { + let size = CONFIG.SW5E.tokenSizes[newSize]; + if (this.isToken) this.token.update({height: size, width: size}); + else if (!data["token.width"] && !hasProperty(data, "token.width")) { + data["token.height"] = size; + data["token.width"] = size; + } + } + + // Reset death save counters + if (this.data.data.attributes.hp.value <= 0 && getProperty(data, "data.attributes.hp.value") > 0) { + setProperty(data, "data.attributes.death.success", 0); + setProperty(data, "data.attributes.death.failure", 0); + } + + // Perform the update + return super.update(data, options); + } + + /* -------------------------------------------- */ + + /** @override */ + async createEmbeddedEntity(embeddedName, itemData, options = {}) { + // Pre-creation steps for owned items + if (embeddedName === "OwnedItem") this._preCreateOwnedItem(itemData, options); + + // Standard embedded entity creation + return super.createEmbeddedEntity(embeddedName, itemData, options); + } + + /* -------------------------------------------- */ + + /** + * A temporary shim function which will eventually (in core fvtt version 0.8.0+) be migrated to the new abstraction layer + * @param itemData + * @param options + * @private + */ + _preCreateOwnedItem(itemData, options) { + if (this.data.type === "vehicle") return; + const isNPC = this.data.type === "npc"; + let initial = {}; + switch (itemData.type) { + case "weapon": + if (getProperty(itemData, "data.equipped") === undefined) { + initial["data.equipped"] = isNPC; // NPCs automatically equip weapons + } + if (getProperty(itemData, "data.proficient") === undefined) { + if (isNPC) { + initial["data.proficient"] = true; // NPCs automatically have equipment proficiency + } else { + const weaponProf = { + natural: true, + simpleVW: "sim", + simpleB: "sim", + simpleLW: "sim", + martialVW: "mar", + martialB: "mar", + martialLW: "mar" + }[itemData.data?.weaponType]; // Player characters check proficiency + const actorWeaponProfs = this.data.data.traits?.weaponProf?.value || []; + const hasWeaponProf = weaponProf === true || actorWeaponProfs.includes(weaponProf); + initial["data.proficient"] = hasWeaponProf; + } + } + break; + + case "equipment": + if (getProperty(itemData, "data.equipped") === undefined) { + initial["data.equipped"] = isNPC; // NPCs automatically equip equipment + } + if (getProperty(itemData, "data.proficient") === undefined) { + if (isNPC) { + initial["data.proficient"] = true; // NPCs automatically have equipment proficiency + } else { + const armorProf = { + natural: true, + clothing: true, + light: "lgt", + medium: "med", + heavy: "hvy", + shield: "shl" + }[itemData.data?.armor?.type]; // Player characters check proficiency + const actorArmorProfs = this.data.data.traits?.armorProf?.value || []; + const hasEquipmentProf = armorProf === true || actorArmorProfs.includes(armorProf); + initial["data.proficient"] = hasEquipmentProf; + } + } + break; + + case "power": + initial["data.prepared"] = true; // automatically prepare powers for everyone + break; + } + mergeObject(itemData, initial); + } + + /* -------------------------------------------- */ + /* Gameplay Mechanics */ + /* -------------------------------------------- */ + + /** @override */ + async modifyTokenAttribute(attribute, value, isDelta, isBar) { + if (attribute === "attributes.hp") { + const hp = getProperty(this.data.data, attribute); + const delta = isDelta ? -1 * value : hp.value + hp.temp - value; + return this.applyDamage(delta); + } + return super.modifyTokenAttribute(attribute, value, isDelta, isBar); + } + + /* -------------------------------------------- */ + + /** + * Apply a certain amount of damage or healing to the health pool for Actor + * @param {number} amount An amount of damage (positive) or healing (negative) to sustain + * @param {number} multiplier A multiplier which allows for resistance, vulnerability, or healing + * @return {Promise} A Promise which resolves once the damage has been applied + */ + async applyDamage(amount = 0, multiplier = 1) { + amount = Math.floor(parseInt(amount) * multiplier); + const hp = this.data.data.attributes.hp; + + // Deduct damage from temp HP first + const tmp = parseInt(hp.temp) || 0; + const dt = amount > 0 ? Math.min(tmp, amount) : 0; + + // Remaining goes to health + const tmpMax = parseInt(hp.tempmax) || 0; + const dh = Math.clamped(hp.value - (amount - dt), 0, hp.max + tmpMax); + + // Update the Actor + const updates = { + "data.attributes.hp.temp": tmp - dt, + "data.attributes.hp.value": dh + }; + + // Delegate damage application to a hook + // TODO replace this in the future with a better modifyTokenAttribute function in the core + const allowed = Hooks.call( + "modifyTokenAttribute", + { + attribute: "attributes.hp", + value: amount, + isDelta: false, + isBar: true + }, + updates + ); + return allowed !== false ? this.update(updates) : this; + } + + /* -------------------------------------------- */ + + /** + * Roll a Skill Check + * Prompt the user for input regarding Advantage/Disadvantage and any Situational Bonus + * @param {string} skillId The skill id (e.g. "ins") + * @param {Object} options Options which configure how the skill check is rolled + * @return {Promise} A Promise which resolves to the created Roll instance + */ + rollSkill(skillId, options = {}) { + const skl = this.data.data.skills[skillId]; + const bonuses = getProperty(this.data.data, "bonuses.abilities") || {}; + + // Compose roll parts and data + const parts = ["@mod"]; + const data = {mod: skl.mod + skl.prof}; + + // Ability test bonus + if (bonuses.check) { + data["checkBonus"] = bonuses.check; + parts.push("@checkBonus"); + } + + // Skill check bonus + if (bonuses.skill) { + data["skillBonus"] = bonuses.skill; + parts.push("@skillBonus"); + } + + // Add provided extra roll parts now because they will get clobbered by mergeObject below + if (options.parts?.length > 0) { + parts.push(...options.parts); + } + + // Reliable Talent applies to any skill check we have full or better proficiency in + const reliableTalent = skl.value >= 1 && this.getFlag("sw5e", "reliableTalent"); + + // Roll and return + const rollData = mergeObject(options, { + parts: parts, + data: data, + title: game.i18n.format("SW5E.SkillPromptTitle", { + skill: CONFIG.SW5E.skills[skillId] || CONFIG.SW5E.starshipSkills[skillId] + }), + halflingLucky: this.getFlag("sw5e", "halflingLucky"), + reliableTalent: reliableTalent, + messageData: {"flags.sw5e.roll": {type: "skill", skillId}} + }); + rollData.speaker = options.speaker || ChatMessage.getSpeaker({actor: this}); + return d20Roll(rollData); + } + + /* -------------------------------------------- */ + + /** + * Roll a generic ability test or saving throw. + * Prompt the user for input on which variety of roll they want to do. + * @param {String}abilityId The ability id (e.g. "str") + * @param {Object} options Options which configure how ability tests or saving throws are rolled + */ + rollAbility(abilityId, options = {}) { + const label = CONFIG.SW5E.abilities[abilityId]; + new Dialog({ + title: game.i18n.format("SW5E.AbilityPromptTitle", {ability: label}), + content: `

${game.i18n.format("SW5E.AbilityPromptText", {ability: label})}

`, + buttons: { + test: { + label: game.i18n.localize("SW5E.ActionAbil"), + callback: () => this.rollAbilityTest(abilityId, options) + }, + save: { + label: game.i18n.localize("SW5E.ActionSave"), + callback: () => this.rollAbilitySave(abilityId, options) + } + } + }).render(true); + } + + /* -------------------------------------------- */ + + /** + * Roll an Ability Test + * Prompt the user for input regarding Advantage/Disadvantage and any Situational Bonus + * @param {String} abilityId The ability ID (e.g. "str") + * @param {Object} options Options which configure how ability tests are rolled + * @return {Promise} A Promise which resolves to the created Roll instance + */ + rollAbilityTest(abilityId, options = {}) { + const label = CONFIG.SW5E.abilities[abilityId]; + const abl = this.data.data.abilities[abilityId]; + + // Construct parts + const parts = ["@mod"]; + const data = {mod: abl.mod}; + + // Add feat-related proficiency bonuses + const feats = this.data.flags.sw5e || {}; + if (feats.remarkableAthlete && SW5E.characterFlags.remarkableAthlete.abilities.includes(abilityId)) { + parts.push("@proficiency"); + data.proficiency = Math.ceil(0.5 * this.data.data.attributes.prof); + } else if (feats.jackOfAllTrades) { + parts.push("@proficiency"); + data.proficiency = Math.floor(0.5 * this.data.data.attributes.prof); + } + + // Add global actor bonus + const bonuses = getProperty(this.data.data, "bonuses.abilities") || {}; + if (bonuses.check) { + parts.push("@checkBonus"); + data.checkBonus = bonuses.check; + } + + // Add provided extra roll parts now because they will get clobbered by mergeObject below + if (options.parts?.length > 0) { + parts.push(...options.parts); + } + + // Roll and return + const rollData = mergeObject(options, { + parts: parts, + data: data, + title: game.i18n.format("SW5E.AbilityPromptTitle", {ability: label}), + halflingLucky: feats.halflingLucky, + messageData: {"flags.sw5e.roll": {type: "ability", abilityId}} + }); + rollData.speaker = options.speaker || ChatMessage.getSpeaker({actor: this}); + return d20Roll(rollData); + } + + /* -------------------------------------------- */ + + /** + * Roll an Ability Saving Throw + * Prompt the user for input regarding Advantage/Disadvantage and any Situational Bonus + * @param {String} abilityId The ability ID (e.g. "str") + * @param {Object} options Options which configure how ability tests are rolled + * @return {Promise} A Promise which resolves to the created Roll instance + */ + rollAbilitySave(abilityId, options = {}) { + const label = CONFIG.SW5E.abilities[abilityId]; + const abl = this.data.data.abilities[abilityId]; + + // Construct parts + const parts = ["@mod"]; + const data = {mod: abl.mod}; + + // Include proficiency bonus + if (abl.prof > 0) { + parts.push("@prof"); + data.prof = abl.prof; + } + + // Include a global actor ability save bonus + const bonuses = getProperty(this.data.data, "bonuses.abilities") || {}; + if (bonuses.save) { + parts.push("@saveBonus"); + data.saveBonus = bonuses.save; + } + + // Add provided extra roll parts now because they will get clobbered by mergeObject below + if (options.parts?.length > 0) { + parts.push(...options.parts); + } + + // Roll and return + const rollData = mergeObject(options, { + parts: parts, + data: data, + title: game.i18n.format("SW5E.SavePromptTitle", {ability: label}), + halflingLucky: this.getFlag("sw5e", "halflingLucky"), + messageData: {"flags.sw5e.roll": {type: "save", abilityId}} + }); + rollData.speaker = options.speaker || ChatMessage.getSpeaker({actor: this}); + return d20Roll(rollData); + } + + /* -------------------------------------------- */ + + /** + * Perform a death saving throw, rolling a d20 plus any global save bonuses + * @param {Object} options Additional options which modify the roll + * @return {Promise} A Promise which resolves to the Roll instance + */ + async rollDeathSave(options = {}) { + // Display a warning if we are not at zero HP or if we already have reached 3 + const death = this.data.data.attributes.death; + if (this.data.data.attributes.hp.value > 0 || death.failure >= 3 || death.success >= 3) { + ui.notifications.warn(game.i18n.localize("SW5E.DeathSaveUnnecessary")); + return null; + } + + // Evaluate a global saving throw bonus + const parts = []; + const data = {}; + const speaker = options.speaker || ChatMessage.getSpeaker({actor: this}); + + // Include a global actor ability save bonus + const bonuses = getProperty(this.data.data, "bonuses.abilities") || {}; + if (bonuses.save) { + parts.push("@saveBonus"); + data.saveBonus = bonuses.save; + } + + // Evaluate the roll + const rollData = mergeObject(options, { + parts: parts, + data: data, + title: game.i18n.localize("SW5E.DeathSavingThrow"), + speaker: speaker, + halflingLucky: this.getFlag("sw5e", "halflingLucky"), + targetValue: 10, + messageData: {"flags.sw5e.roll": {type: "death"}} + }); + rollData.speaker = speaker; + const roll = await d20Roll(rollData); + if (!roll) return null; + + // Take action depending on the result + const success = roll.total >= 10; + const d20 = roll.dice[0].total; + + // Save success + if (success) { + let successes = (death.success || 0) + 1; + + // Critical Success = revive with 1hp + if (d20 === 20) { + await this.update({ + "data.attributes.death.success": 0, + "data.attributes.death.failure": 0, + "data.attributes.hp.value": 1 + }); + await ChatMessage.create({ + content: game.i18n.format("SW5E.DeathSaveCriticalSuccess", {name: this.name}), + speaker + }); + } + + // 3 Successes = survive and reset checks + else if (successes === 3) { + await this.update({ + "data.attributes.death.success": 0, + "data.attributes.death.failure": 0 + }); + await ChatMessage.create({ + content: game.i18n.format("SW5E.DeathSaveSuccess", {name: this.name}), + speaker + }); + } + + // Increment successes + else await this.update({"data.attributes.death.success": Math.clamped(successes, 0, 3)}); + } + + // Save failure + else { + let failures = (death.failure || 0) + (d20 === 1 ? 2 : 1); + await this.update({"data.attributes.death.failure": Math.clamped(failures, 0, 3)}); + if (failures >= 3) { + // 3 Failures = death + await ChatMessage.create({ + content: game.i18n.format("SW5E.DeathSaveFailure", {name: this.name}), + speaker + }); + } + } + + // Return the rolled result + return roll; + } + + /* -------------------------------------------- */ + + /** + * Roll a hit die of the appropriate type, gaining hit points equal to the die roll plus your CON modifier + * @param {string} [denomination] The hit denomination of hit die to roll. Example "d8". + * If no denomination is provided, the first available HD will be used + * @param {boolean} [dialog] Show a dialog prompt for configuring the hit die roll? + * @return {Promise} The created Roll instance, or null if no hit die was rolled + */ + async rollHitDie(denomination, {dialog = true} = {}) { + // If no denomination was provided, choose the first available + let cls = null; + if (!denomination) { + cls = this.itemTypes.class.find((c) => c.data.data.hitDiceUsed < c.data.data.levels); + if (!cls) return null; + denomination = cls.data.data.hitDice; + } + + // Otherwise locate a class (if any) which has an available hit die of the requested denomination + else { + cls = this.items.find((i) => { + const d = i.data.data; + return d.hitDice === denomination && (d.hitDiceUsed || 0) < (d.levels || 1); + }); + } + + // If no class is available, display an error notification + if (!cls) { + ui.notifications.error(game.i18n.format("SW5E.HitDiceWarn", {name: this.name, formula: denomination})); + return null; + } + + // Prepare roll data + const parts = [`1${denomination}`, "@abilities.con.mod"]; + const title = game.i18n.localize("SW5E.HitDiceRoll"); + const rollData = duplicate(this.data.data); + + // Call the roll helper utility + const roll = await damageRoll({ + event: new Event("hitDie"), + parts: parts, + data: rollData, + title: title, + speaker: ChatMessage.getSpeaker({actor: this}), + allowcritical: false, + fastForward: !dialog, + dialogOptions: {width: 350}, + messageData: {"flags.sw5e.roll": {type: "hitDie"}} + }); + if (!roll) return null; + + // Adjust actor data + await cls.update({"data.hitDiceUsed": cls.data.data.hitDiceUsed + 1}); + const hp = this.data.data.attributes.hp; + const dhp = Math.min(hp.max + (hp.tempmax ?? 0) - hp.value, roll.total); + await this.update({"data.attributes.hp.value": hp.value + dhp}); + return roll; + } + + /* -------------------------------------------- */ + + /** + * Roll a hull die of the appropriate type, gaining hull points equal to the die roll plus your CON modifier + * @param {string} [denomination] The hit denomination of hull die to roll. Example "d8". + * If no denomination is provided, the first available HD will be used + * @param {string} [numDice] How many damage dice to roll? + * @param {string} [keep] Which dice to keep? Example "kh1". + * @param {boolean} [dialog] Show a dialog prompt for configuring the hull die roll? + * @return {Promise} The created Roll instance, or null if no hull die was rolled + */ + async rollHullDie(denomination, numDice = "1", keep = "", {dialog = true} = {}) { + // If no denomination was provided, choose the first available + let sship = null; + if (!denomination) { + sship = this.itemTypes.class.find( + (s) => s.data.data.hullDiceUsed < s.data.data.tier + s.data.data.hullDiceStart + ); + if (!sship) return null; + denomination = sship.data.data.hullDice; + } + + // Otherwise locate a starship (if any) which has an available hit die of the requested denomination + else { + sship = this.items.find((i) => { + const d = i.data.data; + return d.hullDice === denomination && (d.hitDiceUsed || 0) < (d.tier || 0) + d.hullDiceStart; + }); + } + + // If no class is available, display an error notification + if (!sship) { + ui.notifications.error(game.i18n.format("SW5E.HullDiceWarn", {name: this.name, formula: denomination})); + return null; + } + + // Prepare roll data + const parts = [`${numDice}${denomination}${keep}`, "@abilities.con.mod"]; + const title = game.i18n.localize("SW5E.HullDiceRoll"); + const rollData = duplicate(this.data.data); + + // Call the roll helper utility + const roll = await damageRoll({ + event: new Event("hitDie"), + parts: parts, + data: rollData, + title: title, + speaker: ChatMessage.getSpeaker({actor: this}), + allowcritical: false, + fastForward: !dialog, + dialogOptions: {width: 350}, + messageData: {"flags.sw5e.roll": {type: "hullDie"}} + }); + if (!roll) return null; + + // Adjust actor data + await sship.update({"data.hullDiceUsed": sship.data.data.hullDiceUsed + 1}); + const hp = this.data.data.attributes.hp; + const dhp = Math.min(hp.max - hp.value, roll.total); + await this.update({"data.attributes.hp.value": hp.value + dhp}); + return roll; + } + + /* -------------------------------------------- */ + + /** + * Roll a hull die of the appropriate type, gaining hull points equal to the die roll plus your CON modifier + * @return {Promise} The created Roll instance, or null if no hull die was rolled + */ + async rollHullDieCheck() { + // If no denomination was provided, choose the first available + let sship = null; + if (!denomination) { + sship = this.itemTypes.class.find( + (s) => s.data.data.hullDiceUsed < s.data.data.tier + s.data.data.hullDiceStart + ); + if (!sship) return null; + denomination = sship.data.data.hullDice; + } + + // Otherwise locate a starship (if any) which has an available hit die of the requested denomination + else { + sship = this.items.find((i) => { + const d = i.data.data; + return d.hullDice === denomination && (d.hitDiceUsed || 0) < (d.tier || 0) + d.hullDiceStart; + }); + } + + // If no class is available, display an error notification + if (!sship) { + ui.notifications.error(game.i18n.format("SW5E.HullDiceWarn", {name: this.name, formula: denomination})); + return null; + } + + // Prepare roll data + const parts = [`${numDice}${denomination}${keep}`, "@abilities.con.mod"]; + const title = game.i18n.localize("SW5E.HullDiceRoll"); + const rollData = duplicate(this.data.data); + + // Call the roll helper utility + const roll = await damageRoll({ + event: new Event("hitDie"), + parts: parts, + data: rollData, + title: title, + speaker: ChatMessage.getSpeaker({actor: this}), + allowcritical: false, + fastForward: !dialog, + dialogOptions: {width: 350}, + messageData: {"flags.sw5e.roll": {type: "hullDie"}} + }); + if (!roll) return null; + + // Adjust actor data + await sship.update({"data.hullDiceUsed": sship.data.data.hullDiceUsed + 1}); + const hp = this.data.data.attributes.hp; + const dhp = Math.min(hp.max - hp.value, roll.total); + await this.update({"data.attributes.hp.value": hp.value + dhp}); + return roll; + } + + /* -------------------------------------------- */ + + /** + * Roll a shield die of the appropriate type, gaining shield points equal to the die roll + * multiplied by the shield regeneration coefficient + * @param {string} [denomination] The denomination of shield die to roll. Example "d8". + * If no denomination is provided, the first available SD will be used + * @param {boolean} [natural] Natural ship shield regeneration (true) or user action (false)? + * @param {string} [numDice] How many damage dice to roll? + * @param {string} [keep] Which dice to keep? Example "kh1". + * @param {boolean} [dialog] Show a dialog prompt for configuring the shield die roll? + * @return {Promise} The created Roll instance, or null if no shield die was rolled + */ + async rollShieldDie(denomination, natural = false, numDice = "1", keep = "", {dialog = true} = {}) { + // If no denomination was provided, choose the first available + let sship = null; + if (!denomination) { + sship = this.itemTypes.class.find( + (s) => s.data.data.shldDiceUsed < s.data.data.tier + s.data.data.shldDiceStart + ); + if (!sship) return null; + denomination = sship.data.data.shldDice; + } + + // Otherwise locate a starship (if any) which has an available hit die of the requested denomination + else { + sship = this.items.find((i) => { + const d = i.data.data; + return d.shldDice === denomination && (d.shldDiceUsed || 0) < (d.tier || 0) + d.shldDiceStart; + }); + } + + // If no starship is available, display an error notification + if (!sship) { + ui.notifications.error(game.i18n.format("SW5E.ShldDiceWarn", {name: this.name, formula: denomination})); + return null; + } + + // if natural regeneration roll max + if (natural) { + numdice = denomination.substring(1); + denomination = ""; + keep = ""; + } + + // Prepare roll data + const parts = [`${numDice}${denomination}${keep} * @attributes.regenRate`]; + const title = game.i18n.localize("SW5E.ShieldDiceRoll"); + const rollData = duplicate(this.data.data); + + // Call the roll helper utility + roll = await damageRoll({ + event: new Event("shldDie"), + parts: parts, + data: rollData, + title: title, + speaker: ChatMessage.getSpeaker({actor: this}), + allowcritical: false, + fastForward: !dialog, + dialogOptions: {width: 350}, + messageData: {"flags.sw5e.roll": {type: "shldDie"}} + }); + if (!roll) return null; + + // Adjust actor data + await sship.update({"data.shldDiceUsed": sship.data.data.shldDiceUsed + 1}); + const hp = this.data.data.attributes.hp; + const dhp = Math.min(hp.tempmax - hp.temp, roll.total); + await this.update({"data.attributes.hp.temp": hp.temp + dhp}); + return roll; + } + + /* -------------------------------------------- */ + + /** + * Cause this Actor to take a Short Rest and regain all Tech Points + * During a Short Rest resources and limited item uses may be recovered + * @param {boolean} dialog Present a dialog window which allows for rolling hit dice as part of the Short Rest + * @param {boolean} chat Summarize the results of the rest workflow as a chat message + * @param {boolean} autoHD Automatically spend Hit Dice if you are missing 3 or more hit points + * @param {boolean} autoHDThreshold A number of missing hit points which would trigger an automatic HD roll + * @return {Promise} A Promise which resolves once the short rest workflow has completed + */ + async shortRest({dialog = true, chat = true, autoHD = false, autoHDThreshold = 3} = {}) { + // Take note of the initial hit points and number of hit dice the Actor has + const hp = this.data.data.attributes.hp; + const hd0 = this.data.data.attributes.hd; + const hp0 = hp.value; + let newDay = false; + + // Display a Dialog for rolling hit dice + if (dialog) { + try { + newDay = await ShortRestDialog.shortRestDialog({actor: this, canRoll: hd0 > 0}); + } catch (err) { + return; + } + } + + // Automatically spend hit dice + else if (autoHD) { + while (hp.value + autoHDThreshold <= hp.max) { + const r = await this.rollHitDie(undefined, {dialog: false}); + if (r === null) break; + } + } + + // Note the change in HP and HD and TP which occurred + const dhd = this.data.data.attributes.hd - hd0; + const dhp = this.data.data.attributes.hp.value - hp0; + const dtp = this.data.data.attributes.tech.points.max - this.data.data.attributes.tech.points.value; + + // Automatically Retore Tech Points + this.update({"data.attributes.tech.points.value": this.data.data.attributes.tech.points.max}); + + // Recover character resources + const updateData = {}; + for (let [k, r] of Object.entries(this.data.data.resources)) { + if (r.max && r.sr) { + updateData[`data.resources.${k}.value`] = r.max; + } + } + + // Recover item uses + const recovery = newDay ? ["sr", "day"] : ["sr"]; + const items = this.items.filter((item) => item.data.data.uses && recovery.includes(item.data.data.uses.per)); + const updateItems = items.map((item) => { + return { + "_id": item._id, + "data.uses.value": item.data.data.uses.max + }; + }); + await this.updateEmbeddedEntity("OwnedItem", updateItems); + + // Display a Chat Message summarizing the rest effects + if (chat) { + // Summarize the rest duration + let restFlavor; + switch (game.settings.get("sw5e", "restVariant")) { + case "normal": + restFlavor = game.i18n.localize("SW5E.ShortRestNormal"); + break; + case "gritty": + restFlavor = game.i18n.localize(newDay ? "SW5E.ShortRestOvernight" : "SW5E.ShortRestGritty"); + break; + case "epic": + restFlavor = game.i18n.localize("SW5E.ShortRestEpic"); + break; + } + + // Summarize the health effects + let srMessage = "SW5E.ShortRestResultShort"; + if (dhd !== 0 && dhp !== 0) { + if (dtp !== 0) { + srMessage = "SW5E.ShortRestResultWithTech"; + } else { + srMessage = "SW5E.ShortRestResult"; + } + } else { + if (dtp !== 0) { + srMessage = "SW5E.ShortRestResultOnlyTech"; + } + } + + // Create a chat message + ChatMessage.create({ + user: game.user._id, + speaker: {actor: this, alias: this.name}, + flavor: restFlavor, + content: game.i18n.format(srMessage, {name: this.name, dice: -dhd, health: dhp, tech: dtp}) + }); + } + + // Return data summarizing the rest effects + return { + dhd: dhd, + dhp: dhp, + dtp: dtp, + updateData: updateData, + updateItems: updateItems, + newDay: newDay + }; + } + + /* -------------------------------------------- */ + + /** + * Take a long rest, recovering HP, HD, resources, Force and Power points and power slots + * @param {boolean} dialog Present a confirmation dialog window whether or not to take a long rest + * @param {boolean} chat Summarize the results of the rest workflow as a chat message + * @param {boolean} newDay Whether the long rest carries over to a new day + * @return {Promise} A Promise which resolves once the long rest workflow has completed + */ + async longRest({dialog = true, chat = true, newDay = true} = {}) { + const data = this.data.data; + + // Maybe present a confirmation dialog + if (dialog) { + try { + newDay = await LongRestDialog.longRestDialog({actor: this}); + } catch (err) { + return; + } + } + + // Recover hit, tech, and force points to full, and eliminate any existing temporary HP, TP, and FP + const dhp = data.attributes.hp.max - data.attributes.hp.value; + const dtp = data.attributes.tech.points.max - data.attributes.tech.points.value; + const dfp = data.attributes.force.points.max - data.attributes.force.points.value; + const updateData = { + "data.attributes.hp.value": data.attributes.hp.max, + "data.attributes.hp.temp": 0, + "data.attributes.hp.tempmax": 0, + "data.attributes.tech.points.value": data.attributes.tech.points.max, + "data.attributes.tech.points.temp": 0, + "data.attributes.tech.points.tempmax": 0, + "data.attributes.force.points.value": data.attributes.force.points.max, + "data.attributes.force.points.temp": 0, + "data.attributes.force.points.tempmax": 0 + }; + + // Recover character resources + for (let [k, r] of Object.entries(data.resources)) { + if (r.max && (r.sr || r.lr)) { + updateData[`data.resources.${k}.value`] = r.max; + } + } + + // Recover power slots + for (let [k, v] of Object.entries(data.powers)) { + updateData[`data.powers.${k}.fvalue`] = Number.isNumeric(v.foverride) ? v.foverride : v.fmax ?? 0; + } + for (let [k, v] of Object.entries(data.powers)) { + updateData[`data.powers.${k}.tvalue`] = Number.isNumeric(v.toverride) ? v.toverride : v.tmax ?? 0; + } + // Determine the number of hit dice which may be recovered + let recoverHD = Math.max(Math.floor(data.details.level / 2), 1); + let dhd = 0; + + // Sort classes which can recover HD, assuming players prefer recovering larger HD first. + const updateItems = this.items + .filter((item) => item.data.type === "class") + .sort((a, b) => { + let da = parseInt(a.data.data.hitDice.slice(1)) || 0; + let db = parseInt(b.data.data.hitDice.slice(1)) || 0; + return db - da; + }) + .reduce((updates, item) => { + const d = item.data.data; + if (recoverHD > 0 && d.hitDiceUsed > 0) { + let delta = Math.min(d.hitDiceUsed || 0, recoverHD); + recoverHD -= delta; + dhd += delta; + updates.push({"_id": item.id, "data.hitDiceUsed": d.hitDiceUsed - delta}); + } + return updates; + }, []); + + // Iterate over owned items, restoring uses per day and recovering Hit Dice + const recovery = newDay ? ["sr", "lr", "day"] : ["sr", "lr"]; + for (let item of this.items) { + const d = item.data.data; + if (d.uses && recovery.includes(d.uses.per)) { + updateItems.push({"_id": item.id, "data.uses.value": d.uses.max}); + } else if (d.recharge && d.recharge.value) { + updateItems.push({"_id": item.id, "data.recharge.charged": true}); + } + } + + // Perform the updates + await this.update(updateData); + if (updateItems.length) await this.updateEmbeddedEntity("OwnedItem", updateItems); + + // Display a Chat Message summarizing the rest effects + let restFlavor; + switch (game.settings.get("sw5e", "restVariant")) { + case "normal": + restFlavor = game.i18n.localize(newDay ? "SW5E.LongRestOvernight" : "SW5E.LongRestNormal"); + break; + case "gritty": + restFlavor = game.i18n.localize("SW5E.LongRestGritty"); + break; + case "epic": + restFlavor = game.i18n.localize("SW5E.LongRestEpic"); + break; + } + + // Determine the chat message to display + if (chat) { + let lrMessage = "SW5E.LongRestResult"; + if (dhp !== 0) lrMessage += "HP"; + if (dfp !== 0) lrMessage += "FP"; + if (dtp !== 0) lrMessage += "TP"; + if (dhd !== 0) lrMessage += "HD"; + ChatMessage.create({ + user: game.user._id, + speaker: {actor: this, alias: this.name}, + flavor: restFlavor, + content: game.i18n.format(lrMessage, {name: this.name, health: dhp, tech: dtp, force: dfp, dice: dhd}) + }); + } + + // Return data summarizing the rest effects + return { + dhd: dhd, + dhp: dhp, + dtp: dtp, + dfp: dfp, + updateData: updateData, + updateItems: updateItems, + newDay: newDay + }; + } + + /* -------------------------------------------- */ + + /** + * Deploy an Actor into this one. + * + * @param {Actor} target The Actor to be deployed. + * @param {boolean} [coord] Deploy as Coordinator + * @param {boolean} [gunner] Deploy as Gunner + * @param {boolean} [mech] Deploy as Mechanic + * @param {boolean} [oper] Deploy as Operator + * @param {boolean} [pilot] Deploy as Pilot + * @param {boolean} [tech] Deploy as Technician + * @param {boolean} [crew] Deploy as Crew + * @param {boolean} [pass] Deploy as Passenger + */ + async deployInto( + target, + { + coord = false, + gunner = false, + mech = false, + oper = false, + pilot = false, + tech = false, + crew = false, + pass = false + } = {} + ) { + // Get the starship Actor data and the new char data + const sship = duplicate(this.toJSON()); + const ssDeploy = sship.data.attributes.deployment; + const char = target; + const charUUID = char.uuid; + const charName = char.data.name; + const charRank = char.data.data.attributes.rank; + let charProf = 0; + if (charRank.total > 0) { + charProf = char.data.data.attributes.prof; + } + + if (coord) { + ssDeploy.coord.uuid = charUUID; + ssDeploy.coord.name = charName; + ssDeploy.coord.rank = charRank.coord; + ssDeploy.coord.prof = charProf; + } + + if (gunner) { + ssDeploy.gunner.uuid = charUUID; + ssDeploy.gunner.name = charName; + ssDeploy.gunner.rank = charRank.gunner; + ssDeploy.gunner.prof = charProf; + } + + if (mech) { + ssDeploy.mechanic.uuid = charUUID; + ssDeploy.mechanic.name = charName; + ssDeploy.mechanic.rank = charRank.mechanic; + ssDeploy.mechanic.prof = charProf; + } + + if (oper) { + ssDeploy.operator.uuid = charUUID; + ssDeploy.operator.name = charName; + ssDeploy.operator.rank = charRank.operator; + ssDeploy.operator.prof = charProf; + } + + if (pilot) { + ssDeploy.pilot.uuid = charUUID; + ssDeploy.pilot.name = charName; + ssDeploy.pilot.rank = charRank.pilot; + ssDeploy.pilot.prof = charProf; + } + + if (tech) { + ssDeploy.technician.uuid = charUUID; + ssDeploy.technician.name = charName; + ssDeploy.technician.rank = charRank.technician; + ssDeploy.technician.prof = charProf; + } + + if (crew) { + ssDeploy.crew.push({uuid: charUUID, name: charName, rank: charRank, prof: charProf}); + } + + if (pass) { + ssDeploy.passenger.push({uuid: charUUID, name: charName, rank: charRank, prof: charProf}); + } + this.update({"data.attributes.deployment": ssDeploy}); + } + + /** + * Transform this Actor into another one. + * + * @param {Actor} target The target Actor. + * @param {boolean} [keepPhysical] Keep physical abilities (str, dex, con) + * @param {boolean} [keepMental] Keep mental abilities (int, wis, cha) + * @param {boolean} [keepSaves] Keep saving throw proficiencies + * @param {boolean} [keepSkills] Keep skill proficiencies + * @param {boolean} [mergeSaves] Take the maximum of the save proficiencies + * @param {boolean} [mergeSkills] Take the maximum of the skill proficiencies + * @param {boolean} [keepClass] Keep proficiency bonus + * @param {boolean} [keepFeats] Keep features + * @param {boolean} [keepPowers] Keep powers + * @param {boolean} [keepItems] Keep items + * @param {boolean} [keepBio] Keep biography + * @param {boolean} [keepVision] Keep vision + * @param {boolean} [transformTokens] Transform linked tokens too + */ + async transformInto( + target, + { + keepPhysical = false, + keepMental = false, + keepSaves = false, + keepSkills = false, + mergeSaves = false, + mergeSkills = false, + keepClass = false, + keepFeats = false, + keepPowers = false, + keepItems = false, + keepBio = false, + keepVision = false, + transformTokens = true + } = {} + ) { + // Ensure the player is allowed to polymorph + const allowed = game.settings.get("sw5e", "allowPolymorphing"); + if (!allowed && !game.user.isGM) { + return ui.notifications.warn(game.i18n.localize("SW5E.PolymorphWarn")); + } + + // Get the original Actor data and the new source data + const o = duplicate(this.toJSON()); + o.flags.sw5e = o.flags.sw5e || {}; + o.flags.sw5e.transformOptions = {mergeSkills, mergeSaves}; + const source = duplicate(target.toJSON()); + + // Prepare new data to merge from the source + const d = { + type: o.type, // Remain the same actor type + name: `${o.name} (${source.name})`, // Append the new shape to your old name + data: source.data, // Get the data model of your new form + items: source.items, // Get the items of your new form + effects: o.effects.concat(source.effects), // Combine active effects from both forms + token: source.token, // New token configuration + img: source.img, // New appearance + permission: o.permission, // Use the original actor permissions + folder: o.folder, // Be displayed in the same sidebar folder + flags: o.flags // Use the original actor flags + }; + + // Additional adjustments + delete d.data.resources; // Don't change your resource pools + delete d.data.currency; // Don't lose currency + delete d.data.bonuses; // Don't lose global bonuses + delete d.token.actorId; // Don't reference the old actor ID + d.token.actorLink = o.token.actorLink; // Keep your actor link + d.token.name = d.name; // Token name same as actor name + d.data.details.alignment = o.data.details.alignment; // Don't change alignment + d.data.attributes.exhaustion = o.data.attributes.exhaustion; // Keep your prior exhaustion level + d.data.attributes.inspiration = o.data.attributes.inspiration; // Keep inspiration + d.data.powers = o.data.powers; // Keep power slots + + // Handle wildcard + if (source.token.randomImg) { + const images = await target.getTokenImages(); + d.token.img = images[Math.floor(Math.random() * images.length)]; + } + + // Keep Token configurations + const tokenConfig = ["displayName", "vision", "actorLink", "disposition", "displayBars", "bar1", "bar2"]; + if (keepVision) { + tokenConfig.push(...["dimSight", "brightSight", "dimLight", "brightLight", "vision", "sightAngle"]); + } + for (let c of tokenConfig) { + d.token[c] = o.token[c]; + } + + // Transfer ability scores + const abilities = d.data.abilities; + for (let k of Object.keys(abilities)) { + const oa = o.data.abilities[k]; + const prof = abilities[k].proficient; + if (keepPhysical && ["str", "dex", "con"].includes(k)) abilities[k] = oa; + else if (keepMental && ["int", "wis", "cha"].includes(k)) abilities[k] = oa; + if (keepSaves) abilities[k].proficient = oa.proficient; + else if (mergeSaves) abilities[k].proficient = Math.max(prof, oa.proficient); + } + + // Transfer skills + if (keepSkills) d.data.skills = o.data.skills; + else if (mergeSkills) { + for (let [k, s] of Object.entries(d.data.skills)) { + s.value = Math.max(s.value, o.data.skills[k].value); + } + } + + // Keep specific items from the original data + d.items = d.items.concat( + o.items.filter((i) => { + if (i.type === "class") return keepClass; + else if (i.type === "feat") return keepFeats; + else if (i.type === "power") return keepPowers; + else return keepItems; + }) + ); + + // Transfer classes for NPCs + if (!keepClass && d.data.details.cr) { + d.items.push({ + type: "class", + name: game.i18n.localize("SW5E.PolymorphTmpClass"), + data: {levels: d.data.details.cr} + }); + } + + // Keep biography + if (keepBio) d.data.details.biography = o.data.details.biography; + + // Keep senses + if (keepVision) d.data.traits.senses = o.data.traits.senses; + + // Set new data flags + if (!this.isPolymorphed || !d.flags.sw5e.originalActor) d.flags.sw5e.originalActor = this.id; + d.flags.sw5e.isPolymorphed = true; + + // Update unlinked Tokens in place since they can simply be re-dropped from the base actor + if (this.isToken) { + const tokenData = d.token; + tokenData.actorData = d; + delete tokenData.actorData.token; + return this.token.update(tokenData); + } + + // Update regular Actors by creating a new Actor with the Polymorphed data + await this.sheet.close(); + Hooks.callAll("sw5e.transformActor", this, target, d, { + keepPhysical, + keepMental, + keepSaves, + keepSkills, + mergeSaves, + mergeSkills, + keepClass, + keepFeats, + keepPowers, + keepItems, + keepBio, + keepVision, + transformTokens + }); + const newActor = await this.constructor.create(d, {renderSheet: true}); + + // Update placed Token instances + if (!transformTokens) return; + const tokens = this.getActiveTokens(true); + const updates = tokens.map((t) => { + const newTokenData = duplicate(d.token); + if (!t.data.actorLink) newTokenData.actorData = newActor.data; + newTokenData._id = t.data._id; + newTokenData.actorId = newActor.id; + return newTokenData; + }); + return canvas.scene?.updateEmbeddedEntity("Token", updates); + } + + /* -------------------------------------------- */ + + /** + * If this actor was transformed with transformTokens enabled, then its + * active tokens need to be returned to their original state. If not, then + * we can safely just delete this actor. + */ + async revertOriginalForm() { + if (!this.isPolymorphed) return; + if (!this.owner) { + return ui.notifications.warn(game.i18n.localize("SW5E.PolymorphRevertWarn")); + } + + // If we are reverting an unlinked token, simply replace it with the base actor prototype + if (this.isToken) { + const baseActor = game.actors.get(this.token.data.actorId); + const prototypeTokenData = duplicate(baseActor.token); + prototypeTokenData.actorData = null; + return this.token.update(prototypeTokenData); + } + + // Obtain a reference to the original actor + const original = game.actors.get(this.getFlag("sw5e", "originalActor")); + if (!original) return; + + // Get the Tokens which represent this actor + if (canvas.ready) { + const tokens = this.getActiveTokens(true); + const tokenUpdates = tokens.map((t) => { + const tokenData = duplicate(original.data.token); + tokenData._id = t.id; + tokenData.actorId = original.id; + return tokenData; + }); + canvas.scene.updateEmbeddedEntity("Token", tokenUpdates); + } + + // Delete the polymorphed Actor and maybe re-render the original sheet + const isRendered = this.sheet.rendered; + if (game.user.isGM) await this.delete(); + original.sheet.render(isRendered); + return original; + } + + /* -------------------------------------------- */ + + /** + * Add additional system-specific sidebar directory context menu options for SW5e Actor entities + * @param {jQuery} html The sidebar HTML + * @param {Array} entryOptions The default array of context menu options + */ + static addDirectoryContextOptions(html, entryOptions) { + entryOptions.push({ + name: "SW5E.PolymorphRestoreTransformation", + icon: '', + callback: (li) => { + const actor = game.actors.get(li.data("entityId")); + return actor.revertOriginalForm(); + }, + condition: (li) => { + const allowed = game.settings.get("sw5e", "allowPolymorphing"); + if (!allowed && !game.user.isGM) return false; + const actor = game.actors.get(li.data("entityId")); + return actor && actor.isPolymorphed; + } + }); + } + + /* -------------------------------------------- */ + /* DEPRECATED METHODS */ + /* -------------------------------------------- */ + + /** + * @deprecated since sw5e 0.97 + */ + getPowerDC(ability) { + console.warn( + `The Actor5e#getPowerDC(ability) method has been deprecated in favor of Actor5e#data.data.abilities[ability].dc` + ); + return this.data.data.abilities[ability]?.dc; + } + + /* -------------------------------------------- */ + + /** + * Cast a Power, consuming a power slot of a certain level + * @param {Item5e} item The power being cast by the actor + * @param {Event} event The originating user interaction which triggered the cast + * @deprecated since sw5e 1.2.0 + */ + async usePower(item, {configureDialog = true} = {}) { + console.warn(`The Actor5e#usePower method has been deprecated in favor of Item5e#roll`); + if (item.data.type !== "power") throw new Error("Wrong Item type"); + return item.roll(); + } +} diff --git a/module/actor/sheets/newSheet/base.js b/module/actor/sheets/newSheet/base.js index 476c3b73..0b2136bc 100644 --- a/module/actor/sheets/newSheet/base.js +++ b/module/actor/sheets/newSheet/base.js @@ -5,7 +5,7 @@ import ActorHitDiceConfig from "../../../apps/hit-dice-config.js"; import ActorMovementConfig from "../../../apps/movement-config.js"; import ActorSensesConfig from "../../../apps/senses-config.js"; import ActorTypeConfig from "../../../apps/actor-type.js"; -import {SW5E} from '../../../config.js'; +import {SW5E} from "../../../config.js"; import {onManageActiveEffect, prepareActiveEffectCategories} from "../../../effects.js"; /** @@ -14,990 +14,976 @@ import {onManageActiveEffect, prepareActiveEffectCategories} from "../../../effe * @extends {ActorSheet} */ export default class ActorSheet5e extends ActorSheet { - constructor(...args) { - super(...args); + constructor(...args) { + super(...args); + + /** + * Track the set of item filters which are applied + * @type {Set} + */ + this._filters = { + inventory: new Set(), + forcePowerbook: new Set(), + techPowerbook: new Set(), + features: new Set(), + effects: new Set() + }; + } + + /* -------------------------------------------- */ + + /** @override */ + static get defaultOptions() { + return mergeObject(super.defaultOptions, { + scrollY: [ + ".inventory .group-list", + ".features .group-list", + ".force-powerbook .group-list", + ".tech-powerbook .group-list", + ".effects .effects-list" + ], + tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}] + }); + } + + /* -------------------------------------------- */ /** - * Track the set of item filters which are applied - * @type {Set} + * A set of item types that should be prevented from being dropped on this type of actor sheet. + * @type {Set} */ - this._filters = { - inventory: new Set(), - forcePowerbook: new Set(), - techPowerbook: new Set(), - features: new Set(), - effects: new Set() - }; - } + static unsupportedItemTypes = new Set(); - /* -------------------------------------------- */ + /* -------------------------------------------- */ - /** @override */ - static get defaultOptions() { - return mergeObject(super.defaultOptions, { - scrollY: [ - ".inventory .group-list", - ".features .group-list", - ".force-powerbook .group-list", - ".tech-powerbook .group-list", - ".effects .effects-list" - ], - tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}] - }); - } - - /* -------------------------------------------- */ - - /** - * A set of item types that should be prevented from being dropped on this type of actor sheet. - * @type {Set} - */ - static unsupportedItemTypes = new Set(); - - /* -------------------------------------------- */ - - /** @override */ - get template() { - if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/newActor/expanded-limited-sheet.html"; - return `systems/sw5e/templates/actors/newActor/${this.actor.data.type}-sheet.html`; - } - - /* -------------------------------------------- */ - - /** @override */ - getData(options) { - - // Basic data - let isOwner = this.actor.isOwner; - const data = { - owner: isOwner, - limited: this.actor.limited, - options: this.options, - editable: this.isEditable, - cssClass: isOwner ? "editable" : "locked", - isCharacter: this.actor.type === "character", - isNPC: this.actor.type === "npc", - isStarship: this.actor.type === "starship", - isVehicle: this.actor.type === 'vehicle', - config: CONFIG.SW5E, - rollData: this.actor.getRollData.bind(this.actor) - }; - - // The Actor's data - const actorData = this.actor.data.toObject(false); - data.actor = actorData; - data.data = actorData.data; - - // Owned Items - data.items = actorData.items; - for ( let i of data.items ) { - const item = this.actor.items.get(i._id); - i.labels = item.labels; - } - data.items.sort((a, b) => (a.sort || 0) - (b.sort || 0)); - - // Labels and filters - data.labels = this.actor.labels || {}; - data.filters = this._filters; - - // Ability Scores - for ( let [a, abl] of Object.entries(actorData.data.abilities)) { - abl.icon = this._getProficiencyIcon(abl.proficient); - abl.hover = CONFIG.SW5E.proficiencyLevels[abl.proficient]; - abl.label = CONFIG.SW5E.abilities[a]; + /** @override */ + get template() { + if (!game.user.isGM && this.actor.limited) + return "systems/sw5e/templates/actors/newActor/expanded-limited-sheet.html"; + return `systems/sw5e/templates/actors/newActor/${this.actor.data.type}-sheet.html`; } - // Skills - if (actorData.data.skills) { - for (let [s, skl] of Object.entries(actorData.data.skills)) { - skl.ability = CONFIG.SW5E.abilityAbbreviations[skl.ability]; - skl.icon = this._getProficiencyIcon(skl.value); - skl.hover = CONFIG.SW5E.proficiencyLevels[skl.value]; - if (data.actor.type === "starship") { - skl.label = CONFIG.SW5E.starshipSkills[s]; - }else{ - skl.label = CONFIG.SW5E.skills[s]; + /* -------------------------------------------- */ + + /** @override */ + getData(options) { + // Basic data + let isOwner = this.actor.isOwner; + const data = { + owner: isOwner, + limited: this.actor.limited, + options: this.options, + editable: this.isEditable, + cssClass: isOwner ? "editable" : "locked", + isCharacter: this.actor.type === "character", + isNPC: this.actor.type === "npc", + isStarship: this.actor.type === "starship", + isVehicle: this.actor.type === "vehicle", + config: CONFIG.SW5E, + rollData: this.actor.getRollData.bind(this.actor) + }; + + // The Actor's data + const actorData = this.actor.data.toObject(false); + data.actor = actorData; + data.data = actorData.data; + + // Owned Items + data.items = actorData.items; + for (let i of data.items) { + const item = this.actor.items.get(i._id); + i.labels = item.labels; } - } - } + data.items.sort((a, b) => (a.sort || 0) - (b.sort || 0)); - // Movement speeds - data.movement = this._getMovementSpeed(actorData); + // Labels and filters + data.labels = this.actor.labels || {}; + data.filters = this._filters; - // Senses - data.senses = this._getSenses(actorData); - - // Update traits - this._prepareTraits(actorData.data.traits); - - // Prepare owned items - this._prepareItems(data); - - // Prepare active effects - data.effects = prepareActiveEffectCategories(this.actor.effects); - - // Return data to the sheet - return data - } - - /* -------------------------------------------- */ - - /** - * Prepare the display of movement speed data for the Actor* - * @param {object} actorData The Actor data being prepared. - * @param {boolean} [largestPrimary=false] Show the largest movement speed as "primary", otherwise show "walk" - * @returns {{primary: string, special: string}} - * @private - */ - _getMovementSpeed(actorData, largestPrimary=false) { - const movement = actorData.data.attributes.movement || {}; - - // Prepare an array of available movement speeds - let speeds = [ - [movement.burrow, `${game.i18n.localize("SW5E.MovementBurrow")} ${movement.burrow}`], - [movement.climb, `${game.i18n.localize("SW5E.MovementClimb")} ${movement.climb}`], - [movement.fly, `${game.i18n.localize("SW5E.MovementFly")} ${movement.fly}` + (movement.hover ? ` (${game.i18n.localize("SW5E.MovementHover")})` : "")], - [movement.swim, `${game.i18n.localize("SW5E.MovementSwim")} ${movement.swim}`] - ] - if ( largestPrimary ) { - speeds.push([movement.walk, `${game.i18n.localize("SW5E.MovementWalk")} ${movement.walk}`]); - } - - // Filter and sort speeds on their values - speeds = speeds.filter(s => !!s[0]).sort((a, b) => b[0] - a[0]); - - // Case 1: Largest as primary - if ( largestPrimary ) { - let primary = speeds.shift(); - return { - primary: `${primary ? primary[1] : "0"} ${movement.units}`, - special: speeds.map(s => s[1]).join(", ") - } - } - - // Case 2: Walk as primary - else { - return { - primary: `${movement.walk || 0} ${movement.units}`, - special: speeds.length ? speeds.map(s => s[1]).join(", ") : "" - } - } - } - - /* -------------------------------------------- */ - - _getSenses(actorData) { - const senses = actorData.data.attributes.senses || {}; - const tags = {}; - for ( let [k, label] of Object.entries(CONFIG.SW5E.senses) ) { - const v = senses[k] ?? 0 - if ( v === 0 ) continue; - tags[k] = `${game.i18n.localize(label)} ${v} ${senses.units}`; - } - if ( !!senses.special ) tags["special"] = senses.special; - return tags; - } - - /* -------------------------------------------- */ - - /** - * Prepare the data structure for traits data like languages, resistances & vulnerabilities, and proficiencies - * @param {object} traits The raw traits data object from the actor data - * @private - */ - _prepareTraits(traits) { - const map = { - "dr": CONFIG.SW5E.damageResistanceTypes, - "di": CONFIG.SW5E.damageResistanceTypes, - "dv": CONFIG.SW5E.damageResistanceTypes, - "ci": CONFIG.SW5E.conditionTypes, - "languages": CONFIG.SW5E.languages, - "armorProf": CONFIG.SW5E.armorProficiencies, - "weaponProf": CONFIG.SW5E.weaponProficiencies, - "toolProf": CONFIG.SW5E.toolProficiencies - }; - for ( let [t, choices] of Object.entries(map) ) { - const trait = traits[t]; - if ( !trait ) continue; - let values = []; - if ( trait.value ) { - values = trait.value instanceof Array ? trait.value : [trait.value]; - } - trait.selected = values.reduce((obj, t) => { - obj[t] = choices[t]; - return obj; - }, {}); - - // Add custom entry - if ( trait.custom ) { - trait.custom.split(";").forEach((c, i) => trait.selected[`custom${i+1}`] = c.trim()); - } - trait.cssClass = !isObjectEmpty(trait.selected) ? "" : "inactive"; - } - } - - /* -------------------------------------------- */ - - /** - * Insert a power into the powerbook object when rendering the character sheet - * @param {Object} data The Actor data being prepared - * @param {Array} powers The power data being prepared - * @param {string} school The school of the powerbook being prepared - * @private - */ - _preparePowerbook(data, powers, school) { - const owner = this.actor.isOwner; - const levels = data.data.powers; - const powerbook = {}; - - // Define some mappings - const sections = { - "atwill": -20, - "innate": -10, - }; - - // Label power slot uses headers - const useLabels = { - "-20": "-", - "-10": "-", - "0": "∞" - }; - - // Format a powerbook entry for a certain indexed level - const registerSection = (sl, i, label, {prepMode="prepared", value, max, override}={}) => { - powerbook[i] = { - order: i, - label: label, - usesSlots: i > 0, - canCreate: owner, - canPrepare: (data.actor.type === "character") && (i >= 1), - powers: [], - uses: useLabels[i] || value || 0, - slots: useLabels[i] || max || 0, - override: override || 0, - dataset: {"type": "power", "level": prepMode in sections ? 1 : i, "preparation.mode": prepMode, "school": school}, - prop: sl - }; - }; - - // Determine the maximum power level which has a slot - const maxLevel = Array.fromRange(10).reduce((max, i) => { - if ( i === 0 ) return max; - const level = levels[`power${i}`]; - if ( (level.max || level.override ) && ( i > max ) ) max = i; - return max; - }, 0); - - // Level-based powercasters have cantrips and leveled slots - if ( maxLevel > 0 ) { - registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]); - for (let lvl = 1; lvl <= maxLevel; lvl++) { - const sl = `power${lvl}`; - registerSection(sl, lvl, CONFIG.SW5E.powerLevels[lvl], levels[sl]); - } - } - - // Iterate over every power item, adding powers to the powerbook by section - powers.forEach(power => { - const mode = power.data.preparation.mode || "prepared"; - let s = power.data.level || 0; - const sl = `power${s}`; - - // Specialized powercasting modes (if they exist) - if ( mode in sections ) { - s = sections[mode]; - if ( !powerbook[s] ){ - const l = levels[mode] || {}; - const config = CONFIG.SW5E.powerPreparationModes[mode]; - registerSection(mode, s, config, { - prepMode: mode, - value: l.value, - max: l.max, - override: l.override - }); + // Ability Scores + for (let [a, abl] of Object.entries(actorData.data.abilities)) { + abl.icon = this._getProficiencyIcon(abl.proficient); + abl.hover = CONFIG.SW5E.proficiencyLevels[abl.proficient]; + abl.label = CONFIG.SW5E.abilities[a]; } - } - // Sections for higher-level powers which the caster "should not" have, but power items exist for - else if ( !powerbook[s] ) { - registerSection(sl, s, CONFIG.SW5E.powerLevels[s], {levels: levels[sl]}); - } - - // Add the power to the relevant heading - powerbook[s].powers.push(power); - }); - - // Sort the powerbook by section level - const sorted = Object.values(powerbook); - sorted.sort((a, b) => a.order - b.order); - return sorted; - } - - /* -------------------------------------------- */ - - /** - * Determine whether an Owned Item will be shown based on the current set of filters - * @return {boolean} - * @private - */ - _filterItems(items, filters) { - return items.filter(item => { - const data = item.data; - - // Action usage - for ( let f of ["action", "bonus", "reaction"] ) { - if ( filters.has(f) ) { - if ((data.activation && (data.activation.type !== f))) return false; + // Skills + if (actorData.data.skills) { + for (let [s, skl] of Object.entries(actorData.data.skills)) { + skl.ability = CONFIG.SW5E.abilityAbbreviations[skl.ability]; + skl.icon = this._getProficiencyIcon(skl.value); + skl.hover = CONFIG.SW5E.proficiencyLevels[skl.value]; + if (data.actor.type === "starship") { + skl.label = CONFIG.SW5E.starshipSkills[s]; + } else { + skl.label = CONFIG.SW5E.skills[s]; + } + } } - } - // Power-specific filters - if ( filters.has("ritual") ) { - if (data.components.ritual !== true) return false; - } - if ( filters.has("concentration") ) { - if (data.components.concentration !== true) return false; - } - if ( filters.has("prepared") ) { - if ( data.level === 0 || ["innate", "always"].includes(data.preparation.mode) ) return true; - if ( this.actor.data.type === "npc" ) return true; - if ( this.actor.data.type === "starship" ) return true; - return data.preparation.prepared; - } + // Movement speeds + data.movement = this._getMovementSpeed(actorData); - // Equipment-specific filters - if ( filters.has("equipped") ) { - if ( data.equipped !== true ) return false; - } - return true; - }); - } + // Senses + data.senses = this._getSenses(actorData); - /* -------------------------------------------- */ + // Update traits + this._prepareTraits(actorData.data.traits); - /** - * Get the font-awesome icon used to display a certain level of skill proficiency - * @private - */ - _getProficiencyIcon(level) { - const icons = { - 0: '', - 0.5: '', - 1: '', - 2: '' - }; - return icons[level] || icons[0]; - } + // Prepare owned items + this._prepareItems(data); - /* -------------------------------------------- */ - /* Event Listeners and Handlers - /* -------------------------------------------- */ + // Prepare active effects + data.effects = prepareActiveEffectCategories(this.actor.effects); - /** @inheritdoc */ - activateListeners(html) { - - // Activate Item Filters - const filterLists = html.find(".filter-list"); - filterLists.each(this._initializeFilterItemList.bind(this)); - filterLists.on("click", ".filter-item", this._onToggleFilter.bind(this)); - - // Item summaries - html.find('.item .item-name.rollable h4').click(event => this._onItemSummary(event)); - - // View Item Sheets - html.find('.item-edit').click(this._onItemEdit.bind(this)); - - // Editable Only Listeners - if ( this.isEditable ) { - - // Input focus and update - const inputs = html.find("input"); - inputs.focus(ev => ev.currentTarget.select()); - inputs.addBack().find('[data-dtype="Number"]').change(this._onChangeInputDelta.bind(this)); - - // Ability Proficiency - html.find('.ability-proficiency').click(this._onToggleAbilityProficiency.bind(this)); - - // Toggle Skill Proficiency - html.find('.skill-proficiency').on("click contextmenu", this._onCycleSkillProficiency.bind(this)); - - // Trait Selector - html.find('.trait-selector').click(this._onTraitSelector.bind(this)); - - // Configure Special Flags - html.find('.config-button').click(this._onConfigMenu.bind(this)); - - // Owned Item management - html.find('.item-create').click(this._onItemCreate.bind(this)); - html.find('.item-delete').click(this._onItemDelete.bind(this)); - html.find('.item-collapse').click(this._onItemCollapse.bind(this)); - html.find('.item-uses input').click(ev => ev.target.select()).change(this._onUsesChange.bind(this)); - html.find('.slot-max-override').click(this._onPowerSlotOverride.bind(this)); - html.find('.increment-class-level').click(this._onIncrementClassLevel.bind(this)); - html.find('.decrement-class-level').click(this._onDecrementClassLevel.bind(this)); - - // Active Effect management - html.find(".effect-control").click(ev => onManageActiveEffect(ev, this.actor)); + // Return data to the sheet + return data; } - // Owner Only Listeners - if ( this.actor.isOwner ) { + /* -------------------------------------------- */ - // Ability Checks - html.find('.ability-name').click(this._onRollAbilityTest.bind(this)); + /** + * Prepare the display of movement speed data for the Actor* + * @param {object} actorData The Actor data being prepared. + * @param {boolean} [largestPrimary=false] Show the largest movement speed as "primary", otherwise show "walk" + * @returns {{primary: string, special: string}} + * @private + */ + _getMovementSpeed(actorData, largestPrimary = false) { + const movement = actorData.data.attributes.movement || {}; - - // Roll Skill Checks - html.find('.skill-name').click(this._onRollSkillCheck.bind(this)); - - // Item Rolling - html.find('.item .item-image').click(event => this._onItemRoll(event)); - html.find('.item .item-recharge').click(event => this._onItemRecharge(event)); - } - - // Otherwise remove rollable classes - else { - html.find(".rollable").each((i, el) => el.classList.remove("rollable")); - } - - // Handle default listeners last so system listeners are triggered first - super.activateListeners(html); - } - - /* -------------------------------------------- */ - - /** - * Iinitialize Item list filters by activating the set of filters which are currently applied - * @private - */ - _initializeFilterItemList(i, ul) { - const set = this._filters[ul.dataset.filter]; - const filters = ul.querySelectorAll(".filter-item"); - for ( let li of filters ) { - if ( set.has(li.dataset.filter) ) li.classList.add("active"); - } - } - - /* -------------------------------------------- */ - /* Event Listeners and Handlers */ - /* -------------------------------------------- */ - - /** - * Handle input changes to numeric form fields, allowing them to accept delta-typed inputs - * @param event - * @private - */ - _onChangeInputDelta(event) { - const input = event.target; - const value = input.value; - if ( ["+", "-"].includes(value[0]) ) { - let delta = parseFloat(value); - input.value = getProperty(this.actor.data, input.name) + delta; - } else if ( value[0] === "=" ) { - input.value = value.slice(1); - } - } - - /* -------------------------------------------- */ - - /** - * Handle spawning the TraitSelector application which allows a checkbox of multiple trait options - * @param {Event} event The click event which originated the selection - * @private - */ - _onConfigMenu(event) { - event.preventDefault(); - const button = event.currentTarget; - let app; - switch ( button.dataset.action ) { - case "hit-dice": - app = new ActorHitDiceConfig(this.object); - break; - case "movement": - app = new ActorMovementConfig(this.object); - break; - case "flags": - app = new ActorSheetFlags(this.object); - break; - case "senses": - app = new ActorSensesConfig(this.object); - break; - case "type": - new ActorTypeConfig(this.object).render(true); - break; - } - app?.render(true); - } - - /* -------------------------------------------- */ - - /** - * Handle cycling proficiency in a Skill - * @param {Event} event A click or contextmenu event which triggered the handler - * @private - */ - _onCycleSkillProficiency(event) { - event.preventDefault(); - const field = $(event.currentTarget).siblings('input[type="hidden"]'); - - // Get the current level and the array of levels - const level = parseFloat(field.val()); - const levels = [0, 1, 0.5, 2]; - let idx = levels.indexOf(level); - - // Toggle next level - forward on click, backwards on right - if ( event.type === "click" ) { - field.val(levels[(idx === levels.length - 1) ? 0 : idx + 1]); - } else if ( event.type === "contextmenu" ) { - field.val(levels[(idx === 0) ? levels.length - 1 : idx - 1]); - } - - // Update the field value and save the form - this._onSubmit(event); - } - - /* -------------------------------------------- */ - - /** @override */ - async _onDropActor(event, data) { - const canPolymorph = game.user.isGM || (this.actor.data.type === "starship") || (this.actor.owner && game.settings.get('sw5e', 'allowPolymorphing')); - if ( !canPolymorph ) return false; - - // Get the target actor - let sourceActor = null; - if (data.pack) { - const pack = game.packs.find(p => p.collection === data.pack); - sourceActor = await pack.getEntity(data.id); - } else { - sourceActor = game.actors.get(data.id); - } - if ( !sourceActor ) return; - - // Define a function to record polymorph settings for future use - const rememberOptions = html => { - const options = {}; - html.find('input').each((i, el) => { - options[el.name] = el.checked; - }); - const settings = mergeObject(game.settings.get('sw5e', 'polymorphSettings') || {}, options); - game.settings.set('sw5e', 'polymorphSettings', settings); - return settings; - }; - - // Create and render the Dialog - - if (this.actor.data.type === "starship") { - return new Dialog({ - title: game.i18n.localize('SW5E.DeploymentPromptTitle') + " " + sourceActor.data.name + " into " + this.actor.data.name, - content: { - i18n: SW5E.deploymentTypes, - isToken: this.actor.isToken - }, - default: 'accept', - buttons: { - deploy: { - icon: '', - label: game.i18n.localize('SW5E.DeploymentAcceptSettings'), - callback: html => this.actor.deployInto(sourceActor, rememberOptions(html)) - }, - cancel: { - icon: '', - label: game.i18n.localize('Cancel') - } + // Prepare an array of available movement speeds + let speeds = [ + [movement.burrow, `${game.i18n.localize("SW5E.MovementBurrow")} ${movement.burrow}`], + [movement.climb, `${game.i18n.localize("SW5E.MovementClimb")} ${movement.climb}`], + [ + movement.fly, + `${game.i18n.localize("SW5E.MovementFly")} ${movement.fly}` + + (movement.hover ? ` (${game.i18n.localize("SW5E.MovementHover")})` : "") + ], + [movement.swim, `${game.i18n.localize("SW5E.MovementSwim")} ${movement.swim}`] + ]; + if (largestPrimary) { + speeds.push([movement.walk, `${game.i18n.localize("SW5E.MovementWalk")} ${movement.walk}`]); } - }, { - classes: ['dialog', 'sw5e'], - width: 600, - template: 'systems/sw5e/templates/apps/deployment-prompt.html' - }).render(true); - } else { - return new Dialog({ - title: game.i18n.localize('SW5E.PolymorphPromptTitle'), - content: { - options: game.settings.get('sw5e', 'polymorphSettings'), - i18n: SW5E.polymorphSettings, - isToken: this.actor.isToken - }, - default: 'accept', - buttons: { - accept: { - icon: '', - label: game.i18n.localize('SW5E.PolymorphAcceptSettings'), - callback: html => this.actor.transformInto(sourceActor, rememberOptions(html)) - }, - wildshape: { - icon: '', - label: game.i18n.localize('SW5E.PolymorphWildShape'), - callback: html => this.actor.transformInto(sourceActor, { - keepBio: true, - keepClass: true, - keepMental: true, - mergeSaves: true, - mergeSkills: true, - transformTokens: rememberOptions(html).transformTokens - }) - }, - polymorph: { - icon: '', - label: game.i18n.localize('SW5E.Polymorph'), - callback: html => this.actor.transformInto(sourceActor, { - transformTokens: rememberOptions(html).transformTokens - }) - }, - cancel: { - icon: '', - label: game.i18n.localize('Cancel') - } + + // Filter and sort speeds on their values + speeds = speeds.filter((s) => !!s[0]).sort((a, b) => b[0] - a[0]); + + // Case 1: Largest as primary + if (largestPrimary) { + let primary = speeds.shift(); + return { + primary: `${primary ? primary[1] : "0"} ${movement.units}`, + special: speeds.map((s) => s[1]).join(", ") + }; } - }, { - classes: ['dialog', 'sw5e'], - width: 600, - template: 'systems/sw5e/templates/apps/polymorph-prompt.html' - }).render(true); - } - } - /* -------------------------------------------- */ - - /** @override */ - async _onDropItemCreate(itemData) { - - // Check to make sure items of this type are allowed on this actor - if ( this.constructor.unsupportedItemTypes.has(itemData.type) ) { - return ui.notifications.warn(game.i18n.format("SW5E.ActorWarningInvalidItem", { - itemType: game.i18n.localize(CONFIG.Item.typeLabels[itemData.type]), - actorType: game.i18n.localize(CONFIG.Actor.typeLabels[this.actor.type]) - })); + // Case 2: Walk as primary + else { + return { + primary: `${movement.walk || 0} ${movement.units}`, + special: speeds.length ? speeds.map((s) => s[1]).join(", ") : "" + }; + } } - // Create a Consumable power scroll on the Inventory tab - if ( (itemData.type === "power") && (this._tabs[0].active === "inventory") ) { - const scroll = await Item5e.createScrollFromPower(itemData); - itemData = scroll.data; + /* -------------------------------------------- */ + + _getSenses(actorData) { + const senses = actorData.data.attributes.senses || {}; + const tags = {}; + for (let [k, label] of Object.entries(CONFIG.SW5E.senses)) { + const v = senses[k] ?? 0; + if (v === 0) continue; + tags[k] = `${game.i18n.localize(label)} ${v} ${senses.units}`; + } + if (!!senses.special) tags["special"] = senses.special; + return tags; } - if ( itemData.data ) { - // Ignore certain statuses - ["equipped", "proficient", "prepared"].forEach(k => delete itemData.data[k]); + /* -------------------------------------------- */ - // Downgrade ATTUNED to REQUIRED - itemData.data.attunement = Math.min(itemData.data.attunement, CONFIG.SW5E.attunementTypes.REQUIRED); + /** + * Prepare the data structure for traits data like languages, resistances & vulnerabilities, and proficiencies + * @param {object} traits The raw traits data object from the actor data + * @private + */ + _prepareTraits(traits) { + const map = { + dr: CONFIG.SW5E.damageResistanceTypes, + di: CONFIG.SW5E.damageResistanceTypes, + dv: CONFIG.SW5E.damageResistanceTypes, + ci: CONFIG.SW5E.conditionTypes, + languages: CONFIG.SW5E.languages, + armorProf: CONFIG.SW5E.armorProficiencies, + weaponProf: CONFIG.SW5E.weaponProficiencies, + toolProf: CONFIG.SW5E.toolProficiencies + }; + for (let [t, choices] of Object.entries(map)) { + const trait = traits[t]; + if (!trait) continue; + let values = []; + if (trait.value) { + values = trait.value instanceof Array ? trait.value : [trait.value]; + } + trait.selected = values.reduce((obj, t) => { + obj[t] = choices[t]; + return obj; + }, {}); + + // Add custom entry + if (trait.custom) { + trait.custom.split(";").forEach((c, i) => (trait.selected[`custom${i + 1}`] = c.trim())); + } + trait.cssClass = !isObjectEmpty(trait.selected) ? "" : "inactive"; + } } - // Stack identical consumables - if ( itemData.type === "consumable" && itemData.flags.core?.sourceId ) { - const similarItem = this.actor.items.find(i => { - const sourceId = i.getFlag("core", "sourceId"); - return sourceId && (sourceId === itemData.flags.core?.sourceId) && - (i.type === "consumable"); - }); - if ( similarItem && itemData.name !== "Power Cell" ) { // Always create a new powercell instead of increasing quantity - return similarItem.update({ - 'data.quantity': similarItem.data.data.quantity + Math.max(itemData.data.quantity, 1) + /* -------------------------------------------- */ + + /** + * Insert a power into the powerbook object when rendering the character sheet + * @param {Object} data The Actor data being prepared + * @param {Array} powers The power data being prepared + * @param {string} school The school of the powerbook being prepared + * @private + */ + _preparePowerbook(data, powers, school) { + const owner = this.actor.isOwner; + const levels = data.data.powers; + const powerbook = {}; + + // Define some mappings + const sections = { + atwill: -20, + innate: -10 + }; + + // Label power slot uses headers + const useLabels = { + "-20": "-", + "-10": "-", + "0": "∞" + }; + + // Format a powerbook entry for a certain indexed level + const registerSection = (sl, i, label, {prepMode = "prepared", value, max, override} = {}) => { + powerbook[i] = { + order: i, + label: label, + usesSlots: i > 0, + canCreate: owner, + canPrepare: data.actor.type === "character" && i >= 1, + powers: [], + uses: useLabels[i] || value || 0, + slots: useLabels[i] || max || 0, + override: override || 0, + dataset: { + "type": "power", + "level": prepMode in sections ? 1 : i, + "preparation.mode": prepMode, + "school": school + }, + prop: sl + }; + }; + + // Determine the maximum power level which has a slot + const maxLevel = Array.fromRange(10).reduce((max, i) => { + if (i === 0) return max; + const level = levels[`power${i}`]; + if ((level.max || level.override) && i > max) max = i; + return max; + }, 0); + + // Level-based powercasters have cantrips and leveled slots + if (maxLevel > 0) { + registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]); + for (let lvl = 1; lvl <= maxLevel; lvl++) { + const sl = `power${lvl}`; + registerSection(sl, lvl, CONFIG.SW5E.powerLevels[lvl], levels[sl]); + } + } + + // Iterate over every power item, adding powers to the powerbook by section + powers.forEach((power) => { + const mode = power.data.preparation.mode || "prepared"; + let s = power.data.level || 0; + const sl = `power${s}`; + + // Specialized powercasting modes (if they exist) + if (mode in sections) { + s = sections[mode]; + if (!powerbook[s]) { + const l = levels[mode] || {}; + const config = CONFIG.SW5E.powerPreparationModes[mode]; + registerSection(mode, s, config, { + prepMode: mode, + value: l.value, + max: l.max, + override: l.override + }); + } + } + + // Sections for higher-level powers which the caster "should not" have, but power items exist for + else if (!powerbook[s]) { + registerSection(sl, s, CONFIG.SW5E.powerLevels[s], {levels: levels[sl]}); + } + + // Add the power to the relevant heading + powerbook[s].powers.push(power); }); - } + + // Sort the powerbook by section level + const sorted = Object.values(powerbook); + sorted.sort((a, b) => a.order - b.order); + return sorted; } - // Create the owned item as normal - return super._onDropItemCreate(itemData); - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** + * Determine whether an Owned Item will be shown based on the current set of filters + * @return {boolean} + * @private + */ + _filterItems(items, filters) { + return items.filter((item) => { + const data = item.data; - /** - * Handle enabling editing for a power slot override value - * @param {MouseEvent} event The originating click event - * @private - */ - async _onPowerSlotOverride (event) { - const span = event.currentTarget.parentElement; - const level = span.dataset.level; - const override = this.actor.data.data.powers[level].override || span.dataset.slots; - const input = document.createElement("INPUT"); - input.type = "text"; - input.name = `data.powers.${level}.override`; - input.value = override; - input.placeholder = span.dataset.slots; - input.dataset.dtype = "Number"; + // Action usage + for (let f of ["action", "bonus", "reaction"]) { + if (filters.has(f)) { + if (data.activation && data.activation.type !== f) return false; + } + } - // Replace the HTML - const parent = span.parentElement; - parent.removeChild(span); - parent.appendChild(input); - } + // Power-specific filters + if (filters.has("ritual")) { + if (data.components.ritual !== true) return false; + } + if (filters.has("concentration")) { + if (data.components.concentration !== true) return false; + } + if (filters.has("prepared")) { + if (data.level === 0 || ["innate", "always"].includes(data.preparation.mode)) return true; + if (this.actor.data.type === "npc") return true; + if (this.actor.data.type === "starship") return true; + return data.preparation.prepared; + } - /* -------------------------------------------- */ - - /** - * Change the uses amount of an Owned Item within the Actor - * @param {Event} event The triggering click event - * @private - */ - async _onUsesChange(event) { - event.preventDefault(); - const itemId = event.currentTarget.closest(".item").dataset.itemId; - const item = this.actor.items.get(itemId); - const uses = Math.clamped(0, parseInt(event.target.value), item.data.data.uses.max); - event.target.value = uses; - return item.update({ 'data.uses.value': uses }); - } - - /* -------------------------------------------- */ - - /** - * Handle rolling of an item from the Actor sheet, obtaining the Item instance and dispatching to it's roll method - * @private - */ - _onItemRoll(event) { - event.preventDefault(); - const itemId = event.currentTarget.closest(".item").dataset.itemId; - const item = this.actor.items.get(itemId); - return item.roll(); - } - - /* -------------------------------------------- */ - - /** - * Handle attempting to recharge an item usage by rolling a recharge check - * @param {Event} event The originating click event - * @private - */ - _onItemRecharge(event) { - event.preventDefault(); - const itemId = event.currentTarget.closest(".item").dataset.itemId; - const item = this.actor.items.get(itemId); - return item.rollRecharge(); - }; - - /* -------------------------------------------- */ - - /** - * Handle rolling of an item from the Actor sheet, obtaining the Item instance and dispatching to it's roll method - * @private - */ - _onItemSummary(event) { - event.preventDefault(); - let li = $(event.currentTarget).parents(".item"), - item = this.actor.items.get(li.data("item-id")), - chatData = item.getChatData({secrets: this.actor.isOwner}); - - // Toggle summary - if ( li.hasClass("expanded") ) { - let summary = li.children(".item-summary"); - summary.slideUp(200, () => summary.remove()); - } else { - let div = $(`
${chatData.description.value}
`); - let props = $(`
`); - chatData.properties.forEach(p => props.append(`${p}`)); - div.append(props); - li.append(div.hide()); - div.slideDown(200); + // Equipment-specific filters + if (filters.has("equipped")) { + if (data.equipped !== true) return false; + } + return true; + }); } - li.toggleClass("expanded"); - } - /* -------------------------------------------- */ + /* -------------------------------------------- */ - /** - * Handle creating a new Owned Item for the actor using initial data defined in the HTML dataset - * @param {Event} event The originating click event - * @private - */ - _onItemCreate(event) { - event.preventDefault(); - const header = event.currentTarget; - const type = header.dataset.type; - const itemData = { - name: game.i18n.format("SW5E.ItemNew", {type: game.i18n.localize(`SW5E.ItemType${type.capitalize()}`)}), - type: type, - data: foundry.utils.deepClone(header.dataset) - }; - delete itemData.data["type"]; - return this.actor.createEmbeddedDocuments("Item", [itemData]); - } - - /* -------------------------------------------- */ - - /** - * Handle editing an existing Owned Item for the Actor - * @param {Event} event The originating click event - * @private - */ - _onItemEdit(event) { - event.preventDefault(); - const li = event.currentTarget.closest(".item"); - const item = this.actor.items.get(li.dataset.itemId); - return item.sheet.render(true); - } - - /* -------------------------------------------- */ - - /** - * Handle deleting an existing Owned Item for the Actor - * @param {Event} event The originating click event - * @private - */ - _onItemDelete(event) { - event.preventDefault(); - const li = event.currentTarget.closest(".item"); - const item = this.actor.items.get(li.dataset.itemId); - if ( item ) return item.delete(); - } - - /** - * Handle collapsing a Feature row on the actor sheet - * @param {Event} event The originating click event - * @private - */ - -_onItemCollapse(event) { - event.preventDefault(); - - event.currentTarget.classList.toggle("active"); - - const li = event.currentTarget.closest("li"); - const content = li.querySelector(".content"); - - if (content.style.display === "none") { - content.style.display = "block"; - } else { - content.style.display = "none"; + /** + * Get the font-awesome icon used to display a certain level of skill proficiency + * @private + */ + _getProficiencyIcon(level) { + const icons = { + 0: '', + 0.5: '', + 1: '', + 2: '' + }; + return icons[level] || icons[0]; } - } -/** - * Handle incrementing class level on the actor sheet - * @param {Event} event The originating click event - * @private - */ - - _onIncrementClassLevel(event) { - event.preventDefault(); + /* -------------------------------------------- */ + /* Event Listeners and Handlers + /* -------------------------------------------- */ - const div = event.currentTarget.closest(".character") - const li = event.currentTarget.closest("li"); + /** @inheritdoc */ + activateListeners(html) { + // Activate Item Filters + const filterLists = html.find(".filter-list"); + filterLists.each(this._initializeFilterItemList.bind(this)); + filterLists.on("click", ".filter-item", this._onToggleFilter.bind(this)); - const actorId = div.id.split("-")[1]; - const itemId = li.dataset.itemId; + // Item summaries + html.find(".item .item-name.rollable h4").click((event) => this._onItemSummary(event)); - const actor = game.actors.get(actorId); - const item = actor.items.get(itemId); + // View Item Sheets + html.find(".item-edit").click(this._onItemEdit.bind(this)); - let levels = item.data.data.levels; - const update = {_id: item.data._id, data: {levels: (levels + 1) }}; + // Editable Only Listeners + if (this.isEditable) { + // Input focus and update + const inputs = html.find("input"); + inputs.focus((ev) => ev.currentTarget.select()); + inputs.addBack().find('[data-dtype="Number"]').change(this._onChangeInputDelta.bind(this)); - actor.updateEmbeddedDocuments("Item", [update]); -} - -/** - * Handle decrementing class level on the actor sheet - * @param {Event} event The originating click event - * @private - */ - - _onDecrementClassLevel(event) { - event.preventDefault(); - - const div = event.currentTarget.closest(".character") - const li = event.currentTarget.closest("li"); - - const actorId = div.id.split("-")[1]; - const itemId = li.dataset.itemId; - - const actor = game.actors.get(actorId); - const item = actor.items.get(itemId); - - let levels = item.data.data.levels; - const update = {_id: item.data._id, data: {levels: (levels - 1) }}; - - actor.updateEmbeddedDocuments("Item", [update]); -} - - /* -------------------------------------------- */ - - /** - * Handle rolling an Ability check, either a test or a saving throw - * @param {Event} event The originating click event - * @private - */ - _onRollAbilityTest(event) { - event.preventDefault(); - let ability = event.currentTarget.parentElement.dataset.ability; - return this.actor.rollAbility(ability, {event: event}); - } - - /* -------------------------------------------- */ - - /** - * Handle rolling a Skill check - * @param {Event} event The originating click event - * @private - */ - _onRollSkillCheck(event) { - event.preventDefault(); - const skill = event.currentTarget.parentElement.dataset.skill; - return this.actor.rollSkill(skill, {event: event}); - } - - /* -------------------------------------------- */ - - /** - * Handle toggling Ability score proficiency level - * @param {Event} event The originating click event - * @private - */ - _onToggleAbilityProficiency(event) { - event.preventDefault(); - const field = event.currentTarget.previousElementSibling; - return this.actor.update({[field.name]: 1 - parseInt(field.value)}); - } - - /* -------------------------------------------- */ - - /** - * Handle toggling of filters to display a different set of owned items - * @param {Event} event The click event which triggered the toggle - * @private - */ - _onToggleFilter(event) { - event.preventDefault(); - const li = event.currentTarget; - const set = this._filters[li.parentElement.dataset.filter]; - const filter = li.dataset.filter; - if ( set.has(filter) ) set.delete(filter); - else set.add(filter); - return this.render(); - } - - /* -------------------------------------------- */ - - /** - * Handle spawning the TraitSelector application which allows a checkbox of multiple trait options - * @param {Event} event The click event which originated the selection - * @private - */ - _onTraitSelector(event) { - event.preventDefault(); - const a = event.currentTarget; - const label = a.parentElement.querySelector("label"); - const choices = CONFIG.SW5E[a.dataset.options]; - const options = { name: a.dataset.target, title: label.innerText, choices }; - return new TraitSelector(this.actor, options).render(true) - } - - /* -------------------------------------------- */ - - /** @override */ - _getHeaderButtons() { - let buttons = super._getHeaderButtons(); - if (this.actor.isPolymorphed) { - buttons.unshift({ - label: 'SW5E.PolymorphRestoreTransformation', - class: "restore-transformation", - icon: "fas fa-backward", - onclick: () => this.actor.revertOriginalForm() - }); - } - return buttons; - } + // Ability Proficiency + html.find(".ability-proficiency").click(this._onToggleAbilityProficiency.bind(this)); + + // Toggle Skill Proficiency + html.find(".skill-proficiency").on("click contextmenu", this._onCycleSkillProficiency.bind(this)); + + // Trait Selector + html.find(".trait-selector").click(this._onTraitSelector.bind(this)); + + // Configure Special Flags + html.find(".config-button").click(this._onConfigMenu.bind(this)); + + // Owned Item management + html.find(".item-create").click(this._onItemCreate.bind(this)); + html.find(".item-delete").click(this._onItemDelete.bind(this)); + html.find(".item-collapse").click(this._onItemCollapse.bind(this)); + html.find(".item-uses input") + .click((ev) => ev.target.select()) + .change(this._onUsesChange.bind(this)); + html.find(".slot-max-override").click(this._onPowerSlotOverride.bind(this)); + html.find(".increment-class-level").click(this._onIncrementClassLevel.bind(this)); + html.find(".decrement-class-level").click(this._onDecrementClassLevel.bind(this)); + + // Active Effect management + html.find(".effect-control").click((ev) => onManageActiveEffect(ev, this.actor)); + } + + // Owner Only Listeners + if (this.actor.isOwner) { + // Ability Checks + html.find(".ability-name").click(this._onRollAbilityTest.bind(this)); + + // Roll Skill Checks + html.find(".skill-name").click(this._onRollSkillCheck.bind(this)); + + // Item Rolling + html.find(".item .item-image").click((event) => this._onItemRoll(event)); + html.find(".item .item-recharge").click((event) => this._onItemRecharge(event)); + } + + // Otherwise remove rollable classes + else { + html.find(".rollable").each((i, el) => el.classList.remove("rollable")); + } + + // Handle default listeners last so system listeners are triggered first + super.activateListeners(html); + } + + /* -------------------------------------------- */ + + /** + * Iinitialize Item list filters by activating the set of filters which are currently applied + * @private + */ + _initializeFilterItemList(i, ul) { + const set = this._filters[ul.dataset.filter]; + const filters = ul.querySelectorAll(".filter-item"); + for (let li of filters) { + if (set.has(li.dataset.filter)) li.classList.add("active"); + } + } + + /* -------------------------------------------- */ + /* Event Listeners and Handlers */ + /* -------------------------------------------- */ + + /** + * Handle input changes to numeric form fields, allowing them to accept delta-typed inputs + * @param event + * @private + */ + _onChangeInputDelta(event) { + const input = event.target; + const value = input.value; + if (["+", "-"].includes(value[0])) { + let delta = parseFloat(value); + input.value = getProperty(this.actor.data, input.name) + delta; + } else if (value[0] === "=") { + input.value = value.slice(1); + } + } + + /* -------------------------------------------- */ + + /** + * Handle spawning the TraitSelector application which allows a checkbox of multiple trait options + * @param {Event} event The click event which originated the selection + * @private + */ + _onConfigMenu(event) { + event.preventDefault(); + const button = event.currentTarget; + let app; + switch (button.dataset.action) { + case "hit-dice": + app = new ActorHitDiceConfig(this.object); + break; + case "movement": + app = new ActorMovementConfig(this.object); + break; + case "flags": + app = new ActorSheetFlags(this.object); + break; + case "senses": + app = new ActorSensesConfig(this.object); + break; + case "type": + new ActorTypeConfig(this.object).render(true); + break; + } + app?.render(true); + } + + /* -------------------------------------------- */ + + /** + * Handle cycling proficiency in a Skill + * @param {Event} event A click or contextmenu event which triggered the handler + * @private + */ + _onCycleSkillProficiency(event) { + event.preventDefault(); + const field = $(event.currentTarget).siblings('input[type="hidden"]'); + + // Get the current level and the array of levels + const level = parseFloat(field.val()); + const levels = [0, 1, 0.5, 2]; + let idx = levels.indexOf(level); + + // Toggle next level - forward on click, backwards on right + if (event.type === "click") { + field.val(levels[idx === levels.length - 1 ? 0 : idx + 1]); + } else if (event.type === "contextmenu") { + field.val(levels[idx === 0 ? levels.length - 1 : idx - 1]); + } + + // Update the field value and save the form + this._onSubmit(event); + } + + /* -------------------------------------------- */ + + /** @override */ + async _onDropActor(event, data) { + const canPolymorph = game.user.isGM || (this.actor.isOwner && game.settings.get("sw5e", "allowPolymorphing")); + if (!canPolymorph) return false; + + // Get the target actor + let sourceActor = null; + if (data.pack) { + const pack = game.packs.find((p) => p.collection === data.pack); + sourceActor = await pack.getEntity(data.id); + } else { + sourceActor = game.actors.get(data.id); + } + if (!sourceActor) return; + + // Define a function to record polymorph settings for future use + const rememberOptions = (html) => { + const options = {}; + html.find("input").each((i, el) => { + options[el.name] = el.checked; + }); + const settings = mergeObject(game.settings.get("sw5e", "polymorphSettings") || {}, options); + game.settings.set("sw5e", "polymorphSettings", settings); + return settings; + }; + + // Create and render the Dialog + return new Dialog( + { + title: game.i18n.localize("SW5E.PolymorphPromptTitle"), + content: { + options: game.settings.get("sw5e", "polymorphSettings"), + i18n: SW5E.polymorphSettings, + isToken: this.actor.isToken + }, + default: "accept", + buttons: { + accept: { + icon: '', + label: game.i18n.localize("SW5E.PolymorphAcceptSettings"), + callback: (html) => this.actor.transformInto(sourceActor, rememberOptions(html)) + }, + wildshape: { + icon: '', + label: game.i18n.localize("SW5E.PolymorphWildShape"), + callback: (html) => + this.actor.transformInto(sourceActor, { + keepBio: true, + keepClass: true, + keepMental: true, + mergeSaves: true, + mergeSkills: true, + transformTokens: rememberOptions(html).transformTokens + }) + }, + polymorph: { + icon: '', + label: game.i18n.localize("SW5E.Polymorph"), + callback: (html) => + this.actor.transformInto(sourceActor, { + transformTokens: rememberOptions(html).transformTokens + }) + }, + cancel: { + icon: '', + label: game.i18n.localize("Cancel") + } + } + }, + { + classes: ["dialog", "sw5e"], + width: 600, + template: "systems/sw5e/templates/apps/polymorph-prompt.html" + } + ).render(true); + } + + /* -------------------------------------------- */ + + /** @override */ + async _onDropItemCreate(itemData) { + // Check to make sure items of this type are allowed on this actor + if (this.constructor.unsupportedItemTypes.has(itemData.type)) { + return ui.notifications.warn( + game.i18n.format("SW5E.ActorWarningInvalidItem", { + itemType: game.i18n.localize(CONFIG.Item.typeLabels[itemData.type]), + actorType: game.i18n.localize(CONFIG.Actor.typeLabels[this.actor.type]) + }) + ); + } + + // Create a Consumable power scroll on the Inventory tab + if (itemData.type === "power" && this._tabs[0].active === "inventory") { + const scroll = await Item5e.createScrollFromPower(itemData); + itemData = scroll.data; + } + + if (itemData.data) { + // Ignore certain statuses + ["equipped", "proficient", "prepared"].forEach((k) => delete itemData.data[k]); + + // Downgrade ATTUNED to REQUIRED + itemData.data.attunement = Math.min(itemData.data.attunement, CONFIG.SW5E.attunementTypes.REQUIRED); + } + + // Stack identical consumables + if (itemData.type === "consumable" && itemData.flags.core?.sourceId) { + const similarItem = this.actor.items.find((i) => { + const sourceId = i.getFlag("core", "sourceId"); + return sourceId && sourceId === itemData.flags.core?.sourceId && i.type === "consumable"; + }); + if (similarItem && itemData.name !== "Power Cell") { + // Always create a new powercell instead of increasing quantity + return similarItem.update({ + "data.quantity": similarItem.data.data.quantity + Math.max(itemData.data.quantity, 1) + }); + } + } + + // Create the owned item as normal + return super._onDropItemCreate(itemData); + } + + /* -------------------------------------------- */ + + /** + * Handle enabling editing for a power slot override value + * @param {MouseEvent} event The originating click event + * @private + */ + async _onPowerSlotOverride(event) { + const span = event.currentTarget.parentElement; + const level = span.dataset.level; + const override = this.actor.data.data.powers[level].override || span.dataset.slots; + const input = document.createElement("INPUT"); + input.type = "text"; + input.name = `data.powers.${level}.override`; + input.value = override; + input.placeholder = span.dataset.slots; + input.dataset.dtype = "Number"; + + // Replace the HTML + const parent = span.parentElement; + parent.removeChild(span); + parent.appendChild(input); + } + + /* -------------------------------------------- */ + + /** + * Change the uses amount of an Owned Item within the Actor + * @param {Event} event The triggering click event + * @private + */ + async _onUsesChange(event) { + event.preventDefault(); + const itemId = event.currentTarget.closest(".item").dataset.itemId; + const item = this.actor.items.get(itemId); + const uses = Math.clamped(0, parseInt(event.target.value), item.data.data.uses.max); + event.target.value = uses; + return item.update({"data.uses.value": uses}); + } + + /* -------------------------------------------- */ + + /** + * Handle rolling of an item from the Actor sheet, obtaining the Item instance and dispatching to it's roll method + * @private + */ + _onItemRoll(event) { + event.preventDefault(); + const itemId = event.currentTarget.closest(".item").dataset.itemId; + const item = this.actor.items.get(itemId); + return item.roll(); + } + + /* -------------------------------------------- */ + + /** + * Handle attempting to recharge an item usage by rolling a recharge check + * @param {Event} event The originating click event + * @private + */ + _onItemRecharge(event) { + event.preventDefault(); + const itemId = event.currentTarget.closest(".item").dataset.itemId; + const item = this.actor.items.get(itemId); + return item.rollRecharge(); + } + + /* -------------------------------------------- */ + + /** + * Handle rolling of an item from the Actor sheet, obtaining the Item instance and dispatching to it's roll method + * @private + */ + _onItemSummary(event) { + event.preventDefault(); + let li = $(event.currentTarget).parents(".item"), + item = this.actor.items.get(li.data("item-id")), + chatData = item.getChatData({secrets: this.actor.isOwner}); + + // Toggle summary + if (li.hasClass("expanded")) { + let summary = li.children(".item-summary"); + summary.slideUp(200, () => summary.remove()); + } else { + let div = $(`
${chatData.description.value}
`); + let props = $(`
`); + chatData.properties.forEach((p) => props.append(`${p}`)); + div.append(props); + li.append(div.hide()); + div.slideDown(200); + } + li.toggleClass("expanded"); + } + + /* -------------------------------------------- */ + + /** + * Handle creating a new Owned Item for the actor using initial data defined in the HTML dataset + * @param {Event} event The originating click event + * @private + */ + _onItemCreate(event) { + event.preventDefault(); + const header = event.currentTarget; + const type = header.dataset.type; + const itemData = { + name: game.i18n.format("SW5E.ItemNew", {type: game.i18n.localize(`SW5E.ItemType${type.capitalize()}`)}), + type: type, + data: foundry.utils.deepClone(header.dataset) + }; + delete itemData.data["type"]; + return this.actor.createEmbeddedDocuments("Item", [itemData]); + } + + /* -------------------------------------------- */ + + /** + * Handle editing an existing Owned Item for the Actor + * @param {Event} event The originating click event + * @private + */ + _onItemEdit(event) { + event.preventDefault(); + const li = event.currentTarget.closest(".item"); + const item = this.actor.items.get(li.dataset.itemId); + return item.sheet.render(true); + } + + /* -------------------------------------------- */ + + /** + * Handle deleting an existing Owned Item for the Actor + * @param {Event} event The originating click event + * @private + */ + _onItemDelete(event) { + event.preventDefault(); + const li = event.currentTarget.closest(".item"); + const item = this.actor.items.get(li.dataset.itemId); + if (item) return item.delete(); + } + + /** + * Handle collapsing a Feature row on the actor sheet + * @param {Event} event The originating click event + * @private + */ + + _onItemCollapse(event) { + event.preventDefault(); + + event.currentTarget.classList.toggle("active"); + + const li = event.currentTarget.closest("li"); + const content = li.querySelector(".content"); + + if (content.style.display === "none") { + content.style.display = "block"; + } else { + content.style.display = "none"; + } + } + + /** + * Handle incrementing class level on the actor sheet + * @param {Event} event The originating click event + * @private + */ + + _onIncrementClassLevel(event) { + event.preventDefault(); + + const div = event.currentTarget.closest(".character"); + const li = event.currentTarget.closest("li"); + + const actorId = div.id.split("-")[1]; + const itemId = li.dataset.itemId; + + const actor = game.actors.get(actorId); + const item = actor.items.get(itemId); + + let levels = item.data.data.levels; + const update = {_id: item.data._id, data: {levels: levels + 1}}; + + actor.updateEmbeddedDocuments("Item", [update]); + } + + /** + * Handle decrementing class level on the actor sheet + * @param {Event} event The originating click event + * @private + */ + + _onDecrementClassLevel(event) { + event.preventDefault(); + + const div = event.currentTarget.closest(".character"); + const li = event.currentTarget.closest("li"); + + const actorId = div.id.split("-")[1]; + const itemId = li.dataset.itemId; + + const actor = game.actors.get(actorId); + const item = actor.items.get(itemId); + + let levels = item.data.data.levels; + const update = {_id: item.data._id, data: {levels: levels - 1}}; + + actor.updateEmbeddedDocuments("Item", [update]); + } + + /* -------------------------------------------- */ + + /** + * Handle rolling an Ability check, either a test or a saving throw + * @param {Event} event The originating click event + * @private + */ + _onRollAbilityTest(event) { + event.preventDefault(); + let ability = event.currentTarget.parentElement.dataset.ability; + return this.actor.rollAbility(ability, {event: event}); + } + + /* -------------------------------------------- */ + + /** + * Handle rolling a Skill check + * @param {Event} event The originating click event + * @private + */ + _onRollSkillCheck(event) { + event.preventDefault(); + const skill = event.currentTarget.parentElement.dataset.skill; + return this.actor.rollSkill(skill, {event: event}); + } + + /* -------------------------------------------- */ + + /** + * Handle toggling Ability score proficiency level + * @param {Event} event The originating click event + * @private + */ + _onToggleAbilityProficiency(event) { + event.preventDefault(); + const field = event.currentTarget.previousElementSibling; + return this.actor.update({[field.name]: 1 - parseInt(field.value)}); + } + + /* -------------------------------------------- */ + + /** + * Handle toggling of filters to display a different set of owned items + * @param {Event} event The click event which triggered the toggle + * @private + */ + _onToggleFilter(event) { + event.preventDefault(); + const li = event.currentTarget; + const set = this._filters[li.parentElement.dataset.filter]; + const filter = li.dataset.filter; + if (set.has(filter)) set.delete(filter); + else set.add(filter); + return this.render(); + } + + /* -------------------------------------------- */ + + /** + * Handle spawning the TraitSelector application which allows a checkbox of multiple trait options + * @param {Event} event The click event which originated the selection + * @private + */ + _onTraitSelector(event) { + event.preventDefault(); + const a = event.currentTarget; + const label = a.parentElement.querySelector("label"); + const choices = CONFIG.SW5E[a.dataset.options]; + const options = {name: a.dataset.target, title: label.innerText, choices}; + return new TraitSelector(this.actor, options).render(true); + } + + /* -------------------------------------------- */ + + /** @override */ + _getHeaderButtons() { + let buttons = super._getHeaderButtons(); + if (this.actor.isPolymorphed) { + buttons.unshift({ + label: "SW5E.PolymorphRestoreTransformation", + class: "restore-transformation", + icon: "fas fa-backward", + onclick: () => this.actor.revertOriginalForm() + }); + } + return buttons; + } } diff --git a/module/actor/sheets/newSheet/character.js b/module/actor/sheets/newSheet/character.js index 3390c482..91c68b1d 100644 --- a/module/actor/sheets/newSheet/character.js +++ b/module/actor/sheets/newSheet/character.js @@ -7,246 +7,339 @@ import Actor5e from "../../entity.js"; * @type {ActorSheet5e} */ export default class ActorSheet5eCharacterNew extends ActorSheet5e { + get template() { + if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/newActor/limited-sheet.html"; + return "systems/sw5e/templates/actors/newActor/character-sheet.html"; + } + /** + * Define default rendering options for the NPC sheet + * @return {Object} + */ + static get defaultOptions() { + return mergeObject(super.defaultOptions, { + classes: ["swalt", "sw5e", "sheet", "actor", "character"], + blockFavTab: true, + subTabs: null, + width: 800, + tabs: [ + { + navSelector: ".root-tabs", + contentSelector: ".sheet-body", + initial: "attributes" + } + ] + }); + } - get template() { - if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/newActor/limited-sheet.html"; - return "systems/sw5e/templates/actors/newActor/character-sheet.html"; - } - /** - * Define default rendering options for the NPC sheet - * @return {Object} - */ - static get defaultOptions() { + /* -------------------------------------------- */ - return mergeObject(super.defaultOptions, { - classes: ["swalt", "sw5e", "sheet", "actor", "character"], - blockFavTab: true, - subTabs: null, - width: 800, - tabs: [{ - navSelector: ".root-tabs", - contentSelector: ".sheet-body", - initial: "attributes" - }], - }); - } + /** + * Add some extra data when rendering the sheet to reduce the amount of logic required within the template. + */ + getData() { + const sheetData = super.getData(); - /* -------------------------------------------- */ + // Temporary HP + let hp = sheetData.data.attributes.hp; + if (hp.temp === 0) delete hp.temp; + if (hp.tempmax === 0) delete hp.tempmax; - /** - * Add some extra data when rendering the sheet to reduce the amount of logic required within the template. - */ - getData() { - const sheetData = super.getData(); + // Resources + sheetData["resources"] = ["primary", "secondary", "tertiary"].reduce((arr, r) => { + const res = sheetData.data.resources[r] || {}; + res.name = r; + res.placeholder = game.i18n.localize("SW5E.Resource" + r.titleCase()); + if (res && res.value === 0) delete res.value; + if (res && res.max === 0) delete res.max; + return arr.concat([res]); + }, []); - // Temporary HP - let hp = sheetData.data.attributes.hp; - if (hp.temp === 0) delete hp.temp; - if (hp.tempmax === 0) delete hp.tempmax; + // Experience Tracking + sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking"); + sheetData["classLabels"] = this.actor.itemTypes.class.map((c) => c.name).join(", "); + sheetData["multiclassLabels"] = this.actor.itemTypes.class + .map((c) => { + return [c.data.data.archetype, c.name, c.data.data.levels].filterJoin(" "); + }) + .join(", "); - // Resources - sheetData["resources"] = ["primary", "secondary", "tertiary"].reduce((arr, r) => { - const res = sheetData.data.resources[r] || {}; - res.name = r; - res.placeholder = game.i18n.localize("SW5E.Resource"+r.titleCase()); - if (res && res.value === 0) delete res.value; - if (res && res.max === 0) delete res.max; - return arr.concat([res]); - }, []); + // Return data for rendering + return sheetData; + } - // Experience Tracking - sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking"); - sheetData["classLabels"] = this.actor.itemTypes.class.map(c => c.name).join(", "); - sheetData["multiclassLabels"] = this.actor.itemTypes.class.map(c => { - return [c.data.data.archetype, c.name, c.data.data.levels].filterJoin(' ') - }).join(', '); + /* -------------------------------------------- */ - // Return data for rendering - return sheetData; - } + /** + * Organize and classify Owned Items for Character sheets + * @private + */ + _prepareItems(data) { + // Categorize items as inventory, powerbook, features, and classes + const inventory = { + weapon: {label: "SW5E.ItemTypeWeaponPl", items: [], dataset: {type: "weapon"}}, + equipment: {label: "SW5E.ItemTypeEquipmentPl", items: [], dataset: {type: "equipment"}}, + consumable: {label: "SW5E.ItemTypeConsumablePl", items: [], dataset: {type: "consumable"}}, + tool: {label: "SW5E.ItemTypeToolPl", items: [], dataset: {type: "tool"}}, + backpack: {label: "SW5E.ItemTypeContainerPl", items: [], dataset: {type: "backpack"}}, + loot: {label: "SW5E.ItemTypeLootPl", items: [], dataset: {type: "loot"}} + }; - /* -------------------------------------------- */ + // Partition items by category + let [ + items, + forcepowers, + techpowers, + feats, + classes, + deployments, + deploymentfeatures, + ventures, + species, + archetypes, + classfeatures, + backgrounds, + fightingstyles, + fightingmasteries, + lightsaberforms + ] = data.items.reduce( + (arr, item) => { + // Item details + item.img = item.img || CONST.DEFAULT_TOKEN; + item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1; + item.attunement = { + [CONFIG.SW5E.attunementTypes.REQUIRED]: { + icon: "fa-sun", + cls: "not-attuned", + title: "SW5E.AttunementRequired" + }, + [CONFIG.SW5E.attunementTypes.ATTUNED]: { + icon: "fa-sun", + cls: "attuned", + title: "SW5E.AttunementAttuned" + } + }[item.data.attunement]; - /** - * Organize and classify Owned Items for Character sheets - * @private - */ - _prepareItems(data) { + // Item usage + item.hasUses = item.data.uses && item.data.uses.max > 0; + item.isOnCooldown = + item.data.recharge && !!item.data.recharge.value && item.data.recharge.charged === false; + item.isDepleted = item.isOnCooldown && item.data.uses.per && item.data.uses.value > 0; + item.hasTarget = !!item.data.target && !["none", ""].includes(item.data.target.type); - // Categorize items as inventory, powerbook, features, and classes - const inventory = { - weapon: { label: "SW5E.ItemTypeWeaponPl", items: [], dataset: {type: "weapon"} }, - equipment: { label: "SW5E.ItemTypeEquipmentPl", items: [], dataset: {type: "equipment"} }, - consumable: { label: "SW5E.ItemTypeConsumablePl", items: [], dataset: {type: "consumable"} }, - tool: { label: "SW5E.ItemTypeToolPl", items: [], dataset: {type: "tool"} }, - backpack: { label: "SW5E.ItemTypeContainerPl", items: [], dataset: {type: "backpack"} }, - loot: { label: "SW5E.ItemTypeLootPl", items: [], dataset: {type: "loot"} } - }; + // Item toggle state + this._prepareItemToggleState(item); - // Partition items by category - let [items, forcepowers, techpowers, feats, classes, deployments, deploymentfeatures, ventures, species, archetypes, classfeatures, backgrounds, fightingstyles, fightingmasteries, lightsaberforms] = data.items.reduce((arr, item) => { + // Primary Class + if (item.type === "class") + item.isOriginalClass = item._id === this.actor.data.data.details.originalClass; - // Item details - item.img = item.img || CONST.DEFAULT_TOKEN; - item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1); - item.attunement = { - [CONFIG.SW5E.attunementTypes.REQUIRED]: { - icon: "fa-sun", - cls: "not-attuned", - title: "SW5E.AttunementRequired" - }, - [CONFIG.SW5E.attunementTypes.ATTUNED]: { - icon: "fa-sun", - cls: "attuned", - title: "SW5E.AttunementAttuned" + // Classify items into types + if (item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school)) arr[1].push(item); + else if (item.type === "power" && ["tec"].includes(item.data.school)) arr[2].push(item); + else if (item.type === "feat") arr[3].push(item); + else if (item.type === "class") arr[4].push(item); + else if (item.type === "deployment") arr[5].push(item); + else if (item.type === "deploymentfeature") arr[6].push(item); + else if (item.type === "venture") arr[7].push(item); + else if (item.type === "species") arr[8].push(item); + else if (item.type === "archetype") arr[9].push(item); + else if (item.type === "classfeature") arr[10].push(item); + else if (item.type === "background") arr[11].push(item); + else if (item.type === "fightingstyle") arr[12].push(item); + else if (item.type === "fightingmastery") arr[13].push(item); + else if (item.type === "lightsaberform") arr[14].push(item); + else if (Object.keys(inventory).includes(item.type)) arr[0].push(item); + return arr; + }, + [[], [], [], [], [], [], [], [], [], [], [], [], [], [], []] + ); + + // Apply active item filters + items = this._filterItems(items, this._filters.inventory); + forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook); + techpowers = this._filterItems(techpowers, this._filters.techPowerbook); + feats = this._filterItems(feats, this._filters.features); + + // Organize items + for (let i of items) { + i.data.quantity = i.data.quantity || 0; + i.data.weight = i.data.weight || 0; + i.totalWeight = (i.data.quantity * i.data.weight).toNearest(0.1); + inventory[i.type].items.push(i); } - }[item.data.attunement]; - // Item usage - item.hasUses = item.data.uses && (item.data.uses.max > 0); - item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false); - item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0)); - item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type)); + // Organize Powerbook and count the number of prepared powers (excluding always, at will, etc...) + const forcePowerbook = this._preparePowerbook(data, forcepowers, "uni"); + const techPowerbook = this._preparePowerbook(data, techpowers, "tec"); - // Item toggle state - this._prepareItemToggleState(item); + // Organize Features + const features = { + classes: { + label: "SW5E.ItemTypeClassPl", + items: [], + hasActions: false, + dataset: {type: "class"}, + isClass: true + }, + classfeatures: { + label: "SW5E.ItemTypeClassFeats", + items: [], + hasActions: true, + dataset: {type: "classfeature"}, + isClassfeature: true + }, + archetype: { + label: "SW5E.ItemTypeArchetype", + items: [], + hasActions: false, + dataset: {type: "archetype"}, + isArchetype: true + }, + deployments: { + label: "SW5E.ItemTypeDeploymentPl", + items: [], + hasActions: false, + dataset: {type: "deployment"}, + isDeployment: true + }, + deploymentfeatures: { + label: "SW5E.ItemTypeDeploymentFeaturePl", + items: [], + hasActions: true, + dataset: {type: "deploymentfeature"}, + isDeploymentfeature: true + }, + ventures: { + label: "SW5E.ItemTypeVenturePl", + items: [], + hasActions: false, + dataset: {type: "venture"}, + isVenture: true + }, + species: { + label: "SW5E.ItemTypeSpecies", + items: [], + hasActions: false, + dataset: {type: "species"}, + isSpecies: true + }, + background: { + label: "SW5E.ItemTypeBackground", + items: [], + hasActions: false, + dataset: {type: "background"}, + isBackground: true + }, + fightingstyles: { + label: "SW5E.ItemTypeFightingStylePl", + items: [], + hasActions: false, + dataset: {type: "fightingstyle"}, + isFightingstyle: true + }, + fightingmasteries: { + label: "SW5E.ItemTypeFightingMasteryPl", + items: [], + hasActions: false, + dataset: {type: "fightingmastery"}, + isFightingmastery: true + }, + lightsaberforms: { + label: "SW5E.ItemTypeLightsaberFormPl", + items: [], + hasActions: false, + dataset: {type: "lightsaberform"}, + isLightsaberform: true + }, + active: { + label: "SW5E.FeatureActive", + items: [], + hasActions: true, + dataset: {"type": "feat", "activation.type": "action"} + }, + passive: {label: "SW5E.FeaturePassive", items: [], hasActions: false, dataset: {type: "feat"}} + }; + for (let f of feats) { + if (f.data.activation.type) features.active.items.push(f); + else features.passive.items.push(f); + } + classes.sort((a, b) => b.data.levels - a.data.levels); + features.classes.items = classes; + features.classfeatures.items = classfeatures; + features.archetype.items = archetypes; + features.deployments.items = deployments; + features.deploymentfeatures.items = deploymentfeatures; + features.ventures.items = ventures; + features.species.items = species; + features.background.items = backgrounds; + features.fightingstyles.items = fightingstyles; + features.fightingmasteries.items = fightingmasteries; + features.lightsaberforms.items = lightsaberforms; - // Primary Class - if ( item.type === "class" ) item.isOriginalClass = ( item._id === this.actor.data.data.details.originalClass ); - - // Classify items into types - if ( item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school) ) arr[1].push(item); - else if ( item.type === "power" && ["tec"].includes(item.data.school) ) arr[2].push(item); - else if ( item.type === "feat" ) arr[3].push(item); - else if ( item.type === "class" ) arr[4].push(item); - else if ( item.type === "deployment" ) arr[5].push(item); - else if ( item.type === "deploymentfeature" ) arr[6].push(item); - else if ( item.type === "venture" ) arr[7].push(item); - else if ( item.type === "species" ) arr[8].push(item); - else if ( item.type === "archetype" ) arr[9].push(item); - else if ( item.type === "classfeature" ) arr[10].push(item); - else if ( item.type === "background" ) arr[11].push(item); - else if ( item.type === "fightingstyle" ) arr[12].push(item); - else if ( item.type === "fightingmastery" ) arr[13].push(item); - else if ( item.type === "lightsaberform" ) arr[14].push(item); - else if ( Object.keys(inventory).includes(item.type ) ) arr[0].push(item); - return arr; - }, [[], [], [], [], [], [], [], [], [], [], [], [], [], [], []]); - - // Apply active item filters - items = this._filterItems(items, this._filters.inventory); - forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook); - techpowers = this._filterItems(techpowers, this._filters.techPowerbook); - feats = this._filterItems(feats, this._filters.features); - - // Organize items - for ( let i of items ) { - i.data.quantity = i.data.quantity || 0; - i.data.weight = i.data.weight || 0; - i.totalWeight = (i.data.quantity * i.data.weight).toNearest(0.1); - inventory[i.type].items.push(i); + // Assign and return + data.inventory = Object.values(inventory); + data.forcePowerbook = forcePowerbook; + data.techPowerbook = techPowerbook; + data.features = Object.values(features); } - // Organize Powerbook and count the number of prepared powers (excluding always, at will, etc...) - const forcePowerbook = this._preparePowerbook(data, forcepowers, "uni"); - const techPowerbook = this._preparePowerbook(data, techpowers, "tec"); + /* -------------------------------------------- */ - // Organize Features - const features = { - classes: { label: "SW5E.ItemTypeClassPl", items: [], hasActions: false, dataset: {type: "class"}, isClass: true }, - classfeatures: { label: "SW5E.ItemTypeClassFeats", items: [], hasActions: true, dataset: {type: "classfeature"}, isClassfeature: true }, - archetype: { label: "SW5E.ItemTypeArchetype", items: [], hasActions: false, dataset: {type: "archetype"}, isArchetype: true }, - deployments: { label: "SW5E.ItemTypeDeploymentPl", items: [], hasActions: false, dataset: {type: "deployment"}, isDeployment: true }, - deploymentfeatures: { label: "SW5E.ItemTypeDeploymentFeaturePl", items: [], hasActions: true, dataset: {type: "deploymentfeature"}, isDeploymentfeature: true }, - ventures: { label: "SW5E.ItemTypeVenturePl", items: [], hasActions: false, dataset: {type: "venture"}, isVenture: true }, - species: { label: "SW5E.ItemTypeSpecies", items: [], hasActions: false, dataset: {type: "species"}, isSpecies: true }, - background: { label: "SW5E.ItemTypeBackground", items: [], hasActions: false, dataset: {type: "background"}, isBackground: true }, - fightingstyles: { label: "SW5E.ItemTypeFightingStylePl", items: [], hasActions: false, dataset: {type: "fightingstyle"}, isFightingstyle: true }, - fightingmasteries: { label: "SW5E.ItemTypeFightingMasteryPl", items: [], hasActions: false, dataset: {type: "fightingmastery"}, isFightingmastery: true }, - lightsaberforms: { label: "SW5E.ItemTypeLightsaberFormPl", items: [], hasActions: false, dataset: {type: "lightsaberform"}, isLightsaberform: true }, - active: { label: "SW5E.FeatureActive", items: [], hasActions: true, dataset: {type: "feat", "activation.type": "action"} }, - passive: { label: "SW5E.FeaturePassive", items: [], hasActions: false, dataset: {type: "feat"} } - }; - for ( let f of feats ) { - if ( f.data.activation.type ) features.active.items.push(f); - else features.passive.items.push(f); + /** + * A helper method to establish the displayed preparation state for an item + * @param {Item} item + * @private + */ + _prepareItemToggleState(item) { + if (item.type === "power") { + const isAlways = getProperty(item.data, "preparation.mode") === "always"; + const isPrepared = getProperty(item.data, "preparation.prepared"); + item.toggleClass = isPrepared ? "active" : ""; + if (isAlways) item.toggleClass = "fixed"; + if (isAlways) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.always; + else if (isPrepared) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.prepared; + else item.toggleTitle = game.i18n.localize("SW5E.PowerUnprepared"); + } else { + const isActive = getProperty(item.data, "equipped"); + item.toggleClass = isActive ? "active" : ""; + item.toggleTitle = game.i18n.localize(isActive ? "SW5E.Equipped" : "SW5E.Unequipped"); + } } - classes.sort((a, b) => b.data.levels - a.data.levels); - features.classes.items = classes; - features.classfeatures.items = classfeatures; - features.archetype.items = archetypes; - features.deployments.items = deployments; - features.deploymentfeatures.items = deploymentfeatures; - features.ventures.items = ventures; - features.species.items = species; - features.background.items = backgrounds; - features.fightingstyles.items = fightingstyles; - features.fightingmasteries.items = fightingmasteries; - features.lightsaberforms.items = lightsaberforms; - - // Assign and return - data.inventory = Object.values(inventory); - data.forcePowerbook = forcePowerbook; - data.techPowerbook = techPowerbook; - data.features = Object.values(features); - } + /* -------------------------------------------- */ + /* Event Listeners and Handlers /* -------------------------------------------- */ - /** - * A helper method to establish the displayed preparation state for an item - * @param {Item} item - * @private - */ - _prepareItemToggleState(item) { - if (item.type === "power") { - const isAlways = getProperty(item.data, "preparation.mode") === "always"; - const isPrepared = getProperty(item.data, "preparation.prepared"); - item.toggleClass = isPrepared ? "active" : ""; - if ( isAlways ) item.toggleClass = "fixed"; - if ( isAlways ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.always; - else if ( isPrepared ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.prepared; - else item.toggleTitle = game.i18n.localize("SW5E.PowerUnprepared"); - } - else { - const isActive = getProperty(item.data, "equipped"); - item.toggleClass = isActive ? "active" : ""; - item.toggleTitle = game.i18n.localize(isActive ? "SW5E.Equipped" : "SW5E.Unequipped"); - } - } + /** + * Activate event listeners using the prepared sheet HTML + * @param html {jQuery} The prepared HTML object ready to be rendered into the DOM + */ + activateListeners(html) { + super.activateListeners(html); + if (!this.isEditable) return; - /* -------------------------------------------- */ - /* Event Listeners and Handlers - /* -------------------------------------------- */ + // Inventory Functions + // html.find(".currency-convert").click(this._onConvertCurrency.bind(this)); - /** - * Activate event listeners using the prepared sheet HTML - * @param html {jQuery} The prepared HTML object ready to be rendered into the DOM - */ - activateListeners(html) { - super.activateListeners(html); - if ( !this.isEditable ) return; + // Item State Toggling + html.find(".item-toggle").click(this._onToggleItem.bind(this)); - // Inventory Functions - // html.find(".currency-convert").click(this._onConvertCurrency.bind(this)); + // Short and Long Rest + html.find(".short-rest").click(this._onShortRest.bind(this)); + html.find(".long-rest").click(this._onLongRest.bind(this)); - // Item State Toggling - html.find('.item-toggle').click(this._onToggleItem.bind(this)); + // Rollable sheet actions + html.find(".rollable[data-action]").click(this._onSheetAction.bind(this)); - // Short and Long Rest - html.find('.short-rest').click(this._onShortRest.bind(this)); - html.find('.long-rest').click(this._onLongRest.bind(this)); - - // Rollable sheet actions - html.find(".rollable[data-action]").click(this._onSheetAction.bind(this)); - - // Send Languages to Chat onClick - html.find('[data-options="share-languages"]').click(event => { - event.preventDefault(); - let langs = this.actor.data.data.traits.languages.value.map(l => CONFIG.SW5E.languages[l] || l).join(", "); - let custom = this.actor.data.data.traits.languages.custom; - if (custom) langs += ", " + custom.replace(/;/g, ","); - let content = ` + // Send Languages to Chat onClick + html.find('[data-options="share-languages"]').click((event) => { + event.preventDefault(); + let langs = this.actor.data.data.traits.languages.value + .map((l) => CONFIG.SW5E.languages[l] || l) + .join(", "); + let custom = this.actor.data.data.traits.languages.custom; + if (custom) langs += ", " + custom.replace(/;/g, ","); + let content = `
@@ -256,404 +349,404 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
`; - // Send to Chat - let rollBlind = false; - let rollMode = game.settings.get("core", "rollMode"); - if (rollMode === "blindroll") rollBlind = true; - let data = { - user: game.user.data._id, - content: content, - blind: rollBlind, - speaker: { - actor: this.actor.data._id, - token: this.actor.token, - alias: this.actor.name - }, - type: CONST.CHAT_MESSAGE_TYPES.OTHER - }; + // Send to Chat + let rollBlind = false; + let rollMode = game.settings.get("core", "rollMode"); + if (rollMode === "blindroll") rollBlind = true; + let data = { + user: game.user.data._id, + content: content, + blind: rollBlind, + speaker: { + actor: this.actor.data._id, + token: this.actor.token, + alias: this.actor.name + }, + type: CONST.CHAT_MESSAGE_TYPES.OTHER + }; - if (["gmroll", "blindroll"].includes(rollMode)) data["whisper"] = ChatMessage.getWhisperRecipients("GM"); - else if (rollMode === "selfroll") data["whisper"] = [game.users.get(game.user.data._id)]; + if (["gmroll", "blindroll"].includes(rollMode)) data["whisper"] = ChatMessage.getWhisperRecipients("GM"); + else if (rollMode === "selfroll") data["whisper"] = [game.users.get(game.user.data._id)]; - ChatMessage.create(data); - }); + ChatMessage.create(data); + }); - // Item Delete Confirmation - html.find('.item-delete').off("click"); - html.find('.item-delete').click(event => { - let li = $(event.currentTarget).parents('.item'); - let itemId = li.attr("data-item-id"); - let item = this.actor.items.get(itemId); - new Dialog({ - title: `Deleting ${item.data.name}`, - content: `

Are you sure you want to delete ${item.data.name}?

`, - buttons: { - Yes: { - icon: '', - label: 'Yes', - callback: dlg => { - this.actor.deleteOwnedItem(itemId); - } - }, - cancel: { - icon: '', - label: 'No' - }, - }, - default: 'cancel' - }).render(true); - }); - } - - /* -------------------------------------------- */ - - /** - * Handle mouse click events for character sheet actions - * @param {MouseEvent} event The originating click event - * @private - */ - _onSheetAction(event) { - event.preventDefault(); - const button = event.currentTarget; - switch( button.dataset.action ) { - case "rollDeathSave": - return this.actor.rollDeathSave({event: event}); - case "rollInitiative": - return this.actor.rollInitiative({createCombatants: true}); + // Item Delete Confirmation + html.find(".item-delete").off("click"); + html.find(".item-delete").click((event) => { + let li = $(event.currentTarget).parents(".item"); + let itemId = li.attr("data-item-id"); + let item = this.actor.items.get(itemId); + new Dialog({ + title: `Deleting ${item.data.name}`, + content: `

Are you sure you want to delete ${item.data.name}?

`, + buttons: { + Yes: { + icon: '', + label: "Yes", + callback: (dlg) => { + this.actor.deleteOwnedItem(itemId); + } + }, + cancel: { + icon: '', + label: "No" + } + }, + default: "cancel" + }).render(true); + }); } - } - /* -------------------------------------------- */ + /* -------------------------------------------- */ - - /** - * Handle toggling the state of an Owned Item within the Actor - * @param {Event} event The triggering click event - * @private - */ - _onToggleItem(event) { - event.preventDefault(); - const itemId = event.currentTarget.closest(".item").dataset.itemId; - const item = this.actor.items.get(itemId); - const attr = item.data.type === "power" ? "data.preparation.prepared" : "data.equipped"; - return item.update({[attr]: !getProperty(item.data, attr)}); - } - - /* -------------------------------------------- */ - - /** - * Take a short rest, calling the relevant function on the Actor instance - * @param {Event} event The triggering click event - * @private - */ - async _onShortRest(event) { - event.preventDefault(); - await this._onSubmit(event); - return this.actor.shortRest(); - } - - /* -------------------------------------------- */ - - /** - * Take a long rest, calling the relevant function on the Actor instance - * @param {Event} event The triggering click event - * @private - */ - async _onLongRest(event) { - event.preventDefault(); - await this._onSubmit(event); - return this.actor.longRest(); - } - - /* -------------------------------------------- */ - - /** @override */ - async _onDropItemCreate(itemData) { - - // Increment the number of class levels of a character instead of creating a new item - if ( itemData.type === "class" ) { - const cls = this.actor.itemTypes.class.find(c => c.name === itemData.name); - let priorLevel = cls?.data.data.levels ?? 0; - if ( !!cls ) { - const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level); - if ( next > priorLevel ) { - itemData.levels = next; - return cls.update({"data.levels": next}); + /** + * Handle mouse click events for character sheet actions + * @param {MouseEvent} event The originating click event + * @private + */ + _onSheetAction(event) { + event.preventDefault(); + const button = event.currentTarget; + switch (button.dataset.action) { + case "rollDeathSave": + return this.actor.rollDeathSave({event: event}); + case "rollInitiative": + return this.actor.rollInitiative({createCombatants: true}); } - } } - // Increment the number of deployment ranks of a character instead of creating a new item - // else if ( itemData.type === "deployment" ) { - // const rnk = this.actor.itemTypes.deployment.find(c => c.name === itemData.name); - // let priorRank = rnk?.data.data.ranks ?? 0; - // if ( !!rnk ) { - // const next = Math.min(priorLevel + 1, 5 + priorRank - this.actor.data.data.details.rank); - // if ( next > priorRank ) { - // itemData.ranks = next; - // return rnk.update({"data.ranks": next}); - // } - // } - // } + /* -------------------------------------------- */ - // Default drop handling if levels were not added - return super._onDropItemCreate(itemData); - } + /** + * Handle toggling the state of an Owned Item within the Actor + * @param {Event} event The triggering click event + * @private + */ + _onToggleItem(event) { + event.preventDefault(); + const itemId = event.currentTarget.closest(".item").dataset.itemId; + const item = this.actor.items.get(itemId); + const attr = item.data.type === "power" ? "data.preparation.prepared" : "data.equipped"; + return item.update({[attr]: !getProperty(item.data, attr)}); + } + + /* -------------------------------------------- */ + + /** + * Take a short rest, calling the relevant function on the Actor instance + * @param {Event} event The triggering click event + * @private + */ + async _onShortRest(event) { + event.preventDefault(); + await this._onSubmit(event); + return this.actor.shortRest(); + } + + /* -------------------------------------------- */ + + /** + * Take a long rest, calling the relevant function on the Actor instance + * @param {Event} event The triggering click event + * @private + */ + async _onLongRest(event) { + event.preventDefault(); + await this._onSubmit(event); + return this.actor.longRest(); + } + + /* -------------------------------------------- */ + + /** @override */ + async _onDropItemCreate(itemData) { + // Increment the number of class levels of a character instead of creating a new item + if (itemData.type === "class") { + const cls = this.actor.itemTypes.class.find((c) => c.name === itemData.name); + let priorLevel = cls?.data.data.levels ?? 0; + if (!!cls) { + const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level); + if (next > priorLevel) { + itemData.levels = next; + return cls.update({"data.levels": next}); + } + } + } + + // Increment the number of deployment ranks of a character instead of creating a new item + // else if ( itemData.type === "deployment" ) { + // const rnk = this.actor.itemTypes.deployment.find(c => c.name === itemData.name); + // let priorRank = rnk?.data.data.ranks ?? 0; + // if ( !!rnk ) { + // const next = Math.min(priorLevel + 1, 5 + priorRank - this.actor.data.data.details.rank); + // if ( next > priorRank ) { + // itemData.ranks = next; + // return rnk.update({"data.ranks": next}); + // } + // } + // } + + // Default drop handling if levels were not added + return super._onDropItemCreate(itemData); + } } async function addFavorites(app, html, data) { - // Thisfunction is adapted for the SwaltSheet from the Favorites Item - // Tab Module created for Foundry VTT - by Felix Müller (Felix#6196 on Discord). - // It is licensed under a Creative Commons Attribution 4.0 International License - // and can be found at https://github.com/syl3r86/favtab. - let favItems = []; - let favFeats = []; - let favPowers = { - 0: { - isCantrip: true, - powers: [] - }, - 1: { - powers: [], - value: data.actor.data.powers.power1.value, - max: data.actor.data.powers.power1.max - }, - 2: { - powers: [], - value: data.actor.data.powers.power2.value, - max: data.actor.data.powers.power2.max - }, - 3: { - powers: [], - value: data.actor.data.powers.power3.value, - max: data.actor.data.powers.power3.max - }, - 4: { - powers: [], - value: data.actor.data.powers.power4.value, - max: data.actor.data.powers.power4.max - }, - 5: { - powers: [], - value: data.actor.data.powers.power5.value, - max: data.actor.data.powers.power5.max - }, - 6: { - powers: [], - value: data.actor.data.powers.power6.value, - max: data.actor.data.powers.power6.max - }, - 7: { - powers: [], - value: data.actor.data.powers.power7.value, - max: data.actor.data.powers.power7.max - }, - 8: { - powers: [], - value: data.actor.data.powers.power8.value, - max: data.actor.data.powers.power8.max - }, - 9: { - powers: [], - value: data.actor.data.powers.power9.value, - max: data.actor.data.powers.power9.max - } - } + // Thisfunction is adapted for the SwaltSheet from the Favorites Item + // Tab Module created for Foundry VTT - by Felix Müller (Felix#6196 on Discord). + // It is licensed under a Creative Commons Attribution 4.0 International License + // and can be found at https://github.com/syl3r86/favtab. + let favItems = []; + let favFeats = []; + let favPowers = { + 0: { + isCantrip: true, + powers: [] + }, + 1: { + powers: [], + value: data.actor.data.powers.power1.value, + max: data.actor.data.powers.power1.max + }, + 2: { + powers: [], + value: data.actor.data.powers.power2.value, + max: data.actor.data.powers.power2.max + }, + 3: { + powers: [], + value: data.actor.data.powers.power3.value, + max: data.actor.data.powers.power3.max + }, + 4: { + powers: [], + value: data.actor.data.powers.power4.value, + max: data.actor.data.powers.power4.max + }, + 5: { + powers: [], + value: data.actor.data.powers.power5.value, + max: data.actor.data.powers.power5.max + }, + 6: { + powers: [], + value: data.actor.data.powers.power6.value, + max: data.actor.data.powers.power6.max + }, + 7: { + powers: [], + value: data.actor.data.powers.power7.value, + max: data.actor.data.powers.power7.max + }, + 8: { + powers: [], + value: data.actor.data.powers.power8.value, + max: data.actor.data.powers.power8.max + }, + 9: { + powers: [], + value: data.actor.data.powers.power9.value, + max: data.actor.data.powers.power9.max + } + }; - let powerCount = 0 - let items = data.actor.items; - for (let item of items) { - if (item.type == "class") continue; - if (item.flags.favtab === undefined || item.flags.favtab.isFavourite === undefined) { - item.flags.favtab = { - isFavourite: false - }; + let powerCount = 0; + let items = data.actor.items; + for (let item of items) { + if (item.type == "class") continue; + if (item.flags.favtab === undefined || item.flags.favtab.isFavourite === undefined) { + item.flags.favtab = { + isFavourite: false + }; + } + let isFav = item.flags.favtab.isFavourite; + if (app.options.editable) { + let favBtn = $( + `` + ); + favBtn.click((ev) => { + app.actor.items.get(item.data._id).update({ + "flags.favtab.isFavourite": !item.flags.favtab.isFavourite + }); + }); + html.find(`.item[data-item-id="${item.data._id}"]`).find(".item-controls").prepend(favBtn); + } + + if (isFav) { + item.powerComps = ""; + if (item.data.components) { + let comps = item.data.components; + let v = comps.vocal ? "V" : ""; + let s = comps.somatic ? "S" : ""; + let m = comps.material ? "M" : ""; + let c = !!comps.concentration; + let r = !!comps.ritual; + item.powerComps = `${v}${s}${m}`; + item.powerCon = c; + item.powerRit = r; + } + + item.editable = app.options.editable; + switch (item.type) { + case "feat": + if (item.flags.favtab.sort === undefined) { + item.flags.favtab.sort = (favFeats.count + 1) * 100000; // initial sort key if not present + } + favFeats.push(item); + break; + case "power": + if (item.data.preparation.mode) { + item.powerPrepMode = ` (${CONFIG.SW5E.powerPreparationModes[item.data.preparation.mode]})`; + } + if (item.data.level) { + favPowers[item.data.level].powers.push(item); + } else { + favPowers[0].powers.push(item); + } + powerCount++; + break; + default: + if (item.flags.favtab.sort === undefined) { + item.flags.favtab.sort = (favItems.count + 1) * 100000; // initial sort key if not present + } + favItems.push(item); + break; + } + } } - let isFav = item.flags.favtab.isFavourite; + + // Alter core CSS to fit new button + // if (app.options.editable) { + // html.find('.powerbook .item-controls').css('flex', '0 0 88px'); + // html.find('.inventory .item-controls, .features .item-controls').css('flex', '0 0 90px'); + // html.find('.favourite .item-controls').css('flex', '0 0 22px'); + // } + + let tabContainer = html.find(".favtabtarget"); + data.favItems = favItems.length > 0 ? favItems.sort((a, b) => a.flags.favtab.sort - b.flags.favtab.sort) : false; + data.favFeats = favFeats.length > 0 ? favFeats.sort((a, b) => a.flags.favtab.sort - b.flags.favtab.sort) : false; + data.favPowers = powerCount > 0 ? favPowers : false; + data.editable = app.options.editable; + + await loadTemplates(["systems/sw5e/templates/actors/newActor/item.hbs"]); + let favtabHtml = $(await renderTemplate("systems/sw5e/templates/actors/newActor/template.hbs", data)); + favtabHtml.find(".item-name h4").click((event) => app._onItemSummary(event)); + if (app.options.editable) { - let favBtn = $(``); - favBtn.click(ev => { - app.actor.items.get(item.data._id).update({ - "flags.favtab.isFavourite": !item.flags.favtab.isFavourite + favtabHtml.find(".item-image").click((ev) => app._onItemRoll(ev)); + let handler = (ev) => app._onDragStart(ev); + favtabHtml.find(".item").each((i, li) => { + if (li.classList.contains("inventory-header")) return; + li.setAttribute("draggable", true); + li.addEventListener("dragstart", handler, false); + }); + //favtabHtml.find('.item-toggle').click(event => app._onToggleItem(event)); + favtabHtml.find(".item-edit").click((ev) => { + let itemId = $(ev.target).parents(".item")[0].dataset.itemId; + app.actor.items.get(itemId).sheet.render(true); + }); + favtabHtml.find(".item-fav").click((ev) => { + let itemId = $(ev.target).parents(".item")[0].dataset.itemId; + let val = !app.actor.items.get(itemId).data.flags.favtab.isFavourite; + app.actor.items.get(itemId).update({ + "flags.favtab.isFavourite": val + }); + }); + + // Sorting + favtabHtml.find(".item").on("drop", (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + + let dropData = JSON.parse(ev.originalEvent.dataTransfer.getData("text/plain")); + // if (dropData.actorId !== app.actor.id || dropData.data.type === 'power') return; + if (dropData.actorId !== app.actor.id) return; + let list = null; + if (dropData.data.type === "feat") list = favFeats; + else list = favItems; + let dragSource = list.find((i) => i.data._id === dropData.data._id); + let siblings = list.filter((i) => i.data._id !== dropData.data._id); + let targetId = ev.target.closest(".item").dataset.itemId; + let dragTarget = siblings.find((s) => s.data._id === targetId); + + if (dragTarget === undefined) return; + const sortUpdates = SortingHelpers.performIntegerSort(dragSource, { + target: dragTarget, + siblings: siblings, + sortKey: "flags.favtab.sort" + }); + const updateData = sortUpdates.map((u) => { + const update = u.update; + update._id = u.target.data._id; + return update; + }); + app.actor.updateEmbeddedEntity("OwnedItem", updateData); }); - }); - html.find(`.item[data-item-id="${item.data._id}"]`).find('.item-controls').prepend(favBtn); } - - if (isFav) { - item.powerComps = ""; - if (item.data.components) { - let comps = item.data.components; - let v = (comps.vocal) ? "V" : ""; - let s = (comps.somatic) ? "S" : ""; - let m = (comps.material) ? "M" : ""; - let c = !!(comps.concentration); - let r = !!(comps.ritual); - item.powerComps = `${v}${s}${m}`; - item.powerCon = c; - item.powerRit = r; - } - - item.editable = app.options.editable; - switch (item.type) { - case 'feat': - if (item.flags.favtab.sort === undefined) { - item.flags.favtab.sort = (favFeats.count + 1) * 100000; // initial sort key if not present - } - favFeats.push(item); - break; - case 'power': - if (item.data.preparation.mode) { - item.powerPrepMode = ` (${CONFIG.SW5E.powerPreparationModes[item.data.preparation.mode]})` - } - if (item.data.level) { - favPowers[item.data.level].powers.push(item); - } else { - favPowers[0].powers.push(item); - } - powerCount++; - break; - default: - if (item.flags.favtab.sort === undefined) { - item.flags.favtab.sort = (favItems.count + 1) * 100000; // initial sort key if not present - } - favItems.push(item); - break; - } - } - } - - // Alter core CSS to fit new button - // if (app.options.editable) { - // html.find('.powerbook .item-controls').css('flex', '0 0 88px'); - // html.find('.inventory .item-controls, .features .item-controls').css('flex', '0 0 90px'); - // html.find('.favourite .item-controls').css('flex', '0 0 22px'); - // } - - let tabContainer = html.find('.favtabtarget'); - data.favItems = favItems.length > 0 ? favItems.sort((a, b) => (a.flags.favtab.sort) - (b.flags.favtab.sort)) : false; - data.favFeats = favFeats.length > 0 ? favFeats.sort((a, b) => (a.flags.favtab.sort) - (b.flags.favtab.sort)) : false; - data.favPowers = powerCount > 0 ? favPowers : false; - data.editable = app.options.editable; - - await loadTemplates(['systems/sw5e/templates/actors/newActor/item.hbs']); - let favtabHtml = $(await renderTemplate('systems/sw5e/templates/actors/newActor/template.hbs', data)); - favtabHtml.find('.item-name h4').click(event => app._onItemSummary(event)); - - if (app.options.editable) { - favtabHtml.find('.item-image').click(ev => app._onItemRoll(ev)); - let handler = ev => app._onDragStart(ev); - favtabHtml.find('.item').each((i, li) => { - if (li.classList.contains("inventory-header")) return; - li.setAttribute("draggable", true); - li.addEventListener("dragstart", handler, false); - }); - //favtabHtml.find('.item-toggle').click(event => app._onToggleItem(event)); - favtabHtml.find('.item-edit').click(ev => { - let itemId = $(ev.target).parents('.item')[0].dataset.itemId; - app.actor.items.get(itemId).sheet.render(true); - }); - favtabHtml.find('.item-fav').click(ev => { - let itemId = $(ev.target).parents('.item')[0].dataset.itemId; - let val = !app.actor.items.get(itemId).data.flags.favtab.isFavourite - app.actor.items.get(itemId).update({ - "flags.favtab.isFavourite": val - }); - }); - - // Sorting - favtabHtml.find('.item').on('drop', ev => { - ev.preventDefault(); - ev.stopPropagation(); - - let dropData = JSON.parse(ev.originalEvent.dataTransfer.getData('text/plain')); - // if (dropData.actorId !== app.actor.id || dropData.data.type === 'power') return; - if (dropData.actorId !== app.actor.id) return; - let list = null; - if (dropData.data.type === 'feat') list = favFeats; - else list = favItems; - let dragSource = list.find(i => i.data._id === dropData.data._id); - let siblings = list.filter(i => i.data._id !== dropData.data._id); - let targetId = ev.target.closest('.item').dataset.itemId; - let dragTarget = siblings.find(s => s.data._id === targetId); - - if (dragTarget === undefined) return; - const sortUpdates = SortingHelpers.performIntegerSort(dragSource, { - target: dragTarget, - siblings: siblings, - sortKey: 'flags.favtab.sort' - }); - const updateData = sortUpdates.map(u => { - const update = u.update; - update._id = u.target.data._id; - return update; - }); - app.actor.updateEmbeddedEntity("OwnedItem", updateData); - }); - } - tabContainer.append(favtabHtml); - // if(app.options.editable) { - // let handler = ev => app._onDragItemStart(ev); - // tabContainer.find('.item').each((i, li) => { - // if (li.classList.contains("inventory-header")) return; - // li.setAttribute("draggable", true); - // li.addEventListener("dragstart", handler, false); - // }); - //} - // try { - // if (game.modules.get("betterrolls5e") && game.modules.get("betterrolls5e").active) BetterRolls.addItemContent(app.object, favtabHtml, ".item .item-name h4", ".item-properties", ".item > .rollable div"); - // } - // catch (err) { - // // Better Rolls not found! - // } - Hooks.callAll("renderedSwaltSheet", app, html, data); + tabContainer.append(favtabHtml); + // if(app.options.editable) { + // let handler = ev => app._onDragItemStart(ev); + // tabContainer.find('.item').each((i, li) => { + // if (li.classList.contains("inventory-header")) return; + // li.setAttribute("draggable", true); + // li.addEventListener("dragstart", handler, false); + // }); + //} + // try { + // if (game.modules.get("betterrolls5e") && game.modules.get("betterrolls5e").active) BetterRolls.addItemContent(app.object, favtabHtml, ".item .item-name h4", ".item-properties", ".item > .rollable div"); + // } + // catch (err) { + // // Better Rolls not found! + // } + Hooks.callAll("renderedSwaltSheet", app, html, data); } async function addSubTabs(app, html, data) { - if(data.options.subTabs == null) { - //let subTabs = []; //{subgroup: '', target: '', active: false} - data.options.subTabs = {}; - html.find('[data-subgroup-selection] [data-subgroup]').each((idx, el) => { - let subgroup = el.getAttribute('data-subgroup'); - let target = el.getAttribute('data-target'); - let targetObj = {target: target, active: el.classList.contains("active")} - if(data.options.subTabs.hasOwnProperty(subgroup)) { - data.options.subTabs[subgroup].push(targetObj); - } else { - data.options.subTabs[subgroup] = []; - data.options.subTabs[subgroup].push(targetObj); - } - }) - } - - for(const group in data.options.subTabs) { - data.options.subTabs[group].forEach(tab => { - if(tab.active) { - html.find(`[data-subgroup=${group}][data-target=${tab.target}]`).addClass('active'); - } else { - html.find(`[data-subgroup=${group}][data-target=${tab.target}]`).removeClass('active'); - } - }) - } - - html.find('[data-subgroup-selection]').children().on('click', event => { - let subgroup = event.target.closest('[data-subgroup]').getAttribute('data-subgroup'); - let target = event.target.closest('[data-target]').getAttribute('data-target'); - html.find(`[data-subgroup=${subgroup}]`).removeClass('active'); - html.find(`[data-subgroup=${subgroup}][data-target=${target}]`).addClass('active'); - let tabId = data.options.subTabs[subgroup].find(tab => { - return tab.target == target - }); - data.options.subTabs[subgroup].map(el => { - el.active = el.target == target; - return el; - }) - - }) - + if (data.options.subTabs == null) { + //let subTabs = []; //{subgroup: '', target: '', active: false} + data.options.subTabs = {}; + html.find("[data-subgroup-selection] [data-subgroup]").each((idx, el) => { + let subgroup = el.getAttribute("data-subgroup"); + let target = el.getAttribute("data-target"); + let targetObj = {target: target, active: el.classList.contains("active")}; + if (data.options.subTabs.hasOwnProperty(subgroup)) { + data.options.subTabs[subgroup].push(targetObj); + } else { + data.options.subTabs[subgroup] = []; + data.options.subTabs[subgroup].push(targetObj); + } + }); + } + for (const group in data.options.subTabs) { + data.options.subTabs[group].forEach((tab) => { + if (tab.active) { + html.find(`[data-subgroup=${group}][data-target=${tab.target}]`).addClass("active"); + } else { + html.find(`[data-subgroup=${group}][data-target=${tab.target}]`).removeClass("active"); + } + }); + } + html.find("[data-subgroup-selection]") + .children() + .on("click", (event) => { + let subgroup = event.target.closest("[data-subgroup]").getAttribute("data-subgroup"); + let target = event.target.closest("[data-target]").getAttribute("data-target"); + html.find(`[data-subgroup=${subgroup}]`).removeClass("active"); + html.find(`[data-subgroup=${subgroup}][data-target=${target}]`).addClass("active"); + let tabId = data.options.subTabs[subgroup].find((tab) => { + return tab.target == target; + }); + data.options.subTabs[subgroup].map((el) => { + el.active = el.target == target; + return el; + }); + }); } Hooks.on("renderActorSheet5eCharacterNew", (app, html, data) => { - addFavorites(app, html, data); - addSubTabs(app, html, data); -}); \ No newline at end of file + addFavorites(app, html, data); + addSubTabs(app, html, data); +}); diff --git a/module/actor/sheets/newSheet/npc.js b/module/actor/sheets/newSheet/npc.js index 59a94334..699005ea 100644 --- a/module/actor/sheets/newSheet/npc.js +++ b/module/actor/sheets/newSheet/npc.js @@ -6,143 +6,154 @@ import ActorSheet5e from "./base.js"; * @extends {ActorSheet5e} */ export default class ActorSheet5eNPCNew extends ActorSheet5e { - - /** @override */ - get template() { - if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/newActor/limited-sheet.html"; - return `systems/sw5e/templates/actors/newActor/npc-sheet.html`; - } - /** @override */ - static get defaultOptions() { - return mergeObject(super.defaultOptions, { - classes: ["sw5e", "sheet", "actor", "npc"], - width: 800, - tabs: [{ - navSelector: ".root-tabs", - contentSelector: ".sheet-body", - initial: "attributes" - }], - }); - } - - /* -------------------------------------------- */ - - /** @override */ - static unsupportedItemTypes = new Set(["class"]); - - /* -------------------------------------------- */ - - /** - * Organize Owned Items for rendering the NPC sheet - * @private - */ - _prepareItems(data) { - - // Categorize Items as Features and Powers - const features = { - weapons: { label: game.i18n.localize("SW5E.AttackPl"), items: [] , hasActions: true, dataset: {type: "weapon", "weapon-type": "natural"} }, - actions: { label: game.i18n.localize("SW5E.ActionPl"), items: [] , hasActions: true, dataset: {type: "feat", "activation.type": "action"} }, - passive: { label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"} }, - equipment: { label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}} - }; - - // Start by classifying items into groups for rendering - let [forcepowers, techpowers, other] = data.items.reduce((arr, item) => { - item.img = item.img || CONST.DEFAULT_TOKEN; - item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1); - item.hasUses = item.data.uses && (item.data.uses.max > 0); - item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false); - item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0)); - item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type)); - if ( item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school) ) arr[0].push(item); - else if ( item.type === "power" && ["tec"].includes(item.data.school) ) arr[1].push(item); - else arr[2].push(item); - return arr; - }, [[], [], []]); - - // Apply item filters - forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook); - techpowers = this._filterItems(techpowers, this._filters.techPowerbook); - other = this._filterItems(other, this._filters.features); - - // Organize Powerbook - const forcePowerbook = this._preparePowerbook(data, forcepowers, "uni"); - const techPowerbook = this._preparePowerbook(data, techpowers, "tec"); - - // Organize Features - for ( let item of other ) { - if ( item.type === "weapon" ) features.weapons.items.push(item); - else if ( item.type === "feat" ) { - if ( item.data.activation.type ) features.actions.items.push(item); - else features.passive.items.push(item); - } - else features.equipment.items.push(item); + /** @override */ + get template() { + if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/newActor/limited-sheet.html"; + return `systems/sw5e/templates/actors/newActor/npc-sheet.html`; + } + /** @override */ + static get defaultOptions() { + return mergeObject(super.defaultOptions, { + classes: ["sw5e", "sheet", "actor", "npc"], + width: 800, + tabs: [ + { + navSelector: ".root-tabs", + contentSelector: ".sheet-body", + initial: "attributes" + } + ] + }); } - // Assign and return - data.features = Object.values(features); - data.forcePowerbook = forcePowerbook; - data.techPowerbook = techPowerbook; - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** @override */ + static unsupportedItemTypes = new Set(["class"]); - /** @inheritdoc */ - getData(options) { - const data = super.getData(options); + /* -------------------------------------------- */ - // Challenge Rating - const cr = parseFloat(data.data.details.cr || 0); - const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"}; - data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1; + /** + * Organize Owned Items for rendering the NPC sheet + * @private + */ + _prepareItems(data) { + // Categorize Items as Features and Powers + const features = { + weapons: { + label: game.i18n.localize("SW5E.AttackPl"), + items: [], + hasActions: true, + dataset: {"type": "weapon", "weapon-type": "natural"} + }, + actions: { + label: game.i18n.localize("SW5E.ActionPl"), + items: [], + hasActions: true, + dataset: {"type": "feat", "activation.type": "action"} + }, + passive: {label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"}}, + equipment: {label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}} + }; - // Creature Type - data.labels["type"] = this.actor.labels.creatureType; - return data; - } + // Start by classifying items into groups for rendering + let [forcepowers, techpowers, other] = data.items.reduce( + (arr, item) => { + item.img = item.img || CONST.DEFAULT_TOKEN; + item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1; + item.hasUses = item.data.uses && item.data.uses.max > 0; + item.isOnCooldown = + item.data.recharge && !!item.data.recharge.value && item.data.recharge.charged === false; + item.isDepleted = item.isOnCooldown && item.data.uses.per && item.data.uses.value > 0; + item.hasTarget = !!item.data.target && !["none", ""].includes(item.data.target.type); + if (item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school)) arr[0].push(item); + else if (item.type === "power" && ["tec"].includes(item.data.school)) arr[1].push(item); + else arr[2].push(item); + return arr; + }, + [[], [], []] + ); - /* -------------------------------------------- */ - /* Object Updates */ - /* -------------------------------------------- */ + // Apply item filters + forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook); + techpowers = this._filterItems(techpowers, this._filters.techPowerbook); + other = this._filterItems(other, this._filters.features); - /** @override */ - async _updateObject(event, formData) { + // Organize Powerbook + const forcePowerbook = this._preparePowerbook(data, forcepowers, "uni"); + const techPowerbook = this._preparePowerbook(data, techpowers, "tec"); - // Format NPC Challenge Rating - const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5}; - let crv = "data.details.cr"; - let cr = formData[crv]; - cr = crs[cr] || parseFloat(cr); - if ( cr ) formData[crv] = cr < 1 ? cr : parseInt(cr); + // Organize Features + for (let item of other) { + if (item.type === "weapon") features.weapons.items.push(item); + else if (item.type === "feat") { + if (item.data.activation.type) features.actions.items.push(item); + else features.passive.items.push(item); + } else features.equipment.items.push(item); + } - // Parent ActorSheet update steps - return super._updateObject(event, formData); - } + // Assign and return + data.features = Object.values(features); + data.forcePowerbook = forcePowerbook; + data.techPowerbook = techPowerbook; + } - /* -------------------------------------------- */ - /* Event Listeners and Handlers */ - /* -------------------------------------------- */ + /* -------------------------------------------- */ - /** @override */ - activateListeners(html) { - super.activateListeners(html); - html.find(".health .rollable").click(this._onRollHPFormula.bind(this)); - } + /** @inheritdoc */ + getData(options) { + const data = super.getData(options); - /* -------------------------------------------- */ + // Challenge Rating + const cr = parseFloat(data.data.details.cr || 0); + const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"}; + data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1; - /** - * Handle rolling NPC health values using the provided formula - * @param {Event} event The original click event - * @private - */ - _onRollHPFormula(event) { - event.preventDefault(); - const formula = this.actor.data.data.attributes.hp.formula; - if ( !formula ) return; - const hp = new Roll(formula).roll().total; - AudioHelper.play({src: CONFIG.sounds.dice}); - this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp}); - } + // Creature Type + data.labels["type"] = this.actor.labels.creatureType; + return data; + } + + /* -------------------------------------------- */ + /* Object Updates */ + /* -------------------------------------------- */ + + /** @override */ + async _updateObject(event, formData) { + // Format NPC Challenge Rating + const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5}; + let crv = "data.details.cr"; + let cr = formData[crv]; + cr = crs[cr] || parseFloat(cr); + if (cr) formData[crv] = cr < 1 ? cr : parseInt(cr); + + // Parent ActorSheet update steps + return super._updateObject(event, formData); + } + + /* -------------------------------------------- */ + /* Event Listeners and Handlers */ + /* -------------------------------------------- */ + + /** @override */ + activateListeners(html) { + super.activateListeners(html); + html.find(".health .rollable").click(this._onRollHPFormula.bind(this)); + } + + /* -------------------------------------------- */ + + /** + * Handle rolling NPC health values using the provided formula + * @param {Event} event The original click event + * @private + */ + _onRollHPFormula(event) { + event.preventDefault(); + const formula = this.actor.data.data.attributes.hp.formula; + if (!formula) return; + const hp = new Roll(formula).roll().total; + AudioHelper.play({src: CONFIG.sounds.dice}); + this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp}); + } } - diff --git a/module/actor/sheets/newSheet/starship.js b/module/actor/sheets/newSheet/starship.js index 145a87e8..bf7e397e 100644 --- a/module/actor/sheets/newSheet/starship.js +++ b/module/actor/sheets/newSheet/starship.js @@ -6,248 +6,164 @@ import ActorSheet5e from "./base.js"; * @extends {ActorSheet5e} */ export default class ActorSheet5eStarship extends ActorSheet5e { - - /** @override */ - get template() { - if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/newActor/limited-sheet.html"; - return `systems/sw5e/templates/actors/newActor/starship.html`; - } - /** @override */ - static get defaultOptions() { - return mergeObject(super.defaultOptions, { - classes: ["sw5e", "sheet", "actor", "starship"], - width: 800, - height: 775, - tabs: [{ - navSelector: ".root-tabs", - contentSelector: ".sheet-body", - initial: "attributes" - }], - }); - } - - /* -------------------------------------------- */ - - /** - * Organize Owned Items for rendering the starship sheet - * @private - */ - _prepareItems(data) { - - // Categorize Items as Features and Powers - const features = { - weapons: { label: game.i18n.localize("SW5E.ItemTypeWeaponPl"), items: [], hasActions: true, dataset: {type: "weapon", "weapon-type": "natural"} }, - passive: { label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"} }, - equipment: { label: game.i18n.localize("SW5E.StarshipEquipment"), items: [], dataset: {type: "equipment"}}, - starshipfeatures: { label: game.i18n.localize("SW5E.StarshipfeaturePl"), items: [], hasActions: true, dataset: {type: "starshipfeature"} }, - starshipmods: { label: game.i18n.localize("SW5E.StarshipmodPl"), items: [], hasActions: false, dataset: {type: "starshipmod"} } - }; - - // Start by classifying items into groups for rendering - let [forcepowers, techpowers, other] = data.items.reduce((arr, item) => { - item.img = item.img || CONST.DEFAULT_TOKEN; - item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1); - item.hasUses = item.data.uses && (item.data.uses.max > 0); - item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false); - item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0)); - item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type)); - if ( item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school) ) arr[0].push(item); - else if ( item.type === "power" && ["tec"].includes(item.data.school) ) arr[1].push(item); - else arr[2].push(item); - return arr; - }, [[], [], []]); - - // Apply item filters - forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook); - techpowers = this._filterItems(techpowers, this._filters.techPowerbook); - other = this._filterItems(other, this._filters.features); - - // Organize Powerbook -// const forcePowerbook = this._preparePowerbook(data, forcepowers, "uni"); -// const techPowerbook = this._preparePowerbook(data, techpowers, "tec"); - - // Organize Features - for ( let item of other ) { - if ( item.type === "weapon" ) features.weapons.items.push(item); - else if ( item.type === "feat" ) { - if ( item.data.activation.type ) features.actions.items.push(item); - else features.passive.items.push(item); - } - else if ( item.type === "starshipfeature" ) { - features.starshipfeatures.items.push(item); - } - else if ( item.type === "starshipmod" ) { - features.starshipmods.items.push(item); - } - else features.equipment.items.push(item); + /** @override */ + get template() { + if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/newActor/limited-sheet.html"; + return `systems/sw5e/templates/actors/newActor/starship.html`; + } + /** @override */ + static get defaultOptions() { + return mergeObject(super.defaultOptions, { + classes: ["sw5e", "sheet", "actor", "starship"], + width: 800, + tabs: [ + { + navSelector: ".root-tabs", + contentSelector: ".sheet-body", + initial: "attributes" + } + ] + }); } - - // Assign and return - data.features = Object.values(features); -// data.forcePowerbook = forcePowerbook; -// data.techPowerbook = techPowerbook; - } - - - /* -------------------------------------------- */ - - /** @override */ - getData(options) { - const data = super.getData(options); - - // Add Size info - data.isTiny = data.actor.data.traits.size === "tiny"; - data.isSmall = data.actor.data.traits.size === "sm"; - data.isMedium = data.actor.data.traits.size === "med"; - data.isLarge = data.actor.data.traits.size === "lg"; - data.isHuge = data.actor.data.traits.size === "huge"; - data.isGargantuan = data.actor.data.traits.size === "grg"; - - // Challenge Rating - const cr = parseFloat(data.data.details.cr || 0); - const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"}; - data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1; - return data; - } - - /* -------------------------------------------- */ - /* Object Updates */ - /* -------------------------------------------- */ - - /** @override */ - async _updateObject(event, formData) { - - // Format NPC Challenge Rating - const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5}; - let crv = "data.details.cr"; - let cr = formData[crv]; - cr = crs[cr] || parseFloat(cr); - if ( cr ) formData[crv] = cr < 1 ? cr : parseInt(cr); - - // Parent ActorSheet update steps - return super._updateObject(event, formData); - } - - /* -------------------------------------------- */ - /* Event Listeners and Handlers */ - /* -------------------------------------------- */ - - /** @override */ - activateListeners(html) { - super.activateListeners(html); - html.find(".health .rollable").click(this._onRollHPFormula.bind(this)); - html.find('.refuel').click(this._onIncrementFuelLevel.bind(this)); - html.find('.burnfuel').click(this._onDecrementFuelLevel.bind(this)); - html.find('#engineslidervalue')[0].addEventListener('input', this._engineSliderUpdate.bind(this)); - html.find('#shieldslidervalue')[0].addEventListener('input', this._shieldSliderUpdate.bind(this)); - html.find('#weaponslidervalue')[0].addEventListener('input', this._weaponSliderUpdate.bind(this)); - } - - /* -------------------------------------------- */ - - /** - * Handle rolling NPC health values using the provided formula - * @param {Event} event The original click event - * @private - */ - _onRollHPFormula(event) { - event.preventDefault(); - const formula = this.actor.data.data.attributes.hp.formula; - if ( !formula ) return; - const hp = new Roll(formula).roll().total; - AudioHelper.play({src: CONFIG.sounds.dice}); - this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp}); - } - - /* -------------------------------------------- */ - - /** - * Handle refueling a starship - * @param {Event} event The original click event - * @private - */ - _onIncrementFuelLevel(event) { - // event.preventDefault(); - // const fuelcaparray = this.actor.data.effects.changes; - // var fuelcappos = fuelcaparray.indexOf('fuel.cap'); - // const refuel = this.actor.data.effect.changes[fuelcappos].value; - this.actor.update({"data.attributes.fuel.value": this.actor.data.data.attributes.fuel.cap}); - } /* -------------------------------------------- */ - /** - * Handle a starship burning fuel - * @param {Event} event The original click event - * @private - */ - _onDecrementFuelLevel(event) { - // event.preventDefault(); - // const fuelcaparray = this.actor.data.effects.changes; - // var fuelcappos = fuelcaparray.indexOf('fuel.cap'); - // const refuel = this.actor.data.effect.changes[fuelcappos].value; - this.actor.update({"data.attributes.fuel.value": this.actor.data.data.attributes.fuel.value - 1}); - } - _engineSliderUpdate(input) { - var symbol; - var coefficient; - switch(input.target.value) { - case "0": - symbol = "↓"; - coefficient = 0.5; - break; - case "1": - symbol = "="; - coefficient = 1; - break; - case "2": - symbol = "↑"; - coefficient = 2; - }; - let slideroutput = symbol; - document.querySelector('#engineslideroutput').value = slideroutput; - this.actor.update({"data.attributes.power.routing.engines": coefficient}); - } + /** + * Organize Owned Items for rendering the starship sheet + * @private + */ + _prepareItems(data) { + // Categorize Items as Features and Powers + const features = { + weapons: { + label: game.i18n.localize("SW5E.ItemTypeWeaponPl"), + items: [], + hasActions: true, + dataset: {"type": "weapon", "weapon-type": "natural"} + }, + passive: {label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"}}, + equipment: {label: game.i18n.localize("SW5E.StarshipEquipment"), items: [], dataset: {type: "equipment"}}, + starshipfeatures: { + label: game.i18n.localize("SW5E.StarshipfeaturePl"), + items: [], + hasActions: true, + dataset: {type: "starshipfeature"} + }, + starshipmods: { + label: game.i18n.localize("SW5E.StarshipmodPl"), + items: [], + hasActions: false, + dataset: {type: "starshipmod"} + } + }; - _shieldSliderUpdate(input) { - var symbol; - var coefficient; - switch(input.target.value) { - case "0": - symbol = "↓"; - coefficient = 0.5; - break; - case "1": - symbol = "="; - coefficient = 1; - break; - case "2": - symbol = "↑"; - coefficient = 2; - }; - let slideroutput = symbol; - document.querySelector('#shieldslideroutput').value = slideroutput; - this.actor.update({"data.attributes.power.routing.shields": coefficient}); - } - - _weaponSliderUpdate(input) { - var symbol; - var coefficient; - switch(input.target.value) { - case "0": - symbol = "↓"; - coefficient = 0.5; - break; - case "1": - symbol = "="; - coefficient = 1; - break; - case "2": - symbol = "↑"; - coefficient = 2; - }; - let slideroutput = symbol; - document.querySelector('#weaponslideroutput').value = slideroutput; - this.actor.update({"data.attributes.power.routing.weapons": coefficient}); - } + // Start by classifying items into groups for rendering + let [forcepowers, techpowers, other] = data.items.reduce( + (arr, item) => { + item.img = item.img || CONST.DEFAULT_TOKEN; + item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1; + item.hasUses = item.data.uses && item.data.uses.max > 0; + item.isOnCooldown = + item.data.recharge && !!item.data.recharge.value && item.data.recharge.charged === false; + item.isDepleted = item.isOnCooldown && item.data.uses.per && item.data.uses.value > 0; + item.hasTarget = !!item.data.target && !["none", ""].includes(item.data.target.type); + if (item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school)) arr[0].push(item); + else if (item.type === "power" && ["tec"].includes(item.data.school)) arr[1].push(item); + else arr[2].push(item); + return arr; + }, + [[], [], []] + ); + + // Apply item filters + forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook); + techpowers = this._filterItems(techpowers, this._filters.techPowerbook); + other = this._filterItems(other, this._filters.features); + + // Organize Powerbook + // const forcePowerbook = this._preparePowerbook(data, forcepowers, "uni"); + // const techPowerbook = this._preparePowerbook(data, techpowers, "tec"); + + // Organize Features + for (let item of other) { + if (item.type === "weapon") features.weapons.items.push(item); + else if (item.type === "feat") { + if (item.data.activation.type) features.actions.items.push(item); + else features.passive.items.push(item); + } else if (item.type === "starshipfeature") { + features.starshipfeatures.items.push(item); + } else if (item.type === "starshipmod") { + features.starshipmods.items.push(item); + } else features.equipment.items.push(item); + } + + // Assign and return + data.features = Object.values(features); + // data.forcePowerbook = forcePowerbook; + // data.techPowerbook = techPowerbook; + } + + /* -------------------------------------------- */ + + /** @override */ + getData(options) { + const data = super.getData(options); + + // Add Size info + data.isTiny = data.actor.data.traits.size === "tiny"; + data.isSmall = data.actor.data.traits.size === "sm"; + data.isMedium = data.actor.data.traits.size === "med"; + data.isLarge = data.actor.data.traits.size === "lg"; + data.isHuge = data.actor.data.traits.size === "huge"; + data.isGargantuan = data.actor.data.traits.size === "grg"; + + // Challenge Rating + const cr = parseFloat(data.data.details.cr || 0); + const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"}; + data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1; + return data; + } + + /* -------------------------------------------- */ + /* Object Updates */ + /* -------------------------------------------- */ + + /** @override */ + async _updateObject(event, formData) { + // Format NPC Challenge Rating + const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5}; + let crv = "data.details.cr"; + let cr = formData[crv]; + cr = crs[cr] || parseFloat(cr); + if (cr) formData[crv] = cr < 1 ? cr : parseInt(cr); + + // Parent ActorSheet update steps + return super._updateObject(event, formData); + } + + /* -------------------------------------------- */ + /* Event Listeners and Handlers */ + /* -------------------------------------------- */ + + /** @override */ + activateListeners(html) { + super.activateListeners(html); + html.find(".health .rollable").click(this._onRollHPFormula.bind(this)); + } + + /* -------------------------------------------- */ + + /** + * Handle rolling NPC health values using the provided formula + * @param {Event} event The original click event + * @private + */ + _onRollHPFormula(event) { + event.preventDefault(); + const formula = this.actor.data.data.attributes.hp.formula; + if (!formula) return; + const hp = new Roll(formula).roll().total; + AudioHelper.play({src: CONFIG.sounds.dice}); + this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp}); + } } diff --git a/module/actor/sheets/newSheet/vehicle.js b/module/actor/sheets/newSheet/vehicle.js index b5e28a1c..a5c6e2ea 100644 --- a/module/actor/sheets/newSheet/vehicle.js +++ b/module/actor/sheets/newSheet/vehicle.js @@ -6,411 +6,427 @@ import ActorSheet5e from "./base.js"; * @type {ActorSheet5e} */ export default class ActorSheet5eVehicle extends ActorSheet5e { - /** - * Define default rendering options for the Vehicle sheet. - * @returns {Object} - */ - static get defaultOptions() { - return mergeObject(super.defaultOptions, { - classes: ["sw5e", "sheet", "actor", "vehicle"], - width: 605, - height: 680 - }); - } - - /* -------------------------------------------- */ - - /** @override */ - static unsupportedItemTypes = new Set(["class"]); - - /* -------------------------------------------- */ - - - /** - * Creates a new cargo entry for a vehicle Actor. - */ - static get newCargo() { - return { - name: '', - quantity: 1 - }; - } - - /* -------------------------------------------- */ - - /** - * Compute the total weight of the vehicle's cargo. - * @param {Number} totalWeight The cumulative item weight from inventory items - * @param {Object} actorData The data object for the Actor being rendered - * @returns {{max: number, value: number, pct: number}} - * @private - */ - _computeEncumbrance(totalWeight, actorData) { - - // Compute currency weight - const totalCoins = Object.values(actorData.data.currency).reduce((acc, denom) => acc + denom, 0); - totalWeight += totalCoins / CONFIG.SW5E.encumbrance.currencyPerWeight; - - // Vehicle weights are an order of magnitude greater. - totalWeight /= CONFIG.SW5E.encumbrance.vehicleWeightMultiplier; - - // Compute overall encumbrance - const max = actorData.data.attributes.capacity.cargo; - const pct = Math.clamped((totalWeight * 100) / max, 0, 100); - return {value: totalWeight.toNearest(0.1), max, pct}; - } - - /* -------------------------------------------- */ - - /** @override */ - _getMovementSpeed(actorData, largestPrimary=true) { - return super._getMovementSpeed(actorData, largestPrimary); - } - - /* -------------------------------------------- */ - - /** - * Prepare items that are mounted to a vehicle and require one or more crew - * to operate. - * @private - */ - _prepareCrewedItem(item) { - - // Determine crewed status - const isCrewed = item.data.crewed; - item.toggleClass = isCrewed ? 'active' : ''; - item.toggleTitle = game.i18n.localize(`SW5E.${isCrewed ? 'Crewed' : 'Uncrewed'}`); - - // Handle crew actions - if (item.type === 'feat' && item.data.activation.type === 'crew') { - item.crew = item.data.activation.cost; - item.cover = game.i18n.localize(`SW5E.${item.data.cover ? 'CoverTotal' : 'None'}`); - if (item.data.cover === .5) item.cover = '½'; - else if (item.data.cover === .75) item.cover = '¾'; - else if (item.data.cover === null) item.cover = '—'; - if (item.crew < 1 || item.crew === null) item.crew = '—'; + /** + * Define default rendering options for the Vehicle sheet. + * @returns {Object} + */ + static get defaultOptions() { + return mergeObject(super.defaultOptions, { + classes: ["sw5e", "sheet", "actor", "vehicle"], + width: 605, + height: 680 + }); } - // Prepare vehicle weapons - if (item.type === 'equipment' || item.type === 'weapon') { - item.threshold = item.data.hp.dt ? item.data.hp.dt : '—'; - } - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** @override */ + static unsupportedItemTypes = new Set(["class"]); - /** - * Organize Owned Items for rendering the Vehicle sheet. - * @private - */ - _prepareItems(data) { - const cargoColumns = [{ - label: game.i18n.localize('SW5E.Quantity'), - css: 'item-qty', - property: 'quantity', - editable: 'Number' - }]; + /* -------------------------------------------- */ - const equipmentColumns = [{ - label: game.i18n.localize('SW5E.Quantity'), - css: 'item-qty', - property: 'data.quantity' - }, { - label: game.i18n.localize('SW5E.AC'), - css: 'item-ac', - property: 'data.armor.value' - }, { - label: game.i18n.localize('SW5E.HP'), - css: 'item-hp', - property: 'data.hp.value', - editable: 'Number' - }, { - label: game.i18n.localize('SW5E.Threshold'), - css: 'item-threshold', - property: 'threshold' - }]; - - const features = { - actions: { - label: game.i18n.localize('SW5E.ActionPl'), - items: [], - crewable: true, - dataset: {type: 'feat', 'activation.type': 'crew'}, - columns: [{ - label: game.i18n.localize('SW5E.VehicleCrew'), - css: 'item-crew', - property: 'crew' - }, { - label: game.i18n.localize('SW5E.Cover'), - css: 'item-cover', - property: 'cover' - }] - }, - equipment: { - label: game.i18n.localize('SW5E.ItemTypeEquipment'), - items: [], - crewable: true, - dataset: {type: 'equipment', 'armor.type': 'vehicle'}, - columns: equipmentColumns - }, - passive: { - label: game.i18n.localize('SW5E.Features'), - items: [], - dataset: {type: 'feat'} - }, - reactions: { - label: game.i18n.localize('SW5E.ReactionPl'), - items: [], - dataset: {type: 'feat', 'activation.type': 'reaction'} - }, - weapons: { - label: game.i18n.localize('SW5E.ItemTypeWeaponPl'), - items: [], - crewable: true, - dataset: {type: 'weapon', 'weapon-type': 'siege'}, - columns: equipmentColumns - } - }; - - const cargo = { - crew: { - label: game.i18n.localize('SW5E.VehicleCrew'), - items: data.data.cargo.crew, - css: 'cargo-row crew', - editableName: true, - dataset: {type: 'crew'}, - columns: cargoColumns - }, - passengers: { - label: game.i18n.localize('SW5E.VehiclePassengers'), - items: data.data.cargo.passengers, - css: 'cargo-row passengers', - editableName: true, - dataset: {type: 'passengers'}, - columns: cargoColumns - }, - cargo: { - label: game.i18n.localize('SW5E.VehicleCargo'), - items: [], - dataset: {type: 'loot'}, - columns: [{ - label: game.i18n.localize('SW5E.Quantity'), - css: 'item-qty', - property: 'data.quantity', - editable: 'Number' - }, { - label: game.i18n.localize('SW5E.Price'), - css: 'item-price', - property: 'data.price', - editable: 'Number' - }, { - label: game.i18n.localize('SW5E.Weight'), - css: 'item-weight', - property: 'data.weight', - editable: 'Number' - }] - } - }; - - // Classify items owned by the vehicle and compute total cargo weight - let totalWeight = 0; - for (const item of data.items) { - this._prepareCrewedItem(item); - - // Handle cargo explicitly - const isCargo = item.flags.sw5e?.vehicleCargo === true; - if ( isCargo ) { - totalWeight += (item.data.weight || 0) * item.data.quantity; - cargo.cargo.items.push(item); - continue; - } - - // Handle non-cargo item types - switch ( item.type ) { - case "weapon": - features.weapons.items.push(item); - break; - case "equipment": - features.equipment.items.push(item); - break; - case "feat": - if ( !item.data.activation.type || (item.data.activation.type === "none") ) features.passive.items.push(item); - else if (item.data.activation.type === 'reaction') features.reactions.items.push(item); - else features.actions.items.push(item); - break; - default: - totalWeight += (item.data.weight || 0) * item.data.quantity; - cargo.cargo.items.push(item); - } + /** + * Creates a new cargo entry for a vehicle Actor. + */ + static get newCargo() { + return { + name: "", + quantity: 1 + }; } - // Update the rendering context data - data.features = Object.values(features); - data.cargo = Object.values(cargo); - data.data.attributes.encumbrance = this._computeEncumbrance(totalWeight, data); - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ - /* Event Listeners and Handlers */ - /* -------------------------------------------- */ + /** + * Compute the total weight of the vehicle's cargo. + * @param {Number} totalWeight The cumulative item weight from inventory items + * @param {Object} actorData The data object for the Actor being rendered + * @returns {{max: number, value: number, pct: number}} + * @private + */ + _computeEncumbrance(totalWeight, actorData) { + // Compute currency weight + const totalCoins = Object.values(actorData.data.currency).reduce((acc, denom) => acc + denom, 0); + totalWeight += totalCoins / CONFIG.SW5E.encumbrance.currencyPerWeight; - /** @override */ - activateListeners(html) { - super.activateListeners(html); - if (!this.isEditable) return; + // Vehicle weights are an order of magnitude greater. + totalWeight /= CONFIG.SW5E.encumbrance.vehicleWeightMultiplier; - html.find('.item-toggle').click(this._onToggleItem.bind(this)); - html.find('.item-hp input') - .click(evt => evt.target.select()) - .change(this._onHPChange.bind(this)); - - html.find('.item:not(.cargo-row) input[data-property]') - .click(evt => evt.target.select()) - .change(this._onEditInSheet.bind(this)); - - html.find('.cargo-row input') - .click(evt => evt.target.select()) - .change(this._onCargoRowChange.bind(this)); - - if (this.actor.data.data.attributes.actions.stations) { - html.find('.counter.actions, .counter.action-thresholds').hide(); - } - } - - /* -------------------------------------------- */ - - /** - * Handle saving a cargo row (i.e. crew or passenger) in-sheet. - * @param event {Event} - * @returns {Promise|null} - * @private - */ - _onCargoRowChange(event) { - event.preventDefault(); - const target = event.currentTarget; - const row = target.closest('.item'); - const idx = Number(row.dataset.itemId); - const property = row.classList.contains('crew') ? 'crew' : 'passengers'; - - // Get the cargo entry - const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[property]); - const entry = cargo[idx]; - if (!entry) return null; - - // Update the cargo value - const key = target.dataset.property || 'name'; - const type = target.dataset.dtype; - let value = target.value; - if (type === 'Number') value = Number(value); - entry[key] = value; - - // Perform the Actor update - return this.actor.update({[`data.cargo.${property}`]: cargo}); - } - - /* -------------------------------------------- */ - - /** - * Handle editing certain values like quantity, price, and weight in-sheet. - * @param event {Event} - * @returns {Promise} - * @private - */ - _onEditInSheet(event) { - event.preventDefault(); - const itemID = event.currentTarget.closest('.item').dataset.itemId; - const item = this.actor.items.get(itemID); - const property = event.currentTarget.dataset.property; - const type = event.currentTarget.dataset.dtype; - let value = event.currentTarget.value; - switch (type) { - case 'Number': value = parseInt(value); break; - case 'Boolean': value = value === 'true'; break; - } - return item.update({[`${property}`]: value}); - } - - /* -------------------------------------------- */ - - /** - * Handle creating a new crew or passenger row. - * @param event {Event} - * @returns {Promise} - * @private - */ - _onItemCreate(event) { - event.preventDefault(); - const target = event.currentTarget; - const type = target.dataset.type; - if (type === 'crew' || type === 'passengers') { - const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]); - cargo.push(this.constructor.newCargo); - return this.actor.update({[`data.cargo.${type}`]: cargo}); - } - return super._onItemCreate(event); - } - - /* -------------------------------------------- */ - - /** - * Handle deleting a crew or passenger row. - * @param event {Event} - * @returns {Promise} - * @private - */ - _onItemDelete(event) { - event.preventDefault(); - const row = event.currentTarget.closest('.item'); - if (row.classList.contains('cargo-row')) { - const idx = Number(row.dataset.itemId); - const type = row.classList.contains('crew') ? 'crew' : 'passengers'; - const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx); - return this.actor.update({[`data.cargo.${type}`]: cargo}); + // Compute overall encumbrance + const max = actorData.data.attributes.capacity.cargo; + const pct = Math.clamped((totalWeight * 100) / max, 0, 100); + return {value: totalWeight.toNearest(0.1), max, pct}; } - return super._onItemDelete(event); - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** @override */ + _getMovementSpeed(actorData, largestPrimary = true) { + return super._getMovementSpeed(actorData, largestPrimary); + } - /** @override */ - async _onDropItemCreate(itemData) { - const cargoTypes = ["weapon", "equipment", "consumable", "tool", "loot", "backpack"]; - const isCargo = cargoTypes.includes(itemData.type) && (this._tabs[0].active === "cargo"); - foundry.utils.setProperty(itemData, "flags.sw5e.vehicleCargo", isCargo); - return super._onDropItemCreate(itemData); - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** + * Prepare items that are mounted to a vehicle and require one or more crew + * to operate. + * @private + */ + _prepareCrewedItem(item) { + // Determine crewed status + const isCrewed = item.data.crewed; + item.toggleClass = isCrewed ? "active" : ""; + item.toggleTitle = game.i18n.localize(`SW5E.${isCrewed ? "Crewed" : "Uncrewed"}`); - /** - * Special handling for editing HP to clamp it within appropriate range. - * @param event {Event} - * @returns {Promise} - * @private - */ - _onHPChange(event) { - event.preventDefault(); - const itemID = event.currentTarget.closest('.item').dataset.itemId; - const item = this.actor.items.get(itemID); - const hp = Math.clamped(0, parseInt(event.currentTarget.value), item.data.data.hp.max); - event.currentTarget.value = hp; - return item.update({'data.hp.value': hp}); - } + // Handle crew actions + if (item.type === "feat" && item.data.activation.type === "crew") { + item.crew = item.data.activation.cost; + item.cover = game.i18n.localize(`SW5E.${item.data.cover ? "CoverTotal" : "None"}`); + if (item.data.cover === 0.5) item.cover = "½"; + else if (item.data.cover === 0.75) item.cover = "¾"; + else if (item.data.cover === null) item.cover = "—"; + if (item.crew < 1 || item.crew === null) item.crew = "—"; + } - /* -------------------------------------------- */ + // Prepare vehicle weapons + if (item.type === "equipment" || item.type === "weapon") { + item.threshold = item.data.hp.dt ? item.data.hp.dt : "—"; + } + } - /** - * Handle toggling an item's crewed status. - * @param event {Event} - * @returns {Promise} - * @private - */ - _onToggleItem(event) { - event.preventDefault(); - const itemID = event.currentTarget.closest('.item').dataset.itemId; - const item = this.actor.items.get(itemID); - const crewed = !!item.data.data.crewed; - return item.update({'data.crewed': !crewed}); - } -}; + /* -------------------------------------------- */ + + /** + * Organize Owned Items for rendering the Vehicle sheet. + * @private + */ + _prepareItems(data) { + const cargoColumns = [ + { + label: game.i18n.localize("SW5E.Quantity"), + css: "item-qty", + property: "quantity", + editable: "Number" + } + ]; + + const equipmentColumns = [ + { + label: game.i18n.localize("SW5E.Quantity"), + css: "item-qty", + property: "data.quantity" + }, + { + label: game.i18n.localize("SW5E.AC"), + css: "item-ac", + property: "data.armor.value" + }, + { + label: game.i18n.localize("SW5E.HP"), + css: "item-hp", + property: "data.hp.value", + editable: "Number" + }, + { + label: game.i18n.localize("SW5E.Threshold"), + css: "item-threshold", + property: "threshold" + } + ]; + + const features = { + actions: { + label: game.i18n.localize("SW5E.ActionPl"), + items: [], + crewable: true, + dataset: {"type": "feat", "activation.type": "crew"}, + columns: [ + { + label: game.i18n.localize("SW5E.VehicleCrew"), + css: "item-crew", + property: "crew" + }, + { + label: game.i18n.localize("SW5E.Cover"), + css: "item-cover", + property: "cover" + } + ] + }, + equipment: { + label: game.i18n.localize("SW5E.ItemTypeEquipment"), + items: [], + crewable: true, + dataset: {"type": "equipment", "armor.type": "vehicle"}, + columns: equipmentColumns + }, + passive: { + label: game.i18n.localize("SW5E.Features"), + items: [], + dataset: {type: "feat"} + }, + reactions: { + label: game.i18n.localize("SW5E.ReactionPl"), + items: [], + dataset: {"type": "feat", "activation.type": "reaction"} + }, + weapons: { + label: game.i18n.localize("SW5E.ItemTypeWeaponPl"), + items: [], + crewable: true, + dataset: {"type": "weapon", "weapon-type": "siege"}, + columns: equipmentColumns + } + }; + + const cargo = { + crew: { + label: game.i18n.localize("SW5E.VehicleCrew"), + items: data.data.cargo.crew, + css: "cargo-row crew", + editableName: true, + dataset: {type: "crew"}, + columns: cargoColumns + }, + passengers: { + label: game.i18n.localize("SW5E.VehiclePassengers"), + items: data.data.cargo.passengers, + css: "cargo-row passengers", + editableName: true, + dataset: {type: "passengers"}, + columns: cargoColumns + }, + cargo: { + label: game.i18n.localize("SW5E.VehicleCargo"), + items: [], + dataset: {type: "loot"}, + columns: [ + { + label: game.i18n.localize("SW5E.Quantity"), + css: "item-qty", + property: "data.quantity", + editable: "Number" + }, + { + label: game.i18n.localize("SW5E.Price"), + css: "item-price", + property: "data.price", + editable: "Number" + }, + { + label: game.i18n.localize("SW5E.Weight"), + css: "item-weight", + property: "data.weight", + editable: "Number" + } + ] + } + }; + + // Classify items owned by the vehicle and compute total cargo weight + let totalWeight = 0; + for (const item of data.items) { + this._prepareCrewedItem(item); + + // Handle cargo explicitly + const isCargo = item.flags.sw5e?.vehicleCargo === true; + if (isCargo) { + totalWeight += (item.data.weight || 0) * item.data.quantity; + cargo.cargo.items.push(item); + continue; + } + + // Handle non-cargo item types + switch (item.type) { + case "weapon": + features.weapons.items.push(item); + break; + case "equipment": + features.equipment.items.push(item); + break; + case "feat": + if (!item.data.activation.type || item.data.activation.type === "none") + features.passive.items.push(item); + else if (item.data.activation.type === "reaction") features.reactions.items.push(item); + else features.actions.items.push(item); + break; + default: + totalWeight += (item.data.weight || 0) * item.data.quantity; + cargo.cargo.items.push(item); + } + } + + // Update the rendering context data + data.features = Object.values(features); + data.cargo = Object.values(cargo); + data.data.attributes.encumbrance = this._computeEncumbrance(totalWeight, data); + } + + /* -------------------------------------------- */ + /* Event Listeners and Handlers */ + /* -------------------------------------------- */ + + /** @override */ + activateListeners(html) { + super.activateListeners(html); + if (!this.isEditable) return; + + html.find(".item-toggle").click(this._onToggleItem.bind(this)); + html.find(".item-hp input") + .click((evt) => evt.target.select()) + .change(this._onHPChange.bind(this)); + + html.find(".item:not(.cargo-row) input[data-property]") + .click((evt) => evt.target.select()) + .change(this._onEditInSheet.bind(this)); + + html.find(".cargo-row input") + .click((evt) => evt.target.select()) + .change(this._onCargoRowChange.bind(this)); + + if (this.actor.data.data.attributes.actions.stations) { + html.find(".counter.actions, .counter.action-thresholds").hide(); + } + } + + /* -------------------------------------------- */ + + /** + * Handle saving a cargo row (i.e. crew or passenger) in-sheet. + * @param event {Event} + * @returns {Promise|null} + * @private + */ + _onCargoRowChange(event) { + event.preventDefault(); + const target = event.currentTarget; + const row = target.closest(".item"); + const idx = Number(row.dataset.itemId); + const property = row.classList.contains("crew") ? "crew" : "passengers"; + + // Get the cargo entry + const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[property]); + const entry = cargo[idx]; + if (!entry) return null; + + // Update the cargo value + const key = target.dataset.property || "name"; + const type = target.dataset.dtype; + let value = target.value; + if (type === "Number") value = Number(value); + entry[key] = value; + + // Perform the Actor update + return this.actor.update({[`data.cargo.${property}`]: cargo}); + } + + /* -------------------------------------------- */ + + /** + * Handle editing certain values like quantity, price, and weight in-sheet. + * @param event {Event} + * @returns {Promise} + * @private + */ + _onEditInSheet(event) { + event.preventDefault(); + const itemID = event.currentTarget.closest(".item").dataset.itemId; + const item = this.actor.items.get(itemID); + const property = event.currentTarget.dataset.property; + const type = event.currentTarget.dataset.dtype; + let value = event.currentTarget.value; + switch (type) { + case "Number": + value = parseInt(value); + break; + case "Boolean": + value = value === "true"; + break; + } + return item.update({[`${property}`]: value}); + } + + /* -------------------------------------------- */ + + /** + * Handle creating a new crew or passenger row. + * @param event {Event} + * @returns {Promise} + * @private + */ + _onItemCreate(event) { + event.preventDefault(); + const target = event.currentTarget; + const type = target.dataset.type; + if (type === "crew" || type === "passengers") { + const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]); + cargo.push(this.constructor.newCargo); + return this.actor.update({[`data.cargo.${type}`]: cargo}); + } + return super._onItemCreate(event); + } + + /* -------------------------------------------- */ + + /** + * Handle deleting a crew or passenger row. + * @param event {Event} + * @returns {Promise} + * @private + */ + _onItemDelete(event) { + event.preventDefault(); + const row = event.currentTarget.closest(".item"); + if (row.classList.contains("cargo-row")) { + const idx = Number(row.dataset.itemId); + const type = row.classList.contains("crew") ? "crew" : "passengers"; + const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx); + return this.actor.update({[`data.cargo.${type}`]: cargo}); + } + + return super._onItemDelete(event); + } + + /* -------------------------------------------- */ + + /** @override */ + async _onDropItemCreate(itemData) { + const cargoTypes = ["weapon", "equipment", "consumable", "tool", "loot", "backpack"]; + const isCargo = cargoTypes.includes(itemData.type) && this._tabs[0].active === "cargo"; + foundry.utils.setProperty(itemData, "flags.sw5e.vehicleCargo", isCargo); + return super._onDropItemCreate(itemData); + } + + /* -------------------------------------------- */ + + /** + * Special handling for editing HP to clamp it within appropriate range. + * @param event {Event} + * @returns {Promise} + * @private + */ + _onHPChange(event) { + event.preventDefault(); + const itemID = event.currentTarget.closest(".item").dataset.itemId; + const item = this.actor.items.get(itemID); + const hp = Math.clamped(0, parseInt(event.currentTarget.value), item.data.data.hp.max); + event.currentTarget.value = hp; + return item.update({"data.hp.value": hp}); + } + + /* -------------------------------------------- */ + + /** + * Handle toggling an item's crewed status. + * @param event {Event} + * @returns {Promise} + * @private + */ + _onToggleItem(event) { + event.preventDefault(); + const itemID = event.currentTarget.closest(".item").dataset.itemId; + const item = this.actor.items.get(itemID); + const crewed = !!item.data.data.crewed; + return item.update({"data.crewed": !crewed}); + } +} diff --git a/module/actor/sheets/oldSheets/base.js b/module/actor/sheets/oldSheets/base.js index f0a7a83b..acab8b2a 100644 --- a/module/actor/sheets/oldSheets/base.js +++ b/module/actor/sheets/oldSheets/base.js @@ -5,7 +5,7 @@ import ActorHitDiceConfig from "../../../apps/hit-dice-config.js"; import ActorMovementConfig from "../../../apps/movement-config.js"; import ActorSensesConfig from "../../../apps/senses-config.js"; import ActorTypeConfig from "../../../apps/actor-type.js"; -import {SW5E} from '../../../config.js'; +import {SW5E} from "../../../config.js"; import {onManageActiveEffect, prepareActiveEffectCategories} from "../../../effects.js"; /** @@ -14,902 +14,907 @@ import {onManageActiveEffect, prepareActiveEffectCategories} from "../../../effe * @extends {ActorSheet} */ export default class ActorSheet5e extends ActorSheet { - constructor(...args) { - super(...args); + constructor(...args) { + super(...args); + + /** + * Track the set of item filters which are applied + * @type {Set} + */ + this._filters = { + inventory: new Set(), + powerbook: new Set(), + features: new Set(), + effects: new Set() + }; + } + + /* -------------------------------------------- */ + + /** @override */ + static get defaultOptions() { + return mergeObject(super.defaultOptions, { + scrollY: [ + ".inventory .inventory-list", + ".features .inventory-list", + ".powerbook .inventory-list", + ".effects .inventory-list" + ], + tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}] + }); + } + + /* -------------------------------------------- */ /** - * Track the set of item filters which are applied - * @type {Set} + * A set of item types that should be prevented from being dropped on this type of actor sheet. + * @type {Set} */ - this._filters = { - inventory: new Set(), - powerbook: new Set(), - features: new Set(), - effects: new Set() - }; - } + static unsupportedItemTypes = new Set(); - /* -------------------------------------------- */ + /* -------------------------------------------- */ - /** @override */ - static get defaultOptions() { - return mergeObject(super.defaultOptions, { - scrollY: [ - ".inventory .inventory-list", - ".features .inventory-list", - ".powerbook .inventory-list", - ".effects .inventory-list" - ], - tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}] - }); - } - - /* -------------------------------------------- */ - - /** - * A set of item types that should be prevented from being dropped on this type of actor sheet. - * @type {Set} - */ - static unsupportedItemTypes = new Set(); - - /* -------------------------------------------- */ - - - /** @override */ - get template() { - if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/oldActor/limited-sheet.html"; - return `systems/sw5e/templates/actors/oldActor/${this.actor.data.type}-sheet.html`; - } - - /* -------------------------------------------- */ - - /** @override */ - getData(options) { - - // Basic data - let isOwner = this.actor.isOwner; - const data = { - owner: isOwner, - limited: this.actor.limited, - options: this.options, - editable: this.isEditable, - cssClass: isOwner ? "editable" : "locked", - isCharacter: this.actor.type === "character", - isNPC: this.actor.type === "npc", - isStarship: this.actor.type === "starship", - isVehicle: this.actor.type === 'vehicle', - config: CONFIG.SW5E, - rollData: this.actor.getRollData.bind(this.actor) - }; - - // The Actor's data - const actorData = this.actor.data.toObject(false); - data.actor = actorData; - data.data = actorData.data; - - // Owned Items - data.items = actorData.items; - for ( let i of data.items ) { - const item = this.actor.items.get(i._id); - i.labels = item.labels; - } - data.items.sort((a, b) => (a.sort || 0) - (b.sort || 0)); - - // Labels and filters - data.labels = this.actor.labels || {}; - data.filters = this._filters; - - // Ability Scores - for ( let [a, abl] of Object.entries(actorData.data.abilities)) { - abl.icon = this._getProficiencyIcon(abl.proficient); - abl.hover = CONFIG.SW5E.proficiencyLevels[abl.proficient]; - abl.label = CONFIG.SW5E.abilities[a]; + /** @override */ + get template() { + if (!game.user.isGM && this.actor.limited) return "systems/sw5e/templates/actors/oldActor/limited-sheet.html"; + return `systems/sw5e/templates/actors/oldActor/${this.actor.data.type}-sheet.html`; } - // Skills - if (actorData.data.skills) { - for ( let [s, skl] of Object.entries(actorData.data.skills)) { - skl.ability = CONFIG.SW5E.abilityAbbreviations[skl.ability]; - skl.icon = this._getProficiencyIcon(skl.value); - skl.hover = CONFIG.SW5E.proficiencyLevels[skl.value]; - skl.label = CONFIG.SW5E.skills[s]; - } - } + /* -------------------------------------------- */ - // Movement speeds - data.movement = this._getMovementSpeed(actorData); + /** @override */ + getData(options) { + // Basic data + let isOwner = this.actor.isOwner; + const data = { + owner: isOwner, + limited: this.actor.limited, + options: this.options, + editable: this.isEditable, + cssClass: isOwner ? "editable" : "locked", + isCharacter: this.actor.type === "character", + isNPC: this.actor.type === "npc", + isStarship: this.actor.type === "starship", + isVehicle: this.actor.type === "vehicle", + config: CONFIG.SW5E, + rollData: this.actor.getRollData.bind(this.actor) + }; - // Senses - data.senses = this._getSenses(actorData); + // The Actor's data + const actorData = this.actor.data.toObject(false); + data.actor = actorData; + data.data = actorData.data; - // Update traits - this._prepareTraits(actorData.data.traits); - - // Prepare owned items - this._prepareItems(data); - - // Prepare active effects - data.effects = prepareActiveEffectCategories(this.actor.effects); - - // Return data to the sheet - return data - } - - /* -------------------------------------------- */ - - /** - * Prepare the display of movement speed data for the Actor* - * @param {object} actorData The Actor data being prepared. - * @param {boolean} [largestPrimary=false] Show the largest movement speed as "primary", otherwise show "walk" - * @returns {{primary: string, special: string}} - * @private - */ - _getMovementSpeed(actorData, largestPrimary=false) { - const movement = actorData.data.attributes.movement || {}; - - // Prepare an array of available movement speeds - let speeds = [ - [movement.burrow, `${game.i18n.localize("SW5E.MovementBurrow")} ${movement.burrow}`], - [movement.climb, `${game.i18n.localize("SW5E.MovementClimb")} ${movement.climb}`], - [movement.fly, `${game.i18n.localize("SW5E.MovementFly")} ${movement.fly}` + (movement.hover ? ` (${game.i18n.localize("SW5E.MovementHover")})` : "")], - [movement.swim, `${game.i18n.localize("SW5E.MovementSwim")} ${movement.swim}`] - ] - if ( largestPrimary ) { - speeds.push([movement.walk, `${game.i18n.localize("SW5E.MovementWalk")} ${movement.walk}`]); - } - - // Filter and sort speeds on their values - speeds = speeds.filter(s => !!s[0]).sort((a, b) => b[0] - a[0]); - - // Case 1: Largest as primary - if ( largestPrimary ) { - let primary = speeds.shift(); - return { - primary: `${primary ? primary[1] : "0"} ${movement.units}`, - special: speeds.map(s => s[1]).join(", ") - } - } - - // Case 2: Walk as primary - else { - return { - primary: `${movement.walk || 0} ${movement.units}`, - special: speeds.length ? speeds.map(s => s[1]).join(", ") : "" - } - } - } - - /* -------------------------------------------- */ - - _getSenses(actorData) { - const senses = actorData.data.attributes.senses || {}; - const tags = {}; - for ( let [k, label] of Object.entries(CONFIG.SW5E.senses) ) { - const v = senses[k] ?? 0 - if ( v === 0 ) continue; - tags[k] = `${game.i18n.localize(label)} ${v} ${senses.units}`; - } - if ( !!senses.special ) tags["special"] = senses.special; - return tags; - } - - /* -------------------------------------------- */ - - /** - * Prepare the data structure for traits data like languages, resistances & vulnerabilities, and proficiencies - * @param {object} traits The raw traits data object from the actor data - * @private - */ - _prepareTraits(traits) { - const map = { - "dr": CONFIG.SW5E.damageResistanceTypes, - "di": CONFIG.SW5E.damageResistanceTypes, - "dv": CONFIG.SW5E.damageResistanceTypes, - "ci": CONFIG.SW5E.conditionTypes, - "languages": CONFIG.SW5E.languages, - "armorProf": CONFIG.SW5E.armorProficiencies, - "weaponProf": CONFIG.SW5E.weaponProficiencies, - "toolProf": CONFIG.SW5E.toolProficiencies - }; - for ( let [t, choices] of Object.entries(map) ) { - const trait = traits[t]; - if ( !trait ) continue; - let values = []; - if ( trait.value ) { - values = trait.value instanceof Array ? trait.value : [trait.value]; - } - trait.selected = values.reduce((obj, t) => { - obj[t] = choices[t]; - return obj; - }, {}); - - // Add custom entry - if ( trait.custom ) { - trait.custom.split(";").forEach((c, i) => trait.selected[`custom${i+1}`] = c.trim()); - } - trait.cssClass = !isObjectEmpty(trait.selected) ? "" : "inactive"; - } - } - - /* -------------------------------------------- */ - - /** - * Insert a power into the powerbook object when rendering the character sheet - * @param {Object} data The Actor data being prepared - * @param {Array} powers The power data being prepared - * @private - */ - _preparePowerbook(data, powers) { - const owner = this.actor.isOwner; - const levels = data.data.powers; - const powerbook = {}; - - // Define some mappings - const sections = { - "atwill": -20, - "innate": -10, - "pact": 0.5 - }; - - // Label power slot uses headers - const useLabels = { - "-20": "-", - "-10": "-", - "0": "∞" - }; - - // Format a powerbook entry for a certain indexed level - const registerSection = (sl, i, label, {prepMode="prepared", value, max, override}={}) => { - powerbook[i] = { - order: i, - label: label, - usesSlots: i > 0, - canCreate: owner, - canPrepare: (data.actor.type === "character") && (i >= 1), - powers: [], - uses: useLabels[i] || value || 0, - slots: useLabels[i] || max || 0, - override: override || 0, - dataset: {"type": "power", "level": prepMode in sections ? 1 : i, "preparation.mode": prepMode}, - prop: sl - }; - }; - - // Determine the maximum power level which has a slot - const maxLevel = Array.fromRange(10).reduce((max, i) => { - if ( i === 0 ) return max; - const level = levels[`power${i}`]; - if ( (level.max || level.override ) && ( i > max ) ) max = i; - return max; - }, 0); - - // Level-based powercasters have cantrips and leveled slots - if ( maxLevel > 0 ) { - registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]); - for (let lvl = 1; lvl <= maxLevel; lvl++) { - const sl = `power${lvl}`; - registerSection(sl, lvl, CONFIG.SW5E.powerLevels[lvl], levels[sl]); - } - } - - // Pact magic users have cantrips and a pact magic section - // TODO: Check if this is needed, we've removed pacts everywhere else - if ( levels.pact && levels.pact.max ) { - if ( !powerbook["0"] ) registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]); - const l = levels.pact; - const config = CONFIG.SW5E.powerPreparationModes.pact; - const level = game.i18n.localize(`SW5E.PowerLevel${levels.pact.level}`); - const label = `${config} — ${level}`; - registerSection("pact", sections.pact, label, { - prepMode: "pact", - value: l.value, - max: l.max, - override: l.override - }); - } - - // Iterate over every power item, adding powers to the powerbook by section - powers.forEach(power => { - const mode = power.data.preparation.mode || "prepared"; - let s = power.data.level || 0; - const sl = `power${s}`; - - // Specialized powercasting modes (if they exist) - if ( mode in sections ) { - s = sections[mode]; - if ( !powerbook[s] ){ - const l = levels[mode] || {}; - const config = CONFIG.SW5E.powerPreparationModes[mode]; - registerSection(mode, s, config, { - prepMode: mode, - value: l.value, - max: l.max, - override: l.override - }); + // Owned Items + data.items = actorData.items; + for (let i of data.items) { + const item = this.actor.items.get(i._id); + i.labels = item.labels; } - } + data.items.sort((a, b) => (a.sort || 0) - (b.sort || 0)); - // Sections for higher-level powers which the caster "should not" have, but power items exist for - else if ( !powerbook[s] ) { - registerSection(sl, s, CONFIG.SW5E.powerLevels[s], {levels: levels[sl]}); - } + // Labels and filters + data.labels = this.actor.labels || {}; + data.filters = this._filters; - // Add the power to the relevant heading - powerbook[s].powers.push(power); - }); - - // Sort the powerbook by section level - const sorted = Object.values(powerbook); - sorted.sort((a, b) => a.order - b.order); - return sorted; - } - - /* -------------------------------------------- */ - - /** - * Determine whether an Owned Item will be shown based on the current set of filters - * @return {boolean} - * @private - */ - _filterItems(items, filters) { - return items.filter(item => { - const data = item.data; - - // Action usage - for ( let f of ["action", "bonus", "reaction"] ) { - if ( filters.has(f) ) { - if ((data.activation && (data.activation.type !== f))) return false; + // Ability Scores + for (let [a, abl] of Object.entries(actorData.data.abilities)) { + abl.icon = this._getProficiencyIcon(abl.proficient); + abl.hover = CONFIG.SW5E.proficiencyLevels[abl.proficient]; + abl.label = CONFIG.SW5E.abilities[a]; } - } - // Power-specific filters - if ( filters.has("ritual") ) { - if (data.components.ritual !== true) return false; - } - if ( filters.has("concentration") ) { - if (data.components.concentration !== true) return false; - } - if ( filters.has("prepared") ) { - if ( data.level === 0 || ["innate", "always"].includes(data.preparation.mode) ) return true; - if ( this.actor.data.type === "npc" ) return true; - return data.preparation.prepared; - } - - // Equipment-specific filters - if ( filters.has("equipped") ) { - if ( data.equipped !== true ) return false; - } - return true; - }); - } - - /* -------------------------------------------- */ - - /** - * Get the font-awesome icon used to display a certain level of skill proficiency - * @private - */ - _getProficiencyIcon(level) { - const icons = { - 0: '', - 0.5: '', - 1: '', - 2: '' - }; - return icons[level] || icons[0]; - } - - /* -------------------------------------------- */ - /* Event Listeners and Handlers - /* -------------------------------------------- */ - - /** @inheritdoc */ - activateListeners(html) { - - // Activate Item Filters - const filterLists = html.find(".filter-list"); - filterLists.each(this._initializeFilterItemList.bind(this)); - filterLists.on("click", ".filter-item", this._onToggleFilter.bind(this)); - - // Item summaries - html.find('.item .item-name.rollable h4').click(event => this._onItemSummary(event)); - - // View Item Sheets - html.find('.item-edit').click(this._onItemEdit.bind(this)); - - // Editable Only Listeners - if ( this.isEditable ) { - - // Input focus and update - const inputs = html.find("input"); - inputs.focus(ev => ev.currentTarget.select()); - inputs.addBack().find('[data-dtype="Number"]').change(this._onChangeInputDelta.bind(this)); - - // Ability Proficiency - html.find('.ability-proficiency').click(this._onToggleAbilityProficiency.bind(this)); - - // Toggle Skill Proficiency - html.find('.skill-proficiency').on("click contextmenu", this._onCycleSkillProficiency.bind(this)); - - // Trait Selector - html.find('.trait-selector').click(this._onTraitSelector.bind(this)); - - // Configure Special Flags - html.find('.config-button').click(this._onConfigMenu.bind(this)); - - // Owned Item management - html.find('.item-create').click(this._onItemCreate.bind(this)); - html.find('.item-delete').click(this._onItemDelete.bind(this)); - html.find('.item-uses input').click(ev => ev.target.select()).change(this._onUsesChange.bind(this)); - html.find('.slot-max-override').click(this._onPowerSlotOverride.bind(this)); - - // Active Effect management - html.find(".effect-control").click(ev => onManageActiveEffect(ev, this.actor)); - } - - // Owner Only Listeners - if ( this.actor.isOwner ) { - - // Ability Checks - html.find('.ability-name').click(this._onRollAbilityTest.bind(this)); - - - // Roll Skill Checks - html.find('.skill-name').click(this._onRollSkillCheck.bind(this)); - - // Item Rolling - html.find('.item .item-image').click(event => this._onItemRoll(event)); - html.find('.item .item-recharge').click(event => this._onItemRecharge(event)); - } - - // Otherwise remove rollable classes - else { - html.find(".rollable").each((i, el) => el.classList.remove("rollable")); - } - - // Handle default listeners last so system listeners are triggered first - super.activateListeners(html); - } - - /* -------------------------------------------- */ - - /** - * Iinitialize Item list filters by activating the set of filters which are currently applied - * @private - */ - _initializeFilterItemList(i, ul) { - const set = this._filters[ul.dataset.filter]; - const filters = ul.querySelectorAll(".filter-item"); - for ( let li of filters ) { - if ( set.has(li.dataset.filter) ) li.classList.add("active"); - } - } - - /* -------------------------------------------- */ - /* Event Listeners and Handlers */ - /* -------------------------------------------- */ - - /** - * Handle input changes to numeric form fields, allowing them to accept delta-typed inputs - * @param event - * @private - */ - _onChangeInputDelta(event) { - const input = event.target; - const value = input.value; - if ( ["+", "-"].includes(value[0]) ) { - let delta = parseFloat(value); - input.value = getProperty(this.actor.data, input.name) + delta; - } else if ( value[0] === "=" ) { - input.value = value.slice(1); - } - } - - /* -------------------------------------------- */ - - /** - * Handle spawning the TraitSelector application which allows a checkbox of multiple trait options - * @param {Event} event The click event which originated the selection - * @private - */ - _onConfigMenu(event) { - event.preventDefault(); - const button = event.currentTarget; - let app; - switch ( button.dataset.action ) { - case "hit-dice": - app = new ActorHitDiceConfig(this.object); - break; - case "movement": - app = new ActorMovementConfig(this.object); - break; - case "flags": - app = new ActorSheetFlags(this.object); - break; - case "senses": - app = new ActorSensesConfig(this.object); - break; - case "type": - new ActorTypeConfig(this.object).render(true); - break; - } - app?.render(true); - } - - /* -------------------------------------------- */ - - /** - * Handle cycling proficiency in a Skill - * @param {Event} event A click or contextmenu event which triggered the handler - * @private - */ - _onCycleSkillProficiency(event) { - event.preventDefault(); - const field = $(event.currentTarget).siblings('input[type="hidden"]'); - - // Get the current level and the array of levels - const level = parseFloat(field.val()); - const levels = [0, 1, 0.5, 2]; - let idx = levels.indexOf(level); - - // Toggle next level - forward on click, backwards on right - if ( event.type === "click" ) { - field.val(levels[(idx === levels.length - 1) ? 0 : idx + 1]); - } else if ( event.type === "contextmenu" ) { - field.val(levels[(idx === 0) ? levels.length - 1 : idx - 1]); - } - - // Update the field value and save the form - this._onSubmit(event); - } - - /* -------------------------------------------- */ - - /** @override */ - async _onDropActor(event, data) { - const canPolymorph = game.user.isGM || (this.actor.isOwner && game.settings.get('sw5e', 'allowPolymorphing')); - if ( !canPolymorph ) return false; - - // Get the target actor - let sourceActor = null; - if (data.pack) { - const pack = game.packs.find(p => p.collection === data.pack); - sourceActor = await pack.getEntity(data.id); - } else { - sourceActor = game.actors.get(data.id); - } - if ( !sourceActor ) return; - - // Define a function to record polymorph settings for future use - const rememberOptions = html => { - const options = {}; - html.find('input').each((i, el) => { - options[el.name] = el.checked; - }); - const settings = mergeObject(game.settings.get('sw5e', 'polymorphSettings') || {}, options); - game.settings.set('sw5e', 'polymorphSettings', settings); - return settings; - }; - - // Create and render the Dialog - return new Dialog({ - title: game.i18n.localize('SW5E.PolymorphPromptTitle'), - content: { - options: game.settings.get('sw5e', 'polymorphSettings'), - i18n: SW5E.polymorphSettings, - isToken: this.actor.isToken - }, - default: 'accept', - buttons: { - accept: { - icon: '', - label: game.i18n.localize('SW5E.PolymorphAcceptSettings'), - callback: html => this.actor.transformInto(sourceActor, rememberOptions(html)) - }, - wildshape: { - icon: '', - label: game.i18n.localize('SW5E.PolymorphWildShape'), - callback: html => this.actor.transformInto(sourceActor, { - keepBio: true, - keepClass: true, - keepMental: true, - mergeSaves: true, - mergeSkills: true, - transformTokens: rememberOptions(html).transformTokens - }) - }, - polymorph: { - icon: '', - label: game.i18n.localize('SW5E.Polymorph'), - callback: html => this.actor.transformInto(sourceActor, { - transformTokens: rememberOptions(html).transformTokens - }) - }, - cancel: { - icon: '', - label: game.i18n.localize('Cancel') + // Skills + if (actorData.data.skills) { + for (let [s, skl] of Object.entries(actorData.data.skills)) { + skl.ability = CONFIG.SW5E.abilityAbbreviations[skl.ability]; + skl.icon = this._getProficiencyIcon(skl.value); + skl.hover = CONFIG.SW5E.proficiencyLevels[skl.value]; + skl.label = CONFIG.SW5E.skills[s]; + } } - } - }, { - classes: ['dialog', 'sw5e'], - width: 600, - template: 'systems/sw5e/templates/apps/polymorph-prompt.html' - }).render(true); - } - /* -------------------------------------------- */ + // Movement speeds + data.movement = this._getMovementSpeed(actorData); - /** @override */ - async _onDropItemCreate(itemData) { + // Senses + data.senses = this._getSenses(actorData); - // Check to make sure items of this type are allowed on this actor - if ( this.constructor.unsupportedItemTypes.has(itemData.type) ) { - return ui.notifications.warn(game.i18n.format("SW5E.ActorWarningInvalidItem", { - itemType: game.i18n.localize(CONFIG.Item.typeLabels[itemData.type]), - actorType: game.i18n.localize(CONFIG.Actor.typeLabels[this.actor.type]) - })); + // Update traits + this._prepareTraits(actorData.data.traits); + + // Prepare owned items + this._prepareItems(data); + + // Prepare active effects + data.effects = prepareActiveEffectCategories(this.actor.effects); + + // Return data to the sheet + return data; } - // Create a Consumable power scroll on the Inventory tab - // TODO: This is pretty non functional as the base items for the scrolls, and the powers, are not defined, maybe consider using holocrons - if ( (itemData.type === "power") && (this._tabs[0].active === "inventory") ) { - const scroll = await Item5e.createScrollFromPower(itemData); - itemData = scroll.data; + /* -------------------------------------------- */ + + /** + * Prepare the display of movement speed data for the Actor* + * @param {object} actorData The Actor data being prepared. + * @param {boolean} [largestPrimary=false] Show the largest movement speed as "primary", otherwise show "walk" + * @returns {{primary: string, special: string}} + * @private + */ + _getMovementSpeed(actorData, largestPrimary = false) { + const movement = actorData.data.attributes.movement || {}; + + // Prepare an array of available movement speeds + let speeds = [ + [movement.burrow, `${game.i18n.localize("SW5E.MovementBurrow")} ${movement.burrow}`], + [movement.climb, `${game.i18n.localize("SW5E.MovementClimb")} ${movement.climb}`], + [ + movement.fly, + `${game.i18n.localize("SW5E.MovementFly")} ${movement.fly}` + + (movement.hover ? ` (${game.i18n.localize("SW5E.MovementHover")})` : "") + ], + [movement.swim, `${game.i18n.localize("SW5E.MovementSwim")} ${movement.swim}`] + ]; + if (largestPrimary) { + speeds.push([movement.walk, `${game.i18n.localize("SW5E.MovementWalk")} ${movement.walk}`]); + } + + // Filter and sort speeds on their values + speeds = speeds.filter((s) => !!s[0]).sort((a, b) => b[0] - a[0]); + + // Case 1: Largest as primary + if (largestPrimary) { + let primary = speeds.shift(); + return { + primary: `${primary ? primary[1] : "0"} ${movement.units}`, + special: speeds.map((s) => s[1]).join(", ") + }; + } + + // Case 2: Walk as primary + else { + return { + primary: `${movement.walk || 0} ${movement.units}`, + special: speeds.length ? speeds.map((s) => s[1]).join(", ") : "" + }; + } } - if ( itemData.data ) { - // Ignore certain statuses - ["equipped", "proficient", "prepared"].forEach(k => delete itemData.data[k]); + /* -------------------------------------------- */ - // Downgrade ATTUNED to REQUIRED - itemData.data.attunement = Math.min(itemData.data.attunement, CONFIG.SW5E.attunementTypes.REQUIRED); + _getSenses(actorData) { + const senses = actorData.data.attributes.senses || {}; + const tags = {}; + for (let [k, label] of Object.entries(CONFIG.SW5E.senses)) { + const v = senses[k] ?? 0; + if (v === 0) continue; + tags[k] = `${game.i18n.localize(label)} ${v} ${senses.units}`; + } + if (!!senses.special) tags["special"] = senses.special; + return tags; } - // Stack identical consumables - if ( itemData.type === "consumable" && itemData.flags.core?.sourceId ) { - const similarItem = this.actor.items.find(i => { - const sourceId = i.getFlag("core", "sourceId"); - return sourceId && (sourceId === itemData.flags.core?.sourceId) && - (i.type === "consumable"); - }); - if ( similarItem ) { - return similarItem.update({ - 'data.quantity': similarItem.data.data.quantity + Math.max(itemData.data.quantity, 1) + /* -------------------------------------------- */ + + /** + * Prepare the data structure for traits data like languages, resistances & vulnerabilities, and proficiencies + * @param {object} traits The raw traits data object from the actor data + * @private + */ + _prepareTraits(traits) { + const map = { + dr: CONFIG.SW5E.damageResistanceTypes, + di: CONFIG.SW5E.damageResistanceTypes, + dv: CONFIG.SW5E.damageResistanceTypes, + ci: CONFIG.SW5E.conditionTypes, + languages: CONFIG.SW5E.languages, + armorProf: CONFIG.SW5E.armorProficiencies, + weaponProf: CONFIG.SW5E.weaponProficiencies, + toolProf: CONFIG.SW5E.toolProficiencies + }; + for (let [t, choices] of Object.entries(map)) { + const trait = traits[t]; + if (!trait) continue; + let values = []; + if (trait.value) { + values = trait.value instanceof Array ? trait.value : [trait.value]; + } + trait.selected = values.reduce((obj, t) => { + obj[t] = choices[t]; + return obj; + }, {}); + + // Add custom entry + if (trait.custom) { + trait.custom.split(";").forEach((c, i) => (trait.selected[`custom${i + 1}`] = c.trim())); + } + trait.cssClass = !isObjectEmpty(trait.selected) ? "" : "inactive"; + } + } + + /* -------------------------------------------- */ + + /** + * Insert a power into the powerbook object when rendering the character sheet + * @param {Object} data The Actor data being prepared + * @param {Array} powers The power data being prepared + * @private + */ + _preparePowerbook(data, powers) { + const owner = this.actor.isOwner; + const levels = data.data.powers; + const powerbook = {}; + + // Define some mappings + const sections = { + atwill: -20, + innate: -10, + pact: 0.5 + }; + + // Label power slot uses headers + const useLabels = { + "-20": "-", + "-10": "-", + "0": "∞" + }; + + // Format a powerbook entry for a certain indexed level + const registerSection = (sl, i, label, {prepMode = "prepared", value, max, override} = {}) => { + powerbook[i] = { + order: i, + label: label, + usesSlots: i > 0, + canCreate: owner, + canPrepare: data.actor.type === "character" && i >= 1, + powers: [], + uses: useLabels[i] || value || 0, + slots: useLabels[i] || max || 0, + override: override || 0, + dataset: {"type": "power", "level": prepMode in sections ? 1 : i, "preparation.mode": prepMode}, + prop: sl + }; + }; + + // Determine the maximum power level which has a slot + const maxLevel = Array.fromRange(10).reduce((max, i) => { + if (i === 0) return max; + const level = levels[`power${i}`]; + if ((level.max || level.override) && i > max) max = i; + return max; + }, 0); + + // Level-based powercasters have cantrips and leveled slots + if (maxLevel > 0) { + registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]); + for (let lvl = 1; lvl <= maxLevel; lvl++) { + const sl = `power${lvl}`; + registerSection(sl, lvl, CONFIG.SW5E.powerLevels[lvl], levels[sl]); + } + } + + // Pact magic users have cantrips and a pact magic section + // TODO: Check if this is needed, we've removed pacts everywhere else + if (levels.pact && levels.pact.max) { + if (!powerbook["0"]) registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]); + const l = levels.pact; + const config = CONFIG.SW5E.powerPreparationModes.pact; + const level = game.i18n.localize(`SW5E.PowerLevel${levels.pact.level}`); + const label = `${config} — ${level}`; + registerSection("pact", sections.pact, label, { + prepMode: "pact", + value: l.value, + max: l.max, + override: l.override + }); + } + + // Iterate over every power item, adding powers to the powerbook by section + powers.forEach((power) => { + const mode = power.data.preparation.mode || "prepared"; + let s = power.data.level || 0; + const sl = `power${s}`; + + // Specialized powercasting modes (if they exist) + if (mode in sections) { + s = sections[mode]; + if (!powerbook[s]) { + const l = levels[mode] || {}; + const config = CONFIG.SW5E.powerPreparationModes[mode]; + registerSection(mode, s, config, { + prepMode: mode, + value: l.value, + max: l.max, + override: l.override + }); + } + } + + // Sections for higher-level powers which the caster "should not" have, but power items exist for + else if (!powerbook[s]) { + registerSection(sl, s, CONFIG.SW5E.powerLevels[s], {levels: levels[sl]}); + } + + // Add the power to the relevant heading + powerbook[s].powers.push(power); }); - } + + // Sort the powerbook by section level + const sorted = Object.values(powerbook); + sorted.sort((a, b) => a.order - b.order); + return sorted; } - // Create the owned item as normal - return super._onDropItemCreate(itemData); - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** + * Determine whether an Owned Item will be shown based on the current set of filters + * @return {boolean} + * @private + */ + _filterItems(items, filters) { + return items.filter((item) => { + const data = item.data; - /** - * Handle enabling editing for a power slot override value - * @param {MouseEvent} event The originating click event - * @private - */ - async _onPowerSlotOverride (event) { - const span = event.currentTarget.parentElement; - const level = span.dataset.level; - const override = this.actor.data.data.powers[level].override || span.dataset.slots; - const input = document.createElement("INPUT"); - input.type = "text"; - input.name = `data.powers.${level}.override`; - input.value = override; - input.placeholder = span.dataset.slots; - input.dataset.dtype = "Number"; + // Action usage + for (let f of ["action", "bonus", "reaction"]) { + if (filters.has(f)) { + if (data.activation && data.activation.type !== f) return false; + } + } - // Replace the HTML - const parent = span.parentElement; - parent.removeChild(span); - parent.appendChild(input); - } + // Power-specific filters + if (filters.has("ritual")) { + if (data.components.ritual !== true) return false; + } + if (filters.has("concentration")) { + if (data.components.concentration !== true) return false; + } + if (filters.has("prepared")) { + if (data.level === 0 || ["innate", "always"].includes(data.preparation.mode)) return true; + if (this.actor.data.type === "npc") return true; + return data.preparation.prepared; + } - /* -------------------------------------------- */ - - /** - * Change the uses amount of an Owned Item within the Actor - * @param {Event} event The triggering click event - * @private - */ - async _onUsesChange(event) { - event.preventDefault(); - const itemId = event.currentTarget.closest(".item").dataset.itemId; - const item = this.actor.items.get(itemId); - const uses = Math.clamped(0, parseInt(event.target.value), item.data.data.uses.max); - event.target.value = uses; - return item.update({ 'data.uses.value': uses }); - } - - /* -------------------------------------------- */ - - /** - * Handle rolling of an item from the Actor sheet, obtaining the Item instance and dispatching to it's roll method - * @private - */ - _onItemRoll(event) { - event.preventDefault(); - const itemId = event.currentTarget.closest(".item").dataset.itemId; - const item = this.actor.items.get(itemId); - return item.roll(); - } - - /* -------------------------------------------- */ - - /** - * Handle attempting to recharge an item usage by rolling a recharge check - * @param {Event} event The originating click event - * @private - */ - _onItemRecharge(event) { - event.preventDefault(); - const itemId = event.currentTarget.closest(".item").dataset.itemId; - const item = this.actor.items.get(itemId); - return item.rollRecharge(); - }; - - /* -------------------------------------------- */ - - /** - * Handle rolling of an item from the Actor sheet, obtaining the Item instance and dispatching to it's roll method - * @private - */ - _onItemSummary(event) { - event.preventDefault(); - let li = $(event.currentTarget).parents(".item"), - item = this.actor.items.get(li.data("item-id")), - chatData = item.getChatData({secrets: this.actor.isOwner}); - - // Toggle summary - if ( li.hasClass("expanded") ) { - let summary = li.children(".item-summary"); - summary.slideUp(200, () => summary.remove()); - } else { - let div = $(`
${chatData.description.value}
`); - let props = $(`
`); - chatData.properties.forEach(p => props.append(`${p}`)); - div.append(props); - li.append(div.hide()); - div.slideDown(200); + // Equipment-specific filters + if (filters.has("equipped")) { + if (data.equipped !== true) return false; + } + return true; + }); } - li.toggleClass("expanded"); - } - /* -------------------------------------------- */ + /* -------------------------------------------- */ - /** - * Handle creating a new Owned Item for the actor using initial data defined in the HTML dataset - * @param {Event} event The originating click event - * @private - */ - _onItemCreate(event) { - event.preventDefault(); - const header = event.currentTarget; - const type = header.dataset.type; - const itemData = { - name: game.i18n.format("SW5E.ItemNew", {type: game.i18n.localize(`SW5E.ItemType${type.capitalize()}`)}), - type: type, - data: foundry.utils.deepClone(header.dataset) - }; - delete itemData.data["type"]; - return this.actor.createEmbeddedDocuments("Item", [itemData]); - } - - /* -------------------------------------------- */ - - /** - * Handle editing an existing Owned Item for the Actor - * @param {Event} event The originating click event - * @private - */ - _onItemEdit(event) { - event.preventDefault(); - const li = event.currentTarget.closest(".item"); - const item = this.actor.items.get(li.dataset.itemId); - return item.sheet.render(true); - } - - /* -------------------------------------------- */ - - /** - * Handle deleting an existing Owned Item for the Actor - * @param {Event} event The originating click event - * @private - */ - _onItemDelete(event) { - event.preventDefault(); - const li = event.currentTarget.closest(".item"); - const item = this.actor.items.get(li.dataset.itemId); - if ( item ) return item.delete(); - } - - /* -------------------------------------------- */ - - /** - * Handle rolling an Ability check, either a test or a saving throw - * @param {Event} event The originating click event - * @private - */ - _onRollAbilityTest(event) { - event.preventDefault(); - let ability = event.currentTarget.parentElement.dataset.ability; - return this.actor.rollAbility(ability, {event: event}); - } - - /* -------------------------------------------- */ - - /** - * Handle rolling a Skill check - * @param {Event} event The originating click event - * @private - */ - _onRollSkillCheck(event) { - event.preventDefault(); - const skill = event.currentTarget.parentElement.dataset.skill; - return this.actor.rollSkill(skill, {event: event}); - } - - /* -------------------------------------------- */ - - /** - * Handle toggling Ability score proficiency level - * @param {Event} event The originating click event - * @private - */ - _onToggleAbilityProficiency(event) { - event.preventDefault(); - const field = event.currentTarget.previousElementSibling; - return this.actor.update({[field.name]: 1 - parseInt(field.value)}); - } - - /* -------------------------------------------- */ - - /** - * Handle toggling of filters to display a different set of owned items - * @param {Event} event The click event which triggered the toggle - * @private - */ - _onToggleFilter(event) { - event.preventDefault(); - const li = event.currentTarget; - const set = this._filters[li.parentElement.dataset.filter]; - const filter = li.dataset.filter; - if ( set.has(filter) ) set.delete(filter); - else set.add(filter); - return this.render(); - } - - /* -------------------------------------------- */ - - /** - * Handle spawning the TraitSelector application which allows a checkbox of multiple trait options - * @param {Event} event The click event which originated the selection - * @private - */ - _onTraitSelector(event) { - event.preventDefault(); - const a = event.currentTarget; - const label = a.parentElement.querySelector("label"); - const choices = CONFIG.SW5E[a.dataset.options]; - const options = { name: a.dataset.target, title: label.innerText, choices }; - return new TraitSelector(this.actor, options).render(true) - } - - /* -------------------------------------------- */ - - /** @override */ - _getHeaderButtons() { - let buttons = super._getHeaderButtons(); - if ( this.actor.isPolymorphed ) { - buttons.unshift({ - label: 'SW5E.PolymorphRestoreTransformation', - class: "restore-transformation", - icon: "fas fa-backward", - onclick: () => this.actor.revertOriginalForm() - }); + /** + * Get the font-awesome icon used to display a certain level of skill proficiency + * @private + */ + _getProficiencyIcon(level) { + const icons = { + 0: '', + 0.5: '', + 1: '', + 2: '' + }; + return icons[level] || icons[0]; } - return buttons; - } -} \ No newline at end of file + + /* -------------------------------------------- */ + /* Event Listeners and Handlers + /* -------------------------------------------- */ + + /** @inheritdoc */ + activateListeners(html) { + // Activate Item Filters + const filterLists = html.find(".filter-list"); + filterLists.each(this._initializeFilterItemList.bind(this)); + filterLists.on("click", ".filter-item", this._onToggleFilter.bind(this)); + + // Item summaries + html.find(".item .item-name.rollable h4").click((event) => this._onItemSummary(event)); + + // View Item Sheets + html.find(".item-edit").click(this._onItemEdit.bind(this)); + + // Editable Only Listeners + if (this.isEditable) { + // Input focus and update + const inputs = html.find("input"); + inputs.focus((ev) => ev.currentTarget.select()); + inputs.addBack().find('[data-dtype="Number"]').change(this._onChangeInputDelta.bind(this)); + + // Ability Proficiency + html.find(".ability-proficiency").click(this._onToggleAbilityProficiency.bind(this)); + + // Toggle Skill Proficiency + html.find(".skill-proficiency").on("click contextmenu", this._onCycleSkillProficiency.bind(this)); + + // Trait Selector + html.find(".trait-selector").click(this._onTraitSelector.bind(this)); + + // Configure Special Flags + html.find(".config-button").click(this._onConfigMenu.bind(this)); + + // Owned Item management + html.find(".item-create").click(this._onItemCreate.bind(this)); + html.find(".item-delete").click(this._onItemDelete.bind(this)); + html.find(".item-uses input") + .click((ev) => ev.target.select()) + .change(this._onUsesChange.bind(this)); + html.find(".slot-max-override").click(this._onPowerSlotOverride.bind(this)); + + // Active Effect management + html.find(".effect-control").click((ev) => onManageActiveEffect(ev, this.actor)); + } + + // Owner Only Listeners + if (this.actor.isOwner) { + // Ability Checks + html.find(".ability-name").click(this._onRollAbilityTest.bind(this)); + + // Roll Skill Checks + html.find(".skill-name").click(this._onRollSkillCheck.bind(this)); + + // Item Rolling + html.find(".item .item-image").click((event) => this._onItemRoll(event)); + html.find(".item .item-recharge").click((event) => this._onItemRecharge(event)); + } + + // Otherwise remove rollable classes + else { + html.find(".rollable").each((i, el) => el.classList.remove("rollable")); + } + + // Handle default listeners last so system listeners are triggered first + super.activateListeners(html); + } + + /* -------------------------------------------- */ + + /** + * Iinitialize Item list filters by activating the set of filters which are currently applied + * @private + */ + _initializeFilterItemList(i, ul) { + const set = this._filters[ul.dataset.filter]; + const filters = ul.querySelectorAll(".filter-item"); + for (let li of filters) { + if (set.has(li.dataset.filter)) li.classList.add("active"); + } + } + + /* -------------------------------------------- */ + /* Event Listeners and Handlers */ + /* -------------------------------------------- */ + + /** + * Handle input changes to numeric form fields, allowing them to accept delta-typed inputs + * @param event + * @private + */ + _onChangeInputDelta(event) { + const input = event.target; + const value = input.value; + if (["+", "-"].includes(value[0])) { + let delta = parseFloat(value); + input.value = getProperty(this.actor.data, input.name) + delta; + } else if (value[0] === "=") { + input.value = value.slice(1); + } + } + + /* -------------------------------------------- */ + + /** + * Handle spawning the TraitSelector application which allows a checkbox of multiple trait options + * @param {Event} event The click event which originated the selection + * @private + */ + _onConfigMenu(event) { + event.preventDefault(); + const button = event.currentTarget; + let app; + switch (button.dataset.action) { + case "hit-dice": + app = new ActorHitDiceConfig(this.object); + break; + case "movement": + app = new ActorMovementConfig(this.object); + break; + case "flags": + app = new ActorSheetFlags(this.object); + break; + case "senses": + app = new ActorSensesConfig(this.object); + break; + case "type": + new ActorTypeConfig(this.object).render(true); + break; + } + app?.render(true); + } + + /* -------------------------------------------- */ + + /** + * Handle cycling proficiency in a Skill + * @param {Event} event A click or contextmenu event which triggered the handler + * @private + */ + _onCycleSkillProficiency(event) { + event.preventDefault(); + const field = $(event.currentTarget).siblings('input[type="hidden"]'); + + // Get the current level and the array of levels + const level = parseFloat(field.val()); + const levels = [0, 1, 0.5, 2]; + let idx = levels.indexOf(level); + + // Toggle next level - forward on click, backwards on right + if (event.type === "click") { + field.val(levels[idx === levels.length - 1 ? 0 : idx + 1]); + } else if (event.type === "contextmenu") { + field.val(levels[idx === 0 ? levels.length - 1 : idx - 1]); + } + + // Update the field value and save the form + this._onSubmit(event); + } + + /* -------------------------------------------- */ + + /** @override */ + async _onDropActor(event, data) { + const canPolymorph = game.user.isGM || (this.actor.isOwner && game.settings.get("sw5e", "allowPolymorphing")); + if (!canPolymorph) return false; + + // Get the target actor + let sourceActor = null; + if (data.pack) { + const pack = game.packs.find((p) => p.collection === data.pack); + sourceActor = await pack.getEntity(data.id); + } else { + sourceActor = game.actors.get(data.id); + } + if (!sourceActor) return; + + // Define a function to record polymorph settings for future use + const rememberOptions = (html) => { + const options = {}; + html.find("input").each((i, el) => { + options[el.name] = el.checked; + }); + const settings = mergeObject(game.settings.get("sw5e", "polymorphSettings") || {}, options); + game.settings.set("sw5e", "polymorphSettings", settings); + return settings; + }; + + // Create and render the Dialog + return new Dialog( + { + title: game.i18n.localize("SW5E.PolymorphPromptTitle"), + content: { + options: game.settings.get("sw5e", "polymorphSettings"), + i18n: SW5E.polymorphSettings, + isToken: this.actor.isToken + }, + default: "accept", + buttons: { + accept: { + icon: '', + label: game.i18n.localize("SW5E.PolymorphAcceptSettings"), + callback: (html) => this.actor.transformInto(sourceActor, rememberOptions(html)) + }, + wildshape: { + icon: '', + label: game.i18n.localize("SW5E.PolymorphWildShape"), + callback: (html) => + this.actor.transformInto(sourceActor, { + keepBio: true, + keepClass: true, + keepMental: true, + mergeSaves: true, + mergeSkills: true, + transformTokens: rememberOptions(html).transformTokens + }) + }, + polymorph: { + icon: '', + label: game.i18n.localize("SW5E.Polymorph"), + callback: (html) => + this.actor.transformInto(sourceActor, { + transformTokens: rememberOptions(html).transformTokens + }) + }, + cancel: { + icon: '', + label: game.i18n.localize("Cancel") + } + } + }, + { + classes: ["dialog", "sw5e"], + width: 600, + template: "systems/sw5e/templates/apps/polymorph-prompt.html" + } + ).render(true); + } + + /* -------------------------------------------- */ + + /** @override */ + async _onDropItemCreate(itemData) { + // Check to make sure items of this type are allowed on this actor + if (this.constructor.unsupportedItemTypes.has(itemData.type)) { + return ui.notifications.warn( + game.i18n.format("SW5E.ActorWarningInvalidItem", { + itemType: game.i18n.localize(CONFIG.Item.typeLabels[itemData.type]), + actorType: game.i18n.localize(CONFIG.Actor.typeLabels[this.actor.type]) + }) + ); + } + + // Create a Consumable power scroll on the Inventory tab + // TODO: This is pretty non functional as the base items for the scrolls, and the powers, are not defined, maybe consider using holocrons + if (itemData.type === "power" && this._tabs[0].active === "inventory") { + const scroll = await Item5e.createScrollFromPower(itemData); + itemData = scroll.data; + } + + if (itemData.data) { + // Ignore certain statuses + ["equipped", "proficient", "prepared"].forEach((k) => delete itemData.data[k]); + + // Downgrade ATTUNED to REQUIRED + itemData.data.attunement = Math.min(itemData.data.attunement, CONFIG.SW5E.attunementTypes.REQUIRED); + } + + // Stack identical consumables + if (itemData.type === "consumable" && itemData.flags.core?.sourceId) { + const similarItem = this.actor.items.find((i) => { + const sourceId = i.getFlag("core", "sourceId"); + return sourceId && sourceId === itemData.flags.core?.sourceId && i.type === "consumable"; + }); + if (similarItem) { + return similarItem.update({ + "data.quantity": similarItem.data.data.quantity + Math.max(itemData.data.quantity, 1) + }); + } + } + + // Create the owned item as normal + return super._onDropItemCreate(itemData); + } + + /* -------------------------------------------- */ + + /** + * Handle enabling editing for a power slot override value + * @param {MouseEvent} event The originating click event + * @private + */ + async _onPowerSlotOverride(event) { + const span = event.currentTarget.parentElement; + const level = span.dataset.level; + const override = this.actor.data.data.powers[level].override || span.dataset.slots; + const input = document.createElement("INPUT"); + input.type = "text"; + input.name = `data.powers.${level}.override`; + input.value = override; + input.placeholder = span.dataset.slots; + input.dataset.dtype = "Number"; + + // Replace the HTML + const parent = span.parentElement; + parent.removeChild(span); + parent.appendChild(input); + } + + /* -------------------------------------------- */ + + /** + * Change the uses amount of an Owned Item within the Actor + * @param {Event} event The triggering click event + * @private + */ + async _onUsesChange(event) { + event.preventDefault(); + const itemId = event.currentTarget.closest(".item").dataset.itemId; + const item = this.actor.items.get(itemId); + const uses = Math.clamped(0, parseInt(event.target.value), item.data.data.uses.max); + event.target.value = uses; + return item.update({"data.uses.value": uses}); + } + + /* -------------------------------------------- */ + + /** + * Handle rolling of an item from the Actor sheet, obtaining the Item instance and dispatching to it's roll method + * @private + */ + _onItemRoll(event) { + event.preventDefault(); + const itemId = event.currentTarget.closest(".item").dataset.itemId; + const item = this.actor.items.get(itemId); + return item.roll(); + } + + /* -------------------------------------------- */ + + /** + * Handle attempting to recharge an item usage by rolling a recharge check + * @param {Event} event The originating click event + * @private + */ + _onItemRecharge(event) { + event.preventDefault(); + const itemId = event.currentTarget.closest(".item").dataset.itemId; + const item = this.actor.items.get(itemId); + return item.rollRecharge(); + } + + /* -------------------------------------------- */ + + /** + * Handle rolling of an item from the Actor sheet, obtaining the Item instance and dispatching to it's roll method + * @private + */ + _onItemSummary(event) { + event.preventDefault(); + let li = $(event.currentTarget).parents(".item"), + item = this.actor.items.get(li.data("item-id")), + chatData = item.getChatData({secrets: this.actor.isOwner}); + + // Toggle summary + if (li.hasClass("expanded")) { + let summary = li.children(".item-summary"); + summary.slideUp(200, () => summary.remove()); + } else { + let div = $(`
${chatData.description.value}
`); + let props = $(`
`); + chatData.properties.forEach((p) => props.append(`${p}`)); + div.append(props); + li.append(div.hide()); + div.slideDown(200); + } + li.toggleClass("expanded"); + } + + /* -------------------------------------------- */ + + /** + * Handle creating a new Owned Item for the actor using initial data defined in the HTML dataset + * @param {Event} event The originating click event + * @private + */ + _onItemCreate(event) { + event.preventDefault(); + const header = event.currentTarget; + const type = header.dataset.type; + const itemData = { + name: game.i18n.format("SW5E.ItemNew", {type: game.i18n.localize(`SW5E.ItemType${type.capitalize()}`)}), + type: type, + data: foundry.utils.deepClone(header.dataset) + }; + delete itemData.data["type"]; + return this.actor.createEmbeddedDocuments("Item", [itemData]); + } + + /* -------------------------------------------- */ + + /** + * Handle editing an existing Owned Item for the Actor + * @param {Event} event The originating click event + * @private + */ + _onItemEdit(event) { + event.preventDefault(); + const li = event.currentTarget.closest(".item"); + const item = this.actor.items.get(li.dataset.itemId); + return item.sheet.render(true); + } + + /* -------------------------------------------- */ + + /** + * Handle deleting an existing Owned Item for the Actor + * @param {Event} event The originating click event + * @private + */ + _onItemDelete(event) { + event.preventDefault(); + const li = event.currentTarget.closest(".item"); + const item = this.actor.items.get(li.dataset.itemId); + if (item) return item.delete(); + } + + /* -------------------------------------------- */ + + /** + * Handle rolling an Ability check, either a test or a saving throw + * @param {Event} event The originating click event + * @private + */ + _onRollAbilityTest(event) { + event.preventDefault(); + let ability = event.currentTarget.parentElement.dataset.ability; + return this.actor.rollAbility(ability, {event: event}); + } + + /* -------------------------------------------- */ + + /** + * Handle rolling a Skill check + * @param {Event} event The originating click event + * @private + */ + _onRollSkillCheck(event) { + event.preventDefault(); + const skill = event.currentTarget.parentElement.dataset.skill; + return this.actor.rollSkill(skill, {event: event}); + } + + /* -------------------------------------------- */ + + /** + * Handle toggling Ability score proficiency level + * @param {Event} event The originating click event + * @private + */ + _onToggleAbilityProficiency(event) { + event.preventDefault(); + const field = event.currentTarget.previousElementSibling; + return this.actor.update({[field.name]: 1 - parseInt(field.value)}); + } + + /* -------------------------------------------- */ + + /** + * Handle toggling of filters to display a different set of owned items + * @param {Event} event The click event which triggered the toggle + * @private + */ + _onToggleFilter(event) { + event.preventDefault(); + const li = event.currentTarget; + const set = this._filters[li.parentElement.dataset.filter]; + const filter = li.dataset.filter; + if (set.has(filter)) set.delete(filter); + else set.add(filter); + return this.render(); + } + + /* -------------------------------------------- */ + + /** + * Handle spawning the TraitSelector application which allows a checkbox of multiple trait options + * @param {Event} event The click event which originated the selection + * @private + */ + _onTraitSelector(event) { + event.preventDefault(); + const a = event.currentTarget; + const label = a.parentElement.querySelector("label"); + const choices = CONFIG.SW5E[a.dataset.options]; + const options = {name: a.dataset.target, title: label.innerText, choices}; + return new TraitSelector(this.actor, options).render(true); + } + + /* -------------------------------------------- */ + + /** @override */ + _getHeaderButtons() { + let buttons = super._getHeaderButtons(); + if (this.actor.isPolymorphed) { + buttons.unshift({ + label: "SW5E.PolymorphRestoreTransformation", + class: "restore-transformation", + icon: "fas fa-backward", + onclick: () => this.actor.revertOriginalForm() + }); + } + return buttons; + } +} diff --git a/module/actor/sheets/oldSheets/character.js b/module/actor/sheets/oldSheets/character.js index dd82ecfe..038a28b4 100644 --- a/module/actor/sheets/oldSheets/character.js +++ b/module/actor/sheets/oldSheets/character.js @@ -7,295 +7,362 @@ import Actor5e from "../../entity.js"; * @type {ActorSheet5e} */ export default class ActorSheet5eCharacter extends ActorSheet5e { + /** + * Define default rendering options for the NPC sheet + * @return {Object} + */ + static get defaultOptions() { + return mergeObject(super.defaultOptions, { + classes: ["sw5e", "sheet", "actor", "character"], + width: 720, + height: 736 + }); + } - /** - * Define default rendering options for the NPC sheet - * @return {Object} - */ - static get defaultOptions() { - return mergeObject(super.defaultOptions, { - classes: ["sw5e", "sheet", "actor", "character"], - width: 720, - height: 736 - }); - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** + * Add some extra data when rendering the sheet to reduce the amount of logic required within the template. + */ + getData() { + const sheetData = super.getData(); - /** - * Add some extra data when rendering the sheet to reduce the amount of logic required within the template. - */ - getData() { - const sheetData = super.getData(); + // Temporary HP + let hp = sheetData.data.attributes.hp; + if (hp.temp === 0) delete hp.temp; + if (hp.tempmax === 0) delete hp.tempmax; - // Temporary HP - let hp = sheetData.data.attributes.hp; - if (hp.temp === 0) delete hp.temp; - if (hp.tempmax === 0) delete hp.tempmax; + // Resources + sheetData["resources"] = ["primary", "secondary", "tertiary"].reduce((arr, r) => { + const res = sheetData.data.resources[r] || {}; + res.name = r; + res.placeholder = game.i18n.localize("SW5E.Resource" + r.titleCase()); + if (res && res.value === 0) delete res.value; + if (res && res.max === 0) delete res.max; + return arr.concat([res]); + }, []); - // Resources - sheetData["resources"] = ["primary", "secondary", "tertiary"].reduce((arr, r) => { - const res = sheetData.data.resources[r] || {}; - res.name = r; - res.placeholder = game.i18n.localize("SW5E.Resource"+r.titleCase()); - if (res && res.value === 0) delete res.value; - if (res && res.max === 0) delete res.max; - return arr.concat([res]); - }, []); + // Experience Tracking + sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking"); + sheetData["classLabels"] = this.actor.itemTypes.class.map((c) => c.name).join(", "); + sheetData["multiclassLabels"] = this.actor.itemTypes.class + .map((c) => { + return [c.data.data.archetype, c.name, c.data.data.levels].filterJoin(" "); + }) + .join(", "); - // Experience Tracking - sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking"); - sheetData["classLabels"] = this.actor.itemTypes.class.map(c => c.name).join(", "); - sheetData["multiclassLabels"] = this.actor.itemTypes.class.map(c => { - return [c.data.data.archetype, c.name, c.data.data.levels].filterJoin(' ') - }).join(', '); + // Return data for rendering + return sheetData; + } - // Return data for rendering - return sheetData; - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** + * Organize and classify Owned Items for Character sheets + * @private + */ + _prepareItems(data) { + // Categorize items as inventory, powerbook, features, and classes + const inventory = { + weapon: {label: "SW5E.ItemTypeWeaponPl", items: [], dataset: {type: "weapon"}}, + equipment: {label: "SW5E.ItemTypeEquipmentPl", items: [], dataset: {type: "equipment"}}, + consumable: {label: "SW5E.ItemTypeConsumablePl", items: [], dataset: {type: "consumable"}}, + tool: {label: "SW5E.ItemTypeToolPl", items: [], dataset: {type: "tool"}}, + backpack: {label: "SW5E.ItemTypeContainerPl", items: [], dataset: {type: "backpack"}}, + loot: {label: "SW5E.ItemTypeLootPl", items: [], dataset: {type: "loot"}} + }; - /** - * Organize and classify Owned Items for Character sheets - * @private - */ - _prepareItems(data) { + // Partition items by category + let [ + items, + powers, + feats, + classes, + species, + archetypes, + classfeatures, + backgrounds, + fightingstyles, + fightingmasteries, + lightsaberforms + ] = data.items.reduce( + (arr, item) => { + // Item details + item.img = item.img || CONST.DEFAULT_TOKEN; + item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1; + item.attunement = { + [CONFIG.SW5E.attunementTypes.REQUIRED]: { + icon: "fa-sun", + cls: "not-attuned", + title: "SW5E.AttunementRequired" + }, + [CONFIG.SW5E.attunementTypes.ATTUNED]: { + icon: "fa-sun", + cls: "attuned", + title: "SW5E.AttunementAttuned" + } + }[item.data.attunement]; - // Categorize items as inventory, powerbook, features, and classes - const inventory = { - weapon: { label: "SW5E.ItemTypeWeaponPl", items: [], dataset: {type: "weapon"} }, - equipment: { label: "SW5E.ItemTypeEquipmentPl", items: [], dataset: {type: "equipment"} }, - consumable: { label: "SW5E.ItemTypeConsumablePl", items: [], dataset: {type: "consumable"} }, - tool: { label: "SW5E.ItemTypeToolPl", items: [], dataset: {type: "tool"} }, - backpack: { label: "SW5E.ItemTypeContainerPl", items: [], dataset: {type: "backpack"} }, - loot: { label: "SW5E.ItemTypeLootPl", items: [], dataset: {type: "loot"} } - }; + // Item usage + item.hasUses = item.data.uses && item.data.uses.max > 0; + item.isOnCooldown = + item.data.recharge && !!item.data.recharge.value && item.data.recharge.charged === false; + item.isDepleted = item.isOnCooldown && item.data.uses.per && item.data.uses.value > 0; + item.hasTarget = !!item.data.target && !["none", ""].includes(item.data.target.type); - // Partition items by category - let [items, powers, feats, classes, species, archetypes, classfeatures, backgrounds, fightingstyles, fightingmasteries, lightsaberforms] = data.items.reduce((arr, item) => { + // Item toggle state + this._prepareItemToggleState(item); - // Item details - item.img = item.img || CONST.DEFAULT_TOKEN; - item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1); - item.attunement = { - [CONFIG.SW5E.attunementTypes.REQUIRED]: { - icon: "fa-sun", - cls: "not-attuned", - title: "SW5E.AttunementRequired" - }, - [CONFIG.SW5E.attunementTypes.ATTUNED]: { - icon: "fa-sun", - cls: "attuned", - title: "SW5E.AttunementAttuned" + // Primary Class + if (item.type === "class") + item.isOriginalClass = item._id === this.actor.data.data.details.originalClass; + + // Classify items into types + if (item.type === "power") arr[1].push(item); + else if (item.type === "feat") arr[2].push(item); + else if (item.type === "class") arr[3].push(item); + else if (item.type === "species") arr[4].push(item); + else if (item.type === "archetype") arr[5].push(item); + else if (item.type === "classfeature") arr[6].push(item); + else if (item.type === "background") arr[7].push(item); + else if (item.type === "fightingstyle") arr[8].push(item); + else if (item.type === "fightingmastery") arr[9].push(item); + else if (item.type === "lightsaberform") arr[10].push(item); + else if (Object.keys(inventory).includes(item.type)) arr[0].push(item); + return arr; + }, + [[], [], [], [], [], [], [], [], [], [], []] + ); + + // Apply active item filters + items = this._filterItems(items, this._filters.inventory); + powers = this._filterItems(powers, this._filters.powerbook); + feats = this._filterItems(feats, this._filters.features); + + // Organize items + for (let i of items) { + i.data.quantity = i.data.quantity || 0; + i.data.weight = i.data.weight || 0; + i.totalWeight = (i.data.quantity * i.data.weight).toNearest(0.1); + inventory[i.type].items.push(i); } - }[item.data.attunement]; - // Item usage - item.hasUses = item.data.uses && (item.data.uses.max > 0); - item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false); - item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0)); - item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type)); + // Organize Powerbook and count the number of prepared powers (excluding always, at will, etc...) + const powerbook = this._preparePowerbook(data, powers); + const nPrepared = powers.filter((s) => { + return s.data.level > 0 && s.data.preparation.mode === "prepared" && s.data.preparation.prepared; + }).length; - // Item toggle state - this._prepareItemToggleState(item); - - // Primary Class - if ( item.type === "class" ) item.isOriginalClass = ( item._id === this.actor.data.data.details.originalClass ); - - // Classify items into types - if ( item.type === "power" ) arr[1].push(item); - else if ( item.type === "feat" ) arr[2].push(item); - else if ( item.type === "class" ) arr[3].push(item); - else if ( item.type === "species" ) arr[4].push(item); - else if ( item.type === "archetype" ) arr[5].push(item); - else if ( item.type === "classfeature" ) arr[6].push(item); - else if ( item.type === "background" ) arr[7].push(item); - else if ( item.type === "fightingstyle" ) arr[8].push(item); - else if ( item.type === "fightingmastery" ) arr[9].push(item); - else if ( item.type === "lightsaberform" ) arr[10].push(item); - else if ( Object.keys(inventory).includes(item.type ) ) arr[0].push(item); - return arr; - }, [[], [], [], [], [], [], [], [], [], [], []]); - - // Apply active item filters - items = this._filterItems(items, this._filters.inventory); - powers = this._filterItems(powers, this._filters.powerbook); - feats = this._filterItems(feats, this._filters.features); - - // Organize items - for ( let i of items ) { - i.data.quantity = i.data.quantity || 0; - i.data.weight = i.data.weight || 0; - i.totalWeight = (i.data.quantity * i.data.weight).toNearest(0.1); - inventory[i.type].items.push(i); - } - - // Organize Powerbook and count the number of prepared powers (excluding always, at will, etc...) - const powerbook = this._preparePowerbook(data, powers); - const nPrepared = powers.filter(s => { - return (s.data.level > 0) && (s.data.preparation.mode === "prepared") && s.data.preparation.prepared; - }).length; - - // Organize Features - const features = { - classes: { label: "SW5E.ItemTypeClassPl", items: [], hasActions: false, dataset: {type: "class"}, isClass: true }, - classfeatures: { label: "SW5E.ItemTypeClassFeats", items: [], hasActions: true, dataset: {type: "classfeature"}, isClassfeature: true }, - archetype: { label: "SW5E.ItemTypeArchetype", items: [], hasActions: false, dataset: {type: "archetype"}, isArchetype: true }, - species: { label: "SW5E.ItemTypeSpecies", items: [], hasActions: false, dataset: {type: "species"}, isSpecies: true }, - background: { label: "SW5E.ItemTypeBackground", items: [], hasActions: false, dataset: {type: "background"}, isBackground: true }, - fightingstyles: { label: "SW5E.ItemTypeFightingStylePl", items: [], hasActions: false, dataset: {type: "fightingstyle"}, isFightingstyle: true }, - fightingmasteries: { label: "SW5E.ItemTypeFightingMasteryPl", items: [], hasActions: false, dataset: {type: "fightingmastery"}, isFightingmastery: true }, - lightsaberforms: { label: "SW5E.ItemTypeLightsaberFormPl", items: [], hasActions: false, dataset: {type: "lightsaberform"}, isLightsaberform: true }, - active: { label: "SW5E.FeatureActive", items: [], hasActions: true, dataset: {type: "feat", "activation.type": "action"} }, - passive: { label: "SW5E.FeaturePassive", items: [], hasActions: false, dataset: {type: "feat"} } - }; - for ( let f of feats ) { - if ( f.data.activation.type ) features.active.items.push(f); - else features.passive.items.push(f); - } - classes.sort((a, b) => b.data.levels - a.data.levels); - features.classes.items = classes; - features.classfeatures.items = classfeatures; - features.archetype.items = archetypes; - features.species.items = species; - features.background.items = backgrounds; - features.fightingstyles.items = fightingstyles; - features.fightingmasteries.items = fightingmasteries; - features.lightsaberforms.items = lightsaberforms; - - // Assign and return - data.inventory = Object.values(inventory); - data.powerbook = powerbook; - data.preparedPowers = nPrepared; - data.features = Object.values(features); - } - - /* -------------------------------------------- */ - - /** - * A helper method to establish the displayed preparation state for an item - * @param {Item} item - * @private - */ - _prepareItemToggleState(item) { - if (item.type === "power") { - const isAlways = getProperty(item.data, "preparation.mode") === "always"; - const isPrepared = getProperty(item.data, "preparation.prepared"); - item.toggleClass = isPrepared ? "active" : ""; - if ( isAlways ) item.toggleClass = "fixed"; - if ( isAlways ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.always; - else if ( isPrepared ) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.prepared; - else item.toggleTitle = game.i18n.localize("SW5E.PowerUnprepared"); - } - else { - const isActive = getProperty(item.data, "equipped"); - item.toggleClass = isActive ? "active" : ""; - item.toggleTitle = game.i18n.localize(isActive ? "SW5E.Equipped" : "SW5E.Unequipped"); - } - } - - /* -------------------------------------------- */ - /* Event Listeners and Handlers - /* -------------------------------------------- */ - - /** - * Activate event listeners using the prepared sheet HTML - * @param html {jQuery} The prepared HTML object ready to be rendered into the DOM - */ - activateListeners(html) { - super.activateListeners(html); - if ( !this.isEditable ) return; - - // Item State Toggling - html.find('.item-toggle').click(this._onToggleItem.bind(this)); - - // Short and Long Rest - html.find('.short-rest').click(this._onShortRest.bind(this)); - html.find('.long-rest').click(this._onLongRest.bind(this)); - - // Rollable sheet actions - html.find(".rollable[data-action]").click(this._onSheetAction.bind(this)); - } - - /* -------------------------------------------- */ - - /** - * Handle mouse click events for character sheet actions - * @param {MouseEvent} event The originating click event - * @private - */ - _onSheetAction(event) { - event.preventDefault(); - const button = event.currentTarget; - switch( button.dataset.action ) { - case "rollDeathSave": - return this.actor.rollDeathSave({event: event}); - case "rollInitiative": - return this.actor.rollInitiative({createCombatants: true}); - } - } - - /* -------------------------------------------- */ - - /** - * Handle toggling the state of an Owned Item within the Actor - * @param {Event} event The triggering click event - * @private - */ - _onToggleItem(event) { - event.preventDefault(); - const itemId = event.currentTarget.closest(".item").dataset.itemId; - const item = this.actor.items.get(itemId); - const attr = item.data.type === "power" ? "data.preparation.prepared" : "data.equipped"; - return item.update({[attr]: !getProperty(item.data, attr)}); - } - - /* -------------------------------------------- */ - - /** - * Take a short rest, calling the relevant function on the Actor instance - * @param {Event} event The triggering click event - * @private - */ - async _onShortRest(event) { - event.preventDefault(); - await this._onSubmit(event); - return this.actor.shortRest(); - } - - /* -------------------------------------------- */ - - /** - * Take a long rest, calling the relevant function on the Actor instance - * @param {Event} event The triggering click event - * @private - */ - async _onLongRest(event) { - event.preventDefault(); - await this._onSubmit(event); - return this.actor.longRest(); - } - - /* -------------------------------------------- */ - - /** @override */ - async _onDropItemCreate(itemData) { - - // Increment the number of class levels a character instead of creating a new item - if ( itemData.type === "class" ) { - const cls = this.actor.itemTypes.class.find(c => c.name === itemData.name); - let priorLevel = cls?.data.data.levels ?? 0; - if ( !!cls ) { - const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level); - if ( next > priorLevel ) { - itemData.levels = next; - return cls.update({"data.levels": next}); + // Organize Features + const features = { + classes: { + label: "SW5E.ItemTypeClassPl", + items: [], + hasActions: false, + dataset: {type: "class"}, + isClass: true + }, + classfeatures: { + label: "SW5E.ItemTypeClassFeats", + items: [], + hasActions: true, + dataset: {type: "classfeature"}, + isClassfeature: true + }, + archetype: { + label: "SW5E.ItemTypeArchetype", + items: [], + hasActions: false, + dataset: {type: "archetype"}, + isArchetype: true + }, + species: { + label: "SW5E.ItemTypeSpecies", + items: [], + hasActions: false, + dataset: {type: "species"}, + isSpecies: true + }, + background: { + label: "SW5E.ItemTypeBackground", + items: [], + hasActions: false, + dataset: {type: "background"}, + isBackground: true + }, + fightingstyles: { + label: "SW5E.ItemTypeFightingStylePl", + items: [], + hasActions: false, + dataset: {type: "fightingstyle"}, + isFightingstyle: true + }, + fightingmasteries: { + label: "SW5E.ItemTypeFightingMasteryPl", + items: [], + hasActions: false, + dataset: {type: "fightingmastery"}, + isFightingmastery: true + }, + lightsaberforms: { + label: "SW5E.ItemTypeLightsaberFormPl", + items: [], + hasActions: false, + dataset: {type: "lightsaberform"}, + isLightsaberform: true + }, + active: { + label: "SW5E.FeatureActive", + items: [], + hasActions: true, + dataset: {"type": "feat", "activation.type": "action"} + }, + passive: {label: "SW5E.FeaturePassive", items: [], hasActions: false, dataset: {type: "feat"}} + }; + for (let f of feats) { + if (f.data.activation.type) features.active.items.push(f); + else features.passive.items.push(f); } - } + classes.sort((a, b) => b.data.levels - a.data.levels); + features.classes.items = classes; + features.classfeatures.items = classfeatures; + features.archetype.items = archetypes; + features.species.items = species; + features.background.items = backgrounds; + features.fightingstyles.items = fightingstyles; + features.fightingmasteries.items = fightingmasteries; + features.lightsaberforms.items = lightsaberforms; + + // Assign and return + data.inventory = Object.values(inventory); + data.powerbook = powerbook; + data.preparedPowers = nPrepared; + data.features = Object.values(features); } - // Default drop handling if levels were not added - return super._onDropItemCreate(itemData); - } + /* -------------------------------------------- */ + + /** + * A helper method to establish the displayed preparation state for an item + * @param {Item} item + * @private + */ + _prepareItemToggleState(item) { + if (item.type === "power") { + const isAlways = getProperty(item.data, "preparation.mode") === "always"; + const isPrepared = getProperty(item.data, "preparation.prepared"); + item.toggleClass = isPrepared ? "active" : ""; + if (isAlways) item.toggleClass = "fixed"; + if (isAlways) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.always; + else if (isPrepared) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.prepared; + else item.toggleTitle = game.i18n.localize("SW5E.PowerUnprepared"); + } else { + const isActive = getProperty(item.data, "equipped"); + item.toggleClass = isActive ? "active" : ""; + item.toggleTitle = game.i18n.localize(isActive ? "SW5E.Equipped" : "SW5E.Unequipped"); + } + } + + /* -------------------------------------------- */ + /* Event Listeners and Handlers + /* -------------------------------------------- */ + + /** + * Activate event listeners using the prepared sheet HTML + * @param html {jQuery} The prepared HTML object ready to be rendered into the DOM + */ + activateListeners(html) { + super.activateListeners(html); + if (!this.isEditable) return; + + // Item State Toggling + html.find(".item-toggle").click(this._onToggleItem.bind(this)); + + // Short and Long Rest + html.find(".short-rest").click(this._onShortRest.bind(this)); + html.find(".long-rest").click(this._onLongRest.bind(this)); + + // Rollable sheet actions + html.find(".rollable[data-action]").click(this._onSheetAction.bind(this)); + } + + /* -------------------------------------------- */ + + /** + * Handle mouse click events for character sheet actions + * @param {MouseEvent} event The originating click event + * @private + */ + _onSheetAction(event) { + event.preventDefault(); + const button = event.currentTarget; + switch (button.dataset.action) { + case "rollDeathSave": + return this.actor.rollDeathSave({event: event}); + case "rollInitiative": + return this.actor.rollInitiative({createCombatants: true}); + } + } + + /* -------------------------------------------- */ + + /** + * Handle toggling the state of an Owned Item within the Actor + * @param {Event} event The triggering click event + * @private + */ + _onToggleItem(event) { + event.preventDefault(); + const itemId = event.currentTarget.closest(".item").dataset.itemId; + const item = this.actor.items.get(itemId); + const attr = item.data.type === "power" ? "data.preparation.prepared" : "data.equipped"; + return item.update({[attr]: !getProperty(item.data, attr)}); + } + + /* -------------------------------------------- */ + + /** + * Take a short rest, calling the relevant function on the Actor instance + * @param {Event} event The triggering click event + * @private + */ + async _onShortRest(event) { + event.preventDefault(); + await this._onSubmit(event); + return this.actor.shortRest(); + } + + /* -------------------------------------------- */ + + /** + * Take a long rest, calling the relevant function on the Actor instance + * @param {Event} event The triggering click event + * @private + */ + async _onLongRest(event) { + event.preventDefault(); + await this._onSubmit(event); + return this.actor.longRest(); + } + + /* -------------------------------------------- */ + + /** @override */ + async _onDropItemCreate(itemData) { + // Increment the number of class levels a character instead of creating a new item + if (itemData.type === "class") { + const cls = this.actor.itemTypes.class.find((c) => c.name === itemData.name); + let priorLevel = cls?.data.data.levels ?? 0; + if (!!cls) { + const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level); + if (next > priorLevel) { + itemData.levels = next; + return cls.update({"data.levels": next}); + } + } + } + + // Default drop handling if levels were not added + return super._onDropItemCreate(itemData); + } } diff --git a/module/actor/sheets/oldSheets/npc.js b/module/actor/sheets/oldSheets/npc.js index 12e85b1f..ce848b0b 100644 --- a/module/actor/sheets/oldSheets/npc.js +++ b/module/actor/sheets/oldSheets/npc.js @@ -6,130 +6,139 @@ import ActorSheet5e from "./base.js"; * @extends {ActorSheet5e} */ export default class ActorSheet5eNPC extends ActorSheet5e { - - /** @override */ - static get defaultOptions() { - return mergeObject(super.defaultOptions, { - classes: ["sw5e", "sheet", "actor", "npc"], - width: 600, - height: 680 - }); - } - - /* -------------------------------------------- */ - - /** @override */ - static unsupportedItemTypes = new Set(["class"]); - - /* -------------------------------------------- */ - - /** - * Organize Owned Items for rendering the NPC sheet - * @private - */ - _prepareItems(data) { - - // Categorize Items as Features and Powers - const features = { - weapons: { label: game.i18n.localize("SW5E.AttackPl"), items: [] , hasActions: true, dataset: {type: "weapon", "weapon-type": "natural"} }, - actions: { label: game.i18n.localize("SW5E.ActionPl"), items: [] , hasActions: true, dataset: {type: "feat", "activation.type": "action"} }, - passive: { label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"} }, - equipment: { label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}} - }; - - // Start by classifying items into groups for rendering - let [powers, other] = data.items.reduce((arr, item) => { - item.img = item.img || CONST.DEFAULT_TOKEN; - item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1); - item.hasUses = item.data.uses && (item.data.uses.max > 0); - item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false); - item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0)); - item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type)); - if ( item.type === "power" ) arr[0].push(item); - else arr[1].push(item); - return arr; - }, [[], []]); - - // Apply item filters - powers = this._filterItems(powers, this._filters.powerbook); - other = this._filterItems(other, this._filters.features); - - // Organize Powerbook - const powerbook = this._preparePowerbook(data, powers); - - // Organize Features - for ( let item of other ) { - if ( item.type === "weapon" ) features.weapons.items.push(item); - else if ( item.type === "feat" ) { - if ( item.data.activation.type ) features.actions.items.push(item); - else features.passive.items.push(item); - } - else features.equipment.items.push(item); + /** @override */ + static get defaultOptions() { + return mergeObject(super.defaultOptions, { + classes: ["sw5e", "sheet", "actor", "npc"], + width: 600, + height: 680 + }); } - // Assign and return - data.features = Object.values(features); - data.powerbook = powerbook; - } + /* -------------------------------------------- */ + /** @override */ + static unsupportedItemTypes = new Set(["class"]); - /* -------------------------------------------- */ + /* -------------------------------------------- */ - /** @inheritdoc */ - getData(options) { - const data = super.getData(options); + /** + * Organize Owned Items for rendering the NPC sheet + * @private + */ + _prepareItems(data) { + // Categorize Items as Features and Powers + const features = { + weapons: { + label: game.i18n.localize("SW5E.AttackPl"), + items: [], + hasActions: true, + dataset: {"type": "weapon", "weapon-type": "natural"} + }, + actions: { + label: game.i18n.localize("SW5E.ActionPl"), + items: [], + hasActions: true, + dataset: {"type": "feat", "activation.type": "action"} + }, + passive: {label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"}}, + equipment: {label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}} + }; - // Challenge Rating - const cr = parseFloat(data.data.details.cr || 0); - const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"}; - data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1; + // Start by classifying items into groups for rendering + let [powers, other] = data.items.reduce( + (arr, item) => { + item.img = item.img || CONST.DEFAULT_TOKEN; + item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1; + item.hasUses = item.data.uses && item.data.uses.max > 0; + item.isOnCooldown = + item.data.recharge && !!item.data.recharge.value && item.data.recharge.charged === false; + item.isDepleted = item.isOnCooldown && item.data.uses.per && item.data.uses.value > 0; + item.hasTarget = !!item.data.target && !["none", ""].includes(item.data.target.type); + if (item.type === "power") arr[0].push(item); + else arr[1].push(item); + return arr; + }, + [[], []] + ); - // Creature Type - data.labels["type"] = this.actor.labels.creatureType; - return data; - } + // Apply item filters + powers = this._filterItems(powers, this._filters.powerbook); + other = this._filterItems(other, this._filters.features); - /* -------------------------------------------- */ - /* Object Updates */ - /* -------------------------------------------- */ + // Organize Powerbook + const powerbook = this._preparePowerbook(data, powers); - /** @override */ - async _updateObject(event, formData) { + // Organize Features + for (let item of other) { + if (item.type === "weapon") features.weapons.items.push(item); + else if (item.type === "feat") { + if (item.data.activation.type) features.actions.items.push(item); + else features.passive.items.push(item); + } else features.equipment.items.push(item); + } - // Format NPC Challenge Rating - const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5}; - let crv = "data.details.cr"; - let cr = formData[crv]; - cr = crs[cr] || parseFloat(cr); - if ( cr ) formData[crv] = cr < 1 ? cr : parseInt(cr); + // Assign and return + data.features = Object.values(features); + data.powerbook = powerbook; + } - // Parent ActorSheet update steps - return super._updateObject(event, formData); - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ - /* Event Listeners and Handlers */ - /* -------------------------------------------- */ + /** @inheritdoc */ + getData(options) { + const data = super.getData(options); - /** @override */ - activateListeners(html) { - super.activateListeners(html); - html.find(".health .rollable").click(this._onRollHPFormula.bind(this)); - } + // Challenge Rating + const cr = parseFloat(data.data.details.cr || 0); + const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"}; + data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1; - /* -------------------------------------------- */ + // Creature Type + data.labels["type"] = this.actor.labels.creatureType; + return data; + } - /** - * Handle rolling NPC health values using the provided formula - * @param {Event} event The original click event - * @private - */ - _onRollHPFormula(event) { - event.preventDefault(); - const formula = this.actor.data.data.attributes.hp.formula; - if ( !formula ) return; - const hp = new Roll(formula).roll().total; - AudioHelper.play({src: CONFIG.sounds.dice}); - this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp}); - } + /* -------------------------------------------- */ + /* Object Updates */ + /* -------------------------------------------- */ + + /** @override */ + async _updateObject(event, formData) { + // Format NPC Challenge Rating + const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5}; + let crv = "data.details.cr"; + let cr = formData[crv]; + cr = crs[cr] || parseFloat(cr); + if (cr) formData[crv] = cr < 1 ? cr : parseInt(cr); + + // Parent ActorSheet update steps + return super._updateObject(event, formData); + } + + /* -------------------------------------------- */ + /* Event Listeners and Handlers */ + /* -------------------------------------------- */ + + /** @override */ + activateListeners(html) { + super.activateListeners(html); + html.find(".health .rollable").click(this._onRollHPFormula.bind(this)); + } + + /* -------------------------------------------- */ + + /** + * Handle rolling NPC health values using the provided formula + * @param {Event} event The original click event + * @private + */ + _onRollHPFormula(event) { + event.preventDefault(); + const formula = this.actor.data.data.attributes.hp.formula; + if (!formula) return; + const hp = new Roll(formula).roll().total; + AudioHelper.play({src: CONFIG.sounds.dice}); + this.actor.update({"data.attributes.hp.value": hp, "data.attributes.hp.max": hp}); + } } diff --git a/module/actor/sheets/oldSheets/vehicle.js b/module/actor/sheets/oldSheets/vehicle.js index b6ec2fc8..a5c6e2ea 100644 --- a/module/actor/sheets/oldSheets/vehicle.js +++ b/module/actor/sheets/oldSheets/vehicle.js @@ -6,411 +6,427 @@ import ActorSheet5e from "./base.js"; * @type {ActorSheet5e} */ export default class ActorSheet5eVehicle extends ActorSheet5e { - /** - * Define default rendering options for the Vehicle sheet. - * @returns {Object} - */ - static get defaultOptions() { - return mergeObject(super.defaultOptions, { - classes: ["sw5e", "sheet", "actor", "vehicle"], - width: 605, - height: 680 - }); - } - - /* -------------------------------------------- */ - - /** @override */ - static unsupportedItemTypes = new Set(["class"]); - - /* -------------------------------------------- */ - - - /** - * Creates a new cargo entry for a vehicle Actor. - */ - static get newCargo() { - return { - name: '', - quantity: 1 - }; - } - - /* -------------------------------------------- */ - - /** - * Compute the total weight of the vehicle's cargo. - * @param {Number} totalWeight The cumulative item weight from inventory items - * @param {Object} actorData The data object for the Actor being rendered - * @returns {{max: number, value: number, pct: number}} - * @private - */ - _computeEncumbrance(totalWeight, actorData) { - - // Compute currency weight - const totalCoins = Object.values(actorData.data.currency).reduce((acc, denom) => acc + denom, 0); - totalWeight += totalCoins / CONFIG.SW5E.encumbrance.currencyPerWeight; - - // Vehicle weights are an order of magnitude greater. - totalWeight /= CONFIG.SW5E.encumbrance.vehicleWeightMultiplier; - - // Compute overall encumbrance - const max = actorData.data.attributes.capacity.cargo; - const pct = Math.clamped((totalWeight * 100) / max, 0, 100); - return {value: totalWeight.toNearest(0.1), max, pct}; - } - - /* -------------------------------------------- */ - - /** @override */ - _getMovementSpeed(actorData, largestPrimary=true) { - return super._getMovementSpeed(actorData, largestPrimary); - } - - /* -------------------------------------------- */ - - /** - * Prepare items that are mounted to a vehicle and require one or more crew - * to operate. - * @private - */ - _prepareCrewedItem(item) { - - // Determine crewed status - const isCrewed = item.data.crewed; - item.toggleClass = isCrewed ? 'active' : ''; - item.toggleTitle = game.i18n.localize(`SW5E.${isCrewed ? 'Crewed' : 'Uncrewed'}`); - - // Handle crew actions - if (item.type === 'feat' && item.data.activation.type === 'crew') { - item.crew = item.data.activation.cost; - item.cover = game.i18n.localize(`SW5E.${item.data.cover ? 'CoverTotal' : 'None'}`); - if (item.data.cover === .5) item.cover = '½'; - else if (item.data.cover === .75) item.cover = '¾'; - else if (item.data.cover === null) item.cover = '—'; - if (item.crew < 1 || item.crew === null) item.crew = '—'; + /** + * Define default rendering options for the Vehicle sheet. + * @returns {Object} + */ + static get defaultOptions() { + return mergeObject(super.defaultOptions, { + classes: ["sw5e", "sheet", "actor", "vehicle"], + width: 605, + height: 680 + }); } - // Prepare vehicle weapons - if (item.type === 'equipment' || item.type === 'weapon') { - item.threshold = item.data.hp.dt ? item.data.hp.dt : '—'; - } - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** @override */ + static unsupportedItemTypes = new Set(["class"]); - /** - * Organize Owned Items for rendering the Vehicle sheet. - * @private - */ - _prepareItems(data) { - const cargoColumns = [{ - label: game.i18n.localize('SW5E.Quantity'), - css: 'item-qty', - property: 'quantity', - editable: 'Number' - }]; + /* -------------------------------------------- */ - const equipmentColumns = [{ - label: game.i18n.localize('SW5E.Quantity'), - css: 'item-qty', - property: 'data.quantity' - }, { - label: game.i18n.localize('SW5E.AC'), - css: 'item-ac', - property: 'data.armor.value' - }, { - label: game.i18n.localize('SW5E.HP'), - css: 'item-hp', - property: 'data.hp.value', - editable: 'Number' - }, { - label: game.i18n.localize('SW5E.Threshold'), - css: 'item-threshold', - property: 'threshold' - }]; - - const features = { - actions: { - label: game.i18n.localize('SW5E.ActionPl'), - items: [], - crewable: true, - dataset: {type: 'feat', 'activation.type': 'crew'}, - columns: [{ - label: game.i18n.localize('SW5E.VehicleCrew'), - css: 'item-crew', - property: 'crew' - }, { - label: game.i18n.localize('SW5E.Cover'), - css: 'item-cover', - property: 'cover' - }] - }, - equipment: { - label: game.i18n.localize('SW5E.ItemTypeEquipment'), - items: [], - crewable: true, - dataset: {type: 'equipment', 'armor.type': 'vehicle'}, - columns: equipmentColumns - }, - passive: { - label: game.i18n.localize('SW5E.Features'), - items: [], - dataset: {type: 'feat'} - }, - reactions: { - label: game.i18n.localize('SW5E.ReactionPl'), - items: [], - dataset: {type: 'feat', 'activation.type': 'reaction'} - }, - weapons: { - label: game.i18n.localize('SW5E.ItemTypeWeaponPl'), - items: [], - crewable: true, - dataset: {type: 'weapon', 'weapon-type': 'siege'}, - columns: equipmentColumns - } - }; - - const cargo = { - crew: { - label: game.i18n.localize('SW5E.VehicleCrew'), - items: data.data.cargo.crew, - css: 'cargo-row crew', - editableName: true, - dataset: {type: 'crew'}, - columns: cargoColumns - }, - passengers: { - label: game.i18n.localize('SW5E.VehiclePassengers'), - items: data.data.cargo.passengers, - css: 'cargo-row passengers', - editableName: true, - dataset: {type: 'passengers'}, - columns: cargoColumns - }, - cargo: { - label: game.i18n.localize('SW5E.VehicleCargo'), - items: [], - dataset: {type: 'loot'}, - columns: [{ - label: game.i18n.localize('SW5E.Quantity'), - css: 'item-qty', - property: 'data.quantity', - editable: 'Number' - }, { - label: game.i18n.localize('SW5E.Price'), - css: 'item-price', - property: 'data.price', - editable: 'Number' - }, { - label: game.i18n.localize('SW5E.Weight'), - css: 'item-weight', - property: 'data.weight', - editable: 'Number' - }] - } - }; - - // Classify items owned by the vehicle and compute total cargo weight - let totalWeight = 0; - for (const item of data.items) { - this._prepareCrewedItem(item); - - // Handle cargo explicitly - const isCargo = item.flags.sw5e?.vehicleCargo === true; - if ( isCargo ) { - totalWeight += (item.data.weight || 0) * item.data.quantity; - cargo.cargo.items.push(item); - continue; - } - - // Handle non-cargo item types - switch ( item.type ) { - case "weapon": - features.weapons.items.push(item); - break; - case "equipment": - features.equipment.items.push(item); - break; - case "feat": - if (!item.data.activation.type || (item.data.activation.type === "none")) features.passive.items.push(item); - else if (item.data.activation.type === 'reaction') features.reactions.items.push(item); - else features.actions.items.push(item); - break; - default: - totalWeight += (item.data.weight || 0) * item.data.quantity; - cargo.cargo.items.push(item); - } + /** + * Creates a new cargo entry for a vehicle Actor. + */ + static get newCargo() { + return { + name: "", + quantity: 1 + }; } - // Update the rendering context data - data.features = Object.values(features); - data.cargo = Object.values(cargo); - data.data.attributes.encumbrance = this._computeEncumbrance(totalWeight, data); - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ - /* Event Listeners and Handlers */ - /* -------------------------------------------- */ + /** + * Compute the total weight of the vehicle's cargo. + * @param {Number} totalWeight The cumulative item weight from inventory items + * @param {Object} actorData The data object for the Actor being rendered + * @returns {{max: number, value: number, pct: number}} + * @private + */ + _computeEncumbrance(totalWeight, actorData) { + // Compute currency weight + const totalCoins = Object.values(actorData.data.currency).reduce((acc, denom) => acc + denom, 0); + totalWeight += totalCoins / CONFIG.SW5E.encumbrance.currencyPerWeight; - /** @override */ - activateListeners(html) { - super.activateListeners(html); - if (!this.isEditable) return; + // Vehicle weights are an order of magnitude greater. + totalWeight /= CONFIG.SW5E.encumbrance.vehicleWeightMultiplier; - html.find('.item-toggle').click(this._onToggleItem.bind(this)); - html.find('.item-hp input') - .click(evt => evt.target.select()) - .change(this._onHPChange.bind(this)); - - html.find('.item:not(.cargo-row) input[data-property]') - .click(evt => evt.target.select()) - .change(this._onEditInSheet.bind(this)); - - html.find('.cargo-row input') - .click(evt => evt.target.select()) - .change(this._onCargoRowChange.bind(this)); - - if (this.actor.data.data.attributes.actions.stations) { - html.find('.counter.actions, .counter.action-thresholds').hide(); - } - } - - /* -------------------------------------------- */ - - /** - * Handle saving a cargo row (i.e. crew or passenger) in-sheet. - * @param event {Event} - * @returns {Promise|null} - * @private - */ - _onCargoRowChange(event) { - event.preventDefault(); - const target = event.currentTarget; - const row = target.closest('.item'); - const idx = Number(row.dataset.itemId); - const property = row.classList.contains('crew') ? 'crew' : 'passengers'; - - // Get the cargo entry - const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[property]); - const entry = cargo[idx]; - if (!entry) return null; - - // Update the cargo value - const key = target.dataset.property || 'name'; - const type = target.dataset.dtype; - let value = target.value; - if (type === 'Number') value = Number(value); - entry[key] = value; - - // Perform the Actor update - return this.actor.update({[`data.cargo.${property}`]: cargo}); - } - - /* -------------------------------------------- */ - - /** - * Handle editing certain values like quantity, price, and weight in-sheet. - * @param event {Event} - * @returns {Promise} - * @private - */ - _onEditInSheet(event) { - event.preventDefault(); - const itemID = event.currentTarget.closest('.item').dataset.itemId; - const item = this.actor.items.get(itemID); - const property = event.currentTarget.dataset.property; - const type = event.currentTarget.dataset.dtype; - let value = event.currentTarget.value; - switch (type) { - case 'Number': value = parseInt(value); break; - case 'Boolean': value = value === 'true'; break; - } - return item.update({[`${property}`]: value}); - } - - /* -------------------------------------------- */ - - /** - * Handle creating a new crew or passenger row. - * @param event {Event} - * @returns {Promise} - * @private - */ - _onItemCreate(event) { - event.preventDefault(); - const target = event.currentTarget; - const type = target.dataset.type; - if (type === 'crew' || type === 'passengers') { - const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]); - cargo.push(this.constructor.newCargo); - return this.actor.update({[`data.cargo.${type}`]: cargo}); - } - return super._onItemCreate(event); - } - - /* -------------------------------------------- */ - - /** - * Handle deleting a crew or passenger row. - * @param event {Event} - * @returns {Promise} - * @private - */ - _onItemDelete(event) { - event.preventDefault(); - const row = event.currentTarget.closest('.item'); - if (row.classList.contains('cargo-row')) { - const idx = Number(row.dataset.itemId); - const type = row.classList.contains('crew') ? 'crew' : 'passengers'; - const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx); - return this.actor.update({[`data.cargo.${type}`]: cargo}); + // Compute overall encumbrance + const max = actorData.data.attributes.capacity.cargo; + const pct = Math.clamped((totalWeight * 100) / max, 0, 100); + return {value: totalWeight.toNearest(0.1), max, pct}; } - return super._onItemDelete(event); - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** @override */ + _getMovementSpeed(actorData, largestPrimary = true) { + return super._getMovementSpeed(actorData, largestPrimary); + } - /** @override */ - async _onDropItemCreate(itemData) { - const cargoTypes = ["weapon", "equipment", "consumable", "tool", "loot", "backpack"]; - const isCargo = cargoTypes.includes(itemData.type) && (this._tabs[0].active === "cargo"); - foundry.utils.setProperty(itemData, "flags.sw5e.vehicleCargo", isCargo); - return super._onDropItemCreate(itemData); - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** + * Prepare items that are mounted to a vehicle and require one or more crew + * to operate. + * @private + */ + _prepareCrewedItem(item) { + // Determine crewed status + const isCrewed = item.data.crewed; + item.toggleClass = isCrewed ? "active" : ""; + item.toggleTitle = game.i18n.localize(`SW5E.${isCrewed ? "Crewed" : "Uncrewed"}`); - /** - * Special handling for editing HP to clamp it within appropriate range. - * @param event {Event} - * @returns {Promise} - * @private - */ - _onHPChange(event) { - event.preventDefault(); - const itemID = event.currentTarget.closest('.item').dataset.itemId; - const item = this.actor.items.get(itemID); - const hp = Math.clamped(0, parseInt(event.currentTarget.value), item.data.data.hp.max); - event.currentTarget.value = hp; - return item.update({'data.hp.value': hp}); - } + // Handle crew actions + if (item.type === "feat" && item.data.activation.type === "crew") { + item.crew = item.data.activation.cost; + item.cover = game.i18n.localize(`SW5E.${item.data.cover ? "CoverTotal" : "None"}`); + if (item.data.cover === 0.5) item.cover = "½"; + else if (item.data.cover === 0.75) item.cover = "¾"; + else if (item.data.cover === null) item.cover = "—"; + if (item.crew < 1 || item.crew === null) item.crew = "—"; + } - /* -------------------------------------------- */ + // Prepare vehicle weapons + if (item.type === "equipment" || item.type === "weapon") { + item.threshold = item.data.hp.dt ? item.data.hp.dt : "—"; + } + } - /** - * Handle toggling an item's crewed status. - * @param event {Event} - * @returns {Promise} - * @private - */ - _onToggleItem(event) { - event.preventDefault(); - const itemID = event.currentTarget.closest('.item').dataset.itemId; - const item = this.actor.items.get(itemID); - const crewed = !!item.data.data.crewed; - return item.update({'data.crewed': !crewed}); - } -}; + /* -------------------------------------------- */ + + /** + * Organize Owned Items for rendering the Vehicle sheet. + * @private + */ + _prepareItems(data) { + const cargoColumns = [ + { + label: game.i18n.localize("SW5E.Quantity"), + css: "item-qty", + property: "quantity", + editable: "Number" + } + ]; + + const equipmentColumns = [ + { + label: game.i18n.localize("SW5E.Quantity"), + css: "item-qty", + property: "data.quantity" + }, + { + label: game.i18n.localize("SW5E.AC"), + css: "item-ac", + property: "data.armor.value" + }, + { + label: game.i18n.localize("SW5E.HP"), + css: "item-hp", + property: "data.hp.value", + editable: "Number" + }, + { + label: game.i18n.localize("SW5E.Threshold"), + css: "item-threshold", + property: "threshold" + } + ]; + + const features = { + actions: { + label: game.i18n.localize("SW5E.ActionPl"), + items: [], + crewable: true, + dataset: {"type": "feat", "activation.type": "crew"}, + columns: [ + { + label: game.i18n.localize("SW5E.VehicleCrew"), + css: "item-crew", + property: "crew" + }, + { + label: game.i18n.localize("SW5E.Cover"), + css: "item-cover", + property: "cover" + } + ] + }, + equipment: { + label: game.i18n.localize("SW5E.ItemTypeEquipment"), + items: [], + crewable: true, + dataset: {"type": "equipment", "armor.type": "vehicle"}, + columns: equipmentColumns + }, + passive: { + label: game.i18n.localize("SW5E.Features"), + items: [], + dataset: {type: "feat"} + }, + reactions: { + label: game.i18n.localize("SW5E.ReactionPl"), + items: [], + dataset: {"type": "feat", "activation.type": "reaction"} + }, + weapons: { + label: game.i18n.localize("SW5E.ItemTypeWeaponPl"), + items: [], + crewable: true, + dataset: {"type": "weapon", "weapon-type": "siege"}, + columns: equipmentColumns + } + }; + + const cargo = { + crew: { + label: game.i18n.localize("SW5E.VehicleCrew"), + items: data.data.cargo.crew, + css: "cargo-row crew", + editableName: true, + dataset: {type: "crew"}, + columns: cargoColumns + }, + passengers: { + label: game.i18n.localize("SW5E.VehiclePassengers"), + items: data.data.cargo.passengers, + css: "cargo-row passengers", + editableName: true, + dataset: {type: "passengers"}, + columns: cargoColumns + }, + cargo: { + label: game.i18n.localize("SW5E.VehicleCargo"), + items: [], + dataset: {type: "loot"}, + columns: [ + { + label: game.i18n.localize("SW5E.Quantity"), + css: "item-qty", + property: "data.quantity", + editable: "Number" + }, + { + label: game.i18n.localize("SW5E.Price"), + css: "item-price", + property: "data.price", + editable: "Number" + }, + { + label: game.i18n.localize("SW5E.Weight"), + css: "item-weight", + property: "data.weight", + editable: "Number" + } + ] + } + }; + + // Classify items owned by the vehicle and compute total cargo weight + let totalWeight = 0; + for (const item of data.items) { + this._prepareCrewedItem(item); + + // Handle cargo explicitly + const isCargo = item.flags.sw5e?.vehicleCargo === true; + if (isCargo) { + totalWeight += (item.data.weight || 0) * item.data.quantity; + cargo.cargo.items.push(item); + continue; + } + + // Handle non-cargo item types + switch (item.type) { + case "weapon": + features.weapons.items.push(item); + break; + case "equipment": + features.equipment.items.push(item); + break; + case "feat": + if (!item.data.activation.type || item.data.activation.type === "none") + features.passive.items.push(item); + else if (item.data.activation.type === "reaction") features.reactions.items.push(item); + else features.actions.items.push(item); + break; + default: + totalWeight += (item.data.weight || 0) * item.data.quantity; + cargo.cargo.items.push(item); + } + } + + // Update the rendering context data + data.features = Object.values(features); + data.cargo = Object.values(cargo); + data.data.attributes.encumbrance = this._computeEncumbrance(totalWeight, data); + } + + /* -------------------------------------------- */ + /* Event Listeners and Handlers */ + /* -------------------------------------------- */ + + /** @override */ + activateListeners(html) { + super.activateListeners(html); + if (!this.isEditable) return; + + html.find(".item-toggle").click(this._onToggleItem.bind(this)); + html.find(".item-hp input") + .click((evt) => evt.target.select()) + .change(this._onHPChange.bind(this)); + + html.find(".item:not(.cargo-row) input[data-property]") + .click((evt) => evt.target.select()) + .change(this._onEditInSheet.bind(this)); + + html.find(".cargo-row input") + .click((evt) => evt.target.select()) + .change(this._onCargoRowChange.bind(this)); + + if (this.actor.data.data.attributes.actions.stations) { + html.find(".counter.actions, .counter.action-thresholds").hide(); + } + } + + /* -------------------------------------------- */ + + /** + * Handle saving a cargo row (i.e. crew or passenger) in-sheet. + * @param event {Event} + * @returns {Promise|null} + * @private + */ + _onCargoRowChange(event) { + event.preventDefault(); + const target = event.currentTarget; + const row = target.closest(".item"); + const idx = Number(row.dataset.itemId); + const property = row.classList.contains("crew") ? "crew" : "passengers"; + + // Get the cargo entry + const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[property]); + const entry = cargo[idx]; + if (!entry) return null; + + // Update the cargo value + const key = target.dataset.property || "name"; + const type = target.dataset.dtype; + let value = target.value; + if (type === "Number") value = Number(value); + entry[key] = value; + + // Perform the Actor update + return this.actor.update({[`data.cargo.${property}`]: cargo}); + } + + /* -------------------------------------------- */ + + /** + * Handle editing certain values like quantity, price, and weight in-sheet. + * @param event {Event} + * @returns {Promise} + * @private + */ + _onEditInSheet(event) { + event.preventDefault(); + const itemID = event.currentTarget.closest(".item").dataset.itemId; + const item = this.actor.items.get(itemID); + const property = event.currentTarget.dataset.property; + const type = event.currentTarget.dataset.dtype; + let value = event.currentTarget.value; + switch (type) { + case "Number": + value = parseInt(value); + break; + case "Boolean": + value = value === "true"; + break; + } + return item.update({[`${property}`]: value}); + } + + /* -------------------------------------------- */ + + /** + * Handle creating a new crew or passenger row. + * @param event {Event} + * @returns {Promise} + * @private + */ + _onItemCreate(event) { + event.preventDefault(); + const target = event.currentTarget; + const type = target.dataset.type; + if (type === "crew" || type === "passengers") { + const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]); + cargo.push(this.constructor.newCargo); + return this.actor.update({[`data.cargo.${type}`]: cargo}); + } + return super._onItemCreate(event); + } + + /* -------------------------------------------- */ + + /** + * Handle deleting a crew or passenger row. + * @param event {Event} + * @returns {Promise} + * @private + */ + _onItemDelete(event) { + event.preventDefault(); + const row = event.currentTarget.closest(".item"); + if (row.classList.contains("cargo-row")) { + const idx = Number(row.dataset.itemId); + const type = row.classList.contains("crew") ? "crew" : "passengers"; + const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx); + return this.actor.update({[`data.cargo.${type}`]: cargo}); + } + + return super._onItemDelete(event); + } + + /* -------------------------------------------- */ + + /** @override */ + async _onDropItemCreate(itemData) { + const cargoTypes = ["weapon", "equipment", "consumable", "tool", "loot", "backpack"]; + const isCargo = cargoTypes.includes(itemData.type) && this._tabs[0].active === "cargo"; + foundry.utils.setProperty(itemData, "flags.sw5e.vehicleCargo", isCargo); + return super._onDropItemCreate(itemData); + } + + /* -------------------------------------------- */ + + /** + * Special handling for editing HP to clamp it within appropriate range. + * @param event {Event} + * @returns {Promise} + * @private + */ + _onHPChange(event) { + event.preventDefault(); + const itemID = event.currentTarget.closest(".item").dataset.itemId; + const item = this.actor.items.get(itemID); + const hp = Math.clamped(0, parseInt(event.currentTarget.value), item.data.data.hp.max); + event.currentTarget.value = hp; + return item.update({"data.hp.value": hp}); + } + + /* -------------------------------------------- */ + + /** + * Handle toggling an item's crewed status. + * @param event {Event} + * @returns {Promise} + * @private + */ + _onToggleItem(event) { + event.preventDefault(); + const itemID = event.currentTarget.closest(".item").dataset.itemId; + const item = this.actor.items.get(itemID); + const crewed = !!item.data.data.crewed; + return item.update({"data.crewed": !crewed}); + } +} diff --git a/module/apps/ability-use-dialog.js b/module/apps/ability-use-dialog.js index 4b92b14d..6c43775d 100644 --- a/module/apps/ability-use-dialog.js +++ b/module/apps/ability-use-dialog.js @@ -3,220 +3,225 @@ * @type {Dialog} */ export default class AbilityUseDialog extends Dialog { - constructor(item, dialogData={}, options={}) { - super(dialogData, options); - this.options.classes = ["sw5e", "dialog"]; + constructor(item, dialogData = {}, options = {}) { + super(dialogData, options); + this.options.classes = ["sw5e", "dialog"]; + + /** + * Store a reference to the Item entity being used + * @type {Item5e} + */ + this.item = item; + } + + /* -------------------------------------------- */ + /* Rendering */ + /* -------------------------------------------- */ /** - * Store a reference to the Item entity being used - * @type {Item5e} + * A constructor function which displays the Power Cast Dialog app for a given Actor and Item. + * Returns a Promise which resolves to the dialog FormData once the workflow has been completed. + * @param {Item5e} item + * @return {Promise} */ - this.item = item; - } + static async create(item) { + if (!item.isOwned) throw new Error("You cannot display an ability usage dialog for an unowned item"); - /* -------------------------------------------- */ - /* Rendering */ - /* -------------------------------------------- */ + // Prepare data + const actorData = item.actor.data.data; + const itemData = item.data.data; + const uses = itemData.uses || {}; + const quantity = itemData.quantity || 0; + const recharge = itemData.recharge || {}; + const recharges = !!recharge.value; + const sufficientUses = (quantity > 0 && !uses.value) || uses.value > 0; - /** - * A constructor function which displays the Power Cast Dialog app for a given Actor and Item. - * Returns a Promise which resolves to the dialog FormData once the workflow has been completed. - * @param {Item5e} item - * @return {Promise} - */ - static async create(item) { - if ( !item.isOwned ) throw new Error("You cannot display an ability usage dialog for an unowned item"); + // Prepare dialog form data + const data = { + item: item.data, + title: game.i18n.format("SW5E.AbilityUseHint", { + type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`), + name: item.name + }), + note: this._getAbilityUseNote(item.data, uses, recharge), + consumePowerSlot: false, + consumeRecharge: recharges, + consumeResource: !!itemData.consume.target, + consumeUses: uses.per && uses.max > 0, + canUse: recharges ? recharge.charged : sufficientUses, + createTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget, + errors: [] + }; + if (item.data.type === "power") this._getPowerData(actorData, itemData, data); - // Prepare data - const actorData = item.actor.data.data; - const itemData = item.data.data; - const uses = itemData.uses || {}; - const quantity = itemData.quantity || 0; - const recharge = itemData.recharge || {}; - const recharges = !!recharge.value; - const sufficientUses = (quantity > 0 && !uses.value) || uses.value > 0; + // Render the ability usage template + const html = await renderTemplate("systems/sw5e/templates/apps/ability-use.html", data); - // Prepare dialog form data - const data = { - item: item.data, - title: game.i18n.format("SW5E.AbilityUseHint", {type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`), name: item.name}), - note: this._getAbilityUseNote(item.data, uses, recharge), - consumePowerSlot: false, - consumeRecharge: recharges, - consumeResource: !!itemData.consume.target, - consumeUses: uses.per && (uses.max > 0), - canUse: recharges ? recharge.charged : sufficientUses, - createTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget, - errors: [] - }; - if ( item.data.type === "power" ) this._getPowerData(actorData, itemData, data); + // Create the Dialog and return data as a Promise + const icon = data.isPower ? "fa-magic" : "fa-fist-raised"; + const label = game.i18n.localize("SW5E.AbilityUse" + (data.isPower ? "Cast" : "Use")); + return new Promise((resolve) => { + const dlg = new this(item, { + title: `${item.name}: ${game.i18n.localize("SW5E.AbilityUseConfig")}`, + content: html, + buttons: { + use: { + icon: ``, + label: label, + callback: (html) => { + const fd = new FormDataExtended(html[0].querySelector("form")); + resolve(fd.toObject()); + } + } + }, + default: "use", + close: () => resolve(null) + }); + dlg.render(true); + }); + } - // Render the ability usage template - const html = await renderTemplate("systems/sw5e/templates/apps/ability-use.html", data); + /* -------------------------------------------- */ + /* Helpers */ + /* -------------------------------------------- */ - // Create the Dialog and return data as a Promise - const icon = data.isPower ? "fa-magic" : "fa-fist-raised"; - const label = game.i18n.localize("SW5E.AbilityUse" + (data.isPower ? "Cast" : "Use")); - return new Promise((resolve) => { - const dlg = new this(item, { - title: `${item.name}: ${game.i18n.localize("SW5E.AbilityUseConfig")}`, - content: html, - buttons: { - use: { - icon: ``, - label: label, - callback: html => { - const fd = new FormDataExtended(html[0].querySelector("form")); - resolve(fd.toObject()); + /** + * Get dialog data related to limited power slots + * @private + */ + static _getPowerData(actorData, itemData, data) { + // Determine whether the power may be up-cast + const lvl = itemData.level; + const consumePowerSlot = lvl > 0 && CONFIG.SW5E.powerUpcastModes.includes(itemData.preparation.mode); + + // If can't upcast, return early and don't bother calculating available power slots + if (!consumePowerSlot) { + mergeObject(data, {isPower: true, consumePowerSlot}); + return; + } + + // Determine the levels which are feasible + let lmax = 0; + let points; + let powerType; + switch (itemData.school) { + case "lgt": + case "uni": + case "drk": { + powerType = "force"; + points = actorData.attributes.force.points.value + actorData.attributes.force.points.temp; + break; + } + case "tec": { + powerType = "tech"; + points = actorData.attributes.tech.points.value + actorData.attributes.tech.points.temp; + break; } - } - }, - default: "use", - close: () => resolve(null) - }); - dlg.render(true); - }); - } - - /* -------------------------------------------- */ - /* Helpers */ - /* -------------------------------------------- */ - - /** - * Get dialog data related to limited power slots - * @private - */ - static _getPowerData(actorData, itemData, data) { - - // Determine whether the power may be up-cast - const lvl = itemData.level; - const consumePowerSlot = (lvl > 0) && CONFIG.SW5E.powerUpcastModes.includes(itemData.preparation.mode); - - // If can't upcast, return early and don't bother calculating available power slots - if (!consumePowerSlot) { - mergeObject(data, { isPower: true, consumePowerSlot }); - return; - } - - // Determine the levels which are feasible - let lmax = 0; - let points; - let powerType; - switch (itemData.school){ - case "lgt": - case "uni": - case "drk": { - powerType = "force" - points = actorData.attributes.force.points.value + actorData.attributes.force.points.temp; - break; - } - case "tec": { - powerType = "tech" - points = actorData.attributes.tech.points.value + actorData.attributes.tech.points.temp; - break; - } - } - - // eliminate point usage for innate casters - if (actorData.attributes.powercasting === 'innate') points = 999; - - - let powerLevels - if (powerType === "force"){ - powerLevels = Array.fromRange(10).reduce((arr, i) => { - if ( i < lvl ) return arr; - const label = CONFIG.SW5E.powerLevels[i]; - const l = actorData.powers["power"+i] || {fmax: 0, foverride: null}; - let max = parseInt(l.foverride || l.fmax || 0); - let slots = Math.clamped(parseInt(l.fvalue || 0), 0, max); - if ( max > 0 ) lmax = i; - if ((max > 0) && (slots > 0) && (points > i)){ - arr.push({ - level: i, - label: i > 0 ? game.i18n.format('SW5E.PowerLevelSlot', {level: label, n: slots}) : label, - canCast: max > 0, - hasSlots: slots > 0 - }); } - return arr; - }, []).filter(sl => sl.level <= lmax); - }else if (powerType === "tech"){ - powerLevels = Array.fromRange(10).reduce((arr, i) => { - if ( i < lvl ) return arr; - const label = CONFIG.SW5E.powerLevels[i]; - const l = actorData.powers["power"+i] || {tmax: 0, toverride: null}; - let max = parseInt(l.override || l.tmax || 0); - let slots = Math.clamped(parseInt(l.tvalue || 0), 0, max); - if ( max > 0 ) lmax = i; - if ((max > 0) && (slots > 0) && (points > i)){ - arr.push({ - level: i, - label: i > 0 ? game.i18n.format('SW5E.PowerLevelSlot', {level: label, n: slots}) : label, - canCast: max > 0, - hasSlots: slots > 0 - }); + + // eliminate point usage for innate casters + if (actorData.attributes.powercasting === "innate") points = 999; + + let powerLevels; + if (powerType === "force") { + powerLevels = Array.fromRange(10) + .reduce((arr, i) => { + if (i < lvl) return arr; + const label = CONFIG.SW5E.powerLevels[i]; + const l = actorData.powers["power" + i] || {fmax: 0, foverride: null}; + let max = parseInt(l.foverride || l.fmax || 0); + let slots = Math.clamped(parseInt(l.fvalue || 0), 0, max); + if (max > 0) lmax = i; + if (max > 0 && slots > 0 && points > i) { + arr.push({ + level: i, + label: i > 0 ? game.i18n.format("SW5E.PowerLevelSlot", {level: label, n: slots}) : label, + canCast: max > 0, + hasSlots: slots > 0 + }); + } + return arr; + }, []) + .filter((sl) => sl.level <= lmax); + } else if (powerType === "tech") { + powerLevels = Array.fromRange(10) + .reduce((arr, i) => { + if (i < lvl) return arr; + const label = CONFIG.SW5E.powerLevels[i]; + const l = actorData.powers["power" + i] || {tmax: 0, toverride: null}; + let max = parseInt(l.override || l.tmax || 0); + let slots = Math.clamped(parseInt(l.tvalue || 0), 0, max); + if (max > 0) lmax = i; + if (max > 0 && slots > 0 && points > i) { + arr.push({ + level: i, + label: i > 0 ? game.i18n.format("SW5E.PowerLevelSlot", {level: label, n: slots}) : label, + canCast: max > 0, + hasSlots: slots > 0 + }); + } + return arr; + }, []) + .filter((sl) => sl.level <= lmax); } - return arr; - }, []).filter(sl => sl.level <= lmax); - } - - - const canCast = powerLevels.some(l => l.hasSlots); - if ( !canCast ) data.errors.push(game.i18n.format("SW5E.PowerCastNoSlots", { - level: CONFIG.SW5E.powerLevels[lvl], - name: data.item.name - })); + const canCast = powerLevels.some((l) => l.hasSlots); + if (!canCast) + data.errors.push( + game.i18n.format("SW5E.PowerCastNoSlots", { + level: CONFIG.SW5E.powerLevels[lvl], + name: data.item.name + }) + ); - // Merge power casting data - return foundry.utils.mergeObject(data, { isPower: true, consumePowerSlot, powerLevels }); - } - - /* -------------------------------------------- */ - - /** - * Get the ability usage note that is displayed - * @private - */ - static _getAbilityUseNote(item, uses, recharge) { - - // Zero quantity - const quantity = item.data.quantity; - if ( quantity <= 0 ) return game.i18n.localize("SW5E.AbilityUseUnavailableHint"); - - // Abilities which use Recharge - if ( !!recharge.value ) { - return game.i18n.format(recharge.charged ? "SW5E.AbilityUseChargedHint" : "SW5E.AbilityUseRechargeHint", { - type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`), - }) + // Merge power casting data + return foundry.utils.mergeObject(data, {isPower: true, consumePowerSlot, powerLevels}); } - // Does not use any resource - if ( !uses.per || !uses.max ) return ""; + /* -------------------------------------------- */ - // Consumables - if ( item.type === "consumable" ) { - let str = "SW5E.AbilityUseNormalHint"; - if ( uses.value > 1 ) str = "SW5E.AbilityUseConsumableChargeHint"; - else if ( item.data.quantity === 1 && uses.autoDestroy ) str = "SW5E.AbilityUseConsumableDestroyHint"; - else if ( item.data.quantity > 1 ) str = "SW5E.AbilityUseConsumableQuantityHint"; - return game.i18n.format(str, { - type: game.i18n.localize(`SW5E.Consumable${item.data.consumableType.capitalize()}`), - value: uses.value, - quantity: item.data.quantity, - max: uses.max, - per: CONFIG.SW5E.limitedUsePeriods[uses.per] - }); - } + /** + * Get the ability usage note that is displayed + * @private + */ + static _getAbilityUseNote(item, uses, recharge) { + // Zero quantity + const quantity = item.data.quantity; + if (quantity <= 0) return game.i18n.localize("SW5E.AbilityUseUnavailableHint"); - // Other Items - else { - return game.i18n.format("SW5E.AbilityUseNormalHint", { - type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`), - value: uses.value, - max: uses.max, - per: CONFIG.SW5E.limitedUsePeriods[uses.per] - }); + // Abilities which use Recharge + if (!!recharge.value) { + return game.i18n.format(recharge.charged ? "SW5E.AbilityUseChargedHint" : "SW5E.AbilityUseRechargeHint", { + type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`) + }); + } + + // Does not use any resource + if (!uses.per || !uses.max) return ""; + + // Consumables + if (item.type === "consumable") { + let str = "SW5E.AbilityUseNormalHint"; + if (uses.value > 1) str = "SW5E.AbilityUseConsumableChargeHint"; + else if (item.data.quantity === 1 && uses.autoDestroy) str = "SW5E.AbilityUseConsumableDestroyHint"; + else if (item.data.quantity > 1) str = "SW5E.AbilityUseConsumableQuantityHint"; + return game.i18n.format(str, { + type: game.i18n.localize(`SW5E.Consumable${item.data.consumableType.capitalize()}`), + value: uses.value, + quantity: item.data.quantity, + max: uses.max, + per: CONFIG.SW5E.limitedUsePeriods[uses.per] + }); + } + + // Other Items + else { + return game.i18n.format("SW5E.AbilityUseNormalHint", { + type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`), + value: uses.value, + max: uses.max, + per: CONFIG.SW5E.limitedUsePeriods[uses.per] + }); + } } - } } diff --git a/module/apps/actor-flags.js b/module/apps/actor-flags.js index cb3a816c..8f5255a5 100644 --- a/module/apps/actor-flags.js +++ b/module/apps/actor-flags.js @@ -3,135 +3,137 @@ * @implements {DocumentSheet} */ export default class ActorSheetFlags extends DocumentSheet { - static get defaultOptions() { - return foundry.utils.mergeObject(super.defaultOptions, { - id: "actor-flags", - classes: ["sw5e"], - template: "systems/sw5e/templates/apps/actor-flags.html", - width: 500, - closeOnSubmit: true - }); - } - - /* -------------------------------------------- */ - - /** @override */ - get title() { - return `${game.i18n.localize('SW5E.FlagsTitle')}: ${this.object.name}`; - } - - /* -------------------------------------------- */ - - /** @override */ - getData() { - const data = {}; - data.actor = this.object; - data.classes = this._getClasses(); - data.flags = this._getFlags(); - data.bonuses = this._getBonuses(); - return data; - } - - /* -------------------------------------------- */ - - /** - * Prepare an object of sorted classes. - * @return {object} - * @private - */ - _getClasses() { - const classes = this.object.items.filter(i => i.type === "class"); - return classes.sort((a, b) => a.name.localeCompare(b.name)).reduce((obj, i) => { - obj[i.id] = i.name; - return obj; - }, {}); - } - - /* -------------------------------------------- */ - - /** - * Prepare an object of flags data which groups flags by section - * Add some additional data for rendering - * @return {object} - * @private - */ - _getFlags() { - const flags = {}; - const baseData = this.document.toJSON(); - for ( let [k, v] of Object.entries(CONFIG.SW5E.characterFlags) ) { - if ( !flags.hasOwnProperty(v.section) ) flags[v.section] = {}; - let flag = foundry.utils.deepClone(v); - flag.type = v.type.name; - flag.isCheckbox = v.type === Boolean; - flag.isSelect = v.hasOwnProperty('choices'); - flag.value = getProperty(baseData.flags, `sw5e.${k}`); - flags[v.section][`flags.sw5e.${k}`] = flag; + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + id: "actor-flags", + classes: ["sw5e"], + template: "systems/sw5e/templates/apps/actor-flags.html", + width: 500, + closeOnSubmit: true + }); } - return flags; - } - /* -------------------------------------------- */ + /* -------------------------------------------- */ - /** - * Get the bonuses fields and their localization strings - * @return {Array} - * @private - */ - _getBonuses() { - const bonuses = [ - {name: "data.bonuses.mwak.attack", label: "SW5E.BonusMWAttack"}, - {name: "data.bonuses.mwak.damage", label: "SW5E.BonusMWDamage"}, - {name: "data.bonuses.rwak.attack", label: "SW5E.BonusRWAttack"}, - {name: "data.bonuses.rwak.damage", label: "SW5E.BonusRWDamage"}, - {name: "data.bonuses.mpak.attack", label: "SW5E.BonusMPAttack"}, - {name: "data.bonuses.mpak.damage", label: "SW5E.BonusMPDamage"}, - {name: "data.bonuses.rpak.attack", label: "SW5E.BonusRPAttack"}, - {name: "data.bonuses.rpak.damage", label: "SW5E.BonusRPDamage"}, - {name: "data.bonuses.abilities.check", label: "SW5E.BonusAbilityCheck"}, - {name: "data.bonuses.abilities.save", label: "SW5E.BonusAbilitySave"}, - {name: "data.bonuses.abilities.skill", label: "SW5E.BonusAbilitySkill"}, - {name: "data.bonuses.power.dc", label: "SW5E.BonusPowerDC"}, - {name: "data.bonuses.power.forceLightDC", label: "SW5E.BonusForceLightPowerDC"}, - {name: "data.bonuses.power.forceDarkDC", label: "SW5E.BonusForceDarkPowerDC"}, - {name: "data.bonuses.power.forceUnivDC", label: "SW5E.BonusForceUnivPowerDC"}, - {name: "data.bonuses.power.techDC", label: "SW5E.BonusTechPowerDC"} - ]; - for ( let b of bonuses ) { - b.value = getProperty(this.object._data, b.name) || ""; + /** @override */ + get title() { + return `${game.i18n.localize("SW5E.FlagsTitle")}: ${this.object.name}`; } - return bonuses; - } - /* -------------------------------------------- */ + /* -------------------------------------------- */ - /** @override */ - async _updateObject(event, formData) { - const actor = this.object; - let updateData = expandObject(formData); + /** @override */ + getData() { + const data = {}; + data.actor = this.object; + data.classes = this._getClasses(); + data.flags = this._getFlags(); + data.bonuses = this._getBonuses(); + return data; + } - // Unset any flags which are "false" - let unset = false; - const flags = updateData.flags.sw5e; - //clone flags to dnd5e for module compatability - updateData.flags.dnd5e = updateData.flags.sw5e - for ( let [k, v] of Object.entries(flags) ) { - if ( [undefined, null, "", false, 0].includes(v) ) { - delete flags[k]; - if ( hasProperty(actor._data.flags, `sw5e.${k}`) ) { - unset = true; - flags[`-=${k}`] = null; + /* -------------------------------------------- */ + + /** + * Prepare an object of sorted classes. + * @return {object} + * @private + */ + _getClasses() { + const classes = this.object.items.filter((i) => i.type === "class"); + return classes + .sort((a, b) => a.name.localeCompare(b.name)) + .reduce((obj, i) => { + obj[i.id] = i.name; + return obj; + }, {}); + } + + /* -------------------------------------------- */ + + /** + * Prepare an object of flags data which groups flags by section + * Add some additional data for rendering + * @return {object} + * @private + */ + _getFlags() { + const flags = {}; + const baseData = this.document.toJSON(); + for (let [k, v] of Object.entries(CONFIG.SW5E.characterFlags)) { + if (!flags.hasOwnProperty(v.section)) flags[v.section] = {}; + let flag = foundry.utils.deepClone(v); + flag.type = v.type.name; + flag.isCheckbox = v.type === Boolean; + flag.isSelect = v.hasOwnProperty("choices"); + flag.value = getProperty(baseData.flags, `sw5e.${k}`); + flags[v.section][`flags.sw5e.${k}`] = flag; } - } + return flags; } - // Clear any bonuses which are whitespace only - for ( let b of Object.values(updateData.data.bonuses ) ) { - for ( let [k, v] of Object.entries(b) ) { - b[k] = v.trim(); - } + /* -------------------------------------------- */ + + /** + * Get the bonuses fields and their localization strings + * @return {Array} + * @private + */ + _getBonuses() { + const bonuses = [ + {name: "data.bonuses.mwak.attack", label: "SW5E.BonusMWAttack"}, + {name: "data.bonuses.mwak.damage", label: "SW5E.BonusMWDamage"}, + {name: "data.bonuses.rwak.attack", label: "SW5E.BonusRWAttack"}, + {name: "data.bonuses.rwak.damage", label: "SW5E.BonusRWDamage"}, + {name: "data.bonuses.mpak.attack", label: "SW5E.BonusMPAttack"}, + {name: "data.bonuses.mpak.damage", label: "SW5E.BonusMPDamage"}, + {name: "data.bonuses.rpak.attack", label: "SW5E.BonusRPAttack"}, + {name: "data.bonuses.rpak.damage", label: "SW5E.BonusRPDamage"}, + {name: "data.bonuses.abilities.check", label: "SW5E.BonusAbilityCheck"}, + {name: "data.bonuses.abilities.save", label: "SW5E.BonusAbilitySave"}, + {name: "data.bonuses.abilities.skill", label: "SW5E.BonusAbilitySkill"}, + {name: "data.bonuses.power.dc", label: "SW5E.BonusPowerDC"}, + {name: "data.bonuses.power.forceLightDC", label: "SW5E.BonusForceLightPowerDC"}, + {name: "data.bonuses.power.forceDarkDC", label: "SW5E.BonusForceDarkPowerDC"}, + {name: "data.bonuses.power.forceUnivDC", label: "SW5E.BonusForceUnivPowerDC"}, + {name: "data.bonuses.power.techDC", label: "SW5E.BonusTechPowerDC"} + ]; + for (let b of bonuses) { + b.value = getProperty(this.object._data, b.name) || ""; + } + return bonuses; } - // Diff the data against any applied overrides and apply - await actor.update(updateData, {diff: false}); - } + /* -------------------------------------------- */ + + /** @override */ + async _updateObject(event, formData) { + const actor = this.object; + let updateData = expandObject(formData); + + // Unset any flags which are "false" + let unset = false; + const flags = updateData.flags.sw5e; + //clone flags to dnd5e for module compatability + updateData.flags.dnd5e = updateData.flags.sw5e; + for (let [k, v] of Object.entries(flags)) { + if ([undefined, null, "", false, 0].includes(v)) { + delete flags[k]; + if (hasProperty(actor._data.flags, `sw5e.${k}`)) { + unset = true; + flags[`-=${k}`] = null; + } + } + } + + // Clear any bonuses which are whitespace only + for (let b of Object.values(updateData.data.bonuses)) { + for (let [k, v] of Object.entries(b)) { + b[k] = v.trim(); + } + } + + // Diff the data against any applied overrides and apply + await actor.update(updateData, {diff: false}); + } } diff --git a/module/apps/actor-type.js b/module/apps/actor-type.js index ad56e72b..7ad1ffe9 100644 --- a/module/apps/actor-type.js +++ b/module/apps/actor-type.js @@ -5,7 +5,6 @@ import Actor5e from "../actor/entity.js"; * @extends {FormApplication} */ export default class ActorTypeConfig extends FormApplication { - /** @inheritdoc */ static get defaultOptions() { return foundry.utils.mergeObject(super.defaultOptions, { @@ -32,23 +31,23 @@ export default class ActorTypeConfig extends FormApplication { /** @override */ getData(options) { - // Get current value or new default - let attr = foundry.utils.getProperty(this.object.data.data, 'details.type'); - if ( foundry.utils.getType(attr) !== "Object" ) attr = { - value: (attr in CONFIG.SW5E.creatureTypes) ? attr : "humanoid", - subtype: "", - swarm: "", - custom: "" - }; + let attr = foundry.utils.getProperty(this.object.data.data, "details.type"); + if (foundry.utils.getType(attr) !== "Object") + attr = { + value: attr in CONFIG.SW5E.creatureTypes ? attr : "humanoid", + subtype: "", + swarm: "", + custom: "" + }; // Populate choices const types = {}; - for ( let [k, v] of Object.entries(CONFIG.SW5E.creatureTypes) ) { + for (let [k, v] of Object.entries(CONFIG.SW5E.creatureTypes)) { types[k] = { label: game.i18n.localize(v), chosen: attr.value === k - } + }; } // Return data for rendering @@ -61,12 +60,14 @@ export default class ActorTypeConfig extends FormApplication { }, subtype: attr.subtype, swarm: attr.swarm, - sizes: Array.from(Object.entries(CONFIG.SW5E.actorSizes)).reverse().reduce((obj, e) => { - obj[e[0]] = e[1]; - return obj; - }, {}), + sizes: Array.from(Object.entries(CONFIG.SW5E.actorSizes)) + .reverse() + .reduce((obj, e) => { + obj[e[0]] = e[1]; + return obj; + }, {}), preview: Actor5e.formatCreatureType(attr) || "–" - } + }; } /* -------------------------------------------- */ @@ -74,7 +75,7 @@ export default class ActorTypeConfig extends FormApplication { /** @override */ async _updateObject(event, formData) { const typeObject = foundry.utils.expandObject(formData); - return this.object.update({ 'data.details.type': typeObject }); + return this.object.update({"data.details.type": typeObject}); } /* -------------------------------------------- */ diff --git a/module/apps/hit-dice-config.js b/module/apps/hit-dice-config.js index d36d6bc2..f4fdf276 100644 --- a/module/apps/hit-dice-config.js +++ b/module/apps/hit-dice-config.js @@ -3,7 +3,6 @@ * @implements {DocumentSheet} */ export default class ActorHitDiceConfig extends DocumentSheet { - /** @override */ static get defaultOptions() { return foundry.utils.mergeObject(super.defaultOptions, { @@ -26,20 +25,22 @@ export default class ActorHitDiceConfig extends DocumentSheet { /** @override */ getData(options) { return { - classes: this.object.items.reduce((classes, item) => { - if (item.data.type === "class") { - // Add the appropriate data only if this item is a "class" - classes.push({ - classItemId: item.data._id, - name: item.data.name, - diceDenom: item.data.data.hitDice, - currentHitDice: item.data.data.levels - item.data.data.hitDiceUsed, - maxHitDice: item.data.data.levels, - canRoll: (item.data.data.levels - item.data.data.hitDiceUsed) > 0 - }); - } - return classes; - }, []).sort((a, b) => parseInt(b.diceDenom.slice(1)) - parseInt(a.diceDenom.slice(1))) + classes: this.object.items + .reduce((classes, item) => { + if (item.data.type === "class") { + // Add the appropriate data only if this item is a "class" + classes.push({ + classItemId: item.data._id, + name: item.data.name, + diceDenom: item.data.data.hitDice, + currentHitDice: item.data.data.levels - item.data.data.hitDiceUsed, + maxHitDice: item.data.data.levels, + canRoll: item.data.data.levels - item.data.data.hitDiceUsed > 0 + }); + } + return classes; + }, []) + .sort((a, b) => parseInt(b.diceDenom.slice(1)) - parseInt(a.diceDenom.slice(1))) }; } @@ -50,7 +51,7 @@ export default class ActorHitDiceConfig extends DocumentSheet { super.activateListeners(html); // Hook up -/+ buttons to adjust the current value in the form - html.find("button.increment,button.decrement").click(event => { + html.find("button.increment,button.decrement").click((event) => { const button = event.currentTarget; const current = button.parentElement.querySelector(".current"); const max = button.parentElement.querySelector(".max"); @@ -67,8 +68,8 @@ export default class ActorHitDiceConfig extends DocumentSheet { async _updateObject(event, formData) { const actorItems = this.object.items; const classUpdates = Object.entries(formData).map(([id, hd]) => ({ - _id: id, - "data.hitDiceUsed": actorItems.get(id).data.data.levels - hd, + "_id": id, + "data.hitDiceUsed": actorItems.get(id).data.data.levels - hd })); return this.object.updateEmbeddedDocuments("Item", classUpdates); } diff --git a/module/apps/long-rest.js b/module/apps/long-rest.js index 3e57cea7..a34e53c7 100644 --- a/module/apps/long-rest.js +++ b/module/apps/long-rest.js @@ -3,65 +3,65 @@ * @extends {Dialog} */ export default class LongRestDialog extends Dialog { - constructor(actor, dialogData = {}, options = {}) { - super(dialogData, options); - this.actor = actor; - } + constructor(actor, dialogData = {}, options = {}) { + super(dialogData, options); + this.actor = actor; + } - /* -------------------------------------------- */ + /* -------------------------------------------- */ - /** @override */ - static get defaultOptions() { - return mergeObject(super.defaultOptions, { - template: "systems/sw5e/templates/apps/long-rest.html", - classes: ["sw5e", "dialog"] - }); - } + /** @override */ + static get defaultOptions() { + return mergeObject(super.defaultOptions, { + template: "systems/sw5e/templates/apps/long-rest.html", + classes: ["sw5e", "dialog"] + }); + } - /* -------------------------------------------- */ + /* -------------------------------------------- */ - /** @override */ - getData() { - const data = super.getData(); - const variant = game.settings.get("sw5e", "restVariant"); - data.promptNewDay = variant !== "gritty"; // It's always a new day when resting 1 week - data.newDay = variant === "normal"; // It's probably a new day when resting normally (8 hours) - return data; - } + /** @override */ + getData() { + const data = super.getData(); + const variant = game.settings.get("sw5e", "restVariant"); + data.promptNewDay = variant !== "gritty"; // It's always a new day when resting 1 week + data.newDay = variant === "normal"; // It's probably a new day when resting normally (8 hours) + return data; + } - /* -------------------------------------------- */ + /* -------------------------------------------- */ - /** - * A helper constructor function which displays the Long Rest confirmation dialog and returns a Promise once it's - * workflow has been resolved. - * @param {Actor5e} actor - * @return {Promise} - */ - static async longRestDialog({ actor } = {}) { - return new Promise((resolve, reject) => { - const dlg = new this(actor, { - title: game.i18n.localize("SW5E.LongRest"), - buttons: { - rest: { - icon: '', - label: game.i18n.localize("SW5E.Rest"), - callback: html => { - let newDay = true; - if (game.settings.get("sw5e", "restVariant") !== "gritty") - newDay = html.find('input[name="newDay"]')[0].checked; - resolve(newDay); - } - }, - cancel: { - icon: '', - label: game.i18n.localize("Cancel"), - callback: reject - } - }, - default: 'rest', - close: reject - }); - dlg.render(true); - }); - } -} \ No newline at end of file + /** + * A helper constructor function which displays the Long Rest confirmation dialog and returns a Promise once it's + * workflow has been resolved. + * @param {Actor5e} actor + * @return {Promise} + */ + static async longRestDialog({actor} = {}) { + return new Promise((resolve, reject) => { + const dlg = new this(actor, { + title: game.i18n.localize("SW5E.LongRest"), + buttons: { + rest: { + icon: '', + label: game.i18n.localize("SW5E.Rest"), + callback: (html) => { + let newDay = true; + if (game.settings.get("sw5e", "restVariant") !== "gritty") + newDay = html.find('input[name="newDay"]')[0].checked; + resolve(newDay); + } + }, + cancel: { + icon: '', + label: game.i18n.localize("Cancel"), + callback: reject + } + }, + default: "rest", + close: reject + }); + dlg.render(true); + }); + } +} diff --git a/module/apps/movement-config.js b/module/apps/movement-config.js index c507965d..3c43170e 100644 --- a/module/apps/movement-config.js +++ b/module/apps/movement-config.js @@ -3,37 +3,36 @@ * @extends {DocumentSheet} */ export default class ActorMovementConfig extends DocumentSheet { - - /** @override */ - static get defaultOptions() { - return foundry.utils.mergeObject(super.defaultOptions, { - classes: ["sw5e"], - template: "systems/sw5e/templates/apps/movement-config.html", - width: 300, - height: "auto" - }); - } - - /* -------------------------------------------- */ - - /** @override */ - get title() { - return `${game.i18n.localize("SW5E.MovementConfig")}: ${this.document.name}`; - } - - /* -------------------------------------------- */ - - /** @override */ - getData(options) { - const sourceMovement = foundry.utils.getProperty(this.document.data._source, "data.attributes.movement") || {}; - const data = { - movement: foundry.utils.deepClone(sourceMovement), - units: CONFIG.SW5E.movementUnits - }; - for ( let [k, v] of Object.entries(data.movement) ) { - if ( ["units", "hover"].includes(k) ) continue; - data.movement[k] = Number.isNumeric(v) ? v.toNearest(0.1) : 0; + /** @override */ + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + classes: ["sw5e"], + template: "systems/sw5e/templates/apps/movement-config.html", + width: 300, + height: "auto" + }); + } + + /* -------------------------------------------- */ + + /** @override */ + get title() { + return `${game.i18n.localize("SW5E.MovementConfig")}: ${this.document.name}`; + } + + /* -------------------------------------------- */ + + /** @override */ + getData(options) { + const sourceMovement = foundry.utils.getProperty(this.document.data._source, "data.attributes.movement") || {}; + const data = { + movement: foundry.utils.deepClone(sourceMovement), + units: CONFIG.SW5E.movementUnits + }; + for (let [k, v] of Object.entries(data.movement)) { + if (["units", "hover"].includes(k)) continue; + data.movement[k] = Number.isNumeric(v) ? v.toNearest(0.1) : 0; + } + return data; } - return data; - } } diff --git a/module/apps/recharge-rest.js b/module/apps/recharge-rest.js deleted file mode 100644 index 1d61c7a4..00000000 --- a/module/apps/recharge-rest.js +++ /dev/null @@ -1,117 +0,0 @@ -/** - * A helper Dialog subclass for rolling Hit Dice on a recharge rest - * @extends {Dialog} - */ -export default class RechargeRestDialog extends Dialog { - constructor(actor, dialogData={}, options={}) { - super(dialogData, options); - - /** - * Store a reference to the Actor entity which is resting - * @type {Actor} - */ - this.actor = actor; - - /** - * Track the most recently used HD denomination for re-rendering the form - * @type {string} - */ - this._denom = null; - } - - /* -------------------------------------------- */ - - /** @override */ - static get defaultOptions() { - return mergeObject(super.defaultOptions, { - template: "systems/sw5e/templates/apps/recharge-rest.html", - classes: ["sw5e", "dialog"] - }); - } - - /* -------------------------------------------- */ - - /** @override */ - getData() { - const data = super.getData(); - - // Determine Hull Dice - data.availableHD = this.actor.data.items.reduce((hd, item) => { - if ( item.type === "starship" ) { - const d = item.data; - const denom = d.hullDice || "d6"; - const available = parseInt(d.hullDiceStart || 1) + parseInt(d.tier || 0) - parseInt(d.hullDiceUsed || 0); - hd[denom] = denom in hd ? hd[denom] + available : available; - } - return hd; - }, {}); - data.canRoll = this.actor.data.data.attributes.hull.dice > 0; - data.denomination = this._denom; - - // Determine rest type - const variant = game.settings.get("sw5e", "restVariant"); - data.promptNewDay = variant !== "epic"; // It's never a new day when only resting 1 minute - data.newDay = false; // It may be a new day, but not by default - return data; - } - - /* -------------------------------------------- */ - - - /** @override */ - activateListeners(html) { - super.activateListeners(html); - let btn = html.find("#roll-hulld"); - btn.click(this._onRollHullDie.bind(this)); - } - - /* -------------------------------------------- */ - - /** - * Handle rolling a Hull Die as part of a Recharge Rest action - * @param {Event} event The triggering click event - * @private - */ - async _onRollHullDie(event) { - event.preventDefault(); - const btn = event.currentTarget; - this._denom = btn.form.hulld.value; - await this.actor.rollHullDie(this._denom); - this.render(); - } - - /* -------------------------------------------- */ - - /** - * A helper constructor function which displays the Short Rest dialog and returns a Promise once it's workflow has - * been resolved. - * @param {Actor5e} actor - * @return {Promise} - */ - static async rechargeRestDialog({actor}={}) { - return new Promise((resolve, reject) => { - const dlg = new this(actor, { - title: "Recharge Rest", - buttons: { - rest: { - icon: '', - label: "Rest", - callback: html => { - let newDay = false; - if (game.settings.get("sw5e", "restVariant") === "gritty") - newDay = html.find('input[name="newDay"]')[0].checked; - resolve(newDay); - } - }, - cancel: { - icon: '', - label: "Cancel", - callback: reject - } - }, - close: reject - }); - dlg.render(true); - }); - } -} diff --git a/module/apps/refitting-rest.js b/module/apps/refitting-rest.js deleted file mode 100644 index 090fa693..00000000 --- a/module/apps/refitting-rest.js +++ /dev/null @@ -1,69 +0,0 @@ -/** - * A helper Dialog subclass for completing a refitting rest - * @extends {Dialog} - */ -export default class RefittingRestDialog extends Dialog { - constructor(actor, dialogData = {}, options = {}) { - super(dialogData, options); - this.actor = actor; - } - - /* -------------------------------------------- */ - - /** @override */ - static get defaultOptions() { - return mergeObject(super.defaultOptions, { - template: "systems/sw5e/templates/apps/refitting-rest.html", - classes: ["sw5e", "dialog"] - }); - } - - /* -------------------------------------------- */ - - /** @override */ - getData() { - const data = super.getData(); - const variant = game.settings.get("sw5e", "restVariant"); - data.promptNewDay = variant !== "gritty"; // It's always a new day when resting 1 week - data.newDay = variant === "normal"; // It's probably a new day when resting normally (8 hours) - return data; - } - - /* -------------------------------------------- */ - - /** - * A helper constructor function which displays the Refitting Rest confirmation dialog and returns a Promise once it's - * workflow has been resolved. - * @param {Actor5e} actor - * @return {Promise} - */ - static async refittingRestDialog({ actor } = {}) { - return new Promise((resolve, reject) => { - const dlg = new this(actor, { - title: "Refitting Rest", - buttons: { - rest: { - icon: '', - label: "Rest", - callback: html => { - let newDay = false; - if (game.settings.get("sw5e", "restVariant") === "normal") - newDay = html.find('input[name="newDay"]')[0].checked; - else if(game.settings.get("sw5e", "restVariant") === "gritty") - newDay = true; - resolve(newDay); - } - }, - cancel: { - icon: '', - label: "Cancel", - callback: reject - } - }, - default: 'rest', - close: reject - }); - dlg.render(true); - }); - } -} \ No newline at end of file diff --git a/module/apps/select-items-prompt.js b/module/apps/select-items-prompt.js index 0eac6497..cd99a278 100644 --- a/module/apps/select-items-prompt.js +++ b/module/apps/select-items-prompt.js @@ -3,7 +3,7 @@ * @type {Dialog} */ export default class SelectItemsPrompt extends Dialog { - constructor(items, dialogData={}, options={}) { + constructor(items, dialogData = {}, options = {}) { super(dialogData, options); this.options.classes = ["sw5e", "dialog", "select-items-prompt", "sheet"]; @@ -18,11 +18,11 @@ export default class SelectItemsPrompt extends Dialog { super.activateListeners(html); // render the item's sheet if its image is clicked - html.on('click', '.item-image', (event) => { + html.on("click", ".item-image", (event) => { const item = this.items.find((feature) => feature.id === event.currentTarget.dataset?.itemId); item?.sheet.render(true); - }) + }); } /** @@ -33,29 +33,27 @@ export default class SelectItemsPrompt extends Dialog { * @param {string} options.hint - Localized hint to display at the top of the prompt * @return {Promise} - list of item ids which the user has selected */ - static async create(items, { - hint - }) { + static async create(items, {hint}) { // Render the ability usage template const html = await renderTemplate("systems/sw5e/templates/apps/select-items-prompt.html", {items, hint}); return new Promise((resolve) => { const dlg = new this(items, { - title: game.i18n.localize('SW5E.SelectItemsPromptTitle'), + title: game.i18n.localize("SW5E.SelectItemsPromptTitle"), content: html, buttons: { apply: { icon: ``, - label: game.i18n.localize('SW5E.Apply'), - callback: html => { + label: game.i18n.localize("SW5E.Apply"), + callback: (html) => { const fd = new FormDataExtended(html[0].querySelector("form")).toObject(); - const selectedIds = Object.keys(fd).filter(itemId => fd[itemId]); + const selectedIds = Object.keys(fd).filter((itemId) => fd[itemId]); resolve(selectedIds); } }, cancel: { icon: '', - label: game.i18n.localize('SW5E.Skip'), + label: game.i18n.localize("SW5E.Skip"), callback: () => resolve([]) } }, diff --git a/module/apps/senses-config.js b/module/apps/senses-config.js index 707ca7fb..e12e5478 100644 --- a/module/apps/senses-config.js +++ b/module/apps/senses-config.js @@ -3,41 +3,41 @@ * @extends {DocumentSheet} */ export default class ActorSensesConfig extends DocumentSheet { - - /** @inheritdoc */ - static get defaultOptions() { - return foundry.utils.mergeObject(super.defaultOptions, { - classes: ["sw5e"], - template: "systems/sw5e/templates/apps/senses-config.html", - width: 300, - height: "auto" - }); - } - - /* -------------------------------------------- */ - - /** @inheritdoc */ - get title() { - return `${game.i18n.localize("SW5E.SensesConfig")}: ${this.document.name}`; - } - - /* -------------------------------------------- */ - - /** @inheritdoc */ - getData(options) { - const senses = foundry.utils.getProperty(this.document.data._source, "data.attributes.senses") || {}; - const data = { - senses: {}, - special: senses.special ?? "", - units: senses.units, movementUnits: CONFIG.SW5E.movementUnits - }; - for ( let [name, label] of Object.entries(CONFIG.SW5E.senses) ) { - const v = senses[name]; - data.senses[name] = { - label: game.i18n.localize(label), - value: Number.isNumeric(v) ? v.toNearest(0.1) : 0 - } + /** @inheritdoc */ + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + classes: ["sw5e"], + template: "systems/sw5e/templates/apps/senses-config.html", + width: 300, + height: "auto" + }); + } + + /* -------------------------------------------- */ + + /** @inheritdoc */ + get title() { + return `${game.i18n.localize("SW5E.SensesConfig")}: ${this.document.name}`; + } + + /* -------------------------------------------- */ + + /** @inheritdoc */ + getData(options) { + const senses = foundry.utils.getProperty(this.document.data._source, "data.attributes.senses") || {}; + const data = { + senses: {}, + special: senses.special ?? "", + units: senses.units, + movementUnits: CONFIG.SW5E.movementUnits + }; + for (let [name, label] of Object.entries(CONFIG.SW5E.senses)) { + const v = senses[name]; + data.senses[name] = { + label: game.i18n.localize(label), + value: Number.isNumeric(v) ? v.toNearest(0.1) : 0 + }; + } + return data; } - return data; - } } diff --git a/module/apps/short-rest.js b/module/apps/short-rest.js index 22a186ab..95e7c68a 100644 --- a/module/apps/short-rest.js +++ b/module/apps/short-rest.js @@ -5,129 +5,130 @@ import LongRestDialog from "./long-rest.js"; * @extends {Dialog} */ export default class ShortRestDialog extends Dialog { - constructor(actor, dialogData={}, options={}) { - super(dialogData, options); + constructor(actor, dialogData = {}, options = {}) { + super(dialogData, options); - /** - * Store a reference to the Actor entity which is resting - * @type {Actor} - */ - this.actor = actor; + /** + * Store a reference to the Actor entity which is resting + * @type {Actor} + */ + this.actor = actor; - /** - * Track the most recently used HD denomination for re-rendering the form - * @type {string} - */ - this._denom = null; - } + /** + * Track the most recently used HD denomination for re-rendering the form + * @type {string} + */ + this._denom = null; + } - /* -------------------------------------------- */ + /* -------------------------------------------- */ - /** @override */ - static get defaultOptions() { - return mergeObject(super.defaultOptions, { - template: "systems/sw5e/templates/apps/short-rest.html", - classes: ["sw5e", "dialog"] - }); - } + /** @override */ + static get defaultOptions() { + return mergeObject(super.defaultOptions, { + template: "systems/sw5e/templates/apps/short-rest.html", + classes: ["sw5e", "dialog"] + }); + } - /* -------------------------------------------- */ + /* -------------------------------------------- */ - /** @override */ - getData() { - const data = super.getData(); + /** @override */ + getData() { + const data = super.getData(); - // Determine Hit Dice - data.availableHD = this.actor.data.items.reduce((hd, item) => { - if ( item.type === "class" ) { - const d = item.data.data; - const denom = d.hitDice || "d6"; - const available = parseInt(d.levels || 1) - parseInt(d.hitDiceUsed || 0); - hd[denom] = denom in hd ? hd[denom] + available : available; - } - return hd; - }, {}); - data.canRoll = this.actor.data.data.attributes.hd > 0; - data.denomination = this._denom; - - // Determine rest type - const variant = game.settings.get("sw5e", "restVariant"); - data.promptNewDay = variant !== "epic"; // It's never a new day when only resting 1 minute - data.newDay = false; // It may be a new day, but not by default - return data; - } - - /* -------------------------------------------- */ - - - /** @override */ - activateListeners(html) { - super.activateListeners(html); - let btn = html.find("#roll-hd"); - btn.click(this._onRollHitDie.bind(this)); - } - - /* -------------------------------------------- */ - - /** - * Handle rolling a Hit Die as part of a Short Rest action - * @param {Event} event The triggering click event - * @private - */ - async _onRollHitDie(event) { - event.preventDefault(); - const btn = event.currentTarget; - this._denom = btn.form.hd.value; - await this.actor.rollHitDie(this._denom); - this.render(); - } - - /* -------------------------------------------- */ - - /** - * A helper constructor function which displays the Short Rest dialog and returns a Promise once it's workflow has - * been resolved. - * @param {Actor5e} actor - * @return {Promise} - */ - static async shortRestDialog({actor}={}) { - return new Promise((resolve, reject) => { - const dlg = new this(actor, { - title: game.i18n.localize("SW5E.ShortRest"), - buttons: { - rest: { - icon: '', - label: game.i18n.localize("SW5E.Rest"), - callback: html => { - let newDay = false; - if (game.settings.get("sw5e", "restVariant") === "gritty") - newDay = html.find('input[name="newDay"]')[0].checked; - resolve(newDay); + // Determine Hit Dice + data.availableHD = this.actor.data.items.reduce((hd, item) => { + if (item.type === "class") { + const d = item.data.data; + const denom = d.hitDice || "d6"; + const available = parseInt(d.levels || 1) - parseInt(d.hitDiceUsed || 0); + hd[denom] = denom in hd ? hd[denom] + available : available; } - }, - cancel: { - icon: '', - label: game.i18n.localize("Cancel"), - callback: reject - } - }, - close: reject - }); - dlg.render(true); - }); - } + return hd; + }, {}); + data.canRoll = this.actor.data.data.attributes.hd > 0; + data.denomination = this._denom; - /* -------------------------------------------- */ + // Determine rest type + const variant = game.settings.get("sw5e", "restVariant"); + data.promptNewDay = variant !== "epic"; // It's never a new day when only resting 1 minute + data.newDay = false; // It may be a new day, but not by default + return data; + } - /** - * A helper constructor function which displays the Long Rest confirmation dialog and returns a Promise once it's - * workflow has been resolved. - * @deprecated - * @param {Actor5e} actor - * @return {Promise} - */ - static async longRestDialog({actor}={}) { - console.warn("WARNING! ShortRestDialog.longRestDialog has been deprecated, use LongRestDialog.longRestDialog instead."); - return LongRestDialog.longRestDialog(...arguments); - } + /* -------------------------------------------- */ + + /** @override */ + activateListeners(html) { + super.activateListeners(html); + let btn = html.find("#roll-hd"); + btn.click(this._onRollHitDie.bind(this)); + } + + /* -------------------------------------------- */ + + /** + * Handle rolling a Hit Die as part of a Short Rest action + * @param {Event} event The triggering click event + * @private + */ + async _onRollHitDie(event) { + event.preventDefault(); + const btn = event.currentTarget; + this._denom = btn.form.hd.value; + await this.actor.rollHitDie(this._denom); + this.render(); + } + + /* -------------------------------------------- */ + + /** + * A helper constructor function which displays the Short Rest dialog and returns a Promise once it's workflow has + * been resolved. + * @param {Actor5e} actor + * @return {Promise} + */ + static async shortRestDialog({actor} = {}) { + return new Promise((resolve, reject) => { + const dlg = new this(actor, { + title: game.i18n.localize("SW5E.ShortRest"), + buttons: { + rest: { + icon: '', + label: game.i18n.localize("SW5E.Rest"), + callback: (html) => { + let newDay = false; + if (game.settings.get("sw5e", "restVariant") === "gritty") + newDay = html.find('input[name="newDay"]')[0].checked; + resolve(newDay); + } + }, + cancel: { + icon: '', + label: game.i18n.localize("Cancel"), + callback: reject + } + }, + close: reject + }); + dlg.render(true); + }); + } + + /* -------------------------------------------- */ + + /** + * A helper constructor function which displays the Long Rest confirmation dialog and returns a Promise once it's + * workflow has been resolved. + * @deprecated + * @param {Actor5e} actor + * @return {Promise} + */ + static async longRestDialog({actor} = {}) { + console.warn( + "WARNING! ShortRestDialog.longRestDialog has been deprecated, use LongRestDialog.longRestDialog instead." + ); + return LongRestDialog.longRestDialog(...arguments); + } } diff --git a/module/apps/trait-selector.js b/module/apps/trait-selector.js index ef3c82c1..6c454cf5 100644 --- a/module/apps/trait-selector.js +++ b/module/apps/trait-selector.js @@ -3,86 +3,85 @@ * @extends {DocumentSheet} */ export default class TraitSelector extends DocumentSheet { - - /** @inheritdoc */ - static get defaultOptions() { - return foundry.utils.mergeObject(super.defaultOptions, { - id: "trait-selector", - classes: ["sw5e", "trait-selector", "subconfig"], - title: "Actor Trait Selection", - template: "systems/sw5e/templates/apps/trait-selector.html", - width: 320, - height: "auto", - choices: {}, - allowCustom: true, - minimum: 0, - maximum: null, - valueKey: "value", - customKey: "custom" - }); - } - - /* -------------------------------------------- */ - - /** - * Return a reference to the target attribute - * @type {string} - */ - get attribute() { - return this.options.name; - } - - /* -------------------------------------------- */ - - /** @override */ - getData() { - const attr = foundry.utils.getProperty(this.object.data, this.attribute); - const o = this.options; - const value = (o.valueKey) ? attr[o.valueKey] ?? [] : attr; - const custom = (o.customKey) ? attr[o.customKey] ?? "" : ""; - - // Populate choices - const choices = Object.entries(o.choices).reduce((obj, e) => { - let [k, v] = e; - obj[k] = { label: v, chosen: attr ? value.includes(k) : false }; - return obj; - }, {}) - - // Return data - return { - allowCustom: o.allowCustom, - choices: choices, - custom: custom - } - } - - /* -------------------------------------------- */ - - /** @override */ - async _updateObject(event, formData) { - const o = this.options; - - // Obtain choices - const chosen = []; - for ( let [k, v] of Object.entries(formData) ) { - if ( (k !== "custom") && v ) chosen.push(k); + /** @inheritdoc */ + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + id: "trait-selector", + classes: ["sw5e", "trait-selector", "subconfig"], + title: "Actor Trait Selection", + template: "systems/sw5e/templates/apps/trait-selector.html", + width: 320, + height: "auto", + choices: {}, + allowCustom: true, + minimum: 0, + maximum: null, + valueKey: "value", + customKey: "custom" + }); } - // Object including custom data - const updateData = {}; - if ( o.valueKey ) updateData[`${this.attribute}.${o.valueKey}`] = chosen; - else updateData[this.attribute] = chosen; - if ( o.allowCustom ) updateData[`${this.attribute}.${o.customKey}`] = formData.custom; + /* -------------------------------------------- */ - // Validate the number chosen - if ( o.minimum && (chosen.length < o.minimum) ) { - return ui.notifications.error(`You must choose at least ${o.minimum} options`); - } - if ( o.maximum && (chosen.length > o.maximum) ) { - return ui.notifications.error(`You may choose no more than ${o.maximum} options`); + /** + * Return a reference to the target attribute + * @type {string} + */ + get attribute() { + return this.options.name; } - // Update the object - this.object.update(updateData); - } + /* -------------------------------------------- */ + + /** @override */ + getData() { + const attr = foundry.utils.getProperty(this.object.data, this.attribute); + const o = this.options; + const value = o.valueKey ? attr[o.valueKey] ?? [] : attr; + const custom = o.customKey ? attr[o.customKey] ?? "" : ""; + + // Populate choices + const choices = Object.entries(o.choices).reduce((obj, e) => { + let [k, v] = e; + obj[k] = {label: v, chosen: attr ? value.includes(k) : false}; + return obj; + }, {}); + + // Return data + return { + allowCustom: o.allowCustom, + choices: choices, + custom: custom + }; + } + + /* -------------------------------------------- */ + + /** @override */ + async _updateObject(event, formData) { + const o = this.options; + + // Obtain choices + const chosen = []; + for (let [k, v] of Object.entries(formData)) { + if (k !== "custom" && v) chosen.push(k); + } + + // Object including custom data + const updateData = {}; + if (o.valueKey) updateData[`${this.attribute}.${o.valueKey}`] = chosen; + else updateData[this.attribute] = chosen; + if (o.allowCustom) updateData[`${this.attribute}.${o.customKey}`] = formData.custom; + + // Validate the number chosen + if (o.minimum && chosen.length < o.minimum) { + return ui.notifications.error(`You must choose at least ${o.minimum} options`); + } + if (o.maximum && chosen.length > o.maximum) { + return ui.notifications.error(`You may choose no more than ${o.maximum} options`); + } + + // Update the object + this.object.update(updateData); + } } diff --git a/module/canvas.js b/module/canvas.js index 622e2a30..72c60c81 100644 --- a/module/canvas.js +++ b/module/canvas.js @@ -1,38 +1,38 @@ /** @override */ -export const measureDistances = function(segments, options={}) { - if ( !options.gridSpaces ) return BaseGrid.prototype.measureDistances.call(this, segments, options); +export const measureDistances = function (segments, options = {}) { + if (!options.gridSpaces) return BaseGrid.prototype.measureDistances.call(this, segments, options); - // Track the total number of diagonals - let nDiagonal = 0; - const rule = this.parent.diagonalRule; - const d = canvas.dimensions; + // Track the total number of diagonals + let nDiagonal = 0; + const rule = this.parent.diagonalRule; + const d = canvas.dimensions; - // Iterate over measured segments - return segments.map(s => { - let r = s.ray; + // Iterate over measured segments + return segments.map((s) => { + let r = s.ray; - // Determine the total distance traveled - let nx = Math.abs(Math.ceil(r.dx / d.size)); - let ny = Math.abs(Math.ceil(r.dy / d.size)); + // Determine the total distance traveled + let nx = Math.abs(Math.ceil(r.dx / d.size)); + let ny = Math.abs(Math.ceil(r.dy / d.size)); - // Determine the number of straight and diagonal moves - let nd = Math.min(nx, ny); - let ns = Math.abs(ny - nx); - nDiagonal += nd; + // Determine the number of straight and diagonal moves + let nd = Math.min(nx, ny); + let ns = Math.abs(ny - nx); + nDiagonal += nd; - // Alternative DMG Movement - if (rule === "5105") { - let nd10 = Math.floor(nDiagonal / 2) - Math.floor((nDiagonal - nd) / 2); - let spaces = (nd10 * 2) + (nd - nd10) + ns; - return spaces * canvas.dimensions.distance; - } + // Alternative DMG Movement + if (rule === "5105") { + let nd10 = Math.floor(nDiagonal / 2) - Math.floor((nDiagonal - nd) / 2); + let spaces = nd10 * 2 + (nd - nd10) + ns; + return spaces * canvas.dimensions.distance; + } - // Euclidean Measurement - else if (rule === "EUCL") { - return Math.round(Math.hypot(nx, ny) * canvas.scene.data.gridDistance); - } + // Euclidean Measurement + else if (rule === "EUCL") { + return Math.round(Math.hypot(nx, ny) * canvas.scene.data.gridDistance); + } - // Standard PHB Movement - else return (ns + nd) * canvas.scene.data.gridDistance; - }); -}; \ No newline at end of file + // Standard PHB Movement + else return (ns + nd) * canvas.scene.data.gridDistance; + }); +}; diff --git a/module/characterImporter.js b/module/characterImporter.js index 8c47aa20..178be216 100644 --- a/module/characterImporter.js +++ b/module/characterImporter.js @@ -1,51 +1,51 @@ export default class CharacterImporter { - // transform JSON from sw5e.com to Foundry friendly format - // and insert new actor - static async transform(rawCharacter) { - const sourceCharacter = JSON.parse(rawCharacter); //source character + // transform JSON from sw5e.com to Foundry friendly format + // and insert new actor + static async transform(rawCharacter) { + const sourceCharacter = JSON.parse(rawCharacter); //source character - const details = { - species: sourceCharacter.attribs.find((e) => e.name == "race").current, - background: sourceCharacter.attribs.find((e) => e.name == "background").current, - alignment: sourceCharacter.attribs.find((e) => e.name == "alignment").current - }; + const details = { + species: sourceCharacter.attribs.find((e) => e.name == "race").current, + background: sourceCharacter.attribs.find((e) => e.name == "background").current, + alignment: sourceCharacter.attribs.find((e) => e.name == "alignment").current + }; - const hp = { - value: sourceCharacter.attribs.find((e) => e.name == "hp").current, - min: 0, - max: sourceCharacter.attribs.find((e) => e.name == "hp").current, - temp: sourceCharacter.attribs.find((e) => e.name == "hp_temp").current - }; + const hp = { + value: sourceCharacter.attribs.find((e) => e.name == "hp").current, + min: 0, + max: sourceCharacter.attribs.find((e) => e.name == "hp").current, + temp: sourceCharacter.attribs.find((e) => e.name == "hp_temp").current + }; - const abilities = { - str: { - value: sourceCharacter.attribs.find((e) => e.name == "strength").current, - proficient: sourceCharacter.attribs.find((e) => e.name == "strength_save_prof").current ? 1 : 0 - }, - dex: { - value: sourceCharacter.attribs.find((e) => e.name == "dexterity").current, - proficient: sourceCharacter.attribs.find((e) => e.name == "dexterity_save_prof").current ? 1 : 0 - }, - con: { - value: sourceCharacter.attribs.find((e) => e.name == "constitution").current, - proficient: sourceCharacter.attribs.find((e) => e.name == "constitution_save_prof").current ? 1 : 0 - }, - int: { - value: sourceCharacter.attribs.find((e) => e.name == "intelligence").current, - proficient: sourceCharacter.attribs.find((e) => e.name == "intelligence_save_prof").current ? 1 : 0 - }, - wis: { - value: sourceCharacter.attribs.find((e) => e.name == "wisdom").current, - proficient: sourceCharacter.attribs.find((e) => e.name == "wisdom_save_prof").current ? 1 : 0 - }, - cha: { - value: sourceCharacter.attribs.find((e) => e.name == "charisma").current, - proficient: sourceCharacter.attribs.find((e) => e.name == "charisma_save_prof").current ? 1 : 0 - } - }; + const abilities = { + str: { + value: sourceCharacter.attribs.find((e) => e.name == "strength").current, + proficient: sourceCharacter.attribs.find((e) => e.name == "strength_save_prof").current ? 1 : 0 + }, + dex: { + value: sourceCharacter.attribs.find((e) => e.name == "dexterity").current, + proficient: sourceCharacter.attribs.find((e) => e.name == "dexterity_save_prof").current ? 1 : 0 + }, + con: { + value: sourceCharacter.attribs.find((e) => e.name == "constitution").current, + proficient: sourceCharacter.attribs.find((e) => e.name == "constitution_save_prof").current ? 1 : 0 + }, + int: { + value: sourceCharacter.attribs.find((e) => e.name == "intelligence").current, + proficient: sourceCharacter.attribs.find((e) => e.name == "intelligence_save_prof").current ? 1 : 0 + }, + wis: { + value: sourceCharacter.attribs.find((e) => e.name == "wisdom").current, + proficient: sourceCharacter.attribs.find((e) => e.name == "wisdom_save_prof").current ? 1 : 0 + }, + cha: { + value: sourceCharacter.attribs.find((e) => e.name == "charisma").current, + proficient: sourceCharacter.attribs.find((e) => e.name == "charisma_save_prof").current ? 1 : 0 + } + }; - /* ----------------------------------------------------------------- */ - /* character.data.skills..value is all that matters + /* ----------------------------------------------------------------- */ + /* character.data.skills..value is all that matters /* values can be 0, 0.5, 1 or 2 /* 0 = regular /* 0.5 = half-proficient @@ -53,272 +53,274 @@ export default class CharacterImporter { /* 2 = expertise /* foundry takes care of calculating the rest /* ----------------------------------------------------------------- */ - const skills = { - acr: { - value: sourceCharacter.attribs.find((e) => e.name == "acrobatics_type").current - }, - ani: { - value: sourceCharacter.attribs.find((e) => e.name == "animal_handling_type").current - }, - ath: { - value: sourceCharacter.attribs.find((e) => e.name == "athletics_type").current - }, - dec: { - value: sourceCharacter.attribs.find((e) => e.name == "deception_type").current - }, - ins: { - value: sourceCharacter.attribs.find((e) => e.name == "insight_type").current - }, - inv: { - value: sourceCharacter.attribs.find((e) => e.name == "investigation_type").current - }, - itm: { - value: sourceCharacter.attribs.find((e) => e.name == "intimidation_type").current - }, - lor: { - value: sourceCharacter.attribs.find((e) => e.name == "lore_type").current - }, - med: { - value: sourceCharacter.attribs.find((e) => e.name == "medicine_type").current - }, - nat: { - value: sourceCharacter.attribs.find((e) => e.name == "nature_type").current - }, - per: { - value: sourceCharacter.attribs.find((e) => e.name == "persuasion_type").current - }, - pil: { - value: sourceCharacter.attribs.find((e) => e.name == "piloting_type").current - }, - prc: { - value: sourceCharacter.attribs.find((e) => e.name == "perception_type").current - }, - prf: { - value: sourceCharacter.attribs.find((e) => e.name == "performance_type").current - }, - slt: { - value: sourceCharacter.attribs.find((e) => e.name == "sleight_of_hand_type").current - }, - ste: { - value: sourceCharacter.attribs.find((e) => e.name == "stealth_type").current - }, - sur: { - value: sourceCharacter.attribs.find((e) => e.name == "survival_type").current - }, - tec: { - value: sourceCharacter.attribs.find((e) => e.name == "technology_type").current - } - }; - - const targetCharacter = { - name: sourceCharacter.name, - type: "character", - data: { - abilities: abilities, - details: details, - skills: skills, - attributes: { - hp: hp - } - } - }; - - let actor = await Actor.create(targetCharacter); - CharacterImporter.addProfessions(sourceCharacter, actor); - } - - // Parse all classes and add them to already created actor. - // "class" is a reserved word, therefore I use profession where I can. - static async addProfessions(sourceCharacter, actor) { - let result = []; - - // parse all class and multiclassX items - // couldn't get Array.filter to work here for some reason - // result = array of objects. each object is a separate class - sourceCharacter.attribs.forEach((e) => { - if (CharacterImporter.classOrMulticlass(e.name)) { - var t = { - profession: CharacterImporter.capitalize(e.current), - type: CharacterImporter.baseOrMulti(e.name), - level: CharacterImporter.getLevel(e, sourceCharacter) + const skills = { + acr: { + value: sourceCharacter.attribs.find((e) => e.name == "acrobatics_type").current + }, + ani: { + value: sourceCharacter.attribs.find((e) => e.name == "animal_handling_type").current + }, + ath: { + value: sourceCharacter.attribs.find((e) => e.name == "athletics_type").current + }, + dec: { + value: sourceCharacter.attribs.find((e) => e.name == "deception_type").current + }, + ins: { + value: sourceCharacter.attribs.find((e) => e.name == "insight_type").current + }, + inv: { + value: sourceCharacter.attribs.find((e) => e.name == "investigation_type").current + }, + itm: { + value: sourceCharacter.attribs.find((e) => e.name == "intimidation_type").current + }, + lor: { + value: sourceCharacter.attribs.find((e) => e.name == "lore_type").current + }, + med: { + value: sourceCharacter.attribs.find((e) => e.name == "medicine_type").current + }, + nat: { + value: sourceCharacter.attribs.find((e) => e.name == "nature_type").current + }, + per: { + value: sourceCharacter.attribs.find((e) => e.name == "persuasion_type").current + }, + pil: { + value: sourceCharacter.attribs.find((e) => e.name == "piloting_type").current + }, + prc: { + value: sourceCharacter.attribs.find((e) => e.name == "perception_type").current + }, + prf: { + value: sourceCharacter.attribs.find((e) => e.name == "performance_type").current + }, + slt: { + value: sourceCharacter.attribs.find((e) => e.name == "sleight_of_hand_type").current + }, + ste: { + value: sourceCharacter.attribs.find((e) => e.name == "stealth_type").current + }, + sur: { + value: sourceCharacter.attribs.find((e) => e.name == "survival_type").current + }, + tec: { + value: sourceCharacter.attribs.find((e) => e.name == "technology_type").current + } }; - result.push(t); - } - }); - // pull classes directly from system compendium and add them to current actor - const professionsPack = await game.packs.get("sw5e.classes").getDocuments(); - result.forEach((prof) => { - let assignedProfession = professionsPack.find((o) => o.name === prof.profession); - assignedProfession.data.data.levels = prof.level; - actor.createEmbeddedDocuments("Item", [assignedProfession.data], { displaySheet: false }); - }); + const targetCharacter = { + name: sourceCharacter.name, + type: "character", + data: { + abilities: abilities, + details: details, + skills: skills, + attributes: { + hp: hp + } + } + }; - this.addSpecies(sourceCharacter.attribs.find((e) => e.name == "race").current, actor); - - this.addPowers( - sourceCharacter.attribs.filter((e) => e.name.search(/repeating_power.+_powername/g) != -1).map((e) => e.current), - actor - ); - - const discoveredItems = sourceCharacter.attribs.filter( - (e) => e.name.search(/repeating_inventory.+_itemname/g) != -1 - ); - const items = discoveredItems.map((item) => { - const id = item.name.match(/-\w{19}/g); - - return { - name: item.current, - quantity: sourceCharacter.attribs.find((e) => e.name === `repeating_inventory_${id}_itemcount`).current - }; - }); - - this.addItems(items, actor); - } - - static async addClasses(profession, level, actor) { - let classes = await game.packs.get("sw5e.classes").getDocuments(); - let assignedClass = classes.find((c) => c.name === profession); - assignedClass.data.data.levels = level; - await actor.createEmbeddedDocuments("Item", [assignedClass.data], { displaySheet: false }); - } - - static classOrMulticlass(name) { - return name === "class" || (name.includes("multiclass") && name.length <= 12); - } - - static baseOrMulti(name) { - if (name === "class") { - return "base_class"; - } else { - return "multi_class"; + let actor = await Actor.create(targetCharacter); + CharacterImporter.addProfessions(sourceCharacter, actor); } - } - static getLevel(item, sourceCharacter) { - if (item.name === "class") { - let result = sourceCharacter.attribs.find((e) => e.name === "base_level").current; - return parseInt(result); - } else { - let result = sourceCharacter.attribs.find((e) => e.name === `${item.name}_lvl`).current; - return parseInt(result); + // Parse all classes and add them to already created actor. + // "class" is a reserved word, therefore I use profession where I can. + static async addProfessions(sourceCharacter, actor) { + let result = []; + + // parse all class and multiclassX items + // couldn't get Array.filter to work here for some reason + // result = array of objects. each object is a separate class + sourceCharacter.attribs.forEach((e) => { + if (CharacterImporter.classOrMulticlass(e.name)) { + var t = { + profession: CharacterImporter.capitalize(e.current), + type: CharacterImporter.baseOrMulti(e.name), + level: CharacterImporter.getLevel(e, sourceCharacter) + }; + result.push(t); + } + }); + + // pull classes directly from system compendium and add them to current actor + const professionsPack = await game.packs.get("sw5e.classes").getDocuments(); + result.forEach((prof) => { + let assignedProfession = professionsPack.find((o) => o.name === prof.profession); + assignedProfession.data.data.levels = prof.level; + actor.createEmbeddedDocuments("Item", [assignedProfession.data], {displaySheet: false}); + }); + + this.addSpecies(sourceCharacter.attribs.find((e) => e.name == "race").current, actor); + + this.addPowers( + sourceCharacter.attribs + .filter((e) => e.name.search(/repeating_power.+_powername/g) != -1) + .map((e) => e.current), + actor + ); + + const discoveredItems = sourceCharacter.attribs.filter( + (e) => e.name.search(/repeating_inventory.+_itemname/g) != -1 + ); + const items = discoveredItems.map((item) => { + const id = item.name.match(/-\w{19}/g); + + return { + name: item.current, + quantity: sourceCharacter.attribs.find((e) => e.name === `repeating_inventory_${id}_itemcount`).current + }; + }); + + this.addItems(items, actor); } - } - static capitalize(str) { - return str.charAt(0).toUpperCase() + str.slice(1); - } - - static async addSpecies(race, actor) { - const species = await game.packs.get("sw5e.species").getDocuments(); - const assignedSpecies = species.find((c) => c.name === race); - const activeEffects = [...assignedSpecies.data.effects][0].data.changes; - const actorData = { data: { abilities: { ...actor.data.data.abilities } } }; - - activeEffects.map((effect) => { - switch (effect.key) { - case "data.abilities.str.value": - actorData.data.abilities.str.value -= effect.value; - break; - - case "data.abilities.dex.value": - actorData.data.abilities.dex.value -= effect.value; - break; - - case "data.abilities.con.value": - actorData.data.abilities.con.value -= effect.value; - break; - - case "data.abilities.int.value": - actorData.data.abilities.int.value -= effect.value; - break; - - case "data.abilities.wis.value": - actorData.data.abilities.wis.value -= effect.value; - break; - - case "data.abilities.cha.value": - actorData.data.abilities.cha.value -= effect.value; - break; - - default: - break; - } - }); - - actor.update(actorData); - - await actor.createEmbeddedDocuments("Item", [assignedSpecies.data], { displaySheet: false }); - } - - static async addPowers(powers, actor) { - const forcePowers = await game.packs.get("sw5e.forcepowers").getDocuments(); - const techPowers = await game.packs.get("sw5e.techpowers").getDocuments(); - - for (const power of powers) { - const createdPower = forcePowers.find((c) => c.name === power) || techPowers.find((c) => c.name === power); - - if (createdPower) { - await actor.createEmbeddedDocuments("Item", [createdPower.data], { displaySheet: false }); - } + static async addClasses(profession, level, actor) { + let classes = await game.packs.get("sw5e.classes").getDocuments(); + let assignedClass = classes.find((c) => c.name === profession); + assignedClass.data.data.levels = level; + await actor.createEmbeddedDocuments("Item", [assignedClass.data], {displaySheet: false}); } - } - static async addItems(items, actor) { - const weapons = await game.packs.get("sw5e.weapons").getDocuments(); - const armors = await game.packs.get("sw5e.armor").getDocuments(); - const adventuringGear = await game.packs.get("sw5e.adventuringgear").getDocuments(); + static classOrMulticlass(name) { + return name === "class" || (name.includes("multiclass") && name.length <= 12); + } - for (const item of items) { - const createdItem = - weapons.find((c) => c.name.toLowerCase() === item.name.toLowerCase()) || - armors.find((c) => c.name.toLowerCase() === item.name.toLowerCase()) || - adventuringGear.find((c) => c.name.toLowerCase() === item.name.toLowerCase()); - - if (createdItem) { - if (item.quantity != 1) { - createdItem.data.data.quantity = item.quantity; + static baseOrMulti(name) { + if (name === "class") { + return "base_class"; + } else { + return "multi_class"; } - - await actor.createEmbeddedDocuments("Item", [createdItem.data], { displaySheet: false }); - } } - } - static addImportButton(html) { - const actionButtons = html.find(".header-actions"); - actionButtons[0].insertAdjacentHTML( - "afterend", - `
` - ); + static getLevel(item, sourceCharacter) { + if (item.name === "class") { + let result = sourceCharacter.attribs.find((e) => e.name === "base_level").current; + return parseInt(result); + } else { + let result = sourceCharacter.attribs.find((e) => e.name === `${item.name}_lvl`).current; + return parseInt(result); + } + } - let characterImportButton = $(".cs-import-button"); - characterImportButton.click(() => { - let content = `

Saved Character JSON Import

+ static capitalize(str) { + return str.charAt(0).toUpperCase() + str.slice(1); + } + + static async addSpecies(race, actor) { + const species = await game.packs.get("sw5e.species").getDocuments(); + const assignedSpecies = species.find((c) => c.name === race); + const activeEffects = [...assignedSpecies.data.effects][0].data.changes; + const actorData = {data: {abilities: {...actor.data.data.abilities}}}; + + activeEffects.map((effect) => { + switch (effect.key) { + case "data.abilities.str.value": + actorData.data.abilities.str.value -= effect.value; + break; + + case "data.abilities.dex.value": + actorData.data.abilities.dex.value -= effect.value; + break; + + case "data.abilities.con.value": + actorData.data.abilities.con.value -= effect.value; + break; + + case "data.abilities.int.value": + actorData.data.abilities.int.value -= effect.value; + break; + + case "data.abilities.wis.value": + actorData.data.abilities.wis.value -= effect.value; + break; + + case "data.abilities.cha.value": + actorData.data.abilities.cha.value -= effect.value; + break; + + default: + break; + } + }); + + actor.update(actorData); + + await actor.createEmbeddedDocuments("Item", [assignedSpecies.data], {displaySheet: false}); + } + + static async addPowers(powers, actor) { + const forcePowers = await game.packs.get("sw5e.forcepowers").getDocuments(); + const techPowers = await game.packs.get("sw5e.techpowers").getDocuments(); + + for (const power of powers) { + const createdPower = forcePowers.find((c) => c.name === power) || techPowers.find((c) => c.name === power); + + if (createdPower) { + await actor.createEmbeddedDocuments("Item", [createdPower.data], {displaySheet: false}); + } + } + } + + static async addItems(items, actor) { + const weapons = await game.packs.get("sw5e.weapons").getDocuments(); + const armors = await game.packs.get("sw5e.armor").getDocuments(); + const adventuringGear = await game.packs.get("sw5e.adventuringgear").getDocuments(); + + for (const item of items) { + const createdItem = + weapons.find((c) => c.name.toLowerCase() === item.name.toLowerCase()) || + armors.find((c) => c.name.toLowerCase() === item.name.toLowerCase()) || + adventuringGear.find((c) => c.name.toLowerCase() === item.name.toLowerCase()); + + if (createdItem) { + if (item.quantity != 1) { + createdItem.data.data.quantity = item.quantity; + } + + await actor.createEmbeddedDocuments("Item", [createdItem.data], {displaySheet: false}); + } + } + } + + static addImportButton(html) { + const actionButtons = html.find(".header-actions"); + actionButtons[0].insertAdjacentHTML( + "afterend", + `
` + ); + + let characterImportButton = $(".cs-import-button"); + characterImportButton.click(() => { + let content = `

Saved Character JSON Import


`; - let importDialog = new Dialog({ - title: "Import Character from SW5e.com", - content: content, - buttons: { - Import: { - icon: ``, - label: "Import Character", - callback: () => { - let characterData = $("#character-json").val(); - console.log("Parsing Character JSON"); - CharacterImporter.transform(characterData); - } - }, - Cancel: { - icon: ``, - label: "Cancel", - callback: () => {} - } - } - }); - importDialog.render(true); - }); - } + let importDialog = new Dialog({ + title: "Import Character from SW5e.com", + content: content, + buttons: { + Import: { + icon: ``, + label: "Import Character", + callback: () => { + let characterData = $("#character-json").val(); + console.log("Parsing Character JSON"); + CharacterImporter.transform(characterData); + } + }, + Cancel: { + icon: ``, + label: "Cancel", + callback: () => {} + } + } + }); + importDialog.render(true); + }); + } } diff --git a/module/chat.js b/module/chat.js index d024d8aa..42d2bf4f 100644 --- a/module/chat.js +++ b/module/chat.js @@ -1,30 +1,29 @@ - /** * Highlight critical success or failure on d20 rolls */ -export const highlightCriticalSuccessFailure = function(message, html, data) { - if ( !message.isRoll || !message.isContentVisible ) return; +export const highlightCriticalSuccessFailure = function (message, html, data) { + if (!message.isRoll || !message.isContentVisible) return; - // Highlight rolls where the first part is a d20 roll - const roll = message.roll; - if ( !roll.dice.length ) return; - const d = roll.dice[0]; + // Highlight rolls where the first part is a d20 roll + const roll = message.roll; + if (!roll.dice.length) return; + const d = roll.dice[0]; - // Ensure it is an un-modified d20 roll - const isD20 = (d.faces === 20) && ( d.values.length === 1 ); - if ( !isD20 ) return; - const isModifiedRoll = ("success" in d.results[0]) || d.options.marginSuccess || d.options.marginFailure; - if ( isModifiedRoll ) return; + // Ensure it is an un-modified d20 roll + const isD20 = d.faces === 20 && d.values.length === 1; + if (!isD20) return; + const isModifiedRoll = "success" in d.results[0] || d.options.marginSuccess || d.options.marginFailure; + if (isModifiedRoll) return; - // Highlight successes and failures - const critical = d.options.critical || 20; - const fumble = d.options.fumble || 1; - if ( d.total >= critical ) html.find(".dice-total").addClass("critical"); - else if ( d.total <= fumble ) html.find(".dice-total").addClass("fumble"); - else if ( d.options.target ) { - if ( roll.total >= d.options.target ) html.find(".dice-total").addClass("success"); - else html.find(".dice-total").addClass("failure"); - } + // Highlight successes and failures + const critical = d.options.critical || 20; + const fumble = d.options.fumble || 1; + if (d.total >= critical) html.find(".dice-total").addClass("critical"); + else if (d.total <= fumble) html.find(".dice-total").addClass("fumble"); + else if (d.options.target) { + if (roll.total >= d.options.target) html.find(".dice-total").addClass("success"); + else html.find(".dice-total").addClass("failure"); + } }; /* -------------------------------------------- */ @@ -32,24 +31,24 @@ export const highlightCriticalSuccessFailure = function(message, html, data) { /** * Optionally hide the display of chat card action buttons which cannot be performed by the user */ -export const displayChatActionButtons = function(message, html, data) { - const chatCard = html.find(".sw5e.chat-card"); - if ( chatCard.length > 0 ) { - const flavor = html.find(".flavor-text"); - if ( flavor.text() === html.find(".item-name").text() ) flavor.remove(); +export const displayChatActionButtons = function (message, html, data) { + const chatCard = html.find(".sw5e.chat-card"); + if (chatCard.length > 0) { + const flavor = html.find(".flavor-text"); + if (flavor.text() === html.find(".item-name").text()) flavor.remove(); - // If the user is the message author or the actor owner, proceed - let actor = game.actors.get(data.message.speaker.actor); - if ( actor && actor.isOwner ) return; - else if ( game.user.isGM || (data.author.id === game.user.id)) return; + // If the user is the message author or the actor owner, proceed + let actor = game.actors.get(data.message.speaker.actor); + if (actor && actor.isOwner) return; + else if (game.user.isGM || data.author.id === game.user.id) return; - // Otherwise conceal action buttons except for saving throw - const buttons = chatCard.find("button[data-action]"); - buttons.each((i, btn) => { - if ( btn.dataset.action === "save" ) return; - btn.style.display = "none" - }); - } + // Otherwise conceal action buttons except for saving throw + const buttons = chatCard.find("button[data-action]"); + buttons.each((i, btn) => { + if (btn.dataset.action === "save") return; + btn.style.display = "none"; + }); + } }; /* -------------------------------------------- */ @@ -63,38 +62,38 @@ export const displayChatActionButtons = function(message, html, data) { * * @return {Array} The extended options Array including new context choices */ -export const addChatMessageContextOptions = function(html, options) { - let canApply = li => { - const message = game.messages.get(li.data("messageId")); - return message?.isRoll && message?.isContentVisible && canvas.tokens?.controlled.length; - }; - options.push( - { - name: game.i18n.localize("SW5E.ChatContextDamage"), - icon: '', - condition: canApply, - callback: li => applyChatCardDamage(li, 1) - }, - { - name: game.i18n.localize("SW5E.ChatContextHealing"), - icon: '', - condition: canApply, - callback: li => applyChatCardDamage(li, -1) - }, - { - name: game.i18n.localize("SW5E.ChatContextDoubleDamage"), - icon: '', - condition: canApply, - callback: li => applyChatCardDamage(li, 2) - }, - { - name: game.i18n.localize("SW5E.ChatContextHalfDamage"), - icon: '', - condition: canApply, - callback: li => applyChatCardDamage(li, 0.5) - } - ); - return options; +export const addChatMessageContextOptions = function (html, options) { + let canApply = (li) => { + const message = game.messages.get(li.data("messageId")); + return message?.isRoll && message?.isContentVisible && canvas.tokens?.controlled.length; + }; + options.push( + { + name: game.i18n.localize("SW5E.ChatContextDamage"), + icon: '', + condition: canApply, + callback: (li) => applyChatCardDamage(li, 1) + }, + { + name: game.i18n.localize("SW5E.ChatContextHealing"), + icon: '', + condition: canApply, + callback: (li) => applyChatCardDamage(li, -1) + }, + { + name: game.i18n.localize("SW5E.ChatContextDoubleDamage"), + icon: '', + condition: canApply, + callback: (li) => applyChatCardDamage(li, 2) + }, + { + name: game.i18n.localize("SW5E.ChatContextHalfDamage"), + icon: '', + condition: canApply, + callback: (li) => applyChatCardDamage(li, 0.5) + } + ); + return options; }; /* -------------------------------------------- */ @@ -108,12 +107,14 @@ export const addChatMessageContextOptions = function(html, options) { * @return {Promise} */ function applyChatCardDamage(li, multiplier) { - const message = game.messages.get(li.data("messageId")); - const roll = message.roll; - return Promise.all(canvas.tokens.controlled.map(t => { - const a = t.actor; - return a.applyDamage(roll.total, multiplier); - })); + const message = game.messages.get(li.data("messageId")); + const roll = message.roll; + return Promise.all( + canvas.tokens.controlled.map((t) => { + const a = t.actor; + return a.applyDamage(roll.total, multiplier); + }) + ); } /* -------------------------------------------- */ diff --git a/module/classFeatures.js b/module/classFeatures.js index 7946c252..17155d10 100644 --- a/module/classFeatures.js +++ b/module/classFeatures.js @@ -1,4 +1 @@ -export const ClassFeatures = { - -}; - +export const ClassFeatures = {}; diff --git a/module/combat.js b/module/combat.js index 58535411..ff7594a5 100644 --- a/module/combat.js +++ b/module/combat.js @@ -1,27 +1,31 @@ - /** * Override the default Initiative formula to customize special behaviors of the SW5e system. * Apply advantage, proficiency, or bonuses where appropriate * Apply the dexterity score as a decimal tiebreaker if requested * See Combat._getInitiativeFormula for more detail. */ -export const _getInitiativeFormula = function() { - const actor = this.actor; - if ( !actor ) return "1d20"; - const init = actor.data.data.attributes.init; +export const _getInitiativeFormula = function () { + const actor = this.actor; + if (!actor) return "1d20"; + const init = actor.data.data.attributes.init; - // Construct initiative formula parts - let nd = 1; - let mods = ""; - if (actor.getFlag("sw5e", "halflingLucky")) mods += "r1=1"; - if (actor.getFlag("sw5e", "initiativeAdv")) { - nd = 2; - mods += "kh"; - } - const parts = [`${nd}d20${mods}`, init.mod, (init.prof !== 0) ? init.prof : null, (init.bonus !== 0) ? init.bonus : null]; + // Construct initiative formula parts + let nd = 1; + let mods = ""; + if (actor.getFlag("sw5e", "halflingLucky")) mods += "r1=1"; + if (actor.getFlag("sw5e", "initiativeAdv")) { + nd = 2; + mods += "kh"; + } + const parts = [ + `${nd}d20${mods}`, + init.mod, + init.prof !== 0 ? init.prof : null, + init.bonus !== 0 ? init.bonus : null + ]; - // Optionally apply Dexterity tiebreaker - const tiebreaker = game.settings.get("sw5e", "initiativeDexTiebreaker"); - if ( tiebreaker ) parts.push(actor.data.data.abilities.dex.value / 100); - return parts.filter(p => p !== null).join(" + "); + // Optionally apply Dexterity tiebreaker + const tiebreaker = game.settings.get("sw5e", "initiativeDexTiebreaker"); + if (tiebreaker) parts.push(actor.data.data.abilities.dex.value / 100); + return parts.filter((p) => p !== null).join(" + "); }; diff --git a/module/config.js b/module/config.js index 394346bf..1770bcea 100644 --- a/module/config.js +++ b/module/config.js @@ -1,4 +1,4 @@ -import {ClassFeatures} from "./classFeatures.js" +import {ClassFeatures} from "./classFeatures.js"; // Namespace SW5e Configuration Values export const SW5E = {}; @@ -12,27 +12,26 @@ SW5E.ASCII = ` \\______ / \\__/\\ //______ /\\__ > \\/ \\/ \\/ \\/ `; - /** * The set of Ability Scores used within the system * @type {Object} */ SW5E.abilities = { - "str": "SW5E.AbilityStr", - "dex": "SW5E.AbilityDex", - "con": "SW5E.AbilityCon", - "int": "SW5E.AbilityInt", - "wis": "SW5E.AbilityWis", - "cha": "SW5E.AbilityCha" + str: "SW5E.AbilityStr", + dex: "SW5E.AbilityDex", + con: "SW5E.AbilityCon", + int: "SW5E.AbilityInt", + wis: "SW5E.AbilityWis", + cha: "SW5E.AbilityCha" }; SW5E.abilityAbbreviations = { - "str": "SW5E.AbilityStrAbbr", - "dex": "SW5E.AbilityDexAbbr", - "con": "SW5E.AbilityConAbbr", - "int": "SW5E.AbilityIntAbbr", - "wis": "SW5E.AbilityWisAbbr", - "cha": "SW5E.AbilityChaAbbr" + str: "SW5E.AbilityStrAbbr", + dex: "SW5E.AbilityDexAbbr", + con: "SW5E.AbilityConAbbr", + int: "SW5E.AbilityIntAbbr", + wis: "SW5E.AbilityWisAbbr", + cha: "SW5E.AbilityChaAbbr" }; /* -------------------------------------------- */ @@ -42,15 +41,15 @@ SW5E.abilityAbbreviations = { * @type {Object} */ SW5E.alignments = { - 'll': "SW5E.AlignmentLL", - 'nl': "SW5E.AlignmentNL", - 'cl': "SW5E.AlignmentCL", - 'lb': "SW5E.AlignmentLB", - 'bn': "SW5E.AlignmentBN", - 'cb': "SW5E.AlignmentCB", - 'ld': "SW5E.AlignmentLD", - 'nd': "SW5E.AlignmentND", - 'cd': "SW5E.AlignmentCD" + ll: "SW5E.AlignmentLL", + nl: "SW5E.AlignmentNL", + cl: "SW5E.AlignmentCL", + lb: "SW5E.AlignmentLB", + bn: "SW5E.AlignmentBN", + cb: "SW5E.AlignmentCB", + ld: "SW5E.AlignmentLD", + nd: "SW5E.AlignmentND", + cd: "SW5E.AlignmentCD" }; /* -------------------------------------------- */ @@ -60,9 +59,9 @@ SW5E.alignments = { * @enum {number} */ SW5E.attunementTypes = { - NONE: 0, - REQUIRED: 1, - ATTUNED: 2, + NONE: 0, + REQUIRED: 1, + ATTUNED: 2 }; /** @@ -70,37 +69,37 @@ SW5E.attunementTypes = { * @type {{"0": string, "1": string, "2": string}} */ SW5E.attunements = { - 0: "SW5E.AttunementNone", - 1: "SW5E.AttunementRequired", - 2: "SW5E.AttunementAttuned" + 0: "SW5E.AttunementNone", + 1: "SW5E.AttunementRequired", + 2: "SW5E.AttunementAttuned" }; /* -------------------------------------------- */ SW5E.weaponProficiencies = { - "blp": "SW5E.WeaponBlasterPistolProficiency", - "chk": "SW5E.WeaponChakramProficiency", - "dbb": "SW5E.WeaponDoubleBladeProficiency", - "dbs": "SW5E.WeaponDoubleSaberProficiency", - "dsh": "SW5E.WeaponDoubleShotoProficiency", - "dsw": "SW5E.WeaponDoubleSwordProficiency", - "hid": "SW5E.WeaponHiddenBladeProficiency", - "imp": "SW5E.WeaponImprovisedProficiency", - "lfl": "SW5E.WeaponLightFoilProficiency", - "lrg": "SW5E.WeaponLightRingProficiency", - "mar": "SW5E.WeaponMartialProficiency", - "mrb": "SW5E.WeaponMartialBlasterProficiency", - "mlw": "SW5E.WeaponMartialLightweaponProficiency", - "mvb": "SW5E.WeaponMartialVibroweaponProficiency", - "ntl": "SW5E.WeaponNaturalProficiency", - "swh": "SW5E.WeaponSaberWhipProficiency", - "sim": "SW5E.WeaponSimpleProficiency", - "smb": "SW5E.WeaponSimpleBlasterProficiency", - "slw": "SW5E.WeaponSimpleLightweaponProficiency", - "svb": "SW5E.WeaponSimpleVibroweaponProficiency", - "tch": "SW5E.WeaponTechbladeProficiency", - "vbr": "SW5E.WeaponVibrorapierProficiency", - "vbw": "SW5E.WeaponVibrowhipProficiency" + blp: "SW5E.WeaponBlasterPistolProficiency", + chk: "SW5E.WeaponChakramProficiency", + dbb: "SW5E.WeaponDoubleBladeProficiency", + dbs: "SW5E.WeaponDoubleSaberProficiency", + dsh: "SW5E.WeaponDoubleShotoProficiency", + dsw: "SW5E.WeaponDoubleSwordProficiency", + hid: "SW5E.WeaponHiddenBladeProficiency", + imp: "SW5E.WeaponImprovisedProficiency", + lfl: "SW5E.WeaponLightFoilProficiency", + lrg: "SW5E.WeaponLightRingProficiency", + mar: "SW5E.WeaponMartialProficiency", + mrb: "SW5E.WeaponMartialBlasterProficiency", + mlw: "SW5E.WeaponMartialLightweaponProficiency", + mvb: "SW5E.WeaponMartialVibroweaponProficiency", + ntl: "SW5E.WeaponNaturalProficiency", + swh: "SW5E.WeaponSaberWhipProficiency", + sim: "SW5E.WeaponSimpleProficiency", + smb: "SW5E.WeaponSimpleBlasterProficiency", + slw: "SW5E.WeaponSimpleLightweaponProficiency", + svb: "SW5E.WeaponSimpleVibroweaponProficiency", + tch: "SW5E.WeaponTechbladeProficiency", + vbr: "SW5E.WeaponVibrorapierProficiency", + vbw: "SW5E.WeaponVibrowhipProficiency" }; /** @@ -108,14 +107,14 @@ SW5E.weaponProficiencies = { * Used when a new player owned item is created * @type {Object} */ - SW5E.weaponProficienciesMap = { - "natural": true, - "simpleVW": "sim", - "simpleB": "sim", - "simpleLW": "sim", - "martialVW": "mar", - "martialB": "mar", - "martialLW": "mar" +SW5E.weaponProficienciesMap = { + natural: true, + simpleVW: "sim", + simpleB: "sim", + simpleLW: "sim", + martialVW: "mar", + martialB: "mar", + martialLW: "mar" }; // TODO: Check to see if this can be used @@ -172,36 +171,36 @@ SW5E.weaponIds = { /* -------------------------------------------- */ SW5E.toolProficiencies = { - "armor": "SW5E.ToolArmormech", - "arms": "SW5E.ToolArmstech", - "arti": "SW5E.ToolArtificer", - "art": "SW5E.ToolArtist", - "astro": "SW5E.ToolAstrotech", - "bio": "SW5E.ToolBiotech", - "con": "SW5E.ToolConstructor", - "cyb": "SW5E.ToolCybertech", - "jew": "SW5E.ToolJeweler", - "sur": "SW5E.ToolSurveyor", - "syn": "SW5E.ToolSynthweaver", - "tin": "SW5E.ToolTinker", - "ant": "SW5E.ToolAntitoxkit", - "arc": "SW5E.ToolArchaeologistKit", - "aud": "SW5E.ToolAudiotechKit", - "bioa": "SW5E.ToolBioanalysisKit", - "brew": "SW5E.ToolBrewerKit", - "chef": "SW5E.ToolChefKit", - "demo": "SW5E.ToolDemolitionKit", - "disg": "SW5E.ToolDisguiseKit", - "forg": "SW5E.ToolForgeryKit", - "mech": "SW5E.ToolMechanicKit", - "game": "SW5E.ToolGamingSet", - "poi": "SW5E.ToolPoisonKit", - "scav": "SW5E.ToolScavengingKit", - "secur": "SW5E.ToolSecurityKit", - "slic": "SW5E.ToolSlicerKit", - "spice": "SW5E.ToolSpiceKit", - "music": "SW5E.ToolMusicalInstrument", - "vehicle": "SW5E.ToolVehicle" + armor: "SW5E.ToolArmormech", + arms: "SW5E.ToolArmstech", + arti: "SW5E.ToolArtificer", + art: "SW5E.ToolArtist", + astro: "SW5E.ToolAstrotech", + bio: "SW5E.ToolBiotech", + con: "SW5E.ToolConstructor", + cyb: "SW5E.ToolCybertech", + jew: "SW5E.ToolJeweler", + sur: "SW5E.ToolSurveyor", + syn: "SW5E.ToolSynthweaver", + tin: "SW5E.ToolTinker", + ant: "SW5E.ToolAntitoxkit", + arc: "SW5E.ToolArchaeologistKit", + aud: "SW5E.ToolAudiotechKit", + bioa: "SW5E.ToolBioanalysisKit", + brew: "SW5E.ToolBrewerKit", + chef: "SW5E.ToolChefKit", + demo: "SW5E.ToolDemolitionKit", + disg: "SW5E.ToolDisguiseKit", + forg: "SW5E.ToolForgeryKit", + mech: "SW5E.ToolMechanicKit", + game: "SW5E.ToolGamingSet", + poi: "SW5E.ToolPoisonKit", + scav: "SW5E.ToolScavengingKit", + secur: "SW5E.ToolSecurityKit", + slic: "SW5E.ToolSlicerKit", + spice: "SW5E.ToolSpiceKit", + music: "SW5E.ToolMusicalInstrument", + vehicle: "SW5E.ToolVehicle" }; // TODO: Same as weapon IDs @@ -257,19 +256,18 @@ SW5E.toolIds = { * @type {Object} */ SW5E.timePeriods = { - "inst": "SW5E.TimeInst", - "turn": "SW5E.TimeTurn", - "round": "SW5E.TimeRound", - "minute": "SW5E.TimeMinute", - "hour": "SW5E.TimeHour", - "day": "SW5E.TimeDay", - "month": "SW5E.TimeMonth", - "year": "SW5E.TimeYear", - "perm": "SW5E.TimePerm", - "spec": "SW5E.Special" + inst: "SW5E.TimeInst", + turn: "SW5E.TimeTurn", + round: "SW5E.TimeRound", + minute: "SW5E.TimeMinute", + hour: "SW5E.TimeHour", + day: "SW5E.TimeDay", + month: "SW5E.TimeMonth", + year: "SW5E.TimeYear", + perm: "SW5E.TimePerm", + spec: "SW5E.Special" }; - /* -------------------------------------------- */ /** @@ -277,49 +275,47 @@ SW5E.timePeriods = { * @type {Object} */ SW5E.abilityActivationTypes = { - "none": "SW5E.None", - "action": "SW5E.Action", - "bonus": "SW5E.BonusAction", - "reaction": "SW5E.Reaction", - "minute": SW5E.timePeriods.minute, - "hour": SW5E.timePeriods.hour, - "day": SW5E.timePeriods.day, - "special": SW5E.timePeriods.spec, - "legendary": "SW5E.LegendaryActionLabel", - "lair": "SW5E.LairActionLabel", - "crew": "SW5E.VehicleCrewAction" + none: "SW5E.None", + action: "SW5E.Action", + bonus: "SW5E.BonusAction", + reaction: "SW5E.Reaction", + minute: SW5E.timePeriods.minute, + hour: SW5E.timePeriods.hour, + day: SW5E.timePeriods.day, + special: SW5E.timePeriods.spec, + legendary: "SW5E.LegendaryActionLabel", + lair: "SW5E.LairActionLabel", + crew: "SW5E.VehicleCrewAction" }; /* -------------------------------------------- */ - SW5E.abilityConsumptionTypes = { - "ammo": "SW5E.ConsumeAmmunition", - "attribute": "SW5E.ConsumeAttribute", - "material": "SW5E.ConsumeMaterial", - "charges": "SW5E.ConsumeCharges" + ammo: "SW5E.ConsumeAmmunition", + attribute: "SW5E.ConsumeAttribute", + material: "SW5E.ConsumeMaterial", + charges: "SW5E.ConsumeCharges" }; - /* -------------------------------------------- */ // Creature Sizes SW5E.actorSizes = { - "tiny": "SW5E.SizeTiny", - "sm": "SW5E.SizeSmall", - "med": "SW5E.SizeMedium", - "lg": "SW5E.SizeLarge", - "huge": "SW5E.SizeHuge", - "grg": "SW5E.SizeGargantuan" + tiny: "SW5E.SizeTiny", + sm: "SW5E.SizeSmall", + med: "SW5E.SizeMedium", + lg: "SW5E.SizeLarge", + huge: "SW5E.SizeHuge", + grg: "SW5E.SizeGargantuan" }; SW5E.tokenSizes = { - "tiny": 1, - "sm": 1, - "med": 1, - "lg": 2, - "huge": 3, - "grg": 4 + tiny: 1, + sm: 1, + med: 1, + lg: 2, + huge: 3, + grg: 4 }; /** @@ -327,10 +323,10 @@ SW5E.tokenSizes = { * @enum {number} */ SW5E.tokenHPColors = { - temp: 0x66CCFF, - tempmax: 0x440066, - negmax: 0x550000 -} + temp: 0x66ccff, + tempmax: 0x440066, + negmax: 0x550000 +}; /* -------------------------------------------- */ @@ -339,17 +335,16 @@ SW5E.tokenHPColors = { * @type {Object} */ SW5E.creatureTypes = { - "aberration": "SW5E.CreatureAberration", - "beast": "SW5E.CreatureBeast", - "construct": "SW5E.CreatureConstruct", - "droid": "SW5E.CreatureDroid", - "force": "SW5E.CreatureForceEntity", - "humanoid": "SW5E.CreatureHumanoid", - "plant": "SW5E.CreaturePlant", - "undead": "SW5E.CreatureUndead" + aberration: "SW5E.CreatureAberration", + beast: "SW5E.CreatureBeast", + construct: "SW5E.CreatureConstruct", + droid: "SW5E.CreatureDroid", + force: "SW5E.CreatureForceEntity", + humanoid: "SW5E.CreatureHumanoid", + plant: "SW5E.CreaturePlant", + undead: "SW5E.CreatureUndead" }; - /* -------------------------------------------- */ /** @@ -357,22 +352,22 @@ SW5E.creatureTypes = { * @type {Object} */ SW5E.itemActionTypes = { - "mwak": "SW5E.ActionMWAK", - "rwak": "SW5E.ActionRWAK", - "mpak": "SW5E.ActionMPAK", - "rpak": "SW5E.ActionRPAK", - "save": "SW5E.ActionSave", - "heal": "SW5E.ActionHeal", - "abil": "SW5E.ActionAbil", - "util": "SW5E.ActionUtil", - "other": "SW5E.ActionOther" + mwak: "SW5E.ActionMWAK", + rwak: "SW5E.ActionRWAK", + mpak: "SW5E.ActionMPAK", + rpak: "SW5E.ActionRPAK", + save: "SW5E.ActionSave", + heal: "SW5E.ActionHeal", + abil: "SW5E.ActionAbil", + util: "SW5E.ActionUtil", + other: "SW5E.ActionOther" }; /* -------------------------------------------- */ SW5E.itemCapacityTypes = { - "items": "SW5E.ItemContainerCapacityItems", - "weight": "SW5E.ItemContainerCapacityWeight" + items: "SW5E.ItemContainerCapacityItems", + weight: "SW5E.ItemContainerCapacityWeight" }; /* -------------------------------------------- */ @@ -382,15 +377,14 @@ SW5E.itemCapacityTypes = { * @type {Object} */ SW5E.limitedUsePeriods = { - "sr": "SW5E.ShortRest", - "lr": "SW5E.LongRest", - "day": "SW5E.Day", - "charges": "SW5E.Charges", - "recharge": "SW5E.Recharge", - "refitting": "SW5E.Refitting" + sr: "SW5E.ShortRest", + lr: "SW5E.LongRest", + day: "SW5E.Day", + charges: "SW5E.Charges", + recharge: "SW5E.Recharge", + refitting: "SW5E.Refitting" }; - /* -------------------------------------------- */ /** @@ -398,23 +392,22 @@ SW5E.limitedUsePeriods = { * @type {Object} */ SW5E.equipmentTypes = { - "light": "SW5E.EquipmentLight", - "medium": "SW5E.EquipmentMedium", - "heavy": "SW5E.EquipmentHeavy", - "hyper": "SW5E.EquipmentHyperdrive", - "bonus": "SW5E.EquipmentBonus", - "natural": "SW5E.EquipmentNatural", - "powerc": "SW5E.EquipmentPowerCoupling", - "reactor": "SW5E.EquipmentReactor", - "shield": "SW5E.EquipmentShield", - "clothing": "SW5E.EquipmentClothing", - "trinket": "SW5E.EquipmentTrinket", - "ssarmor": "SW5E.EquipmentStarshipArmor", - "ssshield": "SW5E.EquipmentStarshipShield", - "vehicle": "SW5E.EquipmentVehicle" + light: "SW5E.EquipmentLight", + medium: "SW5E.EquipmentMedium", + heavy: "SW5E.EquipmentHeavy", + hyper: "SW5E.EquipmentHyperdrive", + bonus: "SW5E.EquipmentBonus", + natural: "SW5E.EquipmentNatural", + powerc: "SW5E.EquipmentPowerCoupling", + reactor: "SW5E.EquipmentReactor", + shield: "SW5E.EquipmentShield", + clothing: "SW5E.EquipmentClothing", + trinket: "SW5E.EquipmentTrinket", + ssarmor: "SW5E.EquipmentStarshipArmor", + ssshield: "SW5E.EquipmentStarshipShield", + vehicle: "SW5E.EquipmentVehicle" }; - /* -------------------------------------------- */ /** @@ -422,10 +415,10 @@ SW5E.equipmentTypes = { * @type {Object} */ SW5E.armorProficiencies = { - "lgt": SW5E.equipmentTypes.light, - "med": SW5E.equipmentTypes.medium, - "hvy": SW5E.equipmentTypes.heavy, - "shl": "SW5E.EquipmentShieldProficiency" + lgt: SW5E.equipmentTypes.light, + med: SW5E.equipmentTypes.medium, + hvy: SW5E.equipmentTypes.heavy, + shl: "SW5E.EquipmentShieldProficiency" }; /** @@ -433,14 +426,14 @@ SW5E.armorProficiencies = { * Used when a new player owned item is created * @type {Object} */ - SW5E.armorProficienciesMap = { - "natural": true, - "clothing": true, - "light": "lgt", - "medium": "med", - "heavy": "hvy", - "shield": "shl" -} +SW5E.armorProficienciesMap = { + natural: true, + clothing: true, + light: "lgt", + medium: "med", + heavy: "hvy", + shield: "shl" +}; /* -------------------------------------------- */ @@ -449,16 +442,16 @@ SW5E.armorProficiencies = { * @type {Object} */ SW5E.consumableTypes = { - "adrenal": "SW5E.ConsumableAdrenal", - "poison": "SW5E.ConsumablePoison", - "explosive": "SW5E.ConsumableExplosive", - "food": "SW5E.ConsumableFood", - "medpac": "SW5E.ConsumableMedpac", - "technology": "SW5E.ConsumableTechnology", - "ammo": "SW5E.ConsumableAmmunition", - "trinket": "SW5E.ConsumableTrinket", - "force": "SW5E.ConsumableForce", - "tech": "SW5E.ConsumableTech" + adrenal: "SW5E.ConsumableAdrenal", + poison: "SW5E.ConsumablePoison", + explosive: "SW5E.ConsumableExplosive", + food: "SW5E.ConsumableFood", + medpac: "SW5E.ConsumableMedpac", + technology: "SW5E.ConsumableTechnology", + ammo: "SW5E.ConsumableAmmunition", + trinket: "SW5E.ConsumableTrinket", + force: "SW5E.ConsumableForce", + tech: "SW5E.ConsumableTech" }; /* -------------------------------------------- */ @@ -468,26 +461,25 @@ SW5E.consumableTypes = { * @type {Object} */ SW5E.currencies = { - "CR": "SW5E.CurrencyCR", - }; + CR: "SW5E.CurrencyCR" +}; /* -------------------------------------------- */ - // Damage Types SW5E.damageTypes = { - "acid": "SW5E.DamageAcid", - "cold": "SW5E.DamageCold", - "energy": "SW5E.DamageEnergy", - "fire": "SW5E.DamageFire", - "force": "SW5E.DamageForce", - "ion": "SW5E.DamageIon", - "kinetic": "SW5E.DamageKinetic", - "lightning": "SW5E.DamageLightning", - "necrotic": "SW5E.DamageNecrotic", - "poison": "SW5E.DamagePoison", - "psychic": "SW5E.DamagePsychic", - "sonic": "SW5E.DamageSonic" + acid: "SW5E.DamageAcid", + cold: "SW5E.DamageCold", + energy: "SW5E.DamageEnergy", + fire: "SW5E.DamageFire", + force: "SW5E.DamageForce", + ion: "SW5E.DamageIon", + kinetic: "SW5E.DamageKinetic", + lightning: "SW5E.DamageLightning", + necrotic: "SW5E.DamageNecrotic", + poison: "SW5E.DamagePoison", + psychic: "SW5E.DamagePsychic", + sonic: "SW5E.DamageSonic" }; // Damage Resistance Types @@ -495,39 +487,38 @@ SW5E.damageResistanceTypes = foundry.utils.deepClone(SW5E.damageTypes); /* -------------------------------------------- */ - // armor Types SW5E.armorPropertiesTypes = { -"Absorptive": "SW5E.ArmorProperAbsorptive", -"Agile": "SW5E.ArmorProperAgile", -"Anchor": "SW5E.ArmorProperAnchor", -"Avoidant": "SW5E.ArmorProperAvoidant", -"Barbed": "SW5E.ArmorProperBarbed", -"Bulky": "SW5E.ArmorProperBulky", -"Charging": "SW5E.ArmorProperCharging", -"Concealing": "SW5E.ArmorProperConcealing", -"Cumbersome": "SW5E.ArmorProperCumbersome", -"Gauntleted": "SW5E.ArmorProperGauntleted", -"Imbalanced": "SW5E.ArmorProperImbalanced", -"Impermeable": "SW5E.ArmorProperImpermeable", -"Insulated": "SW5E.ArmorProperInsulated", -"Interlocking": "SW5E.ArmorProperInterlocking", -"Lambent": "SW5E.ArmorProperLambent", -"Lightweight": "SW5E.ArmorProperLightweight", -"Magnetic": "SW5E.ArmorProperMagnetic", -"Obscured": "SW5E.ArmorProperObscured", -"Obtrusive": "SW5E.ArmorProperObtrusive", -"Powered": "SW5E.ArmorProperPowered", -"Reactive": "SW5E.ArmorProperReactive", -"Regulated": "SW5E.ArmorProperRegulated", -"Reinforced": "SW5E.ArmorProperReinforced", -"Responsive": "SW5E.ArmorProperResponsive", -"Rigid": "SW5E.ArmorProperRigid", -"Silent": "SW5E.ArmorProperSilent", -"Spiked": "SW5E.ArmorProperSpiked", -"Strength": "SW5E.ArmorProperStrength", -"Steadfast": "SW5E.ArmorProperSteadfast", -"Versatile": "SW5E.ArmorProperVersatile" + Absorptive: "SW5E.ArmorProperAbsorptive", + Agile: "SW5E.ArmorProperAgile", + Anchor: "SW5E.ArmorProperAnchor", + Avoidant: "SW5E.ArmorProperAvoidant", + Barbed: "SW5E.ArmorProperBarbed", + Bulky: "SW5E.ArmorProperBulky", + Charging: "SW5E.ArmorProperCharging", + Concealing: "SW5E.ArmorProperConcealing", + Cumbersome: "SW5E.ArmorProperCumbersome", + Gauntleted: "SW5E.ArmorProperGauntleted", + Imbalanced: "SW5E.ArmorProperImbalanced", + Impermeable: "SW5E.ArmorProperImpermeable", + Insulated: "SW5E.ArmorProperInsulated", + Interlocking: "SW5E.ArmorProperInterlocking", + Lambent: "SW5E.ArmorProperLambent", + Lightweight: "SW5E.ArmorProperLightweight", + Magnetic: "SW5E.ArmorProperMagnetic", + Obscured: "SW5E.ArmorProperObscured", + Obtrusive: "SW5E.ArmorProperObtrusive", + Powered: "SW5E.ArmorProperPowered", + Reactive: "SW5E.ArmorProperReactive", + Regulated: "SW5E.ArmorProperRegulated", + Reinforced: "SW5E.ArmorProperReinforced", + Responsive: "SW5E.ArmorProperResponsive", + Rigid: "SW5E.ArmorProperRigid", + Silent: "SW5E.ArmorProperSilent", + Spiked: "SW5E.ArmorProperSpiked", + Strength: "SW5E.ArmorProperStrength", + Steadfast: "SW5E.ArmorProperSteadfast", + Versatile: "SW5E.ArmorProperVersatile" }; /** @@ -536,15 +527,15 @@ SW5E.armorPropertiesTypes = { * @type {Object} */ SW5E.movementTypes = { - "burrow": "SW5E.MovementBurrow", - "climb": "SW5E.MovementClimb", - "crawl": "SW5E.MovementCrawl", - "fly": "SW5E.MovementFly", - "roll": "SW5E.MovementRoll", - "space": "SW5E.MovementSpace", - "swim": "SW5E.MovementSwim", - "turn": "SW5E.MovementTurn", - "walk": "SW5E.MovementWalk", + burrow: "SW5E.MovementBurrow", + climb: "SW5E.MovementClimb", + crawl: "SW5E.MovementCrawl", + fly: "SW5E.MovementFly", + roll: "SW5E.MovementRoll", + space: "SW5E.MovementSpace", + swim: "SW5E.MovementSwim", + turn: "SW5E.MovementTurn", + walk: "SW5E.MovementWalk" }; /** @@ -553,8 +544,8 @@ SW5E.movementTypes = { * @type {Object} */ SW5E.movementUnits = { - "ft": "SW5E.DistFt", - "mi": "SW5E.DistMi" + ft: "SW5E.DistFt", + mi: "SW5E.DistMi" }; /** @@ -563,27 +554,26 @@ SW5E.movementUnits = { * @type {Object} */ SW5E.distanceUnits = { - "none": "SW5E.None", - "self": "SW5E.DistSelf", - "touch": "SW5E.DistTouch", - "spec": "SW5E.Special", - "any": "SW5E.DistAny" + none: "SW5E.None", + self: "SW5E.DistSelf", + touch: "SW5E.DistTouch", + spec: "SW5E.Special", + any: "SW5E.DistAny" }; -for ( let [k, v] of Object.entries(SW5E.movementUnits) ) { - SW5E.distanceUnits[k] = v; +for (let [k, v] of Object.entries(SW5E.movementUnits)) { + SW5E.distanceUnits[k] = v; } /* -------------------------------------------- */ - /** * Configure aspects of encumbrance calculation so that it could be configured by modules * @type {Object} */ SW5E.encumbrance = { - currencyPerWeight: 50, - strMultiplier: 15, - vehicleWeightMultiplier: 2000 // 2000 lbs in a ton + currencyPerWeight: 50, + strMultiplier: 15, + vehicleWeightMultiplier: 2000 // 2000 lbs in a ton }; /* -------------------------------------------- */ @@ -593,58 +583,54 @@ SW5E.encumbrance = { * @type {Object} */ SW5E.targetTypes = { - "none": "SW5E.None", - "self": "SW5E.TargetSelf", - "creature": "SW5E.TargetCreature", - "droid": "SW5E.TargetDroid", - "ally": "SW5E.TargetAlly", - "enemy": "SW5E.TargetEnemy", - "object": "SW5E.TargetObject", - "space": "SW5E.TargetSpace", - "radius": "SW5E.TargetRadius", - "sphere": "SW5E.TargetSphere", - "cylinder": "SW5E.TargetCylinder", - "cone": "SW5E.TargetCone", - "square": "SW5E.TargetSquare", - "cube": "SW5E.TargetCube", - "line": "SW5E.TargetLine", - "starship": "SW5E.TargetStarship", - "wall": "SW5E.TargetWall", - "weapon": "SW5E.TargetWeapon" + none: "SW5E.None", + self: "SW5E.TargetSelf", + creature: "SW5E.TargetCreature", + droid: "SW5E.TargetDroid", + ally: "SW5E.TargetAlly", + enemy: "SW5E.TargetEnemy", + object: "SW5E.TargetObject", + space: "SW5E.TargetSpace", + radius: "SW5E.TargetRadius", + sphere: "SW5E.TargetSphere", + cylinder: "SW5E.TargetCylinder", + cone: "SW5E.TargetCone", + square: "SW5E.TargetSquare", + cube: "SW5E.TargetCube", + line: "SW5E.TargetLine", + starship: "SW5E.TargetStarship", + wall: "SW5E.TargetWall", + weapon: "SW5E.TargetWeapon" }; - /* -------------------------------------------- */ - /** * Map the subset of target types which produce a template area of effect * The keys are SW5E target types and the values are MeasuredTemplate shape types * @type {Object} */ SW5E.areaTargetTypes = { - cone: "cone", - cube: "rect", - cylinder: "circle", - line: "ray", - radius: "circle", - sphere: "circle", - square: "rect", - wall: "ray" + cone: "cone", + cube: "rect", + cylinder: "circle", + line: "ray", + radius: "circle", + sphere: "circle", + square: "rect", + wall: "ray" }; - /* -------------------------------------------- */ // Healing Types SW5E.healingTypes = { - "healing": "SW5E.Healing", - "temphp": "SW5E.HealingTemp" + healing: "SW5E.Healing", + temphp: "SW5E.HealingTemp" }; /* -------------------------------------------- */ - /** * Enumerate the denominations of hit dice which can apply to classes in the SW5E system * @type {string[]} @@ -653,54 +639,311 @@ SW5E.hitDieTypes = ["d4", "d6", "d8", "d10", "d12", "d20"]; /* -------------------------------------------- */ - /** * Enumerate the denominations of power dice which can apply to starships in the SW5E system * @enum {string} */ SW5E.powerDieTypes = [1, "d4", "d6", "d8", "d10", "d12"]; - /* -------------------------------------------- */ - /** - * Enumerate the upgrade costs as they apply to starships in the SW5E system based on Tier. + * Enumerate the base stat and feature settings for starships based on size. * @type {Array.} */ -SW5E.baseUpgradeCost = [0, 3900, 77500, 297000, 620000, 1150000]; - -/* -------------------------------------------- */ - - -/** - * Starship Deployment types - */ - - SW5E.deploymentTypes = { - "coord": "SW5E.DeploymentTypeCoordinator", - "gunner": "SW5E.DeploymentTypeGunner", - "mech": "SW5E.DeploymentTypeMechanic", - "oper": "SW5E.DeploymentTypeOperator", - "pilot": "SW5E.DeploymentTypePilot", - "tech": "SW5E.DeploymentTypeTechnician", - "crew": "SW5E.DeploymentTypeCrew", - "pass": "SW5E.DeploymentTypePassenger" +SW5E.baseStarshipSettings = { + tiny: { + changes: [ + {key: "data.abilities.dex.value", value: 4, mode: 2, priority: 20}, + {key: "data.abilities.dex.proficient", value: 1, mode: 4, priority: 20}, + {key: "data.abilities.con.value", value: -4, mode: 2, priority: 20}, + {key: "data.abilities.int.proficient", value: 1, mode: 4, priority: 20} + ], + attributes: { + crewcap: null, + hd: "1d4", + hp: {value: 4, max: 4, temp: 4, tempmax: 4}, + hsm: 1, + sd: "1d4", + mods: {open: 10, max: 10}, + suites: {open: 0, max: 0}, + movement: {fly: 300, turn: 300} + } + }, + sm: { + changes: [ + {key: "data.abilities.dex.value", value: 2, mode: 2, priority: 20}, + {key: "data.abilities.dex.proficient", value: 1, mode: 4, priority: 20}, + {key: "data.abilities.con.value", value: -2, mode: 2, priority: 20}, + {key: "data.abilities.str.proficient", value: 1, mode: 4, priority: 20} + ], + attributes: { + crewcap: 1, + hd: "3d6", + hp: {value: 6, max: 6, temp: 6, tempmax: 6}, + hsm: 2, + sd: "3d6", + mods: {open: 20, max: 20}, + suites: {open: -1, max: -1}, + movement: {fly: 300, turn: 250} + } + }, + med: { + attributes: { + crewcap: 1, + hd: "5d8", + hp: {value: 8, max: 8, temp: 8, tempmax: 8}, + hsm: 3, + sd: "5d8", + mods: {open: 30, max: 30}, + suites: {open: 3, max: 3}, + movement: {fly: 300, turn: 200} + } + }, + lg: { + changes: [ + {key: "data.abilities.dex.value", value: -2, mode: 2, priority: 20}, + {key: "data.abilities.wis.proficient", value: 1, mode: 4, priority: 20}, + {key: "data.abilities.con.value", value: 2, mode: 2, priority: 20} + ], + attributes: { + crewcap: 200, + hd: "7d10", + hp: {value: 10, max: 10, temp: 10, tempmax: 10}, + hsm: 4, + sd: "7d10", + mods: {open: 50, max: 50}, + suites: {open: 3, max: 3}, + movement: {fly: 300, turn: 150} + } + }, + huge: { + changes: [ + {key: "data.abilities.dex.value", value: -4, mode: 2, priority: 20}, + {key: "data.abilities.wis.proficient", value: 1, mode: 4, priority: 20}, + {key: "data.abilities.con.value", value: 4, mode: 2, priority: 20} + ], + attributes: { + crewcap: 4000, + hd: "9d12", + hp: {value: 12, max: 12, temp: 12, tempmax: 12}, + hsm: 2, + sd: "9d12", + mods: {open: 60, max: 60}, + suites: {open: 6, max: 6}, + movement: {fly: 300, turn: 100} + } + }, + grg: { + changes: [ + {key: "data.abilities.dex.value", value: -6, mode: 2, priority: 20}, + {key: "data.abilities.wis.proficient", value: 1, mode: 4, priority: 20}, + {key: "data.abilities.con.value", value: 6, mode: 2, priority: 20} + ], + attributes: { + crewcap: 80000, + hd: "11d20", + hp: {value: 20, max: 20, temp: 20, tempmax: 20}, + hsm: 3, + sd: "11d20", + mods: {open: 70, max: 70}, + suites: {open: 10, max: 10}, + movement: {fly: 300, turn: 50} + } + } }; /* -------------------------------------------- */ +/** + * The set of starship roles which can be selected in SW5e + * @type {Object} + */ + +SW5E.starshipRolestiny = {}; +SW5E.starshipRolessm = { + bmbr: "SW5E.StarshipBomber", + intc: "SW5E.StarshipInterceptor", + scout: "SW5E.StarshipScout", + scrm: "SW5E.StarshipScrambler", + shtl: "SW5E.StarshipShuttle", + strf: "SW5E.StarshipStrikeFighter" +}; +SW5E.starshipRolesmed = { + cour: "SW5E.StarshipCourier", + frtr: "SW5E.StarshipFreighter", + gnbt: "SW5E.StarshipGunboat", + msbt: "SW5E.StarshipMissileBoat", + nvgt: "SW5E.StarshipNavigator", + yacht: "SW5E.StarshipYacht" +}; +SW5E.starshipRoleslg = { + ambd: "SW5E.StarshipAmbassador", + corv: "SW5E.StarshipCorvette", + crui: "SW5E.StarshipCruiser", + expl: "SW5E.StarshipExplorer", + pics: "SW5E.StarshipPicketShip", + shtd: "SW5E.StarshipShipsTender" +}; +SW5E.starshipRoleshuge = { + btls: "SW5E.StarshipBattleship", + carr: "SW5E.StarshipCarrier", + colo: "SW5E.StarshipColonizer", + cmds: "SW5E.StarshipCommandShip", + intd: "SW5E.StarshipInterdictor", + jugg: "SW5E.StarshipJuggernaut" +}; +SW5E.starshipRolesgrg = { + blks: "SW5E.StarshipBlockadeShip", + flgs: "SW5E.StarshipFlagship", + inct: "SW5E.StarshipIndustrialCenter", + mbmt: "SW5E.StarshipMobileMetropolis", + rsrc: "SW5E.StarshipResearcher", + wars: "SW5E.StarshipWarship" +}; + +/* -------------------------------------------- */ + +/** + * The set of starship role bonuses to starships which can be selected in SW5e + * @type {Object} + */ + +SW5E.starshipRoleBonuses = { + bmbr: {changes: [{key: "data.abilities.wis.value", value: 1, mode: 2, priority: 20}]}, + intc: {changes: [{key: "data.abilities.dex.value", value: 1, mode: 2, priority: 20}]}, + scout: {changes: [{key: "data.abilities.int.value", value: 1, mode: 2, priority: 20}]}, + scrm: {changes: [{key: "data.abilities.cha.value", value: 1, mode: 2, priority: 20}]}, + shtl: {changes: [{key: "data.abilities.con.value", value: 1, mode: 2, priority: 20}]}, + strf: {changes: [{key: "data.abilities.str.value", value: 1, mode: 2, priority: 20}]}, + cour: {changes: [{key: "data.abilities.dex.value", value: 1, mode: 2, priority: 20}]}, + frtr: {changes: [{key: "data.abilities.con.value", value: 1, mode: 2, priority: 20}]}, + gnbt: {changes: [{key: "data.abilities.str.value", value: 1, mode: 2, priority: 20}]}, + msbt: {changes: [{key: "data.abilities.wis.value", value: 1, mode: 2, priority: 20}]}, + nvgt: {changes: [{key: "data.abilities.int.value", value: 1, mode: 2, priority: 20}]}, + yacht: {changes: [{key: "data.abilities.cha.value", value: 1, mode: 2, priority: 20}]}, + ambd: { + changes: [ + {key: "data.abilities.cha.value", value: 1, mode: 2, priority: 20}, + {key: "data.abilities.con.value", value: 1, mode: 2, priority: 20} + ] + }, + corv: { + changes: [ + {key: "data.abilities.str.value", value: 1, mode: 2, priority: 20}, + {key: "data.abilities.dex.value", value: 1, mode: 2, priority: 20} + ] + }, + crui: { + changes: [ + {key: "data.abilities.wis.value", value: 1, mode: 2, priority: 20}, + {key: "data.abilities.str.value", value: 1, mode: 2, priority: 20} + ] + }, + expl: { + changes: [ + {key: "data.abilities.dex.value", value: 1, mode: 2, priority: 20}, + {key: "data.abilities.int.value", value: 1, mode: 2, priority: 20} + ] + }, + pics: { + changes: [ + {key: "data.abilities.dex.value", value: 1, mode: 2, priority: 20}, + {key: "data.abilities.wis.value", value: 1, mode: 2, priority: 20} + ] + }, + shtd: { + changes: [ + {key: "data.abilities.con.value", value: 1, mode: 2, priority: 20}, + {key: "data.abilities.str.value", value: 1, mode: 2, priority: 20} + ] + }, + btls: { + changes: [ + {key: "data.abilities.wis.value", value: 1, mode: 2, priority: 20}, + {key: "data.abilities.str.value", value: 1, mode: 2, priority: 20} + ] + }, + carr: { + changes: [ + {key: "data.abilities.cha.value", value: 1, mode: 2, priority: 20}, + {key: "data.abilities.int.value", value: 1, mode: 2, priority: 20} + ] + }, + colo: { + changes: [ + {key: "data.abilities.con.value", value: 1, mode: 2, priority: 20}, + {key: "data.abilities.int.value", value: 1, mode: 2, priority: 20} + ] + }, + cmds: { + changes: [ + {key: "data.abilities.cha.value", value: 1, mode: 2, priority: 20}, + {key: "data.abilities.wis.value", value: 1, mode: 2, priority: 20} + ] + }, + intd: { + changes: [ + {key: "data.abilities.dex.value", value: 1, mode: 2, priority: 20}, + {key: "data.abilities.str.value", value: 1, mode: 2, priority: 20} + ] + }, + jugg: { + changes: [ + {key: "data.abilities.con.value", value: 1, mode: 2, priority: 20}, + {key: "data.abilities.str.value", value: 1, mode: 2, priority: 20} + ] + }, + blks: { + changes: [ + {key: "data.abilities.dex.value", value: 1, mode: 2, priority: 20}, + {key: "data.abilities.str.value", value: 1, mode: 2, priority: 20} + ] + }, + flgs: { + changes: [ + {key: "data.abilities.cha.value", value: 1, mode: 2, priority: 20}, + {key: "data.abilities.wis.value", value: 1, mode: 2, priority: 20} + ] + }, + inct: { + changes: [ + {key: "data.abilities.con.value", value: 1, mode: 2, priority: 20}, + {key: "data.abilities.str.value", value: 1, mode: 2, priority: 20} + ] + }, + mbmt: { + changes: [ + {key: "data.abilities.con.value", value: 1, mode: 2, priority: 20}, + {key: "data.abilities.wis.value", value: 1, mode: 2, priority: 20} + ] + }, + rsrc: { + changes: [ + {key: "data.abilities.int.value", value: 1, mode: 2, priority: 20}, + {key: "data.abilities.wis.value", value: 1, mode: 2, priority: 20} + ] + }, + wars: { + changes: [ + {key: "data.abilities.wis.value", value: 1, mode: 2, priority: 20}, + {key: "data.abilities.str.value", value: 1, mode: 2, priority: 20} + ] + } +}; + +/* -------------------------------------------- */ /** * The set of possible sensory perception types which an Actor may have * @enum {string} */ SW5E.senses = { - "blindsight": "SW5E.SenseBlindsight", - "darkvision": "SW5E.SenseDarkvision", - "tremorsense": "SW5E.SenseTremorsense", - "truesight": "SW5E.SenseTruesight" + blindsight: "SW5E.SenseBlindsight", + darkvision: "SW5E.SenseDarkvision", + tremorsense: "SW5E.SenseTremorsense", + truesight: "SW5E.SenseTruesight" }; /* -------------------------------------------- */ @@ -710,24 +953,24 @@ SW5E.senses = { * @type {Object} */ SW5E.skills = { - "acr": "SW5E.SkillAcr", - "ani": "SW5E.SkillAni", - "ath": "SW5E.SkillAth", - "dec": "SW5E.SkillDec", - "ins": "SW5E.SkillIns", - "itm": "SW5E.SkillItm", - "inv": "SW5E.SkillInv", - "lor": "SW5E.SkillLor", - "med": "SW5E.SkillMed", - "nat": "SW5E.SkillNat", - "prc": "SW5E.SkillPrc", - "prf": "SW5E.SkillPrf", - "per": "SW5E.SkillPer", - "pil": "SW5E.SkillPil", - "slt": "SW5E.SkillSlt", - "ste": "SW5E.SkillSte", - "sur": "SW5E.SkillSur", - "tec": "SW5E.SkillTec" + acr: "SW5E.SkillAcr", + ani: "SW5E.SkillAni", + ath: "SW5E.SkillAth", + dec: "SW5E.SkillDec", + ins: "SW5E.SkillIns", + itm: "SW5E.SkillItm", + inv: "SW5E.SkillInv", + lor: "SW5E.SkillLor", + med: "SW5E.SkillMed", + nat: "SW5E.SkillNat", + prc: "SW5E.SkillPrc", + prf: "SW5E.SkillPrf", + per: "SW5E.SkillPer", + pil: "SW5E.SkillPil", + slt: "SW5E.SkillSlt", + ste: "SW5E.SkillSte", + sur: "SW5E.SkillSur", + tec: "SW5E.SkillTec" }; /* -------------------------------------------- */ @@ -737,29 +980,29 @@ SW5E.skills = { * @type {Object} */ SW5E.starshipSkills = { - "ast": "SW5E.StarshipSkillAst", - "bst": "SW5E.StarshipSkillBst", - "dat": "SW5E.StarshipSkillDat", - "hid": "SW5E.StarshipSkillHid", - "imp": "SW5E.StarshipSkillImp", - "inf": "SW5E.StarshipSkillInf", - "man": "SW5E.StarshipSkillMan", - "men": "SW5E.StarshipSkillMen", - "pat": "SW5E.StarshipSkillPat", - "prb": "SW5E.StarshipSkillPrb", - "ram": "SW5E.StarshipSkillRam", - "reg": "SW5E.StarshipSkillReg", - "scn": "SW5E.StarshipSkillScn", - "swn": "SW5E.StarshipSkillSwn" + ast: "SW5E.StarshipSkillAst", + bst: "SW5E.StarshipSkillBst", + dat: "SW5E.StarshipSkillDat", + hid: "SW5E.StarshipSkillHid", + imp: "SW5E.StarshipSkillImp", + int: "SW5E.StarshipSkillInt", + man: "SW5E.StarshipSkillMan", + men: "SW5E.StarshipSkillMen", + pat: "SW5E.StarshipSkillPat", + prb: "SW5E.StarshipSkillPrb", + ram: "SW5E.StarshipSkillRam", + reg: "SW5E.StarshipSkillReg", + scn: "SW5E.StarshipSkillScn", + swn: "SW5E.StarshipSkillSwn" }; /* -------------------------------------------- */ SW5E.powerPreparationModes = { - "prepared": "SW5E.PowerPrepPrepared", - "always": "SW5E.PowerPrepAlways", - "atwill": "SW5E.PowerPrepAtWill", - "innate": "SW5E.PowerPrepInnate" + prepared: "SW5E.PowerPrepPrepared", + always: "SW5E.PowerPrepAlways", + atwill: "SW5E.PowerPrepAtWill", + innate: "SW5E.PowerPrepInnate" }; SW5E.powerUpcastModes = ["always", "prepared"]; @@ -770,12 +1013,12 @@ SW5E.powerUpcastModes = ["always", "prepared"]; */ SW5E.powerProgression = { - "none": "SW5E.PowerNone", - "consular": "SW5E.PowerProgCns", - "engineer": "SW5E.PowerProgEng", - "guardian": "SW5E.PowerProgGrd", - "scout": "SW5E.PowerProgSct", - "sentinel": "SW5E.PowerProgSnt" + none: "SW5E.PowerNone", + consular: "SW5E.PowerProgCns", + engineer: "SW5E.PowerProgEng", + guardian: "SW5E.PowerProgGrd", + scout: "SW5E.PowerProgSct", + sentinel: "SW5E.PowerProgSnt" }; /** @@ -783,12 +1026,12 @@ SW5E.powerProgression = { */ SW5E.powersKnown = { - "none": [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], - "consular": [9,11,13,15,17,19,21,23,25,26,28,29,31,32,34,35,37,38,39,40], - "engineer": [6,7,9,10,12,13,15,16,18,19,21,22,23,24,25,26,27,28,29,30], - "guardian": [5,7,9,10,12,13,14,15,17,18,19,20,22,23,24,25,27,28,29,30], - "scout": [0,4,5,6,7,8,9,10,12,13,14,15,16,17,18,19,20,21,22,23], - "sentinel": [7,9,11,13,15,17,18,19,21,22,24,25,26,28,29,30,32,33,34,35] + none: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + consular: [9, 11, 13, 15, 17, 19, 21, 23, 25, 26, 28, 29, 31, 32, 34, 35, 37, 38, 39, 40], + engineer: [6, 7, 9, 10, 12, 13, 15, 16, 18, 19, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30], + guardian: [5, 7, 9, 10, 12, 13, 14, 15, 17, 18, 19, 20, 22, 23, 24, 25, 27, 28, 29, 30], + scout: [0, 4, 5, 6, 7, 8, 9, 10, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23], + sentinel: [7, 9, 11, 13, 15, 17, 18, 19, 21, 22, 24, 25, 26, 28, 29, 30, 32, 33, 34, 35] }; /** @@ -796,14 +1039,14 @@ SW5E.powersKnown = { */ SW5E.powerLimit = { - "none": [0,0,0,0,0,0,0,0,0], - "consular": [1000,1000,1000,1000,1000,1,1,1,1], - "engineer": [1000,1000,1000,1000,1000,1,1,1,1], - "guardian": [1000,1000,1000,1000,1,0,0,0,0], - "scout": [1000,1000,1000,1,1,0,0,0,0], - "sentinel": [1000,1000,1000,1000,1,1,1,0,0], - "innate": [1000,1000,1000,1000,1000,1000,1000,1000,1000], - "dual": [1000,1000,1000,1000,1000,1,1,1,1] + none: [0, 0, 0, 0, 0, 0, 0, 0, 0], + consular: [1000, 1000, 1000, 1000, 1000, 1, 1, 1, 1], + engineer: [1000, 1000, 1000, 1000, 1000, 1, 1, 1, 1], + guardian: [1000, 1000, 1000, 1000, 1, 0, 0, 0, 0], + scout: [1000, 1000, 1000, 1, 1, 0, 0, 0, 0], + sentinel: [1000, 1000, 1000, 1000, 1, 1, 1, 0, 0], + innate: [1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000], + dual: [1000, 1000, 1000, 1000, 1000, 1, 1, 1, 1] }; /** @@ -811,15 +1054,15 @@ SW5E.powerLimit = { */ SW5E.powerMaxLevel = { - "none": [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], - "consular": [1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,9,9], - "engineer": [1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,9,9], - "guardian": [1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5], - "scout": [0,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5], - "sentinel": [1,1,2,2,2,3,3,3,4,4,5,5,5,6,6,6,7,7,7,7], - "multi": [1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,9,9], - "innate": [1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,9,9], - "dual": [1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,9,9] + none: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + consular: [1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 9, 9], + engineer: [1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 9, 9], + guardian: [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5], + scout: [0, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5], + sentinel: [1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 7], + multi: [1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 9, 9], + innate: [1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 9, 9], + dual: [1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 9, 9] }; /** @@ -827,12 +1070,12 @@ SW5E.powerMaxLevel = { */ SW5E.powerPoints = { - "none": [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0], - "consular": [4,8,12,16,20,24,28,32,36,40,44,48,52,56,60,64,68,72,76,80], - "engineer": [2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40], - "guardian": [2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40], - "scout": [0,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20], - "sentinel": [3,6,9,12,15,18,21,24,27,30,33,36,39,42,45,48,51,54,57,60] + none: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + consular: [4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 68, 72, 76, 80], + engineer: [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40], + guardian: [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40], + scout: [0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], + sentinel: [3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57, 60] }; /* -------------------------------------------- */ @@ -842,37 +1085,34 @@ SW5E.powerPoints = { * @type {Object} */ SW5E.powerScalingModes = { - "none": "SW5E.PowerNone", - "atwill": "SW5E.PowerAtWill", - "level": "SW5E.PowerLevel" + none: "SW5E.PowerNone", + atwill: "SW5E.PowerAtWill", + level: "SW5E.PowerLevel" }; /* -------------------------------------------- */ - /** * Define the set of types which a weapon item can take * @type {Object} */ SW5E.weaponTypes = { - - "ammo": "SW5E.WeaponAmmo", - "improv": "SW5E.WeaponImprov", - "martialVW": "SW5E.WeaponMartialVW", - "martialB": "SW5E.WeaponMartialB", - "martialLW": "SW5E.WeaponMartialLW", - "natural": "SW5E.WeaponNatural", - "siege": "SW5E.WeaponSiege", - "simpleVW": "SW5E.WeaponSimpleVW", - "simpleB": "SW5E.WeaponSimpleB", - "simpleLW": "SW5E.WeaponSimpleLW", - "primary (starship)": "SW5E.WeaponPrimarySW", - "secondary (starship)": "SW5E.WeaponSecondarySW", - "tertiary (starship)": "SW5E.WeaponTertiarySW", - "quaternary (starship)": "SW5E.WeaponQuaternarySW" + "ammo": "SW5E.WeaponAmmo", + "improv": "SW5E.WeaponImprov", + "martialVW": "SW5E.WeaponMartialVW", + "martialB": "SW5E.WeaponMartialB", + "martialLW": "SW5E.WeaponMartialLW", + "natural": "SW5E.WeaponNatural", + "siege": "SW5E.WeaponSiege", + "simpleVW": "SW5E.WeaponSimpleVW", + "simpleB": "SW5E.WeaponSimpleB", + "simpleLW": "SW5E.WeaponSimpleLW", + "primary (starship)": "SW5E.WeaponPrimarySW", + "secondary (starship)": "SW5E.WeaponSecondarySW", + "tertiary (starship)": "SW5E.WeaponTertiarySW", + "quaternary (starship)": "SW5E.WeaponQuaternarySW" }; - /* -------------------------------------------- */ /** @@ -880,48 +1120,48 @@ SW5E.weaponTypes = { * @type {Object} */ SW5E.weaponProperties = { - "amm": "SW5E.WeaponPropertiesAmm", - "aut": "SW5E.WeaponPropertiesAut", - "bur": "SW5E.WeaponPropertiesBur", - "con": "SW5E.WeaponPropertiesCon", - "def": "SW5E.WeaponPropertiesDef", - "dex": "SW5E.WeaponPropertiesDex", - "dir": "SW5E.WeaponPropertiesDir", - "drm": "SW5E.WeaponPropertiesDrm", - "dgd": "SW5E.WeaponPropertiesDgd", - "dis": "SW5E.WeaponPropertiesDis", - "dpt": "SW5E.WeaponPropertiesDpt", - "dou": "SW5E.WeaponPropertiesDou", - "exp": "SW5E.WeaponPropertiesExp", - "fin": "SW5E.WeaponPropertiesFin", - "fix": "SW5E.WeaponPropertiesFix", - "foc": "SW5E.WeaponPropertiesFoc", - "hvy": "SW5E.WeaponPropertiesHvy", - "hid": "SW5E.WeaponPropertiesHid", - "hom": "SW5E.WeaponPropertiesHom", - "ion": "SW5E.WeaponPropertiesIon", - "ken": "SW5E.WeaponPropertiesKen", - "lgt": "SW5E.WeaponPropertiesLgt", - "lum": "SW5E.WeaponPropertiesLum", - "mlt": "SW5E.WeaponPropertiesMlt", - "mig": "SW5E.WeaponPropertiesMig", - "ovr": "SW5E.WeaponPropertiesOvr", - "pic": "SW5E.WeaponPropertiesPic", - "pow": "SW5E.WeaponPropertiesPow", - "rap": "SW5E.WeaponPropertiesRap", - "rch": "SW5E.WeaponPropertiesRch", - "rel": "SW5E.WeaponPropertiesRel", - "ret": "SW5E.WeaponPropertiesRet", - "sat": "SW5E.WeaponPropertiesSat", - "shk": "SW5E.WeaponPropertiesShk", - "sil": "SW5E.WeaponPropertiesSil", - "spc": "SW5E.WeaponPropertiesSpc", - "str": "SW5E.WeaponPropertiesStr", - "thr": "SW5E.WeaponPropertiesThr", - "two": "SW5E.WeaponPropertiesTwo", - "ver": "SW5E.WeaponPropertiesVer", - "vic": "SW5E.WeaponPropertiesVic", - "zon": "SW5E.WeaponPropertiesZon" + amm: "SW5E.WeaponPropertiesAmm", + aut: "SW5E.WeaponPropertiesAut", + bur: "SW5E.WeaponPropertiesBur", + con: "SW5E.WeaponPropertiesCon", + def: "SW5E.WeaponPropertiesDef", + dex: "SW5E.WeaponPropertiesDex", + dir: "SW5E.WeaponPropertiesDir", + drm: "SW5E.WeaponPropertiesDrm", + dgd: "SW5E.WeaponPropertiesDgd", + dis: "SW5E.WeaponPropertiesDis", + dpt: "SW5E.WeaponPropertiesDpt", + dou: "SW5E.WeaponPropertiesDou", + exp: "SW5E.WeaponPropertiesExp", + fin: "SW5E.WeaponPropertiesFin", + fix: "SW5E.WeaponPropertiesFix", + foc: "SW5E.WeaponPropertiesFoc", + hvy: "SW5E.WeaponPropertiesHvy", + hid: "SW5E.WeaponPropertiesHid", + hom: "SW5E.WeaponPropertiesHom", + ion: "SW5E.WeaponPropertiesIon", + ken: "SW5E.WeaponPropertiesKen", + lgt: "SW5E.WeaponPropertiesLgt", + lum: "SW5E.WeaponPropertiesLum", + mlt: "SW5E.WeaponPropertiesMlt", + mig: "SW5E.WeaponPropertiesMig", + ovr: "SW5E.WeaponPropertiesOvr", + pic: "SW5E.WeaponPropertiesPic", + pow: "SW5E.WeaponPropertiesPow", + rap: "SW5E.WeaponPropertiesRap", + rch: "SW5E.WeaponPropertiesRch", + rel: "SW5E.WeaponPropertiesRel", + ret: "SW5E.WeaponPropertiesRet", + sat: "SW5E.WeaponPropertiesSat", + shk: "SW5E.WeaponPropertiesShk", + sil: "SW5E.WeaponPropertiesSil", + spc: "SW5E.WeaponPropertiesSpc", + str: "SW5E.WeaponPropertiesStr", + thr: "SW5E.WeaponPropertiesThr", + two: "SW5E.WeaponPropertiesTwo", + ver: "SW5E.WeaponPropertiesVer", + vic: "SW5E.WeaponPropertiesVic", + zon: "SW5E.WeaponPropertiesZon" }; /* -------------------------------------------- */ @@ -931,42 +1171,42 @@ SW5E.weaponProperties = { * @type {Object} */ SW5E.weaponSizes = { - "tiny": "SW5E.SizeTiny", - "sm": "SW5E.SizeSmall", - "med": "SW5E.SizeMedium", - "lg": "SW5E.SizeLarge", - "huge": "SW5E.SizeHuge", - "grg": "SW5E.SizeGargantuan" -}; + tiny: "SW5E.SizeTiny", + sm: "SW5E.SizeSmall", + med: "SW5E.SizeMedium", + lg: "SW5E.SizeLarge", + huge: "SW5E.SizeHuge", + grg: "SW5E.SizeGargantuan" +}; // Power Components SW5E.powerComponents = { - "V": "SW5E.ComponentVerbal", - "S": "SW5E.ComponentSomatic", - "M": "SW5E.ComponentMaterial" + V: "SW5E.ComponentVerbal", + S: "SW5E.ComponentSomatic", + M: "SW5E.ComponentMaterial" }; // Power Schools SW5E.powerSchools = { - "lgt": "SW5E.SchoolLgt", - "uni": "SW5E.SchoolUni", - "drk": "SW5E.SchoolDrk", - "tec": "SW5E.SchoolTec", - "enh": "SW5E.SchoolEnh" + lgt: "SW5E.SchoolLgt", + uni: "SW5E.SchoolUni", + drk: "SW5E.SchoolDrk", + tec: "SW5E.SchoolTec", + enh: "SW5E.SchoolEnh" }; // Power Levels SW5E.powerLevels = { - 0: "SW5E.PowerLevel0", - 1: "SW5E.PowerLevel1", - 2: "SW5E.PowerLevel2", - 3: "SW5E.PowerLevel3", - 4: "SW5E.PowerLevel4", - 5: "SW5E.PowerLevel5", - 6: "SW5E.PowerLevel6", - 7: "SW5E.PowerLevel7", - 8: "SW5E.PowerLevel8", - 9: "SW5E.PowerLevel9" + 0: "SW5E.PowerLevel0", + 1: "SW5E.PowerLevel1", + 2: "SW5E.PowerLevel2", + 3: "SW5E.PowerLevel3", + 4: "SW5E.PowerLevel4", + 5: "SW5E.PowerLevel5", + 6: "SW5E.PowerLevel6", + 7: "SW5E.PowerLevel7", + 8: "SW5E.PowerLevel8", + 9: "SW5E.PowerLevel9" }; // TODO: This is used for spell scrolls, it maps the level to the compendium ID of the item the spell would be bound to @@ -992,23 +1232,23 @@ SW5E.powerScrollIds = { * @enum {string} */ SW5E.sourcePacks = { - ITEMS: "sw5e.items" -} + ITEMS: "sw5e.items" +}; // Polymorph options. SW5E.polymorphSettings = { - keepPhysical: 'SW5E.PolymorphKeepPhysical', - keepMental: 'SW5E.PolymorphKeepMental', - keepSaves: 'SW5E.PolymorphKeepSaves', - keepSkills: 'SW5E.PolymorphKeepSkills', - mergeSaves: 'SW5E.PolymorphMergeSaves', - mergeSkills: 'SW5E.PolymorphMergeSkills', - keepClass: 'SW5E.PolymorphKeepClass', - keepFeats: 'SW5E.PolymorphKeepFeats', - keepPowers: 'SW5E.PolymorphKeepPowers', - keepItems: 'SW5E.PolymorphKeepItems', - keepBio: 'SW5E.PolymorphKeepBio', - keepVision: 'SW5E.PolymorphKeepVision' + keepPhysical: "SW5E.PolymorphKeepPhysical", + keepMental: "SW5E.PolymorphKeepMental", + keepSaves: "SW5E.PolymorphKeepSaves", + keepSkills: "SW5E.PolymorphKeepSkills", + mergeSaves: "SW5E.PolymorphMergeSaves", + mergeSkills: "SW5E.PolymorphMergeSkills", + keepClass: "SW5E.PolymorphKeepClass", + keepFeats: "SW5E.PolymorphKeepFeats", + keepPowers: "SW5E.PolymorphKeepPowers", + keepItems: "SW5E.PolymorphKeepItems", + keepBio: "SW5E.PolymorphKeepBio", + keepVision: "SW5E.PolymorphKeepVision" }; /* -------------------------------------------- */ @@ -1019,10 +1259,10 @@ SW5E.polymorphSettings = { * @type {Object} */ SW5E.proficiencyLevels = { - 0: "SW5E.NotProficient", - 1: "SW5E.Proficient", - 0.5: "SW5E.HalfProficient", - 2: "SW5E.Expertise" + 0: "SW5E.NotProficient", + 1: "SW5E.Proficient", + 0.5: "SW5E.HalfProficient", + 2: "SW5E.Expertise" }; /* -------------------------------------------- */ @@ -1033,157 +1273,156 @@ SW5E.proficiencyLevels = { * in play, we take the highest value. */ SW5E.cover = { - 0: 'SW5E.None', - .5: 'SW5E.CoverHalf', - .75: 'SW5E.CoverThreeQuarters', - 1: 'SW5E.CoverTotal' + 0: "SW5E.None", + 0.5: "SW5E.CoverHalf", + 0.75: "SW5E.CoverThreeQuarters", + 1: "SW5E.CoverTotal" }; /* -------------------------------------------- */ - // Condition Types SW5E.conditionTypes = { - "blinded": "SW5E.ConBlinded", - "charmed": "SW5E.ConCharmed", - "deafened": "SW5E.ConDeafened", - "diseased": "SW5E.ConDiseased", - "exhaustion": "SW5E.ConExhaustion", - "frightened": "SW5E.ConFrightened", - "grappled": "SW5E.ConGrappled", - "incapacitated": "SW5E.ConIncapacitated", - "invisible": "SW5E.ConInvisible", - "paralyzed": "SW5E.ConParalyzed", - "petrified": "SW5E.ConPetrified", - "poisoned": "SW5E.ConPoisoned", - "prone": "SW5E.ConProne", - "restrained": "SW5E.ConRestrained", - "shocked": "SW5E.ConShocked", - "slowed": "SW5E.ConSlowed", - "stunned": "SW5E.ConStunned", - "unconscious": "SW5E.ConUnconscious" + blinded: "SW5E.ConBlinded", + charmed: "SW5E.ConCharmed", + deafened: "SW5E.ConDeafened", + diseased: "SW5E.ConDiseased", + exhaustion: "SW5E.ConExhaustion", + frightened: "SW5E.ConFrightened", + grappled: "SW5E.ConGrappled", + incapacitated: "SW5E.ConIncapacitated", + invisible: "SW5E.ConInvisible", + paralyzed: "SW5E.ConParalyzed", + petrified: "SW5E.ConPetrified", + poisoned: "SW5E.ConPoisoned", + prone: "SW5E.ConProne", + restrained: "SW5E.ConRestrained", + shocked: "SW5E.ConShocked", + slowed: "SW5E.ConSlowed", + stunned: "SW5E.ConStunned", + unconscious: "SW5E.ConUnconscious" }; // Languages SW5E.languages = { - "abyssin": "SW5E.LanguagesAbyssin", - "aleena": "SW5E.LanguagesAleena", - "antarian": "SW5E.LanguagesAntarian", - "anzellan": "SW5E.LanguagesAnzellan", - "aqualish": "SW5E.LanguagesAqualish", - "arconese": "SW5E.LanguagesArconese", - "ardennian": "SW5E.LanguagesArdennian", - "arkanian": "SW5E.LanguagesArkanian", - "balosur": "SW5E.LanguagesBalosur", - "barabel": "SW5E.LanguagesBarabel", - "basic": "SW5E.LanguagesBasic", - "besalisk": "SW5E.LanguagesBesalisk", - "binary": "SW5E.LanguagesBinary", - "bith": "SW5E.LanguagesBith", - "bocce": "SW5E.LanguagesBocce", - "bothese": "SW5E.LanguagesBothese", - "catharese": "SW5E.LanguagesCatharese", - "cerean": "SW5E.LanguagesCerean", - "chadra-fan": "SW5E.LanguagesChadra-Fan", - "chagri": "SW5E.LanguagesChagri", - "cheunh": "SW5E.LanguagesCheunh", - "chevin": "SW5E.LanguagesChevin", - "chironan": "SW5E.LanguagesChironan", - "clawdite": "SW5E.LanguagesClawdite", - "codruese": "SW5E.LanguagesCodruese", - "colicoid": "SW5E.LanguagesColicoid", - "dashadi": "SW5E.LanguagesDashadi", - "defel": "SW5E.LanguagesDefel", - "devaronese": "SW5E.LanguagesDevaronese", - "dosh": "SW5E.LanguagesDosh", - "draethos": "SW5E.LanguagesDraethos", - "durese": "SW5E.LanguagesDurese", - "dug": "SW5E.LanguagesDug", - "ewokese": "SW5E.LanguagesEwokese", - "falleen": "SW5E.LanguagesFalleen", - "felucianese": "SW5E.LanguagesFelucianese", - "gamorrese": "SW5E.LanguagesGamorrese", - "gand": "SW5E.LanguagesGand", - "geonosian": "SW5E.LanguagesGeonosian", - "givin": "SW5E.LanguagesGivin", - "gran": "SW5E.LanguagesGran", - "gungan": "SW5E.LanguagesGungan", - "hapan": "SW5E.LanguagesHapan", - "harchese": "SW5E.LanguagesHarchese", - "herglese": "SW5E.LanguagesHerglese", - "honoghran": "SW5E.LanguagesHonoghran", - "huttese": "SW5E.LanguagesHuttese", - "iktotchese": "SW5E.LanguagesIktotchese", - "ithorese": "SW5E.LanguagesIthorese", - "jawaese": "SW5E.LanguagesJawaese", - "kaleesh": "SW5E.LanguagesKaleesh", - "kaminoan": "SW5E.LanguagesKaminoan", - "karkaran": "SW5E.LanguagesKarkaran", - "keldor": "SW5E.LanguagesKelDor", - "kharan": "SW5E.LanguagesKharan", - "killik": "SW5E.LanguagesKillik", - "klatooinian": "SW5E.LanguagesKlatooinian", - "kubazian": "SW5E.LanguagesKubazian", - "kushiban": "SW5E.LanguagesKushiban", - "kyuzo": "SW5E.LanguagesKyuzo", - "lannik": "SW5E.LanguagesLannik", - "lasat": "SW5E.LanguagesLasat", - "lowickese": "SW5E.LanguagesLowickese", - "lurmese": "SW5E.LanguagesLurmese", - "mandoa": "SW5E.LanguagesMandoa", - "miralukese": "SW5E.LanguagesMiralukese", - "mirialan": "SW5E.LanguagesMirialan", - "moncal": "SW5E.LanguagesMonCal", - "mustafarian": "SW5E.LanguagesMustafarian", - "muun": "SW5E.LanguagesMuun", - "nautila": "SW5E.LanguagesNautila", - "ortolan": "SW5E.LanguagesOrtolan", - "pakpak": "SW5E.LanguagesPakPak", - "pyke": "SW5E.LanguagesPyke", - "quarrenese": "SW5E.LanguagesQuarrenese", - "rakata": "SW5E.LanguagesRakata", - "rattataki": "SW5E.LanguagesRattataki", - "rishii": "SW5E.LanguagesRishii", - "rodese": "SW5E.LanguagesRodese", - "ryn": "SW5E.LanguagesRyn", - "selkatha": "SW5E.LanguagesSelkatha", - "semblan": "SW5E.LanguagesSemblan", - "shistavanen": "SW5E.LanguagesShistavanen", - "shyriiwook": "SW5E.LanguagesShyriiwook", - "sith": "SW5E.LanguagesSith", - "squibbian": "SW5E.LanguagesSquibbian", - "sriluurian": "SW5E.LanguagesSriluurian", - "ssi-ruuvi": "SW5E.LanguagesSsi-ruuvi", - "sullustese": "SW5E.LanguagesSullustese", - "talzzi": "SW5E.LanguagesTalzzi", - "tarasinese": "SW5E.LanguagesTarasinese", - "thisspiasian": "SW5E.LanguagesThisspiasian", - "togorese": "SW5E.LanguagesTogorese", - "togruti": "SW5E.LanguagesTogruti", - "toydarian": "SW5E.LanguagesToydarian", - "tusken": "SW5E.LanguagesTusken", - "twi'leki": "SW5E.LanguagesTwileki", - "ugnaught": "SW5E.LanguagesUgnaught", - "umbaran": "SW5E.LanguagesUmbaran", - "utapese": "SW5E.LanguagesUtapese", - "verpine": "SW5E.LanguagesVerpine", - "vong": "SW5E.LanguagesVong", - "voss": "SW5E.LanguagesVoss", - "yevethan": "SW5E.LanguagesYevethan", - "zabraki": "SW5E.LanguagesZabraki", - "zygerrian": "SW5E.LanguagesZygerrian" + "abyssin": "SW5E.LanguagesAbyssin", + "aleena": "SW5E.LanguagesAleena", + "antarian": "SW5E.LanguagesAntarian", + "anzellan": "SW5E.LanguagesAnzellan", + "aqualish": "SW5E.LanguagesAqualish", + "arconese": "SW5E.LanguagesArconese", + "ardennian": "SW5E.LanguagesArdennian", + "arkanian": "SW5E.LanguagesArkanian", + "balosur": "SW5E.LanguagesBalosur", + "barabel": "SW5E.LanguagesBarabel", + "basic": "SW5E.LanguagesBasic", + "besalisk": "SW5E.LanguagesBesalisk", + "binary": "SW5E.LanguagesBinary", + "bith": "SW5E.LanguagesBith", + "bocce": "SW5E.LanguagesBocce", + "bothese": "SW5E.LanguagesBothese", + "catharese": "SW5E.LanguagesCatharese", + "cerean": "SW5E.LanguagesCerean", + "chadra-fan": "SW5E.LanguagesChadra-Fan", + "chagri": "SW5E.LanguagesChagri", + "cheunh": "SW5E.LanguagesCheunh", + "chevin": "SW5E.LanguagesChevin", + "chironan": "SW5E.LanguagesChironan", + "clawdite": "SW5E.LanguagesClawdite", + "codruese": "SW5E.LanguagesCodruese", + "colicoid": "SW5E.LanguagesColicoid", + "dashadi": "SW5E.LanguagesDashadi", + "defel": "SW5E.LanguagesDefel", + "devaronese": "SW5E.LanguagesDevaronese", + "dosh": "SW5E.LanguagesDosh", + "draethos": "SW5E.LanguagesDraethos", + "durese": "SW5E.LanguagesDurese", + "dug": "SW5E.LanguagesDug", + "ewokese": "SW5E.LanguagesEwokese", + "falleen": "SW5E.LanguagesFalleen", + "felucianese": "SW5E.LanguagesFelucianese", + "gamorrese": "SW5E.LanguagesGamorrese", + "gand": "SW5E.LanguagesGand", + "geonosian": "SW5E.LanguagesGeonosian", + "givin": "SW5E.LanguagesGivin", + "gran": "SW5E.LanguagesGran", + "gungan": "SW5E.LanguagesGungan", + "hapan": "SW5E.LanguagesHapan", + "harchese": "SW5E.LanguagesHarchese", + "herglese": "SW5E.LanguagesHerglese", + "honoghran": "SW5E.LanguagesHonoghran", + "huttese": "SW5E.LanguagesHuttese", + "iktotchese": "SW5E.LanguagesIktotchese", + "ithorese": "SW5E.LanguagesIthorese", + "jawaese": "SW5E.LanguagesJawaese", + "kaleesh": "SW5E.LanguagesKaleesh", + "kaminoan": "SW5E.LanguagesKaminoan", + "karkaran": "SW5E.LanguagesKarkaran", + "keldor": "SW5E.LanguagesKelDor", + "kharan": "SW5E.LanguagesKharan", + "killik": "SW5E.LanguagesKillik", + "klatooinian": "SW5E.LanguagesKlatooinian", + "kubazian": "SW5E.LanguagesKubazian", + "kushiban": "SW5E.LanguagesKushiban", + "kyuzo": "SW5E.LanguagesKyuzo", + "lannik": "SW5E.LanguagesLannik", + "lasat": "SW5E.LanguagesLasat", + "lowickese": "SW5E.LanguagesLowickese", + "lurmese": "SW5E.LanguagesLurmese", + "mandoa": "SW5E.LanguagesMandoa", + "miralukese": "SW5E.LanguagesMiralukese", + "mirialan": "SW5E.LanguagesMirialan", + "moncal": "SW5E.LanguagesMonCal", + "mustafarian": "SW5E.LanguagesMustafarian", + "muun": "SW5E.LanguagesMuun", + "nautila": "SW5E.LanguagesNautila", + "ortolan": "SW5E.LanguagesOrtolan", + "pakpak": "SW5E.LanguagesPakPak", + "pyke": "SW5E.LanguagesPyke", + "quarrenese": "SW5E.LanguagesQuarrenese", + "rakata": "SW5E.LanguagesRakata", + "rattataki": "SW5E.LanguagesRattataki", + "rishii": "SW5E.LanguagesRishii", + "rodese": "SW5E.LanguagesRodese", + "ryn": "SW5E.LanguagesRyn", + "selkatha": "SW5E.LanguagesSelkatha", + "semblan": "SW5E.LanguagesSemblan", + "shistavanen": "SW5E.LanguagesShistavanen", + "shyriiwook": "SW5E.LanguagesShyriiwook", + "sith": "SW5E.LanguagesSith", + "squibbian": "SW5E.LanguagesSquibbian", + "sriluurian": "SW5E.LanguagesSriluurian", + "ssi-ruuvi": "SW5E.LanguagesSsi-ruuvi", + "sullustese": "SW5E.LanguagesSullustese", + "talzzi": "SW5E.LanguagesTalzzi", + "tarasinese": "SW5E.LanguagesTarasinese", + "thisspiasian": "SW5E.LanguagesThisspiasian", + "togorese": "SW5E.LanguagesTogorese", + "togruti": "SW5E.LanguagesTogruti", + "toydarian": "SW5E.LanguagesToydarian", + "tusken": "SW5E.LanguagesTusken", + "twi'leki": "SW5E.LanguagesTwileki", + "ugnaught": "SW5E.LanguagesUgnaught", + "umbaran": "SW5E.LanguagesUmbaran", + "utapese": "SW5E.LanguagesUtapese", + "verpine": "SW5E.LanguagesVerpine", + "vong": "SW5E.LanguagesVong", + "voss": "SW5E.LanguagesVoss", + "yevethan": "SW5E.LanguagesYevethan", + "zabraki": "SW5E.LanguagesZabraki", + "zygerrian": "SW5E.LanguagesZygerrian" }; // Character Level XP Requirements -SW5E.CHARACTER_EXP_LEVELS = [ - 0, 300, 900, 2700, 6500, 14000, 23000, 34000, 48000, 64000, 85000, 100000, - 120000, 140000, 165000, 195000, 225000, 265000, 305000, 355000] -; +SW5E.CHARACTER_EXP_LEVELS = [ + 0, 300, 900, 2700, 6500, 14000, 23000, 34000, 48000, 64000, 85000, 100000, 120000, 140000, 165000, 195000, 225000, + 265000, 305000, 355000 +]; // Challenge Rating XP Levels SW5E.CR_EXP_LEVELS = [ - 10, 200, 450, 700, 1100, 1800, 2300, 2900, 3900, 5000, 5900, 7200, 8400, 10000, 11500, 13000, 15000, 18000, - 20000, 22000, 25000, 33000, 41000, 50000, 62000, 75000, 90000, 105000, 120000, 135000, 155000 + 10, 200, 450, 700, 1100, 1800, 2300, 2900, 3900, 5000, 5900, 7200, 8400, 10000, 11500, 13000, 15000, 18000, 20000, + 22000, 25000, 33000, 41000, 50000, 62000, 75000, 90000, 105000, 120000, 135000, 155000 ]; // Character Features Per Class And Level @@ -1191,354 +1430,354 @@ SW5E.classFeatures = ClassFeatures; // Configure Optional Character Flags SW5E.characterFlags = { - "adaptiveResilience": { - name: "SW5E.FlagsAdaptiveResilience", - hint: "SW5E.FlagsAdaptiveResilienceHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "aggressive": { - name: "SW5E.FlagsAggressive", - hint: "SW5E.FlagsAggressiveHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "amphibious": { - name: "SW5E.FlagsAmphibious", - hint: "SW5E.FlagsAmphibiousHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "armorIntegration": { - name: "SW5E.FlagsArmorIntegration", - hint: "SW5E.FlagsArmorIntegrationHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "businessSavvy": { - name: "SW5E.FlagsBusinessSavvy", - hint: "SW5E.FlagsBusinessSavvyHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "cannibalize": { - name: "SW5E.FlagsCannibalize", - hint: "SW5E.FlagsCannibalizeHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "closedMind": { - name: "SW5E.FlagsClosedMind", - hint: "SW5E.FlagsClosedMindHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "crudeWeaponSpecialists": { - name: "SW5E.FlagsCrudeWeaponSpecialists", - hint: "SW5E.FlagsCrudeWeaponSpecialistsHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "defiant": { - name: "SW5E.FlagsDefiant", - hint: "SW5E.FlagsDefiantHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "detailOriented": { - name: "SW5E.FlagsDetailOriented", - hint: "SW5E.FlagsDetailOrientedHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "enthrallingPheromones": { - name: "SW5E.FlagsEnthrallingPheromones", - hint: "SW5E.FlagsEnthrallingPheromonesHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "extraArms": { - name: "SW5E.FlagsExtraArms", - hint: "SW5E.FlagsExtraArmsHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "forceContention": { - name: "SW5E.FlagsForceContention", - hint: "SW5E.FlagsForceContentionHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "forceInsensitive": { - name: "SW5E.FlagsForceInsensitive", - hint: "SW5E.FlagsForceInsensitiveHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "foreignBiology": { - name: "SW5E.FlagsForeignBiology", - hint: "SW5E.FlagsForeignBiologyHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "furyOfTheSmall": { - name: "SW5E.FlagsFuryOfTheSmall", - hint: "SW5E.FlagsFuryOfTheSmallHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "grovelCowerAndBeg": { - name: "SW5E.FlagsGrovelCowerAndBeg", - hint: "SW5E.FlagsGrovelCowerAndBegHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "inscrutable": { - name: "SW5E.FlagsInscrutable", - hint: "SW5E.FlagsInscrutableHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "keenSenses": { - name: "SW5E.FlagsKeenSenses", - hint: "SW5E.FlagsKeenSensesHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "longlimbed": { - name: "SW5E.FlagsLongLimbed", - hint: "SW5E.FlagsLongLimbedHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "maintenanceMode": { - name: "SW5E.FlagsMaintenanceMode", - hint: "SW5E.FlagsMaintenanceModeHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "maskOfTheWild": { - name: "SW5E.FlagsMaskOfTheWild", - hint: "SW5E.FlagsMaskOfTheWildHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "multipleHearts": { - name: "SW5E.FlagsMultipleHearts", - hint: "SW5E.FlagsMultipleHeartsHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "naturallyStealthy": { - name: "SW5E.FlagsNaturallyStealthy", - hint: "SW5E.FlagsNaturallyStealthyHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "nimbleAgility": { - name: "SW5E.FlagsNimbleAgility", - hint: "SW5E.FlagsNimbleAgilityHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "nimbleEscape": { - name: "SW5E.FlagsNimbleEscape", - hint: "SW5E.FlagsNimbleEscapeHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "nimbleness": { - name: "SW5E.FlagsNimbleness", - hint: "SW5E.FlagsNimblenessHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "pintsized": { - name: "SW5E.FlagsPintsized", - hint: "SW5E.FlagsPintsizedHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "powerfulBuild": { - name: "SW5E.FlagsPowerfulBuild", - hint: "SW5E.FlagsPowerfulBuildHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "precognition": { - name: "SW5E.FlagsPrecognition", - hint: "SW5E.FlagsPrecognitionHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "programmer": { - name: "SW5E.FlagsProgrammer", - hint: "SW5E.FlagsProgrammerHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "puny": { - name: "SW5E.FlagsPuny", - hint: "SW5E.FlagsPunyHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "rapidReconstruction": { - name: "SW5E.FlagsRapidReconstruction", - hint: "SW5E.FlagsRapidReconstructionHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "rapidlyRegenerative": { - name: "SW5E.FlagsRapidlyRegenerative", - hint: "SW5E.FlagsRapidlyRegenerativeHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "regenerative": { - name: "SW5E.FlagsRegenerative", - hint: "SW5E.FlagsRegenerativeHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "savageAttacks": { - name: "SW5E.FlagsSavageAttacks", - hint: "SW5E.FlagsSavageAttacksHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "shapechanger": { - name: "SW5E.FlagsShapechanger", - hint: "SW5E.FlagsShapechangerHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "strongLegged": { - name: "SW5E.FlagsStrongLegged", - hint: "SW5E.FlagsStrongLeggedHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "sunlightSensitivity": { - name: "SW5E.FlagsSunlightSensitivity", - hint: "SW5E.FlagsSunlightSensitivityHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "surpriseAttack": { - name: "SW5E.FlagsSurpriseAttack", - hint: "SW5E.FlagsSurpriseAttackHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "techImpaired": { - name: "SW5E.FlagsTechImpaired", - hint: "SW5E.FlagsTechImpairedHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "techResistance": { - name: "SW5E.FlagsTechResistance", - hint: "SW5E.FlagsTechResistanceHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "tinker": { - name: "SW5E.FlagsTinker", - hint: "SW5E.FlagsTinkerHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "toughness": { - name: "SW5E.FlagsToughness", - hint: "SW5E.FlagsToughnessHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "trance": { - name: "SW5E.FlagsTrance", - hint: "SW5E.FlagsTranceHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "unarmedCombatant": { - name: "SW5E.FlagsUnarmedCombatant", - hint: "SW5E.FlagsUnarmedCombatantHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "undersized": { - name: "SW5E.FlagsUndersized", - hint: "SW5E.FlagsUndersizedHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "unsettlingVisage": { - name: "SW5E.FlagsUnsettlingVisage", - hint: "SW5E.FlagsUnsettlingVisageHint", - section: "SW5E.SpeciesTraits", - type: Boolean - }, - "initiativeAdv": { - name: "SW5E.FlagsInitiativeAdv", - hint: "SW5E.FlagsInitiativeAdvHint", - section: "SW5E.Features", - type: Boolean - }, - "initiativeAlert": { - name: "SW5E.FlagsAlert", - hint: "SW5E.FlagsAlertHint", - section: "SW5E.Features", - type: Boolean - }, - "jackOfAllTrades": { - name: "SW5E.FlagsJOAT", - hint: "SW5E.FlagsJOATHint", - section: "SW5E.Features", - type: Boolean - }, - "observantFeat": { - name: "SW5E.FlagsObservant", - hint: "SW5E.FlagsObservantHint", - skills: ['prc','inv'], - section: "SW5E.Features", - type: Boolean - }, - "reliableTalent": { - name: "SW5E.FlagsReliableTalent", - hint: "SW5E.FlagsReliableTalentHint", - section: "SW5E.Features", - type: Boolean - }, - "remarkableAthlete": { - name: "SW5E.FlagsRemarkableAthlete", - hint: "SW5E.FlagsRemarkableAthleteHint", - abilities: ['str','dex','con'], - section: "SW5E.Features", - type: Boolean - }, - "weaponCriticalThreshold": { - name: "SW5E.FlagsWeaponCritThreshold", - hint: "SW5E.FlagsWeaponCritThresholdHint", - section: "SW5E.Features", - type: Number, - placeholder: 20 - }, - "powerCriticalThreshold": { - name: "SW5E.FlagsPowerCritThreshold", - hint: "SW5E.FlagsPowerCritThresholdHint", - section: "SW5E.Features", - type: Number, - placeholder: 20 - }, - "meleeCriticalDamageDice": { - name: "SW5E.FlagsMeleeCriticalDice", - hint: "SW5E.FlagsMeleeCriticalDiceHint", - section: "SW5E.Features", - type: Number, - placeholder: 0 - } + adaptiveResilience: { + name: "SW5E.FlagsAdaptiveResilience", + hint: "SW5E.FlagsAdaptiveResilienceHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + aggressive: { + name: "SW5E.FlagsAggressive", + hint: "SW5E.FlagsAggressiveHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + amphibious: { + name: "SW5E.FlagsAmphibious", + hint: "SW5E.FlagsAmphibiousHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + armorIntegration: { + name: "SW5E.FlagsArmorIntegration", + hint: "SW5E.FlagsArmorIntegrationHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + businessSavvy: { + name: "SW5E.FlagsBusinessSavvy", + hint: "SW5E.FlagsBusinessSavvyHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + cannibalize: { + name: "SW5E.FlagsCannibalize", + hint: "SW5E.FlagsCannibalizeHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + closedMind: { + name: "SW5E.FlagsClosedMind", + hint: "SW5E.FlagsClosedMindHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + crudeWeaponSpecialists: { + name: "SW5E.FlagsCrudeWeaponSpecialists", + hint: "SW5E.FlagsCrudeWeaponSpecialistsHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + defiant: { + name: "SW5E.FlagsDefiant", + hint: "SW5E.FlagsDefiantHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + detailOriented: { + name: "SW5E.FlagsDetailOriented", + hint: "SW5E.FlagsDetailOrientedHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + enthrallingPheromones: { + name: "SW5E.FlagsEnthrallingPheromones", + hint: "SW5E.FlagsEnthrallingPheromonesHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + extraArms: { + name: "SW5E.FlagsExtraArms", + hint: "SW5E.FlagsExtraArmsHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + forceContention: { + name: "SW5E.FlagsForceContention", + hint: "SW5E.FlagsForceContentionHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + forceInsensitive: { + name: "SW5E.FlagsForceInsensitive", + hint: "SW5E.FlagsForceInsensitiveHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + foreignBiology: { + name: "SW5E.FlagsForeignBiology", + hint: "SW5E.FlagsForeignBiologyHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + furyOfTheSmall: { + name: "SW5E.FlagsFuryOfTheSmall", + hint: "SW5E.FlagsFuryOfTheSmallHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + grovelCowerAndBeg: { + name: "SW5E.FlagsGrovelCowerAndBeg", + hint: "SW5E.FlagsGrovelCowerAndBegHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + inscrutable: { + name: "SW5E.FlagsInscrutable", + hint: "SW5E.FlagsInscrutableHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + keenSenses: { + name: "SW5E.FlagsKeenSenses", + hint: "SW5E.FlagsKeenSensesHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + longlimbed: { + name: "SW5E.FlagsLongLimbed", + hint: "SW5E.FlagsLongLimbedHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + maintenanceMode: { + name: "SW5E.FlagsMaintenanceMode", + hint: "SW5E.FlagsMaintenanceModeHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + maskOfTheWild: { + name: "SW5E.FlagsMaskOfTheWild", + hint: "SW5E.FlagsMaskOfTheWildHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + multipleHearts: { + name: "SW5E.FlagsMultipleHearts", + hint: "SW5E.FlagsMultipleHeartsHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + naturallyStealthy: { + name: "SW5E.FlagsNaturallyStealthy", + hint: "SW5E.FlagsNaturallyStealthyHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + nimbleAgility: { + name: "SW5E.FlagsNimbleAgility", + hint: "SW5E.FlagsNimbleAgilityHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + nimbleEscape: { + name: "SW5E.FlagsNimbleEscape", + hint: "SW5E.FlagsNimbleEscapeHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + nimbleness: { + name: "SW5E.FlagsNimbleness", + hint: "SW5E.FlagsNimblenessHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + pintsized: { + name: "SW5E.FlagsPintsized", + hint: "SW5E.FlagsPintsizedHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + powerfulBuild: { + name: "SW5E.FlagsPowerfulBuild", + hint: "SW5E.FlagsPowerfulBuildHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + precognition: { + name: "SW5E.FlagsPrecognition", + hint: "SW5E.FlagsPrecognitionHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + programmer: { + name: "SW5E.FlagsProgrammer", + hint: "SW5E.FlagsProgrammerHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + puny: { + name: "SW5E.FlagsPuny", + hint: "SW5E.FlagsPunyHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + rapidReconstruction: { + name: "SW5E.FlagsRapidReconstruction", + hint: "SW5E.FlagsRapidReconstructionHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + rapidlyRegenerative: { + name: "SW5E.FlagsRapidlyRegenerative", + hint: "SW5E.FlagsRapidlyRegenerativeHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + regenerative: { + name: "SW5E.FlagsRegenerative", + hint: "SW5E.FlagsRegenerativeHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + savageAttacks: { + name: "SW5E.FlagsSavageAttacks", + hint: "SW5E.FlagsSavageAttacksHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + shapechanger: { + name: "SW5E.FlagsShapechanger", + hint: "SW5E.FlagsShapechangerHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + strongLegged: { + name: "SW5E.FlagsStrongLegged", + hint: "SW5E.FlagsStrongLeggedHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + sunlightSensitivity: { + name: "SW5E.FlagsSunlightSensitivity", + hint: "SW5E.FlagsSunlightSensitivityHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + surpriseAttack: { + name: "SW5E.FlagsSurpriseAttack", + hint: "SW5E.FlagsSurpriseAttackHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + techImpaired: { + name: "SW5E.FlagsTechImpaired", + hint: "SW5E.FlagsTechImpairedHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + techResistance: { + name: "SW5E.FlagsTechResistance", + hint: "SW5E.FlagsTechResistanceHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + tinker: { + name: "SW5E.FlagsTinker", + hint: "SW5E.FlagsTinkerHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + toughness: { + name: "SW5E.FlagsToughness", + hint: "SW5E.FlagsToughnessHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + trance: { + name: "SW5E.FlagsTrance", + hint: "SW5E.FlagsTranceHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + unarmedCombatant: { + name: "SW5E.FlagsUnarmedCombatant", + hint: "SW5E.FlagsUnarmedCombatantHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + undersized: { + name: "SW5E.FlagsUndersized", + hint: "SW5E.FlagsUndersizedHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + unsettlingVisage: { + name: "SW5E.FlagsUnsettlingVisage", + hint: "SW5E.FlagsUnsettlingVisageHint", + section: "SW5E.SpeciesTraits", + type: Boolean + }, + initiativeAdv: { + name: "SW5E.FlagsInitiativeAdv", + hint: "SW5E.FlagsInitiativeAdvHint", + section: "SW5E.Features", + type: Boolean + }, + initiativeAlert: { + name: "SW5E.FlagsAlert", + hint: "SW5E.FlagsAlertHint", + section: "SW5E.Features", + type: Boolean + }, + jackOfAllTrades: { + name: "SW5E.FlagsJOAT", + hint: "SW5E.FlagsJOATHint", + section: "SW5E.Features", + type: Boolean + }, + observantFeat: { + name: "SW5E.FlagsObservant", + hint: "SW5E.FlagsObservantHint", + skills: ["prc", "inv"], + section: "SW5E.Features", + type: Boolean + }, + reliableTalent: { + name: "SW5E.FlagsReliableTalent", + hint: "SW5E.FlagsReliableTalentHint", + section: "SW5E.Features", + type: Boolean + }, + remarkableAthlete: { + name: "SW5E.FlagsRemarkableAthlete", + hint: "SW5E.FlagsRemarkableAthleteHint", + abilities: ["str", "dex", "con"], + section: "SW5E.Features", + type: Boolean + }, + weaponCriticalThreshold: { + name: "SW5E.FlagsWeaponCritThreshold", + hint: "SW5E.FlagsWeaponCritThresholdHint", + section: "SW5E.Features", + type: Number, + placeholder: 20 + }, + powerCriticalThreshold: { + name: "SW5E.FlagsPowerCritThreshold", + hint: "SW5E.FlagsPowerCritThresholdHint", + section: "SW5E.Features", + type: Number, + placeholder: 20 + }, + meleeCriticalDamageDice: { + name: "SW5E.FlagsMeleeCriticalDice", + hint: "SW5E.FlagsMeleeCriticalDiceHint", + section: "SW5E.Features", + type: Number, + placeholder: 0 + } }; // Configure allowed status flags -SW5E.allowedActorFlags = ["isPolymorphed", "originalActor", "dataVersion"].concat(Object.keys(SW5E.characterFlags)); \ No newline at end of file +SW5E.allowedActorFlags = ["isPolymorphed", "originalActor", "dataVersion"].concat(Object.keys(SW5E.characterFlags)); diff --git a/module/dice.js b/module/dice.js index fc70522b..8abc0291 100644 --- a/module/dice.js +++ b/module/dice.js @@ -12,50 +12,55 @@ export {default as DamageRoll} from "./dice/damage-roll.js"; * @return {string} The resulting simplified formula */ export function simplifyRollFormula(formula, data, {constantFirst = false} = {}) { - const roll = new Roll(formula, data); // Parses the formula and replaces any @properties - const terms = roll.terms; + const roll = new Roll(formula, data); // Parses the formula and replaces any @properties + const terms = roll.terms; - // Some terms are "too complicated" for this algorithm to simplify - // In this case, the original formula is returned. - if (terms.some(_isUnsupportedTerm)) return roll.formula; + // Some terms are "too complicated" for this algorithm to simplify + // In this case, the original formula is returned. + if (terms.some(_isUnsupportedTerm)) return roll.formula; - const rollableTerms = []; // Terms that are non-constant, and their associated operators - const constantTerms = []; // Terms that are constant, and their associated operators - let operators = []; // Temporary storage for operators before they are moved to one of the above + const rollableTerms = []; // Terms that are non-constant, and their associated operators + const constantTerms = []; // Terms that are constant, and their associated operators + let operators = []; // Temporary storage for operators before they are moved to one of the above - for (let term of terms) { // For each term - if (term instanceof OperatorTerm) operators.push(term); // If the term is an addition/subtraction operator, push the term into the operators array - else { // Otherwise the term is not an operator - if (term instanceof DiceTerm) { // If the term is something rollable - rollableTerms.push(...operators); // Place all the operators into the rollableTerms array - rollableTerms.push(term); // Then place this rollable term into it as well - } // - else { // Otherwise, this must be a constant - constantTerms.push(...operators); // Place the operators into the constantTerms array - constantTerms.push(term); // Then also add this constant term to that array. - } // - operators = []; // Finally, the operators have now all been assigend to one of the arrays, so empty this before the next iteration. + for (let term of terms) { + // For each term + if (term instanceof OperatorTerm) operators.push(term); + // If the term is an addition/subtraction operator, push the term into the operators array + else { + // Otherwise the term is not an operator + if (term instanceof DiceTerm) { + // If the term is something rollable + rollableTerms.push(...operators); // Place all the operators into the rollableTerms array + rollableTerms.push(term); // Then place this rollable term into it as well + } // + else { + // Otherwise, this must be a constant + constantTerms.push(...operators); // Place the operators into the constantTerms array + constantTerms.push(term); // Then also add this constant term to that array. + } // + operators = []; // Finally, the operators have now all been assigend to one of the arrays, so empty this before the next iteration. + } } - } - const constantFormula = Roll.getFormula(constantTerms); // Cleans up the constant terms and produces a new formula string - const rollableFormula = Roll.getFormula(rollableTerms); // Cleans up the non-constant terms and produces a new formula string + const constantFormula = Roll.getFormula(constantTerms); // Cleans up the constant terms and produces a new formula string + const rollableFormula = Roll.getFormula(rollableTerms); // Cleans up the non-constant terms and produces a new formula string - // Mathematically evaluate the constant formula to produce a single constant term - let constantPart = undefined; - if ( constantFormula ) { - try { - constantPart = Roll.safeEval(constantFormula) - } catch (err) { - console.warn(`Unable to evaluate constant term ${constantFormula} in simplifyRollFormula`); + // Mathematically evaluate the constant formula to produce a single constant term + let constantPart = undefined; + if (constantFormula) { + try { + constantPart = Roll.safeEval(constantFormula); + } catch (err) { + console.warn(`Unable to evaluate constant term ${constantFormula} in simplifyRollFormula`); + } } - } - // Order the rollable and constant terms, either constant first or second depending on the optional argument - const parts = constantFirst ? [constantPart, rollableFormula] : [rollableFormula, constantPart]; + // Order the rollable and constant terms, either constant first or second depending on the optional argument + const parts = constantFirst ? [constantPart, rollableFormula] : [rollableFormula, constantPart]; - // Join the parts with a + sign, pass them to `Roll` once again to clean up the formula - return new Roll(parts.filterJoin(" + ")).formula; + // Join the parts with a + sign, pass them to `Roll` once again to clean up the formula + return new Roll(parts.filterJoin(" + ")).formula; } /* -------------------------------------------- */ @@ -66,11 +71,11 @@ export function simplifyRollFormula(formula, data, {constantFirst = false} = {}) * @return {Boolean} True when unsupported, false if supported */ function _isUnsupportedTerm(term) { - const diceTerm = term instanceof DiceTerm; - const operator = term instanceof OperatorTerm && ["+", "-"].includes(term.operator); - const number = term instanceof NumericTerm; + const diceTerm = term instanceof DiceTerm; + const operator = term instanceof OperatorTerm && ["+", "-"].includes(term.operator); + const number = term instanceof NumericTerm; - return !(diceTerm || operator || number); + return !(diceTerm || operator || number); } /* -------------------------------------------- */ @@ -111,54 +116,75 @@ function _isUnsupportedTerm(term) { * @return {Promise} The evaluated D20Roll, or null if the workflow was cancelled */ export async function d20Roll({ - parts=[], data={}, // Roll creation - advantage, disadvantage, fumble=1, critical=20, targetValue, elvenAccuracy, halflingLucky, reliableTalent, // Roll customization - chooseModifier=false, fastForward=false, event, template, title, dialogOptions, // Dialog configuration - chatMessage=true, messageData={}, rollMode, speaker, flavor // Chat Message customization - }={}) { - - // Handle input arguments - const formula = ["1d20"].concat(parts).join(" + "); - const {advantageMode, isFF} = _determineAdvantageMode({advantage, disadvantage, fastForward, event}); - const defaultRollMode = rollMode || game.settings.get("core", "rollMode"); - if ( chooseModifier && !isFF ) data["mod"] = "@mod"; - - // Construct the D20Roll instance - const roll = new CONFIG.Dice.D20Roll(formula, data, { - flavor: flavor || title, - advantageMode, - defaultRollMode, - critical, - fumble, + parts = [], + data = {}, // Roll creation + advantage, + disadvantage, + fumble = 1, + critical = 20, targetValue, elvenAccuracy, halflingLucky, - reliableTalent - }); + reliableTalent, // Roll customization + chooseModifier = false, + fastForward = false, + event, + template, + title, + dialogOptions, // Dialog configuration + chatMessage = true, + messageData = {}, + rollMode, + speaker, + flavor // Chat Message customization +} = {}) { + // Handle input arguments + const formula = ["1d20"].concat(parts).join(" + "); + const {advantageMode, isFF} = _determineAdvantageMode({advantage, disadvantage, fastForward, event}); + const defaultRollMode = rollMode || game.settings.get("core", "rollMode"); + if (chooseModifier && !isFF) data["mod"] = "@mod"; - // Prompt a Dialog to further configure the D20Roll - if ( !isFF ) { - const configured = await roll.configureDialog({ - title, - chooseModifier, - defaultRollMode: defaultRollMode, - defaultAction: advantageMode, - defaultAbility: data?.item?.ability, - template - }, dialogOptions); - if ( configured === null ) return null; - } + // Construct the D20Roll instance + const roll = new CONFIG.Dice.D20Roll(formula, data, { + flavor: flavor || title, + advantageMode, + defaultRollMode, + critical, + fumble, + targetValue, + elvenAccuracy, + halflingLucky, + reliableTalent + }); - // Evaluate the configured roll - await roll.evaluate({async: true}); + // Prompt a Dialog to further configure the D20Roll + if (!isFF) { + const configured = await roll.configureDialog( + { + title, + chooseModifier, + defaultRollMode: defaultRollMode, + defaultAction: advantageMode, + defaultAbility: data?.item?.ability, + template + }, + dialogOptions + ); + if (configured === null) return null; + } - // Create a Chat Message - if ( speaker ) { - console.warn(`You are passing the speaker argument to the d20Roll function directly which should instead be passed as an internal key of messageData`); - messageData.speaker = speaker; - } - if ( roll && chatMessage ) await roll.toMessage(messageData); - return roll; + // Evaluate the configured roll + await roll.evaluate({async: true}); + + // Create a Chat Message + if (speaker) { + console.warn( + `You are passing the speaker argument to the d20Roll function directly which should instead be passed as an internal key of messageData` + ); + messageData.speaker = speaker; + } + if (roll && chatMessage) await roll.toMessage(messageData); + return roll; } /* -------------------------------------------- */ @@ -167,12 +193,13 @@ export async function d20Roll({ * Determines whether this d20 roll should be fast-forwarded, and whether advantage or disadvantage should be applied * @returns {{isFF: boolean, advantageMode: number}} Whether the roll is fast-forward, and its advantage mode */ -function _determineAdvantageMode({event, advantage=false, disadvantage=false, fastForward=false}={}) { - const isFF = fastForward || (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey)); - let advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.NORMAL; - if ( advantage || event?.altKey ) advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.ADVANTAGE; - else if ( disadvantage || event?.ctrlKey || event?.metaKey ) advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.DISADVANTAGE; - return {isFF, advantageMode}; +function _determineAdvantageMode({event, advantage = false, disadvantage = false, fastForward = false} = {}) { + const isFF = fastForward || (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey)); + let advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.NORMAL; + if (advantage || event?.altKey) advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.ADVANTAGE; + else if (disadvantage || event?.ctrlKey || event?.metaKey) + advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.DISADVANTAGE; + return {isFF, advantageMode}; } /* -------------------------------------------- */ @@ -210,49 +237,67 @@ function _determineAdvantageMode({event, advantage=false, disadvantage=false, fa * @return {Promise} The evaluated DamageRoll, or null if the workflow was canceled */ export async function damageRoll({ - parts=[], data, // Roll creation - critical=false, criticalBonusDice, criticalMultiplier, multiplyNumeric, powerfulCritical, // Damage customization - fastForward=false, event, allowCritical=true, template, title, dialogOptions, // Dialog configuration - chatMessage=true, messageData={}, rollMode, speaker, flavor, // Chat Message customization - }={}) { - - // Handle input arguments - const defaultRollMode = rollMode || game.settings.get("core", "rollMode"); - - // Construct the DamageRoll instance - const formula = parts.join(" + "); - const {isCritical, isFF} = _determineCriticalMode({critical, fastForward, event}); - const roll = new CONFIG.Dice.DamageRoll(formula, data, { - flavor: flavor || title, - critical: isCritical, + parts = [], + data, // Roll creation + critical = false, criticalBonusDice, criticalMultiplier, multiplyNumeric, - powerfulCritical - }); + powerfulCritical, // Damage customization + fastForward = false, + event, + allowCritical = true, + template, + title, + dialogOptions, // Dialog configuration + chatMessage = true, + messageData = {}, + rollMode, + speaker, + flavor // Chat Message customization +} = {}) { + // Handle input arguments + const defaultRollMode = rollMode || game.settings.get("core", "rollMode"); - // Prompt a Dialog to further configure the DamageRoll - if ( !isFF ) { - const configured = await roll.configureDialog({ - title, - defaultRollMode: defaultRollMode, - defaultCritical: isCritical, - template, - allowCritical - }, dialogOptions); - if ( configured === null ) return null; - } + // Construct the DamageRoll instance + const formula = parts.join(" + "); + const {isCritical, isFF} = _determineCriticalMode({critical, fastForward, event}); + const roll = new CONFIG.Dice.DamageRoll(formula, data, { + flavor: flavor || title, + critical: isCritical, + criticalBonusDice, + criticalMultiplier, + multiplyNumeric, + powerfulCritical + }); - // Evaluate the configured roll - await roll.evaluate({async: true}); + // Prompt a Dialog to further configure the DamageRoll + if (!isFF) { + const configured = await roll.configureDialog( + { + title, + defaultRollMode: defaultRollMode, + defaultCritical: isCritical, + template, + allowCritical + }, + dialogOptions + ); + if (configured === null) return null; + } - // Create a Chat Message - if ( speaker ) { - console.warn(`You are passing the speaker argument to the damageRoll function directly which should instead be passed as an internal key of messageData`); - messageData.speaker = speaker; - } - if ( roll && chatMessage ) await roll.toMessage(messageData); - return roll; + // Evaluate the configured roll + await roll.evaluate({async: true}); + + // Create a Chat Message + if (speaker) { + console.warn( + `You are passing the speaker argument to the damageRoll function directly which should instead be passed as an internal key of messageData` + ); + messageData.speaker = speaker; + } + if (roll && chatMessage) await roll.toMessage(messageData); + return roll; } /* -------------------------------------------- */ @@ -261,8 +306,8 @@ export async function damageRoll({ * Determines whether this d20 roll should be fast-forwarded, and whether advantage or disadvantage should be applied * @returns {{isFF: boolean, isCritical: boolean}} Whether the roll is fast-forward, and whether it is a critical hit */ -function _determineCriticalMode({event, critical=false, fastForward=false}={}) { - const isFF = fastForward || (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey)); - if ( event?.altKey ) critical = true; - return {isFF, isCritical: critical}; +function _determineCriticalMode({event, critical = false, fastForward = false} = {}) { + const isFF = fastForward || (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey)); + if (event?.altKey) critical = true; + return {isFF, isCritical: critical}; } diff --git a/module/dice/d20-roll.js b/module/dice/d20-roll.js index c4b40824..fb8a18b0 100644 --- a/module/dice/d20-roll.js +++ b/module/dice/d20-roll.js @@ -16,7 +16,7 @@ export default class D20Roll extends Roll { constructor(formula, data, options) { super(formula, data, options); - if ( !((this.terms[0] instanceof Die) && (this.terms[0].faces === 20)) ) { + if (!(this.terms[0] instanceof Die && this.terms[0].faces === 20)) { throw new Error(`Invalid D20Roll formula provided ${this._formula}`); } this.configureModifiers(); @@ -31,8 +31,8 @@ export default class D20Roll extends Roll { static ADV_MODE = { NORMAL: 0, ADVANTAGE: 1, - DISADVANTAGE: -1, - } + DISADVANTAGE: -1 + }; /** * The HTML template path used to configure evaluation of this Roll @@ -71,28 +71,26 @@ export default class D20Roll extends Roll { d20.modifiers = []; // Halfling Lucky - if ( this.options.halflingLucky ) d20.modifiers.push("r1=1"); + if (this.options.halflingLucky) d20.modifiers.push("r1=1"); // Reliable Talent - if ( this.options.reliableTalent ) d20.modifiers.push("min10"); + if (this.options.reliableTalent) d20.modifiers.push("min10"); // Handle Advantage or Disadvantage - if ( this.hasAdvantage ) { + if (this.hasAdvantage) { d20.number = this.options.elvenAccuracy ? 3 : 2; d20.modifiers.push("kh"); d20.options.advantage = true; - } - else if ( this.hasDisadvantage ) { + } else if (this.hasDisadvantage) { d20.number = 2; d20.modifiers.push("kl"); d20.options.disadvantage = true; - } - else d20.number = 1; + } else d20.number = 1; // Assign critical and fumble thresholds - if ( this.options.critical ) d20.options.critical = this.options.critical; - if ( this.options.fumble ) d20.options.fumble = this.options.fumble; - if ( this.options.targetValue ) d20.options.target = this.options.targetValue; + if (this.options.critical) d20.options.critical = this.options.critical; + if (this.options.fumble) d20.options.fumble = this.options.fumble; + if (this.options.targetValue) d20.options.target = this.options.targetValue; // Re-compile the underlying formula this._formula = this.constructor.getFormula(this.terms); @@ -101,22 +99,21 @@ export default class D20Roll extends Roll { /* -------------------------------------------- */ /** @inheritdoc */ - async toMessage(messageData={}, options={}) { - + async toMessage(messageData = {}, options = {}) { // Evaluate the roll now so we have the results available to determine whether reliable talent came into play - if ( !this._evaluated ) await this.evaluate({async: true}); + if (!this._evaluated) await this.evaluate({async: true}); // Add appropriate advantage mode message flavor and sw5e roll flags messageData.flavor = messageData.flavor || this.options.flavor; - if ( this.hasAdvantage ) messageData.flavor += ` (${game.i18n.localize("SW5E.Advantage")})`; - else if ( this.hasDisadvantage ) messageData.flavor += ` (${game.i18n.localize("SW5E.Disadvantage")})`; + if (this.hasAdvantage) messageData.flavor += ` (${game.i18n.localize("SW5E.Advantage")})`; + else if (this.hasDisadvantage) messageData.flavor += ` (${game.i18n.localize("SW5E.Disadvantage")})`; // Add reliable talent to the d20-term flavor text if it applied - if ( this.options.reliableTalent ) { + if (this.options.reliableTalent) { const d20 = this.dice[0]; - const isRT = d20.results.every(r => !r.active || (r.result < 10)); + const isRT = d20.results.every((r) => !r.active || r.result < 10); const label = `(${game.i18n.localize("SW5E.FlagsReliableTalent")})`; - if ( isRT ) d20.options.flavor = d20.options.flavor ? `${d20.options.flavor} (${label})` : label; + if (isRT) d20.options.flavor = d20.options.flavor ? `${d20.options.flavor} (${label})` : label; } // Record the preferred rollMode @@ -140,8 +137,17 @@ export default class D20Roll extends Roll { * @param {object} options Additional Dialog customization options * @returns {Promise} A resulting D20Roll object constructed with the dialog, or null if the dialog was closed */ - async configureDialog({title, defaultRollMode, defaultAction=D20Roll.ADV_MODE.NORMAL, chooseModifier=false, defaultAbility, template}={}, options={}) { - + async configureDialog( + { + title, + defaultRollMode, + defaultAction = D20Roll.ADV_MODE.NORMAL, + chooseModifier = false, + defaultAbility, + template + } = {}, + options = {} + ) { // Render the Dialog inner HTML const content = await renderTemplate(template ?? this.constructor.EVALUATION_TEMPLATE, { formula: `${this.formula} + @bonus`, @@ -154,32 +160,39 @@ export default class D20Roll extends Roll { let defaultButton = "normal"; switch (defaultAction) { - case D20Roll.ADV_MODE.ADVANTAGE: defaultButton = "advantage"; break; - case D20Roll.ADV_MODE.DISADVANTAGE: defaultButton = "disadvantage"; break; + case D20Roll.ADV_MODE.ADVANTAGE: + defaultButton = "advantage"; + break; + case D20Roll.ADV_MODE.DISADVANTAGE: + defaultButton = "disadvantage"; + break; } // Create the Dialog window and await submission of the form - return new Promise(resolve => { - new Dialog({ - title, - content, - buttons: { - advantage: { - label: game.i18n.localize("SW5E.Advantage"), - callback: html => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.ADVANTAGE)) + return new Promise((resolve) => { + new Dialog( + { + title, + content, + buttons: { + advantage: { + label: game.i18n.localize("SW5E.Advantage"), + callback: (html) => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.ADVANTAGE)) + }, + normal: { + label: game.i18n.localize("SW5E.Normal"), + callback: (html) => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.NORMAL)) + }, + disadvantage: { + label: game.i18n.localize("SW5E.Disadvantage"), + callback: (html) => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.DISADVANTAGE)) + } }, - normal: { - label: game.i18n.localize("SW5E.Normal"), - callback: html => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.NORMAL)) - }, - disadvantage: { - label: game.i18n.localize("SW5E.Disadvantage"), - callback: html => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.DISADVANTAGE)) - } + default: defaultButton, + close: () => resolve(null) }, - default: defaultButton, - close: () => resolve(null) - }, options).render(true); + options + ).render(true); }); } @@ -195,16 +208,16 @@ export default class D20Roll extends Roll { const form = html[0].querySelector("form"); // Append a situational bonus term - if ( form.bonus.value ) { + if (form.bonus.value) { const bonus = new Roll(form.bonus.value, this.data); - if ( !(bonus.terms[0] instanceof OperatorTerm) ) this.terms.push(new OperatorTerm({operator: "+"})); + if (!(bonus.terms[0] instanceof OperatorTerm)) this.terms.push(new OperatorTerm({operator: "+"})); this.terms = this.terms.concat(bonus.terms); } // Customize the modifier - if ( form.ability?.value ) { + if (form.ability?.value) { const abl = this.data.abilities[form.ability.value]; - this.terms.findSplice(t => t.term === "@mod", new NumericTerm({number: abl.mod})); + this.terms.findSplice((t) => t.term === "@mod", new NumericTerm({number: abl.mod})); this.options.flavor += ` (${CONFIG.SW5E.abilities[form.ability.value]})`; } diff --git a/module/dice/damage-roll.js b/module/dice/damage-roll.js index cc901fb2..41545c8b 100644 --- a/module/dice/damage-roll.js +++ b/module/dice/damage-roll.js @@ -13,7 +13,7 @@ export default class DamageRoll extends Roll { constructor(formula, data, options) { super(formula, data, options); // For backwards compatibility, skip rolls which do not have the "critical" option defined - if ( this.options.critical !== undefined ) this.configureDamage(); + if (this.options.critical !== undefined) this.configureDamage(); } /** @@ -42,44 +42,44 @@ export default class DamageRoll extends Roll { */ configureDamage() { let flatBonus = 0; - for ( let [i, term] of this.terms.entries() ) { - + for (let [i, term] of this.terms.entries()) { // Multiply dice terms - if ( term instanceof DiceTerm ) { + if (term instanceof DiceTerm) { term.options.baseNumber = term.options.baseNumber ?? term.number; // Reset back term.number = term.options.baseNumber; - if ( this.isCritical ) { + if (this.isCritical) { let cm = this.options.criticalMultiplier ?? 2; // Powerful critical - maximize damage and reduce the multiplier by 1 - if ( this.options.powerfulCritical ) { - flatBonus += (term.number * term.faces); - cm = Math.max(1, cm-1); + if (this.options.powerfulCritical) { + flatBonus += term.number * term.faces; + cm = Math.max(1, cm - 1); } // Alter the damage term - let cb = (this.options.criticalBonusDice && (i === 0)) ? this.options.criticalBonusDice : 0; + let cb = this.options.criticalBonusDice && i === 0 ? this.options.criticalBonusDice : 0; term.alter(cm, cb); term.options.critical = true; } - } // Multiply numeric terms - else if ( this.options.multiplyNumeric && (term instanceof NumericTerm) ) { + else if (this.options.multiplyNumeric && term instanceof NumericTerm) { term.options.baseNumber = term.options.baseNumber ?? term.number; // Reset back term.number = term.options.baseNumber; - if ( this.isCritical ) { - term.number *= (this.options.criticalMultiplier ?? 2); + if (this.isCritical) { + term.number *= this.options.criticalMultiplier ?? 2; term.options.critical = true; } } } // Add powerful critical bonus - if ( this.options.powerfulCritical && (flatBonus > 0) ) { + if (this.options.powerfulCritical && flatBonus > 0) { this.terms.push(new OperatorTerm({operator: "+"})); - this.terms.push(new NumericTerm({number: flatBonus}, {flavor: game.i18n.localize("SW5E.PowerfulCritical")})); + this.terms.push( + new NumericTerm({number: flatBonus}, {flavor: game.i18n.localize("SW5E.PowerfulCritical")}) + ); } // Re-compile the underlying formula @@ -89,9 +89,9 @@ export default class DamageRoll extends Roll { /* -------------------------------------------- */ /** @inheritdoc */ - toMessage(messageData={}, options={}) { + toMessage(messageData = {}, options = {}) { messageData.flavor = messageData.flavor || this.options.flavor; - if ( this.isCritical ) { + if (this.isCritical) { const label = game.i18n.localize("SW5E.CriticalHit"); messageData.flavor = messageData.flavor ? `${messageData.flavor} (${label})` : label; } @@ -114,34 +114,39 @@ export default class DamageRoll extends Roll { * @param {object} options Additional Dialog customization options * @returns {Promise} A resulting D20Roll object constructed with the dialog, or null if the dialog was closed */ - async configureDialog({title, defaultRollMode, defaultCritical=false, template, allowCritical=true}={}, options={}) { - + async configureDialog( + {title, defaultRollMode, defaultCritical = false, template, allowCritical = true} = {}, + options = {} + ) { // Render the Dialog inner HTML const content = await renderTemplate(template ?? this.constructor.EVALUATION_TEMPLATE, { formula: `${this.formula} + @bonus`, defaultRollMode, - rollModes: CONFIG.Dice.rollModes, + rollModes: CONFIG.Dice.rollModes }); // Create the Dialog window and await submission of the form - return new Promise(resolve => { - new Dialog({ - title, - content, - buttons: { - critical: { - condition: allowCritical, - label: game.i18n.localize("SW5E.CriticalHit"), - callback: html => resolve(this._onDialogSubmit(html, true)) + return new Promise((resolve) => { + new Dialog( + { + title, + content, + buttons: { + critical: { + condition: allowCritical, + label: game.i18n.localize("SW5E.CriticalHit"), + callback: (html) => resolve(this._onDialogSubmit(html, true)) + }, + normal: { + label: game.i18n.localize(allowCritical ? "SW5E.Normal" : "SW5E.Roll"), + callback: (html) => resolve(this._onDialogSubmit(html, false)) + } }, - normal: { - label: game.i18n.localize(allowCritical ? "SW5E.Normal" : "SW5E.Roll"), - callback: html => resolve(this._onDialogSubmit(html, false)) - } + default: defaultCritical ? "critical" : "normal", + close: () => resolve(null) }, - default: defaultCritical ? "critical" : "normal", - close: () => resolve(null) - }, options).render(true); + options + ).render(true); }); } @@ -157,9 +162,9 @@ export default class DamageRoll extends Roll { const form = html[0].querySelector("form"); // Append a situational bonus term - if ( form.bonus.value ) { + if (form.bonus.value) { const bonus = new Roll(form.bonus.value, this.data); - if ( !(bonus.terms[0] instanceof OperatorTerm) ) this.terms.push(new OperatorTerm({operator: "+"})); + if (!(bonus.terms[0] instanceof OperatorTerm)) this.terms.push(new OperatorTerm({operator: "+"})); this.terms = this.terms.concat(bonus.terms); } diff --git a/module/effects.js b/module/effects.js index ef66ef77..b825af87 100644 --- a/module/effects.js +++ b/module/effects.js @@ -4,26 +4,28 @@ * @param {Actor|Item} owner The owning entity which manages this effect */ export function onManageActiveEffect(event, owner) { - event.preventDefault(); - const a = event.currentTarget; - const li = a.closest("li"); - const effect = li.dataset.effectId ? owner.effects.get(li.dataset.effectId) : null; - switch ( a.dataset.action ) { - case "create": - return owner.createEmbeddedDocuments("ActiveEffect", [{ - label: game.i18n.localize("SW5E.EffectNew"), - icon: "icons/svg/aura.svg", - origin: owner.uuid, - "duration.rounds": li.dataset.effectType === "temporary" ? 1 : undefined, - disabled: li.dataset.effectType === "inactive" - }]); - case "edit": - return effect.sheet.render(true); - case "delete": - return effect.delete(); - case "toggle": - return effect.update({disabled: !effect.data.disabled}); - } + event.preventDefault(); + const a = event.currentTarget; + const li = a.closest("li"); + const effect = li.dataset.effectId ? owner.effects.get(li.dataset.effectId) : null; + switch (a.dataset.action) { + case "create": + return owner.createEmbeddedDocuments("ActiveEffect", [ + { + "label": game.i18n.localize("SW5E.EffectNew"), + "icon": "icons/svg/aura.svg", + "origin": owner.uuid, + "duration.rounds": li.dataset.effectType === "temporary" ? 1 : undefined, + "disabled": li.dataset.effectType === "inactive" + } + ]); + case "edit": + return effect.sheet.render(true); + case "delete": + return effect.delete(); + case "toggle": + return effect.update({disabled: !effect.data.disabled}); + } } /** @@ -32,32 +34,31 @@ export function onManageActiveEffect(event, owner) { * @return {object} Data for rendering */ export function prepareActiveEffectCategories(effects) { - // Define effect header categories const categories = { - temporary: { - type: "temporary", - label: game.i18n.localize("SW5E.EffectTemporary"), - effects: [] - }, - passive: { - type: "passive", - label: game.i18n.localize("SW5E.EffectPassive"), - effects: [] - }, - inactive: { - type: "inactive", - label: game.i18n.localize("SW5E.EffectInactive"), - effects: [] - } + temporary: { + type: "temporary", + label: game.i18n.localize("SW5E.EffectTemporary"), + effects: [] + }, + passive: { + type: "passive", + label: game.i18n.localize("SW5E.EffectPassive"), + effects: [] + }, + inactive: { + type: "inactive", + label: game.i18n.localize("SW5E.EffectInactive"), + effects: [] + } }; // Iterate over active effects, classifying them into categories - for ( let e of effects ) { - e._getSourceName(); // Trigger a lookup for the source name - if ( e.data.disabled ) categories.inactive.effects.push(e); - else if ( e.isTemporary ) categories.temporary.effects.push(e); - else categories.passive.effects.push(e); + for (let e of effects) { + e._getSourceName(); // Trigger a lookup for the source name + if (e.data.disabled) categories.inactive.effects.push(e); + else if (e.isTemporary) categories.temporary.effects.push(e); + else categories.passive.effects.push(e); } return categories; -} \ No newline at end of file +} diff --git a/module/item/entity.js b/module/item/entity.js index 5965dfe8..37784699 100644 --- a/module/item/entity.js +++ b/module/item/entity.js @@ -6,306 +6,313 @@ import AbilityUseDialog from "../apps/ability-use-dialog.js"; * @extends {Item} */ export default class Item5e extends Item { + /* -------------------------------------------- */ + /* Item Properties */ + /* -------------------------------------------- */ - /* -------------------------------------------- */ - /* Item Properties */ - /* -------------------------------------------- */ + /** + * Determine which ability score modifier is used by this item + * @type {string|null} + */ + get abilityMod() { + const itemData = this.data.data; + if (!("ability" in itemData)) return null; - /** - * Determine which ability score modifier is used by this item - * @type {string|null} - */ - get abilityMod() { - const itemData = this.data.data; - if (!("ability" in itemData)) return null; + // Case 1 - defined directly by the item + if (itemData.ability) return itemData.ability; + // Case 2 - inferred from a parent actor + else if (this.actor) { + const actorData = this.actor.data.data; - // Case 1 - defined directly by the item - if (itemData.ability) return itemData.ability; + // Powers - Use Actor powercasting modifier based on power school + if (this.data.type === "power") { + switch (this.data.data.school) { + case "lgt": + return "wis"; + case "uni": + return actorData.abilities["wis"].mod >= actorData.abilities["cha"].mod ? "wis" : "cha"; + case "drk": + return "cha"; + case "tec": + return "int"; + } + return "none"; + } - // Case 2 - inferred from a parent actor - else if (this.actor) { - const actorData = this.actor.data.data; + // Tools - default to Intelligence + else if (this.data.type === "tool") return "int"; + // Weapons + else if (this.data.type === "weapon") { + const wt = itemData.weaponType; - // Powers - Use Actor powercasting modifier based on power school - if (this.data.type === "power") { - switch (this.data.data.school) { - case "lgt": return "wis"; - case "uni": return (actorData.abilities["wis"].mod >= actorData.abilities["cha"].mod) ? "wis" : "cha"; - case "drk": return "cha"; - case "tec": return "int"; - } - return "none"; - } - + // Weapons using the powercasting modifier + // No current SW5e weapons use this, but it's worth checking just in case + if (["mpak", "rpak"].includes(itemData.actionType)) { + return actorData.attributes.powercasting || "int"; + } - // Tools - default to Intelligence - else if (this.data.type === "tool") return "int"; + // Finesse weapons - Str or Dex (PHB pg. 147) + else if (itemData.properties.fin === true) { + return actorData.abilities["dex"].mod >= actorData.abilities["str"].mod ? "dex" : "str"; + } - // Weapons - else if (this.data.type === "weapon") { - const wt = itemData.weaponType; - - // Weapons using the powercasting modifier - // No current SW5e weapons use this, but it's worth checking just in case - if (["mpak", "rpak"].includes(itemData.actionType)) { - return actorData.attributes.powercasting || "int"; + // Ranged weapons - Dex (PH p.194) + else if (["simpleB", "martialB"].includes(wt)) return "dex"; + } + return "str"; } - // Finesse weapons - Str or Dex (PHB pg. 147) - else if (itemData.properties.fin === true) { - return (actorData.abilities["dex"].mod >= actorData.abilities["str"].mod) ? "dex" : "str"; + // Case 3 - unknown + return null; + } + + /* -------------------------------------------- */ + + /** + * Does the Item implement an attack roll as part of its usage + * @type {boolean} + */ + get hasAttack() { + return ["mwak", "rwak", "mpak", "rpak"].includes(this.data.data.actionType); + } + + /* -------------------------------------------- */ + + /** + * Does the Item implement a damage roll as part of its usage + * @type {boolean} + */ + get hasDamage() { + return !!(this.data.data.damage && this.data.data.damage.parts.length); + } + + /* -------------------------------------------- */ + + /** + * Does the Item implement a versatile damage roll as part of its usage + * @type {boolean} + */ + get isVersatile() { + return !!(this.hasDamage && this.data.data.damage.versatile); + } + + /* -------------------------------------------- */ + + /** + * Does the item provide an amount of healing instead of conventional damage? + * @return {boolean} + */ + get isHealing() { + return this.data.data.actionType === "heal" && this.data.data.damage.parts.length; + } + + /* -------------------------------------------- */ + + /** + * Does the Item implement a saving throw as part of its usage + * @type {boolean} + */ + get hasSave() { + const save = this.data.data?.save || {}; + return !!(save.ability && save.scaling); + } + + /* -------------------------------------------- */ + + /** + * Does the Item have a target + * @type {boolean} + */ + get hasTarget() { + const target = this.data.data.target; + return target && !["none", ""].includes(target.type); + } + + /* -------------------------------------------- */ + + /** + * Does the Item have an area of effect target + * @type {boolean} + */ + get hasAreaTarget() { + const target = this.data.data.target; + return target && target.type in CONFIG.SW5E.areaTargetTypes; + } + + /* -------------------------------------------- */ + + /** + * A flag for whether this Item is limited in it's ability to be used by charges or by recharge. + * @type {boolean} + */ + get hasLimitedUses() { + let chg = this.data.data.recharge || {}; + let uses = this.data.data.uses || {}; + return !!chg.value || (uses.per && uses.max > 0); + } + + /* -------------------------------------------- */ + /* Data Preparation */ + /* -------------------------------------------- */ + + /** + * Augment the basic Item data model with additional dynamic data. + */ + prepareDerivedData() { + super.prepareDerivedData(); + + // Get the Item's data + const itemData = this.data; + const data = itemData.data; + const C = CONFIG.SW5E; + const labels = (this.labels = {}); + + // Classes + if (itemData.type === "class") { + data.levels = Math.clamped(data.levels, 1, 20); } - // Ranged weapons - Dex (PH p.194) - else if ( ["simpleB", "martialB"].includes(wt) ) return "dex"; - } - return "str"; + // Power Level, School, and Components + if (itemData.type === "power") { + data.preparation.mode = data.preparation.mode || "prepared"; + labels.level = C.powerLevels[data.level]; + labels.school = C.powerSchools[data.school]; + labels.components = Object.entries(data.components).reduce((arr, c) => { + if (c[1] !== true) return arr; + arr.push(c[0].titleCase().slice(0, 1)); + return arr; + }, []); + labels.materials = data?.materials?.value ?? null; + } + + // Feat Items + else if (itemData.type === "feat") { + const act = data.activation; + if (act && act.type === C.abilityActivationTypes.legendary) + labels.featType = game.i18n.localize("SW5E.LegendaryActionLabel"); + else if (act && act.type === C.abilityActivationTypes.lair) + labels.featType = game.i18n.localize("SW5E.LairActionLabel"); + else if (act && act.type) + labels.featType = game.i18n.localize(data.damage.length ? "SW5E.Attack" : "SW5E.Action"); + else labels.featType = game.i18n.localize("SW5E.Passive"); + } + + // TODO: Something with all this + // Species Items + else if (itemData.type === "species") { + // labels.species = C.species[data.species]; + } + // Archetype Items + else if (itemData.type === "archetype") { + // labels.archetype = C.archetype[data.archetype]; + } + // Background Items + else if (itemData.type === "background") { + // labels.background = C.background[data.background]; + } + // Class Feature Items + else if (itemData.type === "classfeature") { + // labels.classFeature = C.classFeature[data.classFeature]; + } + // Deployment Items + else if (itemData.type === "deployment") { + // labels.deployment = C.deployment[data.deployment]; + } + // Venture Items + else if (itemData.type === "venture") { + // labels.venture = C.venture[data.venture]; + } + // Fighting Style Items + else if (itemData.type === "fightingstyle") { + // labels.fightingstyle = C.fightingstyle[data.fightingstyle]; + } + // Fighting Mastery Items + else if (itemData.type === "fightingmastery") { + // labels.fightingmastery = C.fightingmastery[data.fightingmastery]; + } + // Lightsaber Form Items + else if (itemData.type === "lightsaberform") { + // labels.lightsaberform = C.lightsaberform[data.lightsaberform]; + } + + // Equipment Items + else if (itemData.type === "equipment") { + labels.armor = data.armor.value ? `${data.armor.value} ${game.i18n.localize("SW5E.AC")}` : ""; + } + + // Activated Items + if (data.hasOwnProperty("activation")) { + // Ability Activation Label + let act = data.activation || {}; + if (act) labels.activation = [act.cost, C.abilityActivationTypes[act.type]].filterJoin(" "); + + // Target Label + let tgt = data.target || {}; + if (["none", "touch", "self"].includes(tgt.units)) tgt.value = null; + if (["none", "self"].includes(tgt.type)) { + tgt.value = null; + tgt.units = null; + } + labels.target = [tgt.value, C.distanceUnits[tgt.units], C.targetTypes[tgt.type]].filterJoin(" "); + + // Range Label + let rng = data.range || {}; + if (["none", "touch", "self"].includes(rng.units)) { + rng.value = null; + rng.long = null; + } + labels.range = [rng.value, rng.long ? `/ ${rng.long}` : null, C.distanceUnits[rng.units]].filterJoin(" "); + + // Duration Label + let dur = data.duration || {}; + if (["inst", "perm"].includes(dur.units)) dur.value = null; + labels.duration = [dur.value, C.timePeriods[dur.units]].filterJoin(" "); + + // Recharge Label + let chg = data.recharge || {}; + labels.recharge = `${game.i18n.localize("SW5E.Recharge")} [${chg.value}${ + parseInt(chg.value) < 6 ? "+" : "" + }]`; + } + + // Item Actions + if (data.hasOwnProperty("actionType")) { + // Damage + let dam = data.damage || {}; + if (dam.parts) { + labels.damage = dam.parts + .map((d) => d[0]) + .join(" + ") + .replace(/\+ -/g, "- "); + labels.damageTypes = dam.parts.map((d) => C.damageTypes[d[1]]).join(", "); + } + } + + // if this item is owned, we prepareFinalAttributes() at the end of actor init + if (!this.isOwned) this.prepareFinalAttributes(); } - // Case 3 - unknown - return null - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** + * Compute item attributes which might depend on prepared actor data. + */ + prepareFinalAttributes() { + if (this.data.data.hasOwnProperty("actionType")) { + // Saving throws + this.getSaveDC(); - /** - * Does the Item implement an attack roll as part of its usage - * @type {boolean} - */ - get hasAttack() { - return ["mwak", "rwak", "mpak", "rpak"].includes(this.data.data.actionType); - } + // To Hit + this.getAttackToHit(); - /* -------------------------------------------- */ + // Limited Uses + this.prepareMaxUses(); - /** - * Does the Item implement a damage roll as part of its usage - * @type {boolean} - */ - get hasDamage() { - return !!(this.data.data.damage && this.data.data.damage.parts.length); - } - - /* -------------------------------------------- */ - - /** - * Does the Item implement a versatile damage roll as part of its usage - * @type {boolean} - */ - get isVersatile() { - return !!(this.hasDamage && this.data.data.damage.versatile); - } - - /* -------------------------------------------- */ - - /** - * Does the item provide an amount of healing instead of conventional damage? - * @return {boolean} - */ - get isHealing() { - return (this.data.data.actionType === "heal") && this.data.data.damage.parts.length; - } - - /* -------------------------------------------- */ - - /** - * Does the Item implement a saving throw as part of its usage - * @type {boolean} - */ - get hasSave() { - const save = this.data.data?.save || {}; - return !!(save.ability && save.scaling); - } - - /* -------------------------------------------- */ - - /** - * Does the Item have a target - * @type {boolean} - */ - get hasTarget() { - const target = this.data.data.target; - return target && !["none",""].includes(target.type); - } - - /* -------------------------------------------- */ - - /** - * Does the Item have an area of effect target - * @type {boolean} - */ - get hasAreaTarget() { - const target = this.data.data.target; - return target && (target.type in CONFIG.SW5E.areaTargetTypes); - } - - /* -------------------------------------------- */ - - /** - * A flag for whether this Item is limited in it's ability to be used by charges or by recharge. - * @type {boolean} - */ - get hasLimitedUses() { - let chg = this.data.data.recharge || {}; - let uses = this.data.data.uses || {}; - return !!chg.value || (uses.per && (uses.max > 0)); - } - - /* -------------------------------------------- */ - /* Data Preparation */ - /* -------------------------------------------- */ - - /** - * Augment the basic Item data model with additional dynamic data. - */ - prepareDerivedData() { - super.prepareDerivedData(); - - // Get the Item's data - const itemData = this.data; - const data = itemData.data; - const C = CONFIG.SW5E; - const labels = this.labels = {}; - - // Classes - if ( itemData.type === "class" ) { - data.levels = Math.clamped(data.levels, 1, 20); + // Damage Label + this.getDerivedDamageLabel(); + } } - // Power Level, School, and Components - if ( itemData.type === "power" ) { - data.preparation.mode = data.preparation.mode || "prepared"; - labels.level = C.powerLevels[data.level]; - labels.school = C.powerSchools[data.school]; - labels.components = Object.entries(data.components).reduce((arr, c) => { - if ( c[1] !== true ) return arr; - arr.push(c[0].titleCase().slice(0, 1)); - return arr; - }, []); - labels.materials = data?.materials?.value ?? null; - } - - // Feat Items - else if ( itemData.type === "feat" ) { - const act = data.activation; - if ( act && (act.type === C.abilityActivationTypes.legendary) ) labels.featType = game.i18n.localize("SW5E.LegendaryActionLabel"); - else if ( act && (act.type === C.abilityActivationTypes.lair) ) labels.featType = game.i18n.localize("SW5E.LairActionLabel"); - else if ( act && act.type ) labels.featType = game.i18n.localize(data.damage.length ? "SW5E.Attack" : "SW5E.Action"); - else labels.featType = game.i18n.localize("SW5E.Passive"); - } - - // TODO: Something with all this - // Species Items - else if ( itemData.type === "species" ) { - // labels.species = C.species[data.species]; - } - // Archetype Items - else if ( itemData.type === "archetype" ) { - // labels.archetype = C.archetype[data.archetype]; - } - // Background Items - else if ( itemData.type === "background" ) { - // labels.background = C.background[data.background]; - } - // Class Feature Items - else if ( itemData.type === "classfeature" ) { - // labels.classFeature = C.classFeature[data.classFeature]; - } - // Deployment Items - else if ( itemData.type === "deployment" ) { - // labels.deployment = C.deployment[data.deployment]; - } - // Venture Items - else if ( itemData.type === "venture" ) { - // labels.venture = C.venture[data.venture]; - } - // Fighting Style Items - else if ( itemData.type === "fightingstyle" ) { - // labels.fightingstyle = C.fightingstyle[data.fightingstyle]; - } - // Fighting Mastery Items - else if ( itemData.type === "fightingmastery" ) { - // labels.fightingmastery = C.fightingmastery[data.fightingmastery]; - } - // Lightsaber Form Items - else if ( itemData.type === "lightsaberform" ) { - // labels.lightsaberform = C.lightsaberform[data.lightsaberform]; - } - - // Equipment Items - else if ( itemData.type === "equipment" ) { - labels.armor = data.armor.value ? `${data.armor.value} ${game.i18n.localize("SW5E.AC")}` : ""; - } - - // Activated Items - if ( data.hasOwnProperty("activation") ) { - - // Ability Activation Label - let act = data.activation || {}; - if ( act ) labels.activation = [act.cost, C.abilityActivationTypes[act.type]].filterJoin(" "); - - // Target Label - let tgt = data.target || {}; - if (["none", "touch", "self"].includes(tgt.units)) tgt.value = null; - if (["none", "self"].includes(tgt.type)) { - tgt.value = null; - tgt.units = null; - } - labels.target = [tgt.value, C.distanceUnits[tgt.units], C.targetTypes[tgt.type]].filterJoin(" "); - - // Range Label - let rng = data.range || {}; - if ( ["none", "touch", "self"].includes(rng.units) ) { - rng.value = null; - rng.long = null; - } - labels.range = [rng.value, rng.long ? `/ ${rng.long}` : null, C.distanceUnits[rng.units]].filterJoin(" "); - - // Duration Label - let dur = data.duration || {}; - if (["inst", "perm"].includes(dur.units)) dur.value = null; - labels.duration = [dur.value, C.timePeriods[dur.units]].filterJoin(" "); - - // Recharge Label - let chg = data.recharge || {}; - labels.recharge = `${game.i18n.localize("SW5E.Recharge")} [${chg.value}${parseInt(chg.value) < 6 ? "+" : ""}]`; - } - - // Item Actions - if ( data.hasOwnProperty("actionType") ) { - // Damage - let dam = data.damage || {}; - if (dam.parts) { - labels.damage = dam.parts.map(d => d[0]).join(" + ").replace(/\+ -/g, "- "); - labels.damageTypes = dam.parts.map(d => C.damageTypes[d[1]]).join(", "); - } - } - - // if this item is owned, we prepareFinalAttributes() at the end of actor init - if (!this.isOwned) this.prepareFinalAttributes(); - } - - /* -------------------------------------------- */ - - /** - * Compute item attributes which might depend on prepared actor data. - */ - prepareFinalAttributes() { - if ( this.data.data.hasOwnProperty("actionType") ) { - // Saving throws - this.getSaveDC(); - - // To Hit - this.getAttackToHit(); - - // Limited Uses - this.prepareMaxUses(); - - // Damage Label - this.getDerivedDamageLabel(); - } - } - /* -------------------------------------------- */ /** @@ -316,1321 +323,1349 @@ export default class Item5e extends Item { * @returns {Array} array of objects with `formula` and `damageType` */ getDerivedDamageLabel() { - const itemData = this.data.data; - if ( !this.hasDamage || !itemData || !this.isOwned ) return []; + const itemData = this.data.data; + if (!this.hasDamage || !itemData || !this.isOwned) return []; - const rollData = this.getRollData(); + const rollData = this.getRollData(); - const derivedDamage = itemData.damage?.parts?.map((damagePart) => ({ - formula: simplifyRollFormula(damagePart[0], rollData, { constantFirst: false }), - damageType: damagePart[1], - })); + const derivedDamage = itemData.damage?.parts?.map((damagePart) => ({ + formula: simplifyRollFormula(damagePart[0], rollData, {constantFirst: false}), + damageType: damagePart[1] + })); - this.labels.derivedDamage = derivedDamage + this.labels.derivedDamage = derivedDamage; - return derivedDamage; + return derivedDamage; } - /* -------------------------------------------- */ + /* -------------------------------------------- */ - /** - * Update the derived power DC for an item that requires a saving throw - * @returns {number|null} - */ - getSaveDC() { - if ( !this.hasSave ) return; - const save = this.data.data?.save; + /** + * Update the derived power DC for an item that requires a saving throw + * @returns {number|null} + */ + getSaveDC() { + if (!this.hasSave) return; + const save = this.data.data?.save; - // Actor power-DC based scaling - if ( save.scaling === "power" ) { - switch (this.data.data.school) { - case "lgt": { - save.dc = this.isOwned ? getProperty(this.actor.data, "data.attributes.powerForceLightDC") : null; - break; - } - case "uni": { - save.dc = this.isOwned ? getProperty(this.actor.data, "data.attributes.powerForceUnivDC") : null; - break; - } - case "drk": { - save.dc = this.isOwned ? getProperty(this.actor.data, "data.attributes.powerForceDarkDC") : null; - break; - } - case "tec": { - save.dc = this.isOwned ? getProperty(this.actor.data, "data.attributes.powerTechDC") : null; - break; - } - } - } - - // Ability-score based scaling - else if ( save.scaling !== "flat" ) { - save.dc = this.isOwned ? getProperty(this.actor.data, `data.abilities.${save.scaling}.dc`) : null; - } - - // Update labels - const abl = CONFIG.SW5E.abilities[save.ability]; - this.labels.save = game.i18n.format("SW5E.SaveDC", {dc: save.dc || "", ability: abl}); - return save.dc; - } - - /* -------------------------------------------- */ - - /** - * Update a label to the Item detailing its total to hit bonus. - * Sources: - * - item entity's innate attack bonus - * - item's actor's proficiency bonus if applicable - * - item's actor's global bonuses to the given item type - * - item's ammunition if applicable - * - * @returns {Object} returns `rollData` and `parts` to be used in the item's Attack roll - */ - getAttackToHit() { - const itemData = this.data.data; - if ( !this.hasAttack || !itemData ) return; - const rollData = this.getRollData(); - - // Define Roll bonuses - const parts = []; - - // Include the item's innate attack bonus as the initial value and label - if ( itemData.attackBonus ) { - parts.push(itemData.attackBonus) - this.labels.toHit = itemData.attackBonus; - } - - // Take no further action for un-owned items - if ( !this.isOwned ) return {rollData, parts}; - - // Ability score modifier - parts.push(`@mod`); - - // Add proficiency bonus if an explicit proficiency flag is present or for non-item features - if ( !["weapon", "consumable"].includes(this.data.type) || itemData.proficient ) { - parts.push("@prof"); - } - - // Actor-level global bonus to attack rolls - const actorBonus = this.actor.data.data.bonuses?.[itemData.actionType] || {}; - if ( actorBonus.attack ) parts.push(actorBonus.attack); - - // One-time bonus provided by consumed ammunition - if ( (itemData.consume?.type === 'ammo') && !!this.actor.items ) { - const ammoItemData = this.actor.items.get(itemData.consume.target)?.data; - - if (ammoItemData) { - const ammoItemQuantity = ammoItemData.data.quantity; - const ammoCanBeConsumed = ammoItemQuantity && (ammoItemQuantity - (itemData.consume.amount ?? 0) >= 0); - const ammoItemAttackBonus = ammoItemData.data.attackBonus; - const ammoIsTypeConsumable = (ammoItemData.type === "consumable") && (ammoItemData.data.consumableType === "ammo") - if ( ammoCanBeConsumed && ammoItemAttackBonus && ammoIsTypeConsumable ) { - parts.push("@ammo"); - rollData["ammo"] = ammoItemAttackBonus; - } - } - } - - // Condense the resulting attack bonus formula into a simplified label - let toHitLabel = simplifyRollFormula(parts.join('+'), rollData).trim(); - if (toHitLabel.charAt(0) !== '-') { - toHitLabel = '+ ' + toHitLabel - } - this.labels.toHit = toHitLabel; - - // Update labels and return the prepared roll data - return {rollData, parts}; - } - - /* -------------------------------------------- */ - - /** - * Populates the max uses of an item. - * If the item is an owned item and the `max` is not numeric, calculate based on actor data. - */ - prepareMaxUses() { - const data = this.data.data; - if (!data.uses?.max) return; - let max = data.uses.max; - - // if this is an owned item and the max is not numeric, we need to calculate it - if (this.isOwned && !Number.isNumeric(max)) { - if (this.actor.data === undefined) return; - try { - max = Roll.replaceFormulaData(max, this.actor.getRollData(), {missing: 0, warn: true}); - max = Roll.safeEval(max); - } catch(e) { - console.error('Problem preparing Max uses for', this.data.name, e); - return; - } - } - data.uses.max = Number(max); - } - - /* -------------------------------------------- */ - - /** - * Roll the item to Chat, creating a chat card which contains follow up attack or damage roll options - * @param {boolean} [configureDialog] Display a configuration dialog for the item roll, if applicable? - * @param {string} [rollMode] The roll display mode with which to display (or not) the card - * @param {boolean} [createMessage] Whether to automatically create a chat message (if true) or simply return - * the prepared chat message data (if false). - * @return {Promise} - */ - async roll({configureDialog=true, rollMode, createMessage=true}={}) { - let item = this; - const id = this.data.data; // Item system data - const actor = this.actor; - const ad = actor.data.data; // Actor system data - - // Reference aspects of the item data necessary for usage - const hasArea = this.hasAreaTarget; // Is the ability usage an AoE? - const resource = id.consume || {}; // Resource consumption - const recharge = id.recharge || {}; // Recharge mechanic - const uses = id?.uses ?? {}; // Limited uses - const isPower = this.type === "power"; // Does the item require a power slot? - // TODO: Possibly Mod this to not consume slots based on class? - // We could use this for feats and architypes that let a character cast one slot every rest or so - const requirePowerSlot = isPower && (id.level > 0) && CONFIG.SW5E.powerUpcastModes.includes(id.preparation.mode); - - // Define follow-up actions resulting from the item usage - let createMeasuredTemplate = hasArea; // Trigger a template creation - let consumeRecharge = !!recharge.value; // Consume recharge - let consumeResource = !!resource.target && resource.type !== "ammo"; // Consume a linked (non-ammo) resource - let consumePowerSlot = requirePowerSlot; // Consume a power slot - let consumeUsage = !!uses.per; // Consume limited uses - let consumeQuantity = uses.autoDestroy; // Consume quantity of the item in lieu of uses - let consumePowerLevel = null; // Consume a specific category of power slot - if ( requirePowerSlot ) consumePowerLevel = id.preparation.mode === "pact" ? "pact" : `power${id.level}`; - - // Display a configuration dialog to customize the usage - const needsConfiguration = createMeasuredTemplate || consumeRecharge || (consumeResource && !['simpleB', 'martialB'].includes(id.weaponType)) || consumePowerSlot || (consumeUsage && !['simpleB', 'martialB'].includes(id.weaponType)); - if (configureDialog && needsConfiguration) { - const configuration = await AbilityUseDialog.create(this); - if (!configuration) return; - - - // Determine consumption preferences - createMeasuredTemplate = Boolean(configuration.placeTemplate); - consumeUsage = Boolean(configuration.consumeUse); - consumeRecharge = Boolean(configuration.consumeRecharge); - consumeResource = Boolean(configuration.consumeResource); - consumePowerSlot = Boolean(configuration.consumeSlot); - - // Handle power upcasting - if ( requirePowerSlot ) { - consumePowerLevel = `power${configuration.level}`; - if (consumePowerSlot === false) consumePowerLevel = null; - const upcastLevel = parseInt(configuration.level); - if (upcastLevel !== id.level) { - item = this.clone({"data.level": upcastLevel}, {keepId: true}); - item.data.update({_id: this.id}); // Retain the original ID (needed until 0.8.2+) - item.prepareFinalAttributes(); // Power save DC, etc... - } - } - } - - // Determine whether the item can be used by testing for resource consumption - const usage = item._getUsageUpdates({consumeRecharge, consumeResource, consumePowerLevel, consumeUsage, consumeQuantity}); - if ( !usage ) return; - - const {actorUpdates, itemUpdates, resourceUpdates} = usage; - - // Commit pending data updates - if ( !foundry.utils.isObjectEmpty(itemUpdates) ) await item.update(itemUpdates); - if ( consumeQuantity && (item.data.data.quantity === 0) ) await item.delete(); - if ( !foundry.utils.isObjectEmpty(actorUpdates) ) await actor.update(actorUpdates); - if ( !foundry.utils.isObjectEmpty(resourceUpdates) ) { - const resource = actor.items.get(id.consume?.target); - if ( resource ) await resource.update(resourceUpdates); - } - - // Initiate measured template creation - if ( createMeasuredTemplate ) { - const template = game.sw5e.canvas.AbilityTemplate.fromItem(item); - if ( template ) template.drawPreview(); - } - - // Create or return the Chat Message data - return item.displayCard({rollMode, createMessage}); - } - - /* -------------------------------------------- */ - - /** - * Verify that the consumed resources used by an Item are available. - * Otherwise display an error and return false. - * @param {boolean} consumeQuantity Consume quantity of the item if other consumption modes are not available? - * @param {boolean} consumeRecharge Whether the item consumes the recharge mechanic - * @param {boolean} consumeResource Whether the item consumes a limited resource - * @param {string|null} consumePowerLevel The category of power slot to consume, or null - * @param {boolean} consumeUsage Whether the item consumes a limited usage - * @returns {object|boolean} A set of data changes to apply when the item is used, or false - * @private - */ - _getUsageUpdates({consumeQuantity, consumeRecharge, consumeResource, consumePowerLevel, consumeUsage}) { - - // Reference item data - const id = this.data.data; - const actorUpdates = {}; - const itemUpdates = {}; - const resourceUpdates = {}; - - // Consume Recharge - if ( consumeRecharge ) { - const recharge = id.recharge || {}; - if ( recharge.charged === false ) { - ui.notifications.warn(game.i18n.format("SW5E.ItemNoUses", {name: this.name})); - return false; - } - itemUpdates["data.recharge.charged"] = false; - } - - // Consume Limited Resource - if ( consumeResource ) { - const canConsume = this._handleConsumeResource(itemUpdates, actorUpdates, resourceUpdates); - if ( canConsume === false ) return false; - } - - // Consume Power Slots and Force/Tech Points - if ( consumePowerLevel ) { - if ( Number.isNumeric(consumePowerLevel) ) consumePowerLevel = `power${consumePowerLevel}`; - const level = this.actor?.data.data.powers[consumePowerLevel]; - const fp = this.actor.data.data.attributes.force.points; - const tp = this.actor.data.data.attributes.tech.points; - const powerCost = parseInt(id.level,10) + 1; - const innatePower = this.actor.data.data.attributes.powercasting === 'innate'; - if (!innatePower){ - switch (id.school){ - case "lgt": - case "uni": - case "drk": { - const powers = Number(level?.fvalue ?? 0); - if ( powers === 0 ) { - const label = game.i18n.localize(`SW5E.PowerLevel${id.level}`); - ui.notifications.warn(game.i18n.format("SW5E.PowerCastNoSlots", {name: this.name, level: label})); - return false; + // Actor power-DC based scaling + if (save.scaling === "power") { + switch (this.data.data.school) { + case "lgt": { + save.dc = this.isOwned ? getProperty(this.actor.data, "data.attributes.powerForceLightDC") : null; + break; + } + case "uni": { + save.dc = this.isOwned ? getProperty(this.actor.data, "data.attributes.powerForceUnivDC") : null; + break; + } + case "drk": { + save.dc = this.isOwned ? getProperty(this.actor.data, "data.attributes.powerForceDarkDC") : null; + break; + } + case "tec": { + save.dc = this.isOwned ? getProperty(this.actor.data, "data.attributes.powerTechDC") : null; + break; + } } - actorUpdates[`data.powers.${consumePowerLevel}.fvalue`] = Math.max(powers - 1, 0); - if (fp.temp >= powerCost) { - actorUpdates["data.attributes.force.points.temp"] = fp.temp - powerCost; - }else{ - actorUpdates["data.attributes.force.points.value"] = fp.value + fp.temp - powerCost; - actorUpdates["data.attributes.force.points.temp"] = 0; + } + + // Ability-score based scaling + else if (save.scaling !== "flat") { + save.dc = this.isOwned ? getProperty(this.actor.data, `data.abilities.${save.scaling}.dc`) : null; + } + + // Update labels + const abl = CONFIG.SW5E.abilities[save.ability]; + this.labels.save = game.i18n.format("SW5E.SaveDC", {dc: save.dc || "", ability: abl}); + return save.dc; + } + + /* -------------------------------------------- */ + + /** + * Update a label to the Item detailing its total to hit bonus. + * Sources: + * - item entity's innate attack bonus + * - item's actor's proficiency bonus if applicable + * - item's actor's global bonuses to the given item type + * - item's ammunition if applicable + * + * @returns {Object} returns `rollData` and `parts` to be used in the item's Attack roll + */ + getAttackToHit() { + const itemData = this.data.data; + if (!this.hasAttack || !itemData) return; + const rollData = this.getRollData(); + + // Define Roll bonuses + const parts = []; + + // Include the item's innate attack bonus as the initial value and label + if (itemData.attackBonus) { + parts.push(itemData.attackBonus); + this.labels.toHit = itemData.attackBonus; + } + + // Take no further action for un-owned items + if (!this.isOwned) return {rollData, parts}; + + // Ability score modifier + parts.push(`@mod`); + + // Add proficiency bonus if an explicit proficiency flag is present or for non-item features + if (!["weapon", "consumable"].includes(this.data.type) || itemData.proficient) { + parts.push("@prof"); + } + + // Actor-level global bonus to attack rolls + const actorBonus = this.actor.data.data.bonuses?.[itemData.actionType] || {}; + if (actorBonus.attack) parts.push(actorBonus.attack); + + // One-time bonus provided by consumed ammunition + if (itemData.consume?.type === "ammo" && !!this.actor.items) { + const ammoItemData = this.actor.items.get(itemData.consume.target)?.data; + + if (ammoItemData) { + const ammoItemQuantity = ammoItemData.data.quantity; + const ammoCanBeConsumed = ammoItemQuantity && ammoItemQuantity - (itemData.consume.amount ?? 0) >= 0; + const ammoItemAttackBonus = ammoItemData.data.attackBonus; + const ammoIsTypeConsumable = + ammoItemData.type === "consumable" && ammoItemData.data.consumableType === "ammo"; + if (ammoCanBeConsumed && ammoItemAttackBonus && ammoIsTypeConsumable) { + parts.push("@ammo"); + rollData["ammo"] = ammoItemAttackBonus; + } } - break; - } - case "tec": { - const powers = Number(level?.tvalue ?? 0); - if ( powers === 0 ) { - const label = game.i18n.localize(`SW5E.PowerLevel${id.level}`); - ui.notifications.warn(game.i18n.format("SW5E.PowerCastNoSlots", {name: this.name, level: label})); - return false; + } + + // Condense the resulting attack bonus formula into a simplified label + let toHitLabel = simplifyRollFormula(parts.join("+"), rollData).trim(); + if (toHitLabel.charAt(0) !== "-") { + toHitLabel = "+ " + toHitLabel; + } + this.labels.toHit = toHitLabel; + + // Update labels and return the prepared roll data + return {rollData, parts}; + } + + /* -------------------------------------------- */ + + /** + * Populates the max uses of an item. + * If the item is an owned item and the `max` is not numeric, calculate based on actor data. + */ + prepareMaxUses() { + const data = this.data.data; + if (!data.uses?.max) return; + let max = data.uses.max; + + // if this is an owned item and the max is not numeric, we need to calculate it + if (this.isOwned && !Number.isNumeric(max)) { + if (this.actor.data === undefined) return; + try { + max = Roll.replaceFormulaData(max, this.actor.getRollData(), {missing: 0, warn: true}); + max = Roll.safeEval(max); + } catch (e) { + console.error("Problem preparing Max uses for", this.data.name, e); + return; } - actorUpdates[`data.powers.${consumePowerLevel}.tvalue`] = Math.max(powers - 1, 0); - if (tp.temp >= powerCost) { - actorUpdates["data.attributes.tech.points.temp"] = tp.temp - powerCost; - }else{ - actorUpdates["data.attributes.tech.points.value"] = tp.value + tp.temp - powerCost; - actorUpdates["data.attributes.tech.points.temp"] = 0; + } + data.uses.max = Number(max); + } + + /* -------------------------------------------- */ + + /** + * Roll the item to Chat, creating a chat card which contains follow up attack or damage roll options + * @param {boolean} [configureDialog] Display a configuration dialog for the item roll, if applicable? + * @param {string} [rollMode] The roll display mode with which to display (or not) the card + * @param {boolean} [createMessage] Whether to automatically create a chat message (if true) or simply return + * the prepared chat message data (if false). + * @return {Promise} + */ + async roll({configureDialog = true, rollMode, createMessage = true} = {}) { + let item = this; + const id = this.data.data; // Item system data + const actor = this.actor; + const ad = actor.data.data; // Actor system data + + // Reference aspects of the item data necessary for usage + const hasArea = this.hasAreaTarget; // Is the ability usage an AoE? + const resource = id.consume || {}; // Resource consumption + const recharge = id.recharge || {}; // Recharge mechanic + const uses = id?.uses ?? {}; // Limited uses + const isPower = this.type === "power"; // Does the item require a power slot? + // TODO: Possibly Mod this to not consume slots based on class? + // We could use this for feats and architypes that let a character cast one slot every rest or so + const requirePowerSlot = isPower && id.level > 0 && CONFIG.SW5E.powerUpcastModes.includes(id.preparation.mode); + + // Define follow-up actions resulting from the item usage + let createMeasuredTemplate = hasArea; // Trigger a template creation + let consumeRecharge = !!recharge.value; // Consume recharge + let consumeResource = !!resource.target && resource.type !== "ammo"; // Consume a linked (non-ammo) resource + let consumePowerSlot = requirePowerSlot; // Consume a power slot + let consumeUsage = !!uses.per; // Consume limited uses + let consumeQuantity = uses.autoDestroy; // Consume quantity of the item in lieu of uses + let consumePowerLevel = null; // Consume a specific category of power slot + if (requirePowerSlot) consumePowerLevel = id.preparation.mode === "pact" ? "pact" : `power${id.level}`; + + // Display a configuration dialog to customize the usage + const needsConfiguration = + createMeasuredTemplate || + consumeRecharge || + (consumeResource && !["simpleB", "martialB"].includes(id.weaponType)) || + consumePowerSlot || + (consumeUsage && !["simpleB", "martialB"].includes(id.weaponType)); + if (configureDialog && needsConfiguration) { + const configuration = await AbilityUseDialog.create(this); + if (!configuration) return; + + // Determine consumption preferences + createMeasuredTemplate = Boolean(configuration.placeTemplate); + consumeUsage = Boolean(configuration.consumeUse); + consumeRecharge = Boolean(configuration.consumeRecharge); + consumeResource = Boolean(configuration.consumeResource); + consumePowerSlot = Boolean(configuration.consumeSlot); + + // Handle power upcasting + if (requirePowerSlot) { + consumePowerLevel = `power${configuration.level}`; + if (consumePowerSlot === false) consumePowerLevel = null; + const upcastLevel = parseInt(configuration.level); + if (upcastLevel !== id.level) { + item = this.clone({"data.level": upcastLevel}, {keepId: true}); + item.data.update({_id: this.id}); // Retain the original ID (needed until 0.8.2+) + item.prepareFinalAttributes(); // Power save DC, etc... + } } - break; - } } - } - } - - // Consume Limited Usage - if ( consumeUsage ) { - const uses = id.uses || {}; - const available = Number(uses.value ?? 0); - let used = false; - - // Reduce usages - const remaining = Math.max(available - 1, 0); - if ( available >= 1 ) { - used = true; - itemUpdates["data.uses.value"] = remaining; - } - - // Reduce quantity if not reducing usages or if usages hit 0 and we are set to consumeQuantity - if ( consumeQuantity && (!used || (remaining === 0)) ) { - const q = Number(id.quantity ?? 1); - if ( q >= 1 ) { - used = true; - itemUpdates["data.quantity"] = Math.max(q - 1, 0); - itemUpdates["data.uses.value"] = uses.max ?? 1; - } - } - - // If the item was not used, return a warning - if ( !used ) { - ui.notifications.warn(game.i18n.format("SW5E.ItemNoUses", {name: this.name})); - return false; - } - } - - // Return the configured usage - return {itemUpdates, actorUpdates, resourceUpdates}; - } - - /* -------------------------------------------- */ - - /** - * Handle update actions required when consuming an external resource - * @param {object} itemUpdates An object of data updates applied to this item - * @param {object} actorUpdates An object of data updates applied to the item owner (Actor) - * @param {object} resourceUpdates An object of data updates applied to a different resource item (Item) - * @return {boolean|void} Return false to block further progress, or return nothing to continue - * @private - */ - _handleConsumeResource(itemUpdates, actorUpdates, resourceUpdates) { - const actor = this.actor; - const itemData = this.data.data; - const consume = itemData.consume || {}; - if ( !consume.type ) return; - - // No consumed target - const typeLabel = CONFIG.SW5E.abilityConsumptionTypes[consume.type]; - if ( !consume.target ) { - ui.notifications.warn(game.i18n.format("SW5E.ConsumeWarningNoResource", {name: this.name, type: typeLabel})); - return false; - } - - // Identify the consumed resource and its current quantity - let resource = null; - let amount = Number(consume.amount ?? 1); - let quantity = 0; - switch ( consume.type ) { - case "attribute": - resource = getProperty(actor.data.data, consume.target); - quantity = resource || 0; - break; - case "ammo": - case "material": - resource = actor.items.get(consume.target); - quantity = resource ? resource.data.data.quantity : 0; - break; - case "charges": - resource = actor.items.get(consume.target); - if ( !resource ) break; - const uses = resource.data.data.uses; - if ( uses.per && uses.max ) quantity = uses.value; - else if ( resource.data.data.recharge?.value ) { - quantity = resource.data.data.recharge.charged ? 1 : 0; - amount = 1; - } - break; - } - - // Verify that a consumed resource is available - if ( !resource ) { - ui.notifications.warn(game.i18n.format("SW5E.ConsumeWarningNoSource", {name: this.name, type: typeLabel})); - return false; - } - - // Verify that the required quantity is available - let remaining = quantity - amount; - if ( remaining < 0 ) { - ui.notifications.warn(game.i18n.format("SW5E.ConsumeWarningNoQuantity", {name: this.name, type: typeLabel})); - return false; - } - - // Define updates to provided data objects - switch ( consume.type ) { - case "attribute": - actorUpdates[`data.${consume.target}`] = remaining; - break; - case "ammo": - case "material": - resourceUpdates["data.quantity"] = remaining; - break; - case "charges": - const uses = resource.data.data.uses || {}; - const recharge = resource.data.data.recharge || {}; - if ( uses.per && uses.max ) resourceUpdates["data.uses.value"] = remaining; - else if ( recharge.value ) resourceUpdates["data.recharge.charged"] = false; - break; - } - } - - /* -------------------------------------------- */ - - /** - * Display the chat card for an Item as a Chat Message - * @param {object} options Options which configure the display of the item chat card - * @param {string} rollMode The message visibility mode to apply to the created card - * @param {boolean} createMessage Whether to automatically create a ChatMessage entity (if true), or only return - * the prepared message data (if false) - */ - async displayCard({rollMode, createMessage=true}={}) { - - // Render the chat card template - const token = this.actor.token; - const templateData = { - actor: this.actor, - tokenId: token?.uuid || null, - item: this.data, - data: this.getChatData(), - labels: this.labels, - hasAttack: this.hasAttack, - isHealing: this.isHealing, - hasDamage: this.hasDamage, - isVersatile: this.isVersatile, - isPower: this.data.type === "power", - hasSave: this.hasSave, - hasAreaTarget: this.hasAreaTarget, - isTool: this.data.type === "tool" - }; - const html = await renderTemplate("systems/sw5e/templates/chat/item-card.html", templateData); - - // Create the ChatMessage data object - const chatData = { - user: game.user.data._id, - type: CONST.CHAT_MESSAGE_TYPES.OTHER, - content: html, - flavor: this.data.data.chatFlavor || this.name, - speaker: ChatMessage.getSpeaker({actor: this.actor, token}), - flags: {"core.canPopout": true} - }; - - // If the Item was destroyed in the process of displaying its card - embed the item data in the chat message - if ( (this.data.type === "consumable") && !this.actor.items.has(this.id) ) { - chatData.flags["sw5e.itemData"] = this.data; - } - - // Apply the roll mode to adjust message visibility - ChatMessage.applyRollMode(chatData, rollMode || game.settings.get("core", "rollMode")); - - // Create the Chat Message or return its data - return createMessage ? ChatMessage.create(chatData) : chatData; - } - - /* -------------------------------------------- */ - /* Chat Cards */ - /* -------------------------------------------- */ - - /** - * Prepare an object of chat data used to display a card for the Item in the chat log - * @param {Object} htmlOptions Options used by the TextEditor.enrichHTML function - * @return {Object} An object of chat data to render - */ - getChatData(htmlOptions={}) { - const data = foundry.utils.deepClone(this.data.data); - const labels = this.labels; - - // Rich text description - data.description.value = TextEditor.enrichHTML(data.description.value, htmlOptions); - - // Item type specific properties - const props = []; - const fn = this[`_${this.data.type}ChatData`]; - if ( fn ) fn.bind(this)(data, labels, props); - - // Equipment properties - if ( data.hasOwnProperty("equipped") && !["loot", "tool"].includes(this.data.type) ) { - if ( data.attunement === CONFIG.SW5E.attunementTypes.REQUIRED ) props.push(game.i18n.localize(CONFIG.SW5E.attunements[CONFIG.SW5E.attunementTypes.REQUIRED])); - props.push( - game.i18n.localize(data.equipped ? "SW5E.Equipped" : "SW5E.Unequipped"), - game.i18n.localize(data.proficient ? "SW5E.Proficient" : "SW5E.NotProficient"), - ); - } - - // Ability activation properties - if ( data.hasOwnProperty("activation") ) { - props.push( - labels.activation + (data.activation?.condition ? ` (${data.activation.condition})` : ""), - labels.target, - labels.range, - labels.duration - ); - } - - // Filter properties and return - data.properties = props.filter(p => !!p); - return data; - } - - /* -------------------------------------------- */ - - /** - * Prepare chat card data for equipment type items - * @private - */ - _equipmentChatData(data, labels, props) { - props.push( - CONFIG.SW5E.equipmentTypes[data.armor.type], - labels.armor || null, - data.stealth.value ? game.i18n.localize("SW5E.StealthDisadvantage") : null - ); - } - - /* -------------------------------------------- */ - - /** - * Prepare chat card data for weapon type items - * @private - */ - _weaponChatData(data, labels, props) { - props.push( - CONFIG.SW5E.weaponTypes[data.weaponType], - ); - } - - /* -------------------------------------------- */ - - /** - * Prepare chat card data for consumable type items - * @private - */ - _consumableChatData(data, labels, props) { - props.push( - CONFIG.SW5E.consumableTypes[data.consumableType], - data.uses.value + "/" + data.uses.max + " " + game.i18n.localize("SW5E.Charges") - ); - data.hasCharges = data.uses.value >= 0; - } - - /* -------------------------------------------- */ - - /** - * Prepare chat card data for tool type items - * @private - */ - _toolChatData(data, labels, props) { - props.push( - CONFIG.SW5E.abilities[data.ability] || null, - CONFIG.SW5E.proficiencyLevels[data.proficient || 0] - ); - } - - /* -------------------------------------------- */ - - /** - * Prepare chat card data for loot type items - * @private - */ - _lootChatData(data, labels, props) { - props.push( - game.i18n.localize("SW5E.ItemTypeLoot"), - data.weight ? data.weight + " " + game.i18n.localize("SW5E.AbbreviationLbs") : null - ); - } - - /* -------------------------------------------- */ - - /** - * Render a chat card for Power type data - * @return {Object} - * @private - */ - _powerChatData(data, labels, props) { - props.push( - labels.level, - labels.components + (labels.materials ? ` (${labels.materials})` : "") - ); - } - - /* -------------------------------------------- */ - - /** - * Prepare chat card data for items of the "Feat" type - * @private - */ - _featChatData(data, labels, props) { - props.push(data.requirements); - } - - /* -------------------------------------------- */ - /* Item Rolls - Attack, Damage, Saves, Checks */ - /* -------------------------------------------- */ - - /** - * Place an attack roll using an item (weapon, feat, power, or equipment) - * Rely upon the d20Roll logic for the core implementation - * - * @param {object} options Roll options which are configured and provided to the d20Roll function - * @return {Promise} A Promise which resolves to the created Roll instance - */ - async rollAttack(options={}) { - const itemData = this.data.data; - const flags = this.actor.data.flags.sw5e || {}; - if ( !this.hasAttack ) { - throw new Error("You may not place an Attack Roll with this Item."); - } - let title = `${this.name} - ${game.i18n.localize("SW5E.AttackRoll")}`; - - // get the parts and rollData for this item's attack - const {parts, rollData} = this.getAttackToHit(); - - // Handle ammunition consumption - delete this._ammo; - let ammo = null; - let ammoUpdate = null; - const consume = itemData.consume; - if ( consume?.type === "ammo" ) { - ammo = this.actor.items.get(consume.target); - if (ammo?.data) { - const q = ammo.data.data.quantity; - const consumeAmount = consume.amount ?? 0; - if ( q && (q - consumeAmount >= 0) ) { - this._ammo = ammo; - title += ` [${ammo.name}]`; - } - } - - // Get pending ammunition update - const usage = this._getUsageUpdates({consumeResource: true}); - if ( usage === false ) return null; - ammoUpdate = usage.resourceUpdates || {}; - } - - // Compose roll options - const rollConfig = mergeObject({ - parts: parts, - actor: this.actor, - data: rollData, - title: title, - flavor: title, - speaker: ChatMessage.getSpeaker({actor: this.actor}), - dialogOptions: { - width: 400, - top: options.event ? options.event.clientY - 80 : null, - left: window.innerWidth - 710 - }, - messageData: {"flags.sw5e.roll": {type: "attack", itemId: this.id }} - }, options); - rollConfig.event = options.event; - - // Expanded critical hit thresholds - if (( this.data.type === "weapon" ) && flags.weaponCriticalThreshold) { - rollConfig.critical = parseInt(flags.weaponCriticalThreshold); - } else if (( this.data.type === "power" ) && flags.powerCriticalThreshold) { - rollConfig.critical = parseInt(flags.powerCriticalThreshold); - } - - // Elven Accuracy - if ( flags.elvenAccuracy && ["dex", "int", "wis", "cha"].includes(this.abilityMod) ) { - rollConfig.elvenAccuracy = true; - } - - - // Apply Halfling Lucky - if ( flags.halflingLucky ) rollConfig.halflingLucky = true; - - // Invoke the d20 roll helper - const roll = await d20Roll(rollConfig); - if ( roll === false ) return null; - - // Commit ammunition consumption on attack rolls resource consumption if the attack roll was made - if ( ammo && !isObjectEmpty(ammoUpdate) ) await ammo.update(ammoUpdate); - return roll; - } - - /* -------------------------------------------- */ - - /** - * Place a damage roll using an item (weapon, feat, power, or equipment) - * Rely upon the damageRoll logic for the core implementation. - * @param {MouseEvent} [event] An event which triggered this roll, if any - * @param {boolean} [critical] Should damage be rolled as a critical hit? - * @param {number} [powerLevel] If the item is a power, override the level for damage scaling - * @param {boolean} [versatile] If the item is a weapon, roll damage using the versatile formula - * @param {object} [options] Additional options passed to the damageRoll function - * @return {Promise} A Promise which resolves to the created Roll instance - */ - rollDamage({critical=false, event=null, powerLevel=null, versatile=false, options={}}={}) { - if ( !this.hasDamage ) throw new Error("You may not make a Damage Roll with this Item."); - const itemData = this.data.data; - const actorData = this.actor.data.data; - const messageData = {"flags.sw5e.roll": {type: "damage", itemId: this.id }}; - - // Get roll data - const parts = itemData.damage.parts.map(d => d[0]); - const rollData = this.getRollData(); - if ( powerLevel ) rollData.item.level = powerLevel; - - // Configure the damage roll - const actionFlavor = game.i18n.localize(itemData.actionType === "heal" ? "SW5E.Healing" : "SW5E.DamageRoll"); - const title = `${this.name} - ${actionFlavor}`; - const rollConfig = { - actor: this.actor, - critical: critical ?? event?.altKey ?? false, - data: rollData, - event: event, - fastForward: event ? event.shiftKey || event.altKey || event.ctrlKey || event.metaKey : false, - parts: parts, - title: title, - flavor: this.labels.damageTypes.length ? `${title} (${this.labels.damageTypes})` : title, - speaker: ChatMessage.getSpeaker({actor: this.actor}), - dialogOptions: { - width: 400, - top: event ? event.clientY - 80 : null, - left: window.innerWidth - 710 - }, - messageData: messageData - }; - - // Adjust damage from versatile usage - if ( versatile && itemData.damage.versatile ) { - parts[0] = itemData.damage.versatile; - messageData["flags.sw5e.roll"].versatile = true; - } - - // Scale damage from up-casting powers - if ( (this.data.type === "power") ) { - if ( (itemData.scaling.mode === "atwill") ) { - const level = this.actor.data.type === "character" ? actorData.details.level : actorData.details.powerLevel; - this._scaleAtWillDamage(parts, itemData.scaling.formula, level, rollData); - } - else if ( powerLevel && (itemData.scaling.mode === "level") && itemData.scaling.formula ) { - const scaling = itemData.scaling.formula; - this._scalePowerDamage(parts, itemData.level, powerLevel, scaling, rollData); - } - } - - // Add damage bonus formula - const actorBonus = getProperty(actorData, `bonuses.${itemData.actionType}`) || {}; - if ( actorBonus.damage && (parseInt(actorBonus.damage) !== 0) ) { - parts.push(actorBonus.damage); - } - - // Handle ammunition damage - const ammoData = this._ammo?.data; - - // only add the ammunition damage if the ammution is a consumable with type 'ammo' - if ( this._ammo && (ammoData.type === "consumable") && (ammoData.data.consumableType === "ammo") ) { - parts.push("@ammo"); - rollData["ammo"] = ammoData.data.damage.parts.map(p => p[0]).join("+"); - rollConfig.flavor += ` [${this._ammo.name}]`; - delete this._ammo; - } - - // Scale melee critical hit damage - if ( itemData.actionType === "mwak" ) { - rollConfig.criticalBonusDice = this.actor.getFlag("sw5e", "meleeCriticalDamageDice") ?? 0; - } - - // Call the roll helper utility - return damageRoll(mergeObject(rollConfig, options)); - } - - /* -------------------------------------------- */ - - /** - * Adjust an at-will damage formula to scale it for higher level characters and monsters - * @private - */ - _scaleAtWillDamage(parts, scale, level, rollData) { - const add = Math.floor((level + 1) / 6); - if ( add === 0 ) return; - this._scaleDamage(parts, scale || parts.join(" + "), add, rollData); - } - - /* -------------------------------------------- */ - - /** - * Adjust the power damage formula to scale it for power level up-casting - * @param {Array} parts The original damage parts - * @param {number} baseLevel The default power level - * @param {number} powerLevel The casted power level - * @param {string} formula The scaling formula - * @param {object} rollData A data object that should be applied to the scaled damage roll - * @return {string[]} The scaled roll parts - * @private - */ - _scalePowerDamage(parts, baseLevel, powerLevel, formula, rollData) { - const upcastLevels = Math.max(powerLevel - baseLevel, 0); - if ( upcastLevels === 0 ) return parts; - this._scaleDamage(parts, formula, upcastLevels, rollData); - } - - /* -------------------------------------------- */ - - /** - * Scale an array of damage parts according to a provided scaling formula and scaling multiplier - * @param {string[]} parts Initial roll parts - * @param {string} scaling A scaling formula - * @param {number} times A number of times to apply the scaling formula - * @param {object} rollData A data object that should be applied to the scaled damage roll - * @return {string[]} The scaled roll parts - * @private - */ - _scaleDamage(parts, scaling, times, rollData) { - if ( times <= 0 ) return parts; - const p0 = new Roll(parts[0], rollData); - const s = new Roll(scaling, rollData).alter(times); - - // Attempt to simplify by combining like dice terms - let simplified = false; - if ( (s.terms[0] instanceof Die) && (s.terms.length === 1) ) { - const d0 = p0.terms[0]; - const s0 = s.terms[0]; - if ( (d0 instanceof Die) && (d0.faces === s0.faces) && d0.modifiers.equals(s0.modifiers) ) { - d0.number += s0.number; - parts[0] = p0.formula; - simplified = true; - } - } - - // Otherwise add to the first part - if ( !simplified ) { - parts[0] = `${parts[0]} + ${s.formula}`; - } - return parts; - } - - /* -------------------------------------------- */ - - /** - * Place an attack roll using an item (weapon, feat, power, or equipment) - * Rely upon the d20Roll logic for the core implementation - * - * @return {Promise} A Promise which resolves to the created Roll instance - */ - async rollFormula(options={}) { - if ( !this.data.data.formula ) { - throw new Error("This Item does not have a formula to roll!"); - } - - // Define Roll Data - const rollData = this.getRollData(); - if ( options.powerLevel ) rollData.item.level = options.powerLevel; - const title = `${this.name} - ${game.i18n.localize("SW5E.OtherFormula")}`; - - // Invoke the roll and submit it to chat - const roll = new Roll(rollData.item.formula, rollData).roll(); - roll.toMessage({ - speaker: ChatMessage.getSpeaker({actor: this.actor}), - flavor: title, - rollMode: game.settings.get("core", "rollMode"), - messageData: {"flags.sw5e.roll": {type: "other", itemId: this.id }} - }); - return roll; - } - - /* -------------------------------------------- */ - - /** - * Perform an ability recharge test for an item which uses the d6 recharge mechanic - * @return {Promise} A Promise which resolves to the created Roll instance - */ - async rollRecharge() { - const data = this.data.data; - if ( !data.recharge.value ) return; - - // Roll the check - const roll = new Roll("1d6").roll(); - const success = roll.total >= parseInt(data.recharge.value); - - // Display a Chat Message - const promises = [roll.toMessage({ - flavor: `${game.i18n.format("SW5E.ItemRechargeCheck", {name: this.name})} - ${game.i18n.localize(success ? "SW5E.ItemRechargeSuccess" : "SW5E.ItemRechargeFailure")}`, - speaker: ChatMessage.getSpeaker({actor: this.actor, token: this.actor.token}) - })]; - - // Update the Item data - if ( success ) promises.push(this.update({"data.recharge.charged": true})); - return Promise.all(promises).then(() => roll); - } - - /* -------------------------------------------- */ - - /** - * Roll a Tool Check. Rely upon the d20Roll logic for the core implementation - * @prarm {Object} options Roll configuration options provided to the d20Roll function - * @return {Promise} A Promise which resolves to the created Roll instance - */ - rollToolCheck(options={}) { - if ( this.type !== "tool" ) throw "Wrong item type!"; - - // Prepare roll data - let rollData = this.getRollData(); - const parts = [`@mod`, "@prof"]; - const title = `${this.name} - ${game.i18n.localize("SW5E.ToolCheck")}`; - - // Add global actor bonus - const bonuses = getProperty(this.actor.data.data, "bonuses.abilities") || {}; - if ( bonuses.check ) { - parts.push("@checkBonus"); - rollData.checkBonus = bonuses.check; - } - - // Compose the roll data - const rollConfig = mergeObject({ - parts: parts, - data: rollData, - title: title, - speaker: ChatMessage.getSpeaker({actor: this.actor}), - flavor: title, - dialogOptions: { - width: 400, - top: options.event ? options.event.clientY - 80 : null, - left: window.innerWidth - 710, - }, - chooseModifier: true, - halflingLucky: this.actor.getFlag("sw5e", "halflingLucky" ) || false, - reliableTalent: (this.data.data.proficient >= 1) && this.actor.getFlag("sw5e", "reliableTalent"), - messageData: {"flags.sw5e.roll": {type: "tool", itemId: this.id }} - }, options); - rollConfig.event = options.event; - - // Call the roll helper utility - return d20Roll(rollConfig); - } - - /* -------------------------------------------- */ - - /** - * Prepare a data object which is passed to any Roll formulas which are created related to this Item - * @private - */ - getRollData() { - if ( !this.actor ) return null; - const rollData = this.actor.getRollData(); - rollData.item = foundry.utils.deepClone(this.data.data); - - // Include an ability score modifier if one exists - const abl = this.abilityMod; - if ( abl ) { - const ability = rollData.abilities[abl]; - if ( !ability ) { - console.warn(`Item ${this.name} in Actor ${this.actor.name} has an invalid item ability modifier of ${abl} defined`); - } - rollData["mod"] = ability?.mod || 0; - } - - // Include a proficiency score - const prof = ("proficient" in rollData.item) ? (rollData.item.proficient || 0) : 1; - rollData["prof"] = Math.floor(prof * (rollData.attributes.prof || 0)); - return rollData; - } - - /* -------------------------------------------- */ - /* Chat Message Helpers */ - /* -------------------------------------------- */ - - static chatListeners(html) { - html.on('click', '.card-buttons button', this._onChatCardAction.bind(this)); - html.on('click', '.item-name', this._onChatCardToggleContent.bind(this)); - } - - /* -------------------------------------------- */ - - /** - * Handle execution of a chat card action via a click event on one of the card buttons - * @param {Event} event The originating click event - * @returns {Promise} A promise which resolves once the handler workflow is complete - * @private - */ - static async _onChatCardAction(event) { - event.preventDefault(); - - // Extract card data - const button = event.currentTarget; - button.disabled = true; - const card = button.closest(".chat-card"); - const messageId = card.closest(".message").dataset.messageId; - const message = game.messages.get(messageId); - const action = button.dataset.action; - - // Validate permission to proceed with the roll - const isTargetted = action === "save"; - if ( !( isTargetted || game.user.isGM || message.isAuthor ) ) return; - - // Recover the actor for the chat card - const actor = await this._getChatCardActor(card); - if ( !actor ) return; - - // Get the Item from stored flag data or by the item ID on the Actor - const storedData = message.getFlag("sw5e", "itemData"); - const item = storedData ? new this(storedData, {parent: actor}) : actor.items.get(card.dataset.itemId); - if ( !item ) { - return ui.notifications.error(game.i18n.format("SW5E.ActionWarningNoItem", {item: card.dataset.itemId, name: actor.name})) - } - const powerLevel = parseInt(card.dataset.powerLevel) || null; - - // Handle different actions - switch ( action ) { - case "attack": - await item.rollAttack({event}); break; - case "damage": - case "versatile": - await item.rollDamage({ - critical: event.altKey, - event: event, - powerLevel: powerLevel, - versatile: action === "versatile" + // Determine whether the item can be used by testing for resource consumption + const usage = item._getUsageUpdates({ + consumeRecharge, + consumeResource, + consumePowerLevel, + consumeUsage, + consumeQuantity }); - break; - case "formula": - await item.rollFormula({event, powerLevel}); break; - case "save": - const targets = this._getChatCardTargets(card); - for ( let token of targets ) { - const speaker = ChatMessage.getSpeaker({scene: canvas.scene, token: token}); - await token.actor.rollAbilitySave(button.dataset.ability, { event, speaker }); + if (!usage) return; + + const {actorUpdates, itemUpdates, resourceUpdates} = usage; + + // Commit pending data updates + if (!foundry.utils.isObjectEmpty(itemUpdates)) await item.update(itemUpdates); + if (consumeQuantity && item.data.data.quantity === 0) await item.delete(); + if (!foundry.utils.isObjectEmpty(actorUpdates)) await actor.update(actorUpdates); + if (!foundry.utils.isObjectEmpty(resourceUpdates)) { + const resource = actor.items.get(id.consume?.target); + if (resource) await resource.update(resourceUpdates); } - break; - case "toolCheck": - await item.rollToolCheck({event}); break; - case "placeTemplate": - const template = game.sw5e.canvas.AbilityTemplate.fromItem(item); - if ( template ) template.drawPreview(); - break; + + // Initiate measured template creation + if (createMeasuredTemplate) { + const template = game.sw5e.canvas.AbilityTemplate.fromItem(item); + if (template) template.drawPreview(); + } + + // Create or return the Chat Message data + return item.displayCard({rollMode, createMessage}); } - // Re-enable the button - button.disabled = false; - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** + * Verify that the consumed resources used by an Item are available. + * Otherwise display an error and return false. + * @param {boolean} consumeQuantity Consume quantity of the item if other consumption modes are not available? + * @param {boolean} consumeRecharge Whether the item consumes the recharge mechanic + * @param {boolean} consumeResource Whether the item consumes a limited resource + * @param {string|null} consumePowerLevel The category of power slot to consume, or null + * @param {boolean} consumeUsage Whether the item consumes a limited usage + * @returns {object|boolean} A set of data changes to apply when the item is used, or false + * @private + */ + _getUsageUpdates({consumeQuantity, consumeRecharge, consumeResource, consumePowerLevel, consumeUsage}) { + // Reference item data + const id = this.data.data; + const actorUpdates = {}; + const itemUpdates = {}; + const resourceUpdates = {}; - /** - * Handle toggling the visibility of chat card content when the name is clicked - * @param {Event} event The originating click event - * @private - */ - static _onChatCardToggleContent(event) { - event.preventDefault(); - const header = event.currentTarget; - const card = header.closest(".chat-card"); - const content = card.querySelector(".card-content"); - content.style.display = content.style.display === "none" ? "block" : "none"; - } + // Consume Recharge + if (consumeRecharge) { + const recharge = id.recharge || {}; + if (recharge.charged === false) { + ui.notifications.warn(game.i18n.format("SW5E.ItemNoUses", {name: this.name})); + return false; + } + itemUpdates["data.recharge.charged"] = false; + } - /* -------------------------------------------- */ + // Consume Limited Resource + if (consumeResource) { + const canConsume = this._handleConsumeResource(itemUpdates, actorUpdates, resourceUpdates); + if (canConsume === false) return false; + } - /** - * Get the Actor which is the author of a chat card - * @param {HTMLElement} card The chat card being used - * @return {Actor|null} The Actor entity or null - * @private - */ - static async _getChatCardActor(card) { + // Consume Power Slots and Force/Tech Points + if (consumePowerLevel) { + if (Number.isNumeric(consumePowerLevel)) consumePowerLevel = `power${consumePowerLevel}`; + const level = this.actor?.data.data.powers[consumePowerLevel]; + const fp = this.actor.data.data.attributes.force.points; + const tp = this.actor.data.data.attributes.tech.points; + const powerCost = id.level + 1; + const innatePower = this.actor.data.data.attributes.powercasting === "innate"; + if (!innatePower) { + switch (id.school) { + case "lgt": + case "uni": + case "drk": { + const powers = Number(level?.fvalue ?? 0); + if (powers === 0) { + const label = game.i18n.localize(`SW5E.PowerLevel${id.level}`); + ui.notifications.warn( + game.i18n.format("SW5E.PowerCastNoSlots", {name: this.name, level: label}) + ); + return false; + } + actorUpdates[`data.powers.${consumePowerLevel}.fvalue`] = Math.max(powers - 1, 0); + if (fp.temp >= powerCost) { + actorUpdates["data.attributes.force.points.temp"] = fp.temp - powerCost; + } else { + actorUpdates["data.attributes.force.points.value"] = fp.value + fp.temp - powerCost; + actorUpdates["data.attributes.force.points.temp"] = 0; + } + break; + } + case "tec": { + const powers = Number(level?.tvalue ?? 0); + if (powers === 0) { + const label = game.i18n.localize(`SW5E.PowerLevel${id.level}`); + ui.notifications.warn( + game.i18n.format("SW5E.PowerCastNoSlots", {name: this.name, level: label}) + ); + return false; + } + actorUpdates[`data.powers.${consumePowerLevel}.tvalue`] = Math.max(powers - 1, 0); + if (tp.temp >= powerCost) { + actorUpdates["data.attributes.tech.points.temp"] = tp.temp - powerCost; + } else { + actorUpdates["data.attributes.tech.points.value"] = tp.value + tp.temp - powerCost; + actorUpdates["data.attributes.tech.points.temp"] = 0; + } + break; + } + } + } + } - // Case 1 - a synthetic actor from a Token - if ( card.dataset.tokenId ) { - const token = await fromUuid(card.dataset.tokenId); - if ( !token ) return null; - return token.actor; + // Consume Limited Usage + if (consumeUsage) { + const uses = id.uses || {}; + const available = Number(uses.value ?? 0); + let used = false; + + // Reduce usages + const remaining = Math.max(available - 1, 0); + if (available >= 1) { + used = true; + itemUpdates["data.uses.value"] = remaining; + } + + // Reduce quantity if not reducing usages or if usages hit 0 and we are set to consumeQuantity + if (consumeQuantity && (!used || remaining === 0)) { + const q = Number(id.quantity ?? 1); + if (q >= 1) { + used = true; + itemUpdates["data.quantity"] = Math.max(q - 1, 0); + itemUpdates["data.uses.value"] = uses.max ?? 1; + } + } + + // If the item was not used, return a warning + if (!used) { + ui.notifications.warn(game.i18n.format("SW5E.ItemNoUses", {name: this.name})); + return false; + } + } + + // Return the configured usage + return {itemUpdates, actorUpdates, resourceUpdates}; } - // Case 2 - use Actor ID directory - const actorId = card.dataset.actorId; - return game.actors.get(actorId) || null; - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** + * Handle update actions required when consuming an external resource + * @param {object} itemUpdates An object of data updates applied to this item + * @param {object} actorUpdates An object of data updates applied to the item owner (Actor) + * @param {object} resourceUpdates An object of data updates applied to a different resource item (Item) + * @return {boolean|void} Return false to block further progress, or return nothing to continue + * @private + */ + _handleConsumeResource(itemUpdates, actorUpdates, resourceUpdates) { + const actor = this.actor; + const itemData = this.data.data; + const consume = itemData.consume || {}; + if (!consume.type) return; - /** - * Get the Actor which is the author of a chat card - * @param {HTMLElement} card The chat card being used - * @return {Actor[]} An Array of Actor entities, if any - * @private - */ - static _getChatCardTargets(card) { - let targets = canvas.tokens.controlled.filter(t => !!t.actor); - if ( !targets.length && game.user.character ) targets = targets.concat(game.user.character.getActiveTokens()); - if ( !targets.length ) ui.notifications.warn(game.i18n.localize("SW5E.ActionWarningNoToken")); - return targets; - } + // No consumed target + const typeLabel = CONFIG.SW5E.abilityConsumptionTypes[consume.type]; + if (!consume.target) { + ui.notifications.warn( + game.i18n.format("SW5E.ConsumeWarningNoResource", {name: this.name, type: typeLabel}) + ); + return false; + } - /* -------------------------------------------- */ - /* Event Handlers */ - /* -------------------------------------------- */ + // Identify the consumed resource and its current quantity + let resource = null; + let amount = Number(consume.amount ?? 1); + let quantity = 0; + switch (consume.type) { + case "attribute": + resource = getProperty(actor.data.data, consume.target); + quantity = resource || 0; + break; + case "ammo": + case "material": + resource = actor.items.get(consume.target); + quantity = resource ? resource.data.data.quantity : 0; + break; + case "charges": + resource = actor.items.get(consume.target); + if (!resource) break; + const uses = resource.data.data.uses; + if (uses.per && uses.max) quantity = uses.value; + else if (resource.data.data.recharge?.value) { + quantity = resource.data.data.recharge.charged ? 1 : 0; + amount = 1; + } + break; + } - /** @inheritdoc */ - async _preCreate(data, options, user) { - await super._preCreate(data, options, user); - if ( !this.isEmbedded || (this.parent.type === "vehicle") ) return; - const actorData = this.parent.data; - const isNPC = this.parent.type === "npc"; - let updates; - switch (data.type) { - case "equipment": - updates = this._onCreateOwnedEquipment(data, actorData, isNPC); - break; - case "weapon": - updates = this._onCreateOwnedWeapon(data, actorData, isNPC); - break; - case "power": - updates = this._onCreateOwnedPower(data, actorData, isNPC); - break; + // Verify that a consumed resource is available + if (!resource) { + ui.notifications.warn(game.i18n.format("SW5E.ConsumeWarningNoSource", {name: this.name, type: typeLabel})); + return false; + } + + // Verify that the required quantity is available + let remaining = quantity - amount; + if (remaining < 0) { + ui.notifications.warn( + game.i18n.format("SW5E.ConsumeWarningNoQuantity", {name: this.name, type: typeLabel}) + ); + return false; + } + + // Define updates to provided data objects + switch (consume.type) { + case "attribute": + actorUpdates[`data.${consume.target}`] = remaining; + break; + case "ammo": + case "material": + resourceUpdates["data.quantity"] = remaining; + break; + case "charges": + const uses = resource.data.data.uses || {}; + const recharge = resource.data.data.recharge || {}; + if (uses.per && uses.max) resourceUpdates["data.uses.value"] = remaining; + else if (recharge.value) resourceUpdates["data.recharge.charged"] = false; + break; + } } - if (updates) return this.data.update(updates); - } - /* -------------------------------------------- */ + /* -------------------------------------------- */ - /** @inheritdoc */ - _onCreate(data, options, userId) { - super._onCreate(data, options, userId); + /** + * Display the chat card for an Item as a Chat Message + * @param {object} options Options which configure the display of the item chat card + * @param {string} rollMode The message visibility mode to apply to the created card + * @param {boolean} createMessage Whether to automatically create a ChatMessage entity (if true), or only return + * the prepared message data (if false) + */ + async displayCard({rollMode, createMessage = true} = {}) { + // Render the chat card template + const token = this.actor.token; + const templateData = { + actor: this.actor, + tokenId: token?.uuid || null, + item: this.data, + data: this.getChatData(), + labels: this.labels, + hasAttack: this.hasAttack, + isHealing: this.isHealing, + hasDamage: this.hasDamage, + isVersatile: this.isVersatile, + isPower: this.data.type === "power", + hasSave: this.hasSave, + hasAreaTarget: this.hasAreaTarget, + isTool: this.data.type === "tool" + }; + const html = await renderTemplate("systems/sw5e/templates/chat/item-card.html", templateData); - // The below options are only needed for character classes - if ( userId !== game.user.id ) return; - const isCharacterClass = this.parent && (this.parent.type !== "vehicle") && (this.type === "class"); - if ( !isCharacterClass ) return; + // Create the ChatMessage data object + const chatData = { + user: game.user.data._id, + type: CONST.CHAT_MESSAGE_TYPES.OTHER, + content: html, + flavor: this.data.data.chatFlavor || this.name, + speaker: ChatMessage.getSpeaker({actor: this.actor, token}), + flags: {"core.canPopout": true} + }; - // Assign a new primary class - const pc = this.parent.items.get(this.parent.data.data.details.originalClass); - if ( !pc ) this.parent._assignPrimaryClass(); + // If the Item was destroyed in the process of displaying its card - embed the item data in the chat message + if (this.data.type === "consumable" && !this.actor.items.has(this.id)) { + chatData.flags["sw5e.itemData"] = this.data; + } - // Prompt to add new class features - if (options.addFeatures === false) return; - this.parent.getClassFeatures({ - className: this.name, - archetypeName: this.data.data.archetype, - level: this.data.data.levels - }).then(features => { - return this.parent.addEmbeddedItems(features, options.promptAddFeatures); - }); - } + // Apply the roll mode to adjust message visibility + ChatMessage.applyRollMode(chatData, rollMode || game.settings.get("core", "rollMode")); - /* -------------------------------------------- */ - - /** @inheritdoc */ - _onUpdate(changed, options, userId) { - super._onUpdate(changed, options, userId); - - // The below options are only needed for character classes - if ( userId !== game.user.id ) return; - const isCharacterClass = this.parent && (this.parent.type !== "vehicle") && (this.type === "class"); - if ( !isCharacterClass ) return; - - // Prompt to add new class features - const addFeatures = changed["name"] || (changed.data && ["archetype", "levels"].some(k => k in changed.data)); - if ( !addFeatures || (options.addFeatures === false) ) return; - this.parent.getClassFeatures({ - className: changed.name || this.name, - archetypeName: changed.data?.archetype || this.data.data.archetype, - level: changed.data?.levels || this.data.data.levels - }).then(features => { - return this.parent.addEmbeddedItems(features, options.promptAddFeatures); - }); - } - - /* -------------------------------------------- */ - - /** @inheritdoc */ - _onDelete(options, userId) { - super._onDelete(options, userId); - - // Assign a new primary class - if ( this.parent && (this.type === "class") && (userId === game.user.id) ) { - if ( this.id !== this.parent.data.data.details.originalClass ) return; - this.parent._assignPrimaryClass(); + // Create the Chat Message or return its data + return createMessage ? ChatMessage.create(chatData) : chatData; } - } - /* -------------------------------------------- */ + /* -------------------------------------------- */ + /* Chat Cards */ + /* -------------------------------------------- */ - /** - * Pre-creation logic for the automatic configuration of owned equipment type Items - * @private - */ - _onCreateOwnedEquipment(data, actorData, isNPC) { - const updates = {}; - if ( foundry.utils.getProperty(data, "data.equipped") === undefined ) { - updates["data.equipped"] = isNPC; // NPCs automatically equip equipment + /** + * Prepare an object of chat data used to display a card for the Item in the chat log + * @param {Object} htmlOptions Options used by the TextEditor.enrichHTML function + * @return {Object} An object of chat data to render + */ + getChatData(htmlOptions = {}) { + const data = foundry.utils.deepClone(this.data.data); + const labels = this.labels; + + // Rich text description + data.description.value = TextEditor.enrichHTML(data.description.value, htmlOptions); + + // Item type specific properties + const props = []; + const fn = this[`_${this.data.type}ChatData`]; + if (fn) fn.bind(this)(data, labels, props); + + // Equipment properties + if (data.hasOwnProperty("equipped") && !["loot", "tool"].includes(this.data.type)) { + if (data.attunement === CONFIG.SW5E.attunementTypes.REQUIRED) + props.push(game.i18n.localize(CONFIG.SW5E.attunements[CONFIG.SW5E.attunementTypes.REQUIRED])); + props.push( + game.i18n.localize(data.equipped ? "SW5E.Equipped" : "SW5E.Unequipped"), + game.i18n.localize(data.proficient ? "SW5E.Proficient" : "SW5E.NotProficient") + ); + } + + // Ability activation properties + if (data.hasOwnProperty("activation")) { + props.push( + labels.activation + (data.activation?.condition ? ` (${data.activation.condition})` : ""), + labels.target, + labels.range, + labels.duration + ); + } + + // Filter properties and return + data.properties = props.filter((p) => !!p); + return data; } - if ( foundry.utils.getProperty(data, "data.proficient") === undefined ) { - if ( isNPC ) { - updates["data.proficient"] = true; // NPCs automatically have equipment proficiency - } else { - const armorProf = CONFIG.SW5E.armorProficienciesMap[data.data?.armor?.type]; // Player characters check proficiency - const actorArmorProfs = actorData.data.traits?.armorProf?.value || []; - updates["data.proficient"] = (armorProf === true) || actorArmorProfs.includes(armorProf); - } + + /* -------------------------------------------- */ + + /** + * Prepare chat card data for equipment type items + * @private + */ + _equipmentChatData(data, labels, props) { + props.push( + CONFIG.SW5E.equipmentTypes[data.armor.type], + labels.armor || null, + data.stealth.value ? game.i18n.localize("SW5E.StealthDisadvantage") : null + ); } - return updates; - } - /* -------------------------------------------- */ + /* -------------------------------------------- */ - /** - * Pre-creation logic for the automatic configuration of owned power type Items - * @private - */ - _onCreateOwnedPower(data, actorData, isNPC) { - const updates = {}; - updates["data.preparation.prepared"] = true; // Automatically prepare powers for everyone - return updates; - } - - /* -------------------------------------------- */ - - /** - * Pre-creation logic for the automatic configuration of owned weapon type Items - * @private - */ - _onCreateOwnedWeapon(data, actorData, isNPC) { - const updates = {}; - if ( foundry.utils.getProperty(data, "data.equipped") === undefined ) { - updates["data.equipped"] = isNPC; // NPCs automatically equip weapons + /** + * Prepare chat card data for weapon type items + * @private + */ + _weaponChatData(data, labels, props) { + props.push(CONFIG.SW5E.weaponTypes[data.weaponType]); } - if ( foundry.utils.getProperty(data, "data.proficient") === undefined ) { - if ( isNPC ) { - updates["data.proficient"] = true; // NPCs automatically have equipment proficiency - } else { - // TODO: With the changes to make weapon proficiencies more verbose, this may need revising - const weaponProf = CONFIG.SW5E.weaponProficienciesMap[data.data?.weaponType]; // Player characters check proficiency - const actorWeaponProfs = actorData.data.traits?.weaponProf?.value || []; - updates["data.proficient"] = (weaponProf === true) || actorWeaponProfs.includes(weaponProf); - } + + /* -------------------------------------------- */ + + /** + * Prepare chat card data for consumable type items + * @private + */ + _consumableChatData(data, labels, props) { + props.push( + CONFIG.SW5E.consumableTypes[data.consumableType], + data.uses.value + "/" + data.uses.max + " " + game.i18n.localize("SW5E.Charges") + ); + data.hasCharges = data.uses.value >= 0; } - return updates; - } - /* -------------------------------------------- */ - /* Factory Methods */ - /* -------------------------------------------- */ -// TODO: Make work properly - /** - * Create a consumable power scroll Item from a power Item. - * @param {Item5e} power The power to be made into a scroll - * @return {Item5e} The created scroll consumable item - */ - static async createScrollFromPower(power) { + /* -------------------------------------------- */ - // Get power data - const itemData = power instanceof Item5e ? power.data : power; - const {actionType, description, source, activation, duration, target, range, damage, save, level} = itemData.data; + /** + * Prepare chat card data for tool type items + * @private + */ + _toolChatData(data, labels, props) { + props.push(CONFIG.SW5E.abilities[data.ability] || null, CONFIG.SW5E.proficiencyLevels[data.proficient || 0]); + } - // Get scroll data - const scrollUuid = `Compendium.${CONFIG.SW5E.sourcePacks.ITEMS}.${CONFIG.SW5E.powerScrollIds[level]}`; - const scrollItem = await fromUuid(scrollUuid); - const scrollData = scrollItem.data; - delete scrollData._id; + /* -------------------------------------------- */ - // Split the scroll description into an intro paragraph and the remaining details - const scrollDescription = scrollData.data.description.value; - const pdel = '

'; - const scrollIntroEnd = scrollDescription.indexOf(pdel); - const scrollIntro = scrollDescription.slice(0, scrollIntroEnd + pdel.length); - const scrollDetails = scrollDescription.slice(scrollIntroEnd + pdel.length); + /** + * Prepare chat card data for loot type items + * @private + */ + _lootChatData(data, labels, props) { + props.push( + game.i18n.localize("SW5E.ItemTypeLoot"), + data.weight ? data.weight + " " + game.i18n.localize("SW5E.AbbreviationLbs") : null + ); + } - // Create a composite description from the scroll description and the power details - const desc = `${scrollIntro}

${itemData.name} (Level ${level})


${description.value}

Scroll Details


${scrollDetails}`; + /* -------------------------------------------- */ - // Create the power scroll data - const powerScrollData = mergeObject(scrollData, { - name: `${game.i18n.localize("SW5E.PowerScroll")}: ${itemData.name}`, - img: itemData.img, - data: { - "description.value": desc.trim(), - source, - actionType, - activation, - duration, - target, - range, - damage, - save, - level - } - }); - return new this(powerScrollData); - } + /** + * Render a chat card for Power type data + * @return {Object} + * @private + */ + _powerChatData(data, labels, props) { + props.push(labels.level, labels.components + (labels.materials ? ` (${labels.materials})` : "")); + } + + /* -------------------------------------------- */ + + /** + * Prepare chat card data for items of the "Feat" type + * @private + */ + _featChatData(data, labels, props) { + props.push(data.requirements); + } + + /* -------------------------------------------- */ + /* Item Rolls - Attack, Damage, Saves, Checks */ + /* -------------------------------------------- */ + + /** + * Place an attack roll using an item (weapon, feat, power, or equipment) + * Rely upon the d20Roll logic for the core implementation + * + * @param {object} options Roll options which are configured and provided to the d20Roll function + * @return {Promise} A Promise which resolves to the created Roll instance + */ + async rollAttack(options = {}) { + const itemData = this.data.data; + const flags = this.actor.data.flags.sw5e || {}; + if (!this.hasAttack) { + throw new Error("You may not place an Attack Roll with this Item."); + } + let title = `${this.name} - ${game.i18n.localize("SW5E.AttackRoll")}`; + + // get the parts and rollData for this item's attack + const {parts, rollData} = this.getAttackToHit(); + + // Handle ammunition consumption + delete this._ammo; + let ammo = null; + let ammoUpdate = null; + const consume = itemData.consume; + if (consume?.type === "ammo") { + ammo = this.actor.items.get(consume.target); + if (ammo?.data) { + const q = ammo.data.data.quantity; + const consumeAmount = consume.amount ?? 0; + if (q && q - consumeAmount >= 0) { + this._ammo = ammo; + title += ` [${ammo.name}]`; + } + } + + // Get pending ammunition update + const usage = this._getUsageUpdates({consumeResource: true}); + if (usage === false) return null; + ammoUpdate = usage.resourceUpdates || {}; + } + + // Compose roll options + const rollConfig = mergeObject( + { + parts: parts, + actor: this.actor, + data: rollData, + title: title, + flavor: title, + speaker: ChatMessage.getSpeaker({actor: this.actor}), + dialogOptions: { + width: 400, + top: options.event ? options.event.clientY - 80 : null, + left: window.innerWidth - 710 + }, + messageData: {"flags.sw5e.roll": {type: "attack", itemId: this.id}} + }, + options + ); + rollConfig.event = options.event; + + // Expanded critical hit thresholds + if (this.data.type === "weapon" && flags.weaponCriticalThreshold) { + rollConfig.critical = parseInt(flags.weaponCriticalThreshold); + } else if (this.data.type === "power" && flags.powerCriticalThreshold) { + rollConfig.critical = parseInt(flags.powerCriticalThreshold); + } + + // Elven Accuracy + if (flags.elvenAccuracy && ["dex", "int", "wis", "cha"].includes(this.abilityMod)) { + rollConfig.elvenAccuracy = true; + } + + // Apply Halfling Lucky + if (flags.halflingLucky) rollConfig.halflingLucky = true; + + // Invoke the d20 roll helper + const roll = await d20Roll(rollConfig); + if (roll === false) return null; + + // Commit ammunition consumption on attack rolls resource consumption if the attack roll was made + if (ammo && !isObjectEmpty(ammoUpdate)) await ammo.update(ammoUpdate); + return roll; + } + + /* -------------------------------------------- */ + + /** + * Place a damage roll using an item (weapon, feat, power, or equipment) + * Rely upon the damageRoll logic for the core implementation. + * @param {MouseEvent} [event] An event which triggered this roll, if any + * @param {boolean} [critical] Should damage be rolled as a critical hit? + * @param {number} [powerLevel] If the item is a power, override the level for damage scaling + * @param {boolean} [versatile] If the item is a weapon, roll damage using the versatile formula + * @param {object} [options] Additional options passed to the damageRoll function + * @return {Promise} A Promise which resolves to the created Roll instance + */ + rollDamage({critical = false, event = null, powerLevel = null, versatile = false, options = {}} = {}) { + if (!this.hasDamage) throw new Error("You may not make a Damage Roll with this Item."); + const itemData = this.data.data; + const actorData = this.actor.data.data; + const messageData = {"flags.sw5e.roll": {type: "damage", itemId: this.id}}; + + // Get roll data + const parts = itemData.damage.parts.map((d) => d[0]); + const rollData = this.getRollData(); + if (powerLevel) rollData.item.level = powerLevel; + + // Configure the damage roll + const actionFlavor = game.i18n.localize(itemData.actionType === "heal" ? "SW5E.Healing" : "SW5E.DamageRoll"); + const title = `${this.name} - ${actionFlavor}`; + const rollConfig = { + actor: this.actor, + critical: critical ?? event?.altKey ?? false, + data: rollData, + event: event, + fastForward: event ? event.shiftKey || event.altKey || event.ctrlKey || event.metaKey : false, + parts: parts, + title: title, + flavor: this.labels.damageTypes.length ? `${title} (${this.labels.damageTypes})` : title, + speaker: ChatMessage.getSpeaker({actor: this.actor}), + dialogOptions: { + width: 400, + top: event ? event.clientY - 80 : null, + left: window.innerWidth - 710 + }, + messageData: messageData + }; + + // Adjust damage from versatile usage + if (versatile && itemData.damage.versatile) { + parts[0] = itemData.damage.versatile; + messageData["flags.sw5e.roll"].versatile = true; + } + + // Scale damage from up-casting powers + if (this.data.type === "power") { + if (itemData.scaling.mode === "atwill") { + const level = + this.actor.data.type === "character" ? actorData.details.level : actorData.details.powerLevel; + this._scaleAtWillDamage(parts, itemData.scaling.formula, level, rollData); + } else if (powerLevel && itemData.scaling.mode === "level" && itemData.scaling.formula) { + const scaling = itemData.scaling.formula; + this._scalePowerDamage(parts, itemData.level, powerLevel, scaling, rollData); + } + } + + // Add damage bonus formula + const actorBonus = getProperty(actorData, `bonuses.${itemData.actionType}`) || {}; + if (actorBonus.damage && parseInt(actorBonus.damage) !== 0) { + parts.push(actorBonus.damage); + } + + // Handle ammunition damage + const ammoData = this._ammo?.data; + + // only add the ammunition damage if the ammution is a consumable with type 'ammo' + if (this._ammo && ammoData.type === "consumable" && ammoData.data.consumableType === "ammo") { + parts.push("@ammo"); + rollData["ammo"] = ammoData.data.damage.parts.map((p) => p[0]).join("+"); + rollConfig.flavor += ` [${this._ammo.name}]`; + delete this._ammo; + } + + // Scale melee critical hit damage + if (itemData.actionType === "mwak") { + rollConfig.criticalBonusDice = this.actor.getFlag("sw5e", "meleeCriticalDamageDice") ?? 0; + } + + // Call the roll helper utility + return damageRoll(mergeObject(rollConfig, options)); + } + + /* -------------------------------------------- */ + + /** + * Adjust an at-will damage formula to scale it for higher level characters and monsters + * @private + */ + _scaleAtWillDamage(parts, scale, level, rollData) { + const add = Math.floor((level + 1) / 6); + if (add === 0) return; + this._scaleDamage(parts, scale || parts.join(" + "), add, rollData); + } + + /* -------------------------------------------- */ + + /** + * Adjust the power damage formula to scale it for power level up-casting + * @param {Array} parts The original damage parts + * @param {number} baseLevel The default power level + * @param {number} powerLevel The casted power level + * @param {string} formula The scaling formula + * @param {object} rollData A data object that should be applied to the scaled damage roll + * @return {string[]} The scaled roll parts + * @private + */ + _scalePowerDamage(parts, baseLevel, powerLevel, formula, rollData) { + const upcastLevels = Math.max(powerLevel - baseLevel, 0); + if (upcastLevels === 0) return parts; + this._scaleDamage(parts, formula, upcastLevels, rollData); + } + + /* -------------------------------------------- */ + + /** + * Scale an array of damage parts according to a provided scaling formula and scaling multiplier + * @param {string[]} parts Initial roll parts + * @param {string} scaling A scaling formula + * @param {number} times A number of times to apply the scaling formula + * @param {object} rollData A data object that should be applied to the scaled damage roll + * @return {string[]} The scaled roll parts + * @private + */ + _scaleDamage(parts, scaling, times, rollData) { + if (times <= 0) return parts; + const p0 = new Roll(parts[0], rollData); + const s = new Roll(scaling, rollData).alter(times); + + // Attempt to simplify by combining like dice terms + let simplified = false; + if (s.terms[0] instanceof Die && s.terms.length === 1) { + const d0 = p0.terms[0]; + const s0 = s.terms[0]; + if (d0 instanceof Die && d0.faces === s0.faces && d0.modifiers.equals(s0.modifiers)) { + d0.number += s0.number; + parts[0] = p0.formula; + simplified = true; + } + } + + // Otherwise add to the first part + if (!simplified) { + parts[0] = `${parts[0]} + ${s.formula}`; + } + return parts; + } + + /* -------------------------------------------- */ + + /** + * Place an attack roll using an item (weapon, feat, power, or equipment) + * Rely upon the d20Roll logic for the core implementation + * + * @return {Promise} A Promise which resolves to the created Roll instance + */ + async rollFormula(options = {}) { + if (!this.data.data.formula) { + throw new Error("This Item does not have a formula to roll!"); + } + + // Define Roll Data + const rollData = this.getRollData(); + if (options.powerLevel) rollData.item.level = options.powerLevel; + const title = `${this.name} - ${game.i18n.localize("SW5E.OtherFormula")}`; + + // Invoke the roll and submit it to chat + const roll = new Roll(rollData.item.formula, rollData).roll(); + roll.toMessage({ + speaker: ChatMessage.getSpeaker({actor: this.actor}), + flavor: title, + rollMode: game.settings.get("core", "rollMode"), + messageData: {"flags.sw5e.roll": {type: "other", itemId: this.id}} + }); + return roll; + } + + /* -------------------------------------------- */ + + /** + * Perform an ability recharge test for an item which uses the d6 recharge mechanic + * @return {Promise} A Promise which resolves to the created Roll instance + */ + async rollRecharge() { + const data = this.data.data; + if (!data.recharge.value) return; + + // Roll the check + const roll = new Roll("1d6").roll(); + const success = roll.total >= parseInt(data.recharge.value); + + // Display a Chat Message + const promises = [ + roll.toMessage({ + flavor: `${game.i18n.format("SW5E.ItemRechargeCheck", {name: this.name})} - ${game.i18n.localize( + success ? "SW5E.ItemRechargeSuccess" : "SW5E.ItemRechargeFailure" + )}`, + speaker: ChatMessage.getSpeaker({actor: this.actor, token: this.actor.token}) + }) + ]; + + // Update the Item data + if (success) promises.push(this.update({"data.recharge.charged": true})); + return Promise.all(promises).then(() => roll); + } + + /* -------------------------------------------- */ + + /** + * Roll a Tool Check. Rely upon the d20Roll logic for the core implementation + * @prarm {Object} options Roll configuration options provided to the d20Roll function + * @return {Promise} A Promise which resolves to the created Roll instance + */ + rollToolCheck(options = {}) { + if (this.type !== "tool") throw "Wrong item type!"; + + // Prepare roll data + let rollData = this.getRollData(); + const parts = [`@mod`, "@prof"]; + const title = `${this.name} - ${game.i18n.localize("SW5E.ToolCheck")}`; + + // Add global actor bonus + const bonuses = getProperty(this.actor.data.data, "bonuses.abilities") || {}; + if (bonuses.check) { + parts.push("@checkBonus"); + rollData.checkBonus = bonuses.check; + } + + // Compose the roll data + const rollConfig = mergeObject( + { + parts: parts, + data: rollData, + title: title, + speaker: ChatMessage.getSpeaker({actor: this.actor}), + flavor: title, + dialogOptions: { + width: 400, + top: options.event ? options.event.clientY - 80 : null, + left: window.innerWidth - 710 + }, + chooseModifier: true, + halflingLucky: this.actor.getFlag("sw5e", "halflingLucky") || false, + reliableTalent: this.data.data.proficient >= 1 && this.actor.getFlag("sw5e", "reliableTalent"), + messageData: {"flags.sw5e.roll": {type: "tool", itemId: this.id}} + }, + options + ); + rollConfig.event = options.event; + + // Call the roll helper utility + return d20Roll(rollConfig); + } + + /* -------------------------------------------- */ + + /** + * Prepare a data object which is passed to any Roll formulas which are created related to this Item + * @private + */ + getRollData() { + if (!this.actor) return null; + const rollData = this.actor.getRollData(); + rollData.item = foundry.utils.deepClone(this.data.data); + + // Include an ability score modifier if one exists + const abl = this.abilityMod; + if (abl) { + const ability = rollData.abilities[abl]; + if (!ability) { + console.warn( + `Item ${this.name} in Actor ${this.actor.name} has an invalid item ability modifier of ${abl} defined` + ); + } + rollData["mod"] = ability?.mod || 0; + } + + // Include a proficiency score + const prof = "proficient" in rollData.item ? rollData.item.proficient || 0 : 1; + rollData["prof"] = Math.floor(prof * (rollData.attributes.prof || 0)); + return rollData; + } + + /* -------------------------------------------- */ + /* Chat Message Helpers */ + /* -------------------------------------------- */ + + static chatListeners(html) { + html.on("click", ".card-buttons button", this._onChatCardAction.bind(this)); + html.on("click", ".item-name", this._onChatCardToggleContent.bind(this)); + } + + /* -------------------------------------------- */ + + /** + * Handle execution of a chat card action via a click event on one of the card buttons + * @param {Event} event The originating click event + * @returns {Promise} A promise which resolves once the handler workflow is complete + * @private + */ + static async _onChatCardAction(event) { + event.preventDefault(); + + // Extract card data + const button = event.currentTarget; + button.disabled = true; + const card = button.closest(".chat-card"); + const messageId = card.closest(".message").dataset.messageId; + const message = game.messages.get(messageId); + const action = button.dataset.action; + + // Validate permission to proceed with the roll + const isTargetted = action === "save"; + if (!(isTargetted || game.user.isGM || message.isAuthor)) return; + + // Recover the actor for the chat card + const actor = await this._getChatCardActor(card); + if (!actor) return; + + // Get the Item from stored flag data or by the item ID on the Actor + const storedData = message.getFlag("sw5e", "itemData"); + const item = storedData ? new this(storedData, {parent: actor}) : actor.items.get(card.dataset.itemId); + if (!item) { + return ui.notifications.error( + game.i18n.format("SW5E.ActionWarningNoItem", {item: card.dataset.itemId, name: actor.name}) + ); + } + const powerLevel = parseInt(card.dataset.powerLevel) || null; + + // Handle different actions + switch (action) { + case "attack": + await item.rollAttack({event}); + break; + case "damage": + case "versatile": + await item.rollDamage({ + critical: event.altKey, + event: event, + powerLevel: powerLevel, + versatile: action === "versatile" + }); + break; + case "formula": + await item.rollFormula({event, powerLevel}); + break; + case "save": + const targets = this._getChatCardTargets(card); + for (let token of targets) { + const speaker = ChatMessage.getSpeaker({scene: canvas.scene, token: token}); + await token.actor.rollAbilitySave(button.dataset.ability, {event, speaker}); + } + break; + case "toolCheck": + await item.rollToolCheck({event}); + break; + case "placeTemplate": + const template = game.sw5e.canvas.AbilityTemplate.fromItem(item); + if (template) template.drawPreview(); + break; + } + + // Re-enable the button + button.disabled = false; + } + + /* -------------------------------------------- */ + + /** + * Handle toggling the visibility of chat card content when the name is clicked + * @param {Event} event The originating click event + * @private + */ + static _onChatCardToggleContent(event) { + event.preventDefault(); + const header = event.currentTarget; + const card = header.closest(".chat-card"); + const content = card.querySelector(".card-content"); + content.style.display = content.style.display === "none" ? "block" : "none"; + } + + /* -------------------------------------------- */ + + /** + * Get the Actor which is the author of a chat card + * @param {HTMLElement} card The chat card being used + * @return {Actor|null} The Actor entity or null + * @private + */ + static async _getChatCardActor(card) { + // Case 1 - a synthetic actor from a Token + if (card.dataset.tokenId) { + const token = await fromUuid(card.dataset.tokenId); + if (!token) return null; + return token.actor; + } + + // Case 2 - use Actor ID directory + const actorId = card.dataset.actorId; + return game.actors.get(actorId) || null; + } + + /* -------------------------------------------- */ + + /** + * Get the Actor which is the author of a chat card + * @param {HTMLElement} card The chat card being used + * @return {Actor[]} An Array of Actor entities, if any + * @private + */ + static _getChatCardTargets(card) { + let targets = canvas.tokens.controlled.filter((t) => !!t.actor); + if (!targets.length && game.user.character) targets = targets.concat(game.user.character.getActiveTokens()); + if (!targets.length) ui.notifications.warn(game.i18n.localize("SW5E.ActionWarningNoToken")); + return targets; + } + + /* -------------------------------------------- */ + /* Event Handlers */ + /* -------------------------------------------- */ + + /** @inheritdoc */ + async _preCreate(data, options, user) { + await super._preCreate(data, options, user); + if (!this.isEmbedded || this.parent.type === "vehicle") return; + const actorData = this.parent.data; + const isNPC = this.parent.type === "npc"; + let updates; + switch (data.type) { + case "equipment": + updates = this._onCreateOwnedEquipment(data, actorData, isNPC); + break; + case "weapon": + updates = this._onCreateOwnedWeapon(data, actorData, isNPC); + break; + case "power": + updates = this._onCreateOwnedPower(data, actorData, isNPC); + break; + } + if (updates) return this.data.update(updates); + } + + /* -------------------------------------------- */ + + /** @inheritdoc */ + _onCreate(data, options, userId) { + super._onCreate(data, options, userId); + + // The below options are only needed for character classes + if (userId !== game.user.id) return; + const isCharacterClass = this.parent && this.parent.type !== "vehicle" && this.type === "class"; + if (!isCharacterClass) return; + + // Assign a new primary class + const pc = this.parent.items.get(this.parent.data.data.details.originalClass); + if (!pc) this.parent._assignPrimaryClass(); + + // Prompt to add new class features + if (options.addFeatures === false) return; + this.parent + .getClassFeatures({ + className: this.name, + archetypeName: this.data.data.archetype, + level: this.data.data.levels + }) + .then((features) => { + return this.parent.addEmbeddedItems(features, options.promptAddFeatures); + }); + } + + /* -------------------------------------------- */ + + /** @inheritdoc */ + _onUpdate(changed, options, userId) { + super._onUpdate(changed, options, userId); + + // The below options are only needed for character classes + if (userId !== game.user.id) return; + const isCharacterClass = this.parent && this.parent.type !== "vehicle" && this.type === "class"; + if (!isCharacterClass) return; + + // Prompt to add new class features + const addFeatures = changed["name"] || (changed.data && ["archetype", "levels"].some((k) => k in changed.data)); + if (!addFeatures || options.addFeatures === false) return; + this.parent + .getClassFeatures({ + className: changed.name || this.name, + archetypeName: changed.data?.archetype || this.data.data.archetype, + level: changed.data?.levels || this.data.data.levels + }) + .then((features) => { + return this.parent.addEmbeddedItems(features, options.promptAddFeatures); + }); + } + + /* -------------------------------------------- */ + + /** @inheritdoc */ + _onDelete(options, userId) { + super._onDelete(options, userId); + + // Assign a new primary class + if (this.parent && this.type === "class" && userId === game.user.id) { + if (this.id !== this.parent.data.data.details.originalClass) return; + this.parent._assignPrimaryClass(); + } + } + + /* -------------------------------------------- */ + + /** + * Pre-creation logic for the automatic configuration of owned equipment type Items + * @private + */ + _onCreateOwnedEquipment(data, actorData, isNPC) { + const updates = {}; + if (foundry.utils.getProperty(data, "data.equipped") === undefined) { + updates["data.equipped"] = isNPC; // NPCs automatically equip equipment + } + if (foundry.utils.getProperty(data, "data.proficient") === undefined) { + if (isNPC) { + updates["data.proficient"] = true; // NPCs automatically have equipment proficiency + } else { + const armorProf = CONFIG.SW5E.armorProficienciesMap[data.data?.armor?.type]; // Player characters check proficiency + const actorArmorProfs = actorData.data.traits?.armorProf?.value || []; + updates["data.proficient"] = armorProf === true || actorArmorProfs.includes(armorProf); + } + } + return updates; + } + + /* -------------------------------------------- */ + + /** + * Pre-creation logic for the automatic configuration of owned power type Items + * @private + */ + _onCreateOwnedPower(data, actorData, isNPC) { + const updates = {}; + updates["data.preparation.prepared"] = true; // Automatically prepare powers for everyone + return updates; + } + + /* -------------------------------------------- */ + + /** + * Pre-creation logic for the automatic configuration of owned weapon type Items + * @private + */ + _onCreateOwnedWeapon(data, actorData, isNPC) { + const updates = {}; + if (foundry.utils.getProperty(data, "data.equipped") === undefined) { + updates["data.equipped"] = isNPC; // NPCs automatically equip weapons + } + if (foundry.utils.getProperty(data, "data.proficient") === undefined) { + if (isNPC) { + updates["data.proficient"] = true; // NPCs automatically have equipment proficiency + } else { + // TODO: With the changes to make weapon proficiencies more verbose, this may need revising + const weaponProf = CONFIG.SW5E.weaponProficienciesMap[data.data?.weaponType]; // Player characters check proficiency + const actorWeaponProfs = actorData.data.traits?.weaponProf?.value || []; + updates["data.proficient"] = weaponProf === true || actorWeaponProfs.includes(weaponProf); + } + } + return updates; + } + + /* -------------------------------------------- */ + /* Factory Methods */ + /* -------------------------------------------- */ + // TODO: Make work properly + /** + * Create a consumable power scroll Item from a power Item. + * @param {Item5e} power The power to be made into a scroll + * @return {Item5e} The created scroll consumable item + */ + static async createScrollFromPower(power) { + // Get power data + const itemData = power instanceof Item5e ? power.data : power; + const {actionType, description, source, activation, duration, target, range, damage, save, level} = + itemData.data; + + // Get scroll data + const scrollUuid = `Compendium.${CONFIG.SW5E.sourcePacks.ITEMS}.${CONFIG.SW5E.powerScrollIds[level]}`; + const scrollItem = await fromUuid(scrollUuid); + const scrollData = scrollItem.data; + delete scrollData._id; + + // Split the scroll description into an intro paragraph and the remaining details + const scrollDescription = scrollData.data.description.value; + const pdel = "

"; + const scrollIntroEnd = scrollDescription.indexOf(pdel); + const scrollIntro = scrollDescription.slice(0, scrollIntroEnd + pdel.length); + const scrollDetails = scrollDescription.slice(scrollIntroEnd + pdel.length); + + // Create a composite description from the scroll description and the power details + const desc = `${scrollIntro}

${itemData.name} (Level ${level})


${description.value}

Scroll Details


${scrollDetails}`; + + // Create the power scroll data + const powerScrollData = mergeObject(scrollData, { + name: `${game.i18n.localize("SW5E.PowerScroll")}: ${itemData.name}`, + img: itemData.img, + data: { + "description.value": desc.trim(), + source, + actionType, + activation, + duration, + target, + range, + damage, + save, + level + } + }); + return new this(powerScrollData); + } } diff --git a/module/item/sheet.js b/module/item/sheet.js index ba3cfe06..0bb7f64b 100644 --- a/module/item/sheet.js +++ b/module/item/sheet.js @@ -1,361 +1,370 @@ import TraitSelector from "../apps/trait-selector.js"; -import { onManageActiveEffect, prepareActiveEffectCategories } from "../effects.js"; +import {onManageActiveEffect, prepareActiveEffectCategories} from "../effects.js"; /** * Override and extend the core ItemSheet implementation to handle specific item types * @extends {ItemSheet} */ export default class ItemSheet5e extends ItemSheet { - constructor(...args) { - super(...args); + constructor(...args) { + super(...args); - // Expand the default size of the class sheet - if (this.object.data.type === "class") { - this.options.width = this.position.width = 600; - this.options.height = this.position.height = 680; - } - } - - /* -------------------------------------------- */ - - /** @inheritdoc */ - static get defaultOptions() { - return foundry.utils.mergeObject(super.defaultOptions, { - width: 560, - height: 400, - classes: ["sw5e", "sheet", "item"], - resizable: true, - scrollY: [".tab.details"], - tabs: [{ navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description" }] - }); - } - - /* -------------------------------------------- */ - - /** @inheritdoc */ - get template() { - const path = "systems/sw5e/templates/items/"; - return `${path}/${this.item.data.type}.html`; - } - - /* -------------------------------------------- */ - - /** @override */ - async getData(options) { - const data = super.getData(options); - const itemData = data.data; - data.labels = this.item.labels; - data.config = CONFIG.SW5E; - - // Item Type, Status, and Details - data.itemType = game.i18n.localize(`ITEM.Type${data.item.type.titleCase()}`); - data.itemStatus = this._getItemStatus(itemData); - data.itemProperties = this._getItemProperties(itemData); - data.isPhysical = itemData.data.hasOwnProperty("quantity"); - - // Potential consumption targets - data.abilityConsumptionTargets = this._getItemConsumptionTargets(itemData); - - // Action Details - data.hasAttackRoll = this.item.hasAttack; - data.isHealing = itemData.data.actionType === "heal"; - data.isFlatDC = getProperty(itemData, "data.save.scaling") === "flat"; - data.isLine = ["line", "wall"].includes(itemData.data.target?.type); - - // Original maximum uses formula - const sourceMax = foundry.utils.getProperty(this.item.data._source, "data.uses.max"); - if ( sourceMax ) itemData.data.uses.max = sourceMax; - - // Vehicles - data.isCrewed = itemData.data.activation?.type === "crew"; - data.isMountable = this._isItemMountable(itemData); - - // Prepare Active Effects - data.effects = prepareActiveEffectCategories(this.item.effects); - - // Re-define the template data references (backwards compatible) - data.item = itemData; - data.data = itemData.data; - return data; - } - - /* -------------------------------------------- */ - - /** - * Get the valid item consumption targets which exist on the actor - * @param {Object} item Item data for the item being displayed - * @return {{string: string}} An object of potential consumption targets - * @private - */ - _getItemConsumptionTargets(item) { - const consume = item.data.consume || {}; - if (!consume.type) return []; - const actor = this.item.actor; - if (!actor) return {}; - - // Ammunition - if (consume.type === "ammo") { - return actor.itemTypes.consumable.reduce( - (ammo, i) => { - if (i.data.data.consumableType === "ammo") { - ammo[i.id] = `${i.name} (${i.data.data.quantity})`; - } - return ammo; - }, - { [item._id]: `${item.name} (${item.data.quantity})` } - ); - } - - // Attributes - else if (consume.type === "attribute") { - const attributes = TokenDocument.getTrackedAttributes(actor.data.data); - attributes.bar.forEach(a => a.push("value")); - return attributes.bar.concat(attributes.value).reduce((obj, a) => { - let k = a.join("."); - obj[k] = k; - return obj; - }, {}); - } - - // Materials - else if (consume.type === "material") { - return actor.items.reduce((obj, i) => { - if (["consumable", "loot"].includes(i.data.type) && !i.data.data.activation) { - obj[i.id] = `${i.name} (${i.data.data.quantity})`; + // Expand the default size of the class sheet + if (this.object.data.type === "class") { + this.options.width = this.position.width = 600; + this.options.height = this.position.height = 680; } - return obj; - }, {}); } - // Charges - else if (consume.type === "charges") { - return actor.items.reduce((obj, i) => { - // Limited-use items - const uses = i.data.data.uses || {}; - if (uses.per && uses.max) { - const label = - uses.per === "charges" - ? ` (${game.i18n.format("SW5E.AbilityUseChargesLabel", { value: uses.value })})` - : ` (${game.i18n.format("SW5E.AbilityUseConsumableLabel", { max: uses.max, per: uses.per })})`; - obj[i.id] = i.name + label; + /* -------------------------------------------- */ + + /** @inheritdoc */ + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + width: 560, + height: 400, + classes: ["sw5e", "sheet", "item"], + resizable: true, + scrollY: [".tab.details"], + tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}] + }); + } + + /* -------------------------------------------- */ + + /** @inheritdoc */ + get template() { + const path = "systems/sw5e/templates/items/"; + return `${path}/${this.item.data.type}.html`; + } + + /* -------------------------------------------- */ + + /** @override */ + async getData(options) { + const data = super.getData(options); + const itemData = data.data; + data.labels = this.item.labels; + data.config = CONFIG.SW5E; + + // Item Type, Status, and Details + data.itemType = game.i18n.localize(`ITEM.Type${data.item.type.titleCase()}`); + data.itemStatus = this._getItemStatus(itemData); + data.itemProperties = this._getItemProperties(itemData); + data.isPhysical = itemData.data.hasOwnProperty("quantity"); + + // Potential consumption targets + data.abilityConsumptionTargets = this._getItemConsumptionTargets(itemData); + + // Action Details + data.hasAttackRoll = this.item.hasAttack; + data.isHealing = itemData.data.actionType === "heal"; + data.isFlatDC = getProperty(itemData, "data.save.scaling") === "flat"; + data.isLine = ["line", "wall"].includes(itemData.data.target?.type); + + // Original maximum uses formula + const sourceMax = foundry.utils.getProperty(this.item.data._source, "data.uses.max"); + if (sourceMax) itemData.data.uses.max = sourceMax; + + // Vehicles + data.isCrewed = itemData.data.activation?.type === "crew"; + data.isMountable = this._isItemMountable(itemData); + + // Prepare Active Effects + data.effects = prepareActiveEffectCategories(this.item.effects); + + // Re-define the template data references (backwards compatible) + data.item = itemData; + data.data = itemData.data; + return data; + } + + /* -------------------------------------------- */ + + /** + * Get the valid item consumption targets which exist on the actor + * @param {Object} item Item data for the item being displayed + * @return {{string: string}} An object of potential consumption targets + * @private + */ + _getItemConsumptionTargets(item) { + const consume = item.data.consume || {}; + if (!consume.type) return []; + const actor = this.item.actor; + if (!actor) return {}; + + // Ammunition + if (consume.type === "ammo") { + return actor.itemTypes.consumable.reduce( + (ammo, i) => { + if (i.data.data.consumableType === "ammo") { + ammo[i.id] = `${i.name} (${i.data.data.quantity})`; + } + return ammo; + }, + {[item._id]: `${item.name} (${item.data.quantity})`} + ); } - // Recharging items - const recharge = i.data.data.recharge || {}; - if (recharge.value) obj[i.id] = `${i.name} (${game.i18n.format("SW5E.Recharge")})`; - return obj; - }, {}); - } else return {}; - } + // Attributes + else if (consume.type === "attribute") { + const attributes = TokenDocument.getTrackedAttributes(actor.data.data); + attributes.bar.forEach((a) => a.push("value")); + return attributes.bar.concat(attributes.value).reduce((obj, a) => { + let k = a.join("."); + obj[k] = k; + return obj; + }, {}); + } - /* -------------------------------------------- */ + // Materials + else if (consume.type === "material") { + return actor.items.reduce((obj, i) => { + if (["consumable", "loot"].includes(i.data.type) && !i.data.data.activation) { + obj[i.id] = `${i.name} (${i.data.data.quantity})`; + } + return obj; + }, {}); + } - /** - * Get the text item status which is shown beneath the Item type in the top-right corner of the sheet - * @return {string} - * @private - */ - _getItemStatus(item) { - if (item.type === "power") { - return CONFIG.SW5E.powerPreparationModes[item.data.preparation]; - } else if (["weapon", "equipment"].includes(item.type)) { - return game.i18n.localize(item.data.equipped ? "SW5E.Equipped" : "SW5E.Unequipped"); - } else if (item.type === "tool") { - return game.i18n.localize(item.data.proficient ? "SW5E.Proficient" : "SW5E.NotProficient"); - } - } + // Charges + else if (consume.type === "charges") { + return actor.items.reduce((obj, i) => { + // Limited-use items + const uses = i.data.data.uses || {}; + if (uses.per && uses.max) { + const label = + uses.per === "charges" + ? ` (${game.i18n.format("SW5E.AbilityUseChargesLabel", {value: uses.value})})` + : ` (${game.i18n.format("SW5E.AbilityUseConsumableLabel", { + max: uses.max, + per: uses.per + })})`; + obj[i.id] = i.name + label; + } - /* -------------------------------------------- */ - - /** - * Get the Array of item properties which are used in the small sidebar of the description tab - * @return {Array} - * @private - */ - _getItemProperties(item) { - const props = []; - const labels = this.item.labels; - - if (item.type === "weapon") { - props.push( - ...Object.entries(item.data.properties) - .filter((e) => e[1] === true) - .map((e) => CONFIG.SW5E.weaponProperties[e[0]]) - ); - } else if (item.type === "power") { - props.push( - labels.materials, - item.data.components.concentration ? game.i18n.localize("SW5E.Concentration") : null, - item.data.components.ritual ? game.i18n.localize("SW5E.Ritual") : null - ); - } else if (item.type === "equipment") { - props.push(CONFIG.SW5E.equipmentTypes[item.data.armor.type]); - props.push(labels.armor); - } else if (item.type === "feat") { - props.push(labels.featType); - //TODO: Work out these - } else if (item.type === "species") { - //props.push(labels.species); - } else if (item.type === "archetype") { - //props.push(labels.archetype); - } else if (item.type === "background") { - //props.push(labels.background); - } else if (item.type === "classfeature") { - //props.push(labels.classfeature); - } else if (item.type === "deployment") { - //props.push(labels.deployment); - } else if (item.type === "venture") { - //props.push(labels.venture); - } else if (item.type === "fightingmastery") { - //props.push(labels.fightingmastery); - } else if (item.type === "fightingstyle") { - //props.push(labels.fightingstyle); - } else if (item.type === "lightsaberform") { - //props.push(labels.lightsaberform); + // Recharging items + const recharge = i.data.data.recharge || {}; + if (recharge.value) obj[i.id] = `${i.name} (${game.i18n.format("SW5E.Recharge")})`; + return obj; + }, {}); + } else return {}; } - // Action type - if (item.data.actionType) { - props.push(CONFIG.SW5E.itemActionTypes[item.data.actionType]); + /* -------------------------------------------- */ + + /** + * Get the text item status which is shown beneath the Item type in the top-right corner of the sheet + * @return {string} + * @private + */ + _getItemStatus(item) { + if (item.type === "power") { + return CONFIG.SW5E.powerPreparationModes[item.data.preparation]; + } else if (["weapon", "equipment"].includes(item.type)) { + return game.i18n.localize(item.data.equipped ? "SW5E.Equipped" : "SW5E.Unequipped"); + } else if (item.type === "tool") { + return game.i18n.localize(item.data.proficient ? "SW5E.Proficient" : "SW5E.NotProficient"); + } } - // Action usage - if (item.type !== "weapon" && item.data.activation && !isObjectEmpty(item.data.activation)) { - props.push(labels.activation, labels.range, labels.target, labels.duration); - } - return props.filter((p) => !!p); - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** + * Get the Array of item properties which are used in the small sidebar of the description tab + * @return {Array} + * @private + */ + _getItemProperties(item) { + const props = []; + const labels = this.item.labels; - /** - * Is this item a separate large object like a siege engine or vehicle - * component that is usually mounted on fixtures rather than equipped, and - * has its own AC and HP. - * @param item - * @returns {boolean} - * @private - */ - _isItemMountable(item) { - const data = item.data; - return ( - (item.type === "weapon" && data.weaponType === "siege") || - (item.type === "equipment" && data.armor.type === "vehicle") - ); - } + if (item.type === "weapon") { + props.push( + ...Object.entries(item.data.properties) + .filter((e) => e[1] === true) + .map((e) => CONFIG.SW5E.weaponProperties[e[0]]) + ); + } else if (item.type === "power") { + props.push( + labels.materials, + item.data.components.concentration ? game.i18n.localize("SW5E.Concentration") : null, + item.data.components.ritual ? game.i18n.localize("SW5E.Ritual") : null + ); + } else if (item.type === "equipment") { + props.push(CONFIG.SW5E.equipmentTypes[item.data.armor.type]); + props.push(labels.armor); + } else if (item.type === "feat") { + props.push(labels.featType); + //TODO: Work out these + } else if (item.type === "species") { + //props.push(labels.species); + } else if (item.type === "archetype") { + //props.push(labels.archetype); + } else if (item.type === "background") { + //props.push(labels.background); + } else if (item.type === "classfeature") { + //props.push(labels.classfeature); + } else if (item.type === "deployment") { + //props.push(labels.deployment); + } else if (item.type === "venture") { + //props.push(labels.venture); + } else if (item.type === "fightingmastery") { + //props.push(labels.fightingmastery); + } else if (item.type === "fightingstyle") { + //props.push(labels.fightingstyle); + } else if (item.type === "lightsaberform") { + //props.push(labels.lightsaberform); + } - /* -------------------------------------------- */ + // Action type + if (item.data.actionType) { + props.push(CONFIG.SW5E.itemActionTypes[item.data.actionType]); + } - /** @inheritdoc */ - setPosition(position = {}) { - if (!(this._minimized || position.height)) { - position.height = this._tabs[0].active === "details" ? "auto" : this.options.height; - } - return super.setPosition(position); - } - - /* -------------------------------------------- */ - /* Form Submission */ - /* -------------------------------------------- */ - - /** @inheritdoc */ - _getSubmitData(updateData = {}) { - // Create the expanded update data object - const fd = new FormDataExtended(this.form, { editors: this.editors }); - let data = fd.toObject(); - if (updateData) data = mergeObject(data, updateData); - else data = expandObject(data); - - // Handle Damage array - const damage = data.data?.damage; - if (damage) damage.parts = Object.values(damage?.parts || {}).map((d) => [d[0] || "", d[1] || ""]); - - // Return the flattened submission data - return flattenObject(data); - } - - /* -------------------------------------------- */ - - /** @inheritdoc */ - activateListeners(html) { - super.activateListeners(html); - if (this.isEditable) { - html.find(".damage-control").click(this._onDamageControl.bind(this)); - html.find(".trait-selector.class-skills").click(this._onConfigureTraits.bind(this)); - html.find(".effect-control").click((ev) => { - if (this.item.isOwned) return ui.notifications.warn("Managing Active Effects within an Owned Item is not currently supported and will be added in a subsequent update."); - onManageActiveEffect(ev, this.item); - }); - } - } - - /* -------------------------------------------- */ - - /** - * Add or remove a damage part from the damage formula - * @param {Event} event The original click event - * @return {Promise} - * @private - */ - async _onDamageControl(event) { - event.preventDefault(); - const a = event.currentTarget; - - // Add new damage component - if (a.classList.contains("add-damage")) { - await this._onSubmit(event); // Submit any unsaved changes - const damage = this.item.data.data.damage; - return this.item.update({ "data.damage.parts": damage.parts.concat([["", ""]]) }); + // Action usage + if (item.type !== "weapon" && item.data.activation && !isObjectEmpty(item.data.activation)) { + props.push(labels.activation, labels.range, labels.target, labels.duration); + } + return props.filter((p) => !!p); } - // Remove a damage component - if (a.classList.contains("delete-damage")) { - await this._onSubmit(event); // Submit any unsaved changes - const li = a.closest(".damage-part"); - const damage = foundry.utils.deepClone(this.item.data.data.damage); - damage.parts.splice(Number(li.dataset.damagePart), 1); - return this.item.update({ "data.damage.parts": damage.parts }); + /* -------------------------------------------- */ + + /** + * Is this item a separate large object like a siege engine or vehicle + * component that is usually mounted on fixtures rather than equipped, and + * has its own AC and HP. + * @param item + * @returns {boolean} + * @private + */ + _isItemMountable(item) { + const data = item.data; + return ( + (item.type === "weapon" && data.weaponType === "siege") || + (item.type === "equipment" && data.armor.type === "vehicle") + ); } - } - /* -------------------------------------------- */ + /* -------------------------------------------- */ - /** - * Handle spawning the TraitSelector application for selection various options. - * @param {Event} event The click event which originated the selection - * @private - */ - _onConfigureTraits(event) { - event.preventDefault(); - const a = event.currentTarget; - - const options = { - name: a.dataset.target, - title: a.parentElement.innerText, - choices: [], - allowCustom: false - }; - - switch(a.dataset.options) { - case 'saves': - options.choices = CONFIG.SW5E.abilities; - options.valueKey = null; - break; - case 'skills': - const skills = this.item.data.data.skills; - const choiceSet = skills.choices && skills.choices.length ? skills.choices : Object.keys(CONFIG.SW5E.skills); - options.choices = Object.fromEntries(Object.entries(CONFIG.SW5E.skills).filter(skill => choiceSet.includes(skill[0]))); - options.maximum = skills.number; - break; + /** @inheritdoc */ + setPosition(position = {}) { + if (!(this._minimized || position.height)) { + position.height = this._tabs[0].active === "details" ? "auto" : this.options.height; + } + return super.setPosition(position); } - new TraitSelector(this.item, options).render(true); - } - /* -------------------------------------------- */ + /* -------------------------------------------- */ + /* Form Submission */ + /* -------------------------------------------- */ - /** @inheritdoc */ - async _onSubmit(...args) { - if (this._tabs[0].active === "details") this.position.height = "auto"; - await super._onSubmit(...args); - } + /** @inheritdoc */ + _getSubmitData(updateData = {}) { + // Create the expanded update data object + const fd = new FormDataExtended(this.form, {editors: this.editors}); + let data = fd.toObject(); + if (updateData) data = mergeObject(data, updateData); + else data = expandObject(data); + + // Handle Damage array + const damage = data.data?.damage; + if (damage) damage.parts = Object.values(damage?.parts || {}).map((d) => [d[0] || "", d[1] || ""]); + + // Return the flattened submission data + return flattenObject(data); + } + + /* -------------------------------------------- */ + + /** @inheritdoc */ + activateListeners(html) { + super.activateListeners(html); + if (this.isEditable) { + html.find(".damage-control").click(this._onDamageControl.bind(this)); + html.find(".trait-selector.class-skills").click(this._onConfigureTraits.bind(this)); + html.find(".effect-control").click((ev) => { + if (this.item.isOwned) + return ui.notifications.warn( + "Managing Active Effects within an Owned Item is not currently supported and will be added in a subsequent update." + ); + onManageActiveEffect(ev, this.item); + }); + } + } + + /* -------------------------------------------- */ + + /** + * Add or remove a damage part from the damage formula + * @param {Event} event The original click event + * @return {Promise} + * @private + */ + async _onDamageControl(event) { + event.preventDefault(); + const a = event.currentTarget; + + // Add new damage component + if (a.classList.contains("add-damage")) { + await this._onSubmit(event); // Submit any unsaved changes + const damage = this.item.data.data.damage; + return this.item.update({"data.damage.parts": damage.parts.concat([["", ""]])}); + } + + // Remove a damage component + if (a.classList.contains("delete-damage")) { + await this._onSubmit(event); // Submit any unsaved changes + const li = a.closest(".damage-part"); + const damage = foundry.utils.deepClone(this.item.data.data.damage); + damage.parts.splice(Number(li.dataset.damagePart), 1); + return this.item.update({"data.damage.parts": damage.parts}); + } + } + + /* -------------------------------------------- */ + + /** + * Handle spawning the TraitSelector application for selection various options. + * @param {Event} event The click event which originated the selection + * @private + */ + _onConfigureTraits(event) { + event.preventDefault(); + const a = event.currentTarget; + + const options = { + name: a.dataset.target, + title: a.parentElement.innerText, + choices: [], + allowCustom: false + }; + + switch (a.dataset.options) { + case "saves": + options.choices = CONFIG.SW5E.abilities; + options.valueKey = null; + break; + case "skills": + const skills = this.item.data.data.skills; + const choiceSet = + skills.choices && skills.choices.length ? skills.choices : Object.keys(CONFIG.SW5E.skills); + options.choices = Object.fromEntries( + Object.entries(CONFIG.SW5E.skills).filter((skill) => choiceSet.includes(skill[0])) + ); + options.maximum = skills.number; + break; + } + new TraitSelector(this.item, options).render(true); + } + + /* -------------------------------------------- */ + + /** @inheritdoc */ + async _onSubmit(...args) { + if (this._tabs[0].active === "details") this.position.height = "auto"; + await super._onSubmit(...args); + } } diff --git a/module/macros.js b/module/macros.js index 96bb3419..e8919864 100644 --- a/module/macros.js +++ b/module/macros.js @@ -1,4 +1,3 @@ - /* -------------------------------------------- */ /* Hotbar Macros */ /* -------------------------------------------- */ @@ -11,24 +10,24 @@ * @returns {Promise} */ export async function create5eMacro(data, slot) { - if ( data.type !== "Item" ) return; - if (!( "data" in data ) ) return ui.notifications.warn("You can only create macro buttons for owned Items"); - const item = data.data; + if (data.type !== "Item") return; + if (!("data" in data)) return ui.notifications.warn("You can only create macro buttons for owned Items"); + const item = data.data; - // Create the macro command - const command = `game.sw5e.rollItemMacro("${item.name}");`; - let macro = game.macros.entities.find(m => (m.name === item.name) && (m.command === command)); - if ( !macro ) { - macro = await Macro.create({ - name: item.name, - type: "script", - img: item.img, - command: command, - flags: {"sw5e.itemMacro": true} - }); - } - game.user.assignHotbarMacro(macro, slot); - return false; + // Create the macro command + const command = `game.sw5e.rollItemMacro("${item.name}");`; + let macro = game.macros.entities.find((m) => m.name === item.name && m.command === command); + if (!macro) { + macro = await Macro.create({ + name: item.name, + type: "script", + img: item.img, + command: command, + flags: {"sw5e.itemMacro": true} + }); + } + game.user.assignHotbarMacro(macro, slot); + return false; } /* -------------------------------------------- */ @@ -40,20 +39,22 @@ export async function create5eMacro(data, slot) { * @return {Promise} */ export function rollItemMacro(itemName) { - const speaker = ChatMessage.getSpeaker(); - let actor; - if ( speaker.token ) actor = game.actors.tokens[speaker.token]; - if ( !actor ) actor = game.actors.get(speaker.actor); + const speaker = ChatMessage.getSpeaker(); + let actor; + if (speaker.token) actor = game.actors.tokens[speaker.token]; + if (!actor) actor = game.actors.get(speaker.actor); - // Get matching items - const items = actor ? actor.items.filter(i => i.name === itemName) : []; - if ( items.length > 1 ) { - ui.notifications.warn(`Your controlled Actor ${actor.name} has more than one Item with name ${itemName}. The first matched item will be chosen.`); - } else if ( items.length === 0 ) { - return ui.notifications.warn(`Your controlled Actor does not have an item named ${itemName}`); - } - const item = items[0]; + // Get matching items + const items = actor ? actor.items.filter((i) => i.name === itemName) : []; + if (items.length > 1) { + ui.notifications.warn( + `Your controlled Actor ${actor.name} has more than one Item with name ${itemName}. The first matched item will be chosen.` + ); + } else if (items.length === 0) { + return ui.notifications.warn(`Your controlled Actor does not have an item named ${itemName}`); + } + const item = items[0]; - // Trigger the item roll - return item.roll(); + // Trigger the item roll + return item.roll(); } diff --git a/module/migration.js b/module/migration.js index e549e934..ab6eb420 100644 --- a/module/migration.js +++ b/module/migration.js @@ -2,65 +2,68 @@ * Perform a system migration for the entire World, applying migrations for Actors, Items, and Compendium packs * @return {Promise} A Promise which resolves once the migration is completed */ -export const migrateWorld = async function() { - ui.notifications.info(`Applying SW5e System Migration for version ${game.system.data.version}. Please be patient and do not close your game or shut down your server.`, {permanent: true}); +export const migrateWorld = async function () { + ui.notifications.info( + `Applying SW5e System Migration for version ${game.system.data.version}. Please be patient and do not close your game or shut down your server.`, + {permanent: true} + ); - // Migrate World Actors - for await ( let a of game.actors.contents ) { - try { - console.log(`Checking Actor entity ${a.name} for migration needs`); - const updateData = await migrateActorData(a.data); - if ( !foundry.utils.isObjectEmpty(updateData) ) { - console.log(`Migrating Actor entity ${a.name}`); - await a.update(updateData, {enforceTypes: false}); - } - } catch(err) { - err.message = `Failed sw5e system migration for Actor ${a.name}: ${err.message}`; - console.error(err); + // Migrate World Actors + for await (let a of game.actors.contents) { + try { + console.log(`Checking Actor entity ${a.name} for migration needs`); + const updateData = await migrateActorData(a.data); + if (!foundry.utils.isObjectEmpty(updateData)) { + console.log(`Migrating Actor entity ${a.name}`); + await a.update(updateData, {enforceTypes: false}); + } + } catch (err) { + err.message = `Failed sw5e system migration for Actor ${a.name}: ${err.message}`; + console.error(err); + } } - } - // Migrate World Items - for ( let i of game.items.contents ) { - try { - const updateData = migrateItemData(i.toObject()); - if ( !foundry.utils.isObjectEmpty(updateData) ) { - console.log(`Migrating Item entity ${i.name}`); - await i.update(updateData, {enforceTypes: false}); - } - } catch(err) { - err.message = `Failed sw5e system migration for Item ${i.name}: ${err.message}`; - console.error(err); + // Migrate World Items + for (let i of game.items.contents) { + try { + const updateData = migrateItemData(i.toObject()); + if (!foundry.utils.isObjectEmpty(updateData)) { + console.log(`Migrating Item entity ${i.name}`); + await i.update(updateData, {enforceTypes: false}); + } + } catch (err) { + err.message = `Failed sw5e system migration for Item ${i.name}: ${err.message}`; + console.error(err); + } } - } - // Migrate Actor Override Tokens - for ( let s of game.scenes.contents ) { - try { - const updateData = await migrateSceneData(s.data); - if ( !foundry.utils.isObjectEmpty(updateData) ) { - console.log(`Migrating Scene entity ${s.name}`); - await s.update(updateData, {enforceTypes: false}); - // If we do not do this, then synthetic token actors remain in cache - // with the un-updated actorData. - s.tokens.contents.forEach(t => t._actor = null); - } - } catch(err) { - err.message = `Failed sw5e system migration for Scene ${s.name}: ${err.message}`; - console.error(err); + // Migrate Actor Override Tokens + for (let s of game.scenes.contents) { + try { + const updateData = await migrateSceneData(s.data); + if (!foundry.utils.isObjectEmpty(updateData)) { + console.log(`Migrating Scene entity ${s.name}`); + await s.update(updateData, {enforceTypes: false}); + // If we do not do this, then synthetic token actors remain in cache + // with the un-updated actorData. + s.tokens.contents.forEach((t) => (t._actor = null)); + } + } catch (err) { + err.message = `Failed sw5e system migration for Scene ${s.name}: ${err.message}`; + console.error(err); + } } - } - // Migrate World Compendium Packs - for ( let p of game.packs ) { - if ( p.metadata.package !== "world" ) continue; - if ( !["Actor", "Item", "Scene"].includes(p.metadata.entity) ) continue; - await migrateCompendium(p); - } + // Migrate World Compendium Packs + for (let p of game.packs) { + if (p.metadata.package !== "world") continue; + if (!["Actor", "Item", "Scene"].includes(p.metadata.entity)) continue; + await migrateCompendium(p); + } - // Set the migration as complete - game.settings.set("sw5e", "systemMigrationVersion", game.system.data.version); - ui.notifications.info(`SW5e System Migration to version ${game.system.data.version} completed!`, {permanent: true}); + // Set the migration as complete + game.settings.set("sw5e", "systemMigrationVersion", game.system.data.version); + ui.notifications.info(`SW5e System Migration to version ${game.system.data.version} completed!`, {permanent: true}); }; /* -------------------------------------------- */ @@ -70,50 +73,48 @@ export const migrateWorld = async function() { * @param pack * @return {Promise} */ -export const migrateCompendium = async function(pack) { - const entity = pack.metadata.entity; - if ( !["Actor", "Item", "Scene"].includes(entity) ) return; +export const migrateCompendium = async function (pack) { + const entity = pack.metadata.entity; + if (!["Actor", "Item", "Scene"].includes(entity)) return; - // Unlock the pack for editing - const wasLocked = pack.locked; - await pack.configure({locked: false}); + // Unlock the pack for editing + const wasLocked = pack.locked; + await pack.configure({locked: false}); - // Begin by requesting server-side data model migration and get the migrated content - await pack.migrate(); - const documents = await pack.getDocuments(); + // Begin by requesting server-side data model migration and get the migrated content + await pack.migrate(); + const documents = await pack.getDocuments(); - // Iterate over compendium entries - applying fine-tuned migration functions - for await ( let doc of documents ) { - let updateData = {}; - try { - switch (entity) { - case "Actor": - updateData = await migrateActorData(doc.data); - break; - case "Item": - updateData = migrateItemData(doc.toObject()); - break; - case "Scene": - updateData = await migrateSceneData(doc.data); - break; - } - if ( foundry.utils.isObjectEmpty(updateData) ) continue; + // Iterate over compendium entries - applying fine-tuned migration functions + for await (let doc of documents) { + let updateData = {}; + try { + switch (entity) { + case "Actor": + updateData = await migrateActorData(doc.data); + break; + case "Item": + updateData = migrateItemData(doc.toObject()); + break; + case "Scene": + updateData = await migrateSceneData(doc.data); + break; + } + if (foundry.utils.isObjectEmpty(updateData)) continue; - // Save the entry, if data was changed - await doc.update(updateData); - console.log(`Migrated ${entity} entity ${doc.name} in Compendium ${pack.collection}`); + // Save the entry, if data was changed + await doc.update(updateData); + console.log(`Migrated ${entity} entity ${doc.name} in Compendium ${pack.collection}`); + } catch (err) { + // Handle migration failures + err.message = `Failed sw5e system migration for entity ${doc.name} in pack ${pack.collection}: ${err.message}`; + console.error(err); + } } - // Handle migration failures - catch(err) { - err.message = `Failed sw5e system migration for entity ${doc.name} in pack ${pack.collection}: ${err.message}`; - console.error(err); - } - } - - // Apply the original locked status for the pack - await pack.configure({locked: wasLocked}); - console.log(`Migrated all ${entity} entities from Compendium ${pack.collection}`); + // Apply the original locked status for the pack + await pack.configure({locked: wasLocked}); + console.log(`Migrated all ${entity} entities from Compendium ${pack.collection}`); }; /* -------------------------------------------- */ @@ -126,84 +127,82 @@ export const migrateCompendium = async function(pack) { * @param {object} actor The actor data object to update * @return {Object} The updateData to apply */ -export const migrateActorData = async function(actor) { - const updateData = {}; +export const migrateActorData = async function (actor) { + const updateData = {}; - // Actor Data Updates - if(actor.data) { - _migrateActorMovement(actor, updateData); - _migrateActorSenses(actor, updateData); - _migrateActorType(actor, updateData); - } + // Actor Data Updates + if (actor.data) { + _migrateActorMovement(actor, updateData); + _migrateActorSenses(actor, updateData); + _migrateActorType(actor, updateData); + } - // Migrate Owned Items - if ( !!actor.items ) { - const items = await actor.items.reduce(async (memo, i) => { - const results = await memo; + // Migrate Owned Items + if (!!actor.items) { + const items = await actor.items.reduce(async (memo, i) => { + const results = await memo; - // Migrate the Owned Item - const itemData = i instanceof CONFIG.Item.documentClass ? i.toObject() : i; - let itemUpdate = await migrateActorItemData(itemData, actor); + // Migrate the Owned Item + const itemData = i instanceof CONFIG.Item.documentClass ? i.toObject() : i; + let itemUpdate = await migrateActorItemData(itemData, actor); - // Prepared, Equipped, and Proficient for NPC actors - if ( actor.type === "npc" ) { - if (getProperty(itemData.data, "preparation.prepared") === false) itemUpdate["data.preparation.prepared"] = true; - if (getProperty(itemData.data, "equipped") === false) itemUpdate["data.equipped"] = true; - if (getProperty(itemData.data, "proficient") === false) itemUpdate["data.proficient"] = true; - } + // Prepared, Equipped, and Proficient for NPC actors + if (actor.type === "npc") { + if (getProperty(itemData.data, "preparation.prepared") === false) + itemUpdate["data.preparation.prepared"] = true; + if (getProperty(itemData.data, "equipped") === false) itemUpdate["data.equipped"] = true; + if (getProperty(itemData.data, "proficient") === false) itemUpdate["data.proficient"] = true; + } - // Update the Owned Item - if ( !isObjectEmpty(itemUpdate) ) { - itemUpdate._id = itemData._id; - console.log(`Migrating Actor ${actor.name}'s ${i.name}`); - results.push(expandObject(itemUpdate)); - } + // Update the Owned Item + if (!isObjectEmpty(itemUpdate)) { + itemUpdate._id = itemData._id; + console.log(`Migrating Actor ${actor.name}'s ${i.name}`); + results.push(expandObject(itemUpdate)); + } - return results; - }, []); + return results; + }, []); - if ( items.length > 0 ) updateData.items = items; - } + if (items.length > 0) updateData.items = items; + } - // Update NPC data with new datamodel information - if (actor.type === "npc") { - _updateNPCData(actor); - } + // Update NPC data with new datamodel information + if (actor.type === "npc") { + _updateNPCData(actor); + } - // migrate powers last since it relies on item classes being migrated first. - _migrateActorPowers(actor, updateData); - - return updateData; + // migrate powers last since it relies on item classes being migrated first. + _migrateActorPowers(actor, updateData); + + return updateData; }; /* -------------------------------------------- */ - /** * Scrub an Actor's system data, removing all keys which are not explicitly defined in the system template * @param {Object} actorData The data object for an Actor * @return {Object} The scrubbed Actor data */ function cleanActorData(actorData) { + // Scrub system data + const model = game.system.model.Actor[actorData.type]; + actorData.data = filterObject(actorData.data, model); - // Scrub system data - const model = game.system.model.Actor[actorData.type]; - actorData.data = filterObject(actorData.data, model); + // Scrub system flags + const allowedFlags = CONFIG.SW5E.allowedActorFlags.reduce((obj, f) => { + obj[f] = null; + return obj; + }, {}); + if (actorData.flags.sw5e) { + actorData.flags.sw5e = filterObject(actorData.flags.sw5e, allowedFlags); + } - // Scrub system flags - const allowedFlags = CONFIG.SW5E.allowedActorFlags.reduce((obj, f) => { - obj[f] = null; - return obj; - }, {}); - if ( actorData.flags.sw5e ) { - actorData.flags.sw5e = filterObject(actorData.flags.sw5e, allowedFlags); - } - - // Return the scrubbed data - return actorData; + // Return the scrubbed data + return actorData; } - /* -------------------------------------------- */ /** @@ -212,11 +211,11 @@ function cleanActorData(actorData) { * @param {object} item Item data to migrate * @return {object} The updateData to apply */ -export const migrateItemData = function(item) { - const updateData = {}; - _migrateItemClassPowerCasting(item, updateData); - _migrateItemAttunement(item, updateData); - return updateData; +export const migrateItemData = function (item) { + const updateData = {}; + _migrateItemClassPowerCasting(item, updateData); + _migrateItemAttunement(item, updateData); + return updateData; }; /* -------------------------------------------- */ @@ -226,12 +225,12 @@ export const migrateItemData = function(item) { * @param item * @param actor */ -export const migrateActorItemData = async function(item, actor) { - const updateData = {}; - _migrateItemClassPowerCasting(item, updateData); - _migrateItemAttunement(item, updateData); - await _migrateItemPower(item, actor, updateData); - return updateData; +export const migrateActorItemData = async function (item, actor) { + const updateData = {}; + _migrateItemClassPowerCasting(item, updateData); + _migrateItemAttunement(item, updateData); + await _migrateItemPower(item, actor, updateData); + return updateData; }; /* -------------------------------------------- */ @@ -242,33 +241,34 @@ export const migrateActorItemData = async function(item, actor) { * @param {Object} scene The Scene data to Update * @return {Object} The updateData to apply */ - export const migrateSceneData = async function(scene) { - const tokens = await Promise.all(scene.tokens.map(async token => { - const t = token.toJSON(); - if (!t.actorId || t.actorLink) { - t.actorData = {}; - } - else if (!game.actors.has(t.actorId)) { - t.actorId = null; - t.actorData = {}; - } else if ( !t.actorLink ) { - const actorData = duplicate(t.actorData); - actorData.type = token.actor?.type; - const update = migrateActorData(actorData); - ['items', 'effects'].forEach(embeddedName => { - if (!update[embeddedName]?.length) return; - const updates = new Map(update[embeddedName].map(u => [u._id, u])); - t.actorData[embeddedName].forEach(original => { - const update = updates.get(original._id); - if (update) mergeObject(original, update); - }); - delete update[embeddedName]; - }); +export const migrateSceneData = async function (scene) { + const tokens = await Promise.all( + scene.tokens.map(async (token) => { + const t = token.toJSON(); + if (!t.actorId || t.actorLink) { + t.actorData = {}; + } else if (!game.actors.has(t.actorId)) { + t.actorId = null; + t.actorData = {}; + } else if (!t.actorLink) { + const actorData = duplicate(t.actorData); + actorData.type = token.actor?.type; + const update = migrateActorData(actorData); + ["items", "effects"].forEach((embeddedName) => { + if (!update[embeddedName]?.length) return; + const updates = new Map(update[embeddedName].map((u) => [u._id, u])); + t.actorData[embeddedName].forEach((original) => { + const update = updates.get(original._id); + if (update) mergeObject(original, update); + }); + delete update[embeddedName]; + }); - mergeObject(t.actorData, update); - } - return t; - })); + mergeObject(t.actorData, update); + } + return t; + }) + ); return {tokens}; }; @@ -284,87 +284,93 @@ export const migrateActorItemData = async function(item, actor) { * @return {Object} The updated Actor */ function _updateNPCData(actor) { + let actorData = actor.data; + const updateData = {}; + // check for flag.core, if not there is no compendium monster so exit + const hasSource = actor?.flags?.core?.sourceId !== undefined; + if (!hasSource) return actor; + // shortcut out if dataVersion flag is set to 1.2.4 or higher + const hasDataVersion = actor?.flags?.sw5e?.dataVersion !== undefined; + if ( + hasDataVersion && + (actor.flags.sw5e.dataVersion === "1.2.4" || isNewerVersion("1.2.4", actor.flags.sw5e.dataVersion)) + ) + return actor; + // Check to see what the source of NPC is + const sourceId = actor.flags.core.sourceId; + const coreSource = sourceId.substr(0, sourceId.length - 17); + const core_id = sourceId.substr(sourceId.length - 16, 16); + if (coreSource === "Compendium.sw5e.monsters") { + game.packs + .get("sw5e.monsters") + .getEntity(core_id) + .then((monster) => { + const monsterData = monster.data.data; + // copy movement[], senses[], powercasting, force[], tech[], powerForceLevel, powerTechLevel + updateData["data.attributes.movement"] = monsterData.attributes.movement; + updateData["data.attributes.senses"] = monsterData.attributes.senses; + updateData["data.attributes.powercasting"] = monsterData.attributes.powercasting; + updateData["data.attributes.force"] = monsterData.attributes.force; + updateData["data.attributes.tech"] = monsterData.attributes.tech; + updateData["data.details.powerForceLevel"] = monsterData.details.powerForceLevel; + updateData["data.details.powerTechLevel"] = monsterData.details.powerTechLevel; + // push missing powers onto actor + let newPowers = []; + for (let i of monster.items) { + const itemData = i.data; + if (itemData.type === "power") { + const itemCompendium_id = itemData.flags?.core?.sourceId.split(".").slice(-1)[0]; + let hasPower = !!actor.items.find( + (item) => item.flags?.core?.sourceId.split(".").slice(-1)[0] === itemCompendium_id + ); + if (!hasPower) { + // Clone power to new object. Don't know if it is technically needed, but seems to prevent some weirdness. + const newPower = JSON.parse(JSON.stringify(itemData)); - let actorData = actor.data; - const updateData = {}; - // check for flag.core, if not there is no compendium monster so exit - const hasSource = actor?.flags?.core?.sourceId !== undefined; - if (!hasSource) return actor; - // shortcut out if dataVersion flag is set to 1.2.4 or higher - const hasDataVersion = actor?.flags?.sw5e?.dataVersion !== undefined; - if (hasDataVersion && (actor.flags.sw5e.dataVersion === "1.2.4" || isNewerVersion("1.2.4", actor.flags.sw5e.dataVersion))) return actor; - // Check to see what the source of NPC is - const sourceId = actor.flags.core.sourceId; - const coreSource = sourceId.substr(0,sourceId.length-17); - const core_id = sourceId.substr(sourceId.length-16,16); - if (coreSource === "Compendium.sw5e.monsters"){ - game.packs.get("sw5e.monsters").getEntity(core_id).then(monster => { - const monsterData = monster.data.data; - // copy movement[], senses[], powercasting, force[], tech[], powerForceLevel, powerTechLevel - updateData["data.attributes.movement"] = monsterData.attributes.movement; - updateData["data.attributes.senses"] = monsterData.attributes.senses; - updateData["data.attributes.powercasting"] = monsterData.attributes.powercasting; - updateData["data.attributes.force"] = monsterData.attributes.force; - updateData["data.attributes.tech"] = monsterData.attributes.tech; - updateData["data.details.powerForceLevel"] = monsterData.details.powerForceLevel; - updateData["data.details.powerTechLevel"] = monsterData.details.powerTechLevel; - // push missing powers onto actor - let newPowers = []; - for ( let i of monster.items ) { - const itemData = i.data; - if ( itemData.type === "power" ) { - const itemCompendium_id = itemData.flags?.core?.sourceId.split(".").slice(-1)[0]; - let hasPower = !!actor.items.find(item => item.flags?.core?.sourceId.split(".").slice(-1)[0] === itemCompendium_id); - if (!hasPower) { - // Clone power to new object. Don't know if it is technically needed, but seems to prevent some weirdness. - const newPower = JSON.parse(JSON.stringify(itemData)); + newPowers.push(newPower); + } + } + } - newPowers.push(newPower); - } - } - } + // get actor to create new powers + const liveActor = game.actors.get(actor._id); + // create the powers on the actor + liveActor.createEmbeddedEntity("OwnedItem", newPowers); - // get actor to create new powers - const liveActor = game.actors.get(actor._id); - // create the powers on the actor - liveActor.createEmbeddedEntity("OwnedItem", newPowers); + // set flag to check to see if migration has been done so we don't do it again. + liveActor.setFlag("sw5e", "dataVersion", "1.2.4"); + }); + } - // set flag to check to see if migration has been done so we don't do it again. - liveActor.setFlag("sw5e", "dataVersion", "1.2.4"); - }) - } - - - //merge object - actorData = mergeObject(actorData, updateData); - // Return the scrubbed data - return actor; + //merge object + actorData = mergeObject(actorData, updateData); + // Return the scrubbed data + return actor; } - /** * Migrate the actor speed string to movement object * @private */ function _migrateActorMovement(actorData, updateData) { - const ad = actorData.data; + const ad = actorData.data; - // Work is needed if old data is present - const old = actorData.type === 'vehicle' ? ad?.attributes?.speed : ad?.attributes?.speed?.value; - const hasOld = old !== undefined; - if ( hasOld ) { + // Work is needed if old data is present + const old = actorData.type === "vehicle" ? ad?.attributes?.speed : ad?.attributes?.speed?.value; + const hasOld = old !== undefined; + if (hasOld) { + // If new data is not present, migrate the old data + const hasNew = ad?.attributes?.movement?.walk !== undefined; + if (!hasNew && typeof old === "string") { + const s = (old || "").split(" "); + if (s.length > 0) + updateData["data.attributes.movement.walk"] = Number.isNumeric(s[0]) ? parseInt(s[0]) : null; + } - // If new data is not present, migrate the old data - const hasNew = ad?.attributes?.movement?.walk !== undefined; - if ( !hasNew && (typeof old === "string") ) { - const s = (old || "").split(" "); - if ( s.length > 0 ) updateData["data.attributes.movement.walk"] = Number.isNumeric(s[0]) ? parseInt(s[0]) : null; + // Remove the old attribute + updateData["data.attributes.-=speed"] = null; } - - // Remove the old attribute - updateData["data.attributes.-=speed"] = null; - } - return updateData + return updateData; } /* -------------------------------------------- */ @@ -374,58 +380,58 @@ function _migrateActorMovement(actorData, updateData) { * @private */ function _migrateActorPowers(actorData, updateData) { - const ad = actorData.data; + const ad = actorData.data; - // If new Force & Tech data is not present, create it - let hasNewAttrib = ad?.attributes?.force?.level !== undefined; - if ( !hasNewAttrib ) { - updateData["data.attributes.force.known.value"] = 0; - updateData["data.attributes.force.known.max"] = 0; - updateData["data.attributes.force.points.value"] = 0; - updateData["data.attributes.force.points.min"] = 0; - updateData["data.attributes.force.points.max"] = 0; - updateData["data.attributes.force.points.temp"] = null; - updateData["data.attributes.force.points.tempmax"] = null; - updateData["data.attributes.force.level"] = 0; - updateData["data.attributes.tech.known.value"] = 0; - updateData["data.attributes.tech.known.max"] = 0; - updateData["data.attributes.tech.points.value"] = 0; - updateData["data.attributes.tech.points.min"] = 0; - updateData["data.attributes.tech.points.max"] = 0; - updateData["data.attributes.tech.points.temp"] = null; - updateData["data.attributes.tech.points.tempmax"] = null; - updateData["data.attributes.tech.level"] = 0; - } - - // If new Power F/T split data is not present, create it - const hasNewLimit = ad?.powers?.power1?.foverride !== undefined; - if ( !hasNewLimit ) { - for (let i = 1; i <= 9; i++) { - // add new - updateData["data.powers.power" + i + ".fvalue"] = getProperty(ad.powers,"power" + i + ".value"); - updateData["data.powers.power" + i + ".fmax"] = getProperty(ad.powers,"power" + i + ".max"); - updateData["data.powers.power" + i + ".foverride"] = null; - updateData["data.powers.power" + i + ".tvalue"] = getProperty(ad.powers,"power" + i + ".value"); - updateData["data.powers.power" + i + ".tmax"] = getProperty(ad.powers,"power" + i + ".max"); - updateData["data.powers.power" + i + ".toverride"] = null; - //remove old - updateData["data.powers.power" + i + ".-=value"] = null; - updateData["data.powers.power" + i + ".-=override"] = null; + // If new Force & Tech data is not present, create it + let hasNewAttrib = ad?.attributes?.force?.level !== undefined; + if (!hasNewAttrib) { + updateData["data.attributes.force.known.value"] = 0; + updateData["data.attributes.force.known.max"] = 0; + updateData["data.attributes.force.points.value"] = 0; + updateData["data.attributes.force.points.min"] = 0; + updateData["data.attributes.force.points.max"] = 0; + updateData["data.attributes.force.points.temp"] = null; + updateData["data.attributes.force.points.tempmax"] = null; + updateData["data.attributes.force.level"] = 0; + updateData["data.attributes.tech.known.value"] = 0; + updateData["data.attributes.tech.known.max"] = 0; + updateData["data.attributes.tech.points.value"] = 0; + updateData["data.attributes.tech.points.min"] = 0; + updateData["data.attributes.tech.points.max"] = 0; + updateData["data.attributes.tech.points.temp"] = null; + updateData["data.attributes.tech.points.tempmax"] = null; + updateData["data.attributes.tech.level"] = 0; } - } - // If new Bonus Power DC data is not present, create it - const hasNewBonus = ad?.bonuses?.power?.forceLightDC !== undefined; - if ( !hasNewBonus ) { - updateData["data.bonuses.power.forceLightDC"] = ""; - updateData["data.bonuses.power.forceDarkDC"] = ""; - updateData["data.bonuses.power.forceUnivDC"] = ""; - updateData["data.bonuses.power.techDC"] = ""; - } - // Remove the Power DC Bonus - updateData["data.bonuses.power.-=dc"] = null; + // If new Power F/T split data is not present, create it + const hasNewLimit = ad?.powers?.power1?.foverride !== undefined; + if (!hasNewLimit) { + for (let i = 1; i <= 9; i++) { + // add new + updateData["data.powers.power" + i + ".fvalue"] = getProperty(ad.powers, "power" + i + ".value"); + updateData["data.powers.power" + i + ".fmax"] = getProperty(ad.powers, "power" + i + ".max"); + updateData["data.powers.power" + i + ".foverride"] = null; + updateData["data.powers.power" + i + ".tvalue"] = getProperty(ad.powers, "power" + i + ".value"); + updateData["data.powers.power" + i + ".tmax"] = getProperty(ad.powers, "power" + i + ".max"); + updateData["data.powers.power" + i + ".toverride"] = null; + //remove old + updateData["data.powers.power" + i + ".-=value"] = null; + updateData["data.powers.power" + i + ".-=override"] = null; + } + } + // If new Bonus Power DC data is not present, create it + const hasNewBonus = ad?.bonuses?.power?.forceLightDC !== undefined; + if (!hasNewBonus) { + updateData["data.bonuses.power.forceLightDC"] = ""; + updateData["data.bonuses.power.forceDarkDC"] = ""; + updateData["data.bonuses.power.forceUnivDC"] = ""; + updateData["data.bonuses.power.techDC"] = ""; + } - return updateData + // Remove the Power DC Bonus + updateData["data.bonuses.power.-=dc"] = null; + + return updateData; } /* -------------------------------------------- */ @@ -435,35 +441,35 @@ function _migrateActorPowers(actorData, updateData) { * @private */ function _migrateActorSenses(actor, updateData) { - const ad = actor.data; - if ( ad?.traits?.senses === undefined ) return; - const original = ad.traits.senses || ""; - if ( typeof original !== "string" ) return; + const ad = actor.data; + if (ad?.traits?.senses === undefined) return; + const original = ad.traits.senses || ""; + if (typeof original !== "string") return; - // Try to match old senses with the format like "Darkvision 60 ft, Blindsight 30 ft" - const pattern = /([A-z]+)\s?([0-9]+)\s?([A-z]+)?/; - let wasMatched = false; + // Try to match old senses with the format like "Darkvision 60 ft, Blindsight 30 ft" + const pattern = /([A-z]+)\s?([0-9]+)\s?([A-z]+)?/; + let wasMatched = false; - // Match each comma-separated term - for ( let s of original.split(",") ) { - s = s.trim(); - const match = s.match(pattern); - if ( !match ) continue; - const type = match[1].toLowerCase(); - if ( type in CONFIG.SW5E.senses ) { - updateData[`data.attributes.senses.${type}`] = Number(match[2]).toNearest(0.5); - wasMatched = true; + // Match each comma-separated term + for (let s of original.split(",")) { + s = s.trim(); + const match = s.match(pattern); + if (!match) continue; + const type = match[1].toLowerCase(); + if (type in CONFIG.SW5E.senses) { + updateData[`data.attributes.senses.${type}`] = Number(match[2]).toNearest(0.5); + wasMatched = true; + } } - } - // If nothing was matched, but there was an old string - put the whole thing in "special" - if ( !wasMatched && !!original ) { - updateData["data.attributes.senses.special"] = original; - } + // If nothing was matched, but there was an old string - put the whole thing in "special" + if (!wasMatched && !!original) { + updateData["data.attributes.senses.special"] = original; + } - // Remove the old traits.senses string once the migration is complete - updateData["data.traits.-=senses"] = null; - return updateData; + // Remove the old traits.senses string once the migration is complete + updateData["data.traits.-=senses"] = null; + return updateData; } /* -------------------------------------------- */ @@ -473,76 +479,77 @@ function _migrateActorSenses(actor, updateData) { * @private */ function _migrateActorType(actor, updateData) { - const ad = actor.data; - const original = ad.details?.type; - if ( typeof original !== "string" ) return; + const ad = actor.data; + const original = ad.details?.type; + if (typeof original !== "string") return; - // New default data structure - let data = { - "value": "", - "subtype": "", - "swarm": "", - "custom": "" - } + // New default data structure + let data = { + value: "", + subtype: "", + swarm: "", + custom: "" + }; - // Specifics - // (Some of these have weird names, these need to be addressed individually) - if (original === "force entity") { - data.value = "force"; - data.subtype = "storm"; - } else if (original === "human") { - data.value = "humanoid"; - data.subtype = "human"; - } else if (["humanoid (any)", "humanoid (Villainous"].includes(original)) { - data.value = "humanoid"; - } else if (original === "tree") { - data.value = "plant"; - data.subtype = "tree"; - } else if (original === "(humanoid) or Large (beast) force entity") { - data.value = "force"; - } else if (original === "droid (appears human)") { - data.value = "droid"; - } else { - // Match the existing string - const pattern = /^(?:swarm of (?[\w\-]+) )?(?[^(]+?)(?:\((?[^)]+)\))?$/i; - const match = original.trim().match(pattern); - if (match) { + // Specifics + // (Some of these have weird names, these need to be addressed individually) + if (original === "force entity") { + data.value = "force"; + data.subtype = "storm"; + } else if (original === "human") { + data.value = "humanoid"; + data.subtype = "human"; + } else if (["humanoid (any)", "humanoid (Villainous"].includes(original)) { + data.value = "humanoid"; + } else if (original === "tree") { + data.value = "plant"; + data.subtype = "tree"; + } else if (original === "(humanoid) or Large (beast) force entity") { + data.value = "force"; + } else if (original === "droid (appears human)") { + data.value = "droid"; + } else { + // Match the existing string + const pattern = /^(?:swarm of (?[\w\-]+) )?(?[^(]+?)(?:\((?[^)]+)\))?$/i; + const match = original.trim().match(pattern); + if (match) { + // Match a known creature type + const typeLc = match.groups.type.trim().toLowerCase(); + const typeMatch = Object.entries(CONFIG.SW5E.creatureTypes).find(([k, v]) => { + return ( + typeLc === k || + typeLc === game.i18n.localize(v).toLowerCase() || + typeLc === game.i18n.localize(`${v}Pl`).toLowerCase() + ); + }); + if (typeMatch) data.value = typeMatch[0]; + else { + data.value = "custom"; + data.custom = match.groups.type.trim().titleCase(); + } + data.subtype = match.groups.subtype?.trim().titleCase() || ""; - // Match a known creature type - const typeLc = match.groups.type.trim().toLowerCase(); - const typeMatch = Object.entries(CONFIG.SW5E.creatureTypes).find(([k, v]) => { - return (typeLc === k) || - (typeLc === game.i18n.localize(v).toLowerCase()) || - (typeLc === game.i18n.localize(`${v}Pl`).toLowerCase()); - }); - if (typeMatch) data.value = typeMatch[0]; - else { - data.value = "custom"; - data.custom = match.groups.type.trim().titleCase(); - } - data.subtype = match.groups.subtype?.trim().titleCase() || ""; + // Match a swarm + const isNamedSwarm = actor.name.startsWith(game.i18n.localize("SW5E.CreatureSwarm")); + if (match.groups.size || isNamedSwarm) { + const sizeLc = match.groups.size ? match.groups.size.trim().toLowerCase() : "tiny"; + const sizeMatch = Object.entries(CONFIG.SW5E.actorSizes).find(([k, v]) => { + return sizeLc === k || sizeLc === game.i18n.localize(v).toLowerCase(); + }); + data.swarm = sizeMatch ? sizeMatch[0] : "tiny"; + } else data.swarm = ""; + } - // Match a swarm - const isNamedSwarm = actor.name.startsWith(game.i18n.localize("SW5E.CreatureSwarm")); - if (match.groups.size || isNamedSwarm) { - const sizeLc = match.groups.size ? match.groups.size.trim().toLowerCase() : "tiny"; - const sizeMatch = Object.entries(CONFIG.SW5E.actorSizes).find(([k, v]) => { - return (sizeLc === k) || (sizeLc === game.i18n.localize(v).toLowerCase()); - }); - data.swarm = sizeMatch ? sizeMatch[0] : "tiny"; - } else data.swarm = ""; + // No match found + else { + data.value = "custom"; + data.custom = original; + } } - // No match found - else { - data.value = "custom"; - data.custom = original; - } - } - - // Update the actor data - updateData["data.details.type"] = data; - return updateData; + // Update the actor data + updateData["data.details.type"] = data; + return updateData; } /* -------------------------------------------- */ @@ -551,42 +558,41 @@ function _migrateActorType(actor, updateData) { * @private */ function _migrateItemClassPowerCasting(item, updateData) { - if (item.type === "class"){ - switch (item.name){ - case "Consular": - updateData["data.powercasting"] = { - progression: "consular", - ability: "" - }; - break; - case "Engineer": - - updateData["data.powercasting"] = { - progression: "engineer", - ability: "" - }; - break; - case "Guardian": - updateData["data.powercasting"] = { - progression: "guardian", - ability: "" - }; - break; - case "Scout": - updateData["data.powercasting"] = { - progression: "scout", - ability: "" - }; - break; - case "Sentinel": - updateData["data.powercasting"] = { - progression: "sentinel", - ability: "" - }; - break; + if (item.type === "class") { + switch (item.name) { + case "Consular": + updateData["data.powercasting"] = { + progression: "consular", + ability: "" + }; + break; + case "Engineer": + updateData["data.powercasting"] = { + progression: "engineer", + ability: "" + }; + break; + case "Guardian": + updateData["data.powercasting"] = { + progression: "guardian", + ability: "" + }; + break; + case "Scout": + updateData["data.powercasting"] = { + progression: "scout", + ability: "" + }; + break; + case "Sentinel": + updateData["data.powercasting"] = { + progression: "sentinel", + ability: "" + }; + break; + } } - } - return updateData; + return updateData; } /* -------------------------------------------- */ @@ -598,42 +604,45 @@ function _migrateItemClassPowerCasting(item, updateData) { * @private */ async function _migrateItemPower(item, actor, updateData) { - // if item is not a power shortcut out - if (item.type !== "power") return updateData; + // if item is not a power shortcut out + if (item.type !== "power") return updateData; - console.log(`Checking Actor ${actor.name}'s ${item.name} for migration needs`); - // check for flag.core, if not there is no compendium power so exit - const hasSource = item?.flags?.core?.sourceId !== undefined; - if (!hasSource) return updateData; + console.log(`Checking Actor ${actor.name}'s ${item.name} for migration needs`); + // check for flag.core, if not there is no compendium power so exit + const hasSource = item?.flags?.core?.sourceId !== undefined; + if (!hasSource) return updateData; - // shortcut out if dataVersion flag is set to 1.2.4 or higher - const hasDataVersion = item?.flags?.sw5e?.dataVersion !== undefined; - if (hasDataVersion && (item.flags.sw5e.dataVersion === "1.2.4" || isNewerVersion("1.2.4", item.flags.sw5e.dataVersion))) return updateData; - - // Check to see what the source of Power is - const sourceId = item.flags.core.sourceId; - const coreSource = sourceId.substr(0, sourceId.length - 17); - const core_id = sourceId.substr(sourceId.length - 16, 16); - - //if power type is not force or tech exit out - let powerType = "none"; - if (coreSource === "Compendium.sw5e.forcepowers") powerType = "sw5e.forcepowers"; - if (coreSource === "Compendium.sw5e.techpowers") powerType = "sw5e.techpowers"; - if (powerType === "none") return updateData; + // shortcut out if dataVersion flag is set to 1.2.4 or higher + const hasDataVersion = item?.flags?.sw5e?.dataVersion !== undefined; + if ( + hasDataVersion && + (item.flags.sw5e.dataVersion === "1.2.4" || isNewerVersion("1.2.4", item.flags.sw5e.dataVersion)) + ) + return updateData; + + // Check to see what the source of Power is + const sourceId = item.flags.core.sourceId; + const coreSource = sourceId.substr(0, sourceId.length - 17); + const core_id = sourceId.substr(sourceId.length - 16, 16); + + //if power type is not force or tech exit out + let powerType = "none"; + if (coreSource === "Compendium.sw5e.forcepowers") powerType = "sw5e.forcepowers"; + if (coreSource === "Compendium.sw5e.techpowers") powerType = "sw5e.techpowers"; + if (powerType === "none") return updateData; const corePower = duplicate(await game.packs.get(powerType).getEntity(core_id)); console.log(`Updating Actor ${actor.name}'s ${item.name} from compendium`); const corePowerData = corePower.data; // copy Core Power Data over original Power updateData["data"] = corePowerData; - updateData["flags"] = {"sw5e": {"dataVersion": "1.2.4"}}; + updateData["flags"] = {sw5e: {dataVersion: "1.2.4"}}; return updateData; - - - //game.packs.get(powerType).getEntity(core_id).then(corePower => { - //}) + //game.packs.get(powerType).getEntity(core_id).then(corePower => { + + //}) } /* -------------------------------------------- */ @@ -647,10 +656,10 @@ async function _migrateItemPower(item, actor, updateData) { * @private */ function _migrateItemAttunement(item, updateData) { - if ( item.data?.attuned === undefined ) return updateData; - updateData["data.attunement"] = CONFIG.SW5E.attunementTypes.NONE; - updateData["data.-=attuned"] = null; - return updateData; + if (item.data?.attuned === undefined) return updateData; + updateData["data.attunement"] = CONFIG.SW5E.attunementTypes.NONE; + updateData["data.-=attuned"] = null; + return updateData; } /* -------------------------------------------- */ @@ -661,43 +670,41 @@ function _migrateItemAttunement(item, updateData) { * @private */ export async function purgeFlags(pack) { - const cleanFlags = (flags) => { - const flags5e = flags.sw5e || null; - return flags5e ? {sw5e: flags5e} : {}; - }; - await pack.configure({locked: false}); - const content = await pack.getContent(); - for ( let entity of content ) { - const update = {_id: entity.id, flags: cleanFlags(entity.data.flags)}; - if ( pack.entity === "Actor" ) { - update.items = entity.data.items.map(i => { - i.flags = cleanFlags(i.flags); - return i; - }) + const cleanFlags = (flags) => { + const flags5e = flags.sw5e || null; + return flags5e ? {sw5e: flags5e} : {}; + }; + await pack.configure({locked: false}); + const content = await pack.getContent(); + for (let entity of content) { + const update = {_id: entity.id, flags: cleanFlags(entity.data.flags)}; + if (pack.entity === "Actor") { + update.items = entity.data.items.map((i) => { + i.flags = cleanFlags(i.flags); + return i; + }); + } + await pack.updateEntity(update, {recursive: false}); + console.log(`Purged flags from ${entity.name}`); } - await pack.updateEntity(update, {recursive: false}); - console.log(`Purged flags from ${entity.name}`); - } - await pack.configure({locked: true}); + await pack.configure({locked: true}); } /* -------------------------------------------- */ - /** * Purge the data model of any inner objects which have been flagged as _deprecated. * @param {object} data The data to clean * @private */ export function removeDeprecatedObjects(data) { - for ( let [k, v] of Object.entries(data) ) { - if ( getType(v) === "Object" ) { - if (v._deprecated === true) { - console.log(`Deleting deprecated object key ${k}`); - delete data[k]; - } - else removeDeprecatedObjects(v); + for (let [k, v] of Object.entries(data)) { + if (getType(v) === "Object") { + if (v._deprecated === true) { + console.log(`Deleting deprecated object key ${k}`); + delete data[k]; + } else removeDeprecatedObjects(v); + } } - } - return data; + return data; } diff --git a/module/pixi/ability-template.js b/module/pixi/ability-template.js index 3af99181..6799eaec 100644 --- a/module/pixi/ability-template.js +++ b/module/pixi/ability-template.js @@ -1,133 +1,132 @@ -import { SW5E } from "../config.js"; +import {SW5E} from "../config.js"; /** * A helper class for building MeasuredTemplates for 5e powers and abilities * @extends {MeasuredTemplate} */ export default class AbilityTemplate extends MeasuredTemplate { + /** + * A factory method to create an AbilityTemplate instance using provided data from an Item5e instance + * @param {Item5e} item The Item object for which to construct the template + * @return {AbilityTemplate|null} The template object, or null if the item does not produce a template + */ + static fromItem(item) { + const target = getProperty(item.data, "data.target") || {}; + const templateShape = SW5E.areaTargetTypes[target.type]; + if (!templateShape) return null; - /** - * A factory method to create an AbilityTemplate instance using provided data from an Item5e instance - * @param {Item5e} item The Item object for which to construct the template - * @return {AbilityTemplate|null} The template object, or null if the item does not produce a template - */ - static fromItem(item) { - const target = getProperty(item.data, "data.target") || {}; - const templateShape = SW5E.areaTargetTypes[target.type]; - if ( !templateShape ) return null; + // Prepare template data + const templateData = { + t: templateShape, + user: game.user.data._id, + distance: target.value, + direction: 0, + x: 0, + y: 0, + fillColor: game.user.color + }; - // Prepare template data - const templateData = { - t: templateShape, - user: game.user.data._id, - distance: target.value, - direction: 0, - x: 0, - y: 0, - fillColor: game.user.color - }; + // Additional type-specific data + switch (templateShape) { + case "cone": + templateData.angle = CONFIG.MeasuredTemplate.defaults.angle; + break; + case "rect": // 5e rectangular AoEs are always cubes + templateData.distance = Math.hypot(target.value, target.value); + templateData.width = target.value; + templateData.direction = 45; + break; + case "ray": // 5e rays are most commonly 1 square (5 ft) in width + templateData.width = target.width ?? canvas.dimensions.distance; + break; + default: + break; + } - // Additional type-specific data - switch ( templateShape ) { - case "cone": - templateData.angle = CONFIG.MeasuredTemplate.defaults.angle; - break; - case "rect": // 5e rectangular AoEs are always cubes - templateData.distance = Math.hypot(target.value, target.value); - templateData.width = target.value; - templateData.direction = 45; - break; - case "ray": // 5e rays are most commonly 1 square (5 ft) in width - templateData.width = target.width ?? canvas.dimensions.distance; - break; - default: - break; + // Return the template constructed from the item data + const cls = CONFIG.MeasuredTemplate.documentClass; + const template = new cls(templateData, {parent: canvas.scene}); + const object = new this(template); + object.item = item; + object.actorSheet = item.actor?.sheet || null; + return object; } - // Return the template constructed from the item data - const cls = CONFIG.MeasuredTemplate.documentClass; - const template = new cls(templateData, {parent: canvas.scene}); - const object = new this(template); - object.item = item; - object.actorSheet = item.actor?.sheet || null; - return object; - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** + * Creates a preview of the power template + */ + drawPreview() { + const initialLayer = canvas.activeLayer; - /** - * Creates a preview of the power template - */ - drawPreview() { - const initialLayer = canvas.activeLayer; + // Draw the template and switch to the template layer + this.draw(); + this.layer.activate(); + this.layer.preview.addChild(this); - // Draw the template and switch to the template layer - this.draw(); - this.layer.activate(); - this.layer.preview.addChild(this); + // Hide the sheet that originated the preview + if (this.actorSheet) this.actorSheet.minimize(); - // Hide the sheet that originated the preview - if ( this.actorSheet ) this.actorSheet.minimize(); + // Activate interactivity + this.activatePreviewListeners(initialLayer); + } - // Activate interactivity - this.activatePreviewListeners(initialLayer); - } + /* -------------------------------------------- */ - /* -------------------------------------------- */ + /** + * Activate listeners for the template preview + * @param {CanvasLayer} initialLayer The initially active CanvasLayer to re-activate after the workflow is complete + */ + activatePreviewListeners(initialLayer) { + const handlers = {}; + let moveTime = 0; - /** - * Activate listeners for the template preview - * @param {CanvasLayer} initialLayer The initially active CanvasLayer to re-activate after the workflow is complete - */ - activatePreviewListeners(initialLayer) { - const handlers = {}; - let moveTime = 0; + // Update placement (mouse-move) + handlers.mm = (event) => { + event.stopPropagation(); + let now = Date.now(); // Apply a 20ms throttle + if (now - moveTime <= 20) return; + const center = event.data.getLocalPosition(this.layer); + const snapped = canvas.grid.getSnappedPosition(center.x, center.y, 2); + this.data.update({x: snapped.x, y: snapped.y}); + this.refresh(); + moveTime = now; + }; - // Update placement (mouse-move) - handlers.mm = event => { - event.stopPropagation(); - let now = Date.now(); // Apply a 20ms throttle - if ( now - moveTime <= 20 ) return; - const center = event.data.getLocalPosition(this.layer); - const snapped = canvas.grid.getSnappedPosition(center.x, center.y, 2); - this.data.update({x: snapped.x, y: snapped.y}); - this.refresh(); - moveTime = now; - }; + // Cancel the workflow (right-click) + handlers.rc = (event) => { + this.layer.preview.removeChildren(); + canvas.stage.off("mousemove", handlers.mm); + canvas.stage.off("mousedown", handlers.lc); + canvas.app.view.oncontextmenu = null; + canvas.app.view.onwheel = null; + initialLayer.activate(); + this.actorSheet.maximize(); + }; - // Cancel the workflow (right-click) - handlers.rc = event => { - this.layer.preview.removeChildren(); - canvas.stage.off("mousemove", handlers.mm); - canvas.stage.off("mousedown", handlers.lc); - canvas.app.view.oncontextmenu = null; - canvas.app.view.onwheel = null; - initialLayer.activate(); - this.actorSheet.maximize(); - }; + // Confirm the workflow (left-click) + handlers.lc = (event) => { + handlers.rc(event); + const destination = canvas.grid.getSnappedPosition(this.data.x, this.data.y, 2); + this.data.update(destination); + canvas.scene.createEmbeddedDocuments("MeasuredTemplate", [this.data]); + }; - // Confirm the workflow (left-click) - handlers.lc = event => { - handlers.rc(event); - const destination = canvas.grid.getSnappedPosition(this.data.x, this.data.y, 2); - this.data.update(destination); - canvas.scene.createEmbeddedDocuments("MeasuredTemplate", [this.data]); - }; + // Rotate the template by 3 degree increments (mouse-wheel) + handlers.mw = (event) => { + if (event.ctrlKey) event.preventDefault(); // Avoid zooming the browser window + event.stopPropagation(); + let delta = canvas.grid.type > CONST.GRID_TYPES.SQUARE ? 30 : 15; + let snap = event.shiftKey ? delta : 5; + this.data.update({direction: this.data.direction + snap * Math.sign(event.deltaY)}); + this.refresh(); + }; - // Rotate the template by 3 degree increments (mouse-wheel) - handlers.mw = event => { - if ( event.ctrlKey ) event.preventDefault(); // Avoid zooming the browser window - event.stopPropagation(); - let delta = canvas.grid.type > CONST.GRID_TYPES.SQUARE ? 30 : 15; - let snap = event.shiftKey ? delta : 5; - this.data.update({direction: this.data.direction + (snap * Math.sign(event.deltaY))}); - this.refresh(); - }; - - // Activate listeners - canvas.stage.on("mousemove", handlers.mm); - canvas.stage.on("mousedown", handlers.lc); - canvas.app.view.oncontextmenu = handlers.rc; - canvas.app.view.onwheel = handlers.mw; - } + // Activate listeners + canvas.stage.on("mousemove", handlers.mm); + canvas.stage.on("mousedown", handlers.lc); + canvas.app.view.oncontextmenu = handlers.rc; + canvas.app.view.onwheel = handlers.mw; + } } diff --git a/module/settings.js b/module/settings.js index 2adde8d3..3b765c69 100644 --- a/module/settings.js +++ b/module/settings.js @@ -1,145 +1,144 @@ -export const registerSystemSettings = function() { +export const registerSystemSettings = function () { + /** + * Track the system version upon which point a migration was last applied + */ + game.settings.register("sw5e", "systemMigrationVersion", { + name: "System Migration Version", + scope: "world", + config: false, + type: String, + default: game.system.data.version + }); - /** - * Track the system version upon which point a migration was last applied - */ - game.settings.register("sw5e", "systemMigrationVersion", { - name: "System Migration Version", - scope: "world", - config: false, - type: String, - default: game.system.data.version - }); + /** + * Register resting variants + */ + game.settings.register("sw5e", "restVariant", { + name: "SETTINGS.5eRestN", + hint: "SETTINGS.5eRestL", + scope: "world", + config: true, + default: "normal", + type: String, + choices: { + normal: "SETTINGS.5eRestPHB", + gritty: "SETTINGS.5eRestGritty", + epic: "SETTINGS.5eRestEpic" + } + }); - /** - * Register resting variants - */ - game.settings.register("sw5e", "restVariant", { - name: "SETTINGS.5eRestN", - hint: "SETTINGS.5eRestL", - scope: "world", - config: true, - default: "normal", - type: String, - choices: { - "normal": "SETTINGS.5eRestPHB", - "gritty": "SETTINGS.5eRestGritty", - "epic": "SETTINGS.5eRestEpic", - } - }); + /** + * Register diagonal movement rule setting + */ + game.settings.register("sw5e", "diagonalMovement", { + name: "SETTINGS.5eDiagN", + hint: "SETTINGS.5eDiagL", + scope: "world", + config: true, + default: "555", + type: String, + choices: { + 555: "SETTINGS.5eDiagPHB", + 5105: "SETTINGS.5eDiagDMG", + EUCL: "SETTINGS.5eDiagEuclidean" + }, + onChange: (rule) => (canvas.grid.diagonalRule = rule) + }); - /** - * Register diagonal movement rule setting - */ - game.settings.register("sw5e", "diagonalMovement", { - name: "SETTINGS.5eDiagN", - hint: "SETTINGS.5eDiagL", - scope: "world", - config: true, - default: "555", - type: String, - choices: { - "555": "SETTINGS.5eDiagPHB", - "5105": "SETTINGS.5eDiagDMG", - "EUCL": "SETTINGS.5eDiagEuclidean", - }, - onChange: rule => canvas.grid.diagonalRule = rule - }); + /** + * Register Initiative formula setting + */ + game.settings.register("sw5e", "initiativeDexTiebreaker", { + name: "SETTINGS.5eInitTBN", + hint: "SETTINGS.5eInitTBL", + scope: "world", + config: true, + default: false, + type: Boolean + }); - /** - * Register Initiative formula setting - */ - game.settings.register("sw5e", "initiativeDexTiebreaker", { - name: "SETTINGS.5eInitTBN", - hint: "SETTINGS.5eInitTBL", - scope: "world", - config: true, - default: false, - type: Boolean - }); + /** + * Require Currency Carrying Weight + */ + game.settings.register("sw5e", "currencyWeight", { + name: "SETTINGS.5eCurWtN", + hint: "SETTINGS.5eCurWtL", + scope: "world", + config: true, + default: true, + type: Boolean + }); - /** - * Require Currency Carrying Weight - */ - game.settings.register("sw5e", "currencyWeight", { - name: "SETTINGS.5eCurWtN", - hint: "SETTINGS.5eCurWtL", - scope: "world", - config: true, - default: true, - type: Boolean - }); + /** + * Option to disable XP bar for session-based or story-based advancement. + */ + game.settings.register("sw5e", "disableExperienceTracking", { + name: "SETTINGS.5eNoExpN", + hint: "SETTINGS.5eNoExpL", + scope: "world", + config: true, + default: false, + type: Boolean + }); - /** - * Option to disable XP bar for session-based or story-based advancement. - */ - game.settings.register("sw5e", "disableExperienceTracking", { - name: "SETTINGS.5eNoExpN", - hint: "SETTINGS.5eNoExpL", - scope: "world", - config: true, - default: false, - type: Boolean, - }); + /** + * Option to automatically collapse Item Card descriptions + */ + game.settings.register("sw5e", "autoCollapseItemCards", { + name: "SETTINGS.5eAutoCollapseCardN", + hint: "SETTINGS.5eAutoCollapseCardL", + scope: "client", + config: true, + default: false, + type: Boolean, + onChange: (s) => { + ui.chat.render(); + } + }); - /** - * Option to automatically collapse Item Card descriptions - */ - game.settings.register("sw5e", "autoCollapseItemCards", { - name: "SETTINGS.5eAutoCollapseCardN", - hint: "SETTINGS.5eAutoCollapseCardL", - scope: "client", - config: true, - default: false, - type: Boolean, - onChange: s => { - ui.chat.render(); - } - }); + /** + * Option to allow GMs to restrict polymorphing to GMs only. + */ + game.settings.register("sw5e", "allowPolymorphing", { + name: "SETTINGS.5eAllowPolymorphingN", + hint: "SETTINGS.5eAllowPolymorphingL", + scope: "world", + config: true, + default: false, + type: Boolean + }); - /** - * Option to allow GMs to restrict polymorphing to GMs only. - */ - game.settings.register('sw5e', 'allowPolymorphing', { - name: 'SETTINGS.5eAllowPolymorphingN', - hint: 'SETTINGS.5eAllowPolymorphingL', - scope: 'world', - config: true, - default: false, - type: Boolean - }); - - /** - * Remember last-used polymorph settings. - */ - game.settings.register('sw5e', 'polymorphSettings', { - scope: 'client', - default: { - keepPhysical: false, - keepMental: false, - keepSaves: false, - keepSkills: false, - mergeSaves: false, - mergeSkills: false, - keepClass: false, - keepFeats: false, - keepPowers: false, - keepItems: false, - keepBio: false, - keepVision: true, - transformTokens: true - } - }); - game.settings.register("sw5e", "colorTheme", { - name: "SETTINGS.SWColorN", - hint: "SETTINGS.SWColorL", - scope: "world", - config: true, - default: "light", - type: String, - choices: { - "light": "SETTINGS.SWColorLight", - "dark": "SETTINGS.SWColorDark" - } - }); + /** + * Remember last-used polymorph settings. + */ + game.settings.register("sw5e", "polymorphSettings", { + scope: "client", + default: { + keepPhysical: false, + keepMental: false, + keepSaves: false, + keepSkills: false, + mergeSaves: false, + mergeSkills: false, + keepClass: false, + keepFeats: false, + keepPowers: false, + keepItems: false, + keepBio: false, + keepVision: true, + transformTokens: true + } + }); + game.settings.register("sw5e", "colorTheme", { + name: "SETTINGS.SWColorN", + hint: "SETTINGS.SWColorL", + scope: "world", + config: true, + default: "light", + type: String, + choices: { + light: "SETTINGS.SWColorLight", + dark: "SETTINGS.SWColorDark" + } + }); }; diff --git a/module/templates.js b/module/templates.js index a27bdf71..e810b6da 100644 --- a/module/templates.js +++ b/module/templates.js @@ -3,34 +3,33 @@ * Pre-loaded templates are compiled and cached for fast access when rendering * @return {Promise} */ -export const preloadHandlebarsTemplates = async function() { - return loadTemplates([ +export const preloadHandlebarsTemplates = async function () { + return loadTemplates([ + // Shared Partials + "systems/sw5e/templates/actors/parts/active-effects.html", - // Shared Partials - "systems/sw5e/templates/actors/parts/active-effects.html", + // Actor Sheet Partials + "systems/sw5e/templates/actors/oldActor/parts/actor-traits.html", + "systems/sw5e/templates/actors/oldActor/parts/actor-inventory.html", + "systems/sw5e/templates/actors/oldActor/parts/actor-features.html", + "systems/sw5e/templates/actors/oldActor/parts/actor-powerbook.html", + "systems/sw5e/templates/actors/oldActor/parts/actor-notes.html", - // Actor Sheet Partials - "systems/sw5e/templates/actors/oldActor/parts/actor-traits.html", - "systems/sw5e/templates/actors/oldActor/parts/actor-inventory.html", - "systems/sw5e/templates/actors/oldActor/parts/actor-features.html", - "systems/sw5e/templates/actors/oldActor/parts/actor-powerbook.html", - "systems/sw5e/templates/actors/oldActor/parts/actor-notes.html", + "systems/sw5e/templates/actors/newActor/parts/swalt-biography.html", + "systems/sw5e/templates/actors/newActor/parts/swalt-core.html", + "systems/sw5e/templates/actors/newActor/parts/swalt-crew.html", + "systems/sw5e/templates/actors/newActor/parts/swalt-active-effects.html", + "systems/sw5e/templates/actors/newActor/parts/swalt-features.html", + "systems/sw5e/templates/actors/newActor/parts/swalt-inventory.html", + "systems/sw5e/templates/actors/newActor/parts/swalt-force-powerbook.html", + "systems/sw5e/templates/actors/newActor/parts/swalt-tech-powerbook.html", + "systems/sw5e/templates/actors/newActor/parts/swalt-resources.html", + "systems/sw5e/templates/actors/newActor/parts/swalt-traits.html", - "systems/sw5e/templates/actors/newActor/parts/swalt-biography.html", - "systems/sw5e/templates/actors/newActor/parts/swalt-core.html", - "systems/sw5e/templates/actors/newActor/parts/swalt-crew.html", - "systems/sw5e/templates/actors/newActor/parts/swalt-active-effects.html", - "systems/sw5e/templates/actors/newActor/parts/swalt-features.html", - "systems/sw5e/templates/actors/newActor/parts/swalt-inventory.html", - "systems/sw5e/templates/actors/newActor/parts/swalt-force-powerbook.html", - "systems/sw5e/templates/actors/newActor/parts/swalt-tech-powerbook.html", - "systems/sw5e/templates/actors/newActor/parts/swalt-resources.html", - "systems/sw5e/templates/actors/newActor/parts/swalt-traits.html", - - // Item Sheet Partials - "systems/sw5e/templates/items/parts/item-action.html", - "systems/sw5e/templates/items/parts/item-activation.html", - "systems/sw5e/templates/items/parts/item-description.html", - "systems/sw5e/templates/items/parts/item-mountable.html" - ]); + // Item Sheet Partials + "systems/sw5e/templates/items/parts/item-action.html", + "systems/sw5e/templates/items/parts/item-activation.html", + "systems/sw5e/templates/items/parts/item-description.html", + "systems/sw5e/templates/items/parts/item-mountable.html" + ]); }; diff --git a/module/token.js b/module/token.js index db811603..873372cc 100644 --- a/module/token.js +++ b/module/token.js @@ -3,11 +3,10 @@ * @extends {TokenDocument} */ export class TokenDocument5e extends TokenDocument { - /** @inheritdoc */ getBarAttribute(...args) { const data = super.getBarAttribute(...args); - if ( data && (data.attribute === "attributes.hp") ) { + if (data && data.attribute === "attributes.hp") { data.value += parseInt(getProperty(this.actor.data, "data.attributes.hp.temp") || 0); data.max += parseInt(getProperty(this.actor.data, "data.attributes.hp.tempmax") || 0); } @@ -15,19 +14,16 @@ export class TokenDocument5e extends TokenDocument { } } - /* -------------------------------------------- */ - /** * Extend the base Token class to implement additional system-specific logic. * @extends {Token} */ export class Token5e extends Token { - /** @inheritdoc */ _drawBar(number, bar, data) { - if ( data.attribute === "attributes.hp" ) return this._drawHPBar(number, bar, data); + if (data.attribute === "attributes.hp") return this._drawHPBar(number, bar, data); return super._drawBar(number, bar, data); } @@ -41,7 +37,6 @@ export class Token5e extends Token { * @private */ _drawHPBar(number, bar, data) { - // Extract health data let {value, max, temp, tempmax} = this.document.actor.data.data.attributes.hp; temp = Number(temp || 0); @@ -58,42 +53,50 @@ export class Token5e extends Token { // Determine colors to use const blk = 0x000000; - const hpColor = PIXI.utils.rgb2hex([(1-(colorPct/2)), colorPct, 0]); + const hpColor = PIXI.utils.rgb2hex([1 - colorPct / 2, colorPct, 0]); const c = CONFIG.SW5E.tokenHPColors; // Determine the container size (logic borrowed from core) const w = this.w; - let h = Math.max((canvas.dimensions.size / 12), 8); - if ( this.data.height >= 2 ) h *= 1.6; + let h = Math.max(canvas.dimensions.size / 12, 8); + if (this.data.height >= 2) h *= 1.6; const bs = Math.clamped(h / 8, 1, 2); - const bs1 = bs+1; + const bs1 = bs + 1; // Overall bar container - bar.clear() + bar.clear(); bar.beginFill(blk, 0.5).lineStyle(bs, blk, 1.0).drawRoundedRect(0, 0, w, h, 3); // Temporary maximum HP if (tempmax > 0) { const pct = max / effectiveMax; - bar.beginFill(c.tempmax, 1.0).lineStyle(1, blk, 1.0).drawRoundedRect(pct*w, 0, (1-pct)*w, h, 2); + bar.beginFill(c.tempmax, 1.0) + .lineStyle(1, blk, 1.0) + .drawRoundedRect(pct * w, 0, (1 - pct) * w, h, 2); } // Maximum HP penalty else if (tempmax < 0) { const pct = (max + tempmax) / max; - bar.beginFill(c.negmax, 1.0).lineStyle(1, blk, 1.0).drawRoundedRect(pct*w, 0, (1-pct)*w, h, 2); + bar.beginFill(c.negmax, 1.0) + .lineStyle(1, blk, 1.0) + .drawRoundedRect(pct * w, 0, (1 - pct) * w, h, 2); } // Health bar - bar.beginFill(hpColor, 1.0).lineStyle(bs, blk, 1.0).drawRoundedRect(0, 0, valuePct*w, h, 2) + bar.beginFill(hpColor, 1.0) + .lineStyle(bs, blk, 1.0) + .drawRoundedRect(0, 0, valuePct * w, h, 2); // Temporary hit points - if ( temp > 0 ) { - bar.beginFill(c.temp, 1.0).lineStyle(0).drawRoundedRect(bs1, bs1, (tempPct*w)-(2*bs1), h-(2*bs1), 1); + if (temp > 0) { + bar.beginFill(c.temp, 1.0) + .lineStyle(0) + .drawRoundedRect(bs1, bs1, tempPct * w - 2 * bs1, h - 2 * bs1, 1); } // Set position - let posY = (number === 0) ? (this.h - h) : 0; + let posY = number === 0 ? this.h - h : 0; bar.position.set(0, posY); } } diff --git a/package-lock.json b/package-lock.json index 759e9b50..b2fbf9ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1266,9 +1266,9 @@ } }, "hosted-git-info": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", - "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==" + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" }, "image-size": { "version": "0.5.5", @@ -3068,9 +3068,9 @@ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" }, "y18n": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", - "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz", + "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==" }, "yargs": { "version": "7.1.1", diff --git a/packs/packs/adventuringgear.db b/packs/packs/adventuringgear.db index e9b74e5a..1e4f8991 100644 --- a/packs/packs/adventuringgear.db +++ b/packs/packs/adventuringgear.db @@ -52,7 +52,7 @@ {"name":"Traz","permission":{"default":0,"vXYkFWX6qzvOu2jc":3},"type":"tool","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":6,"price":300,"attuned":false,"equipped":false,"rarity":"","identified":true,"ability":"int","chatFlavor":"","proficient":0,"attributes":{"spelldc":10},"damage":{"parts":[]}},"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false,"effects":[]}},"img":"systems/sw5e/packs/Icons/Musical%20Instrument/Traz.webp","_id":"UQu4duMtxYEXKAbo"} {"name":"Tent, two-person","permission":{"default":0,"vXYkFWX6qzvOu2jc":3},"type":"loot","data":{"description":{"value":"","chat":"","unidentified":""},"source":"","quantity":1,"weight":5,"price":20,"attuned":false,"equipped":false,"rarity":"","identified":true,"attributes":{"spelldc":10},"damage":{"parts":[]}},"flags":{},"img":"systems/sw5e/packs/Icons/Utility/Tent.webp","_id":"UxL0trd3omeqzBk4"} {"name":"Homing Beacon","permission":{"default":0,"vXYkFWX6qzvOu2jc":3},"type":"loot","data":{"description":{"value":"

A homing beacon is a device used to track starships or any other entity being transported. Homing beacons transmit using non-mass HoloNet transceivers able to be tracked through hyperspace. Homing beacons are small enough that they can easily be hidden inside a ship, or tucked into some crevice on its exterior.

","chat":"","unidentified":""},"source":"","quantity":1,"weight":1,"price":450,"attuned":false,"equipped":false,"rarity":"","identified":true,"attributes":{"spelldc":10},"damage":{"parts":[]}},"flags":{},"img":"systems/sw5e/packs/Icons/Utility/Homing%20Beacon.webp","_id":"V2hSxkLfq461mvNz"} -{"name":"Power Cell","permission":{"default":0,"vXYkFWX6qzvOu2jc":3},"type":"consumable","data":{"description":{"value":"

Power cells fuel blaster weapons that deal energy or ion damage. Additionally, power cells are used to energize certain tools.

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":1,"price":10,"attuned":false,"equipped":false,"rarity":"","identified":true,"activation":{"type":"none","cost":null,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"width":null,"units":"","type":""},"range":{"value":null,"long":null,"units":""},"uses":{"value":100,"max":"100","per":"charges","autoDestroy":false},"consume":{"type":"","target":"","amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"spell"},"consumableType":"ammo","attributes":{"spelldc":10}},"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false,"effects":[]}},"img":"systems/sw5e/packs/Icons/Ammunition/Power%20Cell.webp","_id":"VUkO1T2aYMuUcBZM"} +{"name":"Power Cell","permission":{"default":0,"vXYkFWX6qzvOu2jc":3},"type":"consumable","data":{"description":{"value":"

Power cells fuel blaster weapons that deal energy or ion damage. Additionally, power cells are used to energize certain tools.

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":1,"price":10,"attuned":false,"equipped":false,"rarity":"","identified":true,"activation": {"type": "none","cost": null,"condition": ""},"duration": {"value": null,"units": ""},"target": {"value": null,"width": null,"units": "","type": ""},"range":{"value": null,"long": null,"units": ""},"uses": {"value": 100,"max": "100","per": "charges","autoDestroy": false},"consume": {"type": "","target": "","amount": null},"ability": null,"actionType": "","attackBonus": 0,"chatFlavor": "","critical": null,"damage": {"parts": [],"versatile": ""},"formula": "","save": {"ability": "","dc": null,"scaling": "spell"},"consumableType": "ammo","attributes": {"spelldc": 10}},"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false,"effects":[]}},"img":"systems/sw5e/packs/Icons/Ammunition/Power%20Cell.webp","_id":"VUkO1T2aYMuUcBZM"} {"name":"Propulsion pack","permission":{"default":0,"vXYkFWX6qzvOu2jc":3},"type":"loot","data":{"description":{"value":"

Propulsion packs enhance underwater movement. Activating or deactivating the propulsion pack requires a bonus action and, while active, you have a swimming speed of 30 feet. The propulsion pack lasts for 1 minute per power cell (to a maximum of 10 minutes) and can be recharged by a power source or replacing the power cells.

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":20,"price":400,"attuned":false,"equipped":false,"rarity":"","identified":true,"attributes":{"spelldc":10},"damage":{"parts":[]}},"flags":{},"img":"systems/sw5e/packs/Icons/Weapon%20or%20Armor%20Accessory/Propulsion%20Pack.webp","_id":"XR1obpDj1PqDLfA8"} {"name":"Emergency Battery","permission":{"default":0,"vXYkFWX6qzvOu2jc":3},"type":"consumable","data":{"description":{"value":"

All non-expendable droids need recharging as they are used. The battery has ten uses. As an action, you can expend one use of the kit to stabilize a droid that has 0 hit points, without needing to make an Intelligence (Technology) check.

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":5,"price":70,"attuned":false,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"units":"","type":"creature"},"range":{"value":null,"long":null,"units":""},"uses":{"value":10,"max":10,"per":"charges","autoDestroy":true},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"other","attackBonus":0,"chatFlavor":"Stabilize Droid","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"spell"},"consumableType":"potion","attributes":{"spelldc":10}},"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false,"effects":[]}},"img":"systems/sw5e/packs/Icons/Medical/Emergency%20Battery.webp","_id":"Z0YM3aYCyCRhL6cx"} {"name":"Smugglepack","permission":{"default":0,"vXYkFWX6qzvOu2jc":3},"type":"backpack","data":{"description":{"value":"

This backpack comes with a main compartment that can store up to 15 lb., not exceeding a volume of 1/2 cubic foot. Additionally, it has a hidden storage compartment that can hold up to 5 lb, not exceeding a volume of 1/4 cubic foot. Finding the hidden compartment requires a DC 15 Investigation check.

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":6,"price":400,"attuned":false,"equipped":false,"rarity":"","identified":true,"capacity":{"type":"weight","value":20,"weightless":false},"currency":{"cp":0,"sp":0,"ep":0,"gp":0,"pp":0},"attributes":{"spelldc":10},"damage":{"parts":[]}},"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false,"effects":[]}},"img":"systems/sw5e/packs/Icons/Storage/Smugglerpack.webp","_id":"Zlj5z56A4oVQ5iEC"} @@ -111,3 +111,4 @@ {"name":"Headcomm","permission":{"default":0,"vXYkFWX6qzvOu2jc":3},"type":"loot","data":{"description":{"value":"

A headcomm can be installed in a helmet or worn independently. It functions as a hands-free commlink.

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":1,"price":200,"attuned":false,"equipped":false,"rarity":"","identified":true,"attributes":{"spelldc":10},"damage":{"parts":[]}},"flags":{},"img":"systems/sw5e/packs/Icons/Communications/Headcomm.webp","_id":"zHERdLuCUPpxzaSJ"} {"name":"Poisoner's Kit","permission":{"default":0,"vXYkFWX6qzvOu2jc":3},"type":"tool","data":{"description":{"value":"

A poisoner’s kit includes the vials, chemicals, and other equipment necessary for the creation of poisons. Proficiency with this kit lets you add your proficiency bonus to any ability checks you make to craft or use poisons.

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":2,"price":500,"attuned":false,"equipped":false,"rarity":"","identified":true,"ability":"int","chatFlavor":"","proficient":0,"attributes":{"spelldc":10},"damage":{"parts":[]}},"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false,"effects":[]}},"img":"systems/sw5e/packs/Icons/Kit/Poisoner_s%20Kit.webp","_id":"zaLzNlsfzRf71Xpl"} {"name":"Mine, Plasma","permission":{"default":0,"vXYkFWX6qzvOu2jc":3},"type":"weapon","data":{"description":{"value":"

When you use your action to set it, this mine sets an imperceptible laser line extending up to 15 feet. When the laser is tripped, the mine explodes, coating the area in a 15-foot radius around it in fire that burns for 1 minute. When a creature enters the fire or starts its turn there it must make a DC 13 Dexterity saving throw. On a failed save, the creature takes 2d6 fire damage, or half as much on a successful one. A construct makes this save with disadvantage.

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":2,"price":550,"attuned":false,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":15,"units":"ft","type":"radius"},"range":{"value":null,"long":null,"units":""},"uses":{"value":1,"max":1,"per":"charges"},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"save","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"dex","dc":13,"scaling":"flat"},"weaponType":"improv","properties":{"amm":false,"fin":false,"fir":false,"foc":false,"hvy":false,"lgt":false,"lod":false,"rch":false,"rel":false,"ret":false,"spc":false,"thr":false,"two":false,"ver":false},"proficient":false,"attributes":{"spelldc":10}},"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false,"effects":[]}},"img":"systems/sw5e/packs/Icons/Explosive/Mine%2C%20Plasma.webp","_id":"zrSOUA8dUg9lHVdS"} +{"name":"Power Cell","permission":{"default":0,"vXYkFWX6qzvOu2jc":3},"type":"consumable","data":{"description":{"value":"

Power cells fuel blaster weapons that deal energy or ion damage. Additionally, power cells are used to energize certain tools.

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":1,"price":10,"attuned":false,"equipped":false,"rarity":"","identified":true,"activation":{"type":"","cost":null,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"units":"","type":""},"range":{"value":null,"long":null,"units":""},"uses":{"value":240,"max":240,"per":"charges","autoDestroy":false},"consume":{"type":"","target":"","amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"spell"},"consumableType":"ammo","attributes":{"spelldc":10}},"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false,"effects":[]}},"img":"systems/sw5e/packs/Icons/Ammunition/Power%20Cell.webp","_id":"VUkO1T2aYMuUcBZM"} diff --git a/packs/packs/species.db b/packs/packs/species.db index f5122423..b3ae3616 100644 --- a/packs/packs/species.db +++ b/packs/packs/species.db @@ -119,3 +119,5 @@ {"_id":"ynuvnI54pMKLwF2a","name":"Echani","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"species","data":{"data":"$characteristics-table","description":{"value":"

Biology and Appearance

Echani are characterized by their white skin, hair, and eyes, and their remarkable tendency to look very much alike one another to outside observers, particularly amongst family members. It is thought that their origins stem from Arkanian experimentation on the human genome, a hypothesis that could explain their physical conformity.

Society and Culture

A matriarchal, caste-based society originating from the Inner Rim world of Eshan, the echani spread to encompass a confederacy of six worlds including Bengali and Thyrsus, known as the Six Sisters, governed by the all-female Echani Command. \r\n\t\r\nEchani generals are sometimes seen by others as having the ability to predict their opponent's next move. This is no biologicial trait inherent to the species, but rather stems from the fact that combat is so ingrained into every level of echani culture; the echani hold to the idea that combat is the truest form of communication, and to know someone fully, you must fight them. While their combat rituals require complete freedom of movement and unarmed martial arts, in warfare, they tend towards light armor and melee weapons, and are considered excellent craftsmen of such.

Names

Echani names tend to lack hard consonants, but are otherwise as variable as human ones. Echani surnames are tied directly to their place in the caste system.

  Male Names. Caelian, Inarin, Losor, Uelis, Yusanis

  Female Names. Astri, Brianna, Isena, Raskta, Senriel

  Surnames. Authal, Elysi, Fenni, Kinro, Lsu","chat":"","unidentified":""},"traits":{"value":"

Ability Score Increase. Your Dexterity score increases by 2, and your Wisdom score increases by 1.

\n

Age. Echani reach adulthood in their late teens and live less than a century.

\n

Alignment. Echani culture's emphasis on honor and combat cause them to tend towards lawful alignments, though there are exceptions.

\n

Size. Echani stand between 5 and a half and 6 feet tall and weigh around 150 lbs, with little variation between them. Your size is Medium.

\n

Speed. Your base walking speed is 30 feet.

\n

Allies of the Force. Whenever you make a Wisdom (Insight) check against someone you know to wield the Force, you are considered to have expertise in the Insight skill.

\n

Combative Culture. You have proficiency in Lore and Acrobatics.

\n

Echani Art. If a humanoid you can see makes a melee weapon attack, you can use your reaction to make a Wisdom (Insight) check against the target's Charisma (Deception). On a success you learn one of the following traits about that creature: it's Strength, Dexterity or Constitution score; bonus to Strength, Dexterity or Constitution saving throws; armor class; or current hit points. On a failure, the target becomes immune to this feature for one day. You can use this ability a number of times equal to your Wisdom modifier (a minimum of once). You regain all expended uses on a long rest.

\n

Martial Upbringing. You have proficiency in light armor, and gain proficiency with two martial vibroweapons of your choice.

\n

Unarmed Combatant. Your unarmed strikes deal 1d6 kinetic damage. You can use your choice of your Strength or Dexterity modifier for the attack and damage rolls. You must use the same modifier for both rolls.

\n

Languages. You can speak, read, and write Galactic Basic and one extra language of your choice.

"},"skinColorOptions":{"value":"Pale tones"},"hairColorOptions":{"value":"White"},"eyeColorOptions":{"value":"Silver"},"distinctions":{"value":"Fair skin, white hair and eyes, remarkable familial similarity."},"heightAverage":{"value":"5'1\""},"heightRollMod":{"value":"+1d10\""},"weightAverage":{"value":"105 lb."},"weightRollMod":{"value":"x(2d4) lb."},"homeworld":{"value":"Eshan"},"slanguage":{"value":"Galactic Basic"},"damage":{"parts":[]},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"source":"EC"},"flags":{"dynamiceffects":{"equipActive":true,"alwaysActive":false,"effects":[{"modSpecKey":"data.details.species","value":"Echani","mode":"=","targetSpecific":false,"id":1,"itemId":"RZ06rVoMQ80tK8W1","active":false,"_targets":[],"label":"Details Species"},{"modSpecKey":"data.abilities.dex.value","value":"2","mode":"+","targetSpecific":false,"id":2,"itemId":"RZ06rVoMQ80tK8W1","active":false,"_targets":[],"label":"Abilities Dexterity"},{"modSpecKey":"data.abilities.wis.value","value":"1","mode":"+","targetSpecific":false,"id":3,"itemId":"RZ06rVoMQ80tK8W1","active":false,"_targets":[],"label":"Abilities Wisdom"},{"modSpecKey":"data.traits.size","value":"med","mode":"=","targetSpecific":false,"id":4,"itemId":"RZ06rVoMQ80tK8W1","active":false,"_targets":[],"label":"Traits Size"},{"modSpecKey":"data.attributes.movement.walk","value":"30","mode":"=","targetSpecific":false,"id":5,"itemId":"RZ06rVoMQ80tK8W1","active":false,"_targets":[],"label":"Attributes Speed"},{"modSpecKey":"data.skills.lor.value","value":"1","mode":"+","targetSpecific":false,"id":6,"itemId":"RZ06rVoMQ80tK8W1","active":false,"_targets":[],"label":"Skills Lore"},{"modSpecKey":"data.skills.acr.value","value":"1","mode":"+","targetSpecific":false,"id":7,"itemId":"RZ06rVoMQ80tK8W1","active":false,"_targets":[],"label":"Skills Acrobatics"},{"modSpecKey":"data.traits.armorProf.value","value":"lgt","mode":"+","targetSpecific":false,"id":8,"itemId":"RZ06rVoMQ80tK8W1","active":false,"_targets":[]},{"modSpecKey":"data.traits.languages.value","value":"basic","mode":"+","targetSpecific":false,"id":9,"itemId":"RZ06rVoMQ80tK8W1","active":false,"_targets":[]}]},"dae":{"activeEquipped":false,"alwaysActive":true}},"img":"systems/sw5e/packs/Icons/Species/Echani.webp","effects":[{"_id":"HNBMxZCToQEy6PSr","flags":{"dae":{"transfer":true,"stackable":false}},"changes":[{"key":"data.details.species","value":"Echani","mode":5,"priority":5},{"key":"data.abilities.dex.value","value":2,"mode":2,"priority":20},{"key":"data.abilities.wis.value","value":1,"mode":2,"priority":20},{"key":"data.traits.size","value":"med","mode":0,"priority":5},{"key":"data.attributes.movement.walk","value":"30","mode":5,"priority":5},{"key":"data.skills.lor.value","value":1,"mode":4,"priority":20},{"key":"data.skills.acr.value","value":1,"mode":4,"priority":20},{"key":"data.traits.armorProf.value","value":"lgt","mode":0,"priority":0},{"key":"data.traits.languages.value","value":"basic","mode":0,"priority":0},{"key":"flags.sw5e.unarmedCombatant","value":"1","mode":5,"priority":20}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"systems/sw5e/packs/Icons/Species/Echani.webp","label":"Echani","tint":"","transfer":true}]} {"_id":"yyCUAG4cUUKh4IUz","name":"Iktotchi","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"species","data":{"data":"$characteristics-table","description":{"value":"

Biology and Appearance

Iktotchi do not have hair, but rather they had a very resistant skin which protected them from the violent winds which crossed the satellite. Both males and females have down-curved cranial horns, which gave them an aggressive aspect. The males' horns are generally a little larger, a remnant from their mountain-dwelling, caprinaen ancestors. The horns are able to regenerate if damaged.

Society and Culture

The Iktotchi are a fiercely guarded and isolationist species - vaunted for their ability to hide their feelings and bury any semblance of emotion. Originating on the harsh, windy moon of Iktotch, which orbits the planet Iktotchon in the Expansion Region, the Iktotch are gifted with precognition, and are courted as often by Jedi as by pirates for their skills.\r\n\r\nIktotchi society is a stratified society. Upward mobility is both possible and encouraged. Iktotchi are an outwardly dispassionate people, which is evidenced by their culture. They have a robust legal system, and suffer little crime. Iktotchi are respectful of cultures other than their own and can easily integrate with others.\r\n\r\nIktotchi who distinguish themselves often earn a titular nickname, by which they are referred to in place of their name. Generally, this is done by accomplishing a remarkable feat that benefits the Iktotchi as whole.

Names

Iktotchi names are generally two syllables. Surnames are familial. Respected Iktotchi often adopt a nickname, which they use in place of their birth name.

  Male Names. Dilnam, Imruth, Kashkil, Yellam

  Female Names. Kemkal, Onyeth, Reshu, Zorlu

  Surnames. Hevil, Kaawi, Mimir, Nudaal, Zelend","chat":"","unidentified":""},"traits":{"value":"

Ability Score Increase. Your Intelligence score increases by 2, and your Strength score increases by 1.

\n

Age. Iktotchi reach adulthood in their late teens and live less than a century.

\n

Alignment. Iktotchi are lawful and tend toward the light side, though there are exceptions.

\n

Size. Iktotchi typically stand between 5 and 6 feet tall and weigh about 170 lbs. Regardless of your position in that range, your size is Medium.

\n

Speed. Your base walking speed is 30 feet.

\n

Precognition. You can see brief fragments of the future that allow you to turn failures into successes. When you roll a 1 on an attack roll, ability check, or saving throw, you can reroll the die and must use the new roll.

\n

Telepathy. You can communicate telepathically with creatures within 30 feet of you. You must share a language with the target in order to communicate in this way.

\n

Horns. Your horns are a natural weapon, which you can use to make unarmed strikes. If you hit with it, you deal kinetic damage equal to 1d6 + your Strength modifier.

\n

Pilot. You have proficiency in the Piloting skill.

\n

Languages. You can speak, read, and write Galactic Basic and Iktotchese.

"},"skinColorOptions":{"value":"Pink"},"hairColorOptions":{"value":"None"},"eyeColorOptions":{"value":"Black"},"distinctions":{"value":"Horns, precognition, telepathy, thick pink skin"},"heightAverage":{"value":"4'11\""},"heightRollMod":{"value":"+2d10\""},"weightAverage":{"value":"120 lb."},"weightRollMod":{"value":"x(2d4) lb."},"homeworld":{"value":"Iktotch, moon of Iktotchon"},"slanguage":{"value":"Iktotchese"},"damage":{"parts":[]},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"colorScheme":{"value":""},"manufacturer":{"value":""},"planguage":{"value":""},"droidDistinctions":{"value":""},"droidLanguage":{"value":""},"source":"EC"},"flags":{"dynamiceffects":{"equipActive":true,"alwaysActive":false,"effects":[{"modSpecKey":"data.details.species","value":"Iktotchi","mode":"=","targetSpecific":false,"id":1,"itemId":"UZKrP5BCcEle7QSh","active":false,"_targets":[],"label":"Details Species"},{"modSpecKey":"data.abilities.int.value","value":"2","mode":"+","targetSpecific":false,"id":2,"itemId":"UZKrP5BCcEle7QSh","active":false,"_targets":[],"label":"Abilities Intelligence"},{"modSpecKey":"data.abilities.str.value","value":"1","mode":"+","targetSpecific":false,"id":3,"itemId":"UZKrP5BCcEle7QSh","active":false,"_targets":[],"label":"Abilities Strength"},{"modSpecKey":"data.traits.size","value":"med","mode":"=","targetSpecific":false,"id":4,"itemId":"UZKrP5BCcEle7QSh","active":false,"_targets":[],"label":"Traits Size"},{"modSpecKey":"data.attributes.movement.walk","value":"30","mode":"=","targetSpecific":false,"id":5,"itemId":"UZKrP5BCcEle7QSh","active":false,"_targets":[],"label":"Attributes Speed"},{"modSpecKey":"data.skills.pil.value","value":"1","mode":"+","targetSpecific":false,"id":6,"itemId":"UZKrP5BCcEle7QSh","active":false,"_targets":[]},{"modSpecKey":"data.traits.languages.value","value":"basic","mode":"+","targetSpecific":false,"id":7,"itemId":"UZKrP5BCcEle7QSh","active":false,"_targets":[]},{"modSpecKey":"data.traits.languages.value","value":"iktotchese","mode":"+","targetSpecific":false,"id":8,"itemId":"UZKrP5BCcEle7QSh","active":false,"_targets":[]},{"modSpecKey":"data.traits.languages.custom","value":"telepathy (30 ft.)","mode":"+","targetSpecific":false,"id":9,"itemId":"UZKrP5BCcEle7QSh","active":false,"_targets":[]}]},"dae":{"activeEquipped":false,"alwaysActive":true}},"img":"systems/sw5e/packs/Icons/Species/Iktotchi.webp","effects":[{"_id":"MAjUMql3tivJTfVO","flags":{"dae":{"transfer":true,"stackable":false}},"changes":[{"key":"data.details.species","value":"Iktotchi","mode":5,"priority":5},{"key":"data.abilities.int.value","value":2,"mode":2,"priority":20},{"key":"data.abilities.str.value","value":1,"mode":2,"priority":20},{"key":"data.traits.size","value":"med","mode":0,"priority":5},{"key":"data.attributes.movement.walk","value":"30","mode":5,"priority":5},{"key":"data.skills.pil.value","value":1,"mode":4,"priority":20},{"key":"data.traits.languages.value","value":"basic","mode":0,"priority":0},{"key":"data.traits.languages.value","value":"iktotchese","mode":0,"priority":0},{"key":"data.traits.languages.custom","value":"telepathy (30 ft.)","mode":0,"priority":0},{"key":"flags.sw5e.precognition","value":"1","mode":5,"priority":20}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"systems/sw5e/packs/Icons/Species/Iktotchi.webp","label":"Iktotchi","tint":"","transfer":true}]} {"_id":"zKpCsa8WCfz9abwv","name":"Killik","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"species","data":{"data":"$characteristics-table","description":{"value":"

Biology and Appearance

\n

Killiks possess a strong chitinous exoskeleton that is glossy and greenish with their carcasses capable of surviving thousands of years of erosion as seen by the colonists of Alderaan. The exoskeleton also contains a number of spiracles which served as their way of breathing. Typically, these Human-sized hive creatures have four arms with each ending in a powerful three-fingered claw. They stand on two stout legs that are capable of leaping great distances. Killiks can communicate with other Killiks through use of pheromones.

\n

Society and Culture

\n

The Killiks have a communal society, with each and every Killik being in mental contact with another. Due to their hive mind, every Killik nest is virtually one individual. Killiks are also peaceful in nature. Their telepathic connection is capable of extending to other species which includes non-insectoids. A willing creature can submit to this telepathy to become a Joiner. They effectively become another vessel of the hive mind. Killiks lose connection to their hive mind at great distances. Those who voluntarily leave the hive mind are referred to as Leavers. It is rare that they are allowed to rejoin their hive without reason.

\n

Names

\n

Killiks are a hive-mind insectoid that typically don't use names. On the off chance they do, it's usually an incomprehensible series of clicking noises. They are receptive to nicknames given by others.

","chat":"","unidentified":""},"traits":{"value":"

Ability Score Increase. Your Intelligence score increases by 2, and your Constitution score increases by 1.

Age. Killiks reach adulthood in their 40s and live an average of 200 years.

Alignment. Killiks' willingness to brainwash or kill their enemies cause them to tend towards the dark side, though there are exceptions.

Size. Killiks stand between 5 and 6 feet tall and weigh about 160 lbs. Regardless of your position in that range, your size is Medium.

Speed. Your base walking speed is 30 feet.

Four-Armed. Killiks have four arms which they can use independently of one another. You can only gain the benefit of items held by two of your arms at any given time, and once per round you can switch which arms you are benefiting from (no action required).

Hardened Carapace. While you are unarmored or wearing light armor, your AC is 13 + your Dexterity modifier.

Strong-Legged. When you make a long jump, you can cover a number of feet up to twice your Strength score. When you make a high jump, you can leap a number of feet up into the air equal to 3 + twice your Strength modifier.

Telepathy. You can communicate telepathically with creatures within 30 feet of you. You must share a language with the target in order to communicate in this way.

Languages. You can speak, read, and write Killik. You can understand spoken and written Galactic Basic, but your vocal cords do not allow you to speak it.

"},"skinColorOptions":{"value":"Brown, chestnut, green, red, scarlet, or yellow"},"hairColorOptions":{"value":"None"},"eyeColorOptions":{"value":"Black or orange"},"distinctions":{"value":"Chitinous armor, mandibles projected from face, four arms ending in long three toed claws protrude from their torsos"},"heightAverage":{"value":"4'9\""},"heightRollMod":{"value":"+2d10\""},"weightAverage":{"value":"110 lb."},"weightRollMod":{"value":"x(2d4) lb."},"homeworld":{"value":"Alderaan"},"slanguage":{"value":"Killik"},"damage":{"parts":[]},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"source":"EC"},"flags":{"dynamiceffects":{"equipActive":true,"alwaysActive":false,"effects":[{"modSpecKey":"data.details.species","value":"Killik","mode":"=","targetSpecific":false,"id":1,"itemId":"pzCpIjsuNVs8encY","active":false,"_targets":[],"label":"Details Species"},{"modSpecKey":"data.abilities.int.value","value":"2","mode":"+","targetSpecific":false,"id":2,"itemId":"pzCpIjsuNVs8encY","active":false,"_targets":[],"label":"Abilities Intelligence"},{"modSpecKey":"data.abilities.con.value","value":"1","mode":"+","targetSpecific":false,"id":3,"itemId":"pzCpIjsuNVs8encY","active":false,"_targets":[],"label":"Abilities Constitution"},{"modSpecKey":"data.traits.size","value":"med","mode":"=","targetSpecific":false,"id":4,"itemId":"pzCpIjsuNVs8encY","active":false,"_targets":[],"label":"Traits Size"},{"modSpecKey":"data.attributes.movement.walk","value":"30","mode":"=","targetSpecific":false,"id":5,"itemId":"pzCpIjsuNVs8encY","active":false,"_targets":[],"label":"Attributes Speed"},{"modSpecKey":"data.attributes.ac.min","value":"13","mode":"=","targetSpecific":false,"id":6,"itemId":"pzCpIjsuNVs8encY","active":false,"_targets":[],"label":"Attributes Armor Class Min"},{"modSpecKey":"data.traits.languages.value","value":"basic","mode":"+","targetSpecific":false,"id":7,"itemId":"pzCpIjsuNVs8encY","active":false,"_targets":[]},{"modSpecKey":"data.traits.languages.value","value":"killik","mode":"+","targetSpecific":false,"id":8,"itemId":"pzCpIjsuNVs8encY","active":false,"_targets":[]},{"modSpecKey":"data.traits.languages.custom","value":"telepathy (30 ft.)","mode":"+","targetSpecific":false,"id":9,"itemId":"pzCpIjsuNVs8encY","active":false,"_targets":[]}]},"dae":{"activeEquipped":false,"alwaysActive":true}},"img":"systems/sw5e/packs/Icons/Species/Killik.webp","effects":[{"_id":"EWQOMXrZbfi4zSPF","flags":{"dae":{"transfer":true,"stackable":false}},"changes":[{"key":"data.details.species","value":"Killik","mode":5,"priority":5},{"key":"data.abilities.int.value","value":2,"mode":2,"priority":20},{"key":"data.abilities.con.value","value":1,"mode":2,"priority":20},{"key":"data.traits.size","value":"med","mode":0,"priority":5},{"key":"data.attributes.movement.walk","value":"30","mode":5,"priority":5},{"key":"data.attributes.ac.value","value":"13+@abilities.dex.mod","mode":5,"priority":1},{"key":"data.traits.languages.value","value":"basic","mode":0,"priority":0},{"key":"data.traits.languages.value","value":"killik","mode":0,"priority":0},{"key":"data.traits.languages.custom","value":"telepathy (30 ft.)","mode":0,"priority":0},{"key":"flags.sw5e.strongLegged","value":"1","mode":5,"priority":20},{"key":"flags.sw5e.extraArms","value":"1","mode":5,"priority":20}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"systems/sw5e/packs/Icons/Species/Killik.webp","label":"Killik","tint":"","transfer":true}]} +{"_id":"J7HJQkcvghtwMAdF","name":"Anzellan","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"species","data":{"data":"$characteristics-table","description":{"value":"

Biology and Appearance

\n

The anzellans are a diminutive species hailing from the secluded planet Anzella. Their eyes have floating corneal micro-lenses that allow them to see microscopic details. Anzellans are a bubbly and receptive people. They are a jovial and trusting people that tend to welcome strangers with open arms. Due to their volcanic homeworld, anzellans are also adapted towards heat. This, coupled with their small size, make them well-suited to working in compact places.

\n

Society and Culture

\n

Anzella is a tropical planet covered in thousands of small volcanic islands. Many of these islands are developed as small villages, with the largest islands designed to accommodate larger species. Anzellan culture is generally based around tourism and crafting; in fact, anzellans are renowned craftsmen due to their discerning eyesight and ability to fit into small spaces. Anzellan government is generally casual. Each village has its own governing council of rotating members; these villages act independently from one another unless their decisions would affect more than a single island. In that case, all of the councils work together to come to a planet-wide decision.

\n

Names

\n

Anzellan names are rarely longer than two syllables, with a bouncy intonation to them. Their surnames are familial.

\n

  Male Names. Babu, Gridel, Moru, Rano, Yodel

\n

  Female Names. Dibi, Fing, Nooni, Teena, Zazi

\n

  Surnames. E'ayoo, Frik, Meer, Tanni, Vrut

","chat":"","unidentified":""},"traits":{"value":"

Ability Score Increase. Your Intelligence score increases by 2, and two other ability scores of your choice increase by 1.

Age. Anzellans are considered adults at ten years old. They are a short-lived species, however, that rarely lives longer than 60 years.

Alignment. Anzellans are a friendly and respectful people, which causes them to tend toward lawful light side, though there are exceptions.

Size. Anzellans stand between 1 and 2 feet tall and weigh around 10 lbs. Regardless of your position in that range, your size is Tiny.

Speed. Your base walking speed is 20 feet.

Crafters. You have proficiency in one tool of your choice.

Detail Oriented. You are practiced at scouring for details. You have advantage on Intelligence (Investigation) checks within 5 feet.

Pintsized. Your tiny stature makes it hard for you to wield bigger weapons. You can't use medium or heavy shields. Additionally, you can't wield weapons with the two-handed or versatile property, and you can only wield one-handed weapons in two hands unless they have the light property.

Puny. Anzellans are too small to pack much of a punch. You have disadvantage on Strength saving throws, and when determining your bonus to attack and damage rolls for weapon attacks using Strength, you can't add more than +3.

Small and Nimble. You are too small and fast to effectively target. You have a +1 bonus to AC, and you have advantage on Dexterity saving throws.

Tanned. You are naturally adapted to hot climates, as described in chapter 5 of the Dungeon Master's Guide.

Technician. You are proficient in the Technology skill.

Tinker. You have proficiency with tinker's tools. You can use these and spend 1 hour and 100 cr worth of materials to construct a Tiny Device (AC 5, 1 hp). You can take the Use an Object action to have your device cause one of the following effects: create a small explosion, create a repeating loud noise for 1 minute, create smoke for 1 minute, create a soothing melody for 1 minute. You can maintain a number of these devices up to your proficiency bonus at once, and a device stops functioning after 24 hours away from you. You can dismantle the device to reclaim the materials used to create it.

Languages. You can speak, read, and write Galactic Basic and Anzellan. Anzellan is characterized by its bouncy sound and emphasis on alternating syllables.

","source":"Expanded Content"},"skinColorOptions":{"value":"Brown, green, or tan"},"hairColorOptions":{"value":"Black, gray, or white"},"eyeColorOptions":{"value":"Black"},"distinctions":{"value":"Diminutive size, wispy eyebrows"},"heightAverage":{"value":"1'0\""},"heightRollMod":{"value":"+2d6\""},"weightAverage":{"value":"3 lb."},"weightRollMod":{"value":"x1 lb."},"homeworld":{"value":"Anzella"},"slanguage":{"value":"Anzellan"},"damage":{"parts":[]},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"source":"EC"},"flags":{"dynamiceffects":{"equipActive":true,"effects":[{"modSpecKey":"data.details.species","value":"Anzellan","mode":"=","targetSpecific":false,"id":1,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Details Species"},{"modSpecKey":"data.abilities.int.value","value":"2","mode":"+","targetSpecific":false,"id":2,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Abilities Intelligence"},{"modSpecKey":"data.traits.size","value":"tin","mode":"=","targetSpecific":false,"id":3,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Traits Size"},{"modSpecKey":"data.attributes.movement.walk","value":"20","mode":"=","targetSpecific":false,"id":4,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Attributes Speed"},{"modSpecKey":"data.attributes.ac.min","value":"1","mode":"+","targetSpecific":false,"id":5,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Attributes Armor Class Min"},{"modSpecKey":"data.skills.tec.value","value":"1","mode":"+","targetSpecific":false,"id":6,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Skills Technology"},{"modSpecKey":"data.traits.toolProf.custom","value":"Tinker's tools","mode":"+","targetSpecific":false,"id":7,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Traits Tool Prof Custom"},{"modSpecKey":"data.traits.languages.value","value":"basic","mode":"+","targetSpecific":false,"id":8,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Traits Language"},{"modSpecKey":"data.traits.languages.value","value":"anzellan","mode":"+","targetSpecific":false,"id":9,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[]}],"alwaysActive":false},"dae":{"activeEquipped":false,"alwaysActive":true}},"img":"systems/sw5e/packs/Icons/Species/Anzellan.webp","effects":[{"_id":"ZGdzAq1Gl4xaxDRD","flags":{"dae":{"transfer":true,"stackable":false}},"changes":[{"key":"data.details.species","value":"Anzellan","mode":5,"priority":5},{"key":"data.abilities.int.value","value":2,"mode":2,"priority":20},{"key":"data.traits.size","value":"tiny","mode":5,"priority":5},{"key":"data.attributes.movement.walk","value":20,"mode":5,"priority":5},{"key":"data.attributes.ac.value","value":1,"mode":2,"priority":20},{"key":"data.skills.tec.value","value":1,"mode":4,"priority":20},{"key":"data.traits.toolProf.custom","value":"Tinker's tools","mode":0,"priority":0},{"key":"data.traits.languages.value","value":"basic","mode":0,"priority":0},{"key":"data.traits.languages.value","value":"anzellan","mode":0,"priority":0},{"key":"flags.sw5e.detailOriented","value":"1","mode":5,"priority":20},{"key":"flags.sw5e.pintsized","value":"1","mode":5,"priority":20},{"key":"flags.sw5e.puny","value":"1","mode":5,"priority":20},{"key":"flags.sw5e.tinker","value":"1","mode":5,"priority":20}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"systems/sw5e/packs/Icons/Species/Anzellan.webp","label":"Anzellan","tint":"","transfer":true}]} +{"_id":"J7HJQkcvghtwMAdF","name":"Anzellan","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"species","data":{"data":"$characteristics-table","description":{"value":"

Biology and Appearance

\n

The anzellans are a diminutive species hailing from the secluded planet Anzella. Their eyes have floating corneal micro-lenses that allow them to see microscopic details. Anzellans are a bubbly and receptive people. They are a jovial and trusting people that tend to welcome strangers with open arms. Due to their volcanic homeworld, anzellans are also adapted towards heat. This, coupled with their small size, make them well-suited to working in compact places.

\n

Society and Culture

\n

Anzella is a tropical planet covered in thousands of small volcanic islands. Many of these islands are developed as small villages, with the largest islands designed to accommodate larger species. Anzellan culture is generally based around tourism and crafting; in fact, anzellans are renowned craftsmen due to their discerning eyesight and ability to fit into small spaces. Anzellan government is generally casual. Each village has its own governing council of rotating members; these villages act independently from one another unless their decisions would affect more than a single island. In that case, all of the councils work together to come to a planet-wide decision.

\n

Names

\n

Anzellan names are rarely longer than two syllables, with a bouncy intonation to them. Their surnames are familial.

\n

  Male Names. Babu, Gridel, Moru, Rano, Yodel

\n

  Female Names. Dibi, Fing, Nooni, Teena, Zazi

\n

  Surnames. E'ayoo, Frik, Meer, Tanni, Vrut

","chat":"","unidentified":""},"traits":{"value":"

Ability Score Increase. Your Intelligence score increases by 2, and two other ability scores of your choice increase by 1.

Age. Anzellans are considered adults at ten years old. They are a short-lived species, however, that rarely lives longer than 60 years.

Alignment. Anzellans are a friendly and respectful people, which causes them to tend toward lawful light side, though there are exceptions.

Size. Anzellans stand between 1 and 2 feet tall and weigh around 10 lbs. Regardless of your position in that range, your size is Tiny.

Speed. Your base walking speed is 20 feet.

Crafters. You have proficiency in one tool of your choice.

Detail Oriented. You are practiced at scouring for details. You have advantage on Intelligence (Investigation) checks within 5 feet.

Pintsized. Your tiny stature makes it hard for you to wield bigger weapons. You can't use medium or heavy shields. Additionally, you can't wield weapons with the two-handed or versatile property, and you can only wield one-handed weapons in two hands unless they have the light property.

Puny. Anzellans are too small to pack much of a punch. You have disadvantage on Strength saving throws, and when determining your bonus to attack and damage rolls for weapon attacks using Strength, you can't add more than +3.

Small and Nimble. You are too small and fast to effectively target. You have a +1 bonus to AC, and you have advantage on Dexterity saving throws.

Tanned. You are naturally adapted to hot climates, as described in chapter 5 of the Dungeon Master's Guide.

Technician. You are proficient in the Technology skill.

Tinker. You have proficiency with tinker's tools. You can use these and spend 1 hour and 100 cr worth of materials to construct a Tiny Device (AC 5, 1 hp). You can take the Use an Object action to have your device cause one of the following effects: create a small explosion, create a repeating loud noise for 1 minute, create smoke for 1 minute, create a soothing melody for 1 minute. You can maintain a number of these devices up to your proficiency bonus at once, and a device stops functioning after 24 hours away from you. You can dismantle the device to reclaim the materials used to create it.

Languages. You can speak, read, and write Galactic Basic and Anzellan. Anzellan is characterized by its bouncy sound and emphasis on alternating syllables.

","source":"Expanded Content"},"skinColorOptions":{"value":"Brown, green, or tan"},"hairColorOptions":{"value":"Black, gray, or white"},"eyeColorOptions":{"value":"Black"},"distinctions":{"value":"Diminutive size, wispy eyebrows"},"heightAverage":{"value":"1'0\""},"heightRollMod":{"value":"+2d6\""},"weightAverage":{"value":"3 lb."},"weightRollMod":{"value":"x1 lb."},"homeworld":{"value":"Anzella"},"slanguage":{"value":"Anzellan"},"damage":{"parts":[]},"armorproperties":{"parts":[]},"weaponproperties":{"parts":[]},"source":"EC"},"flags":{"dynamiceffects":{"equipActive":true,"effects":[{"modSpecKey":"data.details.species","value":"Anzellan","mode":"=","targetSpecific":false,"id":1,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Details Species"},{"modSpecKey":"data.abilities.int.value","value":"2","mode":"+","targetSpecific":false,"id":2,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Abilities Intelligence"},{"modSpecKey":"data.traits.size","value":"tin","mode":"=","targetSpecific":false,"id":3,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Traits Size"},{"modSpecKey":"data.attributes.movement.walk","value":"20","mode":"=","targetSpecific":false,"id":4,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Attributes Speed"},{"modSpecKey":"data.attributes.ac.min","value":"1","mode":"+","targetSpecific":false,"id":5,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Attributes Armor Class Min"},{"modSpecKey":"data.skills.tec.value","value":"1","mode":"+","targetSpecific":false,"id":6,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Skills Technology"},{"modSpecKey":"data.traits.toolProf.custom","value":"Tinker's tools","mode":"+","targetSpecific":false,"id":7,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Traits Tool Prof Custom"},{"modSpecKey":"data.traits.languages.value","value":"basic","mode":"+","targetSpecific":false,"id":8,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[],"label":"Traits Language"},{"modSpecKey":"data.traits.languages.value","value":"anzellan","mode":"+","targetSpecific":false,"id":9,"itemId":"bIEwmrB96DiRuntI","active":false,"_targets":[]}],"alwaysActive":false},"dae":{"activeEquipped":false,"alwaysActive":true}},"img":"systems/sw5e/packs/Icons/Species/Anzellan.webp","effects":[{"_id":"ZGdzAq1Gl4xaxDRD","flags":{"dae":{"transfer":true,"stackable":false}},"changes":[{"key":"data.details.species","value":"Anzellan","mode":5,"priority":5},{"key":"data.abilities.int.value","value":2,"mode":2,"priority":20},{"key":"data.traits.size","value":"tiny","mode":5,"priority":5},{"key":"data.attributes.movement.walk","value":20,"mode":5,"priority":5},{"key":"data.attributes.ac.value","value":1,"mode":2,"priority":20},{"key":"data.skills.tec.value","value":1,"mode":4,"priority":20},{"key":"data.traits.toolProf.custom","value":"Tinker's tools","mode":0,"priority":0},{"key":"data.traits.languages.value","value":"basic","mode":0,"priority":0},{"key":"data.traits.languages.value","value":"anzellan","mode":0,"priority":0},{"key":"flags.sw5e.detailOriented","value":"1","mode":5,"priority":20},{"key":"flags.sw5e.pintsized","value":"1","mode":5,"priority":20},{"key":"flags.sw5e.puny","value":"1","mode":5,"priority":20},{"key":"flags.sw5e.tinker","value":"1","mode":5,"priority":20}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"systems/sw5e/packs/Icons/Species/Anzellan.webp","label":"Anzellan","tint":"","transfer":true}]} diff --git a/packs/packs/starshiparmor.db b/packs/packs/starshiparmor.db index 79733cd0..20efef0d 100644 --- a/packs/packs/starshiparmor.db +++ b/packs/packs/starshiparmor.db @@ -1,6 +1,6 @@ -{"_id":"AAA9PWi1rTiSUIIe","name":"Lightweight Armor","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"equipment","data":{"description":{"value":"

Lightweight armor offers a trade-off of a more maneuverable but less resilient ship. A ship with Lightweight Armor installed has a +2 bonus to armor class, but has one fewer maximum hull point per Hull Die.

","chat":"","unidentified":""},"source":"SotG","quantity":1,"weight":0,"price":3100,"attuned":false,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"","cost":0,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"width":null,"units":"","type":""},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":0,"per":null},"consume":{"type":"","target":null,"amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10,"type":"ssarmor","dex":null},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"speed":{"value":null,"conditions":""},"strength":0,"stealth":false,"proficient":true,"properties":{"Absorptive":false,"Agile":false,"Anchor":false,"Avoidant":false,"Barbed":false,"Bulky":false,"Charging":false,"Concealing":false,"Cumbersome":false,"Gauntleted":false,"Imbalanced":false,"Impermeable":false,"Insulated":false,"Interlocking":false,"Lambent":false,"Lightweight":false,"Magnetic":false,"Obscured":false,"Obtrusive":false,"Powered":false,"Reactive":false,"Regulated":false,"Reinforced":false,"Responsive":false,"Rigid":false,"Silent":false,"Spiked":false,"Steadfast":false,"Strength":false,"Versatile":false},"capx":{"value":""},"dmgred":{"value":"0"},"regrateco":{"value":""},"cscap":{"value":""},"sscap":{"value":""},"fuelcostsmod":{"value":""},"powdicerec":{"value":""},"hdclass":{"value":""},"attributes":{"dr":""}},"flags":{},"img":"systems/sw5e/packs/Icons/Starship%20Equipment/Lightweight%20Armor.webp","effects":[]} -{"_id":"JhX8qXjrDL3pCRmF","name":"Reinforced Armor","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"equipment","data":{"description":{"value":"

Opposite of lightweight armor is reinforced armor. This armor improves a ship's resilience, but makes it less likely to avoid damage. A ship with Reinforced Armor installed has a -1 penalty to armor class, but has one additional maximum hull point per Hull Die.

","chat":"","unidentified":""},"source":"SotG","quantity":1,"weight":0,"price":3700,"attuned":false,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"","cost":0,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"width":null,"units":"","type":""},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":0,"per":null},"consume":{"type":"","target":null,"amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10,"type":"ssarmor","dex":0},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"speed":{"value":null,"conditions":""},"strength":0,"stealth":false,"proficient":true,"properties":{"Absorptive":false,"Agile":false,"Anchor":false,"Avoidant":false,"Barbed":false,"Bulky":false,"Charging":false,"Concealing":false,"Cumbersome":false,"Gauntleted":false,"Imbalanced":false,"Impermeable":false,"Insulated":false,"Interlocking":false,"Lambent":false,"Lightweight":false,"Magnetic":false,"Obscured":false,"Obtrusive":false,"Powered":false,"Reactive":false,"Regulated":false,"Reinforced":false,"Responsive":false,"Rigid":false,"Silent":false,"Spiked":false,"Steadfast":false,"Strength":false,"Versatile":false},"capx":{"value":""},"dmgred":{"value":"6"},"regrateco":{"value":""},"attributes":{"dr":"6"},"cscap":{"value":""},"sscap":{"value":""},"fuelcostsmod":{"value":""},"powdicerec":{"value":""},"hdclass":{"value":""}},"flags":{},"img":"systems/sw5e/packs/Icons/Starship%20Equipment/Reinforced%20Armor.webp","effects":[]} -{"_id":"M7igMGsBIosGA4dS","name":"Quick-Charge Shield","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"equipment","data":{"description":{"value":"

Quick-Charge Shields, opposite of Fortress Shields, offer a reduced capacity but rapidly replenish.

","chat":"","unidentified":""},"source":"SotG","quantity":1,"weight":0,"price":4900,"attuned":false,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"","cost":0,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"width":null,"units":"","type":""},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":0,"per":null},"consume":{"type":"","target":null,"amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":0,"type":"ssshield","dex":null},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"speed":{"value":null,"conditions":""},"strength":0,"stealth":false,"proficient":true,"properties":{"Absorptive":false,"Agile":false,"Anchor":false,"Avoidant":false,"Barbed":false,"Bulky":false,"Charging":false,"Concealing":false,"Cumbersome":false,"Gauntleted":false,"Imbalanced":false,"Impermeable":false,"Insulated":false,"Interlocking":false,"Lambent":false,"Lightweight":false,"Magnetic":false,"Obscured":false,"Obtrusive":false,"Powered":false,"Reactive":false,"Regulated":false,"Reinforced":false,"Responsive":false,"Rigid":false,"Silent":false,"Spiked":false,"Steadfast":false,"Strength":false,"Versatile":false},"capx":{"value":"0.667"},"dmgred":{"value":""},"regrateco":{"value":"1.5"},"attributes":{"dr":""},"cscap":{"value":""},"sscap":{"value":""},"fuelcostsmod":{"value":""},"powdicerec":{"value":""},"hdclass":{"value":""}},"flags":{},"img":"systems/sw5e/packs/Icons/Starship%20Equipment/Quick-Charge%20Shield.webp","effects":[]} -{"_id":"RvtLP3FgKLBYBHSf","name":"Directional Shield","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"equipment","data":{"description":{"value":"

Directional Shields are the most commonly used and balanced shields on the market.

","chat":"","unidentified":""},"source":"SotG","quantity":1,"weight":0,"price":4300,"attuned":false,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"","cost":0,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"width":null,"units":"","type":""},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":0,"per":null},"consume":{"type":"","target":null,"amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":0,"type":"ssshield","dex":null},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"speed":{"value":null,"conditions":""},"strength":0,"stealth":false,"proficient":true,"properties":{"Absorptive":false,"Agile":false,"Anchor":false,"Avoidant":false,"Barbed":false,"Bulky":false,"Charging":false,"Concealing":false,"Cumbersome":false,"Gauntleted":false,"Imbalanced":false,"Impermeable":false,"Insulated":false,"Interlocking":false,"Lambent":false,"Lightweight":false,"Magnetic":false,"Obscured":false,"Obtrusive":false,"Powered":false,"Reactive":false,"Regulated":false,"Reinforced":false,"Responsive":false,"Rigid":false,"Silent":false,"Spiked":false,"Steadfast":false,"Strength":false,"Versatile":false},"capx":{"value":"1"},"dmgred":{"value":""},"regrateco":{"value":"1"},"cscap":{"value":""},"sscap":{"value":""},"fuelcostsmod":{"value":""},"powdicerec":{"value":""},"hdclass":{"value":""},"attributes":{"dr":""}},"flags":{},"img":"systems/sw5e/packs/Icons/Starship%20Equipment/Directional%20Shield.webp","effects":[]} -{"_id":"Wj62TEtwKeG1P2DD","name":"Fortress Shield","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"equipment","data":{"description":{"value":"

Fortress shields offer a higher maximum shield points, but regenerate slower than normal shields.

","chat":"","unidentified":""},"source":"SotG","quantity":1,"weight":0,"price":4650,"attuned":false,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"","cost":0,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"width":null,"units":"","type":""},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":0,"per":null},"consume":{"type":"","target":null,"amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":0,"type":"ssshield","dex":null},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"speed":{"value":null,"conditions":""},"strength":0,"stealth":false,"proficient":true,"properties":{"Absorptive":false,"Agile":false,"Anchor":false,"Avoidant":false,"Barbed":false,"Bulky":false,"Charging":false,"Concealing":false,"Cumbersome":false,"Gauntleted":false,"Imbalanced":false,"Impermeable":false,"Insulated":false,"Interlocking":false,"Lambent":false,"Lightweight":false,"Magnetic":false,"Obscured":false,"Obtrusive":false,"Powered":false,"Reactive":false,"Regulated":false,"Reinforced":false,"Responsive":false,"Rigid":false,"Silent":false,"Spiked":false,"Steadfast":false,"Strength":false,"Versatile":false},"capx":{"value":"1.5"},"dmgred":{"value":""},"regrateco":{"value":"0.667"},"attributes":{"dr":""},"cscap":{"value":""},"sscap":{"value":""},"fuelcostsmod":{"value":""},"powdicerec":{"value":""},"hdclass":{"value":""}},"flags":{},"img":"systems/sw5e/packs/Icons/Starship%20Equipment/Fortress%20Shield.webp","effects":[]} -{"_id":"aG6mKPerYCFmkI00","name":"Deflection Armor","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"equipment","data":{"description":{"value":"

Deflection armor is the most common type of armor aboard ships, and offers no benefit or penalty to armor class or hull points.

","chat":"","unidentified":""},"source":"SotG","quantity":1,"weight":0,"price":3450,"attuned":false,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"","cost":0,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"width":null,"units":"","type":""},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":0,"per":null},"consume":{"type":"","target":null,"amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10,"type":"ssarmor","dex":2},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"speed":{"value":null,"conditions":""},"strength":0,"stealth":false,"proficient":true,"properties":{"Absorptive":false,"Agile":false,"Anchor":false,"Avoidant":false,"Barbed":false,"Bulky":false,"Charging":false,"Concealing":false,"Cumbersome":false,"Gauntleted":false,"Imbalanced":false,"Impermeable":false,"Insulated":false,"Interlocking":false,"Lambent":false,"Lightweight":false,"Magnetic":false,"Obscured":false,"Obtrusive":false,"Powered":false,"Reactive":false,"Regulated":false,"Reinforced":false,"Responsive":false,"Rigid":false,"Silent":false,"Spiked":false,"Steadfast":false,"Strength":false,"Versatile":false},"capx":{"value":""},"dmgred":{"value":"3"},"regrateco":{"value":""},"attributes":{"dr":"3"},"cscap":{"value":""},"sscap":{"value":""},"fuelcostsmod":{"value":""},"powdicerec":{"value":""},"hdclass":{"value":""}},"flags":{},"img":"systems/sw5e/packs/Icons/Starship%20Equipment/Deflection%20Armor.webp","effects":[]} +{"_id":"AAA9PWi1rTiSUIIe","name":"Lightweight Armor","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"equipment","data":{"description":{"value":"

Lightweight armor offers a trade-off of a more maneuverable but less resilient ship. A ship with Lightweight Armor installed has a +2 bonus to armor class, but has one fewer maximum hull point per Hull Die.

","chat":"","unidentified":""},"source":"SotG","quantity":1,"weight":0,"price":3100,"attuned":false,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"","cost":0,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"width":null,"units":"","type":""},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":0,"per":null},"consume":{"type":"","target":null,"amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10,"type":"ssarmor","dex":null},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"speed":{"value":null,"conditions":""},"strength":0,"stealth":false,"proficient":true,"properties":{"Absorptive":false,"Agile":false,"Anchor":false,"Avoidant":false,"Barbed":false,"Bulky":false,"Charging":false,"Concealing":false,"Cumbersome":false,"Gauntleted":false,"Imbalanced":false,"Impermeable":false,"Insulated":false,"Interlocking":false,"Lambent":false,"Lightweight":false,"Magnetic":false,"Obscured":false,"Obtrusive":false,"Powered":false,"Reactive":false,"Regulated":false,"Reinforced":false,"Responsive":false,"Rigid":false,"Silent":false,"Spiked":false,"Steadfast":false,"Strength":false,"Versatile":false},"capx":{"value":""},"hpperhd":{"value":"(-1)"},"regrateco":{"value":""},"cscap":{"value":""},"sscap":{"value":""},"fuelcostsmod":{"value":""},"powdicerec":{"value":""},"hdclass":{"value":""},"attributes":{"dr":""}},"flags":{},"img":"systems/sw5e/packs/Icons/Starship%20Equipment/Lightweight%20Armor.webp","effects":[]} +{"_id":"JhX8qXjrDL3pCRmF","name":"Reinforced Armor","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"equipment","data":{"description":{"value":"

Opposite of lightweight armor is reinforced armor. This armor improves a ship's resilience, but makes it less likely to avoid damage. A ship with Reinforced Armor installed has a -1 penalty to armor class, but has one additional maximum hull point per Hull Die.

","chat":"","unidentified":""},"source":"SotG","quantity":1,"weight":0,"price":3700,"attuned":false,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"","cost":0,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"width":null,"units":"","type":""},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":0,"per":null},"consume":{"type":"","target":null,"amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10,"type":"ssarmor","dex":null},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"speed":{"value":null,"conditions":""},"strength":0,"stealth":false,"proficient":true,"properties":{"Absorptive":false,"Agile":false,"Anchor":false,"Avoidant":false,"Barbed":false,"Bulky":false,"Charging":false,"Concealing":false,"Cumbersome":false,"Gauntleted":false,"Imbalanced":false,"Impermeable":false,"Insulated":false,"Interlocking":false,"Lambent":false,"Lightweight":false,"Magnetic":false,"Obscured":false,"Obtrusive":false,"Powered":false,"Reactive":false,"Regulated":false,"Reinforced":false,"Responsive":false,"Rigid":false,"Silent":false,"Spiked":false,"Steadfast":false,"Strength":false,"Versatile":false},"capx":{"value":""},"hpperhd":{"value":"1"},"regrateco":{"value":""},"attributes":{"dr":"6"},"cscap":{"value":""},"sscap":{"value":""},"fuelcostsmod":{"value":""},"powdicerec":{"value":""},"hdclass":{"value":""}},"flags":{},"img":"systems/sw5e/packs/Icons/Starship%20Equipment/Reinforced%20Armor.webp","effects":[]} +{"_id":"M7igMGsBIosGA4dS","name":"Quick-Charge Shield","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"equipment","data":{"description":{"value":"

Quick-Charge Shields, opposite of Fortress Shields, offer a reduced capacity but rapidly replenish.

","chat":"","unidentified":""},"source":"SotG","quantity":1,"weight":0,"price":4900,"attuned":false,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"","cost":0,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"width":null,"units":"","type":""},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":0,"per":null},"consume":{"type":"","target":null,"amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":0,"type":"ssshield","dex":null},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"speed":{"value":null,"conditions":""},"strength":0,"stealth":false,"proficient":true,"properties":{"Absorptive":false,"Agile":false,"Anchor":false,"Avoidant":false,"Barbed":false,"Bulky":false,"Charging":false,"Concealing":false,"Cumbersome":false,"Gauntleted":false,"Imbalanced":false,"Impermeable":false,"Insulated":false,"Interlocking":false,"Lambent":false,"Lightweight":false,"Magnetic":false,"Obscured":false,"Obtrusive":false,"Powered":false,"Reactive":false,"Regulated":false,"Reinforced":false,"Responsive":false,"Rigid":false,"Silent":false,"Spiked":false,"Steadfast":false,"Strength":false,"Versatile":false},"capx":{"value":"(2/3)"},"hpperhd":{"value":""},"regrateco":{"value":"(3/2)"},"attributes":{"dr":""},"cscap":{"value":""},"sscap":{"value":""},"fuelcostsmod":{"value":""},"powdicerec":{"value":""},"hdclass":{"value":""}},"flags":{},"img":"systems/sw5e/packs/Icons/Starship%20Equipment/Quick-Charge%20Shield.webp","effects":[]} +{"_id":"RvtLP3FgKLBYBHSf","name":"Directional Shield","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"equipment","data":{"description":{"value":"

Directional Shields are the most commonly used and balanced shields on the market.

","chat":"","unidentified":""},"source":"SotG","quantity":1,"weight":0,"price":4300,"attuned":false,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"","cost":0,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"width":null,"units":"","type":""},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":0,"per":null},"consume":{"type":"","target":null,"amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":0,"type":"ssshield","dex":null},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"speed":{"value":null,"conditions":""},"strength":0,"stealth":false,"proficient":true,"properties":{"Absorptive":false,"Agile":false,"Anchor":false,"Avoidant":false,"Barbed":false,"Bulky":false,"Charging":false,"Concealing":false,"Cumbersome":false,"Gauntleted":false,"Imbalanced":false,"Impermeable":false,"Insulated":false,"Interlocking":false,"Lambent":false,"Lightweight":false,"Magnetic":false,"Obscured":false,"Obtrusive":false,"Powered":false,"Reactive":false,"Regulated":false,"Reinforced":false,"Responsive":false,"Rigid":false,"Silent":false,"Spiked":false,"Steadfast":false,"Strength":false,"Versatile":false},"capx":{"value":"1"},"hpperhd":{"value":""},"regrateco":{"value":"1"},"cscap":{"value":""},"sscap":{"value":""},"fuelcostsmod":{"value":""},"powdicerec":{"value":""},"hdclass":{"value":""},"attributes":{"dr":""}},"flags":{},"img":"systems/sw5e/packs/Icons/Starship%20Equipment/Directional%20Shield.webp","effects":[]} +{"_id":"Wj62TEtwKeG1P2DD","name":"Fortress Shield","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"equipment","data":{"description":{"value":"

Fortress shields offer a higher maximum shield points, but regenerate slower than normal shields.

","chat":"","unidentified":""},"source":"SotG","quantity":1,"weight":0,"price":4650,"attuned":false,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"","cost":0,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"width":null,"units":"","type":""},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":0,"per":null},"consume":{"type":"","target":null,"amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":0,"type":"ssshield","dex":null},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"speed":{"value":null,"conditions":""},"strength":0,"stealth":false,"proficient":true,"properties":{"Absorptive":false,"Agile":false,"Anchor":false,"Avoidant":false,"Barbed":false,"Bulky":false,"Charging":false,"Concealing":false,"Cumbersome":false,"Gauntleted":false,"Imbalanced":false,"Impermeable":false,"Insulated":false,"Interlocking":false,"Lambent":false,"Lightweight":false,"Magnetic":false,"Obscured":false,"Obtrusive":false,"Powered":false,"Reactive":false,"Regulated":false,"Reinforced":false,"Responsive":false,"Rigid":false,"Silent":false,"Spiked":false,"Steadfast":false,"Strength":false,"Versatile":false},"capx":{"value":"(3/2)"},"hpperhd":{"value":""},"regrateco":{"value":"(2/3)"},"attributes":{"dr":""},"cscap":{"value":""},"sscap":{"value":""},"fuelcostsmod":{"value":""},"powdicerec":{"value":""},"hdclass":{"value":""}},"flags":{},"img":"systems/sw5e/packs/Icons/Starship%20Equipment/Fortress%20Shield.webp","effects":[]} +{"_id":"aG6mKPerYCFmkI00","name":"Deflection Armor","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"equipment","data":{"description":{"value":"

Deflection armor is the most common type of armor aboard ships, and offers no benefit or penalty to armor class or hull points.

","chat":"","unidentified":""},"source":"SotG","quantity":1,"weight":0,"price":3450,"attuned":false,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"","cost":0,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"width":null,"units":"","type":""},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":0,"per":null},"consume":{"type":"","target":null,"amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10,"type":"ssarmor","dex":null},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"speed":{"value":null,"conditions":""},"strength":0,"stealth":false,"proficient":true,"properties":{"Absorptive":false,"Agile":false,"Anchor":false,"Avoidant":false,"Barbed":false,"Bulky":false,"Charging":false,"Concealing":false,"Cumbersome":false,"Gauntleted":false,"Imbalanced":false,"Impermeable":false,"Insulated":false,"Interlocking":false,"Lambent":false,"Lightweight":false,"Magnetic":false,"Obscured":false,"Obtrusive":false,"Powered":false,"Reactive":false,"Regulated":false,"Reinforced":false,"Responsive":false,"Rigid":false,"Silent":false,"Spiked":false,"Steadfast":false,"Strength":false,"Versatile":false},"capx":{"value":""},"hpperhd":{"value":""},"regrateco":{"value":""},"attributes":{"dr":"3"},"cscap":{"value":""},"sscap":{"value":""},"fuelcostsmod":{"value":""},"powdicerec":{"value":""},"hdclass":{"value":""}},"flags":{},"img":"systems/sw5e/packs/Icons/Starship%20Equipment/Deflection%20Armor.webp","effects":[]} diff --git a/packs/packs/starshipequipment.db b/packs/packs/starshipequipment.db index 03f7dcbf..e6aa1ef7 100644 --- a/packs/packs/starshipequipment.db +++ b/packs/packs/starshipequipment.db @@ -5,10 +5,10 @@ {"_id":"MVXftcjJ1yzsCU3N","name":"Hyperdrive, Class 0.5","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"equipment","data":{"description":{"value":"

The hyperdrive is a propulsion system that allows a starship to reach hyperspeed and traverse the void between stars in the alternate dimension of hyperspace. For a starship to have a hyperdrive, it must have a vacant hyperdrive slot modification.

","chat":"","unidentified":""},"source":"SotG","quantity":1,"weight":null,"price":50000,"attuned":false,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"","cost":0,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"width":null,"units":"","type":""},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":0,"per":null},"consume":{"type":"","target":null,"amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":null,"type":"hyper","dex":null},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"speed":{"value":null,"conditions":""},"strength":0,"stealth":false,"proficient":true,"properties":{"Absorptive":false,"Agile":false,"Anchor":false,"Avoidant":false,"Barbed":false,"Bulky":false,"Charging":false,"Concealing":false,"Cumbersome":false,"Gauntleted":false,"Imbalanced":false,"Impermeable":false,"Insulated":false,"Interlocking":false,"Lambent":false,"Lightweight":false,"Magnetic":false,"Obscured":false,"Obtrusive":false,"Powered":false,"Reactive":false,"Regulated":false,"Reinforced":false,"Responsive":false,"Rigid":false,"Silent":false,"Spiked":false,"Steadfast":false,"Strength":false,"Versatile":false},"capx":{"value":""},"hpperhd":{"value":""},"regrateco":{"value":""},"cscap":{"value":""},"sscap":{"value":""},"fuelcostsmod":{"value":""},"powdicerec":{"value":""},"hdclass":{"value":"0.5"}},"flags":{"core":{"sourceId":"Item.3CA76AXkU73nE53o"}},"img":"systems/sw5e/packs/Icons/Starship%20Equipment/Hyperdrive.webp","effects":[]} {"name":"Hyperdrive, Class 8","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"equipment","data":{"description":{"value":"

The hyperdrive is a propulsion system that allows a starship to reach hyperspeed and traverse the void between stars in the alternate dimension of hyperspace. For a starship to have a hyperdrive, it must have a vacant hyperdrive slot modification.

","chat":"","unidentified":""},"source":"SotG","quantity":1,"weight":null,"price":1000,"attuned":false,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"","cost":0,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"width":null,"units":"","type":""},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":0,"per":null},"consume":{"type":"","target":null,"amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":null,"type":"hyper","dex":null},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"speed":{"value":null,"conditions":""},"strength":0,"stealth":false,"proficient":true,"properties":{"Absorptive":false,"Agile":false,"Anchor":false,"Avoidant":false,"Barbed":false,"Bulky":false,"Charging":false,"Concealing":false,"Cumbersome":false,"Gauntleted":false,"Imbalanced":false,"Impermeable":false,"Insulated":false,"Interlocking":false,"Lambent":false,"Lightweight":false,"Magnetic":false,"Obscured":false,"Obtrusive":false,"Powered":false,"Reactive":false,"Regulated":false,"Reinforced":false,"Responsive":false,"Rigid":false,"Silent":false,"Spiked":false,"Steadfast":false,"Strength":false,"Versatile":false},"capx":{"value":""},"hpperhd":{"value":""},"regrateco":{"value":""},"cscap":{"value":""},"sscap":{"value":""},"fuelcostsmod":{"value":""},"powdicerec":{"value":""},"hdclass":{"value":"8"}},"flags":{"core":{"sourceId":"Item.pJASNAp63U6Y2Yx8"}},"img":"systems/sw5e/packs/Icons/Starship%20Equipment/Hyperdrive.webp","effects":[],"_id":"P84rgL4vBaWw0GJe"} {"name":"Hyperdrive, Class 5","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"equipment","data":{"description":{"value":"

The hyperdrive is a propulsion system that allows a starship to reach hyperspeed and traverse the void between stars in the alternate dimension of hyperspace. For a starship to have a hyperdrive, it must have a vacant hyperdrive slot modification.

","chat":"","unidentified":""},"source":"SotG","quantity":1,"weight":null,"price":2500,"attuned":false,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"","cost":0,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"width":null,"units":"","type":""},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":0,"per":null},"consume":{"type":"","target":null,"amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":null,"type":"hyper","dex":null},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"speed":{"value":null,"conditions":""},"strength":0,"stealth":false,"proficient":true,"properties":{"Absorptive":false,"Agile":false,"Anchor":false,"Avoidant":false,"Barbed":false,"Bulky":false,"Charging":false,"Concealing":false,"Cumbersome":false,"Gauntleted":false,"Imbalanced":false,"Impermeable":false,"Insulated":false,"Interlocking":false,"Lambent":false,"Lightweight":false,"Magnetic":false,"Obscured":false,"Obtrusive":false,"Powered":false,"Reactive":false,"Regulated":false,"Reinforced":false,"Responsive":false,"Rigid":false,"Silent":false,"Spiked":false,"Steadfast":false,"Strength":false,"Versatile":false},"capx":{"value":""},"hpperhd":{"value":""},"regrateco":{"value":""},"cscap":{"value":""},"sscap":{"value":""},"fuelcostsmod":{"value":""},"powdicerec":{"value":""},"hdclass":{"value":"5"}},"flags":{"core":{"sourceId":"Item.UgLbCq7OWL9G9BD7"}},"img":"systems/sw5e/packs/Icons/Starship%20Equipment/Hyperdrive.webp","effects":[],"_id":"U4BhpyMxJdbnNErJ"} -{"_id":"UAiau5ZNXVJAJFUn","name":"Power Core Reactor","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"equipment","data":{"description":{"value":"

Power core reactors have highly variable power output capabilities, but sacrifice fuel economy and as a result.

","chat":"","unidentified":""},"source":"SotG","quantity":1,"weight":null,"price":5750,"attuned":false,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"","cost":0,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"width":null,"units":"","type":""},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":0,"per":null},"consume":{"type":"","target":null,"amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":null,"type":"reactor","dex":null},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"speed":{"value":null,"conditions":""},"strength":0,"stealth":false,"proficient":true,"properties":{"Absorptive":false,"Agile":false,"Anchor":false,"Avoidant":false,"Barbed":false,"Bulky":false,"Charging":false,"Concealing":false,"Cumbersome":false,"Gauntleted":false,"Imbalanced":false,"Impermeable":false,"Insulated":false,"Interlocking":false,"Lambent":false,"Lightweight":false,"Magnetic":false,"Obscured":false,"Obtrusive":false,"Powered":false,"Reactive":false,"Regulated":false,"Reinforced":false,"Responsive":false,"Rigid":false,"Silent":false,"Spiked":false,"Steadfast":false,"Strength":false,"Versatile":false},"capx":{"value":""},"hpperhd":{"value":""},"regrateco":{"value":""},"cscap":{"value":""},"sscap":{"value":""},"fuelcostsmod":{"value":"1.5"},"powdicerec":{"value":"1d2"},"hdclass":{"value":""}},"flags":{"core":{"sourceId":"Item.2MTQUv6r5ePNANyn"}},"img":"systems/sw5e/packs/Icons/Starship%20Equipment/Reactor.webp","effects":[]} +{"_id":"UAiau5ZNXVJAJFUn","name":"Power Core Reactor","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"equipment","data":{"description":{"value":"

Power core reactors have highly variable power output capabilities, but sacrifice fuel economy and as a result.

","chat":"","unidentified":""},"source":"SotG","quantity":1,"weight":null,"price":5750,"attuned":false,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"","cost":0,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"width":null,"units":"","type":""},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":0,"per":null},"consume":{"type":"","target":null,"amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":null,"type":"reactor","dex":null},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"speed":{"value":null,"conditions":""},"strength":0,"stealth":false,"proficient":true,"properties":{"Absorptive":false,"Agile":false,"Anchor":false,"Avoidant":false,"Barbed":false,"Bulky":false,"Charging":false,"Concealing":false,"Cumbersome":false,"Gauntleted":false,"Imbalanced":false,"Impermeable":false,"Insulated":false,"Interlocking":false,"Lambent":false,"Lightweight":false,"Magnetic":false,"Obscured":false,"Obtrusive":false,"Powered":false,"Reactive":false,"Regulated":false,"Reinforced":false,"Responsive":false,"Rigid":false,"Silent":false,"Spiked":false,"Steadfast":false,"Strength":false,"Versatile":false},"capx":{"value":""},"hpperhd":{"value":""},"regrateco":{"value":""},"cscap":{"value":""},"sscap":{"value":""},"fuelcostsmod":{"value":"(3/2)"},"powdicerec":{"value":"1d2"},"hdclass":{"value":""}},"flags":{"core":{"sourceId":"Item.2MTQUv6r5ePNANyn"}},"img":"systems/sw5e/packs/Icons/Starship%20Equipment/Reactor.webp","effects":[]} {"_id":"VzkRXuQx2sqN9nd0","name":"Distributed Power Coupling","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"equipment","data":{"description":{"value":"

Distributed power coupling sacrifices flexibility by allocating power separately to each system.

","chat":"","unidentified":""},"source":"SotG","quantity":1,"weight":null,"price":5100,"attuned":false,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"","cost":0,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"width":null,"units":"","type":""},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":0,"per":null},"consume":{"type":"","target":null,"amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":0,"type":"powerc","dex":null},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"speed":{"value":null,"conditions":""},"strength":0,"stealth":false,"proficient":true,"properties":{"Absorptive":false,"Agile":false,"Anchor":false,"Avoidant":false,"Barbed":false,"Bulky":false,"Charging":false,"Concealing":false,"Cumbersome":false,"Gauntleted":false,"Imbalanced":false,"Impermeable":false,"Insulated":false,"Interlocking":false,"Lambent":false,"Lightweight":false,"Magnetic":false,"Obscured":false,"Obtrusive":false,"Powered":false,"Reactive":false,"Regulated":false,"Reinforced":false,"Responsive":false,"Rigid":false,"Silent":false,"Spiked":false,"Steadfast":false,"Strength":false,"Versatile":false},"capx":{"value":""},"hpperhd":{"value":""},"regrateco":{"value":""},"cscap":{"value":""},"sscap":{"value":"2"},"fuelcostsmod":{"value":""},"powdicerec":{"value":""},"hdclass":{"value":""}},"flags":{"core":{"sourceId":"Item.4zjcBtJhXgpFW2pb"}},"img":"systems/sw5e/packs/Icons/Starship%20Equipment/Power%20Coupling.webp","effects":[]} -{"_id":"ZyEdKtLwSXuUQs0P","name":"Ionization Reactor","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"equipment","data":{"description":{"value":"

Ionization reactors are highly fuel-efficient reactors that trade power output for fuel economy.

","chat":"","unidentified":""},"source":"SotG","quantity":1,"weight":null,"price":5100,"attuned":false,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"","cost":0,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"width":null,"units":"","type":""},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":0,"per":null},"consume":{"type":"","target":null,"amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":null,"type":"reactor","dex":null},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"speed":{"value":null,"conditions":""},"strength":0,"stealth":false,"proficient":true,"properties":{"Absorptive":false,"Agile":false,"Anchor":false,"Avoidant":false,"Barbed":false,"Bulky":false,"Charging":false,"Concealing":false,"Cumbersome":false,"Gauntleted":false,"Imbalanced":false,"Impermeable":false,"Insulated":false,"Interlocking":false,"Lambent":false,"Lightweight":false,"Magnetic":false,"Obscured":false,"Obtrusive":false,"Powered":false,"Reactive":false,"Regulated":false,"Reinforced":false,"Responsive":false,"Rigid":false,"Silent":false,"Spiked":false,"Steadfast":false,"Strength":false,"Versatile":false},"capx":{"value":""},"hpperhd":{"value":""},"regrateco":{"value":""},"cscap":{"value":""},"sscap":{"value":""},"fuelcostsmod":{"value":"0.667"},"powdicerec":{"value":"(1d2)-1"},"hdclass":{"value":""}},"flags":{"core":{"sourceId":"Item.kXo8mNp6GFLYWokY"}},"img":"systems/sw5e/packs/Icons/Starship%20Equipment/Reactor.webp","effects":[]} -{"name":"Fuel Cell Reactor","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"equipment","data":{"description":{"value":"

Fuel cell reactors are the most common and balanced reactors on the market.

","chat":"","unidentified":""},"source":"SotG","quantity":1,"weight":null,"price":4500,"attuned":false,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"","cost":0,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"width":null,"units":"","type":""},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":0,"per":null},"consume":{"type":"","target":null,"amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":null,"type":"reactor","dex":null},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"speed":{"value":null,"conditions":""},"strength":0,"stealth":false,"proficient":true,"properties":{"Absorptive":false,"Agile":false,"Anchor":false,"Avoidant":false,"Barbed":false,"Bulky":false,"Charging":false,"Concealing":false,"Cumbersome":false,"Gauntleted":false,"Imbalanced":false,"Impermeable":false,"Insulated":false,"Interlocking":false,"Lambent":false,"Lightweight":false,"Magnetic":false,"Obscured":false,"Obtrusive":false,"Powered":false,"Reactive":false,"Regulated":false,"Reinforced":false,"Responsive":false,"Rigid":false,"Silent":false,"Spiked":false,"Steadfast":false,"Strength":false,"Versatile":false},"capx":{"value":""},"hpperhd":{"value":""},"regrateco":{"value":""},"cscap":{"value":""},"sscap":{"value":""},"fuelcostsmod":{"value":"1"},"powdicerec":{"value":"1"},"hdclass":{"value":""}},"flags":{"core":{"sourceId":"Item.Qwu6WlJiIgFWq9VF"}},"img":"systems/sw5e/packs/Icons/Starship%20Equipment/Reactor.webp","effects":[],"_id":"jk7zL3cqhufDKsuh"} +{"_id":"ZyEdKtLwSXuUQs0P","name":"Ionization Reactor","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"equipment","data":{"description":{"value":"

Ionization reactors are highly fuel-efficient reactors that trade power output for fuel economy.

","chat":"","unidentified":""},"source":"SotG","quantity":1,"weight":null,"price":5100,"attuned":false,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"","cost":0,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"width":null,"units":"","type":""},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":0,"per":null},"consume":{"type":"","target":null,"amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":null,"type":"reactor","dex":null},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"speed":{"value":null,"conditions":""},"strength":0,"stealth":false,"proficient":true,"properties":{"Absorptive":false,"Agile":false,"Anchor":false,"Avoidant":false,"Barbed":false,"Bulky":false,"Charging":false,"Concealing":false,"Cumbersome":false,"Gauntleted":false,"Imbalanced":false,"Impermeable":false,"Insulated":false,"Interlocking":false,"Lambent":false,"Lightweight":false,"Magnetic":false,"Obscured":false,"Obtrusive":false,"Powered":false,"Reactive":false,"Regulated":false,"Reinforced":false,"Responsive":false,"Rigid":false,"Silent":false,"Spiked":false,"Steadfast":false,"Strength":false,"Versatile":false},"capx":{"value":""},"hpperhd":{"value":""},"regrateco":{"value":""},"cscap":{"value":""},"sscap":{"value":""},"fuelcostsmod":{"value":"(2/3)"},"powdicerec":{"value":"(1d2)-1"},"hdclass":{"value":""}},"flags":{"core":{"sourceId":"Item.kXo8mNp6GFLYWokY"}},"img":"systems/sw5e/packs/Icons/Starship%20Equipment/Reactor.webp","effects":[]} +{"name":"Fuel Cell Reactor","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"equipment","data":{"description":{"value":"

Fuel cell reactors are the most common and balanced reactors on the market.

","chat":"","unidentified":""},"source":"SotG","quantity":1,"weight":null,"price":4500,"attuned":false,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"","cost":0,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"width":null,"units":"","type":""},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":0,"per":null},"consume":{"type":"","target":null,"amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":null,"type":"reactor","dex":null},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"speed":{"value":null,"conditions":""},"strength":0,"stealth":false,"proficient":true,"properties":{"Absorptive":false,"Agile":false,"Anchor":false,"Avoidant":false,"Barbed":false,"Bulky":false,"Charging":false,"Concealing":false,"Cumbersome":false,"Gauntleted":false,"Imbalanced":false,"Impermeable":false,"Insulated":false,"Interlocking":false,"Lambent":false,"Lightweight":false,"Magnetic":false,"Obscured":false,"Obtrusive":false,"Powered":false,"Reactive":false,"Regulated":false,"Reinforced":false,"Responsive":false,"Rigid":false,"Silent":false,"Spiked":false,"Steadfast":false,"Strength":false,"Versatile":false},"capx":{"value":""},"hpperhd":{"value":""},"regrateco":{"value":""},"cscap":{"value":""},"sscap":{"value":""},"fuelcostsmod":{"value":""},"powdicerec":{"value":"1"},"hdclass":{"value":""}},"flags":{"core":{"sourceId":"Item.Qwu6WlJiIgFWq9VF"}},"img":"systems/sw5e/packs/Icons/Starship%20Equipment/Reactor.webp","effects":[],"_id":"jk7zL3cqhufDKsuh"} {"name":"Hyperdrive, Class 1.0","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"equipment","data":{"description":{"value":"

The hyperdrive is a propulsion system that allows a starship to reach hyperspeed and traverse the void between stars in the alternate dimension of hyperspace. For a starship to have a hyperdrive, it must have a vacant hyperdrive slot modification.

","chat":"","unidentified":""},"source":"SotG","quantity":1,"weight":null,"price":15000,"attuned":false,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"","cost":0,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"width":null,"units":"","type":""},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":0,"per":null},"consume":{"type":"","target":null,"amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":null,"type":"hyper","dex":null},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"speed":{"value":null,"conditions":""},"strength":0,"stealth":false,"proficient":true,"properties":{"Absorptive":false,"Agile":false,"Anchor":false,"Avoidant":false,"Barbed":false,"Bulky":false,"Charging":false,"Concealing":false,"Cumbersome":false,"Gauntleted":false,"Imbalanced":false,"Impermeable":false,"Insulated":false,"Interlocking":false,"Lambent":false,"Lightweight":false,"Magnetic":false,"Obscured":false,"Obtrusive":false,"Powered":false,"Reactive":false,"Regulated":false,"Reinforced":false,"Responsive":false,"Rigid":false,"Silent":false,"Spiked":false,"Steadfast":false,"Strength":false,"Versatile":false},"capx":{"value":""},"hpperhd":{"value":""},"regrateco":{"value":""},"cscap":{"value":""},"sscap":{"value":""},"fuelcostsmod":{"value":""},"powdicerec":{"value":""},"hdclass":{"value":"1"}},"flags":{"core":{"sourceId":"Item.MrCZaUig1puXcEVy"}},"img":"systems/sw5e/packs/Icons/Starship%20Equipment/Hyperdrive.webp","effects":[],"_id":"o9DmeVhCJNezkjdI"} {"_id":"oqB8RltTDjHnaS1Y","name":"Direct Power Coupling","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"equipment","data":{"description":{"value":"

Direct power coupling has a central power capacitor that feeds power directly to each system.

","chat":"","unidentified":""},"source":"SotG","quantity":1,"weight":null,"price":4100,"attuned":false,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"","cost":0,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"width":null,"units":"","type":""},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":0,"per":null},"consume":{"type":"","target":null,"amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":0,"type":"powerc","dex":null},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"speed":{"value":null,"conditions":""},"strength":0,"stealth":false,"proficient":true,"properties":{"Absorptive":false,"Agile":false,"Anchor":false,"Avoidant":false,"Barbed":false,"Bulky":false,"Charging":false,"Concealing":false,"Cumbersome":false,"Gauntleted":false,"Imbalanced":false,"Impermeable":false,"Insulated":false,"Interlocking":false,"Lambent":false,"Lightweight":false,"Magnetic":false,"Obscured":false,"Obtrusive":false,"Powered":false,"Reactive":false,"Regulated":false,"Reinforced":false,"Responsive":false,"Rigid":false,"Silent":false,"Spiked":false,"Steadfast":false,"Strength":false,"Versatile":false},"capx":{"value":""},"hpperhd":{"value":""},"regrateco":{"value":""},"cscap":{"value":"4"},"sscap":{"value":""},"fuelcostsmod":{"value":""},"powdicerec":{"value":""},"hdclass":{"value":""}},"flags":{"core":{"sourceId":"Item.MEiHVZHModsp5w0b"}},"img":"systems/sw5e/packs/Icons/Starship%20Equipment/Power%20Coupling.webp","effects":[]} {"name":"Hyperdrive, Class 3","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"equipment","data":{"description":{"value":"

The hyperdrive is a propulsion system that allows a starship to reach hyperspeed and traverse the void between stars in the alternate dimension of hyperspace. For a starship to have a hyperdrive, it must have a vacant hyperdrive slot modification.

","chat":"","unidentified":""},"source":"SotG","quantity":1,"weight":null,"price":7500,"attuned":false,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"","cost":0,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"width":null,"units":"","type":""},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":0,"per":null},"consume":{"type":"","target":null,"amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":null,"type":"hyper","dex":null},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"speed":{"value":null,"conditions":""},"strength":0,"stealth":false,"proficient":true,"properties":{"Absorptive":false,"Agile":false,"Anchor":false,"Avoidant":false,"Barbed":false,"Bulky":false,"Charging":false,"Concealing":false,"Cumbersome":false,"Gauntleted":false,"Imbalanced":false,"Impermeable":false,"Insulated":false,"Interlocking":false,"Lambent":false,"Lightweight":false,"Magnetic":false,"Obscured":false,"Obtrusive":false,"Powered":false,"Reactive":false,"Regulated":false,"Reinforced":false,"Responsive":false,"Rigid":false,"Silent":false,"Spiked":false,"Steadfast":false,"Strength":false,"Versatile":false},"capx":{"value":""},"hpperhd":{"value":""},"regrateco":{"value":""},"cscap":{"value":""},"sscap":{"value":""},"fuelcostsmod":{"value":""},"powdicerec":{"value":""},"hdclass":{"value":"3"}},"flags":{"core":{"sourceId":"Item.bzY549C3zaI3H6mt"}},"img":"systems/sw5e/packs/Icons/Starship%20Equipment/Hyperdrive.webp","effects":[],"_id":"rFm20P0THnzZRUxB"} diff --git a/packs/packs/starships.db b/packs/packs/starships.db deleted file mode 100644 index 32695c68..00000000 --- a/packs/packs/starships.db +++ /dev/null @@ -1,6 +0,0 @@ -{"_id":"6BN8l5E8QtYt103T","name":"Small Starship","permission":{"default":0,"yXqD5rPwgjXHtqeZ":3},"type":"starship","data":{"description":{"value":"

As the fighters cover his approach, Zik Beskin activates his targeting computer and ignores the explosions surrounding him, instead Focusing on the Destroyer's shield generator. It had to come down soon, or this fight was lost. Arsix, behind him, beeps and whirs, preparing the ion pulse missiles for the attack run, prewarming their engines and arming the warheads.

\n

Together a long time now, Beskin and Arsix had spilled their share of blood and oil, respectively, for the Rebellion--usually just while improving their ancient Ywing. But today, the blood and oil spilt wouldn't be their own. As Zik let fly a pair of missiles, he knew they would find their target. Today wasn't over just yet. This was her fourth sortie of the day, and Sheena was tired. The terrorists just kept coming. Every time she was about to shut her eyes a new wave of the Rebels came. And every time they did, she rushed to her TIE Interceptor and joined the alert fighters to take the fighters down before they could blow a hole in the planet's defenses. Every time they retreated before suffering heavy losses. But every time they came back. This last time she had decided to just nap in the cramped cockpit, so when the claxon rang out, she and her ship were basically ready to fly. This time she was going to end them quickly. As she repeatedly squeezed her trigger, she executed Koiogran turns and snap rolls galore, her laser blasts striking true, and the debris of A-wings, X-wings, and B-Wings--along with some frozen traitor remains--floated in space the next few days. At night, she leaned back against her beau, sipping some wine and watch-ing the beautiful streaks of light cross the sky as, piece by piece, the wreckage burned up in the atmosphere during reentry.

\n

R5-S1 locked down the loose stabilizer with his gripper arm as he angled the X-Wing's deflector shields. This ship took a firm gripper to get under control, but R5-S1 was up to the task. As his pilot, Veets, fired his last blast from the overheating cannons on the deployed s-foils, R5-S1 did the work of cooling off the guns, spooling up the hyperdrive, and running the calculations for lightspeed. It was time to go. Small ships have a tiny crew, often only a pilot and perhaps an astromech, but often strike above their weight class, a threat to small and large ships alike.

"},"size":"sm","tier":0,"hullDice":"d6","hullDiceStart":3,"hullDiceRolled":[6,4,4],"hullDiceUsed":0,"shldDice":"d6","shldDiceStart":3,"shldDiceRolled":[6,4,4],"shldDiceUsed":0,"pwrDice":"1","buildBaseCost":50000,"buildMinWorkforce":5,"upgrdCostMult":1,"upgrdMinWorkforce":1,"baseSpaceSpeed":300,"baseTurnSpeed":250,"crewMinWorkforce":1,"modBaseCap":20,"modMaxSuitesBase":-1,"modMaxSuitesMult":1,"modMaxSuiteCap":1,"modCostMult":1,"modMinWorkforce":2,"hardpointMult":1,"equipCostMult":1,"equipMinWorkforce":1,"cargoCap":2,"fuelCost":50,"fuelCap":10,"foodCap":10,"source":"SotG"},"folder":null,"sort":200001,"flags":{},"img":"systems/sw5e/packs/Icons/Starship%20Features/Small.webp","effects":[{"_id":"aVvrOFBux3t6WMEc","flags":{"dae":{"stackable":false,"transfer":true}},"changes":[{"key":"data.abilities.dex.value","value":2,"mode":2,"priority":1},{"key":"data.abilities.con.value","value":-2,"mode":2,"priority":1}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"systems/sw5e/packs/Icons/Starship%20Features/Small.webp","label":"Small Starship","origin":"Item.Kgo48vXNxSZkWp7H","tint":"","transfer":true}]} -{"_id":"6liD1m4hqKSeS5sp","name":"Medium Starship","permission":{"default":0,"yXqD5rPwgjXHtqeZ":3},"type":"starship","data":{"description":{"value":"

The Freighter shudders with the blasts of cannon fire. Despite its maneuvers, the pilot can't quite shake the pursuit. The technician's efforts to reinforce the shields are failing. The mechanic is pumping the reactor for every scrap of energy it can generate. The operator is frantically making the final few calculations for the jump to hyperspace. Finally, just as the ship's shields dissipate, the pilot makes the gut call, jettisoning the illicit cargo. As it distracts and hampers the followers, the freighter shifts power to the thrusters and quickly flies away.

\n

As the pirates activate their tractor beam to attempt to capture the weaponless frigate and its exotic wares, the gunboat escort intercedes. It flies in the line of the tractor, breaking the lock on the frigate, and unleashes a volley of cannon fire. The pirates, incapable of withstanding the salvo, drop the tractor beam and retreat.

\n

As the gunboat pursues to be sure the pirates don't come back for a second bite at the apple, the crew finally locks on target and unleashes a long-range night-stinger missile, putting a permanent end to the pirates' illicit and unwelcome activities.

\n

The captain invites his guests into his well-stocked cantina. He eyes the opposition as they take in his ship, assessing their reactions and noting their expressions. Fully aware of the effect the opulent room has on the unprepared, he easily stifles his grin and gestures for the starry-eyed vis-itors to sit across from him. He indicates for the server to bring drinks as he casually leans back and puts his feet on the table, confident this deal will go his way. He presses a button on the tiny remote in his hand, causing hidden panels to slide away and reveal his mostly-legal wares. \"So,\" he says, \"just how many of these do you need, and where will I deliver them?\"

\n

Medium ships are the bread and butter of the closeknit group. They are large enough to accommodate all of the immediate needs of a crew, while at the same time being small enough to feel cozy.

"},"size":"med","tier":0,"hullDice":"d8","hullDiceStart":5,"hullDiceRolled":[8,5,5,5,5],"hullDiceUsed":0,"shldDice":"d8","shldDiceStart":5,"shldDiceRolled":[8,5,5,5,5],"shldDiceUsed":0,"pwrDice":"1","buildBaseCost":100000,"buildMinWorkforce":10,"upgrdCostMult":2,"upgrdMinWorkforce":5,"baseSpaceSpeed":300,"baseTurnSpeed":200,"crewMinWorkforce":1,"modBaseCap":30,"modMaxSuitesBase":3,"modMaxSuitesMult":1,"modMaxSuiteCap":4,"modCostMult":2,"modMinWorkforce":4,"hardpointMult":1.5,"equipCostMult":2,"equipMinWorkforce":2,"cargoCap":25,"fuelCost":100,"fuelCap":30,"foodCap":120,"source":"SotG"},"folder":null,"sort":300001,"flags":{},"img":"systems/sw5e/packs/Icons/Starship%20Features/Medium.webp","effects":[]} -{"_id":"FH8iBT4uujRUR0j7","name":"Gargantuan Starship","permission":{"default":0,"yXqD5rPwgjXHtqeZ":3},"type":"starship","data":{"description":{"value":"

The smaller ships retreat into the shadow of the blockade ship, fleeing an overwhelming foe. As the dreadnought's shields envelope them, they quickly turn and spring on their pursuers, utilizing the bulwark's shields as they unleash all of the firepower they have to bear.

\n

Meanwhile, the blockade ship unleashes a storm of electromagnetic energy from its antenna array, cutting of communications between enemy ships, effectively isolating the incoming forces from their distant fleet and from each other.

\n

In the center of the fleet, the command ship surveys the battlefield. Wherever the line wavers, the command ship quickly directs ships to reinforce. Finally, the formations of the enemy flag, and the command ship directs the fleet to capitalize on their failure as it determines and uploads targeting coordinates to its torpedo ships.

\n

The warship looms ominously over the battlefield as the two opposing armies crash. Despite the efforts of the enemy line, the warship closes into firing range of the capital ships. Having already determined an ordered targeting precedence, the operating crew confirms final firing solutions for the gunners as they charge up the main super-weapon on the prow of the ship. It unleashes its first devastating blast as the rest of its arsenal begins to lance out at secondary targets nearby.

\n

Gargantuan ships are the dreadnoughts that strike fear into the hearts of the faithless. They are the embodiment of indomitable might: a symbol of total and complete control.

"},"size":"grg","tier":0,"hullDice":"d20","hullDiceStart":11,"hullDiceRolled":[20,11,11,11,11,11,11,11,11,11,11],"hullDiceUsed":0,"shldDice":"d20","shldDiceStart":11,"shldDiceRolled":[20,11,11,11,11,11,11,11,11,11,11],"shldDiceUsed":0,"pwrDice":"1","buildBaseCost":1000000000,"buildMinWorkforce":10000,"upgrdCostMult":1000,"upgrdMinWorkforce":5000,"baseSpaceSpeed":300,"baseTurnSpeed":50,"crewMinWorkforce":80000,"modBaseCap":70,"modMaxSuitesBase":10,"modMaxSuitesMult":4,"modMaxSuiteCap":40000,"modCostMult":500,"modMinWorkforce":1000,"hardpointMult":3,"equipCostMult":500,"equipMinWorkforce":500,"cargoCap":200000,"fuelCost":100000,"fuelCap":1800,"foodCap":576000000,"source":"SotG"},"folder":null,"sort":600001,"flags":{},"img":"systems/sw5e/packs/Icons/Starship%20Features/Gargantuan.webp","effects":[{"_id":"3erWl25IS1iaKUiv","flags":{"dae":{"stackable":false,"transfer":true}},"changes":[{"key":"data.abilities.dex.value","value":-6,"mode":2,"priority":1},{"key":"data.abilities.con.value","value":6,"mode":2,"priority":1}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"systems/sw5e/packs/Icons/Starship%20Features/Gargantuan.webp","label":"Gargantuan Starship","origin":"Item.KjK5001DYmUlFfPF","tint":"","transfer":true}]} -{"_id":"RFKvLuqE13INBxqd","name":"Large Starship","permission":{"default":0,"yXqD5rPwgjXHtqeZ":3},"type":"starship","data":{"description":{"value":"

Trapped in the capital ship's tractor beam, the ambassador frigate moves slowly towards the cruiser. The crew struggles to squeeze more power out of their reactor while the few marines on board take positions next to the hatches, wiping sweat from their brows as they check and re-check their weapons. Finally, bringing all the power the ship has to bare, the frigate is able to break the hold of the tractor beam and regain its trajectory, slowly but surely increasing the distance, before finally escaping the planet's moon and being able to jump to hyperspace. Shouts of joy echo down the ship's corridors and extra rations are ordered in celebration.

\n

Engines burning brightly, the corvette sprints through the blockade, trying to minimize the amount of fire its meager shields will have to absorb. Top and bottom turrets swivel to port, unleashing return fire against inbound interceptors. The ground drops from under everyone's feet as the artificial gravity systems flicker for a second as a pulse weapon detonates nearby. The high-pitched, distant whine of the reactor is barely audible over commands issuing from the bridge. The visual readouts indicated that they were now past the picket line, and the interceptors appeared to be breaking off, unsure of their ability to take on the much larger ship without the support of their battle stations. They'd made it. Looking back at the scopes, the coordinator's head hung down. They'd been the only ones to do so.

\n

As the Pelta-Class Picket ship danced between the larger destroyers and dreadnaughts, it continued its near constant barrage of heavy laser cannon fire, interspersed with individual launches of concussion missiles and proton torpedoes directed at vulnerable parts of the opposing fleet. If too many of those enemy guns came to bear on the the Pelta, it would be in trouble, but it's speed and it's relatively limited firepower made it a less-than juicy target. For now. But that is exactly what it's captain needed. Just a few more clicks and they would be in a perfect flanking position, able to pound the engines of the flag ship as soon as it's shields were brought down by the bombing squad beginning their run now.

\n

Large ships occupy the pinnacle of size for most private owner/operators in the galaxy. Large ships require an extensive crew and are costly to maintain, but can pack quite a punch and may house various suites and operation centers that allow the ship to operate as an impressive and mobile base of operations for wealthy individuals and successful adventurers.

"},"size":"lg","tier":0,"hullDice":"d10","hullDiceStart":7,"hullDiceRolled":[10,6,6,6,6,6,6],"hullDiceUsed":0,"shldDice":"d10","shldDiceStart":7,"shldDiceRolled":[10,6,6,6,6,6,6],"shldDiceUsed":0,"pwrDice":"1","buildBaseCost":1000000,"buildMinWorkforce":100,"upgrdCostMult":10,"upgrdMinWorkforce":50,"baseSpaceSpeed":300,"baseTurnSpeed":150,"crewMinWorkforce":200,"modBaseCap":50,"modMaxSuitesBase":3,"modMaxSuitesMult":2,"modMaxSuiteCap":400,"modCostMult":5,"modMinWorkforce":10,"hardpointMult":2,"equipCostMult":5,"equipMinWorkforce":5,"cargoCap":500,"fuelCost":1000,"fuelCap":300,"foodCap":240000,"source":"SotG"},"folder":null,"sort":400001,"flags":{},"img":"systems/sw5e/packs/Icons/Starship%20Features/Large.webp","effects":[{"_id":"NwpCHKQsAOq16TMa","flags":{"dae":{"stackable":false,"transfer":true}},"changes":[{"key":"data.abilities.dex.value","value":-2,"mode":2,"priority":1},{"key":"data.abilities.con.value","value":2,"mode":2,"priority":1}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"systems/sw5e/packs/Icons/Starship%20Features/Large.webp","label":"Large Ship","origin":"Item.ywTvt1JEvDEoqG3A","tint":"","transfer":true}]} -{"_id":"pgmf0rMYLt4LQtfN","name":"Huge Starship","permission":{"default":0,"yXqD5rPwgjXHtqeZ":3},"type":"starship","data":{"description":{"value":"

The battleship's shields flicker as it absorbs the blows of the attacking fighters. It continues inexorably past them as it's point-defense system peppers it's vicinity with blaster fire to ward them off. As it a approaches the fragile medical frigate the fighters scramble to protect, its gunners lock on to the target before unleashing a fierce volley of turbolaser fire and snapping it in half.

\n

As the carrier leaves hyperspace, snubfighters deploy from its hangars in formations and move to intercept the space station's patrol fighters. Before the enemy craft have the opportunity to respond, the fighters fall upon them, quickly decimating their ranks. But before the snubfighters had even left the carrier, a second wave of small bombers had been prepping for takeoff. As they spew forth from the hangars, they quickly lock on to the space station and launch proton bombs, pulverizing the station in minutes.

\n

With the command given, the operator activates the interdictor's gravity well projectors. the lights inside dim almost imperceptibly as huge amounts of power is drawn from the reactor core and supplemental capacitors to the projectors. Accompanied by a lowpitched hum, the gravity well projectors power up.

\n

Minutes pass for the ship uneventfully, until finally a frigate lurches unceremoniously out of hyperspace into realspace in front of them. The ship then activates its tractor beam, trapping its quarry.

\n

Huge starships, regardless of their specific purpose, are the backbone of any military. They provide a mobile base of operations and function as a staging ground for the faction that controls them.

"},"size":"huge","tier":0,"hullDice":"d12","hullDiceStart":9,"hullDiceRolled":[12,7,7,7,7,7,7,7,7],"hullDiceUsed":0,"shldDice":"d12","shldDiceStart":9,"shldDiceRolled":[12,7,7,7,7,7,7,7,7],"shldDiceUsed":0,"pwrDice":"1","buildBaseCost":100000000,"buildMinWorkforce":1000,"upgrdCostMult":100,"upgrdMinWorkforce":500,"baseSpaceSpeed":300,"baseTurnSpeed":100,"crewMinWorkforce":4000,"modBaseCap":60,"modMaxSuitesBase":6,"modMaxSuitesMult":3,"modMaxSuiteCap":4000,"modCostMult":50,"modMinWorkforce":100,"hardpointMult":2,"equipCostMult":50,"equipMinWorkforce":50,"cargoCap":10000,"fuelCost":10000,"fuelCap":600,"foodCap":9600000,"source":"SotG"},"folder":null,"sort":500001,"flags":{},"img":"systems/sw5e/packs/Icons/Starship%20Features/Huge.webp","effects":[{"_id":"5dnyAxPsWqhRxR2v","flags":{"dae":{"stackable":false,"transfer":true}},"changes":[{"key":"data.abilities.dex.value","value":-4,"mode":2,"priority":1},{"key":"data.abilities.con.value","value":4,"mode":2,"priority":1}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"systems/sw5e/packs/Icons/Starship%20Features/Huge.webp","label":"Huge Starship","origin":"Item.h8l5MTU8C1pDIQGL","tint":"","transfer":true}]} -{"_id":"zC4qM8JMmMzCjMJK","name":"Tiny Starship","permission":{"default":0,"yXqD5rPwgjXHtqeZ":3},"type":"starship","data":{"description":{"value":"

As the droid fighter ducked and weaved through flying blaster bolts and dodged the occasional flak, its optical sensors continued to become more and more occluded as they accumulated a dark haze of dust and smoke. Switching to active radar systems, the droid continued to relentlessly pursue its target: the fleeing Jedi and its small padawn that was barely larger than a youngling. Not that it mattered. Whether either target continued to draw breath a few seconds from now didn't really matter to the droid. Of course, that cessation was its goal, but it didn't really care about that goal...it's just what it was doing. What it had to do. But not exactly what it wanted to do. The droid passed power from its fully charged weapon's capacitors into its guns, which blazed to life and spewed plasma towards the small child's back. It was a perfect shot. How could it not be. That's what it did. Then the Jedi's own plasma weapon flared as it darted across the youngling's back, deflecting the droid's blasts directly back at it. Of course, that is what Jedi's did. And as the bolts tore through the droid ship's hull, into its main computer banks, across its power banks and out its engines, the droid's final computations let it know that it was plummeting to the earth at terminal velocity. It's just what it was doing. What it now had to do. But not exactly what it wanted to do.

\n

The small drone slips silently past the blockade, scanning the defensive formations as it goes. The only chance to save the people on embattled Neth-Feeno was to coordinate a supply drop with them. This little, remote-controlled stealth ship was their only hope. It had to reach the surface to get the plan and maps through. Then it had to return with a full readout of the defenses. Only then could a distracting assault be planned to cover the air drop. As the tiny craft floated past the final sensor pod mounted right next to the final turret canon, the monitoring crew let out a sigh, and wiped sweat from their brows. As their screens, already more static than signal, winked out as the craft's transmissions were cut-off as it passed completely into the black-out zone, the team leader turned to his squad and said, \"May the force be with us today.\"

\n

One thing all Tiny starships have in common is that they are unmanned. Sometimes they are controlled remotely, but more often they are controlled by droids.

"},"size":"tiny","tier":0,"hullDice":"d4","hullDiceStart":1,"hullDiceRolled":[4],"hullDiceUsed":0,"shldDice":"d4","shldDiceStart":1,"shldDiceRolled":[4],"shldDiceUsed":0,"pwrDice":"1","buildBaseCost":10000,"buildMinWorkforce":3,"upgrdCostMult":0.5,"upgrdMinWorkforce":1,"baseSpaceSpeed":300,"baseTurnSpeed":300,"crewMinWorkforce":0,"modBaseCap":10,"modMaxSuitesBase":0,"modMaxSuitesMult":0,"modMaxSuiteCap":0,"modCostMult":0.5,"modMinWorkforce":1,"hardpointMult":1,"equipCostMult":0.5,"equipMinWorkforce":1,"cargoCap":0,"fuelCost":25,"fuelCap":5,"foodCap":0,"source":"SotG"},"folder":null,"sort":100001,"flags":{},"img":"systems/sw5e/packs/Icons/Starship%20Features/Tiny.webp","effects":[{"_id":"5BCYRjiFKUZY8ke9","flags":{"dae":{"stackable":false,"transfer":true}},"changes":[{"key":"data.abilities.dex.value","value":4,"mode":2,"priority":1},{"key":"data.abilities.con.value","value":-4,"mode":2,"priority":1}],"disabled":false,"duration":{"startTime":null,"seconds":null,"rounds":null,"turns":null,"startRound":null,"startTurn":null},"icon":"systems/sw5e/packs/Icons/Starship%20Features/Tiny.webp","label":"Tiny Starship","origin":"Item.k1Cxor0HkSEBpkuN","tint":"","transfer":true}]} diff --git a/packs/packs/weapons.db b/packs/packs/weapons.db index 708c8be2..39e87327 100644 --- a/packs/packs/weapons.db +++ b/packs/packs/weapons.db @@ -137,6 +137,7 @@ {"_id":"uQ2AXesizBRcTjRl","name":"Ion Carbine","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Reload 16

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":8,"price":300,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":60,"long":240,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"ammo","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d3 + @mod","ion"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":2600001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Blasters/Ion%20Carbine.webp","effects":[]} {"_id":"v55dQl0raOAucwgP","name":"Vibromace","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":12,"price":80,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":true,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":6400001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Vibroweapons/Vibromace.webp","effects":[]} {"_id":"w62Yd7ahdYyTH61q","name":"Shatter cannon","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Ammunition (range 80/320), Burst 4, Reload 8, Silent, Strength 15, Two-handed

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":24,"price":1300,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":80,"long":320,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":30},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d10","kinetic"]],"versatile":""},"formula":"","save":{"ability":"dex","dc":null,"scaling":"dex"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":true,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":true,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6999220,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/Shatter%20Cannon.webp","effects":[]} +{"_id":"woDLArHK5OZHsTeU","name":"Disguised Blade","permission":{"default":0,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"","chat":"","unidentified":""},"source":"EC","quantity":1,"weight":1,"price":200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","kinetic"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":true,"dgd":true,"dis":false,"dpt":false,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":true,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":false,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":7150001,"flags":{},"img":"systems/sw5e/packs/Icons/Disguised%20Blade,"effects":[]} {"_id":"xfIWfVXfe5ZfD8S2","name":"IWS (Blaster)","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"
\n

The IWS is a heavy weapon that can fire in three different modes. On your turn, you can use your object interaction to switch between modes, detailed below.

\n

Antiarmor. While in this mode, rather than traditional power cells, the IWS fires grenades. When firing a grenade at long range, creatures within the radius of the grenade’s explosion have advantage on the saving throw.

\n

Blaster. While in this mode, the weapon uses traditional power cells.

\n

Sniper. While in this mode, the weapon uses traditional power cells.

\n
\n
Antiarmor: Special, Ammunition (range 60/240), reload 1, special
\n
Blaster: 1d8 Energy, Ammunition (range 80/320), reload 12
\n
Sniper: 1d12 Energy, Ammunition (range 120/480), reload 4
\n
 
\n
Special, Strength 13, Two-handed
","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":12,"price":7200,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"width":null,"units":"","type":"space"},"range":{"value":80,"long":320,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":20},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d8 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialB","properties":{"amm":true,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6650001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Blasters/IWS.webp","effects":[]} {"_id":"y6faozksI3Bhwnpq","name":"Bowcaster","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"

Burst 4, Reload 4, Strength 11

","chat":"","unidentified":""},"source":"PHB","quantity":1,"weight":16,"price":400,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"enemy"},"range":{"value":50,"long":200,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"charges","target":"","amount":1},"ability":"","actionType":"rwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d10 + @mod","energy"]],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"simpleB","properties":{"amm":true,"aut":false,"bur":true,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":false,"dou":false,"fin":false,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":true,"ret":false,"shk":false,"sil":false,"spc":false,"str":true,"thr":false,"two":true,"ver":false,"vic":false,"mgc":false,"nodam":false,"faulldam":false,"fulldam":false},"proficient":false},"folder":"7rtfHBtXhhiRSS8u","sort":800001,"flags":{"dynamiceffects":{"equipActive":false,"alwaysActive":false}},"img":"systems/sw5e/packs/Icons/Simple%20Blasters/Bowcaster.webp","effects":[]} {"_id":"yVxRMON2OWIGeU4n","name":"Disruptorshiv","permission":{"default":0,"MmfWtlBdw3ij5wl9":3},"type":"weapon","data":{"description":{"value":"

Disruptive, Finesse, Shocking 13

","chat":"","unidentified":""},"source":"WH","quantity":1,"weight":1,"price":900,"attunement":0,"equipped":false,"rarity":"","identified":true,"activation":{"type":"action","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":1,"width":null,"units":"","type":"creature"},"range":{"value":5,"long":null,"units":"ft"},"uses":{"value":0,"max":"0","per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"mwak","attackBonus":"0","chatFlavor":"","critical":null,"damage":{"parts":[["1d4 + @mod","kinetic"]],"versatile":""},"formula":"1d4","save":{"ability":"dex","dc":13,"scaling":"flat"},"armor":{"value":10},"hp":{"value":0,"max":0,"dt":null,"conditions":""},"weaponType":"martialVW","properties":{"amm":false,"aut":false,"bur":false,"def":false,"dex":false,"dir":false,"drm":false,"dgd":false,"dis":false,"dpt":true,"dou":false,"fin":true,"fix":false,"foc":false,"hvy":false,"hid":false,"ken":false,"lgt":false,"lum":false,"mig":false,"pic":false,"rap":false,"rch":false,"rel":false,"ret":false,"shk":true,"sil":false,"spc":false,"str":false,"thr":false,"two":false,"ver":false,"vic":false,"nodam":false,"fulldam":false},"proficient":true},"folder":"7rtfHBtXhhiRSS8u","sort":6750001,"flags":{},"img":"systems/sw5e/packs/Icons/Martial%20Vibroweapons/Disruptorshiv.webp","effects":[]} diff --git a/sw5e-dark.css b/sw5e-dark.css index 5506c78c..424da398 100644 --- a/sw5e-dark.css +++ b/sw5e-dark.css @@ -797,14 +797,3 @@ body.dark-theme .sw5e.sheet.actor.npc .swalt-sheet header div.creature-type:hove body.dark-theme .sw5e.sheet.actor.npc .swalt-sheet header .experience { color: #4f4f4f; } -body.dark-theme .sw5e.sheet.actor .swalt-sheet .panel.resources .traits .fuel-wrapper .fuel-label { - background: #D6D6D6; - color: #1C1C1C; - border: 1px solid #1C1C1C; -} -body.dark-theme .sw5e.sheet.actor .swalt-sheet .panel.resources .traits .fuel-wrapper .fuel { - background: #c40f0f; -} -body.dark-theme .sw5e.sheet.actor .swalt-sheet .panel.resources .traits .fuel-wrapper .fuel .fuel-bar { - background: #0dce0d; -} \ No newline at end of file diff --git a/sw5e-global.css b/sw5e-global.css index 9c5943a1..9f890ada 100644 --- a/sw5e-global.css +++ b/sw5e-global.css @@ -1757,78 +1757,3 @@ input[type="reset"]:disabled { transform: rotate(360deg); } } -.sw5e.sheet.actor .swalt-sheet .panel.resources .traits .fuel-wrapper { - display: grid; - grid-template-columns: 300px 100px; - width: 400px; - justify-self: end; -} -.sw5e.sheet.actor .swalt-sheet .panel.resources .traits .fuel-wrapper .fuel-label { - font-size: 12px; - line-height: 14px; - width: 100%; - text-shadow: none; - padding: 0; - margin: 0; - height: auto; - text-align: center; - margin-left: -2px; - border-radius: 0 4px 4px 0; -} -.sw5e.sheet.actor .swalt-sheet .panel.resources .traits .fuel-wrapper .fuel { - position: relative; - border-radius: 4px; - height: 16px; - margin: 0; - width: 100%; -} -.sw5e.sheet.actor .swalt-sheet .panel.resources .traits .fuel-wrapper .fuel .fuel-bar { - position: absolute; - top: 0; - left: 0; - height: 100%; - border-radius: 4px; - border: none; -} -input[type=range][orient=vertical] { - -webkit-appearance: slider-vertical; - width: 10px; - height: 60px !important; - padding: 0 0 !important; - background-color: #c40f0f !important; - box-sizing: border-box; -} -input[type=range][orient=vertical]::-webkit-slider-runnable-track { - -webkit-appearance: slider-vertical !important; - height: 60px !important; - width: 10px !important; - line-height: 60px !important; - padding-top: 0 !important; - padding-bottom: 0 !important; - margin-top: 0 0 !important; - border-radius: 3px !important; - background: linear-gradient( - to top, - #c40f0f 50%, - #0dce0d 50% - ); -} -input[type=range][orient=vertical]::-webkit-slider-thumb { - -webkit-appearance: none !important; - background-color: #c40f0f !important; - margin-right: -4px !important; - margin-top: 0px !important; - cursor: grab !important; - border-radius: 0 0 0 0 !important; - width: 10px !important; - height: 5px !important; - font-size: 10px; -} -output { - display: block; - margin: 5px auto; - font-size:1.75em; -} -input .vertslider { - height: 60px; -} \ No newline at end of file diff --git a/sw5e-light.css b/sw5e-light.css index d1635367..20f09b81 100644 --- a/sw5e-light.css +++ b/sw5e-light.css @@ -784,14 +784,3 @@ body.light-theme .sw5e.sheet.actor.npc .swalt-sheet header div.creature-type:hov body.light-theme .sw5e.sheet.actor.npc .swalt-sheet header .experience { color: #4f4f4f; } -body.light-theme .sw5e.sheet.actor .swalt-sheet .panel.resources .traits .fuel-wrapper .fuel-label { - background: #D6D6D6; - color: #1C1C1C; - border: 1px solid #1C1C1C; -} -body.light-theme .sw5e.sheet.actor .swalt-sheet .panel.resources .traits .fuel-wrapper .fuel { - background: #c40f0f; -} -body.light-theme .sw5e.sheet.actor .swalt-sheet .panel.resources .traits .fuel-wrapper .fuel .fuel-bar { - background: #0dce0d; -} \ No newline at end of file diff --git a/sw5e.css b/sw5e.css index e41c90da..51b2e6f4 100644 --- a/sw5e.css +++ b/sw5e.css @@ -429,7 +429,6 @@ list-style: none; margin: 0; padding: 0; - display: block; } .sw5e.sheet .items-list .item-name { flex: 2; diff --git a/sw5e.js b/sw5e.js index 91c8e45a..3a1f6439 100644 --- a/sw5e.js +++ b/sw5e.js @@ -8,17 +8,17 @@ */ // Import Modules -import { SW5E } from "./module/config.js"; -import { registerSystemSettings } from "./module/settings.js"; -import { preloadHandlebarsTemplates } from "./module/templates.js"; -import { _getInitiativeFormula } from "./module/combat.js"; -import { measureDistances } from "./module/canvas.js"; +import {SW5E} from "./module/config.js"; +import {registerSystemSettings} from "./module/settings.js"; +import {preloadHandlebarsTemplates} from "./module/templates.js"; +import {_getInitiativeFormula} from "./module/combat.js"; +import {measureDistances} from "./module/canvas.js"; // Import Documents import Actor5e from "./module/actor/entity.js"; import Item5e from "./module/item/entity.js"; import CharacterImporter from "./module/characterImporter.js"; -import { TokenDocument5e, Token5e } from "./module/token.js" +import {TokenDocument5e, Token5e} from "./module/token.js"; // Import Applications import AbilityTemplate from "./module/pixi/ability-template.js"; @@ -46,122 +46,137 @@ import * as migrations from "./module/migration.js"; /* Foundry VTT Initialization */ /* -------------------------------------------- */ -// Keep on while migrating to Foundry version 0.8 -CONFIG.debug.hooks = true; +Hooks.once("init", function () { + console.log(`SW5e | Initializing SW5E System\n${SW5E.ASCII}`); -Hooks.once("init", function() { - console.log(`SW5e | Initializing SW5E System\n${SW5E.ASCII}`); + // Create a SW5E namespace within the game global + game.sw5e = { + applications: { + AbilityUseDialog, + ActorSheetFlags, + ActorSheet5eCharacter, + ActorSheet5eCharacterNew, + ActorSheet5eNPC, + ActorSheet5eNPCNew, + ActorSheet5eVehicle, + ItemSheet5e, + ShortRestDialog, + TraitSelector, + ActorMovementConfig, + ActorSensesConfig + }, + canvas: { + AbilityTemplate + }, + config: SW5E, + dice: dice, + entities: { + Actor5e, + Item5e, + TokenDocument5e, + Token5e + }, + macros: macros, + migrations: migrations, + rollItemMacro: macros.rollItemMacro + }; - // Create a SW5E namespace within the game global - game.sw5e = { - applications: { - AbilityUseDialog, - ActorSheetFlags, - ActorSheet5eCharacter, - ActorSheet5eCharacterNew, - ActorSheet5eNPC, - ActorSheet5eNPCNew, - ActorSheet5eVehicle, - ItemSheet5e, - ShortRestDialog, - TraitSelector, - ActorMovementConfig, - ActorSensesConfig - }, - canvas: { - AbilityTemplate - }, - config: SW5E, - dice: dice, - entities: { - Actor5e, - Item5e, - TokenDocument5e, - Token5e, - }, - macros: macros, - migrations: migrations, - rollItemMacro: macros.rollItemMacro - }; + // Record Configuration Values + CONFIG.SW5E = SW5E; + CONFIG.Actor.documentClass = Actor5e; + CONFIG.Item.documentClass = Item5e; + CONFIG.Token.documentClass = TokenDocument5e; + CONFIG.Token.objectClass = Token5e; + CONFIG.time.roundTime = 6; + CONFIG.fontFamilies = ["Engli-Besh", "Open Sans", "Russo One"]; - // Record Configuration Values - CONFIG.SW5E = SW5E; - CONFIG.Actor.documentClass = Actor5e; - CONFIG.Item.documentClass = Item5e; - CONFIG.Token.documentClass = TokenDocument5e; - CONFIG.Token.objectClass = Token5e; - CONFIG.time.roundTime = 6; - CONFIG.fontFamilies = [ - "Engli-Besh", - "Open Sans", - "Russo One" - ]; + CONFIG.Dice.DamageRoll = dice.DamageRoll; + CONFIG.Dice.D20Roll = dice.D20Roll; - CONFIG.Dice.DamageRoll = dice.DamageRoll; - CONFIG.Dice.D20Roll = dice.D20Roll; + // 5e cone RAW should be 53.13 degrees + CONFIG.MeasuredTemplate.defaults.angle = 53.13; - // 5e cone RAW should be 53.13 degrees - CONFIG.MeasuredTemplate.defaults.angle = 53.13; + // Add DND5e namespace for module compatability + game.dnd5e = game.sw5e; + CONFIG.DND5E = CONFIG.SW5E; - // Add DND5e namespace for module compatability - game.dnd5e = game.sw5e; - CONFIG.DND5E = CONFIG.SW5E; + // Register System Settings + registerSystemSettings(); - // Register System Settings - registerSystemSettings(); + // Patch Core Functions + CONFIG.Combat.initiative.formula = "1d20 + @attributes.init.mod + @attributes.init.prof + @attributes.init.bonus"; + Combatant.prototype._getInitiativeFormula = _getInitiativeFormula; - // Patch Core Functions - CONFIG.Combat.initiative.formula = "1d20 + @attributes.init.mod + @attributes.init.prof + @attributes.init.bonus"; - Combatant.prototype._getInitiativeFormula = _getInitiativeFormula; + // Register Roll Extensions + CONFIG.Dice.rolls.push(dice.D20Roll); + CONFIG.Dice.rolls.push(dice.DamageRoll); - // Register Roll Extensions - CONFIG.Dice.rolls.push(dice.D20Roll); - CONFIG.Dice.rolls.push(dice.DamageRoll); + // Register sheet application classes + Actors.unregisterSheet("core", ActorSheet); + Actors.registerSheet("sw5e", ActorSheet5eCharacterNew, { + types: ["character"], + makeDefault: true, + label: "SW5E.SheetClassCharacter" + }); + Actors.registerSheet("sw5e", ActorSheet5eCharacter, { + types: ["character"], + makeDefault: false, + label: "SW5E.SheetClassCharacterOld" + }); + Actors.registerSheet("sw5e", ActorSheet5eNPCNew, { + types: ["npc"], + makeDefault: true, + label: "SW5E.SheetClassNPC" + }); + Actors.registerSheet("sw5e", ActorSheet5eNPC, { + types: ["npc"], + makeDefault: false, + label: "SW5E.SheetClassNPCOld" + }); + // Actors.registerSheet("sw5e", ActorSheet5eStarship, { + // types: ["starship"], + // makeDefault: true, + // label: "SW5E.SheetClassStarship" + // }); + Actors.registerSheet("sw5e", ActorSheet5eVehicle, { + types: ["vehicle"], + makeDefault: true, + label: "SW5E.SheetClassVehicle" + }); + Items.unregisterSheet("core", ItemSheet); + Items.registerSheet("sw5e", ItemSheet5e, { + types: [ + "weapon", + "equipment", + "consumable", + "tool", + "loot", + "class", + "power", + "feat", + "species", + "backpack", + "archetype", + "classfeature", + "background", + "fightingmastery", + "fightingstyle", + "lightsaberform", + "deployment", + "deploymentfeature", + "starship", + "starshipfeature", + "starshipmod", + "venture" + ], + makeDefault: true, + label: "SW5E.SheetClassItem" + }); - // Register sheet application classes - Actors.unregisterSheet("core", ActorSheet); - Actors.registerSheet("sw5e", ActorSheet5eCharacterNew, { - types: ["character"], - makeDefault: true, - label: "SW5E.SheetClassCharacter" - }); - Actors.registerSheet("sw5e", ActorSheet5eCharacter, { - types: ["character"], - makeDefault: false, - label: "SW5E.SheetClassCharacterOld" - }); - Actors.registerSheet("sw5e", ActorSheet5eNPCNew, { - types: ["npc"], - makeDefault: true, - label: "SW5E.SheetClassNPC" - }); - Actors.registerSheet("sw5e", ActorSheet5eNPC, { - types: ["npc"], - makeDefault: false, - label: "SW5E.SheetClassNPCOld" - }); - Actors.registerSheet("sw5e", ActorSheet5eStarship, { - types: ["starship"], - makeDefault: true, - label: "SW5E.SheetClassStarship" - }); - Actors.registerSheet('sw5e', ActorSheet5eVehicle, { - types: ['vehicle'], - makeDefault: true, - label: "SW5E.SheetClassVehicle" - }); - Items.unregisterSheet("core", ItemSheet); - Items.registerSheet("sw5e", ItemSheet5e, { - types: ['weapon', 'equipment', 'consumable', 'tool', 'loot', 'class', 'power', 'feat', 'species', 'backpack', 'archetype', 'classfeature', 'background', 'fightingmastery', 'fightingstyle', 'lightsaberform', 'deployment', 'deploymentfeature', 'starship', 'starshipfeature', 'starshipmod', 'venture'], - makeDefault: true, - label: "SW5E.SheetClassItem" - }); - - // Preload Handlebars Templates - return preloadHandlebarsTemplates(); + // Preload Handlebars Templates + return preloadHandlebarsTemplates(); }); - /* -------------------------------------------- */ /* Foundry VTT Setup */ /* -------------------------------------------- */ @@ -169,138 +184,175 @@ Hooks.once("init", function() { /** * This function runs after game data has been requested and loaded from the servers, so entities exist */ -Hooks.once("setup", function() { +Hooks.once("setup", function () { + // Localize CONFIG objects once up-front + const toLocalize = [ + "abilities", + "abilityAbbreviations", + "abilityActivationTypes", + "abilityConsumptionTypes", + "actorSizes", + "alignments", + "armorProficiencies", + "armorPropertiesTypes", + "conditionTypes", + "consumableTypes", + "cover", + "currencies", + "damageResistanceTypes", + "damageTypes", + "distanceUnits", + "equipmentTypes", + "healingTypes", + "itemActionTypes", + "languages", + "limitedUsePeriods", + "movementTypes", + "movementUnits", + "polymorphSettings", + "proficiencyLevels", + "senses", + "skills", + "starshipRolessm", + "starshipRolesmed", + "starshipRoleslg", + "starshipRoleshuge", + "starshipRolesgrg", + "starshipSkills", + "powerComponents", + "powerLevels", + "powerPreparationModes", + "powerScalingModes", + "powerSchools", + "targetTypes", + "timePeriods", + "toolProficiencies", + "weaponProficiencies", + "weaponProperties", + "weaponSizes", + "weaponTypes" + ]; - // Localize CONFIG objects once up-front - const toLocalize = [ - "abilities", "abilityAbbreviations", "abilityActivationTypes", "abilityConsumptionTypes", "actorSizes", "alignments", - "armorProficiencies", "armorPropertiesTypes", "conditionTypes", "consumableTypes", "cover", "currencies", "damageResistanceTypes", - "damageTypes", "deploymentTypes", "distanceUnits", "equipmentTypes", "healingTypes", "itemActionTypes", "languages", - "limitedUsePeriods", "movementTypes", "movementUnits", "polymorphSettings", "proficiencyLevels", "senses", "skills", - "starshipSkills", "powerComponents", "powerLevels", "powerPreparationModes", "powerScalingModes", "powerSchools", "targetTypes", - "timePeriods", "toolProficiencies", "weaponProficiencies", "weaponProperties", "weaponSizes", "weaponTypes" - ]; + // Exclude some from sorting where the default order matters + const noSort = [ + "abilities", + "alignments", + "currencies", + "distanceUnits", + "movementUnits", + "itemActionTypes", + "proficiencyLevels", + "limitedUsePeriods", + "powerComponents", + "powerLevels", + "powerPreparationModes", + "weaponTypes" + ]; - // Exclude some from sorting where the default order matters - const noSort = [ - "abilities", "alignments", "currencies", "deploymentTypes", "distanceUnits", "movementUnits", "itemActionTypes", "proficiencyLevels", - "limitedUsePeriods", "powerComponents", "powerLevels", "powerPreparationModes", "weaponTypes" - ]; - - // Localize and sort CONFIG objects - for ( let o of toLocalize ) { - const localized = Object.entries(CONFIG.SW5E[o]).map(e => { - return [e[0], game.i18n.localize(e[1])]; - }); - if ( !noSort.includes(o) ) localized.sort((a, b) => a[1].localeCompare(b[1])); - CONFIG.SW5E[o] = localized.reduce((obj, e) => { - obj[e[0]] = e[1]; - return obj; - }, {}); - } - // add DND5E translation for module compatability - game.i18n.translations.DND5E = game.i18n.translations.SW5E; - // console.log(game.settings.get("sw5e", "colorTheme")); - let theme = game.settings.get("sw5e", "colorTheme") + '-theme'; - document.body.classList.add(theme); + // Localize and sort CONFIG objects + for (let o of toLocalize) { + const localized = Object.entries(CONFIG.SW5E[o]).map((e) => { + return [e[0], game.i18n.localize(e[1])]; + }); + if (!noSort.includes(o)) localized.sort((a, b) => a[1].localeCompare(b[1])); + CONFIG.SW5E[o] = localized.reduce((obj, e) => { + obj[e[0]] = e[1]; + return obj; + }, {}); + } + // add DND5E translation for module compatability + game.i18n.translations.DND5E = game.i18n.translations.SW5E; + // console.log(game.settings.get("sw5e", "colorTheme")); + let theme = game.settings.get("sw5e", "colorTheme") + "-theme"; + document.body.classList.add(theme); }); /* -------------------------------------------- */ /** * Once the entire VTT framework is initialized, check to see if we should perform a data migration */ -Hooks.once("ready", function() { +Hooks.once("ready", function () { + // Wait to register hotbar drop hook on ready so that modules could register earlier if they want to + Hooks.on("hotbarDrop", (bar, data, slot) => macros.create5eMacro(data, slot)); - // Wait to register hotbar drop hook on ready so that modules could register earlier if they want to - Hooks.on("hotbarDrop", (bar, data, slot) => macros.create5eMacro(data, slot)); + // Determine whether a system migration is required and feasible + if (!game.user.isGM) return; + const currentVersion = game.settings.get("sw5e", "systemMigrationVersion"); + const NEEDS_MIGRATION_VERSION = "1.3.5.R1-A6"; + // Check for R1 SW5E versions + const SW5E_NEEDS_MIGRATION_VERSION = "R1-A6"; + const COMPATIBLE_MIGRATION_VERSION = 0.8; + const needsMigration = + currentVersion && + (isNewerVersion(SW5E_NEEDS_MIGRATION_VERSION, currentVersion) || + isNewerVersion(NEEDS_MIGRATION_VERSION, currentVersion)); + if (!needsMigration && needsMigration !== "") return; - // Determine whether a system migration is required and feasible - if ( !game.user.isGM ) return; - const currentVersion = game.settings.get("sw5e", "systemMigrationVersion"); - const NEEDS_MIGRATION_VERSION = "1.3.5.R1-A6"; - // Check for R1 SW5E versions - const SW5E_NEEDS_MIGRATION_VERSION = "R1-A6"; - const COMPATIBLE_MIGRATION_VERSION = 0.80; - const needsMigration = currentVersion && (isNewerVersion(SW5E_NEEDS_MIGRATION_VERSION, currentVersion) || isNewerVersion(NEEDS_MIGRATION_VERSION, currentVersion)); - if (!needsMigration && needsMigration !== "") return; - - // Perform the migration - if ( currentVersion && isNewerVersion(COMPATIBLE_MIGRATION_VERSION, currentVersion) ) { - const warning = `Your SW5e system data is from too old a Foundry version and cannot be reliably migrated to the latest version. The process will be attempted, but errors may occur.`; - ui.notifications.error(warning, {permanent: true}); - } - migrations.migrateWorld(); + // Perform the migration + if (currentVersion && isNewerVersion(COMPATIBLE_MIGRATION_VERSION, currentVersion)) { + const warning = `Your SW5e system data is from too old a Foundry version and cannot be reliably migrated to the latest version. The process will be attempted, but errors may occur.`; + ui.notifications.error(warning, {permanent: true}); + } + migrations.migrateWorld(); }); /* -------------------------------------------- */ /* Canvas Initialization */ /* -------------------------------------------- */ -Hooks.on("canvasInit", function() { - // Extend Diagonal Measurement - canvas.grid.diagonalRule = game.settings.get("sw5e", "diagonalMovement"); - SquareGrid.prototype.measureDistances = measureDistances; +Hooks.on("canvasInit", function () { + // Extend Diagonal Measurement + canvas.grid.diagonalRule = game.settings.get("sw5e", "diagonalMovement"); + SquareGrid.prototype.measureDistances = measureDistances; }); - /* -------------------------------------------- */ /* Other Hooks */ /* -------------------------------------------- */ Hooks.on("renderChatMessage", (app, html, data) => { + // Display action buttons + chat.displayChatActionButtons(app, html, data); - // Display action buttons - chat.displayChatActionButtons(app, html, data); + // Highlight critical success or failure die + chat.highlightCriticalSuccessFailure(app, html, data); - // Highlight critical success or failure die - chat.highlightCriticalSuccessFailure(app, html, data); - - // Optionally collapse the content - if (game.settings.get("sw5e", "autoCollapseItemCards")) html.find(".card-content").hide(); + // Optionally collapse the content + if (game.settings.get("sw5e", "autoCollapseItemCards")) html.find(".card-content").hide(); }); Hooks.on("getChatLogEntryContext", chat.addChatMessageContextOptions); Hooks.on("renderChatLog", (app, html, data) => Item5e.chatListeners(html)); Hooks.on("renderChatPopout", (app, html, data) => Item5e.chatListeners(html)); -Hooks.on('getActorDirectoryEntryContext', Actor5e.addDirectoryContextOptions); -Hooks.on("renderSceneDirectory", (app, html, data)=> { - //console.log(html.find("header.folder-header")); - setFolderBackground(html); +Hooks.on("getActorDirectoryEntryContext", Actor5e.addDirectoryContextOptions); +Hooks.on("renderSceneDirectory", (app, html, data) => { + //console.log(html.find("header.folder-header")); + setFolderBackground(html); }); -Hooks.on("renderActorDirectory", (app, html, data)=> { - setFolderBackground(html); - CharacterImporter.addImportButton(html); +Hooks.on("renderActorDirectory", (app, html, data) => { + setFolderBackground(html); + CharacterImporter.addImportButton(html); }); -Hooks.on("renderItemDirectory", (app, html, data)=> { - setFolderBackground(html); +Hooks.on("renderItemDirectory", (app, html, data) => { + setFolderBackground(html); }); -Hooks.on("renderJournalDirectory", (app, html, data)=> { - setFolderBackground(html); +Hooks.on("renderJournalDirectory", (app, html, data) => { + setFolderBackground(html); }); -Hooks.on("renderRollTableDirectory", (app, html, data)=> { - setFolderBackground(html); +Hooks.on("renderRollTableDirectory", (app, html, data) => { + setFolderBackground(html); }); Hooks.on("ActorSheet5eCharacterNew", (app, html, data) => { - console.log("renderSwaltSheet"); + console.log("renderSwaltSheet"); }); // FIXME: This helper is needed for the vehicle sheet. It should probably be refactored. -Handlebars.registerHelper('getProperty', function (data, property) { - return getProperty(data, property); +Handlebars.registerHelper("getProperty", function (data, property) { + return getProperty(data, property); }); -Handlebars.registerHelper('round', function(value) { - return Math.floor(value); -}); - -Handlebars.registerHelper('debug', function(value) { - console.log(value) - return value; -}) - function setFolderBackground(html) { - html.find("header.folder-header").each(function() { - let bgColor = $(this).css("background-color"); - if(bgColor == undefined) - bgColor = "rgb(255,255,255)"; - $(this).closest('li').css("background-color", bgColor); - }) -} \ No newline at end of file + html.find("header.folder-header").each(function () { + let bgColor = $(this).css("background-color"); + if (bgColor == undefined) bgColor = "rgb(255,255,255)"; + $(this).closest("li").css("background-color", bgColor); + }); +} diff --git a/system.json b/system.json index ca98cd25..3a0a82ca 100644 --- a/system.json +++ b/system.json @@ -109,42 +109,6 @@ "label": "Species Traits", "path": "./packs/packs/speciestraits.db", "entity": "Item" - }, - { - "name": "starshiparmor", - "label": "Starship Armor", - "path": "./packs/packs/starshiparmor.db", - "entity": "Item" - }, - { - "name": "starshipequipment", - "label": "Starship Equipment", - "path": "./packs/packs/starshipequipment.db", - "entity": "Item" - }, - { - "name": "starshipfeatures", - "label": "Starship Features", - "path": "./packs/packs/starshipfeatures.db", - "entity": "Item" - }, - { - "name": "starshipmodifications", - "label": "Starship Modifications", - "path": "./packs/packs/starshipmodifications.db", - "entity": "Item" - }, - { - "name": "starships", - "label": "Starship Types", - "path": "./packs/packs/starships.db", - "entity": "Item" - }, - { - "name": "starshipweapons", - "label": "Starship Weapons", - "path": "./packs/packs/starshipweapons.db", - "entity": "Item" }, { "name": "tables", diff --git a/template.json b/template.json index 2eb68c9a..1681d52e 100644 --- a/template.json +++ b/template.json @@ -1,6 +1,6 @@ { "Actor": { - "types": ["character", "npc", "starship", "vehicle"], + "types": ["character", "npc", "vehicle"], "templates": { "common": { "abilities": { @@ -37,8 +37,8 @@ "value": 10, "min": 0, "max": 10, - "temp": null, - "tempmax": null + "temp": 0, + "tempmax": 0 }, "init": { "value": 0, @@ -89,15 +89,6 @@ }, "creature": { "attributes": { - "rank": { - "total": 0, - "coord": 0, - "gunner": 0, - "mechanic": 0, - "operator": 0, - "pilot": 0, - "technician": 0 - }, "senses": { "darkvision": 0, "blindsight": 0, @@ -425,119 +416,31 @@ "starship": { "templates": ["common"], "attributes": { - "cost": { - "baseBuild": 0, - "baseUpgrade": 0, - "multEquip": 0, - "multModification": 0, - "multUpgrade": 0 - }, + "cargcap": 0, + "crewcap": 0, + "cscap": 0, "death": { "failure": 0, "success": 0 }, - "deployment": { - "coord": { - "uuid": null, - "name": null, - "rank": null, - "prof": null - }, - "gunner": { - "uuid": null, - "name": null, - "rank": null, - "prof": null - }, - "mechanic": { - "uuid": null, - "name": null, - "rank": null, - "prof": null - }, - "operator": { - "uuid": null, - "name": null, - "rank": null, - "prof": null - }, - "pilot": { - "uuid": null, - "name": null, - "rank": null, - "prof": null - }, - "technician": { - "uuid": null, - "name": null, - "rank": null, - "prof": null - }, - "crew": [], - "passenger": [] - }, - "equip": { - "armor": { - "dr": 0, - "maxDex": 99, - "stealthDisadv": false - }, - "hyperdrive": { - "class": null - }, - "powerCoupling": { - "centralCap": 0, - "systemCap": 0 - }, - "reactor": { - "fuelMult": 1, - "powerRecDie": "1" - }, - "size": { - "cargoCap": 0, - "crewMinWorkforce": 0, - "foodCap": 0 - }, - "shields": { - "capMult": 1, - "regenRateMult": 1 - } - }, - "systemDamage": 0, - "fuel": { - "cap": 0, - "cost": 0, - "value": 0 - }, + "dr": 0, + "engpow": 1, + "exhaustion": 0, + "hsm": 1, "hull": { "die": "", "dice": 0, - "dicemax": 0, "formula":"", "value": null, "max": null }, "mods": { - "capUsed": 0, - "capLimit": 10, - "hardpoints":{ - "open": 0, - "max": 0 - }, - "installed": 0, - "suites": { - "open": 0, - "max": 0, - "cap": 0 - } + "open": 10, + "max": 10 }, - "power": { - "die": "", - "routing":{ - "engines": 1, - "shields": 1, - "weapons": 1 - }, + "pwrdice": { + "pwrdie": "", + "recovery": 1, "central": { "value": 0, "max": 0 @@ -566,24 +469,21 @@ "shld": { "die": "", "dice": 0, - "dicemax": 0, - "depleted": false, "formula":"", "value": null, "max": null }, - "used": false, - "workforce": { - "max": 0, - "minBuild": 0, - "minEquip": 0, - "minModification": 0, - "minUpgrade": 0 - } + "shieldpow": 1, + "sscap": 0, + "suites": { + "open": 0, + "max": 0 + }, + "weaponpow": 1 }, "details": { "tier": 0, - "role": [], + "role": "", "source": "" }, "skills": { @@ -607,7 +507,7 @@ "value": 0, "ability": "cha" }, - "inf": { + "int": { "value": 0, "ability": "cha" }, @@ -645,7 +545,7 @@ } }, "traits": { - "size": null + "size": "med" } }, "vehicle": { @@ -932,7 +832,7 @@ "capx": { "value": null }, - "dmgred": { + "hpperhd": { "value": null }, "regrateco": { @@ -1115,34 +1015,12 @@ "size": "", "tier": 0, "hullDice": "d6", - "hullDiceStart": 3, - "hullDiceRolled":[6,4,4], + "hullDiceStart": 1, "hullDiceUsed": 0, "shldDice": "d6", - "shldDiceStart": 3, - "shldDiceRolled":[6,4,4], + "shldDiceStart": 1, "shldDiceUsed": 0, "pwrDice": "1", - "buildBaseCost": 50000, - "buildMinWorkforce": 5, - "upgrdCostMult": 1, - "upgrdMinWorkforce": 1, - "baseSpaceSpeed": 300, - "baseTurnSpeed": 250, - "crewMinWorkforce": 1, - "modBaseCap": 20, - "modMaxSuitesBase": 0, - "modMaxSuitesMult": 1, - "modMaxSuiteCap": 1, - "modCostMult": 1, - "modMinWorkforce": 2, - "hardpointMult": 2, - "equipCostMult": 1, - "equipMinWorkforce": 1, - "cargoCap": 2, - "fuelCost": 50, - "fuelCap": 10, - "foodCap": 10, "source": "SotG" }, "starshipfeature": { diff --git a/templates/actors/newActor/parts/swalt-crew.html b/templates/actors/newActor/parts/swalt-crew.html index 423f7cc4..16c50787 100644 --- a/templates/actors/newActor/parts/swalt-crew.html +++ b/templates/actors/newActor/parts/swalt-crew.html @@ -8,26 +8,6 @@
  • {{localize "SW5E.Reaction"}}
  • -
      -
    1. Coordinator: {{data.attributes.deployment.coord.name}}
    2. -
    3. Rank: {{data.attributes.deployment.coord.rank}}
    4. -
    5. Prof: {{data.attributes.deployment.coord.prof}}
    6. -
    7. Gunner: {{data.attributes.deployment.gunner.name}}
    8. -
    9. Rank: {{data.attributes.deployment.gunner.rank}}
    10. -
    11. Prof: {{data.attributes.deployment.gunner.prof}}
    12. -
    13. Mechanic: {{data.attributes.deployment.mechanic.name}}
    14. -
    15. Rank: {{data.attributes.deployment.mechanic.rank}}
    16. -
    17. Prof: {{data.attributes.deployment.mechanic.prof}}
    18. -
    19. Operator: {{data.attributes.deployment.operator.name}}
    20. -
    21. Rank: {{data.attributes.deployment.operator.rank}}
    22. -
    23. Prof: {{data.attributes.deployment.operator.prof}}
    24. -
    25. Pilot: {{data.attributes.deployment.pilot.name}}
    26. -
    27. Rank: {{data.attributes.deployment.pilot.rank}}
    28. -
    29. Prof: {{data.attributes.deployment.pilot.prof}}
    30. -
    31. Technician: {{data.attributes.deployment.technician.name}}
    32. -
    33. Rank: {{data.attributes.deployment.technician.rank}}
    34. -
    35. Prof: {{data.attributes.deployment.technician.prof}}
    36. -
      {{#each sections as |section sid|}} diff --git a/templates/actors/newActor/starship.html b/templates/actors/newActor/starship.html index 3a5b9d32..5738742c 100644 --- a/templates/actors/newActor/starship.html +++ b/templates/actors/newActor/starship.html @@ -14,8 +14,14 @@
      + {{lookup config.actorSizes data.traits.size}} - + + {{lookup config.starshipRolessm data.details.role}} +
      {{!-- ARMOR CLASS --}} @@ -25,11 +31,10 @@
      -
      - - - -
      +
      + {{ localize "SW5E.Proficiency" }} + {{numberFormat data.attributes.prof decimals=0 sign=true}} +
      {{!-- HULL POINTS --}} @@ -37,15 +42,14 @@

      {{ localize "SW5E.HullPoints" }}

      + data-dtype="Number" placeholder="0" class="value-number" /> / + data-dtype="Number" placeholder="0" class="value-number" />
      -