forked from GitHub-Mirrors/foundry-sw5e
Compare commits
15 commits
master
...
Develop-VJ
Author | SHA1 | Date | |
---|---|---|---|
![]() |
ad6c694d16 | ||
![]() |
5fe2740b5a | ||
![]() |
4952f85df6 | ||
![]() |
657102a9f3 | ||
![]() |
9f1e3c213c | ||
![]() |
d21898a8da | ||
![]() |
89c7f7d7e7 | ||
![]() |
3a7eafe267 | ||
![]() |
d7879fad94 | ||
![]() |
3f4c8119ad | ||
![]() |
40641aed95 | ||
![]() |
a3d4bc69b0 | ||
![]() |
b3007aeecb | ||
![]() |
8b60052f65 | ||
![]() |
ea0a874e38 |
71 changed files with 10716 additions and 11997 deletions
|
@ -11,4 +11,4 @@
|
|||
"jsxBracketSameLine": false,
|
||||
"arrowParens": "always",
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
}
|
||||
|
|
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
|
@ -1,3 +1,2 @@
|
|||
{
|
||||
"editor.formatOnSave": true
|
||||
}
|
10
gulpfile.js
10
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);
|
||||
}
|
||||
|
||||
/* ----------------------------------------- */
|
||||
|
|
85
lang/en.json
85
lang/en.json
|
@ -19,10 +19,11 @@
|
|||
"ITEM.TypeLoot": "Loot",
|
||||
"ITEM.TypePower": "Power",
|
||||
"ITEM.TypeSpecies": "Species",
|
||||
"ITEM.TypeStarshipfeature": "Starship Feature",
|
||||
"ITEM.TypeStarshipfeaturePl": "Starship Features",
|
||||
"ITEM.TypeStarshipmod": "Starship Modification",
|
||||
"ITEM.TypeStarshipmodPl": "Starship Modifications",
|
||||
"ITEM.TypeStarship": "Starship",
|
||||
"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",
|
||||
|
@ -193,7 +194,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",
|
||||
|
@ -330,8 +331,18 @@
|
|||
"SW5E.DeathSavingThrow": "Death Saving Throw",
|
||||
"SW5E.Default": "Default",
|
||||
"SW5E.DefaultAbilityCheck": "Default Ability Check",
|
||||
"SW5E.Deployment": "Deployment",
|
||||
"SW5E.DeploymentPl": "Deployments",
|
||||
"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.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",
|
||||
|
@ -521,12 +532,13 @@
|
|||
"SW5E.Flaws": "Flaws",
|
||||
"SW5E.ForcePowerbook": "Force Powers",
|
||||
"SW5E.Formula": "Formula",
|
||||
"SW5E.FuelCapacity": "Fuel Capacity",
|
||||
"SW5E.FuelCostPerUnit": "Fuel Cost per Unit",
|
||||
"SW5E.FuelCapacity": "Fuel Capacity",
|
||||
"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",
|
||||
|
@ -542,9 +554,12 @@
|
|||
"SW5E.HitDiceWarn": "{name} has no available {formula} Hit Dice remaining!",
|
||||
"SW5E.HP": "Health",
|
||||
"SW5E.HPFormula": "Health Formula",
|
||||
"SW5E.HullDice": "Hull Dice",
|
||||
"SW5E.HullPoints": "Hull Points",
|
||||
"SW5E.HullPointsFormula": "Hull Points 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.HyperdriveClass": "Hyperdrive Class",
|
||||
"SW5E.Ideals": "Ideals",
|
||||
"SW5E.Identified": "Identified",
|
||||
|
@ -618,8 +633,9 @@
|
|||
"SW5E.ItemTypePowerPl": "Powers",
|
||||
"SW5E.ItemTypeSpecies": "Species",
|
||||
"SW5E.ItemTypeSpeciesPl": "Species",
|
||||
"SW5E.ItemTypeStarshipMod": "Starship Modification",
|
||||
"SW5E.ItemTypeStarshipModPl": "Starship Modifications",
|
||||
"SW5E.ItemTypeStarship": "Starship",
|
||||
"SW5E.ItemTypeStarshipMod": "Starship Modification",
|
||||
"SW5E.ItemTypeStarshipModPl": "Starship Modifications",
|
||||
"SW5E.ItemTypeTool": "Tool",
|
||||
"SW5E.ItemTypeToolPl": "Tools",
|
||||
"SW5E.ItemTypeVenture": "Venture",
|
||||
|
@ -786,8 +802,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",
|
||||
|
@ -847,10 +863,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",
|
||||
|
@ -900,7 +916,23 @@
|
|||
"SW5E.Reaction": "Reaction",
|
||||
"SW5E.ReactionPl": "Reactions",
|
||||
"SW5E.Recharge": "Recharge",
|
||||
"SW5E.Refitting": "Refitting",
|
||||
"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.Refuel": "Refuel",
|
||||
"SW5E.RegenerationRateCoefficient": "Regeneration Rate Coefficient",
|
||||
"SW5E.RequiredMaterials": "Required Materials",
|
||||
|
@ -948,10 +980,13 @@
|
|||
"SW5E.SheetClassNPC": "Default NPC Sheet",
|
||||
"SW5E.SheetClassNPCOld": "Old NPC Sheet",
|
||||
"SW5E.SheetClassVehicle": "Default Vehicle Sheet",
|
||||
"SW5E.ShieldDice": "Shield Dice",
|
||||
"SW5E.ShieldPoints": "Shield Points",
|
||||
"SW5E.ShieldPointsFormula": "Shield Points Formula",
|
||||
"SW5E.ShieldRegen": "Regen",
|
||||
"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.ShortRest": "Short Rest",
|
||||
"SW5E.ShortRestEpic": "Short Rest (5 minutes)",
|
||||
"SW5E.ShortRestGritty": "Short Rest (8 hours)",
|
||||
|
@ -1039,7 +1074,7 @@
|
|||
"SW5E.StarshipSkillDat": "Data",
|
||||
"SW5E.StarshipSkillHid": "Hide",
|
||||
"SW5E.StarshipSkillImp": "Impress",
|
||||
"SW5E.StarshipSkillInt": "Interfere",
|
||||
"SW5E.StarshipSkillInf": "Interfere",
|
||||
"SW5E.StarshipSkillMan": "Maneuvering",
|
||||
"SW5E.StarshipSkillMen": "Menace",
|
||||
"SW5E.StarshipSkillPat": "Patch",
|
||||
|
|
|
@ -671,6 +671,77 @@
|
|||
.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 {
|
||||
|
|
|
@ -250,7 +250,45 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.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);
|
||||
|
@ -1123,4 +1161,44 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,6 +34,28 @@ 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;
|
||||
|
|
|
@ -34,6 +34,28 @@ 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;
|
||||
|
|
|
@ -120,6 +120,9 @@ 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);
|
||||
|
||||
|
@ -225,7 +228,11 @@ 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)) || [];
|
||||
}
|
||||
|
||||
|
@ -358,15 +365,87 @@ 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;
|
||||
}
|
||||
|
||||
// Proficiency
|
||||
data.attributes.prof = Math.floor((Math.max(data.details.tier, 1) + 7) / 4);
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -734,10 +813,65 @@ 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);
|
||||
|
@ -910,7 +1044,9 @@ export default class Actor5e extends Actor {
|
|||
const label = CONFIG.SW5E.abilities[abilityId];
|
||||
new Dialog({
|
||||
title: game.i18n.format("SW5E.AbilityPromptTitle", {ability: label}),
|
||||
content: `<p>${game.i18n.format("SW5E.AbilityPromptText", {ability: label})}</p>`,
|
||||
content: `<p>${game.i18n.format("SW5E.AbilityPromptText", {
|
||||
ability: label
|
||||
})}</p>`,
|
||||
buttons: {
|
||||
test: {
|
||||
label: game.i18n.localize("SW5E.ActionAbil"),
|
||||
|
@ -1097,13 +1233,18 @@ 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";
|
||||
|
@ -1112,7 +1253,10 @@ 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);
|
||||
}
|
||||
|
@ -1149,7 +1293,12 @@ 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;
|
||||
}
|
||||
|
||||
|
@ -1184,6 +1333,218 @@ 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<Roll|null>} 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<Roll|null>} 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<Roll|null>} 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.
|
||||
*
|
||||
|
@ -1217,7 +1578,10 @@ 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;
|
||||
}
|
||||
|
@ -1314,7 +1678,10 @@ export default class Actor5e extends Actor {
|
|||
},
|
||||
updateItems: [
|
||||
...hitDiceUpdates,
|
||||
...this._getRestItemUsesRecovery({recoverLongRestUses: longRest, recoverDailyUses: newDay})
|
||||
...this._getRestItemUsesRecovery({
|
||||
recoverLongRestUses: longRest,
|
||||
recoverDailyUses: newDay
|
||||
})
|
||||
],
|
||||
newDay: newDay
|
||||
};
|
||||
|
@ -1537,7 +1904,10 @@ 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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1578,6 +1948,96 @@ 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.
|
||||
*
|
||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -6,154 +6,143 @@ 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 */
|
||||
get template() {
|
||||
if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/newActor/limited-sheet.html";
|
||||
return `systems/sw5e/templates/actors/newActor/npc-sheet.html`;
|
||||
}
|
||||
/** @override */
|
||||
static get defaultOptions() {
|
||||
return mergeObject(super.defaultOptions, {
|
||||
classes: ["sw5e", "sheet", "actor", "npc"],
|
||||
width: 800,
|
||||
tabs: [{
|
||||
navSelector: ".root-tabs",
|
||||
contentSelector: ".sheet-body",
|
||||
initial: "attributes"
|
||||
}],
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
static unsupportedItemTypes = new Set(["class"]);
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Organize Owned Items for rendering the NPC sheet
|
||||
* @private
|
||||
*/
|
||||
_prepareItems(data) {
|
||||
|
||||
// Categorize Items as Features and Powers
|
||||
const features = {
|
||||
weapons: { label: game.i18n.localize("SW5E.AttackPl"), items: [] , hasActions: true, dataset: {type: "weapon", "weapon-type": "natural"} },
|
||||
actions: { label: game.i18n.localize("SW5E.ActionPl"), items: [] , hasActions: true, dataset: {type: "feat", "activation.type": "action"} },
|
||||
passive: { label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"} },
|
||||
equipment: { label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}}
|
||||
};
|
||||
|
||||
// Start by classifying items into groups for rendering
|
||||
let [forcepowers, techpowers, other] = data.items.reduce((arr, item) => {
|
||||
item.img = item.img || CONST.DEFAULT_TOKEN;
|
||||
item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
|
||||
item.hasUses = item.data.uses && (item.data.uses.max > 0);
|
||||
item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false);
|
||||
item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0));
|
||||
item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type));
|
||||
if ( item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school) ) arr[0].push(item);
|
||||
else if ( item.type === "power" && ["tec"].includes(item.data.school) ) arr[1].push(item);
|
||||
else arr[2].push(item);
|
||||
return arr;
|
||||
}, [[], [], []]);
|
||||
|
||||
// Apply item filters
|
||||
forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook);
|
||||
techpowers = this._filterItems(techpowers, this._filters.techPowerbook);
|
||||
other = this._filterItems(other, this._filters.features);
|
||||
|
||||
// Organize Powerbook
|
||||
const forcePowerbook = this._preparePowerbook(data, forcepowers, "uni");
|
||||
const techPowerbook = this._preparePowerbook(data, techpowers, "tec");
|
||||
|
||||
// Organize Features
|
||||
for ( let item of other ) {
|
||||
if ( item.type === "weapon" ) features.weapons.items.push(item);
|
||||
else if ( item.type === "feat" ) {
|
||||
if ( item.data.activation.type ) features.actions.items.push(item);
|
||||
else features.passive.items.push(item);
|
||||
}
|
||||
else features.equipment.items.push(item);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
// Assign and return
|
||||
data.features = Object.values(features);
|
||||
data.forcePowerbook = forcePowerbook;
|
||||
data.techPowerbook = techPowerbook;
|
||||
}
|
||||
|
||||
/** @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 [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;
|
||||
},
|
||||
[[], [], []]
|
||||
);
|
||||
// Creature Type
|
||||
data.labels["type"] = this.actor.labels.creatureType;
|
||||
return data;
|
||||
}
|
||||
|
||||
// Apply item filters
|
||||
forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook);
|
||||
techpowers = this._filterItems(techpowers, this._filters.techPowerbook);
|
||||
other = this._filterItems(other, this._filters.features);
|
||||
/* -------------------------------------------- */
|
||||
/* Object Updates */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
// Organize Powerbook
|
||||
const forcePowerbook = this._preparePowerbook(data, forcepowers, "uni");
|
||||
const techPowerbook = this._preparePowerbook(data, techpowers, "tec");
|
||||
/** @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.forcePowerbook = forcePowerbook;
|
||||
data.techPowerbook = techPowerbook;
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* 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});
|
||||
}
|
||||
/**
|
||||
* 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});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,164 +6,248 @@ 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,
|
||||
tabs: [
|
||||
{
|
||||
navSelector: ".root-tabs",
|
||||
contentSelector: ".sheet-body",
|
||||
initial: "attributes"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
/** @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);
|
||||
}
|
||||
|
||||
// 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});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* 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"}
|
||||
}
|
||||
};
|
||||
/**
|
||||
* 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});
|
||||
}
|
||||
|
||||
// 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});
|
||||
}
|
||||
_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});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,427 +6,411 @@ 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
|
||||
});
|
||||
/**
|
||||
* 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 = '—';
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
// 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'
|
||||
}];
|
||||
|
||||
/**
|
||||
* Creates a new cargo entry for a vehicle Actor.
|
||||
*/
|
||||
static get newCargo() {
|
||||
return {
|
||||
name: "",
|
||||
quantity: 1
|
||||
};
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
/* -------------------------------------------- */
|
||||
/* Event Listeners and Handlers */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
// Vehicle weights are an order of magnitude greater.
|
||||
totalWeight /= CONFIG.SW5E.encumbrance.vehicleWeightMultiplier;
|
||||
/** @override */
|
||||
activateListeners(html) {
|
||||
super.activateListeners(html);
|
||||
if (!this.isEditable) return;
|
||||
|
||||
// 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};
|
||||
html.find('.item-toggle').click(this._onToggleItem.bind(this));
|
||||
html.find('.item-hp input')
|
||||
.click(evt => evt.target.select())
|
||||
.change(this._onHPChange.bind(this));
|
||||
|
||||
html.find('.item:not(.cargo-row) input[data-property]')
|
||||
.click(evt => evt.target.select())
|
||||
.change(this._onEditInSheet.bind(this));
|
||||
|
||||
html.find('.cargo-row input')
|
||||
.click(evt => evt.target.select())
|
||||
.change(this._onCargoRowChange.bind(this));
|
||||
|
||||
if (this.actor.data.data.attributes.actions.stations) {
|
||||
html.find('.counter.actions, .counter.action-thresholds').hide();
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle saving a cargo row (i.e. crew or passenger) in-sheet.
|
||||
* @param event {Event}
|
||||
* @returns {Promise<Actor>|null}
|
||||
* @private
|
||||
*/
|
||||
_onCargoRowChange(event) {
|
||||
event.preventDefault();
|
||||
const target = event.currentTarget;
|
||||
const row = target.closest('.item');
|
||||
const idx = Number(row.dataset.itemId);
|
||||
const property = row.classList.contains('crew') ? 'crew' : 'passengers';
|
||||
|
||||
// Get the cargo entry
|
||||
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[property]);
|
||||
const entry = cargo[idx];
|
||||
if (!entry) return null;
|
||||
|
||||
// Update the cargo value
|
||||
const key = target.dataset.property || 'name';
|
||||
const type = target.dataset.dtype;
|
||||
let value = target.value;
|
||||
if (type === 'Number') value = Number(value);
|
||||
entry[key] = value;
|
||||
|
||||
// Perform the Actor update
|
||||
return this.actor.update({[`data.cargo.${property}`]: cargo});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle editing certain values like quantity, price, and weight in-sheet.
|
||||
* @param event {Event}
|
||||
* @returns {Promise<Item>}
|
||||
* @private
|
||||
*/
|
||||
_onEditInSheet(event) {
|
||||
event.preventDefault();
|
||||
const itemID = event.currentTarget.closest('.item').dataset.itemId;
|
||||
const item = this.actor.items.get(itemID);
|
||||
const property = event.currentTarget.dataset.property;
|
||||
const type = event.currentTarget.dataset.dtype;
|
||||
let value = event.currentTarget.value;
|
||||
switch (type) {
|
||||
case 'Number': value = parseInt(value); break;
|
||||
case 'Boolean': value = value === 'true'; break;
|
||||
}
|
||||
return item.update({[`${property}`]: value});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle creating a new crew or passenger row.
|
||||
* @param event {Event}
|
||||
* @returns {Promise<Actor|Item>}
|
||||
* @private
|
||||
*/
|
||||
_onItemCreate(event) {
|
||||
event.preventDefault();
|
||||
const target = event.currentTarget;
|
||||
const type = target.dataset.type;
|
||||
if (type === 'crew' || type === 'passengers') {
|
||||
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]);
|
||||
cargo.push(this.constructor.newCargo);
|
||||
return this.actor.update({[`data.cargo.${type}`]: cargo});
|
||||
}
|
||||
return super._onItemCreate(event);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle deleting a crew or passenger row.
|
||||
* @param event {Event}
|
||||
* @returns {Promise<Actor|Item>}
|
||||
* @private
|
||||
*/
|
||||
_onItemDelete(event) {
|
||||
event.preventDefault();
|
||||
const row = event.currentTarget.closest('.item');
|
||||
if (row.classList.contains('cargo-row')) {
|
||||
const idx = Number(row.dataset.itemId);
|
||||
const type = row.classList.contains('crew') ? 'crew' : 'passengers';
|
||||
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx);
|
||||
return this.actor.update({[`data.cargo.${type}`]: cargo});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
return super._onItemDelete(event);
|
||||
}
|
||||
|
||||
/** @override */
|
||||
_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"}`);
|
||||
/* -------------------------------------------- */
|
||||
|
||||
// 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 = "—";
|
||||
}
|
||||
/**
|
||||
* Special handling for editing HP to clamp it within appropriate range.
|
||||
* @param event {Event}
|
||||
* @returns {Promise<Item>}
|
||||
* @private
|
||||
*/
|
||||
_onHPChange(event) {
|
||||
event.preventDefault();
|
||||
const itemID = event.currentTarget.closest('.item').dataset.itemId;
|
||||
const item = this.actor.items.get(itemID);
|
||||
const hp = Math.clamped(0, parseInt(event.currentTarget.value), item.data.data.hp.max);
|
||||
event.currentTarget.value = hp;
|
||||
return item.update({'data.hp.value': hp});
|
||||
}
|
||||
|
||||
// Prepare vehicle weapons
|
||||
if (item.type === "equipment" || item.type === "weapon") {
|
||||
item.threshold = item.data.hp.dt ? item.data.hp.dt : "—";
|
||||
}
|
||||
}
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Organize Owned Items for rendering the Vehicle sheet.
|
||||
* @private
|
||||
*/
|
||||
_prepareItems(data) {
|
||||
const cargoColumns = [
|
||||
{
|
||||
label: game.i18n.localize("SW5E.Quantity"),
|
||||
css: "item-qty",
|
||||
property: "quantity",
|
||||
editable: "Number"
|
||||
}
|
||||
];
|
||||
|
||||
const equipmentColumns = [
|
||||
{
|
||||
label: game.i18n.localize("SW5E.Quantity"),
|
||||
css: "item-qty",
|
||||
property: "data.quantity"
|
||||
},
|
||||
{
|
||||
label: game.i18n.localize("SW5E.AC"),
|
||||
css: "item-ac",
|
||||
property: "data.armor.value"
|
||||
},
|
||||
{
|
||||
label: game.i18n.localize("SW5E.HP"),
|
||||
css: "item-hp",
|
||||
property: "data.hp.value",
|
||||
editable: "Number"
|
||||
},
|
||||
{
|
||||
label: game.i18n.localize("SW5E.Threshold"),
|
||||
css: "item-threshold",
|
||||
property: "threshold"
|
||||
}
|
||||
];
|
||||
|
||||
const features = {
|
||||
actions: {
|
||||
label: game.i18n.localize("SW5E.ActionPl"),
|
||||
items: [],
|
||||
crewable: true,
|
||||
dataset: {"type": "feat", "activation.type": "crew"},
|
||||
columns: [
|
||||
{
|
||||
label: game.i18n.localize("SW5E.VehicleCrew"),
|
||||
css: "item-crew",
|
||||
property: "crew"
|
||||
},
|
||||
{
|
||||
label: game.i18n.localize("SW5E.Cover"),
|
||||
css: "item-cover",
|
||||
property: "cover"
|
||||
}
|
||||
]
|
||||
},
|
||||
equipment: {
|
||||
label: game.i18n.localize("SW5E.ItemTypeEquipment"),
|
||||
items: [],
|
||||
crewable: true,
|
||||
dataset: {"type": "equipment", "armor.type": "vehicle"},
|
||||
columns: equipmentColumns
|
||||
},
|
||||
passive: {
|
||||
label: game.i18n.localize("SW5E.Features"),
|
||||
items: [],
|
||||
dataset: {type: "feat"}
|
||||
},
|
||||
reactions: {
|
||||
label: game.i18n.localize("SW5E.ReactionPl"),
|
||||
items: [],
|
||||
dataset: {"type": "feat", "activation.type": "reaction"}
|
||||
},
|
||||
weapons: {
|
||||
label: game.i18n.localize("SW5E.ItemTypeWeaponPl"),
|
||||
items: [],
|
||||
crewable: true,
|
||||
dataset: {"type": "weapon", "weapon-type": "siege"},
|
||||
columns: equipmentColumns
|
||||
}
|
||||
};
|
||||
|
||||
const cargo = {
|
||||
crew: {
|
||||
label: game.i18n.localize("SW5E.VehicleCrew"),
|
||||
items: data.data.cargo.crew,
|
||||
css: "cargo-row crew",
|
||||
editableName: true,
|
||||
dataset: {type: "crew"},
|
||||
columns: cargoColumns
|
||||
},
|
||||
passengers: {
|
||||
label: game.i18n.localize("SW5E.VehiclePassengers"),
|
||||
items: data.data.cargo.passengers,
|
||||
css: "cargo-row passengers",
|
||||
editableName: true,
|
||||
dataset: {type: "passengers"},
|
||||
columns: cargoColumns
|
||||
},
|
||||
cargo: {
|
||||
label: game.i18n.localize("SW5E.VehicleCargo"),
|
||||
items: [],
|
||||
dataset: {type: "loot"},
|
||||
columns: [
|
||||
{
|
||||
label: game.i18n.localize("SW5E.Quantity"),
|
||||
css: "item-qty",
|
||||
property: "data.quantity",
|
||||
editable: "Number"
|
||||
},
|
||||
{
|
||||
label: game.i18n.localize("SW5E.Price"),
|
||||
css: "item-price",
|
||||
property: "data.price",
|
||||
editable: "Number"
|
||||
},
|
||||
{
|
||||
label: game.i18n.localize("SW5E.Weight"),
|
||||
css: "item-weight",
|
||||
property: "data.weight",
|
||||
editable: "Number"
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
// Classify items owned by the vehicle and compute total cargo weight
|
||||
let totalWeight = 0;
|
||||
for (const item of data.items) {
|
||||
this._prepareCrewedItem(item);
|
||||
|
||||
// Handle cargo explicitly
|
||||
const isCargo = item.flags.sw5e?.vehicleCargo === true;
|
||||
if (isCargo) {
|
||||
totalWeight += (item.data.weight || 0) * item.data.quantity;
|
||||
cargo.cargo.items.push(item);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle non-cargo item types
|
||||
switch (item.type) {
|
||||
case "weapon":
|
||||
features.weapons.items.push(item);
|
||||
break;
|
||||
case "equipment":
|
||||
features.equipment.items.push(item);
|
||||
break;
|
||||
case "feat":
|
||||
if (!item.data.activation.type || item.data.activation.type === "none")
|
||||
features.passive.items.push(item);
|
||||
else if (item.data.activation.type === "reaction") features.reactions.items.push(item);
|
||||
else features.actions.items.push(item);
|
||||
break;
|
||||
default:
|
||||
totalWeight += (item.data.weight || 0) * item.data.quantity;
|
||||
cargo.cargo.items.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the rendering context data
|
||||
data.features = Object.values(features);
|
||||
data.cargo = Object.values(cargo);
|
||||
data.data.attributes.encumbrance = this._computeEncumbrance(totalWeight, data);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Event Listeners and Handlers */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
activateListeners(html) {
|
||||
super.activateListeners(html);
|
||||
if (!this.isEditable) return;
|
||||
|
||||
html.find(".item-toggle").click(this._onToggleItem.bind(this));
|
||||
html.find(".item-hp input")
|
||||
.click((evt) => evt.target.select())
|
||||
.change(this._onHPChange.bind(this));
|
||||
|
||||
html.find(".item:not(.cargo-row) input[data-property]")
|
||||
.click((evt) => evt.target.select())
|
||||
.change(this._onEditInSheet.bind(this));
|
||||
|
||||
html.find(".cargo-row input")
|
||||
.click((evt) => evt.target.select())
|
||||
.change(this._onCargoRowChange.bind(this));
|
||||
|
||||
if (this.actor.data.data.attributes.actions.stations) {
|
||||
html.find(".counter.actions, .counter.action-thresholds").hide();
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle saving a cargo row (i.e. crew or passenger) in-sheet.
|
||||
* @param event {Event}
|
||||
* @returns {Promise<Actor>|null}
|
||||
* @private
|
||||
*/
|
||||
_onCargoRowChange(event) {
|
||||
event.preventDefault();
|
||||
const target = event.currentTarget;
|
||||
const row = target.closest(".item");
|
||||
const idx = Number(row.dataset.itemId);
|
||||
const property = row.classList.contains("crew") ? "crew" : "passengers";
|
||||
|
||||
// Get the cargo entry
|
||||
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[property]);
|
||||
const entry = cargo[idx];
|
||||
if (!entry) return null;
|
||||
|
||||
// Update the cargo value
|
||||
const key = target.dataset.property || "name";
|
||||
const type = target.dataset.dtype;
|
||||
let value = target.value;
|
||||
if (type === "Number") value = Number(value);
|
||||
entry[key] = value;
|
||||
|
||||
// Perform the Actor update
|
||||
return this.actor.update({[`data.cargo.${property}`]: cargo});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle editing certain values like quantity, price, and weight in-sheet.
|
||||
* @param event {Event}
|
||||
* @returns {Promise<Item>}
|
||||
* @private
|
||||
*/
|
||||
_onEditInSheet(event) {
|
||||
event.preventDefault();
|
||||
const itemID = event.currentTarget.closest(".item").dataset.itemId;
|
||||
const item = this.actor.items.get(itemID);
|
||||
const property = event.currentTarget.dataset.property;
|
||||
const type = event.currentTarget.dataset.dtype;
|
||||
let value = event.currentTarget.value;
|
||||
switch (type) {
|
||||
case "Number":
|
||||
value = parseInt(value);
|
||||
break;
|
||||
case "Boolean":
|
||||
value = value === "true";
|
||||
break;
|
||||
}
|
||||
return item.update({[`${property}`]: value});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle creating a new crew or passenger row.
|
||||
* @param event {Event}
|
||||
* @returns {Promise<Actor|Item>}
|
||||
* @private
|
||||
*/
|
||||
_onItemCreate(event) {
|
||||
event.preventDefault();
|
||||
const target = event.currentTarget;
|
||||
const type = target.dataset.type;
|
||||
if (type === "crew" || type === "passengers") {
|
||||
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]);
|
||||
cargo.push(this.constructor.newCargo);
|
||||
return this.actor.update({[`data.cargo.${type}`]: cargo});
|
||||
}
|
||||
return super._onItemCreate(event);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle deleting a crew or passenger row.
|
||||
* @param event {Event}
|
||||
* @returns {Promise<Actor|Item>}
|
||||
* @private
|
||||
*/
|
||||
_onItemDelete(event) {
|
||||
event.preventDefault();
|
||||
const row = event.currentTarget.closest(".item");
|
||||
if (row.classList.contains("cargo-row")) {
|
||||
const idx = Number(row.dataset.itemId);
|
||||
const type = row.classList.contains("crew") ? "crew" : "passengers";
|
||||
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx);
|
||||
return this.actor.update({[`data.cargo.${type}`]: cargo});
|
||||
}
|
||||
|
||||
return super._onItemDelete(event);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
async _onDropItemCreate(itemData) {
|
||||
const cargoTypes = ["weapon", "equipment", "consumable", "tool", "loot", "backpack"];
|
||||
const isCargo = cargoTypes.includes(itemData.type) && this._tabs[0].active === "cargo";
|
||||
foundry.utils.setProperty(itemData, "flags.sw5e.vehicleCargo", isCargo);
|
||||
return super._onDropItemCreate(itemData);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Special handling for editing HP to clamp it within appropriate range.
|
||||
* @param event {Event}
|
||||
* @returns {Promise<Item>}
|
||||
* @private
|
||||
*/
|
||||
_onHPChange(event) {
|
||||
event.preventDefault();
|
||||
const itemID = event.currentTarget.closest(".item").dataset.itemId;
|
||||
const item = this.actor.items.get(itemID);
|
||||
const hp = Math.clamped(0, parseInt(event.currentTarget.value), item.data.data.hp.max);
|
||||
event.currentTarget.value = hp;
|
||||
return item.update({"data.hp.value": hp});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle toggling an item's crewed status.
|
||||
* @param event {Event}
|
||||
* @returns {Promise<Item>}
|
||||
* @private
|
||||
*/
|
||||
_onToggleItem(event) {
|
||||
event.preventDefault();
|
||||
const itemID = event.currentTarget.closest(".item").dataset.itemId;
|
||||
const item = this.actor.items.get(itemID);
|
||||
const crewed = !!item.data.data.crewed;
|
||||
return item.update({"data.crewed": !crewed});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Handle toggling an item's crewed status.
|
||||
* @param event {Event}
|
||||
* @returns {Promise<Item>}
|
||||
* @private
|
||||
*/
|
||||
_onToggleItem(event) {
|
||||
event.preventDefault();
|
||||
const itemID = event.currentTarget.closest('.item').dataset.itemId;
|
||||
const item = this.actor.items.get(itemID);
|
||||
const crewed = !!item.data.data.crewed;
|
||||
return item.update({'data.crewed': !crewed});
|
||||
}
|
||||
};
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -7,362 +7,295 @@ 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();
|
||||
|
||||
// Temporary HP
|
||||
let hp = sheetData.data.attributes.hp;
|
||||
if (hp.temp === 0) delete hp.temp;
|
||||
if (hp.tempmax === 0) delete hp.tempmax;
|
||||
|
||||
// Resources
|
||||
sheetData["resources"] = ["primary", "secondary", "tertiary"].reduce((arr, r) => {
|
||||
const res = sheetData.data.resources[r] || {};
|
||||
res.name = r;
|
||||
res.placeholder = game.i18n.localize("SW5E.Resource" + r.titleCase());
|
||||
if (res && res.value === 0) delete res.value;
|
||||
if (res && res.max === 0) delete res.max;
|
||||
return arr.concat([res]);
|
||||
}, []);
|
||||
|
||||
// Experience Tracking
|
||||
sheetData["disableExperience"] = game.settings.get("sw5e", "disableExperienceTracking");
|
||||
sheetData["classLabels"] = this.actor.itemTypes.class.map((c) => c.name).join(", ");
|
||||
sheetData["multiclassLabels"] = this.actor.itemTypes.class
|
||||
.map((c) => {
|
||||
return [c.data.data.archetype, c.name, c.data.data.levels].filterJoin(" ");
|
||||
})
|
||||
.join(", ");
|
||||
|
||||
// Return data for rendering
|
||||
return sheetData;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Organize and classify Owned Items for Character sheets
|
||||
* @private
|
||||
*/
|
||||
_prepareItems(data) {
|
||||
// Categorize items as inventory, powerbook, features, and classes
|
||||
const inventory = {
|
||||
weapon: {label: "SW5E.ItemTypeWeaponPl", items: [], dataset: {type: "weapon"}},
|
||||
equipment: {label: "SW5E.ItemTypeEquipmentPl", items: [], dataset: {type: "equipment"}},
|
||||
consumable: {label: "SW5E.ItemTypeConsumablePl", items: [], dataset: {type: "consumable"}},
|
||||
tool: {label: "SW5E.ItemTypeToolPl", items: [], dataset: {type: "tool"}},
|
||||
backpack: {label: "SW5E.ItemTypeContainerPl", items: [], dataset: {type: "backpack"}},
|
||||
loot: {label: "SW5E.ItemTypeLootPl", items: [], dataset: {type: "loot"}}
|
||||
};
|
||||
|
||||
// Partition items by category
|
||||
let [
|
||||
items,
|
||||
powers,
|
||||
feats,
|
||||
classes,
|
||||
species,
|
||||
archetypes,
|
||||
classfeatures,
|
||||
backgrounds,
|
||||
fightingstyles,
|
||||
fightingmasteries,
|
||||
lightsaberforms
|
||||
] = data.items.reduce(
|
||||
(arr, item) => {
|
||||
// Item details
|
||||
item.img = item.img || CONST.DEFAULT_TOKEN;
|
||||
item.isStack = Number.isNumeric(item.data.quantity) && item.data.quantity !== 1;
|
||||
item.attunement = {
|
||||
[CONFIG.SW5E.attunementTypes.REQUIRED]: {
|
||||
icon: "fa-sun",
|
||||
cls: "not-attuned",
|
||||
title: "SW5E.AttunementRequired"
|
||||
},
|
||||
[CONFIG.SW5E.attunementTypes.ATTUNED]: {
|
||||
icon: "fa-sun",
|
||||
cls: "attuned",
|
||||
title: "SW5E.AttunementAttuned"
|
||||
}
|
||||
}[item.data.attunement];
|
||||
|
||||
// Item usage
|
||||
item.hasUses = item.data.uses && item.data.uses.max > 0;
|
||||
item.isOnCooldown =
|
||||
item.data.recharge && !!item.data.recharge.value && item.data.recharge.charged === false;
|
||||
item.isDepleted = item.isOnCooldown && item.data.uses.per && item.data.uses.value > 0;
|
||||
item.hasTarget = !!item.data.target && !["none", ""].includes(item.data.target.type);
|
||||
|
||||
// Item toggle state
|
||||
this._prepareItemToggleState(item);
|
||||
|
||||
// Primary Class
|
||||
if (item.type === "class")
|
||||
item.isOriginalClass = item._id === this.actor.data.data.details.originalClass;
|
||||
|
||||
// Classify items into types
|
||||
if (item.type === "power") arr[1].push(item);
|
||||
else if (item.type === "feat") arr[2].push(item);
|
||||
else if (item.type === "class") arr[3].push(item);
|
||||
else if (item.type === "species") arr[4].push(item);
|
||||
else if (item.type === "archetype") arr[5].push(item);
|
||||
else if (item.type === "classfeature") arr[6].push(item);
|
||||
else if (item.type === "background") arr[7].push(item);
|
||||
else if (item.type === "fightingstyle") arr[8].push(item);
|
||||
else if (item.type === "fightingmastery") arr[9].push(item);
|
||||
else if (item.type === "lightsaberform") arr[10].push(item);
|
||||
else if (Object.keys(inventory).includes(item.type)) arr[0].push(item);
|
||||
return arr;
|
||||
},
|
||||
[[], [], [], [], [], [], [], [], [], [], []]
|
||||
);
|
||||
|
||||
// Apply active item filters
|
||||
items = this._filterItems(items, this._filters.inventory);
|
||||
powers = this._filterItems(powers, this._filters.powerbook);
|
||||
feats = this._filterItems(feats, this._filters.features);
|
||||
|
||||
// Organize items
|
||||
for (let i of items) {
|
||||
i.data.quantity = i.data.quantity || 0;
|
||||
i.data.weight = i.data.weight || 0;
|
||||
i.totalWeight = (i.data.quantity * i.data.weight).toNearest(0.1);
|
||||
inventory[i.type].items.push(i);
|
||||
}
|
||||
|
||||
// Organize Powerbook and count the number of prepared powers (excluding always, at will, etc...)
|
||||
const powerbook = this._preparePowerbook(data, powers);
|
||||
const nPrepared = powers.filter((s) => {
|
||||
return s.data.level > 0 && s.data.preparation.mode === "prepared" && s.data.preparation.prepared;
|
||||
}).length;
|
||||
|
||||
// Organize Features
|
||||
const features = {
|
||||
classes: {
|
||||
label: "SW5E.ItemTypeClassPl",
|
||||
items: [],
|
||||
hasActions: false,
|
||||
dataset: {type: "class"},
|
||||
isClass: true
|
||||
},
|
||||
classfeatures: {
|
||||
label: "SW5E.ItemTypeClassFeats",
|
||||
items: [],
|
||||
hasActions: true,
|
||||
dataset: {type: "classfeature"},
|
||||
isClassfeature: true
|
||||
},
|
||||
archetype: {
|
||||
label: "SW5E.ItemTypeArchetype",
|
||||
items: [],
|
||||
hasActions: false,
|
||||
dataset: {type: "archetype"},
|
||||
isArchetype: true
|
||||
},
|
||||
species: {
|
||||
label: "SW5E.ItemTypeSpecies",
|
||||
items: [],
|
||||
hasActions: false,
|
||||
dataset: {type: "species"},
|
||||
isSpecies: true
|
||||
},
|
||||
background: {
|
||||
label: "SW5E.ItemTypeBackground",
|
||||
items: [],
|
||||
hasActions: false,
|
||||
dataset: {type: "background"},
|
||||
isBackground: true
|
||||
},
|
||||
fightingstyles: {
|
||||
label: "SW5E.ItemTypeFightingStylePl",
|
||||
items: [],
|
||||
hasActions: false,
|
||||
dataset: {type: "fightingstyle"},
|
||||
isFightingstyle: true
|
||||
},
|
||||
fightingmasteries: {
|
||||
label: "SW5E.ItemTypeFightingMasteryPl",
|
||||
items: [],
|
||||
hasActions: false,
|
||||
dataset: {type: "fightingmastery"},
|
||||
isFightingmastery: true
|
||||
},
|
||||
lightsaberforms: {
|
||||
label: "SW5E.ItemTypeLightsaberFormPl",
|
||||
items: [],
|
||||
hasActions: false,
|
||||
dataset: {type: "lightsaberform"},
|
||||
isLightsaberform: true
|
||||
},
|
||||
active: {
|
||||
label: "SW5E.FeatureActive",
|
||||
items: [],
|
||||
hasActions: true,
|
||||
dataset: {"type": "feat", "activation.type": "action"}
|
||||
},
|
||||
passive: {label: "SW5E.FeaturePassive", items: [], hasActions: false, dataset: {type: "feat"}}
|
||||
};
|
||||
for (let f of feats) {
|
||||
if (f.data.activation.type) features.active.items.push(f);
|
||||
else features.passive.items.push(f);
|
||||
}
|
||||
classes.sort((a, b) => b.data.levels - a.data.levels);
|
||||
features.classes.items = classes;
|
||||
features.classfeatures.items = classfeatures;
|
||||
features.archetype.items = archetypes;
|
||||
features.species.items = species;
|
||||
features.background.items = backgrounds;
|
||||
features.fightingstyles.items = fightingstyles;
|
||||
features.fightingmasteries.items = fightingmasteries;
|
||||
features.lightsaberforms.items = lightsaberforms;
|
||||
|
||||
// Assign and return
|
||||
data.inventory = Object.values(inventory);
|
||||
data.powerbook = powerbook;
|
||||
data.preparedPowers = nPrepared;
|
||||
data.features = Object.values(features);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A helper method to establish the displayed preparation state for an item
|
||||
* @param {Item} item
|
||||
* @private
|
||||
*/
|
||||
_prepareItemToggleState(item) {
|
||||
if (item.type === "power") {
|
||||
const isAlways = getProperty(item.data, "preparation.mode") === "always";
|
||||
const isPrepared = getProperty(item.data, "preparation.prepared");
|
||||
item.toggleClass = isPrepared ? "active" : "";
|
||||
if (isAlways) item.toggleClass = "fixed";
|
||||
if (isAlways) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.always;
|
||||
else if (isPrepared) item.toggleTitle = CONFIG.SW5E.powerPreparationModes.prepared;
|
||||
else item.toggleTitle = game.i18n.localize("SW5E.PowerUnprepared");
|
||||
} else {
|
||||
const isActive = getProperty(item.data, "equipped");
|
||||
item.toggleClass = isActive ? "active" : "";
|
||||
item.toggleTitle = game.i18n.localize(isActive ? "SW5E.Equipped" : "SW5E.Unequipped");
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Event Listeners and Handlers
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Activate event listeners using the prepared sheet HTML
|
||||
* @param html {jQuery} The prepared HTML object ready to be rendered into the DOM
|
||||
*/
|
||||
activateListeners(html) {
|
||||
super.activateListeners(html);
|
||||
if (!this.isEditable) return;
|
||||
/**
|
||||
* Add some extra data when rendering the sheet to reduce the amount of logic required within the template.
|
||||
*/
|
||||
getData() {
|
||||
const sheetData = super.getData();
|
||||
|
||||
// Item State Toggling
|
||||
html.find(".item-toggle").click(this._onToggleItem.bind(this));
|
||||
// Temporary HP
|
||||
let hp = sheetData.data.attributes.hp;
|
||||
if (hp.temp === 0) delete hp.temp;
|
||||
if (hp.tempmax === 0) delete hp.tempmax;
|
||||
|
||||
// Short and Long Rest
|
||||
html.find(".short-rest").click(this._onShortRest.bind(this));
|
||||
html.find(".long-rest").click(this._onLongRest.bind(this));
|
||||
// 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]);
|
||||
}, []);
|
||||
|
||||
// Rollable sheet actions
|
||||
html.find(".rollable[data-action]").click(this._onSheetAction.bind(this));
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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});
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Organize and classify Owned Items for Character sheets
|
||||
* @private
|
||||
*/
|
||||
_prepareItems(data) {
|
||||
|
||||
// Categorize items as inventory, powerbook, features, and classes
|
||||
const inventory = {
|
||||
weapon: { label: "SW5E.ItemTypeWeaponPl", items: [], dataset: {type: "weapon"} },
|
||||
equipment: { label: "SW5E.ItemTypeEquipmentPl", items: [], dataset: {type: "equipment"} },
|
||||
consumable: { label: "SW5E.ItemTypeConsumablePl", items: [], dataset: {type: "consumable"} },
|
||||
tool: { label: "SW5E.ItemTypeToolPl", items: [], dataset: {type: "tool"} },
|
||||
backpack: { label: "SW5E.ItemTypeContainerPl", items: [], dataset: {type: "backpack"} },
|
||||
loot: { label: "SW5E.ItemTypeLootPl", items: [], dataset: {type: "loot"} }
|
||||
};
|
||||
|
||||
// Partition items by category
|
||||
let [items, powers, feats, classes, species, archetypes, classfeatures, backgrounds, fightingstyles, fightingmasteries, lightsaberforms] = data.items.reduce((arr, item) => {
|
||||
|
||||
// Item details
|
||||
item.img = item.img || CONST.DEFAULT_TOKEN;
|
||||
item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
|
||||
item.attunement = {
|
||||
[CONFIG.SW5E.attunementTypes.REQUIRED]: {
|
||||
icon: "fa-sun",
|
||||
cls: "not-attuned",
|
||||
title: "SW5E.AttunementRequired"
|
||||
},
|
||||
[CONFIG.SW5E.attunementTypes.ATTUNED]: {
|
||||
icon: "fa-sun",
|
||||
cls: "attuned",
|
||||
title: "SW5E.AttunementAttuned"
|
||||
}
|
||||
}[item.data.attunement];
|
||||
|
||||
// Item usage
|
||||
item.hasUses = item.data.uses && (item.data.uses.max > 0);
|
||||
item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false);
|
||||
item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0));
|
||||
item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type));
|
||||
|
||||
// Item toggle state
|
||||
this._prepareItemToggleState(item);
|
||||
|
||||
// Primary Class
|
||||
if ( item.type === "class" ) item.isOriginalClass = ( item._id === this.actor.data.data.details.originalClass );
|
||||
|
||||
// Classify items into types
|
||||
if ( item.type === "power" ) arr[1].push(item);
|
||||
else if ( item.type === "feat" ) arr[2].push(item);
|
||||
else if ( item.type === "class" ) arr[3].push(item);
|
||||
else if ( item.type === "species" ) arr[4].push(item);
|
||||
else if ( item.type === "archetype" ) arr[5].push(item);
|
||||
else if ( item.type === "classfeature" ) arr[6].push(item);
|
||||
else if ( item.type === "background" ) arr[7].push(item);
|
||||
else if ( item.type === "fightingstyle" ) arr[8].push(item);
|
||||
else if ( item.type === "fightingmastery" ) arr[9].push(item);
|
||||
else if ( item.type === "lightsaberform" ) arr[10].push(item);
|
||||
else if ( Object.keys(inventory).includes(item.type ) ) arr[0].push(item);
|
||||
return arr;
|
||||
}, [[], [], [], [], [], [], [], [], [], [], []]);
|
||||
|
||||
// Apply active item filters
|
||||
items = this._filterItems(items, this._filters.inventory);
|
||||
powers = this._filterItems(powers, this._filters.powerbook);
|
||||
feats = this._filterItems(feats, this._filters.features);
|
||||
|
||||
// Organize items
|
||||
for ( let i of items ) {
|
||||
i.data.quantity = i.data.quantity || 0;
|
||||
i.data.weight = i.data.weight || 0;
|
||||
i.totalWeight = (i.data.quantity * i.data.weight).toNearest(0.1);
|
||||
inventory[i.type].items.push(i);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
// Organize Powerbook and count the number of prepared powers (excluding always, at will, etc...)
|
||||
const powerbook = this._preparePowerbook(data, powers);
|
||||
const nPrepared = powers.filter(s => {
|
||||
return (s.data.level > 0) && (s.data.preparation.mode === "prepared") && s.data.preparation.prepared;
|
||||
}).length;
|
||||
|
||||
/**
|
||||
* 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)});
|
||||
// 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* 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");
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* 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();
|
||||
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
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @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});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
// Default drop handling if levels were not added
|
||||
return super._onDropItemCreate(itemData);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,139 +6,130 @@ 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 get defaultOptions() {
|
||||
return mergeObject(super.defaultOptions, {
|
||||
classes: ["sw5e", "sheet", "actor", "npc"],
|
||||
width: 600,
|
||||
height: 680
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
static unsupportedItemTypes = new Set(["class"]);
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Organize Owned Items for rendering the NPC sheet
|
||||
* @private
|
||||
*/
|
||||
_prepareItems(data) {
|
||||
|
||||
// Categorize Items as Features and Powers
|
||||
const features = {
|
||||
weapons: { label: game.i18n.localize("SW5E.AttackPl"), items: [] , hasActions: true, dataset: {type: "weapon", "weapon-type": "natural"} },
|
||||
actions: { label: game.i18n.localize("SW5E.ActionPl"), items: [] , hasActions: true, dataset: {type: "feat", "activation.type": "action"} },
|
||||
passive: { label: game.i18n.localize("SW5E.Features"), items: [], dataset: {type: "feat"} },
|
||||
equipment: { label: game.i18n.localize("SW5E.Inventory"), items: [], dataset: {type: "loot"}}
|
||||
};
|
||||
|
||||
// Start by classifying items into groups for rendering
|
||||
let [powers, other] = data.items.reduce((arr, item) => {
|
||||
item.img = item.img || CONST.DEFAULT_TOKEN;
|
||||
item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
|
||||
item.hasUses = item.data.uses && (item.data.uses.max > 0);
|
||||
item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false);
|
||||
item.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0));
|
||||
item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type));
|
||||
if ( item.type === "power" ) arr[0].push(item);
|
||||
else arr[1].push(item);
|
||||
return arr;
|
||||
}, [[], []]);
|
||||
|
||||
// Apply item filters
|
||||
powers = this._filterItems(powers, this._filters.powerbook);
|
||||
other = this._filterItems(other, this._filters.features);
|
||||
|
||||
// Organize Powerbook
|
||||
const powerbook = this._preparePowerbook(data, powers);
|
||||
|
||||
// Organize Features
|
||||
for ( let item of other ) {
|
||||
if ( item.type === "weapon" ) features.weapons.items.push(item);
|
||||
else if ( item.type === "feat" ) {
|
||||
if ( item.data.activation.type ) features.actions.items.push(item);
|
||||
else features.passive.items.push(item);
|
||||
}
|
||||
else features.equipment.items.push(item);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
// Assign and return
|
||||
data.features = Object.values(features);
|
||||
data.powerbook = powerbook;
|
||||
}
|
||||
|
||||
/** @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"}}
|
||||
};
|
||||
/** @inheritdoc */
|
||||
getData(options) {
|
||||
const data = super.getData(options);
|
||||
|
||||
// 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;
|
||||
},
|
||||
[[], []]
|
||||
);
|
||||
// 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;
|
||||
|
||||
// Apply item filters
|
||||
powers = this._filterItems(powers, this._filters.powerbook);
|
||||
other = this._filterItems(other, this._filters.features);
|
||||
// Creature Type
|
||||
data.labels["type"] = this.actor.labels.creatureType;
|
||||
return data;
|
||||
}
|
||||
|
||||
// Organize Powerbook
|
||||
const powerbook = this._preparePowerbook(data, powers);
|
||||
/* -------------------------------------------- */
|
||||
/* Object Updates */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
// 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 */
|
||||
async _updateObject(event, formData) {
|
||||
|
||||
// Assign and return
|
||||
data.features = Object.values(features);
|
||||
data.powerbook = powerbook;
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
getData(options) {
|
||||
const data = super.getData(options);
|
||||
/* -------------------------------------------- */
|
||||
/* Event Listeners and Handlers */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
// 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;
|
||||
/** @override */
|
||||
activateListeners(html) {
|
||||
super.activateListeners(html);
|
||||
html.find(".health .rollable").click(this._onRollHPFormula.bind(this));
|
||||
}
|
||||
|
||||
// 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});
|
||||
}
|
||||
/**
|
||||
* 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});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,427 +6,411 @@ 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
|
||||
});
|
||||
/**
|
||||
* 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 = '—';
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
// 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'
|
||||
}];
|
||||
|
||||
/**
|
||||
* Creates a new cargo entry for a vehicle Actor.
|
||||
*/
|
||||
static get newCargo() {
|
||||
return {
|
||||
name: "",
|
||||
quantity: 1
|
||||
};
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
/* -------------------------------------------- */
|
||||
/* Event Listeners and Handlers */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
// Vehicle weights are an order of magnitude greater.
|
||||
totalWeight /= CONFIG.SW5E.encumbrance.vehicleWeightMultiplier;
|
||||
/** @override */
|
||||
activateListeners(html) {
|
||||
super.activateListeners(html);
|
||||
if (!this.isEditable) return;
|
||||
|
||||
// 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};
|
||||
html.find('.item-toggle').click(this._onToggleItem.bind(this));
|
||||
html.find('.item-hp input')
|
||||
.click(evt => evt.target.select())
|
||||
.change(this._onHPChange.bind(this));
|
||||
|
||||
html.find('.item:not(.cargo-row) input[data-property]')
|
||||
.click(evt => evt.target.select())
|
||||
.change(this._onEditInSheet.bind(this));
|
||||
|
||||
html.find('.cargo-row input')
|
||||
.click(evt => evt.target.select())
|
||||
.change(this._onCargoRowChange.bind(this));
|
||||
|
||||
if (this.actor.data.data.attributes.actions.stations) {
|
||||
html.find('.counter.actions, .counter.action-thresholds').hide();
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle saving a cargo row (i.e. crew or passenger) in-sheet.
|
||||
* @param event {Event}
|
||||
* @returns {Promise<Actor>|null}
|
||||
* @private
|
||||
*/
|
||||
_onCargoRowChange(event) {
|
||||
event.preventDefault();
|
||||
const target = event.currentTarget;
|
||||
const row = target.closest('.item');
|
||||
const idx = Number(row.dataset.itemId);
|
||||
const property = row.classList.contains('crew') ? 'crew' : 'passengers';
|
||||
|
||||
// Get the cargo entry
|
||||
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[property]);
|
||||
const entry = cargo[idx];
|
||||
if (!entry) return null;
|
||||
|
||||
// Update the cargo value
|
||||
const key = target.dataset.property || 'name';
|
||||
const type = target.dataset.dtype;
|
||||
let value = target.value;
|
||||
if (type === 'Number') value = Number(value);
|
||||
entry[key] = value;
|
||||
|
||||
// Perform the Actor update
|
||||
return this.actor.update({[`data.cargo.${property}`]: cargo});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle editing certain values like quantity, price, and weight in-sheet.
|
||||
* @param event {Event}
|
||||
* @returns {Promise<Item>}
|
||||
* @private
|
||||
*/
|
||||
_onEditInSheet(event) {
|
||||
event.preventDefault();
|
||||
const itemID = event.currentTarget.closest('.item').dataset.itemId;
|
||||
const item = this.actor.items.get(itemID);
|
||||
const property = event.currentTarget.dataset.property;
|
||||
const type = event.currentTarget.dataset.dtype;
|
||||
let value = event.currentTarget.value;
|
||||
switch (type) {
|
||||
case 'Number': value = parseInt(value); break;
|
||||
case 'Boolean': value = value === 'true'; break;
|
||||
}
|
||||
return item.update({[`${property}`]: value});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle creating a new crew or passenger row.
|
||||
* @param event {Event}
|
||||
* @returns {Promise<Actor|Item>}
|
||||
* @private
|
||||
*/
|
||||
_onItemCreate(event) {
|
||||
event.preventDefault();
|
||||
const target = event.currentTarget;
|
||||
const type = target.dataset.type;
|
||||
if (type === 'crew' || type === 'passengers') {
|
||||
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]);
|
||||
cargo.push(this.constructor.newCargo);
|
||||
return this.actor.update({[`data.cargo.${type}`]: cargo});
|
||||
}
|
||||
return super._onItemCreate(event);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle deleting a crew or passenger row.
|
||||
* @param event {Event}
|
||||
* @returns {Promise<Actor|Item>}
|
||||
* @private
|
||||
*/
|
||||
_onItemDelete(event) {
|
||||
event.preventDefault();
|
||||
const row = event.currentTarget.closest('.item');
|
||||
if (row.classList.contains('cargo-row')) {
|
||||
const idx = Number(row.dataset.itemId);
|
||||
const type = row.classList.contains('crew') ? 'crew' : 'passengers';
|
||||
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx);
|
||||
return this.actor.update({[`data.cargo.${type}`]: cargo});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
return super._onItemDelete(event);
|
||||
}
|
||||
|
||||
/** @override */
|
||||
_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"}`);
|
||||
/* -------------------------------------------- */
|
||||
|
||||
// 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 = "—";
|
||||
}
|
||||
/**
|
||||
* Special handling for editing HP to clamp it within appropriate range.
|
||||
* @param event {Event}
|
||||
* @returns {Promise<Item>}
|
||||
* @private
|
||||
*/
|
||||
_onHPChange(event) {
|
||||
event.preventDefault();
|
||||
const itemID = event.currentTarget.closest('.item').dataset.itemId;
|
||||
const item = this.actor.items.get(itemID);
|
||||
const hp = Math.clamped(0, parseInt(event.currentTarget.value), item.data.data.hp.max);
|
||||
event.currentTarget.value = hp;
|
||||
return item.update({'data.hp.value': hp});
|
||||
}
|
||||
|
||||
// Prepare vehicle weapons
|
||||
if (item.type === "equipment" || item.type === "weapon") {
|
||||
item.threshold = item.data.hp.dt ? item.data.hp.dt : "—";
|
||||
}
|
||||
}
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Organize Owned Items for rendering the Vehicle sheet.
|
||||
* @private
|
||||
*/
|
||||
_prepareItems(data) {
|
||||
const cargoColumns = [
|
||||
{
|
||||
label: game.i18n.localize("SW5E.Quantity"),
|
||||
css: "item-qty",
|
||||
property: "quantity",
|
||||
editable: "Number"
|
||||
}
|
||||
];
|
||||
|
||||
const equipmentColumns = [
|
||||
{
|
||||
label: game.i18n.localize("SW5E.Quantity"),
|
||||
css: "item-qty",
|
||||
property: "data.quantity"
|
||||
},
|
||||
{
|
||||
label: game.i18n.localize("SW5E.AC"),
|
||||
css: "item-ac",
|
||||
property: "data.armor.value"
|
||||
},
|
||||
{
|
||||
label: game.i18n.localize("SW5E.HP"),
|
||||
css: "item-hp",
|
||||
property: "data.hp.value",
|
||||
editable: "Number"
|
||||
},
|
||||
{
|
||||
label: game.i18n.localize("SW5E.Threshold"),
|
||||
css: "item-threshold",
|
||||
property: "threshold"
|
||||
}
|
||||
];
|
||||
|
||||
const features = {
|
||||
actions: {
|
||||
label: game.i18n.localize("SW5E.ActionPl"),
|
||||
items: [],
|
||||
crewable: true,
|
||||
dataset: {"type": "feat", "activation.type": "crew"},
|
||||
columns: [
|
||||
{
|
||||
label: game.i18n.localize("SW5E.VehicleCrew"),
|
||||
css: "item-crew",
|
||||
property: "crew"
|
||||
},
|
||||
{
|
||||
label: game.i18n.localize("SW5E.Cover"),
|
||||
css: "item-cover",
|
||||
property: "cover"
|
||||
}
|
||||
]
|
||||
},
|
||||
equipment: {
|
||||
label: game.i18n.localize("SW5E.ItemTypeEquipment"),
|
||||
items: [],
|
||||
crewable: true,
|
||||
dataset: {"type": "equipment", "armor.type": "vehicle"},
|
||||
columns: equipmentColumns
|
||||
},
|
||||
passive: {
|
||||
label: game.i18n.localize("SW5E.Features"),
|
||||
items: [],
|
||||
dataset: {type: "feat"}
|
||||
},
|
||||
reactions: {
|
||||
label: game.i18n.localize("SW5E.ReactionPl"),
|
||||
items: [],
|
||||
dataset: {"type": "feat", "activation.type": "reaction"}
|
||||
},
|
||||
weapons: {
|
||||
label: game.i18n.localize("SW5E.ItemTypeWeaponPl"),
|
||||
items: [],
|
||||
crewable: true,
|
||||
dataset: {"type": "weapon", "weapon-type": "siege"},
|
||||
columns: equipmentColumns
|
||||
}
|
||||
};
|
||||
|
||||
const cargo = {
|
||||
crew: {
|
||||
label: game.i18n.localize("SW5E.VehicleCrew"),
|
||||
items: data.data.cargo.crew,
|
||||
css: "cargo-row crew",
|
||||
editableName: true,
|
||||
dataset: {type: "crew"},
|
||||
columns: cargoColumns
|
||||
},
|
||||
passengers: {
|
||||
label: game.i18n.localize("SW5E.VehiclePassengers"),
|
||||
items: data.data.cargo.passengers,
|
||||
css: "cargo-row passengers",
|
||||
editableName: true,
|
||||
dataset: {type: "passengers"},
|
||||
columns: cargoColumns
|
||||
},
|
||||
cargo: {
|
||||
label: game.i18n.localize("SW5E.VehicleCargo"),
|
||||
items: [],
|
||||
dataset: {type: "loot"},
|
||||
columns: [
|
||||
{
|
||||
label: game.i18n.localize("SW5E.Quantity"),
|
||||
css: "item-qty",
|
||||
property: "data.quantity",
|
||||
editable: "Number"
|
||||
},
|
||||
{
|
||||
label: game.i18n.localize("SW5E.Price"),
|
||||
css: "item-price",
|
||||
property: "data.price",
|
||||
editable: "Number"
|
||||
},
|
||||
{
|
||||
label: game.i18n.localize("SW5E.Weight"),
|
||||
css: "item-weight",
|
||||
property: "data.weight",
|
||||
editable: "Number"
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
// Classify items owned by the vehicle and compute total cargo weight
|
||||
let totalWeight = 0;
|
||||
for (const item of data.items) {
|
||||
this._prepareCrewedItem(item);
|
||||
|
||||
// Handle cargo explicitly
|
||||
const isCargo = item.flags.sw5e?.vehicleCargo === true;
|
||||
if (isCargo) {
|
||||
totalWeight += (item.data.weight || 0) * item.data.quantity;
|
||||
cargo.cargo.items.push(item);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle non-cargo item types
|
||||
switch (item.type) {
|
||||
case "weapon":
|
||||
features.weapons.items.push(item);
|
||||
break;
|
||||
case "equipment":
|
||||
features.equipment.items.push(item);
|
||||
break;
|
||||
case "feat":
|
||||
if (!item.data.activation.type || item.data.activation.type === "none")
|
||||
features.passive.items.push(item);
|
||||
else if (item.data.activation.type === "reaction") features.reactions.items.push(item);
|
||||
else features.actions.items.push(item);
|
||||
break;
|
||||
default:
|
||||
totalWeight += (item.data.weight || 0) * item.data.quantity;
|
||||
cargo.cargo.items.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the rendering context data
|
||||
data.features = Object.values(features);
|
||||
data.cargo = Object.values(cargo);
|
||||
data.data.attributes.encumbrance = this._computeEncumbrance(totalWeight, data);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Event Listeners and Handlers */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
activateListeners(html) {
|
||||
super.activateListeners(html);
|
||||
if (!this.isEditable) return;
|
||||
|
||||
html.find(".item-toggle").click(this._onToggleItem.bind(this));
|
||||
html.find(".item-hp input")
|
||||
.click((evt) => evt.target.select())
|
||||
.change(this._onHPChange.bind(this));
|
||||
|
||||
html.find(".item:not(.cargo-row) input[data-property]")
|
||||
.click((evt) => evt.target.select())
|
||||
.change(this._onEditInSheet.bind(this));
|
||||
|
||||
html.find(".cargo-row input")
|
||||
.click((evt) => evt.target.select())
|
||||
.change(this._onCargoRowChange.bind(this));
|
||||
|
||||
if (this.actor.data.data.attributes.actions.stations) {
|
||||
html.find(".counter.actions, .counter.action-thresholds").hide();
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle saving a cargo row (i.e. crew or passenger) in-sheet.
|
||||
* @param event {Event}
|
||||
* @returns {Promise<Actor>|null}
|
||||
* @private
|
||||
*/
|
||||
_onCargoRowChange(event) {
|
||||
event.preventDefault();
|
||||
const target = event.currentTarget;
|
||||
const row = target.closest(".item");
|
||||
const idx = Number(row.dataset.itemId);
|
||||
const property = row.classList.contains("crew") ? "crew" : "passengers";
|
||||
|
||||
// Get the cargo entry
|
||||
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[property]);
|
||||
const entry = cargo[idx];
|
||||
if (!entry) return null;
|
||||
|
||||
// Update the cargo value
|
||||
const key = target.dataset.property || "name";
|
||||
const type = target.dataset.dtype;
|
||||
let value = target.value;
|
||||
if (type === "Number") value = Number(value);
|
||||
entry[key] = value;
|
||||
|
||||
// Perform the Actor update
|
||||
return this.actor.update({[`data.cargo.${property}`]: cargo});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle editing certain values like quantity, price, and weight in-sheet.
|
||||
* @param event {Event}
|
||||
* @returns {Promise<Item>}
|
||||
* @private
|
||||
*/
|
||||
_onEditInSheet(event) {
|
||||
event.preventDefault();
|
||||
const itemID = event.currentTarget.closest(".item").dataset.itemId;
|
||||
const item = this.actor.items.get(itemID);
|
||||
const property = event.currentTarget.dataset.property;
|
||||
const type = event.currentTarget.dataset.dtype;
|
||||
let value = event.currentTarget.value;
|
||||
switch (type) {
|
||||
case "Number":
|
||||
value = parseInt(value);
|
||||
break;
|
||||
case "Boolean":
|
||||
value = value === "true";
|
||||
break;
|
||||
}
|
||||
return item.update({[`${property}`]: value});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle creating a new crew or passenger row.
|
||||
* @param event {Event}
|
||||
* @returns {Promise<Actor|Item>}
|
||||
* @private
|
||||
*/
|
||||
_onItemCreate(event) {
|
||||
event.preventDefault();
|
||||
const target = event.currentTarget;
|
||||
const type = target.dataset.type;
|
||||
if (type === "crew" || type === "passengers") {
|
||||
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]);
|
||||
cargo.push(this.constructor.newCargo);
|
||||
return this.actor.update({[`data.cargo.${type}`]: cargo});
|
||||
}
|
||||
return super._onItemCreate(event);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle deleting a crew or passenger row.
|
||||
* @param event {Event}
|
||||
* @returns {Promise<Actor|Item>}
|
||||
* @private
|
||||
*/
|
||||
_onItemDelete(event) {
|
||||
event.preventDefault();
|
||||
const row = event.currentTarget.closest(".item");
|
||||
if (row.classList.contains("cargo-row")) {
|
||||
const idx = Number(row.dataset.itemId);
|
||||
const type = row.classList.contains("crew") ? "crew" : "passengers";
|
||||
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx);
|
||||
return this.actor.update({[`data.cargo.${type}`]: cargo});
|
||||
}
|
||||
|
||||
return super._onItemDelete(event);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
async _onDropItemCreate(itemData) {
|
||||
const cargoTypes = ["weapon", "equipment", "consumable", "tool", "loot", "backpack"];
|
||||
const isCargo = cargoTypes.includes(itemData.type) && this._tabs[0].active === "cargo";
|
||||
foundry.utils.setProperty(itemData, "flags.sw5e.vehicleCargo", isCargo);
|
||||
return super._onDropItemCreate(itemData);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Special handling for editing HP to clamp it within appropriate range.
|
||||
* @param event {Event}
|
||||
* @returns {Promise<Item>}
|
||||
* @private
|
||||
*/
|
||||
_onHPChange(event) {
|
||||
event.preventDefault();
|
||||
const itemID = event.currentTarget.closest(".item").dataset.itemId;
|
||||
const item = this.actor.items.get(itemID);
|
||||
const hp = Math.clamped(0, parseInt(event.currentTarget.value), item.data.data.hp.max);
|
||||
event.currentTarget.value = hp;
|
||||
return item.update({"data.hp.value": hp});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle toggling an item's crewed status.
|
||||
* @param event {Event}
|
||||
* @returns {Promise<Item>}
|
||||
* @private
|
||||
*/
|
||||
_onToggleItem(event) {
|
||||
event.preventDefault();
|
||||
const itemID = event.currentTarget.closest(".item").dataset.itemId;
|
||||
const item = this.actor.items.get(itemID);
|
||||
const crewed = !!item.data.data.crewed;
|
||||
return item.update({"data.crewed": !crewed});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Handle toggling an item's crewed status.
|
||||
* @param event {Event}
|
||||
* @returns {Promise<Item>}
|
||||
* @private
|
||||
*/
|
||||
_onToggleItem(event) {
|
||||
event.preventDefault();
|
||||
const itemID = event.currentTarget.closest('.item').dataset.itemId;
|
||||
const item = this.actor.items.get(itemID);
|
||||
const crewed = !!item.data.data.crewed;
|
||||
return item.update({'data.crewed': !crewed});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -3,225 +3,220 @@
|
|||
* @type {Dialog}
|
||||
*/
|
||||
export default class AbilityUseDialog extends 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 */
|
||||
/* -------------------------------------------- */
|
||||
constructor(item, dialogData={}, options={}) {
|
||||
super(dialogData, options);
|
||||
this.options.classes = ["sw5e", "dialog"];
|
||||
|
||||
/**
|
||||
* 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}
|
||||
* Store a reference to the Item entity being used
|
||||
* @type {Item5e}
|
||||
*/
|
||||
static async create(item) {
|
||||
if (!item.isOwned) throw new Error("You cannot display an ability usage dialog for an unowned item");
|
||||
this.item = item;
|
||||
}
|
||||
|
||||
// Prepare data
|
||||
const actorData = item.actor.data.data;
|
||||
const itemData = item.data.data;
|
||||
const uses = itemData.uses || {};
|
||||
const quantity = itemData.quantity || 0;
|
||||
const recharge = itemData.recharge || {};
|
||||
const recharges = !!recharge.value;
|
||||
const sufficientUses = (quantity > 0 && !uses.value) || uses.value > 0;
|
||||
/* -------------------------------------------- */
|
||||
/* Rendering */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
// 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);
|
||||
/**
|
||||
* 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");
|
||||
|
||||
// Render the ability usage template
|
||||
const html = await renderTemplate("systems/sw5e/templates/apps/ability-use.html", 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;
|
||||
|
||||
// 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: `<i class="fas ${icon}"></i>`,
|
||||
label: label,
|
||||
callback: (html) => {
|
||||
const fd = new FormDataExtended(html[0].querySelector("form"));
|
||||
resolve(fd.toObject());
|
||||
}
|
||||
}
|
||||
},
|
||||
default: "use",
|
||||
close: () => resolve(null)
|
||||
});
|
||||
dlg.render(true);
|
||||
});
|
||||
}
|
||||
// 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);
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Helpers */
|
||||
/* -------------------------------------------- */
|
||||
// Render the ability usage template
|
||||
const html = await renderTemplate("systems/sw5e/templates/apps/ability-use.html", data);
|
||||
|
||||
/**
|
||||
* 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;
|
||||
// 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: `<i class="fas ${icon}"></i>`,
|
||||
label: label,
|
||||
callback: html => {
|
||||
const fd = new FormDataExtended(html[0].querySelector("form"));
|
||||
resolve(fd.toObject());
|
||||
}
|
||||
case "tec": {
|
||||
powerType = "tech";
|
||||
points = actorData.attributes.tech.points.value + actorData.attributes.tech.points.temp;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
default: "use",
|
||||
close: () => resolve(null)
|
||||
});
|
||||
dlg.render(true);
|
||||
});
|
||||
}
|
||||
|
||||
// eliminate point usage for innate casters
|
||||
if (actorData.attributes.powercasting === "innate") points = 999;
|
||||
/* -------------------------------------------- */
|
||||
/* Helpers */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
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);
|
||||
}
|
||||
/**
|
||||
* Get dialog data related to limited power slots
|
||||
* @private
|
||||
*/
|
||||
static _getPowerData(actorData, itemData, data) {
|
||||
|
||||
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
|
||||
})
|
||||
);
|
||||
// Determine whether the power may be up-cast
|
||||
const lvl = itemData.level;
|
||||
const consumePowerSlot = (lvl > 0) && CONFIG.SW5E.powerUpcastModes.includes(itemData.preparation.mode);
|
||||
|
||||
// Merge power casting data
|
||||
return foundry.utils.mergeObject(data, {isPower: true, consumePowerSlot, powerLevels});
|
||||
// If can't upcast, return early and don't bother calculating available power slots
|
||||
if (!consumePowerSlot) {
|
||||
mergeObject(data, { isPower: true, consumePowerSlot });
|
||||
return;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Get the ability usage note that is displayed
|
||||
* @private
|
||||
*/
|
||||
static _getAbilityUseNote(item, uses, recharge) {
|
||||
// Zero quantity
|
||||
const quantity = item.data.quantity;
|
||||
if (quantity <= 0) return game.i18n.localize("SW5E.AbilityUseUnavailableHint");
|
||||
|
||||
// Abilities which use Recharge
|
||||
if (!!recharge.value) {
|
||||
return game.i18n.format(recharge.charged ? "SW5E.AbilityUseChargedHint" : "SW5E.AbilityUseRechargeHint", {
|
||||
type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`)
|
||||
});
|
||||
}
|
||||
|
||||
// Does not use any resource
|
||||
if (!uses.per || !uses.max) return "";
|
||||
|
||||
// Consumables
|
||||
if (item.type === "consumable") {
|
||||
let str = "SW5E.AbilityUseNormalHint";
|
||||
if (uses.value > 1) str = "SW5E.AbilityUseConsumableChargeHint";
|
||||
else if (item.data.quantity === 1 && uses.autoDestroy) str = "SW5E.AbilityUseConsumableDestroyHint";
|
||||
else if (item.data.quantity > 1) str = "SW5E.AbilityUseConsumableQuantityHint";
|
||||
return game.i18n.format(str, {
|
||||
type: game.i18n.localize(`SW5E.Consumable${item.data.consumableType.capitalize()}`),
|
||||
value: uses.value,
|
||||
quantity: item.data.quantity,
|
||||
max: uses.max,
|
||||
per: CONFIG.SW5E.limitedUsePeriods[uses.per]
|
||||
});
|
||||
}
|
||||
|
||||
// Other Items
|
||||
else {
|
||||
return game.i18n.format("SW5E.AbilityUseNormalHint", {
|
||||
type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`),
|
||||
value: uses.value,
|
||||
max: uses.max,
|
||||
per: CONFIG.SW5E.limitedUsePeriods[uses.per]
|
||||
});
|
||||
}
|
||||
// Determine the levels which are feasible
|
||||
let lmax = 0;
|
||||
let points;
|
||||
let powerType;
|
||||
switch (itemData.school){
|
||||
case "lgt":
|
||||
case "uni":
|
||||
case "drk": {
|
||||
powerType = "force"
|
||||
points = actorData.attributes.force.points.value + actorData.attributes.force.points.temp;
|
||||
break;
|
||||
}
|
||||
case "tec": {
|
||||
powerType = "tech"
|
||||
points = actorData.attributes.tech.points.value + actorData.attributes.tech.points.temp;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// eliminate point usage for innate casters
|
||||
if (actorData.attributes.powercasting === 'innate') points = 999;
|
||||
|
||||
|
||||
let powerLevels
|
||||
if (powerType === "force"){
|
||||
powerLevels = Array.fromRange(10).reduce((arr, i) => {
|
||||
if ( i < lvl ) return arr;
|
||||
const label = CONFIG.SW5E.powerLevels[i];
|
||||
const l = actorData.powers["power"+i] || {fmax: 0, foverride: null};
|
||||
let max = parseInt(l.foverride || l.fmax || 0);
|
||||
let slots = Math.clamped(parseInt(l.fvalue || 0), 0, max);
|
||||
if ( max > 0 ) lmax = i;
|
||||
if ((max > 0) && (slots > 0) && (points > i)){
|
||||
arr.push({
|
||||
level: i,
|
||||
label: i > 0 ? game.i18n.format('SW5E.PowerLevelSlot', {level: label, n: slots}) : label,
|
||||
canCast: max > 0,
|
||||
hasSlots: slots > 0
|
||||
});
|
||||
}
|
||||
return arr;
|
||||
}, []).filter(sl => sl.level <= lmax);
|
||||
}else if (powerType === "tech"){
|
||||
powerLevels = Array.fromRange(10).reduce((arr, i) => {
|
||||
if ( i < lvl ) return arr;
|
||||
const label = CONFIG.SW5E.powerLevels[i];
|
||||
const l = actorData.powers["power"+i] || {tmax: 0, toverride: null};
|
||||
let max = parseInt(l.override || l.tmax || 0);
|
||||
let slots = Math.clamped(parseInt(l.tvalue || 0), 0, max);
|
||||
if ( max > 0 ) lmax = i;
|
||||
if ((max > 0) && (slots > 0) && (points > i)){
|
||||
arr.push({
|
||||
level: i,
|
||||
label: i > 0 ? game.i18n.format('SW5E.PowerLevelSlot', {level: label, n: slots}) : label,
|
||||
canCast: max > 0,
|
||||
hasSlots: slots > 0
|
||||
});
|
||||
}
|
||||
return arr;
|
||||
}, []).filter(sl => sl.level <= lmax);
|
||||
}
|
||||
|
||||
|
||||
|
||||
const canCast = powerLevels.some(l => l.hasSlots);
|
||||
if ( !canCast ) data.errors.push(game.i18n.format("SW5E.PowerCastNoSlots", {
|
||||
level: CONFIG.SW5E.powerLevels[lvl],
|
||||
name: data.item.name
|
||||
}));
|
||||
|
||||
// Merge power casting data
|
||||
return foundry.utils.mergeObject(data, { isPower: true, consumePowerSlot, powerLevels });
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Get the ability usage note that is displayed
|
||||
* @private
|
||||
*/
|
||||
static _getAbilityUseNote(item, uses, recharge) {
|
||||
|
||||
// Zero quantity
|
||||
const quantity = item.data.quantity;
|
||||
if ( quantity <= 0 ) return game.i18n.localize("SW5E.AbilityUseUnavailableHint");
|
||||
|
||||
// Abilities which use Recharge
|
||||
if ( !!recharge.value ) {
|
||||
return game.i18n.format(recharge.charged ? "SW5E.AbilityUseChargedHint" : "SW5E.AbilityUseRechargeHint", {
|
||||
type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`),
|
||||
})
|
||||
}
|
||||
|
||||
// Does not use any resource
|
||||
if ( !uses.per || !uses.max ) return "";
|
||||
|
||||
// Consumables
|
||||
if ( item.type === "consumable" ) {
|
||||
let str = "SW5E.AbilityUseNormalHint";
|
||||
if ( uses.value > 1 ) str = "SW5E.AbilityUseConsumableChargeHint";
|
||||
else if ( item.data.quantity === 1 && uses.autoDestroy ) str = "SW5E.AbilityUseConsumableDestroyHint";
|
||||
else if ( item.data.quantity > 1 ) str = "SW5E.AbilityUseConsumableQuantityHint";
|
||||
return game.i18n.format(str, {
|
||||
type: game.i18n.localize(`SW5E.Consumable${item.data.consumableType.capitalize()}`),
|
||||
value: uses.value,
|
||||
quantity: item.data.quantity,
|
||||
max: uses.max,
|
||||
per: CONFIG.SW5E.limitedUsePeriods[uses.per]
|
||||
});
|
||||
}
|
||||
|
||||
// Other Items
|
||||
else {
|
||||
return game.i18n.format("SW5E.AbilityUseNormalHint", {
|
||||
type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`),
|
||||
value: uses.value,
|
||||
max: uses.max,
|
||||
per: CONFIG.SW5E.limitedUsePeriods[uses.per]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,137 +3,135 @@
|
|||
* @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
|
||||
});
|
||||
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;
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
get title() {
|
||||
return `${game.i18n.localize("SW5E.FlagsTitle")}: ${this.object.name}`;
|
||||
/**
|
||||
* Get the bonuses fields and their localization strings
|
||||
* @return {Array<object>}
|
||||
* @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;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
getData() {
|
||||
const data = {};
|
||||
data.actor = this.object;
|
||||
data.classes = this._getClasses();
|
||||
data.flags = this._getFlags();
|
||||
data.bonuses = this._getBonuses();
|
||||
return data;
|
||||
}
|
||||
/** @override */
|
||||
async _updateObject(event, formData) {
|
||||
const actor = this.object;
|
||||
let updateData = expandObject(formData);
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* 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;
|
||||
// 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;
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Get the bonuses fields and their localization strings
|
||||
* @return {Array<object>}
|
||||
* @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;
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @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 the data against any applied overrides and apply
|
||||
await actor.update(updateData, {diff: false});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import Actor5e from "../actor/entity.js";
|
|||
* @extends {FormApplication}
|
||||
*/
|
||||
export default class ActorTypeConfig extends FormApplication {
|
||||
|
||||
/** @inheritdoc */
|
||||
static get defaultOptions() {
|
||||
return foundry.utils.mergeObject(super.defaultOptions, {
|
||||
|
@ -31,23 +32,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
|
||||
|
@ -60,14 +61,12 @@ 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) || "–"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -75,7 +74,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 });
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
* @implements {DocumentSheet}
|
||||
*/
|
||||
export default class ActorHitDiceConfig extends DocumentSheet {
|
||||
|
||||
/** @override */
|
||||
static get defaultOptions() {
|
||||
return foundry.utils.mergeObject(super.defaultOptions, {
|
||||
|
@ -25,22 +26,20 @@ 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)))
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -51,7 +50,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");
|
||||
|
@ -68,8 +67,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);
|
||||
}
|
||||
|
|
|
@ -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: '<i class="fas fa-bed"></i>',
|
||||
label: game.i18n.localize("SW5E.Rest"),
|
||||
callback: (html) => {
|
||||
let newDay = true;
|
||||
if (game.settings.get("sw5e", "restVariant") !== "gritty")
|
||||
newDay = html.find('input[name="newDay"]')[0].checked;
|
||||
resolve(newDay);
|
||||
}
|
||||
},
|
||||
cancel: {
|
||||
icon: '<i class="fas fa-times"></i>',
|
||||
label: game.i18n.localize("Cancel"),
|
||||
callback: reject
|
||||
}
|
||||
},
|
||||
default: "rest",
|
||||
close: reject
|
||||
});
|
||||
dlg.render(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* A helper constructor function which displays the Long Rest confirmation dialog and returns a Promise once it's
|
||||
* workflow has been resolved.
|
||||
* @param {Actor5e} actor
|
||||
* @return {Promise}
|
||||
*/
|
||||
static async longRestDialog({ actor } = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const dlg = new this(actor, {
|
||||
title: game.i18n.localize("SW5E.LongRest"),
|
||||
buttons: {
|
||||
rest: {
|
||||
icon: '<i class="fas fa-bed"></i>',
|
||||
label: game.i18n.localize("SW5E.Rest"),
|
||||
callback: html => {
|
||||
let newDay = true;
|
||||
if (game.settings.get("sw5e", "restVariant") !== "gritty")
|
||||
newDay = html.find('input[name="newDay"]')[0].checked;
|
||||
resolve(newDay);
|
||||
}
|
||||
},
|
||||
cancel: {
|
||||
icon: '<i class="fas fa-times"></i>',
|
||||
label: game.i18n.localize("Cancel"),
|
||||
callback: reject
|
||||
}
|
||||
},
|
||||
default: 'rest',
|
||||
close: reject
|
||||
});
|
||||
dlg.render(true);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -3,36 +3,37 @@
|
|||
* @extends {DocumentSheet}
|
||||
*/
|
||||
export default class ActorMovementConfig extends DocumentSheet {
|
||||
/** @override */
|
||||
static get defaultOptions() {
|
||||
return foundry.utils.mergeObject(super.defaultOptions, {
|
||||
classes: ["sw5e"],
|
||||
template: "systems/sw5e/templates/apps/movement-config.html",
|
||||
width: 300,
|
||||
height: "auto"
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
get title() {
|
||||
return `${game.i18n.localize("SW5E.MovementConfig")}: ${this.document.name}`;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
getData(options) {
|
||||
const sourceMovement = foundry.utils.getProperty(this.document.data._source, "data.attributes.movement") || {};
|
||||
const data = {
|
||||
movement: foundry.utils.deepClone(sourceMovement),
|
||||
units: CONFIG.SW5E.movementUnits
|
||||
};
|
||||
for (let [k, v] of Object.entries(data.movement)) {
|
||||
if (["units", "hover"].includes(k)) continue;
|
||||
data.movement[k] = Number.isNumeric(v) ? v.toNearest(0.1) : 0;
|
||||
}
|
||||
return data;
|
||||
|
||||
/** @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;
|
||||
}
|
||||
}
|
||||
|
|
117
module/apps/recharge-rest.js
Normal file
117
module/apps/recharge-rest.js
Normal file
|
@ -0,0 +1,117 @@
|
|||
/**
|
||||
* 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: '<i class="fas fa-bed"></i>',
|
||||
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: '<i class="fas fa-times"></i>',
|
||||
label: "Cancel",
|
||||
callback: reject
|
||||
}
|
||||
},
|
||||
close: reject
|
||||
});
|
||||
dlg.render(true);
|
||||
});
|
||||
}
|
||||
}
|
69
module/apps/refitting-rest.js
Normal file
69
module/apps/refitting-rest.js
Normal file
|
@ -0,0 +1,69 @@
|
|||
/**
|
||||
* 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: '<i class="fas fa-bed"></i>',
|
||||
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: '<i class="fas fa-times"></i>',
|
||||
label: "Cancel",
|
||||
callback: reject
|
||||
}
|
||||
},
|
||||
default: 'rest',
|
||||
close: reject
|
||||
});
|
||||
dlg.render(true);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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,27 +33,29 @@ export default class SelectItemsPrompt extends Dialog {
|
|||
* @param {string} options.hint - Localized hint to display at the top of the prompt
|
||||
* @return {Promise<string[]>} - list of item ids which the user has selected
|
||||
*/
|
||||
static async create(items, {hint}) {
|
||||
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: `<i class="fas fa-user-plus"></i>`,
|
||||
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: '<i class="fas fa-forward"></i>',
|
||||
label: game.i18n.localize("SW5E.Skip"),
|
||||
label: game.i18n.localize('SW5E.Skip'),
|
||||
callback: () => resolve([])
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
return data;
|
||||
|
||||
/** @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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,130 +5,129 @@ 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;
|
||||
// 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: '<i class="fas fa-bed"></i>',
|
||||
label: game.i18n.localize("SW5E.Rest"),
|
||||
callback: html => {
|
||||
let newDay = false;
|
||||
if (game.settings.get("sw5e", "restVariant") === "gritty")
|
||||
newDay = html.find('input[name="newDay"]')[0].checked;
|
||||
resolve(newDay);
|
||||
}
|
||||
return hd;
|
||||
}, {});
|
||||
data.canRoll = this.actor.data.data.attributes.hd > 0;
|
||||
data.denomination = this._denom;
|
||||
},
|
||||
cancel: {
|
||||
icon: '<i class="fas fa-times"></i>',
|
||||
label: game.i18n.localize("Cancel"),
|
||||
callback: reject
|
||||
}
|
||||
},
|
||||
close: reject
|
||||
});
|
||||
dlg.render(true);
|
||||
});
|
||||
}
|
||||
|
||||
// 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: '<i class="fas fa-bed"></i>',
|
||||
label: game.i18n.localize("SW5E.Rest"),
|
||||
callback: (html) => {
|
||||
let newDay = false;
|
||||
if (game.settings.get("sw5e", "restVariant") === "gritty")
|
||||
newDay = html.find('input[name="newDay"]')[0].checked;
|
||||
resolve(newDay);
|
||||
}
|
||||
},
|
||||
cancel: {
|
||||
icon: '<i class="fas fa-times"></i>',
|
||||
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);
|
||||
}
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,85 +3,86 @@
|
|||
* @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"
|
||||
});
|
||||
|
||||
/** @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);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
// 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;
|
||||
|
||||
/**
|
||||
* Return a reference to the target attribute
|
||||
* @type {string}
|
||||
*/
|
||||
get attribute() {
|
||||
return this.options.name;
|
||||
// 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`);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @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);
|
||||
}
|
||||
// Update the object
|
||||
this.object.update(updateData);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
};
|
||||
// Standard PHB Movement
|
||||
else return (ns + nd) * canvas.scene.data.gridDistance;
|
||||
});
|
||||
};
|
|
@ -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.<skill_name>.value is all that matters
|
||||
/* ----------------------------------------------------------------- */
|
||||
/* character.data.skills.<skill_name>.value is all that matters
|
||||
/* values can be 0, 0.5, 1 or 2
|
||||
/* 0 = regular
|
||||
/* 0.5 = half-proficient
|
||||
|
@ -53,274 +53,272 @@ 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 skills = {
|
||||
acr: {
|
||||
value: sourceCharacter.attribs.find((e) => e.name == "acrobatics_type").current
|
||||
},
|
||||
ani: {
|
||||
value: sourceCharacter.attribs.find((e) => e.name == "animal_handling_type").current
|
||||
},
|
||||
ath: {
|
||||
value: sourceCharacter.attribs.find((e) => e.name == "athletics_type").current
|
||||
},
|
||||
dec: {
|
||||
value: sourceCharacter.attribs.find((e) => e.name == "deception_type").current
|
||||
},
|
||||
ins: {
|
||||
value: sourceCharacter.attribs.find((e) => e.name == "insight_type").current
|
||||
},
|
||||
inv: {
|
||||
value: sourceCharacter.attribs.find((e) => e.name == "investigation_type").current
|
||||
},
|
||||
itm: {
|
||||
value: sourceCharacter.attribs.find((e) => e.name == "intimidation_type").current
|
||||
},
|
||||
lor: {
|
||||
value: sourceCharacter.attribs.find((e) => e.name == "lore_type").current
|
||||
},
|
||||
med: {
|
||||
value: sourceCharacter.attribs.find((e) => e.name == "medicine_type").current
|
||||
},
|
||||
nat: {
|
||||
value: sourceCharacter.attribs.find((e) => e.name == "nature_type").current
|
||||
},
|
||||
per: {
|
||||
value: sourceCharacter.attribs.find((e) => e.name == "persuasion_type").current
|
||||
},
|
||||
pil: {
|
||||
value: sourceCharacter.attribs.find((e) => e.name == "piloting_type").current
|
||||
},
|
||||
prc: {
|
||||
value: sourceCharacter.attribs.find((e) => e.name == "perception_type").current
|
||||
},
|
||||
prf: {
|
||||
value: sourceCharacter.attribs.find((e) => e.name == "performance_type").current
|
||||
},
|
||||
slt: {
|
||||
value: sourceCharacter.attribs.find((e) => e.name == "sleight_of_hand_type").current
|
||||
},
|
||||
ste: {
|
||||
value: sourceCharacter.attribs.find((e) => e.name == "stealth_type").current
|
||||
},
|
||||
sur: {
|
||||
value: sourceCharacter.attribs.find((e) => e.name == "survival_type").current
|
||||
},
|
||||
tec: {
|
||||
value: sourceCharacter.attribs.find((e) => e.name == "technology_type").current
|
||||
}
|
||||
};
|
||||
|
||||
const targetCharacter = {
|
||||
name: sourceCharacter.name,
|
||||
type: "character",
|
||||
data: {
|
||||
abilities: abilities,
|
||||
details: details,
|
||||
skills: skills,
|
||||
attributes: {
|
||||
hp: hp
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let actor = await Actor.create(targetCharacter);
|
||||
CharacterImporter.addProfessions(sourceCharacter, actor);
|
||||
}
|
||||
|
||||
// Parse all classes and add them to already created actor.
|
||||
// "class" is a reserved word, therefore I use profession where I can.
|
||||
static async addProfessions(sourceCharacter, actor) {
|
||||
let result = [];
|
||||
|
||||
// parse all class and multiclassX items
|
||||
// couldn't get Array.filter to work here for some reason
|
||||
// result = array of objects. each object is a separate class
|
||||
sourceCharacter.attribs.forEach((e) => {
|
||||
if (CharacterImporter.classOrMulticlass(e.name)) {
|
||||
var t = {
|
||||
profession: CharacterImporter.capitalize(e.current),
|
||||
type: CharacterImporter.baseOrMulti(e.name),
|
||||
level: CharacterImporter.getLevel(e, sourceCharacter)
|
||||
};
|
||||
result.push(t);
|
||||
}
|
||||
});
|
||||
|
||||
const targetCharacter = {
|
||||
name: sourceCharacter.name,
|
||||
type: "character",
|
||||
data: {
|
||||
abilities: abilities,
|
||||
details: details,
|
||||
skills: skills,
|
||||
attributes: {
|
||||
hp: hp
|
||||
}
|
||||
}
|
||||
};
|
||||
// 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 });
|
||||
});
|
||||
|
||||
let actor = await Actor.create(targetCharacter);
|
||||
CharacterImporter.addProfessions(sourceCharacter, actor);
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
// 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 getLevel(item, sourceCharacter) {
|
||||
if (item.name === "class") {
|
||||
let result = sourceCharacter.attribs.find((e) => e.name === "base_level").current;
|
||||
return parseInt(result);
|
||||
} else {
|
||||
let result = sourceCharacter.attribs.find((e) => e.name === `${item.name}_lvl`).current;
|
||||
return parseInt(result);
|
||||
}
|
||||
}
|
||||
|
||||
static 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 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 classOrMulticlass(name) {
|
||||
return name === "class" || (name.includes("multiclass") && name.length <= 12);
|
||||
}
|
||||
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 baseOrMulti(name) {
|
||||
if (name === "class") {
|
||||
return "base_class";
|
||||
} else {
|
||||
return "multi_class";
|
||||
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 getLevel(item, sourceCharacter) {
|
||||
if (item.name === "class") {
|
||||
let result = sourceCharacter.attribs.find((e) => e.name === "base_level").current;
|
||||
return parseInt(result);
|
||||
} else {
|
||||
let result = sourceCharacter.attribs.find((e) => e.name === `${item.name}_lvl`).current;
|
||||
return parseInt(result);
|
||||
}
|
||||
}
|
||||
static addImportButton(html) {
|
||||
const actionButtons = html.find(".header-actions");
|
||||
actionButtons[0].insertAdjacentHTML(
|
||||
"afterend",
|
||||
`<div class="header-actions action-buttons flexrow"><button class="create-entity cs-import-button"><i class="fas fa-upload"></i> Import Character</button></div>`
|
||||
);
|
||||
|
||||
static capitalize(str) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
static async addSpecies(race, actor) {
|
||||
const species = await game.packs.get("sw5e.species").getDocuments();
|
||||
const assignedSpecies = species.find((c) => c.name === race);
|
||||
const activeEffects = [...assignedSpecies.data.effects][0].data.changes;
|
||||
const actorData = {data: {abilities: {...actor.data.data.abilities}}};
|
||||
|
||||
activeEffects.map((effect) => {
|
||||
switch (effect.key) {
|
||||
case "data.abilities.str.value":
|
||||
actorData.data.abilities.str.value -= effect.value;
|
||||
break;
|
||||
|
||||
case "data.abilities.dex.value":
|
||||
actorData.data.abilities.dex.value -= effect.value;
|
||||
break;
|
||||
|
||||
case "data.abilities.con.value":
|
||||
actorData.data.abilities.con.value -= effect.value;
|
||||
break;
|
||||
|
||||
case "data.abilities.int.value":
|
||||
actorData.data.abilities.int.value -= effect.value;
|
||||
break;
|
||||
|
||||
case "data.abilities.wis.value":
|
||||
actorData.data.abilities.wis.value -= effect.value;
|
||||
break;
|
||||
|
||||
case "data.abilities.cha.value":
|
||||
actorData.data.abilities.cha.value -= effect.value;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
actor.update(actorData);
|
||||
|
||||
await actor.createEmbeddedDocuments("Item", [assignedSpecies.data], {displaySheet: false});
|
||||
}
|
||||
|
||||
static async addPowers(powers, actor) {
|
||||
const forcePowers = await game.packs.get("sw5e.forcepowers").getDocuments();
|
||||
const techPowers = await game.packs.get("sw5e.techpowers").getDocuments();
|
||||
|
||||
for (const power of powers) {
|
||||
const createdPower = forcePowers.find((c) => c.name === power) || techPowers.find((c) => c.name === power);
|
||||
|
||||
if (createdPower) {
|
||||
await actor.createEmbeddedDocuments("Item", [createdPower.data], {displaySheet: false});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static async addItems(items, actor) {
|
||||
const weapons = await game.packs.get("sw5e.weapons").getDocuments();
|
||||
const armors = await game.packs.get("sw5e.armor").getDocuments();
|
||||
const adventuringGear = await game.packs.get("sw5e.adventuringgear").getDocuments();
|
||||
|
||||
for (const item of items) {
|
||||
const createdItem =
|
||||
weapons.find((c) => c.name.toLowerCase() === item.name.toLowerCase()) ||
|
||||
armors.find((c) => c.name.toLowerCase() === item.name.toLowerCase()) ||
|
||||
adventuringGear.find((c) => c.name.toLowerCase() === item.name.toLowerCase());
|
||||
|
||||
if (createdItem) {
|
||||
if (item.quantity != 1) {
|
||||
createdItem.data.data.quantity = item.quantity;
|
||||
}
|
||||
|
||||
await actor.createEmbeddedDocuments("Item", [createdItem.data], {displaySheet: false});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static addImportButton(html) {
|
||||
const actionButtons = html.find(".header-actions");
|
||||
actionButtons[0].insertAdjacentHTML(
|
||||
"afterend",
|
||||
`<div class="header-actions action-buttons flexrow"><button class="create-entity cs-import-button"><i class="fas fa-upload"></i> Import Character</button></div>`
|
||||
);
|
||||
|
||||
let characterImportButton = $(".cs-import-button");
|
||||
characterImportButton.click(() => {
|
||||
let content = `<h1>Saved Character JSON Import</h1>
|
||||
let characterImportButton = $(".cs-import-button");
|
||||
characterImportButton.click(() => {
|
||||
let content = `<h1>Saved Character JSON Import</h1>
|
||||
<label for="character-json">Paste character JSON here:</label>
|
||||
</br>
|
||||
<textarea id="character-json" name="character-json" rows="10" cols="50"></textarea>`;
|
||||
let importDialog = new Dialog({
|
||||
title: "Import Character from SW5e.com",
|
||||
content: content,
|
||||
buttons: {
|
||||
Import: {
|
||||
icon: `<i class="fas fa-file-import"></i>`,
|
||||
label: "Import Character",
|
||||
callback: () => {
|
||||
let characterData = $("#character-json").val();
|
||||
console.log("Parsing Character JSON");
|
||||
CharacterImporter.transform(characterData);
|
||||
}
|
||||
},
|
||||
Cancel: {
|
||||
icon: `<i class="fas fa-times-circle"></i>`,
|
||||
label: "Cancel",
|
||||
callback: () => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
importDialog.render(true);
|
||||
});
|
||||
}
|
||||
let importDialog = new Dialog({
|
||||
title: "Import Character from SW5e.com",
|
||||
content: content,
|
||||
buttons: {
|
||||
Import: {
|
||||
icon: `<i class="fas fa-file-import"></i>`,
|
||||
label: "Import Character",
|
||||
callback: () => {
|
||||
let characterData = $("#character-json").val();
|
||||
console.log("Parsing Character JSON");
|
||||
CharacterImporter.transform(characterData);
|
||||
}
|
||||
},
|
||||
Cancel: {
|
||||
icon: `<i class="fas fa-times-circle"></i>`,
|
||||
label: "Cancel",
|
||||
callback: () => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
importDialog.render(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
151
module/chat.js
151
module/chat.js
|
@ -1,29 +1,30 @@
|
|||
|
||||
/**
|
||||
* 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");
|
||||
}
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -31,24 +32,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"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -62,38 +63,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: '<i class="fas fa-user-minus"></i>',
|
||||
condition: canApply,
|
||||
callback: (li) => applyChatCardDamage(li, 1)
|
||||
},
|
||||
{
|
||||
name: game.i18n.localize("SW5E.ChatContextHealing"),
|
||||
icon: '<i class="fas fa-user-plus"></i>',
|
||||
condition: canApply,
|
||||
callback: (li) => applyChatCardDamage(li, -1)
|
||||
},
|
||||
{
|
||||
name: game.i18n.localize("SW5E.ChatContextDoubleDamage"),
|
||||
icon: '<i class="fas fa-user-injured"></i>',
|
||||
condition: canApply,
|
||||
callback: (li) => applyChatCardDamage(li, 2)
|
||||
},
|
||||
{
|
||||
name: game.i18n.localize("SW5E.ChatContextHalfDamage"),
|
||||
icon: '<i class="fas fa-user-shield"></i>',
|
||||
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: '<i class="fas fa-user-minus"></i>',
|
||||
condition: canApply,
|
||||
callback: li => applyChatCardDamage(li, 1)
|
||||
},
|
||||
{
|
||||
name: game.i18n.localize("SW5E.ChatContextHealing"),
|
||||
icon: '<i class="fas fa-user-plus"></i>',
|
||||
condition: canApply,
|
||||
callback: li => applyChatCardDamage(li, -1)
|
||||
},
|
||||
{
|
||||
name: game.i18n.localize("SW5E.ChatContextDoubleDamage"),
|
||||
icon: '<i class="fas fa-user-injured"></i>',
|
||||
condition: canApply,
|
||||
callback: li => applyChatCardDamage(li, 2)
|
||||
},
|
||||
{
|
||||
name: game.i18n.localize("SW5E.ChatContextHalfDamage"),
|
||||
icon: '<i class="fas fa-user-shield"></i>',
|
||||
condition: canApply,
|
||||
callback: li => applyChatCardDamage(li, 0.5)
|
||||
}
|
||||
);
|
||||
return options;
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -107,14 +108,12 @@ 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);
|
||||
}));
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
|
|
@ -1 +1,4 @@
|
|||
export const ClassFeatures = {};
|
||||
export const ClassFeatures = {
|
||||
|
||||
};
|
||||
|
||||
|
|
|
@ -1,31 +1,27 @@
|
|||
|
||||
/**
|
||||
* 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(" + ");
|
||||
};
|
||||
|
|
2193
module/config.js
2193
module/config.js
File diff suppressed because it is too large
Load diff
301
module/dice.js
301
module/dice.js
|
@ -12,55 +12,50 @@ 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;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -71,11 +66,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);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -116,75 +111,54 @@ function _isUnsupportedTerm(term) {
|
|||
* @return {Promise<D20Roll|null>} The evaluated D20Roll, or null if the workflow was cancelled
|
||||
*/
|
||||
export async function d20Roll({
|
||||
parts = [],
|
||||
data = {}, // Roll creation
|
||||
advantage,
|
||||
disadvantage,
|
||||
fumble = 1,
|
||||
critical = 20,
|
||||
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,
|
||||
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";
|
||||
reliableTalent
|
||||
});
|
||||
|
||||
// Construct the D20Roll instance
|
||||
const roll = new CONFIG.Dice.D20Roll(formula, data, {
|
||||
flavor: flavor || title,
|
||||
advantageMode,
|
||||
defaultRollMode,
|
||||
critical,
|
||||
fumble,
|
||||
targetValue,
|
||||
elvenAccuracy,
|
||||
halflingLucky,
|
||||
reliableTalent
|
||||
});
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
// Evaluate the configured roll
|
||||
await roll.evaluate({async: true});
|
||||
|
||||
// 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;
|
||||
// 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;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -193,13 +167,12 @@ 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};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -237,67 +210,49 @@ function _determineAdvantageMode({event, advantage = false, disadvantage = false
|
|||
* @return {Promise<DamageRoll|null>} The evaluated DamageRoll, or null if the workflow was canceled
|
||||
*/
|
||||
export async function damageRoll({
|
||||
parts = [],
|
||||
data, // Roll creation
|
||||
critical = false,
|
||||
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,
|
||||
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");
|
||||
powerfulCritical
|
||||
});
|
||||
|
||||
// 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
|
||||
});
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Prompt a Dialog to further configure the DamageRoll
|
||||
if (!isFF) {
|
||||
const configured = await roll.configureDialog(
|
||||
{
|
||||
title,
|
||||
defaultRollMode: defaultRollMode,
|
||||
defaultCritical: isCritical,
|
||||
template,
|
||||
allowCritical
|
||||
},
|
||||
dialogOptions
|
||||
);
|
||||
if (configured === null) return null;
|
||||
}
|
||||
// Evaluate the configured roll
|
||||
await roll.evaluate({async: true});
|
||||
|
||||
// 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;
|
||||
// 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;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -306,8 +261,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};
|
||||
}
|
||||
|
|
|
@ -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,26 +71,28 @@ 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);
|
||||
|
@ -99,21 +101,22 @@ 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
|
||||
|
@ -137,17 +140,8 @@ export default class D20Roll extends Roll {
|
|||
* @param {object} options Additional Dialog customization options
|
||||
* @returns {Promise<D20Roll|null>} A resulting D20Roll object constructed with the dialog, or null if the dialog was closed
|
||||
*/
|
||||
async configureDialog(
|
||||
{
|
||||
title,
|
||||
defaultRollMode,
|
||||
defaultAction = D20Roll.ADV_MODE.NORMAL,
|
||||
chooseModifier = false,
|
||||
defaultAbility,
|
||||
template
|
||||
} = {},
|
||||
options = {}
|
||||
) {
|
||||
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`,
|
||||
|
@ -160,39 +154,32 @@ 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))
|
||||
},
|
||||
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))
|
||||
}
|
||||
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))
|
||||
},
|
||||
default: defaultButton,
|
||||
close: () => resolve(null)
|
||||
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))
|
||||
}
|
||||
},
|
||||
options
|
||||
).render(true);
|
||||
default: defaultButton,
|
||||
close: () => resolve(null)
|
||||
}, options).render(true);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -208,16 +195,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]})`;
|
||||
}
|
||||
|
||||
|
|
|
@ -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,39 +114,34 @@ export default class DamageRoll extends Roll {
|
|||
* @param {object} options Additional Dialog customization options
|
||||
* @returns {Promise<D20Roll|null>} A resulting D20Roll object constructed with the dialog, or null if the dialog was closed
|
||||
*/
|
||||
async configureDialog(
|
||||
{title, defaultRollMode, defaultCritical = false, template, allowCritical = true} = {},
|
||||
options = {}
|
||||
) {
|
||||
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))
|
||||
},
|
||||
normal: {
|
||||
label: game.i18n.localize(allowCritical ? "SW5E.Normal" : "SW5E.Roll"),
|
||||
callback: (html) => resolve(this._onDialogSubmit(html, false))
|
||||
}
|
||||
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))
|
||||
},
|
||||
default: defaultCritical ? "critical" : "normal",
|
||||
close: () => resolve(null)
|
||||
normal: {
|
||||
label: game.i18n.localize(allowCritical ? "SW5E.Normal" : "SW5E.Roll"),
|
||||
callback: html => resolve(this._onDialogSubmit(html, false))
|
||||
}
|
||||
},
|
||||
options
|
||||
).render(true);
|
||||
default: defaultCritical ? "critical" : "normal",
|
||||
close: () => resolve(null)
|
||||
}, options).render(true);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -162,9 +157,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);
|
||||
}
|
||||
|
||||
|
|
85
module/effects.js
vendored
85
module/effects.js
vendored
|
@ -4,28 +4,26 @@
|
|||
* @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});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -34,31 +32,32 @@ 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;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -1,370 +1,361 @@
|
|||
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;
|
||||
// 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})`;
|
||||
}
|
||||
return obj;
|
||||
}, {});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @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})`}
|
||||
);
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}, {});
|
||||
}
|
||||
// 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 {};
|
||||
}
|
||||
|
||||
// Materials
|
||||
else if (consume.type === "material") {
|
||||
return actor.items.reduce((obj, i) => {
|
||||
if (["consumable", "loot"].includes(i.data.type) && !i.data.data.activation) {
|
||||
obj[i.id] = `${i.name} (${i.data.data.quantity})`;
|
||||
}
|
||||
return obj;
|
||||
}, {});
|
||||
}
|
||||
/* -------------------------------------------- */
|
||||
|
||||
// Charges
|
||||
else if (consume.type === "charges") {
|
||||
return actor.items.reduce((obj, i) => {
|
||||
// Limited-use items
|
||||
const uses = i.data.data.uses || {};
|
||||
if (uses.per && uses.max) {
|
||||
const label =
|
||||
uses.per === "charges"
|
||||
? ` (${game.i18n.format("SW5E.AbilityUseChargesLabel", {value: uses.value})})`
|
||||
: ` (${game.i18n.format("SW5E.AbilityUseConsumableLabel", {
|
||||
max: uses.max,
|
||||
per: uses.per
|
||||
})})`;
|
||||
obj[i.id] = i.name + label;
|
||||
}
|
||||
/**
|
||||
* 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");
|
||||
}
|
||||
}
|
||||
|
||||
// Recharging items
|
||||
const recharge = i.data.data.recharge || {};
|
||||
if (recharge.value) obj[i.id] = `${i.name} (${game.i18n.format("SW5E.Recharge")})`;
|
||||
return obj;
|
||||
}, {});
|
||||
} else return {};
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Get the 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);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* 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 type
|
||||
if (item.data.actionType) {
|
||||
props.push(CONFIG.SW5E.itemActionTypes[item.data.actionType]);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
// Action usage
|
||||
if (item.type !== "weapon" && item.data.activation && !isObjectEmpty(item.data.activation)) {
|
||||
props.push(labels.activation, labels.range, labels.target, labels.duration);
|
||||
}
|
||||
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;
|
||||
/* -------------------------------------------- */
|
||||
|
||||
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);
|
||||
}
|
||||
/**
|
||||
* 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")
|
||||
);
|
||||
}
|
||||
|
||||
// Action type
|
||||
if (item.data.actionType) {
|
||||
props.push(CONFIG.SW5E.itemActionTypes[item.data.actionType]);
|
||||
}
|
||||
/* -------------------------------------------- */
|
||||
|
||||
// Action usage
|
||||
if (item.type !== "weapon" && item.data.activation && !isObjectEmpty(item.data.activation)) {
|
||||
props.push(labels.activation, labels.range, labels.target, labels.duration);
|
||||
}
|
||||
return props.filter((p) => !!p);
|
||||
/** @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([["", ""]]) });
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* 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")
|
||||
);
|
||||
// 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 });
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
setPosition(position = {}) {
|
||||
if (!(this._minimized || position.height)) {
|
||||
position.height = this._tabs[0].active === "details" ? "auto" : this.options.height;
|
||||
}
|
||||
return super.setPosition(position);
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* 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([["", ""]])});
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
/** @inheritdoc */
|
||||
async _onSubmit(...args) {
|
||||
if (this._tabs[0].active === "details") this.position.height = "auto";
|
||||
await super._onSubmit(...args);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
|
||||
/* -------------------------------------------- */
|
||||
/* Hotbar Macros */
|
||||
/* -------------------------------------------- */
|
||||
|
@ -10,24 +11,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;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -39,22 +40,20 @@ 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();
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,132 +1,133 @@
|
|||
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;
|
||||
|
||||
// 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
|
||||
};
|
||||
/**
|
||||
* 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;
|
||||
|
||||
// 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;
|
||||
}
|
||||
// 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
|
||||
};
|
||||
|
||||
// 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;
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
/**
|
||||
* Creates a preview of the power template
|
||||
*/
|
||||
drawPreview() {
|
||||
const initialLayer = canvas.activeLayer;
|
||||
|
||||
// Hide the sheet that originated the preview
|
||||
if (this.actorSheet) this.actorSheet.minimize();
|
||||
// Draw the template and switch to the template layer
|
||||
this.draw();
|
||||
this.layer.activate();
|
||||
this.layer.preview.addChild(this);
|
||||
|
||||
// Activate interactivity
|
||||
this.activatePreviewListeners(initialLayer);
|
||||
}
|
||||
// Hide the sheet that originated the preview
|
||||
if ( this.actorSheet ) this.actorSheet.minimize();
|
||||
|
||||
/* -------------------------------------------- */
|
||||
// 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;
|
||||
/* -------------------------------------------- */
|
||||
|
||||
// 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;
|
||||
};
|
||||
/**
|
||||
* 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;
|
||||
|
||||
// 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();
|
||||
};
|
||||
// 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;
|
||||
};
|
||||
|
||||
// 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]);
|
||||
};
|
||||
// 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();
|
||||
};
|
||||
|
||||
// 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();
|
||||
};
|
||||
// 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]);
|
||||
};
|
||||
|
||||
// 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;
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,144 +1,145 @@
|
|||
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
|
||||
});
|
||||
export const registerSystemSettings = function() {
|
||||
|
||||
/**
|
||||
* 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"
|
||||
}
|
||||
});
|
||||
/**
|
||||
* 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 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 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 Initiative formula setting
|
||||
*/
|
||||
game.settings.register("sw5e", "initiativeDexTiebreaker", {
|
||||
name: "SETTINGS.5eInitTBN",
|
||||
hint: "SETTINGS.5eInitTBL",
|
||||
scope: "world",
|
||||
config: true,
|
||||
default: false,
|
||||
type: Boolean
|
||||
});
|
||||
/**
|
||||
* 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
|
||||
});
|
||||
|
||||
/**
|
||||
* Require Currency Carrying Weight
|
||||
*/
|
||||
game.settings.register("sw5e", "currencyWeight", {
|
||||
name: "SETTINGS.5eCurWtN",
|
||||
hint: "SETTINGS.5eCurWtL",
|
||||
scope: "world",
|
||||
config: true,
|
||||
default: true,
|
||||
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
|
||||
});
|
||||
|
||||
/**
|
||||
* 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
|
||||
});
|
||||
/**
|
||||
* 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 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 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 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 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();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 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"
|
||||
}
|
||||
});
|
||||
/**
|
||||
* 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"
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -3,33 +3,34 @@
|
|||
* Pre-loaded templates are compiled and cached for fast access when rendering
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const preloadHandlebarsTemplates = async function () {
|
||||
return loadTemplates([
|
||||
// Shared Partials
|
||||
"systems/sw5e/templates/actors/parts/active-effects.html",
|
||||
export const preloadHandlebarsTemplates = async function() {
|
||||
return loadTemplates([
|
||||
|
||||
// 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",
|
||||
// Shared Partials
|
||||
"systems/sw5e/templates/actors/parts/active-effects.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",
|
||||
// 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",
|
||||
|
||||
// Item Sheet Partials
|
||||
"systems/sw5e/templates/items/parts/item-action.html",
|
||||
"systems/sw5e/templates/items/parts/item-activation.html",
|
||||
"systems/sw5e/templates/items/parts/item-description.html",
|
||||
"systems/sw5e/templates/items/parts/item-mountable.html"
|
||||
]);
|
||||
"systems/sw5e/templates/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"
|
||||
]);
|
||||
};
|
||||
|
|
|
@ -3,10 +3,11 @@
|
|||
* @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);
|
||||
}
|
||||
|
@ -14,16 +15,19 @@ 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);
|
||||
}
|
||||
|
||||
|
@ -37,6 +41,7 @@ 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);
|
||||
|
@ -53,50 +58,42 @@ 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);
|
||||
}
|
||||
}
|
||||
|
|
12
package-lock.json
generated
12
package-lock.json
generated
|
@ -1266,9 +1266,9 @@
|
|||
}
|
||||
},
|
||||
"hosted-git-info": {
|
||||
"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=="
|
||||
"version": "2.8.8",
|
||||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
|
||||
"integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg=="
|
||||
},
|
||||
"image-size": {
|
||||
"version": "0.5.5",
|
||||
|
@ -3068,9 +3068,9 @@
|
|||
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
|
||||
},
|
||||
"y18n": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz",
|
||||
"integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ=="
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
|
||||
"integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE="
|
||||
},
|
||||
"yargs": {
|
||||
"version": "7.1.1",
|
||||
|
|
|
@ -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":"<p>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.</p>","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":"<p>Power cells fuel blaster weapons that deal energy or ion damage. Additionally, power cells are used to energize certain tools.</p>","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":"<p>Power cells fuel blaster weapons that deal energy or ion damage. Additionally, power cells are used to energize certain tools.</p>","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":"<p>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.</p>","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":"<p>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.</p>","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":"<p>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.</p>","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,4 +111,3 @@
|
|||
{"name":"Headcomm","permission":{"default":0,"vXYkFWX6qzvOu2jc":3},"type":"loot","data":{"description":{"value":"<p>A headcomm can be installed in a helmet or worn independently. It functions as a hands-free commlink.</p>","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":"<p>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.</p>","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":"<p>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.</p>","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":"<p>Power cells fuel blaster weapons that deal energy or ion damage. Additionally, power cells are used to energize certain tools.</p>","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"}
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,6 +1,6 @@
|
|||
{"_id":"AAA9PWi1rTiSUIIe","name":"Lightweight Armor","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"equipment","data":{"description":{"value":"<p>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.</p>","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":"<p>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.</p>","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":"<p>Quick-Charge Shields, opposite of Fortress Shields, offer a reduced capacity but rapidly replenish.</p>","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":"<p>Directional Shields are the most commonly used and balanced shields on the market.</p>","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":"<p>Fortress shields offer a higher maximum shield points, but regenerate slower than normal shields.</p>","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":"<p>Deflection armor is the most common type of armor aboard ships, and offers no benefit or penalty to armor class or hull points.</p>","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":[]}
|
||||
{"_id":"AAA9PWi1rTiSUIIe","name":"Lightweight Armor","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"equipment","data":{"description":{"value":"<p>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.</p>","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":"<p>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.</p>","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":"<p>Quick-Charge Shields, opposite of Fortress Shields, offer a reduced capacity but rapidly replenish.</p>","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":"<p>Directional Shields are the most commonly used and balanced shields on the market.</p>","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":"<p>Fortress shields offer a higher maximum shield points, but regenerate slower than normal shields.</p>","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":"<p>Deflection armor is the most common type of armor aboard ships, and offers no benefit or penalty to armor class or hull points.</p>","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":[]}
|
||||
|
|
|
@ -5,10 +5,10 @@
|
|||
{"_id":"MVXftcjJ1yzsCU3N","name":"Hyperdrive, Class 0.5","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"equipment","data":{"description":{"value":"<p>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.</p>","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":"<p>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.</p>","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":"<p>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.</p>","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":"<p>Power core reactors have highly variable power output capabilities, but sacrifice fuel economy and as a result.</p>","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":"UAiau5ZNXVJAJFUn","name":"Power Core Reactor","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"equipment","data":{"description":{"value":"<p>Power core reactors have highly variable power output capabilities, but sacrifice fuel economy and as a result.</p>","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":"VzkRXuQx2sqN9nd0","name":"Distributed Power Coupling","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"equipment","data":{"description":{"value":"<p>Distributed power coupling sacrifices flexibility by allocating power separately to each system.</p>","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":"<p>Ionization reactors are highly fuel-efficient reactors that trade power output for fuel economy.</p>","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":"<p>Fuel cell reactors are the most common and balanced reactors on the market.</p>","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"}
|
||||
{"_id":"ZyEdKtLwSXuUQs0P","name":"Ionization Reactor","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"equipment","data":{"description":{"value":"<p>Ionization reactors are highly fuel-efficient reactors that trade power output for fuel economy.</p>","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":"<p>Fuel cell reactors are the most common and balanced reactors on the market.</p>","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"}
|
||||
{"name":"Hyperdrive, Class 1.0","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"equipment","data":{"description":{"value":"<p>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.</p>","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":"<p>Direct power coupling has a central power capacitor that feeds power directly to each system.</p>","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":"<p>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.</p>","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"}
|
||||
|
|
6
packs/packs/starships.db
Normal file
6
packs/packs/starships.db
Normal file
|
@ -0,0 +1,6 @@
|
|||
{"_id":"6BN8l5E8QtYt103T","name":"Small Starship","permission":{"default":0,"yXqD5rPwgjXHtqeZ":3},"type":"starship","data":{"description":{"value":"<p>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.</p>\n<p>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.</p>\n<p>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.</p>"},"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":"<p>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.</p>\n<p>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.</p>\n<p>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.</p>\n<p>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?\"</p>\n<p>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.</p>"},"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":"<p>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.</p>\n<p>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.</p>\n<p>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.</p>\n<p>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.</p>\n<p>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.</p>"},"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":"<p>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.</p>\n<p>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.</p>\n<p>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.</p>\n<p>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.</p>"},"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":"<p>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.</p>\n<p>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.</p>\n<p>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.</p>\n<p>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.</p>\n<p>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.</p>"},"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":"<p>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.</p>\n<p>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.\"</p>\n<p>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.</p>"},"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}]}
|
|
@ -137,7 +137,6 @@
|
|||
{"_id":"uQ2AXesizBRcTjRl","name":"Ion Carbine","permission":{"default":0,"vXYkFWX6qzvOu2jc":3,"5TZqObbCr9nKC79s":3},"type":"weapon","data":{"description":{"value":"<p>Reload 16</p>","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":"<p>Ammunition (range 80/320), Burst 4, Reload 8, Silent, Strength 15, Two-handed</p>","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":"<div>\n<p>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.</p>\n<p>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.</p>\n<p>Blaster. While in this mode, the weapon uses traditional power cells.</p>\n<p>Sniper. While in this mode, the weapon uses traditional power cells.</p>\n</div>\n<div>Antiarmor: Special, Ammunition (range 60/240), reload 1, special</div>\n<div>Blaster: 1d8 Energy, Ammunition (range 80/320), reload 12</div>\n<div>Sniper: 1d12 Energy, Ammunition (range 120/480), reload 4</div>\n<div> </div>\n<div>Special, Strength 13, Two-handed</div>","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":"<p>Burst 4, Reload 4, Strength 11</p>","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":"<p>Disruptive, Finesse, Shocking 13</p>","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":[]}
|
||||
|
|
|
@ -797,3 +797,14 @@ 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;
|
||||
}
|
|
@ -1757,3 +1757,78 @@ 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;
|
||||
}
|
|
@ -784,3 +784,14 @@ 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;
|
||||
}
|
1
sw5e.css
1
sw5e.css
|
@ -429,6 +429,7 @@
|
|||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: block;
|
||||
}
|
||||
.sw5e.sheet .items-list .item-name {
|
||||
flex: 2;
|
||||
|
|
462
sw5e.js
462
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,137 +46,122 @@ import * as migrations from "./module/migration.js";
|
|||
/* Foundry VTT Initialization */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
Hooks.once("init", function () {
|
||||
console.log(`SW5e | Initializing SW5E System\n${SW5E.ASCII}`);
|
||||
// Keep on while migrating to Foundry version 0.8
|
||||
CONFIG.debug.hooks = true;
|
||||
|
||||
// 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
|
||||
};
|
||||
Hooks.once("init", function() {
|
||||
console.log(`SW5e | Initializing SW5E System\n${SW5E.ASCII}`);
|
||||
|
||||
// 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"];
|
||||
// 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
|
||||
};
|
||||
|
||||
CONFIG.Dice.DamageRoll = dice.DamageRoll;
|
||||
CONFIG.Dice.D20Roll = dice.D20Roll;
|
||||
// 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"
|
||||
];
|
||||
|
||||
// 5e cone RAW should be 53.13 degrees
|
||||
CONFIG.MeasuredTemplate.defaults.angle = 53.13;
|
||||
CONFIG.Dice.DamageRoll = dice.DamageRoll;
|
||||
CONFIG.Dice.D20Roll = dice.D20Roll;
|
||||
|
||||
// Add DND5e namespace for module compatability
|
||||
game.dnd5e = game.sw5e;
|
||||
CONFIG.DND5E = CONFIG.SW5E;
|
||||
// 5e cone RAW should be 53.13 degrees
|
||||
CONFIG.MeasuredTemplate.defaults.angle = 53.13;
|
||||
|
||||
// Register System Settings
|
||||
registerSystemSettings();
|
||||
// Add DND5e namespace for module compatability
|
||||
game.dnd5e = game.sw5e;
|
||||
CONFIG.DND5E = CONFIG.SW5E;
|
||||
|
||||
// Patch Core Functions
|
||||
CONFIG.Combat.initiative.formula = "1d20 + @attributes.init.mod + @attributes.init.prof + @attributes.init.bonus";
|
||||
Combatant.prototype._getInitiativeFormula = _getInitiativeFormula;
|
||||
// Register System Settings
|
||||
registerSystemSettings();
|
||||
|
||||
// Register Roll Extensions
|
||||
CONFIG.Dice.rolls.push(dice.D20Roll);
|
||||
CONFIG.Dice.rolls.push(dice.DamageRoll);
|
||||
// Patch Core Functions
|
||||
CONFIG.Combat.initiative.formula = "1d20 + @attributes.init.mod + @attributes.init.prof + @attributes.init.bonus";
|
||||
Combatant.prototype._getInitiativeFormula = _getInitiativeFormula;
|
||||
|
||||
// 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 Roll Extensions
|
||||
CONFIG.Dice.rolls.push(dice.D20Roll);
|
||||
CONFIG.Dice.rolls.push(dice.DamageRoll);
|
||||
|
||||
// Preload Handlebars Templates
|
||||
return preloadHandlebarsTemplates();
|
||||
// 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();
|
||||
});
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Foundry VTT Setup */
|
||||
/* -------------------------------------------- */
|
||||
|
@ -184,175 +169,138 @@ 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 () {
|
||||
// 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"
|
||||
];
|
||||
Hooks.once("setup", function() {
|
||||
|
||||
// Exclude some from sorting where the default order matters
|
||||
const noSort = [
|
||||
"abilities",
|
||||
"alignments",
|
||||
"currencies",
|
||||
"distanceUnits",
|
||||
"movementUnits",
|
||||
"itemActionTypes",
|
||||
"proficiencyLevels",
|
||||
"limitedUsePeriods",
|
||||
"powerComponents",
|
||||
"powerLevels",
|
||||
"powerPreparationModes",
|
||||
"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"
|
||||
];
|
||||
|
||||
// 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);
|
||||
// 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);
|
||||
});
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/**
|
||||
* Once the entire VTT framework is initialized, check to see if we should perform a data migration
|
||||
*/
|
||||
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));
|
||||
Hooks.once("ready", function() {
|
||||
|
||||
// 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;
|
||||
// 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));
|
||||
|
||||
// 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();
|
||||
// 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();
|
||||
});
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* 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);
|
||||
|
||||
// Highlight critical success or failure die
|
||||
chat.highlightCriticalSuccessFailure(app, html, data);
|
||||
// Display action buttons
|
||||
chat.displayChatActionButtons(app, html, data);
|
||||
|
||||
// Optionally collapse the content
|
||||
if (game.settings.get("sw5e", "autoCollapseItemCards")) html.find(".card-content").hide();
|
||||
// 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();
|
||||
});
|
||||
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);
|
||||
});
|
||||
}
|
||||
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);
|
||||
})
|
||||
}
|
36
system.json
36
system.json
|
@ -109,6 +109,42 @@
|
|||
"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",
|
||||
|
|
178
template.json
178
template.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"Actor": {
|
||||
"types": ["character", "npc", "vehicle"],
|
||||
"types": ["character", "npc", "starship", "vehicle"],
|
||||
"templates": {
|
||||
"common": {
|
||||
"abilities": {
|
||||
|
@ -37,8 +37,8 @@
|
|||
"value": 10,
|
||||
"min": 0,
|
||||
"max": 10,
|
||||
"temp": 0,
|
||||
"tempmax": 0
|
||||
"temp": null,
|
||||
"tempmax": null
|
||||
},
|
||||
"init": {
|
||||
"value": 0,
|
||||
|
@ -89,6 +89,15 @@
|
|||
},
|
||||
"creature": {
|
||||
"attributes": {
|
||||
"rank": {
|
||||
"total": 0,
|
||||
"coord": 0,
|
||||
"gunner": 0,
|
||||
"mechanic": 0,
|
||||
"operator": 0,
|
||||
"pilot": 0,
|
||||
"technician": 0
|
||||
},
|
||||
"senses": {
|
||||
"darkvision": 0,
|
||||
"blindsight": 0,
|
||||
|
@ -416,31 +425,119 @@
|
|||
"starship": {
|
||||
"templates": ["common"],
|
||||
"attributes": {
|
||||
"cargcap": 0,
|
||||
"crewcap": 0,
|
||||
"cscap": 0,
|
||||
"cost": {
|
||||
"baseBuild": 0,
|
||||
"baseUpgrade": 0,
|
||||
"multEquip": 0,
|
||||
"multModification": 0,
|
||||
"multUpgrade": 0
|
||||
},
|
||||
"death": {
|
||||
"failure": 0,
|
||||
"success": 0
|
||||
},
|
||||
"dr": 0,
|
||||
"engpow": 1,
|
||||
"exhaustion": 0,
|
||||
"hsm": 1,
|
||||
"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
|
||||
},
|
||||
"hull": {
|
||||
"die": "",
|
||||
"dice": 0,
|
||||
"dicemax": 0,
|
||||
"formula":"",
|
||||
"value": null,
|
||||
"max": null
|
||||
},
|
||||
"mods": {
|
||||
"open": 10,
|
||||
"max": 10
|
||||
"capUsed": 0,
|
||||
"capLimit": 10,
|
||||
"hardpoints":{
|
||||
"open": 0,
|
||||
"max": 0
|
||||
},
|
||||
"installed": 0,
|
||||
"suites": {
|
||||
"open": 0,
|
||||
"max": 0,
|
||||
"cap": 0
|
||||
}
|
||||
},
|
||||
"pwrdice": {
|
||||
"pwrdie": "",
|
||||
"recovery": 1,
|
||||
"power": {
|
||||
"die": "",
|
||||
"routing":{
|
||||
"engines": 1,
|
||||
"shields": 1,
|
||||
"weapons": 1
|
||||
},
|
||||
"central": {
|
||||
"value": 0,
|
||||
"max": 0
|
||||
|
@ -469,21 +566,24 @@
|
|||
"shld": {
|
||||
"die": "",
|
||||
"dice": 0,
|
||||
"dicemax": 0,
|
||||
"depleted": false,
|
||||
"formula":"",
|
||||
"value": null,
|
||||
"max": null
|
||||
},
|
||||
"shieldpow": 1,
|
||||
"sscap": 0,
|
||||
"suites": {
|
||||
"open": 0,
|
||||
"max": 0
|
||||
},
|
||||
"weaponpow": 1
|
||||
"used": false,
|
||||
"workforce": {
|
||||
"max": 0,
|
||||
"minBuild": 0,
|
||||
"minEquip": 0,
|
||||
"minModification": 0,
|
||||
"minUpgrade": 0
|
||||
}
|
||||
},
|
||||
"details": {
|
||||
"tier": 0,
|
||||
"role": "",
|
||||
"role": [],
|
||||
"source": ""
|
||||
},
|
||||
"skills": {
|
||||
|
@ -507,7 +607,7 @@
|
|||
"value": 0,
|
||||
"ability": "cha"
|
||||
},
|
||||
"int": {
|
||||
"inf": {
|
||||
"value": 0,
|
||||
"ability": "cha"
|
||||
},
|
||||
|
@ -545,7 +645,7 @@
|
|||
}
|
||||
},
|
||||
"traits": {
|
||||
"size": "med"
|
||||
"size": null
|
||||
}
|
||||
},
|
||||
"vehicle": {
|
||||
|
@ -832,7 +932,7 @@
|
|||
"capx": {
|
||||
"value": null
|
||||
},
|
||||
"hpperhd": {
|
||||
"dmgred": {
|
||||
"value": null
|
||||
},
|
||||
"regrateco": {
|
||||
|
@ -1015,12 +1115,34 @@
|
|||
"size": "",
|
||||
"tier": 0,
|
||||
"hullDice": "d6",
|
||||
"hullDiceStart": 1,
|
||||
"hullDiceStart": 3,
|
||||
"hullDiceRolled":[6,4,4],
|
||||
"hullDiceUsed": 0,
|
||||
"shldDice": "d6",
|
||||
"shldDiceStart": 1,
|
||||
"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": 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": {
|
||||
|
|
|
@ -8,6 +8,26 @@
|
|||
<li class="filter-item" data-filter="reaction">{{localize "SW5E.Reaction"}}</li>
|
||||
</ul>
|
||||
|
||||
<ol>
|
||||
<li>Coordinator: {{data.attributes.deployment.coord.name}}</li>
|
||||
<li> Rank: {{data.attributes.deployment.coord.rank}}</li>
|
||||
<li> Prof: {{data.attributes.deployment.coord.prof}}</li>
|
||||
<li>Gunner: {{data.attributes.deployment.gunner.name}}</li>
|
||||
<li> Rank: {{data.attributes.deployment.gunner.rank}}</li>
|
||||
<li> Prof: {{data.attributes.deployment.gunner.prof}}</li>
|
||||
<li>Mechanic: {{data.attributes.deployment.mechanic.name}}</li>
|
||||
<li> Rank: {{data.attributes.deployment.mechanic.rank}}</li>
|
||||
<li> Prof: {{data.attributes.deployment.mechanic.prof}}</li>
|
||||
<li>Operator: {{data.attributes.deployment.operator.name}}</li>
|
||||
<li> Rank: {{data.attributes.deployment.operator.rank}}</li>
|
||||
<li> Prof: {{data.attributes.deployment.operator.prof}}</li>
|
||||
<li>Pilot: {{data.attributes.deployment.pilot.name}}</li>
|
||||
<li> Rank: {{data.attributes.deployment.pilot.rank}}</li>
|
||||
<li> Prof: {{data.attributes.deployment.pilot.prof}}</li>
|
||||
<li>Technician: {{data.attributes.deployment.technician.name}}</li>
|
||||
<li> Rank: {{data.attributes.deployment.technician.rank}}</li>
|
||||
<li> Prof: {{data.attributes.deployment.technician.prof}}</li>
|
||||
</ol>
|
||||
|
||||
<ol class="group-list">
|
||||
{{#each sections as |section sid|}}
|
||||
|
|
|
@ -14,14 +14,8 @@
|
|||
|
||||
</div>
|
||||
<div class="summary">
|
||||
<!-- <input type="text" name="data.traits.size" value="{{data.traits.size}}"
|
||||
placeholder="{{lookup config.actorSizes data.traits.size}}" style="text-transform: capitalize;" /> -->
|
||||
<span class="summary-input" style="text-transform: capitalize;font-family: 'Russo One';display: inline; height: auto; font-size: 17px; font-weight: 400; letter-spacing: 0.5px; line-height: 24px; color: #4f4f4f;">{{lookup config.actorSizes data.traits.size}}</span>
|
||||
<!-- <input type="text" name="data.details.role" value="{{data.details.role}}"
|
||||
placeholder="{{ localize 'SW5E.Role' }}" /> -->
|
||||
<span class="summary-input" style="text-transform: capitalize;font-family: 'Russo One';display: inline; height: auto; font-size: 17px; font-weight: 400; letter-spacing: 0.5px; line-height: 24px; color: #4f4f4f;">{{lookup config.starshipRolessm data.details.role}}</span>
|
||||
<input type="text" name="data.details.source" value="{{data.details.source}}"
|
||||
placeholder="{{ localize 'SW5E.Source' }}" />
|
||||
<input type="text" name="data.details.source" value="{{data.details.source}}" placeholder="{{ localize 'SW5E.Source' }}" />
|
||||
</div>
|
||||
<div class="attributes">
|
||||
{{!-- ARMOR CLASS --}}
|
||||
|
@ -31,10 +25,11 @@
|
|||
<input class="ac-display" name="data.attributes.ac.value" type="text"
|
||||
value="{{data.attributes.ac.value}}" data-dtype="Number" placeholder="10" />
|
||||
</div>
|
||||
<footer class="attribute-footer proficiency">
|
||||
{{ localize "SW5E.Proficiency" }}
|
||||
{{numberFormat data.attributes.prof decimals=0 sign=true}}
|
||||
</footer>
|
||||
<footer class="attribute-footer hit-dice" style="grid-template-columns: 1fr 1fr 1fr; column-gap: 4px;">
|
||||
<button type="button" class="rest short-rest">{{ localize "SW5E.Recharge" }}</button>
|
||||
<button type="button" class="rest long-rest">{{ localize "SW5E.Refitting" }}</button>
|
||||
<button type="button" class="rest long-rest">{{ localize "SW5E.ShieldRegen" }}</button>
|
||||
</footer>
|
||||
</section>
|
||||
|
||||
{{!-- HULL POINTS --}}
|
||||
|
@ -42,14 +37,15 @@
|
|||
<h1 class="attribute-name rollable">{{ localize "SW5E.HullPoints" }}</h1>
|
||||
<div class="attribute-value multiple">
|
||||
<input name="data.attributes.hp.value" type="text" value="{{data.attributes.hp.value}}"
|
||||
data-dtype="Number" placeholder="0" class="value-number" />
|
||||
data-dtype="Number" class="value-number" />
|
||||
<span class="value-separator">/</span>
|
||||
<input name="data.attributes.hp.max" type="text" value="{{data.attributes.hp.max}}"
|
||||
data-dtype="Number" placeholder="0" class="value-number" />
|
||||
data-dtype="Number" class="value-number" />
|
||||
</div>
|
||||
<footer class="attribute-footer hit-points">
|
||||
<input name="data.attributes.hull.formula" class="hpformula" type="text"
|
||||
placeholder="{{ localize 'SW5E.HullPointsFormula' }}" value="{{data.attributes.hull.formula}}" style="min-width: 150px;" />
|
||||
<footer class="attribute-footer" style="line-height: 12px; height: 12px; text-align: center; font-family: 'Russo One';">
|
||||
<!-- <input name="data.attributes.hull.formula" class="hpformula" type="text"
|
||||
placeholder="{{ localize 'SW5E.HullPointsFormula' }}" value="{{data.attributes.hull.formula}}" style="min-width: 150px;" /> -->
|
||||
<strong>{{localize "SW5E.HullDice"}}:</strong> {{data.attributes.hull.dice}}{{data.attributes.hull.die}}
|
||||
</footer>
|
||||
</section>
|
||||
|
||||
|
@ -57,20 +53,19 @@
|
|||
<section class="attribute health" style="box-sizing: border-box; width: 150px;">
|
||||
<h1 class="attribute-name rollable">{{ localize "SW5E.ShieldPoints" }}</h1>
|
||||
<div class="attribute-value multiple">
|
||||
<input name="data.attributes.hp.temp" type="text" value="{{data.attributes.hp.temp}}"
|
||||
<input name="data.attributes.hp.temp" type="text" value="{{round data.attributes.hp.temp}}"
|
||||
data-dtype="Number" placeholder="0" class="value-number" />
|
||||
<span class="value-separator">/</span>
|
||||
<input name="data.attributes.hp.tempmax" type="text" value="{{data.attributes.hp.tempmax}}"
|
||||
<input name="data.attributes.hp.tempmax" type="text" value="{{round data.attributes.hp.tempmax}}"
|
||||
data-dtype="Number" placeholder="0" class="value-number" />
|
||||
</div>
|
||||
<footer class="attribute-footer hit-points">
|
||||
<input name="data.attributes.shld.Formula" class="hpformula" type="text"
|
||||
placeholder="{{ localize 'SW5E.ShieldPointsFormula' }}" value="{{data.attributes.shld.formula}}" style="min-width: 150px;" />
|
||||
<footer class="attribute-footer" style="line-height: 12px; height: 12px; text-align: center; font-family: 'Russo One';">
|
||||
<!-- <input name="data.attributes.shld.formula" class="hpformula" type="text"
|
||||
placeholder="{{ localize 'SW5E.ShieldPointsFormula' }}" value="{{data.attributes.shld.formula}}" style="min-width: 150px;" /> -->
|
||||
<strong>{{localize "SW5E.ShieldDice"}}: </strong> {{data.attributes.shld.dice}}{{data.attributes.shld.die}}
|
||||
</footer>
|
||||
</section>
|
||||
|
||||
{{!-- SPEED / MOVEMENT TYPES --}}
|
||||
|
||||
<section style="box-sizing: border-box; width: 150px;">
|
||||
<h1>{{ localize "SW5E.Movement" }}
|
||||
<a class="config-button" data-action="movement" title="{{localize 'SW5E.MovementConfig'}}"><i class="fas fa-cog"></i></a>
|
||||
|
@ -145,95 +140,79 @@
|
|||
<section class="panel resources">
|
||||
<h1>Resources and Traits</h1>
|
||||
<div class="traits">
|
||||
<label>
|
||||
{{localize "SW5E.Size"}}
|
||||
<select class="actor-size" name="data.traits.size">
|
||||
{{#select data.traits.size}}
|
||||
<option value=""> </option>
|
||||
{{#each config.actorSizes as |label size|}}
|
||||
<option value="{{size}}">{{label}}</option>
|
||||
{{/each}}
|
||||
{{/select}}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
{{localize "SW5E.Role"}}
|
||||
<select class="actor-size" name="data.details.role">
|
||||
{{#select data.details.role}}
|
||||
<option value=""> </option>
|
||||
{{#if isTiny}}
|
||||
{{#each config.starshipRolestiny as |label role|}}
|
||||
<option value="{{role}}">{{label}}</option>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
{{#if isSmall}}
|
||||
{{#each config.starshipRolessm as |label role|}}
|
||||
<option value="{{role}}">{{label}}</option>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
{{#if isMedium}}
|
||||
{{#each config.starshipRolesmed as |label role|}}
|
||||
<option value="{{role}}">{{label}}</option>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
{{#if isLarge}}
|
||||
{{#each config.starshipRoleslg as |label role|}}
|
||||
<option value="{{role}}">{{label}}</option>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
{{#if isHuge}}
|
||||
{{#each config.starshipRoleshuge as |label role|}}
|
||||
<option value="{{role}}">{{label}}</option>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
{{#if isGargantuan}}
|
||||
{{#each config.starshipRolesgrg as |label role|}}
|
||||
<option value="{{role}}">{{label}}</option>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
{{/select}}
|
||||
</select>
|
||||
</label>
|
||||
<br />
|
||||
<label>
|
||||
{{localize "SW5E.HullDice"}}: <input class="hpformula" style="max-width:50px;" name="data.attributes.hd" value="{{data.attributes.hd}}" placeholder="{{data.attributes.hd}}" />
|
||||
</label>
|
||||
<label>
|
||||
{{localize "SW5E.ShieldDice"}}: <input class="hpformula" style="max-width:50px;" name="data.attributes.sd" value="{{data.attributes.sd}}" placeholder="{{data.attributes.sd}}" />
|
||||
</label>
|
||||
<label>
|
||||
{{localize "SW5E.PowerDice"}}:
|
||||
<select class="actor-size" name="data.attributes.pd">
|
||||
{{#select data.attributes.pd}}
|
||||
<option value=""> </option>
|
||||
{{#each config.powerDieTypes as |pd|}}
|
||||
<option value="{{pd}}">{{pd}}</option>
|
||||
{{/each}}
|
||||
{{/select}}
|
||||
</select>
|
||||
</label>
|
||||
<br />
|
||||
<label>
|
||||
{{localize "SW5E.DmgRed"}}: <input class="hpformula" style="max-width:30px;" name="data.attributes.dr" value="{{data.attributes.dr}}" placeholder="0" />
|
||||
</label>
|
||||
<label>
|
||||
{{localize "SW5E.VehicleCargoCapacity"}}: <input class="hpformula" style="max-width:60px;" name="data.attributes.cargcap" value="{{data.attributes.cargcap}}" placeholder="0" /> tons
|
||||
</label>
|
||||
<br />
|
||||
<label>
|
||||
{{localize "SW5E.CrewCap"}}: <input class="hpformula" style="max-width:60px;" name="data.attributes.crewcap" value="{{data.attributes.crewcap}}" placeholder="0" />
|
||||
</label>
|
||||
<br />
|
||||
<label>
|
||||
<table style="border: none; background: none;">
|
||||
<tr>
|
||||
<td>
|
||||
<label>
|
||||
{{localize "SW5E.VehicleCargoCapacity"}}: {{data.attributes.equip.size.cargoCap}} tons
|
||||
</label>
|
||||
</td>
|
||||
<td>
|
||||
<label>
|
||||
{{localize "SW5E.CrewCap"}}: {{data.attributes.equip.size.crewMinWorkforce}}
|
||||
</label>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<label>
|
||||
{{localize "SW5E.FuelCostPerUnit"}}: {{data.attributes.fuel.cost}} cr/unit
|
||||
</label>
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="rest long-rest burnfuel" style="width:40%;" title="Burn 1 Unit of Fuel">{{ localize "SW5E.BurnFuel" }}</button>
|
||||
<button type="button" class="rest long-rest refuel" style="width:40%;" title="Refuel">{{ localize "SW5E.Refuel" }}</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<label>
|
||||
{{localize "SW5E.FuelCapacity"}}
|
||||
</label>
|
||||
{{#with data.attributes.fuel}}
|
||||
<div class="fuel-wrapper" title="Fuel">
|
||||
<div class="fuel {{#if fueled}}fueled{{/if}}">
|
||||
<span class="fuel-bar" style="width:{{pct}}%"></span>
|
||||
|
||||
<i class="fuel-breakpoint fuel-20 arrow-up"></i>
|
||||
<i class="fuel-breakpoint fuel-20 arrow-down"></i>
|
||||
<i class="fuel-breakpoint fuel-40 arrow-up"></i>
|
||||
<i class="fuel-breakpoint fuel-40 arrow-down"></i>
|
||||
<i class="fuel-breakpoint fuel-60 arrow-up"></i>
|
||||
<i class="fuel-breakpoint fuel-60 arrow-down"></i>
|
||||
<i class="fuel-breakpoint fuel-80 arrow-up"></i>
|
||||
<i class="fuel-breakpoint fuel-80 arrow-down"></i>
|
||||
</div>
|
||||
<span class="fuel-label">{{value}} / {{cap}} units</span>
|
||||
</div>
|
||||
{{/with}}
|
||||
<!-- <label>
|
||||
{{localize "SW5E.CentStorageCapacity"}}: <input class="hpformula" style="max-width:30px;" name="data.attributes.cscap" value="{{data.attributes.cscap}}" placeholder="0" />
|
||||
</label>
|
||||
<label>
|
||||
{{localize "SW5E.SysStorageCapacity"}}: <input class="hpformula" style="max-width:30px;" name="data.attributes.sscap" value="{{data.attributes.sscap}}" placeholder="0" />
|
||||
</label>
|
||||
</div>
|
||||
<h1>{{localize "SW5E.PowerRouting"}}</h1>
|
||||
<div class="traits">
|
||||
<label>
|
||||
</label> -->
|
||||
</div>
|
||||
<h1>{{localize "SW5E.PowerRouting"}}</h1>
|
||||
<div class="traits">
|
||||
<table style="border:none;">
|
||||
<tr>
|
||||
<td align="center"><strong>{{localize "SW5E.EnginePl"}}</strong></td>
|
||||
<td rowspan=3><input type="range" orient="vertical" id="engineslidervalue" class="vertslider" value={{data.attributes.power.routing.engines}} step="1" min="0" max="2" ></td>
|
||||
<td align="center"><strong>{{localize "SW5E.EquipmentShieldProficiency"}}</strong></td>
|
||||
<td rowspan=3><input type="range" orient="vertical" id="shieldslidervalue" class="vertslider" value={{data.attributes.power.routing.shields}} step="1" min="0" max="2" ></td>
|
||||
<td align="center"><strong>{{localize "SW5E.ItemTypeWeaponPl"}}</strong></td>
|
||||
<td rowspan=3><input type="range" orient="vertical" id="weaponslidervalue" class="vertslider" value={{data.attributes.power.routing.weapons}} step="1" min="0" max="2" ></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan=2 align="center"><strong><output for=value id="engineslideroutput">=</output></td>
|
||||
<td rowspan=2 align="center"><strong><output for=value id="shieldslideroutput">=</output></td>
|
||||
<td rowspan=2 align="center"><strong><output for=value id="weaponslideroutput">=</output></td>
|
||||
</tr>
|
||||
<tr></tr>
|
||||
</table>
|
||||
|
||||
<!-- <label>
|
||||
{{localize "SW5E.EnginePl"}}:
|
||||
<select name="data.attributes.engpow">
|
||||
{{#select data.attributes.engpow}}
|
||||
|
@ -263,6 +242,24 @@
|
|||
{{/select}}
|
||||
</select>
|
||||
</label>
|
||||
<br /> -->
|
||||
<table style="border: none; width: 400px;">
|
||||
<tr><th colspan=3 align="left">{{localize "SW5E.PowerDieAlloc"}}</th><th colspan=3 align="right">
|
||||
<label>
|
||||
{{localize "SW5E.PowerDie"}}: {{data.attributes.power.die}}
|
||||
</label>
|
||||
</tr>
|
||||
<tr><th style="border-top: 2px solid #0d99cc; border-left: 2px solid #0d99cc; border-right: 2px solid #0d99cc;">Central</th><th style="border-top: 2px solid #0d99cc; border-left: 2px solid #0d99cc; border-right: 2px solid #0d99cc;">Comms</th><th style="border-top: 2px solid #0d99cc; border-left: 2px solid #0d99cc; border-right: 2px solid #0d99cc;">Engines</th><th style="border-top: 2px solid #0d99cc; border-left: 2px solid #0d99cc; border-right: 2px solid #0d99cc;">Sensors</th><th style="border-top: 2px solid #0d99cc; border-left: 2px solid #0d99cc; border-right: 2px solid #0d99cc;">Shields</th><th style="border-top: 2px solid #0d99cc; border-left: 2px solid #0d99cc; border-right: 2px solid #0d99cc;">Weapons</th>
|
||||
<tr>
|
||||
<td style="border-bottom: 2px solid #0d99cc; border-left: 2px solid #0d99cc; border-right: 2px solid #0d99cc;"><input class="hpformula" style="max-width:25px;" name="data.attributes.power.central.value" value="{{data.attributes.power.central.value}}" placeholder="{{data.attributes.power.central.max}}" /> / {{data.attributes.power.central.max}}</td>
|
||||
<td style="border-bottom: 2px solid #0d99cc; border-left: 2px solid #0d99cc; border-right: 2px solid #0d99cc;"><input class="hpformula" style="max-width:25px;" name="data.attributes.power.comms.value" value="{{data.attributes.power.comms.value}}" placeholder="{{data.attributes.power.comms.max}}" /> / {{data.attributes.power.comms.max}}</td>
|
||||
<td style="border-bottom: 2px solid #0d99cc; border-left: 2px solid #0d99cc; border-right: 2px solid #0d99cc;"><input class="hpformula" style="max-width:25px;" name="data.attributes.power.engines.value" value="{{data.attributes.power.engines.value}}" placeholder="{{data.attributes.power.engines.max}}" /> / {{data.attributes.power.engines.max}}</td>
|
||||
<td style="border-bottom: 2px solid #0d99cc; border-left: 2px solid #0d99cc; border-right: 2px solid #0d99cc;"><input class="hpformula" style="max-width:25px;" name="data.attributes.power.sensors.value" value="{{data.attributes.power.sensors.value}}" placeholder="{{data.attributes.power.sensors.max}}" /> / {{data.attributes.power.sensors.max}}</td>
|
||||
<td style="border-bottom: 2px solid #0d99cc; border-left: 2px solid #0d99cc; border-right: 2px solid #0d99cc;"><input class="hpformula" style="max-width:25px;" name="data.attributes.power.shields.value" value="{{data.attributes.power.shields.value}}" placeholder="{{data.attributes.power.shields.max}}" /> / {{data.attributes.power.shields.max}}</td>
|
||||
<td style="border-bottom: 2px solid #0d99cc; border-left: 2px solid #0d99cc; border-right: 2px solid #0d99cc;"><input class="hpformula" style="max-width:25px;" name="data.attributes.power.weapons.value" value="{{data.attributes.power.weapons.value}}" placeholder="{{data.attributes.power.weapons.max}}" /> / {{data.attributes.power.weapons.max}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</label>
|
||||
</div>
|
||||
<section class="counters" style="border: none; margin: 8px 0; display: grid; grid-template-columns: repeat(2, 1fr);">
|
||||
<div class="counter">
|
||||
|
@ -283,23 +280,26 @@
|
|||
<div class="counter">
|
||||
<h4>{{ localize "SW5E.SystemDrainage" }}</h4>
|
||||
<div class="counter-value" style="text-align: left;">
|
||||
<input type="text" name="data.attributes.exhaustion" data-dtype="Number" placeholder="0"
|
||||
value="{{data.attributes.exhaustion}}" />
|
||||
<input type="text" name="data.attributes.systemDamage" data-dtype="Number" placeholder="0"
|
||||
value="{{data.attributes.systemDamage}}" />
|
||||
</div>
|
||||
</div></section>
|
||||
<h1>{{localize "SW5E.StarshipmodPl"}}</h1>
|
||||
<div class="traits">
|
||||
<label>
|
||||
{{localize "SW5E.ModCap"}}: <input class="hpformula" style="max-width:30px;" name="data.attributes.mods.open" value="{{data.attributes.mods.open}}" placeholder="10" />/<input class="hpformula" style="max-width:30px;" name="data.attributes.mods.max" value="{{data.attributes.mods.max}}" placeholder="10" />
|
||||
{{localize "SW5E.ModCap"}}: <input class="hpformula" style="max-width:30px;" name="data.attributes.mods.capUsed" value="{{data.attributes.mods.capUsed}}" placeholder="10" />/ {{data.attributes.mods.capLimit}}
|
||||
</label>
|
||||
<label>
|
||||
{{localize "SW5E.SuiteCap"}}: <input class="hpformula" style="max-width:30px;" name="data.attributes.suites.open" value="{{data.attributes.suites.open}}" placeholder="0" />/<input class="hpformula" style="max-width:30px;" name="data.attributes.suites.max" value="{{data.attributes.suites.max}}" placeholder="0" />
|
||||
{{localize "SW5E.SuiteCap"}}: <input class="hpformula" style="max-width:30px;" name="data.attributes.suites.open" value="{{data.attributes.mods.suites.open}}" placeholder="0" />/ {{data.attributes.mods.suites.max}} - Occupancy: {{data.attributes.mods.suites.cap}}
|
||||
</label>
|
||||
<br />
|
||||
<label>
|
||||
{{localize "SW5E.HardpointSizeMod"}}: <input class="hpformula" style="max-width:30px;" name="data.attributes.hsm" value="{{data.attributes.hsm}}" placeholder="0" />
|
||||
{{localize "SW5E.HardpointsPerRound"}}: {{data.attributes.mods.hardpoints.max}}
|
||||
</label>
|
||||
</div>
|
||||
<label>
|
||||
{{localize "SW5E.DmgRed"}}: {{data.attributes.equip.armor.dr}}
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
{{!-- Cargo & Crew --}}
|
||||
|
|
18
templates/apps/deployment-prompt.html
Normal file
18
templates/apps/deployment-prompt.html
Normal file
|
@ -0,0 +1,18 @@
|
|||
<div class="dialog-content">
|
||||
{{#each content.i18n}}
|
||||
<div>
|
||||
<label class="checkbox" for="{{@key}}">
|
||||
<input type="checkbox" id="{{@key}}" name="{{@key}}" {{checked (lookup ../content.options @key)}}>
|
||||
{{this}}
|
||||
</label>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
<div class="dialog-buttons">
|
||||
{{#each buttons as |button id|}}
|
||||
<button class="dialog-button" data-button="{{id}}">
|
||||
{{{button.icon}}}
|
||||
{{{button.label}}}
|
||||
</button>
|
||||
{{/each}}
|
||||
</div>
|
|
@ -24,6 +24,10 @@
|
|||
<label>{{localize "SW5E.MovementRoll"}}</label>
|
||||
<input name="data.attributes.movement.roll" type="number" step="0.1" value="{{movement.roll}}"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{{localize "SW5E.MovementSpace"}}</label>
|
||||
<input name="data.attributes.movement.space" type="number" step="0.1" value="{{movement.space}}"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{{localize "SW5E.MovementSwim"}}</label>
|
||||
<input name="data.attributes.movement.swim" type="number" step="0.1" value="{{movement.swim}}"/>
|
||||
|
|
38
templates/apps/recharge-rest.html
Normal file
38
templates/apps/recharge-rest.html
Normal file
|
@ -0,0 +1,38 @@
|
|||
<form id="recharge-rest-hd" class="dialog-content" onsubmit="event.preventDefault();">
|
||||
<p>{{ localize "SW5E.RechargeRestHint" }}</p>
|
||||
<div class="form-group">
|
||||
<label>{{ localize "SW5E.RechargeRestSelect" }}</label>
|
||||
<div class="form-fields">
|
||||
<select name="hd">
|
||||
{{#select denomination}}
|
||||
{{#each availableHD as |num denom|}}
|
||||
<option value="{{denom}}">{{denom}} ({{num}} {{ localize "SW5E.available" }})</option>
|
||||
{{/each}}
|
||||
{{/select}}
|
||||
</select>
|
||||
<button id="roll-hulld" {{#unless canRoll}}disabled{{/unless}}>
|
||||
<i class="fas fa-dice-d20"></i> {{ localize "Roll" }}
|
||||
</button>
|
||||
</div>
|
||||
{{#unless canRoll}}
|
||||
<p class="notes">{{ localize "SW5E.RechargeRestNoHullD" }}</p>
|
||||
{{/unless}}
|
||||
</div>
|
||||
|
||||
{{#if promptNewDay}}
|
||||
<div class="form-group">
|
||||
<label>Is New Day?</label>
|
||||
<input type="checkbox" name="newDay" {{checked newDay}}/>
|
||||
<p class="hint">Recover limited use abilities which recharge "per day"?</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="dialog-buttons">
|
||||
{{#each buttons as |button id|}}
|
||||
<button class="dialog-button" data-button="{{id}}">
|
||||
{{{button.icon}}}
|
||||
{{{button.label}}}
|
||||
</button>
|
||||
{{/each}}
|
||||
</div>
|
||||
</form>
|
20
templates/apps/refitting-rest.html
Normal file
20
templates/apps/refitting-rest.html
Normal file
|
@ -0,0 +1,20 @@
|
|||
<form id="refitting-rest" class="dialog-content" onsubmit="event.preventDefault();">
|
||||
<p>Take a refitting rest? On a refitting rest you will recover hull points, your hull dice, and shields.</p>
|
||||
|
||||
{{#if promptNewDay}}
|
||||
<div class="form-group">
|
||||
<label>Is New Day?</label>
|
||||
<input type="checkbox" name="newDay" {{checked newDay}}/>
|
||||
<p class="hint">Recover limited use abilities which recharge "per day"?</p>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="dialog-buttons">
|
||||
{{#each buttons as |button id|}}
|
||||
<button class="dialog-button" data-button="{{id}}">
|
||||
{{{button.icon}}}
|
||||
{{{button.label}}}
|
||||
</button>
|
||||
{{/each}}
|
||||
</div>
|
||||
</form>
|
|
@ -98,11 +98,11 @@
|
|||
|
||||
<div class="form-group" style="width: 60%;">
|
||||
<strong style="color:#4b4a44; font-size: 11px">{{ localize "SW5E.CapacityMultiplier" }}</strong>
|
||||
<input style="min-width: 5px; max-width: 35px; padding: 0;" type="text" name="data.capx.value" value="{{data.capx.value}}" />
|
||||
<input style="min-width: 5px; max-width: 35px; padding: none;" type="text" name="data.capx.value" value="{{data.capx.value}}" />
|
||||
<strong style="color:#4b4a44; font-size: 11px;">{{ localize "SW5E.DmgRed" }}</strong>
|
||||
<input style="min-width: 5px; max-width: 35px; padding: 0;" type="text" name="data.attributes.dr" value="{{data.attributes.dr}}" />
|
||||
<input style="min-width: 5px; max-width: 35px; padding: none;" type="text" name="data.attributes.dmgred.value" value="{{data.attributes.dmgred.value}}" />
|
||||
<strong style="color:#4b4a44; font-size: 11px;">{{ localize "SW5E.RegenerationRateCoefficient" }}</strong>
|
||||
<input style="min-width: 5px; max-width: 35px; padding: 0;" type="text" name="data.regrateco.value" value="{{data.regrateco.value}}" />
|
||||
<input style="min-width: 5px; max-width: 35px; padding: none;" type="text" name="data.attributes.regrateco.value" value="{{data.regrateco.value}}" />
|
||||
</div>
|
||||
|
||||
{{!-- Starship Equipment Properties --}}
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
<nav class="sheet-navigation tabs" data-group="primary">
|
||||
<a class="item active" data-tab="description">{{ localize "SW5E.Description" }}</a>
|
||||
<a class="item" data-tab="details">{{ localize "SW5E.Details" }}</a>
|
||||
<a class="item" data-tab="effects">{{ localize "SW5E.Effects" }}</a>
|
||||
</nav>
|
||||
|
||||
{{!-- Item Sheet Body --}}
|
||||
|
@ -117,6 +118,155 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{{!-- Construction --}}
|
||||
|
||||
<div class="form-group">
|
||||
<label>{{localize "SW5E.StockCost"}}</label>
|
||||
<div class="form-fields">
|
||||
<input type="text" placeholder="0" name="data.buildBaseCost" value="{{data.buildBaseCost}}" data-dtype="Number"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>{{localize "SW5E.MinConstWorkforce"}}</label>
|
||||
<div class="form-fields">
|
||||
<input type="text" placeholder="0" name="data.buildMinWorkforce" value="{{data.buildMinWorkforce}}" data-dtype="Number"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>{{localize "SW5E.UpgradeCostMult"}}</label>
|
||||
<div class="form-fields">
|
||||
<input type="text" placeholder="0" name="data.upgrdCostMult" value="{{data.upgrdCostMult}}" data-dtype="Number"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>{{localize "SW5E.MinUpgradeWorkforce"}}</label>
|
||||
<div class="form-fields">
|
||||
<input type="text" placeholder="0" name="data.upgrdMinWorkforce" value="{{data.upgrdMinWorkforce}}" data-dtype="Number"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>{{localize "SW5E.BaseSpaceSpeed"}}</label>
|
||||
<div class="form-fields">
|
||||
<input type="text" placeholder="0" name="data.baseSpaceSpeed" value="{{data.baseSpaceSpeed}}" data-dtype="Number"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>{{localize "SW5E.BaseTurnSpeed"}}</label>
|
||||
<div class="form-fields">
|
||||
<input type="text" placeholder="0" name="data.baseTurnSpeed" value="{{data.baseTurnSpeed}}" data-dtype="Number"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>{{localize "SW5E.MinCrewWorkforce"}}</label>
|
||||
<div class="form-fields">
|
||||
<input type="text" placeholder="0" name="data.crewMinWorkforce" value="{{data.crewMinWorkforce}}" data-dtype="Number"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{!-- Modifications --}}
|
||||
|
||||
<div class="form-group">
|
||||
<label>{{localize "SW5E.BaseModCap"}}</label>
|
||||
<div class="form-fields">
|
||||
<input type="text" placeholder="0" name="data.modBaseCap" value="{{data.modBaseCap}}" data-dtype="Number"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>{{localize "SW5E.BaseMaxSuites"}}</label>
|
||||
<div class="form-fields">
|
||||
<input type="text" placeholder="0" name="data.modMaxSuitesBase" value="{{data.modMaxSuitesBase}}" data-dtype="Number"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>{{localize "SW5E.BaseMaxConMult"}}</label>
|
||||
<div class="form-fields">
|
||||
<input type="text" placeholder="0" name="data.modMaxSuitesMult" value="{{data.modMaxSuitesMult}}" data-dtype="Number"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>{{localize "SW5E.MaxSuiteCapacity"}}</label>
|
||||
<div class="form-fields">
|
||||
<input type="text" placeholder="0" name="data.modMaxSuiteCap" value="{{data.modMaxSuiteCap}}" data-dtype="Number"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>{{localize "SW5E.ModCostMult"}}</label>
|
||||
<div class="form-fields">
|
||||
<input type="text" placeholder="0" name="data.modCostMult" value="{{data.modCostMult}}" data-dtype="Number"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>{{localize "SW5E.MinModWorkforce"}}</label>
|
||||
<div class="form-fields">
|
||||
<input type="text" placeholder="0" name="data.modMinWorkforce" value="{{data.modMinWorkforce}}" data-dtype="Number"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>{{localize "SW5E.HardpointStrMult"}}</label>
|
||||
<div class="form-fields">
|
||||
<input type="text" placeholder="0" name="data.hardpointMult" value="{{data.hardpointMult}}" data-dtype="Number"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{!-- Equipment --}}
|
||||
|
||||
<div class="form-group">
|
||||
<label>{{localize "SW5E.EquipCostMult"}}</label>
|
||||
<div class="form-fields">
|
||||
<input type="text" placeholder="0" name="data.equipCostMult" value="{{data.equipCostMult}}" data-dtype="Number"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>{{localize "SW5E.MinEquipWorkforce"}}</label>
|
||||
<div class="form-fields">
|
||||
<input type="text" placeholder="0" name="data.equipMinWorkforce" value="{{data.equipMinWorkforce}}" data-dtype="Number"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>{{localize "SW5E.CargoCap"}}</label>
|
||||
<div class="form-fields">
|
||||
<input type="text" placeholder="0" name="data.cargoCap" value="{{data.cargoCap}}" data-dtype="Number"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>{{localize "SW5E.FuelCost"}}</label>
|
||||
<div class="form-fields">
|
||||
<input type="text" placeholder="0" name="data.fuelCost" value="{{data.fuelCost}}" data-dtype="Number"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>{{localize "SW5E.FuelCap"}}</label>
|
||||
<div class="form-fields">
|
||||
<input type="text" placeholder="0" name="data.fuelCap" value="{{data.fuelCap}}" data-dtype="Number"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>{{localize "SW5E.FoodCap"}}</label>
|
||||
<div class="form-fields">
|
||||
<input type="text" placeholder="0" name="data.foodCap" value="{{data.foodCap}}" data-dtype="Number"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{!-- Effects Tab --}}
|
||||
<div class="tab effects flexcol" data-group="primary" data-tab="effects">
|
||||
{{> "systems/sw5e/templates/actors/parts/active-effects.html"}}
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue