Merge branch 'develop' of https://github.com/ThaumRystra/DiceCloud into develop

This commit is contained in:
Thaum Rystra
2024-05-07 11:03:26 +02:00
82 changed files with 887 additions and 1054 deletions

View File

@@ -45,7 +45,7 @@ geojson-utils@1.0.11
google-oauth@1.4.4
hot-code-push@1.0.4
html-tools@1.1.4
htmljs@1.2.0
htmljs@1.2.1
http@2.0.0
id-map@1.1.1
inter-process-messaging@0.1.1
@@ -72,7 +72,7 @@ mobile-status-bar@1.1.0
modern-browsers@0.1.10
modules@0.20.0
modules-runtime@0.13.1
mongo@1.16.8
mongo@1.16.9
mongo-decimal@0.1.3
mongo-dev-server@1.1.0
mongo-id@1.0.8
@@ -80,7 +80,7 @@ npm-mongo@4.17.2
oauth@2.2.1
oauth2@1.3.2
ordered-dict@1.1.0
ostrio:cookies@2.8.0
ostrio:cookies@2.8.1
ostrio:files@2.3.3
patreon-oauth@0.1.0
peerlibrary:assert@0.3.0
@@ -112,7 +112,7 @@ shell-server@0.5.0
simple:json-routes@2.3.1
simple:rest@1.2.1
simple:rest-bearer-token-parser@1.1.1
simple:rest-json-error-handler@1.1.1
simple:rest-json-error-handler@1.1.3
simple:rest-method-mixin@1.1.0
socket-stream-client@0.5.2
spacebars-compiler@1.3.2
@@ -128,4 +128,4 @@ webapp@1.13.8
webapp-hashing@1.1.1
zer0th:meteor-vuetify-loader@0.1.41
zodern:fix-async-stubs@1.0.2
zodern:types@1.0.11
zodern:types@1.0.13

View File

