Merge branch 'Develop'

This commit is contained in:
Kakeman89 2021-01-15 09:37:59 -05:00
commit 1047d71e60
52 changed files with 3117 additions and 1552 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,12 +69,29 @@
line-height: 30px;
}
}
}
}
.attributes {
input.temphp {
width: 48%;
// 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;
}
}
// Temporary HP
input.temphp {
width: 48%;
}
}
}
@ -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
@ -473,7 +486,8 @@
white-space: nowrap;
overflow: hidden;
&:last-child { border-right: none; }
&.item-action {flex: 0 0 100px}
&.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,32 +447,13 @@
font-size: 12px;
text-align: center;
}
.item-name {
h3 {
padding-left: 5px;
//.modesto();
text-align: left;
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;
}
}
}
/* ----------------------------------------- */

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

@ -77,6 +77,9 @@ html {
body {
.openSans(13px, 400);
background-image: url('./ui/SW5e-logo.svg');
background-repeat: no-repeat;
background-size: cover;
}
h1 {

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;
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;
mergeObject(itemData, initial);
}
return super.createOwnedItem(itemData, options);
// 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 = {};
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);
}
/* -------------------------------------------- */
/* 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;
}
}

File diff suppressed because it is too large Load diff

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.
@ -16,7 +16,6 @@ export default class ActorSheet5eNPCNew extends ActorSheet5e {
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
classes: ["sw5e", "sheet", "actor", "npc"],
width: 600,
width: 800,
tabs: [{
navSelector: ".root-tabs",

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"
};
@ -1140,7 +1154,7 @@ SW5E.characterFlags = {
section: "Feats",
type: Boolean
},
"remarkableAthlete": {
"remarkableAthlete": {
name: "SW5E.FlagsRemarkableAthlete",
hint: "SW5E.FlagsRemarkableAthleteHint",
abilities: ['str','dex','con'],

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

@ -152,7 +152,7 @@ export default class Item5e extends Item {
const itemData = this.data;
const data = itemData.data;
const C = CONFIG.SW5E;
const labels = {};
const labels = this.labels = {};
// Classes
if ( itemData.type === "class" ) {
@ -252,12 +252,8 @@ export default class Item5e extends Item {
// Item Actions
if ( data.hasOwnProperty("actionType") ) {
// Saving throws for unowned items
const save = data.save;
if ( save?.ability && !this.isOwned ) {
if ( save.scaling !== "flat" ) save.dc = null;
labels.save = game.i18n.format("SW5E.SaveDC", {dc: save.dc || "", ability: C.abilities[save.ability]});
}
// Saving throws
this.getSaveDC();
// Damage
let dam = data.damage || {};
@ -271,6 +267,30 @@ export default class Item5e extends Item {
this.labels = labels;
}
/**
* Update the derived spell DC for an item that requires a saving throw
* @returns {number|null}
*/
getSaveDC() {
if ( !this.hasSave ) return;
const save = this.data.data?.save;
// Actor spell-DC based scaling
if ( save.scaling === "spell" ) {
save.dc = this.isOwned ? getProperty(this.actor.data, "data.attributes.spelldc") : null;
}
// Ability-score based scaling
else if ( save.scaling !== "flat" ) {
save.dc = this.isOwned ? getProperty(this.actor.data, `data.abilities.${save.scaling}.dc`) : null;
}
// Update labels
const abl = CONFIG.DND5E.abilities[save.ability];
this.labels.save = game.i18n.format("DND5E.SaveDC", {dc: save.dc || "", ability: abl});
return save.dc;
}
/* -------------------------------------------- */
/**

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)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -80,9 +80,6 @@ body {
font-family: 'Open Sans';
font-size: 13px;
font-weight: 400;
background-image: url('./ui/SW5e-logo.svg');
background-repeat: no-repeat;
background-size: cover;
}
h1 {
font-family: 'Russo One';
@ -379,12 +376,12 @@ input[type="reset"]:disabled {
padding: 0;
border: none;
}
.sw5e.chat-card .card-header,
.sw5e.chat-card .card-header img,
.midi-qol-item-card .card-header img {
flex: 0 0 36px;
margin-right: 4px;
}
.sw5e.chat-card .card-header,
.sw5e.chat-card .card-header h3,
.midi-qol-item-card .card-header h3 {
flex: 1;
margin: 0;
@ -945,7 +942,7 @@ input[type="reset"]:disabled {
}
.sw5e.sheet.actor .swalt-sheet nav.sheet-navigation {
display: grid;
grid-template-columns: repeat(7, 1fr);
grid-template-columns: repeat(6, 1fr);
column-gap: 16px;
margin: 4px 0;
}

132
sw5e.css
View file

@ -272,9 +272,12 @@
background: transparent;
}
.sw5e.sheet .editable .rollable:hover {
cursor: pointer;
}
.sw5e.sheet .editable h4.rollable:hover,
.sw5e.sheet .editable .rollable:hover > h4 {
color: #000;
text-shadow: 0 0 10px red;
cursor: pointer;
}
.sw5e.sheet span.sep {
flex: none;
@ -366,6 +369,7 @@
overflow: hidden;
}
.sw5e.sheet .filter-list {
align-items: center;
list-style: none;
margin: 0;
padding: 0;
@ -423,6 +427,28 @@
margin: 0;
padding: 0;
}
.sw5e.sheet .items-list .item-name {
flex: 2;
margin: 0;
overflow: hidden;
font-size: 13px;
text-align: left;
align-items: center;
}
.sw5e.sheet .items-list .item-name h3,
.sw5e.sheet .items-list .item-name h4 {
margin: 0;
white-space: nowrap;
overflow-x: hidden;
}
.sw5e.sheet .items-list .item-controls {
flex: 0 0 60px;
justify-content: space-between;
}
.sw5e.sheet .items-list .item-controls a {
font-size: 12px;
text-align: center;
}
.sw5e.sheet .items-list .item {
align-items: center;
padding: 0 2px;
@ -459,25 +485,10 @@
font-size: 12px;
text-align: center;
}
.sw5e.sheet .items-list .items-header .item-name {
.sw5e.sheet .items-list .items-header h3 {
padding-left: 5px;
font-size: 16px;
}
.sw5e.sheet .items-list .item-name {
flex: 2;
margin: 0;
overflow: hidden;
font-size: 13px;
text-align: left;
align-items: center;
}
.sw5e.sheet .items-list .item-controls {
flex: 0 0 60px;
justify-content: space-between;
}
.sw5e.sheet .items-list .item-controls a {
font-size: 12px;
text-align: center;
font-size: 16px;
}
.sw5e.sheet .effects .item .effect-source,
.sw5e.sheet .effects .item .effect-duration,
@ -586,7 +597,21 @@
height: 30px;
line-height: 30px;
}
.sw5e.sheet.actor .attributes input.temphp {
.sw5e.sheet.actor .sheet-header .attributes .movement h4.attribute-name {
position: relative;
}
.sw5e.sheet.actor .sheet-header .attributes .movement .config-button {
position: absolute;
display: none;
right: 0;
top: 1px;
font-size: 12px;
font-weight: normal;
}
.sw5e.sheet.actor .sheet-header .attributes .movement:hover .config-button {
display: block;
}
.sw5e.sheet.actor .sheet-header .attributes input.temphp {
width: 48%;
}
.sw5e.sheet.actor h4.box-title {
@ -798,7 +823,7 @@
margin: 0 0 3px 0;
justify-content: space-between;
}
.sw5e.sheet.actor .traits .configure-flags {
.sw5e.sheet.actor .traits .config-button {
flex: 1;
}
.sw5e.sheet.actor .traits label {
@ -871,8 +896,8 @@
.sw5e.sheet.actor .inventory-list .item .item-name i.attuned {
color: #7a7971;
}
.sw5e.sheet.actor .inventory-list .item .item-name h4 {
font-size: 14px;
.sw5e.sheet.actor .inventory-list .item .item-name i.not-attuned {
color: #44191A;
}
.sw5e.sheet.actor .inventory-list .item .item-uses input {
width: 24px;
@ -905,6 +930,9 @@
.sw5e.sheet.actor .inventory-list .item-detail.item-action {
flex: 0 0 100px;
}
.sw5e.sheet.actor .inventory-list .item-detail.attunement {
flex: 0 0 24px;
}
.sw5e.sheet.actor .inventory-list .item-weight {
flex: 0 0 60px;
border-left: 1px solid #c9c7b8;
@ -995,24 +1023,22 @@
flex: 0 0 240px;
margin: 0;
}
.sw5e.sheet.actor .powercasting-ability input,
.sw5e.sheet.actor .powercasting-ability label,
.sw5e.sheet.actor .powercasting-ability span {
flex: 0 0 32px;
flex: none;
}
.sw5e.sheet.actor .powercasting-ability input {
flex: 0 0 28px;
text-align: center;
}
.sw5e.sheet.actor .powercasting-ability select {
margin: 0 5px;
flex: 0 0 150px;
}
.sw5e.sheet.actor .powercasting-ability h3.power-dc {
flex: 1;
text-align: right;
flex: 0 0 120px;
}
.sw5e.sheet.actor .power-slots,
.sw5e.sheet.actor .power-comps {
flex: 0 0 75px;
padding-right: 5px;
text-align: right;
flex: none;
padding: 0 5px;
font-size: 12px;
color: #7a7971;
border-right: 1px solid #c9c7b8;
@ -1025,9 +1051,10 @@
font-size: 13px;
font-weight: normal;
}
.sw5e.sheet.actor .power-uses {
padding-right: 8px;
text-align: right !important;
.sw5e.sheet.actor .powerbook .power-uses {
padding-right: 5px;
text-align: right;
color: #7a7971;
}
.sw5e.sheet.actor .power-school,
.sw5e.sheet.actor .power-action,
@ -1069,6 +1096,7 @@
padding-right: 8px;
margin-bottom: 4px;
overflow-y: auto;
scrollbar-width: thin;
}
.sw5e.sheet.item {
min-height: 660px;
@ -1587,7 +1615,7 @@
.sw5e.chat-card .card-footer span,
.midi-qol-item-card .card-footer span {
border-right: 2px groove #FFF;
padding: 0 5px 0 0;
padding: 0 3px 0 0;
font-size: 10px;
}
.sw5e.chat-card .card-footer span:last-child,
@ -1762,7 +1790,7 @@
resize: none;
}
.sw5e.sheet.actor.character .biography {
max-width: calc(-80%);
max-width: calc(100% - 180px);
}
/* ----------------------------------------- */
/* Basic Structure */
@ -1794,9 +1822,6 @@
.sw5e.sheet.actor.npc .summary {
font-size: 18px;
}
.sw5e.sheet.actor.npc .powercasting-ability label {
flex: none;
}
.sw5e.sheet.actor.vehicle .features .item-controls {
flex: 0 0 68px;
}
@ -1817,30 +1842,3 @@
max-width: 40px;
text-align: right;
}
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;
}
input[type="number"]:focus {
box-shadow: 0 0 5px red;
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
}

15
sw5e.js
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
@ -141,12 +142,12 @@ Hooks.once("setup", function() {
// Localize CONFIG objects once up-front
const toLocalize = [
"abilities", "abilityAbbreviations", "abilityActivationTypes", "abilityConsumptionTypes", "actorSizes", "alignments",
"abilities", "abilityAbbreviations", "abilityActivationTypes", "abilityConsumptionTypes", "actorSizes", "alignments",
"armorProficiencies", "armorPropertiesTypes", "conditionTypes", "consumableTypes", "cover", "currencies", "damageResistanceTypes",
"damageTypes", "distanceUnits", "equipmentTypes", "healingTypes", "itemActionTypes", "languages",
"limitedUsePeriods", "movementUnits", "polymorphSettings", "proficiencyLevels", "senses", "skills",
"powerComponents", "powerLevels", "powerPreparationModes", "powerScalingModes", "powerSchools", "targetTypes",
"timePeriods", "toolProficiencies", "weaponProficiencies", "weaponProperties", "weaponTypes"
"limitedUsePeriods", "movementUnits", "polymorphSettings", "proficiencyLevels", "senses", "skills",
"powerComponents", "powerLevels", "powerPreparationModes", "powerScalingModes", "powerSchools", "targetTypes",
"timePeriods", "toolProficiencies", "weaponProficiencies", "weaponProperties", "weaponTypes"
];
// Exclude some from sorting where the default order matters
@ -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.2",
"version": "R1-A1",
"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
@ -519,7 +528,7 @@
},
"target": {
"value": null,
"width": null,
"width": null,
"units": "",
"type": ""
},
@ -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>
<h1>{{ localize "SW5E.Movement" }}</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>

View file

@ -56,17 +56,17 @@
</footer>
</section>
<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" />
</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>
</section>
</div>
<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">
<span>{{movement.primary}}</span>
</div>
<footer class="attribute-footer">
<span>{{movement.special}}</span>
</footer>
</section>
</div>
</header>
@ -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,9 +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' }}" />
<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>
{{/unless}}
</label>
<div class="languages">
@ -111,7 +115,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,28 +31,28 @@
<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="power-slots">
{{#if section.usesSlots}}
<input type="text" name="data.powers.{{section.prop}}.value" value="{{section.uses}}" placeholder="0"
data-dtype="Number"/>
<span class="sep"> / </span>
<span class="power-max" data-level="{{section.prop}}" data-slots="{{section.slots}}">
{{{section.slots}}}
{{#if ../editable}}
<a class="slot-max-override" title="{{localize 'SW5E.PowerProgOverride'}}">
<i class="fas fa-edit"></i>
</a>
<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"
data-dtype="Number"/>
<span class="sep"> / </span>
<span class="power-max" data-level="{{section.prop}}" data-slots="{{section.slots}}">
{{{section.slots}}}
{{#if ../editable}}
<a class="slot-max-override" title="{{localize 'SW5E.PowerProgOverride'}}">
<i class="fas fa-edit"></i>
</a>
{{/if}}
</span>
{{ else }}
<span>{{{section.uses}}}</span>
<span class="sep"> / </span>
<span class="power-max">{{{section.slots}}}</span>
{{/if}}
</span>
{{ else }}
<span>{{{section.uses}}}</span>
<span class="sep"> / </span>
<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,12 +75,11 @@
{{#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 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>

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}}