Compare commits

..

17 Commits

Author SHA1 Message Date
Stefan Zermatten
b6ed9ffb74 Merge branch 'version-2-dev' into version-2 2022-08-25 15:24:25 +02:00
Stefan Zermatten
a84da7d8a5 Buffs can skip var freezing, freeze inline calcs 2022-08-25 15:10:56 +02:00
Stefan Zermatten
249aebea0f Allowed some properties to return damaged action values
When a prop is damaged during an action, it now tries
to show its new value during the rest of that action
2022-08-25 15:10:36 +02:00
Stefan Zermatten
11a527481e Show Point buy on Build tab 2022-08-25 13:18:24 +02:00
Stefan Zermatten
8d729216b5 Properties now have their variable name as a default tag 2022-08-25 12:15:12 +02:00
Stefan Zermatten
1677e8c424 Fixed silence missing from trigger form 2022-08-25 12:14:32 +02:00
Stefan Zermatten
987aacbb67 Silence for triggers also 2022-08-25 12:12:07 +02:00
Stefan Zermatten
2714d0b9d5 Added the ability to silence most action props 2022-08-25 12:10:51 +02:00
Stefan Zermatten
1d98c41168 Fixed slotLevel not having the right value in spell scope 2022-08-25 11:40:30 +02:00
Stefan Zermatten
e42ec4b862 Continued work on point buy UI 2022-08-23 14:44:35 +02:00
Stefan Zermatten
59fc5ab851 Continued work on point buy UI 2022-08-22 15:07:40 +02:00
Stefan Zermatten
5d14c392e8 Added creature new variables to API 2022-08-22 11:58:48 +02:00
Stefan Zermatten
c6ca8c1fa4 Added point buy to computation engine 2022-08-19 14:03:12 +02:00
Stefan Zermatten
28307e26c3 Fixed some issues with skill display 2022-08-19 14:03:03 +02:00
Stefan Zermatten
6d42eb62f0 Merge branch 'version-2' into version-2-dev 2022-08-19 09:18:55 +02:00
Stefan Zermatten
877c9ca099 Fixed cache bashing in checks
Cache should only return clones of data,
not references to the cached data
2022-08-17 17:21:18 +02:00
Stefan Zermatten
9b652fc133 Added point buy form 2022-08-17 13:42:47 +02:00
68 changed files with 1261 additions and 271 deletions

View File

@@ -24,7 +24,7 @@ const damageProperty = new ValidatedMethod({
run({ _id, operation, value }) { run({ _id, operation, value }) {
// Get action context // Get action context
const prop = CreatureProperties.findOne(_id); let prop = CreatureProperties.findOne(_id);
if (!prop) throw new Meteor.Error( if (!prop) throw new Meteor.Error(
'Damage property failed', 'Property doesn\'t exist' 'Damage property failed', 'Property doesn\'t exist'
); );
@@ -42,6 +42,14 @@ const damageProperty = new ValidatedMethod({
`Property of type "${prop.type}" can't be damaged` `Property of type "${prop.type}" can't be damaged`
); );
} }
// Replace the prop by its actionContext counterpart if possible
if (prop.variableName) {
const actionContextProp = actionContext.scope[prop.variableName];
if (actionContextProp?._id === prop._id) {
prop = actionContextProp;
}
}
const result = damagePropertyWork({ prop, operation, value, actionContext }); const result = damagePropertyWork({ prop, operation, value, actionContext });
@@ -94,6 +102,9 @@ export function damagePropertyWork({ prop, operation, value, actionContext }) {
}, { }, {
selector: prop selector: prop
}); });
// Also write it straight to the prop so that it is updated in the actionContext
prop.damage = damage;
prop.value = newValue;
} else if (operation === 'increment'){ } else if (operation === 'increment'){
let currentValue = prop.value || 0; let currentValue = prop.value || 0;
let currentDamage = prop.damage || 0; let currentDamage = prop.damage || 0;
@@ -111,6 +122,9 @@ export function damagePropertyWork({ prop, operation, value, actionContext }) {
}, { }, {
selector: prop selector: prop
}); });
// Also write it straight to the prop so that it is updated in the actionContext
prop.damage += increment;
prop.value -= increment;
} }
applyTriggers(actionContext.triggers?.damageProperty?.after, prop, actionContext); applyTriggers(actionContext.triggers?.damageProperty?.after, prop, actionContext);

View File

