From f4f1c961874aa3033d78f9cb9fa818a96b95ea8f Mon Sep 17 00:00:00 2001 From: Andrey Date: Wed, 29 Jul 2026 23:09:37 +0500 Subject: [PATCH] feat: Menu decorator --- src/decorators/common/index.ts | 3 +- src/decorators/common/menu.decorator.ts | 100 ++++++++++++++++++ src/plugins/regenration.plugin.ts | 11 +- .../decorators/common-decorators.types.ts | 28 ++++- test/gametest.ts | 5 +- 5 files changed, 143 insertions(+), 4 deletions(-) create mode 100644 src/decorators/common/menu.decorator.ts diff --git a/src/decorators/common/index.ts b/src/decorators/common/index.ts index bf5c365..4d41982 100644 --- a/src/decorators/common/index.ts +++ b/src/decorators/common/index.ts @@ -1,2 +1,3 @@ export * from './when.decorator.js' -export * from "./chance.decorator.js" \ No newline at end of file +export * from "./chance.decorator.js" +export * from "./menu.decorator.js" \ No newline at end of file diff --git a/src/decorators/common/menu.decorator.ts b/src/decorators/common/menu.decorator.ts new file mode 100644 index 0000000..3e31a8d --- /dev/null +++ b/src/decorators/common/menu.decorator.ts @@ -0,0 +1,100 @@ +import type { Game } from "@core" +import { CanvasPlugin, ConsolePlugin, GraphicPlugin } from "@plugins" +import type { MenuDecoratorOptions, MenuOutputType } from "@types" +import { registerAnyDecorator } from "@decorators" +import { exec } from "child_process" + +/** + * Decorator marks this Geneartor method as a Menu + * Menu transform generator into Promise, that will completed + * after all choises ends. + * Your generator must return String choise text + * @param customOutput - Custom UI output plugin options + * @param { Game } core - Game, if your plugin doesnt has 'core' or 'game' property + */ +export function Menu({ customUIPropeties, browserProperties, name }: MenuDecoratorOptions, core?: Game) { + return (target: any, methodName: string, descriptor: PropertyDescriptor) => { + registerAnyDecorator(target, methodName, 'menuList', { name }, 'METHOD') + + const original = descriptor.value + + descriptor.value = function (...args: any[]) { + const iterator: Generator = original.apply(this, args) + const plugin = this as any + const game = plugin.core as Game ?? plugin.game as Game ?? core + + if (!game) throw new Error('[Menu]: Decorator error. Game is undefined') + + const plugins = game.getAllPlugins().map(plugin => plugin.name) + + let outputGuiType: MenuOutputType | undefined = undefined; + let outputPluginName: string; + + for (const currentPlugin of plugins) { + if (currentPlugin === ConsolePlugin.name || currentPlugin === GraphicPlugin.name) { + outputGuiType = 'APP' + outputPluginName = currentPlugin + } + else if (currentPlugin === CanvasPlugin.name) { + outputGuiType = 'BROWSER' + outputPluginName = currentPlugin + } + + if (customUIPropeties && currentPlugin === customUIPropeties.name) { + outputGuiType = customUIPropeties.outputType + outputPluginName = currentPlugin + } + } + + if (!outputGuiType) throw new Error('[Menu]: can`t find output UI plugin') + + return new Promise((resolve, reject) => { + function step(value?: any) { + try { + const result = iterator.next(value) + const resultValue = result.value + + if (result.done) return resolve(resultValue) + else { + switch(outputGuiType) { + case 'APP': + if (outputPluginName === ConsolePlugin.name) { + process.stdout.write(resultValue) + process.stdin.once('data', (data: string) => step(data)) + } + else { + exec(`zenity --entry --title="${resultValue}" --text="Ваш выбор:"`, (err, out) => { + if (err) return reject(err) + else return step(out.trim()) + }) + } + + break + case 'BROWSER': + if (!browserProperties) throw new Error('[Menu]: undefined browser properties with Browser output type') + else { + function buttonListener() { + step(browserProperties?.valueEl.value) + + browserProperties?.button.removeEventListener('click', buttonListener) + } + + browserProperties.button.addEventListener('click', buttonListener) + browserProperties.renderEl.innerText = resultValue + } + + break + } + } + } catch (error) { + reject(error) + } + } + + step() + }) + } + + return descriptor + } +} \ No newline at end of file diff --git a/src/plugins/regenration.plugin.ts b/src/plugins/regenration.plugin.ts index 55ef598..b4a5cb3 100644 --- a/src/plugins/regenration.plugin.ts +++ b/src/plugins/regenration.plugin.ts @@ -1,5 +1,5 @@ import type { Game } from "@core"; -import { InjectCore, InjectLiveQuery, InjectLiveQueryObject, InjectStoreValue, OnCustomEvent, OnEvent, OnTick, When, OnTagAdded, OnTagDeleted, Chance, OnUIEvent } from "@decorators"; +import { InjectCore, InjectLiveQuery, InjectLiveQueryObject, InjectStoreValue, OnCustomEvent, OnEvent, OnTick, When, OnTagAdded, OnTagDeleted, Chance, OnUIEvent, Menu } from "@decorators"; import type { IPlugin, IEventInfo } from "@interfaces"; import { anyWorldObjectIsGameObject } from "@utils"; import type { Entity, GameObject } from "@world" @@ -73,6 +73,15 @@ export class RegenerationPlugin implements IPlugin { console.log('LISTEN TAG DELETED') } + @Menu({ + name: "менюшка" + }) + public *firstMenu(): Generator { + const choise = yield 'Ваш выбор?' + + console.log('ВЫБОР -', choise) + } + public install(game: Game) { game.options.store.set(this.storePluginKey, this.HEALTH_REGEN_VALUE) game.registerCustomEvent('regenerate', (o, e, d) => { diff --git a/src/types/decorators/common-decorators.types.ts b/src/types/decorators/common-decorators.types.ts index c52b469..fa92030 100644 --- a/src/types/decorators/common-decorators.types.ts +++ b/src/types/decorators/common-decorators.types.ts @@ -5,4 +5,30 @@ import type { BaseMethodDecorator } from "@types"; * Propeties for When decorator. Accepts When callback, must return true, if * need execute this method on next game tick */ -export type WhenDecoratorProperties = BaseMethodDecorator & { readonly when: (game: Game) => boolean } \ No newline at end of file +export type WhenDecoratorProperties = BaseMethodDecorator & { readonly when: (game: Game) => boolean } + +/** + * Properties for custom UI Plugin menu renderer + */ +export type MenuDecoratorCustomUIProperties = { readonly name: string, readonly outputType: MenuOutputType } + +/** + * Properties for Browser Menu, where button - element to add listener menu, + * valueEl - element with value property, to read menu choise + * renderEl - Element to see innerText + */ +export type MenuDecoratorBrowserProperties = { readonly button: HTMLElement, readonly valueEl: HTMLInputElement, readonly renderEl: HTMLElement } + +/** + * Type of menu decorator options + */ +export type MenuDecoratorOptions = { + readonly customUIPropeties?: MenuDecoratorCustomUIProperties; + readonly browserProperties?: MenuDecoratorBrowserProperties; + readonly name: string; +} + +/** + * Types of render Menu + */ +export type MenuOutputType = 'BROWSER' | 'APP' \ No newline at end of file diff --git a/test/gametest.ts b/test/gametest.ts index c6536b7..1dbbd93 100644 --- a/test/gametest.ts +++ b/test/gametest.ts @@ -15,7 +15,9 @@ const [game, manager, map] = createGame({ gameGeometry: "2D" }) -game.registerPlugin([new RegenerationPlugin(20)]) +const regenPlugin = new RegenerationPlugin(20) + +game.registerPlugin([regenPlugin]) game.registerPlugin(new GraphicPlugin({ appName: "Моя первая игра на stemma", assets: { @@ -25,6 +27,7 @@ game.registerPlugin(new GraphicPlugin({ } })) +regenPlugin.firstMenu() const deligator = new Deligator({ observe: game, source: game,