Merge pull request #172 from unrealkakeman89/Develop

Develop
This commit is contained in:
CK 2021-03-16 10:15:16 -04:00 committed by GitHub
commit fee77e2172
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
213 changed files with 5976 additions and 3824 deletions

2
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,2 @@
{
}

File diff suppressed because it is too large Load diff

1027
lang/fr.json Normal file

File diff suppressed because it is too large Load diff

View file

@ -400,7 +400,8 @@
.tab.features, .tab.features,
.tab.inventory, .tab.inventory,
.tab.powerbook { .tab.force-powerbook,
.tab.tech-powerbook {
overflow-y: hidden; overflow-y: hidden;
} }

View file

@ -4,7 +4,7 @@
/* Basic Structure */ /* Basic Structure */
/* ----------------------------------------- */ /* ----------------------------------------- */
.sw5e.sheet.actor.npc { .sw5e.sheet.actor.npc {
min-width: 600px; min-width: 872px;
min-height: 680px; min-height: 680px;
.header-exp { .header-exp {

File diff suppressed because it is too large Load diff

View file

@ -382,7 +382,8 @@
} }
.tab.powerbook { .tab.force-powerbook,
.tab.tech-powerbook {
.powercasting-ability { .powercasting-ability {
label, label,
h3 { h3 {

View file

@ -92,7 +92,10 @@ export default class Actor5e extends Actor {
init.total = init.mod + init.prof + init.bonus; init.total = init.mod + init.prof + init.bonus;
// Prepare power-casting data // 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); this._computePowercastingProgression(this.data);
// Compute owned item attributes which depend on prepared Actor 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 powers = actorData.data.powers;
const isNPC = actorData.type === 'npc'; const isNPC = actorData.type === 'npc';
// Translate the list of classes into power-casting progression // Translate the list of classes into force and tech power-casting progression
const progression = { const forceProgression = {
total: 0, classes: 0,
slot: 0, levels: 0,
pact: 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 // Tabulate the total power-casting progression
const classes = this.data.items.filter(i => i.type === "class"); const classes = this.data.items.filter(i => i.type === "class");
let priority = 0;
for ( let cls of classes ) { for ( let cls of classes ) {
const d = cls.data; const d = cls.data;
if ( d.powercasting === "none" ) continue; if ( d.powercasting === "none" ) continue;
const levels = d.levels; const levels = d.levels;
const prog = d.powercasting; const prog = d.powercasting;
// Accumulate levels
if ( prog !== "pact" ) {
caster = cls;
progression.total++;
}
switch (prog) { switch (prog) {
case 'third': progression.slot += Math.floor(levels / 3); break; case 'consular':
case 'half': progression.slot += Math.floor(levels / 2); break; priority = 3;
case 'full': progression.slot += levels; break; forceProgression.levels += levels;
case 'artificer': progression.slot += Math.ceil(levels / 2); break; forceProgression.multi += (SW5E.powerMaxLevel['consular'][19]/9)*levels;
case 'pact': progression.pact += levels; break; 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 // EXCEPTION: multi-classed progression uses multi rounded down rather than levels
const isSingleClass = (progression.total === 1) && (progression.slot > 0); if (!isNPC && forceProgression.classes > 1) {
if (!isNPC && isSingleClass && ['half', 'third'].includes(caster.data.powercasting) ) { forceProgression.levels = Math.floor(forceProgression.multi);
const denom = caster.data.powercasting === 'third' ? 3 : 2; forceProgression.maxClassPowerLevel = SW5E.powerMaxLevel['multi'][forceProgression.levels - 1];
progression.slot = Math.ceil(caster.data.levels / denom);
} }
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 // EXCEPTION: NPC with an explicit power-caster level
if (isNPC && actorData.data.details.powerLevel) { if (isNPC && actorData.data.details.powerForceLevel) {
progression.slot = actorData.data.details.powerLevel; 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) ) { for ( let [n, lvl] of Object.entries(powers) ) {
let i = parseInt(n.slice(-1)); let i = parseInt(n.slice(-1));
if ( Number.isNaN(i) ) continue; if ( Number.isNaN(i) ) continue;
if ( Number.isNumeric(lvl.override) ) lvl.max = Math.max(parseInt(lvl.override), 0); if ( Number.isNumeric(lvl.foverride) ) lvl.fmax = Math.max(parseInt(lvl.foverride), 0);
else lvl.max = slots[i-1] || 0; else lvl.fmax = forcePowerLimit[i-1] || 0;
lvl.value = parseInt(lvl.value); 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) for ( let [n, lvl] of Object.entries(powers) ) {
let pl = Math.clamped(progression.pact, 0, 20); let i = parseInt(n.slice(-1));
powers.pact = powers.pact || {}; if ( Number.isNaN(i) ) continue;
if ( (pl === 0) && isNPC && Number.isNumeric(powers.pact.override) ) pl = actorData.data.details.powerLevel; 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 // Set Force and tech power for PC Actors
if ( pl > 0) { if (!isNPC && forceProgression.levels){
powers.pact.level = Math.ceil(Math.min(10, pl) / 2); actorData.data.attributes.force.known.max = forceProgression.powersKnown;
if ( Number.isNumeric(powers.pact.override) ) powers.pact.max = Math.max(parseInt(powers.pact.override), 1); actorData.data.attributes.force.points.max = forceProgression.points + Math.max(actorData.data.abilities.wis.mod,actorData.data.abilities.cha.mod);
else powers.pact.max = Math.max(1, Math.min(pl, 2), Math.min(pl - 8, 3), Math.min(pl - 13, 4)); actorData.data.attributes.force.level = forceProgression.levels;
powers.pact.value = Math.min(powers.pact.value, powers.pact.max); }
} else { if (!isNPC && techProgression.levels){
powers.pact.max = parseInt(powers.pact.override) || 0 actorData.data.attributes.tech.known.max = techProgression.powersKnown;
powers.pact.level = powers.pact.max > 0 ? 1 : 0; 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 * 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} dialog Present a dialog window which allows for rolling hit dice as part of the Short Rest
* @param {boolean} chat Summarize the results of the rest workflow as a chat message * @param {boolean} 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 dhd = this.data.data.attributes.hd - hd0;
const dhp = this.data.data.attributes.hp.value - hp0; 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 // Recover character resources
const updateData = {}; const updateData = {};
for ( let [k, r] of Object.entries(this.data.data.resources) ) { 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 // Recover item uses
const recovery = newDay ? ["sr", "day"] : ["sr"]; const recovery = newDay ? ["sr", "day"] : ["sr"];
const items = this.items.filter(item => item.data.data.uses && recovery.includes(item.data.data.uses.per)); 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 // Summarize the health effects
let srMessage = "SW5E.ShortRestResultShort"; 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 // Create a chat message
ChatMessage.create({ ChatMessage.create({
user: game.user._id, user: game.user._id,
speaker: {actor: this, alias: this.name}, speaker: {actor: this, alias: this.name},
flavor: restFlavor, 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 { return {
dhd: dhd, dhd: dhd,
dhp: dhp, dhp: dhp,
dtp: dtp,
updateData: updateData, updateData: updateData,
updateItems: updateItems, updateItems: updateItems,
newDay: newDay 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} 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} 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 * @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 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 = { const updateData = {
"data.attributes.hp.value": data.attributes.hp.max, "data.attributes.hp.value": data.attributes.hp.max,
"data.attributes.hp.temp": 0, "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 // Recover character resources
@ -1116,13 +1275,11 @@ export default class Actor5e extends Actor {
// Recover power slots // Recover power slots
for ( let [k, v] of Object.entries(data.powers) ) { 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 // Determine the number of hit dice which may be recovered
let recoverHD = Math.max(Math.floor(data.details.level / 2), 1); let recoverHD = Math.max(Math.floor(data.details.level / 2), 1);
let dhd = 0; let dhd = 0;
@ -1169,15 +1326,16 @@ export default class Actor5e extends Actor {
// Determine the chat message to display // Determine the chat message to display
if ( chat ) { if ( chat ) {
let lrMessage = "SW5E.LongRestResultShort"; let lrMessage = "SW5E.LongRestResult";
if((dhp !== 0) && (dhd !== 0)) lrMessage = "SW5E.LongRestResult"; if (dhp !== 0) lrMessage += "HP";
else if ((dhp !== 0) && (dhd === 0)) lrMessage = "SW5E.LongRestResultHitPoints"; if (dfp !== 0) lrMessage += "FP";
else if ((dhp === 0) && (dhd !== 0)) lrMessage = "SW5E.LongRestResultHitDice"; if (dtp !== 0) lrMessage += "TP";
if (dhd !== 0) lrMessage += "HD";
ChatMessage.create({ ChatMessage.create({
user: game.user._id, user: game.user._id,
speaker: {actor: this, alias: this.name}, speaker: {actor: this, alias: this.name},
flavor: restFlavor, 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 { return {
dhd: dhd, dhd: dhd,
dhp: dhp, dhp: dhp,
dtp: dtp,
dfp: dfp,
updateData: updateData, updateData: updateData,
updateItems: updateItems, updateItems: updateItems,
newDay: newDay newDay: newDay

View file

@ -21,7 +21,8 @@ export default class ActorSheet5e extends ActorSheet {
*/ */
this._filters = { this._filters = {
inventory: new Set(), inventory: new Set(),
powerbook: new Set(), forcePowerbook: new Set(),
techPowerbook: new Set(),
features: new Set(), features: new Set(),
effects: new Set() effects: new Set()
}; };
@ -35,7 +36,8 @@ export default class ActorSheet5e extends ActorSheet {
scrollY: [ scrollY: [
".inventory .group-list", ".inventory .group-list",
".features .group-list", ".features .group-list",
".powerbook .group-list", ".force-powerbook .group-list",
".tech-powerbook .group-list",
".effects .effects-list" ".effects .effects-list"
], ],
tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}] 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 * @param {Array} powers The power data being prepared
* @private * @private
*/ */
_preparePowerbook(data, powers) { _preparePowerbook(data, powers, school) {
const owner = this.actor.owner; const owner = this.actor.owner;
const levels = data.data.powers; const levels = data.data.powers;
const powerbook = {}; const powerbook = {};
@ -229,7 +231,6 @@ export default class ActorSheet5e extends ActorSheet {
const sections = { const sections = {
"atwill": -20, "atwill": -20,
"innate": -10, "innate": -10,
"pact": 0.5
}; };
// Label power slot uses headers // Label power slot uses headers
@ -251,7 +252,7 @@ export default class ActorSheet5e extends ActorSheet {
uses: useLabels[i] || value || 0, uses: useLabels[i] || value || 0,
slots: useLabels[i] || max || 0, slots: useLabels[i] || max || 0,
override: override || 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 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 // Iterate over every power item, adding powers to the powerbook by section
powers.forEach(power => { powers.forEach(power => {
const mode = power.data.preparation.mode || "prepared"; const mode = power.data.preparation.mode || "prepared";

View file

@ -84,7 +84,7 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
}; };
// Partition items by category // 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 details
item.img = item.img || DEFAULT_TOKEN; item.img = item.img || DEFAULT_TOKEN;
@ -112,23 +112,25 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
this._prepareItemToggleState(item); this._prepareItemToggleState(item);
// Classify items into types // Classify items into types
if ( item.type === "power" ) arr[1].push(item); if ( item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school) ) arr[1].push(item);
else if ( item.type === "feat" ) arr[2].push(item); else if ( item.type === "power" && ["tec"].includes(item.data.school) ) arr[2].push(item);
else if ( item.type === "class" ) arr[3].push(item); else if ( item.type === "feat" ) arr[3].push(item);
else if ( item.type === "species" ) arr[4].push(item); else if ( item.type === "class" ) arr[4].push(item);
else if ( item.type === "archetype" ) arr[5].push(item); else if ( item.type === "species" ) arr[5].push(item);
else if ( item.type === "classfeature" ) arr[6].push(item); else if ( item.type === "archetype" ) arr[6].push(item);
else if ( item.type === "background" ) arr[7].push(item); else if ( item.type === "classfeature" ) arr[7].push(item);
else if ( item.type === "fightingstyle" ) arr[8].push(item); else if ( item.type === "background" ) arr[8].push(item);
else if ( item.type === "fightingmastery" ) arr[9].push(item); else if ( item.type === "fightingstyle" ) arr[9].push(item);
else if ( item.type === "lightsaberform" ) arr[10].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); else if ( Object.keys(inventory).includes(item.type ) ) arr[0].push(item);
return arr; return arr;
}, [[], [], [], [], [], [], [], [], [], [], []]); }, [[], [], [], [], [], [], [], [], [], [], [], []]);
// Apply active item filters // Apply active item filters
items = this._filterItems(items, this._filters.inventory); 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); feats = this._filterItems(feats, this._filters.features);
// Organize items // 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...) // Organize Powerbook and count the number of prepared powers (excluding always, at will, etc...)
const powerbook = this._preparePowerbook(data, powers); const forcePowerbook = this._preparePowerbook(data, forcepowers, "uni");
const nPrepared = powers.filter(s => { const techPowerbook = this._preparePowerbook(data, techpowers, "tec");
return (s.data.level > 0) && (s.data.preparation.mode === "prepared") && s.data.preparation.prepared;
}).length;
// Organize Features // Organize Features
const features = { const features = {
@ -174,8 +174,8 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
// Assign and return // Assign and return
data.inventory = Object.values(inventory); data.inventory = Object.values(inventory);
data.powerbook = powerbook; data.forcePowerbook = forcePowerbook;
data.preparedPowers = nPrepared; data.techPowerbook = techPowerbook;
data.features = Object.values(features); data.features = Object.values(features);
} }

View file

@ -42,24 +42,27 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
}; };
// Start by classifying items into groups for rendering // 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.img = item.img || DEFAULT_TOKEN;
item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1); item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
item.hasUses = item.data.uses && (item.data.uses.max > 0); 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.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.isDepleted = item.isOnCooldown && (item.data.uses.per && (item.data.uses.value > 0));
item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type)); item.hasTarget = !!item.data.target && !(["none",""].includes(item.data.target.type));
if ( item.type === "power" ) arr[0].push(item); if ( item.type === "power" && ["lgt", "drk", "uni"].includes(item.data.school) ) arr[0].push(item);
else arr[1].push(item); else if ( item.type === "power" && ["tec"].includes(item.data.school) ) arr[1].push(item);
else arr[2].push(item);
return arr; return arr;
}, [[], []]); }, [[], [], []]);
// Apply item filters // 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); other = this._filterItems(other, this._filters.features);
// Organize Powerbook // Organize Powerbook
const powerbook = this._preparePowerbook(data, powers); const forcePowerbook = this._preparePowerbook(data, forcepowers, "uni");
const techPowerbook = this._preparePowerbook(data, techpowers, "tec");
// Organize Features // Organize Features
for ( let item of other ) { for ( let item of other ) {
@ -73,7 +76,8 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
// Assign and return // Assign and return
data.features = Object.values(features); data.features = Object.values(features);
data.powerbook = powerbook; data.forcePowerbook = forcePowerbook;
data.techPowerbook = techPowerbook;
} }

View file

@ -100,32 +100,68 @@ export default class AbilityUseDialog extends Dialog {
// Determine the levels which are feasible // Determine the levels which are feasible
let lmax = 0; let lmax = 0;
const powerLevels = Array.fromRange(10).reduce((arr, i) => { let points;
if ( i < lvl ) return arr; let powerType;
const label = CONFIG.SW5E.powerLevels[i]; switch (itemData.school){
const l = actorData.powers["power"+i] || {max: 0, override: null}; case "lgt":
let max = parseInt(l.override || l.max || 0); case "uni":
let slots = Math.clamped(parseInt(l.value || 0), 0, max); case "drk": {
if ( max > 0 ) lmax = i; powerType = "force"
arr.push({ points = actorData.attributes.force.points.value + actorData.attributes.force.points.temp;
level: i, break;
label: i > 0 ? game.i18n.format('SW5E.PowerLevelSlot', {level: label, n: slots}) : label, }
canCast: max > 0, case "tec": {
hasSlots: slots > 0 powerType = "tech"
}); points = actorData.attributes.tech.points.value + actorData.attributes.tech.points.temp;
return arr; break;
}, []).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
});
} }
// 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); const canCast = powerLevels.some(l => l.hasSlots);
if ( !canCast ) data.errors.push(game.i18n.format("SW5E.PowerCastNoSlots", { if ( !canCast ) data.errors.push(game.i18n.format("SW5E.PowerCastNoSlots", {
level: CONFIG.SW5E.powerLevels[lvl], level: CONFIG.SW5E.powerLevels[lvl],

View file

@ -74,7 +74,11 @@ export default class ActorSheetFlags extends BaseEntitySheet {
{name: "data.bonuses.abilities.check", label: "SW5E.BonusAbilityCheck"}, {name: "data.bonuses.abilities.check", label: "SW5E.BonusAbilityCheck"},
{name: "data.bonuses.abilities.save", label: "SW5E.BonusAbilitySave"}, {name: "data.bonuses.abilities.save", label: "SW5E.BonusAbilitySave"},
{name: "data.bonuses.abilities.skill", label: "SW5E.BonusAbilitySkill"}, {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 ) { for ( let b of bonuses ) {
b.value = getProperty(this.object._data, b.name) || ""; b.value = getProperty(this.object._data, b.name) || "";

View file

@ -517,12 +517,77 @@ SW5E.powerPreparationModes = {
"innate": "SW5E.PowerPrepInnate" "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 = { SW5E.powerProgression = {
"none": "SW5E.PowerNone", "none": "SW5E.PowerNone",
"full": "SW5E.PowerProgFull", "consular": "SW5E.PowerProgCns",
"artificer": "SW5E.PowerProgArt" "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" 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. // Polymorph options.
SW5E.polymorphSettings = { SW5E.polymorphSettings = {
keepPhysical: 'SW5E.PolymorphKeepPhysical', keepPhysical: 'SW5E.PolymorphKeepPhysical',
@ -858,353 +893,353 @@ SW5E.classFeatures = ClassFeatures;
// Configure Optional Character Flags // Configure Optional Character Flags
SW5E.characterFlags = { SW5E.characterFlags = {
"adaptiveResilience": { "adaptiveResilience": {
name: "SW5E.FlagsAdaptiveResilience", 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.", hint: "SW5E.FlagsAdaptiveResilienceHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"aggressive": { "aggressive": {
name: "SW5E.FlagsAggressive", 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.", hint: "SW5E.FlagsAggressiveHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"amphibious": { "amphibious": {
name: "SW5E.FlagsAmphibious", name: "SW5E.FlagsAmphibious",
hint: "You can breathe air and water.", hint: "SW5E.FlagsAmphibiousHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"armorIntegration": { "armorIntegration": {
name: "SW5E.FlagsArmorIntegration", 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 astrotechs implements. You must be proficient in armor in order to have it integrated.", hint: "SW5E.FlagsArmorIntegrationHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"businessSavvy": { "businessSavvy": {
name: "SW5E.FlagsBusinessSavvy", name: "SW5E.FlagsBusinessSavvy",
hint: "Whenever you make a Charisma (Persuasion) check involving haggling you are considered to have expertise in the Persuasion skill.", hint: "SW5E.FlagsBusinessSavvyHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"cannibalize": { "cannibalize": {
name: "SW5E.FlagsCannibalize", 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.", hint: "SW5E.FlagsCannibalizeHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"closedMind": { "closedMind": {
name: "SW5E.FlagsClosedMind", 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.", hint: "SW5E.FlagsClosedMindHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"crudeWeaponSpecialists": { "crudeWeaponSpecialists": {
name: "SW5E.FlagsCrudeWeaponSpecialists", 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 weapons damage suffers a -1 penalty.", hint: "SW5E.FlagsCrudeWeaponSpecialistsHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"defiant": { "defiant": {
name: "SW5E.FlagsDefiant", 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 rolls outcome. Once youve used this feature, you must complete a short or long rest before you can use it again.", hint: "SW5E.FlagsDefiantHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"detailOriented": { "detailOriented": {
name: "SW5E.FlagsDetailOriented", name: "SW5E.FlagsDetailOriented",
hint: "You have advantage on Intelligence (Investigation) checks within 5 feet.", hint: "SW5E.FlagsDetailOrientedHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"enthrallingPheromones": { "enthrallingPheromones": {
name: "SW5E.FlagsEnthrallingPheromones", 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.", hint: "SW5E.FlagsEnthrallingPheromonesHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"extraArms": { "extraArms": {
name: "SW5E.FlagsExtraArms", 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).", hint: "SW5E.FlagsExtraArmsHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"forceContention": { "forceContention": {
name: "SW5E.FlagsForceContention", 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.", hint: "SW5E.FlagsForceContentionHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"forceInsensitive": { "forceInsensitive": {
name: "SW5E.FlagsForceInsensitive", 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.", hint: "SW5E.FlagsForceInsensitiveHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"foreignBiology": { "foreignBiology": {
name: "SW5E.FlagsForeignBiology", 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.", hint: "SW5E.FlagsForeignBiologyHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"furyOfTheSmall": { "furyOfTheSmall": {
name: "SW5E.FlagsFuryOfTheSmall", 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.", hint: "SW5E.FlagsFuryOfTheSmallHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"grovelCowerAndBeg": { "grovelCowerAndBeg": {
name: "SW5E.FlagsGrovelCowerAndBeg", 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 cant use it again until you finish a short or long rest.", hint: "SW5E.FlagsGrovelCowerAndBegHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"inscrutable": { "inscrutable": {
name: "SW5E.FlagsInscrutable", 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.", hint: "SW5E.FlagsInscrutableHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"keenSenses": { "keenSenses": {
name: "SW5E.FlagsKeenSenses", name: "SW5E.FlagsKeenSenses",
hint: "You have advantage on Wisdom (Perception) checks that involve using particular senses (see your species' traits for details).", hint: "SW5E.FlagsKeenSensesHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"longlimbed": { "longlimbed": {
name: "SW5E.FlagsLongLimbed", name: "SW5E.FlagsLongLimbed",
hint: "When you make a melee attack on your turn, your reach for it is 5 feet greater than normal.", hint: "SW5E.FlagsLongLimbedHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"maintenanceMode": { "maintenanceMode": {
name: "SW5E.FlagsMaintenanceMode", 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.", hint: "SW5E.FlagsMaintenanceModeHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"maskOfTheWild": { "maskOfTheWild": {
name: "SW5E.FlagsMaskOfTheWild", 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.", hint: "SW5E.FlagsMaskOfTheWildHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"multipleHearts": { "multipleHearts": {
name: "SW5E.FlagsMultipleHearts", 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.", hint: "SW5E.FlagsMultipleHeartsHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"naturallyStealthy": { "naturallyStealthy": {
name: "SW5E.FlagsNaturallyStealthy", 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.", hint: "SW5E.FlagsNaturallyStealthyHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"nimbleAgility": { "nimbleAgility": {
name: "SW5E.FlagsNimbleAgility", 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.", hint: "SW5E.FlagsNimbleAgilityHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"nimbleEscape": { "nimbleEscape": {
name: "SW5E.FlagsNimbleEscape", name: "SW5E.FlagsNimbleEscape",
hint: "You can take the Disengage or Hide action as a bonus action.", hint: "SW5E.FlagsNimbleEscapeHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"nimbleness": { "nimbleness": {
name: "SW5E.FlagsNimbleness", name: "SW5E.FlagsNimbleness",
hint: "You can move through the space of any creature that is of a size larger than yours.", hint: "SW5E.FlagsNimblenessHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"pintsized": { "pintsized": {
name: "SW5E.FlagsPintsized", name: "SW5E.FlagsPintsized",
hint: "Your tiny stature makes it hard for you to wield bigger weapons. You cant use medium or heavy shields. Additionally, you cant 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.", hint: "SW5E.FlagsPintsizedHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"powerfulBuild": { "powerfulBuild": {
name: "SW5E.FlagsPowerfulBuild", name: "SW5E.FlagsPowerfulBuild",
hint: "SW5E.FlagsPowerfulBuildHint", hint: "SW5E.FlagsPowerfulBuildHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"precognition": { "precognition": {
name: "SW5E.FlagsPrecognition", 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.", hint: "SW5E.FlagsPrecognitionHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"programmer": { "programmer": {
name: "SW5E.FlagsProgrammer", name: "SW5E.FlagsProgrammer",
hint: "Whenever you make an Intelligence (Technology) check related to computers, you are considered to have expertise in the Technology skill.", hint: "SW5E.FlagsProgrammerHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"puny": { "puny": {
name: "SW5E.FlagsPuny", 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 cant add more than +3.", hint: "SW5E.FlagsPunyHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"rapidReconstruction": { "rapidReconstruction": {
name: "SW5E.FlagsRapidReconstruction", 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.", hint: "SW5E.FlagsRapidReconstructionHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"rapidlyRegenerative": { "rapidlyRegenerative": {
name: "SW5E.FlagsRapidlyRegenerative", 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.", hint: "SW5E.FlagsRapidlyRegenerativeHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"regenerative": { "regenerative": {
name: "SW5E.FlagsRegenerative", 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.", hint: "SW5E.FlagsRegenerativeHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"savageAttacks": { "savageAttacks": {
name: "SW5E.FlagsSavageAttacks", name: "SW5E.FlagsSavageAttacks",
hint: "SW5E.FlagsSavageAttacksHint", hint: "SW5E.FlagsSavageAttacksHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"shapechanger": { "shapechanger": {
name: "SW5E.FlagsShapechanger", 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.", hint: "SW5E.FlagsShapechangerHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"strongLegged": { "strongLegged": {
name: "SW5E.FlagsStrongLegged", 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.", hint: "SW5E.FlagsStrongLeggedHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"sunlightSensitivity": { "sunlightSensitivity": {
name: "SW5E.FlagsSunlightSensitivity", 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.", hint: "SW5E.FlagsSunlightSensitivityHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"surpriseAttack": { "surpriseAttack": {
name: "SW5E.FlagsSurpriseAttack", 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.", hint: "SW5E.FlagsSurpriseAttackHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"techImpaired": { "techImpaired": {
name: "SW5E.FlagsTechImpaired", 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.", hint: "SW5E.FlagsTechImpairedHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"techResistance": { "techResistance": {
name: "SW5E.FlagsTechResistance", name: "SW5E.FlagsTechResistance",
hint: "You have advantage on Dexterity and Intelligence saving throws against tech powers.", hint: "SW5E.FlagsTechResistanceHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"tinker": { "tinker": {
name: "SW5E.FlagsTinker", name: "SW5E.FlagsTinker",
hint: "You have proficiency with tinkers 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.", hint: "SW5E.FlagsTinkerHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"toughness": { "toughness": {
name: "SW5E.FlagsToughness", name: "SW5E.FlagsToughness",
hint: "Your hit point maximum increases by 1, and it increases by 1 every time you gain a level.", hint: "SW5E.FlagsToughnessHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"trance": { "trance": {
name: "SW5E.FlagsTrance", 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.", hint: "SW5E.FlagsTranceHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"unarmedCombatant": { "unarmedCombatant": {
name: "SW5E.FlagsUnarmedCombatant", 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.", hint: "SW5E.FlagsUnarmedCombatantHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"undersized": { "undersized": {
name: "SW5E.FlagsUndersized", name: "SW5E.FlagsUndersized",
hint: "Your small stature makes it hard for you to wield bigger weapons. You cant 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.", hint: "SW5E.FlagsUndersizedHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"unsettlingVisage": { "unsettlingVisage": {
name: "SW5E.FlagsUnsettlingVisage", 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.", hint: "SW5E.FlagsUnsettlingVisageHint",
section: "Species Traits", section: "SW5E.SpeciesTraits",
type: Boolean type: Boolean
}, },
"initiativeAdv": { "initiativeAdv": {
name: "SW5E.FlagsInitiativeAdv", name: "SW5E.FlagsInitiativeAdv",
hint: "SW5E.FlagsInitiativeAdvHint", hint: "SW5E.FlagsInitiativeAdvHint",
section: "Feats", section: "SW5E.Features",
type: Boolean type: Boolean
}, },
"initiativeAlert": { "initiativeAlert": {
name: "SW5E.FlagsAlert", name: "SW5E.FlagsAlert",
hint: "SW5E.FlagsAlertHint", hint: "SW5E.FlagsAlertHint",
section: "Feats", section: "SW5E.Features",
type: Boolean type: Boolean
}, },
"jackOfAllTrades": { "jackOfAllTrades": {
name: "SW5E.FlagsJOAT", name: "SW5E.FlagsJOAT",
hint: "SW5E.FlagsJOATHint", hint: "SW5E.FlagsJOATHint",
section: "Feats", section: "SW5E.Features",
type: Boolean type: Boolean
}, },
"observantFeat": { "observantFeat": {
name: "SW5E.FlagsObservant", name: "SW5E.FlagsObservant",
hint: "SW5E.FlagsObservantHint", hint: "SW5E.FlagsObservantHint",
skills: ['prc','inv'], skills: ['prc','inv'],
section: "Feats", section: "SW5E.Features",
type: Boolean type: Boolean
}, },
"reliableTalent": { "reliableTalent": {
name: "SW5E.FlagsReliableTalent", name: "SW5E.FlagsReliableTalent",
hint: "SW5E.FlagsReliableTalentHint", hint: "SW5E.FlagsReliableTalentHint",
section: "Feats", section: "SW5E.Features",
type: Boolean type: Boolean
}, },
"remarkableAthlete": { "remarkableAthlete": {
name: "SW5E.FlagsRemarkableAthlete", name: "SW5E.FlagsRemarkableAthlete",
hint: "SW5E.FlagsRemarkableAthleteHint", hint: "SW5E.FlagsRemarkableAthleteHint",
abilities: ['str','dex','con'], abilities: ['str','dex','con'],
section: "Feats", section: "SW5E.Features",
type: Boolean type: Boolean
}, },
"weaponCriticalThreshold": { "weaponCriticalThreshold": {
name: "SW5E.FlagsWeaponCritThreshold", name: "SW5E.FlagsWeaponCritThreshold",
hint: "SW5E.FlagsWeaponCritThresholdHint", hint: "SW5E.FlagsWeaponCritThresholdHint",
section: "Feats", section: "SW5E.Features",
type: Number, type: Number,
placeholder: 20 placeholder: 20
}, },
"powerCriticalThreshold": { "powerCriticalThreshold": {
name: "SW5E.FlagsPowerCritThreshold", name: "SW5E.FlagsPowerCritThreshold",
hint: "SW5E.FlagsPowerCritThresholdHint", hint: "SW5E.FlagsPowerCritThresholdHint",
section: "Feats", section: "SW5E.Features",
type: Number, type: Number,
placeholder: 20 placeholder: 20
}, },
"meleeCriticalDamageDice": { "meleeCriticalDamageDice": {
name: "SW5E.FlagsMeleeCriticalDice", name: "SW5E.FlagsMeleeCriticalDice",
hint: "SW5E.FlagsMeleeCriticalDiceHint", hint: "SW5E.FlagsMeleeCriticalDiceHint",
section: "Feats", section: "SW5E.Features",
type: Number, type: Number,
placeholder: 0 placeholder: 0
} }
}; };
// Configure allowed status flags // 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
View file

@ -37,17 +37,17 @@ export function prepareActiveEffectCategories(effects) {
const categories = { const categories = {
temporary: { temporary: {
type: "temporary", type: "temporary",
label: "Temporary Effects", label: "SW5E.EffectsCategoryTemporary",
effects: [] effects: []
}, },
passive: { passive: {
type: "passive", type: "passive",
label: "Passive Effects", label: "SW5E.EffectsCategoryPassive",
effects: [] effects: []
}, },
inactive: { inactive: {
type: "inactive", type: "inactive",
label: "Inactive Effects", label: "SW5E.EffectsCategoryInactive",
effects: [] effects: []
} }
}; };

View file

@ -25,8 +25,17 @@ export default class Item5e extends Item {
else if (this.actor) { else if (this.actor) {
const actorData = this.actor.data.data; const actorData = this.actor.data.data;
// Powers - Use Actor powercasting modifier // Powers - Use Actor powercasting modifier based on power school
if (this.data.type === "power") return actorData.attributes.powercasting || "int"; 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 // Tools - default to Intelligence
else if (this.data.type === "tool") return "int"; else if (this.data.type === "tool") return "int";
@ -291,7 +300,24 @@ export default class Item5e extends Item {
// Actor power-DC based scaling // Actor power-DC based scaling
if ( save.scaling === "power" ) { 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 // Ability-score based scaling
@ -394,6 +420,7 @@ export default class Item5e extends Item {
const recharge = id.recharge || {}; // Recharge mechanic const recharge = id.recharge || {}; // Recharge mechanic
const uses = id?.uses ?? {}; // Limited uses const uses = id?.uses ?? {}; // Limited uses
const isPower = this.type === "power"; // Does the item require a power slot? 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); const requirePowerSlot = isPower && (id.level > 0) && CONFIG.SW5E.powerUpcastModes.includes(id.preparation.mode);
// Define follow-up actions resulting from the item usage // 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 consumeUsage = !!uses.per; // Consume limited uses
let consumeQuantity = uses.autoDestroy; // Consume quantity of the item in lieu of 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; const needsConfiguration = createMeasuredTemplate || consumeRecharge || consumeResource || consumePowerSlot || consumeUsage;
if (configureDialog && needsConfiguration) { if (configureDialog && needsConfiguration) {
const configuration = await AbilityUseDialog.create(this); const configuration = await AbilityUseDialog.create(this);
if (!configuration) return; if (!configuration) return;
// Determine consumption preferences // Determine consumption preferences
createMeasuredTemplate = Boolean(configuration.placeTemplate); createMeasuredTemplate = Boolean(configuration.placeTemplate);
@ -420,18 +448,20 @@ export default class Item5e extends Item {
// Handle power upcasting // Handle power upcasting
if ( requirePowerSlot ) { if ( requirePowerSlot ) {
const slotLevel = configuration.level; const slotLevel = configuration.level;
const powerLevel = slotLevel === "pact" ? actor.data.data.powers.pact.level : parseInt(slotLevel); const powerLevel = parseInt(slotLevel);
if (powerLevel !== id.level) { if (powerLevel !== id.level) {
const upcastData = mergeObject(this.data, {"data.level": powerLevel}, {inplace: false}); const upcastData = mergeObject(this.data, {"data.level": powerLevel}, {inplace: false});
item = this.constructor.createOwned(upcastData, actor); // Replace the item with an upcast version 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 // Determine whether the item can be used by testing for resource consumption
const usage = item._getUsageUpdates({consumeRecharge, consumeResource, consumePowerSlot, consumeUsage, consumeQuantity}); const usage = item._getUsageUpdates({consumeRecharge, consumeResource, consumePowerSlot, consumeUsage, consumeQuantity});
if ( !usage ) return; if ( !usage ) return;
const {actorUpdates, itemUpdates, resourceUpdates} = usage; const {actorUpdates, itemUpdates, resourceUpdates} = usage;
// Commit pending data updates // Commit pending data updates
@ -490,17 +520,53 @@ export default class Item5e extends Item {
if ( canConsume === false ) return false; if ( canConsume === false ) return false;
} }
// Consume Power Slots // Consume Power Slots and Force/Tech Points
if ( consumePowerSlot ) { if ( consumePowerSlot ) {
const level = this.actor?.data.data.powers[consumePowerSlot]; const level = this.actor?.data.data.powers[consumePowerSlot];
const powers = Number(level?.value ?? 0); const fp = this.actor.data.data.attributes.force.points;
if ( powers === 0 ) { const tp = this.actor.data.data.attributes.tech.points;
const label = game.i18n.localize(consumePowerSlot === "pact" ? "SW5E.PowerProgPact" : `SW5E.PowerLevel${id.level}`); const powerCost = id.level + 1;
ui.notifications.warn(game.i18n.format("SW5E.PowerCastNoSlots", {name: this.name, level: label})); const innatePower = this.actor.data.data.attributes.powercasting === 'innate';
return false; 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 // Consume Limited Usage
if ( consumeUsage ) { 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 * @private
*/ */
_lootChatData(data, labels, props) { _lootChatData(data, labels, props) {

View file

@ -1,5 +1,5 @@
import TraitSelector from "../apps/trait-selector.js"; 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 * Override and extend the core ItemSheet implementation to handle specific item types
@ -10,8 +10,8 @@ export default class ItemSheet5e extends ItemSheet {
super(...args); super(...args);
// Expand the default size of the class sheet // Expand the default size of the class sheet
if ( this.object.data.type === "class" ) { if (this.object.data.type === "class") {
this.options.width = this.position.width = 600; this.options.width = this.position.width = 600;
this.options.height = this.position.height = 680; this.options.height = this.position.height = 680;
} }
} }
@ -19,14 +19,14 @@ export default class ItemSheet5e extends ItemSheet {
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */ /** @override */
static get defaultOptions() { static get defaultOptions() {
return mergeObject(super.defaultOptions, { return mergeObject(super.defaultOptions, {
width: 560, width: 560,
height: 400, height: 400,
classes: ["sw5e", "sheet", "item"], classes: ["sw5e", "sheet", "item"],
resizable: true, resizable: true,
scrollY: [".tab.details"], 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 // Potential consumption targets
data.abilityConsumptionTargets = this._getItemConsumptionTargets(data.item); data.abilityConsumptionTargets = this._getItemConsumptionTargets(data.item);
// Action Details // Action Detail
data.hasAttackRoll = this.item.hasAttack; data.hasAttackRoll = this.item.hasAttack;
data.isHealing = data.item.data.actionType === "heal"; data.isHealing = data.item.data.actionType === "heal";
data.isFlatDC = getProperty(data.item.data, "save.scaling") === "flat"; data.isFlatDC = getProperty(data.item.data, "save.scaling") === "flat";
data.isLine = ["line", "wall"].includes(data.item.data.target?.type); data.isLine = ["line", "wall"].includes(data.item.data.target?.type);
// Original maximum uses formula // 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 // Vehicles
data.isCrewed = data.item.data.activation?.type === 'crew'; data.isCrewed = data.item.data.activation?.type === "crew";
data.isMountable = this._isItemMountable(data.item); data.isMountable = this._isItemMountable(data.item);
// Prepare Active Effects // Prepare Active Effects
@ -83,22 +83,25 @@ export default class ItemSheet5e extends ItemSheet {
*/ */
_getItemConsumptionTargets(item) { _getItemConsumptionTargets(item) {
const consume = item.data.consume || {}; const consume = item.data.consume || {};
if ( !consume.type ) return []; if (!consume.type) return [];
const actor = this.item.actor; const actor = this.item.actor;
if ( !actor ) return {}; if (!actor) return {};
// Ammunition // Ammunition
if ( consume.type === "ammo" ) { if (consume.type === "ammo") {
return actor.itemTypes.consumable.reduce((ammo, i) => { return actor.itemTypes.consumable.reduce(
if ( i.data.data.consumableType === "ammo" ) { (ammo, i) => {
ammo[i.id] = `${i.name} (${i.data.data.quantity})`; if (i.data.data.consumableType === "ammo") {
} ammo[i.id] = `${i.name} (${i.data.data.quantity})`;
return ammo; }
}, {[item._id]: `${item.name} (${item.data.quantity})`}); return ammo;
},
{ [item._id]: `${item.name} (${item.data.quantity})` }
);
} }
// Attributes // Attributes
else if ( consume.type === "attribute" ) { else if (consume.type === "attribute") {
const attributes = Object.values(CombatTrackerConfig.prototype.getAttributeChoices())[0]; // Bit of a hack const attributes = Object.values(CombatTrackerConfig.prototype.getAttributeChoices())[0]; // Bit of a hack
return attributes.reduce((obj, a) => { return attributes.reduce((obj, a) => {
obj[a] = a; obj[a] = a;
@ -107,9 +110,9 @@ export default class ItemSheet5e extends ItemSheet {
} }
// Materials // Materials
else if ( consume.type === "material" ) { else if (consume.type === "material") {
return actor.items.reduce((obj, i) => { 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})`; obj[i.id] = `${i.name} (${i.data.data.quantity})`;
} }
return obj; return obj;
@ -117,25 +120,24 @@ export default class ItemSheet5e extends ItemSheet {
} }
// Charges // Charges
else if ( consume.type === "charges" ) { else if (consume.type === "charges") {
return actor.items.reduce((obj, i) => { return actor.items.reduce((obj, i) => {
// Limited-use items // Limited-use items
const uses = i.data.data.uses || {}; const uses = i.data.data.uses || {};
if ( uses.per && uses.max ) { if (uses.per && uses.max) {
const label = uses.per === "charges" ? const label =
` (${game.i18n.format("SW5E.AbilityUseChargesLabel", {value: uses.value})})` : uses.per === "charges"
` (${game.i18n.format("SW5E.AbilityUseConsumableLabel", {max: uses.max, per: uses.per})})`; ? ` (${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; obj[i.id] = i.name + label;
} }
// Recharging items // Recharging items
const recharge = i.data.data.recharge || {}; 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; return obj;
}, {}) }, {});
} } else return {};
else return {};
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
@ -146,13 +148,11 @@ export default class ItemSheet5e extends ItemSheet {
* @private * @private
*/ */
_getItemStatus(item) { _getItemStatus(item) {
if ( item.type === "power" ) { if (item.type === "power") {
return CONFIG.SW5E.powerPreparationModes[item.data.preparation]; 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"); 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"); return game.i18n.localize(item.data.proficient ? "SW5E.Proficient" : "SW5E.NotProficient");
} }
} }
@ -168,67 +168,50 @@ export default class ItemSheet5e extends ItemSheet {
const props = []; const props = [];
const labels = this.item.labels; const labels = this.item.labels;
if ( item.type === "weapon" ) { if (item.type === "weapon") {
props.push(...Object.entries(item.data.properties) props.push(
.filter(e => e[1] === true) ...Object.entries(item.data.properties)
.map(e => CONFIG.SW5E.weaponProperties[e[0]])); .filter((e) => e[1] === true)
} .map((e) => CONFIG.SW5E.weaponProperties[e[0]])
);
else if ( item.type === "power" ) { } else if (item.type === "power") {
props.push( props.push(
labels.components, labels.components,
labels.materials, labels.materials,
item.data.components.concentration ? game.i18n.localize("SW5E.Concentration") : null, item.data.components.concentration ? game.i18n.localize("SW5E.Concentration") : null,
item.data.components.ritual ? game.i18n.localize("SW5E.Ritual") : 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(CONFIG.SW5E.equipmentTypes[item.data.armor.type]);
props.push(labels.armor); 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 // Action type
if ( item.data.actionType ) { if (item.data.actionType) {
props.push(CONFIG.SW5E.itemActionTypes[item.data.actionType]); props.push(CONFIG.SW5E.itemActionTypes[item.data.actionType]);
} }
// Action usage // Action usage
if ( (item.type !== "weapon") && item.data.activation && !isObjectEmpty(item.data.activation) ) { if (item.type !== "weapon" && item.data.activation && !isObjectEmpty(item.data.activation)) {
props.push( props.push(labels.activation, labels.range, labels.target, labels.duration);
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) { _isItemMountable(item) {
const data = item.data; const data = item.data;
return (item.type === 'weapon' && data.weaponType === 'siege') return (
|| (item.type === 'equipment' && data.armor.type === 'vehicle'); (item.type === "weapon" && data.weaponType === "siege") ||
(item.type === "equipment" && data.armor.type === "vehicle")
);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */ /** @override */
setPosition(position={}) { setPosition(position = {}) {
if ( !(this._minimized || position.height) ) { if (!(this._minimized || position.height)) {
position.height = (this._tabs[0].active === "details") ? "auto" : this.options.height; position.height = this._tabs[0].active === "details" ? "auto" : this.options.height;
} }
return super.setPosition(position); return super.setPosition(position);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/* Form Submission */ /* Form Submission */
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */ /** @override */
_getSubmitData(updateData={}) { _getSubmitData(updateData = {}) {
// Create the expanded update data object // 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(); let data = fd.toObject();
if ( updateData ) data = mergeObject(data, updateData); if (updateData) data = mergeObject(data, updateData);
else data = expandObject(data); else data = expandObject(data);
// Handle Damage array // Handle Damage array
const damage = data.data?.damage; 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 the flattened submission data
return flattenObject(data); return flattenObject(data);
@ -283,12 +267,15 @@ export default class ItemSheet5e extends ItemSheet {
/** @override */ /** @override */
activateListeners(html) { activateListeners(html) {
super.activateListeners(html); super.activateListeners(html);
if ( this.isEditable ) { if (this.isEditable) {
html.find(".damage-control").click(this._onDamageControl.bind(this)); html.find(".damage-control").click(this._onDamageControl.bind(this));
html.find('.trait-selector.class-skills').click(this._onConfigureClassSkills.bind(this)); html.find(".trait-selector.class-skills").click(this._onConfigureClassSkills.bind(this));
html.find(".effect-control").click(ev => { 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.") if (this.item.isOwned)
onManageActiveEffect(ev, this.item) 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; const a = event.currentTarget;
// Add new damage component // Add new damage component
if ( a.classList.contains("add-damage") ) { if (a.classList.contains("add-damage")) {
await this._onSubmit(event); // Submit any unsaved changes await this._onSubmit(event); // Submit any unsaved changes
const damage = this.item.data.data.damage; 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 // Remove a damage component
if ( a.classList.contains("delete-damage") ) { if (a.classList.contains("delete-damage")) {
await this._onSubmit(event); // Submit any unsaved changes await this._onSubmit(event); // Submit any unsaved changes
const li = a.closest(".damage-part"); const li = a.closest(".damage-part");
const damage = duplicate(this.item.data.data.damage); const damage = duplicate(this.item.data.data.damage);
damage.parts.splice(Number(li.dataset.damagePart), 1); 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, name: a.dataset.target,
title: label.innerText, title: label.innerText,
choices: Object.entries(CONFIG.SW5E.skills).reduce((obj, e) => { 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; return obj;
}, {}), }, {}),
minimum: skills.number, minimum: skills.number,
maximum: skills.number maximum: skills.number
}).render(true) }).render(true);
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** @override */ /** @override */
async _onSubmit(...args) { 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); await super._onSubmit(...args);
} }
} }

View file

@ -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}); 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 // Migrate World Actors
for ( let a of game.actors.entities ) { for await ( let a of game.actors.entities ) {
try { 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) ) { if ( !isObjectEmpty(updateData) ) {
console.log(`Migrating Actor entity ${a.name}`); console.log(`Migrating Actor entity ${a.name}`);
await a.update(updateData, {enforceTypes: false}); await a.update(updateData, {enforceTypes: false});
@ -36,7 +37,7 @@ export const migrateWorld = async function() {
// Migrate Actor Override Tokens // Migrate Actor Override Tokens
for ( let s of game.scenes.entities ) { for ( let s of game.scenes.entities ) {
try { try {
const updateData = migrateSceneData(s.data); const updateData = await migrateSceneData(s.data);
if ( !isObjectEmpty(updateData) ) { if ( !isObjectEmpty(updateData) ) {
console.log(`Migrating Scene entity ${s.name}`); console.log(`Migrating Scene entity ${s.name}`);
await s.update(updateData, {enforceTypes: false}); await s.update(updateData, {enforceTypes: false});
@ -79,18 +80,18 @@ export const migrateCompendium = async function(pack) {
const content = await pack.getContent(); const content = await pack.getContent();
// Iterate over compendium entries - applying fine-tuned migration functions // Iterate over compendium entries - applying fine-tuned migration functions
for ( let ent of content ) { for await ( let ent of content ) {
let updateData = {}; let updateData = {};
try { try {
switch (entity) { switch (entity) {
case "Actor": case "Actor":
updateData = migrateActorData(ent.data); updateData = await migrateActorData(ent.data);
break; break;
case "Item": case "Item":
updateData = migrateItemData(ent.data); updateData = migrateItemData(ent.data);
break; break;
case "Scene": case "Scene":
updateData = migrateSceneData(ent.data); updateData = await migrateSceneData(ent.data);
break; break;
} }
if ( isObjectEmpty(updateData) ) continue; if ( isObjectEmpty(updateData) ) continue;
@ -123,7 +124,7 @@ export const migrateCompendium = async function(pack) {
* @param {object} actor The actor data object to update * @param {object} actor The actor data object to update
* @return {Object} The updateData to apply * @return {Object} The updateData to apply
*/ */
export const migrateActorData = function(actor) { export const migrateActorData = async function(actor) {
const updateData = {}; const updateData = {};
// Actor Data Updates // Actor Data Updates
@ -131,27 +132,40 @@ export const migrateActorData = function(actor) {
_migrateActorSenses(actor, updateData); _migrateActorSenses(actor, updateData);
// Migrate Owned Items // Migrate Owned Items
if ( !actor.items ) return updateData; if ( !!actor.items ) {
let hasItemUpdates = false; let hasItemUpdates = false;
const items = actor.items.map(i => { const items = await actor.items.reduce(async (memo, i) => {
const results = await memo;
// Migrate the Owned Item // Migrate the Owned Item
let itemUpdate = migrateItemData(i); let itemUpdate = await migrateActorItemData(i, actor);
// Prepared, Equipped, and Proficient for NPC actors // Prepared, Equipped, and Proficient for NPC actors
if ( actor.type === "npc" ) { if ( actor.type === "npc" ) {
if (getProperty(i.data, "preparation.prepared") === false) itemUpdate["data.preparation.prepared"] = true; 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, "equipped") === false) itemUpdate["data.equipped"] = true;
if (getProperty(i.data, "proficient") === false) itemUpdate["data.proficient"] = true; if (getProperty(i.data, "proficient") === false) itemUpdate["data.proficient"] = true;
} }
// Update the Owned Item // Update the Owned Item
if ( !isObjectEmpty(itemUpdate) ) { if ( !isObjectEmpty(itemUpdate) ) {
hasItemUpdates = true; hasItemUpdates = true;
return mergeObject(i, itemUpdate, {enforceTypes: false, inplace: false}); console.log(`Migrating Actor ${actor.name}'s ${i.name}`);
} else return i; return [...results, mergeObject(i, itemUpdate, {enforceTypes: false, inplace: false})];
}); } else return [...results, i];
if ( hasItemUpdates ) updateData.items = items; }, []);
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; return updateData;
}; };
@ -191,22 +205,38 @@ function cleanActorData(actorData) {
*/ */
export const migrateItemData = function(item) { export const migrateItemData = function(item) {
const updateData = {}; const updateData = {};
_migrateItemClassPowerCasting(item, updateData);
_migrateItemAttunement(item, updateData); _migrateItemAttunement(item, updateData);
return 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 * 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 * Return an Object of updateData to be applied
* @param {Object} scene The Scene data to Update * @param {Object} scene The Scene data to Update
* @return {Object} The updateData to apply * @return {Object} The updateData to apply
*/ */
export const migrateSceneData = function(scene) { export const migrateSceneData = async function(scene) {
const tokens = duplicate(scene.tokens); const tokens = duplicate(scene.tokens);
return { return {
tokens: tokens.map(t => { tokens: await Promise.all(tokens.map(async (t) => {
if (!t.actorId || t.actorLink || !t.actorData.data) { if (!t.actorId || t.actorLink || !t.actorData.data) {
t.actorData = {}; t.actorData = {};
return t; return t;
@ -216,11 +246,11 @@ export const migrateSceneData = function(scene) {
t.actorId = null; t.actorId = null;
t.actorData = {}; t.actorData = {};
} else if ( !t.actorLink ) { } else if ( !t.actorLink ) {
const updateData = migrateActorData(token.data.actorData); const updateData = await migrateActorData(token.data.actorData);
t.actorData = mergeObject(token.data.actorData, updateData); t.actorData = mergeObject(token.data.actorData, updateData);
} }
return t; return t;
}) }))
}; };
}; };
@ -228,6 +258,72 @@ export const migrateSceneData = function(scene) {
/* Low level migration utilities /* 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 * Migrate the actor speed string to movement object
* @private * @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 * Migrate the actor traits.senses string to attributes.senses object
* @private * @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 * Delete the old data.attuned boolean
* @private * @private
@ -303,7 +535,6 @@ function _migrateItemAttunement(item, updateData) {
return updateData; return updateData;
} }
/* -------------------------------------------- */ /* -------------------------------------------- */
/** /**

View file

@ -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-active-effects.html",
"systems/sw5e/templates/actors/newActor/parts/swalt-features.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-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-resources.html",
"systems/sw5e/templates/actors/newActor/parts/swalt-traits.html", "systems/sw5e/templates/actors/newActor/parts/swalt-traits.html",

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Before After
Before After

Some files were not shown because too many files have changed in this diff Show more