@@ -20,7 +20,7 @@ export default function applyAction(node, actionContext) {
recalculateInlineCalculations(prop.summary, actionContext); recalculateInlineCalculations(prop.summary, actionContext);
content.value = prop.summary.value; content.value = prop.summary.value;
} }
actionContext.addLog(content); if (!prop.silent) actionContext.addLog(content);
// Spend the resources // Spend the resources
const failed = spendResources(prop, actionContext); const failed = spendResources(prop, actionContext);
@@ -188,7 +188,7 @@ function applyChildren(node, actionContext) {
function spendResources(prop, actionContext){ function spendResources(prop, actionContext){
// Check Uses // Check Uses
if (prop.usesLeft <= 0){ if (prop.usesLeft <= 0){
actionContext.addLog({ if (!prop.silent) actionContext.addLog({
name: 'Error', name: 'Error',
value: `${prop.name || 'action'} does not have enough uses left`, value: `${prop.name || 'action'} does not have enough uses left`,
}); });
@@ -196,7 +196,7 @@ function spendResources(prop, actionContext){
} }
// Resources // Resources
if (prop.insufficientResources){ if (prop.insufficientResources){
actionContext.addLog({ if (!prop.silent) actionContext.addLog({
name: 'Error', name: 'Error',
value: 'This creature doesn\'t have sufficient resources to perform this action', value: 'This creature doesn\'t have sufficient resources to perform this action',
}); });
@@ -257,7 +257,7 @@ function spendResources(prop, actionContext){
}, { }, {
selector: prop selector: prop
}); });
actionContext.addLog({ if (!prop.silent) actionContext.addLog({
name: 'Uses left', name: 'Uses left',
value: prop.usesLeft - 1, value: prop.usesLeft - 1,
inline: true, inline: true,
@@ -288,12 +288,12 @@ function spendResources(prop, actionContext){
}); });
// Log all the spending // Log all the spending
if (gainLog.length) actionContext.addLog({ if (gainLog.length && !prop.silent) actionContext.addLog({
name: 'Gained', name: 'Gained',
value: gainLog.join('\n'), value: gainLog.join('\n'),
inline: true, inline: true,
}); });
if (spendLog.length) actionContext.addLog({ if (spendLog.length && !prop.silent) actionContext.addLog({
name: 'Spent', name: 'Spent',
value: spendLog.join('\n'), value: spendLog.join('\n'),
inline: true, inline: true,

View File

@@ -24,7 +24,7 @@ export default function applyAdjustment(node, actionContext){
damageTargets.forEach(target => { damageTargets.forEach(target => {
let stat = target.variables[prop.stat]; let stat = target.variables[prop.stat];
if (!stat?.type) { if (!stat?.type) {
actionContext.addLog({ if (!prop.silent) actionContext.addLog({
name: 'Error', name: 'Error',
value: `Could not apply attribute damage, creature does not have \`${prop.stat}\` set` value: `Could not apply attribute damage, creature does not have \`${prop.stat}\` set`
}); });
@@ -36,7 +36,7 @@ export default function applyAdjustment(node, actionContext){
value, value,
actionContext, actionContext,
}); });
actionContext.addLog({ if (!prop.silent) actionContext.addLog({
name: 'Attribute damage', name: 'Attribute damage',
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` + value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
` ${value}`, ` ${value}`,
@@ -44,7 +44,7 @@ export default function applyAdjustment(node, actionContext){
}); });
}); });
} else { } else {
actionContext.addLog({ if (!prop.silent) actionContext.addLog({
name: 'Attribute damage', name: 'Attribute damage',
value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` + value: `${prop.stat}${prop.operation === 'set' ? ' set to' : ''}` +
` ${value}`, ` ${value}`,

View File

@@ -36,25 +36,25 @@ export default function applyBranch(node, actionContext){
break; break;
case 'hit': case 'hit':
if (scope['$attackHit']?.value){ if (scope['$attackHit']?.value){
if (!targets.length) actionContext.addLog({value: '**On hit**'}); if (!targets.length && !prop.silent) actionContext.addLog({value: '**On hit**'});
applyChildren(); applyChildren();
} }
break; break;
case 'miss': case 'miss':
if (scope['$attackMiss']?.value){ if (scope['$attackMiss']?.value){
if (!targets.length) actionContext.addLog({value: '**On miss**'}); if (!targets.length && !prop.silent) actionContext.addLog({value: '**On miss**'});
applyChildren(); applyChildren();
} }
break; break;
case 'failedSave': case 'failedSave':
if (scope['$saveFailed']?.value){ if (scope['$saveFailed']?.value){
if (!targets.length) actionContext.addLog({value: '**On failed save**'}); if (!targets.length && !prop.silent) actionContext.addLog({value: '**On failed save**'});
applyChildren(); applyChildren();
} }
break; break;
case 'successfulSave': case 'successfulSave':
if (scope['$saveSucceeded']?.value){ if (scope['$saveSucceeded']?.value){
if (!targets.length) actionContext.addLog({value: '**On save**',}); if (!targets.length && !prop.silent) actionContext.addLog({value: '**On save**',});
applyChildren(); applyChildren();
} }
break; break;

View File

@@ -13,6 +13,7 @@ import logErrors from './shared/logErrors.js';
import { insertCreatureLog } from '/imports/api/creature/log/CreatureLogs.js'; import { insertCreatureLog } from '/imports/api/creature/log/CreatureLogs.js';
import cyrb53 from '/imports/api/engine/computation/utility/cyrb53.js'; import cyrb53 from '/imports/api/engine/computation/utility/cyrb53.js';
import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js'; import { applyNodeTriggers } from '/imports/api/engine/actions/applyTriggers.js';
import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX.js';
export default function applyBuff(node, actionContext){ export default function applyBuff(node, actionContext){
applyNodeTriggers(node, 'before', actionContext); applyNodeTriggers(node, 'before', actionContext);
@@ -32,7 +33,9 @@ export default function applyBuff(node, actionContext){
}); });
} }
addChildrenToPropList(node.children); addChildrenToPropList(node.children);
crystalizeVariables({propList, actionContext}); if (!prop.skipCrystalization) {
crystalizeVariables({propList, actionContext});
}
let oldParent = { let oldParent = {
id: prop.parent.id, id: prop.parent.id,
@@ -43,7 +46,7 @@ export default function applyBuff(node, actionContext){
copyNodeListToTarget(propList, target, oldParent); copyNodeListToTarget(propList, target, oldParent);
//Log the buff //Log the buff
if (prop.name || prop.description?.value){ if ((prop.name || prop.description?.value) && !prop.silent){
if (target._id === actionContext.creature._id){ if (target._id === actionContext.creature._id){
// Targeting self // Targeting self
actionContext.addLog({ actionContext.addLog({
@@ -96,6 +99,7 @@ function crystalizeVariables({propList, actionContext}){
delete prop._skipCrystalize; delete prop._skipCrystalize;
return; return;
} }
// Iterate through all the calculations and crystalize them
computedSchemas[prop.type].computedFields().forEach( calcKey => { computedSchemas[prop.type].computedFields().forEach( calcKey => {
applyFnToKey(prop, calcKey, (prop, key) => { applyFnToKey(prop, calcKey, (prop, key) => {
const calcObj = get(prop, key); const calcObj = get(prop, key);
@@ -132,5 +136,36 @@ function crystalizeVariables({propList, actionContext}){
calcObj.hash = cyrb53(calcObj.calculation); calcObj.hash = cyrb53(calcObj.calculation);
}); });
}); });
// For each key in the schema
computedSchemas[prop.type].inlineCalculationFields().forEach( calcKey => {
// That ends in .inlineCalculations
applyFnToKey(prop, calcKey, (prop, key) => {
const inlineCalcObj = get(prop, key);
if (!inlineCalcObj) return;
// If there is no text, skip
if (!inlineCalcObj.text){
return;
}
// Replace all the existing calculations
let index = -1;
inlineCalcObj.text = inlineCalcObj.text.replace(INLINE_CALCULATION_REGEX, () => {
index += 1;
return `{${inlineCalcObj.inlineCalculations[index].calculation}}`;
});
// Set the value to the uncomputed string
inlineCalcObj.value = inlineCalcObj.text;
// Write a new hash
const inlineCalcHash = cyrb53(inlineCalcObj.text);
if (inlineCalcHash === inlineCalcObj.hash) {
// Skip if nothing changed
return;
}
inlineCalcObj.hash = inlineCalcHash;
});
});
}); });
} }

View File

@@ -13,7 +13,7 @@ export default function applyBuffRemover(node, actionContext) {
const prop = node.node; const prop = node.node;
// Log Name // Log Name
if (prop.name){ if (prop.name && !prop.silent){
actionContext.addLog({ name: prop.name }); actionContext.addLog({ name: prop.name });
} }
@@ -29,7 +29,7 @@ export default function applyBuffRemover(node, actionContext) {
}); });
return; return;
} }
removeBuff(nearestBuff, actionContext); removeBuff(nearestBuff, actionContext, prop);
} else { } else {
// Get all the buffs targeted by tags // Get all the buffs targeted by tags
const allBuffs = getPropertiesOfType(actionContext.creature._id, 'buff'); const allBuffs = getPropertiesOfType(actionContext.creature._id, 'buff');
@@ -41,7 +41,7 @@ export default function applyBuffRemover(node, actionContext) {
if (prop.removeAll) { if (prop.removeAll) {
// Remove all matching buffs // Remove all matching buffs
targetedBuffs.forEach(buff => { targetedBuffs.forEach(buff => {
removeBuff(buff, actionContext); removeBuff(buff, actionContext, prop);
}); });
} else { } else {
// Sort in reverse order // Sort in reverse order
@@ -49,7 +49,7 @@ export default function applyBuffRemover(node, actionContext) {
// Remove the one with the highest order // Remove the one with the highest order
const buff = targetedBuffs[0]; const buff = targetedBuffs[0];
if (buff) { if (buff) {
removeBuff(buff, actionContext); removeBuff(buff, actionContext, prop);
} }
} }
} }
@@ -60,8 +60,8 @@ export default function applyBuffRemover(node, actionContext) {
node.children.forEach(child => applyProperty(child, actionContext)); node.children.forEach(child => applyProperty(child, actionContext));
} }
function removeBuff(buff, actionContext) { function removeBuff(buff, actionContext, prop) {
actionContext.addLog({ if (!prop.silent) actionContext.addLog({
name: 'Removed', name: 'Removed',
value: `${buff.name || 'Buff'}` value: `${buff.name || 'Buff'}`
}); });

View File

@@ -128,7 +128,7 @@ export default function applyDamage(node, actionContext){
// There are no targets, just log the result // There are no targets, just log the result
logValue.push(`**${damage}** ${suffix}`); logValue.push(`**${damage}** ${suffix}`);
} }
actionContext.addLog({ if (!prop.silent) actionContext.addLog({
name: logName, name: logName,
value: logValue.join('\n'), value: logValue.join('\n'),
inline: true, inline: true,
@@ -219,6 +219,16 @@ function dealDamage({target, damageType, amount, actionContext}){
if (damageType === 'healing') damageLeft = -totalDamage; if (damageType === 'healing') damageLeft = -totalDamage;
healthBars.forEach(healthBar => { healthBars.forEach(healthBar => {
if (damageLeft === 0) return; if (damageLeft === 0) return;
// Replace the healthbar by the one in the action context if we can
// The damagePropertyWork function bashes the prop with the damage
// So we can use the new value in later action properties
if (healthBar.variableName) {
const targetHealthBar = target.variables[healthBar.variableName];
if (targetHealthBar?._id === healthBar._id) {
healthBar = targetHealthBar;
}
}
// Do the damage
let damageAdded = damagePropertyWork({ let damageAdded = damagePropertyWork({
prop: healthBar, prop: healthBar,
operation: 'increment', operation: 'increment',

View File

@@ -20,7 +20,7 @@ export default function applySavingThrow(node, actionContext){
}); });
return node.children.forEach(child => applyProperty(child, actionContext)); return node.children.forEach(child => applyProperty(child, actionContext));
} }
actionContext.addLog({ if (!prop.silent) actionContext.addLog({
name: prop.name, name: prop.name,
value: `DC **${dc}**`, value: `DC **${dc}**`,
inline: true, inline: true,
@@ -94,7 +94,7 @@ export default function applySavingThrow(node, actionContext){
} else { } else {
scope['$saveFailed'] = {value: true}; scope['$saveFailed'] = {value: true};
} }
actionContext.addLog({ if (!prop.silent) actionContext.addLog({
name: saveSuccess ? 'Successful save' : 'Failed save', name: saveSuccess ? 'Successful save' : 'Failed save',
value: resultPrefix + '\n**' + result + '**', value: resultPrefix + '\n**' + result + '**',
inline: true, inline: true,

View File

@@ -65,7 +65,7 @@ export function applyTrigger(trigger, prop, actionContext) {
recalculateInlineCalculations(trigger.description, actionContext); recalculateInlineCalculations(trigger.description, actionContext);
content.value = trigger.description.value; content.value = trigger.description.value;
} }
actionContext.addLog(content); if(!trigger.silent) actionContext.addLog(content);
// Get all the trigger's properties and apply them // Get all the trigger's properties and apply them
const properties = getPropertyDecendants(actionContext.creature._id, trigger._id); const properties = getPropertyDecendants(actionContext.creature._id, trigger._id);

View File

@@ -9,7 +9,6 @@ import CreatureProperties from '/imports/api/creature/creatureProperties/Creatur
import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; import { assertEditPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js'; import { damagePropertyWork } from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import { doActionWork } from '/imports/api/engine/actions/doAction.js'; import { doActionWork } from '/imports/api/engine/actions/doAction.js';
import { CreatureLogSchema } from '/imports/api/creature/log/CreatureLogs.js';
import ActionContext from '/imports/api/engine/actions/ActionContext.js'; import ActionContext from '/imports/api/engine/actions/ActionContext.js';
const doAction = new ValidatedMethod({ const doAction = new ValidatedMethod({
@@ -65,8 +64,6 @@ const doAction = new ValidatedMethod({
let slotLevel = spell.level || 0; let slotLevel = spell.level || 0;
let slot; let slot;
actionContext.scope['slotLevel'] = slotLevel;
if (slotId && !spell.castWithoutSpellSlots){ if (slotId && !spell.castWithoutSpellSlots){
slot = CreatureProperties.findOne(slotId); slot = CreatureProperties.findOne(slotId);
if (!slot){ if (!slot){
@@ -109,6 +106,8 @@ const doAction = new ValidatedMethod({
}); });
} }
actionContext.scope['slotLevel'] = slotLevel;
// Do the action // Do the action
doActionWork({ doActionWork({
properties, ancestors, actionContext, methodScope: scope, properties, ancestors, actionContext, methodScope: scope,

View File

@@ -14,6 +14,7 @@ const linkDependenciesByType = {
effect: linkEffects, effect: linkEffects,
proficiency: linkProficiencies, proficiency: linkProficiencies,
roll: linkRoll, roll: linkRoll,
pointBuy: linkPointBuy,
propertySlot: linkSlot, propertySlot: linkSlot,
skill: linkSkill, skill: linkSkill,
spell: linkAction, spell: linkAction,
@@ -242,6 +243,28 @@ function linkDamageMultiplier(dependencyGraph, prop) {
}); });
} }
function linkPointBuy(dependencyGraph, prop){
dependOnCalc({ dependencyGraph, prop, key: 'min' });
dependOnCalc({ dependencyGraph, prop, key: 'max' });
dependOnCalc({ dependencyGraph, prop, key: 'cost' });
dependOnCalc({ dependencyGraph, prop, key: 'total' });
prop.values?.forEach(row => {
// Wrap the document in a new object so we don't bash it unintentionally
const pointBuyRow = {
...row,
type: 'pointBuyRow',
tableName: prop.name,
tableId: prop._id,
}
dependencyGraph.addNode(row._id, pointBuyRow);
linkVariableName(dependencyGraph, pointBuyRow);
dependOnCalc({ dependencyGraph, pointBuyRow, key: 'row.min' });
dependOnCalc({ dependencyGraph, pointBuyRow, key: 'row.max' });
dependOnCalc({ dependencyGraph, pointBuyRow, key: 'row.cost' });
});
if (prop.inactive) return;
}
function linkProficiencies(dependencyGraph, prop){ function linkProficiencies(dependencyGraph, prop){
// The stats depend on the proficiency // The stats depend on the proficiency
if (prop.inactive) return; if (prop.inactive) return;

View File

@@ -2,6 +2,7 @@ import _variable from './computeByType/computeVariable.js';
import action from './computeByType/computeAction.js'; import action from './computeByType/computeAction.js';
import attribute from './computeByType/computeAttribute.js'; import attribute from './computeByType/computeAttribute.js';
import skill from './computeByType/computeSkill.js'; import skill from './computeByType/computeSkill.js';
import pointBuy from './computeByType/computePointBuy.js';
import propertySlot from './computeByType/computeSlot.js'; import propertySlot from './computeByType/computeSlot.js';
import container from './computeByType/computeContainer.js'; import container from './computeByType/computeContainer.js';
import _calculation from './computeByType/computeCalculation.js'; import _calculation from './computeByType/computeCalculation.js';
@@ -13,6 +14,7 @@ export default Object.freeze({
attribute, attribute,
container, container,
skill, skill,
pointBuy,
propertySlot, propertySlot,
spell: action, spell: action,
}); });

View File

@@ -0,0 +1,53 @@
import { has } from 'lodash';
import evaluateCalculation from '../../utility/evaluateCalculation.js';
export default function computePointBuy(computation, node) {
const prop = node.data;
const tableMin = prop.min?.value || null;
const tableMax = prop.max?.value || null;
prop.spent = 0;
prop.values?.forEach(row => {
// Clean up added properties
// delete row.tableId;
// delete row.tableName;
// delete row.type;
row.spent = 0;
if (row.value === undefined) return;
const min = has(row, 'min.value') ? row.min.value : tableMin;
const max = has(row, 'max.value') ? row.max.value : tableMax;
const costFunction = EJSON.clone(row.cost || prop.cost);
if (costFunction) costFunction.parseLevel = 'reduce';
// Check min and max
if (min !== null && row.value < min) {
row.value = min;
}
if (max !== null && row.value > max) {
row.value = max;
}
// Evaluate the cost function
if (!costFunction) return;
evaluateCalculation(costFunction, { ...computation.scope, value: row.value });
// Write calculation errors
costFunction.errors?.forEach(error => {
if (error?.message) {
row.errors = row.errors || [];
error.message = 'Cost calculation error.\n' + error.message;
row.errors.push(error);
}
});
if (Number.isFinite(costFunction.value)) {
row.spent = costFunction.value;
prop.spent += costFunction.value;
}
});
prop.pointsLeft = (prop.total?.value || 0) - (prop.spent || 0);
if (prop.spent > prop.total?.value) {
prop.errors = prop.errors || [];
prop.errors.push({
type: 'pointBuyError',
message: 'Spent more than total points available',
});
}
}

View File

@@ -4,7 +4,7 @@
// by computeVariableAsSkill // by computeVariableAsSkill
export default function computeSkill(computation, node){ export default function computeSkill(computation, node){
const prop = node.data; const prop = node.data;
prop.proficiency = prop.baseProficiency; prop.proficiency = prop.baseProficiency || 0;
let profBonus = computation.scope['proficiencyBonus']?.value || 0; let profBonus = computation.scope['proficiencyBonus']?.value || 0;
// Multiply the proficiency bonus by the actual proficiency // Multiply the proficiency bonus by the actual proficiency
if(prop.proficiency === 0.49){ if(prop.proficiency === 0.49){

View File

@@ -8,7 +8,13 @@ export default function aggregateDefinition({node, linkedNode, link}){
// get current defining prop // get current defining prop
const definingProp = node.data.definingProp; const definingProp = node.data.definingProp;
// Find the last defining prop // Find the last defining prop
if (!definingProp || prop.order > definingProp.order){ if (
!definingProp ||
prop.type !== 'pointBuyRow' && (
definingProp.type === 'pointBuyRow' ||
prop.order > definingProp.order
)
) {
// override the current defining prop // override the current defining prop
overrideProp(definingProp, node); overrideProp(definingProp, node);
// set this prop as the new defining prop // set this prop as the new defining prop
@@ -18,9 +24,32 @@ export default function aggregateDefinition({node, linkedNode, link}){
} }
// Aggregate the base value due to the defining properties // Aggregate the base value due to the defining properties
const propBaseValue = prop.baseValue?.value; let propBaseValue = prop.baseValue?.value;
// Point buy rows use prop.value instead of prop.baseValue
if (prop.type === 'pointBuyRow') {
propBaseValue = prop.value;
}
if (propBaseValue === undefined) return; if (propBaseValue === undefined) return;
// Store a summary of the definition as a base value effect
node.data.effects = node.data.effects || [];
if (prop.type === 'pointBuyRow') {
node.data.effects.push({
_id: prop.tableId,
name: prop.tableName,
operation: 'base',
amount: { value: propBaseValue },
type: 'pointBuy',
});
} else {
node.data.effects.push({
_id: prop._id,
name: prop.name,
operation: 'base',
amount: { value: propBaseValue },
type: prop.type,
});
}
if (node.data.baseValue === undefined || propBaseValue > node.data.baseValue){ if (node.data.baseValue === undefined || propBaseValue > node.data.baseValue){
node.data.baseValue = propBaseValue; node.data.baseValue = propBaseValue;
} }

View File

@@ -24,6 +24,7 @@ export default function aggregateEffect({node, linkedNode, link}){
name: linkedNode.data.name, name: linkedNode.data.name,
operation: linkedNode.data.operation, operation: linkedNode.data.operation,
amount: linkedNode.data.amount && {value: linkedNode.data.amount.value}, amount: linkedNode.data.amount && {value: linkedNode.data.amount.value},
type: linkedNode.data.type,
// ancestors: linkedNode.data.ancestors, // ancestors: linkedNode.data.ancestors,
}); });

View File

@@ -33,6 +33,9 @@ export default function computeVariableAsSkill(computation, node, prop){
const aggregator = node.data.effectAggregator; const aggregator = node.data.effectAggregator;
const aggregatorBase = aggregator?.base || 0; const aggregatorBase = aggregator?.base || 0;
// Store effects
prop.effects = node.data.effects;
// If there is no aggregator, determine if the prop can hide, then exit // If there is no aggregator, determine if the prop can hide, then exit
if (!aggregator){ if (!aggregator){
prop.hide = statBase === undefined && prop.hide = statBase === undefined &&
@@ -71,8 +74,6 @@ export default function computeVariableAsSkill(computation, node, prop){
prop.fail = aggregator.fail; prop.fail = aggregator.fail;
// Rollbonus // Rollbonus
prop.rollBonuses = aggregator.rollBonus; prop.rollBonuses = aggregator.rollBonus;
// Store effects
prop.effects = node.data.effects;
} }
function aggregateAbilityEffects({computation, skillNode, abilityNode}){ function aggregateAbilityEffects({computation, skillNode, abilityNode}){

View File

@@ -9,6 +9,7 @@ export default function getEffectivePropTags(prop) {
} }
// Tags for some string properties // Tags for some string properties
if (prop.variableName) tags.push(prop.variableName);
if (prop.damageType) tags.push(prop.damageType); if (prop.damageType) tags.push(prop.damageType);
if (prop.skillType) tags.push(prop.skillType); if (prop.skillType) tags.push(prop.skillType);
if (prop.attributeType) tags.push(prop.attributeType); if (prop.attributeType) tags.push(prop.attributeType);

View File

@@ -97,7 +97,10 @@ export function getCreature(creatureId) {
if (loadedCreatures.has(creatureId)) { if (loadedCreatures.has(creatureId)) {
const loadedCreature = loadedCreatures.get(creatureId); const loadedCreature = loadedCreatures.get(creatureId);
const creature = loadedCreature.creature; const creature = loadedCreature.creature;
if (creature) return creature; if (creature) {
const cloneCreature = EJSON.clone(creature);
return cloneCreature;
}
} }
// console.time(`Cache miss on Creature: ${creatureId}`); // console.time(`Cache miss on Creature: ${creatureId}`);
const creature = Creatures.findOne(creatureId); const creature = Creatures.findOne(creatureId);
@@ -109,7 +112,10 @@ export function getVariables(creatureId) {
if (loadedCreatures.has(creatureId)) { if (loadedCreatures.has(creatureId)) {
const loadedCreature = loadedCreatures.get(creatureId); const loadedCreature = loadedCreatures.get(creatureId);
const variables = loadedCreature.variables; const variables = loadedCreature.variables;
if (variables) return variables; if (variables) {
const cloneVarables = EJSON.clone(variables);
return cloneVarables;
}
} }
// console.time(`Cache miss on variables: ${creatureId}`); // console.time(`Cache miss on variables: ${creatureId}`);
const variables = CreatureVariables.findOne({_creatureId: creatureId}); const variables = CreatureVariables.findOne({_creatureId: creatureId});

View File

@@ -114,6 +114,11 @@ let ActionSchema = createPropertySchema({
type: 'fieldToCompute', type: 'fieldToCompute',
optional: true, optional: true,
}, },
// Prevent the property from showing up in the log
silent: {
type: Boolean,
optional: true,
},
}); });
const ComputedOnlyActionSchema = createPropertySchema({ const ComputedOnlyActionSchema = createPropertySchema({

View File

@@ -31,6 +31,11 @@ const AdjustmentSchema = createPropertySchema({
allowedValues: ['set', 'increment'], allowedValues: ['set', 'increment'],
defaultValue: 'increment', defaultValue: 'increment',
}, },
// Prevent the property from showing up in the log
silent: {
type: Boolean,
optional: true,
},
}); });
const ComputedOnlyAdjustmentSchema = createPropertySchema({ const ComputedOnlyAdjustmentSchema = createPropertySchema({

View File

@@ -176,6 +176,7 @@ let ComputedOnlyAttributeSchema = createPropertySchema({
effects: { effects: {
type: Array, type: Array,
optional: true, optional: true,
removeBeforeCompute: true,
}, },
'effects.$': { 'effects.$': {
type: Object, type: Object,

View File

@@ -37,6 +37,11 @@ let BranchSchema = createPropertySchema({
optional: true, optional: true,
parseLevel: 'compile', parseLevel: 'compile',
}, },
// Prevent the property from showing up in the log
silent: {
type: Boolean,
optional: true,
},
}); });
let ComputedOnlyBranchSchema = createPropertySchema({ let ComputedOnlyBranchSchema = createPropertySchema({

View File

@@ -68,6 +68,11 @@ let BuffRemoverSchema = createPropertySchema({
type: String, type: String,
max: STORAGE_LIMITS.tagLength, max: STORAGE_LIMITS.tagLength,
}, },
// Prevent the property from showing up in the log
silent: {
type: Boolean,
optional: true,
},
}); });
let ComputedOnlyBuffRemoverSchema = createPropertySchema({}); let ComputedOnlyBuffRemoverSchema = createPropertySchema({});

View File

@@ -29,6 +29,16 @@ let BuffSchema = createPropertySchema({
], ],
defaultValue: 'target', defaultValue: 'target',
}, },
// Prevent the property from showing up in the log
silent: {
type: Boolean,
optional: true,
},
// Prevent the children from being crystalized
skipCrystalization: {
type: Boolean,
optional: true,
},
}); });
let ComputedOnlyBuffSchema = createPropertySchema({ let ComputedOnlyBuffSchema = createPropertySchema({

View File

@@ -26,7 +26,12 @@ const DamageSchema = createPropertySchema({
max: STORAGE_LIMITS.calculation, max: STORAGE_LIMITS.calculation,
defaultValue: 'slashing', defaultValue: 'slashing',
regEx: VARIABLE_NAME_REGEX, regEx: VARIABLE_NAME_REGEX,
}, },
// Prevent the property from showing up in the log
silent: {
type: Boolean,
optional: true,
},
}); });
const ComputedOnlyDamageSchema = createPropertySchema({ const ComputedOnlyDamageSchema = createPropertySchema({

View File

@@ -2,6 +2,7 @@ import SimpleSchema from 'simpl-schema';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js';
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js'; import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js'; import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema.js';
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
/* /*
* PointBuys are reason-value attached to skills and abilities * PointBuys are reason-value attached to skills and abilities
@@ -13,13 +14,6 @@ let PointBuySchema = createPropertySchema({
optional: true, optional: true,
max: STORAGE_LIMITS.name, max: STORAGE_LIMITS.name,
}, },
variableName: {
type: String,
optional: true,
regEx: VARIABLE_NAME_REGEX,
min: 2,
max: STORAGE_LIMITS.variableName,
},
ignored: { ignored: {
type: Boolean, type: Boolean,
optional: true, optional: true,
@@ -27,10 +21,18 @@ let PointBuySchema = createPropertySchema({
'values': { 'values': {
type: Array, type: Array,
defaultValue: [], defaultValue: [],
maxCount: STORAGE_LIMITS.pointBuyRowsCount,
}, },
'values.$': { 'values.$': {
type: Object, type: Object,
}, },
'values.$._id': {
type: String,
regEx: SimpleSchema.RegEx.Id,
autoValue(){
if (!this.isSet) return Random.id();
}
},
'values.$.name': { 'values.$.name': {
type: String, type: String,
optional: true, optional: true,
@@ -47,6 +49,18 @@ let PointBuySchema = createPropertySchema({
type: Number, type: Number,
optional: true, optional: true,
}, },
'values.$.min': {
type: 'fieldToCompute',
optional: true,
},
'values.$.max': {
type: 'fieldToCompute',
optional: true,
},
'values.$.cost': {
type: 'fieldToCompute',
optional: true,
},
min: { min: {
type: 'fieldToCompute', type: 'fieldToCompute',
optional: true, optional: true,
@@ -62,6 +76,7 @@ let PointBuySchema = createPropertySchema({
cost: { cost: {
type: 'fieldToCompute', type: 'fieldToCompute',
optional: true, optional: true,
parseLevel: 'compile',
}, },
}); });
@@ -74,11 +89,46 @@ const ComputedOnlyPointBuySchema = createPropertySchema({
type: 'computedOnlyField', type: 'computedOnlyField',
optional: true, optional: true,
}, },
total: { cost: {
type: 'computedOnlyField',
optional: true,
parseLevel: 'compile',
},
'values': {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.pointBuyRowsCount,
},
'values.$': {
type: Object,
},
'values.$.min': {
type: 'computedOnlyField', type: 'computedOnlyField',
optional: true, optional: true,
}, },
cost: { 'values.$.max': {
type: 'computedOnlyField',
optional: true,
},
'values.$.cost': {
type: 'computedOnlyField',
optional: true,
parseLevel: 'compile',
},
'values.$.spent': {
type: Number,
optional: true,
removeBeforeCompute: true,
},
'values.$.errors': {
type: Array,
optional: true,
removeBeforeCompute: true,
},
'values.$.errors.$': {
type: ErrorSchema,
},
total: {
type: 'computedOnlyField', type: 'computedOnlyField',
optional: true, optional: true,
}, },
@@ -87,6 +137,19 @@ const ComputedOnlyPointBuySchema = createPropertySchema({
optional: true, optional: true,
removeBeforeCompute: true, removeBeforeCompute: true,
}, },
pointsLeft: {
type: Number,
optional: true,
removeBeforeCompute: true,
},
errors: {
type: Array,
optional: true,
removeBeforeCompute: true,
},
'errors.$': {
type: ErrorSchema,
},
}); });
const ComputedPointBuySchema = new SimpleSchema() const ComputedPointBuySchema = new SimpleSchema()

View File

@@ -30,6 +30,11 @@ let SavingThrowSchema = createPropertySchema({
optional: true, optional: true,
max: STORAGE_LIMITS.variableName, max: STORAGE_LIMITS.variableName,
}, },
// Prevent the property from showing up in the log
silent: {
type: Boolean,
optional: true,
},
}); });
const ComputedOnlySavingThrowSchema = createPropertySchema({ const ComputedOnlySavingThrowSchema = createPropertySchema({

View File

@@ -135,6 +135,7 @@ let ComputedOnlySkillSchema = createPropertySchema({
effects: { effects: {
type: Array, type: Array,
optional: true, optional: true,
removeBeforeCompute: true,
}, },
'effects.$': { 'effects.$': {
type: Object, type: Object,

View File

@@ -109,6 +109,11 @@ let TriggerSchema = createPropertySchema({
type: String, type: String,
max: STORAGE_LIMITS.tagLength, max: STORAGE_LIMITS.tagLength,
}, },
// Prevent the property from showing up in the log
silent: {
type: Boolean,
optional: true,
},
}); });
const ComputedOnlyTriggerSchema = createPropertySchema({ const ComputedOnlyTriggerSchema = createPropertySchema({

View File

@@ -16,6 +16,7 @@ import { ComputedOnlyFeatureSchema } from '/imports/api/properties/Features.js';
import { ComputedOnlyFolderSchema } from '/imports/api/properties/Folders.js'; import { ComputedOnlyFolderSchema } from '/imports/api/properties/Folders.js';
import { ComputedOnlyItemSchema } from '/imports/api/properties/Items.js'; import { ComputedOnlyItemSchema } from '/imports/api/properties/Items.js';
import { ComputedOnlyNoteSchema } from '/imports/api/properties/Notes.js'; import { ComputedOnlyNoteSchema } from '/imports/api/properties/Notes.js';
import { ComputedOnlyPointBuySchema } from '/imports/api/properties/PointBuys.js';
import { ComputedOnlyProficiencySchema } from '/imports/api/properties/Proficiencies.js'; import { ComputedOnlyProficiencySchema } from '/imports/api/properties/Proficiencies.js';
import { ComputedOnlyReferenceSchema } from '/imports/api/properties/References.js'; import { ComputedOnlyReferenceSchema } from '/imports/api/properties/References.js';
import { ComputedOnlyRollSchema } from '/imports/api/properties/Rolls.js'; import { ComputedOnlyRollSchema } from '/imports/api/properties/Rolls.js';
@@ -46,6 +47,7 @@ const propertySchemasIndex = {
folder: ComputedOnlyFolderSchema, folder: ComputedOnlyFolderSchema,
item: ComputedOnlyItemSchema, item: ComputedOnlyItemSchema,
note: ComputedOnlyNoteSchema, note: ComputedOnlyNoteSchema,
pointBuy: ComputedOnlyPointBuySchema,
proficiency: ComputedOnlyProficiencySchema, proficiency: ComputedOnlyProficiencySchema,
propertySlot: ComputedOnlySlotSchema, propertySlot: ComputedOnlySlotSchema,
reference: ComputedOnlyReferenceSchema, reference: ComputedOnlyReferenceSchema,

View File

@@ -16,6 +16,7 @@ import { ComputedFeatureSchema } from '/imports/api/properties/Features.js';
import { FolderSchema } from '/imports/api/properties/Folders.js'; import { FolderSchema } from '/imports/api/properties/Folders.js';
import { ComputedItemSchema } from '/imports/api/properties/Items.js'; import { ComputedItemSchema } from '/imports/api/properties/Items.js';
import { ComputedNoteSchema } from '/imports/api/properties/Notes.js'; import { ComputedNoteSchema } from '/imports/api/properties/Notes.js';
import { ComputedPointBuySchema } from '/imports/api/properties/PointBuys.js';
import { ProficiencySchema } from '/imports/api/properties/Proficiencies.js'; import { ProficiencySchema } from '/imports/api/properties/Proficiencies.js';
import { ReferenceSchema } from '/imports/api/properties/References.js'; import { ReferenceSchema } from '/imports/api/properties/References.js';
import { ComputedRollSchema } from '/imports/api/properties/Rolls.js'; import { ComputedRollSchema } from '/imports/api/properties/Rolls.js';
@@ -44,6 +45,7 @@ const propertySchemasIndex = {
feature: ComputedFeatureSchema, feature: ComputedFeatureSchema,
folder: FolderSchema, folder: FolderSchema,
note: ComputedNoteSchema, note: ComputedNoteSchema,
pointBuy: ComputedPointBuySchema,
proficiency: ProficiencySchema, proficiency: ProficiencySchema,
propertySlot: ComputedSlotSchema, propertySlot: ComputedSlotSchema,
reference: ReferenceSchema, reference: ReferenceSchema,

View File

@@ -14,6 +14,7 @@ import { EffectSchema } from '/imports/api/properties/Effects.js';
import { FeatureSchema } from '/imports/api/properties/Features.js'; import { FeatureSchema } from '/imports/api/properties/Features.js';
import { FolderSchema } from '/imports/api/properties/Folders.js'; import { FolderSchema } from '/imports/api/properties/Folders.js';
import { NoteSchema } from '/imports/api/properties/Notes.js'; import { NoteSchema } from '/imports/api/properties/Notes.js';
import { PointBuySchema } from '/imports/api/properties/PointBuys.js';
import { ProficiencySchema } from '/imports/api/properties/Proficiencies.js'; import { ProficiencySchema } from '/imports/api/properties/Proficiencies.js';
import { ReferenceSchema } from '/imports/api/properties/References.js'; import { ReferenceSchema } from '/imports/api/properties/References.js';
import { RollSchema } from '/imports/api/properties/Rolls.js'; import { RollSchema } from '/imports/api/properties/Rolls.js';
@@ -44,6 +45,7 @@ const propertySchemasIndex = {
feature: FeatureSchema, feature: FeatureSchema,
folder: FolderSchema, folder: FolderSchema,
note: NoteSchema, note: NoteSchema,
pointBuy: PointBuySchema,
proficiency: ProficiencySchema, proficiency: ProficiencySchema,
propertySlot: SlotSchema, propertySlot: SlotSchema,
reference: ReferenceSchema, reference: ReferenceSchema,

View File

@@ -102,7 +102,13 @@ const PROPERTIES = Object.freeze({
icon: 'mdi-note-outline', icon: 'mdi-note-outline',
name: 'Note', name: 'Note',
helpText: 'Notes about your character and their adventures', helpText: 'Notes about your character and their adventures',
suggestedParents: ['folder'], suggestedParents: ['note', 'folder'],
},
pointBuy: {
icon: 'mdi-table',
name: 'Point Buy',
helpText: 'A point buy table that allows the user to select an array of values that match a given cost',
suggestedParents: [],
}, },
proficiency: { proficiency: {
icon: 'mdi-brightness-1', icon: 'mdi-brightness-1',

View File

@@ -32,6 +32,7 @@ const STORAGE_LIMITS = Object.freeze({
tagCount: 64, tagCount: 64,
writersCount: 20, writersCount: 20,
libraryCollectionCount: 32, libraryCollectionCount: 32,
pointBuyRowsCount: 32,
}); });
export default STORAGE_LIMITS; export default STORAGE_LIMITS;

View File

@@ -1,6 +1,7 @@
import SimpleSchema from 'simpl-schema'; import SimpleSchema from 'simpl-schema';
import Creatures from '/imports/api/creature/creatures/Creatures.js'; import Creatures from '/imports/api/creature/creatures/Creatures.js';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import CreatureVariables from '/imports/api/creature/creatures/CreatureVariables';
import { assertViewPermission } from '/imports/api/creature/creatures/creaturePermissions.js'; import { assertViewPermission } from '/imports/api/creature/creatures/creaturePermissions.js';
import computeCreature from '/imports/api/engine/computeCreature.js'; import computeCreature from '/imports/api/engine/computeCreature.js';
import VERSION from '/imports/constants/VERSION.js'; import VERSION from '/imports/constants/VERSION.js';
@@ -40,6 +41,9 @@ Meteor.publish('api-creature', function(creatureId){
CreatureProperties.find({ CreatureProperties.find({
'ancestors.id': creatureId, 'ancestors.id': creatureId,
}), }),
CreatureVariables.find({
_creatureId: creatureId,
}),
]; ];
}, { }, {
url: 'api/creature/:0' url: 'api/creature/:0'

View File

@@ -0,0 +1,72 @@
<template lang="html">
<v-btn
v-bind="$attrs"
:disabled="isDisabled"
:loading="loading"
@click="click"
>
<slot />
</v-btn>
</template>
<script lang="js">
import { debounce } from 'lodash';
import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
export default {
inject: {
context: { default: {} }
},
props: {
disabled: Boolean,
debounce: {
type: Number,
default: undefined,
},
},
data() {
return {
loading: false,
timesClicked: 0,
};
},
computed: {
isDisabled(){
return this.context.editPermission === false || this.disabled;
},
debounceTime() {
if (Number.isFinite(this.debounce)){
return this.debounce;
} else if (Number.isFinite(this.context.debounceTime)){
return this.context.debounceTime;
} else {
return 750;
}
},
},
created(){
this.debounceClicks = debounce(this.clicks, this.debounceTime);
},
beforeDestroy(){
this.debounceClicks.flush();
},
methods: {
click() {
this.timesClicked += 1;
this.debounceClicks();
this.$emit('click', this.acknowledgeChange);
},
clicks() {
this.$emit('clicks', this.timesClicked, this.acknowledgeChange);
this.loading = true;
this.timesClicked = 0;
},
acknowledgeChange(error){
this.loading = false;
if (error) {
snackbar({ text: error.reason || error.message || error.toString() });
}
},
},
};
</script>

View File

@@ -115,7 +115,7 @@ export default {
}, },
change(val){ change(val){
this.dirty = true; this.dirty = true;
if (this.hasChangeListener) this.loading = true; if (this.hasChangeListener()) this.loading = true;
this.$emit('change', val, this.acknowledgeChange); this.$emit('change', val, this.acknowledgeChange);
}, },
hasChangeListener(){ hasChangeListener(){

View File

@@ -0,0 +1,34 @@
<template lang="html">
<v-slider
ref="input"
v-bind="$attrs"
class="dc-text-field"
:hide-details="!(errors && errors.length)"
:loading="loading"
:error-messages="errors"
:value="safeValue"
:disabled="isDisabled"
:outlined="!regular"
@change="change"
@focus="focused = true"
@blur="focused = false"
>
<template #prepend>
<slot name="prepend" />
</template>
<template #append>
<slot name="append" />
</template>
</v-slider>
</template>
<script lang="js">
import SmartInput from '/imports/ui/components/global/SmartInputMixin.js';
export default {
mixins: [SmartInput],
props: {
regular: Boolean,
},
};
</script>

View File

@@ -5,17 +5,21 @@ import IconPicker from '/imports/ui/components/global/IconPicker.vue';
import TextField from '/imports/ui/components/global/TextField.vue'; import TextField from '/imports/ui/components/global/TextField.vue';
import TextArea from '/imports/ui/components/global/TextArea.vue'; import TextArea from '/imports/ui/components/global/TextArea.vue';
import SmartSelect from '/imports/ui/components/global/SmartSelect.vue'; import SmartSelect from '/imports/ui/components/global/SmartSelect.vue';
import SmartBtn from '/imports/ui/components/global/SmartBtn.vue';
import SmartCombobox from '/imports/ui/components/global/SmartCombobox.vue'; import SmartCombobox from '/imports/ui/components/global/SmartCombobox.vue';
import SmartCheckbox from '/imports/ui/components/global/SmartCheckbox.vue'; import SmartCheckbox from '/imports/ui/components/global/SmartCheckbox.vue';
import SmartSwitch from '/imports/ui/components/global/SmartSwitch.vue'; import SmartSwitch from '/imports/ui/components/global/SmartSwitch.vue';
import SvgIcon from '/imports/ui/components/global/SvgIcon.vue'; import SvgIcon from '/imports/ui/components/global/SvgIcon.vue';
import SmartSlider from '/imports/ui/components/global/SmartSlider.vue';
Vue.component('DatePicker', DatePicker); Vue.component('DatePicker', DatePicker);
Vue.component('IconPicker', IconPicker); Vue.component('IconPicker', IconPicker);
Vue.component('TextField', TextField); Vue.component('TextField', TextField);
Vue.component('TextArea', TextArea); Vue.component('TextArea', TextArea);
Vue.component('SmartSelect', SmartSelect); Vue.component('SmartSelect', SmartSelect);
Vue.component('SmartBtn', SmartBtn);
Vue.component('SmartCombobox', SmartCombobox); Vue.component('SmartCombobox', SmartCombobox);
Vue.component('SmartCheckbox', SmartCheckbox); Vue.component('SmartCheckbox', SmartCheckbox);
Vue.component('SmartSlider', SmartSlider);
Vue.component('SmartSwitch', SmartSwitch); Vue.component('SmartSwitch', SmartSwitch);
Vue.component('SvgIcon', SvgIcon); Vue.component('SvgIcon', SvgIcon);

View File

@@ -55,6 +55,7 @@
/> />
<v-spacer /> <v-spacer />
<v-btn <v-btn
v-if="node.parent.id === parentSlotId"
icon icon
:disabled="context.editPermission === false" :disabled="context.editPermission === false"
@click.stop="remove(node)" @click.stop="remove(node)"
@@ -91,6 +92,7 @@
<build-tree-node-list <build-tree-node-list
v-if="showExpanded" v-if="showExpanded"
:children="computedChildren" :children="computedChildren"
:parent-slot-id="computedSlotId"
@selected="e => $emit('selected', e)" @selected="e => $emit('selected', e)"
/> />
<div v-else> <div v-else>
@@ -147,6 +149,10 @@ export default {
type: Array, type: Array,
default: () => [], default: () => [],
}, },
parentSlotId: {
type: String,
default: undefined,
},
}, },
data(){return { data(){return {
expanded: false, expanded: false,
@@ -197,6 +203,21 @@ export default {
} }
return this.children; return this.children;
}, },
computedSlotId() {
if (this.condenseChild) {
if (this.children[0].node.type === 'propertySlot') {
return this.children[0].node._id;
} else {
return undefined;
}
} else {
if (this.node.type === 'propertySlot') {
return this.node._id;
} else {
return undefined;
}
}
},
canExpand(){ canExpand(){
return !!this.computedChildren.length || this.canFillWithMany; return !!this.computedChildren.length || this.canFillWithMany;
}, },
@@ -230,41 +251,41 @@ export default {
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
.rotate-90 { .rotate-90 {
transform: rotate(90deg) translateZ(0); transform: rotate(90deg) translateZ(0);
} }
.expand-area { .expand-area {
box-shadow: -2px 0px 0px 0px #808080; box-shadow: -2px 0px 0px 0px #808080;
margin-left: 0; margin-left: 0;
} }
.handle { .handle {
cursor: move; cursor: move;
} }
.empty .drag-area { .empty .drag-area {
box-shadow: -2px 0px 0px 0px rgb(128, 128, 128, 0.4); box-shadow: -2px 0px 0px 0px rgb(128, 128, 128, 0.4);
} }
.empty .expand-button { .empty .expand-button {
opacity: 0.4; opacity: 0.4;
} }
.found { .found {
background: rgba(200, 0, 0, 0.1) !important; background: rgba(200, 0, 0, 0.1) !important;
} }
.ghost { .ghost {
opacity: 0.5; opacity: 0.5;
background: rgba(251, 0, 0, 0.3); background: rgba(251, 0, 0, 0.3);
} }
.v-icon.v-icon--disabled { .v-icon.v-icon--disabled {
opacity: 0; opacity: 0;
} }
.v-icon { .v-icon {
transition: none !important; transition: none !important;
} }
.theme--light .tree-node-title:hover { .theme--light .tree-node-title:hover {
background-color: rgba(0,0,0,.04); background-color: rgba(0,0,0,.04);
} }
.theme--dark .tree-node-title:hover { .theme--dark .tree-node-title:hover {
background-color: rgba(255,255,255,.04); background-color: rgba(255,255,255,.04);
} }
.tree-node-title{ .tree-node-title{
transition: background ease 0.3s, color ease 0.15s; transition: background ease 0.3s, color ease 0.15s;
} }

View File

@@ -5,26 +5,31 @@
:key="child.node._id" :key="child.node._id"
:node="child.node" :node="child.node"
:children="child.children" :children="child.children"
:parent-slot-id="parentSlotId"
@selected="e => $emit('selected', e)" @selected="e => $emit('selected', e)"
/> />
</div> </div>
</template> </template>
<script lang="js"> <script lang="js">
import BuildTreeNode from '/imports/ui/creature/buildTree/BuildTreeNode.vue'; import BuildTreeNode from '/imports/ui/creature/buildTree/BuildTreeNode.vue';
export default { export default {
components: { components: {
BuildTreeNode, BuildTreeNode,
}, },
props: { props: {
children: { children: {
type: Array, type: Array,
default: () => [], default: () => [],
}, },
}, parentSlotId: {
data(){ return { type: String,
expanded: false, default: undefined,
}}, },
}; },
data(){ return {
expanded: false,
}},
};
</script> </script>

View File

@@ -98,7 +98,7 @@
Next Next
</v-btn> </v-btn>
<v-btn <v-btn
:disabled="biographyAlert" :disabled="!!biographyAlert"
:text="step < 2" :text="step < 2"
:color="step < 2? '' : 'accent'" :color="step < 2? '' : 'accent'"
@click="submit" @click="submit"

View File

@@ -25,6 +25,7 @@
<v-menu <v-menu
bottom bottom
left left
transition="slide-y-transition"
> >
<template #activator="{ on }"> <template #activator="{ on }">
<v-badge <v-badge
@@ -48,12 +49,21 @@
<v-icon class="mr-2"> <v-icon class="mr-2">
mdi-file-hidden mdi-file-hidden
</v-icon> </v-icon>
{{ hiddenCount }} hidden {{ hiddenCount > 1 ? 'slots' : 'slot' }} {{ hiddenCount }} hidden {{ hiddenCount > 1 ? 'properties' : 'property' }}
</v-subheader> </v-subheader>
<v-list-item
v-for="pointBuy in hiddenPointBuys"
:key="pointBuy._id"
@click="unhideProp(pointBuy._id)"
>
<v-list-item-title>
{{ getPropertyTitle(pointBuy) }}
</v-list-item-title>
</v-list-item>
<v-list-item <v-list-item
v-for="slot in hiddenSlots" v-for="slot in hiddenSlots"
:key="slot._id" :key="slot._id"
@click="unhideSlot(slot._id)" @click="unhideProp(slot._id)"
> >
<v-list-item-title> <v-list-item-title>
{{ getPropertyTitle(slot) }} {{ getPropertyTitle(slot) }}
@@ -226,7 +236,7 @@ export default {
].sort((a, b) => a.order - b.order); ].sort((a, b) => a.order - b.order);
}, },
hiddenCount() { hiddenCount() {
return this.hiddenSlots.length; return this.hiddenSlots.length + this.hiddenPointBuys.length;
}, },
}, },
meteor: { meteor: {
@@ -236,6 +246,16 @@ export default {
variables() { variables() {
return CreatureVariables.findOne({ _creatureId: this.creatureId }) || {}; return CreatureVariables.findOne({ _creatureId: this.creatureId }) || {};
}, },
hiddenPointBuys() {
return CreatureProperties.find({
type: 'pointBuy',
'ancestors.id': this.creatureId,
ignored: true,
pointsLeft: {$ne: 0},
removed: {$ne: true},
inactive: {$ne: true},
}).fetch();
},
hiddenSlots(){ hiddenSlots(){
return CreatureProperties.find({ return CreatureProperties.find({
type: 'propertySlot', type: 'propertySlot',
@@ -284,7 +304,7 @@ export default {
slotBuildTree(){ slotBuildTree(){
const slots = CreatureProperties.find({ const slots = CreatureProperties.find({
'ancestors.id': this.creatureId, 'ancestors.id': this.creatureId,
type: 'propertySlot', type: {$in: ['propertySlot', 'pointBuy']},
$or: [ $or: [
{'slotCondition.value': {$nin: [false, 0, '']}}, {'slotCondition.value': {$nin: [false, 0, '']}},
{'slotCondition.value': {$exists: false}}, {'slotCondition.value': {$exists: false}},
@@ -308,16 +328,15 @@ export default {
]); ]);
traverse(tree, (child, parents) => { traverse(tree, (child, parents) => {
const model = child.node; const model = child.node;
const isSlotWithSpace = model.type === 'propertySlot' && const isSlotWithSpace = model.type === 'propertySlot' && (
model.spaceLeft > 0 || model.spaceLeft > 0 ||
!model.quantityExpected || !model.quantityExpected ||
model.quantityExpected.value === 0; model.quantityExpected.value === 0
);
if(isSlotWithSpace) { if(isSlotWithSpace) {
model._canFill = true; model._canFill = true;
parents.forEach(node => { parents.forEach(node => {
if (node.node.type === 'propertySlot'){ node.node._descendantCanFill = true;
node.node._descendantCanFill = true;
}
}); });
} }
}); });
@@ -385,7 +404,7 @@ export default {
}); });
}, },
getPropertyTitle, getPropertyTitle,
unhideSlot(_id) { unhideProp(_id) {
updateCreatureProperty.call({ updateCreatureProperty.call({
_id, _id,
path: ['ignored'], path: ['ignored'],

View File

@@ -5,6 +5,18 @@
leave-absolute leave-absolute
hide-on-leave hide-on-leave
> >
<div
v-for="pointBuy in pointBuys"
:key="pointBuy._id"
style="transition: all 0.3s !important"
>
<point-buy-card
:model="pointBuy"
hover
@ignore="ignoreProp(pointBuy._id)"
@click="editPointBuy(pointBuy._id)"
/>
</div>
<div <div
v-for="slot in slots" v-for="slot in slots"
:key="slot._id" :key="slot._id"
@@ -13,7 +25,7 @@
<slot-card <slot-card
:model="slot" :model="slot"
hover hover
@ignore="ignoreSlot(slot._id)" @ignore="ignoreProp(slot._id)"
@click="fillSlot(slot._id)" @click="fillSlot(slot._id)"
/> />
</div> </div>
@@ -24,6 +36,7 @@
<script lang="js"> <script lang="js">
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import SlotCard from '/imports/ui/creature/slots/SlotCard.vue'; import SlotCard from '/imports/ui/creature/slots/SlotCard.vue';
import PointBuyCard from '/imports/ui/properties/components/pointBuy/PointBuyCard.vue';
import ColumnLayout from '/imports/ui/components/ColumnLayout.vue'; import ColumnLayout from '/imports/ui/components/ColumnLayout.vue';
import updateCreatureProperty from '/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js'; import updateCreatureProperty from '/imports/api/creature/creatureProperties/methods/updateCreatureProperty.js';
import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js'; import insertPropertyFromLibraryNode from '/imports/api/creature/creatureProperties/methods/insertPropertyFromLibraryNode.js';
@@ -32,13 +45,14 @@ import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
export default { export default {
components: { components: {
SlotCard, SlotCard,
PointBuyCard,
ColumnLayout, ColumnLayout,
}, },
inject: { inject: {
context: { default: {} } context: { default: {} }
}, },
methods: { methods: {
ignoreSlot(_id){ ignoreProp(_id){
updateCreatureProperty.call({ updateCreatureProperty.call({
_id, _id,
path: ['ignored'], path: ['ignored'],
@@ -75,6 +89,16 @@ export default {
} }
}); });
}, },
editPointBuy(_id){
this.$store.commit('pushDialogStack', {
component: 'creature-property-dialog',
elementId: `point-buy-card-${_id}`,
data: {
_id,
startInEditTab: true,
},
});
},
}, },
meteor: { meteor: {
slots(){ slots(){
@@ -99,7 +123,16 @@ export default {
removed: {$ne: true}, removed: {$ne: true},
inactive: {$ne: true}, inactive: {$ne: true},
}); });
} },
pointBuys(){
return CreatureProperties.find({
type: 'pointBuy',
'ancestors.id': this.context.creatureId,
ignored: { $ne: true },
removed: {$ne: true},
inactive: {$ne: true},
});
},
} }
} }
</script> </script>

View File

@@ -37,7 +37,7 @@
import SingleCardLayout from '/imports/ui/layouts/SingleCardLayout.vue' import SingleCardLayout from '/imports/ui/layouts/SingleCardLayout.vue'
import Tabletops from '/imports/api/tabletop/Tabletops.js'; import Tabletops from '/imports/api/tabletop/Tabletops.js';
import insertTabletop from '/imports/api/tabletop/methods/insertTabletop.js'; import insertTabletop from '/imports/api/tabletop/methods/insertTabletop.js';
import snackbar from '/imports/ui/components/snackbars/SnackbarQueue.js'; import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
export default { export default {
components: { components: {

View File

@@ -27,9 +27,9 @@
<div class="text-body-1 mb-1"> <div class="text-body-1 mb-1">
{{ displayedText }} {{ displayedText }}
</div> </div>
<div v-if="!hideBreadcrumbs && model.ancestors"> <div v-if="!hideBreadcrumbs && ancestors">
<breadcrumbs <breadcrumbs
:model="model" :model="{...model, ancestors}"
class="text-caption" class="text-caption"
no-links no-links
no-icons no-icons
@@ -41,94 +41,101 @@
</template> </template>
<script lang="js"> <script lang="js">
import getEffectIcon from '/imports/ui/utility/getEffectIcon.js'; import getEffectIcon from '/imports/ui/utility/getEffectIcon.js';
import Breadcrumbs from '/imports/ui/creature/creatureProperties/Breadcrumbs.vue'; import Breadcrumbs from '/imports/ui/creature/creatureProperties/Breadcrumbs.vue';
import { isFinite } from 'lodash'; import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import { isFinite } from 'lodash';
export default { export default {
components: { components: {
Breadcrumbs, Breadcrumbs,
},
props: {
hideBreadcrumbs: Boolean,
model: {
type: Object,
required: true,
}, },
props: { },
hideBreadcrumbs: Boolean, computed: {
model: { hasClickListener(){
type: Object, return this.$listeners && this.$listeners.click
required: true,
},
}, },
computed: { displayedText(){
hasClickListener(){ if (this.model.operation === 'conditional'){
return this.$listeners && this.$listeners.click return this.model.text || this.model.name || this.operation
}, } else {
displayedText(){ return this.model.name || this.operation
if (this.model.operation === 'conditional'){
return this.model.text || this.model.name || this.operation
} else {
return this.model.name || this.operation
}
},
resolvedValue(){
let amount = this.model.amount;
if (!amount) return;
return amount.value !== undefined ? amount.value : amount.calculation;
},
effectIcon(){
let value = this.resolvedValue;
return getEffectIcon(this.model.operation, value);
},
operation(){
switch(this.model.operation) {
case 'base': return 'Base value';
case 'add': return 'Add';
case 'mul': return 'Multiply';
case 'min': return 'Minimum';
case 'max': return 'Maximum';
case 'advantage': return 'Advantage';
case 'disadvantage': return 'Disadvantage';
case 'passiveAdd': return 'Passive bonus';
case 'fail': return 'Always fail';
case 'conditional': return 'Conditional benefit' ;
default: return '';
}
},
showValue(){
switch(this.model.operation) {
case 'base': return true;
case 'add': return true;
case 'mul': return true;
case 'min': return true;
case 'max': return true;
case 'advantage': return false;
case 'disadvantage': return false;
case 'passiveAdd': return true;
case 'fail': return false;
case 'conditional': return false;
default: return false;
}
},
displayedValue(){
let value = this.resolvedValue;
switch(this.model.operation) {
case 'base': return value;
case 'add': return isFinite(value) ? Math.abs(value) : value;
case 'mul': return value;
case 'min': return value;
case 'max': return value;
case 'advantage': return;
case 'disadvantage': return;
case 'passiveAdd': return isFinite(value) ? Math.abs(value) : value;
case 'fail': return;
case 'conditional': return undefined;
default: return undefined;
}
} }
}, },
methods: { resolvedValue() {
click(e){ let amount = this.model.amount;
this.$emit('click', e); if (!amount) return;
}, return amount.value !== undefined ? amount.value : amount.calculation;
}, },
}; effectIcon(){
let value = this.resolvedValue;
return getEffectIcon(this.model.operation, value);
},
operation(){
switch(this.model.operation) {
case 'base': return 'Base value';
case 'add': return 'Add';
case 'mul': return 'Multiply';
case 'min': return 'Minimum';
case 'max': return 'Maximum';
case 'advantage': return 'Advantage';
case 'disadvantage': return 'Disadvantage';
case 'passiveAdd': return 'Passive bonus';
case 'fail': return 'Always fail';
case 'conditional': return 'Conditional benefit' ;
default: return '';
}
},
showValue(){
switch(this.model.operation) {
case 'base': return true;
case 'add': return true;
case 'mul': return true;
case 'min': return true;
case 'max': return true;
case 'advantage': return false;
case 'disadvantage': return false;
case 'passiveAdd': return true;
case 'fail': return false;
case 'conditional': return false;
default: return false;
}
},
displayedValue(){
let value = this.resolvedValue;
switch(this.model.operation) {
case 'base': return value;
case 'add': return isFinite(value) ? Math.abs(value) : value;
case 'mul': return value;
case 'min': return value;
case 'max': return value;
case 'advantage': return;
case 'disadvantage': return;
case 'passiveAdd': return isFinite(value) ? Math.abs(value) : value;
case 'fail': return;
case 'conditional': return undefined;
default: return undefined;
}
}
},
meteor: {
ancestors() {
const prop = CreatureProperties.findOne(this.model._id);
return prop && prop.ancestors || [];
}
},
methods: {
click(e){
this.$emit('click', e);
},
},
};
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>

View File

@@ -0,0 +1,72 @@
<template>
<v-card
v-if="model"
v-bind="$attrs"
:data-id="`point-buy-card-${model._id}`"
:style="`border: solid 1px ${accentColor};`"
hover
class="slot-card d-flex flex-column"
@mouseover="hover = true"
@mouseleave="hover = false"
@click="$emit('click')"
>
<card-highlight
:active="hover"
/>
<v-card-title>
{{ model.name || 'Point Buy' }}
</v-card-title>
<v-card-text>
{{ model.spent }}
<template v-if="model.total && (typeof model.total.value === 'number')">
/ {{ model.total && model.total.value }}
</template>
</v-card-text>
<v-spacer />
<v-card-actions>
<v-spacer />
<v-btn
icon
color="accent"
@click.stop="$emit('ignore')"
>
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-actions>
</v-card>
</template>
<script lang="js">
import CardHighlight from '/imports/ui/components/CardHighlight.vue';
export default {
components: {
CardHighlight,
},
inject: {
theme: {
default: {
isDark: false,
},
},
},
props: {
model: {
type: Object,
default: undefined,
},
},
data(){ return {
hover: false,
}},
computed: {
accentColor(){
if (this.theme.isDark){
return this.$vuetify.theme.themes.dark.primary;
} else {
return this.$vuetify.theme.themes.light.primary;
}
}
},
}
</script>

View File

@@ -8,7 +8,7 @@
<v-list-item-title class="d-flex align-center"> <v-list-item-title class="d-flex align-center">
<roll-popup <roll-popup
v-if="!hideModifier" v-if="!hideModifier"
class="prof-mod mr-1" class="prof-mod mr-1 flex-shrink-0"
button-class="pl-3 pr-2" button-class="pl-3 pr-2"
text text
:roll-text="displayedModifier" :roll-text="displayedModifier"
@@ -43,7 +43,7 @@
:value="model.proficiency" :value="model.proficiency"
class="prof-icon ml-3 mr-2" class="prof-icon ml-3 mr-2"
/> />
<div> <div class="text-truncate">
{{ model.name }} {{ model.name }}
<template v-if="model.conditionalBenefits && model.conditionalBenefits.length"> <template v-if="model.conditionalBenefits && model.conditionalBenefits.length">
* *

View File

@@ -141,6 +141,18 @@
@change="change('usesUsed', ...arguments)" @change="change('usesUsed', ...arguments)"
/> />
</v-col> </v-col>
<v-col
cols="12"
sm="6"
md="4"
>
<smart-switch
label="Don't show in log"
:value="model.silent"
:error-messages="errors.silent"
@change="change('silent', ...arguments)"
/>
</v-col>
</v-row> </v-row>
<smart-select <smart-select
label="Reset" label="Reset"

View File

@@ -59,6 +59,12 @@
:value="model.tags" :value="model.tags"
@change="change('tags', ...arguments)" @change="change('tags', ...arguments)"
/> />
<smart-switch
label="Don't show in log"
:value="model.silent"
:error-messages="errors.silent"
@change="change('silent', ...arguments)"
/>
<form-section <form-section
v-if="$slots.children" v-if="$slots.children"
name="Children" name="Children"

View File

@@ -38,6 +38,12 @@
:value="model.tags" :value="model.tags"
@change="change('tags', ...arguments)" @change="change('tags', ...arguments)"
/> />
<smart-switch
label="Don't show in log"
:value="model.silent"
:error-messages="errors.silent"
@change="change('silent', ...arguments)"
/>
<form-section <form-section
name="Children" name="Children"
standalone standalone

View File

@@ -47,12 +47,44 @@
<form-section <form-section
name="Advanced" name="Advanced"
> >
<smart-switch <v-row dense>
label="Hide remove button" <v-col
:value="model.hideRemoveButton" cols="12"
:error-messages="errors.hideRemoveButton" sm="6"
@change="change('hideRemoveButton', ...arguments)" md="4"
/> >
<smart-switch
label="Hide remove button"
:value="model.hideRemoveButton"
:error-messages="errors.hideRemoveButton"
@change="change('hideRemoveButton', ...arguments)"
/>
</v-col>
<v-col
cols="12"
sm="6"
md="4"
>
<smart-switch
label="Don't show in log"
:value="model.silent"
:error-messages="errors.silent"
@change="change('silent', ...arguments)"
/>
</v-col>
<v-col
cols="12"
sm="6"
md="4"
>
<smart-switch
label="Don't freeze variables"
:value="model.skipCrystalization"
:error-messages="errors.skipCrystalization"
@change="change('skipCrystalization', ...arguments)"
/>
</v-col>
</v-row>
<smart-combobox <smart-combobox
label="Tags" label="Tags"
multiple multiple

View File

@@ -115,6 +115,20 @@
:value="model.tags" :value="model.tags"
@change="change('tags', ...arguments)" @change="change('tags', ...arguments)"
/> />
<v-row dense>
<v-col
cols="12"
sm="6"
md="4"
>
<smart-switch
label="Don't show in log"
:value="model.silent"
:error-messages="errors.silent"
@change="change('silent', ...arguments)"
/>
</v-col>
</v-row>
</form-section> </form-section>
</form-sections> </form-sections>
</div> </div>

View File

@@ -53,6 +53,12 @@
:error-messages="errors.tags" :error-messages="errors.tags"
@change="change('tags', ...arguments)" @change="change('tags', ...arguments)"
/> />
<smart-switch
label="Don't show in log"
:value="model.silent"
:error-messages="errors.silent"
@change="change('silent', ...arguments)"
/>
<form-section <form-section
v-if="$slots.children" v-if="$slots.children"
name="Children" name="Children"

View File

@@ -0,0 +1,219 @@
<template lang="html">
<div class="point-buy-form">
<point-buy-spend-form
:model="model"
@change="e => $emit('change', e)"
@push="e => $emit('change', e)"
@pull="e => $emit('change', e)"
/>
<form-sections>
<form-section name="Settings">
<v-row dense>
<v-col
cols="12"
md="6"
>
<text-field
ref="focusFirst"
label="Table name"
:value="model.name"
:error-messages="errors.name"
@change="change('name', ...arguments)"
/>
</v-col>
<v-col
cols="12"
md="6"
>
<computed-field
label="Min"
hint="The minimum value for each row"
:model="model.min"
:error-messages="errors.min"
@change="({path, value, ack}) =>
$emit('change', {path: ['min', ...path], value, ack})"
/>
</v-col>
<v-col
cols="12"
md="6"
>
<computed-field
label="Max"
hint="The maximum value for each row"
:model="model.max"
:error-messages="errors.max"
@change="({path, value, ack}) =>
$emit('change', {path: ['max', ...path], value, ack})"
/>
</v-col>
<v-col
cols="12"
md="6"
>
<computed-field
label="Cost"
hint="A function of `value` that determines the cost of each row"
hide-value
:model="model.cost"
:error-messages="errors.cost"
@change="({path, value, ack}) =>
$emit('change', {path: ['cost', ...path], value, ack})"
/>
</v-col>
<v-col
cols="12"
md="6"
>
<computed-field
label="Total available points"
hint="The total allowed cost of all rows"
:model="model.total"
:error-messages="errors.total"
@change="({path, value, ack}) =>
$emit('change', {path: ['total', ...path], value, ack})"
/>
</v-col>
</v-row>
</form-section>
<form-section name="Rows">
<v-slide-x-transition
group
leave-absolute
>
<v-row
v-for="(row, i) in model.values"
:key="row._id"
dense
>
<v-divider
v-if="i"
style="flex-basis: 100%;"
class="mb-6"
/>
<v-col cols="11">
<v-row dense>
<v-col
cols="12"
md="6"
>
<text-field
ref="focusFirst"
label="Row Name"
:value="row.name"
:error-messages="errors.values && errors.values[i] && errors.values[i].name"
@change="change(['values', i, 'name'], ...arguments)"
/>
</v-col>
<v-col
cols="12"
md="6"
>
<text-field
label="Variable name"
:value="row.variableName"
hint="Use this name in calculations to reference this row of the table"
:error-messages="errors.values && errors.values[i] && errors.values[i].variableName"
@change="change(['values', i, 'variableName'], ...arguments)"
/>
</v-col>
<v-col
v-if="row.errors && row.errors.length"
cols="12"
>
<calculation-error-list
:errors="row.errors"
/>
</v-col>
</v-row>
</v-col>
<v-col
cols="1"
class="d-flex align-center justify-center"
>
<v-btn
icon
large
@click="$emit('pull', {path: ['values', i]})"
>
<v-icon>mdi-delete</v-icon>
</v-btn>
</v-col>
</v-row>
<v-row
key="addButton"
dense
justify="end"
class="mb-4"
>
<v-col
cols="1"
class="d-flex justify-center"
>
<v-btn
icon
outlined
:loading="addRowLoading"
:disabled="rowsFull"
@click="addRow"
>
<v-icon>
mdi-plus
</v-icon>
</v-btn>
</v-col>
</v-row>
</v-slide-x-transition>
</form-section>
<form-section
v-if="$slots.children"
name="Children"
>
<slot name="children" />
</form-section>
</form-sections>
</div>
</template>
<script lang="js">
import attributeListMixin from '/imports/ui/properties/forms/shared/lists/attributeListMixin.js';
import propertyFormMixin from '/imports/ui/properties/forms/shared/propertyFormMixin.js';
import { PointBuySchema } from '/imports/api/properties/PointBuys.js';
import CalculationErrorList from '/imports/ui/properties/forms/shared/CalculationErrorList.vue';
import PointBuySpendForm from '/imports/ui/properties/forms/PointBuySpendForm.vue';
export default {
components: {
CalculationErrorList,
PointBuySpendForm,
},
mixins: [propertyFormMixin, attributeListMixin],
data() {
return {
addRowLoading: false,
};
},
computed: {
rowsFull(){
if (!this.model.values) return false;
let maxCount = PointBuySchema.get('values', 'maxCount');
return this.model.values.length >= maxCount;
},
},
methods: {
acknowledgeAddResult(){
this.addRowLoading = false;
},
addRow(){
this.addRowLoading = true;
this.$emit('push', {
path: ['values'],
value: {
_id: Random.id(),
},
ack: this.acknowledgeAddResult,
});
},
},
}
</script>

View File

@@ -0,0 +1,108 @@
<template lang="html">
<div class="point-buy-spend-form">
<v-row
v-if="model.values && model.values.length"
dense
>
<v-col
cols="10"
md="11"
/>
<v-col
cols="2"
md="1"
class="text-truncate d-flex justify-center"
>
Cost
</v-col>
</v-row>
<v-row
v-for="(row, i) in model.values"
:key="row._id"
dense
align="center"
>
<v-col
cols="12"
md="2"
class="d-flex justify-md-end"
>
{{ row.name }}
</v-col>
<v-col
cols="2"
md="1"
class="d-flex justify-md-center justify-end"
>
{{ row.value }}
</v-col>
<v-col
cols="8"
>
<smart-slider
thumb-label
dense
:ticks="max(row) - min(row) <= 20"
:min="min(row)"
:max="max(row)"
:value="row.value"
:error-messages="errors.values && errors.values[i] && errors.values[i].value"
@change="(value, ack) => $emit('change', {path: ['values', i, 'value'], value, ack})"
/>
</v-col>
<v-col
cols="2"
md="1"
class="text-truncate d-flex justify-center"
>
{{ row.spent }}
</v-col>
<v-col
v-if="row.errors && row.errors.length"
cols="12"
>
<calculation-error-list
:errors="row.errors"
/>
</v-col>
</v-row>
<v-row
dense
>
<v-col
v-if="typeof model.spent === 'number'"
cols="12"
class="text-h4 mb-4 pr-8 d-flex justify-end"
:class="{
'error--text': model.spent > (model.total && model.total.value),
'warning--text': model.spent < (model.total && model.total.value),
}"
>
{{ model.spent }}
<template v-if="model.total && (typeof model.total.value === 'number')">
/ {{ model.total && model.total.value }}
</template>
</v-col>
</v-row>
</div>
</template>
<script lang="js">
import propertyFormMixin from '/imports/ui/properties/forms/shared/propertyFormMixin.js';
import CalculationErrorList from '/imports/ui/properties/forms/shared/CalculationErrorList.vue';
export default {
components: {
CalculationErrorList,
},
mixins: [propertyFormMixin],
methods: {
max(row) {
return row.max ? row.max && row.max.value : this.model.max && this.model.max.value;
},
min(row) {
return row.min ? row.min && row.min.value : this.model.min && this.model.min.value;
},
},
};
</script>

View File

@@ -65,6 +65,12 @@
:error-messages="errors.tags" :error-messages="errors.tags"
@change="change('tags', ...arguments)" @change="change('tags', ...arguments)"
/> />
<smart-switch
label="Don't show in log"
:value="model.silent"
:error-messages="errors.silent"
@change="change('silent', ...arguments)"
/>
<form-section <form-section
v-if="$slots.children" v-if="$slots.children"
name="Children" name="Children"

View File

@@ -140,6 +140,12 @@
:value="model.tags" :value="model.tags"
@change="change('tags', ...arguments)" @change="change('tags', ...arguments)"
/> />
<smart-switch
label="Don't show in log"
:value="model.silent"
:error-messages="errors.silent"
@change="change('silent', ...arguments)"
/>
</form-section> </form-section>
</form-sections> </form-sections>
</div> </div>

View File

@@ -73,5 +73,8 @@ export default {
} }
</script> </script>
<style lang="css" scoped> <style lang="css">
.error-list .v-alert__content{
overflow-x: auto;
}
</style> </style>

View File

@@ -6,7 +6,7 @@
@change="(value, ack) => $emit('change', {path: ['calculation'], value, ack})" @change="(value, ack) => $emit('change', {path: ['calculation'], value, ack})"
> >
<template <template
v-if="model.value !== undefined || model.value !== null" v-if="showValue"
#value #value
> >
{{ model.value }} {{ model.value }}
@@ -28,8 +28,20 @@ export default {
type: Object, type: Object,
default: () => ({}), default: () => ({}),
}, },
hideValue: {
type: Boolean,
},
}, },
computed: { computed: {
showValue() {
const value = this.model.value;
if (
this.hideValue ||
(value === undefined || value === null) ||
value == this.model.calculation
) return false;
return true;
},
errorList(){ errorList(){
if (this.model.parseError){ if (this.model.parseError){
return [this.model.parseError, ...this.model.errors]; return [this.model.parseError, ...this.model.errors];

View File

@@ -4,7 +4,7 @@
<v-expansion-panel-header> <v-expansion-panel-header>
{{ name }} {{ name }}
</v-expansion-panel-header> </v-expansion-panel-header>
<v-expansion-panel-content> <v-expansion-panel-content class="pt-2">
<slot /> <slot />
</v-expansion-panel-content> </v-expansion-panel-content>
</v-expansion-panel> </v-expansion-panel>
@@ -13,7 +13,7 @@
<v-expansion-panel-header> <v-expansion-panel-header>
{{ name }} {{ name }}
</v-expansion-panel-header> </v-expansion-panel-header>
<v-expansion-panel-content> <v-expansion-panel-content class="pt-2">
<slot /> <slot />
</v-expansion-panel-content> </v-expansion-panel-content>
</v-expansion-panel> </v-expansion-panel>

View File

@@ -15,6 +15,7 @@ const FeatureForm = () => import('/imports/ui/properties/forms/FeatureForm.vue')
const FolderForm = () => import('/imports/ui/properties/forms/FolderForm.vue'); const FolderForm = () => import('/imports/ui/properties/forms/FolderForm.vue');
const ItemForm = () => import('/imports/ui/properties/forms/ItemForm.vue'); const ItemForm = () => import('/imports/ui/properties/forms/ItemForm.vue');
const NoteForm = () => import('/imports/ui/properties/forms/NoteForm.vue'); const NoteForm = () => import('/imports/ui/properties/forms/NoteForm.vue');
const PointBuyForm = () => import('/imports/ui/properties/forms/PointBuyForm.vue');
const ProficiencyForm = () => import('/imports/ui/properties/forms/ProficiencyForm.vue'); const ProficiencyForm = () => import('/imports/ui/properties/forms/ProficiencyForm.vue');
const ReferenceForm = () => import('/imports/ui/properties/forms/ReferenceForm.vue'); const ReferenceForm = () => import('/imports/ui/properties/forms/ReferenceForm.vue');
const RollForm = () => import('/imports/ui/properties/forms/RollForm.vue'); const RollForm = () => import('/imports/ui/properties/forms/RollForm.vue');
@@ -45,6 +46,7 @@ export default {
folder: FolderForm, folder: FolderForm,
item: ItemForm, item: ItemForm,
note: NoteForm, note: NoteForm,
pointBuy: PointBuyForm,
proficiency: ProficiencyForm, proficiency: ProficiencyForm,
propertySlot: SlotForm, propertySlot: SlotForm,
reference: ReferenceForm, reference: ReferenceForm,

View File

@@ -107,25 +107,18 @@
</v-row> </v-row>
<v-row dense> <v-row dense>
<property-field <property-field
v-if="baseEffects.length || effects.length" v-if="effects && effects.length"
:cols="{col: 12}" :cols="{col: 12}"
name="Effects" name="Effects"
> >
<v-list style="width: 100%;"> <v-list style="width: 100%;">
<attribute-effect
v-for="effect in baseEffects"
:key="effect._id"
:model="effect"
:hide-breadcrumbs="effect._id === model._id"
:data-id="effect._id"
@click="effect._id !== model._id && clickEffect(effect._id)"
/>
<attribute-effect <attribute-effect
v-for="effect in effects" v-for="effect in effects"
:key="effect._id" :key="effect._id"
:model="effect" :model="effect"
:data-id="effect._id" :data-id="effect._id"
@click="clickEffect(effect._id)" :hide-breadcrumbs="effect._id === model._id"
@click="effect._id !== model._id && clickEffect(effect._id)"
/> />
</v-list> </v-list>
</property-field> </property-field>
@@ -137,7 +130,6 @@
import propertyViewerMixin from '/imports/ui/properties/viewers/shared/propertyViewerMixin.js' import propertyViewerMixin from '/imports/ui/properties/viewers/shared/propertyViewerMixin.js'
import numberToSignedString from '/imports/ui/utility/numberToSignedString.js'; import numberToSignedString from '/imports/ui/utility/numberToSignedString.js';
import AttributeEffect from '/imports/ui/properties/components/attributes/AttributeEffect.vue'; import AttributeEffect from '/imports/ui/properties/components/attributes/AttributeEffect.vue';
import CreatureProperties from '/imports/api/creature/creatureProperties/CreatureProperties.js';
import damageProperty from '/imports/api/creature/creatureProperties/methods/damageProperty.js'; import damageProperty from '/imports/api/creature/creatureProperties/methods/damageProperty.js';
import IncrementButton from '/imports/ui/components/IncrementButton.vue'; import IncrementButton from '/imports/ui/components/IncrementButton.vue';
import getProficiencyIcon from '/imports/ui/utility/getProficiencyIcon.js'; import getProficiencyIcon from '/imports/ui/utility/getProficiencyIcon.js';
@@ -211,31 +203,8 @@
}, },
}, },
meteor: { meteor: {
baseEffects(){
if (this.context.creatureId && this.model.variableName){
let creatureId = this.context.creatureId;
return CreatureProperties.find({
'ancestors.id': creatureId,
type: 'attribute',
variableName: this.model.variableName,
removed: {$ne: true},
inactive: {$ne: true},
}).map( prop => ({
_id: prop._id,
name: 'Attribute base value',
operation: 'base',
amount: prop.baseValue,
stats: [prop.variableName],
ancestors: prop.ancestors,
}) ).filter(effect => effect.amount);
} else {
return [];
}
},
effects() { effects() {
return CreatureProperties.find({ return this.model.effects;
_id: { $in: this.model.effects?.map(e => e._id) || [] }
});
}, },
}, },
} }

View File

@@ -0,0 +1,45 @@
<template lang="html">
<div class="point-buy-viewer">
<v-row dense>
<property-field
v-for="(row, i) in model.values"
:key="row._id"
:name="row.name"
:value="row.value"
/>
</v-row>
</div>
</template>
<script lang="js">
import propertyViewerMixin from '/imports/ui/properties/viewers/shared/propertyViewerMixin.js'
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
import { timingOptions, eventOptions, actionPropertyTypeOptions } from '/imports/api/properties/Triggers.js';
export default {
mixins: [propertyViewerMixin],
inject: {
context: {
default: {},
},
},
computed: {
slotTypeName(){
if (!this.model.slotType) return;
return getPropertyName(this.model.slotType);
},
timingText(){
if (!this.model.timing) return;
return timingOptions[this.model.timing];
},
actionPropertyText(){
if (!this.model.actionPropertyType) return;
return actionPropertyTypeOptions[this.model.actionPropertyType];
},
eventText(){
if (!this.model.event) return;
return eventOptions[this.model.event];
},
}
}
</script>

View File

@@ -47,6 +47,12 @@
name="Passive score" name="Passive score"
:value="passiveScore" :value="passiveScore"
/> />
<property-field
v-if="model.overridden"
:cols="{cols: 6, md: 12}"
name="Overridden"
value="Overriden by another property with the same variable name"
/>
</v-row> </v-row>
<v-row dense> <v-row dense>
<property-description <property-description
@@ -55,7 +61,7 @@
/> />
</v-row> </v-row>
<v-row <v-row
v-if="baseEffects.length || ability || effects.length" v-if="ability || (effects && effects.length)"
dense dense
> >
<property-field <property-field
@@ -63,14 +69,6 @@
name="Effects" name="Effects"
> >
<v-list style="width: 100%"> <v-list style="width: 100%">
<attribute-effect
v-for="effect in baseEffects"
:key="effect._id === model._id ? 'this_base' : effect._id"
:model="effect"
:hide-breadcrumbs="effect._id === model._id"
:data-id="effect._id"
@click="effect._id !== model._id && clickEffect(effect._id)"
/>
<attribute-effect <attribute-effect
v-if="ability" v-if="ability"
:key="ability._id" :key="ability._id"
@@ -188,32 +186,8 @@ export default {
variables(){ variables(){
return CreatureVariables.findOne({_creatureId: this.context.creatureId}) || {}; return CreatureVariables.findOne({_creatureId: this.context.creatureId}) || {};
}, },
baseEffects(){
if (this.context.creatureId){
let creatureId = this.context.creatureId;
return CreatureProperties.find({
'ancestors.id': creatureId,
type: 'skill',
variableName: this.model.variableName,
removed: {$ne: true},
inactive: {$ne: true},
}).map( prop => ({
_id: prop._id,
name: 'Skill base value',
operation: 'base',
calculation: prop.baseValueCalculation,
amount: {value: prop.baseValue?.value},
stats: [prop.variableName],
ancestors: prop.ancestors,
}) ).filter(effect => effect.amount?.value);
} else {
return [];
}
},
effects() { effects() {
return CreatureProperties.find({ return this.model.effects;
_id: { $in: this.model.effects?.map(e => e._id) || [] }
});
}, },
baseProficiencies(){ baseProficiencies(){
if (this.context.creatureId){ if (this.context.creatureId){
@@ -265,7 +239,7 @@ export default {
return { return {
_id: abilityProp._id, _id: abilityProp._id,
name: abilityProp.name, name: abilityProp.name,
operation: 'base', operation: 'add',
amount: {value: abilityProp.modifier}, amount: {value: abilityProp.modifier},
stats: [this.model.variableName], stats: [this.model.variableName],
ancestors: abilityProp.ancestors, ancestors: abilityProp.ancestors,

View File

@@ -15,6 +15,7 @@ const FeatureViewer = () => import ('/imports/ui/properties/viewers/FeatureViewe
const FolderViewer = () => import ('/imports/ui/properties/viewers/FolderViewer.vue'); const FolderViewer = () => import ('/imports/ui/properties/viewers/FolderViewer.vue');
const ItemViewer = () => import ('/imports/ui/properties/viewers/ItemViewer.vue'); const ItemViewer = () => import ('/imports/ui/properties/viewers/ItemViewer.vue');
const NoteViewer = () => import ('/imports/ui/properties/viewers/NoteViewer.vue'); const NoteViewer = () => import ('/imports/ui/properties/viewers/NoteViewer.vue');
const PointBuyViewer = () => import ('/imports/ui/properties/viewers/PointBuyViewer.vue');
const ProficiencyViewer = () => import ('/imports/ui/properties/viewers/ProficiencyViewer.vue'); const ProficiencyViewer = () => import ('/imports/ui/properties/viewers/ProficiencyViewer.vue');
const ReferenceViewer = () => import ('/imports/ui/properties/viewers/ReferenceViewer.vue'); const ReferenceViewer = () => import ('/imports/ui/properties/viewers/ReferenceViewer.vue');
const RollViewer = () => import ('/imports/ui/properties/viewers/RollViewer.vue'); const RollViewer = () => import ('/imports/ui/properties/viewers/RollViewer.vue');
@@ -45,6 +46,7 @@ export default {
folder: FolderViewer, folder: FolderViewer,
item: ItemViewer, item: ItemViewer,
note: NoteViewer, note: NoteViewer,
pointBuy: PointBuyViewer,
proficiency: ProficiencyViewer, proficiency: ProficiencyViewer,
propertySlot: SlotViewer, propertySlot: SlotViewer,
roll: RollViewer, roll: RollViewer,

View File

@@ -43,7 +43,7 @@ import TabletopMap from '/imports/ui/tabletop/TabletopMap.vue';
import Creatures from '/imports/api/creature/creatures/Creatures.js'; import Creatures from '/imports/api/creature/creatures/Creatures.js';
import TabletopActionCards from '/imports/ui/tabletop/TabletopActionCards.vue'; import TabletopActionCards from '/imports/ui/tabletop/TabletopActionCards.vue';
import MiniCharacterSheet from '/imports/ui/creature/character/MiniCharacterSheet.vue'; import MiniCharacterSheet from '/imports/ui/creature/character/MiniCharacterSheet.vue';
import snackbar from '/imports/ui/components/snackbars/SnackbarQueue.js'; import { snackbar } from '/imports/ui/components/snackbars/SnackbarQueue.js';
export default { export default {
components: { components: {