Attacks can now be rolled with advantage from the stats tab

TODO the action viewer as well still
This commit is contained in:
Stefan Zermatten
2022-02-25 13:44:09 +02:00
parent f79a6d98ec
commit 59c69a46a8
6 changed files with 159 additions and 67 deletions

View File

@@ -24,16 +24,18 @@ export default function applyAction(node, {creature, targets, scope, log}){
const failed = spendResources({prop, log, scope});
if (failed) return;
const attack = prop.attackRoll || prop.attackRollBonus;
// Attack if there is an attack roll
if (prop.attackRoll && prop.attackRoll.calculation){
if (attack && attack.calculation){
if (targets.length){
targets.forEach(target => {
applyAttackToTarget({prop, target, scope, log});
applyAttackToTarget({attack, target, scope, log});
// Apply the children, but only to the current target
applyChildren(node, {targets: [target], scope, log});
});
} else {
applyAttackWithoutTarget({prop, scope, log});
applyAttackWithoutTarget({attack, scope, log});
applyChildren(node, {creature, targets, scope, log});
}
} else {
@@ -41,44 +43,34 @@ export default function applyAction(node, {creature, targets, scope, log}){
}
}
function applyAttackWithoutTarget({prop, scope, log}){
function applyAttackWithoutTarget({attack, scope, log}){
delete scope['$attackHit'];
delete scope['$attackMiss'];
delete scope['$criticalHit'];
delete scope['$criticalMiss'];
delete scope['$attackRoll'];
recalculateCalculation(prop.attackRoll, scope, log);
recalculateCalculation(attack, scope, log);
let value = rollDice(1, 20)[0];
scope['$attackRoll'] = {value};
let criticalHitTarget = scope.criticalHitTarget?.value || 20;
let criticalHit = value >= criticalHitTarget;
if (criticalHit){
scope['$criticalHit'] = {value: true};
scope['$attackHit'] = {value: true};
} else {
let criticalMiss = value === 1;
if (criticalMiss){
scope['$criticalMiss'] = 1;
log.content.push({
name: 'Critical Miss!',
});
scope['$attackMiss'] = {value: true};
} else {
// Untargeted attacks hit by default
scope['$attackHit'] = {value: true}
}
let {
resultPrefix,
result,
criticalHit,
criticalMiss,
} = rollAttack(attack, scope);
let name = criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit';
if (attack.advantage === 1 || scope['$attackAdvantage']){
name += ' (Advantage)';
} else if(attack.advantage === -1 || scope['$attackDisadvantage']){
name += ' (Disadvantage)';
}
let result = value + prop.attackRoll.value;
scope['$attackRoll'] = {value: result};
log.content.push({
name: criticalHit ? 'Critical Hit!' : 'To Hit',
value: `1d20 [${value}] + ${prop.attackRoll.value} = ` + result,
name,
value: `${resultPrefix} **${result}**`,
});
}
function applyAttackToTarget({prop, target, scope, log}){
function applyAttackToTarget({attack, target, scope, log}){
delete scope['$attackHit'];
delete scope['$attackMiss'];
delete scope['$criticalHit'];
@@ -86,26 +78,30 @@ function applyAttackToTarget({prop, target, scope, log}){
delete scope['$attackDiceRoll'];
delete scope['$attackRoll'];
recalculateCalculation(prop.attackRoll, scope, log);
recalculateCalculation(attack, scope, log);
let {
resultPrefix,
result,
criticalHit,
criticalMiss,
} = rollAttack(attack, scope);
const value = rollDice(1, 20)[0];
scope['$attackDiceRoll'] = {value};
const criticalHitTarget = scope.criticalHitTarget?.value || 20;
const criticalHit = value >= criticalHitTarget;
const criticalMiss = value === 1;
if (criticalHit) scope['$criticalHit'] = {value: true};
if (criticalMiss) scope['$criticalMiss'] = {value: true};
const result = value + prop.attackRoll.value;
scope['$attackRoll'] = {value: result};
if (target.variables.armor){
const armor = target.variables.armor.value;
const name = criticalHit ? 'Critical Hit!' :
criticalMiss ? 'Critical miss!' :
result > armor ? 'Hit!' :
'Miss!'
let name = criticalHit ? 'Critical Hit!' :
criticalMiss ? 'Critical Miss!' :
result > armor ? 'Hit!' : 'Miss!';
if (attack.advantage === 1 || scope['$attackAdvantage']){
name += ' (Advantage)';
} else if(attack.advantage === -1 || scope['$attackDisadvantage']){
name += ' (Disadvantage)';
}
log.content.push({
name,
value: `1d20 {${value}} + ${prop.attackRoll.value} = ` + result,
value: `${resultPrefix} **${result}**`,
});
if ((result > armor) || (criticalHit)){
scope['$attackHit'] = true;
@@ -118,12 +114,62 @@ function applyAttackToTarget({prop, target, scope, log}){
value:'Target has no `armor`',
});
log.content.push({
name: criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical miss!' : 'To Hit',
value: `1d20 {${value}} + ${prop.attackRoll.value} = ` + result,
name: criticalHit ? 'Critical Hit!' : criticalMiss ? 'Critical Miss!' : 'To Hit',
value: `${resultPrefix} **${result}**`,
});
}
}
function rollAttack(attack, scope){
let value, resultPrefix;
if (attack.advantage === 1 || scope['$attackAdvantage']){
const [a, b] = rollDice(2, 20);
if (a >= b) {
value = a;
resultPrefix = `1d20 [ ${a}, ~~${b}~~ ] + ${attack.value} = `;
} else {
value = b;
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] + ${attack.value} = `;
}
} else if (attack.advantage === -1 || scope['$attackDisadvantage']){
const [a, b] = rollDice(2, 20);
if (a <= b) {
value = a;
resultPrefix = `1d20 [ ${a}, ~~${b}~~ ] + ${attack.value} = `;
} else {
value = b;
resultPrefix = `1d20 [ ~~${a}~~, ${b} ] + ${attack.value} = `;
}
} else {
value = rollDice(1, 20)[0];
resultPrefix = `1d20 [${value}] + ${attack.value} = `
}
scope['$attackRoll'] = {value};
const result = value + attack.value;
const {criticalHit, criticalMiss} = applyCrits(value, scope);
return {resultPrefix, result, value, criticalHit, criticalMiss};
}
function applyCrits(value, scope){
let criticalHitTarget = scope.criticalHitTarget?.value || 20;
let criticalHit = value >= criticalHitTarget;
let criticalMiss;
if (criticalHit){
scope['$criticalHit'] = {value: true};
scope['$attackHit'] = {value: true};
} else {
criticalMiss = value === 1;
if (criticalMiss){
scope['$criticalMiss'] = 1;
scope['$attackMiss'] = {value: true};
} else {
// Untargeted attacks hit by default
scope['$attackHit'] = {value: true}
}
}
return {criticalHit, criticalMiss};
}
function applyChildren(node, args){
node.children.forEach(child => applyProperty(child, args));
}

View File

@@ -48,17 +48,27 @@ export default function applySavingThrow(node, {creature, targets, scope, log}){
let value, values, resultPrefix;
if (save.advantage === 1){
values = rollDice(2, 20).sort().reverse();
value = values[0];
resultPrefix = `Advantage: 1d20 [${values[0]},~~${values[1]}~~] + ${save.value} = `
const [a, b] = rollDice(2, 20);
if (a >= b) {
value = a;
resultPrefix = `Advantage: 1d20 [ ${a}, ~~${b}~~ ] + ${save.value} = `;
} else {
value = b;
resultPrefix = `Advantage: 1d20 [ ~~${a}~~, ${b} ] + ${save.value} = `;
}
} else if (save.advantage === -1){
values = rollDice(2, 20).sort();
value = values[0];
resultPrefix = `Disadvantage: 1d20 [${values[0]},~~${values[1]}~~] + ${save.value} = `
const [a, b] = rollDice(2, 20);
if (a <= b) {
value = a;
resultPrefix = `Advantage: 1d20 [ ${a}, ~~${b}~~ ] + ${save.value} = `;
} else {
value = b;
resultPrefix = `Advantage: 1d20 [ ~~${a}~~, ${b} ] + ${save.value} = `;
}
} else {
values = rollDice(1, 20);
value = values[0];
resultPrefix = `1d20 [${value}] + ${save.value} = `
resultPrefix = `1d20 [ ${value} ] + ${save.value} = `
}
scope['$saveDiceRoll'] = {value};
const result = value + save.value || 0;

View File

@@ -24,13 +24,17 @@ const doAction = new ValidatedMethod({
type: String,
regEx: SimpleSchema.RegEx.Id,
},
scope: {
type: Object,
blackbox: true,
},
}).validator(),
mixins: [RateLimiterMixin],
rateLimit: {
numRequests: 10,
timeInterval: 5000,
},
run({actionId, targetIds = []}) {
run({actionId, targetIds = [], scope}) {
let action = CreatureProperties.findOne(actionId);
// Check permissions
let creature = getRootCreatureAncestor(action);
@@ -69,7 +73,7 @@ const doAction = new ValidatedMethod({
});
// Do the action
doActionWork({creature, targets, properties, ancestors, method: this});
doActionWork({creature, targets, properties, ancestors, method: this, methodScope: scope});
// Recompute all involved creatures
computeCreature(creature._id);
@@ -82,7 +86,7 @@ const doAction = new ValidatedMethod({
export default doAction;
export function doActionWork({
creature, targets, properties, ancestors, method
creature, targets, properties, ancestors, method, methodScope = {}
}){
// get the docs
const ancestorScope = getAncestorScope(ancestors);
@@ -102,6 +106,7 @@ export function doActionWork({
const scope = {
...creature.variables,
...ancestorScope,
...methodScope
}
applyProperty(propertyForest[0], {
creature,

View File

@@ -16,7 +16,7 @@ const rollArray = {
};
},
toString(node){
return `${node.diceNum || ''}d${node.diceSize} [${node.values.join(', ')}]`;
return `${node.diceNum || ''}d${node.diceSize} [ ${node.values.join(', ')} ]`;
},
reduce(node, scope, context){
const total = node.values.reduce((a, b) => a + b, 0);

View File

@@ -104,7 +104,7 @@ export default {
},
methods: {
roll(){
this.$emit('roll', {advantage: this.advantage});
this.$emit('roll', {advantage: this.dataAdvantage});
this.open = false;
},
close(){

View File

@@ -5,7 +5,30 @@
>
<div class="layout align-center px-3">
<div class="avatar">
<roll-popup
v-if="rollBonus"
icon
outlined
style="font-size: 16px; letter-spacing: normal;"
class="mr-2"
:color="model.color || 'primary'"
:loading="doActionLoading"
:disabled="model.insufficientResources || !context.editPermission"
:roll-text="rollBonus"
:name="model.name"
:advantage="model.attackRoll && model.attackRoll.advantage"
@roll="doAction"
>
<template v-if="rollBonus && !rollBonusTooLong">
{{ rollBonus }}
</template>
<property-icon
v-else
:model="model"
/>
</roll-popup>
<v-btn
v-else
icon
outlined
style="font-size: 16px; letter-spacing: normal;"
@@ -15,11 +38,7 @@
:disabled="model.insufficientResources || !context.editPermission"
@click.stop="doAction"
>
<template v-if="rollBonus && !rollBonusTooLong">
{{ rollBonus }}
</template>
<property-icon
v-else
:model="model"
/>
</v-btn>
@@ -85,6 +104,7 @@ import doAction from '/imports/api/engine/actions/doAction.js';
import AttributeConsumedView from '/imports/ui/properties/components/actions/AttributeConsumedView.vue';
import ItemConsumedView from '/imports/ui/properties/components/actions/ItemConsumedView.vue';
import PropertyIcon from '/imports/ui/properties/shared/PropertyIcon.vue';
import RollPopup from '/imports/ui/components/RollPopup.vue';
import MarkdownText from '/imports/ui/components/MarkdownText.vue';
export default {
@@ -93,6 +113,7 @@ export default {
ItemConsumedView,
MarkdownText,
PropertyIcon,
RollPopup,
},
inject: {
context: {
@@ -131,7 +152,7 @@ export default {
'theme--dark': this.theme.isDark,
'theme--light': !this.theme.isDark,
'muted-text': this.model.insufficientResources,
'shrink': this.activated,
'active': this.activated,
'elevation-8': this.hovering,
}
},
@@ -143,10 +164,16 @@ export default {
click(e){
this.$emit('click', e);
},
doAction(){
doAction({advantage}){
this.doActionLoading = true;
this.shwing();
doAction.call({actionId: this.model._id}, error => {
doAction.call({
actionId: this.model._id,
scope: {
$attackAdvantage: advantage === 1 || undefined,
$attackDisadvantage: advantage === -1 || undefined,
}
}, error => {
this.doActionLoading = false;
if (error){
console.error(error);
@@ -157,7 +184,7 @@ export default {
this.activated = true;
setTimeout(() => {
this.activated = undefined;
}, 300);
}, 150);
}
}
}
@@ -165,7 +192,11 @@ export default {
<style lang="css" scoped>
.action-card {
transition: box-shadow .4s cubic-bezier(0.25, 0.8, 0.25, 1);
transition: box-shadow .4s cubic-bezier(0.25, 0.8, 0.25, 1),
transform 0.075s ease;
}
.action-card.active {
transform: scale(0.92);
}
.action-title {
font-size: 16px;