diff --git a/bin/config.ts b/bin/config.ts index 3334bb33..ef0e9237 100644 --- a/bin/config.ts +++ b/bin/config.ts @@ -78,6 +78,12 @@ export const packages = (type: "build" | "dev") => { buildScript, dependencies: createDependencies(packagesPath, ['common']), }, + + { + name: "vue", + buildScript, + dependencies: createDependencies(packagesPath, ['client']), + }, // Packages depending on client/server { diff --git a/docs/gui/vue-integration.md b/docs/gui/vue-integration.md new file mode 100644 index 00000000..2e6d91af --- /dev/null +++ b/docs/gui/vue-integration.md @@ -0,0 +1,839 @@ +# Vue.js 3 Integration with RPGJS GUI System + +## Overview + +The `@rpgjs/vue` package enables seamless integration of Vue.js 3 components as user interfaces in RPGJS games. This system provides a unified API for managing both CanvasEngine components (.ce files) and Vue.js components through the same `RpgGui` service. + +## Features + +- **Unified API**: Use the same methods (`display()`, `hide()`, `get()`, `exists()`) for both Vue.js and CanvasEngine components +- **Automatic Synchronization**: Vue components are automatically synchronized with the game state +- **Dependency Management**: Support for Signal-based dependencies, just like CanvasEngine components +- **Event Propagation**: Mouse and keyboard events are properly forwarded between Vue components and the game canvas +- **Auto Display**: Components can be configured to display automatically when dependencies are resolved +- **Memory Management**: Automatic cleanup of subscriptions to prevent memory leaks +- **Vue 3 Composition API**: Full support for Vue 3's Composition API and modern features + +## Installation and Setup + +### 1. Install the Vue Package + +```bash +npm install @rpgjs/vue vue@^3.0.0 +``` + +### 2. Configure the Client + +```typescript +// config/config.client.ts +import { provideVueGui } from '@rpgjs/vue'; +import { provideClientModules } from '@rpgjs/client'; +import InventoryComponent from '../components/InventoryComponent.vue'; +import ShopComponent from '../components/ShopComponent.vue'; + +export default { + providers: [ + // Add Vue GUI provider + provideVueGui({ + selector: '#vue-gui-overlay', // Optional: custom mount element + createIfNotFound: true // Optional: create element if not found + }), + provideClientModules([ + { + id: 'dialog', + component: DialogCanvasComponent + }, + // Vue.js components + { + id: 'inventory', + component: InventoryComponent, + autoDisplay: true, + dependencies: () => [playerSignal] + }, + { + id: 'shop', + component: ShopComponent + } + ]) + ], +}; +``` + +### 3. Provider Options + +```typescript +interface VueGuiProviderOptions { + /** The HTML element where Vue components will be mounted */ + mountElement?: HTMLElement | string; + /** Custom CSS selector for the mount element */ + selector?: string; + /** Whether to create a new div element if none is found */ + createIfNotFound?: boolean; +} +``` + +## Creating Vue 3 Components + +### Basic Vue 3 Component (Options API) + +```vue + + + + + +``` + +### Advanced Component with Composition API + +```vue + + + + + +``` + +### Using Composables (Vue 3 Best Practice) + +```vue + +``` + +### Custom Composables + +```typescript +// composables/useRpgPlayer.ts +import { ref, computed, onMounted, onUnmounted, inject } from 'vue'; + +export function useRpgPlayer() { + const rpgCurrentPlayer = inject('rpgCurrentPlayer'); + const currentPlayer = ref(null); + let subscription = null; + + const playerStats = computed(() => { + if (!currentPlayer.value) return null; + + return { + name: currentPlayer.value.object.name, + level: currentPlayer.value.object.level, + hp: currentPlayer.value.object.hp, + maxHp: currentPlayer.value.object.maxHp, + mp: currentPlayer.value.object.mp, + maxMp: currentPlayer.value.object.maxMp, + gold: currentPlayer.value.object.gold + }; + }); + + onMounted(() => { + if (rpgCurrentPlayer) { + subscription = rpgCurrentPlayer.subscribe((player) => { + currentPlayer.value = player; + }); + } + }); + + onUnmounted(() => { + if (subscription) { + subscription.unsubscribe(); + } + }); + + return { + currentPlayer: readonly(currentPlayer), + playerStats: readonly(playerStats) + }; +} +``` + +```typescript +// composables/useRpgGui.ts +import { inject } from 'vue'; + +export function useRpgGui() { + const rpgGui = inject('rpgGui'); + const rpgGuiClose = inject('rpgGuiClose'); + const rpgGuiInteraction = inject('rpgGuiInteraction'); + + const closeGui = (guiId: string, data?: any) => { + rpgGuiClose(guiId, data); + }; + + const sendInteraction = (guiId: string, action: string, data?: any) => { + rpgGuiInteraction(guiId, action, data); + }; + + const displayGui = (guiId: string, props?: any) => { + rpgGui.display(guiId, props); + }; + + const hideGui = (guiId: string) => { + rpgGui.hide(guiId); + }; + + return { + closeGui, + sendInteraction, + displayGui, + hideGui + }; +} +``` + +## Available Injections + +Vue 3 components have access to these RPGJS injections: + +| Injection | Type | Description | +|-----------|------|-------------| +| `rpgEngine` | `RpgClientEngine` | Main game engine instance | +| `rpgSocket` | `Function` | Returns WebSocket connection | +| `rpgGui` | `RpgGui` | GUI management service | +| `rpgScene` | `Function` | Returns current scene | +| `rpgResource` | `Object` | Access to spritesheets and sounds | +| `rpgObjects` | `Observable` | Stream of all game objects | +| `rpgCurrentPlayer` | `Observable` | Stream of current player | +| `rpgGuiClose` | `Function` | Close GUI component | +| `rpgGuiInteraction` | `Function` | Send interaction to server | +| `rpgKeypress` | `Observable` | Stream of keypress events | +| `rpgSound` | `Object` | Sound management service | + +## Usage Examples + +### Displaying Components + +```typescript +// From client-side code using Composition API +import { inject } from 'vue'; + +export default { + setup() { + const gui = inject('rpgGui'); + + const showInventory = () => { + // Display immediately + gui.display('inventory', { + items: playerItems.value, + gold: playerGold.value + }); + }; + + const showShop = () => { + // Display with dependencies + gui.display('shop', { + shopId: 'weapon-shop', + items: shopItems.value + }, [playerSignal, shopSignal]); + }; + + const hideInventory = () => { + gui.hide('inventory'); + }; + + return { + showInventory, + showShop, + hideInventory + }; + } +}; +``` + +### From Server-Side + +```typescript +// In server events +export default { + player: { + onInput(player: RpgPlayer, input: any) { + if (input.action) { + // Open inventory + player.gui('inventory').open({ + items: player.inventory.items, + gold: player.gold + }); + } + } + } +} +``` + +### With TypeScript Support + +```vue + +``` + +## Event Propagation + +Use the `v-propagate` directive to ensure mouse events are properly forwarded to the game canvas: + +```vue + +``` + +## Component Lifecycle + +### Auto Display + +Components can be configured to display automatically: + +```typescript +{ + id: 'hud', + component: HUDComponent, + autoDisplay: true, + dependencies: () => [playerSignal] +} +``` + +### Manual Control + +```typescript +// Check if component exists +if (gui.exists('inventory')) { + // Display with data + gui.display('inventory', { items: [] }); + + // Hide when done + gui.hide('inventory'); +} +``` + +## Server Integration + +### Opening GUIs from Server + +```typescript +// In server player events +onInput(player: RpgPlayer, input: any) { + if (input.action) { + player.gui('shop').open({ + shopId: 'general-store', + items: getShopItems(), + playerGold: player.gold + }); + } +} +``` + +### Handling GUI Interactions + +```typescript +// In server player events +onGuiInteraction(player: RpgPlayer, guiId: string, name: string, data: any) { + if (guiId === 'inventory' && name === 'use-item') { + const item = player.inventory.getItem(data.itemId); + if (item) { + player.useItem(item); + } + } +} +``` + +### Closing GUIs + +```typescript +onGuiExit(player: RpgPlayer, guiId: string, data: any) { + console.log(`Player closed ${guiId}`, data); + // Handle cleanup if needed +} +``` + +## Best Practices + +### 1. Use Composition API for Complex Components + +```vue + +``` + +### 2. Component Structure with Vue 3 + +```vue + +``` + +### 3. Memory Management with Vue 3 + +```javascript +// Automatic cleanup with onUnmounted +import { onUnmounted } from 'vue'; + +onUnmounted(() => { + if (subscription.value) { + subscription.value.unsubscribe(); + } +}); +``` + +### 4. Modern Styling with CSS Variables + +```css +.game-component { + --primary-color: #3498db; + --secondary-color: #2c3e50; + --danger-color: #e74c3c; + --success-color: #27ae60; + + position: fixed; + z-index: 1000; + pointer-events: auto; + background: var(--secondary-color); + color: white; + border-radius: 8px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5); + + /* Modern CSS features */ + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.1); +} + +@media (max-width: 768px) { + .game-component { + width: 90vw; + max-width: none; + } +} +``` + +### 5. Error Handling with Vue 3 + +```vue + + + +``` + +### 6. Performance Optimization + +```vue + +``` + +## Migration from Vue 2 + +### Key Changes + +1. **Composition API**: Use `setup()` or ` + + + +``` diff --git a/packages/client/src/Gui/Gui.ts b/packages/client/src/Gui/Gui.ts index 4e929167..0e337f22 100644 --- a/packages/client/src/Gui/Gui.ts +++ b/packages/client/src/Gui/Gui.ts @@ -40,6 +40,8 @@ const throwError = (id: string) => { export class RpgGui { private webSocket: AbstractWebsocket; gui = signal>({}); + extraGuis: GuiInstance[] = []; + private vueGuiInstance: any = null; // Reference to VueGui instance constructor(private context: Context) { this.webSocket = inject(context, WebSocketToken); @@ -59,6 +61,63 @@ export class RpgGui { }); } + /** + * Set the VueGui instance reference for Vue component management + * This is called by VueGui when it's initialized + * + * @param vueGuiInstance - The VueGui instance + */ + _setVueGuiInstance(vueGuiInstance: any) { + this.vueGuiInstance = vueGuiInstance; + } + + /** + * Notify VueGui about GUI state changes + * This synchronizes the Vue component display state + * + * @param guiId - The GUI component ID + * @param display - Display state + * @param data - Component data + */ + private _notifyVueGui(guiId: string, display: boolean, data: any = {}) { + if (this.vueGuiInstance && this.vueGuiInstance.vm) { + // Find the GUI in extraGuis + const extraGui = this.extraGuis.find(gui => gui.name === guiId); + if (extraGui) { + // Update the Vue component's display state and data + this.vueGuiInstance.vm.gui[guiId] = { + name: guiId, + display, + data, + attachToSprite: false // Default value, could be configurable + }; + // Trigger Vue reactivity + this.vueGuiInstance.vm.gui = Object.assign({}, this.vueGuiInstance.vm.gui); + } + } + } + + /** + * Initialize Vue components in the VueGui instance + * This should be called after VueGui is mounted + */ + _initializeVueComponents() { + if (this.vueGuiInstance && this.vueGuiInstance.vm) { + // Initialize all extraGuis in the Vue instance + this.extraGuis.forEach(gui => { + this.vueGuiInstance.vm.gui[gui.name] = { + name: gui.name, + display: gui.display(), + data: gui.data(), + attachToSprite: false + }; + }); + + // Trigger Vue reactivity + this.vueGuiInstance.vm.gui = Object.assign({}, this.vueGuiInstance.vm.gui); + } + } + guiInteraction(guiId: string, name: string, data: any) { this.webSocket.emit("gui.interaction", { guiId, @@ -77,10 +136,13 @@ export class RpgGui { /** * Add a GUI component to the system * + * By default, only CanvasEngine components (.ce files) are accepted. + * Vue components should be handled by the @rpgjs/vue package. + * * @param gui - GUI configuration options * @param gui.name - Name or ID of the GUI component * @param gui.id - Alternative ID if name is not provided - * @param gui.component - The component to render + * @param gui.component - The component to render (must be a CanvasEngine component) * @param gui.display - Initial display state (default: false) * @param gui.data - Initial data for the component * @param gui.autoDisplay - Auto display when added (default: false) @@ -90,7 +152,7 @@ export class RpgGui { * ```ts * gui.add({ * name: 'inventory', - * component: InventoryComponent, + * component: InventoryComponent, // Must be a .ce component * autoDisplay: true, * dependencies: () => [playerSignal, inventorySignal] * }); @@ -111,16 +173,36 @@ export class RpgGui { dependencies: gui.dependencies, }; + // Accept both CanvasEngine components (.ce) and Vue components + // Vue components will be handled by VueGui if available + if (typeof gui.component !== 'function') { + guiInstance.component = gui; + this.extraGuis.push(guiInstance); + + // Auto display Vue components if enabled + if (guiInstance.autoDisplay) { + this._notifyVueGui(guiId, true, gui.data || {}); + } + return; + } + this.gui()[guiId] = guiInstance; - // Auto display if enabled - if (guiInstance.autoDisplay) { + // Auto display if enabled and it's a CanvasEngine component + if (guiInstance.autoDisplay && typeof gui.component === 'function') { this.display(guiId); } } get(id: string): GuiInstance | undefined { - return this.gui()[id]; + // Check CanvasEngine GUIs first + const canvasGui = this.gui()[id]; + if (canvasGui) { + return canvasGui; + } + + // Check Vue GUIs in extraGuis + return this.extraGuis.find(gui => gui.name === id); } exists(id: string): boolean { @@ -128,7 +210,14 @@ export class RpgGui { } getAll(): Record { - return this.gui(); + const allGuis = { ...this.gui() }; + + // Add extraGuis to the result + this.extraGuis.forEach(gui => { + allGuis[gui.name] = gui; + }); + + return allGuis; } /** @@ -137,6 +226,7 @@ export class RpgGui { * Displays the GUI immediately if no dependencies are configured, * or waits for all dependencies to be resolved if dependencies are present. * Automatically manages subscriptions to prevent memory leaks. + * Works with both CanvasEngine components and Vue components. * * @param id - The GUI component ID * @param data - Data to pass to the component @@ -158,6 +248,67 @@ export class RpgGui { const guiInstance = this.get(id)!; + // Check if it's a Vue component (in extraGuis) + const isVueComponent = this.extraGuis.some(gui => gui.name === id); + + if (isVueComponent) { + // Handle Vue component display + this._handleVueComponentDisplay(id, data, dependencies, guiInstance); + } else { + // Handle CanvasEngine component display + this._handleCanvasComponentDisplay(id, data, dependencies, guiInstance); + } + } + + /** + * Handle Vue component display logic + * + * @param id - GUI component ID + * @param data - Component data + * @param dependencies - Runtime dependencies + * @param guiInstance - GUI instance + */ + private _handleVueComponentDisplay(id: string, data: any, dependencies: Signal[], guiInstance: GuiInstance) { + // Unsubscribe from previous subscription if exists + if (guiInstance.subscription) { + guiInstance.subscription.unsubscribe(); + guiInstance.subscription = undefined; + } + + // Use runtime dependencies or config dependencies + const deps = dependencies.length > 0 + ? dependencies + : (guiInstance.dependencies ? guiInstance.dependencies() : []); + + if (deps.length > 0) { + // Subscribe to dependencies + guiInstance.subscription = combineLatest( + deps.map(dependency => dependency.observable) + ).subscribe((values) => { + if (values.every(value => value !== undefined)) { + guiInstance.data.set(data); + guiInstance.display.set(true); + this._notifyVueGui(id, true, data); + } + }); + return; + } + + // No dependencies, display immediately + guiInstance.data.set(data); + guiInstance.display.set(true); + this._notifyVueGui(id, true, data); + } + + /** + * Handle CanvasEngine component display logic + * + * @param id - GUI component ID + * @param data - Component data + * @param dependencies - Runtime dependencies + * @param guiInstance - GUI instance + */ + private _handleCanvasComponentDisplay(id: string, data: any, dependencies: Signal[], guiInstance: GuiInstance) { // Unsubscribe from previous subscription if exists if (guiInstance.subscription) { guiInstance.subscription.unsubscribe(); @@ -191,6 +342,7 @@ export class RpgGui { * Hide a GUI component * * Hides the GUI and cleans up any active subscriptions. + * Works with both CanvasEngine components and Vue components. * * @param id - The GUI component ID * @@ -213,5 +365,11 @@ export class RpgGui { } guiInstance.display.set(false); + + // Check if it's a Vue component and notify VueGui + const isVueComponent = this.extraGuis.some(gui => gui.name === id); + if (isVueComponent) { + this._notifyVueGui(id, false); + } } } diff --git a/packages/client/src/RpgClient.ts b/packages/client/src/RpgClient.ts index 4bdcaeff..ad9fb027 100644 --- a/packages/client/src/RpgClient.ts +++ b/packages/client/src/RpgClient.ts @@ -329,7 +329,7 @@ export interface RpgClient { * @prop {Array} [gui] * @memberof RpgClient * */ - gui?: { + gui?: ({ id: string, component: ComponentFunction, /** @@ -342,7 +342,7 @@ export interface RpgClient { * The GUI will only display when all dependencies are resolved (!= undefined) */ dependencies?: () => Signal[] - }[], + } | any)[], /** * Array containing the list of sounds diff --git a/packages/client/src/RpgClientEngine.ts b/packages/client/src/RpgClientEngine.ts index 879ed2df..d41e962a 100644 --- a/packages/client/src/RpgClientEngine.ts +++ b/packages/client/src/RpgClientEngine.ts @@ -60,6 +60,14 @@ export class RpgClientEngine { this.renderer = app.renderer as PIXI.Renderer; this.tick = canvasElement?.propObservables?.context['tick'].observable + + this.hooks.callHooks("client-spritesheets-load", this).subscribe(); + this.hooks.callHooks("client-sounds-load", this).subscribe(); + this.hooks.callHooks("client-gui-load", this).subscribe(); + this.hooks.callHooks("client-particles-load", this).subscribe(); + this.hooks.callHooks("client-componentAnimations-load", this).subscribe(); + this.hooks.callHooks("client-sprite-load", this).subscribe(); + await lastValueFrom(this.hooks.callHooks("client-engine-onStart", this)); // wondow is resize @@ -71,14 +79,6 @@ export class RpgClientEngine { this.hooks.callHooks("client-engine-onStep", this, tick).subscribe(); }) - this.hooks.callHooks("client-spritesheets-load", this).subscribe(); - this.hooks.callHooks("client-sounds-load", this).subscribe(); - this.hooks.callHooks("client-gui-load", this).subscribe(); - this.hooks.callHooks("client-particles-load", this).subscribe(); - this.hooks.callHooks("client-componentAnimations-load", this).subscribe(); - this.hooks.callHooks("client-sprite-load", this).subscribe(); - - await this.webSocket.connection(() => { this.initListeners() this.guiService._initialize() diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index 5a106ed5..f9074626 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -11,4 +11,5 @@ export * from "./components/gui"; export * from "./components/animations"; export * from "./presets"; export * from "./components"; -export * from "./components/gui"; \ No newline at end of file +export * from "./components/gui"; +export { Context } from "@signe/di"; \ No newline at end of file diff --git a/packages/vue/README.md b/packages/vue/README.md new file mode 100644 index 00000000..743607e9 --- /dev/null +++ b/packages/vue/README.md @@ -0,0 +1,294 @@ +# @rpgjs/vue + +Vue.js integration for RPGJS - Allows rendering Vue components over the game canvas. + +## Description + +This package enables you to use Vue.js components as overlays on top of the RPGJS game canvas. It provides a seamless integration between Vue.js reactive components and the game engine, allowing for rich user interfaces while maintaining game performance. + +## Key Features + +- **Vue Component Overlay**: Render Vue.js components on top of the game canvas +- **Event Propagation**: Mouse and keyboard events are properly propagated between Vue components and the game +- **Reactive Integration**: Full Vue.js reactivity system support +- **Dependency Injection**: Access to game engine, socket, and GUI services +- **Tooltip System**: Support for sprite-attached tooltips and overlays +- **Component Filtering**: Automatically filters and handles only Vue components, leaving CanvasEngine (.ce) components to the main engine + +## Installation + +```bash +npm install @rpgjs/vue vue +``` + +## Usage + +### Basic Setup with Dependency Injection (Recommended) + +```typescript +import { provideVueGui } from '@rpgjs/vue' +import { RpgClient } from '@rpgjs/client' + +@RpgClient({ + providers: [ + // Provide Vue GUI service with dependency injection + provideVueGui({ + selector: '#vue-gui-overlay', + createIfNotFound: true + }) + ], + gui: [ + // Vue components will be automatically handled + InventoryVueComponent, + // Canvas Engine components continue to work + DialogCanvasComponent + ] +}) +export class MyRpgClient {} +``` + +### Manual Setup (Advanced) + +```typescript +import { VueGui, VueGuiToken } from '@rpgjs/vue' +import { inject } from '@signe/di' + +// Manual initialization (if needed) +const vueGui = inject(context, VueGuiToken) +``` + +### Provider Options + +The `provideVueGui()` function accepts the following options: + +```typescript +interface VueGuiProviderOptions { + /** The HTML element where Vue components will be mounted */ + mountElement?: HTMLElement | string + /** Custom CSS selector for the mount element */ + selector?: string + /** Whether to create a new div element if none is found */ + createIfNotFound?: boolean +} +``` + +**Examples:** + +```typescript +// Basic usage with CSS selector +provideVueGui({ + selector: '#vue-gui-overlay', + createIfNotFound: true +}) + +// Custom mount element +provideVueGui({ + mountElement: document.getElementById('my-ui-container') +}) + +// Automatic element creation +provideVueGui({ + selector: '.game-ui-overlay', + createIfNotFound: true +}) +``` + +### Component Separation + +The system automatically separates Vue and CanvasEngine components: + +```typescript +gui: [ + // Vue component - automatically handled by VueGui service + { + name: 'inventory', + component: VueInventoryComponent, + display: false + }, + + // Canvas Engine component - handled by main RpgGui + { + name: 'dialog', + component: DialogCanvasComponent, + display: false + } +] +``` + +### Vue Component Example + +```vue + + + + + +``` + +### Available Injections + +Vue components have access to all these injected services: + +#### Legacy Injections (for backward compatibility) +- `engine`: RpgClientEngine instance +- `socket`: WebSocket connection to the server +- `gui`: RpgGui instance for GUI management + +#### Standard RPGJS Vue Injections + +| Injection | Type | Description | +|-----------|------|-------------| +| `rpgEngine` | `RpgClientEngine` | Main game engine instance | +| `rpgSocket` | `Function` | Returns the WebSocket connection | +| `rpgGui` | `RpgGui` | GUI management service | +| `rpgScene` | `Function` | Returns the current game scene | +| `rpgStage` | `PIXI.Container` | Main PIXI display container | +| `rpgResource` | `Object` | Game resources `{ spritesheets: Map, sounds: Map }` | +| `rpgObjects` | `Observable` | Stream of all scene objects (players + events) | +| `rpgCurrentPlayer` | `Observable` | Stream of current player data | +| `rpgGuiClose` | `Function` | Close GUI with data `(name, data?)` | +| `rpgGuiInteraction` | `Function` | GUI interaction `(guiId, name, data)` | +| `rpgKeypress` | `Observable` | Stream of keyboard events | +| `rpgSound` | `Object` | Sound service with `get(id)`, `play(id)` methods | + +#### Usage Examples + +```vue + +``` + +### Event Propagation + +Use the `v-propagate` directive to ensure mouse events are properly forwarded to the game canvas: + +```vue + +``` + +## Component Types + +- **Fixed GUI**: Components that are positioned statically on screen +- **Attached GUI**: Components that follow sprites and game objects (tooltips, health bars, etc.) + +The system automatically handles both types based on the `attachToSprite` property in the component configuration. + +## Architecture + +This package modifies the default behavior of the RPGJS GUI system: + +1. **Main RpgGui**: Now only accepts CanvasEngine components (.ce files) +2. **VueGui**: Handles all Vue.js components separately +3. **Event Bridge**: Ensures proper event propagation between Vue and the game canvas +4. **Component Filter**: Automatically separates Vue components from CanvasEngine components + +## License + +MIT \ No newline at end of file diff --git a/packages/vue/package.json b/packages/vue/package.json new file mode 100644 index 00000000..503ebe35 --- /dev/null +++ b/packages/vue/package.json @@ -0,0 +1,41 @@ +{ + "name": "@rpgjs/vue", + "version": "5.0.0-alpha.10", + "description": "Vue.js integration for RPGJS - Allows rendering Vue components over the game canvas", + "main": "dist/index.js", + "types": "./dist/index.d.ts", + "publishConfig": { + "access": "public" + }, + "scripts": { + "dev": "vite build --watch", + "build": "vite build" + }, + "keywords": [ + "rpg", + "game", + "engine", + "javascript", + "typescript", + "vue", + "vue3" + ], + "author": "Samuel Ronce", + "license": "MIT", + "peerDependencies": { + "vue": "^3.2.25" + }, + "dependencies": { + "@rpgjs/client": "workspace:*", + "@rpgjs/common": "workspace:*", + "rxjs": "^7.8.2" + }, + "devDependencies": { + "@canvasengine/compiler": "2.0.0-beta.22", + "vite": "^6.2.5", + "vite-plugin-dts": "^4.5.3", + "vitest": "^3.1.1", + "vue": "^3.5.13" + }, + "type": "module" +} \ No newline at end of file diff --git a/packages/vue/src/VueGui.ts b/packages/vue/src/VueGui.ts new file mode 100644 index 00000000..ed7f60e2 --- /dev/null +++ b/packages/vue/src/VueGui.ts @@ -0,0 +1,420 @@ +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 { isFunction, RpgCommonPlayer } from '@rpgjs/common' +import { RpgClientEngine, RpgGui, inject, Context } from '@rpgjs/client' +import { Observable } from 'rxjs' + +export const VueGuiToken = "VueGuiToken" + +interface VueInstance extends ComponentPublicInstance { + gui: GuiList, + tooltips: RpgCommonPlayer[] +} + +interface GuiOptions { + data: any, + attachToSprite: boolean + display: boolean, + name: string +} + +interface GuiList { + [guiName: string]: GuiOptions +} + +interface VueGuiOptions { + /** The HTML element where Vue components will be mounted */ + mountElement?: HTMLElement | string + /** Custom CSS selector for the mount element */ + selector?: string + /** Whether to create a new div element if none is found */ + createIfNotFound?: boolean +} + +const _hoisted_1 = { + id: "tooltips", + style: { "position": "absolute", "top": "0", "left": "0" } +} + +function render(_ctx, _cache) { + return (_openBlock(), _createElementBlock("div", {}, [ + (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.fixedGui, (ui: any) => { + return (_openBlock(), _createElementBlock(_Fragment, null, [ + (ui.display) + ? (_openBlock(), _createBlock(_resolveDynamicComponent(ui.name), _normalizeProps(_mergeProps({ key: 0, style: { pointerEvents: 'auto' } }, ui.data)), null, 16 /* FULL_PROPS */)) + : _createCommentVNode("v-if", true) + ], 64 /* STABLE_FRAGMENT */)) + }), 256 /* UNKEYED_FRAGMENT */)), + _createElementVNode("div", _hoisted_1, [ + (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.attachedGui, (ui: any) => { + return (_openBlock(), _createElementBlock(_Fragment, null, [ + (ui.display) + ? (_openBlock(true), _createElementBlock(_Fragment, { key: 0 }, _renderList(_ctx.tooltipFilter(_ctx.tooltips, ui), (tooltip: any) => { + return (_openBlock(), _createElementBlock("div", { + style: _normalizeStyle(_ctx.tooltipPosition(tooltip.position)) + }, [ + (_openBlock(), _createBlock(_resolveDynamicComponent(ui.name), _mergeProps({ ...ui.data, spriteData: tooltip, style: { pointerEvents: 'auto' } }, { + ref_for: true, + ref: ui.name + }), null, 16 /* FULL_PROPS */)) + ], 4 /* STYLE */)) + }), 256 /* UNKEYED_FRAGMENT */)) + : _createCommentVNode("v-if", true) + ], 64 /* STABLE_FRAGMENT */)) + }), 256 /* UNKEYED_FRAGMENT */)) + ]) + ], 32 /* HYDRATE_EVENTS */)) +} + +export class VueGui { + private clientEngine: RpgClientEngine + private parentGui: RpgGui + private app: App + private vm: VueInstance + private socket + + constructor(private context: Context, private options: VueGuiOptions = {}) { + + } + + mount() { + this.clientEngine = inject(RpgClientEngine) + this.parentGui = inject(RpgGui) + + // Establish connection with RpgGui for Vue component management + this.parentGui._setVueGuiInstance(this); + + // Get or create mount element + const mountElement = this.getMountElement() + if (!mountElement) { + throw new Error('No mount element found for VueGui. Please provide a valid element or selector.') + } + + // Get all GUI components from the parent GUI service + const guiVue = this.parentGui.extraGuis + + const obj = { + render, + data() { + return { + gui: {}, + tooltips: [] + } + }, + provide: () => { + return this.getInjectObject() + }, + computed: { + fixedGui() { + return Object.values(this.gui).filter((gui: any) => !gui.attachToSprite) + }, + attachedGui() { + return Object.values(this.gui).filter((gui: any) => gui.attachToSprite) + } + }, + methods: { + tooltipPosition: this.tooltipPosition.bind(this), + tooltipFilter: this.tooltipFilter.bind(this) + } + } + + this.app = createApp(obj) + + for (let ui of guiVue) { + this.app.component(ui.name, ui.component) + } + + // Add propagate directive for event handling + this.app.directive('propagate', { + mounted: (el, binding) => { + el.eventListeners = {}; + const mouseEvents = ['click', 'mousedown', 'mouseup', 'mousemove', 'wheel']; + mouseEvents.forEach(eventType => { + const callback = (ev) => { + // Propagate event to the game engine + this.propagateEvent(ev); + }; + el.eventListeners[eventType] = callback; + el.addEventListener(eventType, callback); + }); + }, + unmounted(el, binding) { + const mouseEvents = ['click', 'mousedown', 'mouseup', 'mousemove', 'wheel']; + mouseEvents.forEach(eventType => { + const callback = el.eventListeners[eventType]; + if (callback) { + el.removeEventListener(eventType, callback); + } + }); + } + }) + + this.vm = this.app.mount(mountElement) as VueInstance + + // Initialize Vue components after mounting + this.parentGui._initializeVueComponents(); + } + + private getMountElement(): HTMLElement { + const { mountElement, selector, createIfNotFound = true } = this.options + + // If mountElement is provided directly + if (mountElement) { + if (typeof mountElement === 'string') { + const element = document.querySelector(mountElement) as HTMLElement + if (element) return element + } else { + return mountElement + } + } + + // If selector is provided + if (selector) { + const element = document.querySelector(selector) as HTMLElement + if (element) return element + } + + // Default selector + const defaultElement = document.querySelector('#vue-gui-overlay') as HTMLElement + if (defaultElement) return defaultElement + + // Create element if not found and createIfNotFound is true + if (createIfNotFound) { + const newElement = document.createElement('div') + newElement.id = 'vue-gui-overlay' + newElement.style.position = 'absolute' + newElement.style.top = '0' + newElement.style.left = '0' + newElement.style.width = '100%' + newElement.style.height = '100%' + newElement.style.pointerEvents = 'none' // Allow canvas events to pass through + + // Try to add to game container + const gameContainer = document.querySelector('#rpg') + if (gameContainer) { + gameContainer.appendChild(newElement) + return newElement + } + + // Fallback to body + document.body.appendChild(newElement) + return newElement + } + + throw new Error('Could not find or create mount element for VueGui') + } + + private getInjectObject() { + return { + // Legacy injections (for backward compatibility) + engine: this.clientEngine, + socket: this.clientEngine.socket, + gui: this.parentGui, + + // Standard RPGJS Vue injections + rpgEngine: this.clientEngine, + rpgSocket: () => this.clientEngine.socket, + rpgGui: this.parentGui, + rpgScene: () => this.clientEngine.scene, + rpgResource: { + spritesheets: this.clientEngine.spritesheets, + sounds: this.clientEngine.sounds + }, + rpgObjects: this.createObjectsObservable(), + rpgCurrentPlayer: this.createCurrentPlayerObservable(), + rpgGuiClose: (name: string, data?: any) => { + this.parentGui.guiClose(name, data) + }, + rpgGuiInteraction: (guiId: string, name: string, data: any = {}) => { + this.parentGui.guiInteraction(guiId, name, data) + }, + rpgKeypress: this.createKeypressObservable(), + rpgSound: this.createSoundService() + } + } + + private createObjectsObservable() { + // Combine players and events into a single observable + const scene = this.clientEngine.scene + if (!scene) return null + + // Create an observable that merges players and events + return new Observable((observer) => { + const subscription1 = scene.players.observable.subscribe((players) => { + const objects = {} + for (const [id, player] of Object.entries(players)) { + objects[id] = { + object: player, + paramsChanged: player // For simplicity, could be enhanced to track actual changes + } + } + observer.next(objects) + }) + + const subscription2 = scene.events.observable.subscribe((events) => { + const objects = {} + for (const [id, event] of Object.entries(events)) { + objects[id] = { + object: event, + paramsChanged: event + } + } + observer.next(objects) + }) + + return () => { + subscription1.unsubscribe() + subscription2.unsubscribe() + } + }) + } + + private createCurrentPlayerObservable() { + const scene = this.clientEngine.scene + if (!scene) return null + + return new Observable((observer) => { + const subscription = scene.currentPlayer.observable.subscribe((player) => { + if (player) { + observer.next({ + object: player, + paramsChanged: player + }) + } + }) + + return () => subscription.unsubscribe() + }) + } + + private createKeypressObservable() { + return new Observable((observer) => { + const keyHandler = (event: KeyboardEvent) => { + // Map keyboard events to RPG controls + const keyMap = this.clientEngine.globalConfig?.keyboardControls || { + up: 'up', + down: 'down', + left: 'left', + right: 'right', + action: 'space', + escape: 'escape' + } + + const inputName = event.key.toLowerCase() + let control: { actionName: string; options: any } | null = null + + // Find matching control + for (const [actionName, keyName] of Object.entries(keyMap)) { + if (keyName === inputName || keyName === event.code.toLowerCase()) { + control = { + actionName, + options: {} + } + break + } + } + + if (control) { + observer.next({ + inputName, + control + }) + } + } + + document.addEventListener('keydown', keyHandler) + + return () => { + document.removeEventListener('keydown', keyHandler) + } + }) + } + + private createSoundService() { + return { + get: (id: string) => { + const sound = this.clientEngine.sounds.get(id) + return { + play: () => { + if (sound && sound.play) { + sound.play() + } + }, + stop: () => { + if (sound && sound.stop) { + sound.stop() + } + }, + pause: () => { + if (sound && sound.pause) { + sound.pause() + } + } + } + }, + play: (id: string) => { + const sound = this.clientEngine.sounds.get(id) + if (sound && sound.play) { + sound.play() + } + } + } + } + + private propagateEvent(event: Event) { + // Propagate mouse events to the canvas/engine + // This allows interaction with the game through Vue components + if (this.clientEngine.renderer) { + // Find the actual canvas element in the DOM + const canvas = document.querySelector('#rpg canvas') as HTMLCanvasElement; + if (canvas && canvas.getBoundingClientRect) { + const rect = canvas.getBoundingClientRect(); + const mouseEvent = event as MouseEvent + + // Create a new mouse event with adjusted coordinates + const newEvent = new MouseEvent(event.type, { + bubbles: event.bubbles, + cancelable: event.cancelable, + clientX: mouseEvent.clientX - rect.left, + clientY: mouseEvent.clientY - rect.top, + button: mouseEvent.button, + buttons: mouseEvent.buttons + }); + canvas.dispatchEvent(newEvent); + } + } + } + + private tooltipPosition(position: any) { + return { + left: position.x + 'px', + top: position.y + 'px', + position: 'absolute' + } + } + + private tooltipFilter(tooltips: any[], ui: any) { + // Filter tooltips based on UI configuration + return tooltips.filter(tooltip => { + // Add filtering logic based on your requirements + return true; + }); + } + + _setSceneReady() { + // Handle scene ready state for tooltips and other dynamic content + if (this.clientEngine.scene) { + // Subscribe to object changes for tooltips + this.vm.tooltips = []; + } + } + + set gui(val: any) { + for (let key in val) { + // Ignore function components (they should only be handled by CanvasEngine) + if (isFunction(val[key].component)) continue + this.vm.gui[key] = val[key] + } + this.vm.gui = Object.assign({}, this.vm.gui) + } +} \ No newline at end of file diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts new file mode 100644 index 00000000..4cbf5bb0 --- /dev/null +++ b/packages/vue/src/index.ts @@ -0,0 +1,2 @@ +export * from './VueGui' +export * from './provider' \ No newline at end of file diff --git a/packages/vue/src/provider.ts b/packages/vue/src/provider.ts new file mode 100644 index 00000000..e3cbbb86 --- /dev/null +++ b/packages/vue/src/provider.ts @@ -0,0 +1,131 @@ +import { Context, inject, RpgClient } from "@rpgjs/client" +import { VueGui, VueGuiToken } from "./VueGui" +import { createModule } from "@rpgjs/common" + +interface VueGuiProviderOptions { + /** The HTML element where Vue components will be mounted */ + mountElement?: HTMLElement | string + /** Custom CSS selector for the mount element */ + selector?: string + /** Whether to create a new div element if none is found */ + createIfNotFound?: boolean +} + +/** + * Creates a dependency injection configuration for Vue GUI overlay on the client side. + * + * This function allows you to render Vue.js components as overlays on top of the RPGJS game canvas. + * It provides a seamless integration between Vue.js reactive components and the game engine, + * allowing for rich user interfaces while maintaining game performance. + * + * The function sets up the necessary service providers for Vue GUI rendering, including: + * - VueGuiToken: Provides the VueGui service with your custom mounting configuration + * - Automatic component filtering: Separates Vue components from CanvasEngine components + * - Event propagation: Ensures proper interaction between Vue components and game canvas + * + * **Design Concept:** + * The function follows the provider pattern used throughout RPGJS, creating a modular way to inject + * Vue GUI rendering capabilities into the client engine. It separates the concern of UI framework + * (Vue.js) from game rendering (CanvasEngine), allowing developers to use modern web UI patterns + * while leveraging the engine's performance. + * + * @param {VueGuiProviderOptions} options - Configuration options for Vue GUI mounting + * @returns {Object} Dependency injection provider configuration + * + * @example + * ```typescript + * import { provideVueGui } from '@rpgjs/vue' + * import { createModule } from '@rpgjs/common' + * + * // Basic usage with automatic element creation + * export function provideVueUIModule() { + * return createModule("VueUI", [ + * provideVueGui({ + * selector: '#vue-gui-container', + * createIfNotFound: true + * }) + * ]) + * } + * + * // Advanced usage with custom mount element + * export function provideCustomVueUI() { + * return createModule("CustomVueUI", [ + * provideVueGui({ + * mountElement: document.getElementById('my-ui-overlay'), + * createIfNotFound: false + * }) + * ]) + * } + * + * // Usage with CSS selector + * export function provideModalVueUI() { + * return createModule("ModalVueUI", [ + * provideVueGui({ + * selector: '.modal-overlay', + * createIfNotFound: true + * }) + * ]) + * } + * ``` + * + * **Integration in your client:** + * ```typescript + * import { RpgClient } from '@rpgjs/client' + * import { provideVueGui } from '@rpgjs/vue' + * + * @RpgClient({ + * providers: [ + * provideVueGui({ + * selector: '#vue-gui-overlay' + * }) + * ], + * gui: [ + * // Vue components will be automatically handled by VueGui + * InventoryVueComponent, + * // CanvasEngine components continue to work normally + * DialogCanvasComponent + * ] + * }) + * export class MyRpgClient {} + * ``` + * + * **Available injections in Vue components:** + * - `engine`: RpgClientEngine instance for game interactions + * - `socket`: WebSocket connection for server communication + * - `gui`: RpgGui instance for GUI management + * + * **Event propagation:** + * Use the `v-propagate` directive in Vue components to ensure mouse/keyboard events + * are properly forwarded to the game canvas when needed. + * + * @since 5.0.0 + * @see {@link VueGuiProviderOptions} for configuration options + * @see {@link VueGui} for the main service class + */ + +export function provideVueGui(options: VueGuiProviderOptions = {}) { + return createModule('VueGui',[ + { + client: { + engine: { + onStart() { + const vueGui = inject(VueGuiToken); + vueGui.mount() + } + } + } as RpgClient, + server: null + }, + { + provide: VueGuiToken, + useFactory: (context: Context) => { + // Only create VueGui on client side + if (context['side'] === 'server') { + console.warn('VueGui is only available on client side') + return null + } + return new VueGui(context, options) + }, + } + ]) +} \ No newline at end of file diff --git a/packages/vue/tsconfig.json b/packages/vue/tsconfig.json new file mode 100644 index 00000000..0f4ba7e7 --- /dev/null +++ b/packages/vue/tsconfig.json @@ -0,0 +1,35 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["dom", "esnext"], + "module": "ES2020", + "outDir": "./lib", + "rootDir": "./src", + "strict": true, + "sourceMap": true, + "strictNullChecks": true, + "strictPropertyInitialization": false, + "moduleResolution": "bundler", + "esModuleInterop": true, + "removeComments": false, + "noUnusedParameters": false, + "noUnusedLocals": false, + "noImplicitThis": false, + "noImplicitAny": false, + "noImplicitReturns": false, + "declaration": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "stripInternal": true, + "skipLibCheck": true + }, + "include": [ + "src", + "src/types/**/*" + ], + "typeRoots": [ + "node_modules/@types", + "node_modules/@rpgjs/client", + "node_modules/vue" + ] +} \ No newline at end of file diff --git a/packages/vue/vite.config.ts b/packages/vue/vite.config.ts new file mode 100644 index 00000000..445ab47d --- /dev/null +++ b/packages/vue/vite.config.ts @@ -0,0 +1,32 @@ +import { defineConfig } from 'vite' +import dts from 'vite-plugin-dts' +import path from 'path' +import { fileURLToPath } from 'url' + +const dirname = path.dirname(fileURLToPath(import.meta.url)) + +export default defineConfig({ + plugins: [ + dts({ + include: ['src/**/*.ts'], + outDir: 'dist' + }) + ], + build: { + target: 'esnext', + sourcemap: true, + minify: false, + lib: { + entry: 'src/index.ts', + formats: ['es'], + fileName: 'index' + }, + rollupOptions: { + external: [/@rpgjs/, 'vue', 'rxjs'], + output: { + preserveModules: true, + preserveModulesRoot: 'src' + } + }, + }, +}) \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7a6934a9..2207fbd0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -174,7 +174,7 @@ importers: version: 2.0.0-beta.21(@types/node@20.19.0)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.8.0) '@hono/vite-dev-server': specifier: ^0.19.1 - version: 0.19.1(hono@4.8.1)(miniflare@4.20250617.1)(wrangler@4.20.3(@cloudflare/workers-types@4.20250620.0)) + version: 0.19.1(hono@4.8.1)(miniflare@4.20250617.1)(wrangler@4.20.3) '@rpgjs/server': specifier: workspace:* version: link:../server @@ -210,6 +210,34 @@ importers: specifier: ^8.18.1 version: 8.18.1 + packages/vue: + dependencies: + '@rpgjs/client': + specifier: workspace:* + version: link:../client + '@rpgjs/common': + specifier: workspace:* + version: link:../common + rxjs: + specifier: ^7.8.2 + version: 7.8.2 + devDependencies: + '@canvasengine/compiler': + specifier: 2.0.0-beta.22 + version: 2.0.0-beta.22(@types/node@24.0.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.8.0) + vite: + specifier: ^6.2.5 + version: 6.2.5(@types/node@24.0.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.8.0) + vite-plugin-dts: + specifier: ^4.5.3 + version: 4.5.3(@types/node@24.0.1)(rollup@4.39.0)(typescript@5.8.3)(vite@6.2.5(@types/node@24.0.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.8.0)) + vitest: + specifier: ^3.1.1 + version: 3.1.1(@types/node@24.0.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.8.0) + vue: + specifier: ^3.5.13 + version: 3.5.17(typescript@5.8.3) + sample: dependencies: '@canvasengine/presets': @@ -230,16 +258,25 @@ importers: '@rpgjs/vite': specifier: workspace:* version: link:../packages/vite + '@rpgjs/vue': + specifier: workspace:* + version: link:../packages/vue '@signe/di': specifier: ^2.3.1 version: 2.3.3 canvasengine: specifier: 2.0.0-beta.28 version: 2.0.0-beta.28(@types/react@19.1.3)(pixi.js@8.10.1)(react@19.1.0) + vue: + specifier: ^3.5.13 + version: 3.5.17(typescript@5.8.3) devDependencies: '@canvasengine/compiler': specifier: 2.0.0-beta.28 version: 2.0.0-beta.28(@types/node@24.0.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.8.0) + '@vitejs/plugin-vue': + specifier: ^6.0.0 + version: 6.0.0(vite@6.2.5(@types/node@24.0.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.8.0))(vue@3.5.17(typescript@5.8.3)) path-browserify: specifier: ^1.0.1 version: 1.0.1 @@ -253,15 +290,28 @@ packages: resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.25.9': resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + '@babel/parser@7.27.0': resolution: {integrity: sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==} engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.28.0': + resolution: {integrity: sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/runtime@7.27.6': resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==} engines: {node: '>=6.9.0'} @@ -270,6 +320,10 @@ packages: resolution: {integrity: sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==} engines: {node: '>=6.9.0'} + '@babel/types@7.28.0': + resolution: {integrity: sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==} + engines: {node: '>=6.9.0'} + '@barvynkoa/particle-emitter@0.0.1': resolution: {integrity: sha512-aNNv5rKlIxnPTA8WVkOoB29GBMuYSAUhDkdCAIk6unuLt5gl/CJbXnH+qNC2uVUOwEYlNu2LXnK+oW6lsG0EYQ==} peerDependencies: @@ -405,9 +459,6 @@ packages: cpu: [x64] os: [win32] - '@cloudflare/workers-types@4.20250620.0': - resolution: {integrity: sha512-EVvRB/DJEm6jhdKg+A4Qm4y/ry1cIvylSgSO3/f/Bv161vldDRxaXM2YoQQWFhLOJOw0qtrHsKOD51KYxV1XCw==} - '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -912,6 +963,9 @@ packages: pixi.js: ^8.2.6 react: '>=19.0.0' + '@rolldown/pluginutils@1.0.0-beta.19': + resolution: {integrity: sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA==} + '@rollup/pluginutils@5.1.4': resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==} engines: {node: '>=14.0.0'} @@ -1130,6 +1184,13 @@ packages: '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@vitejs/plugin-vue@6.0.0': + resolution: {integrity: sha512-iAliE72WsdhjzTOp2DtvKThq1VBC4REhwRcaA+zPAAph6I+OQhUXv+Xu2KS7ElxYtb7Zc/3R30Hwv1DxEo7NXQ==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + vue: ^3.2.25 + '@vitest/expect@3.1.1': resolution: {integrity: sha512-q/zjrW9lgynctNbwvFtQkGK9+vvHA5UzVi2V8APrp1C6fG6/MuYYkmlx4FubuqLycCeSdHD5aadWfua/Vr0EUA==} @@ -1171,9 +1232,21 @@ packages: '@vue/compiler-core@3.5.13': resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==} + '@vue/compiler-core@3.5.17': + resolution: {integrity: sha512-Xe+AittLbAyV0pabcN7cP7/BenRBNcteM4aSDCtRvGw0d9OL+HG1u/XHLY/kt1q4fyMeZYXyIYrsHuPSiDPosA==} + '@vue/compiler-dom@3.5.13': resolution: {integrity: sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==} + '@vue/compiler-dom@3.5.17': + resolution: {integrity: sha512-+2UgfLKoaNLhgfhV5Ihnk6wB4ljyW1/7wUIog2puUqajiC29Lp5R/IKDdkebh9jTbTogTbsgB+OY9cEWzG95JQ==} + + '@vue/compiler-sfc@3.5.17': + resolution: {integrity: sha512-rQQxbRJMgTqwRugtjw0cnyQv9cP4/4BxWfTdRBkqsTfLOHWykLzbOc3C4GGzAmdMDxhzU/1Ija5bTjMVrddqww==} + + '@vue/compiler-ssr@3.5.17': + resolution: {integrity: sha512-hkDbA0Q20ZzGgpj5uZjb9rBzQtIHLS78mMilwrlpWk2Ep37DYntUz0PonQ6kr113vfOEdM+zTBuJDaceNIW0tQ==} + '@vue/compiler-vue2@2.7.16': resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==} @@ -1185,9 +1258,26 @@ packages: typescript: optional: true + '@vue/reactivity@3.5.17': + resolution: {integrity: sha512-l/rmw2STIscWi7SNJp708FK4Kofs97zc/5aEPQh4bOsReD/8ICuBcEmS7KGwDj5ODQLYWVN2lNibKJL1z5b+Lw==} + + '@vue/runtime-core@3.5.17': + resolution: {integrity: sha512-QQLXa20dHg1R0ri4bjKeGFKEkJA7MMBxrKo2G+gJikmumRS7PTD4BOU9FKrDQWMKowz7frJJGqBffYMgQYS96Q==} + + '@vue/runtime-dom@3.5.17': + resolution: {integrity: sha512-8El0M60TcwZ1QMz4/os2MdlQECgGoVHPuLnQBU3m9h3gdNRW9xRmI8iLS4t/22OQlOE6aJvNNlBiCzPHur4H9g==} + + '@vue/server-renderer@3.5.17': + resolution: {integrity: sha512-BOHhm8HalujY6lmC3DbqF6uXN/K00uWiEeF22LfEsm9Q93XeJ/plHTepGwf6tqFcF7GA5oGSSAAUock3VvzaCA==} + peerDependencies: + vue: 3.5.17 + '@vue/shared@3.5.13': resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==} + '@vue/shared@3.5.17': + resolution: {integrity: sha512-CabR+UN630VnsJO/jHWYBC1YVXyMq94KKp6iF5MQgZJs5I8cmjw6oVMO1oDbtBkENSHSSn/UadWlW/OAgdmKrg==} + '@webgpu/types@0.1.60': resolution: {integrity: sha512-8B/tdfRFKdrnejqmvq95ogp8tf52oZ51p3f4QD5m5Paey/qlX4Rhhy5Y8tgFMi7Ms70HzcMMw3EQjH/jdhTwlA==} @@ -2017,6 +2107,10 @@ packages: resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} engines: {node: ^10 || ^12 || >=14} + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + prettier@2.8.8: resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} engines: {node: '>=10.13.0'} @@ -2388,6 +2482,14 @@ packages: vscode-uri@3.1.0: resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + vue@3.5.17: + resolution: {integrity: sha512-LbHV3xPN9BeljML+Xctq4lbz2lVHCR6DtbpTf5XIO6gugpXUN49j2QQPcMj086r9+AkJ0FfUT8xjulKKBkkr9g==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + wait-on@8.0.3: resolution: {integrity: sha512-nQFqAFzZDeRxsu7S3C7LbuxslHhk+gnJZHyethuGKAn2IVleIbTB9I3vJSQiSR+DifUqmdzfPMoMPJfLqMF2vw==} engines: {node: '>=12.0.0'} @@ -2466,12 +2568,20 @@ snapshots: '@babel/helper-string-parser@7.25.9': {} + '@babel/helper-string-parser@7.27.1': {} + '@babel/helper-validator-identifier@7.25.9': {} + '@babel/helper-validator-identifier@7.27.1': {} + '@babel/parser@7.27.0': dependencies: '@babel/types': 7.27.0 + '@babel/parser@7.28.0': + dependencies: + '@babel/types': 7.28.0 + '@babel/runtime@7.27.6': {} '@babel/types@7.27.0': @@ -2479,6 +2589,11 @@ snapshots: '@babel/helper-string-parser': 7.25.9 '@babel/helper-validator-identifier': 7.25.9 + '@babel/types@7.28.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@barvynkoa/particle-emitter@0.0.1(pixi.js@8.10.1)': dependencies: pixi.js: 8.10.1 @@ -2762,9 +2877,6 @@ snapshots: '@cloudflare/workerd-windows-64@1.20250617.0': optional: true - '@cloudflare/workers-types@4.20250620.0': - optional: true - '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 @@ -2940,14 +3052,14 @@ snapshots: dependencies: hono: 4.8.1 - '@hono/vite-dev-server@0.19.1(hono@4.8.1)(miniflare@4.20250617.1)(wrangler@4.20.3(@cloudflare/workers-types@4.20250620.0))': + '@hono/vite-dev-server@0.19.1(hono@4.8.1)(miniflare@4.20250617.1)(wrangler@4.20.3)': dependencies: '@hono/node-server': 1.14.4(hono@4.8.1) hono: 4.8.1 minimatch: 9.0.5 optionalDependencies: miniflare: 4.20250617.1 - wrangler: 4.20.3(@cloudflare/workers-types@4.20250620.0) + wrangler: 4.20.3 '@img/sharp-darwin-arm64@0.33.5': optionalDependencies: @@ -3150,6 +3262,8 @@ snapshots: - '@types/react' optional: true + '@rolldown/pluginutils@1.0.0-beta.19': {} + '@rollup/pluginutils@5.1.4(rollup@4.39.0)': dependencies: '@types/estree': 1.0.7 @@ -3379,6 +3493,12 @@ snapshots: dependencies: '@types/node': 24.0.1 + '@vitejs/plugin-vue@6.0.0(vite@6.2.5(@types/node@24.0.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.8.0))(vue@3.5.17(typescript@5.8.3))': + dependencies: + '@rolldown/pluginutils': 1.0.0-beta.19 + vite: 6.2.5(@types/node@24.0.1)(jiti@2.4.2)(tsx@4.19.3)(yaml@2.8.0) + vue: 3.5.17(typescript@5.8.3) + '@vitest/expect@3.1.1': dependencies: '@vitest/spy': 3.1.1 @@ -3447,11 +3567,41 @@ snapshots: estree-walker: 2.0.2 source-map-js: 1.2.1 + '@vue/compiler-core@3.5.17': + dependencies: + '@babel/parser': 7.28.0 + '@vue/shared': 3.5.17 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + '@vue/compiler-dom@3.5.13': dependencies: '@vue/compiler-core': 3.5.13 '@vue/shared': 3.5.13 + '@vue/compiler-dom@3.5.17': + dependencies: + '@vue/compiler-core': 3.5.17 + '@vue/shared': 3.5.17 + + '@vue/compiler-sfc@3.5.17': + dependencies: + '@babel/parser': 7.28.0 + '@vue/compiler-core': 3.5.17 + '@vue/compiler-dom': 3.5.17 + '@vue/compiler-ssr': 3.5.17 + '@vue/shared': 3.5.17 + estree-walker: 2.0.2 + magic-string: 0.30.17 + postcss: 8.5.6 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.17': + dependencies: + '@vue/compiler-dom': 3.5.17 + '@vue/shared': 3.5.17 + '@vue/compiler-vue2@2.7.16': dependencies: de-indent: 1.0.2 @@ -3470,8 +3620,32 @@ snapshots: optionalDependencies: typescript: 5.8.3 + '@vue/reactivity@3.5.17': + dependencies: + '@vue/shared': 3.5.17 + + '@vue/runtime-core@3.5.17': + dependencies: + '@vue/reactivity': 3.5.17 + '@vue/shared': 3.5.17 + + '@vue/runtime-dom@3.5.17': + dependencies: + '@vue/reactivity': 3.5.17 + '@vue/runtime-core': 3.5.17 + '@vue/shared': 3.5.17 + csstype: 3.1.3 + + '@vue/server-renderer@3.5.17(vue@3.5.17(typescript@5.8.3))': + dependencies: + '@vue/compiler-ssr': 3.5.17 + '@vue/shared': 3.5.17 + vue: 3.5.17(typescript@5.8.3) + '@vue/shared@3.5.13': {} + '@vue/shared@3.5.17': {} + '@webgpu/types@0.1.60': {} '@xmldom/xmldom@0.8.10': {} @@ -3709,8 +3883,7 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - csstype@3.1.3: - optional: true + csstype@3.1.3: {} data-uri-to-buffer@2.0.2: optional: true @@ -4343,6 +4516,12 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + prettier@2.8.8: {} pretty-ms@9.2.0: @@ -4800,6 +4979,16 @@ snapshots: vscode-uri@3.1.0: {} + vue@3.5.17(typescript@5.8.3): + dependencies: + '@vue/compiler-dom': 3.5.17 + '@vue/compiler-sfc': 3.5.17 + '@vue/runtime-dom': 3.5.17 + '@vue/server-renderer': 3.5.17(vue@3.5.17(typescript@5.8.3)) + '@vue/shared': 3.5.17 + optionalDependencies: + typescript: 5.8.3 + wait-on@8.0.3: dependencies: axios: 1.8.4 @@ -4828,7 +5017,7 @@ snapshots: '@cloudflare/workerd-windows-64': 1.20250617.0 optional: true - wrangler@4.20.3(@cloudflare/workers-types@4.20250620.0): + wrangler@4.20.3: dependencies: '@cloudflare/kv-asset-handler': 0.4.0 '@cloudflare/unenv-preset': 2.3.3(unenv@2.0.0-rc.17)(workerd@1.20250617.0) @@ -4839,7 +5028,6 @@ snapshots: unenv: 2.0.0-rc.17 workerd: 1.20250617.0 optionalDependencies: - '@cloudflare/workers-types': 4.20250620.0 fsevents: 2.3.3 transitivePeerDependencies: - bufferutil diff --git a/sample/package.json b/sample/package.json index 5d80e385..8d3aa7b8 100644 --- a/sample/package.json +++ b/sample/package.json @@ -12,6 +12,7 @@ "description": "", "devDependencies": { "@canvasengine/compiler": "2.0.0-beta.28", + "@vitejs/plugin-vue": "^6.0.0", "path-browserify": "^1.0.1", "vite": "^6.2.5" }, @@ -23,8 +24,10 @@ "@rpgjs/server": "workspace:*", "@rpgjs/tiledmap": "workspace:*", "@rpgjs/vite": "workspace:*", + "@rpgjs/vue": "workspace:*", "@signe/di": "^2.3.1", - "canvasengine": "2.0.0-beta.28" + "canvasengine": "2.0.0-beta.28", + "vue": "^3.5.13" }, "type": "module" } diff --git a/sample/src/config/config.client.ts b/sample/src/config/config.client.ts index 4902e4ec..73656b02 100644 --- a/sample/src/config/config.client.ts +++ b/sample/src/config/config.client.ts @@ -11,7 +11,9 @@ import Map from "../components/map.ce"; import Shadow from "../components/shadow.ce"; import WoodComponent from "../components/wood.ce"; import WoodUiComponent from "../components/wood-ui.ce"; +import VueComponent from "../vue-component-with-injections.vue"; import { signal, effect } from 'canvasengine' +import { provideVueGui } from "@rpgjs/vue"; export default { providers: [ @@ -23,6 +25,7 @@ export default { height: 1536, } }), + provideVueGui(), provideClientGlobalConfig(), provideClientModules([ { @@ -70,7 +73,8 @@ export default { const engine = inject(RpgClientEngine) return [engine.scene.currentPlayer] } - } + }, + VueComponent ], componentAnimations: [ { diff --git a/sample/src/server.ts b/sample/src/server.ts index 6fcee685..f7332f99 100644 --- a/sample/src/server.ts +++ b/sample/src/server.ts @@ -35,9 +35,12 @@ export default createServer({ player.setGraphic("hero"); }, onInput(player: RpgPlayer, input: any) { + // if (input.action) { + // player.wood.update(wood => wood + 1) + // player.showComponentAnimation('wood') + // } if (input.action) { - player.wood.update(wood => wood + 1) - player.showComponentAnimation('wood') + player.gui("RpgComponentExample").open() } } }, diff --git a/sample/src/standalone.ts b/sample/src/standalone.ts index b98e4cbd..d65e789e 100644 --- a/sample/src/standalone.ts +++ b/sample/src/standalone.ts @@ -2,6 +2,7 @@ import { mergeConfig } from "@signe/di"; import { provideRpg, startGame } from "@rpgjs/client"; import startServer from "./server"; import configClient from "./config/config.client"; +import { provideVueGui } from "@rpgjs/vue"; startGame( mergeConfig(configClient, { diff --git a/sample/src/vue-component-with-injections.vue b/sample/src/vue-component-with-injections.vue new file mode 100644 index 00000000..464db045 --- /dev/null +++ b/sample/src/vue-component-with-injections.vue @@ -0,0 +1,367 @@ + + + + + \ No newline at end of file diff --git a/sample/vite.config.ts b/sample/vite.config.ts index b0ed8101..36e87fc6 100644 --- a/sample/vite.config.ts +++ b/sample/vite.config.ts @@ -1,9 +1,11 @@ import { defineConfig } from 'vite'; import { rpgjs, tiledMapFolderPlugin } from '@rpgjs/vite'; import startServer from './src/server'; +import vue from '@vitejs/plugin-vue'; export default defineConfig({ plugins: [ + vue(), ...rpgjs({ server: startServer })