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
+
+
+ Test
+
+
+```
+
+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 @@
-
+
@@ -92,9 +92,6 @@ export default {
},
action() {
this.rpgEngine.controls.applyControl(Control.Action)
- },
- propagate(evt) {
- this.rpgEngine.renderer.propagateEvent(evt)
}
}
}
diff --git a/packages/sample2/main/gui/test.tsx b/packages/sample2/main/gui/test.tsx
index 32d09ce0..4366c80b 100644
--- a/packages/sample2/main/gui/test.tsx
+++ b/packages/sample2/main/gui/test.tsx
@@ -1,15 +1,10 @@
import { room } from '@rpgjs/client'
-import { RpgReactContext, useObjects, useCurrentPlayer } from '@rpgjs/client/react'
-import { useContext, useEffect } from 'react'
+import { RpgReactContext, useEventPropagator } from '@rpgjs/client/react'
+import { useContext } from 'react'
export default function Test({ gold }) {
const { rpgCurrentPlayer } = useContext(RpgReactContext)
+ const propagate = useEventPropagator();
- useEffect(() => {
- rpgCurrentPlayer.subscribe(({ object }) => {
- console.log('frontend', object.items);
- })
- }, [])
-
- return <>test>
+ return
test
}
\ No newline at end of file
diff --git a/packages/sample2/main/player.ts b/packages/sample2/main/player.ts
index 4a5dd12b..3b5d3524 100644
--- a/packages/sample2/main/player.ts
+++ b/packages/sample2/main/player.ts
@@ -8,8 +8,6 @@ const player: RpgPlayerHooks = {
player.name = 'YourName'
player.setComponentsTop(Components.text('{position.x},{position.y}'))
},
- onAuth: () => {},
-
onInput(player: RpgPlayer, { input }) {
const map = player.getCurrentMap()
if (input == 'action') {
diff --git a/packages/sample2/rpg.toml b/packages/sample2/rpg.toml
index 8a341b39..f91ca9ee 100644
--- a/packages/sample2/rpg.toml
+++ b/packages/sample2/rpg.toml
@@ -4,7 +4,7 @@ modules = [
'@rpgjs/default-gui',
'@rpgjs/plugin-emotion-bubbles',
'@rpgjs/gamepad',
- '@rpgjs/mobile-gui'
+ # '@rpgjs/mobile-gui'
# '@rpgjs/chat',
# '@rpgjs/title-screen'
]