forked from GitHub-Mirrors/foundry-sw5e
Merge pull request #77 from unrealkakeman89/Develop-VJ
Update Core to 1.2
This commit is contained in:
commit
4cf9ece35c
47 changed files with 1975 additions and 430 deletions
26
lang/en.json
26
lang/en.json
|
@ -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",
|
||||
|
|
|
@ -69,14 +69,31 @@
|
|||
line-height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
// Movement Configuration
|
||||
.movement {
|
||||
h4.attribute-name {
|
||||
position: relative;
|
||||
}
|
||||
.config-button {
|
||||
position: absolute;
|
||||
display: none;
|
||||
right: 0;
|
||||
top: 1px;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
}
|
||||
&:hover .config-button {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.attributes {
|
||||
// Temporary HP
|
||||
input.temphp {
|
||||
width: 48%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------------------------- */
|
||||
/* General Styles */
|
||||
|
@ -341,7 +358,7 @@
|
|||
margin: 0 0 3px 0;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.configure-flags {
|
||||
.config-button {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
|
@ -428,12 +445,8 @@
|
|||
&.rollable .item-image:hover {
|
||||
background-image: url("../../icons/svg/d20-black.svg") !important;
|
||||
}
|
||||
i.attuned {
|
||||
color: @colorTan;
|
||||
}
|
||||
h4 {
|
||||
font-size: 14px;
|
||||
}
|
||||
i.attuned { color: @colorTan; }
|
||||
i.not-attuned { color: @colorCrimson; }
|
||||
}
|
||||
|
||||
// Item uses
|
||||
|
@ -474,6 +487,7 @@
|
|||
overflow: hidden;
|
||||
&:last-child { border-right: none; }
|
||||
&.item-action {flex: 0 0 100px}
|
||||
&.attunement {flex: 0 0 24px}
|
||||
}
|
||||
.item-weight {
|
||||
flex: 0 0 60px;
|
||||
|
@ -576,26 +590,23 @@
|
|||
.powercasting-ability {
|
||||
flex: 0 0 240px;
|
||||
margin: 0;
|
||||
|
||||
input, span {
|
||||
flex: 0 0 32px;
|
||||
label, span {
|
||||
flex: none;
|
||||
}
|
||||
input {
|
||||
flex: 0 0 28px;
|
||||
text-align: center;
|
||||
}
|
||||
select {
|
||||
margin: 0 5px;
|
||||
flex: 0 0 150px;
|
||||
}
|
||||
h3.power-dc {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
flex: 0 0 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.power-slots,
|
||||
.power-comps {
|
||||
flex: 0 0 75px;
|
||||
padding-right: 5px;
|
||||
text-align: right;
|
||||
flex: none;
|
||||
padding: 0 5px;
|
||||
font-size: 12px;
|
||||
color: @colorTan;
|
||||
border-right: 1px solid @colorFaint;
|
||||
|
@ -612,9 +623,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.power-uses {
|
||||
padding-right: 8px;
|
||||
text-align: right !important;
|
||||
.powerbook .power-uses {
|
||||
padding-right: 5px;
|
||||
text-align: right;
|
||||
color: @colorTan;
|
||||
}
|
||||
|
||||
.power-school, .power-action, .power-target {
|
||||
|
@ -663,5 +675,6 @@
|
|||
padding-right: 8px;
|
||||
margin-bottom: 4px;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
}
|
|
@ -178,11 +178,14 @@
|
|||
background: transparent;
|
||||
}
|
||||
|
||||
// Rollable Links
|
||||
// Rollable Titles
|
||||
.editable .rollable:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
.editable h4.rollable:hover,
|
||||
.editable .rollable:hover > h4 {
|
||||
color: #000;
|
||||
text-shadow: 0 0 10px red;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
// Separators
|
||||
|
@ -306,6 +309,7 @@
|
|||
/* ----------------------------------------- */
|
||||
|
||||
.filter-list {
|
||||
align-items: center;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
@ -382,6 +386,30 @@
|
|||
padding: 0;
|
||||
}
|
||||
|
||||
// Item Name
|
||||
.item-name {
|
||||
flex: 2;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
align-items: center;
|
||||
h3, h4 {
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
// Control Buttons
|
||||
.item-controls {
|
||||
flex: 0 0 60px;
|
||||
justify-content: space-between;
|
||||
a {
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
// Individual Item
|
||||
.item {
|
||||
align-items: center;
|
||||
|
@ -419,30 +447,11 @@
|
|||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
.item-name {
|
||||
h3 {
|
||||
padding-left: 5px;
|
||||
//.modesto();
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
// Item Name
|
||||
.item-name {
|
||||
flex: 2;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
// Control Buttons
|
||||
.item-controls {
|
||||
flex: 0 0 60px;
|
||||
justify-content: space-between;
|
||||
a {
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -31,11 +31,4 @@
|
|||
.summary {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.powercasting-ability {
|
||||
label {
|
||||
flex: none;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
import { d20Roll, damageRoll } from "../dice.js";
|
||||
import ShortRestDialog from "../apps/short-rest.js";
|
||||
import LongRestDialog from "../apps/long-rest.js";
|
||||
import AbilityUseDialog from "../apps/ability-use-dialog.js";
|
||||
import AbilityTemplate from "../pixi/ability-template.js";
|
||||
import {SW5E} from '../config.js';
|
||||
|
||||
/**
|
||||
|
@ -163,8 +161,8 @@ export default class Actor5e extends Actor {
|
|||
}
|
||||
|
||||
// Acquire archetype features
|
||||
const subConfig = clsConfig.archetypes[archetypeName] || {};
|
||||
for ( let [l, f] of Object.entries(subConfig.features || {}) ) {
|
||||
const archConfig = clsConfig.archetypes[archetypeName] || {};
|
||||
for ( let [l, f] of Object.entries(archConfig.features || {}) ) {
|
||||
l = parseInt(l);
|
||||
if ( (l <= level) && (l > priorLevel) ) ids = ids.concat(f);
|
||||
}
|
||||
|
@ -207,7 +205,7 @@ export default class Actor5e extends Actor {
|
|||
const updateData = expandObject(u);
|
||||
const config = {
|
||||
className: updateData.name || item.data.name,
|
||||
archetypeName: updateData.data.archetype || item.data.data.archetype,
|
||||
archetypeName: getProperty(updateData, "data.archetype") || item.data.data.archetype,
|
||||
level: getProperty(updateData, "data.levels"),
|
||||
priorLevel: item ? item.data.data.levels : 0
|
||||
}
|
||||
|
@ -356,16 +354,8 @@ export default class Actor5e extends Actor {
|
|||
const data = actorData.data;
|
||||
data.attributes.powerdc = data.attributes.powercasting ? data.abilities[data.attributes.powercasting].dc : 10;
|
||||
|
||||
// Apply powercasting DC to any power items which use it
|
||||
for ( let i of this.items ) {
|
||||
const save = i.data.data.save;
|
||||
if ( save?.ability ) {
|
||||
if ( save.scaling === "power" ) save.dc = data.attributes.powerdc;
|
||||
else if ( save.scaling !== "flat" ) save.dc = data.abilities[save.scaling]?.dc ?? 10;
|
||||
const ability = CONFIG.SW5E.abilities[save.ability];
|
||||
i.labels.save = game.i18n.format("SW5E.SaveDC", {dc: save.dc || "", ability});
|
||||
}
|
||||
}
|
||||
// Compute ability save DCs that depend on the calling actor
|
||||
this.items.forEach(i => i.getSaveDC());
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -546,20 +536,69 @@ export default class Actor5e extends Actor {
|
|||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
async createOwnedItem(itemData, options) {
|
||||
async createEmbeddedEntity(embeddedName, itemData, options={}) {
|
||||
|
||||
// Assume NPCs are always proficient with weapons and always have powers prepared
|
||||
if ( !this.hasPlayerOwner ) {
|
||||
let t = itemData.type;
|
||||
// Pre-creation steps for owned items
|
||||
if ( embeddedName === "OwnedItem" ) this._preCreateOwnedItem(itemData, options);
|
||||
|
||||
// Standard embedded entity creation
|
||||
return super.createEmbeddedEntity(embeddedName, itemData, options);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* A temporary shim function which will eventually (in core fvtt version 0.8.0+) be migrated to the new abstraction layer
|
||||
* @param itemData
|
||||
* @param options
|
||||
* @private
|
||||
*/
|
||||
_preCreateOwnedItem(itemData, options) {
|
||||
if ( this.data.type === "vehicle" ) return;
|
||||
const isNPC = this.data.type === 'npc';
|
||||
let initial = {};
|
||||
if ( t === "weapon" ) initial["data.proficient"] = true;
|
||||
if ( ["weapon", "equipment"].includes(t) ) initial["data.equipped"] = true;
|
||||
if ( t === "power" ) initial["data.prepared"] = true;
|
||||
switch ( itemData.type ) {
|
||||
case "weapon":
|
||||
initial["data.equipped"] = isNPC; // NPCs automatically equip weapons
|
||||
let hasWeaponProf = isNPC; // NPCs automatically have weapon proficiency
|
||||
if ( !isNPC ) {
|
||||
const weaponProf = {
|
||||
"natural": true,
|
||||
"simpleVW": "sim",
|
||||
"simpleB": "sim",
|
||||
"simpleLW": "sim",
|
||||
"martialVW": "mar",
|
||||
"martialB": "mar",
|
||||
"martialLW": "mar"
|
||||
}[itemData.data?.weaponType];
|
||||
const actorWeaponProfs = this.data.data.traits?.weaponProf?.value || [];
|
||||
hasWeaponProf = (weaponProf === true) || actorWeaponProfs.includes(weaponProf);
|
||||
}
|
||||
initial["data.proficient"] = hasWeaponProf;
|
||||
break;
|
||||
case "equipment":
|
||||
initial["data.equipped"] = isNPC; // NPCs automatically equip equipment
|
||||
let hasEquipmentProf = isNPC; // NPCs automatically have equipment proficiency
|
||||
if ( !isNPC ) {
|
||||
const armorProf = {
|
||||
"natural": true,
|
||||
"clothing": true,
|
||||
"light": "lgt",
|
||||
"medium": "med",
|
||||
"heavy": "hvy",
|
||||
"shield": "shl"
|
||||
}[itemData.data?.armor?.type];
|
||||
const actorArmorProfs = this.data.data.traits?.armorProf?.value || [];
|
||||
hasEquipmentProf = (armorProf === true) || actorArmorProfs.includes(armorProf);
|
||||
}
|
||||
initial["data.proficient"] = hasEquipmentProf;
|
||||
break;
|
||||
case "power":
|
||||
initial["data.prepared"] = true; // NPCs automatically prepare powers
|
||||
break;
|
||||
}
|
||||
mergeObject(itemData, initial);
|
||||
}
|
||||
return super.createOwnedItem(itemData, options);
|
||||
}
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Gameplay Mechanics */
|
||||
|
@ -600,77 +639,16 @@ export default class Actor5e extends Actor {
|
|||
"data.attributes.hp.temp": tmp - dt,
|
||||
"data.attributes.hp.value": dh
|
||||
};
|
||||
return this.update(updates);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Cast a Power, consuming a power slot of a certain level
|
||||
* @param {Item5e} item The power being cast by the actor
|
||||
* @param {Event} event The originating user interaction which triggered the cast
|
||||
*/
|
||||
async usePower(item, {configureDialog=true}={}) {
|
||||
if ( item.data.type !== "power" ) throw new Error("Wrong Item type");
|
||||
const itemData = item.data.data;
|
||||
|
||||
// Configure powercasting data
|
||||
let lvl = itemData.level;
|
||||
const usesSlots = (lvl > 0) && CONFIG.SW5E.powerUpcastModes.includes(itemData.preparation.mode);
|
||||
const limitedUses = !!itemData.uses.per;
|
||||
let consumeSlot = `power${lvl}`;
|
||||
let consumeUse = false;
|
||||
let placeTemplate = false;
|
||||
|
||||
// Configure power slot consumption and measured template placement from the form
|
||||
if ( configureDialog && (usesSlots || item.hasAreaTarget || limitedUses) ) {
|
||||
const usage = await AbilityUseDialog.create(item);
|
||||
if ( usage === null ) return;
|
||||
|
||||
// Determine consumption preferences
|
||||
consumeSlot = Boolean(usage.get("consumeSlot"));
|
||||
consumeUse = Boolean(usage.get("consumeUse"));
|
||||
placeTemplate = Boolean(usage.get("placeTemplate"));
|
||||
|
||||
// Determine the cast power level
|
||||
const isPact = usage.get('level') === 'pact';
|
||||
const lvl = isPact ? this.data.data.powers.pact.level : parseInt(usage.get("level"));
|
||||
if ( lvl !== item.data.data.level ) {
|
||||
const upcastData = mergeObject(item.data, {"data.level": lvl}, {inplace: false});
|
||||
item = item.constructor.createOwned(upcastData, this);
|
||||
}
|
||||
|
||||
// Denote the power slot being consumed
|
||||
if ( consumeSlot ) consumeSlot = isPact ? "pact" : `power${lvl}`;
|
||||
}
|
||||
|
||||
// Update Actor data
|
||||
if ( usesSlots && consumeSlot && (lvl > 0) ) {
|
||||
const slots = parseInt(this.data.data.powers[consumeSlot]?.value);
|
||||
if ( slots === 0 || Number.isNaN(slots) ) {
|
||||
return ui.notifications.error(game.i18n.localize("SW5E.PowerCastNoSlots"));
|
||||
}
|
||||
await this.update({
|
||||
[`data.powers.${consumeSlot}.value`]: Math.max(slots - 1, 0)
|
||||
});
|
||||
}
|
||||
|
||||
// Update Item data
|
||||
if ( limitedUses && consumeUse ) {
|
||||
const uses = parseInt(itemData.uses.value || 0);
|
||||
if ( uses <= 0 ) ui.notifications.warn(game.i18n.format("SW5E.ItemNoUses", {name: item.name}));
|
||||
await item.update({"data.uses.value": Math.max(parseInt(item.data.data.uses.value || 0) - 1, 0)})
|
||||
}
|
||||
|
||||
// Initiate ability template placement workflow if selected
|
||||
if ( placeTemplate && item.hasAreaTarget ) {
|
||||
const template = AbilityTemplate.fromItem(item);
|
||||
if ( template ) template.drawPreview();
|
||||
if ( this.sheet.rendered ) this.sheet.minimize();
|
||||
}
|
||||
|
||||
// Invoke the Item roll
|
||||
return item.roll();
|
||||
// Delegate damage application to a hook
|
||||
// TODO replace this in the future with a better modifyTokenAttribute function in the core
|
||||
const allowed = Hooks.call("modifyTokenAttribute", {
|
||||
attribute: "attributes.hp",
|
||||
value: amount,
|
||||
isDelta: false,
|
||||
isBar: true
|
||||
}, updates);
|
||||
return allowed !== false ? this.update(updates) : this;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -989,7 +967,7 @@ export default class Actor5e extends Actor {
|
|||
// Adjust actor data
|
||||
await cls.update({"data.hitDiceUsed": cls.data.data.hitDiceUsed + 1});
|
||||
const hp = this.data.data.attributes.hp;
|
||||
const dhp = Math.min(hp.max - hp.value, roll.total);
|
||||
const dhp = Math.min(hp.max + (hp.tempmax ?? 0) - hp.value, roll.total);
|
||||
await this.update({"data.attributes.hp.value": hp.value + dhp});
|
||||
return roll;
|
||||
}
|
||||
|
@ -1437,4 +1415,18 @@ export default class Actor5e extends Actor {
|
|||
console.warn(`The Actor5e#getPowerDC(ability) method has been deprecated in favor of Actor5e#data.data.abilities[ability].dc`);
|
||||
return this.data.data.abilities[ability]?.dc;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Cast a Power, consuming a power slot of a certain level
|
||||
* @param {Item5e} item The power being cast by the actor
|
||||
* @param {Event} event The originating user interaction which triggered the cast
|
||||
* @deprecated since sw5e 1.2.0
|
||||
*/
|
||||
async usePower(item, {configureDialog=true}={}) {
|
||||
console.warn(`The Actor5e#usePower method has been deprecated in favor of Item5e#roll`);
|
||||
if ( item.data.type !== "power" ) throw new Error("Wrong Item type");
|
||||
return item.roll();
|
||||
}
|
||||
}
|
833
module/actor/sheets/newSheet/base.js
Normal file
833
module/actor/sheets/newSheet/base.js
Normal 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": "∞"
|
||||
};
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import ActorSheet5e from "../base.js";
|
||||
import ActorSheet5e from "./base.js";
|
||||
import Actor5e from "../../entity.js";
|
||||
|
||||
/**
|
||||
|
@ -210,8 +210,8 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
|
|||
html.find('.short-rest').click(this._onShortRest.bind(this));
|
||||
html.find('.long-rest').click(this._onLongRest.bind(this));
|
||||
|
||||
// Death saving throws
|
||||
html.find('.death-save').click(this._onDeathSave.bind(this));
|
||||
// Rollable sheet actions
|
||||
html.find(".rollable[data-action]").click(this._onSheetAction.bind(this));
|
||||
|
||||
// Send Languages to Chat onClick
|
||||
html.find('[data-options="share-languages"]').click(event => {
|
||||
|
@ -277,13 +277,19 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
|
|||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle rolling a death saving throw for the Character
|
||||
* Handle mouse click events for character sheet actions
|
||||
* @param {MouseEvent} event The originating click event
|
||||
* @private
|
||||
*/
|
||||
_onDeathSave(event) {
|
||||
_onSheetAction(event) {
|
||||
event.preventDefault();
|
||||
const button = event.currentTarget;
|
||||
switch( button.dataset.action ) {
|
||||
case "rollDeathSave":
|
||||
return this.actor.rollDeathSave({event: event});
|
||||
case "rollInitiative":
|
||||
return this.actor.rollInitiative({createCombatants: true});
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
@ -330,57 +336,26 @@ export default class ActorSheet5eCharacterNew extends ActorSheet5e {
|
|||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle mouse click events to convert currency to the highest possible denomination
|
||||
* @param {MouseEvent} event The originating click event
|
||||
* @private
|
||||
*/
|
||||
async _onConvertCurrency(event) {
|
||||
event.preventDefault();
|
||||
return Dialog.confirm({
|
||||
title: `${game.i18n.localize("SW5E.CurrencyConvert")}`,
|
||||
content: `<p>${game.i18n.localize("SW5E.CurrencyConvertHint")}</p>`,
|
||||
yes: () => this.actor.convertCurrency()
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
async _onDropItemCreate(itemData) {
|
||||
|
||||
// Upgrade the number of class levels a character has and add features
|
||||
// Increment the number of class levels a character instead of creating a new item
|
||||
if ( itemData.type === "class" ) {
|
||||
const cls = this.actor.itemTypes.class.find(c => c.name === itemData.name);
|
||||
const classWasAlreadyPresent = !!cls;
|
||||
|
||||
// Add new features for class level
|
||||
if ( !classWasAlreadyPresent ) {
|
||||
Actor5e.getClassFeatures(itemData).then(features => {
|
||||
this.actor.createEmbeddedEntity("OwnedItem", features);
|
||||
});
|
||||
let priorLevel = cls?.data.data.levels ?? 0;
|
||||
if ( !!cls ) {
|
||||
const next = Math.min(priorLevel + 1, 20 + priorLevel - this.actor.data.data.details.level);
|
||||
if ( next > priorLevel ) {
|
||||
itemData.levels = next;
|
||||
return cls.update({"data.levels": next});
|
||||
}
|
||||
|
||||
// If the actor already has the class, increment the level instead of creating a new item
|
||||
// then add new features as long as level increases
|
||||
if ( classWasAlreadyPresent ) {
|
||||
const lvl = cls.data.data.levels;
|
||||
const newLvl = Math.min(lvl + 1, 20 + lvl - this.actor.data.data.details.level);
|
||||
if ( !(lvl === newLvl) ) {
|
||||
cls.update({"data.levels": newLvl});
|
||||
itemData.data.levels = newLvl;
|
||||
Actor5e.getClassFeatures(itemData).then(features => {
|
||||
this.actor.createEmbeddedEntity("OwnedItem", features);
|
||||
});
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Default drop handling if levels were not added
|
||||
super._onDropItemCreate(itemData);
|
||||
}
|
||||
}
|
||||
|
||||
async function addFavorites(app, html, data) {
|
||||
// Thisfunction is adapted for the SwaltSheet from the Favorites Item
|
||||
// Tab Module created for Foundry VTT - by Felix Müller (Felix#6196 on Discord).
|
||||
|
|
|
@ -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.
|
||||
|
|
385
module/actor/sheets/newSheet/vehicle.js
Normal file
385
module/actor/sheets/newSheet/vehicle.js
Normal 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});
|
||||
}
|
||||
};
|
|
@ -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();
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import ActorSheet5e from "../base.js";
|
||||
import ActorSheet5e from "./base.js";
|
||||
|
||||
/**
|
||||
* An Actor sheet for Vehicle type actors.
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
|
|
43
module/apps/senses-config.js
Normal file
43
module/apps/senses-config.js
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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"
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{"_id":"BQEDghtJMBfhnnr6","name":"Ysannanite Form","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"lightsaberform","data":{"description":{"value":"<p>As a part of the bonus action to adopt this form, if you took the Attack action, you can engage in Double- or Two-Weapon Fighting.</p>\n<p>Additionally, until the end of your next turn, when making a ranged weapon attack while you are within 5 feet of a hostile creature, you do not have disadvantage on the attack roll.</p>"},"source":{"value":"Expanded Content"}},"flags":{"core":{"sourceId":"Item.QjBBl7KevWVvKDmy"}},"img":"systems/sw5e/packs/Icons/Lightsaber%20Forms/Ysannanite%20Form.webp","effects":[]}
|
||||
{"_id":"CJQwWFwTc98U4BpX","name":"Niman Form","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"lightsaberform","data":{"description":{"value":"<p>As a part of the bonus action to adopt this form, if you took the Attack action, you can engage in Double- or Two-Weapon Fighting.</p><p>Until the start of your next turn, you can use Wisdom or Charisma instead of Strength or Dexterity for the attack and damage rolls of your melee weapon attacks. You must use the same modifier for both rolls.</p>"},"source":{"value":"PHB"}},"flags":{},"img":"systems/sw5e/packs/Icons/Lightsaber%20Forms/Niman%20Form.webp","effects":[]}
|
||||
{"_id":"EMTzwdHtEY41M1RI","name":"Vonil/Ishu Form","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"lightsaberform","data":{"description":{"value":"<p>If you took the Attack action before adopting this form, you can direct one of your companions to strike a creature you hit with an attack. Choose a friendly creature within 5 feet of the creature you hit with an attack that can see or hear you. That creature can immediately use its reaction to make one weapon attack against the same creature you hit with an attack.</p>\n<p>If you did not take the Attack action before adopting this form, and there is a friendly creature within 15 feet of you, you can move up to 10 feet. You must end this movement within 5 feet of the friendly creature.</p>"},"source":{"value":"Expanded Content"}},"flags":{"core":{"sourceId":"Item.u7vBOskAb0MsFSlZ"}},"img":"systems/sw5e/packs/Icons/Lightsaber%20Forms/Vonil-Ishu%20Form.webp","effects":[]}
|
||||
{"_id":"GBccAWbpxgNPTz0D","name":"Makashi Form","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"lightsaberform","data":{"description":{"value":"<p>Until the start of your next turn, when a creature makes a melee weapon attack against you and misses, you can use your reaction to make one melee weapon attack against that creature.</p>"},"source":{"value":"PHB"}},"flags":{},"img":"systems/sw5e/packs/Icons/Lightsaber%20Forms/Makashi%20Form.webp","effects":[]}
|
||||
{"_id":"OmPlZtMXi2b3Vo64","name":"Jar'Kai Form","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"lightsaberform","data":{"description":{"value":"<p>As a part of the bonus action to adopt this form, if you took the Attack action, you can engage in Double- or Two-Weapon Fighting.</p><p>Each time you hit with an attack on this turn, you can move up to 5 feet without provoking opportunity attacks from the creature you hit.</p>"},"source":{"value":"PHB"}},"flags":{},"img":"systems/sw5e/packs/Icons/Lightsaber%20Forms/Jar%27Kai%20Form.webp","effects":[]}
|
||||
{"_id":"Xg7qB6sQLw3V6V4p","name":"Sokan Form","permission":{"default":0,"IpSq6HI4edO6e0Yw":3},"type":"lightsaberform","data":{"description":{"value":"<p>Until the start of your next turn, you ignore difficult terrain. Additionally, when an opponent makes a melee weapon attack against you, you can use your reaction to move to another space within 5 feet of that opponent without provoking opportunity attacks, imposing disadvantage on the triggering roll.</p>"},"source":{"value":"PHB"}},"flags":{},"img":"systems/sw5e/packs/Icons/Lightsaber%20Forms/Sokan%20Form.webp","effects":[]}
|
||||
|
|
7
sw5e.js
7
sw5e.js
|
@ -30,7 +30,8 @@ import ActorSheet5eNPCNew from "./module/actor/sheets/newSheet/npc.js";
|
|||
import ItemSheet5e from "./module/item/sheet.js";
|
||||
import ShortRestDialog from "./module/apps/short-rest.js";
|
||||
import TraitSelector from "./module/apps/trait-selector.js";
|
||||
import MovementConfig from "./module/apps/movement-config.js";
|
||||
import ActorMovementConfig from "./module/apps/movement-config.js";
|
||||
import ActorSensesConfig from "./module/apps/senses-config.js";
|
||||
|
||||
// Import Helpers
|
||||
import * as chat from "./module/chat.js";
|
||||
|
@ -58,7 +59,7 @@ Hooks.once("init", function() {
|
|||
ItemSheet5e,
|
||||
ShortRestDialog,
|
||||
TraitSelector,
|
||||
MovementConfig
|
||||
ActorMovementConfig
|
||||
},
|
||||
canvas: {
|
||||
AbilityTemplate
|
||||
|
@ -186,7 +187,7 @@ Hooks.once("ready", function() {
|
|||
// Determine whether a system migration is required and feasible
|
||||
if ( !game.user.isGM ) return;
|
||||
const currentVersion = game.settings.get("sw5e", "systemMigrationVersion");
|
||||
const NEEDS_MIGRATION_VERSION = "1.1.0";
|
||||
const NEEDS_MIGRATION_VERSION = "1.2.0";
|
||||
const COMPATIBLE_MIGRATION_VERSION = 0.80;
|
||||
const needsMigration = currentVersion && isNewerVersion(NEEDS_MIGRATION_VERSION, currentVersion);
|
||||
if ( !needsMigration ) return;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"name": "sw5e",
|
||||
"title": "SW 5th Edition",
|
||||
"description": "A comprehensive game system for running games of SW 5th Edition in the Foundry VTT environment.",
|
||||
"version": "1.1.1",
|
||||
"version": "1.2.0",
|
||||
"author": "Dev Team",
|
||||
"scripts": [],
|
||||
"esmodules": ["sw5e.js"],
|
||||
|
|
|
@ -85,7 +85,15 @@
|
|||
"units": "ft",
|
||||
"hover": false
|
||||
},
|
||||
"powercasting": "int",
|
||||
"senses": {
|
||||
"darkvision": 0,
|
||||
"blindsight": 0,
|
||||
"tremorsense": 0,
|
||||
"truesight": 0,
|
||||
"units": "ft",
|
||||
"special": ""
|
||||
},
|
||||
"powercasting": "none",
|
||||
"speed": {
|
||||
"_deprecated": true
|
||||
}
|
||||
|
@ -169,7 +177,6 @@
|
|||
}
|
||||
},
|
||||
"traits": {
|
||||
"senses": "",
|
||||
"languages": {
|
||||
"value": [],
|
||||
"custom": ""
|
||||
|
@ -245,6 +252,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"htmlFields": ["details.biography.value", "details.biography.public"],
|
||||
"character": {
|
||||
"templates": ["common", "creature"],
|
||||
"attributes": {
|
||||
|
@ -503,6 +511,7 @@
|
|||
"weight": 0,
|
||||
"price": 0,
|
||||
"attuned": false,
|
||||
"attunement": 0,
|
||||
"equipped": false,
|
||||
"rarity": "",
|
||||
"identified": true
|
||||
|
@ -568,6 +577,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"htmlFields": ["description.value", "description.chat", "description.unidentified"],
|
||||
"archetype": {
|
||||
"templates": ["archetypeDescription"],
|
||||
"className": "",
|
||||
|
@ -675,8 +685,8 @@
|
|||
"supply": 0
|
||||
},
|
||||
"preparation": {
|
||||
"mode": null,
|
||||
"prepared": false
|
||||
"mode": "prepared",
|
||||
"prepared": true
|
||||
},
|
||||
"scaling": {
|
||||
"mode": "none",
|
||||
|
|
|
@ -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"
|
||||
<input name="data.attributes.init.value" type="text" data-dtype="Number" placeholder="0"
|
||||
value="{{numberFormat data.attributes.init.value decimals=0 sign=true}}"/>
|
||||
</footer>
|
||||
</section>
|
||||
|
||||
{{!-- SPEED / MOVEMENT TYPES --}}
|
||||
<section>
|
||||
<h1>{{ localize "SW5E.Speed" }}</h1>
|
||||
<div class="attribute-value">
|
||||
<input name="data.attributes.speed.value" type="text" value="{{data.attributes.speed.value}}"
|
||||
placeholder="0" />
|
||||
<h1>{{ localize "SW5E.Movement" }}</h1>
|
||||
<<div class="attribute-value">
|
||||
<span>{{movement.primary}}</span>
|
||||
</div>
|
||||
<footer class="attribute-footer speed">
|
||||
<input type="text" class="speed" name="data.attributes.speed.special"
|
||||
value="{{data.attributes.speed.special}}" placeholder="{{ localize 'SW5E.SpeedSpecial' }}" />
|
||||
<footer class="attribute-footer">
|
||||
<span>{{movement.special}}</span>
|
||||
</footer>
|
||||
</section>
|
||||
|
||||
|
|
|
@ -56,14 +56,14 @@
|
|||
</footer>
|
||||
</section>
|
||||
<section>
|
||||
<h1>{{ localize "SW5E.Speed" }}</h1>
|
||||
<h1>{{ localize "SW5E.Movement" }}
|
||||
<a class="config-button" data-action="movement" title="{{localize 'SW5E.MovementConfig'}}"><i class="fas fa-cog"></i></a>
|
||||
</h1>
|
||||
<div class="attribute-value">
|
||||
<input name="data.attributes.speed.value" type="text" value="{{data.attributes.speed.value}}"
|
||||
placeholder="0" />
|
||||
<span>{{movement.primary}}</span>
|
||||
</div>
|
||||
<footer class="attribute-footer speed">
|
||||
<input type="text" class="speed" name="data.attributes.speed.special"
|
||||
value="{{data.attributes.speed.special}}" placeholder="{{ localize 'SW5E.SpeedSpecial' }}" />
|
||||
<footer class="attribute-footer">
|
||||
<span>{{movement.special}}</span>
|
||||
</footer>
|
||||
</section>
|
||||
</div>
|
||||
|
@ -75,6 +75,7 @@
|
|||
<button class="item active" data-tab="attributes">{{ localize "SW5E.Attributes" }}</button>
|
||||
<button class="item" data-tab="features">{{ localize "SW5E.Features" }}</button>
|
||||
<button class="item" data-tab="powerbook">{{ localize "SW5E.Powerbook" }}</button>
|
||||
<button class="item" data-tab="effects">{{ localize "SW5E.Effects" }}</button>
|
||||
<button class="item" data-tab="biography">{{ localize "SW5E.Biography" }}</button>
|
||||
</nav>
|
||||
|
||||
|
@ -175,6 +176,11 @@
|
|||
{{> "systems/sw5e/templates/actors/newActor/parts/swalt-powerbook.html"}}
|
||||
</div>
|
||||
|
||||
{{!-- Effects Tab --}}
|
||||
<div class="tab effects flexcol" data-group="primary" data-tab="effects">
|
||||
{{> "systems/sw5e/templates/actors/newActor/parts/swalt-active-effects.html"}}
|
||||
</div>
|
||||
|
||||
{{!-- Biography Tab --}}
|
||||
<div class="tab biography flexcol" data-group="primary" data-tab="biography">
|
||||
<div class="panel">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -11,11 +11,13 @@
|
|||
</label>
|
||||
<label class="{{#unless data.traits.senses}}inactive{{/unless}}">
|
||||
{{#unless isVehicle}}
|
||||
{{localize "SW5E.Senses"}}
|
||||
<input type="text" name="data.traits.senses" value="{{data.traits.senses}}"
|
||||
placeholder="{{ localize 'SW5E.None' }}" />
|
||||
{{/unless}}
|
||||
</label>
|
||||
<label>{{localize "SW5E.Senses"}}</label>
|
||||
<a class="config-button" data-action="senses" title="{{localize 'SW5E.MovementConfig'}}"><i class="fas fa-cog"></i></a>
|
||||
<ul class="traits-list">
|
||||
{{#each senses as |v k|}}
|
||||
<li class="tag {{k}}">{{v}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
<div class="languages">
|
||||
<label data-options="share-languages" class="languages">{{localize "SW5E.Languages"}}</label>
|
||||
<a class="trait-selector" data-options="languages" data-target="data.traits.languages">
|
||||
|
@ -111,7 +113,7 @@
|
|||
{{#unless isVehicle}}
|
||||
<div>
|
||||
<label>{{localize "SW5E.SpecialTraits"}}</label>
|
||||
<a class="configure-flags"><i class="fas fa-cog"></i></a>
|
||||
<a class="config-button" data-action="flags" title="{{localize 'SW5E.SpecialTraits'}}"><i class="fas fa-cog"></i></a>
|
||||
</div>
|
||||
{{/unless}}
|
||||
<ul class="passives"></ul>
|
||||
|
|
158
templates/actors/newActor/vehicle-sheet.html
Normal file
158
templates/actors/newActor/vehicle-sheet.html
Normal 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="—"
|
||||
value="{{data.attributes.hp.value}}" data-dtype="Number">
|
||||
<span class="sep"> / </span>
|
||||
<input name="data.attributes.hp.max" type="text" placeholder="—"
|
||||
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="—"
|
||||
value="{{data.attributes.ac.value}}" data-dtype="Number">
|
||||
</div>
|
||||
<footer class="attribute-footer">
|
||||
<input type="text" name="data.attributes.ac.motionless"
|
||||
placeholder="—" 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="—" 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="—"
|
||||
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"><</span>
|
||||
<input type="text" placeholder="—" data-dtype="Number"
|
||||
value="{{data.attributes.actions.thresholds.[2]}}"
|
||||
name="data.attributes.actions.thresholds.2">
|
||||
<span class="sep"><</span>
|
||||
<input type="text" placeholder="—" data-dtype="Number"
|
||||
value="{{data.attributes.actions.thresholds.[1]}}"
|
||||
name="data.attributes.actions.thresholds.1">
|
||||
<span class="sep"><</span>
|
||||
<input type="text" placeholder="—" 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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
{{/each}}
|
||||
{{/select}}
|
||||
</select>
|
||||
<span>{{localize "SW5E.AbbreviationDC"}} {{data.attributes.powerdc}}</span>
|
||||
</div>
|
||||
|
||||
<ul class="filter-list flexrow" data-filter="powerbook">
|
||||
|
@ -30,8 +31,8 @@
|
|||
<ol class="items-list inventory-list">
|
||||
{{#each powerbook as |section|}}
|
||||
<li class="items-header powerbook-header flexrow">
|
||||
<h3 class="item-name flexrow">{{section.label}}</h3>
|
||||
|
||||
<div class="item-name flexrow">
|
||||
<h3>{{section.label}}</h3>
|
||||
<div class="power-slots">
|
||||
{{#if section.usesSlots}}
|
||||
<input type="text" name="data.powers.{{section.prop}}.value" value="{{section.uses}}" placeholder="0"
|
||||
|
@ -51,7 +52,7 @@
|
|||
<span class="power-max">{{{section.slots}}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="power-school">{{localize "SW5E.PowerSchool"}}</div>
|
||||
<div class="power-action">{{localize "SW5E.PowerUsage"}}</div>
|
||||
<div class="power-target">{{localize "SW5E.PowerTarget"}}</div>
|
||||
|
@ -74,13 +75,12 @@
|
|||
{{#if item.data.uses.per }}
|
||||
<div class="item-detail power-uses">Uses {{item.data.uses.value}} / {{item.data.uses.max}}</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="power-comps">
|
||||
{{#each labels.components}}
|
||||
<span class="power-component {{this}}">{{this}}</span>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="power-school">{{labels.school}}</div>
|
||||
<div class="power-action">{{labels.activation}}</div>
|
||||
<div class="power-target" title="{{localize 'SW5E.Range'}}: {{labels.range}}">
|
||||
|
|
|
@ -12,13 +12,13 @@
|
|||
|
||||
{{#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}}">
|
||||
<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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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/>
|
||||
|
|
20
templates/apps/senses-config.html
Normal file
20
templates/apps/senses-config.html
Normal 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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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}}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue