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 = () => {