Started with async inputs to actions
This commit is contained in:
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"alea",
|
||||
"autorun",
|
||||
"blackbox",
|
||||
"Crits",
|
||||
|
||||
@@ -7,12 +7,12 @@ import propertySchemasIndex from '/imports/api/properties/computedPropertySchema
|
||||
import { storedIconsSchema } from '/imports/api/icons/Icons';
|
||||
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS';
|
||||
|
||||
const CreatureProperties: Mongo.Collection<CreatureProperty> = new Mongo.Collection('creatureProperties');
|
||||
// TODO make this a union type of all CreatureProperty types
|
||||
const CreatureProperties: Mongo.Collection<any> = new Mongo.Collection('creatureProperties');
|
||||
|
||||
export interface CreatureProperty extends TreeDoc {
|
||||
export interface CreatureProperty {
|
||||
_id: string
|
||||
_migrationError?: string
|
||||
type: string
|
||||
tags: string[]
|
||||
disabled?: boolean
|
||||
icon?: {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { loadCreature } from '/imports/api/engine/loadCreatures';
|
||||
import EngineActions, { EngineAction } from '/imports/api/engine/action/EngineActions';
|
||||
import { applyAction } from '/imports/api/engine/action/functions/applyAction';
|
||||
import { LogContent, Removal, Update } from '/imports/api/engine/action/tasks/TaskResult';
|
||||
import inputProvider from '/imports/api/engine/action/functions/inputProviderForTests.testFn';
|
||||
|
||||
const creatureId = Random.id();
|
||||
const targetId = Random.id();
|
||||
@@ -81,11 +82,12 @@ describe('Interrupt action system', function () {
|
||||
[{ value: 'child 2 of index branch' }]
|
||||
);
|
||||
});
|
||||
it('Halts execution of choice branches', async function () {
|
||||
let userInputRequested = false;
|
||||
const requestUserInput = () => { userInputRequested = true; return 0 };
|
||||
await runActionById(choiceBranchId, requestUserInput);
|
||||
assert.isTrue(userInputRequested, 'User input should be requested when a choice branch is applied');
|
||||
it('Gets choices from choice branches', async function () {
|
||||
const action = await runActionById(choiceBranchId);
|
||||
assert.deepEqual(
|
||||
allLogContent(action),
|
||||
[{ value: 'child 1 of choice branch' }]
|
||||
);
|
||||
});
|
||||
it('Applies adjustments', async function () {
|
||||
let action = await runActionById(adjustmentSetId);
|
||||
@@ -205,12 +207,12 @@ function createAction(prop, targetIds?) {
|
||||
return EngineActions.insertAsync(action);
|
||||
}
|
||||
|
||||
async function runActionById(propId, userInputFn = () => 0) {
|
||||
async function runActionById(propId) {
|
||||
const prop = await CreatureProperties.findOneAsync(propId);
|
||||
const actionId = await createAction(prop);
|
||||
const action = await EngineActions.findOneAsync(actionId);
|
||||
if (!action) throw 'Action is expected to exist';
|
||||
await applyAction(action, userInputFn, { simulate: true });
|
||||
await applyAction(action, inputProvider, { simulate: true });
|
||||
return action;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
runActionById
|
||||
} from '/imports/api/engine/action/functions/actionEngineTest.testFn';
|
||||
import { Mutation, Update } from '/imports/api/engine/action/tasks/TaskResult';
|
||||
import Alea from 'alea';
|
||||
|
||||
const [
|
||||
creatureId, targetCreatureId, targetCreature2Id,
|
||||
@@ -93,6 +94,14 @@ describe('Apply Action Properties', function () {
|
||||
await createTestCreature(actionTargetCreature2);
|
||||
});
|
||||
|
||||
it('should generate random numbers reliably given consistent seeds', function () {
|
||||
const aleaFraction = Alea('test', 'seeds');
|
||||
const randomNumbers = [aleaFraction(), aleaFraction(), aleaFraction()];
|
||||
assert.deepEqual(randomNumbers, [
|
||||
0.19889510236680508, 0.9176857066340744, 0.042551583144813776
|
||||
]);
|
||||
});
|
||||
|
||||
it('should run empty actions', async function () {
|
||||
const action = await runActionById(emptyActionId);
|
||||
assert.exists(action);
|
||||
|
||||
@@ -24,7 +24,7 @@ export default async function applyAdjustmentProperty(
|
||||
}
|
||||
|
||||
// Evaluate the amount
|
||||
await recalculateCalculation(prop.amount, action, 'reduce');
|
||||
await recalculateCalculation(prop.amount, action, 'reduce', userInput);
|
||||
const value = +prop.amount.value;
|
||||
if (!isFinite(value)) {
|
||||
return;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { filter } from 'lodash';
|
||||
import { EngineAction } from '/imports/api/engine/action/EngineActions';
|
||||
import InputProvider from '/imports/api/engine/action/functions/InputProvider';
|
||||
import { applyAfterPropTasksForSingleChild, applyAfterTasksSkipChildren, applyDefaultAfterPropTasks, applyTaskToEachTarget } from '/imports/api/engine/action/functions/applyTaskGroups';
|
||||
import { applyAfterPropTasksForSingleChild, applyAfterPropTasksForSomeChildren, applyAfterTasksSkipChildren, applyDefaultAfterPropTasks, applyTaskToEachTarget } from '/imports/api/engine/action/functions/applyTaskGroups';
|
||||
import { getEffectiveActionScope } from '/imports/api/engine/action/functions/getEffectiveActionScope';
|
||||
import recalculateCalculation from '/imports/api/engine/action/functions/recalculateCalculation';
|
||||
import { PropTask } from '/imports/api/engine/action/tasks/Task';
|
||||
@@ -16,7 +17,7 @@ export default async function applyBranchProperty(
|
||||
|
||||
switch (prop.branchType) {
|
||||
case 'if': {
|
||||
await recalculateCalculation(prop.condition, action, 'reduce');
|
||||
await recalculateCalculation(prop.condition, action, 'reduce', userInput);
|
||||
if (prop.condition?.value) {
|
||||
return applyDefaultAfterPropTasks(action, prop, targets, userInput);
|
||||
} else {
|
||||
@@ -28,7 +29,7 @@ export default async function applyBranchProperty(
|
||||
if (!children.length) {
|
||||
return applyAfterTasksSkipChildren(action, prop, targets, userInput);
|
||||
}
|
||||
await recalculateCalculation(prop.condition, action, 'reduce');
|
||||
await recalculateCalculation(prop.condition, action, 'reduce', userInput);
|
||||
if (!isFinite(prop.condition?.value)) {
|
||||
result.appendLog({
|
||||
name: 'Branch Error',
|
||||
@@ -110,21 +111,17 @@ export default async function applyBranchProperty(
|
||||
}
|
||||
return applyDefaultAfterPropTasks(action, prop, targets, userInput);
|
||||
case 'choice': {
|
||||
let index;
|
||||
if (action._isSimulation) {
|
||||
index = await userInput(prop);
|
||||
} else {
|
||||
// TODO
|
||||
throw 'Reading stored user input not implemented'
|
||||
}
|
||||
const children = await getPropertyChildren(action.creatureId, prop);
|
||||
if (!children.length) {
|
||||
let choices: string[];
|
||||
let chosenChildren: typeof children = [];
|
||||
if (children.length) {
|
||||
choices = await userInput.choose(action, children);
|
||||
chosenChildren = filter(children, child => choices.includes(child._id));
|
||||
}
|
||||
if (!children.length || !chosenChildren.length) {
|
||||
return applyAfterTasksSkipChildren(action, prop, targets, userInput);
|
||||
}
|
||||
if (!isFinite(index) || index < 0) index = 0;
|
||||
if (index > children.length - 1) index = children.length - 1;
|
||||
const child = children[index];
|
||||
return applyAfterPropTasksForSingleChild(action, prop, child, targets, userInput);
|
||||
return applyAfterPropTasksForSomeChildren(action, prop, chosenChildren, targets, userInput);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,17 @@ type InputProvider = {
|
||||
rollDice(
|
||||
action: EngineAction, dice: { number: number, diceSize: number }[]
|
||||
): Promise<number[][]>;
|
||||
/**
|
||||
* Choose from a provided selection
|
||||
* @param action
|
||||
* @param choices Options to choose from
|
||||
* @param quantity Number of choices to make [min, max] inclusive, where -1 means no limit
|
||||
*/
|
||||
choose(
|
||||
action: EngineAction,
|
||||
choices: ({ _id: string } & Record<string, any>)[],
|
||||
quantity?: [min: number, max: number],
|
||||
): Promise<string[]>;
|
||||
}
|
||||
|
||||
export default InputProvider;
|
||||
@@ -8,8 +8,7 @@ import { loadCreature } from '/imports/api/engine/loadCreatures';
|
||||
import EngineActions, { EngineAction } from '/imports/api/engine/action/EngineActions';
|
||||
import { applyAction } from '/imports/api/engine/action/functions/applyAction';
|
||||
import { LogContent, Mutation, Removal, Update } from '/imports/api/engine/action/tasks/TaskResult';
|
||||
import InputProvider from '/imports/api/engine/action/functions/InputProvider';
|
||||
|
||||
import inputProvider from '/imports/api/engine/action/functions/inputProviderForTests.testFn';
|
||||
/**
|
||||
* Removes all creatures, properties, and creatureVariable documents from the database
|
||||
*/
|
||||
@@ -59,7 +58,7 @@ export const randomIds = new Array(100).fill(undefined).map(() => Random.id());
|
||||
* @param userInputFn A function that simulates user input
|
||||
* @returns The Engine Action with mutations resulting from running the action
|
||||
*/
|
||||
export async function runActionById(propId, targetIds?, userInput = testInputProvider) {
|
||||
export async function runActionById(propId, targetIds?, userInput = inputProvider) {
|
||||
const prop = await CreatureProperties.findOneAsync(propId);
|
||||
const actionId = await createAction(prop, targetIds);
|
||||
const action = await EngineActions.findOneAsync(actionId);
|
||||
@@ -148,25 +147,3 @@ export function allLogContent(action: EngineAction) {
|
||||
});
|
||||
return contents;
|
||||
}
|
||||
|
||||
const testInputProvider: InputProvider = {
|
||||
/**
|
||||
* For testing, randomness is hard to deal with
|
||||
* rollDice function returns the average roll for every dice rolled
|
||||
* [5d10, 1d4] => [[6,6,6,6,6], [3]]
|
||||
*/
|
||||
async rollDice(action, dice) {
|
||||
const result: number[][] = [];
|
||||
for (const diceRoll of dice) {
|
||||
const averageRoll = Math.round(diceRoll.diceSize / 2);
|
||||
// Return an array full of averagely rolled dice
|
||||
result.push(
|
||||
new Array(diceRoll.number)
|
||||
.fill(averageRoll)
|
||||
)
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export { testInputProvider }
|
||||
@@ -16,7 +16,6 @@ export async function applyChildren(
|
||||
action: EngineAction, prop, targetIds: string[], userInput
|
||||
) {
|
||||
const children = await getPropertyChildren(action.creatureId, prop);
|
||||
// Push the child tasks and related triggers to the stack
|
||||
for (const childProp of children) {
|
||||
await applyTask(action, { prop: childProp, targetIds }, userInput);
|
||||
}
|
||||
@@ -101,6 +100,25 @@ export async function applyAfterPropTasksForSingleChild(
|
||||
await applyAfterChildrenTriggers(action, prop, targetIds, userInput);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of tasks containing the following:
|
||||
* After triggers
|
||||
* After-children triggers
|
||||
* @param action
|
||||
* @param prop
|
||||
* @param targetIds
|
||||
* @returns
|
||||
*/
|
||||
export async function applyAfterPropTasksForSomeChildren(
|
||||
action: EngineAction, prop, children, targetIds: string[], userInput
|
||||
) {
|
||||
await applyAfterTriggers(action, prop, targetIds, userInput);
|
||||
for (const childProp of children) {
|
||||
await applyTask(action, { prop: childProp, targetIds }, userInput);
|
||||
}
|
||||
await applyAfterChildrenTriggers(action, prop, targetIds, userInput);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the trigger tasks for a given trigger path
|
||||
* @param action
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import InputProvider from '/imports/api/engine/action/functions/InputProvider';
|
||||
|
||||
const inputProviderForTests: InputProvider = {
|
||||
/**
|
||||
* For testing, randomness is hard to deal with
|
||||
* rollDice function returns the average roll for every dice rolled
|
||||
* [5d10, 1d4] => [[6,6,6,6,6], [3]]
|
||||
*/
|
||||
async rollDice(action, dice) {
|
||||
const result: number[][] = [];
|
||||
for (const diceRoll of dice) {
|
||||
const averageRoll = Math.round(diceRoll.diceSize / 2);
|
||||
// Return an array full of averagely rolled dice
|
||||
result.push(
|
||||
new Array(diceRoll.number)
|
||||
.fill(averageRoll)
|
||||
)
|
||||
}
|
||||
return result;
|
||||
},
|
||||
/**
|
||||
* For testing, always return the minimum number of choices, always choosing the first options
|
||||
*/
|
||||
async choose(action, choices, quantity = [1, 1]) {
|
||||
const chosen: string[] = [];
|
||||
const choiceQuantity = quantity[0] <= 0 ? 1 : quantity[0];
|
||||
for (let i = 0; i < choiceQuantity && i < choices.length; i += 1) {
|
||||
chosen.push(choices[i]._id);
|
||||
}
|
||||
return chosen;
|
||||
}
|
||||
}
|
||||
|
||||
export default inputProviderForTests;
|
||||
@@ -6,12 +6,24 @@ import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX';
|
||||
import { CreatureProperty } from '/imports/api/creature/creatureProperties/CreatureProperties';
|
||||
import { InlineCalculation } from '/imports/api/properties/subSchemas/inlineCalculationField';
|
||||
import { CalculatedField } from '/imports/api/properties/subSchemas/computedField';
|
||||
import Property from '/imports/api/properties/Properties.type';
|
||||
|
||||
export type CreatureAction = Action & CreatureProperty & {
|
||||
overridden?: boolean
|
||||
insufficientResources?: boolean
|
||||
}
|
||||
|
||||
/*
|
||||
* Actions are things a character can do
|
||||
*/
|
||||
export interface Action extends ActionBase {
|
||||
type: 'action'
|
||||
}
|
||||
|
||||
export interface ActionBase extends CreatureProperty {
|
||||
/**
|
||||
* Base property type for both spells and actions
|
||||
*/
|
||||
export interface ActionBase extends Property {
|
||||
name?: string
|
||||
summary?: InlineCalculation
|
||||
description?: InlineCalculation
|
||||
@@ -23,9 +35,7 @@ export interface ActionBase extends CreatureProperty {
|
||||
usesUsed?: number
|
||||
reset?: string
|
||||
silent?: boolean
|
||||
insufficientResources?: boolean
|
||||
usesLeft?: number
|
||||
overridden?: boolean
|
||||
// Resources
|
||||
resources: {
|
||||
itemsConsumed: {
|
||||
|
||||
@@ -2,11 +2,57 @@ import SimpleSchema from 'simpl-schema';
|
||||
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX';
|
||||
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS';
|
||||
import createPropertySchema from '/imports/api/properties/subSchemas/createPropertySchema';
|
||||
import { CreatureProperty } from '/imports/api/creature/creatureProperties/CreatureProperties';
|
||||
import { CalculatedField } from '/imports/api/properties/subSchemas/computedField';
|
||||
import { InlineCalculation } from '/imports/api/properties/subSchemas/inlineCalculationField';
|
||||
import { ConstantValueType } from '/imports/parser/parseTree/constant';
|
||||
import Property from '/imports/api/properties/Properties.type';
|
||||
|
||||
export type CreatureAttribute = Attribute & CreatureProperty & {
|
||||
total?: ConstantValueType;
|
||||
value?: ConstantValueType;
|
||||
modifier?: number;
|
||||
proficiency?: 0 | 0.49 | 0.5 | 1 | 2;
|
||||
advantage?: -1 | 0 | 1;
|
||||
constitutionMod?: number;
|
||||
hide?: true;
|
||||
overridden?: true;
|
||||
effectIds?: string[];
|
||||
proficiencyIds?: string[];
|
||||
definitions?: { _id: string, type: string, row?: number }[];
|
||||
}
|
||||
|
||||
export interface Attribute extends Property {
|
||||
type: 'attribute';
|
||||
name?: string;
|
||||
variableName?: string;
|
||||
attributeType: 'ability' | 'stat' | 'modifier' | 'hitDice' | 'healthBar' | 'resource' |
|
||||
'spellSlot' | 'utility';
|
||||
hitDiceSize?: 'd1' | 'd2' | 'd4' | 'd6' | 'd8' | 'd10' | 'd12' | 'd20' | 'd100';
|
||||
spellSlotLevel?: CalculatedField;
|
||||
healthBarColorMid?: string;
|
||||
healthBarColorLow?: string;
|
||||
healthBarNoDamage?: true;
|
||||
healthBarNoHealing?: true;
|
||||
healthBarNoDamageOverflow?: true;
|
||||
healthBarNoHealingOverflow?: true;
|
||||
healthBarDamageOrder?: number;
|
||||
healthBarHealingOrder?: number;
|
||||
baseValue?: CalculatedField;
|
||||
description?: InlineCalculation;
|
||||
damage?: number;
|
||||
decimal?: true;
|
||||
ignoreLowerLimit?: true;
|
||||
ignoreUpperLimit?: true;
|
||||
hideWhenTotalZero?: true;
|
||||
hideWhenValueZero?: true;
|
||||
reset?: string;
|
||||
}
|
||||
|
||||
/*
|
||||
* Attributes are numbered stats of a character
|
||||
*/
|
||||
let AttributeSchema = createPropertySchema({
|
||||
const AttributeSchema = createPropertySchema({
|
||||
name: {
|
||||
type: String,
|
||||
optional: true,
|
||||
@@ -134,7 +180,7 @@ let AttributeSchema = createPropertySchema({
|
||||
},
|
||||
});
|
||||
|
||||
let ComputedOnlyAttributeSchema = createPropertySchema({
|
||||
const ComputedOnlyAttributeSchema = createPropertySchema({
|
||||
description: {
|
||||
type: 'computedOnlyInlineCalculationField',
|
||||
optional: true,
|
||||
12
app/imports/api/properties/Properties.type.ts
Normal file
12
app/imports/api/properties/Properties.type.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { TreeDoc } from '/imports/api/parenting/ChildSchema';
|
||||
|
||||
export default interface Property extends TreeDoc {
|
||||
_id: string
|
||||
_migrationError?: string
|
||||
tags: string[]
|
||||
icon?: {
|
||||
name: string
|
||||
shape: string
|
||||
},
|
||||
slotQuantityFilled?: number
|
||||
}
|
||||
5
app/package-lock.json
generated
5
app/package-lock.json
generated
@@ -1790,6 +1790,11 @@
|
||||
"uri-js": "^4.2.2"
|
||||
}
|
||||
},
|
||||
"alea": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/alea/-/alea-1.0.1.tgz",
|
||||
"integrity": "sha512-QU+wv+ziDXaMxRdsQg/aH7sVfWdhKps5YP97IIwFkHCsbDZA3k87JXoZ5/iuemf4ntytzIWeScrRpae8+lDrXA=="
|
||||
},
|
||||
"ansi-colors": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"@chenfengyuan/vue-countdown": "^1.1.5",
|
||||
"@tozd/vue-observer-utils": "^0.5.0",
|
||||
"@types/meteor": "^2.9.8",
|
||||
"alea": "^1.0.1",
|
||||
"aws-sdk": "^2.1559.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"chroma-js": "^2.4.2",
|
||||
|
||||
Reference in New Issue
Block a user