Migrating UI for new data structures

This commit is contained in:
Stefan Zermatten
2021-10-15 11:12:40 +02:00
parent f3c52999e8
commit ea68cdf86f
35 changed files with 511 additions and 271 deletions

View File

@@ -47,17 +47,17 @@ const damageProperty = new ValidatedMethod({
export function damagePropertyWork({property, operation, value}){
let damage, newValue;
if (operation === 'set'){
const currentValue = property.value;
const total = property.total;
// Set represents what we want the value to be after damage
// So we need the actual damage to get to that value
damage = currentValue - value;
damage = total - value;
// Damage can't exceed total value
if (damage > currentValue) damage = currentValue;
if (damage > total) damage = total;
// Damage must be positive
if (damage < 0) damage = 0;
newValue = property.total - damage;
} else if (operation === 'increment'){
let currentValue = property.value - (property.damage || 0);
let currentValue = property.value;
let currentDamage = property.damage;
let increment = value;
// Can't increase damage above the remaining value
@@ -74,6 +74,7 @@ export function damagePropertyWork({property, operation, value}){
}, {
selector: property
});
return damage;
}
export default damageProperty;

View File

@@ -25,7 +25,6 @@ const dealDamage = new ValidatedMethod({
// permissions
let creature = Creatures.findOne(creatureId, {
fields: {
damageMultipliers: 1,
owner: 1,
readers: 1,
writers: 1,
@@ -33,37 +32,42 @@ const dealDamage = new ValidatedMethod({
});
assertEditPermission(creature, this.userId);
// Get all the health bars and do damage to them
let healthBars = CreatureProperties.find({
'ancestors.id': creatureId,
type: 'attribute',
attributeType:'healthBar',
removed: {$ne: true},
inactive: {$ne: true},
}, {
sort: {order: -1},
});
let multiplier = creature.damageMultipliers[damageType];
if (multiplier === undefined) multiplier = 1;
let totalDamage = Math.floor(amount * multiplier);
let damageLeft = totalDamage;
if (damageType === 'healing') damageLeft = -totalDamage;
let propertyIds = [];
let propertiesDependedAponIds = [];
healthBars.forEach(healthBar => {
if (damageLeft === 0) return;
let damageAdded = damagePropertyWork({
property: healthBar,
operation: 'increment',
value: damageLeft,
});
damageLeft -= damageAdded;
propertyIds.push(healthBar._id);
propertiesDependedAponIds.push(...healthBar.dependencies);
});
const totalDamage = dealDamageWork({creature, damageType, amount})
computeCreature(creatureId);
return totalDamage;
},
});
export function dealDamageWork({creature, damageType, amount}){
console.log({damageType, amount})
// Get all the health bars and do damage to them
let healthBars = CreatureProperties.find({
'ancestors.id': creature._id,
type: 'attribute',
attributeType:'healthBar',
removed: {$ne: true},
inactive: {$ne: true},
}, {
sort: {order: -1},
});
//let multiplier = creature.damageMultipliers[damageType];
//if (multiplier === undefined) multiplier = 1;
//let totalDamage = Math.floor(amount * multiplier);
const totalDamage = amount;
let damageLeft = totalDamage;
if (damageType === 'healing') damageLeft = -totalDamage;
let propertyIds = [];
healthBars.forEach(healthBar => {
if (damageLeft === 0) return;
let damageAdded = damagePropertyWork({
property: healthBar,
operation: 'increment',
value: damageLeft,
});
damageLeft -= damageAdded;
propertyIds.push(healthBar._id);
});
return totalDamage;
}
export default dealDamage;

View File

@@ -10,10 +10,10 @@ export default function defaultCharacterProperties(creatureId){
{
type: 'propertySlot',
name: 'Ruleset',
description: 'Choose a starting point for your character, this will define the basic setup of your character sheet. Without a base, your sheet will be empty.',
description: {text: 'Choose a starting point for your character, this will define the basic setup of your character sheet. Without a base, your sheet will be empty.'},
slotTags: ['base'],
tags: [],
quantityExpected: 1,
quantityExpected: {calculation: '1'},
hideWhenFull: true,
spaceLeft: 1,
totalFilled: 0,

View File

@@ -121,6 +121,7 @@ export function insertCreatureLogWork({log, creature, method}){
if (typeof log === 'string'){
log = {content: [{value: log}]};
}
if (!log.content?.length) return;
log.date = new Date();
// Insert it
let id = CreatureLogs.insert(log);

View File

@@ -3,6 +3,7 @@ import adjustment from './applyPropertyByType/applyAdjustment.js';
import branch from './applyPropertyByType/applyBranch.js';
import buff from './applyPropertyByType/applyBuff.js';
import damage from './applyPropertyByType/applyDamage.js';
import note from './applyPropertyByType/applyNote.js';
import roll from './applyPropertyByType/applyRoll.js';
import savingThrow from './applyPropertyByType/applySavingThrow.js';
import toggle from './applyPropertyByType/applyToggle.js';
@@ -13,6 +14,7 @@ const applyPropertyByType = {
branch,
buff,
damage,
note,
roll,
savingThrow,
spell: action,

View File

@@ -9,15 +9,22 @@ import { damagePropertyWork } from '/imports/api/creature/creatureProperties/met
export default function applyAction(node, {creature, targets, scope, log}){
const prop = node.node;
if (prop.target === 'self') targets = [creature];
// Log the name and description
let content = { name: prop.name };
if (prop.description?.text){
recalculateInlineCalculations(prop.description, scope, log);
content.value = prop.description.value;
}
if (content.name || content.value){
log.content.push(content);
}
// Spend the resources
const failed = spendResources({prop, log, scope});
if (failed) return;
let content = { name: prop.name };
if (prop.summary?.text){
recalculateInlineCalculations(prop.summary, scope, log);
content.value = prop.summary.value;
}
log.content.push(content);
// Attack if there is an attack roll
if (prop.attackRoll && prop.attackRoll.calculation){
if (targets.length){
targets.forEach(target => {
@@ -29,6 +36,8 @@ export default function applyAction(node, {creature, targets, scope, log}){
applyAttackWithoutTarget({prop, scope, log});
applyChildren(node, {creature, targets, scope, log});
}
} else {
applyChildren(node, {creature, targets, scope, log});
}
}
@@ -39,18 +48,18 @@ function applyAttackWithoutTarget({prop, scope, log}){
delete scope['$criticalMiss'];
delete scope['$attackRoll'];
recalculateCalculation(prop.rollBonus, scope, log);
recalculateCalculation(prop.attackRoll, scope, log);
let value = rollDice(1, 20)[0];
scope['$attackRoll'] = {value};
let criticalHitTarget = scope.criticalHitTarget?.value || 20;
let criticalHit = value >= criticalHitTarget;
if (criticalHit) scope['$criticalHit'] = {value: true};
let result = value + prop.rollBonus.value;
let result = value + prop.attackRoll.value;
scope['$toHit'] = {value: result};
log.content.push({
name: criticalHit ? 'Critical Hit!' : 'To Hit',
value: `1d20 {${value}} + ${prop.rollBonus.value} = ` + result,
value: `1d20 [${value}] + ${prop.attackRoll.value} = ` + result,
});
}
@@ -62,7 +71,7 @@ function applyAttackToTarget({prop, target, scope, log}){
delete scope['$attackDiceRoll'];
delete scope['$attackRoll'];
recalculateCalculation(prop.rollBonus, scope, log);
recalculateCalculation(prop.attackRoll, scope, log);
const value = rollDice(1, 20)[0];
scope['$attackDiceRoll'] = {value};
@@ -71,7 +80,7 @@ function applyAttackToTarget({prop, target, scope, log}){
const criticalMiss = value === 1;
if (criticalHit) scope['$criticalHit'] = {value: true};
if (criticalMiss) scope['$criticalMiss'] = {value: true};
const result = value + prop.rollBonus.value;
const result = value + prop.attackRoll.value;
scope['$attackRoll'] = {value: result};
if (target.variables.armor){
const armor = target.variables.armor.value;
@@ -81,7 +90,7 @@ function applyAttackToTarget({prop, target, scope, log}){
'Miss!'
log.content.push({
name,
value: `1d20 {${value}} + ${prop.rollBonus.value} = ` + result,
value: `1d20 {${value}} + ${prop.attackRoll.value} = ` + result,
});
if ((result > armor) || (criticalHit)){
scope['$attackHit'] = true;
@@ -95,7 +104,7 @@ function applyAttackToTarget({prop, target, scope, log}){
});
log.content.push({
name: criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical miss!' : 'To Hit',
value: `1d20 {${value}} + ${prop.rollBonus.value} = ` + result,
value: `1d20 {${value}} + ${prop.attackRoll.value} = ` + result,
});
}
}
@@ -106,7 +115,7 @@ function applyChildren(node, args){
function spendResources({prop, log, scope}){
// Check Uses
if (prop.usesUsed >= prop.uses?.value){
if (prop.usesLeft < 0){
log.content.push({
name: 'Error',
value: `${prop.name || 'action'} does not have enough uses left`,
@@ -127,6 +136,7 @@ function spendResources({prop, log, scope}){
let gainLog = [];
try {
prop.resources.itemsConsumed.forEach(itemConsumed => {
recalculateCalculation(itemConsumed.quantity, scope, log);
if (!itemConsumed.itemId){
throw 'No ammo was selected for this prop';
}
@@ -166,7 +176,7 @@ function spendResources({prop, log, scope}){
itemQuantityAdjustments.forEach(adjustQuantityWork);
// Use uses
if (prop.usesResult){
if (prop.usesLeft){
CreatureProperties.update(prop._id, {
$inc: {usesUsed: 1}
}, {
@@ -174,24 +184,29 @@ function spendResources({prop, log, scope}){
});
log.content.push({
name: 'Uses left',
value: prop.usesResult - (prop.usesUsed || 0) - 1,
value: prop.usesLeft - (prop.usesUsed || 0) - 1,
});
}
// Damage stats
prop.resources.attributesConsumed.forEach(attConsumed => {
if (!attConsumed.quantity) return;
recalculateCalculation(attConsumed.quantity, scope, log);
if (!attConsumed.quantity?.value) return;
let stat = scope[attConsumed.variableName];
if (!stat) return;
if (!stat){
spendLog.push(stat.name + ': ' + ' not found');
return;
}
damagePropertyWork({
property: stat,
operation: 'increment',
value: attConsumed.quantity,
value: attConsumed.quantity.value,
});
if (attConsumed.quantity > 0){
spendLog.push(stat.name + ': ' + attConsumed.quantity);
} else if (attConsumed.quantity < 0){
gainLog.push(stat.name + ': ' + -attConsumed.quantity);
if (attConsumed.quantity.value > 0){
spendLog.push(stat.name + ': ' + attConsumed.quantity.value);
} else if (attConsumed.quantity.value < 0){
gainLog.push(stat.name + ': ' + -attConsumed.quantity.value);
}
});

View File

@@ -14,12 +14,8 @@ export default function applyAdjustment(node, {
// Evaluate the amount
recalculateCalculation(prop.amount, scope, log);
prop.amount.errors?.forEach(error => {
if (error.type !== 'info'){
log.content.push({name: 'Error', value: error.message});
}
});
const value = prop.amount.value;
const value = +prop.amount.value;
if (!isFinite(value)) {
return applyChildren(node, {creature, targets, scope, log});
}
@@ -32,12 +28,12 @@ export default function applyAdjustment(node, {
name: 'Error',
value: `Could not apply attribute damage, creature does not have \`${prop.stat}\` set`
});
return;
return applyChildren(node, {creature, targets, scope, log});
}
damagePropertyWork({
property: stat,
operation: prop.operation,
value,
value: value,
});
log.content.push({
name: 'Attribute damage',

View File

@@ -1,8 +1,8 @@
import applyProperty from '../applyProperty.js';
import dealDamage from '/imports/api/creature/creatureProperties/methods/dealDamage.js';
import { dealDamageWork } from '/imports/api/creature/creatureProperties/methods/dealDamage.js';
import {insertCreatureLog} from '/imports/api/creature/log/CreatureLogs.js';
import recalculateCalculation from './shared/recalculateCalculation.js';
import { Context } from '/imports/parser/resolve.js';
import resolve, { Context, toString } from '/imports/parser/resolve.js';
import logErrors from './shared/logErrors.js';
export default function applyDamage(node, {
creature, targets, scope, log
@@ -14,6 +14,12 @@ export default function applyDamage(node, {
};
const prop = node.node;
// Skip if there is no parse node to work with
if (!prop.amount.parseNode) return;
// Choose target
let damageTargets = prop.target === 'self' ? [creature] : targets;
// Determine if the hit is critical
let criticalHit = scope['$criticalHit']?.value &&
@@ -23,40 +29,66 @@ export default function applyDamage(node, {
let context = new Context({
options: {doubleRolls: criticalHit},
});
recalculateCalculation(prop.amount, scope, log, context);
// If we didn't end up with a finite amount, give up
if (!isFinite(prop.amount?.value)) return applyChildren();
// Gather all the lines we need to log into an array
const logValue = [];
const logName = prop.damageType === 'healing' ? 'Healing' : 'Damage';
// Compile the dice roll and store that string first
const {result: compiled} = resolve('compiled', prop.amount.parseNode, scope, context);
logValue.push(toString(compiled));
logErrors(context.errors, log);
// roll the dice only and store that string
const {result: rolled} = resolve('roll', prop.amount.parseNode, scope, context);
logValue.push(toString(rolled));
logErrors(context.errors, log);
// Reset the errors so we don't log the same errors twice
context.errors = [];
// Resolve the roll to a final value
const {result: reduced} = resolve('reduce', rolled, scope, context);
logErrors(context.errors, log);
// Store the result
if (reduced.parseType === 'constant'){
prop.amount.value = reduced.value;
} else if (reduced.parseType === 'error'){
prop.amount.value = null;
} else {
prop.amount.value = toString(reduced);
}
const damage = +reduced.value;
// If we didn't end up with a constant of finite amount, give up
if (reduced?.parseType !== 'constant' && !isFinite(reduced.value)){
return applyChildren();
}
// Memoise the damage suffix for the log
let suffix = (criticalHit ? ' critical ' : ' ') +
prop.damageType +
(prop.damageType !== ' healing ' ? ' damage ': '');
(prop.damageType !== 'healing' ? ' damage ': '');
if (damageTargets && damageTargets.length) {
// Iterate through all the targets
damageTargets.forEach(target => {
let name = prop.damageType === 'healing' ? 'Healing' : 'Damage';
// Deal the damage to the target
let damageDealt = dealDamage.call({
creatureId: target._id,
let damageDealt = dealDamageWork({
creature: target,
damageType: prop.damageType,
amount: prop.amount.value,
amount: damage,
});
// Log the damage done
if (target._id === creature._id){
// Target is same as self, log damage as such
log.content.push({
name,
value: damageDealt + suffix + ' to self',
});
logValue.push(damageDealt + suffix + ' to self');
} else {
log.content.push({
name,
value: 'Dealt ' + damageDealt + suffix + ` ${target.name && ' to '}${target.name}`,
});
logValue.push('Dealt ' + damageDealt + suffix + ` ${target.name && ' to '}${target.name}`);
// Log the damage received on that creature's log as well
insertCreatureLog.call({
log: {
@@ -71,10 +103,11 @@ export default function applyDamage(node, {
});
} else {
// There are no targets, just log the result
log.content.push({
name: prop.damageType === 'healing' ? 'Healing' : 'Damage',
value: prop.amount.value + suffix,
});
logValue.push(damage + suffix);
}
log.content.push({
name: logName,
value: logValue.join('\n'),
});
return applyChildren();
}

View File

@@ -0,0 +1,25 @@
import recalculateInlineCalculations from './shared/recalculateInlineCalculations.js';
import applyProperty from '../applyProperty.js';
export default function applyNote(node, {creature, targets, scope, log}){
const prop = node.node;
// Log Name, summary
let content = { name: prop.name };
if (prop.summary?.text){
recalculateInlineCalculations(prop.summary, scope, log);
content.value = prop.summary.value;
}
if (content.name || content.value){
log.content.push(content);
}
// Log description
if (prop.description?.text){
recalculateInlineCalculations(prop.description, scope, log);
log.content.push({value: prop.description.value});
}
// Apply children
node.children.forEach(child => applyProperty(child, {
creature, targets, scope, log
}));
}

View File

@@ -5,14 +5,14 @@ export default function applyRoll(node, {creature, targets, scope, log}){
const prop = node.node;
if (prop.roll?.calculation){
recalculateCalculation(prop.roll, scope, log, context);
recalculateCalculation(prop.roll, scope, log);
if (isFinite(prop.roll.value)){
scope[prop.variableName] = prop.roll.value;
}
log.content.push({
name: prop.name,
value: prop.variableName + ' = ' + prop.roll + ' = ' + prop.roll.value,
value: prop.variableName + ' = ' + prop.roll.calculation + ' = ' + prop.roll.value,
});
}
return node.children.forEach(child => applyProperty(child, {

View File

@@ -3,11 +3,11 @@ import recalculateCalculation from './shared/recalculateCalculation.js';
import applyProperty from '../applyProperty.js';
export default function applySavingThrow(node, {creature, targets, scope, log}){
let saveTargets = prop.target === 'self' ? [creature] : targets;
const prop = node.node;
recalculateCalculation(prop.dc, scope, log, context);
let saveTargets = prop.target === 'self' ? [creature] : targets;
recalculateCalculation(prop.dc, scope, log);
const dc = (prop.dc?.value);
if (!isFinite(dc)){

View File

@@ -2,7 +2,7 @@ import evaluateCalculation from '/imports/api/engine/computation/utility/evaluat
import logErrors from './logErrors.js';
export default function recalculateCalculation(calc, scope, log, context){
if (!calc.parseNode) return;
if (!calc?.parseNode) return;
calc._parseLevel = 'reduce';
evaluateCalculation(calc, scope, context);
logErrors(calc.errors, log);

View File

@@ -72,9 +72,9 @@ const doAction = new ValidatedMethod({
doActionWork({creature, targets, properties, ancestors, method: this});
// Recompute all involved creatures
Meteor.defer(() => computeCreature(creature._id));
computeCreature(creature._id);
targets.forEach(target => {
Meteor.defer(() => computeCreature(target._id));
computeCreature(target._id);
});
},
});

View File

@@ -0,0 +1,11 @@
import '/imports/api/simpleSchemaConfig.js';
//import testTypes from './testTypes/index.js';
import { doActionWork } from './doAction.js';
import createAction from './tests/createAction.testFn.js';
describe('Do Action', function(){
it('Does an empty action', function(){
doActionWork(createAction({properties: [{type: 'action'}]}));
});
//testTypes.forEach(test => it(test.text, test.fn));
});

View File

@@ -0,0 +1,26 @@
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import Creatures from '/imports/api/creature/creatures/Creatures.js';
export default function createAction({
creature = {_id: 'creatureId'},
targets = [],
properties = [],
ancestors = [],
method
} = {}){
properties = properties.map(cleanProp);
ancestors = ancestors.map(cleanProp);
creature = cleanCreature(creature);
ancestors = ancestors.map(cleanCreature);
return {creature, targets, properties, ancestors, method};
}
function cleanProp(prop){
let schema = CreatureProperties.simpleSchema(prop);
return schema.clean(prop);
}
function cleanCreature(creature){
let schema = Creatures.simpleSchema(creature);
return schema.clean(creature);
}

View File

@@ -0,0 +1,6 @@
import applyAction from './applyAction.testFn.js';
export default [{
text: 'Applies actions',
fn: applyAction,
},];

View File

@@ -3,9 +3,9 @@ import walkDown from '/imports/api/engine/computation/utility/walkdown.js';
export default function computeInactiveStatus(node){
const prop = node.node;
if (isActive(prop)) return;
// Unequipped items disable their children, but are not disabled themselves
// All notes do the same
if (prop.type !== 'item' && prop.type !== 'note' ){
// Unequipped items, notes, and actions disable their children,
// but are not disabled themselves
if (prop.type !== 'item' && prop.type !== 'note' && prop.type !== 'action' ){
prop.inactive = true;
prop.deactivatedBySelf = true;
}
@@ -23,6 +23,7 @@ function isActive(prop){
case 'item': return !!prop.equipped;
case 'spell': return !!prop.prepared || !!prop.alwaysPrepared;
case 'note': return false;
case 'action': return false;
default: return true;
}
}

View File

@@ -1,7 +1,7 @@
import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX.js';
import { prettifyParseError, parse } from '/imports/parser/parser.js';
import applyFnToKey from '/imports/api/engine/computation/utility/applyFnToKey.js';
import { get } from 'lodash';
import { get, unset } from 'lodash';
import errorNode from '/imports/parser/parseTree/error.js';
import cyrb53 from '/imports/api/engine/computation/utility/cyrb53.js';
@@ -53,6 +53,11 @@ function parseAllCalculationFields(prop, schemas){
applyFnToKey(prop, calcKey, (prop, key) => {
const calcObj = get(prop, key);
if (!calcObj) return;
// Delete the whole calculation object if the calculation string isn't set
if (!calcObj.calculation){
unset(prop, calcKey);
return;
}
// Store a reference to all the calculations
prop._computationDetails.calculations.push(calcObj);
// Store the level to compute down to later
@@ -64,12 +69,6 @@ function parseAllCalculationFields(prop, schemas){
}
function parseCalculation(calcObj){
// If there is no calculation clear the cached parse node and error
if (!calcObj.calculation){
delete calcObj.hash;
delete calcObj.parseError;
return;
}
const calcHash = cyrb53(calcObj.calculation);
// If the cached parse calculation is equal to the calculation, skip
if (calcHash === calcObj.hash){

View File

@@ -2,6 +2,9 @@ export default function computeAction(computation, node){
const prop = node.data;
if (prop.uses){
prop.usesLeft = prop.uses.value - (prop.usesUsed || 0);
if (!prop.usesLeft){
prop.insufficientResources = true;
}
}
computeResources(computation, node);
if (!prop.resources) return;

View File

@@ -14,7 +14,7 @@ export default function(){
assert.equal(prop.usesLeft, 2);
const rolled = computation.propsById['rolledDescriptionId'];
assert.equal(rolled.summary.value, 'test roll gets compiled 1d4 + 4 properly');
assert.equal(rolled.summary.value, 'test roll gets compiled d4 + 4 properly');
const itemConsumed = prop.resources.itemsConsumed[0];
assert.equal(itemConsumed.quantity.value, 3);
@@ -26,7 +26,6 @@ export default function(){
const attConsumed = prop.resources.attributesConsumed[0];
assert.equal(attConsumed.quantity.value, 4);
assert.equal(attConsumed.available, 9);
assert.equal(attConsumed.statId, 'resourceVarId');
assert.equal(attConsumed.statName, 'Resource Var');
}