2
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
{
|
||||
}
|
1980
lang/en.json
1027
lang/fr.json
Normal file
|
@ -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 {
|
||||
|
|
|
@ -382,7 +382,8 @@
|
|||
}
|
||||
|
||||
|
||||
.tab.powerbook {
|
||||
.tab.force-powerbook,
|
||||
.tab.tech-powerbook {
|
||||
.powercasting-ability {
|
||||
label,
|
||||
h3 {
|
||||
|
|
|
@ -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.attributes.powercasting !== "none" ? 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) || "";
|
||||
|
|
433
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',
|
||||
|
@ -858,353 +893,353 @@ SW5E.classFeatures = ClassFeatures;
|
|||
// Configure Optional Character Flags
|
||||
SW5E.characterFlags = {
|
||||
"adaptiveResilience": {
|
||||
name: "SW5E.FlagsAdaptiveResilience",
|
||||
hint: "Prolongued use of technology allows members of your species to readily adapt to its effects. You have advantage on Strength and Constitution saving throws against tech powers.",
|
||||
section: "Species Traits",
|
||||
type: Boolean
|
||||
name: "SW5E.FlagsAdaptiveResilience",
|
||||
hint: "SW5E.FlagsAdaptiveResilienceHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"aggressive": {
|
||||
name: "SW5E.FlagsAggressive",
|
||||
hint: "As a bonus action, you can move up to your speed toward an enemy of your choice that you can see or hear. You must end this move closer to the enemy than you started.",
|
||||
section: "Species Traits",
|
||||
type: Boolean
|
||||
name: "SW5E.FlagsAggressive",
|
||||
hint: "SW5E.FlagsAggressiveHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"amphibious": {
|
||||
name: "SW5E.FlagsAmphibious",
|
||||
hint: "You can breathe air and water.",
|
||||
section: "Species Traits",
|
||||
name: "SW5E.FlagsAmphibious",
|
||||
hint: "SW5E.FlagsAmphibiousHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"armorIntegration": {
|
||||
name: "SW5E.FlagsArmorIntegration",
|
||||
hint: "You cannot wear armor, but you can have the armor professionally integrated into your chassis over the course of a long rest. This work must be done by someone proficient with astrotech’s implements. You must be proficient in armor in order to have it integrated.",
|
||||
section: "Species Traits",
|
||||
type: Boolean
|
||||
name: "SW5E.FlagsArmorIntegration",
|
||||
hint: "SW5E.FlagsArmorIntegrationHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"businessSavvy": {
|
||||
name: "SW5E.FlagsBusinessSavvy",
|
||||
hint: "Whenever you make a Charisma (Persuasion) check involving haggling you are considered to have expertise in the Persuasion skill.",
|
||||
section: "Species Traits",
|
||||
type: Boolean
|
||||
name: "SW5E.FlagsBusinessSavvy",
|
||||
hint: "SW5E.FlagsBusinessSavvyHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"cannibalize": {
|
||||
name: "SW5E.FlagsCannibalize",
|
||||
hint: "If you spend at least 1 minute devouring the corpse of a beast or humanoid, you gain temporary hit points equal to your Constitution modifier. Once you've used this feature, you must complete a short or long rest before you can use it again.",
|
||||
section: "Species Traits",
|
||||
name: "SW5E.FlagsCannibalize",
|
||||
hint: "SW5E.FlagsCannibalizeHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"closedMind": {
|
||||
name: "SW5E.FlagsClosedMind",
|
||||
hint: "Members of your species have a natural attunement to the Force, which makes them resistant to its powers. You have advantage on Wisdom and Charisma saving throws against force powers.",
|
||||
section: "Species Traits",
|
||||
type: Boolean
|
||||
name: "SW5E.FlagsClosedMind",
|
||||
hint: "SW5E.FlagsClosedMindHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"crudeWeaponSpecialists": {
|
||||
name: "SW5E.FlagsCrudeWeaponSpecialists",
|
||||
hint: "Members of your species are used to making do with less. You can spend 1 hour, which you can do over the course of a short rest, crafting a weapon out of loose materials. You can craft any simple kinetic weapon, but the weapon’s damage suffers a -1 penalty.",
|
||||
section: "Species Traits",
|
||||
type: Boolean
|
||||
name: "SW5E.FlagsCrudeWeaponSpecialists",
|
||||
hint: "SW5E.FlagsCrudeWeaponSpecialistsHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"defiant": {
|
||||
name: "SW5E.FlagsDefiant",
|
||||
hint: "Members of your species are known to be stubborn and often refuse to give up, even against the worst odds. When you or a creature you can see that can see and understand you makes an ability check, attack roll, or saving throw, you can roll a d4 and add it to their roll (no action required). You can use this before or after the roll, but before the GM determines the roll’s outcome. Once you’ve used this feature, you must complete a short or long rest before you can use it again.",
|
||||
section: "Species Traits",
|
||||
type: Boolean
|
||||
name: "SW5E.FlagsDefiant",
|
||||
hint: "SW5E.FlagsDefiantHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"detailOriented": {
|
||||
name: "SW5E.FlagsDetailOriented",
|
||||
hint: "You have advantage on Intelligence (Investigation) checks within 5 feet.",
|
||||
section: "Species Traits",
|
||||
hint: "SW5E.FlagsDetailOrientedHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"enthrallingPheromones": {
|
||||
name: "SW5E.FlagsEnthrallingPheromones",
|
||||
hint: "You can use your pheromones to influence individuals of both sexes. Whenever you roll a 1 on a Charisma (Persuasion) check, you can reroll the die and must use the new roll. Additionally, once per short or long rest, you can treat a d20 roll of 9 or lower on a Charisma check as a 10. This feature has no effect on droids or constructs.",
|
||||
section: "Species Traits",
|
||||
type: Boolean
|
||||
name: "SW5E.FlagsEnthrallingPheromones",
|
||||
hint: "SW5E.FlagsEnthrallingPheromonesHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"extraArms": {
|
||||
name: "SW5E.FlagsExtraArms",
|
||||
hint: "You possess more than two arms, which you can use independently of one another. You can only gain the benefit of items held by two of your arms at any given time, and once per round you can switch which arms you are benefiting from (no action required).",
|
||||
section: "Species Traits",
|
||||
type: Boolean
|
||||
name: "SW5E.FlagsExtraArms",
|
||||
hint: "SW5E.FlagsExtraArmsHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"forceContention": {
|
||||
name: "SW5E.FlagsForceContention",
|
||||
hint: "Due to their unique physiology, members of your species exhibit a hardiness that allows them to overcome use of the Force. You have advantage on Strength and Constitution saving throws against force powers.",
|
||||
section: "Species Traits",
|
||||
type: Boolean
|
||||
name: "SW5E.FlagsForceContention",
|
||||
hint: "SW5E.FlagsForceContentionHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"forceInsensitive": {
|
||||
name: "SW5E.FlagsForceInsensitive",
|
||||
hint: "While droids can be manipulated by many force powers, they cannot sense the Force. You can not use force powers or take levels in forcecasting classes.",
|
||||
section: "Species Traits",
|
||||
type: Boolean
|
||||
hint: "SW5E.FlagsForceInsensitiveHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"foreignBiology": {
|
||||
name: "SW5E.FlagsForeignBiology",
|
||||
hint: "You wear a breathing apparatus because many atmospheres in the galaxy differ from that of your species' homeworld. If your apparatus is removed while you are in such an environment, you lose consciousness.",
|
||||
section: "Species Traits",
|
||||
type: Boolean
|
||||
name: "SW5E.FlagsForeignBiology",
|
||||
hint: "SW5E.FlagsForeignBiologyHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"furyOfTheSmall": {
|
||||
name: "SW5E.FlagsFuryOfTheSmall",
|
||||
hint: "When you damage a creature with an attack or a power and the creature's size is larger than yours, you can cause the attack or power to deal extra damage to the creature. The extra damage equals your level. Once you use this trait, you can't use it again until you finish a short or long rest.",
|
||||
section: "Species Traits",
|
||||
name: "SW5E.FlagsFuryOfTheSmall",
|
||||
hint: "SW5E.FlagsFuryOfTheSmallHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"grovelCowerAndBeg": {
|
||||
name: "SW5E.FlagsGrovelCowerAndBeg",
|
||||
hint: "As an action on your turn, you can cower pathetically to distract nearby foes. Until the end of your next turn, your allies gain advantage on attack rolls against enemies within 10 feet of you that can see you. Once you use this trait, you can’t use it again until you finish a short or long rest.",
|
||||
section: "Species Traits",
|
||||
type: Boolean
|
||||
name: "SW5E.FlagsGrovelCowerAndBeg",
|
||||
hint: "SW5E.FlagsGrovelCowerAndBegHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"inscrutable": {
|
||||
name: "SW5E.FlagsInscrutable",
|
||||
hint: "Your calm demeaner and control make you hard to read. Wisdom (Insight) checks made against you have disadvantage, and you have advantage on any saving throw against an effect that would read your thoughts.",
|
||||
section: "Species Traits",
|
||||
name: "SW5E.FlagsInscrutable",
|
||||
hint: "SW5E.FlagsInscrutableHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"keenSenses": {
|
||||
name: "SW5E.FlagsKeenSenses",
|
||||
hint: "You have advantage on Wisdom (Perception) checks that involve using particular senses (see your species' traits for details).",
|
||||
section: "Species Traits",
|
||||
hint: "SW5E.FlagsKeenSensesHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"longlimbed": {
|
||||
name: "SW5E.FlagsLongLimbed",
|
||||
hint: "When you make a melee attack on your turn, your reach for it is 5 feet greater than normal.",
|
||||
section: "Species Traits",
|
||||
type: Boolean
|
||||
name: "SW5E.FlagsLongLimbed",
|
||||
hint: "SW5E.FlagsLongLimbedHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"maintenanceMode": {
|
||||
name: "SW5E.FlagsMaintenanceMode",
|
||||
hint: "Rather than sleep, you enter an inactive state to perform routine maintenance for 4 hours each day. You have disadvantage on Wisdom (Perception) checks while performing maintenance.",
|
||||
section: "Species Traits",
|
||||
type: Boolean
|
||||
hint: "SW5E.FlagsMaintenanceModeHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"maskOfTheWild": {
|
||||
name: "SW5E.FlagsMaskOfTheWild",
|
||||
hint: "You can attempt to hide even when you are only lightly obscured by foliage, heavy rain, falling snow, mist, and other natural phenomena.",
|
||||
section: "Species Traits",
|
||||
type: Boolean
|
||||
name: "SW5E.FlagsMaskOfTheWild",
|
||||
hint: "SW5E.FlagsMaskOfTheWildHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"multipleHearts": {
|
||||
name: "SW5E.FlagsMultipleHearts",
|
||||
hint: "When you are reduced to 0 hit points but not killed outright, you can drop to 1 hit point instead. You can't use this feature again until you finish a long rest.",
|
||||
section: "Species Traits",
|
||||
name: "SW5E.FlagsMultipleHearts",
|
||||
hint: "SW5E.FlagsMultipleHeartsHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"naturallyStealthy": {
|
||||
name: "SW5E.FlagsNaturallyStealthy",
|
||||
hint: "You can attempt to hide even when you are obscured only by a creature that is your size or larger than you.",
|
||||
section: "Species Traits",
|
||||
hint: "SW5E.FlagsNaturallyStealthyHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"nimbleAgility": {
|
||||
name: "SW5E.FlagsNimbleAgility",
|
||||
hint: "Your reflexes and agility allow you to move with a burst of speed. When you move on your turn in combat, you can double your speed until the end of the turn. Once you use this trait, you can't use it again until you move 0 feet on one of your turns.",
|
||||
section: "Species Traits",
|
||||
type: Boolean
|
||||
name: "SW5E.FlagsNimbleAgility",
|
||||
hint: "SW5E.FlagsNimbleAgilityHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"nimbleEscape": {
|
||||
name: "SW5E.FlagsNimbleEscape",
|
||||
hint: "You can take the Disengage or Hide action as a bonus action.",
|
||||
section: "Species Traits",
|
||||
hint: "SW5E.FlagsNimbleEscapeHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"nimbleness": {
|
||||
name: "SW5E.FlagsNimbleness",
|
||||
hint: "You can move through the space of any creature that is of a size larger than yours.",
|
||||
section: "Species Traits",
|
||||
type: Boolean
|
||||
name: "SW5E.FlagsNimbleness",
|
||||
hint: "SW5E.FlagsNimblenessHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"pintsized": {
|
||||
name: "SW5E.FlagsPintsized",
|
||||
hint: "Your tiny stature makes it hard for you to wield bigger weapons. You can’t use medium or heavy shields. Additionally, you can’t wield weapons with the two-handed or versatile property, and you can only wield one-handed weapons in two hands unless they have the light property.",
|
||||
section: "Species Traits",
|
||||
type: Boolean
|
||||
hint: "SW5E.FlagsPintsizedHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"powerfulBuild": {
|
||||
name: "SW5E.FlagsPowerfulBuild",
|
||||
hint: "SW5E.FlagsPowerfulBuildHint",
|
||||
section: "Species Traits",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"precognition": {
|
||||
name: "SW5E.FlagsPrecognition",
|
||||
hint: "You can see brief visions of the future that allow you to turn failures into successes. When you roll a 1 on an attack roll, ability check, or saving throw, you can reroll the die and must use the new roll.",
|
||||
section: "Species Traits",
|
||||
name: "SW5E.FlagsPrecognition",
|
||||
hint: "SW5E.FlagsPrecognitionHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"programmer": {
|
||||
name: "SW5E.FlagsProgrammer",
|
||||
hint: "Whenever you make an Intelligence (Technology) check related to computers, you are considered to have expertise in the Technology skill.",
|
||||
section: "Species Traits",
|
||||
hint: "SW5E.FlagsProgrammerHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"puny": {
|
||||
name: "SW5E.FlagsPuny",
|
||||
hint: "Members of your species are too small to pack much of a punch. You have disadvantage on Strength saving throws, and when determining your bonus to attack and damage rolls for weapon attacks using Strength, you can’t add more than +3.",
|
||||
section: "Species Traits",
|
||||
type: Boolean
|
||||
hint: "SW5E.FlagsPunyHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"rapidReconstruction": {
|
||||
name: "SW5E.FlagsRapidReconstruction",
|
||||
hint: "You are built with internal repair mechanisms. As a bonus action, you can choose to spend one of your Hit Dice to recover hit points.",
|
||||
section: "Species Traits",
|
||||
type: Boolean
|
||||
name: "SW5E.FlagsRapidReconstruction",
|
||||
hint: "SW5E.FlagsRapidReconstructionHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"rapidlyRegenerative": {
|
||||
name: "SW5E.FlagsRapidlyRegenerative",
|
||||
hint: "You heal quickly, both at will and in response to danger. As a bonus action, you can choose to spend one of your Hit Dice to recover hit points. Additionally, when you take damage, you can use your reaction and expend a Hit Die to regain hit points as long as the damage would not reduce your hit points to 0.",
|
||||
section: "Species Traits",
|
||||
name: "SW5E.FlagsRapidlyRegenerative",
|
||||
hint: "SW5E.FlagsRapidlyRegenerativeHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"regenerative": {
|
||||
name: "SW5E.FlagsRegenerative",
|
||||
hint: "When you take damage, you can use your reaction and expend a Hit Die to regain hit points as long as the damage would not reduce your hit points to 0.",
|
||||
section: "Species Traits",
|
||||
type: Boolean
|
||||
name: "SW5E.FlagsRegenerative",
|
||||
hint: "SW5E.FlagsRegenerativeHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"savageAttacks": {
|
||||
name: "SW5E.FlagsSavageAttacks",
|
||||
name: "SW5E.FlagsSavageAttacks",
|
||||
hint: "SW5E.FlagsSavageAttacksHint",
|
||||
section: "Species Traits",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"shapechanger": {
|
||||
name: "SW5E.FlagsShapechanger",
|
||||
hint: "As an action, you can change your appearance and your voice. You determine the specifics of the changes, including your coloration, hair length, and sex. You can also adjust your height and weight, but not so much that your size changes. You can make yourself appear as a member of another species, though none of your game statistics change. You can't duplicate the appearance of a creature you've never seen, and you must adopt a form that has the same basic arrangement of limbs that you have. Your clothing and equipment aren't changed by this trait. You stay in the new form until you use an action to revert to your true form or until you die.",
|
||||
section: "Species Traits",
|
||||
type: Boolean
|
||||
name: "SW5E.FlagsShapechanger",
|
||||
hint: "SW5E.FlagsShapechangerHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"strongLegged": {
|
||||
name: "SW5E.FlagsStrongLegged",
|
||||
hint: "When you make a long jump, you can cover a number of feet up to twice your Strength score. When you make a high jump, you can leap a number of feet up into the air equal to 3 + twice your Strength modifier.",
|
||||
section: "Species Traits",
|
||||
name: "SW5E.FlagsStrongLegged",
|
||||
hint: "SW5E.FlagsStrongLeggedHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"sunlightSensitivity": {
|
||||
name: "SW5E.FlagsSunlightSensitivity",
|
||||
hint: "You have disadvantage on attack rolls and on Wisdom (Perception) checks that rely on sight when you, the target of your attack, or whatever you are trying to perceive is in direct sunlight.",
|
||||
section: "Species Traits",
|
||||
name: "SW5E.FlagsSunlightSensitivity",
|
||||
hint: "SW5E.FlagsSunlightSensitivityHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"surpriseAttack": {
|
||||
name: "SW5E.FlagsSurpriseAttack",
|
||||
hint: "If you surprise a creature and hit it with an attack on your first turn in combat, the attack deals an extra 2d6 damage to it. You can use this trait only once per combat.",
|
||||
section: "Species Traits",
|
||||
type: Boolean
|
||||
name: "SW5E.FlagsSurpriseAttack",
|
||||
hint: "SW5E.FlagsSurpriseAttackHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"techImpaired": {
|
||||
name: "SW5E.FlagsTechImpaired",
|
||||
hint: "While members of your species can figure out basic technology, they experience difficulty using more complex equipment like wristpads. You cannot use tech powers or take levels in techcasting classes.",
|
||||
section: "Species Traits",
|
||||
type: Boolean
|
||||
name: "SW5E.FlagsTechImpaired",
|
||||
hint: "SW5E.FlagsTechImpairedHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"techResistance": {
|
||||
name: "SW5E.FlagsTechResistance",
|
||||
hint: "You have advantage on Dexterity and Intelligence saving throws against tech powers.",
|
||||
section: "Species Traits",
|
||||
hint: "SW5E.FlagsTechResistanceHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"tinker": {
|
||||
name: "SW5E.FlagsTinker",
|
||||
hint: "You have proficiency with tinker’s implements. You can use these and spend 1 hour and 100 cr worth of materials to construct a Tiny Device (AC 5, 1 hp). You can take the Use an Object action to have your device cause one of a variety of minor effects (see your species' traits list). You can maintain a number of these devices up to your proficiency bonus at once, and a device stops functioning after 24 hours away from you. You can dismantle the device to reclaim the materials used to create it.",
|
||||
section: "Species Traits",
|
||||
name: "SW5E.FlagsTinker",
|
||||
hint: "SW5E.FlagsTinkerHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"toughness": {
|
||||
name: "SW5E.FlagsToughness",
|
||||
hint: "Your hit point maximum increases by 1, and it increases by 1 every time you gain a level.",
|
||||
section: "Species Traits",
|
||||
type: Boolean
|
||||
name: "SW5E.FlagsToughness",
|
||||
hint: "SW5E.FlagsToughnessHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"trance": {
|
||||
name: "SW5E.FlagsTrance",
|
||||
hint: "Either through meditation or a reduced sleep schedule, you are able to receive the rest you require on a daily basis (see your species' traits for details). After resting in this way, you gain the same benefit that a human does from 8 hours of sleep.",
|
||||
section: "Species Traits",
|
||||
type: Boolean
|
||||
name: "SW5E.FlagsTrance",
|
||||
hint: "SW5E.FlagsTranceHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"unarmedCombatant": {
|
||||
name: "SW5E.FlagsUnarmedCombatant",
|
||||
hint: "Your unarmed strikes deal 1d4 kinetic damage. You can use your choice of your Strength or Dexterity modifier for the attack and damage rolls. You must use the same modifier for both rolls.",
|
||||
section: "Species Traits",
|
||||
hint: "SW5E.FlagsUnarmedCombatantHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"undersized": {
|
||||
name: "SW5E.FlagsUndersized",
|
||||
hint: "Your small stature makes it hard for you to wield bigger weapons. You can’t use heavy shields, martial weapons with the two-handed property unless it also has the light property, and if a martial weapon has the versatile property, you can only wield it in two hands.",
|
||||
section: "Species Traits",
|
||||
hint: "SW5E.FlagsUndersizedHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"unsettlingVisage": {
|
||||
name: "SW5E.FlagsUnsettlingVisage",
|
||||
hint: "When a creature you can see makes an attack roll against you, you can use your reaction to impose disadvantage on the roll. You must use this feature before knowing whether the attack hits or misses. Using this trait reveals your shapeshifting nature to any creature within 30 feet that can see you. Once you use this trait, you can't use it again until you finish a short or long rest.",
|
||||
section: "Species Traits",
|
||||
type: Boolean
|
||||
hint: "SW5E.FlagsUnsettlingVisageHint",
|
||||
section: "SW5E.SpeciesTraits",
|
||||
type: Boolean
|
||||
},
|
||||
"initiativeAdv": {
|
||||
name: "SW5E.FlagsInitiativeAdv",
|
||||
hint: "SW5E.FlagsInitiativeAdvHint",
|
||||
section: "Feats",
|
||||
section: "SW5E.Features",
|
||||
type: Boolean
|
||||
},
|
||||
"initiativeAlert": {
|
||||
name: "SW5E.FlagsAlert",
|
||||
hint: "SW5E.FlagsAlertHint",
|
||||
section: "Feats",
|
||||
section: "SW5E.Features",
|
||||
type: Boolean
|
||||
},
|
||||
"jackOfAllTrades": {
|
||||
name: "SW5E.FlagsJOAT",
|
||||
hint: "SW5E.FlagsJOATHint",
|
||||
section: "Feats",
|
||||
section: "SW5E.Features",
|
||||
type: Boolean
|
||||
},
|
||||
"observantFeat": {
|
||||
name: "SW5E.FlagsObservant",
|
||||
hint: "SW5E.FlagsObservantHint",
|
||||
skills: ['prc','inv'],
|
||||
section: "Feats",
|
||||
section: "SW5E.Features",
|
||||
type: Boolean
|
||||
},
|
||||
"reliableTalent": {
|
||||
name: "SW5E.FlagsReliableTalent",
|
||||
hint: "SW5E.FlagsReliableTalentHint",
|
||||
section: "Feats",
|
||||
section: "SW5E.Features",
|
||||
type: Boolean
|
||||
},
|
||||
"remarkableAthlete": {
|
||||
name: "SW5E.FlagsRemarkableAthlete",
|
||||
hint: "SW5E.FlagsRemarkableAthleteHint",
|
||||
abilities: ['str','dex','con'],
|
||||
section: "Feats",
|
||||
section: "SW5E.Features",
|
||||
type: Boolean
|
||||
},
|
||||
"weaponCriticalThreshold": {
|
||||
name: "SW5E.FlagsWeaponCritThreshold",
|
||||
hint: "SW5E.FlagsWeaponCritThresholdHint",
|
||||
section: "Feats",
|
||||
section: "SW5E.Features",
|
||||
type: Number,
|
||||
placeholder: 20
|
||||
},
|
||||
"powerCriticalThreshold": {
|
||||
name: "SW5E.FlagsPowerCritThreshold",
|
||||
hint: "SW5E.FlagsPowerCritThresholdHint",
|
||||
section: "Feats",
|
||||
section: "SW5E.Features",
|
||||
type: Number,
|
||||
placeholder: 20
|
||||
},
|
||||
"meleeCriticalDamageDice": {
|
||||
name: "SW5E.FlagsMeleeCriticalDice",
|
||||
hint: "SW5E.FlagsMeleeCriticalDiceHint",
|
||||
section: "Feats",
|
||||
section: "SW5E.Features",
|
||||
type: Number,
|
||||
placeholder: 0
|
||||
}
|
||||
};
|
||||
|
||||
// Configure allowed status flags
|
||||
SW5E.allowedActorFlags = ["isPolymorphed", "originalActor"].concat(Object.keys(SW5E.characterFlags));
|
||||
SW5E.allowedActorFlags = ["isPolymorphed", "originalActor", "dataVersion"].concat(Object.keys(SW5E.characterFlags));
|
||||
|
|
6
module/effects.js
vendored
|
@ -37,17 +37,17 @@ export function prepareActiveEffectCategories(effects) {
|
|||
const categories = {
|
||||
temporary: {
|
||||
type: "temporary",
|
||||
label: "Temporary Effects",
|
||||
label: "SW5E.EffectsCategoryTemporary",
|
||||
effects: []
|
||||
},
|
||||
passive: {
|
||||
type: "passive",
|
||||
label: "Passive Effects",
|
||||
label: "SW5E.EffectsCategoryPassive",
|
||||
effects: []
|
||||
},
|
||||
inactive: {
|
||||
type: "inactive",
|
||||
label: "Inactive Effects",
|
||||
label: "SW5E.EffectsCategoryInactive",
|
||||
effects: []
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,9 +6,10 @@ export const migrateWorld = async function() {
|
|||
ui.notifications.info(`Applying SW5e System Migration for version ${game.system.data.version}. Please be patient and do not close your game or shut down your server.`, {permanent: true});
|
||||
|
||||
// Migrate World Actors
|
||||
for ( let a of game.actors.entities ) {
|
||||
for await ( let a of game.actors.entities ) {
|
||||
try {
|
||||
const updateData = migrateActorData(a.data);
|
||||
console.log(`Checking Actor entity ${a.name} for migration needs`);
|
||||
const updateData = await migrateActorData(a.data);
|
||||
if ( !isObjectEmpty(updateData) ) {
|
||||
console.log(`Migrating Actor entity ${a.name}`);
|
||||
await a.update(updateData, {enforceTypes: false});
|
||||
|
@ -36,7 +37,7 @@ export const migrateWorld = async function() {
|
|||
// Migrate Actor Override Tokens
|
||||
for ( let s of game.scenes.entities ) {
|
||||
try {
|
||||
const updateData = migrateSceneData(s.data);
|
||||
const updateData = await migrateSceneData(s.data);
|
||||
if ( !isObjectEmpty(updateData) ) {
|
||||
console.log(`Migrating Scene entity ${s.name}`);
|
||||
await s.update(updateData, {enforceTypes: false});
|
||||
|
@ -79,18 +80,18 @@ export const migrateCompendium = async function(pack) {
|
|||
const content = await pack.getContent();
|
||||
|
||||
// Iterate over compendium entries - applying fine-tuned migration functions
|
||||
for ( let ent of content ) {
|
||||
for await ( let ent of content ) {
|
||||
let updateData = {};
|
||||
try {
|
||||
switch (entity) {
|
||||
case "Actor":
|
||||
updateData = migrateActorData(ent.data);
|
||||
updateData = await migrateActorData(ent.data);
|
||||
break;
|
||||
case "Item":
|
||||
updateData = migrateItemData(ent.data);
|
||||
break;
|
||||
case "Scene":
|
||||
updateData = migrateSceneData(ent.data);
|
||||
updateData = await migrateSceneData(ent.data);
|
||||
break;
|
||||
}
|
||||
if ( isObjectEmpty(updateData) ) continue;
|
||||
|
@ -123,7 +124,7 @@ export const migrateCompendium = async function(pack) {
|
|||
* @param {object} actor The actor data object to update
|
||||
* @return {Object} The updateData to apply
|
||||
*/
|
||||
export const migrateActorData = function(actor) {
|
||||
export const migrateActorData = async function(actor) {
|
||||
const updateData = {};
|
||||
|
||||
// Actor Data Updates
|
||||
|
@ -131,27 +132,40 @@ 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 = await actor.items.reduce(async (memo, i) => {
|
||||
const results = await memo;
|
||||
|
||||
// Migrate the Owned Item
|
||||
let itemUpdate = migrateItemData(i);
|
||||
// Migrate the Owned Item
|
||||
let itemUpdate = await migrateActorItemData(i, actor);
|
||||
|
||||
// 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;
|
||||
console.log(`Migrating Actor ${actor.name}'s ${i.name}`);
|
||||
return [...results, mergeObject(i, itemUpdate, {enforceTypes: false, inplace: false})];
|
||||
} else return [...results, 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,22 +205,38 @@ function cleanActorData(actorData) {
|
|||
*/
|
||||
export const migrateItemData = function(item) {
|
||||
const updateData = {};
|
||||
_migrateItemClassPowerCasting(item, updateData);
|
||||
_migrateItemAttunement(item, updateData);
|
||||
return updateData;
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Migrate a single owned actor Item entity to incorporate latest data model changes
|
||||
* @param item
|
||||
* @param actor
|
||||
*/
|
||||
export const migrateActorItemData = async function(item, actor) {
|
||||
const updateData = {};
|
||||
_migrateItemClassPowerCasting(item, updateData);
|
||||
_migrateItemAttunement(item, updateData);
|
||||
await _migrateItemPower(item, actor, updateData);
|
||||
return updateData;
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Migrate a single Scene entity to incorporate changes to the data model of it's actor data overrides
|
||||
* Return an Object of updateData to be applied
|
||||
* @param {Object} scene The Scene data to Update
|
||||
* @return {Object} The updateData to apply
|
||||
*/
|
||||
export const migrateSceneData = function(scene) {
|
||||
export const migrateSceneData = async function(scene) {
|
||||
const tokens = duplicate(scene.tokens);
|
||||
return {
|
||||
tokens: tokens.map(t => {
|
||||
tokens: await Promise.all(tokens.map(async (t) => {
|
||||
if (!t.actorId || t.actorLink || !t.actorData.data) {
|
||||
t.actorData = {};
|
||||
return t;
|
||||
|
@ -216,11 +246,11 @@ export const migrateSceneData = function(scene) {
|
|||
t.actorId = null;
|
||||
t.actorData = {};
|
||||
} else if ( !t.actorLink ) {
|
||||
const updateData = migrateActorData(token.data.actorData);
|
||||
const updateData = await migrateActorData(token.data.actorData);
|
||||
t.actorData = mergeObject(token.data.actorData, updateData);
|
||||
}
|
||||
return t;
|
||||
})
|
||||
}))
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -228,6 +258,72 @@ 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, if not there is no compendium monster so exit
|
||||
const hasSource = actor?.flags?.core?.sourceId !== undefined;
|
||||
if (!hasSource) return actor;
|
||||
// shortcut out if dataVersion flag is set to 1.2.4 or higher
|
||||
const hasDataVersion = actor?.flags?.sw5e?.dataVersion !== undefined;
|
||||
if (hasDataVersion && (actor.flags.sw5e.dataVersion === "1.2.4" || isNewerVersion("1.2.4", actor.flags.sw5e.dataVersion))) return actor;
|
||||
// Check to see what the source of NPC is
|
||||
const sourceId = actor.flags.core.sourceId;
|
||||
const coreSource = sourceId.substr(0,sourceId.length-17);
|
||||
const core_id = sourceId.substr(sourceId.length-16,16);
|
||||
if (coreSource === "Compendium.sw5e.monsters"){
|
||||
game.packs.get("sw5e.monsters").getEntity(core_id).then(monster => {
|
||||
const monsterData = monster.data.data;
|
||||
// copy movement[], senses[], powercasting, force[], tech[], powerForceLevel, powerTechLevel
|
||||
updateData["data.attributes.movement"] = monsterData.attributes.movement;
|
||||
updateData["data.attributes.senses"] = monsterData.attributes.senses;
|
||||
updateData["data.attributes.powercasting"] = monsterData.attributes.powercasting;
|
||||
updateData["data.attributes.force"] = monsterData.attributes.force;
|
||||
updateData["data.attributes.tech"] = monsterData.attributes.tech;
|
||||
updateData["data.details.powerForceLevel"] = monsterData.details.powerForceLevel;
|
||||
updateData["data.details.powerTechLevel"] = monsterData.details.powerTechLevel;
|
||||
// push missing powers onto actor
|
||||
let newPowers = [];
|
||||
for ( let i of monster.items ) {
|
||||
const itemData = i.data;
|
||||
if ( itemData.type === "power" ) {
|
||||
const itemCompendium_id = itemData.flags?.core?.sourceId.split(".").slice(-1)[0];
|
||||
let hasPower = !!actor.items.find(item => item.flags?.core?.sourceId.split(".").slice(-1)[0] === itemCompendium_id);
|
||||
if (!hasPower) {
|
||||
// Clone power to new object. Don't know if it is technically needed, but seems to prevent some weirdness.
|
||||
const newPower = JSON.parse(JSON.stringify(itemData));
|
||||
|
||||
newPowers.push(newPower);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// get actor to create new powers
|
||||
const liveActor = game.actors.get(actor._id);
|
||||
// create the powers on the actor
|
||||
liveActor.createEmbeddedEntity("OwnedItem", newPowers);
|
||||
|
||||
// set flag to check to see if migration has been done so we don't do it again.
|
||||
liveActor.setFlag("sw5e", "dataVersion", "1.2.4");
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
//merge object
|
||||
actorData = mergeObject(actorData, updateData);
|
||||
// Return the scrubbed data
|
||||
return actor;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Migrate the actor speed string to movement object
|
||||
* @private
|
||||
|
@ -255,6 +351,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
|
||||
|
@ -292,6 +449,81 @@ function _migrateActorSenses(actor, 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;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Update an Power Item's data based on compendium
|
||||
* @param {Object} item The data object for an item
|
||||
* @param {Object} actor The data object for the actor owning the item
|
||||
* @private
|
||||
*/
|
||||
async function _migrateItemPower(item, actor, updateData) {
|
||||
// if item is not a power shortcut out
|
||||
if (item.type !== "power") return updateData;
|
||||
|
||||
console.log(`Checking Actor ${actor.name}'s ${item.name} for migration needs`);
|
||||
// check for flag.core, if not there is no compendium power so exit
|
||||
const hasSource = item?.flags?.core?.sourceId !== undefined;
|
||||
if (!hasSource) return updateData;
|
||||
|
||||
// shortcut out if dataVersion flag is set to 1.2.4 or higher
|
||||
const hasDataVersion = item?.flags?.sw5e?.dataVersion !== undefined;
|
||||
if (hasDataVersion && (item.flags.sw5e.dataVersion === "1.2.4" || isNewerVersion("1.2.4", item.flags.sw5e.dataVersion))) return updateData;
|
||||
|
||||
// Check to see what the source of Power is
|
||||
const sourceId = item.flags.core.sourceId;
|
||||
const coreSource = sourceId.substr(0, sourceId.length - 17);
|
||||
const core_id = sourceId.substr(sourceId.length - 16, 16);
|
||||
|
||||
//if power type is not force or tech exit out
|
||||
let powerType = "none";
|
||||
if (coreSource === "Compendium.sw5e.forcepowers") powerType = "sw5e.forcepowers";
|
||||
if (coreSource === "Compendium.sw5e.techpowers") powerType = "sw5e.techpowers";
|
||||
if (powerType === "none") return updateData;
|
||||
|
||||
const corePower = duplicate(await game.packs.get(powerType).getEntity(core_id));
|
||||
console.log(`Updating Actor ${actor.name}'s ${item.name} from compendium`);
|
||||
const corePowerData = corePower.data;
|
||||
// copy Core Power Data over original Power
|
||||
updateData["data"] = corePowerData;
|
||||
updateData["flags"] = {"sw5e": {"dataVersion": "1.2.4"}};
|
||||
|
||||
return updateData;
|
||||
|
||||
|
||||
//game.packs.get(powerType).getEntity(core_id).then(corePower => {
|
||||
|
||||
//})
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Delete the old data.attuned boolean
|
||||
* @private
|
||||
|
@ -303,7 +535,6 @@ function _migrateItemAttunement(item, updateData) {
|
|||
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/Archetypes/Aqinos Form.webp
Normal file
After Width: | Height: | Size: 10 KiB |
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 |
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
BIN
packs/Icons/Force Powers/Telekinetic Shield.webp
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
packs/Icons/Martial Blasters/ARC Caster.webp
Normal file
After Width: | Height: | Size: 9.5 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 8.6 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 8.1 KiB |
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 8.5 KiB |
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 9.7 KiB |
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 7.6 KiB |
BIN
packs/Icons/Martial Blasters/Disruptor Pistol.webp
Normal file
After Width: | Height: | Size: 9.9 KiB |
BIN
packs/Icons/Martial Blasters/Disruptor Rifle.webp
Normal file
After Width: | Height: | Size: 9.1 KiB |
BIN
packs/Icons/Martial Blasters/Flechette Cannon.webp
Normal file
After Width: | Height: | Size: 8.4 KiB |
BIN
packs/Icons/Martial Blasters/Grenade Launcher.webp
Normal file
After Width: | Height: | Size: 7.9 KiB |
BIN
packs/Icons/Martial Blasters/Hand Blaster.webp
Normal file
After Width: | Height: | Size: 8.9 KiB |
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 9.4 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 7.8 KiB |
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 8.6 KiB |
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 7.5 KiB |
BIN
packs/Icons/Martial Blasters/IWS.webp
Normal file
After Width: | Height: | Size: 9.7 KiB |
BIN
packs/Icons/Martial Blasters/Incinerator Pistol.webp
Normal file
After Width: | Height: | Size: 8.3 KiB |
BIN
packs/Icons/Martial Blasters/Incinerator Rifle.webp
Normal file
After Width: | Height: | Size: 8.6 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.6 KiB |
BIN
packs/Icons/Martial Blasters/Lightbow.webp
Normal file
After Width: | Height: | Size: 8 KiB |
BIN
packs/Icons/Martial Blasters/Nightstinger Rifle.webp
Normal file
After Width: | Height: | Size: 7.9 KiB |
BIN
packs/Icons/Martial Blasters/Rail Gun.webp
Normal file
After Width: | Height: | Size: 8.1 KiB |
BIN
packs/Icons/Martial Blasters/Repeater.webp
Normal file
After Width: | Height: | Size: 8.4 KiB |
Before Width: | Height: | Size: 8.5 KiB |
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 9.2 KiB |