Merge pull request #77 from unrealkakeman89/Develop-VJ

Update Core to 1.2
This commit is contained in:
CK 2021-01-04 16:16:36 -05:00 committed by GitHub
commit 4cf9ece35c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 1975 additions and 430 deletions

View file

@ -47,7 +47,7 @@
"SW5E.AbilityUseHint": "Configure how you would like to use the {name} {type}.",
"SW5E.AbilityUseUnavailableHint": "There are no uses of this item remaining!",
"SW5E.AbilityUseChargedHint": "This {type} is charged and ready to use!",
"SW5E.AbilityUseRechargedHint": "This {type} is depleted and must be recharged!",
"SW5E.AbilityUseRechargeHint": "This {type} is depleted and must be recharged!",
"SW5E.AbilityUseNormalHint": "This {type} has {value} of {max} uses per {per} remaining.",
"SW5E.AbilityUseConsumableChargeHint": "Using this {type} will consume 1 charge of {value} remaining.",
"SW5E.AbilityUseConsumableQuantityHint": "Using this {type} will consume 1 quantity of {quantity} remaining",
@ -85,6 +85,11 @@
"SW5E.AlignmentBN": "Balanced Neutral",
"SW5E.Archetypes": "Archetypes",
"SW5E.Appearance": "Appearance",
"SW5E.Attunement": "Attunement",
"SW5E.AttunementNone": "Attunement Not Required",
"SW5E.AttunementRequired": "Attunement Required",
"SW5E.AttunementAttuned": "Attuned",
"SW5E.Attuned": "Attuned",
"SW5E.ArmorClass": "Armor Class",
"SW5E.AC": "AC",
"SW5E.ArmorProperties": "Armor Properties",
@ -122,7 +127,6 @@
"SW5E.AttackPl": "Attacks",
"SW5E.AttackRoll": "Attack Roll",
"SW5E.Attributes": "Attributes",
"SW5E.Attuned": "Attuned",
"SW5E.Background": "Background",
"SW5E.Biography": "Biography",
"SW5E.Bonds": "Bonds",
@ -189,7 +193,8 @@
"SW5E.ConsumeWarningNoSource": "The designated {type} source that {name} consumes no longer exists!",
"SW5E.ConsumeWarningNoQuantity": "{name} has run out of its designated {type}!",
"SW5E.ConsumeWarningZeroAttribute": "{name} has run out of its designated attribute resource pool!",
"SW5E.ConsumeResource": "Consume Resource?",
"SW5E.ConsumeRecharge": "Consume Recharge?",
"SW5E.ConsumableAmmunition": "Ammunition",
"SW5E.ConsumableFood": "Food",
"SW5E.ConsumablePoison": "Poison",
@ -390,8 +395,6 @@
"SW5E.FlagsReliableTalentHint": "Rogues Reliable Talent Feature.",
"SW5E.FlagsRemarkableAthlete": "Remarkable Athlete.",
"SW5E.FlagsRemarkableAthleteHint": "Half-Proficiency (rounded-up) to physical Ability Checks and Initiative.",
"SW5E.FlagsCritThreshold": "Critical Hit Threshold",
"SW5E.FlagsCritThresholdHint": "Allow for expanded critical range; for example Improved or Superior Critical",
"SW5E.FlagsWeaponCritThreshold": "Weapon Critical Hit Threshold",
"SW5E.FlagsWeaponCritThresholdHint": "An expanded critical hit threshold for weapon attacks.",
"SW5E.FlagsPowerCritThreshold": "Power Critical Hit Threshold",
@ -649,7 +652,7 @@
"SW5E.RollMode": "Roll Mode",
"SW5E.RollSituationalBonus": "Situational Bonus?",
"SW5E.Save": "Save",
"SW5E.Movement": "Movement",
"SW5E.MovementConfig": "Configure Movement Speed",
"SW5E.MovementConfigHint": "Configure the movement speed and special movement attributes of this creature.",
"SW5E.MovementWalk": "Walk",
@ -659,7 +662,14 @@
"SW5E.MovementFly": "Fly",
"SW5E.MovementSwim": "Swim",
"SW5E.MovementUnits": "Units",
"SW5E.Senses": "Senses",
"SW5E.SensesConfig": "Configure Senses",
"SW5E.SensesConfigHint": "Configure any special sensory perception abilities that this actor possesses.",
"SW5E.SenseDarkvision": "Darkvision",
"SW5E.SenseBlindsight": "Blindsight",
"SW5E.SenseTremorsense": "Tremorsense",
"SW5E.SenseTruesight": "Truesight",
"SW5E.SenseSpecial": "Special Senses",
"SW5E.SheetClassCharacter": "Default Character Sheet",
"SW5E.SheetClassCharacterOld": "Old Character Sheet",
"SW5E.SheetClassNPC": "Default NPC Sheet",
@ -728,7 +738,7 @@
"SW5E.PowerAtWill": "At-Will",
"SW5E.PowerCastConsume": "Consume Power Slot?",
"SW5E.PowerCastHint": "Configure how you would like to cast the",
"SW5E.PowerCastNoSlots": "You have no available power slots to cast this power",
"SW5E.PowerCastNoSlots": "You have no available {level} power slots with which to cast {name}",
"SW5E.PowerCastUpcast": "Cast at Level",
"SW5E.PowercasterLevel": "Powercaster Level",
"SW5E.PowerCastingHeader": "Power Casting",

View file

@ -69,14 +69,31 @@
line-height: 30px;
}
}
// Movement Configuration
.movement {
h4.attribute-name {
position: relative;
}
.config-button {
position: absolute;
display: none;
right: 0;
top: 1px;
font-size: 12px;
font-weight: normal;
}
&:hover .config-button {
display: block;
}
}
.attributes {
// Temporary HP
input.temphp {
width: 48%;
}
}
}
/* ----------------------------------------- */
/* General Styles */
@ -341,7 +358,7 @@
margin: 0 0 3px 0;
justify-content: space-between;
}
.configure-flags {
.config-button {
flex: 1;
}
@ -428,12 +445,8 @@
&.rollable .item-image:hover {
background-image: url("../../icons/svg/d20-black.svg") !important;
}
i.attuned {
color: @colorTan;
}
h4 {
font-size: 14px;
}
i.attuned { color: @colorTan; }
i.not-attuned { color: @colorCrimson; }
}
// Item uses
@ -474,6 +487,7 @@
overflow: hidden;
&:last-child { border-right: none; }
&.item-action {flex: 0 0 100px}
&.attunement {flex: 0 0 24px}
}
.item-weight {
flex: 0 0 60px;
@ -576,26 +590,23 @@
.powercasting-ability {
flex: 0 0 240px;
margin: 0;
input, span {
flex: 0 0 32px;
label, span {
flex: none;
}
input {
flex: 0 0 28px;
text-align: center;
}
select {
margin: 0 5px;
flex: 0 0 150px;
}
h3.power-dc {
flex: 1;
text-align: right;
flex: 0 0 120px;
}
}
.power-slots,
.power-comps {
flex: 0 0 75px;
padding-right: 5px;
text-align: right;
flex: none;
padding: 0 5px;
font-size: 12px;
color: @colorTan;
border-right: 1px solid @colorFaint;
@ -612,9 +623,10 @@
}
}
.power-uses {
padding-right: 8px;
text-align: right !important;
.powerbook .power-uses {
padding-right: 5px;
text-align: right;
color: @colorTan;
}
.power-school, .power-action, .power-target {
@ -663,5 +675,6 @@
padding-right: 8px;
margin-bottom: 4px;
overflow-y: auto;
scrollbar-width: thin;
}
}

View file

@ -178,11 +178,14 @@
background: transparent;
}
// Rollable Links
// Rollable Titles
.editable .rollable:hover {
cursor: pointer;
}
.editable h4.rollable:hover,
.editable .rollable:hover > h4 {
color: #000;
text-shadow: 0 0 10px red;
cursor: pointer;
}
// Separators
@ -306,6 +309,7 @@
/* ----------------------------------------- */
.filter-list {
align-items: center;
list-style: none;
margin: 0;
padding: 0;
@ -382,6 +386,30 @@
padding: 0;
}
// Item Name
.item-name {
flex: 2;
margin: 0;
overflow: hidden;
font-size: 13px;
text-align: left;
align-items: center;
h3, h4 {
margin: 0;
white-space: nowrap;
overflow-x: hidden;
}
}
// Control Buttons
.item-controls {
flex: 0 0 60px;
justify-content: space-between;
a {
font-size: 12px;
text-align: center;
}
}
// Individual Item
.item {
align-items: center;
@ -419,30 +447,11 @@
font-size: 12px;
text-align: center;
}
.item-name {
h3 {
padding-left: 5px;
//.modesto();
font-size: 16px;
}
}
// Item Name
.item-name {
flex: 2;
margin: 0;
overflow: hidden;
font-size: 13px;
text-align: left;
align-items: center;
}
// Control Buttons
.item-controls {
flex: 0 0 60px;
justify-content: space-between;
a {
font-size: 12px;
text-align: center;
font-size: 16px;
}
}
}

View file

