diff --git a/Block-Fountain/.dclignore b/Block-Fountain/.dclignore new file mode 100644 index 000000000..7499ff37a --- /dev/null +++ b/Block-Fountain/.dclignore @@ -0,0 +1,20 @@ +.* +bin/*.map +package-lock.json +yarn-lock.json +build.json +export +tsconfig.json +tslint.json +node_modules +*.ts +*.tsx +.vscode +Dockerfile +dist +README.md +*.blend +*.fbx +*.zip +*.rar +src diff --git a/Block-Fountain/.github/workflows/ci.yml b/Block-Fountain/.github/workflows/ci.yml new file mode 100644 index 000000000..f3ccb9e3f --- /dev/null +++ b/Block-Fountain/.github/workflows/ci.yml @@ -0,0 +1,19 @@ +name: test-build + +on: + push: + pull_request: + +jobs: + lint-and-build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Use Node.js 18.x + uses: actions/setup-node@v1 + with: + node-version: 18.x + - name: install dependencies + run: npm install + - name: npm run build + run: npm run build diff --git a/Block-Fountain/.gitignore b/Block-Fountain/.gitignore new file mode 100644 index 000000000..24fe526b3 --- /dev/null +++ b/Block-Fountain/.gitignore @@ -0,0 +1,9 @@ +package-lock.json +*.js +node_modules +bin/ +.DS_Store +**/.DS_Store +npm-debug.log* +yarn-debug.log* +yarn-error.log* \ No newline at end of file diff --git a/Block-Fountain/.vscode/extensions.json b/Block-Fountain/.vscode/extensions.json new file mode 100644 index 000000000..38b0d6772 --- /dev/null +++ b/Block-Fountain/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["decentralandfoundation.decentraland-sdk7"] +} diff --git a/Block-Fountain/.vscode/launch.json b/Block-Fountain/.vscode/launch.json new file mode 100644 index 000000000..a3b7699af --- /dev/null +++ b/Block-Fountain/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + // Use the Decentraland Editor extension of VSCode to debug the scene + // in chrome from VSCode + "version": "0.2.0", + "configurations": [ + { + "type": "chrome", + "request": "launch", + "name": "Debug Decentraland in Chrome", + "url": "${command:decentraland-sdk7.commands.getDebugURL}", + "webRoot": "${workspaceFolder}/bin", + "sourceMapPathOverrides": { + "dcl:///*": "${workspaceFolder}/*" + } + } + ] +} diff --git a/Block-Fountain/README.md b/Block-Fountain/README.md new file mode 100644 index 000000000..ef8b6e5ec --- /dev/null +++ b/Block-Fountain/README.md @@ -0,0 +1,125 @@ +# Block Fountain + +A cube-based fountain with multiple animations that uses P2P to sync animations between players. + +![](screenshot/screenshot.png) + +This scene shows you: + +- How to use websockets to sync events between players +- How to handle multiple animations in a same model +- How to keep the scene's code abstracted into various game objects with state and methods of their own +- How to handle random events + + +## Try it out + +**Setting Up Your Environment** + +To start developing for Decentraland, follow [these instructions](https://docs.decentraland.org/creator/development-guide/sdk7/installation-guide/). + +**Previewing the scene** + +You can run the scene by following [these instructions](https://docs.decentraland.org/creator/development-guide/sdk7/preview-scene/). + +**Scene Usage** + +The fountain has four rings of cubes that each behave independently, each ring has three different animations. + +If left alone, these animations will be randomly triggered at random intervals. These random events aren't synced between players. + +If a player pushes one of the buttons, this will trigger the corresponding animation. The random behavior will stop for a given amount of time, and the animation triggered by the button is synced with all other players in the scene. + +You can test this by opening the preview on multiple browser windows. If a player pushes a button on one of these instances, all other players should see the same animation play at the same time. + +Learn more about how to build your own scenes in our [documentation](https://docs.decentraland.org/) site. + +## Customize the Scene + +To customize the Block Fountain scene or integrate similar functionality into your Decentraland projects, you can follow these guidelines and code snippets: + +### Creating Buttons + +You can create buttons that trigger animations when pressed. Here's how to create a button: + +```typescript +import { Button } from './button'; +import { Vector3 } from '@dcl/sdk/math'; + +// Create a new button +const button1 = new Button( + 'models/buttonModel.glb', // Model path + new Vector3(x, y, z), // Position + new Vector3(0, 0, 0), // Rotation + new Vector3(1, 1, 1), // Scale + 'sounds/click.mp3', // Audio clip URL + 'buttonAnimationName' // Animation name +); +``` + +### Creating Consoles + +Consoles contain interactive buttons and help organize the scene. Here's how to create a console: + +```typescript +import { Console } from './console'; +import { Vector3 } from '@dcl/sdk/math'; +import { MessageBus } from '@dcl/sdk/message-bus'; + +// Create a new console +const cyanConsole = new Console( + new Vector3(x, y, z), // Position + new Vector3(0, 0, 0), // Rotation + new Vector3(1, 1, 1), // Scale + baseEntity, // Parent entity + 'models/consoleModel.glb', // Console model path + 1, // Target ring (1 to 4) + 'models/button1.glb', // Button 1 model path + 'button1Animation', // Button 1 animation name + 'models/button2.glb', // Button 2 model path + 'button2Animation', // Button 2 animation name + 'models/button3.glb', // Button 3 model path + 'button3Animation', // Button 3 animation name + sceneMessageBus // Message bus for synchronization +); +``` + +### Handling Fountain Animations + +Manage fountain animations and synchronize them using the 'RandomFountain' class: + +```typescript +import { RandomFountain } from './randomizer'; +import { Ring } from './ring'; + +// Create and add the fountain animation system +const fountainPlayer = new RandomFountain(rings, 10); +engine.addSystem((dt) => { + fountainPlayer.update(dt); +}); + +// Handle fountain animation events +sceneMessageBus.on('fountainAnim', (e) => { + fountainPlayer.playingMode = 0; + utils.timers.setTimeout(() => { + fountainPlayer.playingMode = 1; + }, 20000); + + // Trigger ring animations according to events + switch (e.anim) { + case 1: + rings[e.ring].play1(); + break; + case 2: + rings[e.ring].play2(); + break; + case 3: + rings[e.ring].play3(); + break; + } +}); +``` + +## Copyright info + +This scene is protected with a standard Apache 2 licence. See the terms and conditions in the [LICENSE](/LICENSE) file. diff --git a/Block-Fountain/assets/scene/main.composite b/Block-Fountain/assets/scene/main.composite new file mode 100644 index 000000000..ce07ea3c4 --- /dev/null +++ b/Block-Fountain/assets/scene/main.composite @@ -0,0 +1,253 @@ +{ + "version": 1, + "components": [ + { + "name": "core::Transform", + "jsonSchema": { + "type": "object", + "properties": { + "position": { + "type": "object", + "properties": { + "x": { + "type": "number" + }, + "y": { + "type": "number" + }, + "z": { + "type": "number" + } + } + }, + "scale": { + "type": "object", + "properties": { + "x": { + "type": "number" + }, + "y": { + "type": "number" + }, + "z": { + "type": "number" + } + } + }, + "rotation": { + "type": "object", + "properties": { + "x": { + "type": "number" + }, + "y": { + "type": "number" + }, + "z": { + "type": "number" + }, + "w": { + "type": "number" + } + } + }, + "parent": { + "type": "integer" + } + }, + "serializationType": "transform" + }, + "data": { + "512": { + "json": { + "position": { + "x": 8, + "y": 1, + "z": 8 + }, + "scale": { + "x": 1, + "y": 1, + "z": 1 + }, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "parent": 0 + } + } + } + }, + { + "name": "core::MeshRenderer", + "jsonSchema": { + "type": "object", + "properties": {}, + "serializationType": "protocol-buffer", + "protocolBuffer": "PBMeshRenderer" + }, + "data": { + "512": { + "json": { + "mesh": { + "$case": "box", + "box": { + "uvs": [] + } + } + } + } + } + }, + { + "name": "core-schema::Name", + "jsonSchema": { + "type": "object", + "properties": { + "value": { + "type": "string", + "serializationType": "utf8-string" + } + }, + "serializationType": "map" + }, + "data": { + "512": { + "json": { + "value": "Magic Cube" + } + } + } + }, + { + "name": "inspector::Scene", + "jsonSchema": { + "type": "object", + "properties": { + "layout": { + "type": "object", + "properties": { + "base": { + "type": "object", + "properties": { + "x": { + "type": "integer", + "serializationType": "int32" + }, + "y": { + "type": "integer", + "serializationType": "int32" + } + }, + "serializationType": "map" + }, + "parcels": { + "type": "array", + "items": { + "type": "object", + "properties": { + "x": { + "type": "integer", + "serializationType": "int32" + }, + "y": { + "type": "integer", + "serializationType": "int32" + } + }, + "serializationType": "map" + }, + "serializationType": "array" + } + }, + "serializationType": "map" + } + }, + "serializationType": "map" + }, + "data": { + "0": { + "json": { + "layout": { + "base": { + "x": 0, + "y": 0 + }, + "parcels": [ + { + "x": 0, + "y": 0 + } + ] + } + } + } + } + }, + { + "name": "inspector::Nodes", + "jsonSchema": { + "type": "object", + "properties": { + "value": { + "type": "array", + "items": { + "type": "object", + "properties": { + "entity": { + "type": "integer", + "serializationType": "entity" + }, + "children": { + "type": "array", + "items": { + "type": "integer", + "serializationType": "entity" + }, + "serializationType": "array" + } + }, + "serializationType": "map" + }, + "serializationType": "array" + } + }, + "serializationType": "map" + }, + "data": { + "0": { + "json": { + "value": [ + { + "entity": 0, + "children": [ + 512 + ] + }, + { + "entity": 512, + "children": [] + } + ] + } + } + } + }, + { + "name": "cube-id", + "jsonSchema": { + "type": "object", + "properties": {}, + "serializationType": "map" + }, + "data": { + "512": { + "json": {} + } + } + } + ] +} \ No newline at end of file diff --git a/Block-Fountain/images/scene-thumbnail.png b/Block-Fountain/images/scene-thumbnail.png new file mode 100644 index 000000000..36e10d0aa Binary files /dev/null and b/Block-Fountain/images/scene-thumbnail.png differ diff --git a/Block-Fountain/models/buttons/Cyan/Base/BaseCyan.glb b/Block-Fountain/models/buttons/Cyan/Base/BaseCyan.glb new file mode 100644 index 000000000..d807d3d81 Binary files /dev/null and b/Block-Fountain/models/buttons/Cyan/Base/BaseCyan.glb differ diff --git a/Block-Fountain/models/buttons/Cyan/Buttons/ButtonA_Cyan.glb b/Block-Fountain/models/buttons/Cyan/Buttons/ButtonA_Cyan.glb new file mode 100644 index 000000000..ed2a348bb Binary files /dev/null and b/Block-Fountain/models/buttons/Cyan/Buttons/ButtonA_Cyan.glb differ diff --git a/Block-Fountain/models/buttons/Cyan/Buttons/ButtonB_Cyan.glb b/Block-Fountain/models/buttons/Cyan/Buttons/ButtonB_Cyan.glb new file mode 100644 index 000000000..0e54624ef Binary files /dev/null and b/Block-Fountain/models/buttons/Cyan/Buttons/ButtonB_Cyan.glb differ diff --git a/Block-Fountain/models/buttons/Cyan/Buttons/ButtonC_Cyan.glb b/Block-Fountain/models/buttons/Cyan/Buttons/ButtonC_Cyan.glb new file mode 100644 index 000000000..5a93b7f9f Binary files /dev/null and b/Block-Fountain/models/buttons/Cyan/Buttons/ButtonC_Cyan.glb differ diff --git a/Block-Fountain/models/buttons/Red/Base/BaseRed.glb b/Block-Fountain/models/buttons/Red/Base/BaseRed.glb new file mode 100644 index 000000000..bd8eb638a Binary files /dev/null and b/Block-Fountain/models/buttons/Red/Base/BaseRed.glb differ diff --git a/Block-Fountain/models/buttons/Red/Buttons/ButtonA_Red.glb b/Block-Fountain/models/buttons/Red/Buttons/ButtonA_Red.glb new file mode 100644 index 000000000..76e27279f Binary files /dev/null and b/Block-Fountain/models/buttons/Red/Buttons/ButtonA_Red.glb differ diff --git a/Block-Fountain/models/buttons/Red/Buttons/ButtonB_Red.glb b/Block-Fountain/models/buttons/Red/Buttons/ButtonB_Red.glb new file mode 100644 index 000000000..e5a72b91f Binary files /dev/null and b/Block-Fountain/models/buttons/Red/Buttons/ButtonB_Red.glb differ diff --git a/Block-Fountain/models/buttons/Red/Buttons/ButtonC_Red.glb b/Block-Fountain/models/buttons/Red/Buttons/ButtonC_Red.glb new file mode 100644 index 000000000..484b95200 Binary files /dev/null and b/Block-Fountain/models/buttons/Red/Buttons/ButtonC_Red.glb differ diff --git a/Block-Fountain/models/buttons/Violet/Base/BaseViolet.glb b/Block-Fountain/models/buttons/Violet/Base/BaseViolet.glb new file mode 100644 index 000000000..faf6cd075 Binary files /dev/null and b/Block-Fountain/models/buttons/Violet/Base/BaseViolet.glb differ diff --git a/Block-Fountain/models/buttons/Violet/Buttons/ButtonA_Violet.glb b/Block-Fountain/models/buttons/Violet/Buttons/ButtonA_Violet.glb new file mode 100644 index 000000000..948e7e227 Binary files /dev/null and b/Block-Fountain/models/buttons/Violet/Buttons/ButtonA_Violet.glb differ diff --git a/Block-Fountain/models/buttons/Violet/Buttons/ButtonB_Violet.glb b/Block-Fountain/models/buttons/Violet/Buttons/ButtonB_Violet.glb new file mode 100644 index 000000000..d4ee336d4 Binary files /dev/null and b/Block-Fountain/models/buttons/Violet/Buttons/ButtonB_Violet.glb differ diff --git a/Block-Fountain/models/buttons/Violet/Buttons/ButtonC_Violet.glb b/Block-Fountain/models/buttons/Violet/Buttons/ButtonC_Violet.glb new file mode 100644 index 000000000..b9f54bbbb Binary files /dev/null and b/Block-Fountain/models/buttons/Violet/Buttons/ButtonC_Violet.glb differ diff --git a/Block-Fountain/models/buttons/Yellow/Base/BaseYellow.glb b/Block-Fountain/models/buttons/Yellow/Base/BaseYellow.glb new file mode 100644 index 000000000..f01698c46 Binary files /dev/null and b/Block-Fountain/models/buttons/Yellow/Base/BaseYellow.glb differ diff --git a/Block-Fountain/models/buttons/Yellow/Buttons/ButtonA_Yellow.glb b/Block-Fountain/models/buttons/Yellow/Buttons/ButtonA_Yellow.glb new file mode 100644 index 000000000..7f964c1d0 Binary files /dev/null and b/Block-Fountain/models/buttons/Yellow/Buttons/ButtonA_Yellow.glb differ diff --git a/Block-Fountain/models/buttons/Yellow/Buttons/ButtonB_Yellow.glb b/Block-Fountain/models/buttons/Yellow/Buttons/ButtonB_Yellow.glb new file mode 100644 index 000000000..25ec12f7f Binary files /dev/null and b/Block-Fountain/models/buttons/Yellow/Buttons/ButtonB_Yellow.glb differ diff --git a/Block-Fountain/models/buttons/Yellow/Buttons/ButtonC_Yellow.glb b/Block-Fountain/models/buttons/Yellow/Buttons/ButtonC_Yellow.glb new file mode 100644 index 000000000..fb8485d69 Binary files /dev/null and b/Block-Fountain/models/buttons/Yellow/Buttons/ButtonC_Yellow.glb differ diff --git a/Block-Fountain/models/fountain/Base.glb b/Block-Fountain/models/fountain/Base.glb new file mode 100644 index 000000000..b4b319da6 Binary files /dev/null and b/Block-Fountain/models/fountain/Base.glb differ diff --git a/Block-Fountain/models/fountain/FirstRing.glb b/Block-Fountain/models/fountain/FirstRing.glb new file mode 100644 index 000000000..19aa9d29c Binary files /dev/null and b/Block-Fountain/models/fountain/FirstRing.glb differ diff --git a/Block-Fountain/models/fountain/FourthRing.glb b/Block-Fountain/models/fountain/FourthRing.glb new file mode 100644 index 000000000..bfab926ca Binary files /dev/null and b/Block-Fountain/models/fountain/FourthRing.glb differ diff --git a/Block-Fountain/models/fountain/SecondRing.glb b/Block-Fountain/models/fountain/SecondRing.glb new file mode 100644 index 000000000..daef3a5ba Binary files /dev/null and b/Block-Fountain/models/fountain/SecondRing.glb differ diff --git a/Block-Fountain/models/fountain/ThirdRing.glb b/Block-Fountain/models/fountain/ThirdRing.glb new file mode 100644 index 000000000..473937548 Binary files /dev/null and b/Block-Fountain/models/fountain/ThirdRing.glb differ diff --git a/Block-Fountain/package.json b/Block-Fountain/package.json new file mode 100644 index 000000000..91f268d84 --- /dev/null +++ b/Block-Fountain/package.json @@ -0,0 +1,32 @@ +{ + "name": "block-fountain-sdk7", + "description": "Block Fountain synced multiplayer SDK7 scene", + "version": "1.0.0", + "bundleDependencies": [ + "@dcl-sdk/utils" + ], + "dependencies": { + "@dcl-sdk/utils": "next" + }, + "devDependencies": { + "@dcl/js-runtime": "next", + "@dcl/sdk": "next" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=6.0.0" + }, + "prettier": { + "semi": false, + "singleQuote": true, + "printWidth": 120, + "trailingComma": "none" + }, + "scripts": { + "build": "sdk-commands build", + "deploy": "sdk-commands deploy", + "start": "sdk-commands start", + "upgrade-sdk": "npm install --save-dev @dcl/sdk@latest", + "upgrade-sdk:next": "npm install --save-dev @dcl/sdk@next" + } +} diff --git a/Block-Fountain/scene-thumbnail.png b/Block-Fountain/scene-thumbnail.png new file mode 100644 index 000000000..36e10d0aa Binary files /dev/null and b/Block-Fountain/scene-thumbnail.png differ diff --git a/Block-Fountain/scene.json b/Block-Fountain/scene.json new file mode 100644 index 000000000..85fee9a26 --- /dev/null +++ b/Block-Fountain/scene.json @@ -0,0 +1,63 @@ +{ + "ecs7": true, + "runtimeVersion": "7", + "display": { + "title": "Block-Fountain", + "description": "Block Fountain synced multiplayer (P2P) SDK7 scene", + "navmapThumbnail": "images/scene-thumbnail.png", + "favicon": "favicon_asset" + }, + "owner": "", + "contact": { + "name": "SDK", + "email": "" + }, + "main": "bin/index.js", + "tags": [], + "scene": { + "parcels": [ + "77,3", + "77,4", + "77,5", + "78,3", + "78,4", + "78,5", + "79,3", + "79,4", + "79,5" + ], + "base": "77,3" + }, + "communications": { + "type": "webrtc", + "signalling": "https://signalling-01.decentraland.org" + }, + "spawnPoints": [ + { + "name": "spawn1", + "default": true, + "position": { + "x": [ + 0, + 3 + ], + "y": [ + 0, + 0 + ], + "z": [ + 0, + 3 + ] + }, + "cameraTarget": { + "x": 8, + "y": 1, + "z": 8 + } + } + ], + "requiredPermissions": [], + "featureToggles": {}, + "name": "Block-Fountain" +} \ No newline at end of file diff --git a/Block-Fountain/sounds/click.mp3 b/Block-Fountain/sounds/click.mp3 new file mode 100644 index 000000000..7e7d8fb79 Binary files /dev/null and b/Block-Fountain/sounds/click.mp3 differ diff --git a/Block-Fountain/src/button.ts b/Block-Fountain/src/button.ts new file mode 100644 index 000000000..484ec2ad6 --- /dev/null +++ b/Block-Fountain/src/button.ts @@ -0,0 +1,176 @@ +import { + engine, + Animator, + Transform, + Entity, + GltfContainer, + AudioSource, + CameraType, + QuaternionType +} from '@dcl/sdk/ecs' +import { Quaternion, Vector3 } from '@dcl/sdk/math' +import { ECS6ComponentAudioSource } from '~system/EngineApi' + +export class ButtonClickComponent { + constructor(public clickAnim: string, public audioSource: string) {} +} + +export class Button { + private buttonClickComponent: ButtonClickComponent + public buttonEntity: Entity + + constructor( + model: string, + position: Vector3, + rotation: Vector3, + scale: Vector3, + audioClipUrl: string, + animationName: string, + parent?: Entity + ) { + // Create new button entity + const button = engine.addEntity() + + // Calculate button's rotation in Euler degrees + const eulerRotationButton = Quaternion.fromEulerDegrees(rotation.x, rotation.y, rotation.z) + + // Add 3D model to button + GltfContainer.create(button, { + src: model + }) + + // Add transform to button + Transform.createOrReplace(button, { + position: position, + rotation: eulerRotationButton, + scale: scale + }) + + // If there's a parent entity, attach the button to it + if (parent) { + Transform.createOrReplace(button, { + position: position, + rotation: eulerRotationButton, + scale: scale, + parent: parent + }) + } + + // Add audio source to button + AudioSource.create(button, { + audioClipUrl: 'sounds/click.mp3', + loop: false, + playing: false + }) + + // Add animator to button + Animator.create(button, { + states: [ + { + clip: animationName, + playing: false, + loop: false + } + ] + }) + + // Initialize the button's properties + this.buttonEntity = button + this.buttonClickComponent = new ButtonClickComponent(animationName, audioClipUrl) + } + + public press(): void { + // Play the button animation + Animator.playSingleAnimation(this.buttonEntity, this.buttonClickComponent.clickAnim) + + // Fetch the button's audio source and play it + const buttonSource = AudioSource.getMutable(this.buttonEntity) + buttonSource.playing = true + } +} + +export class Switch { + private onAnimState: string + private offAnimState: string + private audioSource: ECS6ComponentAudioSource + private switchEntity: Entity + + constructor( + model: string, + position: Vector3, + rotation: Vector3, + onAnim: string, + offAnim: string, + audioClipUrl: string, + parent?: Entity + ) { + // Create new switch entity + this.switchEntity = engine.addEntity() + + // Calculate switch rotation in Euler degrees + const eulerRotationSwitch = Quaternion.fromEulerDegrees(rotation.x, rotation.y, rotation.z) + + // Add a 3D model to the switch + GltfContainer.create(this.switchEntity, { + src: model + }) + + // Add a transform to the switch + Transform.createOrReplace(this.switchEntity, { + position: position, + rotation: eulerRotationSwitch + }) + + // If there's a parent entity, attach the switch to it + if (parent) { + Transform.createOrReplace(this.switchEntity, { + position: position, + rotation: eulerRotationSwitch, + parent: parent + }) + } + + // Create audio source for switch + this.audioSource = AudioSource.create(this.switchEntity, { + audioClipUrl: 'sounds/click.mp3', + loop: false, + playing: false + }) + + // Create animator for the switch with two states + Animator.createOrReplace(this.switchEntity, { + states: [ + { + clip: onAnim, + playing: false, + loop: false + }, + { + clip: offAnim, + playing: false, + loop: false + } + ] + }) + + // Initialize the switch's properties + this.onAnimState = onAnim + this.offAnimState = offAnim + } + + public toggle(value: boolean): void { + // Determine which animation to play based on value + const animationClipName = value ? 'onAnim' : 'offAnim' + + // Stop any currently playing animation + Animator.stopAllAnimations(this.switchEntity) + + // Play the selected animation + Animator.playSingleAnimation(this.switchEntity, animationClipName) + + // Start playing the audio source for the switch + const audioSource = this.audioSource + audioSource.playing = true + audioSource.loop = false + } +} diff --git a/Block-Fountain/src/console.ts b/Block-Fountain/src/console.ts new file mode 100644 index 000000000..32b0a463b --- /dev/null +++ b/Block-Fountain/src/console.ts @@ -0,0 +1,96 @@ +import { Button } from './button' +import { Entity, GltfContainer, Transform, engine, pointerEventsSystem, InputAction } from '@dcl/sdk/ecs' +import { Quaternion, Vector3 } from '@dcl/sdk/math' +import { MessageBus } from '@dcl/sdk/message-bus' + +export class Console { + private consoleEntity: Entity + + constructor( + position: Vector3, + rotation: Vector3, + scale: Vector3, + parent: Entity, + model: string, + targetRing: number, + button1Model: string, + button1Anim: string, + button2Model: string, + button2Anim: string, + button3Model: string, + button3Anim: string, + messagebus: MessageBus + ) { + // Create a new entity + this.consoleEntity = engine.addEntity() + + // Create a quaternion rotation from Euler angles + const eulerRotation = Quaternion.fromEulerDegrees(rotation.x, rotation.y, rotation.z) + + // Set the entity's transform and parent + Transform.create(this.consoleEntity, { + position: position, + rotation: eulerRotation, + scale: scale, + parent: parent + }) + + // Add a 3D model to the entity + GltfContainer.create(this.consoleEntity, { + src: model + }) + + const audioClipUrl = 'sounds/click.mp3' + + // Create three button instances + const button1 = new Button(button1Model, position, rotation, scale, audioClipUrl, button1Anim, parent) + const button2 = new Button(button2Model, position, rotation, scale, audioClipUrl, button2Anim, parent) + const button3 = new Button(button3Model, position, rotation, scale, audioClipUrl, button3Anim, parent) + + // Set up pointer events for each button instance + pointerEventsSystem.onPointerDown( + { + entity: button1.buttonEntity, + opts: { + button: InputAction.IA_POINTER, + hoverText: 'Click' + } + }, + function () { + console.log('clicked entity') + button1.press() + messagebus.emit('fountainAnim', { ring: targetRing, anim: 1 }) + } + ) + + pointerEventsSystem.onPointerDown( + { + entity: button2.buttonEntity, + opts: { + button: InputAction.IA_POINTER, + hoverText: 'Click' + } + }, + function () { + console.log('clicked entity') + button2.press() + messagebus.emit('fountainAnim', { ring: targetRing, anim: 2 }) + } + ) + + pointerEventsSystem.onPointerDown( + { + entity: button3.buttonEntity, + opts: { + button: InputAction.IA_POINTER, + hoverText: 'Click' + } + }, + function () { + console.log('clicked entity') + button3.press() + messagebus.emit('fountainAnim', { ring: targetRing, anim: 3 }) + } + ) + } +} diff --git a/Block-Fountain/src/index.ts b/Block-Fountain/src/index.ts new file mode 100644 index 000000000..8c9ece8b2 --- /dev/null +++ b/Block-Fountain/src/index.ts @@ -0,0 +1,168 @@ +import { engine, GltfContainer, Transform } from '@dcl/sdk/ecs' +import { Vector3 } from '@dcl/sdk/math' +import { MessageBus } from '@dcl/sdk/message-bus' +import { Ring } from './ring' +import { Console } from './console' +import { RandomFountain } from './randomizer' +import * as utils from '@dcl-sdk/utils' + +export function main() { + // Create a message bus to sync animations between players + const sceneMessageBus = new MessageBus() + const rings: Ring[] = [] + + // Create the base entity for the fountain + const base = engine.addEntity() + GltfContainer.create(base, { + src: 'models/fountain/Base.glb' + }) + Transform.create(base, { + position: Vector3.create(24, 0, 24) + }) + + // Create and initialize rings + const ring1 = new Ring( + Vector3.create(0, -0.55, 0), + Vector3.create(0, 0, 0), + Vector3.create(1, 1, 1), + 'models/fountain/FirstRing.glb', + '1stRing_Action_01', + '1stRing_Action_02', + '1stRing_Action_03', + base + ) + + rings.push(ring1) + + const ring2 = new Ring( + Vector3.create(0, -0.6, 0), + Vector3.create(0, 0, 0), + Vector3.create(1, 1, 1), + 'models/fountain/SecondRing.glb', + '2ndRing_Action_01', + '2ndRing_Action_02', + '2ndRing_Action_03', + base + ) + + rings.push(ring2) + + const ring3 = new Ring( + Vector3.create(0, -0.8, 0), + Vector3.create(0, 0, 0), + Vector3.create(1, 1, 1), + 'models/fountain/ThirdRing.glb', + '3rdRing_Action_01', + '3rdRing_Action_02', + '3rdRing_Action_03', + base + ) + + rings.push(ring3) + + const ring4 = new Ring( + Vector3.create(0, -0.8, 0), + Vector3.create(0, 0, 0), + Vector3.create(1, 1, 1), + 'models/fountain/FourthRing.glb', + '4thRing_Action_01', + '4thRing_Action_02', + '4thRing_Action_03', + base + ) + + rings.push(ring4) + + // Create the consoles with interactive buttons + const cyanConsole = new Console( + Vector3.create(-23, 0, 0), + Vector3.create(0, 0, 0), + Vector3.create(1, 1, 1), + base, + 'models/buttons/Cyan/Base/BaseCyan.glb', + 3, + 'models/buttons/Cyan/Buttons/ButtonA_Cyan.glb', + 'ButtonA_Action', + 'models/buttons/Cyan/Buttons/ButtonB_Cyan.glb', + 'ButtonB_Action', + 'models/buttons/Cyan/Buttons/ButtonC_Cyan.glb', + 'ButtonC_Action', + sceneMessageBus + ) + + const redConsole = new Console( + Vector3.create(0, 0, 23), + Vector3.create(0, 90, 0), + Vector3.create(1, 1, 1), + base, + 'models/buttons/Red/Base/BaseRed.glb', + 2, + 'models/buttons/Red/Buttons/ButtonA_Red.glb', + 'ButtonA_Action', + 'models/buttons/Red/Buttons/ButtonB_Red.glb', + 'ButtonB_Action', + 'models/buttons/Red/Buttons/ButtonC_Red.glb', + 'ButtonC_Action', + sceneMessageBus + ) + + const violetConsole = new Console( + Vector3.create(23, 0, 0), + Vector3.create(0, 180, 0), + Vector3.create(1, 1, 1), + base, + 'models/buttons/Violet/Base/BaseViolet.glb', + 1, + 'models/buttons/Violet/Buttons/ButtonA_Violet.glb', + 'ButtonA_Action', + 'models/buttons/Violet/Buttons/ButtonB_Violet.glb', + 'ButtonB_Action', + 'models/buttons/Violet/Buttons/ButtonC_Violet.glb', + 'ButtonC_Action', + sceneMessageBus + ) + + const yellowConsole = new Console( + Vector3.create(0, 0, -23), + Vector3.create(0, 270, 0), + Vector3.create(1, 1, 1), + base, + 'models/buttons/Yellow/Base/BaseYellow.glb', + 0, + 'models/buttons/Yellow/Buttons/ButtonA_Yellow.glb', + 'ButtonA_Action', + 'models/buttons/Yellow/Buttons/ButtonB_Yellow.glb', + 'ButtonB_Action', + 'models/buttons/Yellow/Buttons/ButtonC_Yellow.glb', + 'ButtonC_Action', + sceneMessageBus + ) + + // Handle fountain animation events + sceneMessageBus.on('fountainAnim', (e) => { + fountainPlayer.playingMode = 0 + utils.timers.setTimeout(() => { + fountainPlayer.playingMode = 1 + }, 20000) + + // Trigger ring animations according to events + switch (e.anim) { + case 1: + rings[e.ring].play1() + break + case 2: + rings[e.ring].play2() + break + case 3: + rings[e.ring].play3() + break + } + }) + + /// RANDOMIZER + // Create and add fountain animation system + const fountainPlayer = new RandomFountain(rings, 10) + engine.addSystem((dt) => { + fountainPlayer.update(dt) + }) +} diff --git a/Block-Fountain/src/randomizer.ts b/Block-Fountain/src/randomizer.ts new file mode 100644 index 000000000..576976ca2 --- /dev/null +++ b/Block-Fountain/src/randomizer.ts @@ -0,0 +1,175 @@ +import { Ring } from './ring' + +export class RandomFountain { + // Boolean flags to track the active state of each ring + ringOneActive: boolean = false + ringTwoActive: boolean = false + ringThreeActive: boolean = false + ringFourActive: boolean = false + + // Duration of the animation + animDuration: number + + // Timers for each ring's animation and the main timer + timer1: number + timer2: number + timer3: number + timer4: number + mainTimer: number + + // Playing mode (0 for free control, 1 for random mode) + playingMode: number = 0 + + // Array to hold references to Ring objects + rings: Ring[] + + constructor(rings: Ring[], animDuration: number) { + // Initialize class properties + this.animDuration = animDuration + this.timer1 = 0 + this.timer2 = 0 + this.timer3 = 0 + this.timer4 = 0 + this.mainTimer = 0 + this.playingMode = 1 + this.rings = rings + } + update(dt: number) { + if (this.playingMode === 0) { + // in free control mode + return + } + + if (this.playingMode === 1) { + // random mode + + if (this.ringOneActive) { + this.timer1 -= dt + if (this.timer1 < 0) { + this.ringOneActive = false + } + } + if (this.ringTwoActive) { + this.timer2 -= dt + if (this.timer2 < 0) { + this.ringTwoActive = false + } + } + if (this.ringThreeActive) { + this.timer3 -= dt + if (this.timer3 < 0) { + this.ringThreeActive = false + } + } + + if (this.ringFourActive) { + this.timer4 -= dt + if (this.timer4 < 0) { + this.ringFourActive = false + } + } + + this.mainTimer += dt + + if (this.mainTimer > this.animDuration / 2) { + const randomIndex = Math.floor(Math.random() * 1500) + switch (randomIndex) { + case 1: + if (this.ringOneActive) break + this.rings[0].play1() + this.ringOneActive = true + this.timer1 = this.animDuration + this.mainTimer = 0 + break + + case 2: + if (this.ringOneActive) break + this.rings[0].play2() + this.ringOneActive = true + this.timer1 = this.animDuration + this.mainTimer = 0 + break + + case 3: + if (this.ringOneActive) break + this.rings[0].play3() + this.ringOneActive = true + this.timer1 = this.animDuration + this.mainTimer = 0 + break + + case 4: + if (this.ringTwoActive) break + this.rings[1].play1() + this.ringTwoActive = true + this.timer2 = this.animDuration + this.mainTimer = 0 + break + + case 5: + if (this.ringTwoActive) break + this.rings[1].play2() + this.ringTwoActive = true + this.timer2 = this.animDuration + this.mainTimer = 0 + break + + case 6: + if (this.ringTwoActive) break + this.rings[1].play3() + this.ringTwoActive = true + this.timer2 = this.animDuration + this.mainTimer = 0 + break + case 7: + if (this.ringThreeActive) break + this.rings[2].play1() + this.ringThreeActive = true + this.timer3 = this.animDuration + this.mainTimer = 0 + break + + case 8: + if (this.ringThreeActive) break + this.rings[2].play2() + this.ringThreeActive = true + this.timer3 = this.animDuration + this.mainTimer = 0 + break + + case 9: + if (this.ringThreeActive) break + this.rings[2].play3() + this.ringThreeActive = true + this.timer3 = this.animDuration + this.mainTimer = 0 + break + + case 10: + if (this.ringFourActive) break + this.rings[3].play1() + this.ringFourActive = true + this.timer4 = this.animDuration + this.mainTimer = 0 + break + + case 11: + if (this.ringFourActive) break + this.rings[3].play2() + this.ringFourActive = true + this.timer4 = this.animDuration + this.mainTimer = 0 + break + + case 12: + if (this.ringFourActive) break + this.rings[3].play3() + this.ringFourActive = true + this.timer4 = this.animDuration + this.mainTimer = 0 + break + } + } + } + } +} diff --git a/Block-Fountain/src/ring.ts b/Block-Fountain/src/ring.ts new file mode 100644 index 000000000..592fd3dcb --- /dev/null +++ b/Block-Fountain/src/ring.ts @@ -0,0 +1,81 @@ +import { Entity, engine, Transform, GltfContainer, Animator } from '@dcl/sdk/ecs' +import { Quaternion, Vector3 } from '@dcl/sdk/math' + +export class Ring { + private ringEntity: Entity + animation1: string + animation2: string + animation3: string + + constructor( + position: Vector3, + rotation: Vector3, + scale: Vector3, + model: string, + animation1: string, + animation2: string, + animation3: string, + parent: Entity + ) { + // Create an entity for the ring + this.ringEntity = engine.addEntity() + + // Calculate entity rotation in Euler degrees + const eulerRotationRing = Quaternion.fromEulerDegrees(rotation.x, rotation.y, rotation.z) + + // Add a 3D model to the entity + GltfContainer.create(this.ringEntity, { + src: model + }) + + // Add a transform and parent + Transform.create(this.ringEntity, { + position: position, + rotation: eulerRotationRing, + scale: scale, + parent: parent + }) + + // Initialize animation names + this.animation1 = animation1 + this.animation2 = animation2 + this.animation3 = animation3 + + // Create an animator for the ring with multiple animation states + Animator.create(this.ringEntity, { + states: [ + { + clip: animation1, + playing: false, + loop: false + }, + { + clip: animation2, + playing: false, + loop: false + }, + { + clip: animation3, + playing: false, + loop: false + } + ] + }) + } + + public play1(): void { + // Play the first animation + Animator.playSingleAnimation(this.ringEntity, this.animation1) + console.log('Playing Animation 1') + } + public play2(): void { + // Play the second animation + Animator.playSingleAnimation(this.ringEntity, this.animation2) + console.log('Playing Animation 2') + } + public play3(): void { + // Play the third animation + Animator.playSingleAnimation(this.ringEntity, this.animation3) + console.log('Playing Animation 3') + } +} diff --git a/Block-Fountain/src/utils.ts b/Block-Fountain/src/utils.ts new file mode 100644 index 000000000..e69de29bb diff --git a/Block-Fountain/tsconfig.json b/Block-Fountain/tsconfig.json new file mode 100644 index 000000000..d84fa280f --- /dev/null +++ b/Block-Fountain/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "allowJs": true, + "strict": true + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx" + ], + "extends": "@dcl/sdk/types/tsconfig.ecs7.json" +} \ No newline at end of file diff --git a/clap-meter/README.md b/clap-meter/README.md new file mode 100644 index 000000000..a25bdb97e --- /dev/null +++ b/clap-meter/README.md @@ -0,0 +1,130 @@ +# Clap Meter + +A scene featuring a clap meter that responds to player claps with synchronized animations. + +![demo](https://github.com/decentraland-scenes/clap-meter/blob/main/screenshots/clap-meter.gif) + +## Overview + +The Clap Meter scene showcases synchronized animations triggered by player claps. The project demonstrates the usage of P2P communication to sync events between players, handling multiple animations in a single model, maintaining organized and abstracted code, and managing random events. + +## Try it out + +### Setting Up Your Environment + +To start developing for Decentraland, follow [these instructions](https://docs.decentraland.org/creator/development-guide/sdk7/installation-guide/). + +### Previewing the Scene + +You can run the scene by following [these instructions](https://docs.decentraland.org/creator/development-guide/sdk7/preview-scene/). + +### Scene Interaction + +The clap meter has a needle that responds to claps. It features gradual needle movement based on claps and a cooldown period. The scene adapts to different camera views and provides an engaging experience for players. + +## Development + +Feel free to explore the code and make modifications to the scene. If you have suggestions or improvements, contributions are welcome. + +## Usage + +### Handling Clap Interactions + +To integrate similar functionality into your Decentraland projects, follow these guidelines: + +#### Creating Clap Meter + +Instantiate a `ClapMeter` object to manage the clap meter functionality: + +```typescript +import { ClapMeter } from './clapMeter'; +import { Vector3 } from '@dcl/sdk/math'; + +const clapMeter = new ClapMeter( + new Vector3(x, y, z), // Position + new Vector3(0, 0, 0), // Rotation + new Vector3(1, 1, 1), // Scale + undefined // Parent entity +); +``` + +#### Handling Clap Events + +Listen for clap events and trigger corresponding actions: + +```typescript +import { onPlayerExpressionObservable } from '@dcl/sdk/observables'; + + onPlayerExpressionObservable.add(({ expressionId }) => { + if (expressionId == "clap") { + console.log('clap detected') + isClapping = true; + sceneMessageBus.emit("updateClapMeter", {}); + + // Set a timer to reset isClapping after a certain duration + clapCooldownTimer = utils.timers.setTimeout(() => { + isClapping = false; + clapCooldownTimer = 0; + + // Update needle when the timer expires + sceneMessageBus.emit("updateClapMeter", {}); + }, COOLDOWN_TIME); + } + }); +``` + +### Customizing the Scene + +Adjusting Cooldown and Needle Movement. +Modify the `updateNeedle` method in the `ClapMeter` class to fine-tune the cooldown and needle movement logic: + +```typescript + public updateNeedle: SystemFn = (dt: number) => { + const clapsNeeded = 4; // Number of claps needed to reach the end, higher number = more difficult / lower number = easier + + if (this.cooldownRemaining > 0) { + this.currentNeedleRotation += COOLDOWN_INCREMENT; + + // Clamp the needle rotation to the start angle + if (this.currentNeedleRotation >= START_ANGLE) { + this.currentNeedleRotation = START_ANGLE; + } + + // Decrease remaining cooldown time + this.cooldownRemaining -= dt; + + + if (this.cooldownRemaining <= 0) { + // Cooldown is over, reset the needle + this.cooldownRemaining = 0; + clapMeterFull = false; + } + } + + else if (isClapping && this.currentNeedleRotation > END_ANGLE) { + // If clapping and the needle is not at the end, advance the needle + const angleIncrement = ANGLE_INCREMENT / clapsNeeded; + + this.currentNeedleRotation -= angleIncrement; + + // Clamp the needle rotation to the end angle + if (this.currentNeedleRotation <= END_ANGLE) { + this.currentNeedleRotation = END_ANGLE; + clapMeterFull = true; + + + } + } else if (this.currentNeedleRotation < START_ANGLE) { + // If not clapping and the needle is not at the start, return the needle to start + this.currentNeedleRotation += COOLDOWN_INCREMENT / clapsNeeded; + + // Clamp the needle rotation to the start angle + if (this.currentNeedleRotation >= START_ANGLE) { + this.currentNeedleRotation = START_ANGLE; + } + } +``` + +## Copyright info + +This scene is protected with a standard Apache 2 licence. See the terms and conditions in the [LICENSE](/LICENSE) file. diff --git a/clap-meter/assets/scene/main.composite b/clap-meter/assets/scene/main.composite new file mode 100644 index 000000000..ce07ea3c4 --- /dev/null +++ b/clap-meter/assets/scene/main.composite @@ -0,0 +1,253 @@ +{ + "version": 1, + "components": [ + { + "name": "core::Transform", + "jsonSchema": { + "type": "object", + "properties": { + "position": { + "type": "object", + "properties": { + "x": { + "type": "number" + }, + "y": { + "type": "number" + }, + "z": { + "type": "number" + } + } + }, + "scale": { + "type": "object", + "properties": { + "x": { + "type": "number" + }, + "y": { + "type": "number" + }, + "z": { + "type": "number" + } + } + }, + "rotation": { + "type": "object", + "properties": { + "x": { + "type": "number" + }, + "y": { + "type": "number" + }, + "z": { + "type": "number" + }, + "w": { + "type": "number" + } + } + }, + "parent": { + "type": "integer" + } + }, + "serializationType": "transform" + }, + "data": { + "512": { + "json": { + "position": { + "x": 8, + "y": 1, + "z": 8 + }, + "scale": { + "x": 1, + "y": 1, + "z": 1 + }, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "parent": 0 + } + } + } + }, + { + "name": "core::MeshRenderer", + "jsonSchema": { + "type": "object", + "properties": {}, + "serializationType": "protocol-buffer", + "protocolBuffer": "PBMeshRenderer" + }, + "data": { + "512": { + "json": { + "mesh": { + "$case": "box", + "box": { + "uvs": [] + } + } + } + } + } + }, + { + "name": "core-schema::Name", + "jsonSchema": { + "type": "object", + "properties": { + "value": { + "type": "string", + "serializationType": "utf8-string" + } + }, + "serializationType": "map" + }, + "data": { + "512": { + "json": { + "value": "Magic Cube" + } + } + } + }, + { + "name": "inspector::Scene", + "jsonSchema": { + "type": "object", + "properties": { + "layout": { + "type": "object", + "properties": { + "base": { + "type": "object", + "properties": { + "x": { + "type": "integer", + "serializationType": "int32" + }, + "y": { + "type": "integer", + "serializationType": "int32" + } + }, + "serializationType": "map" + }, + "parcels": { + "type": "array", + "items": { + "type": "object", + "properties": { + "x": { + "type": "integer", + "serializationType": "int32" + }, + "y": { + "type": "integer", + "serializationType": "int32" + } + }, + "serializationType": "map" + }, + "serializationType": "array" + } + }, + "serializationType": "map" + } + }, + "serializationType": "map" + }, + "data": { + "0": { + "json": { + "layout": { + "base": { + "x": 0, + "y": 0 + }, + "parcels": [ + { + "x": 0, + "y": 0 + } + ] + } + } + } + } + }, + { + "name": "inspector::Nodes", + "jsonSchema": { + "type": "object", + "properties": { + "value": { + "type": "array", + "items": { + "type": "object", + "properties": { + "entity": { + "type": "integer", + "serializationType": "entity" + }, + "children": { + "type": "array", + "items": { + "type": "integer", + "serializationType": "entity" + }, + "serializationType": "array" + } + }, + "serializationType": "map" + }, + "serializationType": "array" + } + }, + "serializationType": "map" + }, + "data": { + "0": { + "json": { + "value": [ + { + "entity": 0, + "children": [ + 512 + ] + }, + { + "entity": 512, + "children": [] + } + ] + } + } + } + }, + { + "name": "cube-id", + "jsonSchema": { + "type": "object", + "properties": {}, + "serializationType": "map" + }, + "data": { + "512": { + "json": {} + } + } + } + ] +} \ No newline at end of file diff --git a/clap-meter/images/scene-thumbnail.png b/clap-meter/images/scene-thumbnail.png new file mode 100644 index 000000000..b3dc58066 Binary files /dev/null and b/clap-meter/images/scene-thumbnail.png differ diff --git a/clap-meter/models/Genesis_TX.png b/clap-meter/models/Genesis_TX.png new file mode 100644 index 000000000..0cf38f447 Binary files /dev/null and b/clap-meter/models/Genesis_TX.png differ diff --git a/clap-meter/models/baseDarkWithCollider.glb b/clap-meter/models/baseDarkWithCollider.glb new file mode 100644 index 000000000..fa2e31eaf Binary files /dev/null and b/clap-meter/models/baseDarkWithCollider.glb differ diff --git a/clap-meter/models/clapMeterBoard.glb b/clap-meter/models/clapMeterBoard.glb new file mode 100644 index 000000000..9774dfaa2 Binary files /dev/null and b/clap-meter/models/clapMeterBoard.glb differ diff --git a/clap-meter/models/clapMeterNeedle.glb b/clap-meter/models/clapMeterNeedle.glb new file mode 100644 index 000000000..9b6a87805 Binary files /dev/null and b/clap-meter/models/clapMeterNeedle.glb differ diff --git a/clap-meter/package.json b/clap-meter/package.json new file mode 100644 index 000000000..339ad5113 --- /dev/null +++ b/clap-meter/package.json @@ -0,0 +1,26 @@ +{ + "name": "clap-meter", + "description": "Clap Meter multiplayer P2P SDK7", + "version": "1.0.0", + "bundleDependencies": [ + "@dcl-sdk/utils" + ], + "dependencies": { + "@dcl-sdk/utils": "next" + }, + "devDependencies": { + "@dcl/js-runtime": "next", + "@dcl/sdk": "next" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=6.0.0" + }, + "scripts": { + "build": "sdk-commands build", + "deploy": "sdk-commands deploy", + "start": "sdk-commands start", + "upgrade-sdk": "npm install --save-dev @dcl/sdk@latest", + "upgrade-sdk:next": "npm install --save-dev @dcl/sdk@next" + } +} diff --git a/clap-meter/scene.json b/clap-meter/scene.json new file mode 100644 index 000000000..ce4063fac --- /dev/null +++ b/clap-meter/scene.json @@ -0,0 +1,58 @@ +{ + "ecs7": true, + "runtimeVersion": "7", + "display": { + "title": "clap-meter", + "description": "Clap Meter multiplayer P2P SDK7", + "navmapThumbnail": "images/scene-thumbnail.png", + "favicon": "favicon_asset" + }, + "owner": "", + "contact": { + "name": "SDK", + "email": "" + }, + "main": "bin/index.js", + "tags": [], + "scene": { + "parcels": [ + "82,0" + ], + "base": "82,0" + }, + "communications": { + "type": "webrtc", + "signalling": "https://signalling-01.decentraland.org" + }, + "spawnPoints": [ + { + "name": "spawn1", + "default": true, + "position": { + "x": [ + 0, + 3 + ], + "y": [ + 0, + 0 + ], + "z": [ + 0, + 3 + ] + }, + "cameraTarget": { + "x": 8, + "y": 1, + "z": 8 + } + } + ], + "requiredPermissions": [ + "ALLOW_TO_TRIGGER_AVATAR_EMOTE", + "ALLOW_TO_MOVE_PLAYER_INSIDE_SCENE" + ], + "featureToggles": {}, + "name": "clap-meter" +} \ No newline at end of file diff --git a/clap-meter/screenshots/clap-meter.gif b/clap-meter/screenshots/clap-meter.gif new file mode 100644 index 000000000..23378a8d9 Binary files /dev/null and b/clap-meter/screenshots/clap-meter.gif differ diff --git a/clap-meter/src/clapMeter.ts b/clap-meter/src/clapMeter.ts new file mode 100644 index 000000000..00408d3f3 --- /dev/null +++ b/clap-meter/src/clapMeter.ts @@ -0,0 +1,104 @@ +import { engine, Entity, GltfContainer, Transform, SystemFn, UiText } from '@dcl/sdk/ecs' +import { Quaternion, Vector3 } from '@dcl/sdk/math' +import * as utils from '@dcl-sdk/utils' +import { isClapping } from '.' + +// Config +export const START_ANGLE = 350 +const END_ANGLE = 190 +const ANGLE_INCREMENT = 1 // How many degrees the needle moves forwards +const COOLDOWN_INCREMENT = 0.1 // How many degrees the needle moves backwards +export const COOLDOWN_TIME = 6000 // Cooldown time in milliseconds + +// Create clap meter entities +export const clapMeterNeedle = engine.addEntity() +export const clapMeterBoard = engine.addEntity() + +// Export clapMeterFull variable +export let clapMeterFull: boolean = false + +export class ClapMeter { + private currentNeedleRotation: number = START_ANGLE + private cooldownRemaining: number = 0 + + constructor(position: Vector3, rotation: Vector3, scale: Vector3, parent?: Entity) { + // Add 3D model to clap meter board + GltfContainer.create(clapMeterBoard, { + src: 'models/clapMeterBoard.glb' + }) + + // Calculate rotation in Euler degrees + const eulerRotation = Quaternion.fromEulerDegrees(rotation.x, rotation.y, rotation.z) + + // Add transform to clap meter board + Transform.create(clapMeterBoard, { + position: position, + rotation: eulerRotation, + scale: scale, + parent: parent + }) + + // Add 3D model to clap meter needle + GltfContainer.create(clapMeterNeedle, { + src: 'models/clapMeterNeedle.glb' + }) + + // Set needle rotation to start angle + Transform.create(clapMeterNeedle, { + position: Vector3.create(0, 0.05, 0), + rotation: Quaternion.fromEulerDegrees(0, 0, START_ANGLE), + parent: clapMeterBoard + }) + + // Register the updateNeedle system to the engine + engine.addSystem(this.updateNeedle) + } + + public updateNeedle: SystemFn = (dt: number) => { + const clapsNeeded = 4 // Number of claps needed to reach the end, higher number = more difficult / lower number = easier + + if (this.cooldownRemaining > 0) { + this.currentNeedleRotation += COOLDOWN_INCREMENT + + // Clamp the needle rotation to the start angle + if (this.currentNeedleRotation >= START_ANGLE) { + this.currentNeedleRotation = START_ANGLE + } + + // Decrease remaining cooldown time + this.cooldownRemaining -= dt + + if (this.cooldownRemaining <= 0) { + // Cooldown is over, reset the needle + this.cooldownRemaining = 0 + clapMeterFull = false + } + } else if (isClapping && this.currentNeedleRotation > END_ANGLE) { + // If clapping and the needle is not at the end, advance the needle + const angleIncrement = ANGLE_INCREMENT / clapsNeeded + + this.currentNeedleRotation -= angleIncrement + + // Clamp the needle rotation to the end angle + if (this.currentNeedleRotation <= END_ANGLE) { + this.currentNeedleRotation = END_ANGLE + clapMeterFull = true + } + } else if (this.currentNeedleRotation < START_ANGLE) { + // If not clapping and the needle is not at the start, return the needle to start + this.currentNeedleRotation += COOLDOWN_INCREMENT / clapsNeeded + + // Clamp the needle rotation to the start angle + if (this.currentNeedleRotation >= START_ANGLE) { + this.currentNeedleRotation = START_ANGLE + } + } + + // Update the needle's rotation + Transform.createOrReplace(clapMeterNeedle, { + position: Vector3.create(0, 0.05, 0), + rotation: Quaternion.fromEulerDegrees(0, 0, this.currentNeedleRotation), + parent: clapMeterBoard + }) + } +} diff --git a/clap-meter/src/index.ts b/clap-meter/src/index.ts new file mode 100644 index 000000000..7cb57eff7 --- /dev/null +++ b/clap-meter/src/index.ts @@ -0,0 +1,84 @@ +import { engine, GltfContainer, CameraMode, CameraType, Transform } from '@dcl/sdk/ecs' +import { Vector3 } from '@dcl/sdk/math' +import { MessageBus } from '@dcl/sdk/message-bus' +import { onPlayerExpressionObservable } from '@dcl/sdk/observables' +import { isMenuVisible, onClapMeterFull, setupUi, toggleMenuVisibility } from './ui' +import { ClapMeter, START_ANGLE, clapMeterFull, clapMeterNeedle, clapMeterBoard, COOLDOWN_TIME } from './clapMeter' +import * as utils from '@dcl-sdk/utils' + +export let isClapping: boolean = false + +export function main() { + // draw UI + setupUi() + + // Multiplayer (p2p) + const sceneMessageBus = new MessageBus() + + // Setup scene + const base = engine.addEntity() + GltfContainer.create(base, { + src: 'models/baseDarkWithCollider.glb' + }) + + const clapMeter = new ClapMeter( + Vector3.create(8, 0.5, 8), // Position + Vector3.create(0, 0, 0), // Rotation in Euler degrees + Vector3.create(1, 1, 1), // Scale + undefined // Parent entity + ) + + // Use a timer to control the cooldown + let clapCooldownTimer: ReturnType | null = null + + // Listen for claps + onPlayerExpressionObservable.add(({ expressionId }) => { + if (expressionId == 'clap') { + console.log('clap detected') + isClapping = true + sceneMessageBus.emit('updateClapMeter', {}) + + // Set a timer to reset isClapping after a certain duration + clapCooldownTimer = utils.timers.setTimeout(() => { + isClapping = false + clapCooldownTimer = 0 + + // Update needle when the timer expires + sceneMessageBus.emit('updateClapMeter', {}) + }, COOLDOWN_TIME) + } + }) + + // Update the clap meter for all players + sceneMessageBus.on('updateClapMeter', () => { + clapMeter.updateNeedle(10) + console.log('updated message bus') + + if (clapMeterFull) { + // Trigger an action when the clap meter is full + onClapMeterFull() + } + }) + + function checkCameraMode() { + if (!engine.CameraEntity) return + + let cameraEntity = CameraMode.get(engine.CameraEntity) + + if (cameraEntity.mode == CameraType.CT_THIRD_PERSON) { + console.log('The player is using the 3rd person camera') + + if (isMenuVisible && !clapMeterFull) { + toggleMenuVisibility() // Hide the UI + } + } else { + console.log('The player is using the 1st person camera') + + if (!isMenuVisible && !clapMeterFull) { + toggleMenuVisibility() // Display the UI + } + } + } + + engine.addSystem(checkCameraMode) +} diff --git a/clap-meter/src/ui.tsx b/clap-meter/src/ui.tsx new file mode 100644 index 000000000..55fc86135 --- /dev/null +++ b/clap-meter/src/ui.tsx @@ -0,0 +1,72 @@ +import ReactEcs, { Label, ReactEcsRenderer, UiEntity } from '@dcl/sdk/react-ecs' +import { clapMeterFull } from './clapMeter' +import * as utils from '@dcl-sdk/utils' + +export var isMenuVisible: boolean = false; +var isClapMeterFull = clapMeterFull +let isClapMeterFullVisible: boolean | null = null; +var announcement: string = "For the best experience, \nswitch to 3rd person view by \npressing 'V' key." +var clapMeterFullAnnouncement: string = "The clap meter is full! Nice clapping!" + + +export function setupUi() { + ReactEcsRenderer.setUiRenderer(uiComponent) +} + +const uiComponent = () => ( + + {/* Existing announcement label */} + {isMenuVisible && ( + + + +); + +// Function to toggle the state of the menu +export function toggleMenuVisibility() { + isMenuVisible = !isMenuVisible +} +export function setClapMeterFull(full: boolean) { + isClapMeterFull = full; + + if (full) { + isClapMeterFullVisible = true; + + + } + +} + +export function onClapMeterFull() { + setClapMeterFull(true); + isMenuVisible = false + + utils.timers.setTimeout(() => { + setClapMeterFull(false); + isClapMeterFullVisible = false; + }, + 10000 // Remove the announcement after 10 seconds + ) +} diff --git a/clap-meter/tsconfig.json b/clap-meter/tsconfig.json new file mode 100644 index 000000000..d84fa280f --- /dev/null +++ b/clap-meter/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "allowJs": true, + "strict": true + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx" + ], + "extends": "@dcl/sdk/types/tsconfig.ecs7.json" +} \ No newline at end of file diff --git a/dcl-workspace.json b/dcl-workspace.json index 119240cfa..84e64c08d 100644 --- a/dcl-workspace.json +++ b/dcl-workspace.json @@ -84,6 +84,9 @@ { "path": "coconut-shy" }, + { + "path": "clap-meter" + }, { "path": "boids" }, @@ -201,6 +204,9 @@ { "path": "Block-dog" }, + { + "path": "Block-Fountain" + }, { "path": "BasicInteractions" }, diff --git a/package.json b/package.json index acbc7282f..628a2bc99 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "77,-1-raycast-unit-tests", "80,-2-main-crdt", "BasicInteractions", + "Block-Fountain", "Block-dog", "BouncerUI", "Cube", @@ -70,6 +71,7 @@ "bird-field", "block-portable-experiences", "boids", + "clap-meter", "coconut-shy", "coin-pickup", "cube-wave-16x16",