Skills can now apply to calcs by tag

This commit is contained in:
Stefan Zermatten
2023-06-14 13:56:44 +02:00
parent 442aea2bbe
commit 04de76d20e
9 changed files with 131 additions and 40 deletions

View File

@@ -306,7 +306,7 @@ function linkSavingThrow(dependencyGraph, prop) {
dependOnCalc({ dependencyGraph, prop, key: 'dc' }); dependOnCalc({ dependencyGraph, prop, key: 'dc' });
} }
function linkSkill(dependencyGraph, prop) { function linkSkill(dependencyGraph, prop, computation) {
// Depends on base value // Depends on base value
dependOnCalc({ dependencyGraph, prop, key: 'baseValue' }); dependOnCalc({ dependencyGraph, prop, key: 'baseValue' });
// Link dependents // Link dependents
@@ -318,6 +318,20 @@ function linkSkill(dependencyGraph, prop) {
} }
// Skills depend on the creature's proficiencyBonus // Skills depend on the creature's proficiencyBonus
dependencyGraph.addLink(prop._id, 'proficiencyBonus', 'skillProficiencyBonus'); dependencyGraph.addLink(prop._id, 'proficiencyBonus', 'skillProficiencyBonus');
// Skills can apply their value as a proficiency bonus to calculations based on tag
if (prop.targetByTags) {
getEffectTagTargets(prop, computation).forEach(targetId => {
const targetProp = computation.propsById[targetId];
// Always target a field on the target property, applying a skill to an attribute or
// other skill isn't supported
const key = prop.targetField || getDefaultCalculationField(targetProp);
const calcObj = get(targetProp, key);
if (calcObj && calcObj.calculation) {
dependencyGraph.addLink(`${targetProp._id}.${key}`, prop._id, 'proficiency');
}
});
}
} }
function linkSlot(dependencyGraph, prop) { function linkSlot(dependencyGraph, prop) {

View File

@@ -1,4 +1,5 @@
import evaluateCalculation from '../../utility/evaluateCalculation.js'; import evaluateCalculation from '../../utility/evaluateCalculation.js';
import { getPropertyName } from '/imports/constants/PROPERTIES.js';
export default function computeCalculation(computation, node) { export default function computeCalculation(computation, node) {
const calcObj = node.data; const calcObj = node.data;
@@ -54,6 +55,9 @@ function aggregateCalculationProficiencies(node, computation) {
const calcObj = node.data; const calcObj = node.data;
delete calcObj.proficiencies; delete calcObj.proficiencies;
delete calcObj.proficiency; delete calcObj.proficiency;
let profBonus = computation.scope['proficiencyBonus']?.value || 0;
// Go through all the links and collect them on the calculation
computation.dependencyGraph.forEachLinkedNode( computation.dependencyGraph.forEachLinkedNode(
node.id, node.id,
(linkedNode, link) => { (linkedNode, link) => {
@@ -61,40 +65,51 @@ function aggregateCalculationProficiencies(node, computation) {
if (link.data !== 'proficiency') return; if (link.data !== 'proficiency') return;
// That have data // That have data
if (!linkedNode.data) return; if (!linkedNode.data) return;
// Ignore inactive props // Ignoring inactive props
if (linkedNode.data.inactive) return; if (linkedNode.data.inactive) return;
// Compute the proficiency and value
let proficiency, value;
if (linkedNode.data.type === 'proficiency') {
proficiency = linkedNode.data.value || 0;
// Multiply the proficiency bonus by the actual proficiency
if (proficiency === 0.49) {
// Round down proficiency bonus in the special case
value = Math.floor(profBonus * 0.5);
} else {
value = Math.ceil(profBonus * proficiency);
}
} else if (linkedNode.data.type === 'skill') {
value = linkedNode.data.value || 0;
proficiency = linkedNode.data.proficiency || 0;
}
// Collate proficiencies // Collate proficiencies
calcObj.proficiencies = calcObj.proficiencies || []; calcObj.proficiencies = calcObj.proficiencies || [];
calcObj.proficiencies.push({ calcObj.proficiencies.push({
_id: linkedNode.data._id, _id: linkedNode.data._id,
name: linkedNode.data.name, name: linkedNode.data.name,
value: linkedNode.data.value, type: linkedNode.data.type,
proficiency,
value,
}); });
}, },
true // enumerate only outbound links true // enumerate only outbound links
); );
// Apply the highest proficiency, marking all others as overridden
if (calcObj.proficiencies && typeof calcObj.value === 'number') { if (calcObj.proficiencies && typeof calcObj.value === 'number') {
calcObj.proficiency = 0; calcObj.proficiency = 0;
calcObj.proficiencyBonus = 0;
let currentProf; let currentProf;
calcObj.proficiencies.forEach(prof => { calcObj.proficiencies.forEach(prof => {
if (prof.value > calcObj.proficiency) { if (prof.value > calcObj.proficiencyBonus) {
if (currentProf) currentProf.overridden = true; if (currentProf) currentProf.overridden = true;
calcObj.proficiency = prof.value; calcObj.proficiencyBonus = prof.value;
calcObj.proficiency = prof.proficiency;
currentProf = prof;
} else { } else {
prof.overridden = true; prof.overridden = true;
} }
}); });
// Get the character's proficiency bonus to apply calcObj.value += calcObj.proficiencyBonus;
let profBonus = computation.scope['proficiencyBonus']?.value || 0;
calcObj.proficiencyBonus = profBonus;
let totalBonus;
// Multiply the proficiency bonus by the actual proficiency
if (calcObj.proficiency === 0.49) {
// Round down proficiency bonus in the special case
totalBonus = Math.floor(profBonus * 0.5);
} else {
totalBonus = Math.ceil(profBonus * calcObj.proficiency);
}
calcObj.value += totalBonus;
} }
} }

View File

@@ -50,7 +50,6 @@ let ActionSchema = createPropertySchema({
attackRoll: { attackRoll: {
type: 'fieldToCompute', type: 'fieldToCompute',
optional: true, optional: true,
defaultValue: 'strength.modifier + proficiencyBonus',
}, },
// Calculation of how many times this action can be used // Calculation of how many times this action can be used
uses: { uses: {

View File

@@ -59,6 +59,51 @@ let SkillSchema = createPropertySchema({
type: 'inlineCalculationFieldToCompute', type: 'inlineCalculationFieldToCompute',
optional: true, optional: true,
}, },
// Skills can apply their value to other calculations as a proficiency
// True when applying skill to tagged props
targetByTags: {
type: Boolean,
optional: true,
},
// Which tags the proficiency is applied to
targetTags: {
type: Array,
optional: true,
maxCount: STORAGE_LIMITS.tagCount,
},
'targetTags.$': {
type: String,
max: STORAGE_LIMITS.tagLength,
},
extraTags: {
type: Array,
optional: true,
maxCount: STORAGE_LIMITS.extraTagsCount,
},
'extraTags.$': {
type: Object,
},
'extraTags.$._id': {
type: String,
regEx: SimpleSchema.RegEx.Id,
autoValue() {
if (!this.isSet) return Random.id();
}
},
'extraTags.$.operation': {
type: String,
allowedValues: ['OR', 'NOT'],
defaultValue: 'OR',
},
'extraTags.$.tags': {
type: Array,
defaultValue: [],
maxCount: STORAGE_LIMITS.tagCount,
},
'extraTags.$.tags.$': {
type: String,
max: STORAGE_LIMITS.tagLength,
},
}); });
let ComputedOnlySkillSchema = createPropertySchema({ let ComputedOnlySkillSchema = createPropertySchema({

View File

@@ -7,7 +7,7 @@
> >
<div class="effect-icon"> <div class="effect-icon">
<proficiency-icon <proficiency-icon
:value="model.value" :value="model.proficiency"
class="prof-icon" class="prof-icon"
/> />
</div> </div>
@@ -16,7 +16,7 @@
<span <span
class="effect-value mr-2" class="effect-value mr-2"
> >
{{ proficiencyValue }} {{ displayedValue }}
</span> </span>
{{ displayedText }} {{ displayedText }}
</v-list-item-title> </v-list-item-title>
@@ -26,34 +26,25 @@
<script lang="js"> <script lang="js">
import ProficiencyIcon from '/imports/client/ui/properties/shared/ProficiencyIcon.vue'; import ProficiencyIcon from '/imports/client/ui/properties/shared/ProficiencyIcon.vue';
import numberToSignedString from '/imports/api/utility/numberToSignedString.js';
export default { export default {
components: { components: {
ProficiencyIcon, ProficiencyIcon,
}, },
props: { props: {
hideBreadcrumbs: Boolean,
model: { model: {
type: Object, type: Object,
required: true, required: true,
}, },
proficiencyBonus: {
type: Number,
default: 0,
},
}, },
computed: { computed: {
displayedText(){ displayedText(){
return this.model.name || 'Proficiency' return this.model.name || (this.model.type == 'proficiency' ? 'Proficiency' : 'Skill')
},
displayedValue() {
return numberToSignedString(this.model.value);
}, },
proficiencyValue(){
if (!this.proficiencyBonus) return;
if (this.model.value === 0.49){
return Math.floor(0.5 * this.proficiencyBonus);
} else {
return Math.ceil(this.model.value * this.proficiencyBonus);
}
},
}, },
methods: { methods: {
click(e){ click(e){

View File

@@ -15,9 +15,8 @@
/> />
<computed-field <computed-field
v-else v-else
label="To Hit" label="Base attack roll bonus"
prefix="1d20 + " hint="Must be set for the action to have an attack roll"
hint="The bonus to attack if this action has an attack roll"
:model="model.attackRoll" :model="model.attackRoll"
:error-messages="errors.attackRoll" :error-messages="errors.attackRoll"
@change="({path, value, ack}) => @change="({path, value, ack}) =>

View File

@@ -81,6 +81,24 @@
</v-col> </v-col>
</v-row> </v-row>
</form-section> </form-section>
<form-section name="Apply skill">
<smart-switch
label="Apply skill to targeted tags"
:value="model.targetByTags"
:error-messages="errors.targetByTags"
@change="change('targetByTags', ...arguments)"
/>
<v-expand-transition>
<tag-targeting
v-if="model.targetByTags"
:model="model"
:errors="errors"
@change="e => $emit('change', e)"
@push="e => $emit('push', e)"
@pull="e => $emit('pull', e)"
/>
</v-expand-transition>
</form-section>
<slot /> <slot />
</form-sections> </form-sections>
</div> </div>
@@ -91,11 +109,13 @@ import ProficiencySelect from '/imports/client/ui/properties/forms/shared/Profic
import FormSection from '/imports/client/ui/properties/forms/shared/FormSection.vue'; import FormSection from '/imports/client/ui/properties/forms/shared/FormSection.vue';
import createListOfProperties from '/imports/client/ui/properties/forms/shared/lists/createListOfProperties.js'; import createListOfProperties from '/imports/client/ui/properties/forms/shared/lists/createListOfProperties.js';
import propertyFormMixin from '/imports/client/ui/properties/forms/shared/propertyFormMixin.js'; import propertyFormMixin from '/imports/client/ui/properties/forms/shared/propertyFormMixin.js';
import TagTargeting from '/imports/client/ui/properties/forms/shared/TagTargeting.vue';
export default { export default {
components: { components: {
ProficiencySelect, ProficiencySelect,
FormSection, FormSection,
TagTargeting,
}, },
mixins: [propertyFormMixin], mixins: [propertyFormMixin],
data() { data() {

View File

@@ -9,7 +9,7 @@
v-if="showValue" v-if="showValue"
#value #value
> >
{{ model.value }} {{ displayedValue }}
</template> </template>
<template #prepend> <template #prepend>
<slot name="prepend" /> <slot name="prepend" />
@@ -37,7 +37,7 @@ export default {
}, },
computed: { computed: {
showValue() { showValue() {
const value = this.model.value; let value = this.displayedValue;
if ( if (
this.hideValue || this.hideValue ||
(value === undefined || value === null) || (value === undefined || value === null) ||
@@ -45,6 +45,14 @@ export default {
) return false; ) return false;
return true; return true;
}, },
displayedValue() {
let value = this.model.value;
// Use the base value instead if the calculation has it, because effects can modify the value
if (this.model.baseValue !== undefined) {
value = this.model.baseValue;
}
return value;
},
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

@@ -54,7 +54,7 @@
style="max-width: 100%;" style="max-width: 100%;"
> >
<inline-effect <inline-effect
v-if="typeof calculation.value === 'number'" v-if="typeof calculation.value === 'number' && calculation.baseValue !== 0"
hide-breadcrumbs hide-breadcrumbs
:model="{ :model="{
name: 'Base value', name: 'Base value',