@@ -5,8 +5,8 @@
@font-face {
font-family: "game-icons";
src: url("/fonts/game-icons.eot?817af6e52c83163eb30ece54d9f7d16d?#iefix") format("embedded-opentype"),
url("/fonts/game-icons.woff?817af6e52c83163eb30ece54d9f7d16d") format("woff"),
url("/fonts/game-icons.ttf?817af6e52c83163eb30ece54d9f7d16d") format("truetype");
url("/fonts/game-icons.woff?817af6e52c83163eb30ece54d9f7d16d") format("woff"),
url("/fonts/game-icons.ttf?817af6e52c83163eb30ece54d9f7d16d") format("truetype");
}
.game-icon {

View File

@@ -22,7 +22,7 @@ const insertCreatureFolder = new ValidatedMethod({
owner: userId
}, {
fields: { order: 1 },
sort: { order: -1 }
sort: { left: -1 }
});
if (existingFolders.count() >= 50) {
throw new Meteor.Error('creatureFolders.methods.insert.denied',

View File

@@ -31,7 +31,7 @@ const reorderCreatureFolder = new ValidatedMethod({
owner: userId
}, {
fields: { order: 1, },
sort: { order: -1 }
sort: { left: -1 }
}).forEach((folder, index) => {
if (folder.order !== index) {
CreatureFolders.update(_id, { $set: { order: index } })

View File

@@ -55,7 +55,7 @@ const duplicateProperty = new ValidatedMethod({
removed: { $ne: true },
}, {
limit: DUPLICATE_CHILDREN_LIMIT + 1,
sort: { order: 1 },
sort: { left: 1 },
}).fetch();
// Alert the user if the limit was hit

View File

@@ -8,7 +8,7 @@ export default function getParentRefByTag(creatureId, tag) {
inactive: { $ne: true },
tags: tag,
}, {
sort: { order: 1 },
sort: { left: 1 },
});
return prop && { id: prop._id, collection: 'creatureProperties' };
}

View File

@@ -1,6 +1,5 @@
import '/imports/api/creature/creatureProperties/methods/adjustQuantity';
import '/imports/api/creature/creatureProperties/methods/copyPropertyToLibrary';
import '/imports/api/creature/creatureProperties/methods/damageProperty';
import '/imports/api/creature/creatureProperties/methods/duplicateProperty';
import '/imports/api/creature/creatureProperties/methods/equipItem';
import '/imports/api/creature/creatureProperties/methods/insertProperty';

View File

@@ -159,7 +159,7 @@ function reifyNodeReferences(nodes, visitedRefs = new Set(), depth = 0) {
...getFilter.descendants(referencedNode),
removed: { $ne: true },
}, {
sort: { order: 1 },
sort: { left: 1 },
}).fetch();
// We are adding the referenced node and its descendants

View File

@@ -23,13 +23,7 @@ const restoreProperty = new ValidatedMethod({
assertEditPermission(rootCreature, this.userId);
// Do work
restore({
_id,
collection: CreatureProperties,
extraUpdates: {
$set: { dirty: true }
},
});
restore(CreatureProperties, property, { $set: { dirty: true } });
}
});

View File

@@ -23,7 +23,7 @@ const softRemoveProperty = new ValidatedMethod({
assertEditPermission(rootCreature, this.userId);
// Do work
softRemove({ _id, collection: CreatureProperties });
softRemove(CreatureProperties, property);
}
});

View File

@@ -1,5 +1,4 @@
import '/imports/api/creature/creatures/methods/insertCreature';
import '/imports/api/creature/creatures/methods/removeCreature';
import '/imports/api/creature/creatures/methods/restCreature';
import '/imports/api/creature/creatures/methods/updateCreature';
import '/imports/api/creature/creatures/methods/changeAllowedLibraries';

View File

@@ -1,169 +0,0 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions';
import { union } from 'lodash';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty';
import { getFilter } from '/imports/api/parenting/parentingFunctions';
const restCreature = new ValidatedMethod({
name: 'creature.methods.rest',
validate: new SimpleSchema({
creatureId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
},
restType: {
type: String,
allowedValues: ['shortRest', 'longRest'],
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 5,
timeInterval: 5000,
},
run({ creatureId, restType }) {
// Get action context
const actionContext = new ActionContext(creatureId, [creatureId], this);
// Check permissions
assertEditPermission(actionContext.creature, this.userId);
// Join, sort, and apply before triggers
const beforeTriggers = union(
actionContext.triggers.anyRest?.before, actionContext.triggers[restType]?.before
).sort((a, b) => a.order - b.order);
applyTriggers(beforeTriggers, null, actionContext);
// Rest
actionContext.addLog({
name: restType === 'shortRest' ? 'Short rest' : 'Long rest',
});
doRestWork(restType, actionContext);
// Join, sort, and apply after triggers
const afterTriggers = union(
actionContext.triggers.anyRest?.after, actionContext.triggers[restType]?.after
).sort((a, b) => a.order - b.order);
applyTriggers(afterTriggers, null, actionContext);
// Insert log
actionContext.writeLog();
},
});
function doRestWork(restType, actionContext) {
const creatureId = actionContext.creature._id;
// Long rests reset short rest properties as well
let resetFilter;
if (restType === 'shortRest') {
resetFilter = 'shortRest'
} else {
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
const filter = {
...getFilter.descendantsOfRoot(creatureId),
reset: resetFilter,
removed: { $ne: true },
inactive: { $ne: true },
};
// update all attribute's damage
const attributeFilter = {
...filter,
type: 'attribute',
damage: { $nin: [0, undefined] },
}
CreatureProperties.find(attributeFilter).forEach(prop => {
damagePropertyWork({
prop,
operation: 'increment',
value: -prop.damage ?? 0,
actionContext,
logFunction(increment) {
actionContext.addLog({
name: prop.name,
value: increment < 0 ? `Restored ${-increment}` : `Removed ${-increment}`
});
}
});
});
// Update all action-like properties' usesUsed
const actionFilter = {
...filter,
type: {
$in: ['action', 'spell']
},
usesUsed: { $nin: [0, undefined] },
};
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' },
multi: true,
});
}
function resetHitDice(creatureId, actionContext) {
let hitDice = CreatureProperties.find({
...getFilter.descendantsOfRoot(creatureId),
type: 'attribute',
attributeType: 'hitDice',
removed: { $ne: true },
inactive: { $ne: true },
}).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;
hitDice.forEach(hd => {
if (!recoverableHd) return;
amountToRecover = Math.min(recoverableHd, hd.damage ?? 0);
if (!amountToRecover) return;
recoverableHd -= amountToRecover;
damagePropertyWork({
prop: hd,
operation: 'increment',
value: -amountToRecover,
actionContext,
logFunction(increment) {
actionContext.addLog({
name: hd.name,
value: increment < 0 ? `Restored ${-increment} hit dice` : `Removed ${increment} hit dice`
});
}
});
});
}
export default restCreature;

View File

@@ -103,7 +103,7 @@ const insertDoc = new ValidatedMethod({
doc.parentId = parentId;
const lastOrder = Docs.find({}, { sort: { order: -1 } }).fetch()[0]?.order || 0;
const lastOrder = Docs.find({}, { sort: { left: -1 } }).fetch()[0]?.order || 0;
doc.order = lastOrder + 1;
doc.urlName = 'new-doc-' + (lastOrder + 1);

View File

@@ -10,7 +10,7 @@ export interface EngineAction {
_stepThrough?: boolean;
_decisions?: any[],
creatureId: string;
rootPropId: string;
rootPropId?: string;
targetIds?: string[];
results: TaskResult[];
taskCount: number;

View File

@@ -13,6 +13,7 @@ import numberToSignedString from '/imports/api/utility/numberToSignedString';
import { getNumberFromScope } from '/imports/api/creature/creatures/CreatureVariables';
import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider';
import { CalculatedField } from '/imports/api/properties/subSchemas/computedField';
import applyResetTask from '/imports/api/engine/action/tasks/applyResetTask';
export default async function applyActionProperty(
task: PropTask, action: EngineAction, result: TaskResult, userInput: InputProvider
@@ -72,7 +73,12 @@ export default async function applyActionProperty(
await applyChildren(action, prop, targetIds, userInput);
}
if (prop.actionType === 'event' && prop.variableName) {
resetProperties(action, prop, result, userInput);
await applyResetTask({
subtaskFn: 'reset',
prop,
eventName: prop.variableName,
targetIds: [action.creatureId],
}, action, result, userInput);
}
// Finish
@@ -244,46 +250,3 @@ function applyCrits(value, scope, resultPushScope) {
}
return { criticalHit, criticalMiss };
}
async function resetProperties(action: EngineAction, prop: any, result: TaskResult, userInput: InputProvider) {
const attributes = getPropertiesOfType(action.creatureId, 'attribute');
for (const att of attributes) {
if (att.removed || att.inactive) continue;
if (att.reset !== prop.variableName) continue;
if (!att.damage) continue;
applyTask(action, {
prop: att,
targetIds: [action.creatureId],
subtaskFn: 'damageProp',
params: {
title: getPropertyTitle(att),
operation: 'increment',
value: -att.damage ?? 0,
targetProp: att,
},
}, userInput)
}
const actions = [
...getPropertiesOfType(action.creatureId, 'action'),
...getPropertiesOfType(action.creatureId, 'spell'),
]
for (const act of actions) {
if (act.removed || act.inactive) continue;
if (act.reset !== prop.variableName) continue;
if (!act.usesUsed) continue;
result.mutations.push({
targetIds: [action.creatureId],
updates: [{
propId: act._id,
set: { usesUsed: 0 },
type: act.type,
}],
contents: [{
name: getPropertyTitle(act),
value: act.usesUsed >= 0 ? `Restored ${act.usesUsed} uses` : `Removed ${-act.usesUsed} uses`,
inline: true,
...prop.silent && { silenced: true },
}]
});
}
}

View File

@@ -42,7 +42,7 @@ export default async function applyDamageProperty(
// roll the dice only and store that string
recalculateCalculation(prop.amount, action, 'compile', inputProvider);
const { result: rolled } = await resolve('roll', prop.amount.valueNode, scope, context);
const { result: rolled } = await resolve('roll', prop.amount.valueNode, scope, context, inputProvider);
if (rolled.parseType !== 'constant') {
logValue.push(toString(rolled));
}
@@ -52,7 +52,7 @@ export default async function applyDamageProperty(
context.errors = [];
// Resolve the roll to a final value
const { result: reduced } = await resolve('reduce', rolled, scope, context);
const { result: reduced } = await resolve('reduce', rolled, scope, context, inputProvider);
result.appendParserContextErrors(context, damageTargets);
// Store the result
@@ -102,11 +102,11 @@ export default async function applyDamageProperty(
recalculateCalculation(prop.save.damageFunction, action, 'compile', inputProvider);
context.errors = [];
const { result: saveDamageRolled } = await resolve(
'roll', prop.save.damageFunction.valueNode, scope, context
'roll', prop.save.damageFunction.valueNode, scope, context, inputProvider
);
saveRoll = toString(saveDamageRolled);
const { result: saveDamageResult } = await resolve(
'reduce', saveDamageRolled, scope, context
'reduce', saveDamageRolled, scope, context, inputProvider
);
result.appendParserContextErrors(context, damageTargets);
// If we didn't end up with a constant of finite amount, give up

View File

@@ -4,7 +4,6 @@ import InputProvider from '/imports/api/engine/action/functions/userInput/InputP
import { applyDefaultAfterPropTasks } from '/imports/api/engine/action/functions/applyTaskGroups';
import { getEffectiveActionScope } from '/imports/api/engine/action/functions/getEffectiveActionScope';
import recalculateCalculation from '/imports/api/engine/action/functions/recalculateCalculation';
import { applyUnresolvedEffects } from '/imports/api/engine/action/methods/doCheck';
import { PropTask } from '/imports/api/engine/action/tasks/Task';
import TaskResult from '/imports/api/engine/action/tasks/TaskResult';
import { getVariables } from '/imports/api/engine/loadCreatures';
@@ -64,11 +63,8 @@ export default async function applySavingThrowProperty(
applyDefaultAfterPropTasks(action, prop, [targetId], inputProvider);
}
let rollModifierText = numberToSignedString(save.value, true);
let rollModifier = save.value
const { effectBonus, effectString } = applyUnresolvedEffects(save, scope)
rollModifierText += effectString;
rollModifier += effectBonus;
const rollModifierText = numberToSignedString(save.value, true);
const rollModifier = save.value;
let value, resultPrefix;
if (save.advantage === 1) {

View File

@@ -5,6 +5,7 @@ import recalculateCalculation from '/imports/api/engine/action/functions/recalcu
import TaskResult from '/imports/api/engine/action/tasks/TaskResult';
import applyTask from '/imports/api/engine/action/tasks/applyTask';
import { getSingleProperty } from '/imports/api/engine/loadCreatures';
import { hasAncestorRelationship } from '/imports/api/parenting/parentingFunctions';
export default async function spendResources(
action: EngineAction, prop, targetIds: string[], result: TaskResult, userInput
@@ -69,8 +70,11 @@ export default async function spendResources(
params: {
value: quantity,
item,
// If the item is an ancestor or descendant of this prop, skip the item's children to avoid
// an infinite loop
skipChildren: hasAncestorRelationship(item, prop),
},
}, userInput);
}
}
}
}

View File

@@ -37,8 +37,8 @@ export type Advantage = 0 | 1 | -1;
export type CheckParams = {
advantage: Advantage;
skillVariableName: string;
abilityVariableName: string;
skillVariableName?: string;
abilityVariableName?: string;
dc: number | null;
contest?: true;
targetSkillVariableName?: string;

View File

@@ -5,12 +5,14 @@ import { union } from 'lodash';
import CreatureLogs from '/imports/api/creature/log/CreatureLogs';
import bulkWrite from '/imports/api/engine/shared/bulkWrite';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
import Creatures from '/imports/api/creature/creatures/Creatures';
export default async function writeActionResults(action: EngineAction) {
if (!action._id) throw new Meteor.Error('type-error', 'Action does not have an _id');
EngineActions.remove(action._id);
const engineActionPromise = EngineActions.removeAsync(action._id);
const creaturePropUpdates: any[] = [];
const logContents: any[] = [];
// Collect all the updates and log content
action.results.forEach(result => {
result.mutations.forEach(mutation => {
@@ -18,7 +20,7 @@ export default async function writeActionResults(action: EngineAction) {
logContents.push(...mutationToLogUpdates(mutation));
});
});
const allTargetIds = union(...logContents.map(c => c.targetIds));
const allTargetIds: string[] = union(...logContents.map(c => c.targetIds));
// Write the log
const logPromise = CreatureLogs.insertAsync({
@@ -30,5 +32,14 @@ export default async function writeActionResults(action: EngineAction) {
// Write the bulk updates
const bulkWritePromise = bulkWrite(creaturePropUpdates, CreatureProperties);
return Promise.all([logPromise, bulkWritePromise]);
// Mark the creatures as dirty
const creaturePromise = Creatures.updateAsync({
_id: { $in: [action.creatureId, ...allTargetIds] },
}, {
$set: { dirty: true },
}, {
multi: true,
});
return Promise.all([engineActionPromise, logPromise, bulkWritePromise, creaturePromise]);
}

View File

@@ -1,139 +0,0 @@
import SimpleSchema from 'simpl-schema';
import { ValidatedMethod } from 'meteor/mdg:validated-method';
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions';
import rollDice from '/imports/parser/rollDice';
import numberToSignedString from '/imports/api/utility/numberToSignedString';
import { getSingleProperty } from '/imports/api/engine/loadCreatures';
// TODO Migrate this to the new action engine
const doCheck = new ValidatedMethod({
name: 'creatureProperties.doCheck',
validate: new SimpleSchema({
propId: SimpleSchema.RegEx.Id,
scope: {
type: Object,
blackbox: true,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 10,
timeInterval: 5000,
},
run({ propId, scope }) {
console.warn('do check not implemented');
return;
const prop = CreatureProperties.findOne(propId);
if (!prop) throw new Meteor.Error('not-found', 'The property was not found');
const creatureId = prop.root.id;
const actionContext = new ActionContext(creatureId, [creatureId], this);
Object.assign(actionContext.scope, scope);
actionContext.scope[`#${prop.type}`] = prop;
// Check permissions
assertEditPermission(actionContext.creature, this.userId);
// Do the check
doCheckWork({ prop, actionContext });
},
});
export default doCheck;
export function doCheckWork({ prop, actionContext }) {
applyTriggers(actionContext.triggers.check?.before, prop, actionContext);
rollCheck(prop, actionContext);
applyTriggers(actionContext.triggers.check?.after, prop, actionContext);
// Insert the log
actionContext.writeLog();
}
function rollCheck(prop, actionContext) {
const scope = actionContext.scope;
// get the modifier for the roll
let rollModifier;
let logName = `${prop.name} check`;
if (prop.type === 'skill') {
rollModifier = prop.value;
if (prop.skillType === 'save') {
if (prop.name.match(/save/i)) {
logName = prop.name;
} else {
logName = prop.name ? `${prop.name} save` : 'Saving Throw';
}
}
} else if (prop.type === 'attribute') {
if (prop.attributeType === 'ability') {
rollModifier = prop.modifier;
} else {
rollModifier = prop.value;
}
} else {
throw (`${prop.type} not supported for checks`);
}
let rollModifierText = numberToSignedString(rollModifier, true);
const { effectBonus, effectString } = applyUnresolvedEffects(prop, actionContext)
rollModifierText += effectString;
rollModifier += effectBonus;
let value, values, resultPrefix;
if (scope['~checkAdvantage']?.value === 1) {
logName += ' (Advantage)';
const [a, b] = rollDice(2, 20);
if (a >= b) {
value = a;
resultPrefix = `1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText} = `;
} else {
value = b;
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText} = `;
}
} else if (scope['~checkAdvantage']?.value === -1) {
logName += ' (Disadvantage)';
const [a, b] = rollDice(2, 20);
if (a <= b) {
value = a;
resultPrefix = `1d20 [ ${a}, ~~${b}~~ ] ${rollModifierText} = `;
} else {
value = b;
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] ${rollModifierText} = `;
}
} else {
values = rollDice(1, 20);
value = values[0];
resultPrefix = `1d20 [ ${value} ] ${rollModifierText} = `
}
const result = (value + rollModifier) || 0;
scope['~checkDiceRoll'] = { value };
scope['~checkRoll'] = { value: result };
scope['~checkModifier'] = { value: rollModifier };
actionContext.addLog({
name: logName,
value: `${resultPrefix} **${result}**`,
});
}
// TODO replace this with recalculating and then rolling/reducing the value node
export function applyUnresolvedEffects(prop, actionContext) {
let effectBonus = 0;
let effectString = '';
if (!prop.effectIds) {
return { effectBonus, effectString };
}
prop.effectIds.forEach(id => {
const effect = getSingleProperty(actionContext.creature._id, id);
if (!effect.amount?.parseNode) return;
if (effect.operation !== 'add') return;
recalculateCalculation(effect.amount, actionContext, undefined, 'reduce');
if (typeof effect.amount?.value !== 'number') return;
effectBonus += effect.amount.value;
effectString += ` ${effect.amount.value < 0 ? '-' : '+'} [${effect.amount.calculation}] ${Math.abs(effect.amount.value)}`
});
return { effectBonus, effectString };
}

View File

@@ -1,6 +1,6 @@
import { CheckParams } from '/imports/api/engine/action/functions/userInput/InputProvider';
type Task = PropTask | DamagePropTask | ItemAsAmmoTask | CheckTask;
type Task = PropTask | DamagePropTask | ItemAsAmmoTask | CheckTask | ResetTask;
export default Task;
@@ -31,9 +31,17 @@ export type ItemAsAmmoTask = BaseTask & {
params: {
value: number;
item: any;
skipChildren: boolean;
};
}
export type CheckTask = BaseTask & CheckParams & {
subtaskFn: 'check';
}
export type ResetTask = BaseTask & {
subtaskFn: 'reset',
eventName: string;
// One and only one target
targetIds: [string];
}

View File

@@ -124,8 +124,8 @@ export default async function applyDamagePropTask(
type: targetProp.type,
}],
contents: [{
name: 'Attribute damaged',
value: `${numberToSignedString(-value)} ${getPropertyTitle(targetProp)}`,
name: increment >= 0 ? 'Attribute damaged' : 'Attribute restored',
value: `${numberToSignedString(-increment)} ${getPropertyTitle(targetProp)}`,
inline: true,
...prop.silent && { silenced: true },
}]

View File

@@ -1,6 +1,6 @@
import { EngineAction } from '/imports/api/engine/action/EngineActions';
import {
applyDefaultAfterPropTasks, applyTriggers
applyDefaultAfterPropTasks, applyAfterTasksSkipChildren, applyTriggers
} from '/imports/api/engine/action/functions/applyTaskGroups';
import {
getEffectiveActionScope
@@ -31,7 +31,7 @@ export default async function applyItemAsAmmoTask(task: ItemAsAmmoTask, action:
};
value = scope['~ammoConsumed']?.value || 0;
const itemChildren = await getPropertyChildren(action.creatureId, item);
const itemChildren = task.params.skipChildren ? [] : await getPropertyChildren(action.creatureId, item);
// Do the quantity adjustment
// Check if property has quantity
@@ -53,5 +53,10 @@ export default async function applyItemAsAmmoTask(task: ItemAsAmmoTask, action:
});
await applyTriggers(action, item, [action.creatureId], 'ammo.after', userInput);
return applyDefaultAfterPropTasks(action, item, task.targetIds, userInput);
if (task.params.skipChildren) {
return applyAfterTasksSkipChildren(action, item, task.targetIds, userInput);
} else {
return applyDefaultAfterPropTasks(action, item, task.targetIds, userInput);
}
}

View File

@@ -0,0 +1,172 @@
import { EngineAction } from '/imports/api/engine/action/EngineActions';
import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider';
import { ResetTask } from '/imports/api/engine/action/tasks/Task';
import TaskResult from '/imports/api/engine/action/tasks/TaskResult';
import applyTask from '/imports/api/engine/action/tasks/applyTask';
import { getCreature, getPropertiesByFilter, getPropertiesOfType } from '/imports/api/engine/loadCreatures';
import getPropertyTitle from '/imports/api/utility/getPropertyTitle';
export default async function applyResetTask(
task: ResetTask, action: EngineAction, result: TaskResult, userInput: InputProvider
): Promise<void> {
// Event name must be defined
if (!task.eventName) return;
// This task can only be applied to a single target
if (task.targetIds.length !== 1) {
throw new Meteor.Error('wrong-number-of-targets', `Must reset the properties of a single creature at a time, ${task.targetIds.length} targets were provided`)
}
// Print a title for the event
let name: string;
switch (task.eventName) {
case 'shortRest':
name = 'Short Rest';
break;
case 'longRest':
name = 'Long Rest';
break;
default:
name = task.eventName;
break;
}
result.appendLog({ name }, task.targetIds);
// Reset the properties by this event name
await resetProperties(task, action, result, userInput);
// Reset hit dice on a long rest, starting with the highest dice
if (task.eventName === 'longRest') {
await resetHitDice(task, action, result, userInput);
}
}
export async function resetProperties(task: ResetTask, action: EngineAction, result: TaskResult, userInput: InputProvider) {
const creatureId = task.targetIds[0];
// Long rests reset short rest properties as well
let mongoFilter: Mongo.Selector<object>
if (task.eventName === 'longRest') {
mongoFilter = { reset: { $in: ['shortRest', 'longRest'] } }
} else {
mongoFilter = { reset: task.eventName };
}
const filterFn = (prop) => {
if (task.eventName === 'longRest') {
if (prop.reset !== 'longRest' && prop.reset !== 'shortRest') return false;
} else {
if (prop.reset !== task.eventName) return false;
}
return true;
}
// Attributes
const attributeFilter: Mongo.Selector<object> = {
...mongoFilter,
type: 'attribute',
damage: { $nin: [0, undefined] },
}
const attributeFilterFunction = (att) => {
if (att.type !== 'attribute') return false;
if (!filterFn(att)) return false;
if (att.damage === 0 || att.damage === undefined) return false;
return true;
}
const attributes = getPropertiesByFilter(creatureId, attributeFilterFunction, attributeFilter);
for (const prop of attributes) {
await applyTask(action, {
prop: task.prop || prop,
targetIds: [action.creatureId],
subtaskFn: 'damageProp',
params: {
title: getPropertyTitle(prop),
operation: 'increment',
value: -prop.damage ?? 0,
targetProp: prop,
},
}, userInput);
}
// Action-like properties
const actionFilter = {
...mongoFilter,
type: {
$in: ['action', 'spell']
},
usesUsed: { $nin: [0, undefined] },
};
const actionFilterFunction = (prop) => {
if (prop.type !== 'action' && prop.type !== 'spell') return false;
if (!filterFn(prop)) return false;
if (prop.usesUsed === 0 || prop.usesUsed === undefined) return false;
return true;
}
const actionProps = getPropertiesByFilter(creatureId, actionFilterFunction, actionFilter);
for (const prop of actionProps) {
result.mutations.push({
targetIds: [creatureId],
updates: [{
propId: prop._id,
type: prop.type,
set: { usesUsed: 0 },
}],
contents: [{
name: prop.name,
value: prop.usesUsed >= 0 ? `Restored ${prop.usesUsed} uses` : `Removed ${-prop.usesUsed} uses`
}],
});
}
}
async function resetHitDice(task: ResetTask, action: EngineAction, result: TaskResult, userInput: InputProvider) {
const creatureId = task.targetIds[0];
const hitDice = getPropertiesOfType(creatureId, 'hitDice');
// Use a collator to do sorting in natural order
const collator = new Intl.Collator('en', {
numeric: true, sensitivity: 'base'
});
// Get the hit dice in decending order of hitDiceSize
const 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
const totalHd = hitDice.reduce((sum, hd) => sum + (hd.total || 0), 0);
const creature = getCreature(creatureId);
const resetMultiplier = 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;
for (const hd of hitDice) {
if (!recoverableHd) return;
amountToRecover = Math.min(recoverableHd, hd.damage ?? 0);
if (!amountToRecover) return;
recoverableHd -= amountToRecover;
// Apply the damage prop task
await applyTask(action, {
prop: task.prop || hd,
targetIds: [creatureId],
subtaskFn: 'damageProp',
params: {
title: getPropertyTitle(hd),
operation: 'increment',
value: -amountToRecover,
targetProp: hd,
},
}, userInput);
}
}

View File

@@ -7,6 +7,7 @@ import { getSingleProperty } from '/imports/api/engine/loadCreatures';
import applyProperties from '/imports/api/engine/action/applyProperties';
import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider';
import applyCheckTask from '/imports/api/engine/action/tasks/applyCheckTask';
import applyResetTask from '/imports/api/engine/action/tasks/applyResetTask';
// DamagePropTask promises a number of actual damage done
export default async function applyTask(
@@ -45,6 +46,10 @@ export default async function applyTask(
return applyItemAsAmmoTask(task, action, result, inputProvider);
case 'check':
return applyCheckTask(task, action, result, inputProvider);
case 'reset':
return applyResetTask(task, action, result, inputProvider);
default:
throw 'No case defined for the given subtaskFn';
}
} else {
// Get property

View File

@@ -94,6 +94,36 @@ export function getPropertiesOfType(creatureId, propType) {
return props;
}
/**
* Get the properties of a creature that matches the filters given
* @param creatureId The id of the creature
* @param filterFn A function that returns true if the given prop matches the filter
* @param mongoFilter A mongo selector that is exactly equal to the above function
*/
export function getPropertiesByFilter(creatureId, filterFn: (any) => boolean, mongoFilter: Mongo.Selector<object>) {
const creature = loadedCreatures.get(creatureId);
if (creature) {
const props: CreatureProperty[] = []
for (const prop of creature.properties.values()) {
if (filterFn(prop)) {
props.push(prop);
}
}
props.sort((a, b) => a.left - b.left);
return EJSON.clone(props);
}
// console.time(`Cache miss on creature properties: ${creatureId}`)
const props = CreatureProperties.find({
'root.id': creatureId,
'removed': { $ne: true },
...mongoFilter
}, {
sort: { left: 1 },
}).fetch();
// console.timeEnd(`Cache miss on creature properties: ${creatureId}`);
return props;
}
export function getCreature(creatureId: string) {
const loadedCreature = loadedCreatures.get(creatureId);
const loadedCreatureDoc = loadedCreature?.creature;
@@ -207,7 +237,7 @@ export function getPropertyChildren(creatureId, property) {
'parentId': property._id,
removed: { $ne: true },
}, {
sort: { order: 1 },
sort: { left: 1 },
}).fetch();
}
}
@@ -238,7 +268,7 @@ class LoadedCreature {
'root.id': creatureId,
removed: { $ne: true },
}, {
sort: { order: 1 },
sort: { left: 1 },
}).observeChanges({
added(id, fields) {
fields._id = id;

View File

@@ -284,7 +284,7 @@ const softRemoveLibraryNode = new ValidatedMethod({
run({ _id }) {
let node = LibraryNodes.findOne(_id);
assertNodeEditPermission(node, this.userId);
softRemove({ _id, collection: LibraryNodes });
softRemove(LibraryNodes, node);
}
});
@@ -303,7 +303,7 @@ const restoreLibraryNode = new ValidatedMethod({
let node = LibraryNodes.findOne(_id);
assertNodeEditPermission(node, this.userId);
// Do work
restore(LibraryNodes, _id);
restore(LibraryNodes, node);
}
});

View File

@@ -58,7 +58,7 @@ const copyLibraryNodeTo = new ValidatedMethod({
removed: { $ne: true },
}, {
limit: DUPLICATE_CHILDREN_LIMIT + 1,
sort: { order: 1 },
sort: { left: 1 },
}).fetch();
if (decendants.length > DUPLICATE_CHILDREN_LIMIT) {

View File

@@ -47,7 +47,7 @@ const duplicateLibraryNode = new ValidatedMethod({
removed: { $ne: true },
}, {
limit: DUPLICATE_CHILDREN_LIMIT + 1,
sort: { order: 1 },
sort: { left: 1 },
}).fetch();
if (nodes.length > DUPLICATE_CHILDREN_LIMIT) {

View File

@@ -5,7 +5,7 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
import { Mongo } from 'meteor/mongo';
export function getCollectionByName(name: string): Mongo.Collection<TreeDoc> {
const collection = Mongo.Collection.get(name)
const collection: Mongo.Collection<TreeDoc> = Mongo.Collection.get(name)
if (!collection) {
throw new Meteor.Error('bad-collection-reference',
`Parent references collection ${name}, which does not exist`
@@ -93,16 +93,16 @@ type FilteredDoc = {
export function filterToForest(
collection: Mongo.Collection<TreeDoc>,
rootId: string,
filter?: Mongo.Query<TreeDoc>,
filter?: Mongo.Selector<TreeDoc>,
{
options = <Mongo.Options<object>>{},
options = <Mongo.Options<TreeDoc>>{},
includeFilteredDocAncestors = false,
includeFilteredDocDescendants = false
} = {}
): TreeNode<FilteredDoc>[] {
if (!Meteor.isClient) throw 'Only available on the client';
// Setup the filter
let collectionFilter: Mongo.Query<TreeDoc> = {
let collectionFilter: Mongo.Selector<TreeDoc> = {
'root.id': rootId,
'removed': { $ne: true },
};
@@ -113,16 +113,17 @@ export function filterToForest(
}
}
// Set up the options
let collectionSort = {
let collectionSort: Mongo.Options<TreeDoc>['sort'] = {
left: 1
};
if (options && options.sort) {
if (options.sort) {
collectionSort = {
...collectionSort,
// @ts-expect-error go home typescript you're drunk
...options.sort,
}
}
let collectionOptions: Mongo.Options<object> = {
let collectionOptions: Mongo.Options<TreeDoc> = {
sort: collectionSort,
}
if (options) {
@@ -671,8 +672,8 @@ export function setDocToLastOrder(collection: Mongo.Collection<TreeDoc>, doc: Tr
doc.left = Number.MAX_SAFE_INTEGER;
}
export async function rebuildNestedSets(collection: Mongo.Collection<TreeDoc>, rootId: string) {
const docs = await collection.find({
export function rebuildNestedSets(collection: Mongo.Collection<TreeDoc>, rootId: string) {
const docs = collection.find({
'root.id': rootId,
removed: { $ne: true }
}, {
@@ -681,13 +682,13 @@ export async function rebuildNestedSets(collection: Mongo.Collection<TreeDoc>, r
//Reverse sorting so that arrays can be used as stacks with the first item on top
left: 1,
},
}).fetchAsync();
}).fetch();
const operations = calculateNestedSetOperations(docs);
return writeBulkOperations(collection, operations);
}
export async function rebuildCreatureNestedSets(creatureId) {
export function rebuildCreatureNestedSets(creatureId) {
const docs = getProperties(creatureId);
const operations = calculateNestedSetOperations(docs);
return writeBulkOperations(CreatureProperties as Mongo.Collection<TreeDoc, TreeDoc>, operations);
@@ -823,8 +824,9 @@ export function applyNestedSetProperties<T extends TreeDoc>(docs: T[]): Forest<T
* @param operations An array of bulk operations to write
* @returns Promise<undefined>
*/
async function writeBulkOperations(collection: Mongo.Collection<TreeDoc>, operations) {
if (Meteor.isServer && operations.length) {
function writeBulkOperations(collection: Mongo.Collection<TreeDoc>, operations) {
if (Meteor.isServer) {
if (!operations.length) return Promise.resolve();
return new Promise((resolve, reject) => {
collection.rawCollection().bulkWrite(
operations,
@@ -841,20 +843,19 @@ async function writeBulkOperations(collection: Mongo.Collection<TreeDoc>, operat
} else {
// Don't do latency compensation if there are too many operations, it just causes client
// lag without much benefit
const promises = operations.map(op => {
operations.forEach(op => {
if (op.updateOne) {
return collection.updateAsync(
collection.update(
op.updateOne.filter,
op.updateOne.update,
);
} else if (op.updateMany) {
return collection.updateAsync(
collection.update(
op.updateMany.filter,
op.updateMany.update,
{ multi: true },
)
}
});
return Promise.all(promises);
}
}

View File

@@ -1,19 +1,28 @@
import { getCollectionByName, getFilter } from '/imports/api/parenting/parentingFunctions';
import { TreeDoc } from '/imports/api/parenting/ChildSchema';
export async function softRemove(collection: Mongo.Collection<TreeDoc> | string, doc?: TreeDoc | string) {
export function softRemove(collectionOrName: Mongo.Collection<TreeDoc> | string, docOrId?: TreeDoc | string) {
const removalDate = new Date();
if (typeof collection === 'string') {
collection = getCollectionByName(collection);
let collection: Mongo.Collection<TreeDoc>;
if (typeof collectionOrName === 'string') {
collection = getCollectionByName(collectionOrName);
} else {
collection = collectionOrName;
}
if (typeof doc === 'string') {
doc = await collection.findOneAsync(doc);
let doc: TreeDoc | undefined;
if (typeof docOrId === 'string') {
doc = collection.findOne(docOrId);
} else {
doc = docOrId
}
if (!doc) {
throw new Meteor.Error('not found', 'The document to remove was not found');
}
// Remove this document
const removeDocPromise = collection.updateAsync(
collection.update(
doc._id,
{
$set: {
@@ -23,13 +32,11 @@ export async function softRemove(collection: Mongo.Collection<TreeDoc> | string,
$unset: {
removedWith: 1,
}
}, {
selector: { type: 'any' },
},
}
);
// Remove all the descendants that have not yet been removed, and set them to be
// removed with this document
const removeDescendantsPromise = collection.updateAsync({
collection.update({
...getFilter.descendants(doc),
removed: { $ne: true },
}, {
@@ -39,10 +46,8 @@ export async function softRemove(collection: Mongo.Collection<TreeDoc> | string,
removedWith: doc._id,
}
}, {
selector: { type: 'any' },
multi: true,
});
return Promise.all([removeDocPromise, removeDescendantsPromise]);
}
const restoreError = function () {
@@ -51,18 +56,26 @@ const restoreError = function () {
);
};
export async function restore(collection: Mongo.Collection<TreeDoc> | string, doc: TreeDoc | string, extraUpdates?) {
if (typeof collection === 'string') {
collection = getCollectionByName(collection);
export function restore(collectionOrName: Mongo.Collection<TreeDoc> | string, docOrId: TreeDoc | string, extraUpdates?) {
let collection: Mongo.Collection<TreeDoc>;
if (typeof collectionOrName === 'string') {
collection = getCollectionByName(collectionOrName);
} else {
collection = collectionOrName;
}
if (typeof doc === 'string') {
const foundDoc = await collection.findOneAsync(doc)
if (!foundDoc) {
throw new Meteor.Error('not found', 'The document to remove was not found');
}
doc = foundDoc;
let doc: TreeDoc | undefined;
if (typeof docOrId === 'string') {
doc = collection.findOne(docOrId);
} else {
doc = docOrId
}
const numUpdated: number = await collection.updateAsync({
if (!doc) {
throw new Meteor.Error('not found', 'The document to remove was not found');
}
const numUpdated: number = collection.update({
_id: doc._id,
removedWith: { $exists: false }
}, {
@@ -71,11 +84,11 @@ export async function restore(collection: Mongo.Collection<TreeDoc> | string, do
removedAt: 1,
},
...extraUpdates
}, {
selector: { type: 'any' },
});
if (numUpdated === 0) restoreError();
return collection.updateAsync({
return collection.update({
removedWith: doc._id,
}, {
$unset: {
@@ -84,7 +97,6 @@ export async function restore(collection: Mongo.Collection<TreeDoc> | string, do
removedWith: 1,
}
}, {
selector: { type: 'any' },
multi: true,
});
}) + 1;
}

View File

@@ -1,122 +0,0 @@
<template lang="html">
<div>
<v-menu
v-model="open"
origin="center center"
transition="scale-transition"
nudge-left="100px"
nudge-top="100px"
:close-on-content-click="false"
>
<template #activator="{ on }">
<v-btn
v-bind="$attrs"
:class="buttonClass"
v-on="noClick ? {} : on"
@click="e => { if (!noClick) e.stopPropagation(); }"
>
<slot />
</v-btn>
</template>
<v-sheet class="d-flex flex-column align-center justify-center">
<v-btn-toggle
v-model="dataAdvantage"
color="accent"
>
<v-btn :value="-1">
Disadvantage
</v-btn>
<v-btn :value="1">
Advantage
</v-btn>
</v-btn-toggle>
<div class="ma-1 text-subtitle-2">
{{ name }}
</div>
<div>
<v-scale-transition
origin="center center"
>
<vertical-hex
v-if="dataAdvantage"
style="position:absolute; transition: margin-left 0.3s ease;"
:style="{marginLeft: dataAdvantage == 1 ? '24px' : '-24px'}"
disable-hover
/>
</v-scale-transition>
<vertical-hex @click="roll">
<div>
Roll
</div>
<div v-if="rollText">
{{ rollText }}
</div>
</vertical-hex>
</div>
<v-btn
text
color="primary"
style="align-self: end"
@click="close"
>
Cancel
</v-btn>
</v-sheet>
</v-menu>
</div>
</template>
<script lang="js">
import VerticalHex from '/imports/client/ui/components/VerticalHex.vue';
export default {
components: {
VerticalHex
},
props: {
name: {
type: String,
default: undefined,
},
rollText: {
type: String,
default: undefined,
},
buttonClass: {
type: String,
default: undefined,
},
advantage: {
type: Number,
default: undefined,
},
noClick: Boolean,
},
data(){return {
open: false,
dataAdvantage: this.advantage,
}},
watch: {
advantage(val){
this.dataAdvantage = val;
},
open(val){
if(!val){
this.dataAdvantage = this.advantage;
}
},
},
methods: {
roll(){
this.$emit('roll', {advantage: this.dataAdvantage});
this.open = false;
},
close(){
this.open = false;
}
},
}
</script>
<style lang="css" scoped>
</style>

View File

@@ -1,99 +0,0 @@
<template lang="html">
<v-card>
<template v-if="!result">
<v-btn-toggle v-model="advantage">
<v-btn text>
Advantage
</v-btn>
<v-btn text>
Disadvantage
</v-btn>
</v-btn-toggle>
<v-card-text>
<div class="layout justify-center align-center">
<v-btn
large
fab
outlined
@click="makeRoll"
>
<div class="text-h4">
{{ numberToSignedString(bonus) }}
</div>
</v-btn>
</div>
</v-card-text>
</template>
<template v-else>
<div>
<div class="text-h6">
<span
v-for="(roll, index) of rolls"
:key="index"
class="roll"
:class="{strikethrough: index !== chosenRollIndex}"
>
{{ roll }}
</span>
<span class="ml-1">
{{ numberToSignedString(bonus) }}
</span>
</div>
<div class="text-h4">
{{ result }}
</div>
</div>
</template>
</v-card>
</template>
<script lang="js">
import numberToSignedString from '../../../../api/utility/numberToSignedString';
export default {
props: {
attributeVarName: {
type: String,
default: '',
},
attributeName: {
type: String,
default: '',
},
creatureId: {
type: String,
default: '',
},
bonus: {
type: Number,
required: true,
},
},
data(){return {
advantage: undefined,
result: undefined,
rolls: undefined,
chosenRoll: undefined,
chosenRollIndex: undefined,
}},
methods: {
makeRoll(){
//let {rolls, bonus, chosenRoll, result} = doCheckWork.call();
this.rolls = [12, 8];
if (this.advantage === 1){
this.chosenRoll = 8;
} else {
this.chosenRoll = 12;
}
this.result = this.chosenRoll + this.bonus;
this.chosenRollIndex = this.rolls.indexOf(this.chosenRoll);
},
numberToSignedString,
}
}
</script>
<style lang="css" scoped>
.strikethrough {
text-decoration: line-through;
}
</style>

View File

@@ -3,6 +3,7 @@
:loading="loading"
:disabled="context.editPermission === false"
outlined
:data-id="`rest-btn-${type}`"
style="width: 160px;"
@click="rest"
>
@@ -14,7 +15,7 @@
</template>
<script lang="js">
import restCreature from '/imports/api/creature/creatures/methods/restCreature';
import doAction from '/imports/client/ui/creature/actions/doAction';
export default {
inject: {
@@ -36,19 +37,21 @@ export default {
methods: {
rest(){
this.loading = true;
restCreature.call({
creatureId: this.creatureId,
restType: this.type,
}, error => {
const emptyProp = {
_id: this.creatureId,
root: { id: this.creatureId },
};
doAction(emptyProp, this.$store, `rest-btn-${this.type}`, {
subtaskFn: 'reset',
prop: emptyProp,
targetIds: [this.creatureId],
eventName: this.type,
}).catch(e => {
console.error(e);
}).finally(() => {
this.loading = false;
if (error){
console.error(error);
}
});
}
}
}
</script>
<style lang="css" scoped>
</style>

View File

@@ -1,73 +1,101 @@
<template lang="html">
<dialog-base>
<template slot="toolbar">
<div class="d-flex flex-column">
<v-toolbar
class="base-dialog-toolbar"
>
<v-btn
icon
@click="cancel"
>
<v-icon>mdi-arrow-left</v-icon>
</v-btn>
<v-toolbar-title>
Action
</v-toolbar-title>
</template>
<log-content :model="allLogContent" />
<component
:is="activeInput"
v-if="activeInput"
v-model="userInput"
v-bind="activeInputParams"
@continue="continueAction"
@set-input-ready="setInputReady"
/>
<v-btn
slot="actions"
text
@click="cancel"
>
Cancel
</v-btn>
<v-spacer slot="actions" />
<v-btn
v-show="!actionDone"
slot="actions"
text
:disabled="!userInputReady || !resumeActionFn"
@click="stepAction"
>
Step
</v-btn>
<v-btn
v-if="actionDone"
slot="actions"
text
@click="finishAction"
>
{{ 'Apply Results' }}
</v-btn>
<v-btn
v-else
slot="actions"
text
:disabled="actionBusy"
@click="startAction"
>
{{ 'Start' }}
</v-btn>
</dialog-base>
</v-toolbar>
<div class="action-dialog-content">
<div class="action-dialog-layout d-flex">
<component
:is="activeInput"
v-if="activeInput"
v-model="userInput"
class="action-input"
v-bind="activeInputParams"
@continue="continueAction"
@set-input-ready="setInputReady"
/>
<div
class="log-preview card-raised-background d-flex flex-column align-end justify-end"
style="flex-basis: 256px;"
>
<v-card
v-if="allLogContent && allLogContent.length"
class="ma-2 log-entry"
>
<v-card-text
class="pa-2"
>
<log-content :model="allLogContent" />
</v-card-text>
</v-card>
</div>
</div>
</div>
<v-card-actions>
<v-btn
text
@click="cancel"
>
Cancel
</v-btn>
<v-spacer slot="actions" />
<v-btn
v-show="!actionDone"
text
:disabled="!userInputReady || !resumeActionFn"
@click="stepAction"
>
Step
</v-btn>
<v-btn
v-if="actionDone"
text
@click="finishAction"
>
{{ 'Apply Results' }}
</v-btn>
<v-btn
v-else
text
:disabled="actionBusy"
@click="startAction"
>
{{ 'Start' }}
</v-btn>
</v-card-actions>
</div>
</template>
<script lang="js">
import DialogBase from '/imports/client/ui/dialogStack/DialogBase.vue';
import EngineActions from '/imports/api/engine/action/EngineActions';
import applyAction from '/imports/api/engine/action/functions/applyAction';
import getDeterministicDiceRoller from '/imports/api/engine/action/functions/userInput/getDeterministicDiceRoller';
import AdvantageInput from '/imports/client/ui/creature/actions/input/AdvantageInput.vue';
import CheckInput from '/imports/client/ui/creature/actions/input/CheckInput.vue';
import RollInput from '/imports/client/ui/creature/actions/input/RollInput.vue';
import getDeterministicDiceRoller from '/imports/api/engine/action/functions/userInput/getDeterministicDiceRoller';
import ChoiceInput from '/imports/client/ui/creature/actions/input/ChoiceInput.vue';
import DialogBase from '/imports/client/ui/dialogStack/DialogBase.vue';
import EngineActions from '/imports/api/engine/action/EngineActions';
import LogContent from '/imports/client/ui/log/LogContent.vue';
import RollInput from '/imports/client/ui/creature/actions/input/RollInput.vue';
export default {
components: {
DialogBase,
AdvantageInput,
CheckInput,
RollInput,
ChoiceInput,
DialogBase,
LogContent,
RollInput,
},
props: {
actionId: {
@@ -133,7 +161,11 @@ export default {
applyAction(
this.actionResult, this, { simulate: true, stepThrough, task: this.task}
).then(() => {
this.actionDone = true
this.actionDone = true;
// If we aren't stepping through close the dialog and apply the action
if (!this.actionResult._stepThrough) {
this.$store.dispatch('popDialogStack', this.actionResult);
}
});
},
stepAction() {
@@ -172,17 +204,26 @@ export default {
},
// inputProvider methods
async rollDice(dice) {
return Promise.resolve(this.deterministicDiceRoller(dice));
/* Dice Animation and user control goes here:
this.activeInputParams = {
deterministicDiceRoller: this.deterministicDiceRoller,
dice
};
this.activeInput = 'roll-input';
return this.promiseInput();
*/
},
async nextStep(task) {
return this.promiseInput();
},
async choose(choices, quantity) {
this.userInput = [];
this.activeInputParams = {
choices,
quantity
};
this.activeInput = 'choice-input'
return this.promiseInput();
},
async advantage(suggestedAdvantage) {
@@ -199,3 +240,45 @@ export default {
}
};
</script>
<style lang="css" scoped>
.base-dialog-toolbar {
z-index: 2;
border-radius: 2px 2px 0 0;
}
.action-dialog-content {
container-type: size;
flex-grow: 1;
overflow: auto;
}
.action-dialog-content, .action-dialog-layout {
height: 100%;
}
.action-input {
flex-grow: 1;
height: 100%;
overflow-y: auto;
}
.log-preview {
flex-basis: 256px;
height: 100%;
overflow-y: auto;
}
@container (max-width: 600px) {
.action-dialog-layout {
flex-direction: column;
}
.action-input {
height: unset;
}
.log-preview {
flex-basis: 300px;
}
}
</style>

View File

@@ -5,6 +5,7 @@ import EngineActions, { EngineAction } from '/imports/api/engine/action/EngineAc
import InputProvider from '/imports/api/engine/action/functions/userInput/InputProvider';
import applyAction from '/imports/api/engine/action/functions/applyAction';
import { runAction } from '/imports/api/engine/action/methods/runAction';
import getDeterministicDiceRoller from '/imports/api/engine/action/functions/userInput/getDeterministicDiceRoller';
/**
* Apply an action on the client that first creates the action on both the client and server, then
@@ -44,7 +45,7 @@ export default async function doAction(
// Either way, call the action method afterwards
try {
const finishedAction = await applyAction(
action, errorOnInputRequest, { simulate: true, task }
action, getErrorOnInputRequestProvider(action._id), { simulate: true, task }
);
return callActionMethod(finishedAction, task);
} catch (e) {
@@ -76,10 +77,13 @@ const throwInputRequestedError = () => {
throw 'input-requested';
}
const errorOnInputRequest: InputProvider = {
nextStep: throwInputRequestedError,
rollDice: throwInputRequestedError,
choose: throwInputRequestedError,
advantage: throwInputRequestedError,
check: throwInputRequestedError,
function getErrorOnInputRequestProvider(actionId) {
const errorOnInputRequest: InputProvider = {
nextStep: throwInputRequestedError,
rollDice: getDeterministicDiceRoller(actionId),
choose: throwInputRequestedError,
advantage: throwInputRequestedError,
check: throwInputRequestedError,
}
return errorOnInputRequest;
}

View File

@@ -52,7 +52,6 @@ export default {
methods: {
emitInput(e) {
e = e || 0;
console.log(e);
this.$emit('input', e)
}
}

View File

@@ -0,0 +1,87 @@
<template>
<div class="choice-input">
<v-expansion-panels
accordion
tile
multiple
hover
>
<v-expansion-panel
v-for="prop in choices"
:key="prop._id"
:model="prop"
:data-id="prop._id"
>
<v-expansion-panel-header>
<template #default="{ open }">
<v-checkbox
v-model="selectedItems"
class="my-0 py-0 mr-2 flex-grow-0"
hide-details
:value="prop._id"
:disabled="!selectedItems.includes(prop._id) && selectedItems.length >= quantity.max"
@click.stop
/>
<tree-node-view :model="prop" />
<template v-if="open">
<v-spacer />
<v-btn
icon
class="flex-grow-0"
@click.stop="openPropertyDetails(prop._id)"
>
<v-icon>mdi-window-restore</v-icon>
</v-btn>
</template>
</template>
</v-expansion-panel-header>
<v-expansion-panel-content class="py-4">
<property-viewer :model="prop" />
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
<v-btn
:disabled="!canContinue"
@click="$emit('continue');"
>
Done
</v-btn>
</div>
</template>
<script lang="js">
import TreeNodeView from '/imports/client/ui/properties/treeNodeViews/TreeNodeView.vue';
import PropertyViewer from '/imports/client/ui/properties/shared/PropertyViewer.vue';
export default {
components: {
TreeNodeView,
PropertyViewer,
},
props: {
choices: {
type: Array,
required: true
},
quantity: {
type: Object,
default: () => ({min: 0, max: 1}),
},
},
data() {
return {
selectedItems: [],
};
},
computed: {
canContinue() {
return this.selectedItems.length >= this.quantity.min;
}
},
watch: {
selectedItems(val) {
this.$emit('input', val)
},
},
};
</script>

View File

@@ -155,7 +155,7 @@ export default {
const userId = Meteor.userId();
let folders = CreatureFolders.find(
{owner: userId, archived: {$ne: true}},
{sort: {order: 1}},
{sort: {left: 1}},
).map(folder => {
folder.creatures = Creatures.find(
{
@@ -189,7 +189,7 @@ export default {
const userId = Meteor.userId();
let folders = CreatureFolders.find(
{owner: userId},
{sort: {order: 1}},
{sort: {left: 1}},
).map(folder => {
folder.creatures = ArchiveCreatureFiles.find(
{

View File

@@ -319,7 +319,7 @@ export default {
removed: {$ne: true},
inactive: {$ne: true},
}, {
sort: {order: 1}
sort: {left: 1}
}).fetch();
},
classLevels() {
@@ -331,7 +331,7 @@ export default {
removed: {$ne: true},
inactive: {$ne: true},
}, {
sort: {order: 1}
sort: {left: 1}
});
},
slotBuildTree(){

View File

@@ -76,7 +76,7 @@ export default {
removed: { $ne: true },
inactive: { $ne: true },
}, {
sort: { order: 1 }
sort: { left: 1 }
});
},
},

View File

@@ -163,7 +163,7 @@ export default {
removed: { $ne: true },
inactive: { $ne: true },
}, {
sort: { order: 1 },
sort: { left: 1 },
}).fetch();
},
creature() {
@@ -188,7 +188,7 @@ export default {
removed: { $ne: true },
inactive: { $ne: true },
}, {
sort: { order: 1 },
sort: { left: 1 },
});
},
carriedItems() {
@@ -204,7 +204,7 @@ export default {
deactivatedByAncestor: { $ne: true },
deactivatedByToggle: { $ne: true },
}, {
sort: { order: 1 },
sort: { left: 1 },
});
},
equippedItems() {
@@ -215,7 +215,7 @@ export default {
removed: { $ne: true },
inactive: { $ne: true },
}, {
sort: { order: 1 },
sort: { left: 1 },
});
},
equipmentParentRef() {

View File

@@ -90,7 +90,7 @@ export default {
...getFilter.descendantsOfRoot(this.creatureId),
$nor: [getFilter.descendantsOfAll(allNotes)],
}, {
sort: {order: 1},
sort: {left: 1},
});
},
creature(){

View File

@@ -115,7 +115,7 @@ export default {
{ hideWhenValueZero: true, value: 0 },
],
}, {
sort: { order: 1 }
sort: { left: 1 }
});
},
spellLists() {
@@ -168,7 +168,7 @@ export default {
removed: { $ne: true },
inactive: { $ne: true },
}, {
sort: { order: 1 }
sort: { left: 1 }
});
},
},

View File

@@ -12,7 +12,7 @@
v-for="healthBar in properties.attribute.healthBar"
:key="healthBar._id"
:model="healthBar"
@change="({ type, value }) => incrementChange(healthBar._id, { type, value: -value })"
@change="({ type, value }) => incrementChange(healthBar._id, { type, value })"
@click="clickProperty({_id: healthBar._id})"
/>
</v-card>
@@ -392,7 +392,6 @@
<script lang="js">
import Creatures from '/imports/api/creature/creatures/Creatures';
import softRemoveProperty from '/imports/api/creature/creatureProperties/methods/softRemoveProperty';
import damageProperty from '/imports/api/creature/creatureProperties/methods/damageProperty';
import HealthBar from '/imports/client/ui/properties/components/attributes/HealthBar.vue';
import AttributeCard from '/imports/client/ui/properties/components/attributes/AttributeCard.vue';
import AbilityListTile from '/imports/client/ui/properties/components/attributes/AbilityListTile.vue';
@@ -412,6 +411,8 @@ import FolderGroupCard from '/imports/client/ui/properties/components/folders/Fo
import { get, set, uniqBy } from 'lodash';
import { docsToForest } from '/imports/api/parenting/parentingFunctions';
import { getFilter } from '/imports/api/parenting/parentingFunctions';
import doAction from '/imports/client/ui/creature/actions/doAction';
import getPropertyTitle from '/imports/client/ui/properties/shared/getPropertyTitle';
function walkDown(forest, callback){
let stack = [...forest];
@@ -517,7 +518,6 @@ export default {
return uniqBy(conditionals, '_id');
},
},
// @ts-ignore Meteor isn't defined on vue
meteor: {
properties() {
const creature = this.creature;
@@ -557,7 +557,7 @@ export default {
if (creature.settings.hideUnusedStats) {
filter.hide = { $ne: true };
}
const allProps = CreatureProperties.find(filter, { sort: { order: -1 } }).fetch();
const allProps = CreatureProperties.find(filter, { sort: { left: -1 } }).fetch();
const forest = docsToForest(allProps);
const properties = { folder: {}, attribute: {}, skill: {} };
walkDown(forest, node => {
@@ -593,7 +593,7 @@ export default {
deactivatedByToggle: { $ne: true },
showUI: true,
}, {
sort: { order: 1 }
sort: { left: 1 }
});
},
},
@@ -613,14 +613,24 @@ export default {
});
},
incrementChange(_id, { type, value, ack }) {
damageProperty.call({
_id,
operation: type,
value: -value
}, error => {
const model = CreatureProperties.findOne(_id);
if (type === 'increment') value = -value;
doAction(model, this.$store, model._id, {
subtaskFn: 'damageProp',
prop: model,
targetIds: [model.root.id],
params: {
title: getPropertyTitle(model),
operation: type,
value,
targetProp: model,
}
}).then(() =>{
ack?.();
}).catch((error) => {
if (ack) {
ack(error);
} else if (error) {
} else {
snackbar({ text: error.reason || error.message || error.toString() });
console.error(error);
}
@@ -637,7 +647,3 @@ export default {
},
};
</script>
<style lang="css" scoped>
</style>

View File

@@ -212,7 +212,7 @@ export default {
removed: {$ne: true},
inactive: {$ne: true},
}, {
sort: {order: 1}
sort: {left: 1}
}).fetch();
},
classLevels() {
@@ -224,7 +224,7 @@ export default {
removed: {$ne: true},
inactive: {$ne: true},
}, {
sort: {order: 1}
sort: {left: 1}
});
},
editPermission() {

View File

@@ -111,7 +111,7 @@ export default {
removed: { $ne: true },
inactive: { $ne: true },
}, {
sort: { order: 1 },
sort: { left: 1 },
});
},
creature() {
@@ -133,7 +133,7 @@ export default {
removed: { $ne: true },
inactive: { $ne: true },
}, {
sort: { order: 1 },
sort: { left: 1 },
}).map(c => {
c.items = CreatureProperties.find({
'parentId': c._id,
@@ -143,7 +143,7 @@ export default {
deactivatedByAncestor: { $ne: true },
deactivatedByToggle: { $ne: true },
}, {
sort: { order: 1 },
sort: { left: 1 },
}).fetch();
return c;
});
@@ -158,7 +158,7 @@ export default {
deactivatedByAncestor: { $ne: true },
deactivatedByToggle: { $ne: true },
}, {
sort: { order: 1 },
sort: { left: 1 },
});
},
equippedItems() {
@@ -169,7 +169,7 @@ export default {
removed: { $ne: true },
inactive: { $ne: true },
}, {
sort: { order: 1 },
sort: { left: 1 },
});
},
equipmentParentRef() {

View File

@@ -70,7 +70,7 @@ export default {
removed: { $ne: true },
inactive: { $ne: true },
}, {
sort: { order: 1 }
sort: { left: 1 }
}).fetch();
},
spellsWithoutList() {
@@ -96,7 +96,7 @@ export default {
removed: { $ne: true },
inactive: { $ne: true },
}, {
sort: { order: 1 }
sort: { left: 1 }
}).map(sl => {
sl.spells = CreatureProperties.find({
...getFilter.descendants(sl),

View File

@@ -363,7 +363,7 @@ import { uniqBy } from 'lodash';
import { getFilter } from '/imports/api/parenting/parentingFunctions';
const getProperties = function (creature, filter, options = {
sort: { order: 1 }
sort: { left: 1 }
}) {
if (!creature) return;
if (creature.settings.hideUnusedStats) {
@@ -434,7 +434,7 @@ export default {
deactivatedByToggle: { $ne: true },
showUI: true,
}, {
sort: { order: 1 }
sort: { left: 1 }
});
},
healthBars() {

View File

@@ -175,7 +175,7 @@ export default {
'ancestors.id': this.model._id,
'removed': { $ne: true },
}, {
sort: {order: 1}
sort: {left: 1}
}).map(prop => {
// Get all the props we don't want to show the decendants of and
// where they might appear in the ancestor list

View File

@@ -72,7 +72,6 @@
<script lang="js">
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
import damageProperty from '/imports/api/creature/creatureProperties/methods/damageProperty';
import pushToProperty from '/imports/api/creature/creatureProperties/methods/pushToProperty';
import pullFromProperty from '/imports/api/creature/creatureProperties/methods/pullFromProperty';
import softRemoveProperty from '/imports/api/creature/creatureProperties/methods/softRemoveProperty';
@@ -94,6 +93,7 @@ import Breadcrumbs from '/imports/client/ui/creature/creatureProperties/Breadcru
import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode';
import PropertyViewer from '/imports/client/ui/properties/shared/PropertyViewer.vue';
import copyPropertyToLibrary from '/imports/api/creature/creatureProperties/methods/copyPropertyToLibrary';
import doAction from '/imports/client/ui/creature/actions/doAction';
export default {
components: {
@@ -177,7 +177,27 @@ export default {
updateCreatureProperty.call({_id: this.currentId, path, value}, ack);
},
damage({operation, value, ack}){
damageProperty.call({_id: this.currentId, operation, value}, ack);
const model = this.model;
doAction(model, this.$store, model._id, {
subtaskFn: 'damageProp',
prop: model,
targetIds: [model.root.id],
params: {
title: getPropertyTitle(model),
operation: operation,
value,
targetProp: model,
}
}).then(() =>{
ack?.();
}).catch((error) => {
if (ack) {
ack(error);
} else {
snackbar({ text: error.reason || error.message || error.toString() });
console.error(error);
}
});
},
push({path, value, ack}){
pushToProperty.call({_id: this.currentId, path, value}, ack);

View File

@@ -0,0 +1,79 @@
<template lang="html">
<tree-node-list
v-if="model && model.root"
:children="children"
:group="group"
:organize="organize"
:start-expanded="expanded"
:root="model.root"
@selected="e => $emit('selected', e)"
@move-within-root="moveWithinRoot"
@move-between-roots="moveBetweenRoots"
/>
</template>
<script lang="js">
import { docsToForest, getFilter } from '/imports/api/parenting/parentingFunctions';
import TreeNodeList from '/imports/client/ui/components/tree/TreeNodeList.vue';
import { moveBetweenRoots, moveWithinRoot } from '/imports/api/parenting/organizeMethods';
import { getCollectionByName } from '/imports/api/parenting/parentingFunctions';
export default {
components: {
TreeNodeList,
},
props: {
// The document for which we are finding children
model: {
type: Object,
default: undefined,
},
organize: Boolean,
group: {
type: String,
default: 'creatureProperties'
},
collection: {
type: String,
default: 'creatureProperties'
},
expanded: Boolean,
},
meteor: {
children() {
if (!this.model?.root) return [];
const collection = getCollectionByName(this.collection);
const docs = collection.find({
removed: { $ne: true },
...getFilter.descendants(this.model),
}, {
sort: { left: 1 }
}).fetch();
this.$emit('length', docs.length);
return docsToForest(docs);
},
},
methods: {
moveWithinRoot({ doc, newPosition }) {
moveWithinRoot.callAsync({
docRef: {
id: doc._id,
collection: this.collection,
},
newPosition,
});
},
moveBetweenRoots({ doc, newPosition, newRootRef }) {
moveBetweenRoots.callAsync({
docRef: {
id: doc._id,
collection: this.collection,
},
newPosition,
newRootRef,
});
},
},
};
</script>

View File

@@ -47,7 +47,7 @@ export default {
return Session.get('editingDocs');
},
docs() {
const docs = Docs.find({ removed: {$ne: true} }, { sort: {order: 1} }).fetch();
const docs = Docs.find({ removed: {$ne: true} }, { sort: {left: 1} }).fetch();
return docsToForest(docs);
},
},

View File

@@ -113,7 +113,7 @@ export default {
const userId = Meteor.userId();
let folders = CreatureFolders.find(
{ owner: userId, archived: { $ne: true } },
{ sort: { order: 1 } },
{ sort: { left: 1 } },
).map(folder => {
folder.creatures = Creatures.find(
{

View File

@@ -190,7 +190,7 @@ export default {
removed: true,
removedWith: { $exists: false },
}, {
sort: { order: 1 },
sort: { left: 1 },
});
},
isOwner() {

View File

@@ -112,7 +112,7 @@ export default {
const userId = Meteor.userId();
let folders = CreatureFolders.find(
{ owner: userId, archived: { $ne: true } },
{ sort: { order: 1 } },
{ sort: { left: 1 } },
).map(folder => {
folder.creatures = Creatures.find(
{

View File

@@ -113,13 +113,13 @@ export default {
'parent': undefined,
removed: { $ne: true },
}, {
sort: { order: 1 }
sort: { left: 1 }
});
return Docs.find({
'parentId': this.doc._id,
removed: { $ne: true },
}, {
sort: { order: 1 }
sort: { left: 1 }
})
},
siblingDocs() {
@@ -128,7 +128,7 @@ export default {
'parentId': this.doc.parent?.id,
removed: { $ne: true },
}, {
sort: { order: 1 }
sort: { left: 1 }
});
},
editing() {

View File

@@ -167,10 +167,11 @@
style="width: 100%"
class="pa-2 no-hover"
>
<creature-properties-tree
<descendant-properties-tree
style="width: 100%;"
organize
:root="{collection, id: model._id}"
:model="model"
:root="model.root"
:collection="collection"
@selected="e => $emit('select-sub-property', e)"
/>
@@ -225,7 +226,7 @@ import InlineComputationField from '/imports/client/ui/properties/forms/shared/I
import FormSection, { FormSections } from '/imports/client/ui/properties/forms/shared/FormSection.vue';
import propertyFormIndex from '/imports/client/ui/properties/forms/shared/propertyFormIndex';
import IconColorMenu from '/imports/client/ui/properties/forms/shared/IconColorMenu.vue';
import CreaturePropertiesTree from '/imports/client/ui/creature/creatureProperties/CreaturePropertiesTree.vue';
import DescendantPropertiesTree from '/imports/client/ui/creature/creatureProperties/DescendantPropertiesTree.vue';
import OutlinedInput from '/imports/client/ui/properties/viewers/shared/OutlinedInput.vue';
import { getSuggestedChildren } from '/imports/constants/PROPERTIES';
import PROPERTIES from '/imports/constants/PROPERTIES';
@@ -243,7 +244,7 @@ export default {
FormSection,
FormSections,
IconColorMenu,
CreaturePropertiesTree,
DescendantPropertiesTree,
OutlinedInput,
...propertyFormIndex,
},

View File

@@ -106,7 +106,6 @@ import ActionConditionView from '/imports/client/ui/properties/components/action
import AttributeConsumedView from '/imports/client/ui/properties/components/actions/AttributeConsumedView.vue';
import ItemConsumedView from '/imports/client/ui/properties/components/actions/ItemConsumedView.vue';
import PropertyIcon from '/imports/client/ui/properties/shared/PropertyIcon.vue';
import RollPopup from '/imports/client/ui/components/RollPopup.vue';
import MarkdownText from '/imports/client/ui/components/MarkdownText.vue';
import CardHighlight from '/imports/client/ui/components/CardHighlight.vue';
import TreeNodeList from '/imports/client/ui/components/tree/TreeNodeList.vue';
@@ -121,7 +120,6 @@ export default {
ItemConsumedView,
MarkdownText,
PropertyIcon,
RollPopup,
CardHighlight,
TreeNodeList,
},
@@ -198,7 +196,7 @@ export default {
'ancestors.id': this.model._id,
'removed': { $ne: true },
}, {
sort: {order: 1}
sort: {left: 1}
}).map(prop => {
// Get all the props we don't want to show the decendants of and
// where they might appear in the ancestor list
@@ -225,7 +223,12 @@ export default {
this.$emit('click', e);
},
doAction() {
doAction(this.model, this.$store, this.model._id);
this.doActionLoading = true;
doAction(this.model, this.$store, this.model._id).catch((e) => {
console.error(e);
}).finally(() => {
this.doActionLoading = false;
});
},
}
}

View File

@@ -46,7 +46,7 @@ export default {
removed: {$ne: true},
inactive: {$ne: true},
}, {
sort: {order: 1},
sort: {left: 1},
fields: {equipped: false},
});
}

View File

@@ -7,16 +7,14 @@
class="ma-0"
style="min-width: 40px;"
>
<roll-popup
button-class="mr-4 py-2"
<v-btn
class="mr-4 py-2"
text
height="82"
:roll-text="numberToSignedString(model.modifier)"
:name="model.name"
:advantage="model.advantage"
:data-id="`check-btn-${model._id}`"
:loading="checkLoading"
:disabled="!context.editPermission"
@roll="check"
@click.stop="check"
>
<div>
<div class="text-h4 mod">
@@ -40,7 +38,7 @@
</template>
</div>
</div>
</roll-popup>
</v-btn>
</v-list-item-action>
<v-list-item-content>
@@ -64,15 +62,11 @@
</template>
<script lang="js">
import doCheck from '/imports/api/engine/action/methods/doCheck';
import numberToSignedString from '/imports/api/utility/numberToSignedString';
import RollPopup from '/imports/client/ui/components/RollPopup.vue';
import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue';
import doAction from '/imports/client/ui/creature/actions/doAction';
export default {
components: {
RollPopup,
},
inject: {
context: {
default: {},
@@ -96,19 +90,21 @@ export default {
click(e) {
this.$emit('click', e);
},
check({ advantage }) {
check() {
this.checkLoading = true;
doCheck.call({
propId: this.model._id,
scope: {
'~checkAdvantage': { value: advantage },
},
}, error => {
doAction(this.model, this.$store, `check-btn-${this.model._id}`, {
subtaskFn: 'check',
prop: this.model,
targetIds: [this.model.root.id],
advantage: this.model.advantage,
skillVariableName: undefined,
abilityVariableName: this.model.variableName,
dc: null,
}).catch(error => {
snackbar({ text: error.reason || error.message || error.toString() });
console.error(error);
}).finally(() => {
this.checkLoading = false;
if (error) {
console.error(error);
snackbar({ text: error.reason });
}
});
},
},
@@ -153,4 +149,3 @@ export default {
min-width: 42px;
}
</style>
../../../../../api/engine/action/methods/doCheck

View File

@@ -5,23 +5,20 @@
@mouseover="$emit('mouseover')"
@mouseleave="$emit('mouseleave')"
>
<roll-popup
<v-btn
v-if="model.attributeType === 'modifier' || model.type === 'skill'"
button-class="px-0"
class="px-0"
text
height="70"
min-width="72"
:roll-text="computedValue && computedValue.toString()"
:name="model.name"
:advantage="model.advantage"
:loading="checkLoading"
:disabled="!context.editPermission"
@roll="check"
@click.stop="check"
>
<v-card-title class="value text-h4 flex-shrink-0">
{{ computedValue }}
</v-card-title>
</roll-popup>
</v-btn>
<v-card-title
v-else
class="value text-h4 flex-shrink-0"
@@ -47,15 +44,11 @@
</template>
<script lang="js">
import RollPopup from '/imports/client/ui/components/RollPopup.vue';
import doCheck from '/imports/api/engine/action/methods/doCheck.js';
import {snackbar} from '/imports/client/ui/components/snackbars/SnackbarQueue';
import numberToSignedString from '/imports/api/utility/numberToSignedString';
import doAction from '/imports/client/ui/creature/actions/doAction';
export default {
components: {
RollPopup,
},
inject: {
context: {
default: {},
@@ -82,19 +75,21 @@ export default {
},
methods: {
signed: numberToSignedString,
check({advantage}){
check(){
this.checkLoading = true;
doCheck.call({
propId: this.model._id,
scope: {
'~checkAdvantage': { value: advantage },
},
}, error => {
doAction(this.model, this.$store, `check-btn-${this.model._id}`, {
subtaskFn: 'check',
prop: this.model,
targetIds: [this.model.root.id],
advantage: this.model.advantage,
skillVariableName: this.model.variableName,
abilityVariableName: this.model.ability,
dc: null,
}).catch(error => {
snackbar({ text: error.reason || error.message || error.toString() });
console.error(error);
}).finally(() => {
this.checkLoading = false;
if (error){
console.error(error);
snackbar({text: error.reason});
}
});
},
},
@@ -106,4 +101,4 @@ export default {
min-width: 72px;
justify-content: center;
}
</style>../../../../../api/engine/action/methods/doCheck
</style>

View File

@@ -151,8 +151,9 @@ export default {
cancelEdit() {
this.editing = false;
},
changeIncrementMenu(e) {
this.$emit('change', e);
changeIncrementMenu({ type, value }) {
if (type === 'increment') value = -value;
this.$emit('change', { type, value });
this.editing = false;
}
},

View File

@@ -1,27 +0,0 @@
<template lang="html">
<v-card class="pa-2">
<health-bar
v-for="attribute in attributes"
:key="attribute._id"
:model="attribute"
@change="e => $emit('change', {_id: attribute._id, change: e})"
@click="e => $emit('click', {_id: attribute._id})"
/>
</v-card>
</template>
<script lang="js">
import HealthBar from '/imports/client/ui/properties/components/attributes/HealthBar.vue';
export default {
components: {
HealthBar,
},
props: {
attributes: {
type: Array,
required: true
},
},
}
</script>

View File

@@ -1,75 +0,0 @@
<template lang="html">
<div
v-if="attributes.length"
class="px-2 pt-2"
>
<health-bar-card
:attributes="attributes"
@change="healthBarChanged"
@click="healthBarClicked"
/>
</div>
</template>
<script lang="js">
import Creatures from '/imports/api/creature/creatures/Creatures';
import damageProperty from '/imports/api/creature/creatureProperties/methods/damageProperty';
import HealthBarCard from '/imports/client/ui/properties/components/attributes/HealthBarCard.vue';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
export default {
components: {
HealthBarCard,
},
props: {
creatureId: {
type: String,
required: true
},
},
meteor: {
creature() {
return Creatures.findOne(this.creatureId, { fields: { settings: 1 } });
},
attributes() {
let creature = this.creature;
if (!creature) return;
let filter = {
'ancestors.id': creature._id,
type: 'attribute',
attributeType: 'healthBar',
removed: { $ne: true },
inactive: { $ne: true },
overridden: { $ne: true },
$nor: [
{ hideWhenTotalZero: true, total: 0 },
{ hideWhenValueZero: true, value: 0 },
],
};
if (creature.settings.hideUnusedStats) {
filter.hide = { $ne: true };
}
return CreatureProperties.find(filter, {
sort: { order: 1 }
});
},
},
methods: {
healthBarClicked({ _id }) {
this.$store.commit('pushDialogStack', {
component: 'creature-property-dialog',
elementId: `${_id}`,
data: { _id },
});
},
healthBarChanged({ _id, change }) {
damageProperty.call({
_id,
operation: change.type,
value: change.value
});
},
},
};
</script>

View File

@@ -64,9 +64,10 @@
</template>
<script lang="js">
import damageProperty from '/imports/api/creature/creatureProperties/methods/damageProperty';
import numberToSignedString from '/imports/api/utility/numberToSignedString';
import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue';
import doAction from '/imports/client/ui/creature/actions/doAction';
import getPropertyTitle from '/imports/client/ui/properties/shared/getPropertyTitle';
export default {
inject: {
@@ -97,14 +98,24 @@ export default {
// return true
return false;
},
damageProperty({type, value, ack}) {
damageProperty.call({
_id: this.model._id,
operation: type,
value: value
}, error => {
if (ack) ack(error);
if (error) {
damageProperty({ type, value, ack }) {
const model = this.model;
doAction(model, this.$store, model._id, {
subtaskFn: 'damageProp',
prop: model,
targetIds: [model.root.id],
params: {
title: getPropertyTitle(model),
operation: type,
value,
targetProp: model,
}
}).then(() =>{
ack?.();
}).catch((error) => {
if (ack) {
ack(error);
} else {
snackbar({ text: error.reason || error.message || error.toString() });
console.error(error);
}

View File

@@ -61,7 +61,7 @@ export default {
{ hideWhenValueZero: true, value: 0 },
],
}, {
sort: { order: 1 },
sort: { left: 1 },
}).forEach(prop => {
if (propComponents[prop.type]) {
props.push(prop);

View File

@@ -13,7 +13,7 @@
v-else-if="model.attributeType === 'hitDice'"
:model="model"
@click="$emit('click')"
@change="({ type, value }) => damageProperty({type, value: -value})"
@change="damageProperty"
/>
<health-bar
v-else-if="model.attributeType === 'healthBar'"
@@ -30,7 +30,7 @@
v-else-if="model.attributeType === 'resource'"
:model="model"
@click="$emit('click')"
@change="({ type, value, ack }) => damageProperty({type, value: -value, ack})"
@change="damageProperty"
@mouseover="hover = true"
@mouseleave="hover = false"
/>
@@ -62,8 +62,9 @@ import ResourceCardContent from '/imports/client/ui/properties/components/attrib
import AttributeCardContent from '/imports/client/ui/properties/components/attributes/AttributeCardContent.vue';
import CardHighlight from '/imports/client/ui/components/CardHighlight.vue';
import FolderGroupChildren from '/imports/client/ui/properties/components/folders/folderGroupComponents/FolderGroupChildren.vue';
import damageProperty from '/imports/api/creature/creatureProperties/methods/damageProperty';
import doAction from '/imports/client/ui/creature/actions/doAction';
import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue';
import getPropertyTitle from '/imports/client/ui/properties/shared/getPropertyTitle';
export default {
components: {
@@ -91,20 +92,30 @@ export default {
hover: false,
}},
methods: {
damageProperty(change) {
damageProperty.call({
_id: this.model._id,
operation: change.type,
value: change.value
}, e => {
console.log(change);
change.ack?.(e);
damageProperty({value, type, ack}) {
const model = this.model;
if (type === 'increment') value = -value;
doAction(model, this.$store, model._id, {
subtaskFn: 'damageProp',
prop: model,
targetIds: [model.root.id],
params: {
title: getPropertyTitle(model),
operation: type,
value,
targetProp: model,
}
}).then(() =>{
ack?.();
}).catch((error) => {
if (ack) {
ack(error);
} else {
snackbar({ text: error.reason || error.message || error.toString() });
console.error(error);
}
});
},
log({_id}) {
console.log(...arguments)
this.$emit('click-property', { _id });
}
}
}
</script>

View File

@@ -54,7 +54,7 @@ export default {
{ hideWhenValueZero: true, value: 0 },
],
}, {
sort: { order: 1 },
sort: { left: 1 },
}).forEach(prop => {
if (propComponents[prop.type]) {
props.push(prop);

View File

@@ -48,14 +48,14 @@ export default {
removed: { $ne: true },
inactive: { $ne: true },
}, {
sort: { order: 1 }
sort: { left: 1 }
});
const slotIds = slots.map(s => s._id);
const slotChildren = CreatureProperties.find({
'parentId': { $in: slotIds },
removed: { $ne: true },
}, {
sort: { order: 1 },
sort: { left: 1 },
});
const tree = nodeArrayToTree([
...slots.fetch(),

View File

@@ -101,7 +101,7 @@ export default {
deactivatedByAncestor: { $ne: true },
deactivatedByToggle: { $ne: true },
}, {
sort: { order: 1 },
sort: { left: 1 },
});
},
}

View File

@@ -10,6 +10,7 @@
v-if="!hideModifier"
text
tile
:loading="checkLoading"
:disabled="!context.editPermission"
:data-id="`check-btn-${model._id}`"
class="pl-3 pr-2 prof-mod mr-1 flex-shrink-0"
@@ -58,6 +59,7 @@
import ProficiencyIcon from '/imports/client/ui/properties/shared/ProficiencyIcon.vue';
import numberToSignedString from '/imports/api/utility/numberToSignedString';
import doAction from '/imports/client/ui/creature/actions/doAction';
import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue';
export default {
components: {
@@ -101,6 +103,7 @@ export default {
this.$emit('click', e);
},
check() {
this.checkLoading = true;
doAction(this.model, this.$store, `check-btn-${this.model._id}`, {
subtaskFn: 'check',
prop: this.model,
@@ -109,9 +112,12 @@ export default {
skillVariableName: this.model.variableName,
abilityVariableName: this.model.ability,
dc: null,
}).catch(e => {
console.error(e);
})
}).catch(error => {
snackbar({ text: error.reason || error.message || error.toString() });
console.error(error);
}).finally(() => {
this.checkLoading = false;
});
},
}
}

View File

@@ -159,20 +159,7 @@
>
Cancel
</v-btn>
<roll-popup
v-if="selectedSpell && selectedSpell.attackRoll"
text
color="primary"
class="mx-2"
:disabled="!canCast"
:name="selectedSpell.name"
:advantage="selectedSpell.attackRoll && selectedSpell.attackRoll.advantage"
@roll="cast"
>
Cast
</roll-popup>
<v-btn
v-else
text
:disabled="!canCast"
class="mx-2 px-4"
@@ -192,7 +179,6 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
import spellsWithSubheaders from '/imports/client/ui/properties/components/spells/spellsWithSubheaders';
import SpellSlotListTile from '/imports/client/ui/properties/components/attributes/SpellSlotListTile.vue';
import SpellListTile from '/imports/client/ui/properties/components/spells/SpellListTile.vue';
import RollPopup from '/imports/client/ui/components/RollPopup.vue';
import { find } from 'lodash';
const slotFilter = {
@@ -207,7 +193,6 @@ const slotFilter = {
export default {
components: {
DialogBase,
RollPopup,
SplitListLayout,
SpellSlotListTile,
SpellListTile,
@@ -403,7 +388,7 @@ export default {
};
}
return CreatureProperties.find(filter, {
sort: { order: 1 }
sort: { left: 1 }
});
},
spellSlots() {

View File

@@ -15,7 +15,7 @@ export default function createListOfProperties(filter = {}, getNamesWithValues)
});
}
}
let options = { sort: { order: 1, variableName: 1 } }
let options = { sort: { left: 1, variableName: 1 } }
CreatureProperties.find(filter, options).forEach(addUniquePropertys);
LibraryNodes.find(filter, options).forEach(addUniquePropertys);
if (getNamesWithValues) return propertyList;

View File

@@ -134,9 +134,9 @@
name="Child properties"
:cols="{cols: 12}"
>
<creature-properties-tree
<descendant-properties-tree
style="width: 100%;"
:root="{collection, id: model._id}"
:model="model"
:collection="collection"
@length="childrenLength = $event"
@selected="selectSubProperty"
@@ -155,14 +155,14 @@ import CreaturePropertiesTree from '/imports/client/ui/creature/creatureProperti
import PropertyField from '/imports/client/ui/properties/viewers/shared/PropertyField.vue';
import { getPropertyName } from '/imports/constants/PROPERTIES';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
import TreeNodeView from '/imports/client/ui/properties/treeNodeViews/TreeNodeView.vue';
import DescendantPropertiesTree from '/imports/client/ui/creature/creatureProperties/DescendantPropertiesTree.vue';
export default {
components: {
...propertyViewerIndex,
CreaturePropertiesTree,
PropertyField,
TreeNodeView,
DescendantPropertiesTree,
},
props: {
model: {

View File

@@ -141,12 +141,13 @@
import propertyViewerMixin from '/imports/client/ui/properties/viewers/shared/propertyViewerMixin'
import numberToSignedString from '../../../../api/utility/numberToSignedString';
import AttributeEffect from '/imports/client/ui/properties/components/attributes/AttributeEffect.vue';
import damageProperty from '/imports/api/creature/creatureProperties/methods/damageProperty';
import IncrementButton from '/imports/client/ui/components/IncrementButton.vue';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties';
import getProficiencyIcon from '/imports/client/ui/utility/getProficiencyIcon';
import {snackbar} from '/imports/client/ui/components/snackbars/SnackbarQueue';
import sortEffects from '/imports/client/ui/utility/sortEffects';
import doAction from '/imports/client/ui/creature/actions/doAction';
import getPropertyTitle from '/imports/client/ui/properties/shared/getPropertyTitle';
export default {
components: {
@@ -208,18 +209,24 @@
data: {_id: id},
});
},
damageProperty({type, value}) {
damageProperty({ type, value }) {
const model = this.model;
this.damagePropertyLoading = true;
damageProperty.call({
_id: this.model._id,
operation: type,
value: value
}, error => {
this.damagePropertyLoading = false;
if (error){
snackbar({text: error.reason});
console.error(error);
doAction(model, this.$store, model._id, {
subtaskFn: 'damageProp',
prop: model,
targetIds: [model.root.id],
params: {
title: getPropertyTitle(model),
operation: type,
value,
targetProp: model,
}
}).catch((error) => {
snackbar({ text: error.reason || error.message || error.toString() });
console.error(error);
}).finally(() => {
this.damagePropertyLoading = false;
});
},
},

View File

@@ -193,7 +193,7 @@ export default {
return CreatureProperties.find({
_id: {$in: this.model.proficiencyIds},
}, {
sort: {order: 1}
sort: {left: 1}
}).fetch();
},
ability() {

View File

@@ -10,36 +10,7 @@
class="avatar"
:style="{ opacity: active ? '' : '0.5'}"
>
<roll-popup
v-if="rollBonus"
:icon="!active"
:outlined="!active"
:fab="active"
style="letter-spacing: normal;"
class="mr-2"
:no-click="!active"
:style="{
fontSize: active ? '24px' : '16px'
}"
:large="active"
:color="model.color || 'primary'"
:loading="doActionLoading"
:disabled="model.insufficientResources || !context.editPermission || !!targetingError"
:roll-text="rollBonus"
:name="model.name"
:advantage="model.attackRoll && model.attackRoll.advantage"
@roll="doAction"
>
<template v-if="rollBonus && !rollBonusTooLong">
{{ rollBonus }}
</template>
<property-icon
v-else
:model="model"
/>
</roll-popup>
<v-btn
v-else
:icon="!active"
:outlined="!active"
:fab="active"
@@ -157,7 +128,6 @@ import numberToSignedString from '/imports/api/utility/numberToSignedString.js';
import AttributeConsumedView from '/imports/client/ui/properties/components/actions/AttributeConsumedView.vue';
import ItemConsumedView from '/imports/client/ui/properties/components/actions/ItemConsumedView.vue';
import PropertyIcon from '/imports/client/ui/properties/shared/PropertyIcon.vue';
import RollPopup from '/imports/client/ui/components/RollPopup.vue';
import MarkdownText from '/imports/client/ui/components/MarkdownText.vue';
import { snackbar } from '/imports/client/ui/components/snackbars/SnackbarQueue.js';
import CardHighlight from '/imports/client/ui/components/CardHighlight.vue';
@@ -172,7 +142,6 @@ export default {
ItemConsumedView,
MarkdownText,
PropertyIcon,
RollPopup,
CardHighlight,
TreeNodeList,
},
@@ -248,7 +217,7 @@ export default {
'ancestors.id': this.model._id,
'removed': { $ne: true },
}, {
sort: {order: 1}
sort: {left: 1}
}).map(prop => {
// Get all the props we don't want to show the decendants of and
// where they might appear in the ancestor list

View File

@@ -149,7 +149,7 @@ const getProperties = function (creatureId, selector = {}) {
],
...selector,
}, {
sort: { order: 1 }
sort: { left: 1 }
});
}

View File

@@ -327,7 +327,7 @@ export default {
const propsById = {};
const props = [];
CreatureProperties.find(filter, {
sort: { order: -1 },
sort: { left: -1 },
fields: { _id: 1, type: 1 },
}).forEach(prop => {
props.push(prop);

View File

@@ -243,7 +243,7 @@ Meteor.publish('libraryNodes', function (libraryId, extraFields) {
LibraryNodes.find({
'root.id': libraryId,
}, {
sort: { order: 1 },
sort: { left: 1 },
fields,
}),
];
@@ -288,7 +288,7 @@ Meteor.publish('softRemovedLibraryNodes', function (libraryId) {
removed: true,
removedWith: { $exists: false },
}, {
sort: { order: 1 },
sort: { left: 1 },
}),
];
});
@@ -309,7 +309,7 @@ Meteor.publish('descendantLibraryNodes', function (nodeId) {
LibraryNodes.find({
'ancestors.id': nodeId,
}, {
sort: { order: 1 },
sort: { left: 1 },
}),
];
});