Added custom sheet events

Made rest buttons optional
This commit is contained in:
Stefan Zermatten
2022-11-08 23:01:09 +02:00
parent 60b21c1901
commit 1ec29365cb
17 changed files with 481 additions and 275 deletions

View File

@@ -18,6 +18,11 @@ let CreatureSettingsSchema = new SimpleSchema({
type: Boolean,
optional: true,
},
//hide rest buttons
hideRestButtons: {
type: Boolean,
optional: true,
},
// Swap around the modifier and stat
swapStatAndModifier: {
type: Boolean,

View File

@@ -49,7 +49,7 @@ const restCreature = new ValidatedMethod({
applyTriggers(afterTriggers, null, actionContext);
// Insert log
actionContext.writeLog();
actionContext.writeLog();
},
});
@@ -57,88 +57,123 @@ function doRestWork(restType, actionContext) {
const creatureId = actionContext.creature._id;
// Long rests reset short rest properties as well
let resetFilter;
if (restType === 'shortRest'){
if (restType === 'shortRest') {
resetFilter = 'shortRest'
} else {
resetFilter = {$in: ['shortRest', 'longRest']}
resetFilter = { $in: ['shortRest', 'longRest'] }
}
resetProperties(creatureId, resetFilter, actionContext);
// Reset half hit dice on a long rest, starting with the highest dice
if (restType === 'longRest') {
resetHitDice(creatureId, actionContext);
}
}
export function resetProperties(creatureId, resetFilter, actionContext) {
// Only apply to active properties
let filter = {
const filter = {
'ancestors.id': creatureId,
reset: resetFilter,
removed: { $ne: true },
inactive: { $ne: true },
};
// update all attribute's damage
filter.type = 'attribute';
CreatureProperties.update(filter, {
const attributeFilter = {
...filter,
type: 'attribute',
damage: { $ne: 0 },
}
CreatureProperties.find(attributeFilter, {
fields: { name: 1, damage: 1 }
}).forEach(prop => {
actionContext.addLog({
name: prop.name,
value: prop.damage >= 0 ? `Restored ${prop.damage}` : `Removed ${-prop.damage}`
});
});
CreatureProperties.update(attributeFilter, {
$set: {
damage: 0,
dirty: true,
}
}, {
selector: {type: 'attribute'},
selector: { type: 'attribute' },
multi: true,
});
// Update all action-like properties' usesUsed
filter.type = {$in: [
'action',
'attack',
'spell'
]};
CreatureProperties.update(filter, {
const actionFilter = {
...filter,
type: {
$in: ['action', 'spell']
},
usesUsed: { $ne: 0 },
};
CreatureProperties.find(actionFilter, {
fields: { name: 1, usesUsed: 1 }
}).forEach(prop => {
actionContext.addLog({
name: prop.name,
value: prop.usesUsed >= 0 ? `Restored ${prop.usesUsed} uses` : `Removed ${-prop.usesUsed} uses`
});
});
CreatureProperties.update(actionFilter, {
$set: {
usesUsed: 0,
dirty: true,
}
}, {
selector: {type: 'action'},
selector: { type: 'action' },
multi: true,
});
// Reset half hit dice on a long rest, starting with the highest dice
if (restType === 'longRest'){
let hitDice = CreatureProperties.find({
'ancestors.id': creatureId,
type: 'attribute',
attributeType: 'hitDice',
removed: {$ne: true},
inactive: {$ne: true},
}, {
fields: {
hitDiceSize: 1,
damage: 1,
total: 1,
}
function resetHitDice(creatureId, actionContext) {
let hitDice = CreatureProperties.find({
'ancestors.id': creatureId,
type: 'attribute',
attributeType: 'hitDice',
removed: { $ne: true },
inactive: { $ne: true },
}, {
fields: {
hitDiceSize: 1,
damage: 1,
total: 1,
}
}).fetch();
// Use a collator to do sorting in natural order
let collator = new Intl.Collator('en', {
numeric: true, sensitivity: 'base'
});
// Get the hit dice in decending order of hitDiceSize
let compare = (a, b) => collator.compare(b.hitDiceSize, a.hitDiceSize)
hitDice.sort(compare);
// Get the total number of hit dice that can be recovered this rest
let totalHd = hitDice.reduce((sum, hd) => sum + (hd.total || 0), 0);
let resetMultiplier = actionContext.creature.settings.hitDiceResetMultiplier || 0.5;
let recoverableHd = Math.max(Math.floor(totalHd * resetMultiplier), 1);
// recover each hit dice in turn until the recoverable amount is used up
let amountToRecover, resultingDamage;
hitDice.forEach(hd => {
if (!recoverableHd) return;
amountToRecover = Math.min(recoverableHd, hd.damage || 0);
if (!amountToRecover) return;
recoverableHd -= amountToRecover;
resultingDamage = hd.damage - amountToRecover;
actionContext.addLog({
name: hd.name,
value: amountToRecover >= 0 ? `Restored ${amountToRecover} hit dice` : `Removed ${-amountToRecover} hit dice`
});
CreatureProperties.update(hd._id, {
$set: {
damage: resultingDamage,
dirty: true,
}
}).fetch();
// Use a collator to do sorting in natural order
let collator = new Intl.Collator('en', {
numeric: true, sensitivity: 'base'
}, {
selector: { type: 'attribute' },
});
// Get the hit dice in decending order of hitDiceSize
let compare = (a, b) => collator.compare(b.hitDiceSize, a.hitDiceSize)
hitDice.sort(compare);
// Get the total number of hit dice that can be recovered this rest
let totalHd = hitDice.reduce((sum, hd) => sum + (hd.total || 0), 0);
let resetMultiplier = actionContext.creature.settings.hitDiceResetMultiplier || 0.5;
let recoverableHd = Math.max(Math.floor(totalHd*resetMultiplier), 1);
// recover each hit dice in turn until the recoverable amount is used up
let amountToRecover, resultingDamage;
hitDice.forEach(hd => {
if (!recoverableHd) return;
amountToRecover = Math.min(recoverableHd, hd.damage || 0);
if (!amountToRecover) return;
recoverableHd -= amountToRecover;
resultingDamage = hd.damage - amountToRecover;
CreatureProperties.update(hd._id, {
$set: {
damage: resultingDamage,
dirty: true,
}
}, {
selector: {type: 'attribute'},
});
});
}
});
}
export default restCreature;

View File

@@ -7,6 +7,7 @@ import { adjustQuantityWork } from '/imports/api/creature/creatureProperties/met
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
import { resetProperties } from '/imports/api/creature/creatures/methods/restCreature.js';
export default function applyAction(node, actionContext) {
applyNodeTriggers(node, 'before', actionContext);
@@ -16,7 +17,7 @@ export default function applyAction(node, actionContext) {
// Log the name and summary
let content = { name: prop.name };
if (prop.summary?.text){
if (prop.summary?.text) {
recalculateInlineCalculations(prop.summary, actionContext);
content.value = prop.summary.value;
}
@@ -29,24 +30,27 @@ export default function applyAction(node, actionContext) {
const attack = prop.attackRoll || prop.attackRollBonus;
// Attack if there is an attack roll
if (attack && attack.calculation){
if (targets.length){
if (attack && attack.calculation) {
if (targets.length) {
targets.forEach(target => {
applyAttackToTarget({attack, target, actionContext});
applyAttackToTarget({ attack, target, actionContext });
// Apply the children, but only to the current target
actionContext.targets = [target];
applyChildren(node, actionContext);
});
} else {
applyAttackWithoutTarget({attack, actionContext});
applyAttackWithoutTarget({ attack, actionContext });
applyChildren(node, actionContext);
}
} else {
applyChildren(node, actionContext);
}
if (prop.actionType === 'event' && prop.variableName) {
resetProperties(actionContext.creature._id, prop.variableName, actionContext);
}
}
function applyAttackWithoutTarget({attack, actionContext}){
function applyAttackWithoutTarget({ attack, actionContext }) {
delete actionContext.scope['$attackHit'];
delete actionContext.scope['$attackMiss'];
delete actionContext.scope['$criticalHit'];
@@ -62,16 +66,16 @@ function applyAttackWithoutTarget({attack, actionContext}){
criticalMiss,
} = rollAttack(attack, scope);
let name = criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit';
if (scope['$attackAdvantage'] === 1){
if (scope['$attackAdvantage'] === 1) {
name += ' (Advantage)';
} else if(scope['$attackAdvantage'] === -1){
} else if (scope['$attackAdvantage'] === -1) {
name += ' (Disadvantage)';
}
if (!criticalMiss){
scope['$attackHit'] = {value: true}
if (!criticalMiss) {
scope['$attackHit'] = { value: true }
}
if (!criticalHit){
scope['$attackMiss'] = {value: true};
if (!criticalHit) {
scope['$attackMiss'] = { value: true };
}
actionContext.addLog({
@@ -81,7 +85,7 @@ function applyAttackWithoutTarget({attack, actionContext}){
});
}
function applyAttackToTarget({attack, target, actionContext}){
function applyAttackToTarget({ attack, target, actionContext }) {
const scope = actionContext.scope;
delete scope['$attackHit'];
delete scope['$attackMiss'];
@@ -99,15 +103,15 @@ function applyAttackToTarget({attack, target, actionContext}){
criticalMiss,
} = rollAttack(attack, scope);
if (target.variables.armor){
if (target.variables.armor) {
const armor = target.variables.armor.value;
let name = criticalHit ? 'Critical Hit!' :
criticalMiss ? 'Critical Miss!' :
result > armor ? 'Hit!' : 'Miss!';
if (scope['$attackAdvantage'] === 1){
result > armor ? 'Hit!' : 'Miss!';
if (scope['$attackAdvantage'] === 1) {
name += ' (Advantage)';
} else if(scope['$attackAdvantage'] === -1){
} else if (scope['$attackAdvantage'] === -1) {
name += ' (Disadvantage)';
}
@@ -116,15 +120,15 @@ function applyAttackToTarget({attack, target, actionContext}){
value: `${resultPrefix}\n**${result}**`,
inline: true,
});
if (criticalMiss || result < armor){
scope['$attackMiss'] = {value: true};
if (criticalMiss || result < armor) {
scope['$attackMiss'] = { value: true };
} else {
scope['$attackHit'] = {value: true};
scope['$attackHit'] = { value: true };
}
} else {
actionContext.addLog({
name: 'Error',
value:'Target has no `armor`',
value: 'Target has no `armor`',
});
actionContext.addLog({
name: criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit',
@@ -134,10 +138,10 @@ function applyAttackToTarget({attack, target, actionContext}){
}
}
function rollAttack(attack, scope){
function rollAttack(attack, scope) {
const rollModifierText = numberToSignedString(attack.value, true);
let value, resultPrefix;
if (scope['$attackAdvantage'] === 1){
if (scope['$attackAdvantage'] === 1) {
const [a, b] = rollDice(2, 20);
if (a >= b) {
value = a;
@@ -146,7 +150,7 @@ function rollAttack(attack, scope){
value = b;
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText}`;
}
} else if (scope['$attackAdvantage'] === -1){
} else if (scope['$attackAdvantage'] === -1) {
const [a, b] = rollDice(2, 20);
if (a <= b) {
value = a;
@@ -159,25 +163,25 @@ function rollAttack(attack, scope){
value = rollDice(1, 20)[0];
resultPrefix = `1d20 [${value}] ${rollModifierText}`
}
scope['$attackRoll'] = {value};
scope['$attackRoll'] = { value };
const result = value + attack.value;
const {criticalHit, criticalMiss} = applyCrits(value, scope);
return {resultPrefix, result, value, criticalHit, criticalMiss};
const { criticalHit, criticalMiss } = applyCrits(value, scope);
return { resultPrefix, result, value, criticalHit, criticalMiss };
}
function applyCrits(value, scope){
function applyCrits(value, scope) {
let criticalHitTarget = scope.criticalHitTarget?.value || 20;
let criticalHit = value >= criticalHitTarget;
let criticalMiss;
if (criticalHit){
scope['$criticalHit'] = {value: true};
if (criticalHit) {
scope['$criticalHit'] = { value: true };
} else {
criticalMiss = value === 1;
if (criticalMiss){
scope['$criticalMiss'] = {value: true};
if (criticalMiss) {
scope['$criticalMiss'] = { value: true };
}
}
return {criticalHit, criticalMiss};
return { criticalHit, criticalMiss };
}
function applyChildren(node, actionContext) {
@@ -185,9 +189,9 @@ function applyChildren(node, actionContext) {
node.children.forEach(child => applyProperty(child, actionContext));
}
function spendResources(prop, actionContext){
function spendResources(prop, actionContext) {
// Check Uses
if (prop.usesLeft <= 0){
if (prop.usesLeft <= 0) {
if (!prop.silent) actionContext.addLog({
name: 'Error',
value: `${prop.name || 'action'} does not have enough uses left`,
@@ -195,7 +199,7 @@ function spendResources(prop, actionContext){
return true;
}
// Resources
if (prop.insufficientResources){
if (prop.insufficientResources) {
if (!prop.silent) actionContext.addLog({
name: 'Error',
value: 'This creature doesn\'t have sufficient resources to perform this action',
@@ -209,14 +213,14 @@ function spendResources(prop, actionContext){
try {
prop.resources.itemsConsumed.forEach(itemConsumed => {
recalculateCalculation(itemConsumed.quantity, actionContext);
if (!itemConsumed.itemId){
if (!itemConsumed.itemId) {
throw 'No ammo was selected for this prop';
}
let item = CreatureProperties.findOne(itemConsumed.itemId);
if (!item || item.ancestors[0].id !== prop.ancestors[0].id){
if (!item || item.ancestors[0].id !== prop.ancestors[0].id) {
throw 'The prop\'s ammo was not found on the creature';
}
if (!item.equipped){
if (!item.equipped) {
throw 'The selected ammo is not equipped';
}
if (
@@ -229,16 +233,16 @@ function spendResources(prop, actionContext){
value: itemConsumed.quantity.value,
});
let logName = item.name;
if (itemConsumed.quantity.value > 1 || itemConsumed.quantity.value < -1){
if (itemConsumed.quantity.value > 1 || itemConsumed.quantity.value < -1) {
logName = item.plural || logName;
}
if (itemConsumed.quantity.value > 0){
if (itemConsumed.quantity.value > 0) {
spendLog.push(logName + ': ' + itemConsumed.quantity.value);
} else if (itemConsumed.quantity.value < 0){
} else if (itemConsumed.quantity.value < 0) {
gainLog.push(logName + ': ' + -itemConsumed.quantity.value);
}
});
} catch (e){
} catch (e) {
actionContext.addLog({
name: 'Error',
value: e,
@@ -251,9 +255,9 @@ function spendResources(prop, actionContext){
itemQuantityAdjustments.forEach(adjustQuantityWork);
// Use uses
if (prop.usesLeft){
if (prop.usesLeft) {
CreatureProperties.update(prop._id, {
$inc: {usesUsed: 1}
$inc: { usesUsed: 1 }
}, {
selector: prop
});
@@ -270,7 +274,7 @@ function spendResources(prop, actionContext){
if (!attConsumed.quantity?.value) return;
let stat = actionContext.scope[attConsumed.variableName];
if (!stat){
if (!stat) {
spendLog.push(stat.name + ': ' + ' not found');
return;
}
@@ -280,9 +284,9 @@ function spendResources(prop, actionContext){
value: attConsumed.quantity.value,
actionContext,
});
if (attConsumed.quantity.value > 0){
if (attConsumed.quantity.value > 0) {
spendLog.push(stat.name + ': ' + attConsumed.quantity.value);
} else if (attConsumed.quantity.value < 0){
} else if (attConsumed.quantity.value < 0) {
gainLog.push(stat.name + ': ' + -attConsumed.quantity.value);
}
});

View File

@@ -2,6 +2,7 @@ import SimpleSchema from 'simpl-schema';
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
import { storedIconsSchema } from '/imports/api/icons/Icons.js';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
/*
* Actions are things a character can do
@@ -24,9 +25,17 @@ let ActionSchema = createPropertySchema({
// long actions take longer than 1 round to cast
actionType: {
type: String,
allowedValues: ['action', 'bonus', 'attack', 'reaction', 'free', 'long'],
allowedValues: ['action', 'bonus', 'attack', 'reaction', 'free', 'long', 'event'],
defaultValue: 'action',
},
// If the action type is an event, what is the variable name of that event?
variableName: {
type: String,
optional: true,
regEx: VARIABLE_NAME_REGEX,
min: 2,
max: STORAGE_LIMITS.variableName,
},
// Who is the action directed at
target: {
type: String,
@@ -56,8 +65,10 @@ let ActionSchema = createPropertySchema({
// How this action's uses are reset automatically
reset: {
type: String,
allowedValues: ['longRest', 'shortRest'],
optional: true,
regEx: VARIABLE_NAME_REGEX,
min: 2,
max: STORAGE_LIMITS.variableName,
},
// Resources
resources: {
@@ -74,7 +85,7 @@ let ActionSchema = createPropertySchema({
'resources.itemsConsumed.$._id': {
type: String,
regEx: SimpleSchema.RegEx.Id,
autoValue(){
autoValue() {
if (!this.isSet) return Random.id();
}
},
@@ -101,7 +112,7 @@ let ActionSchema = createPropertySchema({
'resources.attributesConsumed.$._id': {
type: String,
regEx: SimpleSchema.RegEx.Id,
autoValue(){
autoValue() {
if (!this.isSet) return Random.id();
}
},
@@ -218,4 +229,4 @@ const ComputedActionSchema = new SimpleSchema()
.extend(ActionSchema)
.extend(ComputedOnlyActionSchema);
export { ActionSchema, ComputedOnlyActionSchema, ComputedActionSchema};
export { ActionSchema, ComputedOnlyActionSchema, ComputedActionSchema };

View File

@@ -129,7 +129,9 @@ let AttributeSchema = createPropertySchema({
reset: {
type: String,
optional: true,
allowedValues: ['shortRest', 'longRest'],
regEx: VARIABLE_NAME_REGEX,
min: 2,
max: STORAGE_LIMITS.variableName,
},
});