@ -73,7 +73,7 @@
span {
border-right: 2px groove #FFF;
padding: 0 5px 0 0;
padding: 0 3px 0 0;
font-size: 10px;
&:last-child {

View file

@ -31,11 +31,4 @@
.summary {
font-size: 18px;
}
.powercasting-ability {
label {
flex: none;
}
}
}

View file

@ -6,33 +6,3 @@
@import "character.less";
@import "npc.less";
@import "vehicle.less";
// TODO: Remove number styling after 0.7.x
input[type="number"] {
width: calc(100% - 2px);
min-width: 20px;
height: 26px;
background: rgba(0, 0, 0, 0.05);
padding: 1px 3px;
margin: 0;
color: #191813;
font-family: inherit;
font-size: inherit;
text-align: inherit;
line-height: inherit;
border: 1px solid #7a7971;
border-radius: 3px;
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
-moz-appearance: textfield;
&:focus {
box-shadow: 0 0 5px red;
}
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
}

View file

@ -1,8 +1,6 @@
import { d20Roll, damageRoll } from "../dice.js";
import ShortRestDialog from "../apps/short-rest.js";
import LongRestDialog from "../apps/long-rest.js";
import AbilityUseDialog from "../apps/ability-use-dialog.js";
import AbilityTemplate from "../pixi/ability-template.js";
import {SW5E} from '../config.js';
/**
@ -163,8 +161,8 @@ export default class Actor5e extends Actor {
}
// Acquire archetype features
const subConfig = clsConfig.archetypes[archetypeName] || {};
for ( let [l, f] of Object.entries(subConfig.features || {}) ) {
const archConfig = clsConfig.archetypes[archetypeName] || {};
for ( let [l, f] of Object.entries(archConfig.features || {}) ) {
l = parseInt(l);
if ( (l <= level) && (l > priorLevel) ) ids = ids.concat(f);
}
@ -207,7 +205,7 @@ export default class Actor5e extends Actor {
const updateData = expandObject(u);
const config = {
className: updateData.name || item.data.name,
archetypeName: updateData.data.archetype || item.data.data.archetype,
archetypeName: getProperty(updateData, "data.archetype") || item.data.data.archetype,
level: getProperty(updateData, "data.levels"),
priorLevel: item ? item.data.data.levels : 0
}
@ -356,16 +354,8 @@ export default class Actor5e extends Actor {
const data = actorData.data;
data.attributes.powerdc = data.attributes.powercasting ? data.abilities[data.attributes.powercasting].dc : 10;
// Apply powercasting DC to any power items which use it
for ( let i of this.items ) {
const save = i.data.data.save;
if ( save?.ability ) {
if ( save.scaling === "power" ) save.dc = data.attributes.powerdc;
else if ( save.scaling !== "flat" ) save.dc = data.abilities[save.scaling]?.dc ?? 10;
const ability = CONFIG.SW5E.abilities[save.ability];
i.labels.save = game.i18n.format("SW5E.SaveDC", {dc: save.dc || "", ability});
}
}
// Compute ability save DCs that depend on the calling actor
this.items.forEach(i => i.getSaveDC());
}
/* -------------------------------------------- */
@ -546,20 +536,69 @@ export default class Actor5e extends Actor {
/* -------------------------------------------- */
/** @override */
async createOwnedItem(itemData, options) {
async createEmbeddedEntity(embeddedName, itemData, options={}) {
// Assume NPCs are always proficient with weapons and always have powers prepared
if ( !this.hasPlayerOwner ) {
let t = itemData.type;
// Pre-creation steps for owned items
if ( embeddedName === "OwnedItem" ) this._preCreateOwnedItem(itemData, options);
// Standard embedded entity creation
return super.createEmbeddedEntity(embeddedName, itemData, options);
}
/* -------------------------------------------- */
/**
* A temporary shim function which will eventually (in core fvtt version 0.8.0+) be migrated to the new abstraction layer
* @param itemData
* @param options
* @private
*/
_preCreateOwnedItem(itemData, options) {
if ( this.data.type === "vehicle" ) return;
const isNPC = this.data.type === 'npc';
let initial = {};
if ( t === "weapon" ) initial["data.proficient"] = true;
if ( ["weapon", "equipment"].includes(t) ) initial["data.equipped"] = true;
if ( t === "power" ) initial["data.prepared"] = true;
switch ( itemData.type ) {
case "weapon":
initial["data.equipped"] = isNPC; // NPCs automatically equip weapons
let hasWeaponProf = isNPC; // NPCs automatically have weapon proficiency
if ( !isNPC ) {
const weaponProf = {
"natural": true,
"simpleVW": "sim",
"simpleB": "sim",
"simpleLW": "sim",
"martialVW": "mar",
"martialB": "mar",
"martialLW": "mar"
}[itemData.data?.weaponType];
const actorWeaponProfs = this.data.data.traits?.weaponProf?.value || [];
hasWeaponProf = (weaponProf === true) || actorWeaponProfs.includes(weaponProf);
}
initial["data.proficient"] = hasWeaponProf;
break;
case "equipment":
initial["data.equipped"] = isNPC; // NPCs automatically equip equipment
let hasEquipmentProf = isNPC; // NPCs automatically have equipment proficiency
if ( !isNPC ) {
const armorProf = {
"natural": true,
"clothing": true,
"light": "lgt",
"medium": "med",
"heavy": "hvy",
"shield": "shl"
}[itemData.data?.armor?.type];
const actorArmorProfs = this.data.data.traits?.armorProf?.value || [];
hasEquipmentProf = (armorProf === true) || actorArmorProfs.includes(armorProf);
}
initial["data.proficient"] = hasEquipmentProf;
break;
case "power":
initial["data.prepared"] = true; // NPCs automatically prepare powers
break;
}
mergeObject(itemData, initial);
}
return super.createOwnedItem(itemData, options);
}
/* -------------------------------------------- */
/* Gameplay Mechanics */
@ -600,77 +639,16 @@ export default class Actor5e extends Actor {
"data.attributes.hp.temp": tmp - dt,
"data.attributes.hp.value": dh
};
return this.update(updates);
}
/* -------------------------------------------- */
/**
* Cast a Power, consuming a power slot of a certain level
* @param {Item5e} item The power being cast by the actor
* @param {Event} event The originating user interaction which triggered the cast
*/
async usePower(item, {configureDialog=true}={}) {
if ( item.data.type !== "power" ) throw new Error("Wrong Item type");
const itemData = item.data.data;
// Configure powercasting data
let lvl = itemData.level;
const usesSlots = (lvl > 0) && CONFIG.SW5E.powerUpcastModes.includes(itemData.preparation.mode);
const limitedUses = !!itemData.uses.per;
let consumeSlot = `power${lvl}`;
let consumeUse = false;
let placeTemplate = false;
// Configure power slot consumption and measured template placement from the form
if ( configureDialog && (usesSlots || item.hasAreaTarget || limitedUses) ) {
const usage = await AbilityUseDialog.create(item);
if ( usage === null ) return;
// Determine consumption preferences
consumeSlot = Boolean(usage.get("consumeSlot"));
consumeUse = Boolean(usage.get("consumeUse"));
placeTemplate = Boolean(usage.get("placeTemplate"));
// Determine the cast power level
const isPact = usage.get('level') === 'pact';
const lvl = isPact ? this.data.data.powers.pact.level : parseInt(usage.get("level"));
if ( lvl !== item.data.data.level ) {
const upcastData = mergeObject(item.data, {"data.level": lvl}, {inplace: false});
item = item.constructor.createOwned(upcastData, this);
}
// Denote the power slot being consumed
if ( consumeSlot ) consumeSlot = isPact ? "pact" : `power${lvl}`;
}
// Update Actor data
if ( usesSlots && consumeSlot && (lvl > 0) ) {
const slots = parseInt(this.data.data.powers[consumeSlot]?.value);
if ( slots === 0 || Number.isNaN(slots) ) {
return ui.notifications.error(game.i18n.localize("SW5E.PowerCastNoSlots"));
}
await this.update({
[`data.powers.${consumeSlot}.value`]: Math.max(slots - 1, 0)
});
}
// Update Item data
if ( limitedUses && consumeUse ) {
const uses = parseInt(itemData.uses.value || 0);
if ( uses <= 0 ) ui.notifications.warn(game.i18n.format("SW5E.ItemNoUses", {name: item.name}));
await item.update({"data.uses.value": Math.max(parseInt(item.data.data.uses.value || 0) - 1, 0)})
}
// Initiate ability template placement workflow if selected
if ( placeTemplate && item.hasAreaTarget ) {
const template = AbilityTemplate.fromItem(item);
if ( template ) template.drawPreview();
if ( this.sheet.rendered ) this.sheet.minimize();
}
// Invoke the Item roll
return item.roll();
// Delegate damage application to a hook
// TODO replace this in the future with a better modifyTokenAttribute function in the core
const allowed = Hooks.call("modifyTokenAttribute", {
attribute: "attributes.hp",
value: amount,
isDelta: false,
isBar: true
}, updates);
return allowed !== false ? this.update(updates) : this;
}
/* -------------------------------------------- */
@ -989,7 +967,7 @@ export default class Actor5e extends Actor {
// Adjust actor data
await cls.update({"data.hitDiceUsed": cls.data.data.hitDiceUsed + 1});
const hp = this.data.data.attributes.hp;
const dhp = Math.min(hp.max - hp.value, roll.total);
const dhp = Math.min(hp.max + (hp.tempmax ?? 0) - hp.value, roll.total);
await this.update({"data.attributes.hp.value": hp.value + dhp});
return roll;
}
@ -1437,4 +1415,18 @@ export default class Actor5e extends Actor {
console.warn(`The Actor5e#getPowerDC(ability) method has been deprecated in favor of Actor5e#data.data.abilities[ability].dc`);
return this.data.data.abilities[ability]?.dc;
}
/* -------------------------------------------- */
/**
* Cast a Power, consuming a power slot of a certain level
* @param {Item5e} item The power being cast by the actor
* @param {Event} event The originating user interaction which triggered the cast
* @deprecated since sw5e 1.2.0
*/
async usePower(item, {configureDialog=true}={}) {
console.warn(`The Actor5e#usePower method has been deprecated in favor of Item5e#roll`);
if ( item.data.type !== "power" ) throw new Error("Wrong Item type");
return item.roll();
}
}

View file

@ -0,0 +1,833 @@
import Item5e from "../../../item/entity.js";
import TraitSelector from "../../../apps/trait-selector.js";
import ActorSheetFlags from "../../../apps/actor-flags.js";
import ActorMovementConfig from "../../../apps/movement-config.js";
import ActorSensesConfig from "../../../apps/senses-config.js";
import {SW5E} from '../../../config.js';
import {onManageActiveEffect, prepareActiveEffectCategories} from "../../../effects.js";
/**
* Extend the basic ActorSheet class to suppose SW5e-specific logic and functionality.
* This sheet is an Abstract layer which is not used.
* @extends {ActorSheet}
*/
export default class ActorSheet5e extends ActorSheet {
constructor(...args) {
super(...args);
/**
* Track the set of item filters which are applied
* @type {Set}
*/
this._filters = {
inventory: new Set(),
powerbook: new Set(),
features: new Set(),
effects: new Set()
};
}
/* -------------------------------------------- */
/** @override */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
scrollY: [
".inventory .inventory-list",
".features .inventory-list",
".powerbook .inventory-list",
".effects .inventory-list"
],
tabs: [{navSelector: ".tabs", contentSelector: ".sheet-body", initial: "description"}]
});
}
/* -------------------------------------------- */
/** @override */
get template() {
if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/newActor/expanded-limited-sheet.html";
return `systems/sw5e/templates/actors/newActor/${this.actor.data.type}-sheet.html`;
}
/* -------------------------------------------- */
/** @override */
getData() {
// Basic data
let isOwner = this.entity.owner;
const data = {
owner: isOwner,
limited: this.entity.limited,
options: this.options,
editable: this.isEditable,
cssClass: isOwner ? "editable" : "locked",
isCharacter: this.entity.data.type === "character",
isNPC: this.entity.data.type === "npc",
isVehicle: this.entity.data.type === 'vehicle',
config: CONFIG.SW5E,
};
// The Actor and its Items
data.actor = duplicate(this.actor.data);
data.items = this.actor.items.map(i => {
i.data.labels = i.labels;
return i.data;
});
data.items.sort((a, b) => (a.sort || 0) - (b.sort || 0));
data.data = data.actor.data;
data.labels = this.actor.labels || {};
data.filters = this._filters;
// Ability Scores
for ( let [a, abl] of Object.entries(data.actor.data.abilities)) {
abl.icon = this._getProficiencyIcon(abl.proficient);
abl.hover = CONFIG.SW5E.proficiencyLevels[abl.proficient];
abl.label = CONFIG.SW5E.abilities[a];
}
// Skills
if (data.actor.data.skills) {
for ( let [s, skl] of Object.entries(data.actor.data.skills)) {
skl.ability = CONFIG.SW5E.abilityAbbreviations[skl.ability];
skl.icon = this._getProficiencyIcon(skl.value);
skl.hover = CONFIG.SW5E.proficiencyLevels[skl.value];
skl.label = CONFIG.SW5E.skills[s];
}
}
// Movement speeds
data.movement = this._getMovementSpeed(data.actor);
// Senses
data.senses = this._getSenses(data.actor);
// Update traits
this._prepareTraits(data.actor.data.traits);
// Prepare owned items
this._prepareItems(data);
// Prepare active effects
data.effects = prepareActiveEffectCategories(this.entity.effects);
// Return data to the sheet
return data
}
/* -------------------------------------------- */
/**
* Prepare the display of movement speed data for the Actor
* @param {object} actorData
* @returns {{primary: string, special: string}}
* @private
*/
_getMovementSpeed(actorData) {
const movement = actorData.data.attributes.movement || {};
const speeds = [
[movement.burrow, `${game.i18n.localize("SW5E.MovementBurrow")} ${movement.burrow}`],
[movement.climb, `${game.i18n.localize("SW5E.MovementClimb")} ${movement.climb}`],
[movement.fly, `${game.i18n.localize("SW5E.MovementFly")} ${movement.fly}` + (movement.hover ? ` (${game.i18n.localize("SW5E.MovementHover")})` : "")],
[movement.swim, `${game.i18n.localize("SW5E.MovementSwim")} ${movement.swim}`]
].filter(s => !!s[0]).sort((a, b) => b[0] - a[0]);
return {
primary: `${movement.walk || 0} ${movement.units}`,
special: speeds.length ? speeds.map(s => s[1]).join(", ") : ""
}
}
/* -------------------------------------------- */
_getSenses(actorData) {
const senses = actorData.data.attributes.senses || {};
const tags = {};
for ( let [k, label] of Object.entries(CONFIG.SW5E.senses) ) {
const v = senses[k] ?? 0
if ( v === 0 ) continue;
tags[k] = `${game.i18n.localize(label)} ${v} ${senses.units}`;
}
if ( !!senses.special ) tags["special"] = senses.special;
return tags;
}
/* -------------------------------------------- */
/**
* Prepare the data structure for traits data like languages, resistances & vulnerabilities, and proficiencies
* @param {object} traits The raw traits data object from the actor data
* @private
*/
_prepareTraits(traits) {
const map = {
"dr": CONFIG.SW5E.damageResistanceTypes,
"di": CONFIG.SW5E.damageResistanceTypes,
"dv": CONFIG.SW5E.damageResistanceTypes,
"ci": CONFIG.SW5E.conditionTypes,
"languages": CONFIG.SW5E.languages,
"armorProf": CONFIG.SW5E.armorProficiencies,
"weaponProf": CONFIG.SW5E.weaponProficiencies,
"toolProf": CONFIG.SW5E.toolProficiencies
};
for ( let [t, choices] of Object.entries(map) ) {
const trait = traits[t];
if ( !trait ) continue;
let values = [];
if ( trait.value ) {
values = trait.value instanceof Array ? trait.value : [trait.value];
}
trait.selected = values.reduce((obj, t) => {
obj[t] = choices[t];
return obj;
}, {});
// Add custom entry
if ( trait.custom ) {
trait.custom.split(";").forEach((c, i) => trait.selected[`custom${i+1}`] = c.trim());
}
trait.cssClass = !isObjectEmpty(trait.selected) ? "" : "inactive";
}
}
/* -------------------------------------------- */
/**
* Insert a power into the powerbook object when rendering the character sheet
* @param {Object} data The Actor data being prepared
* @param {Array} powers The power data being prepared
* @private
*/
_preparePowerbook(data, powers) {
const owner = this.actor.owner;
const levels = data.data.powers;
const powerbook = {};
// Define some mappings
const sections = {
"atwill": -20,
"innate": -10,
"pact": 0.5
};
// Label power slot uses headers
const useLabels = {
"-20": "-",
"-10": "-",
"0": "&infin;"
};
// Format a powerbook entry for a certain indexed level
const registerSection = (sl, i, label, {prepMode="prepared", value, max, override}={}) => {
powerbook[i] = {
order: i,
label: label,
usesSlots: i > 0,
canCreate: owner,
canPrepare: (data.actor.type === "character") && (i >= 1),
powers: [],
uses: useLabels[i] || value || 0,
slots: useLabels[i] || max || 0,
override: override || 0,
dataset: {"type": "power", "level": prepMode in sections ? 1 : i, "preparation.mode": prepMode},
prop: sl
};
};
// Determine the maximum power level which has a slot
const maxLevel = Array.fromRange(10).reduce((max, i) => {
if ( i === 0 ) return max;
const level = levels[`power${i}`];
if ( (level.max || level.override ) && ( i > max ) ) max = i;
return max;
}, 0);
// Level-based powercasters have cantrips and leveled slots
if ( maxLevel > 0 ) {
registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]);
for (let lvl = 1; lvl <= maxLevel; lvl++) {
const sl = `power${lvl}`;
registerSection(sl, lvl, CONFIG.SW5E.powerLevels[lvl], levels[sl]);
}
}
// Pact magic users have cantrips and a pact magic section
if ( levels.pact && levels.pact.max ) {
if ( !powerbook["0"] ) registerSection("power0", 0, CONFIG.SW5E.powerLevels[0]);
const l = levels.pact;
const config = CONFIG.SW5E.powerPreparationModes.pact;
registerSection("pact", sections.pact, config, {
prepMode: "pact",
value: l.value,
max: l.max,
override: l.override
});
}
// Iterate over every power item, adding powers to the powerbook by section
powers.forEach(power => {
const mode = power.data.preparation.mode || "prepared";
let s = power.data.level || 0;
const sl = `power${s}`;
// Specialized powercasting modes (if they exist)
if ( mode in sections ) {
s = sections[mode];
if ( !powerbook[s] ){
const l = levels[mode] || {};
const config = CONFIG.SW5E.powerPreparationModes[mode];
registerSection(mode, s, config, {
prepMode: mode,
value: l.value,
max: l.max,
override: l.override
});
}
}
// Sections for higher-level powers which the caster "should not" have, but power items exist for
else if ( !powerbook[s] ) {
registerSection(sl, s, CONFIG.SW5E.powerLevels[s], {levels: levels[sl]});
}
// Add the power to the relevant heading
powerbook[s].powers.push(power);
});
// Sort the powerbook by section level
const sorted = Object.values(powerbook);
sorted.sort((a, b) => a.order - b.order);
return sorted;
}
/* -------------------------------------------- */
/**
* Determine whether an Owned Item will be shown based on the current set of filters
* @return {boolean}
* @private
*/
_filterItems(items, filters) {
return items.filter(item => {
const data = item.data;
// Action usage
for ( let f of ["action", "bonus", "reaction"] ) {
if ( filters.has(f) ) {
if ((data.activation && (data.activation.type !== f))) return false;
}
}
// Power-specific filters
if ( filters.has("ritual") ) {
if (data.components.ritual !== true) return false;
}
if ( filters.has("concentration") ) {
if (data.components.concentration !== true) return false;
}
if ( filters.has("prepared") ) {
if ( data.level === 0 || ["innate", "always"].includes(data.preparation.mode) ) return true;
if ( this.actor.data.type === "npc" ) return true;
return data.preparation.prepared;
}
// Equipment-specific filters
if ( filters.has("equipped") ) {
if ( data.equipped !== true ) return false;
}
return true;
});
}
/* -------------------------------------------- */
/**
* Get the font-awesome icon used to display a certain level of skill proficiency
* @private
*/
_getProficiencyIcon(level) {
const icons = {
0: '<i class="far fa-circle"></i>',
0.5: '<i class="fas fa-adjust"></i>',
1: '<i class="fas fa-check"></i>',
2: '<i class="fas fa-check-double"></i>'
};
return icons[level];
}
/* -------------------------------------------- */
/* Event Listeners and Handlers
/* -------------------------------------------- */
/**
* Activate event listeners using the prepared sheet HTML
* @param html {HTML} The prepared HTML object ready to be rendered into the DOM
*/
activateListeners(html) {
// Activate Item Filters
const filterLists = html.find(".filter-list");
filterLists.each(this._initializeFilterItemList.bind(this));
filterLists.on("click", ".filter-item", this._onToggleFilter.bind(this));
// Item summaries
html.find('.item .item-name.rollable h4').click(event => this._onItemSummary(event));
// Editable Only Listeners
if ( this.isEditable ) {
// Input focus and update
const inputs = html.find("input");
inputs.focus(ev => ev.currentTarget.select());
inputs.addBack().find('[data-dtype="Number"]').change(this._onChangeInputDelta.bind(this));
// Ability Proficiency
html.find('.ability-proficiency').click(this._onToggleAbilityProficiency.bind(this));
// Toggle Skill Proficiency
html.find('.skill-proficiency').on("click contextmenu", this._onCycleSkillProficiency.bind(this));
// Trait Selector
html.find('.trait-selector').click(this._onTraitSelector.bind(this));
// Configure Special Flags
html.find('.config-button').click(this._onConfigMenu.bind(this));
// Owned Item management
html.find('.item-create').click(this._onItemCreate.bind(this));
html.find('.item-edit').click(this._onItemEdit.bind(this));
html.find('.item-delete').click(this._onItemDelete.bind(this));
html.find('.item-uses input').click(ev => ev.target.select()).change(this._onUsesChange.bind(this));
html.find('.slot-max-override').click(this._onPowerSlotOverride.bind(this));
// Active Effect management
html.find(".effect-control").click(ev => onManageActiveEffect(ev, this.entity));
}
// Owner Only Listeners
if ( this.actor.owner ) {
// Ability Checks
html.find('.ability-name').click(this._onRollAbilityTest.bind(this));
// Roll Skill Checks
html.find('.skill-name').click(this._onRollSkillCheck.bind(this));
// Item Rolling
html.find('.item .item-image').click(event => this._onItemRoll(event));
html.find('.item .item-recharge').click(event => this._onItemRecharge(event));
}
// Otherwise remove rollable classes
else {
html.find(".rollable").each((i, el) => el.classList.remove("rollable"));
}
// Handle default listeners last so system listeners are triggered first
super.activateListeners(html);
}
/* -------------------------------------------- */
/**
* Iinitialize Item list filters by activating the set of filters which are currently applied
* @private
*/
_initializeFilterItemList(i, ul) {
const set = this._filters[ul.dataset.filter];
const filters = ul.querySelectorAll(".filter-item");
for ( let li of filters ) {
if ( set.has(li.dataset.filter) ) li.classList.add("active");
}
}
/* -------------------------------------------- */
/* Event Listeners and Handlers */
/* -------------------------------------------- */
/**
* Handle input changes to numeric form fields, allowing them to accept delta-typed inputs
* @param event
* @private
*/
_onChangeInputDelta(event) {
const input = event.target;
const value = input.value;
if ( ["+", "-"].includes(value[0]) ) {
let delta = parseFloat(value);
input.value = getProperty(this.actor.data, input.name) + delta;
} else if ( value[0] === "=" ) {
input.value = value.slice(1);
}
}
/* -------------------------------------------- */
/**
* Handle spawning the TraitSelector application which allows a checkbox of multiple trait options
* @param {Event} event The click event which originated the selection
* @private
*/
_onConfigMenu(event) {
event.preventDefault();
const button = event.currentTarget;
switch ( button.dataset.action ) {
case "movement":
new ActorMovementConfig(this.object).render(true);
break;
case "flags":
new ActorSheetFlags(this.object).render(true);
break;
case "senses":
new ActorSensesConfig(this.object).render(true);
break;
}
}
/* -------------------------------------------- */
/**
* Handle cycling proficiency in a Skill
* @param {Event} event A click or contextmenu event which triggered the handler
* @private
*/
_onCycleSkillProficiency(event) {
event.preventDefault();
const field = $(event.currentTarget).siblings('input[type="hidden"]');
// Get the current level and the array of levels
const level = parseFloat(field.val());
const levels = [0, 1, 0.5, 2];
let idx = levels.indexOf(level);
// Toggle next level - forward on click, backwards on right
if ( event.type === "click" ) {
field.val(levels[(idx === levels.length - 1) ? 0 : idx + 1]);
} else if ( event.type === "contextmenu" ) {
field.val(levels[(idx === 0) ? levels.length - 1 : idx - 1]);
}
// Update the field value and save the form
this._onSubmit(event);
}
/* -------------------------------------------- */
/** @override */
async _onDropActor(event, data) {
const canPolymorph = game.user.isGM || (this.actor.owner && game.settings.get('sw5e', 'allowPolymorphing'));
if ( !canPolymorph ) return false;
// Get the target actor
let sourceActor = null;
if (data.pack) {
const pack = game.packs.find(p => p.collection === data.pack);
sourceActor = await pack.getEntity(data.id);
} else {
sourceActor = game.actors.get(data.id);
}
if ( !sourceActor ) return;
// Define a function to record polymorph settings for future use
const rememberOptions = html => {
const options = {};
html.find('input').each((i, el) => {
options[el.name] = el.checked;
});
const settings = mergeObject(game.settings.get('sw5e', 'polymorphSettings') || {}, options);
game.settings.set('sw5e', 'polymorphSettings', settings);
return settings;
};
// Create and render the Dialog
return new Dialog({
title: game.i18n.localize('SW5E.PolymorphPromptTitle'),
content: {
options: game.settings.get('sw5e', 'polymorphSettings'),
i18n: SW5E.polymorphSettings,
isToken: this.actor.isToken
},
default: 'accept',
buttons: {
accept: {
icon: '<i class="fas fa-check"></i>',
label: game.i18n.localize('SW5E.PolymorphAcceptSettings'),
callback: html => this.actor.transformInto(sourceActor, rememberOptions(html))
},
wildshape: {
icon: '<i class="fas fa-paw"></i>',
label: game.i18n.localize('SW5E.PolymorphWildShape'),
callback: html => this.actor.transformInto(sourceActor, {
keepBio: true,
keepClass: true,
keepMental: true,
mergeSaves: true,
mergeSkills: true,
transformTokens: rememberOptions(html).transformTokens
})
},
polymorph: {
icon: '<i class="fas fa-pastafarianism"></i>',
label: game.i18n.localize('SW5E.Polymorph'),
callback: html => this.actor.transformInto(sourceActor, {
transformTokens: rememberOptions(html).transformTokens
})
},
cancel: {
icon: '<i class="fas fa-times"></i>',
label: game.i18n.localize('Cancel')
}
}
}, {
classes: ['dialog', 'sw5e'],
width: 600,
template: 'systems/sw5e/templates/apps/polymorph-prompt.html'
}).render(true);
}
/* -------------------------------------------- */
/** @override */
async _onDropItemCreate(itemData) {
// Create a Consumable power scroll on the Inventory tab
if ( (itemData.type === "power") && (this._tabs[0].active === "inventory") ) {
const scroll = await Item5e.createScrollFromPower(itemData);
itemData = scroll.data;
}
// Create the owned item as normal
return super._onDropItemCreate(itemData);
}
/* -------------------------------------------- */
/**
* Handle enabling editing for a power slot override value
* @param {MouseEvent} event The originating click event
* @private
*/
async _onPowerSlotOverride (event) {
const span = event.currentTarget.parentElement;
const level = span.dataset.level;
const override = this.actor.data.data.powers[level].override || span.dataset.slots;
const input = document.createElement("INPUT");
input.type = "text";
input.name = `data.powers.${level}.override`;
input.value = override;
input.placeholder = span.dataset.slots;
input.dataset.dtype = "Number";
// Replace the HTML
const parent = span.parentElement;
parent.removeChild(span);
parent.appendChild(input);
}
/* -------------------------------------------- */
/**
* Change the uses amount of an Owned Item within the Actor
* @param {Event} event The triggering click event
* @private
*/
async _onUsesChange(event) {
event.preventDefault();
const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.getOwnedItem(itemId);
const uses = Math.clamped(0, parseInt(event.target.value), item.data.data.uses.max);
event.target.value = uses;
return item.update({ 'data.uses.value': uses });
}
/* -------------------------------------------- */
/**
* Handle rolling of an item from the Actor sheet, obtaining the Item instance and dispatching to it's roll method
* @private
*/
_onItemRoll(event) {
event.preventDefault();
const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.getOwnedItem(itemId);
return item.roll();
}
/* -------------------------------------------- */
/**
* Handle attempting to recharge an item usage by rolling a recharge check
* @param {Event} event The originating click event
* @private
*/
_onItemRecharge(event) {
event.preventDefault();
const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.getOwnedItem(itemId);
return item.rollRecharge();
};
/* -------------------------------------------- */
/**
* Handle rolling of an item from the Actor sheet, obtaining the Item instance and dispatching to it's roll method
* @private
*/
_onItemSummary(event) {
event.preventDefault();
let li = $(event.currentTarget).parents(".item"),
item = this.actor.getOwnedItem(li.data("item-id")),
chatData = item.getChatData({secrets: this.actor.owner});
// Toggle summary
if ( li.hasClass("expanded") ) {
let summary = li.children(".item-summary");
summary.slideUp(200, () => summary.remove());
} else {
let div = $(`<div class="item-summary">${chatData.description.value}</div>`);
let props = $(`<div class="item-properties"></div>`);
chatData.properties.forEach(p => props.append(`<span class="tag">${p}</span>`));
div.append(props);
li.append(div.hide());
div.slideDown(200);
}
li.toggleClass("expanded");
}
/* -------------------------------------------- */
/**
* Handle creating a new Owned Item for the actor using initial data defined in the HTML dataset
* @param {Event} event The originating click event
* @private
*/
_onItemCreate(event) {
event.preventDefault();
const header = event.currentTarget;
const type = header.dataset.type;
const itemData = {
name: game.i18n.format("SW5E.ItemNew", {type: type.capitalize()}),
type: type,
data: duplicate(header.dataset)
};
delete itemData.data["type"];
return this.actor.createEmbeddedEntity("OwnedItem", itemData);
}
/* -------------------------------------------- */
/**
* Handle editing an existing Owned Item for the Actor
* @param {Event} event The originating click event
* @private
*/
_onItemEdit(event) {
event.preventDefault();
const li = event.currentTarget.closest(".item");
const item = this.actor.getOwnedItem(li.dataset.itemId);
item.sheet.render(true);
}
/* -------------------------------------------- */
/**
* Handle deleting an existing Owned Item for the Actor
* @param {Event} event The originating click event
* @private
*/
_onItemDelete(event) {
event.preventDefault();
const li = event.currentTarget.closest(".item");
this.actor.deleteOwnedItem(li.dataset.itemId);
}
/* -------------------------------------------- */
/**
* Handle rolling an Ability check, either a test or a saving throw
* @param {Event} event The originating click event
* @private
*/
_onRollAbilityTest(event) {
event.preventDefault();
let ability = event.currentTarget.parentElement.dataset.ability;
this.actor.rollAbility(ability, {event: event});
}
/* -------------------------------------------- */
/**
* Handle rolling a Skill check
* @param {Event} event The originating click event
* @private
*/
_onRollSkillCheck(event) {
event.preventDefault();
const skill = event.currentTarget.parentElement.dataset.skill;
this.actor.rollSkill(skill, {event: event});
}
/* -------------------------------------------- */
/**
* Handle toggling Ability score proficiency level
* @param {Event} event The originating click event
* @private
*/
_onToggleAbilityProficiency(event) {
event.preventDefault();
const field = event.currentTarget.previousElementSibling;
this.actor.update({[field.name]: 1 - parseInt(field.value)});
}
/* -------------------------------------------- */
/**
* Handle toggling of filters to display a different set of owned items
* @param {Event} event The click event which triggered the toggle
* @private
*/
_onToggleFilter(event) {
event.preventDefault();
const li = event.currentTarget;
const set = this._filters[li.parentElement.dataset.filter];
const filter = li.dataset.filter;
if ( set.has(filter) ) set.delete(filter);
else set.add(filter);
this.render();
}
/* -------------------------------------------- */
/**
* Handle spawning the TraitSelector application which allows a checkbox of multiple trait options
* @param {Event} event The click event which originated the selection
* @private
*/
_onTraitSelector(event) {
event.preventDefault();
const a = event.currentTarget;
const label = a.parentElement.querySelector("label");
const choices = CONFIG.SW5E[a.dataset.options];
const options = { name: a.dataset.target, title: label.innerText, choices };
new TraitSelector(this.actor, options).render(true)
}
/* -------------------------------------------- */
/** @override */
_getHeaderButtons() {
let buttons = super._getHeaderButtons();
// Add button to revert polymorph
if ( !this.actor.isPolymorphed || this.actor.isToken ) return buttons;
buttons.unshift({
label: 'SW5E.PolymorphRestoreTransformation',
class: "restore-transformation",
icon: "fas fa-backward",
onclick: ev => this.actor.revertOriginalForm()
});
return buttons;
}
}

View file

@ -1,4 +1,4 @@
import ActorSheet5e from "../base.js";
import ActorSheet5e from "./base.js";
import Actor5e from "../../entity.js";
/**
@ -210,8 +210,8 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
html.find('.short-rest').click(this._onShortRest.bind(this));
html.find('.long-rest').click(this._onLongRest.bind(this));
// Death saving throws
html.find('.death-save').click(this._onDeathSave.bind(this));
// Rollable sheet actions
html.find(".rollable[data-action]").click(this._onSheetAction.bind(this));
// Send Languages to Chat onClick
html.find('[data-options="share-languages"]').click(event => {
@ -277,13 +277,19 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
/* -------------------------------------------- */
/**
* Handle rolling a death saving throw for the Character
* Handle mouse click events for character sheet actions
* @param {MouseEvent} event The originating click event
* @private
*/
_onDeathSave(event) {
_onSheetAction(event) {
event.preventDefault();
const button = event.currentTarget;
switch( button.dataset.action ) {
case "rollDeathSave":
return this.actor.rollDeathSave({event: event});
case "rollInitiative":
return this.actor.rollInitiative({createCombatants: true});
}
}
/* -------------------------------------------- */
@ -330,57 +336,26 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
/* -------------------------------------------- */
/**
* Handle mouse click events to convert currency to the highest possible denomination
* @param {MouseEvent} event The originating click event
* @private
*/
async _onConvertCurrency(event) {
event.preventDefault();
return Dialog.confirm({
title: `${game.i18n.localize("SW5E.CurrencyConvert")}`,
content: `<p>${game.i18n.localize("SW5E.CurrencyConvertHint")}</p>`,
yes: () => this.actor.convertCurrency()
});
}
/* -------------------------------------------- */
/** @override */
async _onDropItemCreate(itemData) {
// Upgrade the number of class levels a character has and add features
// Increment the number of class levels a character instead of creating a new item
if ( itemData.type === "class" ) {
const cls = this.actor.itemTypes.class.find(c => c.name === itemData.name);
const classWasAlreadyPresent = !!cls;
// Add new features for class level
if ( !classWasAlreadyPresent ) {
Actor5e.getClassFeatures(itemData).then(features => {
this.actor.createEmbeddedEntity("OwnedItem", features);
});
let priorLevel = cls?.data.data.levels ?? 0;
if ( !!cls ) {
const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level);
if ( next > priorLevel ) {
itemData.levels = next;
return cls.update({"data.levels": next});
}
// If the actor already has the class, increment the level instead of creating a new item
// then add new features as long as level increases
if ( classWasAlreadyPresent ) {
const lvl = cls.data.data.levels;
const newLvl = Math.min(lvl + 1, 20 + lvl - this.actor.data.data.details.level);
if ( !(lvl === newLvl) ) {
cls.update({"data.levels": newLvl});
itemData.data.levels = newLvl;
Actor5e.getClassFeatures(itemData).then(features => {
this.actor.createEmbeddedEntity("OwnedItem", features);
});
}
return
}
}
// Default drop handling if levels were not added
super._onDropItemCreate(itemData);
}
}
async function addFavorites(app, html, data) {
// Thisfunction is adapted for the SwaltSheet from the Favorites Item
// Tab Module created for Foundry VTT - by Felix Müller (Felix#6196 on Discord).

View file

@ -1,4 +1,4 @@
import ActorSheet5e from "../base.js";
import ActorSheet5e from "./base.js";
/**
* An Actor sheet for NPC type characters in the SW5E system.

View file

@ -0,0 +1,385 @@
import ActorSheet5e from "./base.js";
/**
* An Actor sheet for Vehicle type actors.
* Extends the base ActorSheet5e class.
* @type {ActorSheet5e}
*/
export default class ActorSheet5eVehicle extends ActorSheet5e {
/**
* Define default rendering options for the Vehicle sheet.
* @returns {Object}
*/
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
classes: ["sw5e", "sheet", "actor", "vehicle"],
width: 605,
height: 680
});
}
/* -------------------------------------------- */
/**
* Creates a new cargo entry for a vehicle Actor.
*/
static get newCargo() {
return {
name: '',
quantity: 1
};
}
/* -------------------------------------------- */
/**
* Compute the total weight of the vehicle's cargo.
* @param {Number} totalWeight The cumulative item weight from inventory items
* @param {Object} actorData The data object for the Actor being rendered
* @returns {{max: number, value: number, pct: number}}
* @private
*/
_computeEncumbrance(totalWeight, actorData) {
// Compute currency weight
const totalCoins = Object.values(actorData.data.currency).reduce((acc, denom) => acc + denom, 0);
totalWeight += totalCoins / CONFIG.SW5E.encumbrance.currencyPerWeight;
// Vehicle weights are an order of magnitude greater.
totalWeight /= CONFIG.SW5E.encumbrance.vehicleWeightMultiplier;
// Compute overall encumbrance
const max = actorData.data.attributes.capacity.cargo;
const pct = Math.clamped((totalWeight * 100) / max, 0, 100);
return {value: totalWeight.toNearest(0.1), max, pct};
}
/* -------------------------------------------- */
/**
* Prepare items that are mounted to a vehicle and require one or more crew
* to operate.
* @private
*/
_prepareCrewedItem(item) {
// Determine crewed status
const isCrewed = item.data.crewed;
item.toggleClass = isCrewed ? 'active' : '';
item.toggleTitle = game.i18n.localize(`SW5E.${isCrewed ? 'Crewed' : 'Uncrewed'}`);
// Handle crew actions
if (item.type === 'feat' && item.data.activation.type === 'crew') {
item.crew = item.data.activation.cost;
item.cover = game.i18n.localize(`SW5E.${item.data.cover ? 'CoverTotal' : 'None'}`);
if (item.data.cover === .5) item.cover = '½';
else if (item.data.cover === .75) item.cover = '¾';
else if (item.data.cover === null) item.cover = '—';
if (item.crew < 1 || item.crew === null) item.crew = '—';
}
// Prepare vehicle weapons
if (item.type === 'equipment' || item.type === 'weapon') {
item.threshold = item.data.hp.dt ? item.data.hp.dt : '—';
}
}
/* -------------------------------------------- */
/** @override */
_getMovementSpeed(actorData) {
return {primary: "", special: ""};
}
/* -------------------------------------------- */
/**
* Organize Owned Items for rendering the Vehicle sheet.
* @private
*/
_prepareItems(data) {
const cargoColumns = [{
label: game.i18n.localize('SW5E.Quantity'),
css: 'item-qty',
property: 'quantity',
editable: 'Number'
}];
const equipmentColumns = [{
label: game.i18n.localize('SW5E.Quantity'),
css: 'item-qty',
property: 'data.quantity'
}, {
label: game.i18n.localize('SW5E.AC'),
css: 'item-ac',
property: 'data.armor.value'
}, {
label: game.i18n.localize('SW5E.HP'),
css: 'item-hp',
property: 'data.hp.value',
editable: 'Number'
}, {
label: game.i18n.localize('SW5E.Threshold'),
css: 'item-threshold',
property: 'threshold'
}];
const features = {
actions: {
label: game.i18n.localize('SW5E.ActionPl'),
items: [],
crewable: true,
dataset: {type: 'feat', 'activation.type': 'crew'},
columns: [{
label: game.i18n.localize('SW5E.VehicleCrew'),
css: 'item-crew',
property: 'crew'
}, {
label: game.i18n.localize('SW5E.Cover'),
css: 'item-cover',
property: 'cover'
}]
},
equipment: {
label: game.i18n.localize('SW5E.ItemTypeEquipment'),
items: [],
crewable: true,
dataset: {type: 'equipment', 'armor.type': 'vehicle'},
columns: equipmentColumns
},
passive: {
label: game.i18n.localize('SW5E.Features'),
items: [],
dataset: {type: 'feat'}
},
reactions: {
label: game.i18n.localize('SW5E.ReactionPl'),
items: [],
dataset: {type: 'feat', 'activation.type': 'reaction'}
},
weapons: {
label: game.i18n.localize('SW5E.ItemTypeWeaponPl'),
items: [],
crewable: true,
dataset: {type: 'weapon', 'weapon-type': 'siege'},
columns: equipmentColumns
}
};
const cargo = {
crew: {
label: game.i18n.localize('SW5E.VehicleCrew'),
items: data.data.cargo.crew,
css: 'cargo-row crew',
editableName: true,
dataset: {type: 'crew'},
columns: cargoColumns
},
passengers: {
label: game.i18n.localize('SW5E.VehiclePassengers'),
items: data.data.cargo.passengers,
css: 'cargo-row passengers',
editableName: true,
dataset: {type: 'passengers'},
columns: cargoColumns
},
cargo: {
label: game.i18n.localize('SW5E.VehicleCargo'),
items: [],
dataset: {type: 'loot'},
columns: [{
label: game.i18n.localize('SW5E.Quantity'),
css: 'item-qty',
property: 'data.quantity',
editable: 'Number'
}, {
label: game.i18n.localize('SW5E.Price'),
css: 'item-price',
property: 'data.price',
editable: 'Number'
}, {
label: game.i18n.localize('SW5E.Weight'),
css: 'item-weight',
property: 'data.weight',
editable: 'Number'
}]
}
};
let totalWeight = 0;
for (const item of data.items) {
this._prepareCrewedItem(item);
if (item.type === 'weapon') features.weapons.items.push(item);
else if (item.type === 'equipment') features.equipment.items.push(item);
else if (item.type === 'loot') {
totalWeight += (item.data.weight || 0) * item.data.quantity;
cargo.cargo.items.push(item);
}
else if (item.type === 'feat') {
if (!item.data.activation.type || item.data.activation.type === 'none') {
features.passive.items.push(item);
}
else if (item.data.activation.type === 'reaction') features.reactions.items.push(item);
else features.actions.items.push(item);
}
}
data.features = Object.values(features);
data.cargo = Object.values(cargo);
data.data.attributes.encumbrance = this._computeEncumbrance(totalWeight, data);
}
/* -------------------------------------------- */
/* Event Listeners and Handlers */
/* -------------------------------------------- */
/** @override */
activateListeners(html) {
super.activateListeners(html);
if (!this.options.editable) return;
html.find('.item-toggle').click(this._onToggleItem.bind(this));
html.find('.item-hp input')
.click(evt => evt.target.select())
.change(this._onHPChange.bind(this));
html.find('.item:not(.cargo-row) input[data-property]')
.click(evt => evt.target.select())
.change(this._onEditInSheet.bind(this));
html.find('.cargo-row input')
.click(evt => evt.target.select())
.change(this._onCargoRowChange.bind(this));
if (this.actor.data.data.attributes.actions.stations) {
html.find('.counter.actions, .counter.action-thresholds').hide();
}
}
/* -------------------------------------------- */
/**
* Handle saving a cargo row (i.e. crew or passenger) in-sheet.
* @param event {Event}
* @returns {Promise<Actor>|null}
* @private
*/
_onCargoRowChange(event) {
event.preventDefault();
const target = event.currentTarget;
const row = target.closest('.item');
const idx = Number(row.dataset.itemId);
const property = row.classList.contains('crew') ? 'crew' : 'passengers';
// Get the cargo entry
const cargo = duplicate(this.actor.data.data.cargo[property]);
const entry = cargo[idx];
if (!entry) return null;
// Update the cargo value
const key = target.dataset.property || 'name';
const type = target.dataset.dtype;
let value = target.value;
if (type === 'Number') value = Number(value);
entry[key] = value;
// Perform the Actor update
return this.actor.update({[`data.cargo.${property}`]: cargo});
}
/* -------------------------------------------- */
/**
* Handle editing certain values like quantity, price, and weight in-sheet.
* @param event {Event}
* @returns {Promise<Item>}
* @private
*/
_onEditInSheet(event) {
event.preventDefault();
const itemID = event.currentTarget.closest('.item').dataset.itemId;
const item = this.actor.items.get(itemID);
const property = event.currentTarget.dataset.property;
const type = event.currentTarget.dataset.dtype;
let value = event.currentTarget.value;
switch (type) {
case 'Number': value = parseInt(value); break;
case 'Boolean': value = value === 'true'; break;
}
return item.update({[`${property}`]: value});
}
/* -------------------------------------------- */
/**
* Handle creating a new crew or passenger row.
* @param event {Event}
* @returns {Promise<Actor|Item>}
* @private
*/
_onItemCreate(event) {
event.preventDefault();
const target = event.currentTarget;
const type = target.dataset.type;
if (type === 'crew' || type === 'passengers') {
const cargo = duplicate(this.actor.data.data.cargo[type]);
cargo.push(this.constructor.newCargo);
return this.actor.update({[`data.cargo.${type}`]: cargo});
}
return super._onItemCreate(event);
}
/* -------------------------------------------- */
/**
* Handle deleting a crew or passenger row.
* @param event {Event}
* @returns {Promise<Actor|Item>}
* @private
*/
_onItemDelete(event) {
event.preventDefault();
const row = event.currentTarget.closest('.item');
if (row.classList.contains('cargo-row')) {
const idx = Number(row.dataset.itemId);
const type = row.classList.contains('crew') ? 'crew' : 'passengers';
const cargo = duplicate(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx);
return this.actor.update({[`data.cargo.${type}`]: cargo});
}
return super._onItemDelete(event);
}
/* -------------------------------------------- */
/**
* Special handling for editing HP to clamp it within appropriate range.
* @param event {Event}
* @returns {Promise<Item>}
* @private
*/
_onHPChange(event) {
event.preventDefault();
const itemID = event.currentTarget.closest('.item').dataset.itemId;
const item = this.actor.items.get(itemID);
const hp = Math.clamped(0, parseInt(event.currentTarget.value), item.data.data.hp.max);
event.currentTarget.value = hp;
return item.update({'data.hp.value': hp});
}
/* -------------------------------------------- */
/**
* Handle toggling an item's crewed status.
* @param event {Event}
* @returns {Promise<Item>}
* @private
*/
_onToggleItem(event) {
event.preventDefault();
const itemID = event.currentTarget.closest('.item').dataset.itemId;
const item = this.actor.items.get(itemID);
const crewed = !!item.data.data.crewed;
return item.update({'data.crewed': !crewed});
}
};

