diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js index 5a0477b9..7d4e58f1 100644 --- a/docs/.vitepress/config.js +++ b/docs/.vitepress/config.js @@ -38,11 +38,27 @@ const apiMenu = [ { text: "GUI Class", link: "/classes/gui" }, { text: "Sound Class", link: "/classes/sound" }, { text: "Resource Class", link: "/classes/resource" }, - { text: "Keyboard Class", link: "/classes/keyboard" }, - { text: "Vue Inject Class", link: "/classes/vue-inject" } + { text: "Keyboard Class", link: "/classes/keyboard" } ] }, + { + text: 'VueJS', + collapsed: false, + sidebarDepth: 2, + items: [ + { text: "Vue Inject Class", link: "/classes/vue-inject" }, + { text: "Vue directives", link: "/api-gui/vue-directive" } + ] + }, + { + text: 'React', + collapsed: false, + sidebarDepth: 2, + items: [ + { text: "React Hooks", link: "/api-gui/react" } + ] + }, { text: 'Testing', collapsed: false, @@ -252,6 +268,7 @@ module.exports = { '/commands/': apiMenu, '/database/': apiMenu, '/api/': apiMenu, + '/api-gui/': apiMenu, '/guide/': guideMenu, '/gui/': guideMenu, '/advanced/': guideMenu, diff --git a/docs/api-gui/react.md b/docs/api-gui/react.md new file mode 100644 index 00000000..3cf7a957 --- /dev/null +++ b/docs/api-gui/react.md @@ -0,0 +1,67 @@ +# React Hooks + +## Introduction + +React hooks are a powerful feature in React that allow you to use state and other React features without writing a class. In the context of RPGJS, a framework for creating RPG games, React hooks can be particularly useful for managing game state and interactions. + +## 1. `useEventPropagator()` + +This hook is used to propagate events within the canvas element of your RPGJS game. + +### Importing + +```javascript +import { useEventPropagator } from '@rpgjs/client/react'; +``` + +### Usage + +```javascript +export default function Test() { + const propagate = useEventPropagator(); + + return
test
; +} +``` + +In this example, the `useEventPropagator` hook is used to create a `propagate` function. This function is then passed to a `div` element as a reference (`ref`). This setup allows events within the `div` to be propagated through the RPGJS game canvas. + +--- + +## 2. `RpgReactContext` + +This hook provides access to the RPGJS context, allowing you to interact with various game states like the current player's health points (HP). + +### Importing + +```javascript +import { RpgReactContext } from '@rpgjs/client/react'; +import { useContext, useEffect, useState } from 'react'; +``` + +### Usage + +```javascript +export default function MyGUI({ foo }) { + const { rpgCurrentPlayer } = useContext(RpgReactContext); + const [hp, setHp] = useState(0); + + useEffect(() => { + const subscription = rpgCurrentPlayer.subscribe(({ object }) => { + setHp(object.hp); + }); + + return () => { + subscription.unsubscribe(); + }; + }, []); + + return ( +
+

{hp}

+
+ ); +} +``` + +In this example, `RpgReactContext` is used to access the current player's state. The `useContext` hook retrieves the `rpgCurrentPlayer` from `RpgReactContext`. We then use `useState` to manage the player's HP locally. The `useEffect` hook is used to subscribe to changes in the player's HP, updating the local state accordingly. When the component unmounts, the subscription is unsubscribed. \ No newline at end of file diff --git a/docs/api-gui/vue-directive.md b/docs/api-gui/vue-directive.md new file mode 100644 index 00000000..08277e42 --- /dev/null +++ b/docs/api-gui/vue-directive.md @@ -0,0 +1,17 @@ +# Directive for VueJS + +## `v-propagate` + +The `v-propagate` directive is straightforward to use. Simply add it to any element in your VueJS template to enable event propagation for that element within the RPGJS canvas. + +### Example + +```vue + +``` + +In this example, the `v-propagate` directive is attached to a `div` element. Any events that occur within this `div` will be propagated through the RPGJS game canvas. This is particularly useful for integrating VueJS-based GUI elements with the RPGJS game canvas, allowing for seamless interaction between the GUI and the game. \ No newline at end of file diff --git a/docs/guide/inputs.md b/docs/guide/inputs.md index 2107dd09..9cf14d7c 100644 --- a/docs/guide/inputs.md +++ b/docs/guide/inputs.md @@ -39,4 +39,18 @@ The key corresponds to the type of control (used by the keyboard, the mouse, or You have information here: [Set Inputs](/classes/keyboard.html#set-inputs) -> If you want to use keyboard numbers, don't use "1" but "n1", etc. \ No newline at end of file +::: tip Keyboard numbers +If you want to use keyboard numbers, don't use "1" but "n1", etc. +::: + +::: tip Mouse Events +Since v4.2.0, you can use mouse events. + +Example: + +```toml +[inputs.action] + name = "action" + bind = ["space", "enter", "click"], +``` +::: \ No newline at end of file diff --git a/packages/client/src/Gui/React.ts b/packages/client/src/Gui/React.ts index d66cd557..d6d2ea0d 100644 --- a/packages/client/src/Gui/React.ts +++ b/packages/client/src/Gui/React.ts @@ -1,9 +1,10 @@ import { createRoot } from 'react-dom/client'; import { createElement, Fragment, useState, createContext, useEffect, useContext, useCallback, useSyncExternalStore, useRef } from 'react' import { RpgClientEngine } from '../RpgClientEngine'; -import { RpgRenderer } from '../Renderer'; +import { EVENTS_MAP, RpgRenderer } from '../Renderer'; import { BehaviorSubject, map, tap, combineLatest, Subject } from 'rxjs'; import type { Gui } from './Gui'; +import { inject } from '../inject'; export { useStore } from '@nanostores/react' export const RpgReactContext = createContext({} as any) @@ -51,6 +52,30 @@ export const useCurrentPlayer = () => { return useSyncExternalStore(subscribe, () => currentPlayerRef.current); } +export const useEventPropagator = () => { + const ref = useRef(null); + useEffect(() => { + if (ref.current) { + const element = ref.current as HTMLElement; + const eventListeners = {}; + const renderer = inject(RpgRenderer) + + EVENTS_MAP.MouseEvent.forEach(eventType => { + const listener = event => renderer.propagateEvent(event) + element.addEventListener(eventType, listener); + eventListeners[eventType] = listener; + }); + + return () => { + EVENTS_MAP.MouseEvent.forEach(eventType => { + element.removeEventListener(eventType, eventListeners[eventType]); + }); + }; + } + }, [ref]); + return ref +}; + export class ReactGui { private app: any private clientEngine: RpgClientEngine diff --git a/packages/client/src/Gui/Vue.ts b/packages/client/src/Gui/Vue.ts index 6f618e9b..b0bfa939 100644 --- a/packages/client/src/Gui/Vue.ts +++ b/packages/client/src/Gui/Vue.ts @@ -1,7 +1,7 @@ import { renderList as _renderList, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock, resolveDynamicComponent as _resolveDynamicComponent, normalizeProps as _normalizeProps, guardReactiveProps as _guardReactiveProps, createBlock as _createBlock, mergeProps as _mergeProps, createCommentVNode as _createCommentVNode, normalizeStyle as _normalizeStyle, createElementVNode as _createElementVNode } from "vue" import { App, ComponentPublicInstance, createApp } from 'vue' import { RpgCommonPlayer, Utils } from '@rpgjs/common' -import { RpgRenderer } from '../Renderer' +import { EVENTS_MAP, RpgRenderer } from '../Renderer' import { GameEngineClient } from '../GameEngine' import { RpgClientEngine } from '../RpgClientEngine' import type { Gui } from './Gui' @@ -33,7 +33,7 @@ const _hoisted_1 = { style: { "position": "absolute", "top": "0", "left": "0" } } function render(_ctx, _cache) { - return (_openBlock(), _createElementBlock("div", { }, [ + return (_openBlock(), _createElementBlock("div", {}, [ (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.fixedGui, (ui) => { return (_openBlock(), _createElementBlock(_Fragment, null, [ (ui.display) @@ -49,7 +49,7 @@ function render(_ctx, _cache) { return (_openBlock(), _createElementBlock("div", { style: _normalizeStyle(_ctx.tooltipPosition(tooltip.position)) }, [ - (_openBlock(), _createBlock(_resolveDynamicComponent(ui.name), _mergeProps({ ...ui.data, spriteData: tooltip, style: { pointerEvents: 'auto' } }, { + (_openBlock(), _createBlock(_resolveDynamicComponent(ui.name), _mergeProps({ ...ui.data, spriteData: tooltip, style: { pointerEvents: 'auto' } }, { ref_for: true, ref: ui.name }), null, 16 /* FULL_PROPS */)) @@ -99,9 +99,6 @@ export class VueGui { methods: { tooltipPosition: parentGui.tooltipPosition.bind(parentGui), tooltipFilter: parentGui.tooltipFilter.bind(parentGui) - }, - mounted() { - } } @@ -113,6 +110,27 @@ export class VueGui { this.app.component(ui.name, ui.gui) } + this.app.directive('propagate', { + mounted: (el, binding) => { + el.eventListeners = {}; + EVENTS_MAP.MouseEvent.forEach(eventType => { + const callback = (ev) => { + this.renderer.propagateEvent(ev); + }; + el.eventListeners[eventType] = callback; + el.addEventListener(eventType, callback); + }); + }, + unmounted(el, binding) { + EVENTS_MAP.MouseEvent.forEach(eventType => { + const callback = el.eventListeners[eventType]; + if (callback) { + el.removeEventListener(eventType, callback); + } + }); + } + }) + this.vm = this.app.mount(rootEl) as VueInstance this.renderer.app = this.app this.renderer.vm = this.vm diff --git a/packages/client/src/KeyboardControls.ts b/packages/client/src/KeyboardControls.ts index 4cc3a95f..99f86b56 100644 --- a/packages/client/src/KeyboardControls.ts +++ b/packages/client/src/KeyboardControls.ts @@ -361,6 +361,18 @@ export class KeyboardControls { return this.boundKeys[inputName] } + /** + * Returns all controls + * + * @method getControls() + * @since 4.2.0 + * @returns { { [key: string]: BoundKey } } + * @memberof KeyboardControls + */ + getControls(): { [key: string]: BoundKey } { + return this.boundKeys + } + /** * Triggers an input according to the name of the control * @@ -509,6 +521,18 @@ export class KeyboardControls { * Control.Action | action * Control.Back | back * + * @enum {string} Mouse Event + * + * click | Click + * dblclick | Double Click + * mousedown | Mouse Down + * mouseup | Mouse Up + * mouseover | Mouse Over + * mousemove | Mouse Move + * mouseout | Mouse Out + * contextmenu | Context Menu + * + * * @enum {string} Input * * break | Pause diff --git a/packages/client/src/Renderer.ts b/packages/client/src/Renderer.ts index fbbc0844..9ebb61c5 100644 --- a/packages/client/src/Renderer.ts +++ b/packages/client/src/Renderer.ts @@ -10,6 +10,7 @@ import { Subject, forkJoin } from 'rxjs' import { GameEngineClient } from './GameEngine' import { SpinnerGraphic } from './Effects/Spinner' import { autoDetectRenderer, Container, EventBoundary, FederatedEvent, FederatedPointerEvent, Graphics, ICanvas, IRenderer } from 'pixi.js' +import { KeyboardControls } from './KeyboardControls' const { elementToPositionAbsolute } = Utils @@ -22,6 +23,13 @@ enum ContainerName { Map = 'map' } +export const EVENTS_MAP = { + MouseEvent: ['click', 'dblclick', 'mousedown', 'mouseup', 'mousemove', 'mouseenter', 'mouseleave', 'mouseover', 'mouseout', 'contextmenu', 'wheel'], + KeyboardEvent: ['keydown', 'keyup', 'keypress', 'keydownoutside', 'keyupoutside', 'keypressoutside'], + PointerEvent: ['pointerdown', 'pointerup', 'pointermove', 'pointerover', 'pointerout', 'pointerenter', 'pointerleave', 'pointercancel'], + TouchEvent: ['touchstart', 'touchend', 'touchmove', 'touchcancel'] +}; + export class RpgRenderer { private gameEngine: GameEngineClient = this.context.inject(GameEngineClient) private clientEngine: RpgClientEngine = this.context.inject(RpgClientEngine) @@ -83,8 +91,8 @@ export class RpgRenderer { this.spinner.y = h * 0.5 } - get canvas(): ICanvas { - return this.renderer.view + get canvas(): HTMLCanvasElement { + return this.renderer.view as HTMLCanvasElement } get height(): number { @@ -140,6 +148,21 @@ export class RpgRenderer { await RpgGui._initialize(this.context, this.guiEl) this.resize() + this.bindMouseControls() + + } + + private bindMouseControls() { + const controlInstance = this.context.inject(KeyboardControls) + const controls = controlInstance.getControls() + for (let key in controls) { + const { actionName } = controls[key] + if (EVENTS_MAP.MouseEvent.includes(key)) { + this.canvas.addEventListener(key, (e) => { + controlInstance.applyControl(actionName) + }) + } + } } /** @internal */ @@ -275,8 +298,7 @@ export class RpgRenderer { * @returns {void} */ propagateEvent(ev: MouseEvent) { - const canvas = this.renderer.view as HTMLCanvasElement; - const rect = canvas.getBoundingClientRect(); + const rect = this.canvas.getBoundingClientRect(); const canvasX = rect.left + window.scrollX; const canvasY = rect.top + window.scrollY; const realX = ev.clientX - canvasX; @@ -286,7 +308,8 @@ export class RpgRenderer { event.global.set(realX, realY); event.type = ev.type; const hitTestTarget = boundary.hitTest(realX, realY); - hitTestTarget?.dispatchEvent(event); + hitTestTarget?.dispatchEvent(event) + this.canvas.dispatchEvent(new MouseEvent(ev.type, ev)) } /*** @@ -299,14 +322,7 @@ export class RpgRenderer { * @returns {void} */ addPropagateEventsFrom(el: HTMLElement) { - const eventMap = { - MouseEvent: ['click', 'mousedown', 'mouseup', 'mousemove', 'mouseenter', 'mouseleave', 'mouseover', 'mouseout', 'contextmenu', 'wheel'], - KeyboardEvent: ['keydown', 'keyup', 'keypress', 'keydownoutside', 'keyupoutside', 'keypressoutside'], - PointerEvent: ['pointerdown', 'pointerup', 'pointermove', 'pointerover', 'pointerout', 'pointerenter', 'pointerleave', 'pointercancel'], - TouchEvent: ['touchstart', 'touchend', 'touchmove', 'touchcancel'] - }; - - for (let [_Constructor, events] of Object.entries(eventMap)) { + for (let [_Constructor, events] of Object.entries(EVENTS_MAP)) { for (let type of events) { el.addEventListener(type, (e) => { const _class = window[_Constructor] ?? MouseEvent diff --git a/packages/client/src/RpgClientEngine.ts b/packages/client/src/RpgClientEngine.ts index 7b01e44b..e0685771 100644 --- a/packages/client/src/RpgClientEngine.ts +++ b/packages/client/src/RpgClientEngine.ts @@ -178,7 +178,7 @@ export class RpgClientEngine { this.addSound(sound, id) }) - // obsolete + // deprecated if (typeof __RPGJS_PRODUCTION__ != 'undefined' && __RPGJS_PRODUCTION__) { if ('serviceWorker' in navigator) { window.addEventListener('load', () => { diff --git a/packages/plugins/mobile-gui/src/controls/main.vue b/packages/plugins/mobile-gui/src/controls/main.vue index cee014be..e72d1d37 100644 --- a/packages/plugins/mobile-gui/src/controls/main.vue +++ b/packages/plugins/mobile-gui/src/controls/main.vue @@ -1,5 +1,5 @@