diff --git a/packages/plugins/interpreter/src/blocks/gold.ts b/packages/plugins/interpreter/src/blocks/gold.ts new file mode 100644 index 00000000..a54c92c0 --- /dev/null +++ b/packages/plugins/interpreter/src/blocks/gold.ts @@ -0,0 +1,41 @@ +import type { RpgPlayer } from "@rpgjs/server"; +import type { Block } from "../types/block"; +import { Group } from "../types/group"; + +export type ChangeGold = { + id: 'gold' + value: number +} + +export default (formatMessage): Block => ({ + id: 'gold', + group: Group.Param, + title: formatMessage({ + defaultMessage: 'Show Choices', + description: 'Choices block', + id: 'block.choice' + }), + description: formatMessage({ + defaultMessage: 'Display a choice', + description: 'Choices block description', + id: 'block.choice.description' + }), + display: ['gold'], + schema: { + type: 'object', + properties: { + value: { + type: 'number', + title: formatMessage({ + defaultMessage: 'Value', + description: 'Set Value', + }), + } + }, + required: ['value'] + }, + async execute(player: RpgPlayer, dataBlock: ChangeGold) { + player.gold += dataBlock.value + return undefined + } +}) \ No newline at end of file diff --git a/packages/plugins/interpreter/src/blocks/index.ts b/packages/plugins/interpreter/src/blocks/index.ts index 9596777a..5cd21615 100644 --- a/packages/plugins/interpreter/src/blocks/index.ts +++ b/packages/plugins/interpreter/src/blocks/index.ts @@ -1,13 +1,16 @@ import choice from './choice' import text from './text' +import gold from './gold' import type { Choice } from './choice' import type { Text } from './text' +import type { ChangeGold } from './gold' export type Flow = { - [blockId: string]: Choice | Text + [blockId: string]: Choice | Text | ChangeGold } export default [ choice, - text + text, + gold ] \ No newline at end of file diff --git a/packages/plugins/interpreter/src/interpreter.ts b/packages/plugins/interpreter/src/interpreter.ts index fa536640..8a973f3f 100644 --- a/packages/plugins/interpreter/src/interpreter.ts +++ b/packages/plugins/interpreter/src/interpreter.ts @@ -1,5 +1,5 @@ import { Observable, of, switchMap, from, throwError, timeout, map } from "rxjs" -import type { Block, BlockExecuteReturn } from "./types/block" +import type { Block, BlockExecuteReturn, Parameters } from "./types/block" import type { Edge } from "./types/edge" import { formatMessage } from "./format-message" import blocks, { Flow } from "./blocks" @@ -13,6 +13,7 @@ export class RpgInterpreter { private recursionLimit = 1000; // set the recursion limit private currentRecursionDepth = 0; // track the current recursion depth private executionTimeout = 5000; // set the execution timeout in milliseconds + private parametersSchema?: Parameters constructor( private dataBlocks: Flow, @@ -20,11 +21,13 @@ export class RpgInterpreter { options: { recursionLimit?: number; executionTimeout?: number; + parametersSchema?: (formatMessage?) => Parameters } = {}, _formatMessage = formatMessage ) { this.recursionLimit = options.recursionLimit || this.recursionLimit; this.executionTimeout = options.executionTimeout || this.executionTimeout; + this.parametersSchema = options.parametersSchema?.(_formatMessage) blocks.forEach(block => { const _blocks = block(_formatMessage) this.blocksById[_blocks.id] = _blocks @@ -44,18 +47,33 @@ export class RpgInterpreter { * * @return {Observable} The RPG interpreter execution as an Observable. */ - start({ player, blockId }: { player: RpgPlayer, blockId: string }): Observable { - const validate = this.validateFlow(blockId) - if (validate === null) { - return this.executeFlow(player, blockId) + start({ player, blockId, parameters }: { player: RpgPlayer, blockId: string, parameters?}): Observable { + const validateParams = this.validateParameters(parameters) + if (validateParams === null) { + const validate = this.validateFlow(blockId) + if (validate === null) { + return this.executeFlow(player, blockId, parameters) + } + return throwError(() => validate) } - return throwError(() => validate) + return throwError(() => validateParams) } getHistory() { return this.executionHistory } + validateParameters(parameters: any): ZodError | null { + if (this.parametersSchema) { + const schemaZod = z.object(jsonSchemaToZod(this.parametersSchema)); + const parse = schemaZod.safeParse(parameters); + if (!parse.success) { + return parse.error + } + } + return null + } + /** * Validate a block. * @@ -183,7 +201,7 @@ export class RpgInterpreter { * * @return {Observable} The flow execution as an Observable. */ - private executeFlow(player: RpgPlayer, blockId: string): Observable { + private executeFlow(player: RpgPlayer, blockId: string, parameters?: Parameters): Observable { // Prevent recursion beyond the limit if (this.currentRecursionDepth++ > this.recursionLimit) { return throwError(() => new Error('Recursion limit exceeded')); diff --git a/packages/plugins/interpreter/src/types/block.ts b/packages/plugins/interpreter/src/types/block.ts index 40258610..1feeaf61 100644 --- a/packages/plugins/interpreter/src/types/block.ts +++ b/packages/plugins/interpreter/src/types/block.ts @@ -15,6 +15,8 @@ export interface Block { execute: (player: RpgPlayer, dataBlock: any) => BlockExecute; } +export interface Parameters extends SchemaInterface {} + interface DisplayItem { key: string; hasHandles: boolean; diff --git a/packages/plugins/interpreter/src/types/group.ts b/packages/plugins/interpreter/src/types/group.ts index 2dfd6478..03af2514 100644 --- a/packages/plugins/interpreter/src/types/group.ts +++ b/packages/plugins/interpreter/src/types/group.ts @@ -1,4 +1,5 @@ export enum Group { UI = 'ui', Logic = 'logic', + Param = 'param', } \ No newline at end of file diff --git a/packages/plugins/interpreter/src/validate.ts b/packages/plugins/interpreter/src/validate.ts index 5b9894f7..0c72c3cc 100644 --- a/packages/plugins/interpreter/src/validate.ts +++ b/packages/plugins/interpreter/src/validate.ts @@ -3,6 +3,9 @@ import { z, ZodType } from "zod"; const percent = z.number().min(0).max(100) const typeMap: Record z.ZodType> = { + 'code': () => z.string(), + 'color': () => z.string().regex(/^#(?:[0-9a-fA-F]{3}){1,2}$/, 'Invalid color format'), + 'date': () => z.date(), 'password': () => z.string(), 'email': () => z.string().email(), 'text': () => z.string(), diff --git a/packages/plugins/interpreter/tests/interpreter.spec.ts b/packages/plugins/interpreter/tests/interpreter.spec.ts index a8e32ac3..94503022 100644 --- a/packages/plugins/interpreter/tests/interpreter.spec.ts +++ b/packages/plugins/interpreter/tests/interpreter.spec.ts @@ -106,6 +106,7 @@ describe('Interpreter', () => { player = { showText: vi.fn().mockResolvedValue(undefined), showChoices: vi.fn().mockResolvedValue({ text: 'test', value: 0 }), + gold: 0 } }) @@ -207,6 +208,69 @@ describe('Interpreter', () => { }))).rejects.toThrow('Timeout has occurred'); }) + describe('With Parameters', () => { + const fnSchema = (formatMessage) => { + return { + type: 'object', + properties: { + gold: { + type: 'number', + title: formatMessage({ + defaultMessage: 'Value', + description: 'Set Value', + }), + } + }, + required: ['gold'] + } + } + + test('Flow with Parameters, Wrong Param', async () => { + const interpreter = new RpgInterpreter({ + block1: { + id: 'text', + text: 'Hello World 1' + } + }, {}, { + parametersSchema: fnSchema + }) + + await expect(lastValueFrom(interpreter.start({ + player, + blockId: 'block1' + }))).rejects.toThrow( + expect.objectContaining({ + errors: expect.arrayContaining([ + expect.objectContaining({ + code: 'invalid_type' + }) + ]) + }) + ) + }) + + test('Flow with Parameters, True Param', async () => { + const interpreter = new RpgInterpreter({ + block1: { + id: 'gold', + value: 10 + } + }, {}, { + parametersSchema: fnSchema + }) + + await lastValueFrom(interpreter.start({ + player, + blockId: 'block1', + parameters: { + gold: 100 + } + })) + + expect(player.gold).toEqual(10) + }) + }) + afterEach(() => { vi.restoreAllMocks() })