View file

@ -1,9 +1,10 @@
import Item5e from "../../item/entity.js";
import TraitSelector from "../../apps/trait-selector.js";
import ActorSheetFlags from "../../apps/actor-flags.js";
import MovementConfig from "../../apps/movement-config.js";
import {SW5E} from '../../config.js';
import {onManageActiveEffect, prepareActiveEffectCategories} from "../../effects.js";
import Item5e from "../../../item/entity.js";
import TraitSelector from "../../../apps/trait-selector.js";
import ActorSheetFlags from "../../../apps/actor-flags.js";
import ActorMovementConfig from "../../../apps/movement-config.js";
import ActorSensesConfig from "../../../apps/senses-config.js";
import {SW5E} from '../../../config.js';
import {onManageActiveEffect, prepareActiveEffectCategories} from "../../../effects.js";
/**
* Extend the basic ActorSheet class to suppose SW5e-specific logic and functionality.
@ -99,6 +100,9 @@ export default class ActorSheet5e extends ActorSheet {
// Movement speeds
data.movement = this._getMovementSpeed(data.actor);
// Senses
data.senses = this._getSenses(data.actor);
// Update traits
this._prepareTraits(data.actor.data.traits);
@ -121,7 +125,7 @@ export default class ActorSheet5e extends ActorSheet {
* @private
*/
_getMovementSpeed(actorData) {
const movement = actorData.data.attributes.movement;
const movement = actorData.data.attributes.movement || {};
const speeds = [
[movement.burrow, `${game.i18n.localize("SW5E.MovementBurrow")} ${movement.burrow}`],
[movement.climb, `${game.i18n.localize("SW5E.MovementClimb")} ${movement.climb}`],
@ -136,6 +140,20 @@ export default class ActorSheet5e extends ActorSheet {
/* -------------------------------------------- */
_getSenses(actorData) {
const senses = actorData.data.attributes.senses || {};
const tags = {};
for ( let [k, label] of Object.entries(CONFIG.SW5E.senses) ) {
const v = senses[k] ?? 0
if ( v === 0 ) continue;
tags[k] = `${game.i18n.localize(label)} ${v} ${senses.units}`;
}
if ( !!senses.special ) tags["special"] = senses.special;
return tags;
}
/* -------------------------------------------- */
/**
* Prepare the data structure for traits data like languages, resistances & vulnerabilities, and proficiencies
* @param {object} traits The raw traits data object from the actor data
@ -373,8 +391,7 @@ export default class ActorSheet5e extends ActorSheet {
html.find('.trait-selector').click(this._onTraitSelector.bind(this));
// Configure Special Flags
html.find('.configure-movement').click(this._onMovementConfig.bind(this));
html.find('.configure-flags').click(this._onConfigureFlags.bind(this));
html.find('.config-button').click(this._onConfigMenu.bind(this));
// Owned Item management
html.find('.item-create').click(this._onItemCreate.bind(this));
@ -448,11 +465,24 @@ export default class ActorSheet5e extends ActorSheet {
/* -------------------------------------------- */
/**
* Handle click events for the Traits tab button to configure special Character Flags
* Handle spawning the TraitSelector application which allows a checkbox of multiple trait options
* @param {Event} event The click event which originated the selection
* @private
*/
_onConfigureFlags(event) {
_onConfigMenu(event) {
event.preventDefault();
new ActorSheetFlags(this.actor).render(true);
const button = event.currentTarget;
switch ( button.dataset.action ) {
case "movement":
new ActorMovementConfig(this.object).render(true);
break;
case "flags":
new ActorSheetFlags(this.object).render(true);
break;
case "senses":
new ActorSensesConfig(this.object).render(true);
break;
}
}
/* -------------------------------------------- */
@ -529,6 +559,8 @@ export default class ActorSheet5e extends ActorSheet {
icon: '<i class="fas fa-paw"></i>',
label: game.i18n.localize('SW5E.PolymorphWildShape'),
callback: html => this.actor.transformInto(sourceActor, {
keepBio: true,
keepClass: true,
keepMental: true,
mergeSaves: true,
mergeSkills: true,
@ -619,14 +651,7 @@ export default class ActorSheet5e extends ActorSheet {
event.preventDefault();
const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.getOwnedItem(itemId);
// Roll powers through the actor
if ( item.data.type === "power" ) {
return this.actor.usePower(item, {configureDialog: !event.shiftKey});
}
// Otherwise roll the Item directly
else return item.roll();
return item.roll();
}
/* -------------------------------------------- */
@ -687,7 +712,7 @@ export default class ActorSheet5e extends ActorSheet {
data: duplicate(header.dataset)
};
delete itemData.data["type"];
return this.actor.createOwnedItem(itemData);
return this.actor.createEmbeddedEntity("OwnedItem", itemData);
}
/* -------------------------------------------- */
@ -791,18 +816,6 @@ export default class ActorSheet5e extends ActorSheet {
/* -------------------------------------------- */
/**
* Handle spawning the TraitSelector application which allows a checkbox of multiple trait options
* @param {Event} event The click event which originated the selection
* @private
*/
_onMovementConfig(event) {
event.preventDefault();
new MovementConfig(this.object).render(true);
}
/* -------------------------------------------- */
/** @override */
_getHeaderButtons() {
let buttons = super._getHeaderButtons();

View file

@ -1,4 +1,4 @@
import ActorSheet5e from "../base.js";
import ActorSheet5e from "./base.js";
import Actor5e from "../../entity.js";
/**
@ -75,6 +75,18 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
// Item details
item.img = item.img || DEFAULT_TOKEN;
item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
item.attunement = {
1: {
icon: "fa-sun",
cls: "not-attuned",
title: "SW5E.AttunementRequired"
},
2: {
icon: "fa-sun",
cls: "attuned",
title: "SW5E.AttunementAttuned"
}
}[item.data.attunement];
// Item usage
item.hasUses = item.data.uses && (item.data.uses.max > 0);
@ -203,7 +215,7 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
/* -------------------------------------------- */
/**
* Handle rolling a death saving throw for the Character
* Handle mouse click events for character sheet actions
* @param {MouseEvent} event The originating click event
* @private
*/
@ -263,37 +275,21 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
/** @override */
async _onDropItemCreate(itemData) {
let addLevel = false;
// Upgrade the number of class levels a character has and add features
// Increment the number of class levels a character instead of creating a new item
if ( itemData.type === "class" ) {
const cls = this.actor.itemTypes.class.find(c => c.name === itemData.name);
let priorLevel = cls?.data.data.levels ?? 0;
const hasClass = !!cls;
// Increment levels instead of creating a new item
if ( hasClass ) {
if ( !!cls ) {
const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level);
if ( next > priorLevel ) {
itemData.levels = next;
await cls.update({"data.levels": next});
addLevel = true;
return cls.update({"data.levels": next});
}
}
// Add class features
if ( !hasClass || addLevel ) {
const features = await Actor5e.getClassFeatures({
className: itemData.name,
archetypeName: itemData.data.archetype,
level: itemData.levels,
priorLevel: priorLevel
});
await this.actor.createEmbeddedEntity("OwnedItem", features);
}
}
// Default drop handling if levels were not added
if ( !addLevel ) super._onDropItemCreate(itemData);
super._onDropItemCreate(itemData);
}
}

View file

@ -1,4 +1,4 @@
import ActorSheet5e from "../base.js";
import ActorSheet5e from "./base.js";
/**
* An Actor sheet for NPC type characters in the SW5E system.

View file

@ -1,4 +1,4 @@
import ActorSheet5e from "../base.js";
import ActorSheet5e from "./base.js";
/**
* An Actor sheet for Vehicle type actors.

View file

@ -34,15 +34,19 @@ export default class AbilityUseDialog extends Dialog {
const quantity = itemData.quantity || 0;
const recharge = itemData.recharge || {};
const recharges = !!recharge.value;
const sufficientUses = (quantity > 0 && !uses.value) || uses.value > 0;
// Prepare dialog form data
const data = {
item: item.data,
title: game.i18n.format("SW5E.AbilityUseHint", item.data),
note: this._getAbilityUseNote(item.data, uses, recharge),
hasLimitedUses: uses.max || recharges,
canUse: recharges ? recharge.charged : (quantity > 0 && !uses.value) || uses.value > 0,
hasPlaceableTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget,
consumePowerSlot: false,
consumeRecharge: recharges,
consumeResource: !!itemData.consume.target,
consumeUses: uses.max,
canUse: recharges ? recharge.charged : sufficientUses,
createTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget,
errors: []
};
if ( item.data.type === "power" ) this._getPowerData(actorData, itemData, data);
@ -50,7 +54,7 @@ export default class AbilityUseDialog extends Dialog {
// Render the ability usage template
const html = await renderTemplate("systems/sw5e/templates/apps/ability-use.html", data);
// Create the Dialog and return as a Promise
// Create the Dialog and return data as a Promise
const icon = data.isPower ? "fa-magic" : "fa-fist-raised";
const label = game.i18n.localize("SW5E.AbilityUse" + (data.isPower ? "Cast" : "Use"));
return new Promise((resolve) => {
@ -61,7 +65,10 @@ export default class AbilityUseDialog extends Dialog {
use: {
icon: `<i class="fas ${icon}"></i>`,
label: label,
callback: html => resolve(new FormData(html[0].querySelector("form")))
callback: html => {
const fd = new FormDataExtended(html[0].querySelector("form"));
resolve(fd.toObject());
}
}
},
default: "use",
@ -83,11 +90,11 @@ export default class AbilityUseDialog extends Dialog {
// Determine whether the power may be up-cast
const lvl = itemData.level;
const canUpcast = (lvl > 0) && CONFIG.SW5E.powerUpcastModes.includes(itemData.preparation.mode);
const consumePowerSlot = (lvl > 0) && CONFIG.SW5E.powerUpcastModes.includes(itemData.preparation.mode);
// If can't upcast, return early and don't bother calculating available power slots
if (!canUpcast) {
data = mergeObject(data, { isPower: true, canUpcast });
if (!consumePowerSlot) {
mergeObject(data, { isPower: true, consumePowerSlot });
return;
}
@ -122,7 +129,7 @@ export default class AbilityUseDialog extends Dialog {
const canCast = powerLevels.some(l => l.hasSlots);
// Return merged data
data = mergeObject(data, { isPower: true, canUpcast, powerLevels });
data = mergeObject(data, { isPower: true, consumePowerSlot, powerLevels });
if ( !canCast ) data.errors.push("SW5E.PowerCastNoSlots");
}

View file

@ -2,21 +2,27 @@
* A simple form to set actor movement speeds
* @implements {BaseEntitySheet}
*/
export default class MovementConfig extends BaseEntitySheet {
export default class ActorMovementConfig extends BaseEntitySheet {
/** @override */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
title: "SW5E.MovementConfig",
classes: ["sw5e"],
template: "systems/sw5e/templates/apps/movement-config.html",
width: 240,
width: 300,
height: "auto"
});
}
/* -------------------------------------------- */
/** @override */
get title() {
return `${game.i18n.localize("SW5E.MovementConfig")}: ${this.entity.name}`;
}
/* -------------------------------------------- */
/** @override */
getData(options) {
const data = {

View file

@ -0,0 +1,43 @@
/**
* A simple form to set actor movement speeds
* @implements {BaseEntitySheet}
*/
export default class ActorSensesConfig extends BaseEntitySheet {
/** @override */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
classes: ["sw5e"],
template: "systems/sw5e/templates/apps/senses-config.html",
width: 300,
height: "auto"
});
}
/* -------------------------------------------- */
/** @override */
get title() {
return `${game.i18n.localize("SW5E.SensesConfig")}: ${this.entity.name}`;
}
/* -------------------------------------------- */
/** @override */
getData(options) {
const senses = this.entity._data.data.attributes?.senses ?? {};
const data = {
senses: {},
special: senses.special ?? "",
units: senses.units, movementUnits: CONFIG.SW5E.movementUnits
};
for ( let [name, label] of Object.entries(CONFIG.SW5E.senses) ) {
const v = senses[name];
data.senses[name] = {
label: game.i18n.localize(label),
value: Number.isNumeric(v) ? v.toNearest(0.1) : 0
}
}
return data;
}
}

View file

@ -36,8 +36,8 @@ export default class TraitSelector extends FormApplication {
getData() {
// Get current values
let attr = getProperty(this.object._data, this.attribute) || {};
attr.value = attr.value || [];
let attr = getProperty(this.object._data, this.attribute);
if ( getType(attr) !== "Object" ) attr = {value: [], custom: ""};
// Populate choices
const choices = duplicate(this.options.choices);

View file

@ -8,9 +8,9 @@ SW5E.ASCII = `__________________________________________
_
| |
___| |_ __ _ _ ____ ____ _ _ __ ___
/ __| __/ _\ | |__\ \ /\ / / _\ | |__/ __|
\__ \ || (_) | | \ V V / (_) | | \__ \
|___/\__\__/_|_| \_/\_/ \__/_|_| |___/
/ __| __/ _\\ | |__\\ \\ /\\ / / _\\ | |__/ __|
\\__ \\ || (_) | | \\ \V \V / (_) | | \\__ \\
|___/\\__\\__/_|_| \\_/\\_/ \\__/_|_| |___/
__________________________________________`;
@ -54,6 +54,20 @@ SW5E.alignments = {
'cd': "SW5E.AlignmentCD"
};
/* -------------------------------------------- */
/**
* An enumeration of item attunement states
* @type {{"0": string, "1": string, "2": string}}
*/
SW5E.attunements = {
0: "SW5E.AttunementNone",
1: "SW5E.AttunementRequired",
2: "SW5E.AttunementAttuned"
};
/* -------------------------------------------- */
SW5E.weaponProficiencies = {
"sim": "SW5E.WeaponSimpleProficiency",
@ -433,14 +447,14 @@ SW5E.hitDieTypes = ["d4", "d6", "d8", "d10", "d12", "d20"];
/* -------------------------------------------- */
/**
* Character senses options
* @type {Object}
* The set of possible sensory perception types which an Actor may have
* @type {object}
*/
SW5E.senses = {
"bs": "SW5E.SenseBS",
"dv": "SW5E.SenseDV",
"ts": "SW5E.SenseTS",
"tr": "SW5E.SenseTR"
"blindsight": "SW5E.SenseBlindsight",
"darkvision": "SW5E.SenseDarkvision",
"tremorsense": "SW5E.SenseTremorsense",
"truesight": "SW5E.SenseTruesight"
};

View file

@ -214,7 +214,6 @@ export async function damageRoll({parts, actor, data, event={}, rollMode=null, t
messageData.speaker = speaker || ChatMessage.getSpeaker();
const messageOptions = {rollMode: rollMode || game.settings.get("core", "rollMode")};
parts = parts.concat(["@bonus"]);
fastForward = fastForward ?? (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
// Define inner roll function
const _roll = function(parts, crit, form) {
@ -236,6 +235,7 @@ export async function damageRoll({parts, actor, data, event={}, rollMode=null, t
roll.terms[0].alter(1, criticalBonusDice);
roll._formula = roll.formula;
}
roll.dice.forEach(d => d.options.critical = true);
messageData.flavor += ` (${game.i18n.localize("SW5E.Critical")})`;
if ( "flags.sw5e.roll" in messageData ) messageData["flags.sw5e.roll"].critical = true;
}
@ -251,7 +251,7 @@ export async function damageRoll({parts, actor, data, event={}, rollMode=null, t
};
// Create the Roll instance
const roll = fastForward ? _roll(parts, critical || event.altKey) : await _damageRollDialog({
const roll = fastForward ? _roll(parts, critical) : await _damageRollDialog({
template, title, parts, data, allowCritical, rollMode: messageOptions.rollMode, dialogOptions, roll: _roll
});

View file

@ -55,6 +55,5 @@ export function rollItemMacro(itemName) {
const item = items[0];
// Trigger the item roll
if ( item.data.type === "power" ) return actor.usePower(item);
return item.roll();
}

View file

@ -3,7 +3,7 @@
* @return {Promise} A Promise which resolves once the migration is completed
*/
export const migrateWorld = async function() {
ui.notifications.info(`Applying SW5E System Migration for version ${game.system.data.version}. Please be patient and do not close your game or shut down your server.`, {permanent: true});
ui.notifications.info(`Applying SW5e System Migration for version ${game.system.data.version}. Please be patient and do not close your game or shut down your server.`, {permanent: true});
// Migrate World Actors
for ( let a of game.actors.entities ) {
@ -56,7 +56,7 @@ export const migrateWorld = async function() {
// Set the migration as complete
game.settings.set("sw5e", "systemMigrationVersion", game.system.data.version);
ui.notifications.info(`SW5E System Migration to version ${game.system.data.version} completed!`, {permanent: true});
ui.notifications.info(`SW5e System Migration to version ${game.system.data.version} completed!`, {permanent: true});
};
/* -------------------------------------------- */
@ -129,6 +129,7 @@ export const migrateActorData = function(actor) {
// Actor Data Updates
_migrateActorBonuses(actor, updateData);
_migrateActorMovement(actor, updateData);
_migrateActorSenses(actor, updateData);
// Migrate Owned Items
if ( !actor.items ) return updateData;
@ -191,6 +192,7 @@ function cleanActorData(actorData) {
*/
export const migrateItemData = function(item) {
const updateData = {};
_migrateItemAttunement(item, updateData);
return updateData;
};
@ -242,19 +244,72 @@ function _migrateActorBonuses(actor, updateData) {
/* -------------------------------------------- */
/**
* Migrate the actor bonuses object
* Migrate the actor speed string to movement object
* @private
*/
function _migrateActorMovement(actor, updateData) {
if ( actor.data.attributes?.movement?.walk !== undefined ) return;
const s = (actor.data.attributes?.speed?.value || "").split(" ");
const ad = actor.data;
const old = ad?.attributes?.speed?.value;
if ( old === undefined ) return;
const s = (old || "").split(" ");
if ( s.length > 0 ) updateData["data.attributes.movement.walk"] = Number.isNumeric(s[0]) ? parseInt(s[0]) : null;
updateData["data.attributes.-=speed"] = null;
return updateData
}
/* -------------------------------------------- */
/**
* Migrate the actor traits.senses string to attributes.senses object
* @private
*/
function _migrateActorSenses(actor, updateData) {
const ad = actor.data;
if ( ad?.traits?.senses === undefined ) return;
const original = ad.traits.senses || "";
// Try to match old senses with the format like "Darkvision 60 ft, Blindsight 30 ft"
const pattern = /([A-z]+)\s?([0-9]+)\s?([A-z]+)?/
let wasMatched = false;
// Match each comma-separated term
for ( let s of original.split(",") ) {
s = s.trim();
const match = s.match(pattern);
if ( !match ) continue;
const type = match[1].toLowerCase();
if ( type in CONFIG.SW5E.senses ) {
updateData[`data.attributes.senses.${type}`] = Number(match[2]).toNearest(0.5);
wasMatched = true;
}
}
// If nothing was matched, but there was an old string - put the whole thing in "special"
if ( !wasMatched && !!original ) {
updateData["data.attributes.senses.special"] = original;
}
// Remove the old traits.senses string once the migration is complete
updateData["data.traits.-=senses"] = null;
return updateData;
}
/* -------------------------------------------- */
/**
* Delete the old data.attuned boolean
* @private
*/
function _migrateItemAttunement(item, updateData) {
if ( item.data.attuned === undefined ) return;
updateData["data.attunement"] = 0;
updateData["data.-=attuned"] = null;
return updateData;
}
/* -------------------------------------------- */
/**
* A general tool to purge flags from all entities in a Compendium pack.
* @param {Compendium} pack The compendium pack to clean

View file

@ -45,7 +45,10 @@ export default class AbilityTemplate extends MeasuredTemplate {
}
// Return the template constructed from the item data
return new this(templateData);
const template = new this(templateData);
template.item = item;
template.actorSheet = item.actor?.sheet || null;
return template;
}
/* -------------------------------------------- */
@ -55,9 +58,16 @@ export default class AbilityTemplate extends MeasuredTemplate {
*/
drawPreview() {
const initialLayer = canvas.activeLayer;
// Draw the template and switch to the template layer
this.draw();
this.layer.activate();
this.layer.preview.addChild(this);
// Hide the sheet that originated the preview
if ( this.actorSheet ) this.actorSheet.minimize();
// Activate interactivity
this.activatePreviewListeners(initialLayer);
}
@ -92,6 +102,7 @@ export default class AbilityTemplate extends MeasuredTemplate {
canvas.app.view.oncontextmenu = null;
canvas.app.view.onwheel = null;
initialLayer.activate();
this.actorSheet.maximize();
};
// Confirm the workflow (left-click)

View file

@ -1,5 +1,6 @@
{"_id":"BQEDghtJMBfhnnr6","name":"Ysannanite Form","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"lightsaberform","data":{"description":{"value":"<p>As a part of the bonus action to adopt this form, if you took the Attack action, you can engage in Double- or Two-Weapon Fighting.</p>\n<p>Additionally, until the end of your next turn, when making a ranged weapon attack while you are within 5 feet of a hostile creature, you do not have disadvantage on the attack roll.</p>"},"source":{"value":"Expanded Content"}},"flags":{"core":{"sourceId":"Item.QjBBl7KevWVvKDmy"}},"img":"systems/sw5e/packs/Icons/Lightsaber%20Forms/Ysannanite%20Form.webp","effects":[]}
{"_id":"CJQwWFwTc98U4BpX","name":"Niman Form","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"lightsaberform","data":{"description":{"value":"<p>As a part of the bonus action to adopt this form, if you took the Attack action, you can engage in Double- or Two-Weapon Fighting.</p><p>Until the start of your next turn, you can use Wisdom or Charisma instead of Strength or Dexterity for the attack and damage rolls of your melee weapon attacks. You must use the same modifier for both rolls.</p>"},"source":{"value":"PHB"}},"flags":{},"img":"systems/sw5e/packs/Icons/Lightsaber%20Forms/Niman%20Form.webp","effects":[]}
{"_id":"EMTzwdHtEY41M1RI","name":"Vonil/Ishu Form","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"lightsaberform","data":{"description":{"value":"<p>If you took the Attack action before adopting this form, you can direct one of your companions to strike a creature you hit with an attack.&nbsp; Choose a friendly creature within 5 feet of the creature you hit with an attack that can see or hear you. That creature can immediately use its reaction to make one weapon attack against the same creature you hit with an attack.</p>\n<p>If you did not take the Attack action before adopting this form, and there is a friendly creature within 15 feet of you, you can move up to 10 feet. You must end this movement within 5 feet of the friendly creature.</p>"},"source":{"value":"Expanded Content"}},"flags":{"core":{"sourceId":"Item.u7vBOskAb0MsFSlZ"}},"img":"systems/sw5e/packs/Icons/Lightsaber%20Forms/Vonil-Ishu%20Form.webp","effects":[]}
{"_id":"GBccAWbpxgNPTz0D","name":"Makashi Form","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"lightsaberform","data":{"description":{"value":"<p>Until the start of your next turn, when a creature makes a melee weapon attack against you and misses, you can use your reaction to make one melee weapon attack against that creature.</p>"},"source":{"value":"PHB"}},"flags":{},"img":"systems/sw5e/packs/Icons/Lightsaber%20Forms/Makashi%20Form.webp","effects":[]}
{"_id":"OmPlZtMXi2b3Vo64","name":"Jar'Kai Form","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"lightsaberform","data":{"description":{"value":"<p>As a part of the bonus action to adopt this form, if you took the Attack action, you can engage in Double- or Two-Weapon Fighting.</p><p>Each time you hit with an attack on this turn, you can move up to 5 feet without provoking opportunity attacks from the creature you hit.</p>"},"source":{"value":"PHB"}},"flags":{},"img":"systems/sw5e/packs/Icons/Lightsaber%20Forms/Jar%27Kai%20Form.webp","effects":[]}
{"_id":"Xg7qB6sQLw3V6V4p","name":"Sokan Form","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"lightsaberform","data":{"description":{"value":"<p>Until the start of your next turn, you ignore difficult terrain. Additionally, when an opponent makes a melee weapon attack against you, you can use your reaction to move to another space within 5 feet of that opponent without provoking opportunity attacks, imposing disadvantage on the triggering roll.</p>"},"source":{"value":"PHB"}},"flags":{},"img":"systems/sw5e/packs/Icons/Lightsaber%20Forms/Sokan%20Form.webp","effects":[]}

View file

@ -30,7 +30,8 @@ import ActorSheet5eNPCNew from "./module/actor/sheets/newSheet/npc.js";
import ItemSheet5e from "./module/item/sheet.js";
import ShortRestDialog from "./module/apps/short-rest.js";
import TraitSelector from "./module/apps/trait-selector.js";
import MovementConfig from "./module/apps/movement-config.js";
import ActorMovementConfig from "./module/apps/movement-config.js";
import ActorSensesConfig from "./module/apps/senses-config.js";
// Import Helpers
import * as chat from "./module/chat.js";
@ -58,7 +59,7 @@ Hooks.once("init", function() {
ItemSheet5e,
ShortRestDialog,
TraitSelector,
MovementConfig
ActorMovementConfig
},
canvas: {
AbilityTemplate
@ -186,7 +187,7 @@ Hooks.once("ready", function() {
// Determine whether a system migration is required and feasible
if ( !game.user.isGM ) return;
const currentVersion = game.settings.get("sw5e", "systemMigrationVersion");
const NEEDS_MIGRATION_VERSION = "1.1.0";
const NEEDS_MIGRATION_VERSION = "1.2.0";
const COMPATIBLE_MIGRATION_VERSION = 0.80;
const needsMigration = currentVersion && isNewerVersion(NEEDS_MIGRATION_VERSION, currentVersion);
if ( !needsMigration ) return;

View file

@ -2,7 +2,7 @@
"name": "sw5e",
"title": "SW 5th Edition",
"description": "A comprehensive game system for running games of SW 5th Edition in the Foundry VTT environment.",
"version": "1.1.1",
"version": "1.2.0",
"author": "Dev Team",
"scripts": [],
"esmodules": ["sw5e.js"],

View file

@ -85,7 +85,15 @@
"units": "ft",
"hover": false
},
"powercasting": "int",
"senses": {
"darkvision": 0,
"blindsight": 0,
"tremorsense": 0,
"truesight": 0,
"units": "ft",
"special": ""
},
"powercasting": "none",
"speed": {
"_deprecated": true
}
@ -169,7 +177,6 @@
}
},
"traits": {
"senses": "",
"languages": {
"value": [],
"custom": ""
@ -245,6 +252,7 @@
}
}
},
"htmlFields": ["details.biography.value", "details.biography.public"],
"character": {
"templates": ["common", "creature"],
"attributes": {
@ -503,6 +511,7 @@
"weight": 0,
"price": 0,
"attuned": false,
"attunement": 0,
"equipped": false,
"rarity": "",
"identified": true
@ -568,6 +577,7 @@
}
}
},
"htmlFields": ["description.value", "description.chat", "description.unidentified"],
"archetype": {
"templates": ["archetypeDescription"],
"className": "",
@ -675,8 +685,8 @@
"supply": 0
},
"preparation": {
"mode": null,
"prepared": false
"mode": "prepared",
"prepared": true
},
"scaling": {
"mode": "none",

View file

@ -29,7 +29,7 @@
<input type="text" name="data.details.alignment" value="{{data.details.alignment}}"
placeholder="{{ localize 'SW5E.Alignment' }}" />
<div class="proficiency">
Proficiency {{numberFormat data.attributes.prof decimals=0 sign=true}}
{{ localize "SW5E.Proficiency" }} {{numberFormat data.attributes.prof decimals=0 sign=true}}
</div>
</div>
{{!-- Header Attributes --}}
@ -77,27 +77,25 @@
{{!-- INITIATIVE --}}
<section>
<h1>{{ localize "SW5E.Initiative" }}</h1>
<h1 class="attribute-name box-title rollable" data-action="rollInitiative">{{ localize "SW5E.Initiative" }}</h1>
<div class="attribute-value">
<span class="initiative">{{numberFormat data.attributes.init.total decimals=0 sign=true}}</span>
</div>
<footer class="attribute-footer initiative">
<span>{{ localize "SW5E.Modifier" }}</span>
<input name="data.attributes.init.value" type="text" placeholder="0" data-dtype="Number"
value="{{numberFormat data.attributes.init.value decimals=0 sign=true}}" />
<input name="data.attributes.init.value" type="text" data-dtype="Number" placeholder="0"
value="{{numberFormat data.attributes.init.value decimals=0 sign=true}}"/>
</footer>
</section>
{{!-- SPEED / MOVEMENT TYPES --}}
<section>
<h1>{{ localize "SW5E.Speed" }}</h1>
<div class="attribute-value">
<input name="data.attributes.speed.value" type="text" value="{{data.attributes.speed.value}}"
placeholder="0" />
<h1>{{ localize "SW5E.Movement" }}</h1>
<<div class="attribute-value">
<span>{{movement.primary}}</span>
</div>
<footer class="attribute-footer speed">
<input type="text" class="speed" name="data.attributes.speed.special"
value="{{data.attributes.speed.special}}" placeholder="{{ localize 'SW5E.SpeedSpecial' }}" />
<footer class="attribute-footer">
<span>{{movement.special}}</span>
</footer>
</section>

View file

@ -56,14 +56,14 @@
</footer>
</section>
<section>
<h1>{{ localize "SW5E.Speed" }}</h1>
<h1>{{ localize "SW5E.Movement" }}
<a class="config-button" data-action="movement" title="{{localize 'SW5E.MovementConfig'}}"><i class="fas fa-cog"></i></a>
</h1>
<div class="attribute-value">
<input name="data.attributes.speed.value" type="text" value="{{data.attributes.speed.value}}"
placeholder="0" />
<span>{{movement.primary}}</span>
</div>
<footer class="attribute-footer speed">
<input type="text" class="speed" name="data.attributes.speed.special"
value="{{data.attributes.speed.special}}" placeholder="{{ localize 'SW5E.SpeedSpecial' }}" />
<footer class="attribute-footer">
<span>{{movement.special}}</span>
</footer>
</section>
</div>
@ -75,6 +75,7 @@
<button class="item active" data-tab="attributes">{{ localize "SW5E.Attributes" }}</button>
<button class="item" data-tab="features">{{ localize "SW5E.Features" }}</button>
<button class="item" data-tab="powerbook">{{ localize "SW5E.Powerbook" }}</button>
<button class="item" data-tab="effects">{{ localize "SW5E.Effects" }}</button>
<button class="item" data-tab="biography">{{ localize "SW5E.Biography" }}</button>
</nav>
@ -175,6 +176,11 @@
{{> "systems/sw5e/templates/actors/newActor/parts/swalt-powerbook.html"}}
</div>
{{!-- Effects Tab --}}
<div class="tab effects flexcol" data-group="primary" data-tab="effects">
{{> "systems/sw5e/templates/actors/newActor/parts/swalt-active-effects.html"}}
</div>
{{!-- Biography Tab --}}
<div class="tab biography flexcol" data-group="primary" data-tab="biography">
<div class="panel">

View file

@ -23,7 +23,7 @@
<div class="effect-duration">{{effect.duration.label}}</div>
<div class="item-controls effect-controls flexrow">
<a class="effect-control" data-action="toggle" title="{{localize 'SW5E.EffectToggle'}}">
<i class="fas fa-circle-notch"></i>
<i class="fas {{#if effect.data.disabled}}fa-check{{else}}fa-times{{/if}}"></i>
</a>
<a class="effect-control" data-action="edit" title="{{localize 'SW5E.EffectEdit'}}">
<i class="fas fa-edit"></i>

View file

@ -11,11 +11,13 @@
</label>
<label class="{{#unless data.traits.senses}}inactive{{/unless}}">
{{#unless isVehicle}}
{{localize "SW5E.Senses"}}
<input type="text" name="data.traits.senses" value="{{data.traits.senses}}"
placeholder="{{ localize 'SW5E.None' }}" />
{{/unless}}
</label>
<label>{{localize "SW5E.Senses"}}</label>
<a class="config-button" data-action="senses" title="{{localize 'SW5E.MovementConfig'}}"><i class="fas fa-cog"></i></a>
<ul class="traits-list">
{{#each senses as |v k|}}
<li class="tag {{k}}">{{v}}</li>
{{/each}}
</ul>
<div class="languages">
<label data-options="share-languages" class="languages">{{localize "SW5E.Languages"}}</label>
<a class="trait-selector" data-options="languages" data-target="data.traits.languages">
@ -111,7 +113,7 @@
{{#unless isVehicle}}
<div>
<label>{{localize "SW5E.SpecialTraits"}}</label>
<a class="configure-flags"><i class="fas fa-cog"></i></a>
<a class="config-button" data-action="flags" title="{{localize 'SW5E.SpecialTraits'}}"><i class="fas fa-cog"></i></a>
</div>
{{/unless}}
<ul class="passives"></ul>

View file

@ -0,0 +1,158 @@
<form class="{{cssClass}} flexcol" autocomplete="off">
<header class="sheet-header flexrow">
<img class="profile" src="{{actor.img}}" title="{{actor.name}}" alt="{{actor.name}}"
data-edit="img">
<section class="header-details flexrow">
<h1 class="charnam">
<input name="name" type="text" value="{{actor.name}}"
placeholder="{{localize 'SW5E.Name'}}">
</h1>
<ul class="summary flexrow">
<li>
<span>{{lookup config.actorSizes data.traits.size}}</span>
</li>
<li>
<span>{{localize 'SW5E.Vehicle'}}</span>
</li>
<li>
<input type="text" name="data.traits.dimensions"
value="{{data.traits.dimensions}}"
placeholder="{{localize 'SW5E.Dimensions'}}">
</li>
<li>
<input type="text" name="data.details.source"
value="{{data.details.source}}"
placeholder="{{localize 'SW5E.Source'}}">
</li>
</ul>
<ul class="attributes flexrow">
<li class="attribute health">
<h4 class="attribute-name box-title">{{localize 'SW5E.Health'}}</h4>
<div class="attribute-value multiple">
<input name="data.attributes.hp.value" type="text" placeholder="&mdash;"
value="{{data.attributes.hp.value}}" data-dtype="Number">
<span class="sep"> &sol; </span>
<input name="data.attributes.hp.max" type="text" placeholder="&mdash;"
value="{{data.attributes.hp.max}}" data-dtype="Number">
</div>
<footer class="attribute-footer">
<input name="data.attributes.hp.dt" type="text" class="temphp"
placeholder="{{localize 'SW5E.Threshold'}}"
value="{{data.attributes.hp.dt}}" data-dtype="Number">
<input name="data.attributes.hp.mt" type="text" class="temphp"
placeholder="{{localize 'SW5E.VehicleMishap'}}"
value="{{data.attributes.hp.mt}}" data-dtype="Number">
</footer>
</li>
<li class="attribute">
<h4 class="attribute-name box-title">{{localize 'SW5E.ArmorClass'}}</h4>
<div class="attribute-value">
<input name="data.attributes.ac.value" type="text" placeholder="&mdash;"
value="{{data.attributes.ac.value}}" data-dtype="Number">
</div>
<footer class="attribute-footer">
<input type="text" name="data.attributes.ac.motionless"
placeholder="&mdash;" value="{{data.attributes.ac.motionless}}">
</footer>
</li>
<li class="attribute">
<h4 class="attribute-name box-title">{{localize 'SW5E.Speed'}}</h4>
<div class="attribute-value">
<input name="data.attributes.speed" type="text" placeholder="&mdash;" value="{{data.attributes.speed}}"/>
</div>
</li>
</ul>
</section>
</header>
<nav class="sheet-navigation tabs" data-group="primary">
<a class="item active" data-tab="attributes">{{localize 'SW5E.Attributes'}}</a>
<a class="item" data-tab="features">{{localize 'SW5E.Features'}}</a>
<a class="item" data-tab="cargo">{{localize 'SW5E.VehicleCargoCrew'}}</a>
<a class="item" data-tab="biography">{{localize 'SW5E.Description'}}</a>
</nav>
<section class="sheet-body">
<div class="tab attributes flexrow" data-group="primary" data-tab="attributes">
<ul class="ability-scores flexrow">
{{#each data.abilities as |ability id|}}
<li class="ability" data-ability="{{id}}">
<h4 class="ability-name box-title rollable">{{ability.label}}</h4>
<input class="ability-score" name="data.abilities.{{id}}.value" type="text"
value="{{ability.value}}" data-dtype="Number" placeholder="0">
<div class="ability-modifiers flexrow">
<span class="ability-mod" title="{{localize 'SW5E.Modifier'}}">
{{numberFormat ability.mod decimals=0 sign=true}}
</span>
</div>
</li>
{{/each}}
</ul>
<section class="center-pane flexcol">
<div class="counters">
<div class="counter flexrow creature-cap">
<h4>{{localize 'SW5E.VehicleCreatureCapacity'}}</h4>
<div class="counter-value">
<input type="text" placeholder="&mdash;"
name="data.attributes.capacity.creature"
value="{{data.attributes.capacity.creature}}">
</div>
</div>
<div class="counter flexrow cargo-cap">
<h4>{{localize 'SW5E.VehicleCargoCapacity'}}</h4>
<div class="counter-value">
<input type="text" name="data.attributes.capacity.cargo" placeholder="0"
data-dtype="Number" value="{{data.attributes.capacity.cargo}}">
</div>
</div>
<div class="counter flexrow stations">
<h4>{{localize 'SW5E.VehicleActionStations'}}</h4>
<div class="counter-value">
<input name="data.attributes.actions.stations" type="checkbox"
data-dtype="Boolean" value="{{data.attributes.actions.stations}}"
{{checked data.attributes.actions.stations}}>
</div>
</div>
<div class="counter flexrow actions">
<h4>{{localize 'SW5E.ActionPl'}}</h4>
<div class="counter-value">
<input type="text" name="data.attributes.actions.value" placeholder="0"
data-dtype="Number" value="{{data.attributes.actions.value}}">
</div>
</div>
<div class="counter flexrow action-thresholds">
<h4>{{localize 'SW5E.VehicleActionThresholds'}}</h4>
<div class="counter-value">
<span class="sep">&lt;</span>
<input type="text" placeholder="&mdash;" data-dtype="Number"
value="{{data.attributes.actions.thresholds.[2]}}"
name="data.attributes.actions.thresholds.2">
<span class="sep">&lt;</span>
<input type="text" placeholder="&mdash;" data-dtype="Number"
value="{{data.attributes.actions.thresholds.[1]}}"
name="data.attributes.actions.thresholds.1">
<span class="sep">&lt;</span>
<input type="text" placeholder="&mdash;" data-dtype="Number"
value="{{data.attributes.actions.thresholds.[0]}}"
name="data.attributes.actions.thresholds.0">
</div>
</div>
</div>
{{> 'systems/sw5e/templates/actors/newActor/parts/swalt-traits.html'}}
</section>
</div>
<div class="tab features flexcol" data-group="primary" data-tab="features">
{{> 'systems/sw5e/templates/actors/newActor/parts/swalt-features.html' sections=features}}
</div>
<div class="tab cargo flexcol" data-group="primary" data-tab="cargo">
{{> 'systems/sw5e/templates/actors/newActor/parts/swalt-inventory.html' sections=cargo}}
</div>
<div class="tab biography flexcol" data-group="primary" data-tab="biography">
{{editor content=data.details.biography.value target='data.details.biography.value'
button=true owner=owner editable=editable}}
</div>
</section>
</form>

View file

@ -81,9 +81,10 @@
</footer>
</li>
<li class="attribute">
<li class="attribute movement">
<h4 class="attribute-name box-title">
{{ localize "SW5E.Speed" }}
{{ localize "SW5E.Movement" }}
<a class="config-button" data-action="movement" title="{{localize 'SW5E.MovementConfig'}}"><i class="fas fa-cog"></i></a>
</h4>
<div class="attribute-value">
<span>{{movement.primary}}</span>

View file

@ -61,9 +61,10 @@
</footer>
</li>
<li class="attribute">
<li class="attribute movement">
<h4 class="attribute-name box-title">
{{ localize "SW5E.Speed" }}
{{ localize "SW5E.Movement" }}
<a class="config-button" data-action="movement" title="{{localize 'SW5E.MovementConfig'}}"><i class="fas fa-cog"></i></a>
</h4>
<div class="attribute-value">
<span>{{movement.primary}}</span>

View file

@ -15,6 +15,7 @@
{{/each}}
{{/select}}
</select>
<span>{{localize "SW5E.AbbreviationDC"}} {{data.attributes.powerdc}}</span>
</div>
<ul class="filter-list flexrow" data-filter="powerbook">
@ -30,8 +31,8 @@
<ol class="items-list inventory-list">
{{#each powerbook as |section|}}
<li class="items-header powerbook-header flexrow">
<h3 class="item-name flexrow">{{section.label}}</h3>
<div class="item-name flexrow">
<h3>{{section.label}}</h3>
<div class="power-slots">
{{#if section.usesSlots}}
<input type="text" name="data.powers.{{section.prop}}.value" value="{{section.uses}}" placeholder="0"
@ -51,7 +52,7 @@
<span class="power-max">{{{section.slots}}}</span>
{{/if}}
</div>
</div>
<div class="power-school">{{localize "SW5E.PowerSchool"}}</div>
<div class="power-action">{{localize "SW5E.PowerUsage"}}</div>
<div class="power-target">{{localize "SW5E.PowerTarget"}}</div>
@ -74,13 +75,12 @@
{{#if item.data.uses.per }}
<div class="item-detail power-uses">Uses {{item.data.uses.value}} / {{item.data.uses.max}}</div>
{{/if}}
</div>
<div class="power-comps">
{{#each labels.components}}
<span class="power-component {{this}}">{{this}}</span>
{{/each}}
</div>
</div>
<div class="power-school">{{labels.school}}</div>
<div class="power-action">{{labels.activation}}</div>
<div class="power-target" title="{{localize 'SW5E.Range'}}: {{labels.range}}">

View file

@ -11,14 +11,14 @@
</div>
{{#unless isVehicle}}
<div class="form-group ">
<label>{{localize "SW5E.MovementConfig"}}</label>
<a class="configure-movement" title="{{localize 'SW5E.MovementConfig'}}"><i class="fas fa-cog"></i></a>
</div>
<div class="form-group {{#unless data.traits.senses}}inactive{{/unless}}">
<div class="form-group">
<label>{{localize "SW5E.Senses"}}</label>
<input type="text" name="data.traits.senses" value="{{data.traits.senses}}" placeholder="{{ localize 'SW5E.None' }}"/>
<a class="config-button" data-action="senses" title="{{localize 'SW5E.MovementConfig'}}"><i class="fas fa-cog"></i></a>
<ul class="traits-list">
{{#each senses as |v k|}}
<li class="tag {{k}}">{{v}}</li>
{{/each}}
</ul>
</div>
<div class="form-group {{data.traits.languages.cssClass}}">
@ -123,7 +123,7 @@
{{#unless isVehicle}}
<div class="form-group ">
<label>{{localize "SW5E.SpecialTraits"}}</label>
<a class="configure-flags"><i class="fas fa-cog"></i></a>
<a class="config-button" data-action="flags" title="{{localize 'SW5E.SpecialTraits'}}"><i class="fas fa-cog"></i></a>
</div>
{{/unless}}
</div>

View file

@ -22,7 +22,7 @@
<div class="effect-duration">{{effect.duration.label}}</div>
<div class="item-controls effect-controls flexrow">
<a class="effect-control" data-action="toggle" title="{{localize 'SW5E.EffectToggle'}}">
<i class="fas fa-circle-notch"></i>
<i class="fas {{#if effect.data.disabled}}fa-check{{else}}fa-times{{/if}}"></i>
</a>
<a class="effect-control" data-action="edit" title="{{localize 'SW5E.EffectEdit'}}">
<i class="fas fa-edit"></i>

View file

@ -5,11 +5,11 @@
<p class="notification error">{{localize this}}</p>
{{/each}}
{{#if canUpcast}}
{{#if consumePowerSlot}}
<div class="form-group">
<label>{{ localize "SW5E.PowerCastUpcast" }}</label>
<div class="form-fields">
<select name="level" {{#unless canUpcast}}disabled{{/unless}}>
<select name="level">
{{#select item.data.level}}
{{#each powerLevels as |l|}}
<option value="{{l.level}}" {{#unless l.canCast}}disabled{{/unless}}>{{l.label}}</option>
@ -24,13 +24,25 @@
</div>
{{/if}}
{{#if hasLimitedUses}}
{{#if consumeUses}}
<div class="form-group">
<label class="checkbox"><input type="checkbox" name="consumeUse" checked/>{{ localize "SW5E.AbilityUseConsume" }}</label>
</div>
{{/if}}
{{#if hasPlaceableTemplate}}
{{#if consumeResource}}
<div class="form-group">
<label class="checkbox"><input type="checkbox" name="consumeResource" checked/>{{ localize "SW5E.ConsumeResource" }}</label>
</div>
{{/if}}
{{#if consumeRecharge}}
<div class="form-group">
<label class="checkbox"><input type="checkbox" name="consumeRecharge" checked/>{{ localize "SW5E.ConsumeRecharge" }}</label>
</div>
{{/if}}
{{#if createTemplate}}
<div class="form-group">
<label class="checkbox">
<input type="checkbox" name="placeTemplate" checked/>

View file

@ -0,0 +1,20 @@
<form autocomplete="off">
<p class="notes">{{localize "SW5E.SensesConfigHint"}}</p>
{{#each senses as |sense name|}}
<div class="form-group">
<label>{{sense.label}}</label>
<input name="data.attributes.senses.{{name}}" type="number" step="0.1" value="{{sense.value}}"/>
</div>
{{/each}}
<div class="form-group">
<label>{{localize "SW5E.MovementUnits"}}</label>
<select name="data.attributes.senses.units">
{{selectOptions movementUnits selected=units}}
</select>
</div>
<div class="form-group">
<label>{{localize "SW5E.SenseSpecial"}}</label>
<input type="text" name="data.attributes.senses.special" value="{{special}}"/>
</div>
<button type="submit" name="submit" value="1"><i class="far fa-save"></i> {{ localize "Submit"}}</button>
</form>

View file

@ -107,7 +107,7 @@
<label>
{{localize "SW5E.ClassSkillsChosen"}}
{{#if editable }}
<a class="trait-selector class-skills" data-edit="data.skills" data-options="skills">
<a class="trait-selector class-skills" data-target="data.skills" data-options="skills">
<i class="fas fa-edit"></i></a>
{{/if}}
</label>

View file

@ -57,6 +57,13 @@
</select>
</div>
<div class="form-group">
<label>{{localize "SW5E.Attunement"}}</label>
<select name="data.attunement" data-dtype="Number">
{{selectOptions config.attunements selected=data.attunement localize=true}}
</select>
</div>
<div class="form-group stacked">
<label>{{ localize "SW5E.ItemConsumableStatus" }}</label>
<label class="checkbox">
@ -65,9 +72,6 @@
<label class="checkbox">
<input type="checkbox" name="data.identified" {{checked data.identified}}/> {{ localize "SW5E.Identified" }}
</label>
<label class="checkbox">
<input type="checkbox" name="data.attuned" {{checked data.attuned}}/> {{ localize "SW5E.Attuned" }}
</label>
</div>
<h3 class="form-header">{{ localize "SW5E.ItemConsumableUsage" }}</h3>

View file

@ -59,6 +59,13 @@
</div>
{{#unless isMountable}}
<div class="form-group">
<label>{{localize "SW5E.Attunement"}}</label>
<select name="data.attunement" data-dtype="Number">
{{selectOptions config.attunements selected=data.attunement localize=true}}
</select>
</div>
{{!-- Equipment Status --}}
<div class="form-group stacked">
<label>{{ localize "SW5E.ItemEquipmentStatus" }}</label>
@ -71,9 +78,6 @@
<label class="checkbox">
<input type="checkbox" name="data.identified" {{checked data.identified}}/> {{ localize "SW5E.Identified" }}
</label>
<label class="checkbox">
<input type="checkbox" name="data.attuned" {{checked data.attuned}}/> {{ localize "SW5E.Attuned" }}
</label>
</div>
{{/unless}}

View file

@ -75,6 +75,5 @@
<div class="tab effects flexcol" data-group="primary" data-tab="effects">
{{> "systems/sw5e/templates/actors/parts/active-effects.html"}}
</div>
</section>
</form>

View file

@ -30,7 +30,7 @@
<div class="form-group">
<label>{{ localize "SW5E.ItemAttackBonus" }}</label>
<div class="form-fields">
<input type="text" name="data.attackBonus" value="{{data.attackBonus}}" data-dtype="Number"/>
<input type="text" name="data.attackBonus" value="{{data.attackBonus}}"/>
</div>
</div>
{{/if}}

View file

@ -113,7 +113,7 @@
<div class="form-fields">
<input type="text" name="data.uses.value" value="{{data.uses.value}}" data-dtype="Number"/>
<span class="sep">{{ localize "SW5E.of" }}</span>
<input type="text" name="data.uses.max" value="{{data.uses.max}}" data-dtype="Number"/>
<input type="text" name="data.uses.max" value="{{data.uses.max}}"/>
<span class="sep">{{ localize "SW5E.per" }}</span>
<select name="data.uses.per">
{{#select data.uses.per}}

View file

@ -58,6 +58,13 @@
</div>
{{#unless isMountable}}
<div class="form-group">
<label>{{localize "SW5E.Attunement"}}</label>
<select name="data.attunement" data-dtype="Number">
{{selectOptions config.attunements selected=data.attunement localize=true}}
</select>
</div>
{{!-- Weapon Status --}}
<div class="form-group stacked">
<label>{{ localize "SW5E.ItemWeaponStatus" }}</label>
@ -71,9 +78,6 @@
<label class="checkbox">
<input type="checkbox" name="data.identified" {{checked data.identified}}/> {{ localize "SW5E.Identified" }}
</label>
<label class="checkbox">
<input type="checkbox" name="data.attuned" {{checked data.attuned}}/> {{ localize "SW5E.Attuned" }}
</label>
</div>
</div>
{{/unless}}