forked from GitHub-Mirrors/foundry-sw5e
Add files via upload
This commit is contained in:
parent
8d1045325f
commit
0106a61b43
8 changed files with 1501 additions and 0 deletions
46
module/canvas.js
Normal file
46
module/canvas.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
/**
|
||||
* Measure the distance between two pixel coordinates
|
||||
* See BaseGrid.measureDistance for more details
|
||||
*
|
||||
* @param {Object} p0 The origin coordinate {x, y}
|
||||
* @param {Object} p1 The destination coordinate {x, y}
|
||||
* @param {boolean} gridSpaces Enforce grid distance (if true) vs. direct point-to-point (if false)
|
||||
* @return {number} The distance between p1 and p0
|
||||
*/
|
||||
export const measureDistance = function(p0, p1, {gridSpaces=true}={}) {
|
||||
if ( !gridSpaces ) return BaseGrid.prototype.measureDistance.bind(this)(p0, p1, {gridSpaces});
|
||||
let gs = canvas.dimensions.size,
|
||||
ray = new Ray(p0, p1),
|
||||
nx = Math.abs(Math.ceil(ray.dx / gs)),
|
||||
ny = Math.abs(Math.ceil(ray.dy / gs));
|
||||
|
||||
// Get the number of straight and diagonal moves
|
||||
let nDiagonal = Math.min(nx, ny),
|
||||
nStraight = Math.abs(ny - nx);
|
||||
|
||||
// Alternative DMG Movement
|
||||
if ( this.parent.diagonalRule === "5105" ) {
|
||||
let nd10 = Math.floor(nDiagonal / 2);
|
||||
let spaces = (nd10 * 2) + (nDiagonal - nd10) + nStraight;
|
||||
return spaces * canvas.dimensions.distance;
|
||||
}
|
||||
|
||||
// Standard PHB Movement
|
||||
else return (nStraight + nDiagonal) * 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(data['temp'] || 0);
|
||||
data.max += parseInt(data['tempmax'] || 0);
|
||||
}
|
||||
return data;
|
||||
};
|
91
module/chat.js
Normal file
91
module/chat.js
Normal file
|
@ -0,0 +1,91 @@
|
|||
import {Actor5e} from "./actor/entity.js";
|
||||
|
||||
/**
|
||||
* Highlight critical success or failure on d20 rolls
|
||||
*/
|
||||
export const highlightCriticalSuccessFailure = function(message, html, data) {
|
||||
if ( !message.isRoll || !message.isRollVisible || !message.roll.parts.length ) return;
|
||||
|
||||
// Highlight rolls where the first part is a d20 roll
|
||||
const roll = message.roll;
|
||||
let d = roll.parts[0];
|
||||
const isD20Roll = d instanceof Die && (d.faces === 20) && (d.results.length === 1);
|
||||
if ( !isD20Roll ) return;
|
||||
|
||||
// Ensure it is not a modified roll
|
||||
const isModifiedRoll = ("success" in d.rolls[0]) || d.options.marginSuccess || d.options.marginFailure;
|
||||
if ( isModifiedRoll ) return;
|
||||
|
||||
// Highlight successes and failures
|
||||
if ( d.options.critical && (d.total >= d.options.critical) ) html.find(".dice-total").addClass("critical");
|
||||
else if ( d.options.fumble && (d.total <= d.options.fumble) ) html.find(".dice-total").addClass("fumble");
|
||||
else if ( d.options.target ) {
|
||||
if ( roll.total >= d.options.target ) html.find(".dice-total").addClass("success");
|
||||
else html.find(".dice-total").addClass("failure");
|
||||
}
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Optionally hide the display of chat card action buttons which cannot be performed by the user
|
||||
*/
|
||||
export const displayChatActionButtons = function(message, html, data) {
|
||||
const chatCard = html.find(".sw5e.chat-card");
|
||||
if ( chatCard.length > 0 ) {
|
||||
|
||||
// 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;
|
||||
else if ( game.user.isGM || (data.author.id === game.user.id)) return;
|
||||
|
||||
// Otherwise conceal action buttons except for saving throw
|
||||
const buttons = chatCard.find("button[data-action]");
|
||||
buttons.each((i, btn) => {
|
||||
if ( btn.dataset.action === "save" ) return;
|
||||
btn.style.display = "none"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* This function is used to hook into the Chat Log context menu to add additional options to each message
|
||||
* These options make it easy to conveniently apply damage to controlled tokens based on the value of a Roll
|
||||
*
|
||||
* @param {HTMLElement} html The Chat Message being rendered
|
||||
* @param {Array} options The Array of Context Menu options
|
||||
*
|
||||
* @return {Array} The extended options Array including new context choices
|
||||
*/
|
||||
export const addChatMessageContextOptions = function(html, options) {
|
||||
let canApply = li => canvas.tokens.controlledTokens.length && li.find(".dice-roll").length;
|
||||
options.push(
|
||||
{
|
||||
name: game.i18n.localize("SW5E.ChatContextDamage"),
|
||||
icon: '<i class="fas fa-user-minus"></i>',
|
||||
condition: canApply,
|
||||
callback: li => Actor5e.applyDamage(li, 1)
|
||||
},
|
||||
{
|
||||
name: game.i18n.localize("SW5E.ChatContextHealing"),
|
||||
icon: '<i class="fas fa-user-plus"></i>',
|
||||
condition: canApply,
|
||||
callback: li => Actor5e.applyDamage(li, -1)
|
||||
},
|
||||
{
|
||||
name: game.i18n.localize("SW5E.ChatContextDoubleDamage"),
|
||||
icon: '<i class="fas fa-user-injured"></i>',
|
||||
condition: canApply,
|
||||
callback: li => Actor5e.applyDamage(li, 2)
|
||||
},
|
||||
{
|
||||
name: game.i18n.localize("SW5E.ChatContextHalfDamage"),
|
||||
icon: '<i class="fas fa-user-shield"></i>',
|
||||
condition: canApply,
|
||||
callback: li => Actor5e.applyDamage(li, 0.5)
|
||||
}
|
||||
);
|
||||
return options;
|
||||
};
|
16
module/combat.js
Normal file
16
module/combat.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
|
||||
/**
|
||||
* Override the default Initiative formula to customize special behaviors of the D&D5e system.
|
||||
* Apply advantage, proficiency, or bonuses where appropriate
|
||||
* 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;
|
||||
if ( !actor ) return "1d20";
|
||||
const init = actor.data.data.attributes.init;
|
||||
const parts = ["1d20", init.mod, (init.prof !== 0) ? init.prof : null, (init.bonus !== 0) ? init.bonus : null];
|
||||
if ( actor.getFlag("sw5e", "initiativeAdv") ) parts[0] = "2d20kh";
|
||||
if ( CONFIG.Combat.initiative.tiebreaker ) parts.push(actor.data.data.abilities.dex.value / 100);
|
||||
return parts.filter(p => p !== null).join(" + ");
|
||||
};
|
714
module/config.js
Normal file
714
module/config.js
Normal file
|
@ -0,0 +1,714 @@
|
|||
// Namespace D&D5e Configuration Values
|
||||
export const SW5E = {};
|
||||
|
||||
// ASCII Artwork
|
||||
SW5E.ASCII = `_______________________________
|
||||
_
|
||||
| |
|
||||
___| |_ __ _ _ ____ ____ _ _ __ ___
|
||||
/ __| __/ _` | '__\ \ /\ / / _` | '__/ __|
|
||||
\__ \ || (_| | | \ V V / (_| | | \__ \
|
||||
|___/\__\__,_|_| \_/\_/ \__,_|_| |___/
|
||||
_______________________________`;
|
||||
|
||||
|
||||
/**
|
||||
* The set of Ability Scores used within the system
|
||||
* @type {Object}
|
||||
*/
|
||||
SW5E.abilities = {
|
||||
"str": "SW5E.AbilityStr",
|
||||
"dex": "SW5E.AbilityDex",
|
||||
"con": "SW5E.AbilityCon",
|
||||
"int": "SW5E.AbilityInt",
|
||||
"wis": "SW5E.AbilityWis",
|
||||
"cha": "SW5E.AbilityCha"
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Character alignment options
|
||||
* @type {Object}
|
||||
*/
|
||||
SW5E.alignments = {
|
||||
'll': "SW5E.AlignmentLL",
|
||||
'nl': "SW5E.AlignmentNL",
|
||||
'cl': "SW5E.AlignmentCL",
|
||||
'lb': "SW5E.AlignmentLB",
|
||||
'bn': "SW5E.AlignmentBN",
|
||||
'cb': "SW5E.AlignmentCB",
|
||||
'ld': "SW5E.AlignmentLD",
|
||||
'nd': "SW5E.AlignmentND",
|
||||
'cd': "SW5E.AlignmentCD"
|
||||
};
|
||||
|
||||
|
||||
SW5E.weaponProficiencies = {
|
||||
"sim": "SW5E.WeaponSimpleProficiency",
|
||||
"bla": "SW5E.WeaponBlasterProficiency"
|
||||
};
|
||||
|
||||
SW5E.toolProficiencies = {
|
||||
"art": "SW5E.ToolArtisans",
|
||||
"disg": "SW5E.ToolDisguiseKit",
|
||||
"forg": "SW5E.ToolForgeryKit",
|
||||
"game": "SW5E.ToolGamingSet",
|
||||
"herb": "SW5E.ToolHerbalismKit",
|
||||
"music": "SW5E.ToolMusicalInstrument",
|
||||
"navg": "SW5E.ToolNavigators",
|
||||
"pois": "SW5E.ToolPoisonersKit",
|
||||
"thief": "SW5E.ToolThieves",
|
||||
"vehicle": "SW5E.ToolVehicle"
|
||||
};
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* This Object defines the various lengths of time which can occur in D&D5e
|
||||
* @type {Object}
|
||||
*/
|
||||
SW5E.timePeriods = {
|
||||
"inst": "SW5E.TimeInst",
|
||||
"turn": "SW5E.TimeTurn",
|
||||
"round": "SW5E.TimeRound",
|
||||
"minute": "SW5E.TimeMinute",
|
||||
"hour": "SW5E.TimeHour",
|
||||
"day": "SW5E.TimeDay",
|
||||
"month": "SW5E.TimeMonth",
|
||||
"year": "SW5E.TimeYear",
|
||||
"perm": "SW5E.TimePerm",
|
||||
"spec": "SW5E.Special"
|
||||
};
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* This describes the ways that an ability can be activated
|
||||
* @type {Object}
|
||||
*/
|
||||
SW5E.abilityActivationTypes = {
|
||||
"none": "SW5E.None",
|
||||
"action": "SW5E.Action",
|
||||
"bonus": "SW5E.BonusAction",
|
||||
"reaction": "SW5E.Reaction",
|
||||
"minute": SW5E.timePeriods.minute,
|
||||
"hour": SW5E.timePeriods.hour,
|
||||
"day": SW5E.timePeriods.day,
|
||||
"special": SW5E.timePeriods.spec,
|
||||
"legendary": "SW5E.LegAct",
|
||||
"lair": "SW5E.LairAct"
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
// Creature Sizes
|
||||
SW5E.actorSizes = {
|
||||
"tiny": "SW5E.SizeTiny",
|
||||
"sm": "SW5E.SizeSmall",
|
||||
"med": "SW5E.SizeMedium",
|
||||
"lg": "SW5E.SizeLarge",
|
||||
"huge": "SW5E.SizeHuge",
|
||||
"grg": "SW5E.SizeGargantuan"
|
||||
};
|
||||
|
||||
SW5E.tokenSizes = {
|
||||
"tiny": 1,
|
||||
"sm": 1,
|
||||
"med": 1,
|
||||
"lg": 2,
|
||||
"huge": 3,
|
||||
"grg": 4
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Classification types for item action types
|
||||
* @type {Object}
|
||||
*/
|
||||
SW5E.itemActionTypes = {
|
||||
"mwak": "SW5E.ActionMWAK",
|
||||
"rwak": "SW5E.ActionRWAK",
|
||||
"mpak": "SW5E.ActionMPAK",
|
||||
"rpak": "SW5E.ActionRPAK",
|
||||
"save": "SW5E.ActionSave",
|
||||
"heal": "SW5E.ActionHeal",
|
||||
"abil": "SW5E.ActionAbil",
|
||||
"util": "SW5E.ActionUtil",
|
||||
"other": "SW5E.ActionOther"
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
SW5E.itemCapacityTypes = {
|
||||
"items": "SW5E.ItemContainerCapacityItems",
|
||||
"weight": "SW5E.ItemContainerCapacityWeight"
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Enumerate the lengths of time over which an item can have limited use ability
|
||||
* @type {Object}
|
||||
*/
|
||||
SW5E.limitedUsePeriods = {
|
||||
"sr": "SW5E.ShortRest",
|
||||
"lr": "SW5E.LongRest",
|
||||
"day": "SW5E.Day",
|
||||
"charges": "SW5E.Charges"
|
||||
};
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The set of equipment types for armor, clothing, and other objects which can ber worn by the character
|
||||
* @type {Object}
|
||||
*/
|
||||
SW5E.equipmentTypes = {
|
||||
"light": "SW5E.EquipmentLight",
|
||||
"medium": "SW5E.EquipmentMedium",
|
||||
"heavy": "SW5E.EquipmentHeavy",
|
||||
"bonus": "SW5E.EquipmentBonus",
|
||||
"natural": "SW5E.EquipmentNatural",
|
||||
"shield": "SW5E.EquipmentShield",
|
||||
"clothing": "SW5E.EquipmentClothing",
|
||||
"trinket": "SW5E.EquipmentTrinket"
|
||||
};
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The set of Armor Proficiencies which a character may have
|
||||
* @type {Object}
|
||||
*/
|
||||
SW5E.armorProficiencies = {
|
||||
"lgt": SW5E.equipmentTypes.light,
|
||||
"med": SW5E.equipmentTypes.medium,
|
||||
"hvy": SW5E.equipmentTypes.heavy,
|
||||
"shl": "SW5E.EquipmentShieldProficiency"
|
||||
};
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Enumerate the valid consumable types which are recognized by the system
|
||||
* @type {Object}
|
||||
*/
|
||||
SW5E.consumableTypes = {
|
||||
"adrenal": "SW5E.ConsumableAdrenal",
|
||||
"poison": "SW5E.ConsumablePoison",
|
||||
"explosive": "SW5E.ConsumableExplosive",
|
||||
"food": "SW5E.ConsumableFood",
|
||||
"medpac": "SW5E.ConsumableMedpac",
|
||||
"technology": "SW5E.ConsumableTechnology",
|
||||
"ammunition": "SW5E.ConsumableAmmunition",
|
||||
"trinket": "SW5E.ConsumableTrinket"
|
||||
};
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The valid currency denominations supported by the 5e system
|
||||
* @type {Object}
|
||||
*/
|
||||
SW5E.currencies = {
|
||||
"CR": "SW5E.CurrencyCR",
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
|
||||
// Damage Types
|
||||
SW5E.damageTypes = {
|
||||
"acid": "SW5E.DamageAcid",
|
||||
"cold": "SW5E.DamageCold",
|
||||
"energy": "SW5E.DamageEnergy",
|
||||
"fire": "SW5E.DamageFire",
|
||||
"force": "SW5E.DamageForce",
|
||||
"ion": "SW5E.DamageIon",
|
||||
"kinetic": "SW5E.DamageKinetic",
|
||||
"lightning": "SW5E.DamageLightning",
|
||||
"necrotic": "SW5E.DamageNecrotic",
|
||||
"poison": "SW5E.DamagePoison",
|
||||
"psychic": "SW5E.DamagePsychic",
|
||||
"Sonic": "SW5E.DamageSonic",
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
SW5E.distanceUnits = {
|
||||
"none": "SW5E.None",
|
||||
"self": "SW5E.DistSelf",
|
||||
"touch": "SW5E.DistTouch",
|
||||
"ft": "SW5E.DistFt",
|
||||
"mi": "SW5E.DistMi",
|
||||
"spec": "SW5E.Special",
|
||||
"any": "SW5E.DistAny"
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
|
||||
/**
|
||||
* Configure aspects of encumbrance calculation so that it could be configured by modules
|
||||
* @type {Object}
|
||||
*/
|
||||
SW5E.encumbrance = {
|
||||
currencyPerWeight: 50,
|
||||
strMultiplier: 15
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* This Object defines the types of single or area targets which can be applied in D&D5e
|
||||
* @type {Object}
|
||||
*/
|
||||
SW5E.targetTypes = {
|
||||
"none": "SW5E.None",
|
||||
"self": "SW5E.TargetSelf",
|
||||
"creature": "SW5E.TargetCreature",
|
||||
"ally": "SW5E.TargetAlly",
|
||||
"enemy": "SW5E.TargetEnemy",
|
||||
"object": "SW5E.TargetObject",
|
||||
"space": "SW5E.TargetSpace",
|
||||
"radius": "SW5E.TargetRadius",
|
||||
"sphere": "SW5E.TargetSphere",
|
||||
"cylinder": "SW5E.TargetCylinder",
|
||||
"cone": "SW5E.TargetCone",
|
||||
"square": "SW5E.TargetSquare",
|
||||
"cube": "SW5E.TargetCube",
|
||||
"line": "SW5E.TargetLine",
|
||||
"wall": "SW5E.TargetWall"
|
||||
};
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
|
||||
/**
|
||||
* Map the subset of target types which produce a template area of effect
|
||||
* The keys are SW5E target types and the values are MeasuredTemplate shape types
|
||||
* @type {Object}
|
||||
*/
|
||||
SW5E.areaTargetTypes = {
|
||||
cone: "cone",
|
||||
cube: "rect",
|
||||
cylinder: "circle",
|
||||
line: "ray",
|
||||
radius: "circle",
|
||||
sphere: "circle",
|
||||
square: "rect",
|
||||
wall: "ray"
|
||||
};
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
// Healing Types
|
||||
SW5E.healingTypes = {
|
||||
"healing": "SW5E.Healing",
|
||||
"temphp": "SW5E.HealingTemp"
|
||||
};
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
|
||||
/**
|
||||
* Enumerate the denominations of hit dice which can apply to classes in the D&D5E system
|
||||
* @type {Array.<string>}
|
||||
*/
|
||||
SW5E.hitDieTypes = ["d6", "d8", "d10", "d12"];
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Character senses options
|
||||
* @type {Object}
|
||||
*/
|
||||
SW5E.senses = {
|
||||
"bs": "SW5E.SenseBS",
|
||||
"dv": "SW5E.SenseDV",
|
||||
"ts": "SW5E.SenseTS",
|
||||
"tr": "SW5E.SenseTR"
|
||||
};
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The set of skill which can be trained in D&D5e
|
||||
* @type {Object}
|
||||
*/
|
||||
SW5E.skills = {
|
||||
"acr": "SW5E.SkillAcr",
|
||||
"ani": "SW5E.SkillAni",
|
||||
"ath": "SW5E.SkillAth",
|
||||
"dec": "SW5E.SkillDec",
|
||||
"ins": "SW5E.SkillIns",
|
||||
"itm": "SW5E.SkillItm",
|
||||
"inv": "SW5E.SkillInv",
|
||||
"lor": "SW5E.SkillLor",
|
||||
"med": "SW5E.SkillMed",
|
||||
"nat": "SW5E.SkillNat",
|
||||
"prc": "SW5E.SkillPrc",
|
||||
"prf": "SW5E.SkillPrf",
|
||||
"per": "SW5E.SkillPer",
|
||||
"pil": "SW5E.SkillPil",
|
||||
"slt": "SW5E.SkillSlt",
|
||||
"ste": "SW5E.SkillSte",
|
||||
"sur": "SW5E.SkillSur",
|
||||
"tec": "SW5E.SkillTec",
|
||||
};
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
SW5E.powerPreparationModes = {
|
||||
"always": "SW5E.PowerPrepAlways",
|
||||
"atwill": "SW5E.PowerPrepAtWill",
|
||||
"innate": "SW5E.PowerPrepInnate",
|
||||
"prepared": "SW5E.PowerPrepPrepared"
|
||||
};
|
||||
|
||||
SW5E.powerUpcastModes = ["always"];
|
||||
|
||||
|
||||
SW5E.powerProgression = {
|
||||
"none": "SW5E.PowerNone",
|
||||
"full": "SW5E.PowerProgFull",
|
||||
"artificer": "SW5E.PowerProgArt"
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* The available choices for how power damage scaling may be computed
|
||||
* @type {Object}
|
||||
*/
|
||||
SW5E.powerScalingModes = {
|
||||
"none": "SW5E.PowerNone",
|
||||
"atwill": "SW5E.PowerAtWill",
|
||||
"level": "SW5E.PowerLevel"
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
// Weapon Types
|
||||
SW5E.weaponTypes = {
|
||||
"simpleVW": "SW5E.WeaponSimpleVW",
|
||||
"simpleB": "SW5E.WeaponSimpleB",
|
||||
"simpleLW": "SW5E.WeaponSimpleLW",
|
||||
"martialVW": "SW5E.WeaponMartialVW",
|
||||
"martialB": "SW5E.WeaponMartialB",
|
||||
"martialLW": "SW5E.WeaponMartialLW",
|
||||
"natural": "SW5E.WeaponNatural",
|
||||
"improv": "SW5E.WeaponImprov",
|
||||
"ammo": "SW5E.WeaponAmmo"
|
||||
};
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Define the set of weapon property flags which can exist on a weapon
|
||||
* @type {Object}
|
||||
*/
|
||||
SW5E.weaponProperties = {
|
||||
"amm": "SW5E.WeaponPropertiesAmm",
|
||||
"bur": "SW5E.WeaponPropertiesBur",
|
||||
"def": "SW5E.WeaponPropertiesDef",
|
||||
"dex": "SW5E.WeaponPropertiesDex",
|
||||
"drm": "SW5E.WeaponPropertiesBur",
|
||||
"dgd": "SW5E.WeaponPropertiesDgd",
|
||||
"dis": "SW5E.WeaponPropertiesDis",
|
||||
"dpt": "SW5E.WeaponPropertiesDpt",
|
||||
"dou": "SW5E.WeaponPropertiesDou",
|
||||
"hvy": "SW5E.WeaponPropertiesHvy",
|
||||
"hid": "SW5E.WeaponPropertiesHid",
|
||||
"fin": "SW5E.WeaponPropertiesFin",
|
||||
"fix": "SW5E.WeaponPropertiesFix",
|
||||
"foc": "SW5E.WeaponPropertiesFoc",
|
||||
"ken": "SW5E.WeaponPropertiesKen",
|
||||
"lgt": "SW5E.WeaponPropertiesLgt",
|
||||
"lum": "SW5E.WeaponPropertiesLum",
|
||||
"pic": "SW5E.WeaponPropertiesPic",
|
||||
"rap": "SW5E.WeaponPropertiesRap",
|
||||
"rch": "SW5E.WeaponPropertiesRch",
|
||||
"rel": "SW5E.WeaponPropertiesRel",
|
||||
"ret": "SW5E.WeaponPropertiesRet",
|
||||
"shk": "SW5E.WeaponPropertiesShk",
|
||||
"spc": "SW5E.WeaponPropertiesSpc",
|
||||
"str": "SW5E.WeaponPropertiesStr",
|
||||
"thr": "SW5E.WeaponPropertiesThr",
|
||||
"two": "SW5E.WeaponPropertiesTwo",
|
||||
"ver": "SW5E.WeaponPropertiesVer",
|
||||
"vic": "SW5E.WeaponPropertiesVic"
|
||||
};
|
||||
|
||||
|
||||
// Power Components
|
||||
SW5E.powerComponents = {
|
||||
"V": "SW5E.ComponentVerbal",
|
||||
"S": "SW5E.ComponentSomatic",
|
||||
"M": "SW5E.ComponentMaterial"
|
||||
};
|
||||
|
||||
// Power Schools
|
||||
SW5E.powerSchools = {
|
||||
"lgt": "SW5E.SchoolLgt",
|
||||
"uni": "SW5E.SchoolUni",
|
||||
"drk": "SW5E.SchoolDrk",
|
||||
"tec": "SW5E.SchoolTec",
|
||||
"enh": "SW5E.SchoolEnh"
|
||||
};
|
||||
|
||||
|
||||
// Power Levels
|
||||
SW5E.powerLevels = {
|
||||
0: "SW5E.PowerLevel0",
|
||||
1: "SW5E.PowerLevel1",
|
||||
2: "SW5E.PowerLevel2",
|
||||
3: "SW5E.PowerLevel3",
|
||||
4: "SW5E.PowerLevel4",
|
||||
5: "SW5E.PowerLevel5",
|
||||
6: "SW5E.PowerLevel6",
|
||||
7: "SW5E.PowerLevel7",
|
||||
8: "SW5E.PowerLevel8",
|
||||
9: "SW5E.PowerLevel9"
|
||||
};
|
||||
|
||||
/**
|
||||
* Define the standard slot progression by character level.
|
||||
* The entries of this array represent the power slot progression for a full power-caster.
|
||||
* @type {Array[]}
|
||||
*/
|
||||
SW5E.SPELL_SLOT_TABLE = [
|
||||
[2],
|
||||
[3],
|
||||
[4, 2],
|
||||
[4, 3],
|
||||
[4, 3, 2],
|
||||
[4, 3, 3],
|
||||
[4, 3, 3, 1],
|
||||
[4, 3, 3, 2],
|
||||
[4, 3, 3, 3, 1],
|
||||
[4, 3, 3, 3, 2],
|
||||
[4, 3, 3, 3, 2, 1],
|
||||
[4, 3, 3, 3, 2, 1],
|
||||
[4, 3, 3, 3, 2, 1, 1],
|
||||
[4, 3, 3, 3, 2, 1, 1],
|
||||
[4, 3, 3, 3, 2, 1, 1, 1],
|
||||
[4, 3, 3, 3, 2, 1, 1, 1],
|
||||
[4, 3, 3, 3, 2, 1, 1, 1, 1],
|
||||
[4, 3, 3, 3, 3, 1, 1, 1, 1],
|
||||
[4, 3, 3, 3, 3, 2, 1, 1, 1],
|
||||
[4, 3, 3, 3, 3, 2, 2, 1, 1]
|
||||
];
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
// Polymorph options.
|
||||
SW5E.polymorphSettings = {
|
||||
keepPhysical: 'SW5E.PolymorphKeepPhysical',
|
||||
keepMental: 'SW5E.PolymorphKeepMental',
|
||||
keepSaves: 'SW5E.PolymorphKeepSaves',
|
||||
keepSkills: 'SW5E.PolymorphKeepSkills',
|
||||
mergeSaves: 'SW5E.PolymorphMergeSaves',
|
||||
mergeSkills: 'SW5E.PolymorphMergeSkills',
|
||||
keepClass: 'SW5E.PolymorphKeepClass',
|
||||
keepFeats: 'SW5E.PolymorphKeepFeats',
|
||||
keepPowers: 'SW5E.PolymorphKeepPowers',
|
||||
keepItems: 'SW5E.PolymorphKeepItems',
|
||||
keepBio: 'SW5E.PolymorphKeepBio',
|
||||
keepVision: 'SW5E.PolymorphKeepVision'
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Skill, ability, and tool proficiency levels
|
||||
* Each level provides a proficiency multiplier
|
||||
* @type {Object}
|
||||
*/
|
||||
SW5E.proficiencyLevels = {
|
||||
0: "SW5E.NotProficient",
|
||||
1: "SW5E.Proficient",
|
||||
0.5: "SW5E.HalfProficient",
|
||||
2: "SW5E.Expertise"
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
|
||||
// Condition Types
|
||||
SW5E.conditionTypes = {
|
||||
"blinded": "SW5E.ConBlinded",
|
||||
"charmed": "SW5E.ConCharmed",
|
||||
"deafened": "SW5E.ConDeafened",
|
||||
"exhaustion": "SW5E.ConExhaustion",
|
||||
"frightened": "SW5E.ConFrightened",
|
||||
"grappled": "SW5E.ConGrappled",
|
||||
"incapacitated": "SW5E.ConIncapacitated",
|
||||
"invisible": "SW5E.ConInvisible",
|
||||
"paralyzed": "SW5E.ConParalyzed",
|
||||
"petrified": "SW5E.ConPetrified",
|
||||
"poisoned": "SW5E.ConPoisoned",
|
||||
"prone": "SW5E.ConProne",
|
||||
"restrained": "SW5E.ConRestrained",
|
||||
"shocked": "SW5E.ConShocked",
|
||||
"stunned": "SW5E.ConStunned",
|
||||
"unconscious": "SW5E.ConUnconscious"
|
||||
};
|
||||
|
||||
// Languages
|
||||
SW5E.languages = {
|
||||
"basic": "SW5E.LanguagesBasic",
|
||||
"binary": "SW5E.LanguagesBinary",
|
||||
"bith": "SW5E.LanguagesBith",
|
||||
"bocce": "SW5E.LanguagesBocce",
|
||||
"bothese": "SW5E.LanguagesBothese",
|
||||
"catharese": "SW5E.LanguagesCartharese",
|
||||
"cheunh": "SW5E.LanguagesCheunh",
|
||||
"durese": "SW5E.LanguagesDurese",
|
||||
"dug": "SW5E.LanguagesDug",
|
||||
"ewokese": "SW5E.LanguagesEwokese",
|
||||
"gamorrese": "SW5E.LanguagesGamorrese",
|
||||
"geonosian": "SW5E.LanguagesGeonosian",
|
||||
"hapan": "SW5E.LanguagesHapan",
|
||||
"huttese": "SW5E.LanguagesHuttese",
|
||||
"jawaese": "SW5E.LanguagesJawaese",
|
||||
"kaleesh": "SW5E.LanguagesKaleesh",
|
||||
"kaminoan": "SW5E.LanguagesKaminoan",
|
||||
"keldor": "SW5E.LanguagesKelDor",
|
||||
"mandoa": "SW5E.LanguagesMandoa",
|
||||
"moncal": "SW5E.LanguagesMonCal",
|
||||
"pakpak": "SW5E.LanguagesPakPak",
|
||||
"rodese": "SW5E.LanguagesRodese",
|
||||
"sith": "SW5E.LanguagesSith",
|
||||
"togruti": "SW5E.LanguagesTogruti",
|
||||
"dosh": "SW5E.LanguagesDosh",
|
||||
"twi'leki": "SW5E.LanguagesTwi'leki",
|
||||
"tusken": "SW5E.LanguagesTusken",
|
||||
"shyriiwook": "SW5E.LanguagesShyriiwook",
|
||||
"zabraki": "SW5E.LanguagesZabraki",
|
||||
"vong": "SW5E.LanguagesVong"
|
||||
};
|
||||
|
||||
// Character Level XP Requirements
|
||||
SW5E.CHARACTER_EXP_LEVELS = [
|
||||
0, 300, 900, 2700, 6500, 14000, 23000, 34000, 48000, 64000, 85000, 100000,
|
||||
120000, 140000, 165000, 195000, 225000, 265000, 305000, 355000]
|
||||
;
|
||||
|
||||
// Challenge Rating XP Levels
|
||||
SW5E.CR_EXP_LEVELS = [
|
||||
10, 200, 450, 700, 1100, 1800, 2300, 2900, 3900, 5000, 5900, 7200, 8400, 10000, 11500, 13000, 15000, 18000,
|
||||
20000, 22000, 25000, 33000, 41000, 50000, 62000, 75000, 90000, 105000, 120000, 135000, 155000
|
||||
];
|
||||
|
||||
// Configure Optional Character Flags
|
||||
SW5E.characterFlags = {
|
||||
"detailOriented": {
|
||||
name: "Detail Oriented",
|
||||
hint: "You have advantage on Intelligence (Investigation) checks within 5 feet.",
|
||||
section: "Racial Traits",
|
||||
type: Boolean
|
||||
},
|
||||
"keenSenses": {
|
||||
name: "Keen Hearing and Smell",
|
||||
hint: "You have advantage on Wisdom (Perception) checks that involve hearing or smell.",
|
||||
section: "Racial Traits",
|
||||
type: Boolean
|
||||
},
|
||||
"naturallyStealthy": {
|
||||
name: "Naturally Stealthy",
|
||||
hint: "You can attempt to hide even when you are obscured only by a creature that is your size or larger than you.",
|
||||
section: "Racial Traits",
|
||||
type: Boolean
|
||||
},
|
||||
"nimbleEscape": {
|
||||
name: "Nimble Escape",
|
||||
hint: "You can take the Disengage or Hide action as a bonus action.",
|
||||
section: "Racial Traits",
|
||||
type: Boolean
|
||||
},
|
||||
"powerfulBuild": {
|
||||
name: "Powerful Build",
|
||||
hint: "You count as one size larger when determining your carrying capacity and the weight you can push, drag, or lift.",
|
||||
section: "Racial Traits",
|
||||
type: Boolean
|
||||
},
|
||||
"programmer": {
|
||||
name: "Programmer",
|
||||
hint: "Whenever you make an Intelligence (Technology) check related to computers, you are considered to have expertise in the Technology skill.",
|
||||
section: "Racial Traits",
|
||||
type: Boolean
|
||||
},
|
||||
"techResistance": {
|
||||
name: "Tech Resistance",
|
||||
hint: "You have advantage on Dexterity and Intelligence saving throws against tech powers.",
|
||||
section: "Racial Traits",
|
||||
type: Boolean
|
||||
},
|
||||
"unarmedCombatant": {
|
||||
name: "Unarmed Combatant",
|
||||
hint: "Your unarmed strikes deal 1d4 kinetic damage. You can use your choice of your Strength or Dexterity modifier for the attack and damage rolls. You must use the same modifier for both rolls.",
|
||||
section: "Racial Traits",
|
||||
type: Boolean
|
||||
},
|
||||
"undersized": {
|
||||
name: "Undersized",
|
||||
hint: "You can’t use heavy shields, martial weapons with the two-handed property unless it also has the light property, and if a martial weapon has the versatile property, you can only wield it in two hands.",
|
||||
section: "Racial Traits",
|
||||
type: Boolean
|
||||
},
|
||||
"initiativeAdv": {
|
||||
name: "Advantage on Initiative",
|
||||
hint: "Provided by feats or magical items.",
|
||||
section: "Feats",
|
||||
type: Boolean
|
||||
},
|
||||
"initiativeAlert": {
|
||||
name: "Alert Feat",
|
||||
hint: "Provides +5 to Initiative.",
|
||||
section: "Feats",
|
||||
type: Boolean
|
||||
},
|
||||
"jackOfAllTrades": {
|
||||
name: "Jack of All Trades",
|
||||
hint: "Half-Proficiency to Ability Checks in which you are not already Proficient.",
|
||||
section: "Feats",
|
||||
type: Boolean
|
||||
},
|
||||
"observantFeat": {
|
||||
name: "Observant Feat",
|
||||
hint: "Provides a +5 to passive Perception and Investigation.",
|
||||
skills: ['prc','inv'],
|
||||
section: "Feats",
|
||||
type: Boolean
|
||||
},
|
||||
"remarkableAthlete": {
|
||||
name: "Remarkable Athlete.",
|
||||
hint: "Half-Proficiency (rounded-up) to physical Ability Checks and Initiative.",
|
||||
abilities: ['str','dex','con'],
|
||||
section: "Feats",
|
||||
type: Boolean
|
||||
},
|
||||
"weaponCriticalThreshold": {
|
||||
name: "Critical Hit Threshold",
|
||||
hint: "Allow for expanded critical range; for example Improved or Superior Critical",
|
||||
section: "Feats",
|
||||
type: Number,
|
||||
placeholder: 20
|
||||
}
|
||||
};
|
251
module/dice.js
Normal file
251
module/dice.js
Normal file
|
@ -0,0 +1,251 @@
|
|||
export class Dice5e {
|
||||
|
||||
/**
|
||||
* 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?
|
||||
*
|
||||
* @return {Promise} A Promise which resolves once the roll workflow has completed
|
||||
*/
|
||||
static async d20Roll({parts=[], data={}, event={}, rollMode=null, template=null, title=null, speaker=null,
|
||||
flavor=null, fastForward=null, onClose, dialogOptions,
|
||||
advantage=null, disadvantage=null, critical=20, fumble=1, targetValue=null,
|
||||
elvenAccuracy=false, halflingLucky=false}={}) {
|
||||
|
||||
// Handle input arguments
|
||||
flavor = flavor || title;
|
||||
speaker = speaker || ChatMessage.getSpeaker();
|
||||
parts = parts.concat(["@bonus"]);
|
||||
rollMode = rollMode || game.settings.get("core", "rollMode");
|
||||
let rolled = false;
|
||||
|
||||
// Define inner roll function
|
||||
const _roll = function(parts, adv, form=null) {
|
||||
|
||||
// Determine the d20 roll and modifiers
|
||||
let nd = 1;
|
||||
let mods = halflingLucky ? "r=1" : "";
|
||||
|
||||
// Handle advantage
|
||||
if ( adv === 1 ) {
|
||||
nd = elvenAccuracy ? 3 : 2;
|
||||
flavor += ` (${game.i18n.localize("SW5E.Advantage")})`;
|
||||
mods += "kh";
|
||||
}
|
||||
|
||||
// Handle disadvantage
|
||||
else if ( adv === -1 ) {
|
||||
nd = 2;
|
||||
flavor += ` (${game.i18n.localize("SW5E.Disadvantage")})`;
|
||||
mods += "kl";
|
||||
}
|
||||
|
||||
// Include the d20 roll
|
||||
parts.unshift(`${nd}d20${mods}`);
|
||||
|
||||
// Optionally include a situational bonus
|
||||
if ( form !== null ) data['bonus'] = form.bonus.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;
|
||||
flavor += ` (${CONFIG.SW5E.abilities[data.ability]})`;
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the roll and flag critical thresholds on the d20
|
||||
let roll = new Roll(parts.join(" + "), data).roll();
|
||||
const d20 = roll.parts[0];
|
||||
d20.options.critical = critical;
|
||||
d20.options.fumble = fumble;
|
||||
if ( targetValue ) d20.options.target = targetValue;
|
||||
|
||||
// Convert the roll to a chat message and return the roll
|
||||
rollMode = form ? form.rollMode.value : rollMode;
|
||||
roll.toMessage({
|
||||
speaker: speaker,
|
||||
flavor: flavor
|
||||
}, { rollMode });
|
||||
rolled = true;
|
||||
return roll;
|
||||
};
|
||||
|
||||
// Determine whether the roll can be fast-forward
|
||||
if ( fastForward === null ) {
|
||||
fastForward = event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey);
|
||||
}
|
||||
|
||||
// Optionally allow fast-forwarding to specify advantage or disadvantage
|
||||
if ( fastForward ) {
|
||||
if ( advantage || event.altKey ) return _roll(parts, 1);
|
||||
else if ( disadvantage || event.ctrlKey || event.metaKey ) return _roll(parts, -1);
|
||||
else return _roll(parts, 0);
|
||||
}
|
||||
|
||||
// Render modal dialog
|
||||
template = template || "systems/sw5e/templates/chat/roll-dialog.html";
|
||||
let dialogData = {
|
||||
formula: parts.join(" + "),
|
||||
data: data,
|
||||
rollMode: rollMode,
|
||||
rollModes: CONFIG.rollModes,
|
||||
config: CONFIG.SW5E
|
||||
};
|
||||
const html = await renderTemplate(template, dialogData);
|
||||
|
||||
// Create the Dialog window
|
||||
let roll;
|
||||
return new Promise(resolve => {
|
||||
new Dialog({
|
||||
title: title,
|
||||
content: html,
|
||||
buttons: {
|
||||
advantage: {
|
||||
label: game.i18n.localize("SW5E.Advantage"),
|
||||
callback: html => roll = _roll(parts, 1, html[0].children[0])
|
||||
},
|
||||
normal: {
|
||||
label: game.i18n.localize("SW5E.Normal"),
|
||||
callback: html => roll = _roll(parts, 0, html[0].children[0])
|
||||
},
|
||||
disadvantage: {
|
||||
label: game.i18n.localize("SW5E.Disadvantage"),
|
||||
callback: html => roll = _roll(parts, -1, html[0].children[0])
|
||||
}
|
||||
},
|
||||
default: "normal",
|
||||
close: html => {
|
||||
if (onClose) onClose(html, parts, data);
|
||||
resolve(rolled ? roll : false)
|
||||
}
|
||||
}, dialogOptions).render(true);
|
||||
})
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* 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, 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 {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
|
||||
*
|
||||
* @return {Promise} A Promise which resolves once the roll workflow has completed
|
||||
*/
|
||||
static async damageRoll({parts, actor, data, event={}, rollMode=null, template, title, speaker, flavor,
|
||||
allowCritical=true, critical=false, fastForward=null, onClose, dialogOptions}) {
|
||||
|
||||
// Handle input arguments
|
||||
flavor = flavor || title;
|
||||
speaker = speaker || ChatMessage.getSpeaker();
|
||||
rollMode = game.settings.get("core", "rollMode");
|
||||
let rolled = false;
|
||||
|
||||
// Define inner roll function
|
||||
const _roll = function(parts, crit, form) {
|
||||
data['bonus'] = form ? form.bonus.value : 0;
|
||||
let roll = new Roll(parts.join("+"), data);
|
||||
|
||||
// Modify the damage formula for critical hits
|
||||
if ( crit === true ) {
|
||||
let add = (actor && actor.getFlag("sw5e", "savageAttacks")) ? 1 : 0;
|
||||
let mult = 2;
|
||||
roll.alter(add, mult);
|
||||
flavor = `${flavor} (${game.i18n.localize("SW5E.Critical")})`;
|
||||
}
|
||||
|
||||
// Convert the roll to a chat message
|
||||
rollMode = form ? form.rollMode.value : rollMode;
|
||||
roll.toMessage({
|
||||
speaker: speaker,
|
||||
flavor: flavor
|
||||
}, { rollMode });
|
||||
rolled = true;
|
||||
return roll;
|
||||
};
|
||||
|
||||
// Determine whether the roll can be fast-forward
|
||||
if ( fastForward === null ) {
|
||||
fastForward = event && (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey);
|
||||
}
|
||||
|
||||
// Modify the roll and handle fast-forwarding
|
||||
if ( fastForward ) return _roll(parts, critical || event.altKey);
|
||||
else parts = parts.concat(["@bonus"]);
|
||||
|
||||
// Render modal dialog
|
||||
template = template || "systems/sw5e/templates/chat/roll-dialog.html";
|
||||
let dialogData = {
|
||||
formula: parts.join(" + "),
|
||||
data: data,
|
||||
rollMode: rollMode,
|
||||
rollModes: CONFIG.rollModes
|
||||
};
|
||||
const html = await renderTemplate(template, dialogData);
|
||||
|
||||
// Create the Dialog window
|
||||
let roll;
|
||||
return new Promise(resolve => {
|
||||
new Dialog({
|
||||
title: title,
|
||||
content: html,
|
||||
buttons: {
|
||||
critical: {
|
||||
condition: allowCritical,
|
||||
label: game.i18n.localize("SW5E.CriticalHit"),
|
||||
callback: html => roll = _roll(parts, true, html[0].children[0])
|
||||
},
|
||||
normal: {
|
||||
label: game.i18n.localize(allowCritical ? "SW5E.Normal" : "SW5E.Roll"),
|
||||
callback: html => roll = _roll(parts, false, html[0].children[0])
|
||||
},
|
||||
},
|
||||
default: "normal",
|
||||
close: html => {
|
||||
if (onClose) onClose(html, parts, data);
|
||||
resolve(rolled ? roll : false);
|
||||
}
|
||||
}, dialogOptions).render(true);
|
||||
});
|
||||
}
|
||||
}
|
224
module/migration.js
Normal file
224
module/migration.js
Normal file
|
@ -0,0 +1,224 @@
|
|||
/**
|
||||
* Perform a system migration for the entire World, applying migrations for Actors, Items, and Compendium packs
|
||||
* @return {Promise} A Promise which resolves once the migration is completed
|
||||
*/
|
||||
export const migrateWorld = async function() {
|
||||
ui.notifications.info(`Applying SW5E System Migration for version ${game.system.data.version}. Please be patient and do not close your game or shut down your server.`, {permanent: true});
|
||||
|
||||
// Migrate World Actors
|
||||
for ( let a of game.actors.entities ) {
|
||||
try {
|
||||
const updateData = migrateActorData(a.data);
|
||||
if ( !isObjectEmpty(updateData) ) {
|
||||
console.log(`Migrating Actor entity ${a.name}`);
|
||||
await a.update(updateData, {enforceTypes: false});
|
||||
}
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate World Items
|
||||
for ( let i of game.items.entities ) {
|
||||
try {
|
||||
const updateData = migrateItemData(i.data);
|
||||
if ( !isObjectEmpty(updateData) ) {
|
||||
console.log(`Migrating Item entity ${i.name}`);
|
||||
await i.update(updateData, {enforceTypes: false});
|
||||
}
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate Actor Override Tokens
|
||||
for ( let s of game.scenes.entities ) {
|
||||
try {
|
||||
const updateData = migrateSceneData(s.data);
|
||||
if ( !isObjectEmpty(updateData) ) {
|
||||
console.log(`Migrating Scene entity ${s.name}`);
|
||||
await s.update(updateData, {enforceTypes: false});
|
||||
}
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate World Compendium Packs
|
||||
const packs = game.packs.filter(p => {
|
||||
return (p.metadata.package === "world") && ["Actor", "Item", "Scene"].includes(p.metadata.entity)
|
||||
});
|
||||
for ( let p of packs ) {
|
||||
await migrateCompendium(p);
|
||||
}
|
||||
|
||||
// Set the migration as complete
|
||||
game.settings.set("sw5e", "systemMigrationVersion", game.system.data.version);
|
||||
ui.notifications.info(`SW5E System Migration to version ${game.system.data.version} completed!`, {permanent: true});
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Apply migration rules to all Entities within a single Compendium pack
|
||||
* @param pack
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const migrateCompendium = async function(pack) {
|
||||
const entity = pack.metadata.entity;
|
||||
if ( !["Actor", "Item", "Scene"].includes(entity) ) return;
|
||||
|
||||
// Begin by requesting server-side data model migration and get the migrated content
|
||||
await pack.migrate();
|
||||
const content = await pack.getContent();
|
||||
|
||||
// Iterate over compendium entries - applying fine-tuned migration functions
|
||||
for ( let ent of content ) {
|
||||
try {
|
||||
let updateData = null;
|
||||
if (entity === "Item") updateData = migrateItemData(ent.data);
|
||||
else if (entity === "Actor") updateData = migrateActorData(ent.data);
|
||||
else if ( entity === "Scene" ) updateData = migrateSceneData(ent.data);
|
||||
if (!isObjectEmpty(updateData)) {
|
||||
expandObject(updateData);
|
||||
updateData["_id"] = ent._id;
|
||||
await pack.updateEntity(updateData);
|
||||
console.log(`Migrated ${entity} entity ${ent.name} in Compendium ${pack.collection}`);
|
||||
}
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
console.log(`Migrated all ${entity} entities from Compendium ${pack.collection}`);
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Entity Type Migration Helpers */
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export const migrateActorData = function(actor) {
|
||||
const updateData = {};
|
||||
|
||||
// Actor Data Updates
|
||||
_migrateActorBonuses(actor, updateData);
|
||||
|
||||
// Remove deprecated fields
|
||||
_migrateRemoveDeprecated(actor, updateData);
|
||||
|
||||
// Migrate Owned Items
|
||||
if ( !actor.items ) return updateData;
|
||||
let hasItemUpdates = false;
|
||||
const items = actor.items.map(i => {
|
||||
|
||||
// Migrate the Owned Item
|
||||
let itemUpdate = migrateItemData(i);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
return updateData;
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Migrate a single Item entity to incorporate latest data model changes
|
||||
* @param item
|
||||
*/
|
||||
export const migrateItemData = function(item) {
|
||||
const updateData = {};
|
||||
|
||||
// Remove deprecated fields
|
||||
_migrateRemoveDeprecated(item, updateData);
|
||||
|
||||
// Return the migrated update data
|
||||
return updateData;
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Migrate a single Scene entity to incorporate changes to the data model of it's actor data overrides
|
||||
* Return an Object of updateData to be applied
|
||||
* @param {Object} scene The Scene data to Update
|
||||
* @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;
|
||||
})
|
||||
};
|
||||
};
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Low level migration utilities
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Migrate the actor bonuses object
|
||||
* @private
|
||||
*/
|
||||
function _migrateActorBonuses(actor, updateData) {
|
||||
const b = game.system.model.Actor.character.bonuses;
|
||||
for ( let k of Object.keys(actor.data.bonuses || {}) ) {
|
||||
if ( k in b ) updateData[`data.bonuses.${k}`] = b[k];
|
||||
else updateData[`data.bonuses.-=${k}`] = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
|
||||
/**
|
||||
* A general migration to remove all fields from the data model which are flagged with a _deprecated tag
|
||||
* @private
|
||||
*/
|
||||
const _migrateRemoveDeprecated = function(ent, updateData) {
|
||||
const flat = flattenObject(ent.data);
|
||||
|
||||
// Identify objects to deprecate
|
||||
const toDeprecate = Object.entries(flat).filter(e => e[0].endsWith("_deprecated") && (e[1] === true)).map(e => {
|
||||
let parent = e[0].split(".");
|
||||
parent.pop();
|
||||
return parent.join(".");
|
||||
});
|
||||
|
||||
// Remove them
|
||||
for ( let k of toDeprecate ) {
|
||||
let parts = k.split(".");
|
||||
parts[parts.length-1] = "-=" + parts[parts.length-1];
|
||||
updateData[`data.${parts.join(".")}`] = null;
|
||||
}
|
||||
};
|
134
module/settings.js
Normal file
134
module/settings.js
Normal file
|
@ -0,0 +1,134 @@
|
|||
export const registerSystemSettings = function() {
|
||||
|
||||
/**
|
||||
* Track the system version upon which point a migration was last applied
|
||||
*/
|
||||
game.settings.register("sw5e", "systemMigrationVersion", {
|
||||
name: "System Migration Version",
|
||||
scope: "world",
|
||||
config: false,
|
||||
type: Number,
|
||||
default: 0
|
||||
});
|
||||
|
||||
/**
|
||||
* Register diagonal movement rule setting
|
||||
*/
|
||||
game.settings.register("sw5e", "diagonalMovement", {
|
||||
name: "SETTINGS.5eDiagN",
|
||||
hint: "SETTINGS.5eDiagL",
|
||||
scope: "world",
|
||||
config: true,
|
||||
default: "555",
|
||||
type: String,
|
||||
choices: {
|
||||
"555": "SETTINGS.5eDiagPHB",
|
||||
"5105": "SETTINGS.5eDiagDMG"
|
||||
},
|
||||
onChange: rule => canvas.grid.diagonalRule = rule
|
||||
});
|
||||
|
||||
/**
|
||||
* Register Initiative formula setting
|
||||
*/
|
||||
function _set5eInitiative(tiebreaker) {
|
||||
CONFIG.Combat.initiative.tiebreaker = tiebreaker;
|
||||
CONFIG.Combat.initiative.decimals = tiebreaker ? 2 : 0;
|
||||
if ( ui.combat && ui.combat._rendered ) ui.combat.render();
|
||||
}
|
||||
game.settings.register("sw5e", "initiativeDexTiebreaker", {
|
||||
name: "SETTINGS.5eInitTBN",
|
||||
hint: "SETTINGS.5eInitTBL",
|
||||
scope: "world",
|
||||
config: true,
|
||||
default: false,
|
||||
type: Boolean,
|
||||
onChange: enable => _set5eInitiative(enable)
|
||||
});
|
||||
_set5eInitiative(game.settings.get("sw5e", "initiativeDexTiebreaker"));
|
||||
|
||||
/**
|
||||
* Require Currency Carrying Weight
|
||||
*/
|
||||
game.settings.register("sw5e", "currencyWeight", {
|
||||
name: "SETTINGS.5eCurWtN",
|
||||
hint: "SETTINGS.5eCurWtL",
|
||||
scope: "world",
|
||||
config: true,
|
||||
default: true,
|
||||
type: Boolean
|
||||
});
|
||||
|
||||
/**
|
||||
* Option to disable XP bar for session-based or story-based advancement.
|
||||
*/
|
||||
game.settings.register("sw5e", "disableExperienceTracking", {
|
||||
name: "SETTINGS.5eNoExpN",
|
||||
hint: "SETTINGS.5eNoExpL",
|
||||
scope: "world",
|
||||
config: true,
|
||||
default: false,
|
||||
type: Boolean,
|
||||
});
|
||||
|
||||
/**
|
||||
* Option to automatically create Power Measured Template on roll
|
||||
*/
|
||||
game.settings.register("sw5e", "alwaysPlacePowerTemplate", {
|
||||
name: "SETTINGS.5eAutoPowerTemplateN",
|
||||
hint: "SETTINGS.5eAutoPowerTemplateL",
|
||||
scope: "client",
|
||||
config: true,
|
||||
default: false,
|
||||
type: Boolean
|
||||
});
|
||||
|
||||
/**
|
||||
* Option to automatically collapse Item Card descriptions
|
||||
*/
|
||||
game.settings.register("sw5e", "autoCollapseItemCards", {
|
||||
name: "SETTINGS.5eAutoCollapseCardN",
|
||||
hint: "SETTINGS.5eAutoCollapseCardL",
|
||||
scope: "client",
|
||||
config: true,
|
||||
default: false,
|
||||
type: Boolean,
|
||||
onChange: s => {
|
||||
ui.chat.render();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Option to allow GMs to restrict polymorphing to GMs only.
|
||||
*/
|
||||
game.settings.register('sw5e', 'allowPolymorphing', {
|
||||
name: 'SETTINGS.5eAllowPolymorphingN',
|
||||
hint: 'SETTINGS.5eAllowPolymorphingL',
|
||||
scope: 'world',
|
||||
config: true,
|
||||
default: false,
|
||||
type: Boolean
|
||||
});
|
||||
|
||||
/**
|
||||
* Remember last-used polymorph settings.
|
||||
*/
|
||||
game.settings.register('sw5e', 'polymorphSettings', {
|
||||
scope: 'client',
|
||||
default: {
|
||||
keepPhysical: false,
|
||||
keepMental: false,
|
||||
keepSaves: false,
|
||||
keepSkills: false,
|
||||
mergeSaves: false,
|
||||
mergeSkills: false,
|
||||
keepClass: false,
|
||||
keepFeats: false,
|
||||
keepPowers: false,
|
||||
keepItems: false,
|
||||
keepBio: false,
|
||||
keepVision: true,
|
||||
transformTokens: true
|
||||
}
|
||||
});
|
||||
};
|
25
module/templates.js
Normal file
25
module/templates.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
* Define a set of template paths to pre-load
|
||||
* Pre-loaded templates are compiled and cached for fast access when rendering
|
||||
* @return {Promise}
|
||||
*/
|
||||
export const preloadHandlebarsTemplates = async function() {
|
||||
|
||||
// Define template paths to load
|
||||
const templatePaths = [
|
||||
|
||||
// Actor Sheet Partials
|
||||
"systems/sw5e/templates/actors/parts/actor-traits.html",
|
||||
"systems/sw5e/templates/actors/parts/actor-inventory.html",
|
||||
"systems/sw5e/templates/actors/parts/actor-features.html",
|
||||
"systems/sw5e/templates/actors/parts/actor-powerbook.html",
|
||||
|
||||
// Item Sheet Partials
|
||||
"systems/sw5e/templates/items/parts/item-action.html",
|
||||
"systems/sw5e/templates/items/parts/item-activation.html",
|
||||
"systems/sw5e/templates/items/parts/item-description.html"
|
||||
];
|
||||
|
||||
// Load the template parts
|
||||
return loadTemplates(templatePaths);
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue