DND5e Core 1.3.5

DND5e Core 1.3.5 modded to SW5e System

Combining with DND5e Core 1.3.2 to see one big commit since last core update

DND5e Core 1.3.2 modded to SW5e System
This commit is contained in:
supervj 2021-05-18 09:11:03 -04:00
parent c208552f70
commit b56a074697
147 changed files with 3615 additions and 1875 deletions

133
dnd5e.css
View file

@ -77,9 +77,7 @@
/* Tags */
}
.sw5e .window-content {
background: url("ui/parchment.jpg") repeat;
font-size: 13px;
color: #191813;
}
.sw5e input[type="text"],
.sw5e input[type="number"],
@ -100,6 +98,8 @@
.sw5e select:disabled,
.sw5e textarea:disabled {
color: #4b4a44;
border: 1px solid transparent !important;
outline: none !important;
}
.sw5e input:disabled:hover,
.sw5e select:disabled:hover,
@ -115,24 +115,6 @@
background: rgba(0, 0, 0, 0.1);
border: 2px groove #eeede0;
}
.sw5e label.checkbox {
flex: auto;
padding: 0;
margin: 0;
height: 22px;
line-height: 22px;
font-size: 11px;
}
.sw5e label.checkbox > input[type="checkbox"] {
width: 16px;
height: 16px;
margin: 0 2px 0 0;
position: relative;
top: 4px;
}
.sw5e label.checkbox.right > input[type="checkbox"] {
margin: 0 0 0 2px;
}
.sw5e .form-group label {
flex: 2;
color: #4b4a44;
@ -165,11 +147,12 @@
.sw5e .form-group .form-fields > *:last-child {
margin-right: 0;
}
.sw5e .form-group.stacked label {
.sw5e .form-group.stacked > label {
flex: 0 0 100%;
margin: 0;
}
.sw5e .form-group.stacked label.checkbox {
.sw5e .form-group.stacked label.checkbox,
.sw5e .form-group.stacked label.radio {
flex: auto;
text-align: left;
}
@ -193,6 +176,26 @@
background: rgba(0, 0, 0, 0.05);
}
/* ----------------------------------------- */
/* Hit Dice Config Sheet Specifically */
/* ----------------------------------------- */
.sw5e.hd-config .form-group button.increment,
.sw5e.hd-config .form-group button.decrement {
flex: 0 0 1rem;
line-height: 1rem;
}
.sw5e.hd-config .form-group button.decrement {
margin-right: 0;
}
.sw5e.hd-config .form-group span.sep {
margin: 0;
}
.sw5e.hd-config .form-group input {
flex: 0 0 2rem;
text-align: center;
margin-left: 2px;
margin-right: 2px;
}
/* ----------------------------------------- */
/* Entity Sheets Specifically */
/* ----------------------------------------- */
.sw5e.sheet {
@ -488,15 +491,59 @@
/* ----------------------------------------- */
/* Trait Selector
/* ----------------------------------------- */
#trait-selector .trait-list {
.trait-selector .trait-list {
list-style: none;
margin: 0;
padding: 0;
}
#trait-selector input[type="text"] {
.trait-selector input[type="text"] {
height: 24px;
margin: 2px;
}
/* ----------------------------------------- */
/* Actor Type Config Sheet Specifically */
/* ----------------------------------------- */
.actor-type .trait-list {
display: flex;
flex-wrap: wrap;
}
.actor-type .trait-list li {
flex-basis: 50%;
flex-grow: 1;
}
.actor-type .trait-list li.form-group {
flex-basis: 100%;
}
.actor-type label.radio {
display: flex;
flex: auto;
font-size: 12px;
line-height: 20px;
font-weight: normal;
}
.actor-type label.radio > input[type="radio"] {
margin: 0 5px 0 0;
}
.actor-type li.custom-type input[type="radio"] {
display: none;
}
/* ----------------------------------------- */
/* Add Feature Prompt Specifically */
/* ----------------------------------------- */
.sw5e.select-items-prompt .dialog-content {
margin-bottom: 1em;
}
.sw5e.select-items-prompt .items-list {
margin-top: 0.5em;
}
.sw5e.select-items-prompt .item-name > label,
.sw5e.select-items-prompt .item-image,
.sw5e.select-items-prompt input {
cursor: pointer;
}
.sw5e.select-items-prompt .item-name > label {
align-items: center;
}
.sw5e.sheet.actor {
/* ----------------------------------------- */
/* Sheet Header */
@ -528,6 +575,9 @@
/* Powerbook */
/* ----------------------------------------- */
/* ----------------------------------------- */
/* Features Tab */
/* ----------------------------------------- */
/* ----------------------------------------- */
/* TinyMCE */
/* ----------------------------------------- */
}
@ -581,10 +631,12 @@
height: 30px;
line-height: 30px;
}
.sw5e.sheet.actor .sheet-header .attributes .movement h4.attribute-name {
.sw5e.sheet.actor .sheet-header .attributes .movement h4.attribute-name,
.sw5e.sheet.actor .sheet-header .attributes .hit-dice h4.attribute-name {
position: relative;
}
.sw5e.sheet.actor .sheet-header .attributes .movement .config-button {
.sw5e.sheet.actor .sheet-header .attributes .movement .config-button,
.sw5e.sheet.actor .sheet-header .attributes .hit-dice .config-button {
position: absolute;
display: none;
right: 0;
@ -592,7 +644,8 @@
font-size: 12px;
font-weight: normal;
}
.sw5e.sheet.actor .sheet-header .attributes .movement:hover .config-button {
.sw5e.sheet.actor .sheet-header .attributes .movement:hover .config-button,
.sw5e.sheet.actor .sheet-header .attributes .hit-dice:hover .config-button {
display: block;
}
.sw5e.sheet.actor .sheet-header .attributes input.temphp {
@ -1057,6 +1110,9 @@
.sw5e.sheet.actor .powerbook-empty .item-controls {
flex: 1;
}
.sw5e.sheet.actor .features i.original-class {
color: #4b4a44;
}
.sw5e.sheet.actor .editor {
padding: 0 8px;
}
@ -1421,7 +1477,7 @@
text-align: right;
padding-right: 5px;
}
.sw5e.sheet.actor.character .resource .attribute-value input {
.sw5e.sheet.actor.character .resource .attribute-value > input {
flex: 0 0 25%;
}
.sw5e.sheet.actor.character .resource .attribute-value label.recharge {
@ -1431,6 +1487,7 @@
font-size: 11px;
text-align: center;
color: #4b4a44;
align-items: center;
}
.sw5e.sheet.actor.character .resource .attribute-value label.recharge input[type="checkbox"] {
height: 14px;
@ -1528,6 +1585,26 @@
.sw5e.sheet.actor.npc .summary {
font-size: 18px;
}
.sw5e.sheet.actor.npc .summary .creature-type {
display: flex;
justify-content: space-between;
width: 1em;
padding: 0 3px;
}
.sw5e.sheet.actor.npc .summary .creature-type span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sw5e.sheet.actor.npc .summary .creature-type .config-button {
display: none;
font-size: 12px;
font-weight: normal;
line-height: 2em;
}
.sw5e.sheet.actor.npc .summary .creature-type:hover .config-button {
display: block;
}
.sw5e.sheet.actor.vehicle .features .item-controls {
flex: 0 0 68px;
}

View file

@ -13,11 +13,12 @@ import { SW5E } from "./module/config.js";
import { registerSystemSettings } from "./module/settings.js";
import { preloadHandlebarsTemplates } from "./module/templates.js";
import { _getInitiativeFormula } from "./module/combat.js";
import { measureDistances, getBarAttribute } from "./module/canvas.js";
import { measureDistances } from "./module/canvas.js";
// Import Entities
// Import Documents
import Actor5e from "./module/actor/entity.js";
import Item5e from "./module/item/entity.js";
import { TokenDocument5e, Token5e } from "./module/token.js";
// Import Applications
import AbilityTemplate from "./module/pixi/ability-template.js";
@ -56,7 +57,8 @@ Hooks.once("init", function() {
ItemSheet5e,
ShortRestDialog,
TraitSelector,
ActorMovementConfig
ActorMovementConfig,
ActorSensesConfig
},
canvas: {
AbilityTemplate
@ -66,6 +68,8 @@ Hooks.once("init", function() {
entities: {
Actor5e,
Item5e,
TokenDocument5e,
Token5e,
},
macros: macros,
migrations: migrations,
@ -74,10 +78,15 @@ Hooks.once("init", function() {
// Record Configuration Values
CONFIG.SW5E = SW5E;
CONFIG.Actor.entityClass = Actor5e;
CONFIG.Item.entityClass = Item5e;
CONFIG.Actor.documentClass = Actor5e;
CONFIG.Item.documentClass = Item5e;
CONFIG.Token.documentClass = TokenDocument5e;
CONFIG.Token.objectClass = Token5e;
CONFIG.time.roundTime = 6;
CONFIG.Dice.DamageRoll = dice.DamageRoll;
CONFIG.Dice.D20Roll = dice.D20Roll;
// 5e cone RAW should be 53.13 degrees
CONFIG.MeasuredTemplate.defaults.angle = 53.13;
@ -86,7 +95,11 @@ Hooks.once("init", function() {
// Patch Core Functions
CONFIG.Combat.initiative.formula = "1d20 + @attributes.init.mod + @attributes.init.prof + @attributes.init.bonus";
Combat.prototype._getInitiativeFormula = _getInitiativeFormula;
Combatant.prototype._getInitiativeFormula = _getInitiativeFormula;
// Register Roll Extensions
CONFIG.Dice.rolls.push(dice.D20Roll);
CONFIG.Dice.rolls.push(dice.DamageRoll);
// Register sheet application classes
Actors.unregisterSheet("core", ActorSheet);
@ -112,7 +125,7 @@ Hooks.once("init", function() {
});
// Preload Handlebars Templates
preloadHandlebarsTemplates();
return preloadHandlebarsTemplates();
});
@ -167,9 +180,11 @@ 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.2.1";
const NEEDS_MIGRATION_VERSION = "1.3.4";
const COMPATIBLE_MIGRATION_VERSION = 0.80;
const needsMigration = currentVersion && isNewerVersion(NEEDS_MIGRATION_VERSION, currentVersion);
const totalDocuments = game.actors.size + game.scenes.size + game.items.size;
if ( !currentVersion && totalDocuments === 0 ) return game.settings.set("sw5e", "systemMigrationVersion", game.system.data.version);
const needsMigration = !currentVersion || isNewerVersion(NEEDS_MIGRATION_VERSION, currentVersion);
if ( !needsMigration ) return;
// Perform the migration
@ -185,13 +200,9 @@ Hooks.once("ready", function() {
/* -------------------------------------------- */
Hooks.on("canvasInit", function() {
// Extend Diagonal Measurement
canvas.grid.diagonalRule = game.settings.get("sw5e", "diagonalMovement");
SquareGrid.prototype.measureDistances = measureDistances;
// Extend Token Resource Bars
Token.prototype.getBarAttribute = getBarAttribute;
});
@ -215,7 +226,7 @@ Hooks.on("renderChatLog", (app, html, data) => Item5e.chatListeners(html));
Hooks.on("renderChatPopout", (app, html, data) => Item5e.chatListeners(html));
Hooks.on('getActorDirectoryEntryContext', Actor5e.addDirectoryContextOptions);
// TODO I should remove this
// FIXME: This helper is needed for the vehicle sheet. It should probably be refactored.
Handlebars.registerHelper('getProperty', function (data, property) {
return getProperty(data, property);
});

View file

@ -49,6 +49,7 @@
"SW5E.AbilityUseConsumableLabel": "{max} per {per}",
"SW5E.AbilityUseCast": "Cast Power",
"SW5E.AbilityUseUse": "Use Ability",
"SW5E.AbilityUseConfig": "Usage Configuration",
"SW5E.Action": "Action",
"SW5E.ActionPl": "Actions",
"SW5E.ActionAbil": "Ability Check",
@ -62,7 +63,10 @@
"SW5E.ActionUtil": "Utility",
"SW5E.ActionWarningNoItem": "The requested item {item} no longer exists on Actor {name}",
"SW5E.ActionWarningNoToken": "You must have one or more controlled Tokens in order to use this option.",
"SW5E.ActorWarningInvalidItem": "{itemType} items cannot be added to a {actorType}.",
"SW5E.Add": "Add",
"SW5E.AddEmbeddedItemPromptHint": "Do you want to add these items to your character sheet?",
"SW5E.SelectItemsPromptTitle": "Select Items",
"SW5E.Advantage": "Advantage",
"SW5E.Alignment": "Alignment",
"SW5E.AlignmentCE": "Chaotic Evil",
@ -75,6 +79,7 @@
"SW5E.AlignmentNG": "Neutral Good",
"SW5E.AlignmentTN": "True Neutral",
"SW5E.Appearance": "Appearance",
"SW5E.Apply": "Apply",
"SW5E.Attunement": "Attunement",
"SW5E.AttunementNone": "Attunement Not Required",
"SW5E.AttunementRequired": "Attunement Required",
@ -116,7 +121,11 @@
"SW5E.ChatContextHalfDamage": "Apply Half Damage",
"SW5E.ChatFlavor": "Chat Message Flavor",
"SW5E.ClassLevels": "Class Levels",
"SW5E.ClassMakeOriginal": "Original Class",
"SW5E.ClassMakeOriginalHint": "First class taken by character used to determine certain class traits when multiclassing.",
"SW5E.ClassName": "Class Name",
"SW5E.ClassOriginal": "Original Class",
"SW5E.ClassSaves": "Saving Throws",
"SW5E.ClassSkillsNumber": "Number of Starting Skills",
"SW5E.ClassSkillsChosen": "Chosen Class Skills",
"SW5E.ComponentMaterial": "Material",
@ -170,8 +179,46 @@
"SW5E.CoverThreeQuarters": "Three Quarters",
"SW5E.CoverTotal": "Total",
"SW5E.CostGP": "Cost (GP)",
"SW5E.CreatureAberration": "Aberration",
"SW5E.CreatureAberrationPl": "Aberrations",
"SW5E.CreatureBeast": "Beast",
"SW5E.CreatureBeastPl": "Beasts",
"SW5E.CreatureCelestial": "Celestial",
"SW5E.CreatureCelestialPl": "Celestials",
"SW5E.CreatureConstruct": "Construct",
"SW5E.CreatureConstructPl": "Constructs",
"SW5E.CreatureDragon": "Dragon",
"SW5E.CreatureDragonPl": "Dragons",
"SW5E.CreatureElemental": "Elemental",
"SW5E.CreatureElementalPl": "Elementals",
"SW5E.CreatureFey": "Fey",
"SW5E.CreatureFeyPl": "Fey",
"SW5E.CreatureFiend": "Fiend",
"SW5E.CreatureFiendPl": "Fiends",
"SW5E.CreatureGiant": "Giant",
"SW5E.CreatureGiantPl": "Giants",
"SW5E.CreatureHumanoid": "Humanoid",
"SW5E.CreatureHumanoidPl": "Humanoids",
"SW5E.CreatureMonstrosity": "Monstrosity",
"SW5E.CreatureMonstrosityPl": "Monstrosities",
"SW5E.CreatureOoze": "Ooze",
"SW5E.CreatureOozePl": "Oozes",
"SW5E.CreaturePlant": "Plant",
"SW5E.CreaturePlantPl": "Plants",
"SW5E.CreatureType": "Creature Type",
"SW5E.CreatureTypeTitle": "Configure Creature Type",
"SW5E.CreatureSwarm": "Swarm",
"SW5E.CreatureSwarmSize": "Swarm Size",
"SW5E.CreatureSwarmPhrase": "Swarm of {size} {type}",
"SW5E.CreatureTypeConfig": "Configure Creature Type",
"SW5E.CreatureTypeSelectorCustom": "Custom Type",
"SW5E.CreatureTypeSelectorSubtype": "Subtype",
"SW5E.CreatureUndead": "Undead",
"SW5E.CreatureUndeadPl": "Undead",
"SW5E.Crewed": "Crewed",
"SW5E.Critical": "Critical",
"SW5E.CriticalHit": "Critical Hit",
"SW5E.PowerfulCritical": "Powerful Critical",
"SW5E.Currency": "Currency",
"SW5E.CurrencyCP": "Copper",
"SW5E.CurrencyConvert": "Convert All Currency",
@ -238,14 +285,20 @@
"SW5E.EffectToggle": "Toggle Effect",
"SW5E.EffectEdit": "Edit Effect",
"SW5E.EffectDelete": "Delete Effect",
"SW5E.EffectTemporary": "Temporary Effects",
"SW5E.EffectPassive": "Passive Effects",
"SW5E.EffectInactive": "Inactive Effects",
"SW5E.EffectNew": "New Effect",
"SW5E.ItemTypeClass": "Class",
"SW5E.ItemTypeClassPl": "Class Levels",
"SW5E.ItemTypeConsumable": "Consumable",
"SW5E.ItemTypeConsumablePl": "Consumables",
"SW5E.ItemTypeContainer": "Container",
"SW5E.ItemTypeBackpack": "Container",
"SW5E.ItemTypeContainerPl": "Containers",
"SW5E.ItemTypeEquipment": "Equipment",
"SW5E.ItemTypeEquipmentPl": "Equipment",
"SW5E.ItemTypeFeat": "Feature",
"SW5E.ItemTypeLoot": "Loot",
"SW5E.ItemTypeLootPl": "Loot",
"SW5E.ItemTypeTool": "Tool",
@ -263,6 +316,7 @@
"SW5E.FeatureRechargeResult": "1d6 Result",
"SW5E.FeatureUsage": "Feature Usage",
"SW5E.Features": "Features",
"SW5E.Feats": "Feats",
"SW5E.FeetAbbr": "ft.",
"SW5E.Filter": "Filter",
"SW5E.FilterNoPowers": "No powers found for this set of filters.",
@ -307,6 +361,10 @@
"SW5E.HealthConditions": "Health Conditions",
"SW5E.HPFormula": "Health Formula",
"SW5E.HitDice": "Hit Dice",
"SW5E.HitDiceConfig": "Adjust Hit Dice",
"SW5E.HitDiceConfigHint": "Adjust remaining hit dice levels for each class.",
"SW5E.HitDiceMax": "Maximum Hit Dice",
"SW5E.HitDiceRemaining": "Remaining Hit Dice",
"SW5E.HitDiceRoll": "Roll Hit Dice",
"SW5E.HitDiceUsed": "Hit Dice Used",
"SW5E.HitDiceWarn": "{name} has no available {formula} Hit Dice remaining!",
@ -395,6 +453,7 @@
"SW5E.LongRestGritty": "Long Rest (7 days)",
"SW5E.LongRestEpic": "Long Rest (1 hour)",
"SW5E.LongRestOvernight": "Long Rest (New Day)",
"SW5E.LongRestHint": "Take a long rest? On a long rest you will recover hit points, half your maximum hit dice, class resources, limited use item charges, and power slots.",
"SW5E.LongRestResult": "{name} takes a long rest and recovers {health} Hit Points and {dice} Hit Dice.",
"SW5E.LongRestResultHitDice": "{name} takes a long rest and recovers {dice} Hit Dice.",
"SW5E.LongRestResultHitPoints": "{name} takes a long rest and recovers {health} Hit Points.",
@ -438,6 +497,7 @@
"SW5E.Proficient": "Proficient",
"SW5E.Quantity": "Quantity",
"SW5E.Race": "Race",
"SW5E.RacialTraits": "Racial Traits",
"SW5E.Range": "Range",
"SW5E.Rarity": "Rarity",
"SW5E.Reaction": "Reaction",
@ -448,11 +508,12 @@
"SW5E.ResourcePrimary": "Resource 1",
"SW5E.ResourceSecondary": "Resource 2",
"SW5E.ResourceTertiary": "Resource 3",
"SW5E.Rest": "Rest",
"SW5E.RestL": "L. Rest",
"SW5E.RestS": "S. Rest",
"SW5E.Ritual": "Ritual",
"SW5E.Roll": "Roll",
"SW5E.RollExample": "e.g. +1d4",
"SW5E.RollExample": "e.g. 1d4",
"SW5E.RollMode": "Roll Mode",
"SW5E.RollSituationalBonus": "Situational Bonus?",
"SW5E.Movement": "Movement",
@ -465,6 +526,8 @@
"SW5E.MovementFly": "Fly",
"SW5E.MovementSwim": "Swim",
"SW5E.MovementUnits": "Units",
"SW5E.NewDay": "Is New Day?",
"SW5E.NewDayHint": "Recover limited use abilities which recharge \"per day\"?",
"SW5E.Senses": "Senses",
"SW5E.SensesConfig": "Configure Senses",
"SW5E.SensesConfigHint": "Configure any special sensory perception abilities that this actor possesses.",
@ -525,12 +588,13 @@
"SW5E.SkillSte": "Stealth",
"SW5E.SkillSur": "Survival",
"SW5E.SkillPromptTitle": "{skill} Skill Check",
"SW5E.Skip": "Skip",
"SW5E.Source": "Source",
"SW5E.Special": "Special",
"SW5E.SpecialTraits": "Special Traits",
"SW5E.Speed": "Speed",
"SW5E.SpeedSpecial": "Special Movement",
"SW5E.PowerAbility": "Powercasting",
"SW5E.PowerAbility": "Powercasting Ability",
"SW5E.PowerAdd": "Add Power",
"SW5E.PowerCantrip": "Cantrip",
"SW5E.PowerCastConsume": "Consume Power Slot?",
@ -626,6 +690,7 @@
"SW5E.TraitToolProf": "Tool Proficiencies",
"SW5E.TraitWeaponProf": "Weapon Proficiencies",
"SW5E.Type": "Type",
"SW5E.Uncrewed": "Uncrewed",
"SW5E.Unequipped": "Not Equipped",
"SW5E.Unlimited": "Unlimited",
"SW5E.Usage": "Usage",
@ -698,9 +763,11 @@
"SETTINGS.5eInitTBN": "Initiative Dexterity Tiebreaker",
"SETTINGS.5eNoExpL": "Remove experience bars from character sheets.",
"SETTINGS.5eNoExpN": "Disable Experience Tracking",
"SETTINGS.5eReset": "Reset",
"SETTINGS.5eRestL": "Configure which rest variant should be used for games within this system.",
"SETTINGS.5eRestN": "Rest Variant",
"SETTINGS.5eRestPHB": "Player's Handbook (LR: 8 hours, SR: 1 hour)",
"SETTINGS.5eRestGritty": "Gritty Realism (LR: 7 days, SR: 8 hours)",
"SETTINGS.5eRestEpic": "Epic Heroism (LR: 1 hour, SR: 1 min)"
"SETTINGS.5eRestEpic": "Epic Heroism (LR: 1 hour, SR: 1 min)",
"SETTINGS.5eUndoChanges": "Undo Changes"
}

View file

@ -71,7 +71,7 @@
}
// Movement Configuration
.movement {
.movement, .hit-dice {
h4.attribute-name {
position: relative;
}
@ -646,6 +646,15 @@
// Empty powerbook controls
.powerbook-empty .item-controls { flex: 1; }
/* ----------------------------------------- */
/* Features Tab */
/* ----------------------------------------- */
// Original class icon
.features i.original-class {
color: #4b4a44
}
/* ----------------------------------------- */
/* TinyMCE */
/* ----------------------------------------- */
@ -669,4 +678,4 @@
overflow-y: auto;
scrollbar-width: thin;
}
}
}

View file

@ -10,9 +10,7 @@
.sw5e {
.window-content {
background: @sheetBackground;
font-size: 13px;
color: @colorDark;
}
/* ----------------------------------------- */
@ -44,6 +42,8 @@
select:disabled,
textarea:disabled {
color: @colorOlive;
border: 1px solid transparent !important;
outline: none !important;
&:hover,
&:focus {
box-shadow: none !important;
@ -58,28 +58,6 @@
border: @borderGroove;
}
// Checkbox Labels
// TODO: THIS CAN BE MOSTLY REMOVED NOW THAT IT IS IN CORE, see core forms.less
label.checkbox {
flex: auto;
padding: 0;
margin: 0;
height: 22px;
line-height: 22px;
font-size: 11px;
> input[type="checkbox"] {
width: 16px;
height: 16px;
margin: 0 2px 0 0;
position: relative;
top: 4px;
}
&.right > input[type="checkbox"] {
margin: 0 0 0 2px;
}
}
/* Form Groups */
.form-group {
label {
@ -98,11 +76,12 @@
// Stacked Groups
.form-group.stacked {
label {
> label {
flex: 0 0 100%;
margin: 0;
}
label.checkbox {
label.checkbox,
label.radio {
flex: auto;
text-align: left;
}
@ -131,6 +110,35 @@
}
/* ----------------------------------------- */
/* Hit Dice Config Sheet Specifically */
/* ----------------------------------------- */
.sw5e.hd-config {
.form-group {
button.increment, button.decrement {
flex: 0 0 1rem;
line-height: 1rem;
}
button.decrement {
margin-right: 0;
}
span.sep {
margin: 0;
}
input {
flex: 0 0 2rem;
text-align: center;
margin-left: 2px;
margin-right: 2px;
}
}
}
/* ----------------------------------------- */
/* Entity Sheets Specifically */
/* ----------------------------------------- */
@ -475,7 +483,7 @@
/* Trait Selector
/* ----------------------------------------- */
#trait-selector {
.trait-selector {
.trait-list {
list-style: none;
margin: 0;
@ -486,4 +494,57 @@
height: 24px;
margin: 2px;
}
}
}
/* ----------------------------------------- */
/* Actor Type Config Sheet Specifically */
/* ----------------------------------------- */
.actor-type {
.trait-list {
display: flex;
flex-wrap: wrap;
li {
flex-basis: 50%;
flex-grow: 1;
}
li.form-group {
flex-basis: 100%;
}
}
label.radio {
display: flex;
flex: auto;
font-size: 12px;
line-height: 20px;
font-weight: normal;
> input[type="radio"] {
margin: 0 5px 0 0;
}
}
li.custom-type input[type="radio"] {
display: none;
}
}
/* ----------------------------------------- */
/* Add Feature Prompt Specifically */
/* ----------------------------------------- */
.sw5e.select-items-prompt {
.dialog-content {
margin-bottom: 1em;
}
.items-list {
margin-top: 0.5em;
}
.item-name > label, .item-image, input {
cursor: pointer;
}
.item-name > label {
align-items: center;
}
}

View file

@ -89,7 +89,7 @@
// Custom Resources
.resource .attribute-value {
input {
> input {
flex: 0 0 25%;
}
label.recharge {
@ -99,6 +99,7 @@
font-size: 11px;
text-align: center;
color: @colorOlive;
align-items: center;
input[type="checkbox"] {
height: 14px;
width: 14px;

View file

@ -30,5 +30,28 @@
.summary {
font-size: 18px;
.creature-type {
display: flex;
justify-content: space-between;
width: 1em;
padding: 0 3px;
span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.config-button {
display: none;
font-size: 12px;
font-weight: normal;
line-height: 2em;
}
&:hover .config-button {
display: block;
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,10 @@
import Item5e from "../../item/entity.js";
import TraitSelector from "../../apps/trait-selector.js";
import ActorSheetFlags from "../../apps/actor-flags.js";
import ActorHitDiceConfig from "../../apps/hit-dice-config.js";
import ActorMovementConfig from "../../apps/movement-config.js";
import ActorSensesConfig from "../../apps/senses-config.js";
import ActorTypeConfig from "../../apps/actor-type.js";
import {SW5E} from '../../config.js';
import {onManageActiveEffect, prepareActiveEffectCategories} from "../../effects.js";
@ -44,6 +46,14 @@ export default class ActorSheet5e extends ActorSheet {
/* -------------------------------------------- */
/**
* A set of item types that should be prevented from being dropped on this type of actor sheet.
* @type {Set<string>}
*/
static unsupportedItemTypes = new Set();
/* -------------------------------------------- */
/** @override */
get template() {
if ( !game.user.isGM && this.actor.limited ) return "systems/sw5e/templates/actors/limited-sheet.html";
@ -53,43 +63,50 @@ export default class ActorSheet5e extends ActorSheet {
/* -------------------------------------------- */
/** @override */
getData() {
getData(options) {
// Basic data
let isOwner = this.entity.owner;
let isOwner = this.actor.isOwner;
const data = {
owner: isOwner,
limited: this.entity.limited,
limited: this.actor.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',
isCharacter: this.actor.type === "character",
isNPC: this.actor.type === "npc",
isVehicle: this.actor.type === 'vehicle',
config: CONFIG.SW5E,
rollData: this.actor.getRollData.bind(this.actor)
};
// 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;
});
// The Actor's data
const actorData = this.actor.data.toObject(false);
data.actor = actorData;
data.data = actorData.data;
// Owned Items
data.items = actorData.items;
for ( let i of data.items ) {
const item = this.actor.items.get(i._id);
i.labels = item.labels;
}
data.items.sort((a, b) => (a.sort || 0) - (b.sort || 0));
data.data = data.actor.data;
// Labels and filters
data.labels = this.actor.labels || {};
data.filters = this._filters;
// Ability Scores
for ( let [a, abl] of Object.entries(data.actor.data.abilities)) {
for ( let [a, abl] of Object.entries(actorData.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)) {
if ( actorData.data.skills ) {
for (let [s, skl] of Object.entries(actorData.data.skills)) {
skl.ability = CONFIG.SW5E.abilityAbbreviations[skl.ability];
skl.icon = this._getProficiencyIcon(skl.value);
skl.hover = CONFIG.SW5E.proficiencyLevels[skl.value];
@ -98,19 +115,19 @@ export default class ActorSheet5e extends ActorSheet {
}
// Movement speeds
data.movement = this._getMovementSpeed(data.actor);
data.movement = this._getMovementSpeed(actorData);
// Senses
data.senses = this._getSenses(data.actor);
data.senses = this._getSenses(actorData);
// Update traits
this._prepareTraits(data.actor.data.traits);
this._prepareTraits(actorData.data.traits);
// Prepare owned items
this._prepareItems(data);
// Prepare active effects
data.effects = prepareActiveEffectCategories(this.entity.effects);
data.effects = prepareActiveEffectCategories(this.actor.effects);
// Return data to the sheet
return data
@ -221,7 +238,7 @@ export default class ActorSheet5e extends ActorSheet {
* @private
*/
_preparePowerbook(data, powers) {
const owner = this.actor.owner;
const owner = this.actor.isOwner;
const levels = data.data.powers;
const powerbook = {};
@ -278,7 +295,9 @@ export default class ActorSheet5e extends ActorSheet {
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, {
const level = game.i18n.localize(`SW5E.PowerLevel${levels.pact.level}`);
const label = `${config}${level}`;
registerSection("pact", sections.pact, label, {
prepMode: "pact",
value: l.value,
max: l.max,
@ -381,10 +400,7 @@ export default class ActorSheet5e extends ActorSheet {
/* 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
*/
/** @inheritdoc */
activateListeners(html) {
// Activate Item Filters
@ -395,6 +411,9 @@ export default class ActorSheet5e extends ActorSheet {
// Item summaries
html.find('.item .item-name.rollable h4').click(event => this._onItemSummary(event));
// View Item Sheets
html.find('.item-edit').click(this._onItemEdit.bind(this));
// Editable Only Listeners
if ( this.isEditable ) {
@ -417,22 +436,20 @@ export default class ActorSheet5e extends ActorSheet {
// 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));
html.find(".effect-control").click(ev => onManageActiveEffect(ev, this.actor));
}
// Owner Only Listeners
if ( this.actor.owner ) {
if ( this.actor.isOwner ) {
// Ability Checks
html.find('.ability-name').click(this._onRollAbilityTest.bind(this));
// Roll Skill Checks
html.find('.skill-name').click(this._onRollSkillCheck.bind(this));
@ -494,17 +511,25 @@ export default class ActorSheet5e extends ActorSheet {
_onConfigMenu(event) {
event.preventDefault();
const button = event.currentTarget;
let app;
switch ( button.dataset.action ) {
case "hit-dice":
app = new ActorHitDiceConfig(this.object);
break;
case "movement":
new ActorMovementConfig(this.object).render(true);
app = new ActorMovementConfig(this.object);
break;
case "flags":
new ActorSheetFlags(this.object).render(true);
app = new ActorSheetFlags(this.object);
break;
case "senses":
new ActorSensesConfig(this.object).render(true);
app = new ActorSensesConfig(this.object);
break;
case "type":
new ActorTypeConfig(this.object).render(true);
break;
}
app?.render(true);
}
/* -------------------------------------------- */
@ -538,7 +563,7 @@ export default class ActorSheet5e extends ActorSheet {
/** @override */
async _onDropActor(event, data) {
const canPolymorph = game.user.isGM || (this.actor.owner && game.settings.get('sw5e', 'allowPolymorphing'));
const canPolymorph = game.user.isGM || (this.actor.isOwner && game.settings.get('sw5e', 'allowPolymorphing'));
if ( !canPolymorph ) return false;
// Get the target actor
@ -613,15 +638,40 @@ export default class ActorSheet5e extends ActorSheet {
/** @override */
async _onDropItemCreate(itemData) {
// Check to make sure items of this type are allowed on this actor
if ( this.constructor.unsupportedItemTypes.has(itemData.type) ) {
return ui.notifications.warn(game.i18n.format("SW5E.ActorWarningInvalidItem", {
itemType: game.i18n.localize(CONFIG.Item.typeLabels[itemData.type]),
actorType: game.i18n.localize(CONFIG.Actor.typeLabels[this.actor.type])
}));
}
// 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;
}
// Ignore certain statuses
if ( itemData.data ) {
["attunement", "equipped", "proficient", "prepared"].forEach(k => delete itemData.data[k]);
// Ignore certain statuses
["equipped", "proficient", "prepared"].forEach(k => delete itemData.data[k]);
// Downgrade ATTUNED to REQUIRED
itemData.data.attunement = Math.min(itemData.data.attunement, CONFIG.SW5E.attunementTypes.REQUIRED);
}
// Stack identical consumables
if ( itemData.type === "consumable" && itemData.flags.core?.sourceId ) {
const similarItem = this.actor.items.find(i => {
const sourceId = i.getFlag("core", "sourceId");
return sourceId && (sourceId === itemData.flags.core?.sourceId) &&
(i.type === "consumable");
});
if ( similarItem ) {
return similarItem.update({
'data.quantity': similarItem.data.data.quantity + Math.max(itemData.data.quantity, 1)
});
}
}
// Create the owned item as normal
@ -662,7 +712,7 @@ export default class ActorSheet5e extends ActorSheet {
async _onUsesChange(event) {
event.preventDefault();
const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.getOwnedItem(itemId);
const item = this.actor.items.get(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 });
@ -677,7 +727,7 @@ export default class ActorSheet5e extends ActorSheet {
_onItemRoll(event) {
event.preventDefault();
const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.getOwnedItem(itemId);
const item = this.actor.items.get(itemId);
return item.roll();
}
@ -691,7 +741,7 @@ export default class ActorSheet5e extends ActorSheet {
_onItemRecharge(event) {
event.preventDefault();
const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.getOwnedItem(itemId);
const item = this.actor.items.get(itemId);
return item.rollRecharge();
};
@ -704,8 +754,8 @@ export default class ActorSheet5e extends ActorSheet {
_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});
item = this.actor.items.get(li.data("item-id")),
chatData = item.getChatData({secrets: this.actor.isOwner});
// Toggle summary
if ( li.hasClass("expanded") ) {
@ -734,12 +784,12 @@ export default class ActorSheet5e extends ActorSheet {
const header = event.currentTarget;
const type = header.dataset.type;
const itemData = {
name: game.i18n.format("SW5E.ItemNew", {type: type.capitalize()}),
name: game.i18n.format("SW5E.ItemNew", {type: game.i18n.localize(`SW5E.ItemType${type.capitalize()}`)}),
type: type,
data: duplicate(header.dataset)
data: foundry.utils.deepClone(header.dataset)
};
delete itemData.data["type"];
return this.actor.createEmbeddedEntity("OwnedItem", itemData);
return this.actor.createEmbeddedDocuments("Item", [itemData]);
}
/* -------------------------------------------- */
@ -752,8 +802,8 @@ export default class ActorSheet5e extends ActorSheet {
_onItemEdit(event) {
event.preventDefault();
const li = event.currentTarget.closest(".item");
const item = this.actor.getOwnedItem(li.dataset.itemId);
item.sheet.render(true);
const item = this.actor.items.get(li.dataset.itemId);
return item.sheet.render(true);
}
/* -------------------------------------------- */
@ -766,7 +816,8 @@ export default class ActorSheet5e extends ActorSheet {
_onItemDelete(event) {
event.preventDefault();
const li = event.currentTarget.closest(".item");
this.actor.deleteOwnedItem(li.dataset.itemId);
const item = this.actor.items.get(li.dataset.itemId);
if ( item ) return item.delete();
}
/* -------------------------------------------- */
@ -779,7 +830,7 @@ export default class ActorSheet5e extends ActorSheet {
_onRollAbilityTest(event) {
event.preventDefault();
let ability = event.currentTarget.parentElement.dataset.ability;
this.actor.rollAbility(ability, {event: event});
return this.actor.rollAbility(ability, {event: event});
}
/* -------------------------------------------- */
@ -792,7 +843,7 @@ export default class ActorSheet5e extends ActorSheet {
_onRollSkillCheck(event) {
event.preventDefault();
const skill = event.currentTarget.parentElement.dataset.skill;
this.actor.rollSkill(skill, {event: event});
return this.actor.rollSkill(skill, {event: event});
}
/* -------------------------------------------- */
@ -805,7 +856,7 @@ export default class ActorSheet5e extends ActorSheet {
_onToggleAbilityProficiency(event) {
event.preventDefault();
const field = event.currentTarget.previousElementSibling;
this.actor.update({[field.name]: 1 - parseInt(field.value)});
return this.actor.update({[field.name]: 1 - parseInt(field.value)});
}
/* -------------------------------------------- */
@ -822,7 +873,7 @@ export default class ActorSheet5e extends ActorSheet {
const filter = li.dataset.filter;
if ( set.has(filter) ) set.delete(filter);
else set.add(filter);
this.render();
return this.render();
}
/* -------------------------------------------- */
@ -838,7 +889,7 @@ export default class ActorSheet5e extends ActorSheet {
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)
return new TraitSelector(this.actor, options).render(true)
}
/* -------------------------------------------- */
@ -846,15 +897,14 @@ export default class ActorSheet5e extends ActorSheet {
/** @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()
});
if ( this.actor.isPolymorphed ) {
buttons.unshift({
label: 'SW5E.PolymorphRestoreTransformation',
class: "restore-transformation",
icon: "fas fa-backward",
onclick: () => this.actor.revertOriginalForm()
});
}
return buttons;
}
}
}

View file

@ -76,7 +76,7 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
let [items, powers, feats, classes] = data.items.reduce((arr, item) => {
// Item details
item.img = item.img || DEFAULT_TOKEN;
item.img = item.img || CONST.DEFAULT_TOKEN;
item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
item.attunement = {
[CONFIG.SW5E.attunementTypes.REQUIRED]: {
@ -100,6 +100,9 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
// Item toggle state
this._prepareItemToggleState(item);
// Primary Class
if ( item.type === "class" ) item.isOriginalClass = ( item._id === this.actor.data.data.details.originalClass );
// Classify items into types
if ( item.type === "power" ) arr[1].push(item);
else if ( item.type === "feat" ) arr[2].push(item);
@ -137,7 +140,7 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
if ( f.data.activation.type ) features.active.items.push(f);
else features.passive.items.push(f);
}
classes.sort((a, b) => b.levels - a.levels);
classes.sort((a, b) => b.data.levels - a.data.levels);
features.classes.items = classes;
// Assign and return
@ -177,11 +180,11 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
/**
* Activate event listeners using the prepared sheet HTML
* @param html {HTML} The prepared HTML object ready to be rendered into the DOM
* @param html {jQuery} The prepared HTML object ready to be rendered into the DOM
*/
activateListeners(html) {
super.activateListeners(html);
if ( !this.options.editable ) return;
if ( !this.isEditable ) return;
// Item State Toggling
html.find('.item-toggle').click(this._onToggleItem.bind(this));
@ -228,7 +231,7 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
_onToggleItem(event) {
event.preventDefault();
const itemId = event.currentTarget.closest(".item").dataset.itemId;
const item = this.actor.getOwnedItem(itemId);
const item = this.actor.items.get(itemId);
const attr = item.data.type === "power" ? "data.preparation.prepared" : "data.equipped";
return item.update({[attr]: !getProperty(item.data, attr)});
}
@ -278,6 +281,6 @@ export default class ActorSheet5eCharacter extends ActorSheet5e {
}
// Default drop handling if levels were not added
super._onDropItemCreate(itemData);
return super._onDropItemCreate(itemData);
}
}

View file

@ -1,3 +1,4 @@
import Actor5e from "../entity.js";
import ActorSheet5e from "../sheets/base.js";
/**
@ -18,6 +19,11 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
/* -------------------------------------------- */
/** @override */
static unsupportedItemTypes = new Set(["class"]);
/* -------------------------------------------- */
/**
* Organize Owned Items for rendering the NPC sheet
* @private
@ -34,7 +40,7 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
// Start by classifying items into groups for rendering
let [powers, other] = data.items.reduce((arr, item) => {
item.img = item.img || DEFAULT_TOKEN;
item.img = item.img || CONST.DEFAULT_TOKEN;
item.isStack = Number.isNumeric(item.data.quantity) && (item.data.quantity !== 1);
item.hasUses = item.data.uses && (item.data.uses.max > 0);
item.isOnCooldown = item.data.recharge && !!item.data.recharge.value && (item.data.recharge.charged === false);
@ -67,17 +73,19 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
data.powerbook = powerbook;
}
/* -------------------------------------------- */
/** @override */
getData() {
const data = super.getData();
/** @inheritdoc */
getData(options) {
const data = super.getData(options);
// Challenge Rating
const cr = parseFloat(data.data.details.cr || 0);
const crLabels = {0: "0", 0.125: "1/8", 0.25: "1/4", 0.5: "1/2"};
data.labels["cr"] = cr >= 1 ? String(cr) : crLabels[cr] || 1;
// Creature Type
data.labels["type"] = this.actor.labels.creatureType;
return data;
}
@ -86,7 +94,7 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
/* -------------------------------------------- */
/** @override */
_updateObject(event, formData) {
async _updateObject(event, formData) {
// Format NPC Challenge Rating
const crs = {"1/8": 0.125, "1/4": 0.25, "1/2": 0.5};
@ -96,7 +104,7 @@ export default class ActorSheet5eNPC extends ActorSheet5e {
if ( cr ) formData[crv] = cr < 1 ? cr : parseInt(cr);
// Parent ActorSheet update steps
super._updateObject(event, formData);
return super._updateObject(event, formData);
}
/* -------------------------------------------- */

View file

@ -20,6 +20,11 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
/* -------------------------------------------- */
/** @override */
static unsupportedItemTypes = new Set(["class"]);
/* -------------------------------------------- */
/**
* Creates a new cargo entry for a vehicle Actor.
*/
@ -206,24 +211,39 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
}
};
// Classify items owned by the vehicle and compute total cargo weight
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') {
// Handle cargo explicitly
const isCargo = item.flags.sw5e?.vehicleCargo === true;
if ( isCargo ) {
totalWeight += (item.data.weight || 0) * item.data.quantity;
cargo.cargo.items.push(item);
continue;
}
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);
// Handle non-cargo item types
switch ( item.type ) {
case "weapon":
features.weapons.items.push(item);
break;
case "equipment":
features.equipment.items.push(item);
break;
case "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);
break;
default:
totalWeight += (item.data.weight || 0) * item.data.quantity;
cargo.cargo.items.push(item);
}
}
// Update the rendering context data
data.features = Object.values(features);
data.cargo = Object.values(cargo);
data.data.attributes.encumbrance = this._computeEncumbrance(totalWeight, data);
@ -236,7 +256,7 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
/** @override */
activateListeners(html) {
super.activateListeners(html);
if (!this.options.editable) return;
if ( !this.isEditable ) return;
html.find('.item-toggle').click(this._onToggleItem.bind(this));
html.find('.item-hp input')
@ -272,7 +292,7 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
const property = row.classList.contains('crew') ? 'crew' : 'passengers';
// Get the cargo entry
const cargo = duplicate(this.actor.data.data.cargo[property]);
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[property]);
const entry = cargo[idx];
if (!entry) return null;
@ -322,7 +342,7 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
const target = event.currentTarget;
const type = target.dataset.type;
if (type === 'crew' || type === 'passengers') {
const cargo = duplicate(this.actor.data.data.cargo[type]);
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]);
cargo.push(this.constructor.newCargo);
return this.actor.update({[`data.cargo.${type}`]: cargo});
}
@ -343,15 +363,24 @@ export default class ActorSheet5eVehicle extends ActorSheet5e {
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);
const cargo = foundry.utils.deepClone(this.actor.data.data.cargo[type]).filter((_, i) => i !== idx);
return this.actor.update({[`data.cargo.${type}`]: cargo});
}
return super._onItemDelete(event);
}
/* -------------------------------------------- */
/** @override */
async _onDropItemCreate(itemData) {
const cargoTypes = ["weapon", "equipment", "consumable", "tool", "loot", "backpack"];
const isCargo = cargoTypes.includes(itemData.type) && (this._tabs[0].active === "cargo");
foundry.utils.setProperty(itemData, "flags.sw5e.vehicleCargo", isCargo);
return super._onDropItemCreate(itemData);
}
/* -------------------------------------------- */
/**
* Special handling for editing HP to clamp it within appropriate range.
* @param event {Event}

View file

@ -39,12 +39,12 @@ export default class AbilityUseDialog extends Dialog {
// Prepare dialog form data
const data = {
item: item.data,
title: game.i18n.format("SW5E.AbilityUseHint", item.data),
title: game.i18n.format("SW5E.AbilityUseHint", {type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`), name: item.name}),
note: this._getAbilityUseNote(item.data, uses, recharge),
consumePowerSlot: false,
consumeRecharge: recharges,
consumeResource: !!itemData.consume.target,
consumeUses: uses.max,
consumeUses: uses.per && (uses.max > 0),
canUse: recharges ? recharge.charged : sufficientUses,
createTemplate: game.user.can("TEMPLATE_CREATE") && item.hasAreaTarget,
errors: []
@ -59,7 +59,7 @@ export default class AbilityUseDialog extends Dialog {
const label = game.i18n.localize("SW5E.AbilityUse" + (data.isPower ? "Cast" : "Use"));
return new Promise((resolve) => {
const dlg = new this(item, {
title: `${item.name}: Usage Configuration`,
title: `${item.name}: ${game.i18n.localize("SW5E.AbilityUseConfig")}`,
content: html,
buttons: {
use: {
@ -133,7 +133,7 @@ export default class AbilityUseDialog extends Dialog {
}));
// Merge power casting data
return mergeObject(data, { isPower: true, consumePowerSlot, powerLevels });
return foundry.utils.mergeObject(data, { isPower: true, consumePowerSlot, powerLevels });
}
/* -------------------------------------------- */
@ -151,7 +151,7 @@ export default class AbilityUseDialog extends Dialog {
// Abilities which use Recharge
if ( !!recharge.value ) {
return game.i18n.format(recharge.charged ? "SW5E.AbilityUseChargedHint" : "SW5E.AbilityUseRechargeHint", {
type: item.type,
type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`),
})
}
@ -165,7 +165,7 @@ export default class AbilityUseDialog extends Dialog {
else if ( item.data.quantity === 1 && uses.autoDestroy ) str = "SW5E.AbilityUseConsumableDestroyHint";
else if ( item.data.quantity > 1 ) str = "SW5E.AbilityUseConsumableQuantityHint";
return game.i18n.format(str, {
type: item.data.consumableType,
type: game.i18n.localize(`SW5E.Consumable${item.data.consumableType.capitalize()}`),
value: uses.value,
quantity: item.data.quantity,
max: uses.max,
@ -176,17 +176,11 @@ export default class AbilityUseDialog extends Dialog {
// Other Items
else {
return game.i18n.format("SW5E.AbilityUseNormalHint", {
type: item.type,
type: game.i18n.localize(`SW5E.ItemType${item.type.capitalize()}`),
value: uses.value,
max: uses.max,
per: CONFIG.SW5E.limitedUsePeriods[uses.per]
});
}
}
/* -------------------------------------------- */
static _handleSubmit(formData, item) {
}
}

View file

@ -1,11 +1,10 @@
/**
* An application class which provides advanced configuration for special character flags which modify an Actor
* @implements {BaseEntitySheet}
* @implements {DocumentSheet}
*/
export default class ActorSheetFlags extends BaseEntitySheet {
export default class ActorSheetFlags extends DocumentSheet {
static get defaultOptions() {
const options = super.defaultOptions;
return mergeObject(options, {
return foundry.utils.mergeObject(super.defaultOptions, {
id: "actor-flags",
classes: ["sw5e"],
template: "systems/sw5e/templates/apps/actor-flags.html",
@ -27,6 +26,7 @@ export default class ActorSheetFlags extends BaseEntitySheet {
getData() {
const data = {};
data.actor = this.object;
data.classes = this._getClasses();
data.flags = this._getFlags();
data.bonuses = this._getBonuses();
return data;
@ -34,17 +34,33 @@ export default class ActorSheetFlags extends BaseEntitySheet {
/* -------------------------------------------- */
/**
* Prepare an object of sorted classes.
* @return {object}
* @private
*/
_getClasses() {
const classes = this.object.items.filter(i => i.type === "class");
return classes.sort((a, b) => a.name.localeCompare(b.name)).reduce((obj, i) => {
obj[i.id] = i.name;
return obj;
}, {});
}
/* -------------------------------------------- */
/**
* Prepare an object of flags data which groups flags by section
* Add some additional data for rendering
* @return {object}
* @private
*/
_getFlags() {
const flags = {};
const baseData = this.entity._data;
const baseData = this.document.toJSON();
for ( let [k, v] of Object.entries(CONFIG.SW5E.characterFlags) ) {
if ( !flags.hasOwnProperty(v.section) ) flags[v.section] = {};
let flag = duplicate(v);
let flag = foundry.utils.deepClone(v);
flag.type = v.type.name;
flag.isCheckbox = v.type === Boolean;
flag.isSelect = v.hasOwnProperty('choices');

110
module/apps/actor-type.js Normal file
View file

@ -0,0 +1,110 @@
import Actor5e from "../actor/entity.js";
/**
* A specialized form used to select from a checklist of attributes, traits, or properties
* @extends {FormApplication}
*/
export default class ActorTypeConfig extends FormApplication {
/** @inheritdoc */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["sw5e", "actor-type", "trait-selector"],
template: "systems/sw5e/templates/apps/actor-type.html",
title: "SW5E.CreatureTypeTitle",
width: 280,
height: "auto",
choices: {},
allowCustom: true,
minimum: 0,
maximum: null
});
}
/* -------------------------------------------- */
/** @override */
get id() {
return `actor-type-${this.object.id}`;
}
/* -------------------------------------------- */
/** @override */
getData(options) {
// Get current value or new default
let attr = foundry.utils.getProperty(this.object.data.data, 'details.type');
if ( foundry.utils.getType(attr) !== "Object" ) attr = {
value: (attr in CONFIG.SW5E.creatureTypes) ? attr : "humanoid",
subtype: "",
swarm: "",
custom: ""
};
// Populate choices
const types = {};
for ( let [k, v] of Object.entries(CONFIG.SW5E.creatureTypes) ) {
types[k] = {
label: game.i18n.localize(v),
chosen: attr.value === k
}
}
// Return data for rendering
return {
types: types,
custom: {
value: attr.custom,
label: game.i18n.localize("SW5E.CreatureTypeSelectorCustom"),
chosen: attr.value === "custom"
},
subtype: attr.subtype,
swarm: attr.swarm,
sizes: Array.from(Object.entries(CONFIG.SW5E.actorSizes)).reverse().reduce((obj, e) => {
obj[e[0]] = e[1];
return obj;
}, {}),
preview: Actor5e.formatCreatureType(attr) || ""
}
}
/* -------------------------------------------- */
/** @override */
async _updateObject(event, formData) {
const typeObject = foundry.utils.expandObject(formData);
return this.object.update({ 'data.details.type': typeObject });
}
/* -------------------------------------------- */
/* Event Listeners and Handlers */
/* -------------------------------------------- */
/** @inheritdoc */
activateListeners(html) {
super.activateListeners(html);
html.find("input[name='custom']").focusin(this._onCustomFieldFocused.bind(this));
}
/* -------------------------------------------- */
/** @inheritdoc */
_onChangeInput(event) {
super._onChangeInput(event);
const typeObject = foundry.utils.expandObject(this._getSubmitData());
this.form["preview"].value = Actor5e.formatCreatureType(typeObject) || "—";
}
/* -------------------------------------------- */
/**
* Select the custom radio button when the custom text field is focused.
* @param {FocusEvent} event The original focusin event
* @private
*/
_onCustomFieldFocused(event) {
this.form.querySelector("input[name='value'][value='custom']").checked = true;
this._onChangeInput(event);
}
}

View file

@ -0,0 +1,91 @@
/**
* A simple form to set actor hit dice amounts
* @implements {DocumentSheet}
*/
export default class ActorHitDiceConfig extends DocumentSheet {
/** @override */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["sw5e", "hd-config", "dialog"],
template: "systems/sw5e/templates/apps/hit-dice-config.html",
width: 360,
height: "auto"
});
}
/* -------------------------------------------- */
/** @override */
get title() {
return `${game.i18n.localize("SW5E.HitDiceConfig")}: ${this.object.name}`;
}
/* -------------------------------------------- */
/** @override */
getData(options) {
return {
classes: this.object.items.reduce((classes, item) => {
if (item.data.type === "class") {
// Add the appropriate data only if this item is a "class"
classes.push({
classItemId: item.data._id,
name: item.data.name,
diceDenom: item.data.data.hitDice,
currentHitDice: item.data.data.levels - item.data.data.hitDiceUsed,
maxHitDice: item.data.data.levels,
canRoll: (item.data.data.levels - item.data.data.hitDiceUsed) > 0
});
}
return classes;
}, []).sort((a, b) => parseInt(b.diceDenom.slice(1)) - parseInt(a.diceDenom.slice(1)))
};
}
/* -------------------------------------------- */
/** @override */
activateListeners(html) {
super.activateListeners(html);
// Hook up -/+ buttons to adjust the current value in the form
html.find("button.increment,button.decrement").click(event => {
const button = event.currentTarget;
const current = button.parentElement.querySelector(".current");
const max = button.parentElement.querySelector(".max");
const direction = button.classList.contains("increment") ? 1 : -1;
current.value = Math.clamped(parseInt(current.value) + direction, 0, parseInt(max.value));
});
html.find("button.roll-hd").click(this._onRollHitDie.bind(this));
}
/* -------------------------------------------- */
/** @override */
async _updateObject(event, formData) {
const actorItems = this.object.items;
const classUpdates = Object.entries(formData).map(([id, hd]) => ({
_id: id,
"data.hitDiceUsed": actorItems.get(id).data.data.levels - hd,
}));
return this.object.updateEmbeddedDocuments("Item", classUpdates);
}
/* -------------------------------------------- */
/**
* Rolls the hit die corresponding with the class row containing the event's target button.
* @param {MouseEvent} event
* @private
*/
async _onRollHitDie(event) {
event.preventDefault();
const button = event.currentTarget;
await this.object.rollHitDie(button.dataset.hdDenom);
// Re-render dialog to reflect changed hit dice quantities
this.render();
}
}

View file

@ -40,23 +40,21 @@ export default class LongRestDialog extends Dialog {
static async longRestDialog({ actor } = {}) {
return new Promise((resolve, reject) => {
const dlg = new this(actor, {
title: "Long Rest",
title: game.i18n.localize("SW5E.LongRest"),
buttons: {
rest: {
icon: '<i class="fas fa-bed"></i>',
label: "Rest",
label: game.i18n.localize("SW5E.Rest"),
callback: html => {
let newDay = false;
if (game.settings.get("sw5e", "restVariant") === "normal")
let newDay = true;
if (game.settings.get("sw5e", "restVariant") !== "gritty")
newDay = html.find('input[name="newDay"]')[0].checked;
else if(game.settings.get("sw5e", "restVariant") === "gritty")
newDay = true;
resolve(newDay);
}
},
cancel: {
icon: '<i class="fas fa-times"></i>',
label: "Cancel",
label: game.i18n.localize("Cancel"),
callback: reject
}
},

View file

@ -1,12 +1,12 @@
/**
* A simple form to set actor movement speeds
* @implements {BaseEntitySheet}
* @extends {DocumentSheet}
*/
export default class ActorMovementConfig extends BaseEntitySheet {
export default class ActorMovementConfig extends DocumentSheet {
/** @override */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["sw5e"],
template: "systems/sw5e/templates/apps/movement-config.html",
width: 300,
@ -18,17 +18,18 @@ export default class ActorMovementConfig extends BaseEntitySheet {
/** @override */
get title() {
return `${game.i18n.localize("SW5E.MovementConfig")}: ${this.entity.name}`;
return `${game.i18n.localize("SW5E.MovementConfig")}: ${this.document.name}`;
}
/* -------------------------------------------- */
/** @override */
getData(options) {
const sourceMovement = foundry.utils.getProperty(this.document.data._source, "data.attributes.movement") || {};
const data = {
movement: duplicate(this.entity._data.data.attributes.movement),
movement: foundry.utils.deepClone(sourceMovement),
units: CONFIG.SW5E.movementUnits
}
};
for ( let [k, v] of Object.entries(data.movement) ) {
if ( ["units", "hover"].includes(k) ) continue;
data.movement[k] = Number.isNumeric(v) ? v.toNearest(0.1) : 0;

View file

@ -0,0 +1,68 @@
/**
* A Dialog to prompt the user to select from a list of items.
* @type {Dialog}
*/
export default class SelectItemsPrompt extends Dialog {
constructor(items, dialogData={}, options={}) {
super(dialogData, options);
this.options.classes = ["sw5e", "dialog", "select-items-prompt", "sheet"];
/**
* Store a reference to the Item entities being used
* @type {Array<Item5e>}
*/
this.items = items;
}
activateListeners(html) {
super.activateListeners(html);
// render the item's sheet if its image is clicked
html.on('click', '.item-image', (event) => {
const item = this.items.find((feature) => feature.id === event.currentTarget.dataset?.itemId);
item?.sheet.render(true);
})
}
/**
* A constructor function which displays the AddItemPrompt app for a given Actor and Item set.
* Returns a Promise which resolves to the dialog FormData once the workflow has been completed.
* @param {Array<Item5e>} items
* @param {Object} options
* @param {string} options.hint - Localized hint to display at the top of the prompt
* @return {Promise<string[]>} - list of item ids which the user has selected
*/
static async create(items, {
hint
}) {
// Render the ability usage template
const html = await renderTemplate("systems/sw5e/templates/apps/select-items-prompt.html", {items, hint});
return new Promise((resolve) => {
const dlg = new this(items, {
title: game.i18n.localize('SW5E.SelectItemsPromptTitle'),
content: html,
buttons: {
apply: {
icon: `<i class="fas fa-user-plus"></i>`,
label: game.i18n.localize('SW5E.Apply'),
callback: html => {
const fd = new FormDataExtended(html[0].querySelector("form")).toObject();
const selectedIds = Object.keys(fd).filter(itemId => fd[itemId]);
resolve(selectedIds);
}
},
cancel: {
icon: '<i class="fas fa-forward"></i>',
label: game.i18n.localize('SW5E.Skip'),
callback: () => resolve([])
}
},
default: "apply",
close: () => resolve([])
});
dlg.render(true);
});
}
}

View file

@ -1,12 +1,12 @@
/**
* A simple form to set actor movement speeds
* @implements {BaseEntitySheet}
* A simple form to set Actor movement speeds.
* @extends {DocumentSheet}
*/
export default class ActorSensesConfig extends BaseEntitySheet {
export default class ActorSensesConfig extends DocumentSheet {
/** @override */
/** @inheritdoc */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["sw5e"],
template: "systems/sw5e/templates/apps/senses-config.html",
width: 300,
@ -16,16 +16,16 @@ export default class ActorSensesConfig extends BaseEntitySheet {
/* -------------------------------------------- */
/** @override */
/** @inheritdoc */
get title() {
return `${game.i18n.localize("SW5E.SensesConfig")}: ${this.entity.name}`;
return `${game.i18n.localize("SW5E.SensesConfig")}: ${this.document.name}`;
}
/* -------------------------------------------- */
/** @override */
/** @inheritdoc */
getData(options) {
const senses = this.entity._data.data.attributes?.senses ?? {};
const senses = foundry.utils.getProperty(this.document.data._source, "data.attributes.senses") || {};
const data = {
senses: {},
special: senses.special ?? "",

View file

@ -40,7 +40,7 @@ export default class ShortRestDialog extends Dialog {
// Determine Hit Dice
data.availableHD = this.actor.data.items.reduce((hd, item) => {
if ( item.type === "class" ) {
const d = item.data;
const d = item.data.data;
const denom = d.hitDice || "d6";
const available = parseInt(d.levels || 1) - parseInt(d.hitDiceUsed || 0);
hd[denom] = denom in hd ? hd[denom] + available : available;
@ -93,11 +93,11 @@ export default class ShortRestDialog extends Dialog {
static async shortRestDialog({actor}={}) {
return new Promise((resolve, reject) => {
const dlg = new this(actor, {
title: "Short Rest",
title: game.i18n.localize("SW5E.ShortRest"),
buttons: {
rest: {
icon: '<i class="fas fa-bed"></i>',
label: "Rest",
label: game.i18n.localize("SW5E.Rest"),
callback: html => {
let newDay = false;
if (game.settings.get("sw5e", "restVariant") === "gritty")
@ -107,7 +107,7 @@ export default class ShortRestDialog extends Dialog {
},
cancel: {
icon: '<i class="fas fa-times"></i>',
label: "Cancel",
label: game.i18n.localize("Cancel"),
callback: reject
}
},

View file

@ -1,14 +1,14 @@
/**
* A specialized form used to select from a checklist of attributes, traits, or properties
* @implements {FormApplication}
* @extends {DocumentSheet}
*/
export default class TraitSelector extends FormApplication {
export default class TraitSelector extends DocumentSheet {
/** @override */
/** @inheritdoc */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
return foundry.utils.mergeObject(super.defaultOptions, {
id: "trait-selector",
classes: ["sw5e"],
classes: ["sw5e", "trait-selector", "subconfig"],
title: "Actor Trait Selection",
template: "systems/sw5e/templates/apps/trait-selector.html",
width: 320,
@ -16,7 +16,9 @@ export default class TraitSelector extends FormApplication {
choices: {},
allowCustom: true,
minimum: 0,
maximum: null
maximum: null,
valueKey: "value",
customKey: "custom"
});
}
@ -24,7 +26,7 @@ export default class TraitSelector extends FormApplication {
/**
* Return a reference to the target attribute
* @type {String}
* @type {string}
*/
get attribute() {
return this.options.name;
@ -34,52 +36,50 @@ export default class TraitSelector extends FormApplication {
/** @override */
getData() {
// Get current values
let attr = getProperty(this.object._data, this.attribute);
if ( getType(attr) !== "Object" ) attr = {value: [], custom: ""};
const attr = foundry.utils.getProperty(this.object.data, this.attribute);
const o = this.options;
const value = (o.valueKey) ? attr[o.valueKey] ?? [] : attr;
const custom = (o.customKey) ? attr[o.customKey] ?? "" : "";
// Populate choices
const choices = duplicate(this.options.choices);
for ( let [k, v] of Object.entries(choices) ) {
choices[k] = {
label: v,
chosen: attr ? attr.value.includes(k) : false
}
}
const choices = Object.entries(o.choices).reduce((obj, e) => {
let [k, v] = e;
obj[k] = { label: v, chosen: attr ? value.includes(k) : false };
return obj;
}, {})
// Return data
return {
allowCustom: this.options.allowCustom,
allowCustom: o.allowCustom,
choices: choices,
custom: attr ? attr.custom : ""
custom: custom
}
}
/* -------------------------------------------- */
/** @override */
_updateObject(event, formData) {
const updateData = {};
async _updateObject(event, formData) {
const o = this.options;
// Obtain choices
const chosen = [];
for ( let [k, v] of Object.entries(formData) ) {
if ( (k !== "custom") && v ) chosen.push(k);
}
updateData[`${this.attribute}.value`] = chosen;
// Object including custom data
const updateData = {};
if ( o.valueKey ) updateData[`${this.attribute}.${o.valueKey}`] = chosen;
else updateData[this.attribute] = chosen;
if ( o.allowCustom ) updateData[`${this.attribute}.${o.customKey}`] = formData.custom;
// Validate the number chosen
if ( this.options.minimum && (chosen.length < this.options.minimum) ) {
return ui.notifications.error(`You must choose at least ${this.options.minimum} options`);
if ( o.minimum && (chosen.length < o.minimum) ) {
return ui.notifications.error(`You must choose at least ${o.minimum} options`);
}
if ( this.options.maximum && (chosen.length > this.options.maximum) ) {
return ui.notifications.error(`You may choose no more than ${this.options.maximum} options`);
}
// Include custom
if ( this.options.allowCustom ) {
updateData[`${this.attribute}.custom`] = formData.custom;
if ( o.maximum && (chosen.length > o.maximum) ) {
return ui.notifications.error(`You may choose no more than ${o.maximum} options`);
}
// Update the object

View file

@ -36,19 +36,3 @@ export const measureDistances = function(segments, options={}) {
else return (ns + nd) * canvas.scene.data.gridDistance;
});
};
/* -------------------------------------------- */
/**
* Hijack Token health bar rendering to include temporary and temp-max health in the bar display
* TODO: This should probably be replaced with a formal Token class extension
*/
const _TokenGetBarAttribute = Token.prototype.getBarAttribute;
export const getBarAttribute = function(...args) {
const data = _TokenGetBarAttribute.bind(this)(...args);
if ( data && (data.attribute === "attributes.hp") ) {
data.value += parseInt(getProperty(this.actor.data, "data.attributes.hp.temp") || 0);
data.max += parseInt(getProperty(this.actor.data, "data.attributes.hp.tempmax") || 0);
}
return data;
};

View file

@ -40,7 +40,7 @@ export const displayChatActionButtons = function(message, html, data) {
// If the user is the message author or the actor owner, proceed
let actor = game.actors.get(data.message.speaker.actor);
if ( actor && actor.owner ) return;
if ( actor && actor.isOwner ) return;
else if ( game.user.isGM || (data.author.id === game.user.id)) return;
// Otherwise conceal action buttons except for saving throw
@ -66,7 +66,7 @@ export const displayChatActionButtons = function(message, html, data) {
export const addChatMessageContextOptions = function(html, options) {
let canApply = li => {
const message = game.messages.get(li.data("messageId"));
return message?.isRoll && message?.isContentVisible && canvas?.tokens.controlled.length;
return message?.isRoll && message?.isContentVisible && canvas.tokens?.controlled.length;
};
options.push(
{

View file

@ -5,20 +5,19 @@
* Apply the dexterity score as a decimal tiebreaker if requested
* See Combat._getInitiativeFormula for more detail.
*/
export const _getInitiativeFormula = function(combatant) {
const actor = combatant.actor;
export const _getInitiativeFormula = function() {
const actor = this.actor;
if ( !actor ) return "1d20";
const init = actor.data.data.attributes.init;
// Construct initiative formula parts
let nd = 1;
let mods = "";
if (actor.getFlag("sw5e", "halflingLucky")) mods += "r1=1";
if (actor.getFlag("sw5e", "initiativeAdv")) {
nd = 2;
mods += "kh";
}
const parts = [`${nd}d20${mods}`, init.mod, (init.prof !== 0) ? init.prof : null, (init.bonus !== 0) ? init.bonus : null];
// Optionally apply Dexterity tiebreaker

View file

@ -64,7 +64,7 @@ SW5E.attunementTypes = {
NONE: 0,
REQUIRED: 1,
ATTUNED: 2,
}
};
/**
* An enumeration of item attunement states
@ -84,6 +84,69 @@ SW5E.weaponProficiencies = {
"mar": "SW5E.WeaponMartialProficiency"
};
/**
* A map of weapon item proficiency to actor item proficiency
* Used when a new player owned item is created
* @type {Object}
*/
SW5E.weaponProficienciesMap = {
"natural": true,
"simpleM": "sim",
"simpleR": "sim",
"martialM": "mar",
"martialR": "mar"
}
/**
* The basic weapon types in 5e. This enables specific weapon proficiencies or
* starting equipment provided by classes and backgrounds.
*
* @enum {string}
*/
SW5E.weaponIds = {
"battleaxe": "I0WocDSuNpGJayPb",
"blowgun": "wNWK6yJMHG9ANqQV",
"club": "nfIRTECQIG81CvM4",
"dagger": "0E565kQUBmndJ1a2",
"dart": "3rCO8MTIdPGSW6IJ",
"flail": "UrH3sMdnUDckIHJ6",
"glaive": "rOG1OM2ihgPjOvFW",
"greataxe": "1Lxk6kmoRhG8qQ0u",
"greatclub": "QRCsxkCwWNwswL9o",
"greatsword": "xMkP8BmFzElcsMaR",
"halberd": "DMejWAc8r8YvDPP1",
"handaxe": "eO7Fbv5WBk5zvGOc",
"handcrossbow": "qaSro7kFhxD6INbZ",
"heavycrossbow": "RmP0mYRn2J7K26rX",
"javelin": "DWLMnODrnHn8IbAG",
"lance": "RnuxdHUAIgxccVwj",
"lightcrossbow": "ddWvQRLmnnIS0eLF",
"lighthammer": "XVK6TOL4sGItssAE",
"longbow": "3cymOVja8jXbzrdT",
"longsword": "10ZP2Bu3vnCuYMIB",
"mace": "Ajyq6nGwF7FtLhDQ",
"maul": "DizirD7eqjh8n95A",
"morningstar": "dX8AxCh9o0A9CkT3",
"net": "aEiM49V8vWpWw7rU",
"pike": "tC0kcqZT9HHAO0PD",
"quarterstaff": "g2dWN7PQiMRYWzyk",
"rapier": "Tobce1hexTnDk4sV",
"scimitar": "fbC0Mg1a73wdFbqO",
"shortsword": "osLzOwQdPtrK3rQH",
"sickle": "i4NeNZ30ycwPDHMx",
"spear": "OG4nBBydvmfWYXIk",
"shortbow": "GJv6WkD7D2J6rP6M",
"sling": "3gynWO9sN4OLGMWD",
"trident": "F65ANO66ckP8FDMa",
"warpick": "2YdfjN1PIIrSHZii",
"warhammer": "F0Df164Xv1gWcYt0",
"whip": "QKTyxoO0YDnAsbYe"
};
/* -------------------------------------------- */
SW5E.toolProficiencies = {
"art": "SW5E.ToolArtisans",
"disg": "SW5E.ToolDisguiseKit",
@ -97,6 +160,50 @@ SW5E.toolProficiencies = {
"vehicle": "SW5E.ToolVehicle"
};
/**
* The basic tool types in 5e. This enables specific tool proficiencies or
* starting equipment provided by classes and backgrounds.
*
* @enum {string}
*/
SW5E.toolIds = {
"alchemist": "SztwZhbhZeCqyAes",
"bagpipes": "yxHi57T5mmVt0oDr",
"brewer": "Y9S75go1hLMXUD48",
"calligrapher": "jhjo20QoiD5exf09",
"card": "YwlHI3BVJapz4a3E",
"carpenter": "8NS6MSOdXtUqD7Ib",
"cartographer": "fC0lFK8P4RuhpfaU",
"cobbler": "hM84pZnpCqKfi8XH",
"cook": "Gflnp29aEv5Lc1ZM",
"dice": "iBuTM09KD9IoM5L8",
"disg": "IBhDAr7WkhWPYLVn",
"drum": "69Dpr25pf4BjkHKb",
"dulcimer": "NtdDkjmpdIMiX7I2",
"flute": "eJOrPcAz9EcquyRQ",
"forg": "cG3m4YlHfbQlLEOx",
"glassblower": "rTbVrNcwApnuTz5E",
"herb": "i89okN7GFTWHsvPy",
"horn": "aa9KuBy4dst7WIW9",
"jeweler": "YfBwELTgPFHmQdHh",
"leatherworker": "PUMfwyVUbtyxgYbD",
"lute": "qBydtUUIkv520DT7",
"lyre": "EwG1EtmbgR3bM68U",
"mason": "skUih6tBvcBbORzA",
"navg": "YHCmjsiXxZ9UdUhU",
"painter": "ccm5xlWhx74d6lsK",
"panflute": "G5m5gYIx9VAUWC3J",
"pois": "il2GNi8C0DvGLL9P",
"potter": "hJS8yEVkqgJjwfWa",
"shawm": "G3cqbejJpfB91VhP",
"smith": "KndVe2insuctjIaj",
"thief": "woWZ1sO5IUVGzo58",
"tinker": "0d08g1i5WXnNrCNA",
"viol": "baoe3U5BfMMMxhCU",
"weaver": "ap9prThUB2y9lDyj",
"woodcarver": "xKErqkLo4ASYr5EP",
};
/* -------------------------------------------- */
@ -133,8 +240,8 @@ SW5E.abilityActivationTypes = {
"hour": SW5E.timePeriods.hour,
"day": SW5E.timePeriods.day,
"special": SW5E.timePeriods.spec,
"legendary": "SW5E.LegAct",
"lair": "SW5E.LairAct",
"legendary": "SW5E.LegendaryActionLabel",
"lair": "SW5E.LairActionLabel",
"crew": "SW5E.VehicleCrewAction"
};
@ -170,6 +277,40 @@ SW5E.tokenSizes = {
"grg": 4
};
/**
* Colors used to visualize temporary and temporary maximum HP in token health bars
* @enum {number}
*/
SW5E.tokenHPColors = {
temp: 0x66CCFF,
tempmax: 0x440066,
negmax: 0x550000
}
/* -------------------------------------------- */
/**
* Creature types
* @type {Object}
*/
SW5E.creatureTypes = {
"aberration": "SW5E.CreatureAberration",
"beast": "SW5E.CreatureBeast",
"celestial": "SW5E.CreatureCelestial",
"construct": "SW5E.CreatureConstruct",
"dragon": "SW5E.CreatureDragon",
"elemental": "SW5E.CreatureElemental",
"fey": "SW5E.CreatureFey",
"fiend": "SW5E.CreatureFiend",
"giant": "SW5E.CreatureGiant",
"humanoid": "SW5E.CreatureHumanoid",
"monstrosity": "SW5E.CreatureMonstrosity",
"ooze": "SW5E.CreatureOoze",
"plant": "SW5E.CreaturePlant",
"undead": "SW5E.CreatureUndead"
};
/* -------------------------------------------- */
/**
@ -212,7 +353,7 @@ SW5E.limitedUsePeriods = {
/* -------------------------------------------- */
/**
* The set of equipment types for armor, clothing, and other objects which can ber worn by the character
* The set of equipment types for armor, clothing, and other objects which can be worn by the character
* @type {Object}
*/
SW5E.equipmentTypes = {
@ -241,6 +382,20 @@ SW5E.armorProficiencies = {
"shl": "SW5E.EquipmentShieldProficiency"
};
/**
* A map of armor item proficiency to actor item proficiency
* Used when a new player owned item is created
* @type {Object}
*/
SW5E.armorProficienciesMap = {
"natural": true,
"clothing": true,
"light": "lgt",
"medium": "med",
"heavy": "hvy",
"shield": "shl"
}
/* -------------------------------------------- */
@ -306,7 +461,7 @@ SW5E.damageTypes = {
};
// Damage Resistance Types
SW5E.damageResistanceTypes = mergeObject(duplicate(SW5E.damageTypes), {
SW5E.damageResistanceTypes = mergeObject(foundry.utils.deepClone(SW5E.damageTypes), {
"physical": "SW5E.DamagePhysical"
});
@ -324,7 +479,7 @@ SW5E.movementTypes = {
"fly": "SW5E.MovementFly",
"swim": "SW5E.MovementSwim",
"walk": "SW5E.MovementWalk",
}
};
/**
* The valid units of measure for movement distances in the game system.
@ -334,7 +489,7 @@ SW5E.movementTypes = {
SW5E.movementUnits = {
"ft": "SW5E.DistFt",
"mi": "SW5E.DistMi"
}
};
/**
* The valid units of measure for the range of an action or effect.
@ -424,7 +579,7 @@ SW5E.healingTypes = {
/**
* Enumerate the denominations of hit dice which can apply to classes
* @type {Array.<string>}
* @type {string[]}
*/
SW5E.hitDieTypes = ["d6", "d8", "d10", "d12"];
@ -433,7 +588,7 @@ SW5E.hitDieTypes = ["d6", "d8", "d10", "d12"];
/**
* The set of possible sensory perception types which an Actor may have
* @type {object}
* @enum {string}
*/
SW5E.senses = {
"blindsight": "SW5E.SenseBlindsight",
@ -583,18 +738,26 @@ SW5E.powerLevels = {
// Power Scroll Compendium UUIDs
SW5E.powerScrollIds = {
0: 'Compendium.sw5e.items.rQ6sO7HDWzqMhSI3',
1: 'Compendium.sw5e.items.9GSfMg0VOA2b4uFN',
2: 'Compendium.sw5e.items.XdDp6CKh9qEvPTuS',
3: 'Compendium.sw5e.items.hqVKZie7x9w3Kqds',
4: 'Compendium.sw5e.items.DM7hzgL836ZyUFB1',
5: 'Compendium.sw5e.items.wa1VF8TXHmkrrR35',
6: 'Compendium.sw5e.items.tI3rWx4bxefNCexS',
7: 'Compendium.sw5e.items.mtyw4NS1s7j2EJaD',
8: 'Compendium.sw5e.items.aOrinPg7yuDZEuWr',
9: 'Compendium.sw5e.items.O4YbkJkLlnsgUszZ'
0: "rQ6sO7HDWzqMhSI3",
1: "9GSfMg0VOA2b4uFN",
2: "XdDp6CKh9qEvPTuS",
3: "hqVKZie7x9w3Kqds",
4: "DM7hzgL836ZyUFB1",
5: "wa1VF8TXHmkrrR35",
6: "tI3rWx4bxefNCexS",
7: "mtyw4NS1s7j2EJaD",
8: "aOrinPg7yuDZEuWr",
9: "O4YbkJkLlnsgUszZ"
};
/**
* Compendium packs used for localized items.
* @enum {string}
*/
SW5E.sourcePacks = {
ITEMS: "sw5e.items"
}
/**
* Define the standard slot progression by character level.
* The entries of this array represent the power slot progression for a full power-caster.
@ -741,83 +904,83 @@ SW5E.characterFlags = {
"diamondSoul": {
name: "SW5E.FlagsDiamondSoul",
hint: "SW5E.FlagsDiamondSoulHint",
section: "Feats",
section: "SW5E.Feats",
type: Boolean
},
"elvenAccuracy": {
name: "SW5E.FlagsElvenAccuracy",
hint: "SW5E.FlagsElvenAccuracyHint",
section: "Racial Traits",
section: "SW5E.RacialTraits",
type: Boolean
},
"halflingLucky": {
name: "SW5E.FlagsHalflingLucky",
hint: "SW5E.FlagsHalflingLuckyHint",
section: "Racial Traits",
section: "SW5E.RacialTraits",
type: Boolean
},
"initiativeAdv": {
name: "SW5E.FlagsInitiativeAdv",
hint: "SW5E.FlagsInitiativeAdvHint",
section: "Feats",
section: "SW5E.Feats",
type: Boolean
},
"initiativeAlert": {
name: "SW5E.FlagsAlert",
hint: "SW5E.FlagsAlertHint",
section: "Feats",
section: "SW5E.Feats",
type: Boolean
},
"jackOfAllTrades": {
name: "SW5E.FlagsJOAT",
hint: "SW5E.FlagsJOATHint",
section: "Feats",
section: "SW5E.Feats",
type: Boolean
},
"observantFeat": {
name: "SW5E.FlagsObservant",
hint: "SW5E.FlagsObservantHint",
skills: ['prc','inv'],
section: "Feats",
section: "SW5E.Feats",
type: Boolean
},
"powerfulBuild": {
name: "SW5E.FlagsPowerfulBuild",
hint: "SW5E.FlagsPowerfulBuildHint",
section: "Racial Traits",
section: "SW5E.RacialTraits",
type: Boolean
},
"reliableTalent": {
name: "SW5E.FlagsReliableTalent",
hint: "SW5E.FlagsReliableTalentHint",
section: "Feats",
section: "SW5E.Feats",
type: Boolean
},
"remarkableAthlete": {
name: "SW5E.FlagsRemarkableAthlete",
hint: "SW5E.FlagsRemarkableAthleteHint",
abilities: ['str','dex','con'],
section: "Feats",
section: "SW5E.Feats",
type: Boolean
},
"weaponCriticalThreshold": {
name: "SW5E.FlagsWeaponCritThreshold",
hint: "SW5E.FlagsWeaponCritThresholdHint",
section: "Feats",
section: "SW5E.Feats",
type: Number,
placeholder: 20
},
"powerCriticalThreshold": {
name: "SW5E.FlagsPowerCritThreshold",
hint: "SW5E.FlagsPowerCritThresholdHint",
section: "Feats",
section: "SW5E.Feats",
type: Number,
placeholder: 20
},
"meleeCriticalDamageDice": {
name: "SW5E.FlagsMeleeCriticalDice",
hint: "SW5E.FlagsMeleeCriticalDiceHint",
section: "Feats",
section: "SW5E.Feats",
type: Number,
placeholder: 0
},

View file

@ -1,3 +1,6 @@
export {default as D20Roll} from "./dice/d20-roll.js";
export {default as DamageRoll} from "./dice/damage-roll.js";
/**
* A standardized helper function for simplifying the constant parts of a multipart roll formula
*
@ -20,28 +23,36 @@ export function simplifyRollFormula(formula, data, {constantFirst = false} = {})
const constantTerms = []; // Terms that are constant, and their associated operators
let operators = []; // Temporary storage for operators before they are moved to one of the above
for (let term of terms) { // For each term
if (["+", "-"].includes(term)) operators.push(term); // If the term is an addition/subtraction operator, push the term into the operators array
else { // Otherwise the term is not an operator
if (term instanceof DiceTerm) { // If the term is something rollable
rollableTerms.push(...operators); // Place all the operators into the rollableTerms array
rollableTerms.push(term); // Then place this rollable term into it as well
} //
else { // Otherwise, this must be a constant
constantTerms.push(...operators); // Place the operators into the constantTerms array
constantTerms.push(term); // Then also add this constant term to that array.
} //
operators = []; // Finally, the operators have now all been assigend to one of the arrays, so empty this before the next iteration.
for (let term of terms) { // For each term
if (term instanceof OperatorTerm) operators.push(term); // If the term is an addition/subtraction operator, push the term into the operators array
else { // Otherwise the term is not an operator
if (term instanceof DiceTerm) { // If the term is something rollable
rollableTerms.push(...operators); // Place all the operators into the rollableTerms array
rollableTerms.push(term); // Then place this rollable term into it as well
} //
else { // Otherwise, this must be a constant
constantTerms.push(...operators); // Place the operators into the constantTerms array
constantTerms.push(term); // Then also add this constant term to that array.
} //
operators = []; // Finally, the operators have now all been assigend to one of the arrays, so empty this before the next iteration.
}
}
const constantFormula = Roll.cleanFormula(constantTerms); // Cleans up the constant terms and produces a new formula string
const rollableFormula = Roll.cleanFormula(rollableTerms); // Cleans up the non-constant terms and produces a new formula string
const constantFormula = Roll.getFormula(constantTerms); // Cleans up the constant terms and produces a new formula string
const rollableFormula = Roll.getFormula(rollableTerms); // Cleans up the non-constant terms and produces a new formula string
const constantPart = roll._safeEval(constantFormula); // Mathematically evaluate the constant formula to produce a single constant term
// Mathematically evaluate the constant formula to produce a single constant term
let constantPart = undefined;
if ( constantFormula ) {
try {
constantPart = Roll.safeEval(constantFormula)
} catch (err) {
console.warn(`Unable to evaluate constant term ${constantFormula} in simplifyRollFormula`);
}
}
const parts = constantFirst ? // Order the rollable and constant terms, either constant first or second depending on the optional argumen
[constantPart, rollableFormula] : [rollableFormula, constantPart];
// Order the rollable and constant terms, either constant first or second depending on the optional argument
const parts = constantFirst ? [constantPart, rollableFormula] : [rollableFormula, constantPart];
// Join the parts with a + sign, pass them to `Roll` once again to clean up the formula
return new Roll(parts.filterJoin(" + ")).formula;
@ -56,315 +67,201 @@ export function simplifyRollFormula(formula, data, {constantFirst = false} = {})
*/
function _isUnsupportedTerm(term) {
const diceTerm = term instanceof DiceTerm;
const operator = ["+", "-"].includes(term);
const number = !isNaN(Number(term));
const operator = term instanceof OperatorTerm && ["+", "-"].includes(term.operator);
const number = term instanceof NumericTerm;
return !(diceTerm || operator || number);
}
/* -------------------------------------------- */
/* D20 Roll */
/* -------------------------------------------- */
/**
* A standardized helper function for managing core 5e "d20 rolls"
*
* A standardized helper function for managing core 5e d20 rolls.
* Holding SHIFT, ALT, or CTRL when the attack is rolled will "fast-forward".
* This chooses the default options of a normal attack with no bonus, Advantage, or Disadvantage respectively
*
* @param {Array} parts The dice roll component parts, excluding the initial d20
* @param {Object} data Actor or item data against which to parse the roll
* @param {Event|object} event The triggering event which initiated the roll
* @param {string} rollMode A specific roll mode to apply as the default for the resulting roll
* @param {string|null} template The HTML template used to render the roll dialog
* @param {string|null} title The dice roll UI window title
* @param {Object} speaker The ChatMessage speaker to pass when creating the chat
* @param {string|null} flavor Flavor text to use in the posted chat message
* @param {Boolean} fastForward Allow fast-forward advantage selection
* @param {Function} onClose Callback for actions to take when the dialog form is closed
* @param {Object} dialogOptions Modal dialog options
* @param {boolean} advantage Apply advantage to the roll (unless otherwise specified)
* @param {boolean} disadvantage Apply disadvantage to the roll (unless otherwise specified)
* @param {number} critical The value of d20 result which represents a critical success
* @param {number} fumble The value of d20 result which represents a critical failure
* @param {number} targetValue Assign a target value against which the result of this roll should be compared
* @param {boolean} elvenAccuracy Allow Elven Accuracy to modify this roll?
* @param {boolean} halflingLucky Allow Halfling Luck to modify this roll?
* @param {boolean} reliableTalent Allow Reliable Talent to modify this roll?
* @param {boolean} chatMessage Automatically create a Chat Message for the result of this roll
* @param {object} messageData Additional data which is applied to the created Chat Message, if any
* @param {string[]} parts The dice roll component parts, excluding the initial d20
* @param {object} data Actor or item data against which to parse the roll
*
* @return {Promise} A Promise which resolves once the roll workflow has completed
* @param {boolean} [advantage] Apply advantage to the roll (unless otherwise specified)
* @param {boolean} [disadvantage] Apply disadvantage to the roll (unless otherwise specified)
* @param {number} [critical] The value of d20 result which represents a critical success
* @param {number} [fumble] The value of d20 result which represents a critical failure
* @param {number} [targetValue] Assign a target value against which the result of this roll should be compared
* @param {boolean} [elvenAccuracy] Allow Elven Accuracy to modify this roll?
* @param {boolean} [halflingLucky] Allow Halfling Luck to modify this roll?
* @param {boolean} [reliableTalent] Allow Reliable Talent to modify this roll?
* @param {boolean} [chooseModifier=false] Choose the ability modifier that should be used when the roll is made
* @param {boolean} [fastForward=false] Allow fast-forward advantage selection
* @param {Event} [event] The triggering event which initiated the roll
* @param {string} [rollMode] A specific roll mode to apply as the default for the resulting roll
* @param {string} [template] The HTML template used to render the roll dialog
* @param {string} [title] The dialog window title
* @param {Object} [dialogOptions] Modal dialog options
*
* @param {boolean} [chatMessage=true] Automatically create a Chat Message for the result of this roll
* @param {object} [messageData={}] Additional data which is applied to the created Chat Message, if any
* @param {string} [rollMode] A specific roll mode to apply as the default for the resulting roll
* @param {object} [speaker] The ChatMessage speaker to pass when creating the chat
* @param {string} [flavor] Flavor text to use in the posted chat message
*
* @return {Promise<D20Roll|null>} The evaluated D20Roll, or null if the workflow was cancelled
*/
export async function d20Roll({parts=[], data={}, event={}, rollMode=null, template=null, title=null, speaker=null,
flavor=null, fastForward=null, dialogOptions,
advantage=null, disadvantage=null, critical=20, fumble=1, targetValue=null,
elvenAccuracy=false, halflingLucky=false, reliableTalent=false,
chatMessage=true, messageData={}}={}) {
export async function d20Roll({
parts=[], data={}, // Roll creation
advantage, disadvantage, fumble=1, critical=20, targetValue, elvenAccuracy, halflingLucky, reliableTalent, // Roll customization
chooseModifier=false, fastForward=false, event, template, title, dialogOptions, // Dialog configuration
chatMessage=true, messageData={}, rollMode, speaker, flavor // Chat Message customization
}={}) {
// Prepare Message Data
messageData.flavor = flavor || title;
messageData.speaker = speaker || ChatMessage.getSpeaker();
const messageOptions = {rollMode: rollMode || game.settings.get("core", "rollMode")};
parts = parts.concat(["@bonus"]);
// Handle input arguments
const formula = ["1d20"].concat(parts).join(" + ");
const {advantageMode, isFF} = _determineAdvantageMode({advantage, disadvantage, fastForward, event});
const defaultRollMode = rollMode || game.settings.get("core", "rollMode");
if ( chooseModifier && !isFF ) data["mod"] = "@mod";
// Handle fast-forward events
let adv = 0;
fastForward = fastForward ?? (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
if (fastForward) {
if ( advantage ?? event.altKey ) adv = 1;
else if ( disadvantage ?? (event.ctrlKey || event.metaKey) ) adv = -1;
// Construct the D20Roll instance
const roll = new CONFIG.Dice.D20Roll(formula, data, {
flavor: flavor || title,
advantageMode,
defaultRollMode,
critical,
fumble,
targetValue,
elvenAccuracy,
halflingLucky,
reliableTalent
});
// Prompt a Dialog to further configure the D20Roll
if ( !isFF ) {
const configured = await roll.configureDialog({
title,
chooseModifier,
defaultRollMode: defaultRollMode,
defaultAction: advantageMode,
defaultAbility: data?.item?.ability,
template
}, dialogOptions);
if ( configured === null ) return null;
}
// Define the inner roll function
const _roll = (parts, adv, form) => {
// Determine the d20 roll and modifiers
let nd = 1;
let mods = halflingLucky ? "r1=1" : "";
// Handle advantage
if (adv === 1) {
nd = elvenAccuracy ? 3 : 2;
messageData.flavor += ` (${game.i18n.localize("SW5E.Advantage")})`;
if ( "flags.sw5e.roll" in messageData ) messageData["flags.sw5e.roll"].advantage = true;
mods += "kh";
}
// Handle disadvantage
else if (adv === -1) {
nd = 2;
messageData.flavor += ` (${game.i18n.localize("SW5E.Disadvantage")})`;
if ( "flags.sw5e.roll" in messageData ) messageData["flags.sw5e.roll"].disadvantage = true;
mods += "kl";
}
// Prepend the d20 roll
let formula = `${nd}d20${mods}`;
if (reliableTalent) formula = `{${nd}d20${mods},10}kh`;
parts.unshift(formula);
// Optionally include a situational bonus
if ( form ) {
data['bonus'] = form.bonus.value;
messageOptions.rollMode = form.rollMode.value;
}
if (!data["bonus"]) parts.pop();
// Optionally include an ability score selection (used for tool checks)
const ability = form ? form.ability : null;
if (ability && ability.value) {
data.ability = ability.value;
const abl = data.abilities[data.ability];
if (abl) {
data.mod = abl.mod;
messageData.flavor += ` (${CONFIG.SW5E.abilities[data.ability]})`;
}
}
// Execute the roll
let roll = new Roll(parts.join(" + "), data);
try {
roll.roll();
} catch (err) {
console.error(err);
ui.notifications.error(`Dice roll evaluation failed: ${err.message}`);
return null;
}
// Flag d20 options for any 20-sided dice in the roll
for (let d of roll.dice) {
if (d.faces === 20) {
d.options.critical = critical;
d.options.fumble = fumble;
if ( adv === 1 ) d.options.advantage = true;
else if ( adv === -1 ) d.options.disadvantage = true;
if (targetValue) d.options.target = targetValue;
}
}
// If reliable talent was applied, add it to the flavor text
if (reliableTalent && roll.dice[0].total < 10) {
messageData.flavor += ` (${game.i18n.localize("SW5E.FlagsReliableTalent")})`;
}
return roll;
};
// Create the Roll instance
const roll = fastForward ? _roll(parts, adv) :
await _d20RollDialog({template, title, parts, data, rollMode: messageOptions.rollMode, dialogOptions, roll: _roll});
// Evaluate the configured roll
await roll.evaluate({async: true});
// Create a Chat Message
if ( roll && chatMessage ) roll.toMessage(messageData, messageOptions);
if ( speaker ) {
console.warn(`You are passing the speaker argument to the d20Roll function directly which should instead be passed as an internal key of messageData`);
messageData.speaker = speaker;
}
if ( roll && chatMessage ) await roll.toMessage(messageData);
return roll;
}
/* -------------------------------------------- */
/**
* Present a Dialog form which creates a d20 roll once submitted
* @return {Promise<Roll>}
* @private
* Determines whether this d20 roll should be fast-forwarded, and whether advantage or disadvantage should be applied
* @returns {{isFF: boolean, advantageMode: number}} Whether the roll is fast-forward, and its advantage mode
*/
async function _d20RollDialog({template, title, parts, data, rollMode, dialogOptions, roll}={}) {
// Render modal dialog
template = template || "systems/sw5e/templates/chat/roll-dialog.html";
let dialogData = {
formula: parts.join(" + "),
data: data,
rollMode: rollMode,
rollModes: CONFIG.Dice.rollModes,
config: CONFIG.SW5E
};
const html = await renderTemplate(template, dialogData);
// Create the Dialog window
return new Promise(resolve => {
new Dialog({
title: title,
content: html,
buttons: {
advantage: {
label: game.i18n.localize("SW5E.Advantage"),
callback: html => resolve(roll(parts, 1, html[0].querySelector("form")))
},
normal: {
label: game.i18n.localize("SW5E.Normal"),
callback: html => resolve(roll(parts, 0, html[0].querySelector("form")))
},
disadvantage: {
label: game.i18n.localize("SW5E.Disadvantage"),
callback: html => resolve(roll(parts, -1, html[0].querySelector("form")))
}
},
default: "normal",
close: () => resolve(null)
}, dialogOptions).render(true);
});
function _determineAdvantageMode({event, advantage=false, disadvantage=false, fastForward=false}={}) {
const isFF = fastForward || (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
let advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.NORMAL;
if ( advantage || event?.altKey ) advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.ADVANTAGE;
else if ( disadvantage || event?.ctrlKey || event?.metaKey ) advantageMode = CONFIG.Dice.D20Roll.ADV_MODE.DISADVANTAGE;
return {isFF, advantageMode};
}
/* -------------------------------------------- */
/* Damage Roll */
/* -------------------------------------------- */
/**
* A standardized helper function for managing core 5e "d20 rolls"
*
* A standardized helper function for managing core 5e damage rolls.
* Holding SHIFT, ALT, or CTRL when the attack is rolled will "fast-forward".
* This chooses the default options of a normal attack with no bonus, Critical, or no bonus respectively
*
* @param {Array} parts The dice roll component parts, excluding the initial d20
* @param {Actor} actor The Actor making the damage roll
* @param {Object} data Actor or item data against which to parse the roll
* @param {Event|object}[event The triggering event which initiated the roll
* @param {string} rollMode A specific roll mode to apply as the default for the resulting roll
* @param {String} template The HTML template used to render the roll dialog
* @param {String} title The dice roll UI window title
* @param {Object} speaker The ChatMessage speaker to pass when creating the chat
* @param {string} flavor Flavor text to use in the posted chat message
* @param {boolean} allowCritical Allow the opportunity for a critical hit to be rolled
* @param {Boolean} critical Flag this roll as a critical hit for the purposes of fast-forward rolls
* @param {number} criticalBonusDice A number of bonus damage dice that are added for critical hits
* @param {number} criticalMultiplier A critical hit multiplier which is applied to critical hits
* @param {Boolean} fastForward Allow fast-forward advantage selection
* @param {Function} onClose Callback for actions to take when the dialog form is closed
* @param {Object} dialogOptions Modal dialog options
* @param {boolean} chatMessage Automatically create a Chat Message for the result of this roll
* @param {object} messageData Additional data which is applied to the created Chat Message, if any
* @param {string[]} parts The dice roll component parts, excluding the initial d20
* @param {object} [data] Actor or item data against which to parse the roll
*
* @return {Promise} A Promise which resolves once the roll workflow has completed
* @param {boolean} [critical=false] Flag this roll as a critical hit for the purposes of fast-forward or default dialog action
* @param {number} [criticalBonusDice=0] A number of bonus damage dice that are added for critical hits
* @param {number} [criticalMultiplier=2] A critical hit multiplier which is applied to critical hits
* @param {boolean} [multiplyNumeric=false] Multiply numeric terms by the critical multiplier
* @param {boolean} [powerfulCritical=false] Apply the "powerful criticals" house rule to critical hits
* @param {boolean} [fastForward=false] Allow fast-forward advantage selection
* @param {Event}[event] The triggering event which initiated the roll
* @param {boolean} [allowCritical=true] Allow the opportunity for a critical hit to be rolled
* @param {string} [template] The HTML template used to render the roll dialog
* @param {string} [title] The dice roll UI window title
* @param {object} [dialogOptions] Configuration dialog options
*
* @param {boolean} [chatMessage=true] Automatically create a Chat Message for the result of this roll
* @param {object} [messageData={}] Additional data which is applied to the created Chat Message, if any
* @param {string} [rollMode] A specific roll mode to apply as the default for the resulting roll
* @param {object} [speaker] The ChatMessage speaker to pass when creating the chat
* @param {string} [flavor] Flavor text to use in the posted chat message
*
* @return {Promise<DamageRoll|null>} The evaluated DamageRoll, or null if the workflow was canceled
*/
export async function damageRoll({parts, actor, data, event={}, rollMode=null, template, title, speaker, flavor,
allowCritical=true, critical=false, criticalBonusDice=0, criticalMultiplier=2, fastForward=null,
dialogOptions={}, chatMessage=true, messageData={}}={}) {
export async function damageRoll({
parts=[], data, // Roll creation
critical=false, criticalBonusDice, criticalMultiplier, multiplyNumeric, powerfulCritical, // Damage customization
fastForward=false, event, allowCritical=true, template, title, dialogOptions, // Dialog configuration
chatMessage=true, messageData={}, rollMode, speaker, flavor, // Chat Message customization
}={}) {
// Prepare Message Data
messageData.flavor = flavor || title;
messageData.speaker = speaker || ChatMessage.getSpeaker();
const messageOptions = {rollMode: rollMode || game.settings.get("core", "rollMode")};
parts = parts.concat(["@bonus"]);
// Handle input arguments
const defaultRollMode = rollMode || game.settings.get("core", "rollMode");
// Define inner roll function
const _roll = function(parts, crit, form) {
// Optionally include a situational bonus
if ( form ) {
data['bonus'] = form.bonus.value;
messageOptions.rollMode = form.rollMode.value;
}
if (!data["bonus"]) parts.pop();
// Create the damage roll
let roll = new Roll(parts.join("+"), data);
// Modify the damage formula for critical hits
if ( crit === true ) {
roll.alter(criticalMultiplier, 0); // Multiply all dice
if ( roll.terms[0] instanceof Die ) { // Add bonus dice for only the main dice term
roll.terms[0].alter(1, criticalBonusDice);
roll._formula = roll.formula;
}
messageData.flavor += ` (${game.i18n.localize("SW5E.Critical")})`;
if ( "flags.sw5e.roll" in messageData ) messageData["flags.sw5e.roll"].critical = true;
}
// Execute the roll
try {
roll.evaluate()
if ( crit ) roll.dice.forEach(d => d.options.critical = true); // TODO workaround core bug which wipes Roll#options on roll
return roll;
} catch(err) {
console.error(err);
ui.notifications.error(`Dice roll evaluation failed: ${err.message}`);
return null;
}
};
// Create the Roll instance
const roll = fastForward ? _roll(parts, critical) : await _damageRollDialog({
template, title, parts, data, allowCritical, rollMode: messageOptions.rollMode, dialogOptions, roll: _roll
// Construct the DamageRoll instance
const formula = parts.join(" + ");
const {isCritical, isFF} = _determineCriticalMode({critical, fastForward, event});
const roll = new CONFIG.Dice.DamageRoll(formula, data, {
flavor: flavor || title,
critical: isCritical,
criticalBonusDice,
criticalMultiplier,
multiplyNumeric,
powerfulCritical
});
// Create a Chat Message
if ( roll && chatMessage ) roll.toMessage(messageData, messageOptions);
return roll;
// Prompt a Dialog to further configure the DamageRoll
if ( !isFF ) {
const configured = await roll.configureDialog({
title,
defaultRollMode: defaultRollMode,
defaultCritical: isCritical,
template,
allowCritical
}, dialogOptions);
if ( configured === null ) return null;
}
// Evaluate the configured roll
await roll.evaluate({async: true});
// Create a Chat Message
if ( speaker ) {
console.warn(`You are passing the speaker argument to the damageRoll function directly which should instead be passed as an internal key of messageData`);
messageData.speaker = speaker;
}
if ( roll && chatMessage ) await roll.toMessage(messageData);
return roll;
}
/* -------------------------------------------- */
/**
* Present a Dialog form which creates a damage roll once submitted
* @return {Promise<Roll>}
* @private
* Determines whether this d20 roll should be fast-forwarded, and whether advantage or disadvantage should be applied
* @returns {{isFF: boolean, isCritical: boolean}} Whether the roll is fast-forward, and whether it is a critical hit
*/
async function _damageRollDialog({template, title, parts, data, allowCritical, rollMode, dialogOptions, roll}={}) {
// Render modal dialog
template = template || "systems/sw5e/templates/chat/roll-dialog.html";
let dialogData = {
formula: parts.join(" + "),
data: data,
rollMode: rollMode,
rollModes: CONFIG.Dice.rollModes
};
const html = await renderTemplate(template, dialogData);
// Create the Dialog window
return new Promise(resolve => {
new Dialog({
title: title,
content: html,
buttons: {
critical: {
condition: allowCritical,
label: game.i18n.localize("SW5E.CriticalHit"),
callback: html => resolve(roll(parts, true, html[0].querySelector("form")))
},
normal: {
label: game.i18n.localize(allowCritical ? "SW5E.Normal" : "SW5E.Roll"),
callback: html => resolve(roll(parts, false, html[0].querySelector("form")))
},
},
default: "normal",
close: () => resolve(null)
}, dialogOptions).render(true);
});
function _determineCriticalMode({event, critical=false, fastForward=false}={}) {
const isFF = fastForward || (event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey));
if ( event?.altKey ) critical = true;
return {isFF, isCritical: critical};
}

215
module/dice/d20-roll.js Normal file
View file

@ -0,0 +1,215 @@
/**
* A type of Roll specific to a d20-based check, save, or attack roll in the 5e system.
* @param {string} formula The string formula to parse
* @param {object} data The data object against which to parse attributes within the formula
* @param {object} [options={}] Extra optional arguments which describe or modify the D20Roll
* @param {number} [options.advantageMode] What advantage modifier to apply to the roll (none, advantage, disadvantage)
* @param {number} [options.critical] The value of d20 result which represents a critical success
* @param {number} [options.fumble] The value of d20 result which represents a critical failure
* @param {(number)} [options.targetValue] Assign a target value against which the result of this roll should be compared
* @param {boolean} [options.elvenAccuracy=false] Allow Elven Accuracy to modify this roll?
* @param {boolean} [options.halflingLucky=false] Allow Halfling Luck to modify this roll?
* @param {boolean} [options.reliableTalent=false] Allow Reliable Talent to modify this roll?
*/
export default class D20Roll extends Roll {
constructor(formula, data, options) {
super(formula, data, options);
if ( !((this.terms[0] instanceof Die) && (this.terms[0].faces === 20)) ) {
throw new Error(`Invalid D20Roll formula provided ${this._formula}`);
}
this.configureModifiers();
}
/* -------------------------------------------- */
/**
* Advantage mode of a 5e d20 roll
* @enum {number}
*/
static ADV_MODE = {
NORMAL: 0,
ADVANTAGE: 1,
DISADVANTAGE: -1,
}
/**
* The HTML template path used to configure evaluation of this Roll
* @type {string}
*/
static EVALUATION_TEMPLATE = "systems/sw5e/templates/chat/roll-dialog.html";
/* -------------------------------------------- */
/**
* A convenience reference for whether this D20Roll has advantage
* @type {boolean}
*/
get hasAdvantage() {
return this.options.advantageMode === D20Roll.ADV_MODE.ADVANTAGE;
}
/**
* A convenience reference for whether this D20Roll has disadvantage
* @type {boolean}
*/
get hasDisadvantage() {
return this.options.advantageMode === D20Roll.ADV_MODE.DISADVANTAGE;
}
/* -------------------------------------------- */
/* D20 Roll Methods */
/* -------------------------------------------- */
/**
* Apply optional modifiers which customize the behavior of the d20term
* @private
*/
configureModifiers() {
const d20 = this.terms[0];
d20.modifiers = [];
// Halfling Lucky
if ( this.options.halflingLucky ) d20.modifiers.push("r1=1");
// Reliable Talent
if ( this.options.reliableTalent ) d20.modifiers.push("min10");
// Handle Advantage or Disadvantage
if ( this.hasAdvantage ) {
d20.number = this.options.elvenAccuracy ? 3 : 2;
d20.modifiers.push("kh");
d20.options.advantage = true;
}
else if ( this.hasDisadvantage ) {
d20.number = 2;
d20.modifiers.push("kl");
d20.options.disadvantage = true;
}
else d20.number = 1;
// Assign critical and fumble thresholds
if ( this.options.critical ) d20.options.critical = this.options.critical;
if ( this.options.fumble ) d20.options.fumble = this.options.fumble;
if ( this.options.targetValue ) d20.options.target = this.options.targetValue;
// Re-compile the underlying formula
this._formula = this.constructor.getFormula(this.terms);
}
/* -------------------------------------------- */
/** @inheritdoc */
async toMessage(messageData={}, options={}) {
// Evaluate the roll now so we have the results available to determine whether reliable talent came into play
if ( !this._evaluated ) await this.evaluate({async: true});
// Add appropriate advantage mode message flavor and sw5e roll flags
messageData.flavor = messageData.flavor || this.options.flavor;
if ( this.hasAdvantage ) messageData.flavor += ` (${game.i18n.localize("SW5E.Advantage")})`;
else if ( this.hasDisadvantage ) messageData.flavor += ` (${game.i18n.localize("SW5E.Disadvantage")})`;
// Add reliable talent to the d20-term flavor text if it applied
if ( this.options.reliableTalent ) {
const d20 = this.dice[0];
const isRT = d20.results.every(r => !r.active || (r.result < 10));
const label = `(${game.i18n.localize("SW5E.FlagsReliableTalent")})`;
if ( isRT ) d20.options.flavor = d20.options.flavor ? `${d20.options.flavor} (${label})` : label;
}
// Record the preferred rollMode
options.rollMode = options.rollMode ?? this.options.rollMode;
return super.toMessage(messageData, options);
}
/* -------------------------------------------- */
/* Configuration Dialog */
/* -------------------------------------------- */
/**
* Create a Dialog prompt used to configure evaluation of an existing D20Roll instance.
* @param {object} data Dialog configuration data
* @param {string} [data.title] The title of the shown dialog window
* @param {number} [data.defaultRollMode] The roll mode that the roll mode select element should default to
* @param {number} [data.defaultAction] The button marked as default
* @param {boolean} [data.chooseModifier] Choose which ability modifier should be applied to the roll?
* @param {string} [data.defaultAbility] For tool rolls, the default ability modifier applied to the roll
* @param {string} [data.template] A custom path to an HTML template to use instead of the default
* @param {object} options Additional Dialog customization options
* @returns {Promise<D20Roll|null>} A resulting D20Roll object constructed with the dialog, or null if the dialog was closed
*/
async configureDialog({title, defaultRollMode, defaultAction=D20Roll.ADV_MODE.NORMAL, chooseModifier=false, defaultAbility, template}={}, options={}) {
// Render the Dialog inner HTML
const content = await renderTemplate(template ?? this.constructor.EVALUATION_TEMPLATE, {
formula: `${this.formula} + @bonus`,
defaultRollMode,
rollModes: CONFIG.Dice.rollModes,
chooseModifier,
defaultAbility,
abilities: CONFIG.SW5E.abilities
});
let defaultButton = "normal";
switch (defaultAction) {
case D20Roll.ADV_MODE.ADVANTAGE: defaultButton = "advantage"; break;
case D20Roll.ADV_MODE.DISADVANTAGE: defaultButton = "disadvantage"; break;
}
// Create the Dialog window and await submission of the form
return new Promise(resolve => {
new Dialog({
title,
content,
buttons: {
advantage: {
label: game.i18n.localize("SW5E.Advantage"),
callback: html => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.ADVANTAGE))
},
normal: {
label: game.i18n.localize("SW5E.Normal"),
callback: html => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.NORMAL))
},
disadvantage: {
label: game.i18n.localize("SW5E.Disadvantage"),
callback: html => resolve(this._onDialogSubmit(html, D20Roll.ADV_MODE.DISADVANTAGE))
}
},
default: defaultButton,
close: () => resolve(null)
}, options).render(true);
});
}
/* -------------------------------------------- */
/**
* Handle submission of the Roll evaluation configuration Dialog
* @param {jQuery} html The submitted dialog content
* @param {number} advantageMode The chosen advantage mode
* @private
*/
_onDialogSubmit(html, advantageMode) {
const form = html[0].querySelector("form");
// Append a situational bonus term
if ( form.bonus.value ) {
const bonus = new Roll(form.bonus.value, this.data);
if ( !(bonus.terms[0] instanceof OperatorTerm) ) this.terms.push(new OperatorTerm({operator: "+"}));
this.terms = this.terms.concat(bonus.terms);
}
// Customize the modifier
if ( form.ability?.value ) {
const abl = this.data.abilities[form.ability.value];
this.terms.findSplice(t => t.term === "@mod", new NumericTerm({number: abl.mod}));
this.options.flavor += ` (${CONFIG.SW5E.abilities[form.ability.value]})`;
}
// Apply advantage or disadvantage
this.options.advantageMode = advantageMode;
this.options.rollMode = form.rollMode.value;
this.configureModifiers();
return this;
}
}

181
module/dice/damage-roll.js Normal file
View file

@ -0,0 +1,181 @@
/**
* A type of Roll specific to a damage (or healing) roll in the 5e system.
* @param {string} formula The string formula to parse
* @param {object} data The data object against which to parse attributes within the formula
* @param {object} [options={}] Extra optional arguments which describe or modify the DamageRoll
* @param {number} [options.criticalBonusDice=0] A number of bonus damage dice that are added for critical hits
* @param {number} [options.criticalMultiplier=2] A critical hit multiplier which is applied to critical hits
* @param {boolean} [options.multiplyNumeric=false] Multiply numeric terms by the critical multiplier
* @param {boolean} [options.powerfulCritical=false] Apply the "powerful criticals" house rule to critical hits
*
*/
export default class DamageRoll extends Roll {
constructor(formula, data, options) {
super(formula, data, options);
// For backwards compatibility, skip rolls which do not have the "critical" option defined
if ( this.options.critical !== undefined ) this.configureDamage();
}
/**
* The HTML template path used to configure evaluation of this Roll
* @type {string}
*/
static EVALUATION_TEMPLATE = "systems/sw5e/templates/chat/roll-dialog.html";
/* -------------------------------------------- */
/**
* A convenience reference for whether this DamageRoll is a critical hit
* @type {boolean}
*/
get isCritical() {
return this.options.critical;
}
/* -------------------------------------------- */
/* Damage Roll Methods */
/* -------------------------------------------- */
/**
* Apply optional modifiers which customize the behavior of the d20term
* @private
*/
configureDamage() {
let flatBonus = 0;
for ( let [i, term] of this.terms.entries() ) {
// Multiply dice terms
if ( term instanceof DiceTerm ) {
term.options.baseNumber = term.options.baseNumber ?? term.number; // Reset back
term.number = term.options.baseNumber;
if ( this.isCritical ) {
let cm = this.options.criticalMultiplier ?? 2;
// Powerful critical - maximize damage and reduce the multiplier by 1
if ( this.options.powerfulCritical ) {
flatBonus += (term.number * term.faces);
cm = Math.max(1, cm-1);
}
// Alter the damage term
let cb = (this.options.criticalBonusDice && (i === 0)) ? this.options.criticalBonusDice : 0;
term.alter(cm, cb);
term.options.critical = true;
}
}
// Multiply numeric terms
else if ( this.options.multiplyNumeric && (term instanceof NumericTerm) ) {
term.options.baseNumber = term.options.baseNumber ?? term.number; // Reset back
term.number = term.options.baseNumber;
if ( this.isCritical ) {
term.number *= (this.options.criticalMultiplier ?? 2);
term.options.critical = true;
}
}
}
// Add powerful critical bonus
if ( this.options.powerfulCritical && (flatBonus > 0) ) {
this.terms.push(new OperatorTerm({operator: "+"}));
this.terms.push(new NumericTerm({number: flatBonus}, {flavor: game.i18n.localize("SW5E.PowerfulCritical")}));
}
// Re-compile the underlying formula
this._formula = this.constructor.getFormula(this.terms);
}
/* -------------------------------------------- */
/** @inheritdoc */
toMessage(messageData={}, options={}) {
messageData.flavor = messageData.flavor || this.options.flavor;
if ( this.isCritical ) {
const label = game.i18n.localize("SW5E.CriticalHit");
messageData.flavor = messageData.flavor ? `${messageData.flavor} (${label})` : label;
}
options.rollMode = options.rollMode ?? this.options.rollMode;
return super.toMessage(messageData, options);
}
/* -------------------------------------------- */
/* Configuration Dialog */
/* -------------------------------------------- */
/**
* Create a Dialog prompt used to configure evaluation of an existing D20Roll instance.
* @param {object} data Dialog configuration data
* @param {string} [data.title] The title of the shown dialog window
* @param {number} [data.defaultRollMode] The roll mode that the roll mode select element should default to
* @param {string} [data.defaultCritical] Should critical be selected as default
* @param {string} [data.template] A custom path to an HTML template to use instead of the default
* @param {boolean} [data.allowCritical=true] Allow critical hit to be chosen as a possible damage mode
* @param {object} options Additional Dialog customization options
* @returns {Promise<D20Roll|null>} A resulting D20Roll object constructed with the dialog, or null if the dialog was closed
*/
async configureDialog({title, defaultRollMode, defaultCritical=false, template, allowCritical=true}={}, options={}) {
// Render the Dialog inner HTML
const content = await renderTemplate(template ?? this.constructor.EVALUATION_TEMPLATE, {
formula: `${this.formula} + @bonus`,
defaultRollMode,
rollModes: CONFIG.Dice.rollModes,
});
// Create the Dialog window and await submission of the form
return new Promise(resolve => {
new Dialog({
title,
content,
buttons: {
critical: {
condition: allowCritical,
label: game.i18n.localize("SW5E.CriticalHit"),
callback: html => resolve(this._onDialogSubmit(html, true))
},
normal: {
label: game.i18n.localize(allowCritical ? "SW5E.Normal" : "SW5E.Roll"),
callback: html => resolve(this._onDialogSubmit(html, false))
}
},
default: defaultCritical ? "critical" : "normal",
close: () => resolve(null)
}, options).render(true);
});
}
/* -------------------------------------------- */
/**
* Handle submission of the Roll evaluation configuration Dialog
* @param {jQuery} html The submitted dialog content
* @param {boolean} isCritical Is the damage a critical hit?
* @private
*/
_onDialogSubmit(html, isCritical) {
const form = html[0].querySelector("form");
// Append a situational bonus term
if ( form.bonus.value ) {
const bonus = new Roll(form.bonus.value, this.data);
if ( !(bonus.terms[0] instanceof OperatorTerm) ) this.terms.push(new OperatorTerm({operator: "+"}));
this.terms = this.terms.concat(bonus.terms);
}
// Apply advantage or disadvantage
this.options.critical = isCritical;
this.options.rollMode = form.rollMode.value;
this.configureDamage();
return this;
}
/* -------------------------------------------- */
/** @inheritdoc */
static fromData(data) {
const roll = super.fromData(data);
roll._formula = this.getFormula(roll.terms);
return roll;
}
}

View file

@ -0,0 +1,15 @@
/**
* @deprecated since 1.3.0
* @ignore
*/
async function d20Dialog(data, options) {
throw new Error(`The d20Dialog helper method is deprecated in favor of D20Roll#configureDialog`);
}
/**
* @deprecated since 1.3.0
* @ignore
*/
async function damageDialog(data, options) {
throw new Error(`The damageDialog helper method is deprecated in favor of DamageRoll#configureDialog`);
}

12
module/effects.js vendored
View file

@ -10,13 +10,13 @@ export function onManageActiveEffect(event, owner) {
const effect = li.dataset.effectId ? owner.effects.get(li.dataset.effectId) : null;
switch ( a.dataset.action ) {
case "create":
return ActiveEffect.create({
label: "New Effect",
return owner.createEmbeddedDocuments("ActiveEffect", [{
label: game.i18n.localize("SW5E.EffectNew"),
icon: "icons/svg/aura.svg",
origin: owner.uuid,
"duration.rounds": li.dataset.effectType === "temporary" ? 1 : undefined,
disabled: li.dataset.effectType === "inactive"
}, owner).create();
}]);
case "edit":
return effect.sheet.render(true);
case "delete":
@ -37,17 +37,17 @@ export function prepareActiveEffectCategories(effects) {
const categories = {
temporary: {
type: "temporary",
label: "Temporary Effects",
label: game.i18n.localize("SW5E.EffectTemporary"),
effects: []
},
passive: {
type: "passive",
label: "Passive Effects",
label: game.i18n.localize("SW5E.EffectPassive"),
effects: []
},
inactive: {
type: "inactive",
label: "Inactive Effects",
label: game.i18n.localize("SW5E.EffectInactive"),
effects: []
}
};

View file

@ -2,7 +2,8 @@ import {simplifyRollFormula, d20Roll, damageRoll} from "../dice.js";
import AbilityUseDialog from "../apps/ability-use-dialog.js";
/**
* Override and extend the basic :class:`Item` implementation
* Override and extend the basic Item implementation
* @extends {Item}
*/
export default class Item5e extends Item {
@ -35,12 +36,14 @@ export default class Item5e extends Item {
else if (this.data.type === "weapon") {
const wt = itemData.weaponType;
// Melee weapons - Str or Dex if Finesse (PHB pg. 147)
if ( ["simpleM", "martialM"].includes(wt) ) {
if (itemData.properties.fin === true) { // Finesse weapons
return (actorData.abilities["dex"].mod >= actorData.abilities["str"].mod) ? "dex" : "str";
}
return "str";
//Weapons using the powercasting modifier
if (["mpak", "rpak"].includes(itemData.actionType)) {
return actorData.attributes.powercasting || "int";
}
// Finesse weapons - Str or Dex (PHB pg. 147)
else if (itemData.properties.fin === true) {
return (actorData.abilities["dex"].mod >= actorData.abilities["str"].mod) ? "dex" : "str";
}
// Ranged weapons - Dex (PH p.194)
@ -135,7 +138,7 @@ export default class Item5e extends Item {
get hasLimitedUses() {
let chg = this.data.data.recharge || {};
let uses = this.data.data.uses || {};
return !!chg.value || (!!uses.per && (uses.max > 0));
return !!chg.value || (uses.per && (uses.max > 0));
}
/* -------------------------------------------- */
@ -145,8 +148,8 @@ export default class Item5e extends Item {
/**
* Augment the basic Item data model with additional dynamic data.
*/
prepareData() {
super.prepareData();
prepareDerivedData() {
super.prepareDerivedData();
// Get the Item's data
const itemData = this.data;
@ -204,7 +207,7 @@ export default class Item5e extends Item {
// Range Label
let rng = data.range || {};
if (["none", "touch", "self"].includes(rng.units) || (rng.value === 0)) {
if ( ["none", "touch", "self"].includes(rng.units) ) {
rng.value = null;
rng.long = null;
}
@ -222,36 +225,66 @@ export default class Item5e extends Item {
// Item Actions
if ( data.hasOwnProperty("actionType") ) {
// if this item is owned, we populate the label and saving throw during actor init
if (!this.isOwned) {
// Saving throws
this.getSaveDC();
// To Hit
this.getAttackToHit();
}
// Damage
let dam = data.damage || {};
if ( dam.parts ) {
labels.damage = dam.parts.map(d => d[0]).join(" + ").replace(/\+ -/g, "- ");
labels.damageTypes = dam.parts.map(d => C.damageTypes[d[1]]).join(", ");
}
}
// if this item is owned, we prepareFinalAttributes() at the end of actor init
if (!this.isOwned) this.prepareFinalAttributes();
}
/* -------------------------------------------- */
/**
* Compute item attributes which might depend on prepared actor data.
*/
prepareFinalAttributes() {
if ( this.data.data.hasOwnProperty("actionType") ) {
// Saving throws
this.getSaveDC();
// To Hit
this.getAttackToHit();
// Limited Uses
if ( this.isOwned && !!data.uses?.max ) {
let max = data.uses.max;
if ( !Number.isNumeric(max) ) {
max = Roll.replaceFormulaData(max, this.actor.getRollData());
if ( Roll.MATH_PROXY.safeEval ) max = Roll.MATH_PROXY.safeEval(max);
}
data.uses.max = Number(max);
}
this.prepareMaxUses();
// Damage Label
this.getDerivedDamageLabel();
}
}
/* -------------------------------------------- */
/**
* Populate a label with the compiled and simplified damage formula
* based on owned item actor data. This is only used for display
* purposes and is not related to Item5e#rollDamage
*
* @returns {Array} array of objects with `formula` and `damageType`
*/
getDerivedDamageLabel() {
const itemData = this.data.data;
if ( !this.hasDamage || !itemData || !this.isOwned ) return [];
const rollData = this.getRollData();
const derivedDamage = itemData.damage?.parts?.map((damagePart) => ({
formula: simplifyRollFormula(damagePart[0], rollData, { constantFirst: false }),
damageType: damagePart[1],
}));
this.labels.derivedDamage = derivedDamage
return derivedDamage;
}
/* -------------------------------------------- */
/**
* Update the derived power DC for an item that requires a saving throw
* @returns {number|null}
@ -346,6 +379,31 @@ export default class Item5e extends Item {
/* -------------------------------------------- */
/**
* Populates the max uses of an item.
* If the item is an owned item and the `max` is not numeric, calculate based on actor data.
*/
prepareMaxUses() {
const data = this.data.data;
if (!data.uses?.max) return;
let max = data.uses.max;
// if this is an owned item and the max is not numeric, we need to calculate it
if (this.isOwned && !Number.isNumeric(max)) {
if (this.actor.data === undefined) return;
try {
max = Roll.replaceFormulaData(max, this.actor.getRollData(), {missing: 0, warn: true});
max = Roll.safeEval(max);
} catch(e) {
console.error('Problem preparing Max uses for', this.data.name, e);
return;
}
}
data.uses.max = Number(max);
}
/* -------------------------------------------- */
/**
* Roll the item to Chat, creating a chat card which contains follow up attack or damage roll options
* @param {boolean} [configureDialog] Display a configuration dialog for the item roll, if applicable?
@ -356,10 +414,11 @@ export default class Item5e extends Item {
*/
async roll({configureDialog=true, rollMode, createMessage=true}={}) {
let item = this;
const id = this.data.data; // Item system data
const actor = this.actor;
const ad = actor.data.data; // Actor system data
// Reference aspects of the item data necessary for usage
const id = this.data.data; // Item data
const hasArea = this.hasAreaTarget; // Is the ability usage an AoE?
const resource = id.consume || {}; // Resource consumption
const recharge = id.recharge || {}; // Recharge mechanic
@ -374,6 +433,8 @@ export default class Item5e extends Item {
let consumePowerSlot = requirePowerSlot; // Consume a power slot
let consumeUsage = !!uses.per; // Consume limited uses
let consumeQuantity = uses.autoDestroy; // Consume quantity of the item in lieu of uses
let consumePowerLevel = null; // Consume a specific category of power slot
if ( requirePowerSlot ) consumePowerLevel = id.preparation.mode === "pact" ? "pact" : `power${id.level}`;
// Display a configuration dialog to customize the usage
const needsConfiguration = createMeasuredTemplate || consumeRecharge || consumeResource || consumePowerSlot || consumeUsage;
@ -390,26 +451,27 @@ export default class Item5e extends Item {
// Handle power upcasting
if ( requirePowerSlot ) {
const slotLevel = configuration.level;
const powerLevel = slotLevel === "pact" ? actor.data.data.powers.pact.level : parseInt(slotLevel);
if (powerLevel !== id.level) {
const upcastData = mergeObject(this.data, {"data.level": powerLevel}, {inplace: false});
item = this.constructor.createOwned(upcastData, actor); // Replace the item with an upcast version
consumePowerLevel = configuration.level === "pact" ? "pact" : `power${configuration.level}`;
if ( consumePowerSlot === false ) consumePowerLevel = null;
const upcastLevel = configuration.level === "pact" ? ad.powers.pact.level : parseInt(configuration.level);
if (upcastLevel !== id.level) {
item = this.clone({"data.level": upcastLevel}, {keepId: true});
item.data.update({_id: this.id}); // Retain the original ID (needed until 0.8.2+)
item.prepareFinalAttributes(); // Power save DC, etc...
}
if ( consumePowerSlot ) consumePowerSlot = slotLevel === "pact" ? "pact" : `power${powerLevel}`;
}
}
// Determine whether the item can be used by testing for resource consumption
const usage = item._getUsageUpdates({consumeRecharge, consumeResource, consumePowerSlot, consumeUsage, consumeQuantity});
const usage = item._getUsageUpdates({consumeRecharge, consumeResource, consumePowerLevel, consumeUsage, consumeQuantity});
if ( !usage ) return;
const {actorUpdates, itemUpdates, resourceUpdates} = usage;
// Commit pending data updates
if ( !isObjectEmpty(itemUpdates) ) await item.update(itemUpdates);
if ( !foundry.utils.isObjectEmpty(itemUpdates) ) await item.update(itemUpdates);
if ( consumeQuantity && (item.data.data.quantity === 0) ) await item.delete();
if ( !isObjectEmpty(actorUpdates) ) await actor.update(actorUpdates);
if ( !isObjectEmpty(resourceUpdates) ) {
if ( !foundry.utils.isObjectEmpty(actorUpdates) ) await actor.update(actorUpdates);
if ( !foundry.utils.isObjectEmpty(resourceUpdates) ) {
const resource = actor.items.get(id.consume?.target);
if ( resource ) await resource.update(resourceUpdates);
}
@ -432,12 +494,12 @@ export default class Item5e extends Item {
* @param {boolean} consumeQuantity Consume quantity of the item if other consumption modes are not available?
* @param {boolean} consumeRecharge Whether the item consumes the recharge mechanic
* @param {boolean} consumeResource Whether the item consumes a limited resource
* @param {string|boolean} consumePowerSlot A level of power slot consumed, or false
* @param {string|null} consumePowerLevel The category of power slot to consume, or null
* @param {boolean} consumeUsage Whether the item consumes a limited usage
* @returns {object|boolean} A set of data changes to apply when the item is used, or false
* @private
*/
_getUsageUpdates({consumeQuantity=false, consumeRecharge=false, consumeResource=false, consumePowerSlot=false, consumeUsage=false}) {
_getUsageUpdates({consumeQuantity, consumeRecharge, consumeResource, consumePowerLevel, consumeUsage}) {
// Reference item data
const id = this.data.data;
@ -462,15 +524,16 @@ export default class Item5e extends Item {
}
// Consume Power Slots
if ( consumePowerSlot ) {
const level = this.actor?.data.data.powers[consumePowerSlot];
if ( consumePowerLevel ) {
if ( Number.isNumeric(consumePowerLevel) ) consumePowerLevel = `power${consumePowerLevel}`;
const level = this.actor?.data.data.powers[consumePowerLevel];
const powers = Number(level?.value ?? 0);
if ( powers === 0 ) {
const label = game.i18n.localize(consumePowerSlot === "pact" ? "SW5E.PowerProgPact" : `SW5E.PowerLevel${id.level}`);
const label = game.i18n.localize(consumePowerLevel === "pact" ? "SW5E.PowerProgPact" : `SW5E.PowerLevel${id.level}`);
ui.notifications.warn(game.i18n.format("SW5E.PowerCastNoSlots", {name: this.name, level: label}));
return false;
}
actorUpdates[`data.powers.${consumePowerSlot}.value`] = Math.max(powers - 1, 0);
actorUpdates[`data.powers.${consumePowerLevel}.value`] = Math.max(powers - 1, 0);
}
// Consume Limited Usage
@ -598,11 +661,11 @@ export default class Item5e extends Item {
*/
async displayCard({rollMode, createMessage=true}={}) {
// Basic template rendering data
// Render the chat card template
const token = this.actor.token;
const templateData = {
actor: this.actor,
tokenId: token ? `${token.scene._id}.${token.id}` : null,
tokenId: token?.uuid || null,
item: this.data,
data: this.getChatData(),
labels: this.labels,
@ -612,13 +675,10 @@ export default class Item5e extends Item {
isVersatile: this.isVersatile,
isPower: this.data.type === "power",
hasSave: this.hasSave,
hasAreaTarget: this.hasAreaTarget
hasAreaTarget: this.hasAreaTarget,
isTool: this.data.type === "tool"
};
// Render the chat card template
const templateType = ["tool"].includes(this.data.type) ? this.data.type : "item";
const template = `systems/sw5e/templates/chat/${templateType}-card.html`;
const html = await renderTemplate(template, templateData);
const html = await renderTemplate("systems/sw5e/templates/chat/item-card.html", templateData);
// Create the ChatMessage data object
const chatData = {
@ -652,7 +712,7 @@ export default class Item5e extends Item {
* @return {Object} An object of chat data to render
*/
getChatData(htmlOptions={}) {
const data = duplicate(this.data.data);
const data = foundry.utils.deepClone(this.data.data);
const labels = this.labels;
// Rich text description
@ -846,10 +906,8 @@ export default class Item5e extends Item {
}
// Elven Accuracy
if ( ["weapon", "power"].includes(this.data.type) ) {
if (flags.elvenAccuracy && ["dex", "int", "wis", "cha"].includes(this.abilityMod)) {
rollConfig.elvenAccuracy = true;
}
if ( flags.elvenAccuracy && ["dex", "int", "wis", "cha"].includes(this.abilityMod) ) {
rollConfig.elvenAccuracy = true;
}
// Apply Halfling Lucky
@ -1086,11 +1144,17 @@ export default class Item5e extends Item {
const parts = [`@mod`, "@prof"];
const title = `${this.name} - ${game.i18n.localize("SW5E.ToolCheck")}`;
// Add global actor bonus
const bonuses = getProperty(this.actor.data.data, "bonuses.abilities") || {};
if ( bonuses.check ) {
parts.push("@checkBonus");
rollData.checkBonus = bonuses.check;
}
// Compose the roll data
const rollConfig = mergeObject({
parts: parts,
data: rollData,
template: "systems/sw5e/templates/chat/tool-roll-dialog.html",
title: title,
speaker: ChatMessage.getSpeaker({actor: this.actor}),
flavor: title,
@ -1099,6 +1163,7 @@ export default class Item5e extends Item {
top: options.event ? options.event.clientY - 80 : null,
left: window.innerWidth - 710,
},
chooseModifier: true,
halflingLucky: this.actor.getFlag("sw5e", "halflingLucky" ) || false,
reliableTalent: (this.data.data.proficient >= 1) && this.actor.getFlag("sw5e", "reliableTalent"),
messageData: {"flags.sw5e.roll": {type: "tool", itemId: this.id }}
@ -1118,13 +1183,16 @@ export default class Item5e extends Item {
getRollData() {
if ( !this.actor ) return null;
const rollData = this.actor.getRollData();
rollData.item = duplicate(this.data.data);
rollData.item = foundry.utils.deepClone(this.data.data);
// Include an ability score modifier if one exists
const abl = this.abilityMod;
if ( abl ) {
const ability = rollData.abilities[abl];
rollData["mod"] = ability.mod || 0;
if ( !ability ) {
console.warn(`Item ${this.name} in Actor ${this.actor.name} has an invalid item ability modifier of ${abl} defined`);
}
rollData["mod"] = ability?.mod || 0;
}
// Include a proficiency score
@ -1166,12 +1234,12 @@ export default class Item5e extends Item {
if ( !( isTargetted || game.user.isGM || message.isAuthor ) ) return;
// Recover the actor for the chat card
const actor = this._getChatCardActor(card);
const actor = await this._getChatCardActor(card);
if ( !actor ) return;
// Get the Item from stored flag data or by the item ID on the Actor
const storedData = message.getFlag("sw5e", "itemData");
const item = storedData ? this.createOwned(storedData, actor) : actor.getOwnedItem(card.dataset.itemId);
const item = storedData ? new this(storedData, {parent: actor}) : actor.items.get(card.dataset.itemId);
if ( !item ) {
return ui.notifications.error(game.i18n.format("SW5E.ActionWarningNoItem", {item: card.dataset.itemId, name: actor.name}))
}
@ -1234,17 +1302,12 @@ export default class Item5e extends Item {
* @return {Actor|null} The Actor entity or null
* @private
*/
static _getChatCardActor(card) {
static async _getChatCardActor(card) {
// Case 1 - a synthetic actor from a Token
const tokenKey = card.dataset.tokenId;
if (tokenKey) {
const [sceneId, tokenId] = tokenKey.split(".");
const scene = game.scenes.get(sceneId);
if (!scene) return null;
const tokenData = scene.getEmbeddedEntity("Token", tokenId);
if (!tokenData) return null;
const token = new Token(tokenData);
if ( card.dataset.tokenId ) {
const token = await fromUuid(card.dataset.tokenId);
if ( !token ) return null;
return token.actor;
}
@ -1258,7 +1321,7 @@ export default class Item5e extends Item {
/**
* Get the Actor which is the author of a chat card
* @param {HTMLElement} card The chat card being used
* @return {Array.<Actor>} An Array of Actor entities, if any
* @return {Actor[]} An Array of Actor entities, if any
* @private
*/
static _getChatCardTargets(card) {
@ -1268,6 +1331,153 @@ export default class Item5e extends Item {
return targets;
}
/* -------------------------------------------- */
/* Event Handlers */
/* -------------------------------------------- */
/** @inheritdoc */
async _preCreate(data, options, user) {
await super._preCreate(data, options, user);
if ( !this.isEmbedded || (this.parent.type === "vehicle") ) return;
const actorData = this.parent.data;
const isNPC = this.parent.type === "npc";
let updates;
switch (data.type) {
case "equipment":
updates = this._onCreateOwnedEquipment(data, actorData, isNPC);
break;
case "weapon":
updates = this._onCreateOwnedWeapon(data, actorData, isNPC);
break;
case "power":
updates = this._onCreateOwnedPower(data, actorData, isNPC);
break;
}
if (updates) return this.data.update(updates);
}
/* -------------------------------------------- */
/** @inheritdoc */
_onCreate(data, options, userId) {
super._onCreate(data, options, userId);
// The below options are only needed for character classes
if ( userId !== game.user.id ) return;
const isCharacterClass = this.parent && (this.parent.type !== "vehicle") && (this.type === "class");
if ( !isCharacterClass ) return;
// Assign a new primary class
const pc = this.parent.items.get(this.parent.data.data.details.originalClass);
if ( !pc ) this.parent._assignPrimaryClass();
// Prompt to add new class features
if (options.addFeatures === false) return;
this.parent.getClassFeatures({
className: this.name,
archetypeName: this.data.data.archetype,
level: this.data.data.levels
}).then(features => {
return this.parent.addEmbeddedItems(features, options.promptAddFeatures);
});
}
/* -------------------------------------------- */
/** @inheritdoc */
_onUpdate(changed, options, userId) {
super._onUpdate(changed, options, userId);
// The below options are only needed for character classes
if ( userId !== game.user.id ) return;
const isCharacterClass = this.parent && (this.parent.type !== "vehicle") && (this.type === "class");
if ( !isCharacterClass ) return;
// Prompt to add new class features
const addFeatures = changed["name"] || (changed.data && ["archetype", "levels"].some(k => k in changed.data));
if ( !addFeatures || (options.addFeatures === false) ) return;
this.parent.getClassFeatures({
className: changed.name || this.name,
archetypeName: changed.data?.archetype || this.data.data.archetype,
level: changed.data?.levels || this.data.data.levels
}).then(features => {
return this.parent.addEmbeddedItems(features, options.promptAddFeatures);
});
}
/* -------------------------------------------- */
/** @inheritdoc */
_onDelete(options, userId) {
super._onDelete(options, userId);
// Assign a new primary class
if ( this.parent && (this.type === "class") && (userId === game.user.id) ) {
if ( this.id !== this.parent.data.data.details.originalClass ) return;
this.parent._assignPrimaryClass();
}
}
/* -------------------------------------------- */
/**
* Pre-creation logic for the automatic configuration of owned equipment type Items
* @private
*/
_onCreateOwnedEquipment(data, actorData, isNPC) {
const updates = {};
if ( foundry.utils.getProperty(data, "data.equipped") === undefined ) {
updates["data.equipped"] = isNPC; // NPCs automatically equip equipment
}
if ( foundry.utils.getProperty(data, "data.proficient") === undefined ) {
if ( isNPC ) {
updates["data.proficient"] = true; // NPCs automatically have equipment proficiency
} else {
const armorProf = CONFIG.SW5E.armorProficienciesMap[data.data?.armor?.type]; // Player characters check proficiency
const actorArmorProfs = actorData.data.traits?.armorProf?.value || [];
updates["data.proficient"] = (armorProf === true) || actorArmorProfs.includes(armorProf);
}
}
return updates;
}
/* -------------------------------------------- */
/**
* Pre-creation logic for the automatic configuration of owned power type Items
* @private
*/
_onCreateOwnedPower(data, actorData, isNPC) {
const updates = {};
if ( foundry.utils.getProperty(data, "data.proficient") === undefined ) {
updates["data.prepared"] = isNPC; // NPCs automatically prepare powers
}
return updates;
}
/* -------------------------------------------- */
/**
* Pre-creation logic for the automatic configuration of owned weapon type Items
* @private
*/
_onCreateOwnedWeapon(data, actorData, isNPC) {
const updates = {};
if ( foundry.utils.getProperty(data, "data.equipped") === undefined ) {
updates["data.equipped"] = isNPC; // NPCs automatically equip weapons
}
if ( foundry.utils.getProperty(data, "data.proficient") === undefined ) {
if ( isNPC ) {
updates["data.proficient"] = true; // NPCs automatically have equipment proficiency
} else {
const weaponProf = CONFIG.SW5E.weaponProficienciesMap[data.data?.weaponType]; // Player characters check proficiency
const actorWeaponProfs = actorData.data.traits?.weaponProf?.value || [];
updates["data.proficient"] = (weaponProf === true) || actorWeaponProfs.includes(weaponProf);
}
}
return updates;
}
/* -------------------------------------------- */
/* Factory Methods */
/* -------------------------------------------- */
@ -1276,7 +1486,6 @@ export default class Item5e extends Item {
* Create a consumable power scroll Item from a power Item.
* @param {Item5e} power The power to be made into a scroll
* @return {Item5e} The created scroll consumable item
* @private
*/
static async createScrollFromPower(power) {
@ -1285,7 +1494,7 @@ export default class Item5e extends Item {
const {actionType, description, source, activation, duration, target, range, damage, save, level} = itemData.data;
// Get scroll data
const scrollUuid = CONFIG.SW5E.powerScrollIds[level];
const scrollUuid = `Compendium.${CONFIG.SW5E.sourcePacks.ITEMS}.${CONFIG.SW5E.powerScrollIds[level]}`;
const scrollItem = await fromUuid(scrollUuid);
const scrollData = scrollItem.data;
delete scrollData._id;

View file

@ -18,9 +18,9 @@ export default class ItemSheet5e extends ItemSheet {
/* -------------------------------------------- */
/** @override */
/** @inheritdoc */
static get defaultOptions() {
return mergeObject(super.defaultOptions, {
return foundry.utils.mergeObject(super.defaultOptions, {
width: 560,
height: 400,
classes: ["sw5e", "sheet", "item"],
@ -32,7 +32,7 @@ export default class ItemSheet5e extends ItemSheet {
/* -------------------------------------------- */
/** @override */
/** @inheritdoc */
get template() {
const path = "systems/sw5e/templates/items/";
return `${path}/${this.item.data.type}.html`;
@ -43,33 +43,39 @@ export default class ItemSheet5e extends ItemSheet {
/** @override */
async getData(options) {
const data = super.getData(options);
const itemData = data.data;
data.labels = this.item.labels;
data.config = CONFIG.SW5E;
// Item Type, Status, and Details
data.itemType = game.i18n.localize(`ITEM.Type${data.item.type.titleCase()}`);
data.itemStatus = this._getItemStatus(data.item);
data.itemProperties = this._getItemProperties(data.item);
data.isPhysical = data.item.data.hasOwnProperty("quantity");
data.itemStatus = this._getItemStatus(itemData);
data.itemProperties = this._getItemProperties(itemData);
data.isPhysical = itemData.data.hasOwnProperty("quantity");
// Potential consumption targets
data.abilityConsumptionTargets = this._getItemConsumptionTargets(data.item);
data.abilityConsumptionTargets = this._getItemConsumptionTargets(itemData);
// Action Details
data.hasAttackRoll = this.item.hasAttack;
data.isHealing = data.item.data.actionType === "heal";
data.isFlatDC = getProperty(data.item.data, "save.scaling") === "flat";
data.isLine = ["line", "wall"].includes(data.item.data.target?.type);
data.isHealing = itemData.data.actionType === "heal";
data.isFlatDC = getProperty(itemData, "data.save.scaling") === "flat";
data.isLine = ["line", "wall"].includes(itemData.data.target?.type);
// Original maximum uses formula
if ( this.item._data.data?.uses?.max ) data.data.uses.max = this.item._data.data.uses.max;
const sourceMax = foundry.utils.getProperty(this.item.data._source, "data.uses.max");
if ( sourceMax ) itemData.data.uses.max = sourceMax;
// Vehicles
data.isCrewed = data.item.data.activation?.type === 'crew';
data.isMountable = this._isItemMountable(data.item);
data.isCrewed = itemData.data.activation?.type === 'crew';
data.isMountable = this._isItemMountable(itemData);
// Prepare Active Effects
data.effects = prepareActiveEffectCategories(this.entity.effects);
data.effects = prepareActiveEffectCategories(this.item.effects);
// Re-define the template data references (backwards compatible)
data.item = itemData;
data.data = itemData.data;
return data;
}
@ -99,9 +105,11 @@ export default class ItemSheet5e extends ItemSheet {
// Attributes
else if ( consume.type === "attribute" ) {
const attributes = Object.values(CombatTrackerConfig.prototype.getAttributeChoices())[0]; // Bit of a hack
return attributes.reduce((obj, a) => {
obj[a] = a;
const attributes = TokenDocument.getTrackedAttributes(actor.data.data);
attributes.bar.forEach(a => a.push("value"));
return attributes.bar.concat(attributes.value).reduce((obj, a) => {
let k = a.join(".");
obj[k] = k;
return obj;
}, {});
}
@ -227,7 +235,7 @@ export default class ItemSheet5e extends ItemSheet {
/* -------------------------------------------- */
/** @override */
/** @inheritdoc */
setPosition(position={}) {
if ( !(this._minimized || position.height) ) {
position.height = (this._tabs[0].active === "details") ? "auto" : this.options.height;
@ -239,7 +247,7 @@ export default class ItemSheet5e extends ItemSheet {
/* Form Submission */
/* -------------------------------------------- */
/** @override */
/** @inheritdoc */
_getSubmitData(updateData={}) {
// Create the expanded update data object
@ -258,12 +266,12 @@ export default class ItemSheet5e extends ItemSheet {
/* -------------------------------------------- */
/** @override */
/** @inheritdoc */
activateListeners(html) {
super.activateListeners(html);
if ( this.isEditable ) {
html.find(".damage-control").click(this._onDamageControl.bind(this));
html.find('.trait-selector.class-skills').click(this._onConfigureClassSkills.bind(this));
html.find('.trait-selector').click(this._onConfigureTraits.bind(this));
html.find(".effect-control").click(ev => {
if ( this.item.isOwned ) return ui.notifications.warn("Managing Active Effects within an Owned Item is not currently supported and will be added in a subsequent update.")
onManageActiveEffect(ev, this.item)
@ -294,7 +302,7 @@ export default class ItemSheet5e extends ItemSheet {
if ( a.classList.contains("delete-damage") ) {
await this._onSubmit(event); // Submit any unsaved changes
const li = a.closest(".damage-part");
const damage = duplicate(this.item.data.data.damage);
const damage = foundry.utils.deepClone(this.item.data.data.damage);
damage.parts.splice(Number(li.dataset.damagePart), 1);
return this.item.update({"data.damage.parts": damage.parts});
}
@ -303,33 +311,37 @@ export default class ItemSheet5e extends ItemSheet {
/* -------------------------------------------- */
/**
* Handle spawning the TraitSelector application which allows a checkbox of multiple trait options
* Handle spawning the TraitSelector application for selection various options.
* @param {Event} event The click event which originated the selection
* @private
*/
_onConfigureClassSkills(event) {
_onConfigureTraits(event) {
event.preventDefault();
const skills = this.item.data.data.skills;
const choices = skills.choices && skills.choices.length ? skills.choices : Object.keys(CONFIG.SW5E.skills);
const a = event.currentTarget;
const label = a.parentElement;
// Render the Trait Selector dialog
new TraitSelector(this.item, {
const options = {
name: a.dataset.target,
title: label.innerText,
choices: Object.entries(CONFIG.SW5E.skills).reduce((obj, e) => {
if ( choices.includes(e[0] ) ) obj[e[0]] = e[1];
return obj;
}, {}),
minimum: skills.number,
maximum: skills.number
}).render(true)
title: a.parentElement.innerText,
choices: [],
allowCustom: false
};
switch(a.dataset.options) {
case 'saves':
options.choices = CONFIG.SW5E.abilities;
options.valueKey = null;
break;
case 'skills':
const skills = this.item.data.data.skills;
const choiceSet = skills.choices && skills.choices.length ? skills.choices : Object.keys(CONFIG.SW5E.skills);
options.choices = Object.fromEntries(Object.entries(CONFIG.SW5E.skills).filter(skill => choiceSet.includes(skill[0])));
options.maximum = skills.number;
break;
}
new TraitSelector(this.item, options).render(true);
}
/* -------------------------------------------- */
/** @override */
/** @inheritdoc */
async _onSubmit(...args) {
if ( this._tabs[0].active === "details" ) this.position.height = "auto";
await super._onSubmit(...args);

View file

@ -6,10 +6,10 @@ export const migrateWorld = async function() {
ui.notifications.info(`Applying SW5e System Migration for version ${game.system.data.version}. Please be patient and do not close your game or shut down your server.`, {permanent: true});
// Migrate World Actors
for ( let a of game.actors.entities ) {
for ( let a of game.actors.contents ) {
try {
const updateData = migrateActorData(a.data);
if ( !isObjectEmpty(updateData) ) {
if ( !foundry.utils.isObjectEmpty(updateData) ) {
console.log(`Migrating Actor entity ${a.name}`);
await a.update(updateData, {enforceTypes: false});
}
@ -20,10 +20,10 @@ export const migrateWorld = async function() {
}
// Migrate World Items
for ( let i of game.items.entities ) {
for ( let i of game.items.contents ) {
try {
const updateData = migrateItemData(i.data);
if ( !isObjectEmpty(updateData) ) {
const updateData = migrateItemData(i.toObject());
if ( !foundry.utils.isObjectEmpty(updateData) ) {
console.log(`Migrating Item entity ${i.name}`);
await i.update(updateData, {enforceTypes: false});
}
@ -34,12 +34,15 @@ export const migrateWorld = async function() {
}
// Migrate Actor Override Tokens
for ( let s of game.scenes.entities ) {
for ( let s of game.scenes.contents ) {
try {
const updateData = migrateSceneData(s.data);
if ( !isObjectEmpty(updateData) ) {
if ( !foundry.utils.isObjectEmpty(updateData) ) {
console.log(`Migrating Scene entity ${s.name}`);
await s.update(updateData, {enforceTypes: false});
// If we do not do this, then synthetic token actors remain in cache
// with the un-updated actorData.
s.tokens.contents.forEach(t => t._actor = null);
}
} catch(err) {
err.message = `Failed sw5e system migration for Scene ${s.name}: ${err.message}`;
@ -76,40 +79,39 @@ export const migrateCompendium = async function(pack) {
// Begin by requesting server-side data model migration and get the migrated content
await pack.migrate();
const content = await pack.getContent();
const documents = await pack.getDocuments();
// Iterate over compendium entries - applying fine-tuned migration functions
for ( let ent of content ) {
for ( let doc of documents ) {
let updateData = {};
try {
switch (entity) {
case "Actor":
updateData = migrateActorData(ent.data);
updateData = migrateActorData(doc.data);
break;
case "Item":
updateData = migrateItemData(ent.data);
updateData = migrateItemData(doc.toObject());
break;
case "Scene":
updateData = migrateSceneData(ent.data);
updateData = migrateSceneData(doc.data);
break;
}
if ( isObjectEmpty(updateData) ) continue;
// Save the entry, if data was changed
updateData["_id"] = ent._id;
await pack.updateEntity(updateData);
console.log(`Migrated ${entity} entity ${ent.name} in Compendium ${pack.collection}`);
if ( foundry.utils.isObjectEmpty(updateData) ) continue;
await doc.update(updateData);
console.log(`Migrated ${entity} entity ${doc.name} in Compendium ${pack.collection}`);
}
// Handle migration failures
catch(err) {
err.message = `Failed sw5e system migration for entity ${ent.name} in pack ${pack.collection}: ${err.message}`;
err.message = `Failed sw5e system migration for entity ${doc.name} in pack ${pack.collection}: ${err.message}`;
console.error(err);
}
}
// Apply the original locked status for the pack
pack.configure({locked: wasLocked});
await pack.configure({locked: wasLocked});
console.log(`Migrated all ${entity} entities from Compendium ${pack.collection}`);
};
@ -120,38 +122,42 @@ export const migrateCompendium = async function(pack) {
/**
* Migrate a single Actor entity to incorporate latest data model changes
* Return an Object of updateData to be applied
* @param {Actor} actor The actor to Update
* @return {Object} The updateData to apply
* @param {object} actor The actor data object to update
* @return {Object} The updateData to apply
*/
export const migrateActorData = function(actor) {
const updateData = {};
// Actor Data Updates
_migrateActorMovement(actor, updateData);
_migrateActorSenses(actor, updateData);
if (actor.data) {
_migrateActorMovement(actor, updateData);
_migrateActorSenses(actor, updateData);
_migrateActorType(actor, updateData);
}
// Migrate Owned Items
if ( !actor.items ) return updateData;
let hasItemUpdates = false;
const items = actor.items.map(i => {
const items = actor.items.reduce((arr, i) => {
// Migrate the Owned Item
let itemUpdate = migrateItemData(i);
const itemData = i instanceof CONFIG.Item.documentClass ? i.toObject() : i;
let itemUpdate = migrateItemData(itemData);
// Prepared, Equipped, and Proficient for NPC actors
if ( actor.type === "npc" ) {
if (getProperty(i.data, "preparation.prepared") === false) itemUpdate["data.preparation.prepared"] = true;
if (getProperty(i.data, "equipped") === false) itemUpdate["data.equipped"] = true;
if (getProperty(i.data, "proficient") === false) itemUpdate["data.proficient"] = true;
if (getProperty(itemData.data, "preparation.prepared") === false) itemUpdate["data.preparation.prepared"] = true;
if (getProperty(itemData.data, "equipped") === false) itemUpdate["data.equipped"] = true;
if (getProperty(itemData.data, "proficient") === false) itemUpdate["data.proficient"] = true;
}
// Update the Owned Item
if ( !isObjectEmpty(itemUpdate) ) {
hasItemUpdates = true;
return mergeObject(i, itemUpdate, {enforceTypes: false, inplace: false});
} else return i;
});
if ( hasItemUpdates ) updateData.items = items;
itemUpdate._id = itemData._id;
arr.push(expandObject(itemUpdate));
}
return arr;
}, []);
if ( items.length > 0 ) updateData.items = items;
return updateData;
};
@ -187,11 +193,14 @@ function cleanActorData(actorData) {
/**
* Migrate a single Item entity to incorporate latest data model changes
* @param item
*
* @param {object} item Item data to migrate
* @return {object} The updateData to apply
*/
export const migrateItemData = function(item) {
const updateData = {};
_migrateItemAttunement(item, updateData);
_migrateItemPowercasting(item, updateData);
return updateData;
};
@ -204,24 +213,34 @@ export const migrateItemData = function(item) {
* @return {Object} The updateData to apply
*/
export const migrateSceneData = function(scene) {
const tokens = duplicate(scene.tokens);
return {
tokens: tokens.map(t => {
if (!t.actorId || t.actorLink || !t.actorData.data) {
t.actorData = {};
return t;
}
const token = new Token(t);
if ( !token.actor ) {
t.actorId = null;
t.actorData = {};
} else if ( !t.actorLink ) {
const updateData = migrateActorData(token.data.actorData);
t.actorData = mergeObject(token.data.actorData, updateData);
}
return t;
})
};
const tokens = scene.tokens.map(token => {
const t = token.toJSON();
if (!t.actorId || t.actorLink) {
t.actorData = {};
}
else if ( !game.actors.has(t.actorId) ){
t.actorId = null;
t.actorData = {};
}
else if ( !t.actorLink ) {
const actorData = duplicate(t.actorData);
actorData.type = token.actor?.type;
const update = migrateActorData(actorData);
['items', 'effects'].forEach(embeddedName => {
if (!update[embeddedName]?.length) return;
const updates = new Map(update[embeddedName].map(u => [u._id, u]));
t.actorData[embeddedName].forEach(original => {
const update = updates.get(original._id);
if (update) mergeObject(original, update);
});
delete update[embeddedName];
});
mergeObject(t.actorData, update);
}
return t;
});
return {tokens};
};
/* -------------------------------------------- */
@ -232,13 +251,24 @@ export const migrateSceneData = function(scene) {
* Migrate the actor speed string to movement object
* @private
*/
function _migrateActorMovement(actor, updateData) {
const ad = actor.data;
const old = actor.type === 'vehicle' ? ad?.attributes?.speed : ad?.attributes?.speed?.value;
if ( typeof old !== "string" ) 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;
function _migrateActorMovement(actorData, updateData) {
const ad = actorData.data;
// Work is needed if old data is present
const old = actorData.type === 'vehicle' ? ad?.attributes?.speed : ad?.attributes?.speed?.value;
const hasOld = old !== undefined;
if ( hasOld ) {
// If new data is not present, migrate the old data
const hasNew = ad?.attributes?.movement?.walk !== undefined;
if ( !hasNew && (typeof old === "string") ) {
const s = (old || "").split(" ");
if ( s.length > 0 ) updateData["data.attributes.movement.walk"] = Number.isNumeric(s[0]) ? parseInt(s[0]) : null;
}
// Remove the old attribute
updateData["data.attributes.-=speed"] = null;
}
return updateData
}
@ -252,9 +282,10 @@ function _migrateActorSenses(actor, updateData) {
const ad = actor.data;
if ( ad?.traits?.senses === undefined ) return;
const original = ad.traits.senses || "";
if ( typeof original !== "string" ) return;
// 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]+)?/
const pattern = /([A-z]+)\s?([0-9]+)\s?([A-z]+)?/;
let wasMatched = false;
// Match each comma-separated term
@ -281,17 +312,101 @@ function _migrateActorSenses(actor, updateData) {
/* -------------------------------------------- */
/**
* Migrate the actor details.type string to object
* @private
*/
function _migrateActorType(actor, updateData) {
const ad = actor.data;
const original = ad.details?.type;
if ( typeof original !== "string" ) return;
// New default data structure
let data = {
"value": "",
"subtype": "",
"swarm": "",
"custom": ""
}
// Match the existing string
const pattern = /^(?:swarm of (?<size>[\w\-]+) )?(?<type>[^(]+?)(?:\((?<subtype>[^)]+)\))?$/i;
const match = original.trim().match(pattern);
if ( match ) {
// Match a known creature type
const typeLc = match.groups.type.trim().toLowerCase();
const typeMatch = Object.entries(CONFIG.SW5E.creatureTypes).find(([k, v]) => {
return (typeLc === k) ||
(typeLc === game.i18n.localize(v).toLowerCase()) ||
(typeLc === game.i18n.localize(`${v}Pl`).toLowerCase());
});
if (typeMatch) data.value = typeMatch[0];
else {
data.value = "custom";
data.custom = match.groups.type.trim().titleCase();
}
data.subtype = match.groups.subtype?.trim().titleCase() || "";
// Match a swarm
const isNamedSwarm = actor.name.startsWith(game.i18n.localize("SW5E.CreatureSwarm"));
if ( match.groups.size || isNamedSwarm ) {
const sizeLc = match.groups.size ? match.groups.size.trim().toLowerCase() : "tiny";
const sizeMatch = Object.entries(CONFIG.SW5E.actorSizes).find(([k, v]) => {
return (sizeLc === k) || (sizeLc === game.i18n.localize(v).toLowerCase());
});
data.swarm = sizeMatch ? sizeMatch[0] : "tiny";
}
else data.swarm = "";
}
// No match found
else {
data.value = "custom";
data.custom = original;
}
// Update the actor data
updateData["data.details.type"] = data;
return updateData;
}
/* -------------------------------------------- */
/**
* Delete the old data.attuned boolean
*
* @param {object} item Item data to migrate
* @param {object} updateData Existing update to expand upon
* @return {object} The updateData to apply
* @private
*/
function _migrateItemAttunement(item, updateData) {
if ( item.data.attuned === undefined ) return;
if ( item.data?.attuned === undefined ) return updateData;
updateData["data.attunement"] = CONFIG.SW5E.attunementTypes.NONE;
updateData["data.-=attuned"] = null;
return updateData;
}
/* -------------------------------------------- */
/**
* Replace class powercasting string to object.
*
* @param {object} item Item data to migrate
* @param {object} updateData Existing update to expand upon
* @return {object} The updateData to apply
* @private
*/
function _migrateItemPowercasting(item, updateData) {
if ( item.type !== "class" || (foundry.utils.getType(item.data.powercasting) === "Object") ) return updateData;
updateData["data.powercasting"] = {
progression: item.data.powercasting,
ability: ""
};
return updateData;
}
/* -------------------------------------------- */

View file

@ -45,10 +45,12 @@ export default class AbilityTemplate extends MeasuredTemplate {
}
// Return the template constructed from the item data
const template = new this(templateData);
template.item = item;
template.actorSheet = item.actor?.sheet || null;
return template;
const cls = CONFIG.MeasuredTemplate.documentClass;
const template = new cls(templateData, {parent: canvas.scene});
const object = new this(template);
object.item = item;
object.actorSheet = item.actor?.sheet || null;
return object;
}
/* -------------------------------------------- */
@ -88,8 +90,7 @@ export default class AbilityTemplate extends MeasuredTemplate {
if ( now - moveTime <= 20 ) return;
const center = event.data.getLocalPosition(this.layer);
const snapped = canvas.grid.getSnappedPosition(center.x, center.y, 2);
this.data.x = snapped.x;
this.data.y = snapped.y;
this.data.update({x: snapped.x, y: snapped.y});
this.refresh();
moveTime = now;
};
@ -108,14 +109,9 @@ export default class AbilityTemplate extends MeasuredTemplate {
// Confirm the workflow (left-click)
handlers.lc = event => {
handlers.rc(event);
// Confirm final snapped position
const destination = canvas.grid.getSnappedPosition(this.x, this.y, 2);
this.data.x = destination.x;
this.data.y = destination.y;
// Create the template
canvas.scene.createEmbeddedEntity("MeasuredTemplate", this.data);
const destination = canvas.grid.getSnappedPosition(this.data.x, this.data.y, 2);
this.data.update(destination);
canvas.scene.createEmbeddedDocuments("MeasuredTemplate", [this.data]);
};
// Rotate the template by 3 degree increments (mouse-wheel)
@ -124,7 +120,7 @@ export default class AbilityTemplate extends MeasuredTemplate {
event.stopPropagation();
let delta = canvas.grid.type > CONST.GRID_TYPES.SQUARE ? 30 : 15;
let snap = event.shiftKey ? delta : 5;
this.data.direction += (snap * Math.sign(event.deltaY));
this.data.update({direction: this.data.direction + (snap * Math.sign(event.deltaY))});
this.refresh();
};

99
module/token.js Normal file
View file

@ -0,0 +1,99 @@
/**
* Extend the base TokenDocument class to implement system-specific HP bar logic.
* @extends {TokenDocument}
*/
export class TokenDocument5e extends TokenDocument {
/** @inheritdoc */
getBarAttribute(...args) {
const data = super.getBarAttribute(...args);
if ( data && (data.attribute === "attributes.hp") ) {
data.value += parseInt(getProperty(this.actor.data, "data.attributes.hp.temp") || 0);
data.max += parseInt(getProperty(this.actor.data, "data.attributes.hp.tempmax") || 0);
}
return data;
}
}
/* -------------------------------------------- */
/**
* Extend the base Token class to implement additional system-specific logic.
* @extends {Token}
*/
export class Token5e extends Token {
/** @inheritdoc */
_drawBar(number, bar, data) {
if ( data.attribute === "attributes.hp" ) return this._drawHPBar(number, bar, data);
return super._drawBar(number, bar, data);
}
/* -------------------------------------------- */
/**
* Specialized drawing function for HP bars.
* @param {number} number The Bar number
* @param {PIXI.Graphics} bar The Bar container
* @param {object} data Resource data for this bar
* @private
*/
_drawHPBar(number, bar, data) {
// Extract health data
let {value, max, temp, tempmax} = this.document.actor.data.data.attributes.hp;
temp = Number(temp || 0);
tempmax = Number(tempmax || 0);
// Differentiate between effective maximum and displayed maximum
const effectiveMax = Math.max(0, max + tempmax);
let displayMax = max + (tempmax > 0 ? tempmax : 0);
// Allocate percentages of the total
const tempPct = Math.clamped(temp, 0, displayMax) / displayMax;
const valuePct = Math.clamped(value, 0, effectiveMax) / displayMax;
const colorPct = Math.clamped(value, 0, effectiveMax) / displayMax;
// Determine colors to use
const blk = 0x000000;
const hpColor = PIXI.utils.rgb2hex([(1-(colorPct/2)), colorPct, 0]);
const c = CONFIG.SW5E.tokenHPColors;
// Determine the container size (logic borrowed from core)
const w = this.w;
let h = Math.max((canvas.dimensions.size / 12), 8);
if ( this.data.height >= 2 ) h *= 1.6;
const bs = Math.clamped(h / 8, 1, 2);
const bs1 = bs+1;
// Overall bar container
bar.clear()
bar.beginFill(blk, 0.5).lineStyle(bs, blk, 1.0).drawRoundedRect(0, 0, w, h, 3);
// Temporary maximum HP
if (tempmax > 0) {
const pct = max / effectiveMax;
bar.beginFill(c.tempmax, 1.0).lineStyle(1, blk, 1.0).drawRoundedRect(pct*w, 0, (1-pct)*w, h, 2);
}
// Maximum HP penalty
else if (tempmax < 0) {
const pct = (max + tempmax) / max;
bar.beginFill(c.negmax, 1.0).lineStyle(1, blk, 1.0).drawRoundedRect(pct*w, 0, (1-pct)*w, h, 2);
}
// Health bar
bar.beginFill(hpColor, 1.0).lineStyle(bs, blk, 1.0).drawRoundedRect(0, 0, valuePct*w, h, 2)
// Temporary hit points
if ( temp > 0 ) {
bar.beginFill(c.temp, 1.0).lineStyle(0).drawRoundedRect(bs1, bs1, (tempPct*w)-(2*bs1), h-(2*bs1), 1);
}
// Set position
let posY = (number === 0) ? (this.h - h) : 0;
bar.position.set(0, posY);
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -26,7 +26,7 @@
{"_id":"7NML6SkyvOsZ17iq","name":"Aggressive","permission":{"default":0},"type":"feat","data":{"description":{"value":"<p>As a bonus action, the {creature} can move up to its speed toward a hostile creature that it can see.</p>","chat":"","unidentified":""},"source":"","activation":{"type":"bonus","cost":1,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"units":"","type":""},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"requirements":"","recharge":{"value":null,"charged":false}},"flags":{},"img":"systems/sw5e/icons/skills/red_22.jpg","effects":[]}
{"_id":"7WfeHV27l7DMcuTG","name":"Flyby","permission":{"default":0},"type":"feat","data":{"description":{"value":"<p>The {creature} doesn't provoke opportunity attacks when it flies out of an enemy's reach.</p>","chat":"","unidentified":""},"source":"","activation":{"type":"","cost":0,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"units":"","type":""},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":0,"per":"","type":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":"","value":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power","value":""},"requirements":"Owl","recharge":{"value":null,"charged":false},"time":{"value":"","_deprecated":true},"damageType":{"value":"","_deprecated":true}},"flags":{},"img":"systems/sw5e/icons/skills/shadow_15.jpg","effects":[]}
{"_id":"7nbokvWfSckl2iWz","name":"Blind Senses","permission":{"default":0},"type":"feat","data":{"description":{"value":"<p>The {creature} can't use its blindsight while deafened and unable to smell.</p>","chat":"","unidentified":""},"source":"","activation":{"type":"","cost":null,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"units":"","type":""},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":0,"per":null},"consume":{"type":"","target":null,"amount":null},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"requirements":"","recharge":{"value":null,"charged":false}},"flags":{},"img":"systems/sw5e/icons/skills/shadow_12.jpg","effects":[]}
{"_id":"8C6hkMXWLeymmJ5C","name":"Blinding Gaze","permission":{"default":0},"type":"feat","data":{"description":{"value":"<section class=\"secret\">The {creature} targets one creature it can see within 30 feet of it. If the target can see it, the target must succeed on a <strong>DC 15 Constitution</strong> saving throw or be blinded until magic such as the lesser restoration power removes the blindness.\n<p>The {creature} targets one creature it can see within 30 feet of it. If the target can see it, the target must make a <strong>Constitution</strong> saving throw.</p>\n</section>\n<p>The {creature} targets one creature it can see within 30 feet of it. If the target can see it, the target must succeed on a Constitution saving throw or be blinded.</p>","chat":"","unidentified":""},"source":"","activation":{"type":"legendary","cost":3,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"units":"","type":""},"range":{"value":30,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"attribute","target":"","amount":3},"ability":"","actionType":"other","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"con","dc":15,"scaling":"flat"},"requirements":"","recharge":{"value":null,"charged":false}},"flags":{},"img":"systems/sw5e/icons/skills/light_01.jpg","effects":[]}
{"_id":"8C6hkMXWLeymmJ5C","name":"Blinding Gaze","permission":{"default":0},"type":"feat","data":{"description":{"value":"<section class=\"secret\">The {creature} targets one creature it can see within 30 feet of it. If the target can see it, the target must succeed on a <strong>DC 15 Constitution</strong> saving throw or be blinded until magic such as the lesser restoration power removes the blindness.\n</section>\n<p>The {creature} targets one creature it can see within 30 feet of it. If the target can see it, the target must succeed on a Constitution saving throw or be blinded.</p>","chat":"","unidentified":""},"source":"","activation":{"type":"legendary","cost":3,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"units":"","type":""},"range":{"value":30,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"attribute","target":"","amount":3},"ability":"","actionType":"other","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"con","dc":15,"scaling":"flat"},"requirements":"","recharge":{"value":null,"charged":false}},"flags":{},"img":"systems/sw5e/icons/skills/light_01.jpg","effects":[]}
{"_id":"8FX2KlWyBAKEYGzs","name":"Relentless","permission":{"default":0},"type":"feat","data":{"description":{"value":"<section class=\"secret\">\n<p>If the {creature} takes <strong>7 damage</strong> or less that would reduce it to <strong>0 hit points</strong>, it is reduced to <strong>1 hit point</strong> instead. Recharges on a short or long rest.</p>\n</section>\n<p>If the {creature} takes damage that would reduce it to <strong>0 hit points</strong>, it is reduced to <strong>1 hit point</strong> instead.</p>","chat":"","unidentified":""},"source":"","activation":{"type":"special","cost":null,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"units":"","type":""},"range":{"value":null,"long":null,"units":""},"uses":{"value":1,"max":1,"per":"sr"},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"requirements":"","recharge":{"value":null,"charged":false}},"flags":{},"img":"systems/sw5e/icons/skills/affliction_01.jpg","effects":[]}
{"_id":"8H0t1US0zvNPWtQ9","name":"Adhesive","permission":{"default":0},"type":"feat","data":{"description":{"value":"<section class=\"secret\"><p>The {creature} adheres to anything that touches it. A Huge or smaller creature adhered to the {creature} is also grappled by it <strong>escape DC 13</strong>. Ability checks made to escape this grapple have disadvantage.</p></section><p>The {creature} adheres to anything that touches it. A Huge or smaller creature adhered to the {creature} is also grappled by it. Make an escape check, with disadvantage.</p>","chat":"","unidentified":""},"source":"","activation":{"type":"","cost":null,"condition":""},"duration":{"value":null,"units":""},"target":{"value":null,"units":"","type":""},"range":{"value":null,"long":null,"units":""},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"","target":"","amount":null},"ability":"","actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"flat"},"requirements":"","recharge":{"value":null,"charged":false}},"flags":{},"img":"systems/sw5e/icons/skills/green_08.jpg","effects":[]}
{"_id":"8l0R2MIYxWxYJT6O","name":"Shimmering Shield","permission":{"default":0},"type":"feat","data":{"description":{"value":"<p>The {creature} creates a shimmering, magical field around itself or another creature it can see within 60 feet of it. The target gains a +2 bonus to AC until the end of the {creature}'s next turn.</p>","chat":"","unidentified":""},"source":"","activation":{"type":"legendary","cost":2,"condition":""},"duration":{"value":1,"units":"turn"},"target":{"value":1,"units":"","type":"creature"},"range":{"value":60,"long":null,"units":"ft"},"uses":{"value":0,"max":0,"per":""},"consume":{"type":"attribute","target":"","amount":2},"ability":null,"actionType":"","attackBonus":0,"chatFlavor":"","critical":null,"damage":{"parts":[],"versatile":""},"formula":"","save":{"ability":"","dc":null,"scaling":"power"},"requirements":"","recharge":{"value":null,"charged":false}},"flags":{},"img":"systems/sw5e/icons/powers/light-sky-3.jpg","effects":[]}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -2,7 +2,7 @@
"name": "sw5e",
"title": "SW5e - Fifth Edition System",
"description": "A system for playing the fifth edition of the worlds most popular role-playing game in the Foundry Virtual Tabletop environment.",
"version": "1.2.3",
"version": "1.3.5",
"author": "Atropos",
"scripts": [],
"esmodules": ["sw5e.js"],
@ -91,9 +91,9 @@
"gridUnits": "ft",
"primaryTokenAttribute": "attributes.hp",
"secondaryTokenAttribute": null,
"minimumCoreVersion": "0.7.6",
"compatibleCoreVersion": "0.7.9",
"minimumCoreVersion": "0.8.2",
"compatibleCoreVersion": "0.8.7",
"url": "https://gitlab.com/foundrynet/sw5e",
"manifest": "https://gitlab.com/foundrynet/sw5e/raw/master/system.json",
"download": "https://gitlab.com/foundrynet/sw5e/-/archive/release-1.2.3/sw5e-release-1.2.3.zip"
"download": "https://gitlab.com/foundrynet/sw5e/-/archive/release-1.3.5/sw5e-release-1.3.5.zip"
}

View file

@ -261,15 +261,12 @@
"success": 0,
"failure": 0
},
"encumbrance": {
"value": null,
"max": null
},
"exhaustion": 0,
"inspiration": 0
},
"details": {
"background": "",
"originalClass": "",
"xp": {
"value": 0,
"min": 0,
@ -319,7 +316,12 @@
"npc": {
"templates": ["common", "creature"],
"details": {
"type": "",
"type": {
"value": "",
"subtype": "",
"swarm": "",
"custom": ""
},
"environment": "",
"cr": 1,
"powerLevel": 0,
@ -502,12 +504,16 @@
"archetype": "",
"hitDice": "d6",
"hitDiceUsed": 0,
"saves": [],
"skills": {
"number": 2,
"choices": [],
"value": []
},
"powercasting": "none"
"powercasting": {
"progression": "none",
"ability": ""
}
},
"consumable": {
"templates": ["itemDescription", "physicalItem", "activatedEffect", "action"],

View file

@ -60,8 +60,11 @@
</footer>
</li>
<li class="attribute">
<h4 class="attribute-name box-title">{{ localize "SW5E.HitDice" }}</h4>
<li class="attribute hit-dice">
<h4 class="attribute-name box-title">
{{ localize "SW5E.HitDice" }}
<a class="config-button" data-action="hit-dice" title="{{localize 'SW5E.HitDiceConfig'}}"><i class="fas fa-cog"></i></a>
</h4>
<div class="attribute-value multiple">
<label class="hit-dice">{{data.attributes.hd}} <span class="sep"> / </span> {{data.details.level}}</label>
</div>
@ -131,10 +134,10 @@
<h4 class="ability-name box-title rollable">{{ability.label}}</h4>
<input class="ability-score" name="data.abilities.{{id}}.value" type="number" value="{{ability.value}}" placeholder="10"/>
<div class="ability-modifiers flexrow">
<span class="ability-mod" title="Modifier">{{numberFormat ability.mod decimals=0 sign=true}}</span>
<span class="ability-mod" title="{{ localize 'SW5E.Modifier' }}">{{numberFormat ability.mod decimals=0 sign=true}}</span>
<input type="hidden" name="data.abilities.{{id}}.proficient" value="{{ability.proficient}}" data-dtype="Number"/>
<a class="proficiency-toggle ability-proficiency" title="{{ localize 'SW5E.Proficiency' }}">{{{ability.icon}}}</a>
<span class="ability-save" title="Saving Throw">{{numberFormat ability.save decimals=0 sign=true}}</span>
<span class="ability-save" title="{{ localize 'SW5E.SavingThrow' }}">{{numberFormat ability.save decimals=0 sign=true}}</span>
</div>
</li>
{{/each}}
@ -142,15 +145,17 @@
{{!-- Skills --}}
<ul class="skills-list">
{{#each data.skills as |skill s|}}
{{#each config.skills as |label s|}}
{{#with (lookup ../data.skills s) as |skill|}}
<li class="skill flexrow {{#if skill.value}}proficient{{/if}}" data-skill="{{s}}">
<input type="hidden" name="data.skills.{{s}}.value" value="{{skill.value}}" data-dtype="Number"/>
<a class="proficiency-toggle skill-proficiency" title="{{skill.hover}}">{{{skill.icon}}}</a>
<h4 class="skill-name rollable">{{skill.label}}</h4>
<h4 class="skill-name rollable">{{label}}</h4>
<span class="skill-ability">{{skill.ability}}</span>
<span class="skill-mod">{{numberFormat skill.total decimals=0 sign=true}}</span>
<span class="skill-passive">({{skill.passive}})</span>
</li>
{{/with}}
{{/each}}
</ul>
@ -165,16 +170,16 @@
placeholder="{{res.placeholder}}" />
</h4>
<div class="attribute-value">
<label class="recharge checkbox">
{{ localize "SW5E.AbbreviationSR" }} <input name="data.resources.{{res.name}}.sr" type="checkbox" {{checked res.sr}}/>
<label class="recharge checkbox flexcol">
<span>{{ localize "SW5E.AbbreviationSR" }}</span><input name="data.resources.{{res.name}}.sr" type="checkbox" {{checked res.sr}}/>
</label>
<input name="data.resources.{{res.name}}.value" type="number" value="{{res.value}}" placeholder="0"/>
<span class="sep"> / </span>
<input name="data.resources.{{res.name}}.max" type="number" value="{{res.max}}" placeholder="0"/>
<label class="recharge checkbox">
{{ localize "SW5E.AbbreviationLR" }} <input name="data.resources.{{res.name}}.lr" type="checkbox" {{checked res.lr}}/>
<label class="recharge checkbox flexcol">
<span>{{ localize "SW5E.AbbreviationLR" }}</span><input name="data.resources.{{res.name}}.lr" type="checkbox" {{checked res.lr}}/>
</label>
</div>
</li>
@ -250,7 +255,7 @@
<textarea name="data.details.flaw">{{data.details.flaw}}</textarea>
</div>
<div class="biography">
{{editor content=data.details.biography.value target="data.details.biography.value" button=true owner=owner editable=editable}}
{{editor content=data.details.biography.value target="data.details.biography.value" button=true owner=owner editable=editable rollData=rollData}}
</div>
</div>
</section>

View file

@ -14,7 +14,7 @@
{{!-- Sheet Body --}}
<section class="sheet-body">
<div class="tab biography">
{{editor content=data.details.biography.value target="data.details.biography.value" button=true owner=owner editable=editable}}
{{editor content=data.details.biography.value target="data.details.biography.value" button=true owner=owner editable=editable rollData=rollData}}
</div>
</section>
</form>

View file

@ -27,8 +27,9 @@
<li>
<input type="text" name="data.details.alignment" value="{{data.details.alignment}}" placeholder="{{ localize 'SW5E.Alignment' }}"/>
</li>
<li>
<input type="text" name="data.details.type" value="{{data.details.type}}" placeholder="{{ localize 'SW5E.Type' }}"/>
<li class="creature-type">
<span title="{{labels.type}}">{{labels.type}}</span>
<a class="config-button" data-action="type" title="{{localize 'SW5E.CreatureTypeConfig'}}"><i class="fas fa-cog"></i></a>
</li>
<li>
<input type="text" name="data.details.source" value="{{data.details.source}}" placeholder="{{ localize 'SW5E.Source' }}"/>
@ -97,10 +98,10 @@
<h4 class="ability-name box-title rollable">{{ability.label}}</h4>
<input class="ability-score" name="data.abilities.{{id}}.value" type="number" value="{{ability.value}}" placeholder="10"/>
<div class="ability-modifiers flexrow">
<span class="ability-mod" title="Modifier">{{numberFormat ability.mod decimals=0 sign=true}}</span>
<span class="ability-mod" title="{{ localize 'SW5E.Modifier' }}">{{numberFormat ability.mod decimals=0 sign=true}}</span>
<input type="hidden" name="data.abilities.{{id}}.proficient" value="{{ability.proficient}}" data-dtype="Number"/>
<a class="proficiency-toggle ability-proficiency" title="{{ localize 'SW5E.Proficiency' }}">{{{ability.icon}}}</a>
<span class="ability-save" title="Saving Throw">{{numberFormat ability.save decimals=0 sign=true}}</span>
<span class="ability-save" title="{{ localize 'SW5E.SavingThrow' }}">{{numberFormat ability.save decimals=0 sign=true}}</span>
</div>
</li>
{{/each}}
@ -108,15 +109,17 @@
{{!-- Skills --}}
<ul class="skills-list">
{{#each data.skills as |skill s|}}
{{#each config.skills as |label s|}}
{{#with (lookup ../data.skills s) as |skill|}}
<li class="skill flexrow {{#if skill.value}}proficient{{/if}}" data-skill="{{s}}">
<input type="hidden" name="data.skills.{{s}}.value" value="{{skill.value}}" data-dtype="Number"/>
<a class="proficiency-toggle skill-proficiency" title="{{skill.hover}}">{{{skill.icon}}}</a>
<h4 class="skill-name rollable">{{skill.label}}</h4>
<h4 class="skill-name rollable">{{label}}</h4>
<span class="skill-ability">{{skill.ability}}</span>
<span class="skill-mod">{{numberFormat skill.total decimals=0 sign=true}}</span>
<span class="skill-passive">({{skill.passive}})</span>
</li>
{{/with}}
{{/each}}
</ul>
@ -172,7 +175,7 @@
{{!-- Biography Tab --}}
<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}}
{{editor content=data.details.biography.value target="data.details.biography.value" button=true owner=owner editable=editable rollData=rollData}}
</div>
</section>
</form>

View file

@ -2,8 +2,8 @@
{{#each effects as |section sid|}}
<li class="items-header flexrow" data-effect-type="{{section.type}}">
<h3 class="item-name effect-name flexrow">{{localize section.label}}</h3>
<div class="effect-source">Source</div>
<div class="effect-source">Duration</div>
<div class="effect-source">{{localize "SW5E.Source"}}</div>
<div class="effect-source">{{localize "SW5E.Duration"}}</div>
<div class="item-controls effect-controls flexrow">
<a class="effect-control" data-action="create" title="{{localize 'SW5E.EffectCreate'}}">
<i class="fas fa-plus"></i> {{localize "SW5E.Add"}}

View file

@ -38,7 +38,10 @@
<li class="item flexrow {{#if isDepleted}}depleted{{/if}}" data-item-id="{{item._id}}">
<div class="item-name flexrow rollable">
<div class="item-image" style="background-image: url('{{item.img}}')"></div>
<h4>{{item.name}}</h4>
<h4>
{{item.name}}
{{#if item.isOriginalClass}} <i class="original-class fas fa-sun" title="{{localize 'SW5E.ClassOriginal'}}"></i>{{/if}}
</h4>
</div>
{{#if section.hasActions}}
@ -52,7 +55,6 @@
<input type="text" value="{{item.data.uses.value}}" placeholder="0"/>/ {{item.data.uses.max}}
{{/if}}
</div>
<div class="item-detail item-action">
{{#if item.data.activation.type }}
{{item.labels.activation}}
@ -64,14 +66,14 @@
{{item.data.archetype}}
</div>
<div class="item-detail item-action">
Level {{item.data.levels}}
{{localize "SW5E.Level"}} {{item.data.levels}}
</div>
{{/if}}
{{#if section.columns}}
{{#each section.columns}}
<div class="item-detail {{css}}">
{{#with (getProperty item property)}}
{{#with (lookup item property)}}
{{#if ../editable}}
<input type="text" value="{{this}}" placeholder="&mdash;"
data-dtype="{{../editable}}">
@ -91,8 +93,8 @@
<i class="fas fa-sun"></i>
</a>
{{/if}}
<a class="item-control item-edit" title="Edit Item"><i class="fas fa-edit"></i></a>
<a class="item-control item-delete" title="Delete Item"><i class="fas fa-trash"></i></a>
<a class="item-control item-edit" title="{{localize 'SW5E.ItemEdit'}}"><i class="fas fa-edit"></i></a>
<a class="item-control item-delete" title="{{localize 'SW5E.ItemDelete'}}"><i class="fas fa-trash"></i></a>
</div>
{{/if}}
</li>

View file

@ -4,7 +4,7 @@
<ol class="currency flexrow">
<h3>
{{localize "SW5E.Currency"}}
<a class="currency-convert rollable" data-action="convertCurrency" title="Convert Currency">
<a class="currency-convert rollable" data-action="convertCurrency" title="{{localize 'SW5E.CurrencyConvert'}}">
<i class="fas fa-coins"></i>
</a>
</h3>
@ -89,13 +89,13 @@
{{/each}}
{{else}}
{{#if ../../isCharacter}}
<div class="item-detail item-weight">
{{#if item.totalWeight}}
<div class="item-detail">
{{ item.totalWeight }} {{localize "SW5E.AbbreviationLbs"}}
</div>
{{/if}}
</div>
<div class="item-detail item-weight">
{{#if item.totalWeight}}
<div class="item-detail">
{{ item.totalWeight }} {{localize "SW5E.AbbreviationLbs"}}
</div>
{{/if}}
</div>
{{/if}}
<div class="item-detail item-uses">

View file

@ -1,7 +1,7 @@
<div class="inventory-filters powerbook-filters flexrow">
<div class="form-group powercasting-ability">
{{#unless isNPC}}
<label>{{localize "SW5E.PowerAbility"}}</label>
<label>{{localize "SW5E.Powercasting"}}</label>
{{else}}
<label>{{localize "SW5E.Level"}}</label>
<input class="powercasting-level" type="text" name="data.details.powerLevel"
@ -86,7 +86,7 @@
<div class="power-target" title="{{localize 'SW5E.Range'}}: {{labels.range}}">
{{#if labels.target}}
{{labels.target}}
{{else}}None
{{else}}{{localize 'SW5E.None'}}
{{/if}}
</div>
@ -95,8 +95,8 @@
{{#if section.canPrepare}}
<a class="item-control item-toggle {{item.toggleClass}}" title="{{item.toggleTitle}}"><i class="fas fa-sun"></i></a>
{{/if}}
<a class="item-control item-edit" title="Edit Item"><i class="fas fa-edit"></i></a>
<a class="item-control item-delete" title="Delete Item"><i class="fas fa-trash"></i></a>
<a class="item-control item-edit" title="{{localize 'SW5E.ItemEdit'}}"><i class="fas fa-edit"></i></a>
<a class="item-control item-delete" title="{{localize 'SW5E.ItemDelete'}}"><i class="fas fa-trash"></i></a>
</div>
{{/if}}
</li>

View file

@ -158,7 +158,7 @@
<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}}
button=true owner=owner editable=editable rollData=rollData}}
</div>
</section>
</form>

View file

@ -2,6 +2,15 @@
<section class="form-body">
<p class="notes">{{localize 'SW5E.FlagsInstructions'}}</p>
<h3 class="form-header">{{localize "SW5E.ItemTypeClass"}}</h3>
<div class="form-group">
<label>{{localize "SW5E.ClassMakeOriginal"}}</label>
<select name="data.details.originalClass" data-dtype="String">
{{selectOptions classes selected=actor.data.data.details.originalClass}}
</select>
<p class="notes">{{localize "SW5E.ClassMakeOriginalHint"}}</p>
</div>
{{#each flags as |fs section|}}
<h3 class="form-header">{{localize section}}</h3>
{{#each fs as |flag key|}}

View file

@ -0,0 +1,38 @@
<form autocomplete="off">
<input type="text" name="preview" value="{{preview}}" disabled>
<div class="form-group stacked">
<label>{{ localize "SW5E.CreatureType" }}</label>
<ul class="trait-list">
{{#each types as |type key|}}
<li>
<label class="radio">
<input type="radio" name="value" value="{{key}}" data-dtype="String" {{checked type.chosen}}/>
{{type.label}}
</label>
</li>
{{/each}}
<li class="custom-type form-group">
<input type="radio" name="value" value="custom" title="{{custom.label}}" {{checked custom.chosen}}/>
<label>{{custom.label}}</label>
<input type="text" name="custom" value="{{custom.value}}"/>
</li>
<li class="subtype form-group">
<label>{{ localize "SW5E.CreatureTypeSelectorSubtype" }}</label>
<input type="text" name="subtype" value="{{subtype}}"/>
</li>
</ul>
</div>
<div class="swarm-size form-group">
<label>{{ localize "SW5E.CreatureSwarmSize" }}</label>
<select name="swarm">
{{selectOptions sizes selected=swarm blank="" localize=true}}
</select>
</div>
<button type="submit" name="submit" value="1">
<i class="far fa-save"></i> {{ localize "SW5E.TraitSave"}}
</button>
</form>

View file

@ -0,0 +1,23 @@
<form autocomplete="off">
<p class="notes">{{ localize "SW5E.HitDiceConfigHint" }}</p>
{{#each classes}}
<div class="form-group">
<label>{{this.name}} ({{this.diceDenom}})</label>
<div class="form-fields">
<button class="decrement" type="button">-</button>
<input title="{{ localize 'SW5E.HitDiceRemaining' }}" class="current" name="{{this.classItemId}}" type="number" value="{{this.currentHitDice}}" />
<span class="sep">/</span>
<input title="{{ localize 'SW5E.HitDiceMax' }}" class="max" type="number" value="{{this.maxHitDice}}" disabled />
<button class="increment" type="button">+</button>
<button class="roll-hd" data-hd-denom="{{this.diceDenom}}" {{#unless this.canRoll}}disabled{{/unless}}>
<i class="fas fa-dice-d20"></i> {{ localize "SW5E.Roll" }}
</button>
</div>
</div>
{{/each}}
<div class="dialog-buttons">
<button type="reset" name="reset"><i class="fas fa-redo"></i> {{ localize "SETTINGS.5eUndoChanges" }}</button>
<button type="submit" name="submit" value="1"><i class="far fa-save"></i> {{ localize "SETTINGS.Save"}}</button>
</div>
</form>

View file

@ -1,11 +1,11 @@
<form id="long-rest" class="dialog-content" onsubmit="event.preventDefault();">
<p>Take a long rest? On a long rest you will recover hit points, half your maximum hit dice, class resources, limited use item charges, and power slots.</p>
<p>{{ localize "SW5E.LongRestHint" }}</p>
{{#if promptNewDay}}
<div class="form-group">
<label>Is New Day?</label>
<label>{{ localize "SW5E.NewDay" }}</label>
<input type="checkbox" name="newDay" {{checked newDay}}/>
<p class="hint">Recover limited use abilities which recharge "per day"?</p>
<p class="hint">{{ localize "SW5E.NewDayHint" }}</p>
</div>
{{/if}}

View file

@ -0,0 +1,19 @@
<form>
<p class="hint">{{hint}}</p>
<ul class="items-list">
{{#each items}}
<li class="item flexrow">
<div class="item-name flexrow">
<div class="item-image" style="background-image:url({{data.img}})" data-item-id="{{id}}"></div>
<label class="flexrow">
<h4>{{data.name}}</h4>
<input type="checkbox" checked name="{{id}}" />
</label>
</div>
</li>
{{/each}}
</ul>
</form>

View file

@ -11,7 +11,7 @@
{{/select}}
</select>
<button id="roll-hd" {{#unless canRoll}}disabled{{/unless}}>
<i class="fas fa-dice-d20"></i> {{ localize "Roll" }}
<i class="fas fa-dice-d20"></i> {{ localize "SW5E.Roll" }}
</button>
</div>
{{#unless canRoll}}
@ -21,9 +21,9 @@
{{#if promptNewDay}}
<div class="form-group">
<label>Is New Day?</label>
<label>{{ localize "SW5E.NewDay" }}</label>
<input type="checkbox" name="newDay" {{checked newDay}}/>
<p class="hint">Recover limited use abilities which recharge "per day"?</p>
<p class="hint">{{ localize "SW5E.NewDayHint" }}</p>
</div>
{{/if}}

View file

@ -39,6 +39,10 @@
{{#if hasAreaTarget}}
<button data-action="placeTemplate">{{ localize "SW5E.PlaceTemplate" }}</button>
{{/if}}
{{#if isTool}}
<button data-action="toolCheck" data-ability="{{data.ability.value}}">{{ localize "SW5E.Use" }} {{item.name}}</button>
{{/if}}
</div>
<footer class="card-footer">

View file

@ -3,6 +3,14 @@
<label>{{ localize "SW5E.Formula" }}</label>
<input type="text" name="formula" value="{{formula}}" disabled/>
</div>
{{#if chooseModifier}}
<div class="form-group">
<label>{{ localize "SW5E.AbilityModifier" }}</label>
<select name="ability">
{{selectOptions abilities selected=defaultAbility}}
</select>
</div>
{{/if}}
<div class="form-group">
<label>{{ localize "SW5E.RollSituationalBonus" }}</label>
<input type="text" name="bonus" value="" placeholder="{{ localize 'SW5E.RollExample' }}"/>
@ -10,11 +18,7 @@
<div class="form-group">
<label>{{ localize "SW5E.RollMode" }}</label>
<select name="rollMode">
{{#select rollMode}}
{{#each rollModes as |label mode|}}
<option value="{{mode}}">{{localize label}}</option>
{{/each}}
{{/select}}
{{selectOptions rollModes selected=defaultRollMode localize=true}}
</select>
</div>
</form>
</form>

View file

@ -1,18 +0,0 @@
<div class="sw5e chat-card item-card" data-actor-id="{{actor._id}}" data-item-id="{{item._id}}" {{#if tokenId}}data-token-id="{{tokenId}}"{{/if}}>
<header class="card-header flexrow">
<img src="{{item.img}}" title="{{item.name}}" width="36" height="36"/>
<h3 class="item-name">{{item.name}}</h3>
</header>
<div class="card-content">{{{data.description.value}}}</div>
<div class="card-buttons">
<button data-action="toolCheck" data-ability="{{data.ability.value}}">{{ localize "SW5E.Use" }} {{item.name}}</button>
</div>
<footer class="card-footer">
{{#each data.properties}}
<span>{{this}}</span>
{{/each}}
</footer>
</div>

View file

@ -1,33 +0,0 @@
<form>
<div class="form-group">
<label>{{ localize "SW5E.Formula" }}</label>
<input type="text" name="formula" value="{{formula}}" disabled/>
</div>
<div class="form-group">
<label>{{ localize "SW5E.AbilityModifier" }}</label>
<select name="ability">
{{#select data.item.ability}}
{{#each config.abilities as |ability a|}}
<option value="{{a}}">{{ability}}</option>
{{/each}}
{{/select}}
</select>
</div>
<div class="form-group">
<label>{{ localize "SW5E.RollSituationalBonus" }}</label>
<input type="text" name="bonus" value="" placeholder="{{ localize 'SW5E.RollExample' }}"/>
</div>
<div class="form-group">
<label>{{ localize "SW5E.RollMode" }}</label>
<select name="rollMode">
{{#select rollMode}}
{{#each rollModes as |label mode|}}
<option value="{{mode}}">{{localize label}}</option>
{{/each}}
{{/select}}
</select>
</div>
</form>

View file

@ -76,19 +76,44 @@
</div>
</div>
{{!-- Powercasting --}}
<div class="form-group">
<label>{{localize 'SW5E.PowerProgression'}}</label>
<div class="form-fields">
<select name="data.powercasting">
{{#select data.powercasting}}
{{#each config.powerProgression}}
<option value="{{@key}}">{{localize this}}</option>
{{/each}}
{{/select}}
<select name="data.powercasting.progression">
{{selectOptions config.powerProgression selected=data.powercasting.progression localize=true}}
</select>
</div>
</div>
<div class="form-group">
<label>{{localize "SW5E.PowerAbility"}}</label>
<div class="form-fields">
<select name="data.powercasting.ability">
{{selectOptions config.abilities selected=data.powercasting.ability blank=""}}
</select>
</div>
</div>
{{!-- Proficiencies --}}
<div class="form-group">
<label>
{{ localize "SW5E.ClassSaves" }}
{{#if editable}}
<a class="trait-selector class-saves" data-target="data.saves" data-options="saves">
<i class="fas fa-edit"></i>
</a>
{{/if}}
</label>
<div class="form-fields">
<ul class="traits-list">
{{#each data.saves}}
<li class="tag {{this}}">{{lookup ../config.abilities this}}</li>
{{/each}}
</ul>
</div>
</div>
{{!-- Level 1 skills --}}
<div class="form-group">
<label>{{localize "SW5E.ClassSkillsNumber"}}</label>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 348 KiB

After

Width:  |  Height:  |  Size: 368 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 172 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 115 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

After

Width:  |  Height:  |  Size: 138 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 483 KiB

After

Width:  |  Height:  |  Size: 475 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 483 KiB

After

Width:  |  Height:  |  Size: 487 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 284 KiB

After

Width:  |  Height:  |  Size: 302 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 513 KiB

After

Width:  |  Height:  |  Size: 501 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 510 KiB

After

Width:  |  Height:  |  Size: 538 KiB

Before After
Before After

BIN
tokens/beast/GiantEagle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 395 KiB

After

Width:  |  Height:  |  Size: 384 KiB

Before After
Before After

BIN
tokens/beast/GiantHyena.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 499 KiB

After

Width:  |  Height:  |  Size: 502 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 971 KiB

After

Width:  |  Height:  |  Size: 988 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 138 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 284 KiB

After

Width:  |  Height:  |  Size: 290 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 368 KiB

After

Width:  |  Height:  |  Size: 469 KiB

Before After
Before After

BIN
tokens/beast/Lion.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 KiB

BIN
tokens/beast/Lioness.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 622 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 284 KiB

After

Width:  |  Height:  |  Size: 298 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 322 KiB

After

Width:  |  Height:  |  Size: 380 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 KiB

After

Width:  |  Height:  |  Size: 467 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 409 KiB

After

Width:  |  Height:  |  Size: 419 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 366 KiB

After

Width:  |  Height:  |  Size: 341 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

After

Width:  |  Height:  |  Size: 162 KiB

Before After
Before After

BIN
tokens/beast/Tiger.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 KiB

After

Width:  |  Height:  |  Size: 383 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 546 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 580 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 KiB

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