diff --git a/.eslintrc b/.eslintrc index 2847ba7..f445eb7 100644 --- a/.eslintrc +++ b/.eslintrc @@ -17,15 +17,17 @@ "plugin:react/recommended", "plugin:prettier/recommended", "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended" + "plugin:@typescript-eslint/recommended", + "prettier" ], "settings": { "react": { - "version": "detect", + "version": "detect" } }, "rules": { "quotes": ["error", "single"], + "object-curly-spacing": ["off"], "@typescript-eslint/ban-ts-comment": "off", "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-unused-vars": "error" diff --git a/.prettierrc.json b/.prettierrc.json index b050e9e..f4d0a4b 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -3,5 +3,6 @@ "tabWidth": 2, "printWidth": 100, "singleQuote": true, - "arrowParens": "avoid" -} + "arrowParens": "avoid", + "bracketSpacing": true +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ac4322..1d628b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/). +## [1.4.0] - 2024-10-08 + +### Added +- **EventStorageReceiver**: Added `EventStorageReceiver` to store events for future processing. +- **InsertPasswordEmitter**: Improved `InsertPasswordEmitter` to notify the `InteractionDirector` if the password was successfully resolved. +- **InsertPasswordPrompt**: Enhanced the UX and flow of the password prompt. + +### Fixed +- **RotateReceiver, TeleportReceiver, TranslateReceiver**: Fixed issues in the behavior of rotate, teleport, and translate receivers. +- **Fade utilities**: Resolved bugs in the fade utilities (`fadeIn` and `fadeOut`). + +### Changed +- **appState**: Minor updates in the global state management with `appState`. +- **InteractionDirector**: Improved component state handling and event emission logic. +- **Eslint and Prettier**: Updated eslint and prettier configurations. + ## [1.3.0] - 2024-09-20 ### Added diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index bd19f83..7222add 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -1,40 +1,38 @@ --- name: Bug Report -about: Report a bug to help us improve +about: Report an issue related to data models or schema errors title: '' labels: 'bug' assignees: '' --- -**Describe the bug** -A clear and concise description of what the bug is. +**Describe the bug** +A clear and concise description of what the bug is related to the data model or schema (e.g., validation error, incorrect schema, missing field). -**To Reproduce** -Steps to reproduce the behavior: +**To Reproduce** +Steps to reproduce the issue: -1. Go to '...' -2. Click on '...' -3. Scroll down to '...' -4. See error +1. Query or operation that triggered the bug. +2. The expected vs actual result. +3. Example of data or payload used (if applicable). -**Expected behavior** -A clear and concise description of what you expected to happen. +**Expected behavior** +A clear and concise description of what you expected the schema or data model to do. -**Screenshots** -If applicable, add screenshots to help explain your problem. +**Logs and Error Messages** +If applicable, include relevant logs or error messages that help diagnose the issue (e.g., validation errors, MongoDB errors). -**Desktop (please complete the following information):** +**Database Details** -- OS: [e.g. Windows, macOS] -- Browser [e.g. Chrome, Safari] -- Version [e.g. 22] +- MongoDB version: [e.g. 4.2] +- Mongoose version: [e.g. 6.0] +- Node.js version: [e.g. 20.x] -**Smartphone (please complete the following information):** +**Environment (if applicable)** -- Device: [e.g. iPhone6] -- OS: [e.g. iOS8.1] -- Browser [e.g. stock browser, Safari] -- Version [e.g. 22] +- OS: [e.g. Ubuntu, macOS] +- Node.js Version: [e.g. 16.14.0] +- Mongoose Version: [e.g. 6.2.1] -**Additional context** -Add any other context about the problem here. +**Additional context** +Add any other context related to the problem. For example, is it happening only with certain queries or data? diff --git a/package.json b/package.json index 064dcbc..c3bac2b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "numinia-oncyber", - "version": "1.3.0", + "version": "1.4.0", "description": "This project is a warehouse of components designed to implement various functionalities within the Oncyber platform. These components can be used to enhance and customize spaces or levels in Oncyber.", "private": "true", "type": "module", @@ -22,7 +22,7 @@ "oncyber" ], "author": "Numinia", - "license": "ISC", + "license": "MIT", "bugs": { "url": "https://github.com/numengames/numinia-oncyber/issues" }, @@ -58,4 +58,4 @@ "react-dom": "^18.3.1", "three": "^0.163.0" } -} +} \ No newline at end of file diff --git a/src/behaviors/emitters/BaseEmitter.ts b/src/behaviors/emitters/BaseEmitter.ts index cde7db2..83117e8 100644 --- a/src/behaviors/emitters/BaseEmitter.ts +++ b/src/behaviors/emitters/BaseEmitter.ts @@ -1,12 +1,16 @@ import { Folder, Param, $Param, ScriptBehavior } from '@oo/scripting'; -import InteractionDirector from '../../common/interactions/InteractionDirector'; +import InteractionDirector, { + InteractionDirectorOptionParams, +} from '../../common/interactions/InteractionDirector'; interface BaseEmitterParams { triggerKey?: string; interactionMode: string; triggerDistance?: number; + xInteractionAdjustment?: number; yInteractionAdjustment?: number; + zInteractionAdjustment?: number; } /** @@ -19,10 +23,10 @@ export default class BaseEmitter extends ScriptBehavior { }; @Param({ name: 'Signal sender' }) - private enterSignal = $Param.Signal(); + private senderSignal = $Param.Signal(); @Param({ type: 'boolean', defaultValue: false, name: 'Can emit signals multiple times?' }) - private doesEmitMultipleTimes = false; + private isActiveMultipleTimes = false; @Folder('Interaction Mode') @Param({ @@ -32,19 +36,41 @@ export default class BaseEmitter extends ScriptBehavior { options: ['Auto', 'Key'], }) private interactionMode = 'Auto'; + @Param({ type: 'string', name: 'Trigger key', visible: (params: BaseEmitterParams) => params.interactionMode === 'Key', }) private triggerKey = 'E'; + + @Param({ + min: -20, + step: 0.1, + type: 'number', + name: 'X Key dialog adjustment', + visible: (params: BaseEmitterParams) => params.interactionMode === 'Key', + }) + private xInteractionAdjustment = 0; + @Param({ + min: -20, step: 0.1, type: 'number', - name: 'Key dialog adjustment', + name: 'Y Key dialog adjustment', visible: (params: BaseEmitterParams) => params.interactionMode === 'Key', }) private yInteractionAdjustment = 0; + + @Param({ + min: -20, + step: 0.1, + type: 'number', + name: 'Z Key dialog adjustment', + visible: (params: BaseEmitterParams) => params.interactionMode === 'Key', + }) + private zInteractionAdjustment = 0; + @Param({ min: 0.1, step: 0.1, @@ -55,10 +81,47 @@ export default class BaseEmitter extends ScriptBehavior { }) private triggerDistance = 2; + private options?: InteractionDirectorOptionParams; + /** * Called when the script is ready. */ onReady = async () => { - await InteractionDirector.handle(this, () => this.enterSignal.emit()); + this.options = { + interactionAdjustment: { + x: this.xInteractionAdjustment, + y: this.yInteractionAdjustment, + z: this.zInteractionAdjustment, + }, + triggerDistance: this.triggerDistance, + isActiveMultipleTimes: this.isActiveMultipleTimes, + }; + + await InteractionDirector.handle( + { + host: this.host, + triggerKey: this.triggerKey, + options: { interactionMode: this.interactionMode }, + }, + this.handleInteractionStart.bind(this), + this.handleInteractionEnd.bind(this), + this.options, + ); }; + + /** + * Handles what happens when the interaction starts. + * @param {Function} callback - Optional callback to notify success or failure. + */ + private handleInteractionStart(callback?: (response: boolean) => void) { + if (callback) { + callback(true); + } + this.senderSignal.emit(); + } + + /** + * Handles what happens when the interaction ends. + */ + private handleInteractionEnd() {} } diff --git a/src/behaviors/emitters/insert-password/InsertPasswordEmitter.tsx b/src/behaviors/emitters/insert-password/InsertPasswordEmitter.tsx index 365d396..1d311b1 100644 --- a/src/behaviors/emitters/insert-password/InsertPasswordEmitter.tsx +++ b/src/behaviors/emitters/insert-password/InsertPasswordEmitter.tsx @@ -1,19 +1,20 @@ import * as React from 'react'; import { Folder, Param, $Param, ScriptBehavior, UI } from '@oo/scripting'; -import InsertPasswordPrompt from './InsertPasswordPrompt.tsx'; -import InteractionDirector from '../../../common/interactions/InteractionDirector'; +import InsertPasswordPrompt from './InsertPasswordPrompt'; +import InteractionDirector, { + InteractionDirectorOptionParams, +} from '../../../common/interactions/InteractionDirector.ts'; interface InsertPasswordEmitterParams { triggerKey?: string; interactionMode: string; triggerDistance?: number; + xInteractionAdjustment?: number; yInteractionAdjustment?: number; + zInteractionAdjustment?: number; } -/** - * Main class to handle the emitter in the script. - */ export default class InsertPasswordEmitter extends ScriptBehavior { static config = { title: 'Emitter - Insert Password', @@ -22,17 +23,20 @@ export default class InsertPasswordEmitter extends ScriptBehavior { private renderer = UI.createRenderer(); - @Param({ name: 'Signal sender' }) - private enterSignal = $Param.Signal(); + @Param({ name: 'Password solved signal sender' }) + private passwordSolvedSignal = $Param.Signal(); + + @Param({ name: 'Password error signal sender' }) + private passwordErrorSignal = $Param.Signal(); @Param({ type: 'string', - name: 'Master password' + name: 'Master password', }) private masterPassword = ''; @Param({ type: 'boolean', defaultValue: false, name: 'Can emit signals multiple times?' }) - private doesEmitMultipleTimes = false; + private isActiveMultipleTimes = false; @Folder('Interaction Mode') @Param({ @@ -42,20 +46,41 @@ export default class InsertPasswordEmitter extends ScriptBehavior { options: ['Auto', 'Key'], }) private interactionMode = 'Auto'; + @Param({ type: 'string', name: 'Trigger key', visible: (params: InsertPasswordEmitterParams) => params.interactionMode === 'Key', }) - private triggerKey = 'E'; + + @Param({ + min: -20, + step: 0.1, + type: 'number', + name: 'X Key dialog adjustment', + visible: (params: InsertPasswordEmitterParams) => params.interactionMode === 'Key', + }) + private xInteractionAdjustment = 0; + @Param({ + min: -20, step: 0.1, type: 'number', - name: 'Key dialog adjustment', + name: 'Y Key dialog adjustment', visible: (params: InsertPasswordEmitterParams) => params.interactionMode === 'Key', }) private yInteractionAdjustment = 0; + + @Param({ + min: -20, + step: 0.1, + type: 'number', + name: 'Z Key dialog adjustment', + visible: (params: InsertPasswordEmitterParams) => params.interactionMode === 'Key', + }) + private zInteractionAdjustment = 0; + @Param({ min: 0.1, step: 0.1, @@ -66,21 +91,62 @@ export default class InsertPasswordEmitter extends ScriptBehavior { }) private triggerDistance = 2; + private options?: InteractionDirectorOptionParams; + onReady = async () => { - await InteractionDirector.handle(this, this.showInsertPassword.bind(this)); + this.options = { + interactionAdjustment: { + x: this.xInteractionAdjustment, + y: this.yInteractionAdjustment, + z: this.zInteractionAdjustment, + }, + triggerDistance: this.triggerDistance, + isActiveMultipleTimes: this.isActiveMultipleTimes, + }; + + try { + await InteractionDirector.handle( + { + host: this.host, + triggerKey: this.triggerKey, + options: { interactionMode: this.interactionMode }, + }, + this.handleInteractionStart.bind(this), + this.handleInteractionEnd.bind(this), + this.options, + ); + } catch (error) { + console.error('Error handling interaction:', error); + } }; - private showInsertPassword() { + private handleInteractionStart(callback?: (response: boolean) => void) { + const handleSolvedPassword = () => { + if (callback) { + callback(true); + } + this.passwordSolvedSignal.emit(); + }; + this.renderer.render( + onSuccess={handleSolvedPassword.bind(this)} + />, ); } - private handleSolvedPassword = () => { - this.passwordSolvedSignal.emit() + private handleInteractionEnd() { + this.hidePasswordPrompt(); + } + + private hidePasswordPrompt() { + this.renderer.render(null); + } + + private handleErrorPassword() { + this.passwordErrorSignal.emit(); } } diff --git a/src/behaviors/emitters/insert-password/InsertPasswordPrompt.tsx b/src/behaviors/emitters/insert-password/InsertPasswordPrompt.tsx index 7dbc32c..21f94fd 100644 --- a/src/behaviors/emitters/insert-password/InsertPasswordPrompt.tsx +++ b/src/behaviors/emitters/insert-password/InsertPasswordPrompt.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { useState } from 'react'; interface PasswordPromptProps { + onError: () => void; onSuccess: () => void; masterPassword: string; } @@ -99,6 +100,7 @@ const styles = ` `; const InsertPasswordPrompt = ({ + onError, onSuccess, masterPassword, }: PasswordPromptProps): JSX.Element | null => { @@ -118,6 +120,7 @@ const InsertPasswordPrompt = ({ } else { setPassword(''); setErrorMessage('Incorrect password'); + onError(); } }; @@ -127,6 +130,8 @@ const InsertPasswordPrompt = ({ if (!isVisible) return null; + document.exitPointerLock(); + return ( <>
diff --git a/src/behaviors/receivers/DiscordReceiver.ts b/src/behaviors/receivers/DiscordReceiver.ts new file mode 100644 index 0000000..b88a4cf --- /dev/null +++ b/src/behaviors/receivers/DiscordReceiver.ts @@ -0,0 +1,73 @@ +import { ScriptBehavior, Receiver, Param, World } from '@oo/scripting' + +import { getEventQueue, clearEventQueue } from '../../common/state/appState'; + +export default class DiscordReceiver extends ScriptBehavior { + private processing = false; + + @Param({ + type: 'string', + name: 'Server URL', + }) + private serverUrl = ''; + + @Receiver() + async processQueue(): Promise { + if (this.processing) { + console.log('Already processing queue.'); + return; + } + + this.processing = true; + + const eventQueue = getEventQueue(); + + if (eventQueue.length === 0) { + console.log('No events to process.'); + this.processing = false; + return; + } + + try { + for (const event of eventQueue) { + await this.postRequest({ + url: this.serverUrl, + options: { + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + // webhookUrl: event.webhookUrl, + worldName: World.name, + message: event.message, + objectId: event.objectId, + eventType: event.eventType, + }), + }, + }); + console.log(`Notification sent for ${event.eventType} event.`); + } + } catch (error) { + console.error('Failed to send notification:', error); + } finally { + clearEventQueue(); + this.processing = false; + } + } + + private async postRequest({ url, options = {} }: { url: string; options?: RequestInit }): Promise { + try { + const response = await fetch(url, { + ...options, + method: 'POST', + }); + if (!response.ok) { + throw new Error(`Request failed with status ${response.status}`); + } + return response; + } catch (error) { + console.error(`Network error:`, error); + throw error; + } + } +} \ No newline at end of file diff --git a/src/behaviors/receivers/EventStorageReceiver.ts b/src/behaviors/receivers/EventStorageReceiver.ts new file mode 100644 index 0000000..eae33f4 --- /dev/null +++ b/src/behaviors/receivers/EventStorageReceiver.ts @@ -0,0 +1,73 @@ +import { ScriptBehavior, Receiver, Param, World } from '@oo/scripting' + +import { getEventQueue, clearEventQueue } from '../../common/state/appState'; + +export default class EventStorageReceiver extends ScriptBehavior { + private processing = false; + + @Param({ + type: 'string', + name: 'Server URL', + }) + private serverUrl = ''; + + @Receiver() + async processQueue(): Promise { + if (this.processing) { + console.log('Already processing queue.'); + return; + } + + this.processing = true; + + const eventQueue = getEventQueue(); + + if (eventQueue.length === 0) { + console.log('No events to process.'); + this.processing = false; + return; + } + + try { + for (const event of eventQueue) { + await this.postRequest({ + url: this.serverUrl, + options: { + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + // webhookUrl: event.webhookUrl, + worldName: World.name, + message: event.message, + objectId: event.objectId, + eventType: event.eventType, + }), + }, + }); + console.log(`Notification sent for ${event.eventType} event.`); + } + } catch (error) { + console.error('Failed to send notification:', error); + } finally { + clearEventQueue(); + this.processing = false; + } + } + + private async postRequest({ url, options = {} }: { url: string; options?: RequestInit }): Promise { + try { + const response = await fetch(url, { + ...options, + method: 'POST', + }); + if (!response.ok) { + throw new Error(`Request failed with status ${response.status}`); + } + return response; + } catch (error) { + console.error(`Network error:`, error); + throw error; + } + } +} \ No newline at end of file diff --git a/src/behaviors/receivers/FadeInReceiver.ts b/src/behaviors/receivers/FadeInReceiver.ts new file mode 100644 index 0000000..11de923 --- /dev/null +++ b/src/behaviors/receivers/FadeInReceiver.ts @@ -0,0 +1,81 @@ +import anime from 'animejs'; +import { Player, Component3D, Receiver, Param, ScriptBehavior, Folder } from '@oo/scripting'; + +export default class FadeInReceiver extends ScriptBehavior { + @Param({ + min: 0, + step: 0.1, + type: 'number', + defaultValue: 0, + name: 'Duration', + }) + private duration = 0; + @Param({ + min: 0, + max: 1, + step: 0.1, + type: 'number', + name: 'Opacity', + defaultValue: 1, + }) + private opacity = 1; + @Param({ + min: 0, + step: 0.1, + type: 'number', + defaultValue: 0, + name: 'Execution delay (in seconds)', + }) + private holdTimeDuration = 0; + @Param({ + type: 'select', + name: 'Easing', + defaultValue: 'linear', + options: [ + 'linear', + 'easeInQuad', + 'easeOutQuad', + 'easeInOutQuad', + 'easeInCubic', + 'easeOutCubic', + 'easeInOutCubic', + 'easeInQuart', + 'easeOutQuart', + 'easeInOutQuart', + 'easeInExpo', + 'easeOutExpo', + 'easeInOutExpo', + 'easeInBack', + 'easeOutBack', + 'easeInOutBack', + 'easeInElastic', + 'easeOutElastic', + 'easeInOutElastic', + 'easeInBounce', + 'easeOutBounce', + 'easeInOutBounce', + ], + }) + private easing = 'linear'; + + @Folder('Signal Receiver') + @Receiver() + run() { + try { + const delay = this.holdTimeDuration * 1000; + + const target = this.host.name.includes('Avatar Picker') ? Player.avatar : this.host; + + setTimeout(() => { + anime({ + targets: target, + easing: this.easing, + opacity: this.opacity, + duration: this.duration * 1000, + }); + }, delay); + } catch (error) { + console.error('FadeInReceiver failed -', (error as Error).message); + } + } +} diff --git a/src/behaviors/receivers/FadeOutReceiver.ts b/src/behaviors/receivers/FadeOutReceiver.ts new file mode 100644 index 0000000..d6fe351 --- /dev/null +++ b/src/behaviors/receivers/FadeOutReceiver.ts @@ -0,0 +1,81 @@ +import anime from 'animejs'; +import { Player, Component3D, Receiver, Param, ScriptBehavior, Folder } from '@oo/scripting'; + +export default class FadeOutReceiver extends ScriptBehavior { + @Param({ + min: 0, + step: 0.1, + type: 'number', + defaultValue: 0, + name: 'Duration', + }) + private duration = 0; + @Param({ + min: 0, + max: 1, + step: 0.1, + type: 'number', + name: 'Opacity', + defaultValue: 0, + }) + private opacity = 0; + @Param({ + min: 0, + step: 0.1, + type: 'number', + defaultValue: 0, + name: 'Execution delay (in seconds)', + }) + private holdTimeDuration = 0; + @Param({ + type: 'select', + name: 'Easing', + defaultValue: 'linear', + options: [ + 'linear', + 'easeInQuad', + 'easeOutQuad', + 'easeInOutQuad', + 'easeInCubic', + 'easeOutCubic', + 'easeInOutCubic', + 'easeInQuart', + 'easeOutQuart', + 'easeInOutQuart', + 'easeInExpo', + 'easeOutExpo', + 'easeInOutExpo', + 'easeInBack', + 'easeOutBack', + 'easeInOutBack', + 'easeInElastic', + 'easeOutElastic', + 'easeInOutElastic', + 'easeInBounce', + 'easeOutBounce', + 'easeInOutBounce', + ], + }) + private easing = 'linear'; + + @Folder('Signal Receiver') + @Receiver() + run() { + try { + const delay = this.holdTimeDuration * 1000; + + const target = this.host.name.includes('Avatar Picker') ? Player.avatar : this.host; + + setTimeout(() => { + anime({ + targets: target, + easing: this.easing, + opacity: this.opacity, + duration: this.duration * 1000, + }); + }, delay); + } catch (error) { + console.error('FadeOutReceiver failed -', (error as Error).message); + } + } +} diff --git a/src/behaviors/receivers/RedirectReceiver.ts b/src/behaviors/receivers/RedirectReceiver.ts index 77467d1..46f2ce1 100644 --- a/src/behaviors/receivers/RedirectReceiver.ts +++ b/src/behaviors/receivers/RedirectReceiver.ts @@ -1,15 +1,4 @@ -import { Player, Component3D, Receiver, Param, ScriptBehavior, Folder } from '@oo/scripting'; - -import fadeOut from '../../common/utils/fadeOut'; -import isValidUrl from '../../common/utils/isValidUrl'; - -interface RedirectReceiverParams { - redirectMode: string; - redirectToUrl: string; - fadeOutDuration?: number; - holdTimeDuration?: number; - isFadingAvailable?: boolean; -} +import { Component3D, Receiver, Param, ScriptBehavior, Folder } from '@oo/scripting'; const goToAnotherSpace = (url: string, mode: string): void => { const newWindow = window.open(url, mode); @@ -21,10 +10,6 @@ const goToAnotherSpace = (url: string, mode: string): void => { } }; -const applyFadeOut = (target: Component3D, duration: number): void => { - fadeOut({ target, duration }); -}; - const performRedirection = (redirectMode: string, redirectToUrl: string) => { if (redirectMode === 'Existing') { window.location.href = redirectToUrl; @@ -44,24 +29,12 @@ export default class RedirectReceiver extends ScriptBehavior { }) private redirectMode = 'Existing'; - @Param({ type: 'boolean', defaultValue: false, name: 'Enable fadeIn - fadeOut' }) - private isFadingAvailable = false; - - @Param({ - min: 0.1, - step: 0.1, - type: 'number', - name: 'FadeOut duration', - visible: (params: RedirectReceiverParams) => params.isFadingAvailable === true, - }) - private fadeOutDuration = 0; @Param({ min: 0, step: 0.1, type: 'number', defaultValue: 1, name: 'Receiver execution delay (in seconds)', - visible: (params: RedirectReceiverParams) => params.isFadingAvailable === true, }) private holdTimeDuration = 0; @@ -69,22 +42,20 @@ export default class RedirectReceiver extends ScriptBehavior { @Receiver() run() { try { - if (!isValidUrl(this.redirectToUrl)) { - console.error('Invalid URL provided:', this.redirectToUrl); + if (!this.redirectToUrl) { + console.error('You have to provide a URL:', this.redirectToUrl); return; } - if (this.isFadingAvailable && this.fadeOutDuration) { - applyFadeOut(Player.avatar, this.fadeOutDuration); - } + const parsedUrl = new URL(this.redirectToUrl); const delay = this.holdTimeDuration * 1000; setTimeout(() => { - performRedirection(this.redirectMode, this.redirectToUrl); + performRedirection(this.redirectMode, parsedUrl.toString()); }, delay); } catch (error) { - console.error('Redirection failed -', (error as Error).message); + console.error('Redirect receiver failed -', (error as Error).message); } } } diff --git a/src/behaviors/receivers/RemoveCollisionReceiver.ts b/src/behaviors/receivers/RemoveCollisionReceiver.ts new file mode 100644 index 0000000..7eae805 --- /dev/null +++ b/src/behaviors/receivers/RemoveCollisionReceiver.ts @@ -0,0 +1,26 @@ +import { Component3D, Receiver, Param, ScriptBehavior, Folder } from '@oo/scripting'; + +export default class RemoveCollisionReceiver extends ScriptBehavior { + @Param({ + min: 0, + step: 0.1, + type: 'number', + defaultValue: 0, + name: 'Receiver execution delay (in seconds)', + }) + private holdTimeDuration = 0; + + @Folder('Signal Receiver') + @Receiver() + run() { + try { + const delay = this.holdTimeDuration * 1000; + + setTimeout(() => { + this.host.collider.enabled = false; + }, delay); + } catch (error) { + console.error('Remove collision receiver failed -', (error as Error).message); + } + } +} diff --git a/src/behaviors/receivers/RotateReceiver.ts b/src/behaviors/receivers/RotateReceiver.ts index dc750a1..0697303 100644 --- a/src/behaviors/receivers/RotateReceiver.ts +++ b/src/behaviors/receivers/RotateReceiver.ts @@ -7,39 +7,42 @@ export default class RotateReceiver extends ScriptBehavior { min: 0, step: 0.1, type: 'number', - defaultValue: 1, + defaultValue: 0, name: 'Receiver execution delay (in seconds)', }) private holdTimeDuration = 0; @Folder('Rotate Action Config') @Param({ - min: 0, - max: 360, step: 0.1, + max: 360, + min: -360, type: 'number', defaultValue: 0, name: 'Rotation X axis', }) private rotationX = 0; + @Param({ - min: 0, - max: 360, step: 0.1, + max: 360, + min: -360, type: 'number', defaultValue: 0, name: 'Rotation Y axis', }) private rotationY = 0; + @Param({ - min: 0, - max: 360, step: 0.1, + max: 360, + min: -360, type: 'number', defaultValue: 0, name: 'Rotation Z axis', }) private rotationZ = 0; + @Param({ min: 0, max: 20, @@ -50,12 +53,34 @@ export default class RotateReceiver extends ScriptBehavior { }) private duration = 0.5; - @Folder() @Param({ type: 'select', name: 'Easing', defaultValue: 'linear', - options: ['linear', 'easeInQuad', 'easeOutQuad', 'easeInOutQuad', 'easeInCubic', 'easeOutCubic', 'easeInOutCubic', 'easeInQuart', 'easeOutQuart', 'easeInOutQuart', 'easeInExpo', 'easeOutExpo', 'easeInOutExpo', 'easeInBack', 'easeOutBack', 'easeInOutBack', 'easeInElastic', 'easeOutElastic', 'easeInOutElastic', 'easeInBounce', 'easeOutBounce', 'easeInOutBounce'] + options: [ + 'linear', + 'easeInQuad', + 'easeOutQuad', + 'easeInOutQuad', + 'easeInCubic', + 'easeOutCubic', + 'easeInOutCubic', + 'easeInQuart', + 'easeOutQuart', + 'easeInOutQuart', + 'easeInExpo', + 'easeOutExpo', + 'easeInOutExpo', + 'easeInBack', + 'easeOutBack', + 'easeInOutBack', + 'easeInElastic', + 'easeOutElastic', + 'easeInOutElastic', + 'easeInBounce', + 'easeOutBounce', + 'easeInOutBounce', + ], }) private easing = 'linear'; @@ -63,15 +88,19 @@ export default class RotateReceiver extends ScriptBehavior { run() { const delay = this.holdTimeDuration * 1000; - setTimeout(() => { - anime({ - easing: this.easing, - targets: this.host.rotation, - duration: this.duration * 1000, - rotateX: this.host.rotation.x + this.rotationX * (Math.PI / 180), - rotateY: this.host.rotation.y + this.rotationY * (Math.PI / 180), - rotateZ: this.host.rotation.z + this.rotationZ * (Math.PI / 180), - }); - }, delay); + try { + setTimeout(() => { + anime({ + easing: this.easing, + targets: this.host.rotation, + duration: this.duration * 1000, + x: this.host.rotation.x + this.rotationX * (Math.PI / 180), + y: this.host.rotation.y + this.rotationY * (Math.PI / 180), + z: this.host.rotation.z + this.rotationZ * (Math.PI / 180), + }); + }, delay); + } catch (error) { + console.error('Rotate receiver failed', error); + } } } diff --git a/src/behaviors/receivers/TeleportReceiver.ts b/src/behaviors/receivers/TeleportReceiver.ts index 6647c35..9ba76ab 100644 --- a/src/behaviors/receivers/TeleportReceiver.ts +++ b/src/behaviors/receivers/TeleportReceiver.ts @@ -9,24 +9,7 @@ import { } from '@oo/scripting'; import { Vector3 } from 'three'; -import fadeIn from '../../common/utils/fadeIn'; -import fadeOut from '../../common/utils/fadeOut'; - -interface TeleportReceiverInputParams { - fadeInDuration?: number; - fadeOutDuration?: number; - holdTimeDuration?: number; - isFadingAvailable?: boolean; - targetComponent: Component3D; -} - -const applyFadeOut = (target: Component3D, duration: number) => { - fadeOut({ target, duration }); -}; - -const applyFadeIn = (target: Component3D, duration: number) => { - fadeIn({ target, duration }); -}; +import { addEventToQueue } from '../../common/state/appState'; export default class TeleportReceiver extends ScriptBehavior { @Param({ @@ -35,35 +18,18 @@ export default class TeleportReceiver extends ScriptBehavior { }) private targetComponent = $Param.Component('any'); - @Folder('Animations') - @Param({ type: 'boolean', defaultValue: false, name: 'Enable fadeIn - fadeOut' }) - private isFadingAvailable = false; - @Param({ - min: 0.1, - step: 0.1, - type: 'number', - name: 'FadeIn duration', - visible: (params: TeleportReceiverInputParams) => params.isFadingAvailable === true, - }) - private fadeInDuration = 0; - @Param({ - min: 0.1, - step: 0.1, - type: 'number', - name: 'FadeOut duration', - visible: (params: TeleportReceiverInputParams) => params.isFadingAvailable === true, - }) - private fadeOutDuration = 0; @Param({ min: 0, step: 0.1, type: 'number', defaultValue: 1, name: 'Receiver execution delay (in seconds)', - visible: (params: TeleportReceiverInputParams) => params.isFadingAvailable === true, }) private holdTimeDuration = 0; + @Param({ name: 'Log signal sender' }) + private logSignalSender = $Param.Signal(); + private teleportAvatar(target: Component3D) { const targetPosition = this.targetComponent.position; const dimension = this.targetComponent.getDimensions(); @@ -87,18 +53,17 @@ export default class TeleportReceiver extends ScriptBehavior { throw new Error('Collider must be activated to have rigidBody property available'); } - if (this.isFadingAvailable && this.fadeOutDuration) { - applyFadeOut(target, this.fadeOutDuration); - } + addEventToQueue({ + eventType: 'teleport', + objectId: this.host.name, + message: `Teleported to ${this.targetComponent.name || 'unknown target'}`, + }); const delay = this.holdTimeDuration * 1000; setTimeout(() => { this.teleportAvatar(target); - - if (this.isFadingAvailable && this.fadeInDuration) { - applyFadeIn(target, this.fadeInDuration); - } + this.logSignalSender.emit(); }, delay); } catch (error) { console.error( diff --git a/src/behaviors/receivers/TranslateReceiver.ts b/src/behaviors/receivers/TranslateReceiver.ts index 28f361d..3774a44 100644 --- a/src/behaviors/receivers/TranslateReceiver.ts +++ b/src/behaviors/receivers/TranslateReceiver.ts @@ -1,4 +1,4 @@ -import { ScriptBehavior, Component3D, Receiver, Folder, Param } from '@oo/scripting'; +import { ScriptBehavior, Component3D, Receiver, Folder, Param, $Param } from '@oo/scripting'; import anime from 'animejs'; @@ -7,39 +7,45 @@ export default class TranslateReceiver extends ScriptBehavior { min: 0, step: 0.1, type: 'number', - defaultValue: 1, + defaultValue: 0, name: 'Receiver execution delay (in seconds)', }) private holdTimeDuration = 0; + @Param({ name: 'Log signal sender' }) + private logSignalSender = $Param.Signal(); + @Folder('Translate Receiver Config') @Param({ - min: 0, - max: 360, step: 0.1, + max: 1000, + min: -1000, type: 'number', defaultValue: 0, name: 'Translate to X axis', }) private positionX = 0; + @Param({ - min: 0, - max: 360, step: 0.1, + max: 1000, + min: -1000, type: 'number', defaultValue: 0, name: 'Translate to Y axis', }) private positionY = 0; + @Param({ - min: 0, - max: 360, step: 0.1, + max: 1000, + min: -1000, type: 'number', defaultValue: 0, name: 'Translate to Z axis', }) private positionZ = 0; + @Param({ min: 0, max: 20, @@ -49,6 +55,7 @@ export default class TranslateReceiver extends ScriptBehavior { name: 'translate duration', }) private duration = 0.5; + @Param({ type: 'select', name: 'Easing', @@ -84,15 +91,21 @@ export default class TranslateReceiver extends ScriptBehavior { run() { const delay = this.holdTimeDuration * 1000; - setTimeout(() => { - anime({ - easing: this.easing, - targets: this.host.position, - duration: this.duration * 1000, - translateX: this.host.position.x + this.positionX, - translateY: this.host.position.y + this.positionY, - translateZ: this.host.position.z + this.positionZ, - }); - }, delay); + try { + setTimeout(() => { + anime({ + easing: this.easing, + targets: this.host.position, + duration: this.duration * 1000, + x: this.host.position.x + this.positionX, + y: this.host.position.y + this.positionY, + z: this.host.position.z + this.positionZ, + }); + + this.logSignalSender.emit(); + }, delay); + } catch (error) { + console.error('Translate receiver failed', error); + } } } diff --git a/src/common/interactions/InteractionDirector.ts b/src/common/interactions/InteractionDirector.ts index 2a11952..3ce2891 100644 --- a/src/common/interactions/InteractionDirector.ts +++ b/src/common/interactions/InteractionDirector.ts @@ -1,117 +1,137 @@ import { Component3D, Components, Player } from '@oo/scripting'; -/** - * Parameters for configuring an interaction in the InteractionDirector. - */ -interface InteractionDirectorParams { - /** - * Optional distance within which the interaction is active. - */ - distance?: number; +export interface InteractionAdjustment { + x: number; + y: number; + z: number; +} - /** - * The 3D component that serves as the reference for the interaction's position. - */ - host: Component3D; +export interface InteractionDirectorOptionParams { + triggerDistance?: number; + interactionAdjustment: InteractionAdjustment; + isActiveMultipleTimes: boolean; +} - /** - * The key that triggers the interaction. - */ +interface InteractionDirectorParams { + host: Component3D; triggerKey: string; - - /** - * Adjustment to the Y position of the interaction. - */ - yInteractionAdjustment: number; - - /** - * Define if its possible to interact with the element just once or multiple times. - */ - isActiveMultipleTimes: boolean; + options?: Record; } -/** - * Manages and creates interactions in the environment, such as key-triggered or proximity-based interactions. - */ export default class InteractionDirector { /** * Handles the interaction based on the specified interaction mode. - * @param {any} context - The context containing interaction mode and other parameters. - * @param {() => void} action - The action to be executed when the interaction is triggered. + * @param {InteractionDirectorParams} params - Parameters for the interaction. + * @param {(callback?: (response: boolean) => void) => void} handleInteractionStart - The action to be executed when entering the interaction range. + * @param {() => void} handleInteractionEnd - The action to be executed when exiting the interaction range. + * @param {InteractionDirectorOptionParams} options - Options to configure the interaction. */ - static async handle(context: any, action: () => void) { + static async handle( + params: InteractionDirectorParams, + handleInteractionStart: (callback?: (response: boolean) => void) => void, + handleInteractionEnd: () => void, + options?: InteractionDirectorOptionParams, + ) { const director = new InteractionDirector(); - switch (context.interactionMode) { + switch (params.options?.interactionMode) { case 'Key': - await director.createInteractionByKey(context, action); + await director.createKeyInteraction(params, handleInteractionStart, options); break; case 'Auto': - await director.createInteractionAuto(context, action); + await director.createProximityInteraction( + params, + handleInteractionStart, + handleInteractionEnd, + options, + ); break; default: - return; + throw new Error('Unknown interaction mode'); } } /** - * Creates an interaction that is triggered by a key press. - * @param {InteractionDirectorParams} params - The parameters for creating the key-triggered interaction. - * @param {() => void} action - The action to be executed when the interaction is triggered. + * Creates an interaction triggered by a key press. + * @param {InteractionDirectorParams} params - Parameters for the interaction. + * @param {(callback?: (response: boolean) => void) => void} onInteractionSuccess - Action to execute upon interaction success. + * @param {InteractionDirectorOptionParams} options - Interaction configuration options. */ - private async createInteractionByKey( - { distance, triggerKey, host, yInteractionAdjustment, isActiveMultipleTimes }: InteractionDirectorParams, - action: () => void, + private async createKeyInteraction( + { triggerKey, host }: InteractionDirectorParams, + onInteractionSuccess: (callback?: (response: boolean) => void) => void, + options?: InteractionDirectorOptionParams, ) { - const interaction = await Components.create({ - distance, - type: 'interaction', - distanceTarget: Player.avatar.position, - atlas: `keyboard_${triggerKey.toLowerCase()}_outline`, - key: `Key${triggerKey.toUpperCase()}`, - }); + try { + const interaction = await Components.create({ + active: true, + type: 'interaction', + key: `Key${triggerKey.toUpperCase()}`, + distanceTarget: Player.avatar.position, + distance: options?.triggerDistance || 0, + atlas: `keyboard_${triggerKey.toLowerCase()}_outline`, + }); - this.updateInteractionPosition({ interaction, host, yInteractionAdjustment }); + this.updateInteractionPosition({ interaction, host, options }); - interaction.active = true; - - interaction.onInteraction(() => { - action(); - interaction.active = isActiveMultipleTimes; - }); + interaction.onInteraction(() => { + onInteractionSuccess(response => { + if (response && !options?.isActiveMultipleTimes) { + interaction.destroy(); + } + }); + }); + } catch (error) { + console.error('Failed to create key interaction:', error); + } } /** * Creates an automatic interaction based on proximity. - * @param {InteractionDirectorParams} params - The parameters for creating the proximity-based interaction. - * @param {() => void} action - The action to be executed when the interaction is triggered. + * @param {InteractionDirectorParams} params - Parameters for the interaction. + * @param {(callback?: (response: boolean) => void) => void} onInteractionEnter - Action to execute when entering the interaction range. + * @param {() => void} onInteractionExit - Action to execute when exiting the interaction range. + * @param {InteractionDirectorOptionParams} options - Interaction configuration options. */ - private async createInteractionAuto({ host, isActiveMultipleTimes }: InteractionDirectorParams, action: () => void) { - function handleAction() { - action(); - host.collider.isSensor = isActiveMultipleTimes; - }; + private async createProximityInteraction( + { host }: InteractionDirectorParams, + onInteractionEnter: (callback?: (response: boolean) => void) => void, + onInteractionExit: () => void, + options?: InteractionDirectorOptionParams, + ) { + host.onSensorEnter(() => { + onInteractionEnter(response => { + if (response && !options?.isActiveMultipleTimes) { + host.collider.enabled = false; + } + }); + }); - host.onSensorEnter(handleAction); + host.onSensorExit(onInteractionExit); } /** * Updates the position of the interaction relative to a host component. - * @param {object} params - The parameters for updating the interaction position. + * @param {object} params - Parameters for updating the interaction position. * @param {any} params.interaction - The interaction to update. * @param {Component3D} params.host - The 3D component whose position is used as the reference. - * @param {number} [params.yInteractionAdjustment=0] - Additional offset to adjust the Y position. + * @param {InteractionDirectorOptionParams} options - Options to configure the position adjustment. */ private updateInteractionPosition({ - interaction, host, - yInteractionAdjustment = 0, + interaction, + options, }: { - interaction: any; //TODO: Explore how to setup oo-oncyber.d.ts to allow here ScriptComponent avoiding issues from the linter + interaction: any; // TODO: Setup oo-oncyber.d.ts to avoid issues from the linter host: Component3D; - yInteractionAdjustment?: number; + options?: InteractionDirectorOptionParams; }) { interaction.position.copy(host.position); - interaction.position.y = host.position.y + host.getDimensions().y + yInteractionAdjustment; + interaction.position.x = + host.position.x + host.getDimensions().x + (options?.interactionAdjustment.x || 0); + interaction.position.y = + host.position.y + host.getDimensions().y + (options?.interactionAdjustment.y || 0); + interaction.position.z = + host.position.z + host.getDimensions().z + (options?.interactionAdjustment.z || 0); } } diff --git a/src/common/state/appState.ts b/src/common/state/appState.ts index 1e42beb..a38aa5e 100644 --- a/src/common/state/appState.ts +++ b/src/common/state/appState.ts @@ -3,6 +3,12 @@ import { UI } from '@oo/scripting'; import App from '../../components/App.tsx'; +interface EventData { + eventType: string; + objectId: string; + message: string; +} + class Store> { renderer = UI.createRenderer(); @@ -41,7 +47,19 @@ class Store> { } } -export const store = new Store({}); +export const store = new Store({ + name: '', + userId: '', + eventQueue: [] as EventData[], +}); + +export const addEventToQueue = (eventData: EventData) => { + const currentQueue = store.getSnapshot().eventQueue; + store.setState({ eventQueue: [...currentQueue, eventData] }); +}; + +export const getEventQueue = () => store.getSnapshot().eventQueue; +export const clearEventQueue = () => store.setState({ eventQueue: [] }); store.subscribe(() => { store.renderer.render(React.createElement(App)); diff --git a/src/common/utils/fadeIn.ts b/src/common/utils/fadeIn.ts deleted file mode 100644 index e568646..0000000 --- a/src/common/utils/fadeIn.ts +++ /dev/null @@ -1,18 +0,0 @@ -import anime from 'animejs'; -import { Component3D } from '@oo/scripting'; - -interface FadeInInputParams { - easing?: string; - opacity?: number; - duration?: number; - target: Component3D; -} - -export default ({ target, duration = 0, opacity = 1, easing = 'linear' }: FadeInInputParams) => { - anime({ - easing, - opacity, - targets: target, - duration: duration * 1000, - }); -}; \ No newline at end of file diff --git a/src/common/utils/fadeOut.ts b/src/common/utils/fadeOut.ts deleted file mode 100644 index 753f080..0000000 --- a/src/common/utils/fadeOut.ts +++ /dev/null @@ -1,18 +0,0 @@ -import anime from 'animejs'; -import { Component3D } from '@oo/scripting'; - -interface FadeOutInputParams { - easing?: string; - opacity?: number; - duration?: number; - target: Component3D; -} - -export default ({ target, duration = 0, opacity = 0, easing = 'linear' }: FadeOutInputParams) => { - anime({ - easing, - opacity, - targets: target, - duration: duration * 1000, - }); -}; diff --git a/src/common/utils/isValidUrl.ts b/src/common/utils/isValidUrl.ts deleted file mode 100644 index ffeb145..0000000 --- a/src/common/utils/isValidUrl.ts +++ /dev/null @@ -1,8 +0,0 @@ -export default function isValidUrl(url: string): boolean { - try { - const parsedUrl = new URL(url); - return parsedUrl.protocol === 'https:'; - } catch { - return false; - } -} diff --git a/src/core/main.ts b/src/core/main.ts index 5677e10..3e56a31 100644 --- a/src/core/main.ts +++ b/src/core/main.ts @@ -1,4 +1,4 @@ -import { World } from '@oo/scripting'; +import { World, Player } from '@oo/scripting'; import { store } from '../common/state/appState'; @@ -21,7 +21,8 @@ export default class Game { World.name = 'Numinian tools - Insert Password'; console.log('Game: start'); - store.setState({}); + const { name, userId } = Player.data; + store.setState({ userId, name }); }; onUpdate = () => {