Merge branch 'professorbunbury-sw5e' of https://github.com/unrealkakeman89/sw5e into professorbunbury-sw5e
2
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
{
|
||||
}
|
|
@ -31,6 +31,6 @@ Please reach out on the SW5E Foundry Dev Discord with any questions.
|
|||
## Compatible Modules and Optimum Settings
|
||||
|
||||
- DAE (Dynamic Active Effects) is needed for many automatic features.
|
||||
-**Please enable: "Include active effects in special traits display" in "Configure Game Settings> Module Settings> Dynamic Active Effects".**
|
||||
- **Please enable: "Include active effects in special traits display" in "Configure Game Settings> Module Settings> Dynamic Active Effects".**
|
||||
- Midi QoL is compatible with great features
|
||||
- Token Action Hud has compatibility
|
||||
|
|
42
lang/en.json
|
@ -144,6 +144,10 @@
|
|||
"SW5E.BonusRWDamage": "Ranged Weapon Damage Bonus",
|
||||
"SW5E.BonusSaveForm": "Update Bonuses",
|
||||
"SW5E.BonusPowerDC": "Global Power DC Bonus",
|
||||
"SW5E.BonusForceLightPowerDC": "Global Force Light Power DC Bonus",
|
||||
"SW5E.BonusForceDarkPowerDC": "Global Force Dark Power DC Bonus",
|
||||
"SW5E.BonusForceUnivPowerDC": "Global Force Universal Power DC Bonus",
|
||||
"SW5E.BonusTechPowerDC": "Global Tech Power DC Bonus",
|
||||
"SW5E.BonusTitle": "Configure Actor Bonuses",
|
||||
"SW5E.Bonuses": "Global Bonuses",
|
||||
"SW5E.BonusesHint": "Define global bonuses as formulas which are added to certain rolls. For example: 1d4 + 2",
|
||||
|
@ -588,10 +592,23 @@
|
|||
"SW5E.LongRestGritty": "Long Rest (7 days)",
|
||||
"SW5E.LongRestEpic": "Long Rest (1 hour)",
|
||||
"SW5E.LongRestOvernight": "Long Rest (New Day)",
|
||||
"SW5E.LongRestResult": "{name} takes a long rest and recovers {health} Hit Points and {dice} Hit Dice.",
|
||||
"SW5E.LongRestResultHitDice": "{name} takes a long rest and recovers {dice} Hit Dice.",
|
||||
"SW5E.LongRestResultHitPoints": "{name} takes a long rest and recovers {health} Hit Points.",
|
||||
"SW5E.LongRestResultShort": "{name} takes a long rest.",
|
||||
"SW5E.LongRestResult": "{name} takes a long rest.",
|
||||
"SW5E.LongRestResultFP": "{name} takes a long rest and recovers {force} Force Points.",
|
||||
"SW5E.LongRestResultFPHD": "{name} takes a long rest and recovers {force} Force Points and {dice} Hit Dice.",
|
||||
"SW5E.LongRestResultFPTP": "{name} takes a long rest and recovers {force} Force Points and {tech} Tech Points.",
|
||||
"SW5E.LongRestResultFPTPHD": "{name} takes a long rest and recovers {force} Force Points, {tech} Tech Points and {dice} Hit Dice.",
|
||||
"SW5E.LongRestResultHD": "{name} takes a long rest and recovers {dice} Hit Dice.",
|
||||
"SW5E.LongRestResultHP": "{name} takes a long rest and recovers {health} Hit Points.",
|
||||
"SW5E.LongRestResultHPFP": "{name} takes a long rest and recovers {health} Hit Points and {force} Force Points.",
|
||||
"SW5E.LongRestResultHPHD": "{name} takes a long rest and recovers {health} Hit Points and {dice} Hit Dice.",
|
||||
"SW5E.LongRestResultHPTP": "{name} takes a long rest and recovers {health} Hit Points and {tech} Tech Points.",
|
||||
"SW5E.LongRestResultHPFPHD": "{name} takes a long rest and recovers {health} Hit Points, {force} Force Points, and {dice} Hit Dice.",
|
||||
"SW5E.LongRestResultHPFPTP": "{name} takes a long rest and recovers {health} Hit Points, {force} Force Points and {tech} Tech Points.",
|
||||
"SW5E.LongRestResultHPTPHD": "{name} takes a long rest and recovers {health} Hit Points, {tech} Tech Points, and {dice} Hit Dice.",
|
||||
"SW5E.LongRestResultHPFPTPHD": "{name} takes a long rest and recovers {health} Hit Points, {force} Force Points, {tech} Tech Points and {dice} Hit Dice.",
|
||||
"SW5E.LongRestResultTP": "{name} takes a long rest and recovers {tech} Tech Points.",
|
||||
"SW5E.LongRestResultTPHD": "{name} takes a long rest and recovers {tech} Tech Points and {dice} Hit Dice.",
|
||||
|
||||
"SW5E.Max": "Max",
|
||||
"SW5E.Modifier": "Modifier",
|
||||
"SW5E.Name": "Character Name",
|
||||
|
@ -662,7 +679,6 @@
|
|||
"SW5E.MovementFly": "Fly",
|
||||
"SW5E.MovementSwim": "Swim",
|
||||
"SW5E.MovementUnits": "Units",
|
||||
"SW5E.Senses": "Senses",
|
||||
"SW5E.SensesConfig": "Configure Senses",
|
||||
"SW5E.SensesConfigHint": "Configure any special sensory perception abilities that this actor possesses.",
|
||||
"SW5E.SenseDarkvision": "Darkvision",
|
||||
|
@ -699,6 +715,8 @@
|
|||
"SW5E.ShortRestHint": "Take a short rest? On a short rest you may spend remaining Hit Dice and recover primary or secondary resources.",
|
||||
"SW5E.ShortRestNoHD": "No Hit Dice remaining",
|
||||
"SW5E.ShortRestResult": "{name} takes a short rest spending {dice} Hit Dice to recover {health} Hit Points.",
|
||||
"SW5E.ShortRestResultWithTech": "{name} takes a short rest spending {dice} Hit Dice to recover {health} Hit Points and {tech} Tech Points.",
|
||||
"SW5E.ShortRestResultOnlyTech": "{name} takes a short rest to recover {tech} Tech Points.",
|
||||
"SW5E.ShortRestResultShort": "{name} takes a short rest.",
|
||||
"SW5E.ShortRestSelect": "Select Dice to Roll",
|
||||
"SW5E.Size": "Size",
|
||||
|
@ -746,8 +764,13 @@
|
|||
"SW5E.PowerComponents": "Power Components",
|
||||
"SW5E.PowerCreate": "Create Power",
|
||||
"SW5E.PowerDC": "Power DC",
|
||||
"SW5E.UniversalPowerDC": "Universal Power DC",
|
||||
"SW5E.LightPowerDC": "Light Power DC",
|
||||
"SW5E.DarkPowerDC": "Dark Power DC",
|
||||
"SW5E.TechPowerDC": "Tech Power DC",
|
||||
"SW5E.PowerDetails": "Power Details",
|
||||
"SW5E.PowerEffects": "Power Effects",
|
||||
"SW5E.PowersKnown": "Powers Known",
|
||||
"SW5E.PowerLevel": "Power Level",
|
||||
"SW5E.PowerLevel0": "At-Will",
|
||||
"SW5E.PowerLevel1": "1st Level",
|
||||
|
@ -772,8 +795,11 @@
|
|||
"SW5E.PowerPrepared": "Prepared",
|
||||
"SW5E.PowerConcentrationMode": "Power Concentration Mode",
|
||||
"SW5E.PowerConcentrating": "Concentrating",
|
||||
"SW5E.PowerProgArt": "Artificer",
|
||||
"SW5E.PowerProgFull": "Full Caster",
|
||||
"SW5E.PowerProgCns": "Consular",
|
||||
"SW5E.PowerProgEng": "Engineer",
|
||||
"SW5E.PowerProgGrd": "Guardian",
|
||||
"SW5E.PowerProgSct": "Scout",
|
||||
"SW5E.PowerProgSnt": "Sentinel",
|
||||
"SW5E.PowerProgOverride": "Override slots",
|
||||
"SW5E.PowerProgression": "Power Progression",
|
||||
"SW5E.PowerSchool": "Power School",
|
||||
|
@ -781,6 +807,8 @@
|
|||
"SW5E.PowerUnprepared": "Unprepared",
|
||||
"SW5E.PowerUsage": "Power Usage",
|
||||
"SW5E.Powerbook": "Powerbook",
|
||||
"SW5E.ForcePowerbook": "Force Powers",
|
||||
"SW5E.TechPowerbook": "Tech Powers",
|
||||
"SW5E.SpeciesDescription": "Description",
|
||||
"SW5E.SpeciesTraits": "Species Traits",
|
||||
"SW5E.StealthDisadvantage": "Stealth Disadvantage",
|
||||
|
|
|
@ -400,7 +400,8 @@
|
|||
|
||||
.tab.features,
|
||||
.tab.inventory,
|
||||
.tab.powerbook {
|
||||
.tab.force-powerbook,
|
||||
.tab.tech-powerbook {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
/* Basic Structure */
|
||||
/* ----------------------------------------- */
|
||||
.sw5e.sheet.actor.npc {
|
||||
min-width: 600px;
|
||||
min-width: 872px;
|
||||
min-height: 680px;
|
||||
|
||||
.header-exp {
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
@blockquoteShadow: 0 0 20px rgba(@colorBlue, 0.8);
|
||||
|
||||
//forms
|
||||
@inputBackgroundColor: @colorGray;
|
||||
@inputBackgroundColor: white;
|
||||
@inputBorderNormal: @colorLightGray;
|
||||
@inputBorderHover: @colorGray;
|
||||
@inputBorderFocus: @colorRed;
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
//SW5e Colors
|
||||
@colorBlack: #1C1C1C;
|
||||
@colorDarkGray: #363636;
|
||||
@colorGray: #a9a9a9;
|
||||
@colorGray: #4f4f4f;
|
||||
@colorLightGray: #828282;
|
||||
@colorPaleGray: #D6D6D6;
|
||||
@colorRed: #c40f0f;
|
||||
|
|
|
@ -382,7 +382,8 @@
|
|||
}
|
||||
|
||||
|
||||
.tab.powerbook {
|
||||
.tab.force-powerbook,
|
||||
.tab.tech-powerbook {
|
||||
.powercasting-ability {
|
||||
label,
|
||||
h3 {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
input[type="text"], input[type="number"], input[type="password"], input[type="date"], input[type="time"], select, textarea {
|
||||
border: 1px solid @inputBorderNormal;
|
||||
color: @inputTextColor;
|
||||
border: 1px solid @inputBorderNormal;
|
||||
&:hover {
|
||||
border-color: @inputBorderHover;
|
||||
}
|
||||
|
@ -50,4 +49,4 @@ form {
|
|||
.notes, .hint {
|
||||
color: rgba(@bodyFontColor, 0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,9 +39,19 @@ body.dark-theme {
|
|||
border-width: 0 0 1px 0;
|
||||
border-bottom: 1px solid @hrColor;
|
||||
}
|
||||
|
||||
select {
|
||||
color: white;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
input[type="text"], input[type="number"], input[type="password"], input[type="date"], input[type="time"], select, textarea {
|
||||
color: @inputTextColor;
|
||||
}
|
||||
|
||||
@import "components/forms-themes.less";
|
||||
@import "components/sidebar-themes.less";
|
||||
@import "components/foundry-nav-themes.less";
|
||||
@import "components/foundry-app-window-themes.less";
|
||||
@import "components/actor-themes.less";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -92,7 +92,10 @@ export default class Actor5e extends Actor {
|
|||
init.total = init.mod + init.prof + init.bonus;
|
||||
|
||||
// Prepare power-casting data
|
||||
data.attributes.powerdc = data.attributes.powercasting ? data.abilities[data.attributes.powercasting].dc : 10;
|
||||
data.attributes.powerForceLightDC = 8 + data.abilities.wis.mod + data.attributes.prof ?? 10;
|
||||
data.attributes.powerForceDarkDC = 8 + data.abilities.cha.mod + data.attributes.prof ?? 10;
|
||||
data.attributes.powerForceUnivDC = Math.max(data.attributes.powerForceLightDC,data.attributes.powerForceDarkDC) ?? 10;
|
||||
data.attributes.powerTechDC = 8 + data.abilities.int.mod + data.attributes.prof ?? 10;
|
||||
this._computePowercastingProgression(this.data);
|
||||
|
||||
// Compute owned item attributes which depend on prepared Actor data
|
||||
|
@ -364,75 +367,213 @@ export default class Actor5e extends Actor {
|
|||
const powers = actorData.data.powers;
|
||||
const isNPC = actorData.type === 'npc';
|
||||
|
||||
// Translate the list of classes into power-casting progression
|
||||
const progression = {
|
||||
total: 0,
|
||||
slot: 0,
|
||||
pact: 0
|
||||
// Translate the list of classes into force and tech power-casting progression
|
||||
const forceProgression = {
|
||||
classes: 0,
|
||||
levels: 0,
|
||||
multi: 0,
|
||||
maxClass: "none",
|
||||
maxClassPriority: 0,
|
||||
maxClassLevels: 0,
|
||||
maxClassPowerLevel: 0,
|
||||
powersKnown: 0,
|
||||
points: 0
|
||||
};
|
||||
const techProgression = {
|
||||
classes: 0,
|
||||
levels: 0,
|
||||
multi: 0,
|
||||
maxClass: "none",
|
||||
maxClassPriority: 0,
|
||||
maxClassLevels: 0,
|
||||
maxClassPowerLevel: 0,
|
||||
powersKnown: 0,
|
||||
points: 0
|
||||
};
|
||||
|
||||
// Keep track of the last seen caster in case we're in a single-caster situation.
|
||||
let caster = null;
|
||||
|
||||
// Tabulate the total power-casting progression
|
||||
const classes = this.data.items.filter(i => i.type === "class");
|
||||
let priority = 0;
|
||||
for ( let cls of classes ) {
|
||||
const d = cls.data;
|
||||
if ( d.powercasting === "none" ) continue;
|
||||
const levels = d.levels;
|
||||
const prog = d.powercasting;
|
||||
|
||||
// Accumulate levels
|
||||
if ( prog !== "pact" ) {
|
||||
caster = cls;
|
||||
progression.total++;
|
||||
}
|
||||
switch (prog) {
|
||||
case 'third': progression.slot += Math.floor(levels / 3); break;
|
||||
case 'half': progression.slot += Math.floor(levels / 2); break;
|
||||
case 'full': progression.slot += levels; break;
|
||||
case 'artificer': progression.slot += Math.ceil(levels / 2); break;
|
||||
case 'pact': progression.pact += levels; break;
|
||||
}
|
||||
case 'consular':
|
||||
priority = 3;
|
||||
forceProgression.levels += levels;
|
||||
forceProgression.multi += (SW5E.powerMaxLevel['consular'][19]/9)*levels;
|
||||
forceProgression.classes++;
|
||||
// see if class controls high level forcecasting
|
||||
if ((levels >= forceProgression.maxClassLevels) && (priority > forceProgression.maxClassPriority)){
|
||||
forceProgression.maxClass = 'consular';
|
||||
forceProgression.maxClassLevels = levels;
|
||||
forceProgression.maxClassPriority = priority;
|
||||
forceProgression.maxClassPowerLevel = SW5E.powerMaxLevel['consular'][Math.clamped((levels - 1), 0, 20)];
|
||||
}
|
||||
// calculate points and powers known
|
||||
forceProgression.powersKnown += SW5E.powersKnown['consular'][Math.clamped((levels - 1), 0, 20)];
|
||||
forceProgression.points += SW5E.powerPoints['consular'][Math.clamped((levels - 1), 0, 20)];
|
||||
break;
|
||||
case 'engineer':
|
||||
priority = 2
|
||||
techProgression.levels += levels;
|
||||
techProgression.multi += (SW5E.powerMaxLevel['engineer'][19]/9)*levels;
|
||||
techProgression.classes++;
|
||||
// see if class controls high level techcasting
|
||||
if ((levels >= techProgression.maxClassLevels) && (priority > techProgression.maxClassPriority)){
|
||||
techProgression.maxClass = 'engineer';
|
||||
techProgression.maxClassLevels = levels;
|
||||
techProgression.maxClassPriority = priority;
|
||||
techProgression.maxClassPowerLevel = SW5E.powerMaxLevel['engineer'][Math.clamped((levels - 1), 0, 20)];
|
||||
}
|
||||
techProgression.powersKnown += SW5E.powersKnown['engineer'][Math.clamped((levels - 1), 0, 20)];
|
||||
techProgression.points += SW5E.powerPoints['engineer'][Math.clamped((levels - 1), 0, 20)];
|
||||
break;
|
||||
case 'guardian':
|
||||
priority = 1;
|
||||
forceProgression.levels += levels;
|
||||
forceProgression.multi += (SW5E.powerMaxLevel['guardian'][19]/9)*levels;
|
||||
forceProgression.classes++;
|
||||
// see if class controls high level forcecasting
|
||||
if ((levels >= forceProgression.maxClassLevels) && (priority > forceProgression.maxClassPriority)){
|
||||
forceProgression.maxClass = 'guardian';
|
||||
forceProgression.maxClassLevels = levels;
|
||||
forceProgression.maxClassPriority = priority;
|
||||
forceProgression.maxClassPowerLevel = SW5E.powerMaxLevel['guardian'][Math.clamped((levels - 1), 0, 20)];
|
||||
}
|
||||
forceProgression.powersKnown += SW5E.powersKnown['guardian'][Math.clamped((levels - 1), 0, 20)];
|
||||
forceProgression.points += SW5E.powerPoints['guardian'][Math.clamped((levels - 1), 0, 20)];
|
||||
break;
|
||||
case 'scout':
|
||||
priority = 1;
|
||||
techProgression.levels += levels;
|
||||
techProgression.multi += (SW5E.powerMaxLevel['scout'][19]/9)*levels;
|
||||
techProgression.classes++;
|
||||
// see if class controls high level techcasting
|
||||
if ((levels >= techProgression.maxClassLevels) && (priority > techProgression.maxClassPriority)){
|
||||
techProgression.maxClass = 'scout';
|
||||
techProgression.maxClassLevels = levels;
|
||||
techProgression.maxClassPriority = priority;
|
||||
techProgression.maxClassPowerLevel = SW5E.powerMaxLevel['scout'][Math.clamped((levels - 1), 0, 20)];
|
||||
}
|
||||
techProgression.powersKnown += SW5E.powersKnown['scout'][Math.clamped((levels - 1), 0, 20)];
|
||||
techProgression.points += SW5E.powerPoints['scout'][Math.clamped((levels - 1), 0, 20)];
|
||||
break;
|
||||
case 'sentinel':
|
||||
priority = 2;
|
||||
forceProgression.levels += levels;
|
||||
forceProgression.multi += (SW5E.powerMaxLevel['sentinel'][19]/9)*levels;
|
||||
forceProgression.classes++;
|
||||
// see if class controls high level forcecasting
|
||||
if ((levels >= forceProgression.maxClassLevels) && (priority > forceProgression.maxClassPriority)){
|
||||
forceProgression.maxClass = 'sentinel';
|
||||
forceProgression.maxClassLevels = levels;
|
||||
forceProgression.maxClassPriority = priority;
|
||||
forceProgression.maxClassPowerLevel = SW5E.powerMaxLevel['sentinel'][Math.clamped((levels - 1), 0, 20)];
|
||||
}
|
||||
forceProgression.powersKnown += SW5E.powersKnown['sentinel'][Math.clamped((levels - 1), 0, 20)];
|
||||
forceProgression.points += SW5E.powerPoints['sentinel'][Math.clamped((levels - 1), 0, 20)];
|
||||
break; }
|
||||
}
|
||||
|
||||
// EXCEPTION: single-classed non-full progression rounds up, rather than down
|
||||
const isSingleClass = (progression.total === 1) && (progression.slot > 0);
|
||||
if (!isNPC && isSingleClass && ['half', 'third'].includes(caster.data.powercasting) ) {
|
||||
const denom = caster.data.powercasting === 'third' ? 3 : 2;
|
||||
progression.slot = Math.ceil(caster.data.levels / denom);
|
||||
// EXCEPTION: multi-classed progression uses multi rounded down rather than levels
|
||||
if (!isNPC && forceProgression.classes > 1) {
|
||||
forceProgression.levels = Math.floor(forceProgression.multi);
|
||||
forceProgression.maxClassPowerLevel = SW5E.powerMaxLevel['multi'][forceProgression.levels - 1];
|
||||
}
|
||||
|
||||
if (!isNPC && techProgression.classes > 1) {
|
||||
techProgression.levels = Math.floor(techProgression.multi);
|
||||
techProgression.maxClassPowerLevel = SW5E.powerMaxLevel['multi'][techProgression.levels - 1];
|
||||
}
|
||||
|
||||
// EXCEPTION: NPC with an explicit power-caster level
|
||||
if (isNPC && actorData.data.details.powerLevel) {
|
||||
progression.slot = actorData.data.details.powerLevel;
|
||||
if (isNPC && actorData.data.details.powerForceLevel) {
|
||||
forceProgression.levels = actorData.data.details.powerForceLevel;
|
||||
actorData.data.attributes.force.level = forceProgression.levels;
|
||||
forceProgression.maxClass = actorData.data.attributes.powercasting;
|
||||
forceProgression.maxClassPowerLevel = SW5E.powerMaxLevel[forceProgression.maxClass][Math.clamped((forceProgression.levels - 1), 0, 20)];
|
||||
}
|
||||
if (isNPC && actorData.data.details.powerTechLevel) {
|
||||
techProgression.levels = actorData.data.details.powerTechLevel;
|
||||
actorData.data.attributes.tech.level = techProgression.levels;
|
||||
techProgression.maxClass = actorData.data.attributes.powercasting;
|
||||
techProgression.maxClassPowerLevel = SW5E.powerMaxLevel[techProgression.maxClass][Math.clamped((techProgression.levels - 1), 0, 20)];
|
||||
}
|
||||
|
||||
// Look up the number of slots per level from the powerLimit table
|
||||
let forcePowerLimit = Array.from(SW5E.powerLimit['none']);
|
||||
for (let i = 0; i < (forceProgression.maxClassPowerLevel); i++) {
|
||||
forcePowerLimit[i] = SW5E.powerLimit[forceProgression.maxClass][i];
|
||||
}
|
||||
|
||||
// Look up the number of slots per level from the progression table
|
||||
const levels = Math.clamped(progression.slot, 0, 20);
|
||||
const slots = SW5E.SPELL_SLOT_TABLE[levels - 1] || [];
|
||||
for ( let [n, lvl] of Object.entries(powers) ) {
|
||||
let i = parseInt(n.slice(-1));
|
||||
if ( Number.isNaN(i) ) continue;
|
||||
if ( Number.isNumeric(lvl.override) ) lvl.max = Math.max(parseInt(lvl.override), 0);
|
||||
else lvl.max = slots[i-1] || 0;
|
||||
lvl.value = parseInt(lvl.value);
|
||||
if ( Number.isNumeric(lvl.foverride) ) lvl.fmax = Math.max(parseInt(lvl.foverride), 0);
|
||||
else lvl.fmax = forcePowerLimit[i-1] || 0;
|
||||
if (isNPC){
|
||||
lvl.fvalue = lvl.fmax;
|
||||
}else{
|
||||
lvl.fvalue = Math.min(parseInt(lvl.fvalue || lvl.value || lvl.fmax),lvl.fmax);
|
||||
}
|
||||
}
|
||||
|
||||
let techPowerLimit = Array.from(SW5E.powerLimit['none']);
|
||||
for (let i = 0; i < (techProgression.maxClassPowerLevel); i++) {
|
||||
techPowerLimit[i] = SW5E.powerLimit[techProgression.maxClass][i];
|
||||
}
|
||||
|
||||
// Determine the Actor's pact magic level (if any)
|
||||
let pl = Math.clamped(progression.pact, 0, 20);
|
||||
powers.pact = powers.pact || {};
|
||||
if ( (pl === 0) && isNPC && Number.isNumeric(powers.pact.override) ) pl = actorData.data.details.powerLevel;
|
||||
for ( let [n, lvl] of Object.entries(powers) ) {
|
||||
let i = parseInt(n.slice(-1));
|
||||
if ( Number.isNaN(i) ) continue;
|
||||
if ( Number.isNumeric(lvl.toverride) ) lvl.tmax = Math.max(parseInt(lvl.toverride), 0);
|
||||
else lvl.tmax = techPowerLimit[i-1] || 0;
|
||||
if (isNPC){
|
||||
lvl.tvalue = lvl.tmax;
|
||||
}else{
|
||||
lvl.tvalue = Math.min(parseInt(lvl.tvalue || lvl.value || lvl.tmax),lvl.tmax);
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the number of Warlock pact slots per level
|
||||
if ( pl > 0) {
|
||||
powers.pact.level = Math.ceil(Math.min(10, pl) / 2);
|
||||
if ( Number.isNumeric(powers.pact.override) ) powers.pact.max = Math.max(parseInt(powers.pact.override), 1);
|
||||
else powers.pact.max = Math.max(1, Math.min(pl, 2), Math.min(pl - 8, 3), Math.min(pl - 13, 4));
|
||||
powers.pact.value = Math.min(powers.pact.value, powers.pact.max);
|
||||
} else {
|
||||
powers.pact.max = parseInt(powers.pact.override) || 0
|
||||
powers.pact.level = powers.pact.max > 0 ? 1 : 0;
|
||||
// Set Force and tech power for PC Actors
|
||||
if (!isNPC && forceProgression.levels){
|
||||
actorData.data.attributes.force.known.max = forceProgression.powersKnown;
|
||||
actorData.data.attributes.force.points.max = forceProgression.points + Math.max(actorData.data.abilities.wis.mod,actorData.data.abilities.cha.mod);
|
||||
actorData.data.attributes.force.level = forceProgression.levels;
|
||||
}
|
||||
if (!isNPC && techProgression.levels){
|
||||
actorData.data.attributes.tech.known.max = techProgression.powersKnown;
|
||||
actorData.data.attributes.tech.points.max = techProgression.points + actorData.data.abilities.int.mod;
|
||||
actorData.data.attributes.tech.level = techProgression.levels;
|
||||
}
|
||||
|
||||
// Tally Powers Known and check for migration first to avoid errors
|
||||
let hasKnownPowers = actorData?.data?.attributes?.force?.known?.value !== undefined;
|
||||
if ( hasKnownPowers ) {
|
||||
const knownPowers = this.data.items.filter(i => i.type === "power");
|
||||
let knownForcePowers = 0;
|
||||
let knownTechPowers = 0;
|
||||
for ( let knownPower of knownPowers ) {
|
||||
const d = knownPower.data;
|
||||
switch (knownPower.data.school){
|
||||
case "lgt":
|
||||
case "uni":
|
||||
case "drk":{
|
||||
knownForcePowers++;
|
||||
break;
|
||||
}
|
||||
case "tec":{
|
||||
knownTechPowers++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
actorData.data.attributes.force.known.value = knownForcePowers;
|
||||
actorData.data.attributes.tech.known.value = knownTechPowers;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -983,7 +1124,7 @@ export default class Actor5e extends Actor {
|
|||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Cause this Actor to take a Short Rest
|
||||
* Cause this Actor to take a Short Rest and regain all Tech Points
|
||||
* During a Short Rest resources and limited item uses may be recovered
|
||||
* @param {boolean} dialog Present a dialog window which allows for rolling hit dice as part of the Short Rest
|
||||
* @param {boolean} chat Summarize the results of the rest workflow as a chat message
|
||||
|
@ -1016,10 +1157,14 @@ export default class Actor5e extends Actor {
|
|||
}
|
||||
}
|
||||
|
||||
// Note the change in HP and HD which occurred
|
||||
// Note the change in HP and HD and TP which occurred
|
||||
const dhd = this.data.data.attributes.hd - hd0;
|
||||
const dhp = this.data.data.attributes.hp.value - hp0;
|
||||
const dtp = this.data.data.attributes.tech.points.max - this.data.data.attributes.tech.points.value;
|
||||
|
||||
// Automatically Retore Tech Points
|
||||
this.update({"data.attributes.tech.points.value": this.data.data.attributes.tech.points.max});
|
||||
|
||||
// Recover character resources
|
||||
const updateData = {};
|
||||
for ( let [k, r] of Object.entries(this.data.data.resources) ) {
|
||||
|
@ -1028,11 +1173,6 @@ export default class Actor5e extends Actor {
|
|||
}
|
||||
}
|
||||
|
||||
// Recover pact slots.
|
||||
const pact = this.data.data.powers.pact;
|
||||
updateData['data.powers.pact.value'] = pact.override || pact.max;
|
||||
await this.update(updateData);
|
||||
|
||||
// Recover item uses
|
||||
const recovery = newDay ? ["sr", "day"] : ["sr"];
|
||||
const items = this.items.filter(item => item.data.data.uses && recovery.includes(item.data.data.uses.per));
|
||||
|
@ -1057,14 +1197,24 @@ export default class Actor5e extends Actor {
|
|||
|
||||
// Summarize the health effects
|
||||
let srMessage = "SW5E.ShortRestResultShort";
|
||||
if ((dhd !== 0) && (dhp !== 0)) srMessage = "SW5E.ShortRestResult";
|
||||
if ((dhd !== 0) && (dhp !== 0)){
|
||||
if (dtp !== 0){
|
||||
srMessage = "SW5E.ShortRestResultWithTech";
|
||||
}else{
|
||||
srMessage = "SW5E.ShortRestResult";
|
||||
}
|
||||
}else{
|
||||
if (dtp !== 0){
|
||||
srMessage = "SW5E.ShortRestResultOnlyTech";
|
||||
}
|
||||
}
|
||||
|
||||
// Create a chat message
|
||||
ChatMessage.create({
|
||||
user: game.user._id,
|
||||
speaker: {actor: this, alias: this.name},
|
||||
flavor: restFlavor,
|
||||
content: game.i18n.format(srMessage, {name: this.name, dice: -dhd, health: dhp})
|
||||
content: game.i18n.format(srMessage, {name: this.name, dice: -dhd, health: dhp, tech: dtp})
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1072,6 +1222,7 @@ export default class Actor5e extends Actor {
|
|||
return {
|
||||
dhd: dhd,
|
||||
dhp: dhp,
|
||||
dtp: dtp,
|
||||
updateData: updateData,
|
||||
updateItems: updateItems,
|
||||
newDay: newDay
|
||||
|
@ -1081,7 +1232,7 @@ export default class Actor5e extends Actor {
|
|||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Take a long rest, recovering HP, HD, resources, and power slots
|
||||
* Take a long rest, recovering HP, HD, resources, Force and Power points and power slots
|
||||
* @param {boolean} dialog Present a confirmation dialog window whether or not to take a long rest
|
||||
* @param {boolean} chat Summarize the results of the rest workflow as a chat message
|
||||
* @param {boolean} newDay Whether the long rest carries over to a new day
|
||||
|
@ -1099,12 +1250,20 @@ export default class Actor5e extends Actor {
|
|||
}
|
||||
}
|
||||
|
||||
// Recover hit points to full, and eliminate any existing temporary HP
|
||||
// Recover hit, tech, and force points to full, and eliminate any existing temporary HP, TP, and FP
|
||||
const dhp = data.attributes.hp.max - data.attributes.hp.value;
|
||||
const dtp = data.attributes.tech.points.max - data.attributes.tech.points.value;
|
||||
const dfp = data.attributes.force.points.max - data.attributes.force.points.value;
|
||||
const updateData = {
|
||||
"data.attributes.hp.value": data.attributes.hp.max,
|
||||
"data.attributes.hp.temp": 0,
|
||||
"data.attributes.hp.tempmax": 0
|
||||
"data.attributes.hp.tempmax": 0,
|
||||
"data.attributes.tech.points.value": data.attributes.tech.points.max,
|
||||
"data.attributes.tech.points.temp": 0,
|
||||
"data.attributes.tech.points.tempmax": 0,
|
||||
"data.attributes.force.points.value": data.attributes.force.points.max,
|
||||
"data.attributes.force.points.temp": 0,
|
||||
"data.attributes.force.points.tempmax": 0
|
||||
};
|
||||
|
||||
// Recover character resources
|
||||
|
@ -1116,13 +1275,11 @@ export default class Actor5e extends Actor {
|
|||
|
||||
// Recover power slots
|
||||
for ( let [k, v] of Object.entries(data.powers) ) {
|
||||
updateData[`data.powers.${k}.value`] = Number.isNumeric(v.override) ? v.override : (v.max ?? 0);
|
||||
updateData[`data.powers.${k}.fvalue`] = Number.isNumeric(v.foverride) ? v.foverride : (v.fmax ?? 0);
|
||||
}
|
||||
for ( let [k, v] of Object.entries(data.powers) ) {
|
||||
updateData[`data.powers.${k}.tvalue`] = Number.isNumeric(v.toverride) ? v.toverride : (v.tmax ?? 0);
|
||||
}
|
||||
|
||||
// Recover pact slots.
|
||||
const pact = data.powers.pact;
|
||||
updateData['data.powers.pact.value'] = pact.override || pact.max;
|
||||
|
||||
// Determine the number of hit dice which may be recovered
|
||||
let recoverHD = Math.max(Math.floor(data.details.level / 2), 1);
|
||||
let dhd = 0;
|
||||
|
@ -1169,15 +1326,16 @@ export default class Actor5e extends Actor {
|
|||
|
||||
// Determine the chat message to display
|
||||
if ( chat ) {
|
||||
let lrMessage = "SW5E.LongRestResultShort";
|
||||
if((dhp !== 0) && (dhd !== 0)) lrMessage = "SW5E.LongRestResult";
|
||||
else if ((dhp !== 0) && (dhd === 0)) lrMessage = "SW5E.LongRestResultHitPoints";
|
||||
else if ((dhp === 0) && (dhd !== 0)) lrMessage = "SW5E.LongRestResultHitDice";
|
||||
let lrMessage = "SW5E.LongRestResult";
|
||||
if (dhp !== 0) lrMessage += "HP";
|
||||
if (dfp !== 0) lrMessage += "FP";
|
||||
if (dtp !== 0) lrMessage += "TP";
|
||||
if (dhd !== 0) lrMessage += "HD";
|
||||
ChatMessage.create({
|
||||
user: game.user._id,
|
||||
speaker: {actor: this, alias: this.name},
|
||||
flavor: restFlavor,
|
||||
content: game.i18n.format(lrMessage, {name: this.name, health: dhp, dice: dhd})
|
||||
content: game.i18n.format(lrMessage, {name: this.name, health: dhp, tech: dtp, force: dfp, dice: dhd})
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1185,6 +1343,8 @@ export default class Actor5e extends Actor {
|
|||
return {
|
||||
dhd: dhd,
|
||||
dhp: dhp,
|
||||
dtp: dtp,
|
||||
dfp: dfp,
|
||||
updateData: updateData,
|
||||
updateItems: updateItems,
|
||||
newDay: newDay
|
||||
|
|
|
@ -21,7 +21,8 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
*/
|
||||
this._filters = {
|
||||
inventory: new Set(),
|
||||
powerbook: new Set(),
|
||||
forcePowerbook: new Set(),
|
||||
techPowerbook: new Set(),
|
||||
features: new Set(),
|
||||
effects: new Set()
|
||||
};
|
||||
|
@ -35,7 +36,8 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
scrollY: [
|
||||
".inventory .group-list",
|
||||
".features .group-list",
|
||||
".powerbook .group-list",
|
||||
".force-powerbook .group-list",
|
||||
".tech-powerbook .group-list",
|
||||
".effects .effects-list"
|
||||
],
|
||||
tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}]
|
||||
|
@ -220,7 +222,7 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
* @param {Array} powers The power data being prepared
|
||||
* @private
|
||||
*/
|
||||
_preparePowerbook(data, powers) {
|
||||
_preparePowerbook(data, powers, school) {
|
||||
const owner = this.actor.owner;
|
||||
const levels = data.data.powers;
|
||||
const powerbook = {};
|
||||
|
@ -229,7 +231,6 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
const sections = {
|
||||
"atwill": -20,
|
||||
"innate": -10,
|
||||
"pact": 0.5
|
||||
};
|
||||
|
||||
// Label power slot uses headers
|
||||
|
@ -251,7 +252,7 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
uses: useLabels[i] || value || 0,
|
||||
slots: useLabels[i] || max || 0,
|
||||
override: override || 0,
|
||||
dataset: {"type": "power", "level": prepMode in sections ? 1 : i, "preparation.mode": prepMode},
|
||||
dataset: {"type": "power", "level": prepMode in sections ? 1 : i, "preparation.mode": prepMode, "school": school},
|
||||
prop: sl
|
||||
};
|
||||
};
|
||||
|
@ -273,19 +274,6 @@ export default class ActorSheet5e extends ActorSheet {
|
|||
}
|
||||
}
|
||||
|
||||
// Pact magic users have cantrips and a pact magic section
|
||||
if ( levels.pact && levels.pact.max ) {
|
||||
if ( !powerbook["0"] ) registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]);
|
||||
const l = levels.pact;
|
||||
const config = CONFIG.SW5E.powerPreparationModes.pact;
|
||||
registerSection("pact", sections.pact, config, {
|
||||
prepMode: "pact",
|
||||
value: l.value,
|
||||
max: l.max,
|
||||
override: l.override
|
||||
});
|
||||
}
|
||||
|
||||
// Iterate over every power item, adding powers to the powerbook by section
|
||||
powers.forEach(power => {
|
||||
const mode = power.data.preparation.mode || "prepared";
|
||||
|
|
|
@ -84,7 +84,7 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
|
|||
};
|
||||
|
||||
// Partition items by category
|
||||
let [items, powers, feats, classes, species, archetypes, classfeatures, backgrounds, fightingstyles, fightingmasteries, lightsaberforms] = data.items.reduce((arr, item) => {
|
||||
let [items, forcepowers, techpowers, feats, classes, species, archetypes, classfeatures, backgrounds, fightingstyles, fightingmasteries, lightsaberforms] = data.items.reduce((arr, item) => {
|
||||
|
||||
// Item details
|
||||
item.img = item.img || DEFAULT_TOKEN;
|
||||
|
@ -112,23 +112,25 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
|
|||
this._prepareItemToggleState(item);
|
||||
|
||||
// Classify items into types
|
||||
if ( item.type === "power" ) arr[1].push(item);
|
||||
else if ( item.type === "feat" ) arr[2].push(item);
|
||||
else if ( item.type === "class" ) arr[3].push(item);
|
||||
else if ( 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);
|
||||
if ( item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school) ) arr[1].push(item);
|
||||
else if ( item.type === "power" && ["tec"].includes(item.data.school) ) arr[2].push(item);
|
||||
else if ( item.type === "feat" ) arr[3].push(item);
|
||||
else if ( item.type === "class" ) arr[4].push(item);
|
||||
else if ( item.type === "species" ) arr[5].push(item);
|
||||
else if ( item.type === "archetype" ) arr[6].push(item);
|
||||
else if ( item.type === "classfeature" ) arr[7].push(item);
|
||||
else if ( item.type === "background" ) arr[8].push(item);
|
||||
else if ( item.type === "fightingstyle" ) arr[9].push(item);
|
||||
else if ( item.type === "fightingmastery" ) arr[10].push(item);
|
||||
else if ( item.type === "lightsaberform" ) arr[11].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);
|
||||
forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook);
|
||||
techpowers = this._filterItems(techpowers, this._filters.techPowerbook);
|
||||
feats = this._filterItems(feats, this._filters.features);
|
||||
|
||||
// Organize items
|
||||
|
@ -140,10 +142,8 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
|
|||
}
|
||||
|
||||
// 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;
|
||||
const forcePowerbook = this._preparePowerbook(data, forcepowers, "uni");
|
||||
const techPowerbook = this._preparePowerbook(data, techpowers, "tec");
|
||||
|
||||
// Organize Features
|
||||
const features = {
|
||||
|
@ -174,8 +174,8 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
|
|||
|
||||
// Assign and return
|
||||
data.inventory = Object.values(inventory);
|
||||
data.powerbook = powerbook;
|
||||
data.preparedPowers = nPrepared;
|
||||
data.forcePowerbook = forcePowerbook;
|
||||
data.techPowerbook = techPowerbook;
|
||||
data.features = Object.values(features);
|
||||
}
|
||||
|
||||
|
|
|
@ -42,24 +42,27 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
|
|||
};
|
||||
|
||||
// Start by classifying items into groups for rendering
|
||||
let [powers, other] = data.items.reduce((arr, item) => {
|
||||
let [forcepowers, techpowers, other] = data.items.reduce((arr, item) => {
|
||||
item.img = item.img || 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);
|
||||
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
|
||||
powers = this._filterItems(powers, this._filters.powerbook);
|
||||
forcepowers = this._filterItems(forcepowers, this._filters.forcePowerbook);
|
||||
techpowers = this._filterItems(techpowers, this._filters.techPowerbook);
|
||||
other = this._filterItems(other, this._filters.features);
|
||||
|
||||
// Organize Powerbook
|
||||
const powerbook = this._preparePowerbook(data, powers);
|
||||
const forcePowerbook = this._preparePowerbook(data, forcepowers, "uni");
|
||||
const techPowerbook = this._preparePowerbook(data, techpowers, "tec");
|
||||
|
||||
// Organize Features
|
||||
for ( let item of other ) {
|
||||
|
@ -73,7 +76,8 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
|
|||
|
||||
// Assign and return
|
||||
data.features = Object.values(features);
|
||||
data.powerbook = powerbook;
|
||||
data.forcePowerbook = forcePowerbook;
|
||||
data.techPowerbook = techPowerbook;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -100,32 +100,68 @@ export default class AbilityUseDialog extends Dialog {
|
|||
|
||||
// Determine the levels which are feasible
|
||||
let lmax = 0;
|
||||
const powerLevels = Array.fromRange(10).reduce((arr, i) => {
|
||||
if ( i < lvl ) return arr;
|
||||
const label = CONFIG.SW5E.powerLevels[i];
|
||||
const l = actorData.powers["power"+i] || {max: 0, override: null};
|
||||
let max = parseInt(l.override || l.max || 0);
|
||||
let slots = Math.clamped(parseInt(l.value || 0), 0, max);
|
||||
if ( max > 0 ) lmax = i;
|
||||
arr.push({
|
||||
level: i,
|
||||
label: i > 0 ? game.i18n.format('SW5E.PowerLevelSlot', {level: label, n: slots}) : label,
|
||||
canCast: max > 0,
|
||||
hasSlots: slots > 0
|
||||
});
|
||||
return arr;
|
||||
}, []).filter(sl => sl.level <= lmax);
|
||||
|
||||
// If this character has pact slots, present them as an option for casting the power.
|
||||
const pact = actorData.powers.pact;
|
||||
if (pact.level >= lvl) {
|
||||
powerLevels.push({
|
||||
level: 'pact',
|
||||
label: `${game.i18n.format('SW5E.PowerLevelPact', {level: pact.level, n: pact.value})}`,
|
||||
canCast: true,
|
||||
hasSlots: pact.value > 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],
|
||||
|
|
|
@ -74,7 +74,11 @@ export default class ActorSheetFlags extends BaseEntitySheet {
|
|||
{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.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) || "";
|
||||
|
|
103
module/config.js
|
@ -517,12 +517,77 @@ SW5E.powerPreparationModes = {
|
|||
"innate": "SW5E.PowerPrepInnate"
|
||||
};
|
||||
|
||||
SW5E.powerUpcastModes = ["always", "pact", "prepared"];
|
||||
SW5E.powerUpcastModes = ["always", "prepared"];
|
||||
|
||||
/**
|
||||
* The available choices for power progression for a character class
|
||||
* @type {Object}
|
||||
*/
|
||||
|
||||
SW5E.powerProgression = {
|
||||
"none": "SW5E.PowerNone",
|
||||
"full": "SW5E.PowerProgFull",
|
||||
"artificer": "SW5E.PowerProgArt"
|
||||
"consular": "SW5E.PowerProgCns",
|
||||
"engineer": "SW5E.PowerProgEng",
|
||||
"guardian": "SW5E.PowerProgGrd",
|
||||
"scout": "SW5E.PowerProgSct",
|
||||
"sentinel": "SW5E.PowerProgSnt"
|
||||
};
|
||||
|
||||
/**
|
||||
* The max number of known powers available to each class per level
|
||||
*/
|
||||
|
||||
SW5E.powersKnown = {
|
||||
"none": [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
|
||||
"consular": [9,11,13,15,17,19,21,23,25,26,28,29,31,32,34,35,37,38,39,40],
|
||||
"engineer": [6,7,9,10,12,13,15,16,18,19,21,22,23,24,25,26,27,28,29,30],
|
||||
"guardian": [5,7,9,10,12,13,14,15,17,18,19,20,22,23,24,25,27,28,29,30],
|
||||
"scout": [0,4,5,6,7,8,9,10,12,13,14,15,16,17,18,19,20,21,22,23],
|
||||
"sentinel": [7,9,11,13,15,17,18,19,21,22,24,25,26,28,29,30,32,33,34,35]
|
||||
};
|
||||
|
||||
/**
|
||||
* The max number of powers cast for each power level per long rest
|
||||
*/
|
||||
|
||||
SW5E.powerLimit = {
|
||||
"none": [0,0,0,0,0,0,0,0,0],
|
||||
"consular": [1000,1000,1000,1000,1000,1,1,1,1],
|
||||
"engineer": [1000,1000,1000,1000,1000,1,1,1,1],
|
||||
"guardian": [1000,1000,1000,1000,1,0,0,0,0],
|
||||
"scout": [1000,1000,1000,1,1,0,0,0,0],
|
||||
"sentinel": [1000,1000,1000,1000,1,1,1,0,0],
|
||||
"innate": [1000,1000,1000,1000,1000,1000,1000,1000,1000],
|
||||
"dual": [1000,1000,1000,1000,1000,1,1,1,1]
|
||||
};
|
||||
|
||||
/**
|
||||
* The max level of a known/overpowered power available to each class per level
|
||||
*/
|
||||
|
||||
SW5E.powerMaxLevel = {
|
||||
"none": [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
|
||||
"consular": [1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,9,9],
|
||||
"engineer": [1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,9,9],
|
||||
"guardian": [1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5],
|
||||
"scout": [0,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5],
|
||||
"sentinel": [1,1,2,2,2,3,3,3,4,4,5,5,5,6,6,6,7,7,7,7],
|
||||
"multi": [1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,9,9],
|
||||
"innate": [1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,9,9],
|
||||
"dual": [1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,9,9]
|
||||
};
|
||||
|
||||
/**
|
||||
* The number of base force/tech points available to each class per level
|
||||
*/
|
||||
|
||||
SW5E.powerPoints = {
|
||||
"none": [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
|
||||
"consular": [4,8,12,16,20,24,28,32,36,40,44,48,52,56,60,64,68,72,76,80],
|
||||
"engineer": [2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40],
|
||||
"guardian": [2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40],
|
||||
"scout": [0,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
|
||||
"sentinel": [3,6,9,12,15,18,21,24,27,30,33,36,39,42,45,48,51,54,57,60]
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -631,36 +696,6 @@ SW5E.powerLevels = {
|
|||
9: "SW5E.PowerLevel9"
|
||||
};
|
||||
|
||||
/**
|
||||
* Define the standard slot progression by character level.
|
||||
* The entries of this array represent the power slot progression for a full power-caster.
|
||||
* @type {Array[]}
|
||||
*/
|
||||
SW5E.SPELL_SLOT_TABLE = [
|
||||
[2],
|
||||
[3],
|
||||
[4, 2],
|
||||
[4, 3],
|
||||
[4, 3, 2],
|
||||
[4, 3, 3],
|
||||
[4, 3, 3, 1],
|
||||
[4, 3, 3, 2],
|
||||
[4, 3, 3, 3, 1],
|
||||
[4, 3, 3, 3, 2],
|
||||
[4, 3, 3, 3, 2, 1],
|
||||
[4, 3, 3, 3, 2, 1],
|
||||
[4, 3, 3, 3, 2, 1, 1],
|
||||
[4, 3, 3, 3, 2, 1, 1],
|
||||
[4, 3, 3, 3, 2, 1, 1, 1],
|
||||
[4, 3, 3, 3, 2, 1, 1, 1],
|
||||
[4, 3, 3, 3, 2, 1, 1, 1, 1],
|
||||
[4, 3, 3, 3, 3, 1, 1, 1, 1],
|
||||
[4, 3, 3, 3, 3, 2, 1, 1, 1],
|
||||
[4, 3, 3, 3, 3, 2, 2, 1, 1]
|
||||
];
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
// Polymorph options.
|
||||
SW5E.polymorphSettings = {
|
||||
keepPhysical: 'SW5E.PolymorphKeepPhysical',
|
||||
|
@ -1207,4 +1242,4 @@ SW5E.characterFlags = {
|
|||
};
|
||||
|
||||
// Configure allowed status flags
|
||||
SW5E.allowedActorFlags = ["isPolymorphed", "originalActor"].concat(Object.keys(SW5E.characterFlags));
|
||||
SW5E.allowedActorFlags = ["isPolymorphed", "originalActor", "dataVersion"].concat(Object.keys(SW5E.characterFlags));
|
||||
|
|
|
@ -25,8 +25,17 @@ export default class Item5e extends Item {
|
|||
else if (this.actor) {
|
||||
const actorData = this.actor.data.data;
|
||||
|
||||
// Powers - Use Actor powercasting modifier
|
||||
if (this.data.type === "power") return actorData.attributes.powercasting || "int";
|
||||
// Powers - Use Actor powercasting modifier based on power school
|
||||
if (this.data.type === "power") {
|
||||
switch (this.data.data.school) {
|
||||
case "lgt": return "wis";
|
||||
case "uni": return (actorData.abilities["wis"].mod >= actorData.abilities["cha"].mod) ? "wis" : "cha";
|
||||
case "drk": return "cha";
|
||||
case "tec": return "int";
|
||||
}
|
||||
return "none";
|
||||
}
|
||||
|
||||
|
||||
// Tools - default to Intelligence
|
||||
else if (this.data.type === "tool") return "int";
|
||||
|
@ -291,7 +300,24 @@ export default class Item5e extends Item {
|
|||
|
||||
// Actor power-DC based scaling
|
||||
if ( save.scaling === "power" ) {
|
||||
save.dc = this.isOwned ? getProperty(this.actor.data, "data.attributes.powerdc") : null;
|
||||
switch (this.data.data.school) {
|
||||
case "lgt": {
|
||||
save.dc = this.isOwned ? getProperty(this.actor.data, "data.attributes.powerForceLightDC") : null;
|
||||
break;
|
||||
}
|
||||
case "uni": {
|
||||
save.dc = this.isOwned ? getProperty(this.actor.data, "data.attributes.powerForceUnivDC") : null;
|
||||
break;
|
||||
}
|
||||
case "drk": {
|
||||
save.dc = this.isOwned ? getProperty(this.actor.data, "data.attributes.powerForceDarkDC") : null;
|
||||
break;
|
||||
}
|
||||
case "tec": {
|
||||
save.dc = this.isOwned ? getProperty(this.actor.data, "data.attributes.powerTechDC") : null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ability-score based scaling
|
||||
|
@ -394,6 +420,7 @@ export default class Item5e extends Item {
|
|||
const recharge = id.recharge || {}; // Recharge mechanic
|
||||
const uses = id?.uses ?? {}; // Limited uses
|
||||
const isPower = this.type === "power"; // Does the item require a power slot?
|
||||
// TODO: Possibly Mod this to not consume slots based on class?
|
||||
const requirePowerSlot = isPower && (id.level > 0) && CONFIG.SW5E.powerUpcastModes.includes(id.preparation.mode);
|
||||
|
||||
// Define follow-up actions resulting from the item usage
|
||||
|
@ -404,11 +431,12 @@ export default class Item5e extends Item {
|
|||
let consumeUsage = !!uses.per; // Consume limited uses
|
||||
let consumeQuantity = uses.autoDestroy; // Consume quantity of the item in lieu of uses
|
||||
|
||||
// Display a configuration dialog to customize the usage
|
||||
// Display a configuration dialog to customize the usage
|
||||
const needsConfiguration = createMeasuredTemplate || consumeRecharge || consumeResource || consumePowerSlot || consumeUsage;
|
||||
if (configureDialog && needsConfiguration) {
|
||||
const configuration = await AbilityUseDialog.create(this);
|
||||
if (!configuration) return;
|
||||
|
||||
|
||||
// Determine consumption preferences
|
||||
createMeasuredTemplate = Boolean(configuration.placeTemplate);
|
||||
|
@ -420,18 +448,20 @@ export default class Item5e extends Item {
|
|||
// Handle power upcasting
|
||||
if ( requirePowerSlot ) {
|
||||
const slotLevel = configuration.level;
|
||||
const powerLevel = slotLevel === "pact" ? actor.data.data.powers.pact.level : parseInt(slotLevel);
|
||||
const powerLevel = parseInt(slotLevel);
|
||||
|
||||
if (powerLevel !== id.level) {
|
||||
const upcastData = mergeObject(this.data, {"data.level": powerLevel}, {inplace: false});
|
||||
item = this.constructor.createOwned(upcastData, actor); // Replace the item with an upcast version
|
||||
}
|
||||
if ( consumePowerSlot ) consumePowerSlot = slotLevel === "pact" ? "pact" : `power${powerLevel}`;
|
||||
if ( consumePowerSlot ) consumePowerSlot = `power${powerLevel}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine whether the item can be used by testing for resource consumption
|
||||
const usage = item._getUsageUpdates({consumeRecharge, consumeResource, consumePowerSlot, consumeUsage, consumeQuantity});
|
||||
if ( !usage ) return;
|
||||
|
||||
const {actorUpdates, itemUpdates, resourceUpdates} = usage;
|
||||
|
||||
// Commit pending data updates
|
||||
|
@ -490,17 +520,53 @@ export default class Item5e extends Item {
|
|||
if ( canConsume === false ) return false;
|
||||
}
|
||||
|
||||
// Consume Power Slots
|
||||
// Consume Power Slots and Force/Tech Points
|
||||
if ( consumePowerSlot ) {
|
||||
const level = this.actor?.data.data.powers[consumePowerSlot];
|
||||
const powers = Number(level?.value ?? 0);
|
||||
if ( powers === 0 ) {
|
||||
const label = game.i18n.localize(consumePowerSlot === "pact" ? "SW5E.PowerProgPact" : `SW5E.PowerLevel${id.level}`);
|
||||
ui.notifications.warn(game.i18n.format("SW5E.PowerCastNoSlots", {name: this.name, level: label}));
|
||||
return false;
|
||||
const fp = this.actor.data.data.attributes.force.points;
|
||||
const tp = this.actor.data.data.attributes.tech.points;
|
||||
const powerCost = id.level + 1;
|
||||
const innatePower = this.actor.data.data.attributes.powercasting === 'innate';
|
||||
if (!innatePower){
|
||||
switch (id.school){
|
||||
case "lgt":
|
||||
case "uni":
|
||||
case "drk": {
|
||||
const powers = Number(level?.fvalue ?? 0);
|
||||
if ( powers === 0 ) {
|
||||
const label = game.i18n.localize(`SW5E.PowerLevel${id.level}`);
|
||||
ui.notifications.warn(game.i18n.format("SW5E.PowerCastNoSlots", {name: this.name, level: label}));
|
||||
return false;
|
||||
}
|
||||
actorUpdates[`data.powers.${consumePowerSlot}.fvalue`] = Math.max(powers - 1, 0);
|
||||
if (fp.temp >= powerCost) {
|
||||
actorUpdates["data.attributes.force.points.temp"] = fp.temp - powerCost;
|
||||
}else{
|
||||
actorUpdates["data.attributes.force.points.value"] = fp.value + fp.temp - powerCost;
|
||||
actorUpdates["data.attributes.force.points.temp"] = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "tec": {
|
||||
const powers = Number(level?.tvalue ?? 0);
|
||||
if ( powers === 0 ) {
|
||||
const label = game.i18n.localize(`SW5E.PowerLevel${id.level}`);
|
||||
ui.notifications.warn(game.i18n.format("SW5E.PowerCastNoSlots", {name: this.name, level: label}));
|
||||
return false;
|
||||
}
|
||||
actorUpdates[`data.powers.${consumePowerSlot}.tvalue`] = Math.max(powers - 1, 0);
|
||||
if (tp.temp >= powerCost) {
|
||||
actorUpdates["data.attributes.tech.points.temp"] = tp.temp - powerCost;
|
||||
}else{
|
||||
actorUpdates["data.attributes.tech.points.value"] = tp.value + tp.temp - powerCost;
|
||||
actorUpdates["data.attributes.tech.points.temp"] = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
actorUpdates[`data.powers.${consumePowerSlot}.value`] = Math.max(powers - 1, 0);
|
||||
}
|
||||
|
||||
|
||||
// Consume Limited Usage
|
||||
if ( consumeUsage ) {
|
||||
|
@ -772,7 +838,7 @@ export default class Item5e extends Item {
|
|||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Prepare chat card data for tool type items
|
||||
* Prepare chat card data for loot type items
|
||||
* @private
|
||||
*/
|
||||
_lootChatData(data, labels, props) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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
|
||||
|
@ -10,8 +10,8 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
super(...args);
|
||||
|
||||
// Expand the default size of the class sheet
|
||||
if ( this.object.data.type === "class" ) {
|
||||
this.options.width = this.position.width = 600;
|
||||
if (this.object.data.type === "class") {
|
||||
this.options.width = this.position.width = 600;
|
||||
this.options.height = this.position.height = 680;
|
||||
}
|
||||
}
|
||||
|
@ -19,14 +19,14 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
static get defaultOptions() {
|
||||
return mergeObject(super.defaultOptions, {
|
||||
static get defaultOptions() {
|
||||
return mergeObject(super.defaultOptions, {
|
||||
width: 560,
|
||||
height: 400,
|
||||
classes: ["sw5e", "sheet", "item"],
|
||||
resizable: true,
|
||||
scrollY: [".tab.details"],
|
||||
tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}]
|
||||
tabs: [{ navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description" }]
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -55,17 +55,17 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
// Potential consumption targets
|
||||
data.abilityConsumptionTargets = this._getItemConsumptionTargets(data.item);
|
||||
|
||||
// Action Details
|
||||
// Action Detail
|
||||
data.hasAttackRoll = this.item.hasAttack;
|
||||
data.isHealing = data.item.data.actionType === "heal";
|
||||
data.isFlatDC = getProperty(data.item.data, "save.scaling") === "flat";
|
||||
data.isLine = ["line", "wall"].includes(data.item.data.target?.type);
|
||||
|
||||
// Original maximum uses formula
|
||||
if ( this.item._data.data?.uses?.max ) data.data.uses.max = this.item._data.data.uses.max;
|
||||
if (this.item._data.data?.uses?.max) data.data.uses.max = this.item._data.data.uses.max;
|
||||
|
||||
// Vehicles
|
||||
data.isCrewed = data.item.data.activation?.type === 'crew';
|
||||
data.isCrewed = data.item.data.activation?.type === "crew";
|
||||
data.isMountable = this._isItemMountable(data.item);
|
||||
|
||||
// Prepare Active Effects
|
||||
|
@ -83,22 +83,25 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
*/
|
||||
_getItemConsumptionTargets(item) {
|
||||
const consume = item.data.consume || {};
|
||||
if ( !consume.type ) return [];
|
||||
if (!consume.type) return [];
|
||||
const actor = this.item.actor;
|
||||
if ( !actor ) return {};
|
||||
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})`});
|
||||
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" ) {
|
||||
else if (consume.type === "attribute") {
|
||||
const attributes = Object.values(CombatTrackerConfig.prototype.getAttributeChoices())[0]; // Bit of a hack
|
||||
return attributes.reduce((obj, a) => {
|
||||
obj[a] = a;
|
||||
|
@ -107,9 +110,9 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
}
|
||||
|
||||
// Materials
|
||||
else if ( consume.type === "material" ) {
|
||||
else if (consume.type === "material") {
|
||||
return actor.items.reduce((obj, i) => {
|
||||
if ( ["consumable", "loot"].includes(i.data.type) && !i.data.data.activation ) {
|
||||
if (["consumable", "loot"].includes(i.data.type) && !i.data.data.activation) {
|
||||
obj[i.id] = `${i.name} (${i.data.data.quantity})`;
|
||||
}
|
||||
return obj;
|
||||
|
@ -117,25 +120,24 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
}
|
||||
|
||||
// Charges
|
||||
else if ( consume.type === "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})})`;
|
||||
if (uses.per && uses.max) {
|
||||
const label =
|
||||
uses.per === "charges"
|
||||
? ` (${game.i18n.format("SW5E.AbilityUseChargesLabel", { value: uses.value })})`
|
||||
: ` (${game.i18n.format("SW5E.AbilityUseConsumableLabel", { max: uses.max, per: uses.per })})`;
|
||||
obj[i.id] = i.name + label;
|
||||
}
|
||||
|
||||
// Recharging items
|
||||
const recharge = i.data.data.recharge || {};
|
||||
if ( recharge.value ) obj[i.id] = `${i.name} (${game.i18n.format("SW5E.Recharge")})`;
|
||||
if (recharge.value) obj[i.id] = `${i.name} (${game.i18n.format("SW5E.Recharge")})`;
|
||||
return obj;
|
||||
}, {})
|
||||
}
|
||||
else return {};
|
||||
}, {});
|
||||
} else return {};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -146,13 +148,11 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
* @private
|
||||
*/
|
||||
_getItemStatus(item) {
|
||||
if ( item.type === "power" ) {
|
||||
if (item.type === "power") {
|
||||
return CONFIG.SW5E.powerPreparationModes[item.data.preparation];
|
||||
}
|
||||
else if ( ["weapon", "equipment"].includes(item.type) ) {
|
||||
} else if (["weapon", "equipment"].includes(item.type)) {
|
||||
return game.i18n.localize(item.data.equipped ? "SW5E.Equipped" : "SW5E.Unequipped");
|
||||
}
|
||||
else if ( item.type === "tool" ) {
|
||||
} else if (item.type === "tool") {
|
||||
return game.i18n.localize(item.data.proficient ? "SW5E.Proficient" : "SW5E.NotProficient");
|
||||
}
|
||||
}
|
||||
|
@ -168,67 +168,50 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
const props = [];
|
||||
const labels = this.item.labels;
|
||||
|
||||
if ( item.type === "weapon" ) {
|
||||
props.push(...Object.entries(item.data.properties)
|
||||
.filter(e => e[1] === true)
|
||||
.map(e => CONFIG.SW5E.weaponProperties[e[0]]));
|
||||
}
|
||||
|
||||
else if ( item.type === "power" ) {
|
||||
if (item.type === "weapon") {
|
||||
props.push(
|
||||
...Object.entries(item.data.properties)
|
||||
.filter((e) => e[1] === true)
|
||||
.map((e) => CONFIG.SW5E.weaponProperties[e[0]])
|
||||
);
|
||||
} else if (item.type === "power") {
|
||||
props.push(
|
||||
labels.components,
|
||||
labels.materials,
|
||||
item.data.components.concentration ? game.i18n.localize("SW5E.Concentration") : null,
|
||||
item.data.components.ritual ? game.i18n.localize("SW5E.Ritual") : null
|
||||
)
|
||||
}
|
||||
|
||||
else if ( item.type === "equipment" ) {
|
||||
);
|
||||
} 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);
|
||||
} 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 === "fightingmastery") {
|
||||
//props.push(labels.fightingmastery);
|
||||
} else if (item.type === "fightingstyle") {
|
||||
//props.push(labels.fightingstyle);
|
||||
} else if (item.type === "lightsaberform") {
|
||||
//props.push(labels.lightsaberform);
|
||||
}
|
||||
|
||||
else if ( item.type === "feat" ) {
|
||||
props.push(labels.featType);
|
||||
}
|
||||
|
||||
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 === "fightingmastery" ) {
|
||||
//props.push(labels.fightingmastery);
|
||||
}
|
||||
else if ( item.type === "fightingstyle" ) {
|
||||
//props.push(labels.fightingstyle);
|
||||
}
|
||||
else if ( item.type === "lightsaberform" ) {
|
||||
//props.push(labels.lightsaberform);
|
||||
}
|
||||
|
||||
// Action type
|
||||
if ( item.data.actionType ) {
|
||||
if (item.data.actionType) {
|
||||
props.push(CONFIG.SW5E.itemActionTypes[item.data.actionType]);
|
||||
}
|
||||
|
||||
// Action usage
|
||||
if ( (item.type !== "weapon") && item.data.activation && !isObjectEmpty(item.data.activation) ) {
|
||||
props.push(
|
||||
labels.activation,
|
||||
labels.range,
|
||||
labels.target,
|
||||
labels.duration
|
||||
)
|
||||
if (item.type !== "weapon" && item.data.activation && !isObjectEmpty(item.data.activation)) {
|
||||
props.push(labels.activation, labels.range, labels.target, labels.duration);
|
||||
}
|
||||
return props.filter(p => !!p);
|
||||
return props.filter((p) => !!p);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -243,36 +226,37 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
*/
|
||||
_isItemMountable(item) {
|
||||
const data = item.data;
|
||||
return (item.type === 'weapon' && data.weaponType === 'siege')
|
||||
|| (item.type === 'equipment' && data.armor.type === 'vehicle');
|
||||
return (
|
||||
(item.type === "weapon" && data.weaponType === "siege") ||
|
||||
(item.type === "equipment" && data.armor.type === "vehicle")
|
||||
);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
setPosition(position={}) {
|
||||
if ( !(this._minimized || position.height) ) {
|
||||
position.height = (this._tabs[0].active === "details") ? "auto" : this.options.height;
|
||||
setPosition(position = {}) {
|
||||
if (!(this._minimized || position.height)) {
|
||||
position.height = this._tabs[0].active === "details" ? "auto" : this.options.height;
|
||||
}
|
||||
return super.setPosition(position);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Form Submission */
|
||||
/* -------------------------------------------- */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_getSubmitData(updateData={}) {
|
||||
|
||||
_getSubmitData(updateData = {}) {
|
||||
// Create the expanded update data object
|
||||
const fd = new FormDataExtended(this.form, {editors: this.editors});
|
||||
const fd = new FormDataExtended(this.form, { editors: this.editors });
|
||||
let data = fd.toObject();
|
||||
if ( updateData ) data = mergeObject(data, updateData);
|
||||
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] || ""]);
|
||||
if (damage) damage.parts = Object.values(damage?.parts || {}).map((d) => [d[0] || "", d[1] || ""]);
|
||||
|
||||
// Return the flattened submission data
|
||||
return flattenObject(data);
|
||||
|
@ -283,12 +267,15 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
/** @override */
|
||||
activateListeners(html) {
|
||||
super.activateListeners(html);
|
||||
if ( this.isEditable ) {
|
||||
if (this.isEditable) {
|
||||
html.find(".damage-control").click(this._onDamageControl.bind(this));
|
||||
html.find('.trait-selector.class-skills').click(this._onConfigureClassSkills.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)
|
||||
html.find(".trait-selector.class-skills").click(this._onConfigureClassSkills.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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -306,19 +293,19 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
const a = event.currentTarget;
|
||||
|
||||
// Add new damage component
|
||||
if ( a.classList.contains("add-damage") ) {
|
||||
await this._onSubmit(event); // Submit any unsaved changes
|
||||
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([["", ""]])});
|
||||
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
|
||||
if (a.classList.contains("delete-damage")) {
|
||||
await this._onSubmit(event); // Submit any unsaved changes
|
||||
const li = a.closest(".damage-part");
|
||||
const damage = duplicate(this.item.data.data.damage);
|
||||
damage.parts.splice(Number(li.dataset.damagePart), 1);
|
||||
return this.item.update({"data.damage.parts": damage.parts});
|
||||
return this.item.update({ "data.damage.parts": damage.parts });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -341,19 +328,19 @@ export default class ItemSheet5e extends ItemSheet {
|
|||
name: a.dataset.target,
|
||||
title: label.innerText,
|
||||
choices: Object.entries(CONFIG.SW5E.skills).reduce((obj, e) => {
|
||||
if ( choices.includes(e[0] ) ) obj[e[0]] = e[1];
|
||||
if (choices.includes(e[0])) obj[e[0]] = e[1];
|
||||
return obj;
|
||||
}, {}),
|
||||
minimum: skills.number,
|
||||
maximum: skills.number
|
||||
}).render(true)
|
||||
}).render(true);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
async _onSubmit(...args) {
|
||||
if ( this._tabs[0].active === "details" ) this.position.height = "auto";
|
||||
if (this._tabs[0].active === "details") this.position.height = "auto";
|
||||
await super._onSubmit(...args);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -131,27 +131,37 @@ export const migrateActorData = function(actor) {
|
|||
_migrateActorSenses(actor, updateData);
|
||||
|
||||
// Migrate Owned Items
|
||||
if ( !actor.items ) return updateData;
|
||||
let hasItemUpdates = false;
|
||||
const items = actor.items.map(i => {
|
||||
if ( !!actor.items ) {
|
||||
let hasItemUpdates = false;
|
||||
const items = actor.items.map(i => {
|
||||
|
||||
// Migrate the Owned Item
|
||||
let itemUpdate = migrateItemData(i);
|
||||
// Migrate the Owned Item
|
||||
let itemUpdate = migrateItemData(i);
|
||||
|
||||
// Prepared, Equipped, and Proficient for NPC actors
|
||||
if ( actor.type === "npc" ) {
|
||||
if (getProperty(i.data, "preparation.prepared") === false) itemUpdate["data.preparation.prepared"] = true;
|
||||
if (getProperty(i.data, "equipped") === false) itemUpdate["data.equipped"] = true;
|
||||
if (getProperty(i.data, "proficient") === false) itemUpdate["data.proficient"] = true;
|
||||
}
|
||||
// Prepared, Equipped, and Proficient for NPC actors
|
||||
if ( actor.type === "npc" ) {
|
||||
if (getProperty(i.data, "preparation.prepared") === false) itemUpdate["data.preparation.prepared"] = true;
|
||||
if (getProperty(i.data, "equipped") === false) itemUpdate["data.equipped"] = true;
|
||||
if (getProperty(i.data, "proficient") === false) itemUpdate["data.proficient"] = true;
|
||||
}
|
||||
|
||||
// Update the Owned Item
|
||||
if ( !isObjectEmpty(itemUpdate) ) {
|
||||
hasItemUpdates = true;
|
||||
return mergeObject(i, itemUpdate, {enforceTypes: false, inplace: false});
|
||||
} else return i;
|
||||
});
|
||||
if ( hasItemUpdates ) updateData.items = items;
|
||||
// Update the Owned Item
|
||||
if ( !isObjectEmpty(itemUpdate) ) {
|
||||
hasItemUpdates = true;
|
||||
return mergeObject(i, itemUpdate, {enforceTypes: false, inplace: false});
|
||||
} else return i;
|
||||
});
|
||||
if ( hasItemUpdates ) updateData.items = items;
|
||||
}
|
||||
|
||||
// Update NPC data with new datamodel information
|
||||
if (actor.type === "npc") {
|
||||
_updateNPCData(actor);
|
||||
}
|
||||
|
||||
// migrate powers last since it relies on item classes being migrated first.
|
||||
_migrateActorPowers(actor, updateData);
|
||||
|
||||
return updateData;
|
||||
};
|
||||
|
||||
|
@ -191,6 +201,7 @@ function cleanActorData(actorData) {
|
|||
*/
|
||||
export const migrateItemData = function(item) {
|
||||
const updateData = {};
|
||||
_migrateItemClassPowerCasting(item, updateData)
|
||||
_migrateItemAttunement(item, updateData);
|
||||
return updateData;
|
||||
};
|
||||
|
@ -228,6 +239,69 @@ export const migrateSceneData = function(scene) {
|
|||
/* Low level migration utilities
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Update an NPC Actor's data based on compendium
|
||||
* @param {Object} actor The data object for an Actor
|
||||
* @return {Object} The updated Actor
|
||||
*/
|
||||
function _updateNPCData(actor) {
|
||||
|
||||
let actorData = actor.data;
|
||||
const updateData = {};
|
||||
// check for flag.core
|
||||
const hasSource = actor?.flags?.core?.sourceId !== undefined;
|
||||
if (!hasSource) return actor;
|
||||
// shortcut out if dataVersion flag is set to 1.2.4
|
||||
const sourceId = actor.flags.core.sourceId;
|
||||
const coreSource = sourceId.substr(0,sourceId.length-17);
|
||||
const core_id = sourceId.substr(sourceId.length-16,16);
|
||||
if (coreSource === "Compendium.sw5e.monsters"){
|
||||
game.packs.get("sw5e.monsters").getEntity(core_id).then(monster => {
|
||||
const monsterData = monster.data.data;
|
||||
// copy movement[], senses[], powercasting, force[], tech[], powerForceLevel, powerTechLevel
|
||||
updateData["data.attributes.movement"] = monsterData.attributes.movement;
|
||||
updateData["data.attributes.senses"] = monsterData.attributes.senses;
|
||||
updateData["data.attributes.powercasting"] = monsterData.attributes.powercasting;
|
||||
updateData["data.attributes.force"] = monsterData.attributes.force;
|
||||
updateData["data.attributes.tech"] = monsterData.attributes.tech;
|
||||
updateData["data.details.powerForceLevel"] = monsterData.details.powerForceLevel;
|
||||
updateData["data.details.powerTechLevel"] = monsterData.details.powerTechLevel;
|
||||
// push missing powers onto actor
|
||||
let newPowers = [];
|
||||
for ( let i of monster.items ) {
|
||||
const itemData = i.data;
|
||||
if ( itemData.type === "power" ) {
|
||||
const itemCompendium_id = itemData.flags?.core?.sourceId.split(".").slice(-1)[0];
|
||||
let hasPower = !!actor.items.find(item => item.flags?.core?.sourceId.split(".").slice(-1)[0] === itemCompendium_id);
|
||||
if (!hasPower) {
|
||||
// Clone power to new object. Don't know if it is technically needed, but seems to prevent some weirdness.
|
||||
const newPower = JSON.parse(JSON.stringify(itemData));
|
||||
|
||||
newPowers.push(newPower);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const liveActor = game.actors.get(actor._id);
|
||||
|
||||
liveActor.createEmbeddedEntity("OwnedItem", newPowers);
|
||||
|
||||
// let updateActor = await actor.createOwnedItem(newPowers);
|
||||
// set flag to check to see if migration has been done so we don't do it again.
|
||||
liveActor.setFlag("sw5e", "dataVersion", "1.2.4");
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
//merge object
|
||||
actorData = mergeObject(actorData, updateData);
|
||||
// Return the scrubbed data
|
||||
return actor;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Migrate the actor speed string to movement object
|
||||
* @private
|
||||
|
@ -255,6 +329,67 @@ function _migrateActorMovement(actorData, updateData) {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Migrate the actor speed string to movement object
|
||||
* @private
|
||||
*/
|
||||
function _migrateActorPowers(actorData, updateData) {
|
||||
const ad = actorData.data;
|
||||
|
||||
// If new Force & Tech data is not present, create it
|
||||
let hasNewAttrib = ad?.attributes?.force?.level !== undefined;
|
||||
if ( !hasNewAttrib ) {
|
||||
updateData["data.attributes.force.known.value"] = 0;
|
||||
updateData["data.attributes.force.known.max"] = 0;
|
||||
updateData["data.attributes.force.points.value"] = 0;
|
||||
updateData["data.attributes.force.points.min"] = 0;
|
||||
updateData["data.attributes.force.points.max"] = 0;
|
||||
updateData["data.attributes.force.points.temp"] = 0;
|
||||
updateData["data.attributes.force.points.tempmax"] = 0;
|
||||
updateData["data.attributes.force.level"] = 0;
|
||||
updateData["data.attributes.tech.known.value"] = 0;
|
||||
updateData["data.attributes.tech.known.max"] = 0;
|
||||
updateData["data.attributes.tech.points.value"] = 0;
|
||||
updateData["data.attributes.tech.points.min"] = 0;
|
||||
updateData["data.attributes.tech.points.max"] = 0;
|
||||
updateData["data.attributes.tech.points.temp"] = 0;
|
||||
updateData["data.attributes.tech.points.tempmax"] = 0;
|
||||
updateData["data.attributes.tech.level"] = 0;
|
||||
}
|
||||
|
||||
// If new Power F/T split data is not present, create it
|
||||
const hasNewLimit = ad?.powers?.power1?.foverride !== undefined;
|
||||
if ( !hasNewLimit ) {
|
||||
for (let i = 1; i <= 9; i++) {
|
||||
// add new
|
||||
updateData["data.powers.power" + i + ".fvalue"] = getProperty(ad.powers,"power" + i + ".value");
|
||||
updateData["data.powers.power" + i + ".fmax"] = getProperty(ad.powers,"power" + i + ".max");
|
||||
updateData["data.powers.power" + i + ".foverride"] = null;
|
||||
updateData["data.powers.power" + i + ".tvalue"] = getProperty(ad.powers,"power" + i + ".value");
|
||||
updateData["data.powers.power" + i + ".tmax"] = getProperty(ad.powers,"power" + i + ".max");
|
||||
updateData["data.powers.power" + i + ".toverride"] = null;
|
||||
//remove old
|
||||
updateData["data.powers.power" + i + ".-=value"] = null;
|
||||
updateData["data.powers.power" + i + ".-=override"] = null;
|
||||
}
|
||||
}
|
||||
// If new Bonus Power DC data is not present, create it
|
||||
const hasNewBonus = ad?.bonuses?.power?.forceLightDC !== undefined;
|
||||
if ( !hasNewBonus ) {
|
||||
updateData["data.bonuses.power.forceLightDC"] = "";
|
||||
updateData["data.bonuses.power.forceDarkDC"] = "";
|
||||
updateData["data.bonuses.power.forceUnivDC"] = "";
|
||||
updateData["data.bonuses.power.techDC"] = "";
|
||||
}
|
||||
|
||||
// Remove the Power DC Bonus
|
||||
updateData["data.bonuses.power.-=dc"] = null;
|
||||
|
||||
return updateData
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Migrate the actor traits.senses string to attributes.senses object
|
||||
* @private
|
||||
|
@ -290,6 +425,35 @@ function _migrateActorSenses(actor, updateData) {
|
|||
return updateData;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
function _migrateItemClassPowerCasting(item, updateData) {
|
||||
if (item.type === "class"){
|
||||
switch (item.name){
|
||||
case "Consular":
|
||||
updateData["data.powercasting"] = "consular";
|
||||
break;
|
||||
case "Engineer":
|
||||
updateData["data.powercasting"] = "engineer";
|
||||
break;
|
||||
case "Guardian":
|
||||
updateData["data.powercasting"] = "guardian";
|
||||
break;
|
||||
case "Scout":
|
||||
updateData["data.powercasting"] = "scout";
|
||||
break;
|
||||
case "Sentinel":
|
||||
updateData["data.powercasting"] = "sentinel";
|
||||
break;
|
||||
}
|
||||
}
|
||||
return updateData;
|
||||
}
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
|
|
|
@ -21,7 +21,8 @@ export const preloadHandlebarsTemplates = async function() {
|
|||
"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-powerbook.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",
|
||||
|
||||
|
|
BIN
packs/Icons/Backgrounds/(Un)Retired Adventurer.webp
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
packs/Icons/Backgrounds/Addict.webp
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
packs/Icons/Backgrounds/Agent.webp
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
packs/Icons/Backgrounds/Barbarian.webp
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
packs/Icons/Backgrounds/Bounty Hunter.webp
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
packs/Icons/Backgrounds/City Watch.webp
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
packs/Icons/Backgrounds/Clone Trooper.webp
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
packs/Icons/Backgrounds/Companion.webp
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
packs/Icons/Backgrounds/Courtier.webp
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
packs/Icons/Backgrounds/Crime Lord.webp
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
packs/Icons/Backgrounds/Criminal.webp
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
packs/Icons/Backgrounds/Dathomir Witch.webp
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
packs/Icons/Backgrounds/Entertainer.webp
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
packs/Icons/Backgrounds/Faction Adventurer.webp
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
packs/Icons/Backgrounds/Faction Artisan.webp
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
packs/Icons/Backgrounds/Faction Merchant.webp
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
packs/Icons/Backgrounds/Far Traveler.webp
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
packs/Icons/Backgrounds/Farmer.webp
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
packs/Icons/Backgrounds/Folk Hero.webp
Normal file
After Width: | Height: | Size: 9 KiB |
BIN
packs/Icons/Backgrounds/Force Adept.webp
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
packs/Icons/Backgrounds/Gambler.webp
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
packs/Icons/Backgrounds/Gladiator.webp
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
packs/Icons/Backgrounds/Hermit.webp
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
packs/Icons/Backgrounds/Holonet Technician.webp
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
packs/Icons/Backgrounds/Investigator.webp
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
packs/Icons/Backgrounds/Jedi.webp
Normal file
After Width: | Height: | Size: 9.6 KiB |
BIN
packs/Icons/Backgrounds/Jensaarai.webp
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
packs/Icons/Backgrounds/Laborer.webp
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
packs/Icons/Backgrounds/Lawyer.webp
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
packs/Icons/Backgrounds/Mandalorian.webp
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
packs/Icons/Backgrounds/Mercenary.webp
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
packs/Icons/Backgrounds/Noble.webp
Normal file
After Width: | Height: | Size: 9.4 KiB |
BIN
packs/Icons/Backgrounds/Nomad.webp
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
packs/Icons/Backgrounds/Office Worker.webp
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
packs/Icons/Backgrounds/Outlaw.webp
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
packs/Icons/Backgrounds/Pirate.webp
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
packs/Icons/Backgrounds/Racer.webp
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
packs/Icons/Backgrounds/Scientist.webp
Normal file
After Width: | Height: | Size: 8.6 KiB |
BIN
packs/Icons/Backgrounds/Scoundrel.webp
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
packs/Icons/Backgrounds/Servant.webp
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
packs/Icons/Backgrounds/Sith.webp
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
packs/Icons/Backgrounds/Smuggler.webp
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
packs/Icons/Backgrounds/Soldier.webp
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
packs/Icons/Backgrounds/Spacer.webp
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
packs/Icons/Backgrounds/Student.webp
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
packs/Icons/Backgrounds/Teacher.webp
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
packs/Icons/Backgrounds/Urchin.webp
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
packs/Icons/Class Features/BSKR-ARCH-Action.webp
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
packs/Icons/Class Features/BSKR-ARCH-Bonus.webp
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
packs/Icons/Class Features/BSKR-ARCH-Passive.webp
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
packs/Icons/Class Features/BSKR-ARCH-Reaction.webp
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
packs/Icons/Class Features/BSKR-Action.webp
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
packs/Icons/Class Features/BSKR-Bonus.webp
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
packs/Icons/Class Features/BSKR-Passive.webp
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
packs/Icons/Class Features/BSKR-Reaction.webp
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
packs/Icons/Class Features/CSLR-ARCH-Action.webp
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
packs/Icons/Class Features/CSLR-ARCH-Bonus.webp
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
packs/Icons/Class Features/CSLR-ARCH-Passive.webp
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
packs/Icons/Class Features/CSLR-ARCH-Reaction.webp
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
packs/Icons/Class Features/CSLR-Action.webp
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
packs/Icons/Class Features/CSLR-Bonus.webp
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
packs/Icons/Class Features/CSLR-Passive.webp
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
packs/Icons/Class Features/CSLR-Reaction.webp
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
packs/Icons/Class Features/ENGR-ARCH-Action.webp
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
packs/Icons/Class Features/ENGR-ARCH-Bonus.webp
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
packs/Icons/Class Features/ENGR-ARCH-Passive.webp
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
packs/Icons/Class Features/ENGR-ARCH-Reaction.webp
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
packs/Icons/Class Features/ENGR-Action.webp
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
packs/Icons/Class Features/ENGR-Bonus.webp
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
packs/Icons/Class Features/ENGR-Passive.webp
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
packs/Icons/Class Features/ENGR-Reaction.webp
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
packs/Icons/Class Features/FGTR-ARCH-Action.webp
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
packs/Icons/Class Features/FGTR-ARCH-Bonus.webp
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
packs/Icons/Class Features/FGTR-ARCH-Passive.webp
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
packs/Icons/Class Features/FGTR-ARCH-Reaction.webp
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
packs/Icons/Class Features/FGTR-Action.webp
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
packs/Icons/Class Features/FGTR-Bonus.webp
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
packs/Icons/Class Features/FGTR-Passive.webp
Normal file
After Width: | Height: | Size: 1.7 KiB |