├── .gitignore ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── public ├── assets │ ├── other │ │ ├── keyboard-icon.png │ │ ├── nintendo-logo-screen.png │ │ ├── overlay.png │ │ ├── sound-icon-mute.png │ │ ├── sound-icon.png │ │ └── stop-sign.png │ ├── sources │ │ ├── space-invaders │ │ │ ├── enemy-kill.png │ │ │ ├── enemy-missile-electric-01.png │ │ │ ├── enemy-missile-electric-02.png │ │ │ ├── enemy-missile-electric-03.png │ │ │ ├── enemy-missile-electric-04.png │ │ │ ├── enemy-missile-explode.png │ │ │ ├── enemy01-frame01.png │ │ │ ├── enemy01-frame02.png │ │ │ ├── player-hit.png │ │ │ ├── player-missile-explode.png │ │ │ ├── player-missile.png │ │ │ ├── player.png │ │ │ ├── space-invaders-logo.png │ │ │ ├── start-text.png │ │ │ └── title-screen-clean.png │ │ └── tetris │ │ │ ├── block-i-edge.png │ │ │ ├── block-i-middle.png │ │ │ ├── block-j.png │ │ │ ├── block-l.png │ │ │ ├── block-o.png │ │ │ ├── block-s.png │ │ │ ├── block-t.png │ │ │ ├── block-z.png │ │ │ ├── game-over-block.png │ │ │ ├── game-over-frame.png │ │ │ ├── gameplay-screen.png │ │ │ ├── license-screen.png │ │ │ └── title-screen.png │ ├── space-invaders.tps │ ├── spritesheets │ │ ├── space-invaders-sheet.json │ │ ├── space-invaders-sheet.png │ │ ├── tetris-sheet.json │ │ └── tetris-sheet.png │ └── tetris.tps ├── audio │ ├── eject-cartridge.mp3 │ ├── enemy-killed.mp3 │ ├── game-boy-load.mp3 │ ├── insert-cartridge.mp3 │ ├── line-clear.mp3 │ ├── move-side.mp3 │ ├── player-killed.mp3 │ ├── player-shoot.mp3 │ ├── power-switch.mp3 │ ├── rotate-shape.mp3 │ ├── shape-fall.mp3 │ ├── tetris-game-over-final.mp3 │ ├── tetris-game-over.mp3 │ ├── tetris-music.mp3 │ ├── tetris-pause.mp3 │ └── zelda-intro-sound.mp3 ├── favicon │ └── favicon.ico ├── fonts │ ├── dogicapixel.ttf │ └── tetris.ttf ├── models │ ├── game-boy-cartridge.glb │ └── game-boy.glb ├── textures │ ├── background.jpg │ ├── baked-cartridge-pocket-with-cartridge.jpg │ ├── baked-cartridge-pocket.jpg │ ├── baked-cartridge-space-invaders-in-pocket.jpg │ ├── baked-cartridge-space-invaders.jpg │ ├── baked-cartridge-tetris-in-pocket.jpg │ ├── baked-cartridge-tetris.jpg │ ├── baked-cartridge-zelda-in-pocket.jpg │ ├── baked-cartridge-zelda.jpg │ ├── baked-game-boy.jpg │ ├── baked-power-indicator.jpg │ └── baked-screen-shadow.png └── video │ └── zelda-intro.mp4 ├── src ├── core │ ├── base-scene.js │ ├── configs │ │ ├── debug-config.js │ │ ├── global-light-config.js │ │ ├── scene-config.js │ │ └── sounds-config.js │ ├── helpers │ │ ├── delayed-call.js │ │ ├── gui-helper │ │ │ ├── gui-helper.js │ │ │ └── scene-3d-debug-menu.js │ │ └── utils.js │ ├── loader.js │ ├── loading-overlay.js │ └── materials.js ├── main-scene.js ├── main.js ├── scene │ ├── game-boy-scene │ │ ├── background │ │ │ ├── background-shaders │ │ │ │ ├── background-fragment.glsl │ │ │ │ └── background-vertex.glsl │ │ │ └── background.js │ │ ├── camera-controller │ │ │ ├── camera-controller-config.js │ │ │ └── camera-controller.js │ │ ├── cartridges │ │ │ ├── cartridge.js │ │ │ ├── cartridges-controller.js │ │ │ └── data │ │ │ │ └── cartridges-config.js │ │ ├── data │ │ │ └── game-boy-scene-data.js │ │ ├── game-boy-debug.js │ │ ├── game-boy-games │ │ │ ├── data │ │ │ │ ├── games-classes.js │ │ │ │ └── games-config.js │ │ │ ├── game-boy-games.js │ │ │ ├── games │ │ │ │ ├── game-abstract.js │ │ │ │ ├── shared │ │ │ │ │ └── game-screen-abstract.js │ │ │ │ ├── space-invaders │ │ │ │ │ ├── data │ │ │ │ │ │ ├── space-invaders-config.js │ │ │ │ │ │ └── space-invaders-data.js │ │ │ │ │ ├── screens │ │ │ │ │ │ ├── game-over-screen.js │ │ │ │ │ │ ├── gameplay-screen │ │ │ │ │ │ │ ├── enemies-controller │ │ │ │ │ │ │ │ ├── data │ │ │ │ │ │ │ │ │ └── enemy-config.js │ │ │ │ │ │ │ │ ├── enemies-controller.js │ │ │ │ │ │ │ │ └── enemy.js │ │ │ │ │ │ │ ├── gameplay-screen.js │ │ │ │ │ │ │ ├── missile │ │ │ │ │ │ │ │ ├── enemy-missile.js │ │ │ │ │ │ │ │ ├── missile-config.js │ │ │ │ │ │ │ │ └── player-missile.js │ │ │ │ │ │ │ ├── player.js │ │ │ │ │ │ │ └── ui-elements │ │ │ │ │ │ │ │ ├── player-lives.js │ │ │ │ │ │ │ │ └── score.js │ │ │ │ │ │ ├── round-screen.js │ │ │ │ │ │ └── title-screen.js │ │ │ │ │ └── space-invaders.js │ │ │ │ ├── tetris │ │ │ │ │ ├── data │ │ │ │ │ │ ├── tetris-config.js │ │ │ │ │ │ └── tetris-data.js │ │ │ │ │ ├── screens │ │ │ │ │ │ ├── gameplay-screen.js │ │ │ │ │ │ │ ├── field │ │ │ │ │ │ │ │ ├── field.js │ │ │ │ │ │ │ │ └── shape │ │ │ │ │ │ │ │ │ ├── shape-config.js │ │ │ │ │ │ │ │ │ └── shape.js │ │ │ │ │ │ │ ├── gameplay-screen.js │ │ │ │ │ │ │ ├── next-shape.js │ │ │ │ │ │ │ └── popups │ │ │ │ │ │ │ │ ├── game-over-popup.js │ │ │ │ │ │ │ │ └── pause-popup.js │ │ │ │ │ │ ├── license-screen │ │ │ │ │ │ │ └── license-screen.js │ │ │ │ │ │ └── title-screen │ │ │ │ │ │ │ └── title-screen.js │ │ │ │ │ └── tetris.js │ │ │ │ └── zelda │ │ │ │ │ └── zelda.js │ │ │ ├── overlay │ │ │ │ └── volume-overlay.js │ │ │ └── screens │ │ │ │ ├── damaged-cartridge-screen.js │ │ │ │ ├── loading-screen.js │ │ │ │ ├── no-cartridge-screen.js │ │ │ │ └── screen-abstract.js │ │ ├── game-boy-scene-controller.js │ │ ├── game-boy-scene.js │ │ └── game-boy │ │ │ ├── data │ │ │ ├── game-boy-config.js │ │ │ └── game-boy-data.js │ │ │ ├── game-boy-audio │ │ │ ├── game-boy-audio-config.js │ │ │ ├── game-boy-audio-data.js │ │ │ └── game-boy-audio.js │ │ │ ├── game-boy.js │ │ │ ├── mix-texture-bitmap-shaders │ │ │ ├── mix-texture-bitmap-fragment.glsl │ │ │ └── mix-texture-bitmap-vertex.glsl │ │ │ └── mix-texture-color-shaders │ │ │ ├── mix-texture-color-fragment.glsl │ │ │ └── mix-texture-color-vertex.glsl │ ├── raycaster-controller.js │ └── scene3d.js ├── style.css └── ui │ ├── overlay.js │ ├── sound-icon.js │ └── ui.js └── vite.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | .vercel 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Interactive Game Boy - Three.js Journey Challenge 2 | ![screenshot-for-post](https://github.com/Snokke/game-boy-challenge/assets/36459180/339a94c1-4a83-406e-9a56-829173cd136d) 3 | 🔥 **Live: [gameboy.andriibabintsev.com](https://gameboy.andriibabintsev.com/)** 4 | 5 | This is my project for **Three.js Journey Challenge** - challenge by **Bruno Simon** ([threejs-journey.com](https://threejs-journey.com/)), where participants should create project in two weeks with main renderer library Three.js on a given theme. Theme of this challenge was **Game Boy**. And I was fortunate to have won this challenge: [Winners](https://threejs-journey.com/challenges/001-game-boy)🏆 6 | 7 | My idea was to create an interactive Game Boy and from scratch create at least one game for it (without emulator). 8 | 9 | All buttons of Game Boy are active, including the power switch on top and the volume controller at the side. You can also insert any of three cartridges into the Game Boy. 10 | Making the model of the Game Boy was a challenge, but I'm happy with the final result of the model and the grainy texture. 11 | 12 | ## Games 13 | ![games](https://github.com/Snokke/game-boy-challenge/assets/36459180/c1cde4d1-63da-4899-844b-1cecd10edb91) 14 | **Tetris** - this is not an emulator. I always wanted to try to create some classic game, so making Tetris was really fun and interesting. It's a full game with all the main logic (only Type-A game - endless game), all shapes, music, SFX, scores, and so on. Also there is one new shape - invisible shape (you can turn it off is control panel) 15 | 16 | **Space Invaders** - this is also not an emulator. I tried to recreate legendary old classic Space Invaders. Invaders are coming, kill them all! 🛸 17 | 18 | **Legend of Zelda** - give it a try, but I have a feeling that there is something wrong with the cartridge 👀 19 | 20 | ## Controls 21 | - Arrows, WASD - D-pad 22 | - Z, Space - A button 23 | - X - B button 24 | - Enter - START 25 | 26 | Mouse Scroll - Zoom to the Game Boy. On mobile, tap on the screen to zoom in/out. After you rotate the Game Boy, you can reset the rotation by clicking on the background. Also, in Tetris, you can turn off the music by pressing SELECT. 27 | 28 | ## Technical details 29 | - 3D engine: [Three.js](https://threejs.org/) 30 | - 2D engine for games: [PixiJS](https://pixijs.com/) 31 | - Control panel: [Tweakpane](https://cocopon.github.io/tweakpane/) 32 | - Models are done with [Blender](https://www.blender.org/) 33 | 34 | ## Setup 35 | Download [Node.js](https://nodejs.org/en/download). Run this followed commands: 36 | 37 | ``` 38 | # Install dependencies 39 | npm install 40 | 41 | # Run the local server at localhost:5173 42 | npm start 43 | 44 | # Build for production in the dist/ directory 45 | npm run build 46 | ``` 47 | 48 | ## Copyrights 49 | Nintendo logo is trademark of Nintendo. 50 | Tetris logo and Tetriminos are trademarks of Tetris Holding. 51 | Space Invaders logo is trademark of Taito Corporation. 52 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Interactive Game Boy - Andrii Babintsev 10 | 11 | 12 | 13 |
14 |
15 | 16 |
Made by Andrii Babintsev | Source code 17 |
18 |
19 |
20 |
21 |
22 |
23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "game-boy-challenge", 3 | "version": "0.1.0", 4 | "description": "Game Boy Challenge - Andrii Babintsev", 5 | "license": "UNLICENSED", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "vite", 9 | "start": "vite", 10 | "build": "vite build", 11 | "preview": "vite preview" 12 | }, 13 | "devDependencies": { 14 | "three-webgl-stats": "^1.0.5", 15 | "vite": "^4.1.0", 16 | "vite-plugin-glsl": "^1.1.2" 17 | }, 18 | "dependencies": { 19 | "@vercel/analytics": "^1.0.1", 20 | "black-engine": "^0.5.16", 21 | "ismobilejs": "^1.1.1", 22 | "pixi.js": "^7.2.4", 23 | "three": "^0.150.0", 24 | "tweakpane": "^3.1.7", 25 | "vercel": "^28.17.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /public/assets/other/keyboard-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/assets/other/keyboard-icon.png -------------------------------------------------------------------------------- /public/assets/other/nintendo-logo-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/assets/other/nintendo-logo-screen.png -------------------------------------------------------------------------------- /public/assets/other/overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/assets/other/overlay.png -------------------------------------------------------------------------------- /public/assets/other/sound-icon-mute.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/assets/other/sound-icon-mute.png -------------------------------------------------------------------------------- /public/assets/other/sound-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/assets/other/sound-icon.png -------------------------------------------------------------------------------- /public/assets/other/stop-sign.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/assets/other/stop-sign.png -------------------------------------------------------------------------------- /public/assets/sources/space-invaders/enemy-kill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/assets/sources/space-invaders/enemy-kill.png -------------------------------------------------------------------------------- /public/assets/sources/space-invaders/enemy-missile-electric-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/assets/sources/space-invaders/enemy-missile-electric-01.png -------------------------------------------------------------------------------- /public/assets/sources/space-invaders/enemy-missile-electric-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/assets/sources/space-invaders/enemy-missile-electric-02.png -------------------------------------------------------------------------------- /public/assets/sources/space-invaders/enemy-missile-electric-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/assets/sources/space-invaders/enemy-missile-electric-03.png -------------------------------------------------------------------------------- /public/assets/sources/space-invaders/enemy-missile-electric-04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/assets/sources/space-invaders/enemy-missile-electric-04.png -------------------------------------------------------------------------------- /public/assets/sources/space-invaders/enemy-missile-explode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/assets/sources/space-invaders/enemy-missile-explode.png -------------------------------------------------------------------------------- /public/assets/sources/space-invaders/enemy01-frame01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/assets/sources/space-invaders/enemy01-frame01.png -------------------------------------------------------------------------------- /public/assets/sources/space-invaders/enemy01-frame02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/assets/sources/space-invaders/enemy01-frame02.png -------------------------------------------------------------------------------- /public/assets/sources/space-invaders/player-hit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/assets/sources/space-invaders/player-hit.png -------------------------------------------------------------------------------- /public/assets/sources/space-invaders/player-missile-explode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/assets/sources/space-invaders/player-missile-explode.png -------------------------------------------------------------------------------- /public/assets/sources/space-invaders/player-missile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/assets/sources/space-invaders/player-missile.png -------------------------------------------------------------------------------- /public/assets/sources/space-invaders/player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/assets/sources/space-invaders/player.png -------------------------------------------------------------------------------- /public/assets/sources/space-invaders/space-invaders-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/assets/sources/space-invaders/space-invaders-logo.png -------------------------------------------------------------------------------- /public/assets/sources/space-invaders/start-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/assets/sources/space-invaders/start-text.png -------------------------------------------------------------------------------- /public/assets/sources/space-invaders/title-screen-clean.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/assets/sources/space-invaders/title-screen-clean.png -------------------------------------------------------------------------------- /public/assets/sources/tetris/block-i-edge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/assets/sources/tetris/block-i-edge.png -------------------------------------------------------------------------------- /public/assets/sources/tetris/block-i-middle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/assets/sources/tetris/block-i-middle.png -------------------------------------------------------------------------------- /public/assets/sources/tetris/block-j.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/assets/sources/tetris/block-j.png -------------------------------------------------------------------------------- /public/assets/sources/tetris/block-l.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/assets/sources/tetris/block-l.png -------------------------------------------------------------------------------- /public/assets/sources/tetris/block-o.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/assets/sources/tetris/block-o.png -------------------------------------------------------------------------------- /public/assets/sources/tetris/block-s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/assets/sources/tetris/block-s.png -------------------------------------------------------------------------------- /public/assets/sources/tetris/block-t.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/assets/sources/tetris/block-t.png -------------------------------------------------------------------------------- /public/assets/sources/tetris/block-z.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/assets/sources/tetris/block-z.png -------------------------------------------------------------------------------- /public/assets/sources/tetris/game-over-block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/assets/sources/tetris/game-over-block.png -------------------------------------------------------------------------------- /public/assets/sources/tetris/game-over-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/assets/sources/tetris/game-over-frame.png -------------------------------------------------------------------------------- /public/assets/sources/tetris/gameplay-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/assets/sources/tetris/gameplay-screen.png -------------------------------------------------------------------------------- /public/assets/sources/tetris/license-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/assets/sources/tetris/license-screen.png -------------------------------------------------------------------------------- /public/assets/sources/tetris/title-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/assets/sources/tetris/title-screen.png -------------------------------------------------------------------------------- /public/assets/spritesheets/space-invaders-sheet.json: -------------------------------------------------------------------------------- 1 | {"frames": { 2 | 3 | "enemy-kill.png": 4 | { 5 | "frame": {"x":225,"y":107,"w":8,"h":7}, 6 | "rotated": false, 7 | "trimmed": false, 8 | "spriteSourceSize": {"x":0,"y":0,"w":8,"h":7}, 9 | "sourceSize": {"w":8,"h":7} 10 | }, 11 | "enemy-missile-electric-01.png": 12 | { 13 | "frame": {"x":234,"y":1,"w":3,"h":7}, 14 | "rotated": false, 15 | "trimmed": false, 16 | "spriteSourceSize": {"x":0,"y":0,"w":3,"h":7}, 17 | "sourceSize": {"w":3,"h":7} 18 | }, 19 | "enemy-missile-electric-02.png": 20 | { 21 | "frame": {"x":234,"y":10,"w":3,"h":7}, 22 | "rotated": false, 23 | "trimmed": false, 24 | "spriteSourceSize": {"x":0,"y":0,"w":3,"h":7}, 25 | "sourceSize": {"w":3,"h":7} 26 | }, 27 | "enemy-missile-electric-03.png": 28 | { 29 | "frame": {"x":234,"y":19,"w":3,"h":7}, 30 | "rotated": false, 31 | "trimmed": false, 32 | "spriteSourceSize": {"x":0,"y":0,"w":3,"h":7}, 33 | "sourceSize": {"w":3,"h":7} 34 | }, 35 | "enemy-missile-electric-04.png": 36 | { 37 | "frame": {"x":234,"y":28,"w":3,"h":7}, 38 | "rotated": false, 39 | "trimmed": false, 40 | "spriteSourceSize": {"x":0,"y":0,"w":3,"h":7}, 41 | "sourceSize": {"w":3,"h":7} 42 | }, 43 | "enemy-missile-explode.png": 44 | { 45 | "frame": {"x":233,"y":134,"w":8,"h":4}, 46 | "rotated": true, 47 | "trimmed": false, 48 | "spriteSourceSize": {"x":0,"y":0,"w":8,"h":4}, 49 | "sourceSize": {"w":8,"h":4} 50 | }, 51 | "enemy01-frame01.png": 52 | { 53 | "frame": {"x":225,"y":116,"w":8,"h":7}, 54 | "rotated": false, 55 | "trimmed": false, 56 | "spriteSourceSize": {"x":0,"y":0,"w":8,"h":7}, 57 | "sourceSize": {"w":8,"h":7} 58 | }, 59 | "enemy01-frame02.png": 60 | { 61 | "frame": {"x":225,"y":125,"w":8,"h":7}, 62 | "rotated": false, 63 | "trimmed": false, 64 | "spriteSourceSize": {"x":0,"y":0,"w":8,"h":7}, 65 | "sourceSize": {"w":8,"h":7} 66 | }, 67 | "player-hit.png": 68 | { 69 | "frame": {"x":225,"y":80,"w":16,"h":8}, 70 | "rotated": true, 71 | "trimmed": false, 72 | "spriteSourceSize": {"x":0,"y":0,"w":16,"h":8}, 73 | "sourceSize": {"w":16,"h":8} 74 | }, 75 | "player-missile-explode.png": 76 | { 77 | "frame": {"x":225,"y":134,"w":6,"h":8}, 78 | "rotated": false, 79 | "trimmed": false, 80 | "spriteSourceSize": {"x":0,"y":0,"w":6,"h":8}, 81 | "sourceSize": {"w":6,"h":8} 82 | }, 83 | "player-missile.png": 84 | { 85 | "frame": {"x":234,"y":37,"w":3,"h":7}, 86 | "rotated": false, 87 | "trimmed": false, 88 | "spriteSourceSize": {"x":0,"y":0,"w":3,"h":7}, 89 | "sourceSize": {"w":3,"h":7} 90 | }, 91 | "player.png": 92 | { 93 | "frame": {"x":225,"y":98,"w":9,"h":7}, 94 | "rotated": false, 95 | "trimmed": false, 96 | "spriteSourceSize": {"x":0,"y":0,"w":9,"h":7}, 97 | "sourceSize": {"w":9,"h":7} 98 | }, 99 | "space-invaders-logo.png": 100 | { 101 | "frame": {"x":162,"y":1,"w":143,"h":61}, 102 | "rotated": true, 103 | "trimmed": false, 104 | "spriteSourceSize": {"x":0,"y":0,"w":143,"h":61}, 105 | "sourceSize": {"w":143,"h":61} 106 | }, 107 | "start-text.png": 108 | { 109 | "frame": {"x":225,"y":1,"w":77,"h":7}, 110 | "rotated": true, 111 | "trimmed": false, 112 | "spriteSourceSize": {"x":0,"y":0,"w":77,"h":7}, 113 | "sourceSize": {"w":77,"h":7} 114 | }, 115 | "title-screen-clean.png": 116 | { 117 | "frame": {"x":1,"y":1,"w":159,"h":144}, 118 | "rotated": false, 119 | "trimmed": true, 120 | "spriteSourceSize": {"x":0,"y":0,"w":159,"h":144}, 121 | "sourceSize": {"w":160,"h":144} 122 | }}, 123 | "animations": { 124 | "enemy-missile-electric": ["enemy-missile-electric-01.png","enemy-missile-electric-02.png","enemy-missile-electric-03.png","enemy-missile-electric-04.png"], 125 | "enemy01-frame": ["enemy01-frame01.png","enemy01-frame02.png"] 126 | }, 127 | "meta": { 128 | "app": "https://www.codeandweb.com/texturepacker", 129 | "version": "1.0", 130 | "image": "space-invaders-sheet.png", 131 | "format": "RGBA8888", 132 | "size": {"w":238,"h":146}, 133 | "scale": "1", 134 | "smartupdate": "$TexturePacker:SmartUpdate:fe2ac2983e21f74685e625913b7b4442:e16db42579996303db64adbde75d74b3:e5467e72355ab70bb93be2f3c4313ea1$" 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /public/assets/spritesheets/space-invaders-sheet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/assets/spritesheets/space-invaders-sheet.png -------------------------------------------------------------------------------- /public/assets/spritesheets/tetris-sheet.json: -------------------------------------------------------------------------------- 1 | {"frames": { 2 | 3 | "block-i-edge.png": 4 | { 5 | "frame": {"x":1,"y":1,"w":8,"h":8}, 6 | "rotated": false, 7 | "trimmed": false, 8 | "spriteSourceSize": {"x":0,"y":0,"w":8,"h":8}, 9 | "sourceSize": {"w":8,"h":8} 10 | }, 11 | "block-i-middle.png": 12 | { 13 | "frame": {"x":11,"y":1,"w":8,"h":8}, 14 | "rotated": false, 15 | "trimmed": false, 16 | "spriteSourceSize": {"x":0,"y":0,"w":8,"h":8}, 17 | "sourceSize": {"w":8,"h":8} 18 | }, 19 | "block-j.png": 20 | { 21 | "frame": {"x":21,"y":1,"w":8,"h":8}, 22 | "rotated": false, 23 | "trimmed": false, 24 | "spriteSourceSize": {"x":0,"y":0,"w":8,"h":8}, 25 | "sourceSize": {"w":8,"h":8} 26 | }, 27 | "block-l.png": 28 | { 29 | "frame": {"x":31,"y":1,"w":8,"h":8}, 30 | "rotated": false, 31 | "trimmed": false, 32 | "spriteSourceSize": {"x":0,"y":0,"w":8,"h":8}, 33 | "sourceSize": {"w":8,"h":8} 34 | }, 35 | "block-o.png": 36 | { 37 | "frame": {"x":41,"y":1,"w":8,"h":8}, 38 | "rotated": false, 39 | "trimmed": false, 40 | "spriteSourceSize": {"x":0,"y":0,"w":8,"h":8}, 41 | "sourceSize": {"w":8,"h":8} 42 | }, 43 | "block-s.png": 44 | { 45 | "frame": {"x":51,"y":1,"w":8,"h":8}, 46 | "rotated": false, 47 | "trimmed": false, 48 | "spriteSourceSize": {"x":0,"y":0,"w":8,"h":8}, 49 | "sourceSize": {"w":8,"h":8} 50 | }, 51 | "block-t.png": 52 | { 53 | "frame": {"x":61,"y":1,"w":8,"h":8}, 54 | "rotated": false, 55 | "trimmed": false, 56 | "spriteSourceSize": {"x":0,"y":0,"w":8,"h":8}, 57 | "sourceSize": {"w":8,"h":8} 58 | }, 59 | "block-z.png": 60 | { 61 | "frame": {"x":71,"y":1,"w":8,"h":8}, 62 | "rotated": false, 63 | "trimmed": false, 64 | "spriteSourceSize": {"x":0,"y":0,"w":8,"h":8}, 65 | "sourceSize": {"w":8,"h":8} 66 | }, 67 | "game-over-block.png": 68 | { 69 | "frame": {"x":81,"y":1,"w":8,"h":8}, 70 | "rotated": false, 71 | "trimmed": false, 72 | "spriteSourceSize": {"x":0,"y":0,"w":8,"h":8}, 73 | "sourceSize": {"w":8,"h":8} 74 | }, 75 | "game-over-frame.png": 76 | { 77 | "frame": {"x":91,"y":1,"w":64,"h":54}, 78 | "rotated": false, 79 | "trimmed": false, 80 | "spriteSourceSize": {"x":0,"y":0,"w":64,"h":54}, 81 | "sourceSize": {"w":64,"h":54} 82 | }, 83 | "gameplay-screen.png": 84 | { 85 | "frame": {"x":1,"y":57,"w":160,"h":144}, 86 | "rotated": false, 87 | "trimmed": false, 88 | "spriteSourceSize": {"x":0,"y":0,"w":160,"h":144}, 89 | "sourceSize": {"w":160,"h":144} 90 | }, 91 | "license-screen.png": 92 | { 93 | "frame": {"x":1,"y":203,"w":160,"h":144}, 94 | "rotated": false, 95 | "trimmed": false, 96 | "spriteSourceSize": {"x":0,"y":0,"w":160,"h":144}, 97 | "sourceSize": {"w":160,"h":144} 98 | }, 99 | "title-screen.png": 100 | { 101 | "frame": {"x":1,"y":349,"w":160,"h":144}, 102 | "rotated": false, 103 | "trimmed": false, 104 | "spriteSourceSize": {"x":0,"y":0,"w":160,"h":144}, 105 | "sourceSize": {"w":160,"h":144} 106 | }}, 107 | "meta": { 108 | "app": "https://www.codeandweb.com/texturepacker", 109 | "version": "1.0", 110 | "image": "tetris-sheet.png", 111 | "format": "RGBA8888", 112 | "size": {"w":162,"h":494}, 113 | "scale": "1", 114 | "smartupdate": "$TexturePacker:SmartUpdate:0d16ae2ed823dc0a32d6a0a87747a10e:bdb92aac0fccc6005838fed748ba20d4:30dfe819d18eb464f80ec5afa0c11fd4$" 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /public/assets/spritesheets/tetris-sheet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/assets/spritesheets/tetris-sheet.png -------------------------------------------------------------------------------- /public/assets/tetris.tps: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | fileFormatVersion 5 | 6 6 | texturePackerVersion 7 | 7.0.0 8 | autoSDSettings 9 | 10 | 11 | scale 12 | 1 13 | extension 14 | 15 | spriteFilter 16 | 17 | acceptFractionalValues 18 | 19 | maxTextureSize 20 | 21 | width 22 | -1 23 | height 24 | -1 25 | 26 | 27 | 28 | allowRotation 29 | 30 | shapeDebug 31 | 32 | dpi 33 | 72 34 | dataFormat 35 | pixijs4 36 | textureFileName 37 | spritesheets/tetris-sheet.png 38 | flipPVR 39 | 40 | pvrQualityLevel 41 | 3 42 | astcQualityLevel 43 | 2 44 | basisUniversalQualityLevel 45 | 2 46 | etc1QualityLevel 47 | 70 48 | etc2QualityLevel 49 | 70 50 | dxtCompressionMode 51 | DXT_PERCEPTUAL 52 | ditherType 53 | NearestNeighbour 54 | backgroundColor 55 | 0 56 | libGdx 57 | 58 | filtering 59 | 60 | x 61 | Linear 62 | y 63 | Linear 64 | 65 | 66 | shapePadding 67 | 0 68 | jpgQuality 69 | 80 70 | pngOptimizationLevel 71 | 1 72 | webpQualityLevel 73 | 101 74 | textureSubPath 75 | 76 | textureFormat 77 | png 78 | borderPadding 79 | 0 80 | maxTextureSize 81 | 82 | width 83 | 2048 84 | height 85 | 2048 86 | 87 | fixedTextureSize 88 | 89 | width 90 | -1 91 | height 92 | -1 93 | 94 | algorithmSettings 95 | 96 | algorithm 97 | MaxRects 98 | freeSizeMode 99 | Best 100 | sizeConstraints 101 | AnySize 102 | forceSquared 103 | 104 | maxRects 105 | 106 | heuristic 107 | Best 108 | 109 | basic 110 | 111 | sortBy 112 | Best 113 | order 114 | Ascending 115 | 116 | polygon 117 | 118 | alignToGrid 119 | 1 120 | 121 | 122 | dataFileNames 123 | 124 | data 125 | 126 | name 127 | spritesheets/tetris-sheet.json 128 | 129 | 130 | multiPackMode 131 | MultiPackOff 132 | forceIdenticalLayout 133 | 134 | outputFormat 135 | RGBA8888 136 | alphaHandling 137 | ClearTransparentPixels 138 | contentProtection 139 | 140 | key 141 | 142 | 143 | autoAliasEnabled 144 | 145 | trimSpriteNames 146 | 147 | prependSmartFolderName 148 | 149 | autodetectAnimations 150 | 151 | globalSpriteSettings 152 | 153 | scale 154 | 1 155 | scaleMode 156 | Smooth 157 | extrude 158 | 1 159 | trimThreshold 160 | 1 161 | trimMargin 162 | 1 163 | trimMode 164 | Trim 165 | tracerTolerance 166 | 200 167 | heuristicMask 168 | 169 | defaultPivotPoint 170 | 0,0 171 | writePivotPoints 172 | 173 | 174 | individualSpriteSettings 175 | 176 | sources/tetris/block-i-edge.png 177 | sources/tetris/block-i-middle.png 178 | sources/tetris/block-j.png 179 | sources/tetris/block-l.png 180 | sources/tetris/block-o.png 181 | sources/tetris/block-s.png 182 | sources/tetris/block-t.png 183 | sources/tetris/block-z.png 184 | sources/tetris/game-over-block.png 185 | 186 | pivotPoint 187 | 0,0 188 | spriteScale 189 | 1 190 | scale9Enabled 191 | 192 | scale9Borders 193 | 2,2,4,4 194 | scale9Paddings 195 | 2,2,4,4 196 | scale9FromFile 197 | 198 | 199 | sources/tetris/game-over-frame.png 200 | 201 | pivotPoint 202 | 0,0 203 | spriteScale 204 | 1 205 | scale9Enabled 206 | 207 | scale9Borders 208 | 16,14,32,27 209 | scale9Paddings 210 | 16,14,32,27 211 | scale9FromFile 212 | 213 | 214 | sources/tetris/gameplay-screen.png 215 | sources/tetris/license-screen.png 216 | sources/tetris/title-screen.png 217 | 218 | pivotPoint 219 | 0,0 220 | spriteScale 221 | 1 222 | scale9Enabled 223 | 224 | scale9Borders 225 | 40,36,80,72 226 | scale9Paddings 227 | 40,36,80,72 228 | scale9FromFile 229 | 230 | 231 | 232 | fileLists 233 | 234 | default 235 | 236 | files 237 | 238 | sources/tetris 239 | 240 | 241 | 242 | ignoreFileList 243 | 244 | replaceList 245 | 246 | ignoredWarnings 247 | 248 | pixijs-multipack-2023-05-25 249 | 250 | commonDivisorX 251 | 1 252 | commonDivisorY 253 | 1 254 | packNormalMaps 255 | 256 | autodetectNormalMaps 257 | 258 | normalMapFilter 259 | 260 | normalMapSuffix 261 | 262 | normalMapSheetFileName 263 | 264 | exporterProperties 265 | 266 | 267 | 268 | -------------------------------------------------------------------------------- /public/audio/eject-cartridge.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/audio/eject-cartridge.mp3 -------------------------------------------------------------------------------- /public/audio/enemy-killed.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/audio/enemy-killed.mp3 -------------------------------------------------------------------------------- /public/audio/game-boy-load.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/audio/game-boy-load.mp3 -------------------------------------------------------------------------------- /public/audio/insert-cartridge.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/audio/insert-cartridge.mp3 -------------------------------------------------------------------------------- /public/audio/line-clear.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/audio/line-clear.mp3 -------------------------------------------------------------------------------- /public/audio/move-side.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/audio/move-side.mp3 -------------------------------------------------------------------------------- /public/audio/player-killed.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/audio/player-killed.mp3 -------------------------------------------------------------------------------- /public/audio/player-shoot.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/audio/player-shoot.mp3 -------------------------------------------------------------------------------- /public/audio/power-switch.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/audio/power-switch.mp3 -------------------------------------------------------------------------------- /public/audio/rotate-shape.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/audio/rotate-shape.mp3 -------------------------------------------------------------------------------- /public/audio/shape-fall.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/audio/shape-fall.mp3 -------------------------------------------------------------------------------- /public/audio/tetris-game-over-final.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/audio/tetris-game-over-final.mp3 -------------------------------------------------------------------------------- /public/audio/tetris-game-over.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/audio/tetris-game-over.mp3 -------------------------------------------------------------------------------- /public/audio/tetris-music.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/audio/tetris-music.mp3 -------------------------------------------------------------------------------- /public/audio/tetris-pause.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/audio/tetris-pause.mp3 -------------------------------------------------------------------------------- /public/audio/zelda-intro-sound.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/audio/zelda-intro-sound.mp3 -------------------------------------------------------------------------------- /public/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/favicon/favicon.ico -------------------------------------------------------------------------------- /public/fonts/dogicapixel.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/fonts/dogicapixel.ttf -------------------------------------------------------------------------------- /public/fonts/tetris.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/fonts/tetris.ttf -------------------------------------------------------------------------------- /public/models/game-boy-cartridge.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/models/game-boy-cartridge.glb -------------------------------------------------------------------------------- /public/models/game-boy.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/models/game-boy.glb -------------------------------------------------------------------------------- /public/textures/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/textures/background.jpg -------------------------------------------------------------------------------- /public/textures/baked-cartridge-pocket-with-cartridge.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/textures/baked-cartridge-pocket-with-cartridge.jpg -------------------------------------------------------------------------------- /public/textures/baked-cartridge-pocket.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/textures/baked-cartridge-pocket.jpg -------------------------------------------------------------------------------- /public/textures/baked-cartridge-space-invaders-in-pocket.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/textures/baked-cartridge-space-invaders-in-pocket.jpg -------------------------------------------------------------------------------- /public/textures/baked-cartridge-space-invaders.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/textures/baked-cartridge-space-invaders.jpg -------------------------------------------------------------------------------- /public/textures/baked-cartridge-tetris-in-pocket.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/textures/baked-cartridge-tetris-in-pocket.jpg -------------------------------------------------------------------------------- /public/textures/baked-cartridge-tetris.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/textures/baked-cartridge-tetris.jpg -------------------------------------------------------------------------------- /public/textures/baked-cartridge-zelda-in-pocket.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/textures/baked-cartridge-zelda-in-pocket.jpg -------------------------------------------------------------------------------- /public/textures/baked-cartridge-zelda.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/textures/baked-cartridge-zelda.jpg -------------------------------------------------------------------------------- /public/textures/baked-game-boy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/textures/baked-game-boy.jpg -------------------------------------------------------------------------------- /public/textures/baked-power-indicator.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/textures/baked-power-indicator.jpg -------------------------------------------------------------------------------- /public/textures/baked-screen-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/textures/baked-screen-shadow.png -------------------------------------------------------------------------------- /public/video/zelda-intro.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Snokke/game-boy-challenge/0c809f6747288c9729f8b90dabf4bec1a4c2bdaa/public/video/zelda-intro.mp4 -------------------------------------------------------------------------------- /src/core/configs/debug-config.js: -------------------------------------------------------------------------------- 1 | import { GAME_TYPE } from "../../scene/game-boy-scene/game-boy-games/data/games-config"; 2 | import { SPACE_INVADERS_SCREEN_TYPE } from "../../scene/game-boy-scene/game-boy-games/games/space-invaders/data/space-invaders-data"; 3 | import { TETRIS_SCREEN_TYPE } from "../../scene/game-boy-scene/game-boy-games/games/tetris/data/tetris-data"; 4 | 5 | const DEBUG_CONFIG = { 6 | fpsMeter: false, 7 | rendererStats: false, 8 | orbitControls: false, 9 | startState: { 10 | disableIntro: false, 11 | // zoomIn: true, 12 | // enableGameBoy: true, 13 | // loadGame: GAME_TYPE.Tetris, 14 | // loadGame: GAME_TYPE.SpaceInvaders, 15 | // startScreen: SPACE_INVADERS_SCREEN_TYPE.Round, 16 | // startScreen: TETRIS_SCREEN_TYPE.Gameplay, 17 | }, 18 | }; 19 | 20 | export default DEBUG_CONFIG; 21 | -------------------------------------------------------------------------------- /src/core/configs/global-light-config.js: -------------------------------------------------------------------------------- 1 | const GLOBAL_LIGHT_CONFIG = { 2 | ambient: { 3 | enabled: false, 4 | color: 0xFFEFE4, 5 | intensity: 2, 6 | }, 7 | } 8 | 9 | export { GLOBAL_LIGHT_CONFIG }; 10 | -------------------------------------------------------------------------------- /src/core/configs/scene-config.js: -------------------------------------------------------------------------------- 1 | const SCENE_CONFIG = { 2 | backgroundColor: 0x999999, // 0x201919 3 | antialias: false, 4 | fxaaPass: false, 5 | maxPixelRatio: 2, 6 | isMobile: false, 7 | outlinePass: { 8 | enabled: true, 9 | color: '#ffffff', 10 | edgeGlow: 1, 11 | edgeStrength: 4, 12 | edgeThickness: 1, 13 | pulsePeriod: 4, 14 | }, 15 | }; 16 | 17 | export default SCENE_CONFIG; 18 | -------------------------------------------------------------------------------- /src/core/configs/sounds-config.js: -------------------------------------------------------------------------------- 1 | const SOUNDS_CONFIG = { 2 | enabled: true, 3 | masterVolume: 0.5, 4 | gameBoyVolume: 0.5, 5 | } 6 | 7 | export { SOUNDS_CONFIG }; 8 | -------------------------------------------------------------------------------- /src/core/helpers/delayed-call.js: -------------------------------------------------------------------------------- 1 | import { Black, GameObject, Tween } from 'black-engine'; 2 | 3 | /** 4 | * @author Comics 5 | */ 6 | export default class Delayed { 7 | constructor() { 8 | } 9 | 10 | static call(delay, callback, context, ...params) { 11 | 12 | if (!Delayed.helper) { 13 | Delayed.helper = Black.stage.addChild(new GameObject()); 14 | } 15 | 16 | if (delay > 0) { 17 | let t = new Tween({}, delay * 0.001); 18 | t.on('complete', () => { 19 | callback.apply(context, params); 20 | this.__removeCall(callback); 21 | }); 22 | Delayed.helper.addComponent(t); 23 | Delayed.calls[callback] = t; 24 | return t; 25 | } else { 26 | callback.apply(context, params); 27 | } 28 | } 29 | 30 | static __removeCall(callback) { 31 | Delayed.calls[callback] = null; 32 | delete Delayed.calls[callback]; 33 | } 34 | 35 | static kill(callback) { 36 | if (Delayed.calls[callback]) { 37 | Delayed.calls[callback].stop(); 38 | Delayed.calls[callback].removeFromParent(); 39 | Delayed.__removeCall(callback); 40 | } 41 | } 42 | } 43 | 44 | Delayed.helper = null; 45 | Delayed.calls = {}; 46 | -------------------------------------------------------------------------------- /src/core/helpers/gui-helper/gui-helper.js: -------------------------------------------------------------------------------- 1 | import { Pane } from 'tweakpane'; 2 | import isMobile from 'ismobilejs'; 3 | import DEBUG_CONFIG from '../../configs/debug-config'; 4 | 5 | export default class GUIHelper { 6 | constructor() { 7 | this.gui = new Pane({ 8 | title: 'Control panel', 9 | }); 10 | 11 | this.gui.hidden = true; 12 | this.gui.containerElem_.style.width = '275px'; 13 | 14 | const isMobileDevice = isMobile(window.navigator).any; 15 | 16 | // if (isMobileDevice) { 17 | this.gui.expanded = false; 18 | // } 19 | 20 | GUIHelper.instance = this; 21 | 22 | return this.gui; 23 | } 24 | 25 | getFolder(name) { 26 | const folders = this.gui.children; 27 | 28 | for (let i = 0; i < folders.length; i += 1) { 29 | const folder = folders[i]; 30 | 31 | if (folder.title === name) { 32 | return folder; 33 | } 34 | } 35 | 36 | return null; 37 | } 38 | 39 | getController(folder, name) { 40 | for (let i = 0; i < folder.children.length; i += 1) { 41 | const controller = folder.children[i]; 42 | 43 | if (controller.label === name) { 44 | return controller; 45 | } 46 | } 47 | 48 | return null; 49 | } 50 | 51 | getControllerFromFolder(folderName, controllerName) { 52 | const folder = this.getFolder(folderName); 53 | 54 | if (folder) { 55 | return this.getController(folder, controllerName); 56 | } 57 | 58 | return null; 59 | } 60 | 61 | showAfterAssetsLoad() { 62 | if (!DEBUG_CONFIG.withoutUIMode) { 63 | this.gui.hidden = false; 64 | } 65 | } 66 | 67 | static getGui() { 68 | return GUIHelper.instance.gui; 69 | } 70 | 71 | static getFolder(name) { 72 | return GUIHelper.instance.getFolder(name); 73 | } 74 | 75 | static getController(folder, name) { 76 | return GUIHelper.instance.getController(folder, name); 77 | } 78 | 79 | static getControllerFromFolder(folderName, controllerName) { 80 | return GUIHelper.instance.getControllerFromFolder(folderName, controllerName); 81 | } 82 | } 83 | 84 | GUIHelper.instance = null; 85 | -------------------------------------------------------------------------------- /src/core/helpers/gui-helper/scene-3d-debug-menu.js: -------------------------------------------------------------------------------- 1 | import { Black } from 'black-engine'; 2 | import DEBUG_CONFIG from "../../configs/debug-config"; 3 | import RendererStats from 'three-webgl-stats'; 4 | import Stats from 'three/addons/libs/stats.module.js'; 5 | import GUIHelper from "./gui-helper"; 6 | import { OrbitControls } from "three/addons/controls/OrbitControls"; 7 | 8 | export default class Scene3DDebugMenu { 9 | constructor(scene, camera, renderer) { 10 | this._scene = scene; 11 | this._camera = camera; 12 | this._renderer = renderer; 13 | 14 | this._fpsStats = null; 15 | this._rendererStats = null; 16 | this._orbitControls = null; 17 | this._gridHelper = null; 18 | this._axesHelper = null; 19 | this._baseGUI = null; 20 | 21 | this._isAssetsLoaded = false; 22 | 23 | this._init(); 24 | } 25 | 26 | preUpdate() { 27 | if (DEBUG_CONFIG.fpsMeter) { 28 | this._fpsStats.begin(); 29 | } 30 | } 31 | 32 | postUpdate() { 33 | if (DEBUG_CONFIG.fpsMeter) { 34 | this._fpsStats.end(); 35 | } 36 | } 37 | 38 | update() { 39 | if (DEBUG_CONFIG.orbitControls) { 40 | this._orbitControls.update(); 41 | } 42 | 43 | if (DEBUG_CONFIG.rendererStats) { 44 | this._rendererStats.update(this._renderer); 45 | } 46 | } 47 | 48 | showAfterAssetsLoad() { 49 | this._isAssetsLoaded = true; 50 | 51 | if (DEBUG_CONFIG.fpsMeter) { 52 | this._fpsStats.dom.style.visibility = 'visible'; 53 | } 54 | 55 | if (DEBUG_CONFIG.rendererStats) { 56 | this._rendererStats.domElement.style.visibility = 'visible'; 57 | } 58 | 59 | if (DEBUG_CONFIG.orbitControls) { 60 | this._orbitControls.enabled = true; 61 | } 62 | 63 | GUIHelper.instance.showAfterAssetsLoad(); 64 | } 65 | 66 | getOrbitControls() { 67 | return this._orbitControls; 68 | } 69 | 70 | _init() { 71 | this._initRendererStats(); 72 | this._initFPSMeter(); 73 | this._initOrbitControls(); 74 | 75 | this._initLilGUIHelper(); 76 | } 77 | 78 | _initRendererStats() { 79 | if (DEBUG_CONFIG.rendererStats) { 80 | const rendererStats = this._rendererStats = new RendererStats(); 81 | 82 | rendererStats.domElement.style.position = 'absolute'; 83 | rendererStats.domElement.style.left = '0px'; 84 | rendererStats.domElement.style.bottom = '0px'; 85 | document.body.appendChild(rendererStats.domElement); 86 | 87 | if (!this._isAssetsLoaded) { 88 | this._rendererStats.domElement.style.visibility = 'hidden'; 89 | } 90 | } 91 | } 92 | 93 | _initFPSMeter() { 94 | if (DEBUG_CONFIG.fpsMeter) { 95 | const stats = this._fpsStats = new Stats(); 96 | stats.showPanel(0); 97 | document.body.appendChild(stats.dom); 98 | 99 | if (!this._isAssetsLoaded) { 100 | this._fpsStats.dom.style.visibility = 'hidden'; 101 | } 102 | } 103 | } 104 | 105 | _initOrbitControls() { 106 | const orbitControls = this._orbitControls = new OrbitControls(this._camera, Black.engine.containerElement); 107 | 108 | orbitControls.target.set(0, 0, 0); 109 | 110 | orbitControls.enableDamping = true; 111 | orbitControls.dampingFactor = 0.07; 112 | orbitControls.rotateSpeed = 0.5; 113 | orbitControls.panSpeed = 0.5; 114 | 115 | if (!this._isAssetsLoaded) { 116 | orbitControls.enabled = false; 117 | } 118 | } 119 | 120 | _initLilGUIHelper() { 121 | new GUIHelper(); 122 | } 123 | 124 | onFpsMeterClick() { 125 | if (DEBUG_CONFIG.fpsMeter) { 126 | if (!this._fpsStats) { 127 | this._initFPSMeter(); 128 | } 129 | this._fpsStats.dom.style.display = 'block'; 130 | } else { 131 | this._fpsStats.dom.style.display = 'none'; 132 | } 133 | } 134 | 135 | onRendererStatsClick(rendererStatsState) { 136 | if (DEBUG_CONFIG.rendererStats) { 137 | if (rendererStatsState) { 138 | if (!this._rendererStats) { 139 | this._initRendererStats(); 140 | } 141 | 142 | this._rendererStats.domElement.style.display = 'block'; 143 | } else { 144 | this._rendererStats.domElement.style.display = 'none'; 145 | } 146 | } 147 | } 148 | 149 | onOrbitControlsClick(orbitControlsState) { 150 | if (orbitControlsState) { 151 | if (!this._orbitControls) { 152 | this._initOrbitControls(); 153 | } 154 | 155 | this._orbitControls.enabled = true; 156 | } else { 157 | this._orbitControls.enabled = false; 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/core/helpers/utils.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import Loader from "../loader"; 3 | 4 | const boundingBox = new THREE.Box3(); 5 | 6 | export default class Utils { 7 | static createObject(name) { 8 | const object = Loader.assets[name]; 9 | 10 | if (!object) { 11 | throw new Error(`Object ${name} is not found.`); 12 | } 13 | 14 | const group = new THREE.Group(); 15 | const children = [...object.scene.children]; 16 | 17 | for (let i = 0; i < children.length; i += 1) { 18 | const child = children[i]; 19 | group.add(child); 20 | } 21 | 22 | return group; 23 | } 24 | 25 | static getBoundingBox(target) { 26 | boundingBox.setFromObject(target); 27 | const size = boundingBox.getSize(new THREE.Vector3()); 28 | 29 | return size; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/core/loader.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import * as PIXI from 'pixi.js'; 3 | import { AssetManager, GameObject, MessageDispatcher } from 'black-engine'; 4 | import { GLTFLoader } from 'three/addons/loaders/GLTFLoader'; 5 | 6 | const textures = [ 7 | 'baked-game-boy.jpg', 8 | 'baked-cartridge-tetris.jpg', 9 | 'baked-cartridge-tetris-in-pocket.jpg', 10 | 'baked-cartridge-zelda.jpg', 11 | 'baked-cartridge-zelda-in-pocket.jpg', 12 | 'baked-cartridge-space-invaders.jpg', 13 | 'baked-cartridge-space-invaders-in-pocket.jpg', 14 | 'baked-power-indicator.jpg', 15 | 'baked-screen-shadow.png', 16 | 'baked-cartridge-pocket.jpg', 17 | 'baked-cartridge-pocket-with-cartridge.jpg', 18 | 19 | 'background.jpg', 20 | ]; 21 | 22 | const models = [ 23 | 'game-boy.glb', 24 | 'game-boy-cartridge.glb', 25 | ]; 26 | 27 | const images = [ 28 | 'other/overlay.png', 29 | 'other/sound-icon.png', 30 | 'other/sound-icon-mute.png', 31 | ]; 32 | 33 | const pixiAssets = [ 34 | 'assets/other/nintendo-logo-screen.png', 35 | 'assets/other/stop-sign.png', 36 | 37 | 'assets/spritesheets/tetris-sheet.json', 38 | 'fonts/tetris.ttf', 39 | 40 | 'assets/spritesheets/space-invaders-sheet.json', 41 | 'fonts/dogicapixel.ttf', 42 | ]; 43 | 44 | const sounds = [ 45 | 'power-switch.mp3', 46 | 'insert-cartridge.mp3', 47 | 'eject-cartridge.mp3', 48 | 'game-boy-load.mp3', 49 | 'zelda-intro-sound.mp3', 50 | 51 | // tetris 52 | 'tetris-music.mp3', 53 | 'move-side.mp3', 54 | 'rotate-shape.mp3', 55 | 'shape-fall.mp3', 56 | 'line-clear.mp3', 57 | 'tetris-pause.mp3', 58 | 'tetris-game-over.mp3', 59 | 'tetris-game-over-final.mp3', 60 | 61 | // space invaders 62 | 'player-shoot.mp3', 63 | 'enemy-killed.mp3', 64 | 'player-killed.mp3', 65 | ]; 66 | 67 | const loadingPercentElement = document.querySelector('.loading-percent'); 68 | let progressRatio = 0; 69 | const blackAssetsProgressPart = 0; 70 | let isSoundsLoaded = false; // eslint-disable-line no-unused-vars 71 | 72 | export default class Loader extends GameObject { 73 | constructor() { 74 | super(); 75 | 76 | Loader.assets = {}; 77 | Loader.events = new MessageDispatcher(); 78 | 79 | this._threeJSManager = new THREE.LoadingManager(this._onThreeJSAssetsLoaded, this._onThreeJSAssetsProgress); 80 | this._blackManager = new AssetManager(); 81 | 82 | this._soundsCountLoaded = 0; 83 | 84 | this._loadBlackAssets(); 85 | } 86 | 87 | _loadBlackAssets() { 88 | const imagesBasePath = '/assets/'; 89 | 90 | images.forEach((textureFilename) => { 91 | const imageFullPath = `${imagesBasePath}${textureFilename}`; 92 | const imageName = textureFilename.replace(/\.[^/.]+$/, ""); 93 | this._blackManager.enqueueImage(imageName, imageFullPath); 94 | }); 95 | 96 | this._blackManager.on('complete', this._onBlackAssetsLoaded, this); 97 | this._blackManager.on('progress', this._onBlackAssetsProgress, this); 98 | 99 | this._blackManager.loadQueue(); 100 | } 101 | 102 | _onBlackAssetsProgress(item, progress) { // eslint-disable-line no-unused-vars 103 | // progressRatio = progress; 104 | 105 | // const percent = Math.floor(progressRatio * 100); 106 | // loadingPercentElement.innerHTML = `${percent}%`; 107 | } 108 | 109 | _onBlackAssetsLoaded() { 110 | this.removeFromParent(); 111 | this._loadPixiAssets(); 112 | } 113 | 114 | _loadPixiAssets() { 115 | const texturesNames = []; 116 | 117 | pixiAssets.forEach((textureFilename) => { 118 | const textureName = textureFilename.replace(/\.[^/.]+$/, ""); 119 | PIXI.Assets.add(textureName, textureFilename); 120 | 121 | texturesNames.push(textureName); 122 | }); 123 | 124 | const texturesPromise = PIXI.Assets.load(texturesNames); 125 | 126 | texturesPromise.then((textures) => { 127 | texturesNames.forEach((name) => { 128 | this._onAssetLoad(textures[name], name); 129 | }); 130 | 131 | this._loadThreeJSAssets(); 132 | }); 133 | } 134 | 135 | _loadThreeJSAssets() { 136 | this._loadTextures(); 137 | this._loadModels(); 138 | this._loadAudio(); 139 | 140 | if (textures.length === 0 && models.length === 0 && sounds.length === 0) { 141 | this._onThreeJSAssetsLoaded(); 142 | } 143 | } 144 | 145 | _onThreeJSAssetsLoaded() { 146 | setTimeout(() => { 147 | loadingPercentElement.innerHTML = `100%`; 148 | loadingPercentElement.classList.add('ended'); 149 | 150 | setTimeout(() => { 151 | loadingPercentElement.style.display = 'none'; 152 | }, 300); 153 | }, 450); 154 | 155 | 156 | setTimeout(() => { 157 | const customEvent = new Event('onLoad'); 158 | document.dispatchEvent(customEvent); 159 | 160 | if (isSoundsLoaded) { 161 | Loader.events.post('onAudioLoaded'); 162 | } 163 | }, 100); 164 | } 165 | 166 | _onThreeJSAssetsProgress(itemUrl, itemsLoaded, itemsTotal) { 167 | progressRatio = Math.min(blackAssetsProgressPart + (itemsLoaded / itemsTotal), 0.98); 168 | 169 | const percent = Math.floor(progressRatio * 100); 170 | loadingPercentElement.innerHTML = `${percent}%`; 171 | } 172 | 173 | _loadTextures() { 174 | const textureLoader = new THREE.TextureLoader(this._threeJSManager); 175 | 176 | const texturesBasePath = '/textures/'; 177 | 178 | textures.forEach((textureFilename) => { 179 | const textureFullPath = `${texturesBasePath}${textureFilename}`; 180 | const textureName = textureFilename.replace(/\.[^/.]+$/, ""); 181 | Loader.assets[textureName] = textureLoader.load(textureFullPath); 182 | }); 183 | } 184 | 185 | _loadModels() { 186 | const gltfLoader = new GLTFLoader(this._threeJSManager); 187 | 188 | const modelsBasePath = '/models/'; 189 | 190 | models.forEach((modelFilename) => { 191 | const modelFullPath = `${modelsBasePath}${modelFilename}`; 192 | const modelName = modelFilename.replace(/\.[^/.]+$/, ""); 193 | gltfLoader.load(modelFullPath, (gltfModel) => this._onAssetLoad(gltfModel, modelName)); 194 | }); 195 | } 196 | 197 | _loadAudio() { 198 | const audioLoader = new THREE.AudioLoader(this._threeJSManager); 199 | 200 | const audioBasePath = '/audio/'; 201 | 202 | sounds.forEach((audioFilename) => { 203 | const audioFullPath = `${audioBasePath}${audioFilename}`; 204 | const audioName = audioFilename.replace(/\.[^/.]+$/, ""); 205 | audioLoader.load(audioFullPath, (audioBuffer) => { 206 | this._onAssetLoad(audioBuffer, audioName); 207 | 208 | this._soundsCountLoaded += 1; 209 | 210 | if (this._soundsCountLoaded === sounds.length) { 211 | isSoundsLoaded = true; 212 | Loader.events.post('onAudioLoaded'); 213 | } 214 | }); 215 | }); 216 | } 217 | 218 | _onAssetLoad(asset, name) { 219 | Loader.assets[name] = asset; 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/core/loading-overlay.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { TWEEN } from '/node_modules/three/examples/jsm/libs/tween.module.min.js'; 3 | 4 | export default class LoadingOverlay extends THREE.Group { 5 | constructor() { 6 | super(); 7 | 8 | this._overlayMaterial = null; 9 | 10 | this._init(); 11 | } 12 | 13 | hide() { 14 | new TWEEN.Tween(this._overlayMaterial.uniforms.uAlpha) 15 | .to({ value: 0 }, 400) 16 | .easing(TWEEN.Easing.Linear.None) 17 | .start() 18 | .onComplete(() => { 19 | this.visible = false; 20 | }); 21 | } 22 | 23 | _init() { 24 | const overlayGeometry = new THREE.PlaneGeometry(2, 2, 1, 1); 25 | const overlayMaterial = this._overlayMaterial = new THREE.ShaderMaterial({ 26 | transparent: true, 27 | uniforms: { 28 | uAlpha: { value: 1 }, 29 | }, 30 | vertexShader: ` 31 | void main() 32 | { 33 | gl_Position = vec4(position, 0.5); 34 | } 35 | `, 36 | fragmentShader: ` 37 | uniform float uAlpha; 38 | 39 | void main() 40 | { 41 | gl_FragColor = vec4(0.0, 0.0, 0.0, uAlpha); 42 | } 43 | `, 44 | }); 45 | 46 | const overlay = new THREE.Mesh(overlayGeometry, overlayMaterial); 47 | this.add(overlay); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/core/materials.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import Loader from './loader'; 3 | 4 | export default class Materials { 5 | constructor() { 6 | 7 | this.bakedMaterial = null; 8 | 9 | this._initMaterials(); 10 | 11 | Materials.instance = this; 12 | } 13 | 14 | _initMaterials() { 15 | // this._initBakedTexture(); 16 | } 17 | 18 | _initBakedTexture() { 19 | const bakedTexture = Loader.assets['']; 20 | bakedTexture.flipY = false; 21 | 22 | this.bakedMaterial = new THREE.MeshBasicMaterial({ 23 | map: bakedTexture, 24 | }); 25 | } 26 | 27 | static getMaterial(type) { 28 | let material; 29 | 30 | switch (type) { 31 | case Materials.type.bakedMaterial: 32 | material = Materials.instance.bakedMaterial; 33 | break; 34 | } 35 | 36 | return material; 37 | } 38 | } 39 | 40 | Materials.instance = null; 41 | 42 | Materials.type = { 43 | bakedMaterial: 'BAKED_MATERIAL', 44 | }; 45 | -------------------------------------------------------------------------------- /src/main-scene.js: -------------------------------------------------------------------------------- 1 | import { Black, MessageDispatcher } from "black-engine"; 2 | import UI from "./ui/ui"; 3 | import Scene3D from "./scene/scene3d"; 4 | 5 | export default class MainScene { 6 | constructor(data) { 7 | this.events = new MessageDispatcher(); 8 | 9 | this._data = data; 10 | this._scene = data.scene; 11 | this._camera = data.camera; 12 | 13 | this._scene3D = null; 14 | this._ui = null; 15 | 16 | this._init(); 17 | } 18 | 19 | afterAssetsLoad() { 20 | Black.stage.addChild(this._ui); 21 | this._scene.add(this._scene3D); 22 | } 23 | 24 | update(dt) { 25 | this._scene3D.update(dt); 26 | } 27 | 28 | _init() { 29 | this._scene3D = new Scene3D(this._data); 30 | this._ui = new UI(); 31 | 32 | this._initSignals(); 33 | } 34 | 35 | _initSignals() { 36 | this._ui.on('onPointerMove', (msg, x, y) => this._scene3D.onPointerMove(x, y)); 37 | this._ui.on('onPointerDown', (msg, x, y) => this._scene3D.onPointerDown(x, y)); 38 | this._ui.on('onPointerUp', (msg, x, y) => this._scene3D.onPointerUp(x, y)); 39 | this._ui.on('onWheelScroll', (msg, delta) => this._scene3D.onWheelScroll(delta)); 40 | this._ui.on('onSoundChanged', () => this._scene3D.onSoundChanged()); 41 | 42 | this._scene3D.events.on('fpsMeterChanged', () => this.events.post('fpsMeterChanged')); 43 | this._scene3D.events.on('onSoundsEnabledChanged', () => this._ui.updateSoundIcon()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import './style.css'; 2 | import BaseScene from './core/base-scene'; 3 | import { inject } from '@vercel/analytics'; 4 | 5 | inject(); 6 | 7 | const baseScene = new BaseScene(); 8 | 9 | document.addEventListener('onLoad', () => { 10 | baseScene.createGameScene(); 11 | 12 | setTimeout(() => baseScene.afterAssetsLoaded(), 300); 13 | }); 14 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/background/background-shaders/background-fragment.glsl: -------------------------------------------------------------------------------- 1 | varying vec2 vUv; 2 | 3 | uniform float uTime; 4 | uniform vec3 color01; 5 | uniform vec3 color02; 6 | 7 | void main() 8 | { 9 | float uAngle = 0.0; 10 | vec2 direction = vec2(cos(uAngle), sin(uAngle)); 11 | float dotValue = dot(vUv, direction); 12 | float gradientOffset = (dotValue + 1.0) * 0.5; 13 | 14 | // vec3 color = mix(color01, color02, vUv.x); 15 | vec3 color = mix(color01, color02, gradientOffset); 16 | 17 | gl_FragColor = vec4(color, 1.0); 18 | } 19 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/background/background-shaders/background-vertex.glsl: -------------------------------------------------------------------------------- 1 | varying vec2 vUv; 2 | 3 | void main() 4 | { 5 | vec4 modelPosition = modelMatrix * vec4(position, 1.0); 6 | vec4 viewPosition = viewMatrix * modelPosition; 7 | vec4 projectionPosition = projectionMatrix * viewPosition; 8 | gl_Position = projectionPosition; 9 | 10 | vUv = uv; 11 | } 12 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/background/background.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { SCENE_OBJECT_TYPE } from '../data/game-boy-scene-data'; 3 | import { MessageDispatcher } from 'black-engine'; 4 | import Loader from '../../../core/loader'; 5 | import vertexShader from './background-shaders/background-vertex.glsl'; 6 | import fragmentShader from './background-shaders/background-fragment.glsl'; 7 | 8 | export default class Background extends THREE.Group { 9 | constructor() { 10 | super(); 11 | 12 | this.events = new MessageDispatcher(); 13 | 14 | this._view = null; 15 | this._sceneObjectType = SCENE_OBJECT_TYPE.Background; 16 | 17 | this._init(); 18 | } 19 | 20 | update(dt) { 21 | // this._view.material.uniforms.uTime.value += dt; 22 | } 23 | 24 | onPointerDown(object) { 25 | this.events.post('onClick'); 26 | } 27 | 28 | getMesh() { 29 | return this._view; 30 | } 31 | 32 | getOutlineMeshes(object) { 33 | return [object]; 34 | } 35 | 36 | onPointerOver() { } 37 | 38 | _init() { 39 | const texture = Loader.assets['background']; 40 | 41 | const geometry = new THREE.PlaneGeometry(50, 50); 42 | const material = new THREE.MeshBasicMaterial({ 43 | map: texture, 44 | // color: 0x666666, // 0x999999 45 | }); 46 | 47 | // const material = new THREE.ShaderMaterial({ 48 | // uniforms: { 49 | // uTime: { value: 0 }, 50 | // uAngle: { value: 0 }, 51 | // color01: { value: new THREE.Color(0x463fcc) }, 52 | // color02: { value: new THREE.Color(0xca4a75) }, 53 | // }, 54 | // vertexShader: vertexShader, 55 | // fragmentShader: fragmentShader, 56 | // }); 57 | 58 | const view = this._view = new THREE.Mesh(geometry, material); 59 | this.add(view); 60 | 61 | view.userData['isActive'] = true; 62 | view.userData['sceneObjectType'] = this._sceneObjectType; 63 | view.userData['showOutline'] = false; 64 | 65 | view.position.set(0, 0, -15); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/camera-controller/camera-controller-config.js: -------------------------------------------------------------------------------- 1 | const CAMERA_CONTROLLER_CONFIG = { 2 | minDistance: 3.2, 3 | maxDistance: 6, 4 | zoomSpeed: 0.4, 5 | mobileMinDistance: 3.8, 6 | } 7 | 8 | export { CAMERA_CONTROLLER_CONFIG }; 9 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/camera-controller/camera-controller.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { GAME_BOY_CONFIG } from '../game-boy/data/game-boy-config'; 3 | import { MessageDispatcher } from 'black-engine'; 4 | import DEBUG_CONFIG from '../../../core/configs/debug-config'; 5 | import { CAMERA_CONTROLLER_CONFIG } from './camera-controller-config'; 6 | import SCENE_CONFIG from '../../../core/configs/scene-config'; 7 | 8 | export default class CameraController { 9 | constructor(camera) { 10 | 11 | this.events = new MessageDispatcher(); 12 | 13 | this._camera = camera; 14 | 15 | this._zoomObject = new THREE.Object3D(); 16 | this._rotationDragPreviousState = true; 17 | this._minDistance = SCENE_CONFIG.isMobile ? CAMERA_CONTROLLER_CONFIG.mobileMinDistance : CAMERA_CONTROLLER_CONFIG.minDistance; 18 | 19 | this._zoomDistance = this._camera.position.z; 20 | 21 | this._init(); 22 | } 23 | 24 | update(dt) { 25 | if (DEBUG_CONFIG.orbitControls) { 26 | return; 27 | } 28 | 29 | this._camera.position.lerp(this._zoomObject.position, dt * 60 * 0.04); 30 | this._camera.quaternion.slerp(this._zoomObject.quaternion, dt * 60 * 0.04); 31 | } 32 | 33 | onWheelScroll(delta) { 34 | if (DEBUG_CONFIG.orbitControls) { 35 | return; 36 | } 37 | 38 | const zoomDelta = delta * CAMERA_CONTROLLER_CONFIG.zoomSpeed; 39 | const minDistance = this._minDistance; 40 | const maxDistance = CAMERA_CONTROLLER_CONFIG.maxDistance; 41 | 42 | this._zoomDistance += zoomDelta; 43 | this._zoomDistance = THREE.MathUtils.clamp(this._zoomDistance, minDistance, maxDistance); 44 | 45 | const cursorRotationCoeff = minDistance - (THREE.MathUtils.clamp(this._zoomDistance, minDistance, maxDistance) - minDistance); 46 | 47 | GAME_BOY_CONFIG.rotation.cursorRotationSpeed = 0.2 - (cursorRotationCoeff / minDistance) * 0.2; 48 | 49 | this._zoomObject.position.z = this._zoomDistance; 50 | this._zoomObject.position.y = (-this._zoomObject.position.z + maxDistance - 0.4) * 0.13; 51 | 52 | const zoomPercent = 1 - (this._zoomDistance - minDistance) / (maxDistance - minDistance); 53 | this.events.post('onZoom', zoomPercent); 54 | 55 | if (cursorRotationCoeff > GAME_BOY_CONFIG.rotation.zoomThresholdToDisableRotation) { 56 | GAME_BOY_CONFIG.rotation.rotationDragEnabled = false; 57 | 58 | if (this._rotationDragPreviousState !== GAME_BOY_CONFIG.rotation.rotationDragEnabled) { 59 | this._rotationDragPreviousState = GAME_BOY_CONFIG.rotation.rotationDragEnabled; 60 | 61 | this.events.post('onRotationDragDisabled'); 62 | } 63 | } else { 64 | GAME_BOY_CONFIG.rotation.rotationDragEnabled = true; 65 | 66 | if (this._rotationDragPreviousState !== GAME_BOY_CONFIG.rotation.rotationDragEnabled) { 67 | this._rotationDragPreviousState = GAME_BOY_CONFIG.rotation.rotationDragEnabled; 68 | 69 | } 70 | } 71 | } 72 | 73 | zoomIn() { 74 | for (let i = 0; i < 10; i++) { 75 | this.onWheelScroll(-1); 76 | } 77 | } 78 | 79 | zoomOut() { 80 | for (let i = 0; i < 10; i++) { 81 | this.onWheelScroll(1); 82 | } 83 | } 84 | 85 | _init() { 86 | this._zoomObject.position.copy(this._camera.position); 87 | 88 | if (DEBUG_CONFIG.startState.zoomIn) { 89 | this.zoomIn(); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/cartridges/cartridge.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import Loader from '../../../core/loader'; 3 | import { CARTRIDGES_BY_TYPE_CONFIG } from './data/cartridges-config'; 4 | import { SCENE_OBJECT_TYPE } from '../data/game-boy-scene-data'; 5 | 6 | export default class Cartridge extends THREE.Group { 7 | constructor(type) { 8 | super(); 9 | 10 | this._type = type; 11 | this._config = CARTRIDGES_BY_TYPE_CONFIG[type]; 12 | this._sceneObjectType = SCENE_OBJECT_TYPE.Cartridges; 13 | this._isInserted = false; 14 | 15 | this._mesh = null; 16 | 17 | this._init(); 18 | } 19 | 20 | getMesh() { 21 | return this._mesh; 22 | } 23 | 24 | getType() { 25 | return this._type; 26 | } 27 | 28 | disableActivity() { 29 | this._mesh.userData['isActive'] = false; 30 | } 31 | 32 | enableActivity() { 33 | this._mesh.userData['isActive'] = true; 34 | } 35 | 36 | setInserted() { 37 | this._isInserted = true; 38 | } 39 | 40 | setNotInserted() { 41 | this._isInserted = false; 42 | } 43 | 44 | isInserted() { 45 | return this._isInserted; 46 | } 47 | 48 | setStandardTexture() { 49 | this._mesh.material.map = this._standardTexture; 50 | } 51 | 52 | setInPocketTexture() { 53 | this._mesh.material.map = this._inPocketTexture; 54 | } 55 | 56 | _init() { 57 | const model = Loader.assets['game-boy-cartridge'].scene.clone(); 58 | this.add(model); 59 | 60 | const standardTexture = this._standardTexture = Loader.assets[this._config.texture]; 61 | standardTexture.flipY = false; 62 | 63 | const inPocketTexture = this._inPocketTexture = Loader.assets[this._config.textureInPocket]; 64 | inPocketTexture.flipY = false; 65 | 66 | const material = new THREE.MeshBasicMaterial({ 67 | map: standardTexture, 68 | }); 69 | 70 | const mesh = this._mesh = model.children[0]; 71 | mesh.material = material; 72 | 73 | mesh.userData['isActive'] = true; 74 | mesh.userData['sceneObjectType'] = this._sceneObjectType; 75 | mesh.userData['partType'] = this._type; 76 | mesh.userData['showOutline'] = true; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/cartridges/cartridges-controller.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { TWEEN } from '/node_modules/three/examples/jsm/libs/tween.module.min.js'; 3 | import Cartridge from './cartridge'; 4 | import { CARTRIDGES_CONFIG, CARTRIDGE_TYPE } from './data/cartridges-config'; 5 | import { MessageDispatcher } from 'black-engine'; 6 | import Delayed from '../../../core/helpers/delayed-call'; 7 | import { GAME_BOY_CONFIG } from '../game-boy/data/game-boy-config'; 8 | 9 | export default class CartridgesController extends THREE.Group { 10 | constructor() { 11 | super(); 12 | 13 | this.events = new MessageDispatcher(); 14 | 15 | this._cartridges = {}; 16 | this._cartridgesArray = []; 17 | 18 | this._timeByCartridgeType = {}; 19 | this._showCartridgeObjects = {}; 20 | this._showCartridgeTween = {}; 21 | this._isCartridgeShown = {}; 22 | this._positionObject = {}; 23 | this._cartridgeDisableFloating = {}; 24 | this._insertedCartridge = null; 25 | 26 | this._isInsertingActive = false; 27 | this._isEjectingActive = false; 28 | 29 | this._init(); 30 | } 31 | 32 | update(dt) { 33 | this._cartridgesArray.forEach(cartridge => { 34 | const cartridgeType = cartridge.getType(); 35 | 36 | if (!this._cartridgeDisableFloating[cartridgeType]) { 37 | const floatingConfig = CARTRIDGES_CONFIG.floating[cartridgeType]; 38 | this._timeByCartridgeType[cartridgeType] += dt; 39 | 40 | cartridge.rotation.z = Math.sin(this._timeByCartridgeType[cartridgeType] * floatingConfig.speed * 0.5) * floatingConfig.rotation.z * THREE.MathUtils.DEG2RAD; 41 | 42 | this._positionObject[cartridgeType].position.y = cartridge.startPosition.y + Math.sin(this._timeByCartridgeType[cartridgeType] * floatingConfig.speed) * floatingConfig.amplitude; 43 | cartridge.position.lerp(this._positionObject[cartridgeType].position, 0.1); 44 | } 45 | }); 46 | } 47 | 48 | getAllMeshes() { 49 | const allMeshes = []; 50 | 51 | this._cartridgesArray.forEach(cartridge => { 52 | allMeshes.push(cartridge.getMesh()); 53 | }); 54 | 55 | return allMeshes; 56 | } 57 | 58 | onPointerDown(mesh) { 59 | this._disableCartridges(); 60 | 61 | const cartridgeType = mesh.userData['partType']; 62 | const cartridge = this._cartridges[cartridgeType]; 63 | 64 | const insertedCartridge = this._checkIsCartridgeInserted(cartridge); 65 | 66 | if (insertedCartridge !== null) { 67 | this.events.post('onCartridgeEjecting'); 68 | this._moveCartridgeFromGameBoy(insertedCartridge, 7, 200); 69 | 70 | this.events.post('onCartridgeInserting'); 71 | this._moveCartridgeToGameBoy(cartridge, 3.2); 72 | } else { 73 | 74 | if (!cartridge.isInserted()) { 75 | this.events.post('onCartridgeInserting'); 76 | this._moveCartridgeToGameBoy(cartridge, 5); 77 | } else { 78 | this.events.post('onCartridgeEjecting'); 79 | this._moveCartridgeFromGameBoy(cartridge, 5, 400); 80 | } 81 | } 82 | } 83 | 84 | onZoomChanged(zoomPercent) { 85 | this._cartridgesArray.forEach(cartridge => { 86 | const cartridgeType = cartridge.getType(); 87 | this._positionObject[cartridgeType].position.x = cartridge.startPosition.x - 1.8 * zoomPercent; 88 | }); 89 | } 90 | 91 | ejectCartridge() { 92 | if (this._insertedCartridge) { 93 | const mesh = this._insertedCartridge.getMesh(); 94 | this.onPointerDown(mesh); 95 | } 96 | } 97 | 98 | insertCartridge(cartridgeType) { 99 | if (this._insertedCartridge) { 100 | const insertedCartridgeType = this._insertedCartridge.getType(); 101 | 102 | if (insertedCartridgeType !== cartridgeType) { 103 | const mesh = this._cartridges[cartridgeType].getMesh(); 104 | this.onPointerDown(mesh); 105 | } 106 | } else { 107 | const mesh = this._cartridges[cartridgeType].getMesh(); 108 | this.onPointerDown(mesh); 109 | } 110 | } 111 | 112 | _checkIsCartridgeInserted(clickedCartridge) { 113 | let isCartridgeInserted = null; 114 | 115 | this._cartridgesArray.forEach(cartridge => { 116 | if (cartridge.isInserted() && cartridge.getType() !== clickedCartridge.getType()) { 117 | isCartridgeInserted = cartridge; 118 | } 119 | }); 120 | 121 | return isCartridgeInserted; 122 | } 123 | 124 | _moveCartridgeToGameBoy(cartridge, speed) { 125 | const cartridgeType = cartridge.getType(); 126 | cartridge.setInserted(); 127 | 128 | this._insertedCartridge = cartridge; 129 | this._isInsertingActive = true; 130 | 131 | this._cartridgeDisableFloating[cartridgeType] = true; 132 | cartridge.lastRotation = cartridge.rotation.clone(); 133 | 134 | const positions = CARTRIDGES_CONFIG.positions.insert; 135 | const distance = cartridge.position.distanceTo(positions.beforeInsert); 136 | const time = distance / (speed * 0.001); 137 | 138 | new TWEEN.Tween(cartridge.position) 139 | .to({ 140 | x: [positions.middle.x, positions.beforeInsert.x], 141 | y: [positions.middle.y, positions.beforeInsert.y], 142 | z: [positions.middle.z, positions.beforeInsert.z], 143 | }, time) 144 | .interpolation(TWEEN.Interpolation.Bezier) 145 | .easing(TWEEN.Easing.Sinusoidal.Out) 146 | .start() 147 | .onComplete(() => { 148 | new TWEEN.Tween(cartridge.position) 149 | .to({ x: positions.slot.x, y: positions.slot.y, z: positions.slot.z }, 400) 150 | .easing(TWEEN.Easing.Back.In) 151 | .delay(100) 152 | .start() 153 | .onComplete(() => { 154 | this._onCartridgeInserted(cartridge); 155 | }); 156 | 157 | Delayed.call(200, () => this.events.post('cartridgeInsertSound')); 158 | }); 159 | 160 | new TWEEN.Tween(cartridge.rotation) 161 | .to({ x: 0, y: Math.PI, z: 0 }, time) 162 | .easing(TWEEN.Easing.Quartic.Out) 163 | .start(); 164 | } 165 | 166 | _moveCartridgeFromGameBoy(cartridge, speed, ejectTime) { 167 | const cartridgeType = cartridge.getType(); 168 | cartridge.setNotInserted(); 169 | 170 | this._isEjectingActive = true; 171 | GAME_BOY_CONFIG.currentCartridge = 'NONE'; 172 | this._insertedCartridge = null; 173 | this.events.post('cartridgeTypeChanged'); 174 | this.events.post('cartridgeEjectSound'); 175 | this.events.post('cartridgeStartEjecting'); 176 | 177 | const positions = CARTRIDGES_CONFIG.positions.eject; 178 | const floatingConfig = CARTRIDGES_CONFIG.floating[cartridgeType]; 179 | 180 | cartridge.setStandardTexture(); 181 | 182 | const moveTween = new TWEEN.Tween(cartridge.position) 183 | .to({ x: positions.beforeEject.x, y: positions.beforeEject.y, z: positions.beforeEject.z }, ejectTime) 184 | .easing(TWEEN.Easing.Sinusoidal.Out) 185 | .delay(400) 186 | .start() 187 | .onComplete(() => { 188 | const distance = cartridge.position.distanceTo(floatingConfig.startPosition); 189 | const time = distance / (speed * 0.001); 190 | 191 | new TWEEN.Tween(cartridge.position) 192 | .to({ 193 | x: [positions.middle.x, floatingConfig.startPosition.x], 194 | y: [positions.middle.y, floatingConfig.startPosition.y], 195 | z: [positions.middle.z, floatingConfig.startPosition.z], 196 | }, time) 197 | .interpolation(TWEEN.Interpolation.Bezier) 198 | .easing(TWEEN.Easing.Sinusoidal.Out) 199 | .start() 200 | .onComplete(() => { 201 | cartridge.position.copy(floatingConfig.startPosition); 202 | this._onCartridgeEjected(cartridgeType); 203 | }); 204 | 205 | new TWEEN.Tween(cartridge.rotation) 206 | .to({ 207 | x: cartridge.lastRotation.x, 208 | y: cartridge.lastRotation.y, 209 | z: cartridge.lastRotation.z, 210 | }, time) 211 | .easing(TWEEN.Easing.Quartic.Out) 212 | .start(); 213 | }) 214 | 215 | moveTween.onStart(() => { 216 | this.add(cartridge); 217 | }); 218 | } 219 | 220 | _onCartridgeInserted(cartridge) { 221 | this._isInsertingActive = false; 222 | const cartridgeType = cartridge.getType(); 223 | GAME_BOY_CONFIG.currentCartridge = cartridgeType; 224 | this.events.post('cartridgeTypeChanged'); 225 | this.events.post('onCartridgeInserted', cartridge); 226 | cartridge.setInPocketTexture(); 227 | 228 | this._enableCartridges(); 229 | } 230 | 231 | _onCartridgeEjected(cartridgeType) { 232 | this._isEjectingActive = false; 233 | this._cartridgeDisableFloating[cartridgeType] = false; 234 | this._enableCartridges(); 235 | this.events.post('onCartridgeEjected'); 236 | } 237 | 238 | _disableCartridges() { 239 | this._cartridgesArray.forEach(cartridge => { 240 | cartridge.disableActivity(); 241 | }); 242 | } 243 | 244 | _enableCartridges() { 245 | if (this._isInsertingActive || this._isEjectingActive) { 246 | return; 247 | } 248 | 249 | this._cartridgesArray.forEach(cartridge => { 250 | cartridge.enableActivity(); 251 | }); 252 | } 253 | 254 | onPointerOver(object) { } 255 | 256 | onPointerOut() { } 257 | 258 | _moveOtherCartridgesToStartPosition(cartridgeType) { 259 | for (const type in this._isCartridgeShown) { 260 | if (type !== cartridgeType) { 261 | this._moveCartridgeToInitPosition(type); 262 | } 263 | } 264 | } 265 | 266 | _moveCartridgeToInitPosition(cartridgeType) { 267 | if (this._isCartridgeShown[cartridgeType]) { 268 | this._isCartridgeShown[cartridgeType] = false; 269 | this.stopTween(cartridgeType); 270 | 271 | this._showCartridgeTween[cartridgeType] = new TWEEN.Tween(this._showCartridgeObjects[cartridgeType].position) 272 | .to({ y: 0 }, 500) 273 | .easing(TWEEN.Easing.Sinusoidal.Out) 274 | .start(); 275 | } 276 | } 277 | 278 | getOutlineMeshes(object) { 279 | return [object]; 280 | } 281 | 282 | stopTween(cartridgeType) { 283 | if (this._showCartridgeTween[cartridgeType]) { 284 | this._showCartridgeTween[cartridgeType].stop(); 285 | } 286 | } 287 | 288 | _init() { 289 | this._initCartridges(); 290 | this._initShowCartridgeObjects(); 291 | } 292 | 293 | _initCartridges() { 294 | const cartridgesTypes = [ 295 | CARTRIDGE_TYPE.Tetris, 296 | CARTRIDGE_TYPE.Zelda, 297 | CARTRIDGE_TYPE.SpaceInvaders, 298 | ]; 299 | 300 | for (let i = 0; i < cartridgesTypes.length; i++) { 301 | const type = cartridgesTypes[i]; 302 | const config = CARTRIDGES_CONFIG.floating[type]; 303 | 304 | const cartridge = new Cartridge(type); 305 | this.add(cartridge); 306 | 307 | cartridge.position.copy(config.startPosition); 308 | cartridge.startPosition = cartridge.position.clone(); 309 | 310 | cartridge.rotation.y = config.rotation.y * THREE.MathUtils.DEG2RAD; 311 | cartridge.rotation.x = config.rotation.x * THREE.MathUtils.DEG2RAD; 312 | 313 | this._cartridges[type] = cartridge; 314 | this._cartridgesArray.push(cartridge); 315 | } 316 | } 317 | 318 | _initShowCartridgeObjects() { 319 | this._cartridgesArray.forEach(cartridge => { 320 | const cartridgeType = cartridge.getType(); 321 | this._showCartridgeObjects[cartridgeType] = new THREE.Object3D(); 322 | this._isCartridgeShown[cartridgeType] = false; 323 | this._cartridgeDisableFloating[cartridgeType] = false; 324 | this._timeByCartridgeType[cartridgeType] = 0; 325 | this._positionObject[cartridgeType] = new THREE.Object3D(); 326 | this._positionObject[cartridgeType].position.copy(cartridge.position); 327 | }); 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/cartridges/data/cartridges-config.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { GAME_TYPE } from '../../game-boy-games/data/games-config'; 3 | 4 | const CARTRIDGE_TYPE = { 5 | Tetris: 'TETRIS', 6 | Zelda: 'ZELDA', 7 | SpaceInvaders: 'SPACE_INVADERS', 8 | } 9 | 10 | const CARTRIDGES_CONFIG = { 11 | positions: { 12 | insert: { 13 | middle: new THREE.Vector3(-2.6, 3.6, 1.2), 14 | beforeInsert: new THREE.Vector3(0, 2.8, -0.28), 15 | slot: new THREE.Vector3(0, 1.03, -0.28), 16 | }, 17 | eject: { 18 | beforeEject: new THREE.Vector3(0, 2.8, -0.28), 19 | middle: new THREE.Vector3(-2.2, 3.5, -0.3), 20 | } 21 | }, 22 | floating: { 23 | [CARTRIDGE_TYPE.Tetris]: { 24 | startPosition: new THREE.Vector3(-2.8, -1, 0.7), 25 | rotation: new THREE.Vector3(0, 5, 2), 26 | amplitude: 0.05, 27 | speed: 0.3, 28 | }, 29 | [CARTRIDGE_TYPE.Zelda]: { 30 | startPosition: new THREE.Vector3(-3.3, 0.1, 0.2), 31 | rotation: new THREE.Vector3(-3, 0, -1), 32 | amplitude: 0.03, 33 | speed: 0.4, 34 | }, 35 | [CARTRIDGE_TYPE.SpaceInvaders]: { 36 | startPosition: new THREE.Vector3(-2.7, 1.2, -0.3), 37 | rotation: new THREE.Vector3(0, -5, -2), 38 | amplitude: 0.04, 39 | speed: 0.5, 40 | }, 41 | } 42 | } 43 | 44 | const CARTRIDGES_BY_TYPE_CONFIG = { 45 | [CARTRIDGE_TYPE.Tetris]: { 46 | texture: 'baked-cartridge-tetris', 47 | textureInPocket: 'baked-cartridge-tetris-in-pocket', 48 | game: GAME_TYPE.Tetris, 49 | }, 50 | [CARTRIDGE_TYPE.Zelda]: { 51 | texture: 'baked-cartridge-zelda', 52 | textureInPocket: 'baked-cartridge-zelda-in-pocket', 53 | game: GAME_TYPE.Zelda, 54 | }, 55 | [CARTRIDGE_TYPE.SpaceInvaders]: { 56 | texture: 'baked-cartridge-space-invaders', 57 | textureInPocket: 'baked-cartridge-space-invaders-in-pocket', 58 | game: GAME_TYPE.SpaceInvaders, 59 | }, 60 | } 61 | 62 | export { 63 | CARTRIDGES_CONFIG, 64 | CARTRIDGES_BY_TYPE_CONFIG, 65 | CARTRIDGE_TYPE, 66 | }; 67 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/data/game-boy-scene-data.js: -------------------------------------------------------------------------------- 1 | const SCENE_OBJECT_TYPE = { 2 | GameBoy: 'GAME_BOY', 3 | Cartridges: 'CARTRIDGES', 4 | Background: 'BACKGROUND', 5 | } 6 | 7 | export { SCENE_OBJECT_TYPE }; 8 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy-games/data/games-classes.js: -------------------------------------------------------------------------------- 1 | import SpaceInvaders from "../games/space-invaders/space-invaders"; 2 | import Tetris from "../games/tetris/tetris"; 3 | import Zelda from "../games/zelda/zelda"; 4 | import { GAME_TYPE } from "./games-config"; 5 | 6 | const GAMES_CLASSES = { 7 | [GAME_TYPE.Tetris]: Tetris, 8 | [GAME_TYPE.Zelda]: Zelda, 9 | [GAME_TYPE.SpaceInvaders]: SpaceInvaders, 10 | } 11 | 12 | export { GAMES_CLASSES }; 13 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy-games/data/games-config.js: -------------------------------------------------------------------------------- 1 | const GAME_TYPE = { 2 | Tetris: 'TETRIS', 3 | Zelda: 'ZELDA', 4 | SpaceInvaders: 'SPACE_INVADERS', 5 | } 6 | 7 | export { GAME_TYPE }; 8 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy-games/game-boy-games.js: -------------------------------------------------------------------------------- 1 | import * as PIXI from 'pixi.js'; 2 | import LoadingScreen from './screens/loading-screen'; 3 | import { GAME_BOY_CONFIG } from '../game-boy/data/game-boy-config'; 4 | import { TWEEN } from '/node_modules/three/examples/jsm/libs/tween.module.min.js'; 5 | import NoCartridgeScreen from './screens/no-cartridge-screen'; 6 | import DamagedCartridgeScreen from './screens/damaged-cartridge-screen'; 7 | import { GAME_TYPE } from './data/games-config'; 8 | import { GAMES_CLASSES } from './data/games-classes'; 9 | import VolumeOverlay from './overlay/volume-overlay'; 10 | import GameBoyAudio from '../game-boy/game-boy-audio/game-boy-audio'; 11 | import { SOUNDS_CONFIG } from '../../../core/configs/sounds-config'; 12 | import { MessageDispatcher } from 'black-engine'; 13 | import DEBUG_CONFIG from '../../../core/configs/debug-config'; 14 | 15 | export default class GameBoyGames { 16 | constructor(application) { 17 | 18 | this.events = new MessageDispatcher(); 19 | 20 | this._application = application; 21 | 22 | this._container = null; 23 | this._loadingScreen = null; 24 | this._noCartridgeScreen = null; 25 | this._damagedCartridgeScreen = null; 26 | this._volumeOverlay = null; 27 | this._allScreens = []; 28 | this._games = {}; 29 | this._powerOffTween = null; 30 | this._isUpdateEnabled = GAME_BOY_CONFIG.powerOn; 31 | 32 | this._gameType = null; 33 | 34 | this._init(); 35 | } 36 | 37 | update(dt) { 38 | if (!this._isUpdateEnabled) { 39 | return; 40 | } 41 | 42 | if (this._gameType !== null && this._games[this._gameType].visible) { 43 | this._games[this._gameType].update(dt); 44 | } 45 | } 46 | 47 | onPowerOn() { 48 | GAME_BOY_CONFIG.updateTexture = true; 49 | this._isUpdateEnabled = true; 50 | 51 | this._hideAllScreens(); 52 | this._hideAllGames(); 53 | this._stopPowerOffTween(); 54 | 55 | this._container.alpha = 1; 56 | this._container.visible = true; 57 | 58 | if (DEBUG_CONFIG.startState.loadGame) { 59 | this.setGame(DEBUG_CONFIG.startState.loadGame); 60 | this.startGame(); 61 | } else { 62 | this._loadingScreen.show(); 63 | } 64 | } 65 | 66 | onPowerOff() { 67 | this._isUpdateEnabled = false; 68 | 69 | this._stopPowerOffTween(); 70 | this._allScreens.forEach(screen => screen.stopTweens()); 71 | 72 | if (this._gameType) { 73 | this._games[this._gameType].stopTweens(); 74 | } 75 | 76 | this._powerOffTween = new TWEEN.Tween(this._container) 77 | .to({ alpha: 0 }, 500) 78 | .easing(TWEEN.Easing.Sinusoidal.Out) 79 | .start() 80 | .onComplete(() => { 81 | this._container.visible = false; 82 | GAME_BOY_CONFIG.updateTexture = false; 83 | 84 | if (this._gameType) { 85 | this._hideCurrentGame(); 86 | } 87 | }); 88 | } 89 | 90 | onVolumeChanged() { 91 | if (GAME_BOY_CONFIG.powerOn) { 92 | this._volumeOverlay.onVolumeChanged(); 93 | } 94 | 95 | const gameBoyVolume = SOUNDS_CONFIG.gameBoyVolume; 96 | GameBoyAudio.changeGameBoyVolume(gameBoyVolume); 97 | } 98 | 99 | onButtonPress(buttonType) { 100 | if (!GAME_BOY_CONFIG.powerOn) { 101 | return; 102 | } 103 | 104 | if (this._gameType !== null) { 105 | this._games[this._gameType].onButtonPress(buttonType); 106 | } 107 | } 108 | 109 | onButtonUp(buttonType) { 110 | if (!GAME_BOY_CONFIG.powerOn) { 111 | return; 112 | } 113 | 114 | if (this._gameType !== null) { 115 | this._games[this._gameType].onButtonUp(buttonType); 116 | } 117 | } 118 | 119 | setGame(gameType) { 120 | this._gameType = gameType; 121 | } 122 | 123 | setNoGame() { 124 | this._gameType = null; 125 | } 126 | 127 | startGame() { 128 | this._showCurrentGame(); 129 | } 130 | 131 | restartTetris(level) { 132 | this._games[GAME_TYPE.Tetris].startGameAtLevel(level); 133 | } 134 | 135 | disableTetrisFalling() { 136 | this._games[GAME_TYPE.Tetris].disableFalling(); 137 | } 138 | 139 | clearTetrisBottomLine() { 140 | this._games[GAME_TYPE.Tetris].clearBottomLine(); 141 | } 142 | 143 | _showCurrentGame() { 144 | this._games[this._gameType].show(); 145 | this.events.post('gameStarted', this._gameType); 146 | } 147 | 148 | _hideCurrentGame() { 149 | this._games[this._gameType].hide(); 150 | this.events.post('gameStopped', this._gameType); 151 | } 152 | 153 | _hideAllGames() { 154 | for (const gameType in this._games) { 155 | this._games[gameType].hide(); 156 | } 157 | } 158 | 159 | _hideAllScreens() { 160 | this._allScreens.forEach(screen => screen.hide()); 161 | } 162 | 163 | _stopPowerOffTween() { 164 | if (this._powerOffTween) { 165 | this._powerOffTween.stop(); 166 | } 167 | } 168 | 169 | _init() { 170 | this._initRootContainer(); 171 | this._initScreens(); 172 | this._initGames(); 173 | this._initOverlays(); 174 | this._addColorMatrixFilter(); 175 | 176 | this._initSignals(); 177 | 178 | this._container.scale.set(GAME_BOY_CONFIG.screen.scale); 179 | } 180 | 181 | _initRootContainer() { 182 | const container = this._container = new PIXI.Container(); 183 | this._application.stage.addChild(container); 184 | } 185 | 186 | _initScreens() { 187 | this._initLoadingScreen(); 188 | this._initNoCartridgeScreen(); 189 | this._initDamagedCartridgeScreen(); 190 | 191 | this._allScreens = [ 192 | this._loadingScreen, 193 | this._noCartridgeScreen, 194 | this._damagedCartridgeScreen, 195 | ]; 196 | } 197 | 198 | _initOverlays() { 199 | this._initVolumeOverlay(); 200 | } 201 | 202 | _addColorMatrixFilter() { 203 | const brightness = 0.2; 204 | 205 | const tint = 0x646e3c; 206 | const r = tint >> 16 & 0xFF; 207 | const g = tint >> 8 & 0xFF; 208 | const b = tint & 0xFF; 209 | 210 | const colorMatrix = [ 211 | r / 255, 0, 0, 0, brightness, 212 | 0, g / 255, 0, 0, brightness, 213 | 0, 0, b / 255, 0, brightness, 214 | 0, 0, 0, 1, 0 215 | ]; 216 | 217 | const filter = new PIXI.ColorMatrixFilter(); 218 | filter.matrix = colorMatrix; 219 | this._container.filters = [filter]; 220 | } 221 | 222 | _initVolumeOverlay() { 223 | const volumeOverlay = this._volumeOverlay = new VolumeOverlay(); 224 | this._container.addChild(volumeOverlay); 225 | 226 | volumeOverlay.x = GAME_BOY_CONFIG.screen.width * 0.5; 227 | volumeOverlay.y = GAME_BOY_CONFIG.screen.height - 15; 228 | } 229 | 230 | _initGames() { 231 | const activeGames = [ 232 | GAME_TYPE.Tetris, 233 | GAME_TYPE.Zelda, 234 | GAME_TYPE.SpaceInvaders, 235 | ]; 236 | 237 | activeGames.forEach(gameType => { 238 | const gameClass = GAMES_CLASSES[gameType]; 239 | 240 | if (gameClass) { 241 | const game = new gameClass(); 242 | this._container.addChild(game); 243 | 244 | this._games[gameType] = game; 245 | } else { 246 | this._games[gameType] = this._damagedCartridgeScreen; 247 | } 248 | }); 249 | } 250 | 251 | _initLoadingScreen() { 252 | const loadingScreen = this._loadingScreen = new LoadingScreen(); 253 | this._container.addChild(loadingScreen); 254 | } 255 | 256 | _initNoCartridgeScreen() { 257 | const noCartridgeScreen = this._noCartridgeScreen = new NoCartridgeScreen(); 258 | this._container.addChild(noCartridgeScreen); 259 | } 260 | 261 | _initDamagedCartridgeScreen() { 262 | const damagedCartridgeScreen = this._damagedCartridgeScreen = new DamagedCartridgeScreen(); 263 | this._container.addChild(damagedCartridgeScreen); 264 | } 265 | 266 | _initSignals() { 267 | this._loadingScreen.events.on('onComplete', () => this._onLoadingComplete()); 268 | this._games[GAME_TYPE.Tetris].events.on('onBestScoreChange', () => this.events.post('onTetrisBestScoreChange')); 269 | this._games[GAME_TYPE.SpaceInvaders].events.on('onBestScoreChange', () => this.events.post('onSpaceInvadersBestScoreChange')); 270 | } 271 | 272 | _onLoadingComplete() { 273 | if (!GAME_BOY_CONFIG.powerOn) { 274 | return; 275 | } 276 | 277 | if (this._gameType === null) { 278 | this._noCartridgeScreen.show(); 279 | } else { 280 | this.startGame(); 281 | } 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy-games/games/game-abstract.js: -------------------------------------------------------------------------------- 1 | import * as PIXI from 'pixi.js'; 2 | 3 | export default class GameAbstract extends PIXI.Container { 4 | constructor() { 5 | super(); 6 | } 7 | 8 | update(dt) { } 9 | 10 | show() { 11 | this.visible = true; 12 | } 13 | 14 | hide() { 15 | this.visible = false; 16 | } 17 | 18 | stopTweens() {} 19 | 20 | onButtonPress(buttonType) { } 21 | 22 | onButtonUp(buttonType) { } 23 | } 24 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy-games/games/shared/game-screen-abstract.js: -------------------------------------------------------------------------------- 1 | import * as PIXI from 'pixi.js'; 2 | 3 | export default class GameScreenAbstract extends PIXI.Container { 4 | constructor() { 5 | super(); 6 | 7 | this.events = new PIXI.utils.EventEmitter(); 8 | 9 | this._screenType = null; 10 | 11 | this.visible = false; 12 | } 13 | 14 | show() { 15 | this.visible = true; 16 | } 17 | 18 | hide() { 19 | this.visible = false; 20 | } 21 | 22 | getScreenType() { 23 | return this._screenType; 24 | } 25 | 26 | update(dt) { } 27 | 28 | onButtonPress(buttonType) { } 29 | 30 | onButtonUp(buttonType) { } 31 | 32 | reset() { } 33 | 34 | stopTweens() { } 35 | } 36 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy-games/games/space-invaders/data/space-invaders-config.js: -------------------------------------------------------------------------------- 1 | import { CARTRIDGE_STATE } from "../../../../game-boy/data/game-boy-data"; 2 | 3 | const SPACE_INVADERS_CONFIG = { 4 | cartridgeState: CARTRIDGE_STATE.NotInserted, 5 | player: { 6 | speed: 2, 7 | reloadTime: 300, 8 | livesAtStart: 3, 9 | }, 10 | field: { 11 | width: 158, 12 | height: 130, 13 | }, 14 | currentRound: 1, 15 | bestScore: 0, 16 | playerInvincible: false, 17 | } 18 | 19 | export { SPACE_INVADERS_CONFIG }; 20 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy-games/games/space-invaders/data/space-invaders-data.js: -------------------------------------------------------------------------------- 1 | const SPACE_INVADERS_SCREEN_TYPE = { 2 | Title: 'TITLE', 3 | Gameplay: 'GAMEPLAY', 4 | Round: 'ROUND', 5 | GameOver: 'GAME_OVER', 6 | } 7 | 8 | const PLAYER_MOVEMENT_STATE = { 9 | Left: 'LEFT', 10 | Right: 'RIGHT', 11 | None: 'NONE', 12 | } 13 | 14 | export { 15 | SPACE_INVADERS_SCREEN_TYPE, 16 | PLAYER_MOVEMENT_STATE, 17 | }; 18 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy-games/games/space-invaders/screens/game-over-screen.js: -------------------------------------------------------------------------------- 1 | import * as PIXI from 'pixi.js'; 2 | import { GAME_BOY_CONFIG } from "../../../../game-boy/data/game-boy-config"; 3 | import GameScreenAbstract from "../../shared/game-screen-abstract"; 4 | import Delayed from '../../../../../../core/helpers/delayed-call'; 5 | 6 | export default class GameOverScreen extends GameScreenAbstract { 7 | constructor() { 8 | super(); 9 | 10 | this._timer = null; 11 | 12 | this._init(); 13 | } 14 | 15 | show() { 16 | super.show(); 17 | 18 | this._timer = Delayed.call(2000, () => { 19 | this.events.emit('onGameOverEnd'); 20 | }); 21 | } 22 | 23 | stopTweens() { 24 | if (this._timer) { 25 | this._timer.stop(); 26 | } 27 | } 28 | 29 | _init() { 30 | const text = new PIXI.Text('GAME OVER', new PIXI.TextStyle({ 31 | fontFamily: 'dogicapixel', 32 | fontSize: 8, 33 | fill: 0x000000, 34 | })); 35 | 36 | this.addChild(text); 37 | 38 | text.x = GAME_BOY_CONFIG.screen.width * 0.5 - 30; 39 | text.y = GAME_BOY_CONFIG.screen.height * 0.5 - 4; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy-games/games/space-invaders/screens/gameplay-screen/enemies-controller/data/enemy-config.js: -------------------------------------------------------------------------------- 1 | const ENEMY_CONFIG = { 2 | rows: 5, 3 | columns: 8, 4 | } 5 | 6 | const ENEMY_TYPE = { 7 | Enemy01: 'Enemy01', 8 | Enemy02: 'Enemy02', 9 | } 10 | 11 | const ENEMIES_CONFIG = { 12 | [ENEMY_TYPE.Enemy01]: { 13 | textures: [ 14 | 'enemy01-frame01.png', 15 | 'enemy01-frame02.png', 16 | ], 17 | score: 10, 18 | }, 19 | [ENEMY_TYPE.Enemy02]: { 20 | textures: [ 21 | 'enemy01-frame01.png', 22 | 'enemy01-frame02.png', 23 | ], 24 | score: 20, 25 | }, 26 | } 27 | 28 | const ENEMY_MOVEMENT_DIRECTION = { 29 | Left: 'LEFT', 30 | Right: 'RIGHT', 31 | } 32 | 33 | export { 34 | ENEMIES_CONFIG, 35 | ENEMY_TYPE, 36 | ENEMY_CONFIG, 37 | ENEMY_MOVEMENT_DIRECTION, 38 | }; 39 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy-games/games/space-invaders/screens/gameplay-screen/enemies-controller/enemies-controller.js: -------------------------------------------------------------------------------- 1 | import * as PIXI from 'pixi.js'; 2 | import { ENEMY_CONFIG, ENEMY_MOVEMENT_DIRECTION, ENEMY_TYPE } from './data/enemy-config'; 3 | import Enemy from './enemy'; 4 | import Delayed from '../../../../../../../../core/helpers/delayed-call'; 5 | import { SPACE_INVADERS_CONFIG } from '../../../data/space-invaders-config'; 6 | 7 | export default class EnemiesController extends PIXI.Container { 8 | constructor() { 9 | super(); 10 | 11 | this.events = new PIXI.utils.EventEmitter(); 12 | 13 | this._movementDirection = ENEMY_MOVEMENT_DIRECTION.Right; 14 | this._previousMovementDirection = ENEMY_MOVEMENT_DIRECTION.Right; 15 | this._enemies = []; 16 | this._removeEnemyTimers = []; 17 | this._showEnemiesTimers = []; 18 | } 19 | 20 | update(dt) { 21 | for (let row = 0; row < ENEMY_CONFIG.rows; row++) { 22 | for (let column = 0; column < ENEMY_CONFIG.columns; column++) { 23 | const enemy = this._enemies[row][column]; 24 | 25 | if (enemy) { 26 | enemy.update(dt); 27 | } 28 | } 29 | } 30 | } 31 | 32 | spawnEnemies() { 33 | this._createEnemies(); 34 | this._showEnemies(); 35 | } 36 | 37 | getEnemies() { 38 | return this._enemies; 39 | } 40 | 41 | stopTweens() { 42 | for (let i = 0; i < this._removeEnemyTimers.length; i++) { 43 | const timer = this._removeEnemyTimers[i]; 44 | 45 | if (timer) { 46 | timer.stop(); 47 | } 48 | } 49 | 50 | this._removeEnemyTimers = []; 51 | 52 | for (let i = 0; i < this._showEnemiesTimers.length; i++) { 53 | const timer = this._showEnemiesTimers[i]; 54 | 55 | if (timer) { 56 | timer.stop(); 57 | } 58 | } 59 | 60 | this._showEnemiesTimers = []; 61 | } 62 | 63 | reset() { 64 | this.stopTweens(); 65 | 66 | if (this._enemies.length !== 0) { 67 | for (let row = 0; row < ENEMY_CONFIG.rows; row++) { 68 | for (let column = 0; column < ENEMY_CONFIG.columns; column++) { 69 | const enemy = this._enemies[row][column]; 70 | 71 | if (enemy) { 72 | this.removeChild(enemy); 73 | } 74 | } 75 | } 76 | 77 | this._enemies = []; 78 | } 79 | 80 | this._previousMovementDirection = ENEMY_MOVEMENT_DIRECTION.Right; 81 | } 82 | 83 | removeEnemy(enemy) { 84 | enemy.kill(); 85 | 86 | const removeEnemyTimer = Delayed.call(300, () => { 87 | const row = this._enemies.findIndex(enemies => enemies.includes(enemy)); 88 | const column = this._enemies[row].findIndex(item => item === enemy); 89 | 90 | this.removeChild(enemy); 91 | 92 | this._enemies[row][column] = null; 93 | }); 94 | 95 | this._removeEnemyTimers.push(removeEnemyTimer); 96 | 97 | if (!this._checkIsAnyEnemyAlive()) { 98 | this.events.emit('allEnemiesKilled'); 99 | } 100 | } 101 | 102 | updateBottomEnemies() { 103 | for (let column = 0; column < this._enemies[0].length; column++) { 104 | for (let row = this._enemies.length - 1; row >= 0; row--) { 105 | const enemy = this._enemies[row][column]; 106 | 107 | if (enemy && enemy.isActive()) { 108 | enemy.enableShooting(); 109 | // enemy.setTint(0xff0000); 110 | break; 111 | } 112 | } 113 | } 114 | } 115 | 116 | _checkIsAnyEnemyAlive() { 117 | for (let row = 0; row < ENEMY_CONFIG.rows; row++) { 118 | for (let column = 0; column < ENEMY_CONFIG.columns; column++) { 119 | const enemy = this._enemies[row][column]; 120 | 121 | if (enemy && enemy.isActive()) { 122 | return true; 123 | } 124 | } 125 | } 126 | 127 | return false; 128 | } 129 | 130 | _createEnemies() { 131 | for (let row = 0; row < ENEMY_CONFIG.rows; row++) { 132 | this._enemies.push([]); 133 | 134 | for (let column = 0; column < ENEMY_CONFIG.columns; column++) { 135 | const enemy = new Enemy(ENEMY_TYPE.Enemy01); 136 | this.addChild(enemy); 137 | 138 | enemy.x = 20 + column * 16; 139 | enemy.y = 32 + row * 8; 140 | 141 | this._enemies[row].push(enemy); 142 | 143 | this._initSignals(enemy); 144 | } 145 | } 146 | } 147 | 148 | _showEnemies() { 149 | const delay = 15; 150 | 151 | let index = 0; 152 | 153 | for (let row = ENEMY_CONFIG.rows - 1; row >= 0; row--) { 154 | for (let column = ENEMY_CONFIG.columns - 1; column >= 0; column--) { 155 | const timer = Delayed.call(delay * index, () => { 156 | const enemy = this._enemies[row][column]; 157 | enemy.activate(); 158 | enemy.show(); 159 | }); 160 | 161 | this._showEnemiesTimers.push(timer); 162 | 163 | index++; 164 | } 165 | } 166 | 167 | Delayed.call(delay * index, () => { 168 | this.updateBottomEnemies(); 169 | }); 170 | } 171 | 172 | _initSignals(enemy) { 173 | enemy.events.on('changeDirectionToLeft', () => this._onChangeDirection(ENEMY_MOVEMENT_DIRECTION.Left)); 174 | enemy.events.on('changeDirectionToRight', () => this._onChangeDirection(ENEMY_MOVEMENT_DIRECTION.Right)); 175 | enemy.events.on('shoot', () => this.events.emit('enemyShoot', enemy)); 176 | } 177 | 178 | _onChangeDirection(direction) { 179 | this._movementDirection = direction; 180 | 181 | if (this._previousMovementDirection === this._movementDirection) { 182 | return; 183 | } 184 | 185 | this._previousMovementDirection = this._movementDirection; 186 | 187 | for (let row = ENEMY_CONFIG.rows - 1; row >= 0; row--) { 188 | for (let column = ENEMY_CONFIG.columns - 1; column >= 0; column--) { 189 | const enemy = this._enemies[row][column]; 190 | 191 | if (enemy) { 192 | enemy.setDirection(direction); 193 | enemy.moveDown(); 194 | enemy.increaseSpeed(); 195 | 196 | this._checkEnemyReachedBottom(enemy); 197 | } 198 | } 199 | } 200 | } 201 | 202 | _checkEnemyReachedBottom(enemy) { 203 | if (enemy.y >= SPACE_INVADERS_CONFIG.field.height - enemy.height + 6) { 204 | this.events.emit('enemyReachedBottom'); 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy-games/games/space-invaders/screens/gameplay-screen/enemies-controller/enemy.js: -------------------------------------------------------------------------------- 1 | import * as PIXI from 'pixi.js'; 2 | import Loader from '../../../../../../../../core/loader'; 3 | import { ENEMIES_CONFIG, ENEMY_MOVEMENT_DIRECTION } from './data/enemy-config'; 4 | import { SPACE_INVADERS_CONFIG } from '../../../data/space-invaders-config'; 5 | 6 | export default class Enemy extends PIXI.Container { 7 | constructor(type) { 8 | super(); 9 | 10 | this.events = new PIXI.utils.EventEmitter(); 11 | 12 | this._type = type; 13 | this._config = ENEMIES_CONFIG[type]; 14 | this._view = null; 15 | 16 | this._textureIndex = 0; 17 | this._isActive = false; 18 | 19 | this._speed = 0.5 + SPACE_INVADERS_CONFIG.currentRound * 0.5; 20 | 21 | if (this._speed > 10) { 22 | this._speed = 10; 23 | } 24 | 25 | this._moveTime = 0; 26 | this._moveInterval = 500 / this._speed; 27 | this._moveDirection = ENEMY_MOVEMENT_DIRECTION.Right; 28 | 29 | this._isShootingEnabled = false; 30 | 31 | this._init(); 32 | } 33 | 34 | update(dt) { 35 | if (!this._isActive) { 36 | return; 37 | } 38 | 39 | this._moveTime += dt * 1000; 40 | 41 | if (this._moveTime >= this._moveInterval) { 42 | this._moveTime = 0; 43 | this._move(); 44 | } 45 | 46 | if (this._isShootingEnabled) { 47 | this._checkToShoot(); 48 | } 49 | } 50 | 51 | activate() { 52 | this._isActive = true; 53 | } 54 | 55 | isActive() { 56 | return this._isActive; 57 | } 58 | 59 | show() { 60 | this.visible = true; 61 | } 62 | 63 | getType() { 64 | return this._type; 65 | } 66 | 67 | kill() { 68 | this._isActive = false; 69 | 70 | const spriteSheet = Loader.assets['assets/spritesheets/space-invaders-sheet']; 71 | const texture = spriteSheet.textures['enemy-kill.png']; 72 | this._view.texture = texture; 73 | } 74 | 75 | setDirection(direction) { 76 | this._moveDirection = direction; 77 | } 78 | 79 | moveDown() { 80 | this.y += 12; 81 | } 82 | 83 | increaseSpeed() { 84 | this._speed += 1; 85 | 86 | if (this._speed > 15) { 87 | this._speed = 15; 88 | } 89 | 90 | this._moveInterval = 500 / this._speed; 91 | } 92 | 93 | setTint(color) { 94 | this._view.tint = color; 95 | } 96 | 97 | enableShooting() { 98 | this._isShootingEnabled = true; 99 | } 100 | 101 | _checkToShoot() { 102 | const chance = Math.random() * 1000; 103 | 104 | if (chance > 998) { 105 | this.events.emit('shoot'); 106 | } 107 | } 108 | 109 | _move() { 110 | if (this.x >= SPACE_INVADERS_CONFIG.field.width - this.width) { 111 | this.events.emit('changeDirectionToLeft'); 112 | } 113 | 114 | if (this.x <= 0) { 115 | this.events.emit('changeDirectionToRight'); 116 | } 117 | 118 | if (this._moveDirection === ENEMY_MOVEMENT_DIRECTION.Right) { 119 | this.x += 1; 120 | } 121 | 122 | if (this._moveDirection === ENEMY_MOVEMENT_DIRECTION.Left) { 123 | this.x -= 1; 124 | } 125 | 126 | this._updateTexture(); 127 | } 128 | 129 | _updateTexture() { 130 | this._textureIndex = (this._textureIndex + 1) % this._config.textures.length; 131 | const spriteSheet = Loader.assets['assets/spritesheets/space-invaders-sheet']; 132 | const texture = spriteSheet.textures[this._config.textures[this._textureIndex]]; 133 | this._view.texture = texture; 134 | } 135 | 136 | _init() { 137 | this._initView(); 138 | 139 | this.visible = false; 140 | } 141 | 142 | _initView() { 143 | const spriteSheet = Loader.assets['assets/spritesheets/space-invaders-sheet']; 144 | const texture = spriteSheet.textures[this._config.textures[this._textureIndex]]; 145 | 146 | const view = this._view = new PIXI.Sprite(texture); 147 | this.addChild(view); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy-games/games/space-invaders/screens/gameplay-screen/missile/enemy-missile.js: -------------------------------------------------------------------------------- 1 | import * as PIXI from 'pixi.js'; 2 | import Loader from '../../../../../../../../core/loader'; 3 | import { MISSILES_CONFIG } from './missile-config'; 4 | import { GAME_BOY_CONFIG } from '../../../../../../game-boy/data/game-boy-config'; 5 | 6 | export default class EnemyMissile extends PIXI.Container { 7 | constructor(type) { 8 | super(); 9 | 10 | this._type = type; 11 | this._config = MISSILES_CONFIG[this._type]; 12 | this._speed = this._config.speed; 13 | 14 | this._isMissileActive = false; 15 | this._textureIndex = 0; 16 | 17 | this._init(); 18 | } 19 | 20 | activate() { 21 | this._isMissileActive = true; 22 | } 23 | 24 | deactivate() { 25 | this._isMissileActive = false; 26 | } 27 | 28 | isActive() { 29 | return this._isMissileActive; 30 | } 31 | 32 | getSpeed() { 33 | return this._speed; 34 | } 35 | 36 | explode() { 37 | const spriteSheet = Loader.assets['assets/spritesheets/space-invaders-sheet']; 38 | const texture = spriteSheet.textures['enemy-missile-explode.png']; 39 | this._view.texture = texture; 40 | 41 | this._view.x -= 2; 42 | } 43 | 44 | updateTexture() { 45 | this._textureIndex = (this._textureIndex + 1) % this._config.textures.length; 46 | const spriteSheet = Loader.assets['assets/spritesheets/space-invaders-sheet']; 47 | const texture = spriteSheet.textures[this._config.textures[this._textureIndex]]; 48 | this._view.texture = texture; 49 | } 50 | 51 | _init() { 52 | const spriteSheet = Loader.assets['assets/spritesheets/space-invaders-sheet']; 53 | const texture = spriteSheet.textures[this._config.textures[this._textureIndex]]; 54 | 55 | const view = this._view = new PIXI.Sprite(texture); 56 | this.addChild(view); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy-games/games/space-invaders/screens/gameplay-screen/missile/missile-config.js: -------------------------------------------------------------------------------- 1 | const MISSILE_TYPE = { 2 | Player: 'PLAYER', 3 | Electric: 'ELECTRIC', 4 | } 5 | 6 | const MISSILES_CONFIG = { 7 | [MISSILE_TYPE.Player]: { 8 | textures: [ 9 | 'player-missile.png', 10 | ], 11 | speed: 2, 12 | }, 13 | [MISSILE_TYPE.Electric]: { 14 | textures: [ 15 | 'enemy-missile-electric-01.png', 16 | 'enemy-missile-electric-02.png', 17 | 'enemy-missile-electric-03.png', 18 | 'enemy-missile-electric-04.png', 19 | ], 20 | speed: 1, 21 | }, 22 | } 23 | 24 | export { 25 | MISSILE_TYPE, 26 | MISSILES_CONFIG, 27 | }; 28 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy-games/games/space-invaders/screens/gameplay-screen/missile/player-missile.js: -------------------------------------------------------------------------------- 1 | import * as PIXI from 'pixi.js'; 2 | import Loader from '../../../../../../../../core/loader'; 3 | import { MISSILES_CONFIG, MISSILE_TYPE } from './missile-config'; 4 | 5 | export default class PlayerMissile extends PIXI.Container { 6 | constructor() { 7 | super(); 8 | 9 | this._type = MISSILE_TYPE.Player; 10 | this._config = MISSILES_CONFIG[this._type]; 11 | this._speed = this._config.speed; 12 | 13 | this._isMissileActive = false; 14 | 15 | this._init(); 16 | } 17 | 18 | activate() { 19 | this._isMissileActive = true; 20 | } 21 | 22 | deactivate() { 23 | this._isMissileActive = false; 24 | } 25 | 26 | isActive() { 27 | return this._isMissileActive; 28 | } 29 | 30 | getSpeed() { 31 | return this._speed; 32 | } 33 | 34 | explode() { 35 | const spriteSheet = Loader.assets['assets/spritesheets/space-invaders-sheet']; 36 | const texture = spriteSheet.textures['player-missile-explode.png']; 37 | this._view.texture = texture; 38 | 39 | this._view.x -= 2; 40 | this._view.y -= 5; 41 | } 42 | 43 | _init() { 44 | const spriteSheet = Loader.assets['assets/spritesheets/space-invaders-sheet']; 45 | const texture = spriteSheet.textures[this._config.textures[0]]; 46 | 47 | const view = this._view = new PIXI.Sprite(texture); 48 | this.addChild(view); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy-games/games/space-invaders/screens/gameplay-screen/player.js: -------------------------------------------------------------------------------- 1 | import * as PIXI from 'pixi.js'; 2 | import Loader from '../../../../../../../core/loader'; 3 | import { PLAYER_MOVEMENT_STATE } from '../../data/space-invaders-data'; 4 | 5 | export default class Player extends PIXI.Container { 6 | constructor() { 7 | super(); 8 | 9 | this._view = null; 10 | this._playerHit = null; 11 | this._moveState = PLAYER_MOVEMENT_STATE.None; 12 | this._isActive = true; 13 | 14 | this._init(); 15 | } 16 | 17 | setMovementState(direction) { 18 | this._moveState = direction; 19 | } 20 | 21 | getMovementState() { 22 | return this._moveState; 23 | } 24 | 25 | isActive() { 26 | return this._isActive; 27 | } 28 | 29 | reset() { 30 | this._moveState = PLAYER_MOVEMENT_STATE.None; 31 | this._isActive = true; 32 | this._view.visible = true; 33 | this._playerHit.visible = false; 34 | } 35 | 36 | showHit() { 37 | this._view.visible = false; 38 | this._playerHit.visible = true; 39 | this._isActive = false; 40 | } 41 | 42 | hideHit() { 43 | this._view.visible = true; 44 | this._playerHit.visible = false; 45 | this._isActive = true; 46 | } 47 | 48 | _init() { 49 | this._initView(); 50 | this._initHit(); 51 | } 52 | 53 | _initView() { 54 | const spriteSheet = Loader.assets['assets/spritesheets/space-invaders-sheet']; 55 | const texture = spriteSheet.textures['player.png']; 56 | 57 | const view = this._view = new PIXI.Sprite(texture); 58 | this.addChild(view); 59 | } 60 | 61 | _initHit() { 62 | const spriteSheet = Loader.assets['assets/spritesheets/space-invaders-sheet']; 63 | const texture = spriteSheet.textures['player-hit.png']; 64 | 65 | const playerHit = this._playerHit = new PIXI.Sprite(texture); 66 | this.addChild(playerHit); 67 | 68 | playerHit.visible = false; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy-games/games/space-invaders/screens/gameplay-screen/ui-elements/player-lives.js: -------------------------------------------------------------------------------- 1 | import * as PIXI from 'pixi.js'; 2 | import Loader from '../../../../../../../../core/loader'; 3 | import { SPACE_INVADERS_CONFIG } from '../../../data/space-invaders-config'; 4 | 5 | export default class PlayerLives extends PIXI.Container { 6 | constructor() { 7 | super(); 8 | 9 | this.events = new PIXI.utils.EventEmitter(); 10 | 11 | this._lives = SPACE_INVADERS_CONFIG.player.livesAtStart; 12 | this._livesViews = []; 13 | 14 | this._init(); 15 | } 16 | 17 | loseLife() { 18 | this._lives--; 19 | 20 | const lifeView = this._livesViews.pop(); 21 | this.removeChild(lifeView); 22 | 23 | if (this._lives === 0) { 24 | this.events.emit('gameOver'); 25 | } 26 | } 27 | 28 | reset() { 29 | this._lives = SPACE_INVADERS_CONFIG.player.livesAtStart; 30 | 31 | for (let i = 0; i < this._livesViews.length; i++) { 32 | const lifeView = this._livesViews[i]; 33 | this.removeChild(lifeView); 34 | } 35 | 36 | this._livesViews = []; 37 | 38 | this._init(); 39 | } 40 | 41 | _init() { 42 | for (let i = 0; i < SPACE_INVADERS_CONFIG.player.livesAtStart; i++) { 43 | const lifeView = this._createLifeView(); 44 | this.addChild(lifeView); 45 | 46 | lifeView.x = i * 10; 47 | 48 | this._livesViews.push(lifeView); 49 | } 50 | } 51 | 52 | _createLifeView() { 53 | const spriteSheet = Loader.assets['assets/spritesheets/space-invaders-sheet']; 54 | const texture = spriteSheet.textures['player.png']; 55 | const view = new PIXI.Sprite(texture); 56 | 57 | return view; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy-games/games/space-invaders/screens/gameplay-screen/ui-elements/score.js: -------------------------------------------------------------------------------- 1 | import * as PIXI from 'pixi.js'; 2 | import { SPACE_INVADERS_CONFIG } from '../../../data/space-invaders-config'; 3 | 4 | export default class Score extends PIXI.Container { 5 | constructor() { 6 | super(); 7 | 8 | this.events = new PIXI.utils.EventEmitter(); 9 | 10 | this._scoreText = null; 11 | this._score = 0; 12 | 13 | this._init(); 14 | } 15 | 16 | addScore(score) { 17 | this._score += score; 18 | this._scoreText.text = this._score.toString().padStart(5, '0'); 19 | 20 | if (this._score > SPACE_INVADERS_CONFIG.bestScore) { 21 | SPACE_INVADERS_CONFIG.bestScore = this._score; 22 | this.events.emit('onBestScoreChange'); 23 | } 24 | } 25 | 26 | reset() { 27 | this._score = 0; 28 | this._scoreText.text = '00000'; 29 | } 30 | 31 | _init() { 32 | const caption = new PIXI.Text('SCORE', new PIXI.TextStyle({ 33 | fontFamily: 'dogicapixel', 34 | fontSize: 8, 35 | fill: 0x000000, 36 | })); 37 | 38 | this.addChild(caption); 39 | 40 | const scoreText = this._scoreText = new PIXI.Text('00000', new PIXI.TextStyle({ 41 | fontFamily: 'dogicapixel', 42 | fontSize: 8, 43 | fill: 0x000000, 44 | })); 45 | 46 | this.addChild(scoreText); 47 | 48 | scoreText.x = 40; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy-games/games/space-invaders/screens/round-screen.js: -------------------------------------------------------------------------------- 1 | import * as PIXI from "pixi.js"; 2 | import { GAME_BOY_CONFIG } from "../../../../game-boy/data/game-boy-config"; 3 | import GameScreenAbstract from "../../shared/game-screen-abstract"; 4 | import { SPACE_INVADERS_CONFIG } from "../data/space-invaders-config"; 5 | import Delayed from "../../../../../../core/helpers/delayed-call"; 6 | 7 | 8 | export default class RoundScreen extends GameScreenAbstract { 9 | constructor() { 10 | super(); 11 | 12 | this._timer = null; 13 | this._roundNumber = null; 14 | 15 | this._init(); 16 | } 17 | 18 | show() { 19 | super.show(); 20 | 21 | this._timer = Delayed.call(1300, () => { 22 | this.events.emit('onRoundEnd'); 23 | }); 24 | } 25 | 26 | stopTweens() { 27 | if (this._timer) { 28 | this._timer.stop(); 29 | } 30 | } 31 | 32 | updateRound() { 33 | const roundString = SPACE_INVADERS_CONFIG.currentRound.toString().padStart(2, '0'); 34 | this._roundNumber.text = roundString; 35 | } 36 | 37 | _init() { 38 | this._initRoundText(); 39 | 40 | this.visible = false; 41 | } 42 | 43 | _initRoundText() { 44 | const roundText = new PIXI.Text('ROUND', new PIXI.TextStyle({ 45 | fontFamily: 'dogicapixel', 46 | fontSize: 8, 47 | fill: 0x000000, 48 | })); 49 | 50 | this.addChild(roundText); 51 | 52 | const roundString = SPACE_INVADERS_CONFIG.currentRound.toString().padStart(2, '0'); 53 | const roundNumber = this._roundNumber = new PIXI.Text(roundString, new PIXI.TextStyle({ 54 | fontFamily: 'dogicapixel', 55 | fontSize: 8, 56 | fill: 0x000000, 57 | })); 58 | 59 | this.addChild(roundNumber); 60 | 61 | const readyText = new PIXI.Text('READY!', new PIXI.TextStyle({ 62 | fontFamily: 'dogicapixel', 63 | fontSize: 8, 64 | fill: 0x000000, 65 | })); 66 | 67 | this.addChild(readyText); 68 | 69 | roundText.x = GAME_BOY_CONFIG.screen.width * 0.5 - 20; 70 | roundText.y = GAME_BOY_CONFIG.screen.height * 0.5 - 20; 71 | 72 | roundNumber.x = GAME_BOY_CONFIG.screen.width * 0.5 - 8; 73 | roundNumber.y = GAME_BOY_CONFIG.screen.height * 0.5 - 11; 74 | 75 | readyText.x = GAME_BOY_CONFIG.screen.width * 0.5 - 21; 76 | readyText.y = GAME_BOY_CONFIG.screen.height * 0.5 + 7; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy-games/games/space-invaders/screens/title-screen.js: -------------------------------------------------------------------------------- 1 | import * as PIXI from "pixi.js"; 2 | import Loader from "../../../../../../core/loader"; 3 | import { GAME_BOY_CONFIG } from "../../../../game-boy/data/game-boy-config"; 4 | import GameScreenAbstract from "../../shared/game-screen-abstract"; 5 | import { TWEEN } from '/node_modules/three/examples/jsm/libs/tween.module.min.js'; 6 | import Delayed from "../../../../../../core/helpers/delayed-call"; 7 | import { BUTTON_TYPE } from "../../../../game-boy/data/game-boy-data"; 8 | 9 | 10 | export default class TitleScreen extends GameScreenAbstract { 11 | constructor() { 12 | super(); 13 | 14 | this._logo = null; 15 | this._logoTween = null; 16 | this._titleScreenClean = null; 17 | this._startText = null; 18 | this._blinkTimer = null; 19 | this._isButtonsEnabled = false; 20 | 21 | this._blinkTime = 700; 22 | 23 | this._init(); 24 | } 25 | 26 | show() { 27 | super.show(); 28 | 29 | this._showLogo(); 30 | } 31 | 32 | hide() { 33 | super.hide(); 34 | 35 | this.stopTweens(); 36 | this.reset(); 37 | } 38 | 39 | onButtonPress(buttonType) { 40 | if (!this._isButtonsEnabled) { 41 | return; 42 | } 43 | 44 | if (buttonType === BUTTON_TYPE.Start || buttonType === BUTTON_TYPE.A || buttonType === BUTTON_TYPE.B) { 45 | this.events.emit('onStartGame'); 46 | } 47 | } 48 | 49 | 50 | stopTweens() { 51 | if (this._logoTween) { 52 | this._logoTween.stop(); 53 | } 54 | 55 | if (this._blinkTimer) { 56 | this._blinkTimer.stop(); 57 | } 58 | } 59 | 60 | reset() { 61 | this._logo.y = 145; 62 | this._titleScreenClean.visible = false; 63 | this._startText.visible = false; 64 | this._isButtonsEnabled = false; 65 | } 66 | 67 | _showLogo() { 68 | this._logoTween = new TWEEN.Tween(this._logo) 69 | .to({ y: 20 }, 1500) 70 | .easing(TWEEN.Easing.Linear.None) 71 | .start() 72 | .onComplete(() => { 73 | this._titleScreenClean.visible = true; 74 | this._startText.visible = true; 75 | this._isButtonsEnabled = true; 76 | this._blinkStartText(); 77 | }); 78 | } 79 | 80 | _blinkStartText() { 81 | this._blinkTimer = Delayed.call(this._blinkTime, () => { 82 | this._startText.visible = !this._startText.visible; 83 | this._blinkStartText(); 84 | }); 85 | } 86 | 87 | _init() { 88 | this._initLogo(); 89 | this._initTitleScreenClean(); 90 | this._initStartText(); 91 | 92 | this.visible = false; 93 | } 94 | 95 | _initLogo() { 96 | const spriteSheet = Loader.assets['assets/spritesheets/space-invaders-sheet']; 97 | const texture = spriteSheet.textures['space-invaders-logo.png']; 98 | 99 | const logo = this._logo = new PIXI.Sprite(texture); 100 | this.addChild(logo); 101 | 102 | logo.x = 9; 103 | logo.y = 145; 104 | } 105 | 106 | _initTitleScreenClean() { 107 | const spriteSheet = Loader.assets['assets/spritesheets/space-invaders-sheet']; 108 | const texture = spriteSheet.textures['title-screen-clean.png']; 109 | 110 | const titleScreenClean = this._titleScreenClean = new PIXI.Sprite(texture); 111 | this.addChild(titleScreenClean); 112 | 113 | titleScreenClean.visible = false; 114 | } 115 | 116 | _initStartText() { 117 | const spriteSheet = Loader.assets['assets/spritesheets/space-invaders-sheet']; 118 | const texture = spriteSheet.textures['start-text.png']; 119 | 120 | const startText = this._startText = new PIXI.Sprite(texture); 121 | this.addChild(startText); 122 | 123 | startText.x = 41; 124 | startText.y = 83; 125 | startText.visible = false; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy-games/games/space-invaders/space-invaders.js: -------------------------------------------------------------------------------- 1 | import { MessageDispatcher } from "black-engine"; 2 | import DEBUG_CONFIG from "../../../../../core/configs/debug-config"; 3 | import { GAME_TYPE } from "../../data/games-config"; 4 | import GameAbstract from "../game-abstract"; 5 | import { SPACE_INVADERS_CONFIG } from "./data/space-invaders-config"; 6 | import { SPACE_INVADERS_SCREEN_TYPE } from "./data/space-invaders-data"; 7 | import GameOverScreen from "./screens/game-over-screen"; 8 | import GameplayScreen from "./screens/gameplay-screen/gameplay-screen"; 9 | import RoundScreen from "./screens/round-screen"; 10 | import TitleScreen from "./screens/title-screen"; 11 | 12 | export default class SpaceInvaders extends GameAbstract { 13 | constructor() { 14 | super(); 15 | 16 | this.events = new MessageDispatcher(); 17 | 18 | this._screens = {}; 19 | this._currentScreenType = null; 20 | 21 | this._init(); 22 | } 23 | 24 | update(dt) { 25 | this._screens[this._currentScreenType].update(dt); 26 | } 27 | 28 | show() { 29 | super.show(); 30 | 31 | this._reset(); 32 | 33 | if (DEBUG_CONFIG.startState.loadGame === GAME_TYPE.SpaceInvaders && DEBUG_CONFIG.startState.startScreen) { 34 | this._showScreen(DEBUG_CONFIG.startState.startScreen); 35 | } else { 36 | this._showScreen(SPACE_INVADERS_SCREEN_TYPE.Title); 37 | } 38 | } 39 | 40 | hide() { 41 | super.hide(); 42 | 43 | for (let screenType in this._screens) { 44 | this._screens[screenType].hide(); 45 | } 46 | 47 | this._reset(); 48 | } 49 | 50 | onButtonPress(buttonType) { 51 | if (!this._currentScreenType) { 52 | return; 53 | } 54 | 55 | this._screens[this._currentScreenType].onButtonPress(buttonType); 56 | } 57 | 58 | onButtonUp(buttonType) { 59 | if (!this._currentScreenType) { 60 | return; 61 | } 62 | 63 | this._screens[this._currentScreenType].onButtonUp(buttonType); 64 | } 65 | 66 | stopTweens() { 67 | for (let screenType in this._screens) { 68 | this._screens[screenType].stopTweens(); 69 | } 70 | } 71 | 72 | _reset() { 73 | SPACE_INVADERS_CONFIG.currentRound = 1; 74 | 75 | for (let screenType in this._screens) { 76 | this._screens[screenType].reset(); 77 | } 78 | } 79 | 80 | _showScreen(screenType) { 81 | this._currentScreenType = screenType; 82 | this._screens[screenType].show(); 83 | } 84 | 85 | _init() { 86 | this._initScreens(); 87 | this._initSignals(); 88 | } 89 | 90 | _initScreens() { 91 | this._initTitleScreen(); 92 | this._initGameplayScreen(); 93 | this._initRoundScreen(); 94 | this._initGameOverScreen(); 95 | } 96 | 97 | _initTitleScreen() { 98 | const titleScreen = new TitleScreen(); 99 | this.addChild(titleScreen); 100 | 101 | this._screens[SPACE_INVADERS_SCREEN_TYPE.Title] = titleScreen; 102 | } 103 | 104 | _initGameplayScreen() { 105 | const gameplayScreen = new GameplayScreen(); 106 | this.addChild(gameplayScreen); 107 | 108 | this._screens[SPACE_INVADERS_SCREEN_TYPE.Gameplay] = gameplayScreen; 109 | } 110 | 111 | _initRoundScreen() { 112 | const roundScreen = new RoundScreen(); 113 | this.addChild(roundScreen); 114 | 115 | this._screens[SPACE_INVADERS_SCREEN_TYPE.Round] = roundScreen; 116 | } 117 | 118 | _initGameOverScreen() { 119 | const gameOverScreen = new GameOverScreen(); 120 | this.addChild(gameOverScreen); 121 | 122 | this._screens[SPACE_INVADERS_SCREEN_TYPE.GameOver] = gameOverScreen; 123 | } 124 | 125 | _initSignals() { 126 | this._screens[SPACE_INVADERS_SCREEN_TYPE.Title].events.on('onStartGame', () => this._onStartGame()); 127 | this._screens[SPACE_INVADERS_SCREEN_TYPE.Round].events.on('onRoundEnd', () => this._onRoundEnd()); 128 | this._screens[SPACE_INVADERS_SCREEN_TYPE.Gameplay].events.on('onGameOver', () => this._onGameOver()); 129 | this._screens[SPACE_INVADERS_SCREEN_TYPE.Gameplay].events.on('onAllEnemiesKilled', () => this._onNextRound()); 130 | this._screens[SPACE_INVADERS_SCREEN_TYPE.Gameplay].events.on('onBestScoreChange', () => this.events.post('onBestScoreChange')); 131 | this._screens[SPACE_INVADERS_SCREEN_TYPE.GameOver].events.on('onGameOverEnd', () => this._onGameOverScreenEnd()); 132 | } 133 | 134 | _onStartGame() { 135 | this._screens[SPACE_INVADERS_SCREEN_TYPE.Title].hide(); 136 | this._screens[SPACE_INVADERS_SCREEN_TYPE.Round].updateRound(); 137 | this._screens[SPACE_INVADERS_SCREEN_TYPE.Gameplay].resetLivesScores(); 138 | this._showScreen(SPACE_INVADERS_SCREEN_TYPE.Round); 139 | } 140 | 141 | _onRoundEnd() { 142 | this._screens[SPACE_INVADERS_SCREEN_TYPE.Round].hide(); 143 | this._showScreen(SPACE_INVADERS_SCREEN_TYPE.Gameplay); 144 | } 145 | 146 | _onNextRound() { 147 | this._screens[SPACE_INVADERS_SCREEN_TYPE.Gameplay].hide(); 148 | 149 | SPACE_INVADERS_CONFIG.currentRound++; 150 | this._screens[SPACE_INVADERS_SCREEN_TYPE.Gameplay].reset(); 151 | this._screens[SPACE_INVADERS_SCREEN_TYPE.Round].updateRound(); 152 | 153 | this._showScreen(SPACE_INVADERS_SCREEN_TYPE.Round); 154 | } 155 | 156 | _onGameOver() { 157 | SPACE_INVADERS_CONFIG.currentRound = 1; 158 | 159 | this._screens[SPACE_INVADERS_SCREEN_TYPE.Gameplay].hide(); 160 | this._showScreen(SPACE_INVADERS_SCREEN_TYPE.GameOver); 161 | } 162 | 163 | _onGameOverScreenEnd() { 164 | this._screens[SPACE_INVADERS_SCREEN_TYPE.GameOver].hide(); 165 | this._reset(); 166 | this._showScreen(SPACE_INVADERS_SCREEN_TYPE.Title); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy-games/games/tetris/data/tetris-config.js: -------------------------------------------------------------------------------- 1 | import * as PIXI from 'pixi.js'; 2 | import { CARTRIDGE_STATE } from '../../../../game-boy/data/game-boy-data'; 3 | 4 | const TETRIS_CONFIG = { 5 | cartridgeState: CARTRIDGE_STATE.NotInserted, 6 | startLevel: 0, 7 | field: { 8 | width: 10, 9 | height: 20, 10 | position: new PIXI.Point(16, -16), 11 | }, 12 | blockSize: 8, 13 | shapeSpawnPosition: new PIXI.Point(4, 3), 14 | linesBlinkTime: 300, 15 | linesBlinkCount: 3, 16 | fastFallInterval: 30, 17 | originalTetrisFramesPerSecond: 59.73, 18 | scorePerLine: [40, 100, 300, 1200], 19 | scoreForSoftDrop: 1, 20 | bestScore: 0, 21 | isMusicAllowed: true, 22 | allowInvisibleShape: true, 23 | } 24 | 25 | const LEVELS_CONFIG = [ 26 | { framesPerRow: 53 }, // 0 27 | { framesPerRow: 49 }, // 1 28 | { framesPerRow: 45 }, // 2 29 | { framesPerRow: 41 }, // 3 30 | { framesPerRow: 37 }, // 4 31 | { framesPerRow: 33 }, // 5 32 | { framesPerRow: 28 }, // 6 33 | { framesPerRow: 22 }, // 7 34 | { framesPerRow: 17 }, // 8 35 | { framesPerRow: 11 }, // 9 36 | { framesPerRow: 10 }, // 10 37 | { framesPerRow: 9 }, // 11 38 | { framesPerRow: 8 }, // 12 39 | { framesPerRow: 7 }, // 13 40 | { framesPerRow: 6 }, // 14 41 | { framesPerRow: 6 }, // 15 42 | { framesPerRow: 5 }, // 16 43 | { framesPerRow: 5 }, // 17 44 | { framesPerRow: 4 }, // 18 45 | { framesPerRow: 4 }, // 19 46 | { framesPerRow: 3 }, // 20 47 | ] 48 | 49 | export { 50 | TETRIS_CONFIG, 51 | LEVELS_CONFIG, 52 | }; 53 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy-games/games/tetris/data/tetris-data.js: -------------------------------------------------------------------------------- 1 | const TETRIS_SCREEN_TYPE = { 2 | License: 'LICENSE', 3 | Title: 'TITLE', 4 | Gameplay: 'GAMEPLAY', 5 | } 6 | 7 | export { TETRIS_SCREEN_TYPE }; 8 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy-games/games/tetris/screens/gameplay-screen.js/field/shape/shape-config.js: -------------------------------------------------------------------------------- 1 | import * as PIXI from 'pixi.js'; 2 | 3 | const SHAPE_TYPE = { 4 | I: 'I', 5 | J: 'J', 6 | L: 'L', 7 | O: 'O', 8 | S: 'S', 9 | T: 'T', 10 | Z: 'Z', 11 | Invisible: 'INVISIBLE', 12 | } 13 | 14 | const SHAPE_DIRECTION = { 15 | Up: 'UP', 16 | Right: 'RIGHT', 17 | Down: 'DOWN', 18 | Left: 'LEFT', 19 | } 20 | 21 | const DIRECTION_SEQUENCE = [ 22 | SHAPE_DIRECTION.Up, 23 | SHAPE_DIRECTION.Right, 24 | SHAPE_DIRECTION.Down, 25 | SHAPE_DIRECTION.Left, 26 | ]; 27 | 28 | const ROTATE_TYPE = { 29 | Clockwise: 'CLOCKWISE', 30 | CounterClockwise: 'COUNTER_CLOCKWISE', 31 | } 32 | 33 | const SHAPE_CONFIG = { 34 | [SHAPE_TYPE.I]: { 35 | textureEdge: 'block-i-edge.png', 36 | textureMiddle: 'block-i-middle.png', 37 | blocksView: [ 38 | [1, 1, 1, 1], 39 | ], 40 | pivot: new PIXI.Point(1, 0), 41 | availableDirections: [ 42 | SHAPE_DIRECTION.Up, 43 | SHAPE_DIRECTION.Left, 44 | ], 45 | }, 46 | [SHAPE_TYPE.J]: { 47 | texture: 'block-j.png', 48 | blocksView: [ 49 | [1, 1, 1], 50 | [0, 0, 1], 51 | ], 52 | pivot: new PIXI.Point(1, 0), 53 | availableDirections: [ 54 | SHAPE_DIRECTION.Up, 55 | SHAPE_DIRECTION.Right, 56 | SHAPE_DIRECTION.Down, 57 | SHAPE_DIRECTION.Left, 58 | ], 59 | }, 60 | [SHAPE_TYPE.L]: { 61 | texture: 'block-l.png', 62 | blocksView: [ 63 | [1, 1, 1], 64 | [1, 0, 0], 65 | ], 66 | pivot: new PIXI.Point(1, 0), 67 | availableDirections: [ 68 | SHAPE_DIRECTION.Up, 69 | SHAPE_DIRECTION.Right, 70 | SHAPE_DIRECTION.Down, 71 | SHAPE_DIRECTION.Left, 72 | ], 73 | }, 74 | [SHAPE_TYPE.O]: { 75 | texture: 'block-o.png', 76 | blocksView: [ 77 | [1, 1], 78 | [1, 1], 79 | ], 80 | pivot: new PIXI.Point(0, 0), 81 | availableDirections: [], 82 | }, 83 | [SHAPE_TYPE.S]: { 84 | texture: 'block-s.png', 85 | blocksView: [ 86 | [0, 1, 1], 87 | [1, 1, 0], 88 | ], 89 | pivot: new PIXI.Point(1, 0), 90 | availableDirections: [ 91 | SHAPE_DIRECTION.Up, 92 | SHAPE_DIRECTION.Right, 93 | ], 94 | }, 95 | [SHAPE_TYPE.T]: { 96 | texture: 'block-t.png', 97 | blocksView: [ 98 | [1, 1, 1], 99 | [0, 1, 0], 100 | ], 101 | pivot: new PIXI.Point(1, 0), 102 | availableDirections: [ 103 | SHAPE_DIRECTION.Up, 104 | SHAPE_DIRECTION.Right, 105 | SHAPE_DIRECTION.Down, 106 | SHAPE_DIRECTION.Left, 107 | ], 108 | }, 109 | [SHAPE_TYPE.Z]: { 110 | texture: 'block-z.png', 111 | blocksView: [ 112 | [1, 1, 0], 113 | [0, 1, 1], 114 | ], 115 | pivot: new PIXI.Point(1, 0), 116 | availableDirections: [ 117 | SHAPE_DIRECTION.Up, 118 | SHAPE_DIRECTION.Right, 119 | ], 120 | }, 121 | [SHAPE_TYPE.Invisible]: { 122 | texture: 'block-o.png', 123 | tint: '#ec8976', 124 | blocksView: [ 125 | [1, 1, 1], 126 | ], 127 | pivot: new PIXI.Point(1, 0), 128 | availableDirections: [ 129 | SHAPE_DIRECTION.Up, 130 | SHAPE_DIRECTION.Right, 131 | SHAPE_DIRECTION.Down, 132 | SHAPE_DIRECTION.Left, 133 | ], 134 | }, 135 | } 136 | 137 | export { 138 | SHAPE_TYPE, 139 | SHAPE_DIRECTION, 140 | DIRECTION_SEQUENCE, 141 | ROTATE_TYPE, 142 | SHAPE_CONFIG, 143 | }; 144 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy-games/games/tetris/screens/gameplay-screen.js/field/shape/shape.js: -------------------------------------------------------------------------------- 1 | import * as PIXI from 'pixi.js'; 2 | import { TETRIS_CONFIG } from '../../../../data/tetris-config'; 3 | import Loader from '../../../../../../../../../core/loader'; 4 | import { GAME_BOY_CONFIG } from '../../../../../../../game-boy/data/game-boy-config'; 5 | import { DIRECTION_SEQUENCE, ROTATE_TYPE, SHAPE_CONFIG, SHAPE_DIRECTION, SHAPE_TYPE } from './shape-config'; 6 | import { TWEEN } from '/node_modules/three/examples/jsm/libs/tween.module.min.js'; 7 | 8 | 9 | export default class Shape extends PIXI.Container { 10 | constructor(type) { 11 | super(); 12 | 13 | this._type = type; 14 | this._blocksView = []; 15 | this._shapePivot = null; 16 | this._direction = SHAPE_DIRECTION.Up; 17 | this._blockPosition = new PIXI.Point(0, 0); 18 | this._distanceFallen = 0; 19 | 20 | this._init(); 21 | } 22 | 23 | setPosition(x, y) { 24 | this._blockPosition.set(x, y); 25 | 26 | this.x = this._blockPosition.x * TETRIS_CONFIG.blockSize; 27 | this.y = this._blockPosition.y * TETRIS_CONFIG.blockSize; 28 | } 29 | 30 | moveRight() { 31 | this.x += TETRIS_CONFIG.blockSize; 32 | this._blockPosition.x += 1; 33 | } 34 | 35 | moveLeft() { 36 | this.x -= TETRIS_CONFIG.blockSize; 37 | this._blockPosition.x -= 1; 38 | } 39 | 40 | moveDown() { 41 | this.y += TETRIS_CONFIG.blockSize; 42 | this._blockPosition.y += 1; 43 | 44 | this._distanceFallen += 1; 45 | } 46 | 47 | getBlockPosition() { 48 | return this._blockPosition; 49 | } 50 | 51 | getBlocksView() { 52 | return this._blocksView; 53 | } 54 | 55 | getPivot() { 56 | return this._shapePivot; 57 | } 58 | 59 | getType() { 60 | return this._type; 61 | } 62 | 63 | getFallenDistance() { 64 | return this._distanceFallen; 65 | } 66 | 67 | getDirection() { 68 | return this._direction; 69 | } 70 | 71 | rotate(rotateType) { 72 | if (SHAPE_CONFIG[this._type].availableDirections.length === 0) { 73 | return; 74 | } 75 | 76 | if (rotateType === ROTATE_TYPE.Clockwise) { 77 | this._rotateClockwise(); 78 | } else { 79 | this._rotateCounterClockwise(); 80 | } 81 | 82 | if (this._type === SHAPE_TYPE.I) { 83 | this._updateShapeIBlocksPosition(); 84 | } else { 85 | this._updateShapeBlocksPosition(); 86 | } 87 | } 88 | 89 | _rotateClockwise() { 90 | const blocksView = this._blocksView; 91 | const newBlocksView = []; 92 | 93 | for (let row = 0; row < blocksView[0].length; row++) { 94 | newBlocksView.push([]); 95 | 96 | for (let column = 0; column < blocksView.length; column++) { 97 | newBlocksView[row][column] = blocksView[blocksView.length - column - 1][row]; 98 | } 99 | } 100 | 101 | this._blocksView = newBlocksView; 102 | this._shapePivot = new PIXI.Point(this._blocksView[0].length - this._shapePivot.y - 1, this._shapePivot.x); 103 | 104 | const availableDirections = SHAPE_CONFIG[this._type].availableDirections; 105 | this._direction = this._getNextDirection(); 106 | 107 | if (!availableDirections.includes(this._direction)) { 108 | this._rotateClockwise(); 109 | } 110 | } 111 | 112 | _rotateCounterClockwise() { 113 | const blocksView = this._blocksView; 114 | const newBlocksView = []; 115 | 116 | for (let row = 0; row < blocksView[0].length; row++) { 117 | newBlocksView.push([]); 118 | 119 | for (let column = 0; column < blocksView.length; column++) { 120 | newBlocksView[row][column] = blocksView[column][blocksView[0].length - row - 1]; 121 | } 122 | } 123 | 124 | this._blocksView = newBlocksView; 125 | this._shapePivot = new PIXI.Point(this._shapePivot.y, this._blocksView.length - this._shapePivot.x - 1); 126 | 127 | const availableDirections = SHAPE_CONFIG[this._type].availableDirections; 128 | this._direction = this._getPreviousDirection(); 129 | 130 | if (!availableDirections.includes(this._direction)) { 131 | this._rotateCounterClockwise(); 132 | } 133 | } 134 | 135 | _getNextDirection() { 136 | let newDirection; 137 | 138 | for (let i = 0; i < DIRECTION_SEQUENCE.length; i += 1) { 139 | if (DIRECTION_SEQUENCE[i] === this._direction) { 140 | if (i === DIRECTION_SEQUENCE.length - 1) { 141 | newDirection = DIRECTION_SEQUENCE[0]; 142 | } else { 143 | newDirection = DIRECTION_SEQUENCE[i + 1]; 144 | } 145 | 146 | break; 147 | } 148 | } 149 | 150 | return newDirection; 151 | } 152 | 153 | _getPreviousDirection() { 154 | let newDirection; 155 | 156 | for (let i = 0; i < DIRECTION_SEQUENCE.length; i += 1) { 157 | if (DIRECTION_SEQUENCE[i] === this._direction) { 158 | if (i === 0) { 159 | newDirection = DIRECTION_SEQUENCE[DIRECTION_SEQUENCE.length - 1]; 160 | } else { 161 | newDirection = DIRECTION_SEQUENCE[i - 1]; 162 | } 163 | 164 | break; 165 | } 166 | } 167 | 168 | return newDirection; 169 | } 170 | 171 | _init() { 172 | if (this._type === SHAPE_TYPE.I) { 173 | this._initShapeI(); 174 | } else { 175 | this._initShape(); 176 | } 177 | } 178 | 179 | _initShapeI() { 180 | const config = SHAPE_CONFIG[this._type]; 181 | const blocksView = config.blocksView; 182 | 183 | const spriteSheet = Loader.assets['assets/spritesheets/tetris-sheet']; 184 | const blockTexture = spriteSheet.textures[config.textureMiddle]; 185 | const edgeTexture = spriteSheet.textures[config.textureEdge]; 186 | 187 | for (let row = 0; row < blocksView.length; row++) { 188 | this._blocksView[row] = []; 189 | 190 | for (let column = 0; column < blocksView[0].length; column++) { 191 | if (blocksView[row][column] === 1) { 192 | const texture = (column === 0 || column === blocksView[0].length - 1) ? edgeTexture : blockTexture; 193 | const block = new PIXI.Sprite(texture); 194 | this.addChild(block); 195 | 196 | this._blocksView[row][column] = block; 197 | } else { 198 | this._blocksView[row][column] = null; 199 | } 200 | } 201 | } 202 | 203 | this._shapePivot = config.pivot; 204 | this._updateShapeIBlocksPosition(); 205 | } 206 | 207 | _initShape() { 208 | const config = SHAPE_CONFIG[this._type]; 209 | const blocksView = config.blocksView; 210 | const spriteSheet = Loader.assets['assets/spritesheets/tetris-sheet']; 211 | const blockTexture = spriteSheet.textures[config.texture]; 212 | 213 | for (let row = 0; row < blocksView.length; row++) { 214 | this._blocksView[row] = []; 215 | 216 | for (let column = 0; column < blocksView[0].length; column++) { 217 | if (blocksView[row][column] === 1) { 218 | const block = new PIXI.Sprite(blockTexture); 219 | this.addChild(block); 220 | 221 | if (config.tint) { 222 | block.tint = config.tint; 223 | } 224 | 225 | this._blocksView[row][column] = block; 226 | } else { 227 | this._blocksView[row][column] = null; 228 | } 229 | } 230 | } 231 | 232 | this._shapePivot = config.pivot; 233 | this._updateShapeBlocksPosition(); 234 | 235 | if (this._type === SHAPE_TYPE.Invisible) { 236 | this.alpha = 0.7; 237 | 238 | this._blinkTween = new TWEEN.Tween(this) 239 | .to({ alpha: 0.3 }, 700) 240 | .easing(TWEEN.Easing.Sinusoidal.InOut) 241 | .yoyo(true) 242 | .repeat(Infinity) 243 | .start() 244 | } 245 | } 246 | 247 | _updateShapeBlocksPosition() { 248 | let index = 0; 249 | 250 | for (let row = 0; row < this._blocksView.length; row++) { 251 | for (let column = 0; column < this._blocksView[0].length; column++) { 252 | const block = this._blocksView[row][column]; 253 | 254 | if (block !== null) { 255 | block.x = (column - this._shapePivot.x) * TETRIS_CONFIG.blockSize; 256 | block.y = (row - this._shapePivot.y) * TETRIS_CONFIG.blockSize; 257 | 258 | index += 1; 259 | } 260 | } 261 | } 262 | } 263 | 264 | _updateShapeIBlocksPosition() { 265 | let index = 0; 266 | 267 | for (let row = 0; row < this._blocksView.length; row++) { 268 | for (let column = 0; column < this._blocksView[0].length; column++) { 269 | const block = this._blocksView[row][column]; 270 | 271 | if (block !== null) { 272 | if (this._direction === SHAPE_DIRECTION.Up) { 273 | block.rotation = 0; 274 | block.x = (column - this._shapePivot.x) * TETRIS_CONFIG.blockSize; 275 | block.y = (row - this._shapePivot.y) * TETRIS_CONFIG.blockSize; 276 | 277 | if (column === this._blocksView[0].length - 1) { 278 | block.rotation = Math.PI; 279 | block.x += 1 * TETRIS_CONFIG.blockSize; 280 | block.y += 1 * TETRIS_CONFIG.blockSize; 281 | } 282 | } 283 | 284 | if (this._direction === SHAPE_DIRECTION.Left) { 285 | block.rotation = -Math.PI * 0.5; 286 | block.x = (column - this._shapePivot.y + 2) * TETRIS_CONFIG.blockSize; 287 | block.y = (row - this._shapePivot.x - 1) * TETRIS_CONFIG.blockSize; 288 | 289 | if (row === 0) { 290 | block.rotation = Math.PI * 0.5; 291 | block.x += 1 * TETRIS_CONFIG.blockSize; 292 | block.y -= 1 * TETRIS_CONFIG.blockSize; 293 | } 294 | } 295 | 296 | index += 1; 297 | } 298 | } 299 | } 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy-games/games/tetris/screens/gameplay-screen.js/gameplay-screen.js: -------------------------------------------------------------------------------- 1 | import * as PIXI from 'pixi.js'; 2 | import GameScreenAbstract from '../../../shared/game-screen-abstract'; 3 | import { TETRIS_SCREEN_TYPE } from '../../data/tetris-data'; 4 | import Loader from '../../../../../../../core/loader'; 5 | import { GAME_BOY_CONFIG } from '../../../../../game-boy/data/game-boy-config'; 6 | import Field from './field/field'; 7 | import NextShape from './next-shape'; 8 | import GameOverPopup from './popups/game-over-popup'; 9 | import PausePopup from './popups/pause-popup'; 10 | import { BUTTON_TYPE } from '../../../../../game-boy/data/game-boy-data'; 11 | import { TETRIS_CONFIG } from '../../data/tetris-config'; 12 | import GameBoyAudio from '../../../../../game-boy/game-boy-audio/game-boy-audio'; 13 | import { GAME_BOY_SOUND_TYPE } from '../../../../../game-boy/game-boy-audio/game-boy-audio-data'; 14 | 15 | export default class GameplayScreen extends GameScreenAbstract { 16 | constructor() { 17 | super(); 18 | 19 | this._screenType = TETRIS_SCREEN_TYPE.Gameplay; 20 | this._field = null; 21 | this._gameOverPopup = null; 22 | this._pausePopup = null; 23 | this._linesCount = null; 24 | this._score = null; 25 | 26 | this._isGameActive = false; 27 | this._isPaused = false; 28 | this._gameOver = false; 29 | 30 | this._init(); 31 | } 32 | 33 | update(dt) { 34 | if (this._isGameActive && !this._isPaused) { 35 | this._field.update(dt); 36 | } 37 | } 38 | 39 | onButtonPress(buttonType) { 40 | if (this._isGameActive) { 41 | if (!this._isPaused) { 42 | this._field.onButtonPress(buttonType); 43 | } 44 | 45 | if (buttonType === BUTTON_TYPE.Start) { 46 | this._onPauseClick(); 47 | } 48 | } 49 | 50 | if (buttonType === BUTTON_TYPE.Select) { 51 | GameBoyAudio.switchSound(GAME_BOY_SOUND_TYPE.TetrisMusic); 52 | TETRIS_CONFIG.isMusicAllowed = !TETRIS_CONFIG.isMusicAllowed; 53 | } 54 | 55 | if (this._gameOver) { 56 | this._gameOverPopup.onButtonPress(buttonType); 57 | } 58 | } 59 | 60 | onButtonUp(buttonType) { 61 | if (this._isGameActive && !this._isPaused) { 62 | this._field.onButtonUp(buttonType); 63 | } 64 | } 65 | 66 | show() { 67 | super.show(); 68 | 69 | this._startGame(); 70 | } 71 | 72 | stopTweens() { 73 | this._gameOverPopup.stopTweens(); 74 | this._field.stopTweens(); 75 | } 76 | 77 | reset() { 78 | this._field.reset(); 79 | this._gameOverPopup.reset(); 80 | this._pausePopup.reset(); 81 | 82 | this._isGameActive = false; 83 | this._isPaused = false; 84 | this._gameOver = false; 85 | 86 | this._linesCount.text = '0' 87 | this._score.text = '0'; 88 | this._level.text = TETRIS_CONFIG.startLevel.toString(); 89 | } 90 | 91 | disableFalling() { 92 | this._field.switchFalling(); 93 | } 94 | 95 | clearBottomLine() { 96 | this._field.clearBottomLine(); 97 | } 98 | 99 | _onPauseClick() { 100 | this._isPaused = !this._isPaused; 101 | 102 | if (this._isPaused) { 103 | this._pausePopup.show(); 104 | this._field.hide(); 105 | } else { 106 | this._pausePopup.hide(); 107 | this._field.show(); 108 | } 109 | } 110 | 111 | _startGame() { 112 | this._field.reset(); 113 | this._gameOver = false; 114 | this._isGameActive = true; 115 | 116 | this._field.show(); 117 | this._field.startGame(); 118 | } 119 | 120 | _init() { 121 | this._initBackground(); 122 | this._initField(); 123 | this._initLinesCount(); 124 | this._initLevel(); 125 | this._initScore(); 126 | this._initPopups(); 127 | this._initSignals(); 128 | } 129 | 130 | _initBackground() { 131 | const spriteSheet = Loader.assets['assets/spritesheets/tetris-sheet']; 132 | const texture = spriteSheet.textures['gameplay-screen.png']; 133 | 134 | const screen = new PIXI.Sprite(texture); 135 | this.addChild(screen); 136 | } 137 | 138 | _initField() { 139 | const field = this._field = new Field(); 140 | this.addChild(field); 141 | } 142 | 143 | _initLinesCount() { 144 | const linesCount = this._linesCount = new PIXI.Text('0', new PIXI.TextStyle({ 145 | fontFamily: 'tetris', 146 | fontSize: 8, 147 | fill: 0x000000, 148 | })); 149 | 150 | this.addChild(linesCount); 151 | linesCount.anchor.set(1, 0); 152 | 153 | linesCount.x = GAME_BOY_CONFIG.screen.width - 15; 154 | linesCount.y = 78; 155 | } 156 | 157 | _initLevel() { 158 | const level = this._level = new PIXI.Text(TETRIS_CONFIG.startLevel, new PIXI.TextStyle({ 159 | fontFamily: 'tetris', 160 | fontSize: 8, 161 | fill: 0x000000, 162 | })); 163 | 164 | this.addChild(level); 165 | level.anchor.set(1, 0); 166 | 167 | level.x = GAME_BOY_CONFIG.screen.width - 15; 168 | level.y = 54; 169 | } 170 | 171 | _initScore() { 172 | const score = this._score = new PIXI.Text('0', new PIXI.TextStyle({ 173 | fontFamily: 'tetris', 174 | fontSize: 8, 175 | fill: 0x000000, 176 | })); 177 | 178 | this.addChild(score); 179 | score.anchor.set(1, 0); 180 | 181 | score.x = GAME_BOY_CONFIG.screen.width - 7; 182 | score.y = 22; 183 | } 184 | 185 | _initPopups() { 186 | this._initGameOverPopup(); 187 | this._initPausePopup(); 188 | } 189 | 190 | _initGameOverPopup() { 191 | const gameOverPopup = this._gameOverPopup = new GameOverPopup(); 192 | this.addChild(gameOverPopup); 193 | 194 | gameOverPopup.x = TETRIS_CONFIG.field.position.x; 195 | gameOverPopup.y = TETRIS_CONFIG.field.position.y + 16; 196 | } 197 | 198 | _initPausePopup() { 199 | const pausePopup = this._pausePopup = new PausePopup(); 200 | this.addChild(pausePopup); 201 | 202 | pausePopup.x = TETRIS_CONFIG.field.position.x; 203 | pausePopup.y = TETRIS_CONFIG.field.position.y + 16; 204 | } 205 | 206 | _initSignals() { 207 | this._field.events.on('onChangedNextShape', (shapeType) => this._updateNextShape(shapeType)); 208 | this._field.events.on('onLose', () => this._onLose()); 209 | this._field.events.on('onFilledRowsCountChange', (linesCount) => this._onFilledRowsCountChange(linesCount)); 210 | this._field.events.on('onLevelChanged', (level) => this._onLevelChange(level)); 211 | this._field.events.on('onScoreChange', (score) => this._onScoreChange(score)); 212 | this._gameOverPopup.events.on('onWallShowed', () => this._onWallShowed()); 213 | this._gameOverPopup.events.on('onGameOverPopupClick', () => this._onGameOverPopupClick()); 214 | } 215 | 216 | _updateNextShape(shapeType) { 217 | if (this._nextShape) { 218 | this.removeChild(this._nextShape); 219 | } 220 | 221 | const nextShape = this._nextShape = new NextShape(shapeType); 222 | this.addChild(nextShape); 223 | 224 | nextShape.x = GAME_BOY_CONFIG.screen.width - 40 + nextShape.getWidth() * 0.5; 225 | nextShape.y = GAME_BOY_CONFIG.screen.height - 40 + nextShape.getHeight() * 0.5; 226 | 227 | nextShape.show(); 228 | } 229 | 230 | _onLose() { 231 | GameBoyAudio.stopSound(GAME_BOY_SOUND_TYPE.TetrisMusic); 232 | this._isGameActive = false; 233 | this._gameOver = true; 234 | 235 | this._nextShape.hide(); 236 | this._gameOverPopup.show(); 237 | } 238 | 239 | _onFilledRowsCountChange(linesCount) { 240 | if (linesCount >= 9999) { 241 | linesCount = 9999; 242 | } 243 | 244 | this._linesCount.text = linesCount; 245 | } 246 | 247 | _onScoreChange(score) { 248 | if (score >= 999999) { 249 | score = 999999; 250 | } 251 | 252 | if (score > TETRIS_CONFIG.bestScore) { 253 | TETRIS_CONFIG.bestScore = score; 254 | this.events.emit('onBestScoreChange'); 255 | } 256 | 257 | this._score.text = score; 258 | } 259 | 260 | _onLevelChange(level) { 261 | this._level.text = level; 262 | } 263 | 264 | _onWallShowed() { 265 | this._field.hide(); 266 | } 267 | 268 | _onGameOverPopupClick() { 269 | this._gameOverPopup.hide(); 270 | this._startGame(); 271 | 272 | if (TETRIS_CONFIG.isMusicAllowed) { 273 | GameBoyAudio.playSound(GAME_BOY_SOUND_TYPE.TetrisMusic); 274 | } 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy-games/games/tetris/screens/gameplay-screen.js/next-shape.js: -------------------------------------------------------------------------------- 1 | import * as PIXI from 'pixi.js'; 2 | import { TETRIS_CONFIG } from '../../data/tetris-config'; 3 | import Loader from '../../../../../../../core/loader'; 4 | import { SHAPE_CONFIG, SHAPE_TYPE } from './field/shape/shape-config'; 5 | 6 | export default class NextShape extends PIXI.Container { 7 | constructor(type) { 8 | super(); 9 | 10 | this._type = type; 11 | this._blocksView = []; 12 | this._shapePivot = null; 13 | 14 | this._width = 0; 15 | this._height = 0; 16 | 17 | this._init(); 18 | } 19 | 20 | show() { 21 | this.visible = true; 22 | } 23 | 24 | hide() { 25 | this.visible = false; 26 | } 27 | 28 | getWidth() { 29 | return this._width; 30 | } 31 | 32 | getHeight() { 33 | return this._height; 34 | } 35 | 36 | _init() { 37 | if (this._type === SHAPE_TYPE.I) { 38 | this._initShapeI(); 39 | } else { 40 | this._initShape(); 41 | } 42 | 43 | this.cacheAsBitmap = true; 44 | } 45 | 46 | _initShapeI() { 47 | const config = SHAPE_CONFIG[this._type]; 48 | const blocksView = config.blocksView; 49 | 50 | const spriteSheet = Loader.assets['assets/spritesheets/tetris-sheet']; 51 | const blockTexture = spriteSheet.textures[config.textureMiddle]; 52 | const edgeTexture = spriteSheet.textures[config.textureEdge]; 53 | 54 | for (let row = 0; row < blocksView.length; row++) { 55 | this._blocksView[row] = []; 56 | 57 | for (let column = 0; column < blocksView[0].length; column++) { 58 | if (blocksView[row][column] === 1) { 59 | const texture = (column === 0 || column === blocksView[0].length - 1) ? edgeTexture : blockTexture; 60 | const block = new PIXI.Sprite(texture); 61 | this.addChild(block); 62 | 63 | this._blocksView[row][column] = block; 64 | } else { 65 | this._blocksView[row][column] = null; 66 | } 67 | } 68 | } 69 | 70 | this._height = this._blocksView.length * TETRIS_CONFIG.blockSize + 16; 71 | this._width = this._blocksView[0].length * TETRIS_CONFIG.blockSize - 16; 72 | this._shapePivot = config.pivot; 73 | this._updateShapeIBlocksPosition(); 74 | } 75 | 76 | _initShape() { 77 | const config = SHAPE_CONFIG[this._type]; 78 | const blocksView = config.blocksView; 79 | const spriteSheet = Loader.assets['assets/spritesheets/tetris-sheet']; 80 | const blockTexture = spriteSheet.textures[config.texture]; 81 | 82 | for (let row = 0; row < blocksView.length; row++) { 83 | this._blocksView[row] = []; 84 | 85 | for (let column = 0; column < blocksView[0].length; column++) { 86 | if (blocksView[row][column] === 1) { 87 | const block = new PIXI.Sprite(blockTexture); 88 | this.addChild(block); 89 | 90 | if (config.tint) { 91 | block.tint = config.tint; 92 | } 93 | 94 | this._blocksView[row][column] = block; 95 | } else { 96 | this._blocksView[row][column] = null; 97 | } 98 | } 99 | } 100 | 101 | this._height = this._blocksView.length * TETRIS_CONFIG.blockSize; 102 | 103 | if (this._type === SHAPE_TYPE.Invisible) { 104 | this._height = this._blocksView.length * TETRIS_CONFIG.blockSize * 3; 105 | } 106 | 107 | this._width = this._blocksView[0].length * TETRIS_CONFIG.blockSize; 108 | this._shapePivot = config.pivot; 109 | this._updateShapeBlocksPosition(); 110 | } 111 | 112 | _updateShapeBlocksPosition() { 113 | let index = 0; 114 | 115 | for (let row = 0; row < this._blocksView.length; row++) { 116 | for (let column = 0; column < this._blocksView[0].length; column++) { 117 | const block = this._blocksView[row][column]; 118 | 119 | if (block !== null) { 120 | block.x = (column - this._shapePivot.x) * TETRIS_CONFIG.blockSize; 121 | block.y = (row - this._shapePivot.y) * TETRIS_CONFIG.blockSize; 122 | 123 | index += 1; 124 | } 125 | } 126 | } 127 | } 128 | 129 | _updateShapeIBlocksPosition() { 130 | let index = 0; 131 | 132 | for (let row = 0; row < this._blocksView.length; row++) { 133 | for (let column = 0; column < this._blocksView[0].length; column++) { 134 | const block = this._blocksView[row][column]; 135 | 136 | if (block !== null) { 137 | block.rotation = 0; 138 | block.x = (column - this._shapePivot.x) * TETRIS_CONFIG.blockSize; 139 | block.y = (row - this._shapePivot.y) * TETRIS_CONFIG.blockSize; 140 | 141 | if (column === this._blocksView[0].length - 1) { 142 | block.rotation = Math.PI; 143 | block.x += 1 * TETRIS_CONFIG.blockSize; 144 | block.y += 1 * TETRIS_CONFIG.blockSize; 145 | } 146 | 147 | index += 1; 148 | } 149 | } 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy-games/games/tetris/screens/gameplay-screen.js/popups/game-over-popup.js: -------------------------------------------------------------------------------- 1 | import * as PIXI from 'pixi.js'; 2 | import { TETRIS_CONFIG } from '../../../data/tetris-config'; 3 | import Loader from '../../../../../../../../core/loader'; 4 | import Delayed from '../../../../../../../../core/helpers/delayed-call'; 5 | import { BUTTON_TYPE } from '../../../../../../game-boy/data/game-boy-data'; 6 | import GameBoyAudio from '../../../../../../game-boy/game-boy-audio/game-boy-audio'; 7 | import { GAME_BOY_SOUND_TYPE } from '../../../../../../game-boy/game-boy-audio/game-boy-audio-data'; 8 | 9 | export default class GameOverPopup extends PIXI.Container { 10 | constructor() { 11 | super(); 12 | 13 | this.events = new PIXI.utils.EventEmitter(); 14 | 15 | this._width = TETRIS_CONFIG.field.width * TETRIS_CONFIG.blockSize; 16 | this._height = TETRIS_CONFIG.field.height * TETRIS_CONFIG.blockSize; 17 | 18 | this._wallContainer = null; 19 | this._blockLines = []; 20 | this._gameOverContainer = null; 21 | this._animationTimer = null; 22 | this._lineAnimationTimers = []; 23 | 24 | this._isGameOverShowed = false; 25 | 26 | this._showWallLineDelay = 40; 27 | 28 | this._init(); 29 | } 30 | 31 | show() { 32 | this.visible = true; 33 | this._showWall(); 34 | } 35 | 36 | hide() { 37 | this.visible = false; 38 | this._wallContainer.visible = false; 39 | this._gameOverContainer.visible = false; 40 | 41 | this._isGameOverShowed = false; 42 | } 43 | 44 | onButtonPress(buttonType) { 45 | if (this._isGameOverShowed && buttonType === BUTTON_TYPE.Start) { 46 | this.events.emit('onGameOverPopupClick'); 47 | } 48 | } 49 | 50 | stopTweens() { 51 | if (this._animationTimer) { 52 | this._animationTimer.stop(); 53 | } 54 | 55 | this._lineAnimationTimers.forEach((timer) => { 56 | if (timer) { 57 | timer.stop(); 58 | } 59 | }); 60 | } 61 | 62 | reset() { 63 | this.visible = false; 64 | this._wallContainer.visible = false; 65 | this._gameOverContainer.visible = false; 66 | 67 | for (let i = 0; i < this._blockLines.length; i += 1) { 68 | this._blockLines[i].visible = false; 69 | } 70 | 71 | this._isGameOverShowed = false; 72 | } 73 | 74 | _showWall() { 75 | this._wallContainer.visible = true; 76 | 77 | GameBoyAudio.playSound(GAME_BOY_SOUND_TYPE.TetrisGameOver); 78 | this._wallShowAnimation(); 79 | } 80 | 81 | _wallShowAnimation() { 82 | let index = 0; 83 | 84 | for (let i = this._blockLines.length - 1; i >= 0; i--) { 85 | const lineAnimationTimer = Delayed.call(this._showWallLineDelay * index, () => { 86 | this._blockLines[i].visible = true; 87 | }); 88 | 89 | index += 1; 90 | this._lineAnimationTimers[i] = lineAnimationTimer; 91 | } 92 | 93 | this._animationTimer = Delayed.call(this._showWallLineDelay * this._blockLines.length + 100, () => { 94 | this.events.emit('onWallShowed'); 95 | this._gameOverContainer.visible = true; 96 | this._wallHideAnimation(); 97 | 98 | GameBoyAudio.playSound(GAME_BOY_SOUND_TYPE.TetrisGameOverFinal); 99 | }); 100 | } 101 | 102 | _wallHideAnimation() { 103 | let index = 0; 104 | 105 | for (let i = this._blockLines.length - 1; i >= 0; i--) { 106 | const lineAnimationTimer = Delayed.call(this._showWallLineDelay * index, () => { 107 | this._blockLines[i].visible = false; 108 | }); 109 | 110 | index += 1; 111 | this._lineAnimationTimers[i] = lineAnimationTimer; 112 | } 113 | 114 | this._animationTimer = Delayed.call(this._showWallLineDelay * this._blockLines.length, () => { 115 | this._wallContainer.visible = false; 116 | this._isGameOverShowed = true; 117 | }); 118 | } 119 | 120 | _init() { 121 | this._initGameOverContainer(); 122 | this._initWall(); 123 | 124 | this.visible = false; 125 | } 126 | 127 | _initWall() { 128 | const wallContainer = this._wallContainer = new PIXI.Container(); 129 | this.addChild(wallContainer); 130 | 131 | for (let i = 0; i < TETRIS_CONFIG.field.height; i++) { 132 | const blockLine = this._createBlockLine(); 133 | wallContainer.addChild(blockLine); 134 | 135 | blockLine.y = i * TETRIS_CONFIG.blockSize; 136 | 137 | blockLine.visible = false; 138 | this._blockLines.push(blockLine); 139 | } 140 | 141 | wallContainer.visible = false; 142 | } 143 | 144 | _initGameOverContainer() { 145 | const gameOverContainer = this._gameOverContainer = new PIXI.Container(); 146 | this.addChild(gameOverContainer); 147 | 148 | this._initGameOverFrame(); 149 | this._initTryAgainText(); 150 | 151 | gameOverContainer.cacheAsBitmap = true; 152 | gameOverContainer.visible = false; 153 | } 154 | 155 | _initGameOverFrame() { 156 | const spriteSheet = Loader.assets['assets/spritesheets/tetris-sheet']; 157 | const texture = spriteSheet.textures['game-over-frame.png']; 158 | 159 | const gameOverFrame = new PIXI.Sprite(texture); 160 | this._gameOverContainer.addChild(gameOverFrame); 161 | 162 | gameOverFrame.anchor.set(0.5); 163 | 164 | gameOverFrame.x = this._width * 0.5; 165 | gameOverFrame.y = 44; 166 | 167 | const text01 = this._createTextLine('GAME'); 168 | const text02 = this._createTextLine('OVER'); 169 | 170 | this._gameOverContainer.addChild(text01, text02); 171 | 172 | text01.x = this._width * 0.5 + 1; 173 | text01.y = 30; 174 | 175 | text02.x = this._width * 0.5 + 1; 176 | text02.y = 46; 177 | } 178 | 179 | _initTryAgainText() { 180 | const text01 = this._createTextLine('TRY'); 181 | const text02 = this._createTextLine('AGAIN'); 182 | 183 | this._gameOverContainer.addChild(text01, text02); 184 | 185 | text01.x = this._width * 0.5; 186 | text01.y = 96; 187 | 188 | text02.x = this._width * 0.5; 189 | text02.y = 108; 190 | } 191 | 192 | _createTextLine(string) { 193 | const text = new PIXI.Text(string, new PIXI.TextStyle({ 194 | fontFamily: 'tetris', 195 | fontSize: 8, 196 | fill: 0x000000, 197 | })); 198 | 199 | text.anchor.set(0.5, 0); 200 | 201 | return text; 202 | } 203 | 204 | _createBlockLine() { 205 | const blockLineContainer = new PIXI.Container(); 206 | this.addChild(blockLineContainer); 207 | 208 | const spriteSheet = Loader.assets['assets/spritesheets/tetris-sheet']; 209 | const texture = spriteSheet.textures['game-over-block.png']; 210 | 211 | for (let i = 0; i < TETRIS_CONFIG.field.width; i++) { 212 | const block = new PIXI.Sprite(texture); 213 | blockLineContainer.addChild(block); 214 | 215 | block.x = i * TETRIS_CONFIG.blockSize; 216 | } 217 | 218 | blockLineContainer.cacheAsBitmap = true; 219 | 220 | return blockLineContainer; 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy-games/games/tetris/screens/gameplay-screen.js/popups/pause-popup.js: -------------------------------------------------------------------------------- 1 | import * as PIXI from 'pixi.js'; 2 | import { TETRIS_CONFIG } from '../../../data/tetris-config'; 3 | import GameBoyAudio from '../../../../../../game-boy/game-boy-audio/game-boy-audio'; 4 | import { GAME_BOY_SOUND_TYPE } from '../../../../../../game-boy/game-boy-audio/game-boy-audio-data'; 5 | 6 | export default class PausePopup extends PIXI.Container { 7 | constructor() { 8 | super(); 9 | 10 | this._width = TETRIS_CONFIG.field.width * TETRIS_CONFIG.blockSize; 11 | this._height = TETRIS_CONFIG.field.height * TETRIS_CONFIG.blockSize; 12 | 13 | this._init(); 14 | } 15 | 16 | show() { 17 | this.visible = true; 18 | GameBoyAudio.playSound(GAME_BOY_SOUND_TYPE.TetrisPause); 19 | } 20 | 21 | hide() { 22 | this.visible = false; 23 | } 24 | 25 | reset() { 26 | this.visible = false; 27 | } 28 | 29 | _init() { 30 | this._createTextLine('PAUSE', 32); 31 | this._createTextLine('PRESS', 80); 32 | this._createTextLine('START TO', 92); 33 | this._createTextLine('CONTINUE', 104); 34 | 35 | this.cacheAsBitmap = true; 36 | 37 | this.visible = false; 38 | } 39 | 40 | _createTextLine(string, y) { 41 | const text = new PIXI.Text(string, new PIXI.TextStyle({ 42 | fontFamily: 'tetris', 43 | fontSize: 8, 44 | fill: 0x000000, 45 | })); 46 | 47 | this.addChild(text); 48 | text.anchor.set(0.5, 0); 49 | 50 | text.x = this._width * 0.5; 51 | text.y = y; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy-games/games/tetris/screens/license-screen/license-screen.js: -------------------------------------------------------------------------------- 1 | import * as PIXI from 'pixi.js'; 2 | import Loader from '../../../../../../../core/loader'; 3 | import GameScreenAbstract from '../../../shared/game-screen-abstract'; 4 | import Delayed from '../../../../../../../core/helpers/delayed-call'; 5 | import { TETRIS_SCREEN_TYPE } from '../../data/tetris-data'; 6 | 7 | export default class LicenseScreen extends GameScreenAbstract { 8 | constructor() { 9 | super(); 10 | 11 | this._screenType = TETRIS_SCREEN_TYPE.License; 12 | this._delay = null; 13 | 14 | this._init(); 15 | } 16 | 17 | show() { 18 | super.show(); 19 | 20 | this._delay = Delayed.call(2000, () => { 21 | this.events.emit('onComplete'); 22 | }); 23 | } 24 | 25 | stopTweens() { 26 | if (this._delay) { 27 | this._delay.stop(); 28 | } 29 | } 30 | 31 | _init() { 32 | const spriteSheet = Loader.assets['assets/spritesheets/tetris-sheet']; 33 | const texture = spriteSheet.textures['license-screen.png']; 34 | 35 | const screen = new PIXI.Sprite(texture); 36 | this.addChild(screen); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy-games/games/tetris/screens/title-screen/title-screen.js: -------------------------------------------------------------------------------- 1 | import * as PIXI from 'pixi.js'; 2 | import Loader from '../../../../../../../core/loader'; 3 | import { GAME_BOY_CONFIG } from '../../../../../game-boy/data/game-boy-config'; 4 | import GameScreenAbstract from '../../../shared/game-screen-abstract'; 5 | import Delayed from '../../../../../../../core/helpers/delayed-call'; 6 | import { TETRIS_SCREEN_TYPE } from '../../data/tetris-data'; 7 | import { BUTTON_TYPE } from '../../../../../game-boy/data/game-boy-data'; 8 | import GameBoyAudio from '../../../../../game-boy/game-boy-audio/game-boy-audio'; 9 | import { GAME_BOY_SOUND_TYPE } from '../../../../../game-boy/game-boy-audio/game-boy-audio-data'; 10 | import { TETRIS_CONFIG } from '../../data/tetris-config'; 11 | 12 | export default class TitleScreen extends GameScreenAbstract { 13 | constructor() { 14 | super(); 15 | 16 | this._screenType = TETRIS_SCREEN_TYPE.Title; 17 | this._arrow = null; 18 | this._blinkTimer = null; 19 | 20 | this._init(); 21 | } 22 | 23 | show() { 24 | super.show(); 25 | 26 | GameBoyAudio.playSound(GAME_BOY_SOUND_TYPE.TetrisMusic); 27 | this._blinkArrow(); 28 | } 29 | 30 | onButtonPress(buttonType) { 31 | if (buttonType === BUTTON_TYPE.Start || buttonType === BUTTON_TYPE.A || buttonType === BUTTON_TYPE.B) { 32 | this.events.emit('onStartGame'); 33 | } 34 | 35 | if (buttonType === BUTTON_TYPE.Select) { 36 | GameBoyAudio.switchSound(GAME_BOY_SOUND_TYPE.TetrisMusic); 37 | TETRIS_CONFIG.isMusicAllowed = !TETRIS_CONFIG.isMusicAllowed; 38 | } 39 | } 40 | 41 | stopTweens() { 42 | if (this._blinkTimer) { 43 | this._blinkTimer.stop(); 44 | } 45 | } 46 | 47 | _blinkArrow() { 48 | this._blinkTimer = Delayed.call(700, () => { 49 | this._arrow.visible = !this._arrow.visible; 50 | this._blinkArrow(); 51 | }); 52 | } 53 | 54 | _init() { 55 | this._initBackground(); 56 | this._initStartText(); 57 | this._initArrow(); 58 | } 59 | 60 | _initBackground() { 61 | const spriteSheet = Loader.assets['assets/spritesheets/tetris-sheet']; 62 | const texture = spriteSheet.textures['title-screen.png']; 63 | 64 | const screen = new PIXI.Sprite(texture); 65 | this.addChild(screen); 66 | } 67 | 68 | _initStartText() { 69 | const text = new PIXI.Text('Start game', new PIXI.TextStyle({ 70 | fontFamily: 'tetris', 71 | fontSize: 8, 72 | })); 73 | 74 | this.addChild(text); 75 | 76 | text.anchor.set(0.5, 0); 77 | 78 | text.x = GAME_BOY_CONFIG.screen.width * 0.5; 79 | text.y = 113; 80 | } 81 | 82 | _initArrow() { 83 | const arrow = this._arrow = new PIXI.Graphics(); 84 | this.addChild(arrow); 85 | 86 | arrow.beginFill(0x000000); 87 | arrow.moveTo(0, 0); 88 | arrow.lineTo(4, 3); 89 | arrow.lineTo(0, 6); 90 | 91 | arrow.x = GAME_BOY_CONFIG.screen.width * 0.5 - 45; 92 | arrow.y = 116; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy-games/games/tetris/tetris.js: -------------------------------------------------------------------------------- 1 | import LicenseScreen from './screens/license-screen/license-screen'; 2 | import TitleScreen from './screens/title-screen/title-screen'; 3 | import GameplayScreen from './screens/gameplay-screen.js/gameplay-screen'; 4 | import GameAbstract from '../game-abstract'; 5 | import { TETRIS_SCREEN_TYPE } from './data/tetris-data'; 6 | import { MessageDispatcher } from 'black-engine'; 7 | import { TETRIS_CONFIG } from './data/tetris-config'; 8 | import DEBUG_CONFIG from '../../../../../core/configs/debug-config'; 9 | import { GAME_TYPE } from '../../data/games-config'; 10 | 11 | export default class Tetris extends GameAbstract { 12 | constructor() { 13 | super(); 14 | 15 | this.events = new MessageDispatcher(); 16 | 17 | this._screens = {}; 18 | this._currentScreenType = null; 19 | 20 | this._init(); 21 | } 22 | 23 | update(dt) { 24 | this._screens[this._currentScreenType].update(dt); 25 | } 26 | 27 | show() { 28 | super.show(); 29 | 30 | this._reset(); 31 | 32 | if (DEBUG_CONFIG.startState.loadGame === GAME_TYPE.Tetris && DEBUG_CONFIG.startState.startScreen) { 33 | this._showScreen(DEBUG_CONFIG.startState.startScreen); 34 | } else { 35 | this._showScreen(TETRIS_SCREEN_TYPE.License); 36 | } 37 | } 38 | 39 | hide() { 40 | super.hide(); 41 | 42 | this._hideAllScreens(); 43 | this._reset(); 44 | } 45 | 46 | onButtonPress(buttonType) { 47 | if (!this._currentScreenType) { 48 | return; 49 | } 50 | 51 | this._screens[this._currentScreenType].onButtonPress(buttonType); 52 | } 53 | 54 | onButtonUp(buttonType) { 55 | if (!this._currentScreenType) { 56 | return; 57 | } 58 | 59 | this._screens[this._currentScreenType].onButtonUp(buttonType); 60 | } 61 | 62 | stopTweens() { 63 | for (let screenType in this._screens) { 64 | this._screens[screenType].stopTweens(); 65 | } 66 | } 67 | 68 | startGameAtLevel(level) { 69 | TETRIS_CONFIG.startLevel = level; 70 | 71 | this.stopTweens(); 72 | this._hideAllScreens(); 73 | this._reset(); 74 | 75 | this._showScreen(TETRIS_SCREEN_TYPE.Gameplay); 76 | } 77 | 78 | disableFalling() { 79 | this._screens[TETRIS_SCREEN_TYPE.Gameplay].disableFalling(); 80 | } 81 | 82 | clearBottomLine() { 83 | this._screens[TETRIS_SCREEN_TYPE.Gameplay].clearBottomLine(); 84 | } 85 | 86 | _reset() { 87 | for (let screenType in this._screens) { 88 | this._screens[screenType].reset(); 89 | } 90 | } 91 | 92 | _hideAllScreens() { 93 | for (let screenType in this._screens) { 94 | this._screens[screenType].hide(); 95 | } 96 | } 97 | 98 | _showScreen(screenType) { 99 | this._currentScreenType = screenType; 100 | this._screens[screenType].show(); 101 | } 102 | 103 | _init() { 104 | this._initScreens(); 105 | this._initSignals(); 106 | 107 | this.visible = false; 108 | } 109 | 110 | _initScreens() { 111 | this._initLicenseScreen(); 112 | this._initTitleScreen(); 113 | this._initGameplayScreen(); 114 | } 115 | 116 | _initLicenseScreen() { 117 | const licenseScreen = new LicenseScreen(); 118 | this.addChild(licenseScreen); 119 | 120 | this._screens[TETRIS_SCREEN_TYPE.License] = licenseScreen; 121 | } 122 | 123 | _initTitleScreen() { 124 | const titleScreen = new TitleScreen(); 125 | this.addChild(titleScreen); 126 | 127 | this._screens[TETRIS_SCREEN_TYPE.Title] = titleScreen; 128 | } 129 | 130 | _initGameplayScreen() { 131 | const gameplayScreen = new GameplayScreen(); 132 | this.addChild(gameplayScreen); 133 | 134 | this._screens[TETRIS_SCREEN_TYPE.Gameplay] = gameplayScreen; 135 | } 136 | 137 | _initSignals() { 138 | this._screens[TETRIS_SCREEN_TYPE.License].events.on('onComplete', () => this._onLicenseScreenComplete()); 139 | this._screens[TETRIS_SCREEN_TYPE.Title].events.on('onStartGame', () => this._onStartGame()); 140 | this._screens[TETRIS_SCREEN_TYPE.Gameplay].events.on('onBestScoreChange', () => this.events.post('onBestScoreChange')); 141 | } 142 | 143 | _onLicenseScreenComplete() { 144 | this._screens[TETRIS_SCREEN_TYPE.License].hide(); 145 | this._showScreen(TETRIS_SCREEN_TYPE.Title); 146 | } 147 | 148 | _onStartGame() { 149 | this._screens[TETRIS_SCREEN_TYPE.Title].hide(); 150 | this._showScreen(TETRIS_SCREEN_TYPE.Gameplay); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy-games/games/zelda/zelda.js: -------------------------------------------------------------------------------- 1 | import * as PIXI from 'pixi.js'; 2 | import GameAbstract from "../game-abstract"; 3 | import GameBoyAudio from '../../../game-boy/game-boy-audio/game-boy-audio'; 4 | import { GAME_BOY_SOUND_TYPE } from '../../../game-boy/game-boy-audio/game-boy-audio-data'; 5 | 6 | export default class Zelda extends GameAbstract { 7 | constructor() { 8 | super(); 9 | 10 | this.events = new PIXI.utils.EventEmitter(); 11 | } 12 | 13 | show() { 14 | super.show(); 15 | 16 | GameBoyAudio.playSound(GAME_BOY_SOUND_TYPE.ZeldaIntro); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy-games/overlay/volume-overlay.js: -------------------------------------------------------------------------------- 1 | import * as PIXI from 'pixi.js'; 2 | import { GAME_BOY_CONFIG } from '../../game-boy/data/game-boy-config'; 3 | import Delayed from '../../../../core/helpers/delayed-call'; 4 | import { SOUNDS_CONFIG } from '../../../../core/configs/sounds-config'; 5 | 6 | export default class VolumeOverlay extends PIXI.Container { 7 | constructor() { 8 | super(); 9 | 10 | this._volumeBarParts = []; 11 | this._hideTimer = null; 12 | 13 | this._width = 104; 14 | this._height = 20; 15 | 16 | this._init(); 17 | } 18 | 19 | show() { 20 | this.visible = true; 21 | } 22 | 23 | hide() { 24 | this.visible = false; 25 | } 26 | 27 | onVolumeChanged() { 28 | const volume = SOUNDS_CONFIG.gameBoyVolume; 29 | this.setVolume(volume); 30 | 31 | this.show(); 32 | 33 | if (this._hideTimer) { 34 | this._hideTimer.stop(); 35 | } 36 | 37 | this._hideTimer = Delayed.call(GAME_BOY_CONFIG.volumeController.hideTime, () => { 38 | this.hide(); 39 | }); 40 | } 41 | 42 | setVolume(volume) { 43 | const volumeBarPartsCount = Math.round(volume * 20); 44 | 45 | for (let i = 0; i < this._volumeBarParts.length; i++) { 46 | if (i < volumeBarPartsCount) { 47 | this._volumeBarParts[i].visible = true; 48 | } else { 49 | this._volumeBarParts[i].visible = false; 50 | } 51 | } 52 | } 53 | 54 | _init() { 55 | this._initFrame(); 56 | this._initVolumeText(); 57 | this._initVolumeBar(); 58 | 59 | this.pivot.x = this.width * 0.5; 60 | this.pivot.y = this.height * 0.5; 61 | 62 | this.visible = false; 63 | } 64 | 65 | _initFrame() { 66 | const borderThickness = 1; 67 | 68 | const frame = new PIXI.Graphics(); 69 | this.addChild(frame); 70 | 71 | frame.beginFill(0x000000); 72 | frame.drawRect(0, 0, this._width, this._height); 73 | frame.endFill(); 74 | 75 | const background = new PIXI.Graphics(); 76 | this.addChild(background); 77 | 78 | background.beginFill(0xffffff); 79 | background.drawRect(0, 0, this._width - borderThickness * 2, this._height - borderThickness * 2); 80 | background.endFill(); 81 | 82 | background.x = borderThickness; 83 | background.y = borderThickness; 84 | } 85 | 86 | _initVolumeText() { 87 | const text = new PIXI.Text('VOLUME', new PIXI.TextStyle({ 88 | fontFamily: 'tetris', 89 | fontSize: 8, 90 | fill: 0x000000, 91 | })); 92 | 93 | this.addChild(text); 94 | 95 | text.x = 3; 96 | } 97 | 98 | _initVolumeBar() { 99 | const volumeParts = 20; 100 | 101 | for (let i = 0; i < volumeParts; i += 1) { 102 | const volumePart = new PIXI.Graphics(); 103 | this.addChild(volumePart); 104 | 105 | volumePart.beginFill(0x000000); 106 | volumePart.drawRect(0, 0, 4, 7); 107 | volumePart.endFill(); 108 | 109 | volumePart.x = 2 + i * 5; 110 | volumePart.y = 11; 111 | 112 | volumePart.visible = false; 113 | this._volumeBarParts.push(volumePart); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy-games/screens/damaged-cartridge-screen.js: -------------------------------------------------------------------------------- 1 | import * as PIXI from 'pixi.js'; 2 | import { GAME_BOY_CONFIG } from '../../game-boy/data/game-boy-config'; 3 | import Loader from '../../../../core/loader'; 4 | import ScreenAbstract from './screen-abstract'; 5 | 6 | export default class DamagedCartridgeScreen extends ScreenAbstract { 7 | constructor() { 8 | super(); 9 | 10 | this._init(); 11 | } 12 | 13 | _init() { 14 | this._initStopSign(); 15 | this._initText(); 16 | 17 | this.visible = false; 18 | } 19 | 20 | _initStopSign() { 21 | const texture = Loader.assets['assets/other/stop-sign']; 22 | 23 | const stopSign = new PIXI.Sprite(texture); 24 | this.addChild(stopSign); 25 | 26 | stopSign.anchor.set(0.5); 27 | 28 | stopSign.x = GAME_BOY_CONFIG.screen.width * 0.5; 29 | stopSign.y = GAME_BOY_CONFIG.screen.height * 0.5 - 20; 30 | } 31 | 32 | _initText() { 33 | const textContainer = new PIXI.Container(); 34 | this.addChild(textContainer); 35 | 36 | const textLine01 = this._createTextLine('The cartridge'); 37 | const textLine02 = this._createTextLine('is not working'); 38 | textContainer.addChild(textLine01, textLine02); 39 | 40 | textLine01.y = -5; 41 | textLine02.y = 5; 42 | 43 | textContainer.x = GAME_BOY_CONFIG.screen.width * 0.5; 44 | textContainer.y = GAME_BOY_CONFIG.screen.height * 0.5 + 30; 45 | } 46 | 47 | _createTextLine(string) { 48 | const text = new PIXI.Text(string, new PIXI.TextStyle({ 49 | fontFamily: 'tetris', 50 | fontSize: 8, 51 | fill: 0x000000, 52 | })); 53 | 54 | text.anchor.set(0.5, 0); 55 | 56 | return text; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy-games/screens/loading-screen.js: -------------------------------------------------------------------------------- 1 | import * as PIXI from 'pixi.js'; 2 | import Loader from '../../../../core/loader'; 3 | import { GAME_BOY_CONFIG } from '../../game-boy/data/game-boy-config'; 4 | import { TWEEN } from '/node_modules/three/examples/jsm/libs/tween.module.min.js'; 5 | import Delayed from '../../../../core/helpers/delayed-call'; 6 | import ScreenAbstract from './screen-abstract'; 7 | import GameBoyAudio from '../../game-boy/game-boy-audio/game-boy-audio'; 8 | import { GAME_BOY_SOUND_TYPE } from '../../game-boy/game-boy-audio/game-boy-audio-data'; 9 | 10 | export default class LoadingScreen extends ScreenAbstract { 11 | constructor() { 12 | super(); 13 | 14 | this.events = new PIXI.utils.EventEmitter(); 15 | 16 | this._logo = null; 17 | this._movingTween = null; 18 | this._delayToStart = null; 19 | 20 | this._init(); 21 | } 22 | 23 | show() { 24 | this.visible = true; 25 | 26 | this.stopTweens(); 27 | this._logo.y = -15; 28 | 29 | this._movingTween = new TWEEN.Tween(this._logo) 30 | .to({ y: GAME_BOY_CONFIG.screen.height * 0.5 }, 2500) 31 | .easing(TWEEN.Easing.Linear.None) 32 | .start() 33 | .onComplete(() => { 34 | GameBoyAudio.playSound(GAME_BOY_SOUND_TYPE.GameBoyLoad); 35 | 36 | this._delayToStart = Delayed.call(1000, () => { 37 | this.hide(); 38 | this.events.emit('onComplete'); 39 | }); 40 | }); 41 | } 42 | 43 | hide() { 44 | this.visible = false; 45 | 46 | this.stopTweens(); 47 | } 48 | 49 | stopTweens() { 50 | if (this._movingTween) { 51 | this._movingTween.stop(); 52 | } 53 | 54 | if (this._delayToStart) { 55 | this._delayToStart.stop(); 56 | } 57 | } 58 | 59 | _init() { 60 | this._initLogo(); 61 | 62 | this.hide(); 63 | } 64 | 65 | _initLogo() { 66 | const texture = Loader.assets['assets/other/nintendo-logo-screen']; 67 | 68 | const logo = this._logo = new PIXI.Sprite(texture); 69 | logo.anchor.set(0.5); 70 | this.addChild(logo); 71 | 72 | logo.x = GAME_BOY_CONFIG.screen.width * 0.5; 73 | logo.y = GAME_BOY_CONFIG.screen.height * 0.5; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy-games/screens/no-cartridge-screen.js: -------------------------------------------------------------------------------- 1 | import * as PIXI from 'pixi.js'; 2 | import { GAME_BOY_CONFIG } from '../../game-boy/data/game-boy-config'; 3 | import ScreenAbstract from './screen-abstract'; 4 | 5 | export default class NoCartridgeScreen extends ScreenAbstract { 6 | constructor() { 7 | super(); 8 | 9 | this._init(); 10 | } 11 | 12 | _init() { 13 | this._initText(); 14 | 15 | this.visible = false; 16 | } 17 | 18 | _initText() { 19 | const text = new PIXI.Text('insert cartridge', new PIXI.TextStyle({ 20 | fontFamily: 'tetris', 21 | fontSize: 8, 22 | fill: 0x00000, 23 | })); 24 | 25 | this.addChild(text); 26 | text.anchor.set(0.5, 0); 27 | 28 | text.x = GAME_BOY_CONFIG.screen.width * 0.5; 29 | text.y = GAME_BOY_CONFIG.screen.height * 0.5 - 5; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy-games/screens/screen-abstract.js: -------------------------------------------------------------------------------- 1 | import * as PIXI from 'pixi.js'; 2 | 3 | export default class ScreenAbstract extends PIXI.Container { 4 | constructor() { 5 | super(); 6 | } 7 | 8 | update(dt) { } 9 | 10 | show() { 11 | this.visible = true; 12 | } 13 | 14 | hide() { 15 | this.visible = false; 16 | } 17 | 18 | stopTweens() { } 19 | 20 | onButtonPress(buttonType) { } 21 | 22 | onButtonUp(buttonType) { } 23 | } 24 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy-scene.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import GameBoyController from './game-boy-scene-controller'; 3 | import GameBoy from './game-boy/game-boy'; 4 | import CartridgesController from './cartridges/cartridges-controller'; 5 | import { SCENE_OBJECT_TYPE } from './data/game-boy-scene-data'; 6 | import GameBoyGames from './game-boy-games/game-boy-games'; 7 | import GameBoyDebug from './game-boy-debug'; 8 | import CameraController from './camera-controller/camera-controller'; 9 | import Background from './background/background'; 10 | import { MessageDispatcher } from 'black-engine'; 11 | import SCENE_CONFIG from '../../core/configs/scene-config'; 12 | 13 | export default class GameBoyScene extends THREE.Group { 14 | constructor(data, raycasterController) { 15 | super(); 16 | 17 | this.events = new MessageDispatcher(); 18 | 19 | this._data = data; 20 | this._data.raycasterController = raycasterController; 21 | 22 | this._gameBoyController = null; 23 | this._gameBoyGames = null; 24 | this._gameBoyDebug = null; 25 | this._activeObjects = {}; 26 | 27 | this._isSoundPlayed = false; 28 | 29 | this._init(); 30 | } 31 | 32 | update(dt) { 33 | if (dt > 0.1) { 34 | dt = 0.1; 35 | } 36 | 37 | this._gameBoyController.update(dt); 38 | this._gameBoyGames.update(dt); 39 | } 40 | 41 | onPointerMove(x, y) { 42 | this._gameBoyController.onPointerMove(x, y); 43 | } 44 | 45 | onPointerDown(x, y) { 46 | this._gameBoyController.onPointerDown(x, y); 47 | } 48 | 49 | onPointerUp(x, y) { 50 | this._gameBoyController.onPointerUp(x, y); 51 | } 52 | 53 | onWheelScroll(delta) { 54 | this._gameBoyController.onWheelScroll(delta); 55 | } 56 | 57 | onSoundChanged() { 58 | this._gameBoyController.onUISoundIconChanged(); 59 | } 60 | 61 | _init() { 62 | this._initGameBoy(); 63 | this._initCartridgesController(); 64 | this._initGameBoyGames(); 65 | this._initGameBoyDebug(); 66 | this._initCameraController(); 67 | this._initBackground(); 68 | this._configureRaycaster(); 69 | this._initGameBoyController(); 70 | this._initEmptySound(); 71 | 72 | this._initSignals(); 73 | } 74 | 75 | _initGameBoy() { 76 | const pixiCanvas = this._data.pixiApplication.view; 77 | const pixiApplication = this._data.pixiApplication; 78 | const audioListener = this._data.audioListener; 79 | 80 | const gameBoy = new GameBoy(pixiCanvas, pixiApplication, audioListener); 81 | this.add(gameBoy); 82 | 83 | this._activeObjects[SCENE_OBJECT_TYPE.GameBoy] = gameBoy; 84 | } 85 | 86 | _initCartridgesController() { 87 | const cartridgesController = new CartridgesController(); 88 | this.add(cartridgesController); 89 | 90 | this._activeObjects[SCENE_OBJECT_TYPE.Cartridges] = cartridgesController; 91 | } 92 | 93 | _initGameBoyGames() { 94 | const pixiApplication = this._data.pixiApplication; 95 | 96 | this._gameBoyGames = new GameBoyGames(pixiApplication); 97 | } 98 | 99 | _initGameBoyDebug() { 100 | this._gameBoyDebug = new GameBoyDebug(); 101 | } 102 | 103 | _initCameraController() { 104 | this._cameraController = new CameraController(this._data.camera); 105 | } 106 | 107 | _initBackground() { 108 | const background = this._background = new Background(); 109 | this.add(background); 110 | 111 | this._activeObjects[SCENE_OBJECT_TYPE.Background] = background; 112 | } 113 | 114 | _configureRaycaster() { 115 | const allMeshes = []; 116 | const gameBoy = this._activeObjects[SCENE_OBJECT_TYPE.GameBoy]; 117 | const cartridges = this._activeObjects[SCENE_OBJECT_TYPE.Cartridges]; 118 | 119 | allMeshes.push(...gameBoy.getAllMeshes()); 120 | allMeshes.push(...cartridges.getAllMeshes()); 121 | allMeshes.push(this._background.getMesh()); 122 | 123 | this._data.raycasterController.addMeshes(allMeshes); 124 | } 125 | 126 | _initGameBoyController() { 127 | this._data.activeObjects = this._activeObjects; 128 | this._data.games = this._gameBoyGames; 129 | this._data.gameBoyDebug = this._gameBoyDebug; 130 | this._data.cameraController = this._cameraController; 131 | this._data.background = this._background; 132 | 133 | this._gameBoyController = new GameBoyController(this._data); 134 | } 135 | 136 | _initEmptySound() { 137 | if (SCENE_CONFIG.isMobile) { 138 | window.addEventListener('touchstart', () => { 139 | if (this._isSoundPlayed) { 140 | return; 141 | } 142 | 143 | const sound = new THREE.PositionalAudio(this._data.audioListener); 144 | sound.setVolume(0); 145 | sound.play(); 146 | 147 | this._isSoundPlayed = true; 148 | }); 149 | } 150 | } 151 | 152 | _initSignals() { 153 | this._gameBoyController.events.on('fpsMeterChanged', () => this.events.post('fpsMeterChanged')); 154 | this._gameBoyController.events.on('onSoundsEnabledChanged', () => this.events.post('onSoundsEnabledChanged')); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy/data/game-boy-config.js: -------------------------------------------------------------------------------- 1 | import { BUTTON_TYPE, GAME_BOY_PART_TYPE } from "./game-boy-data"; 2 | 3 | const GAME_BOY_CONFIG = { 4 | powerOn: false, 5 | updateTexture: true, 6 | currentCartridge: 'NONE', 7 | screen: { 8 | width: 160 * 1, 9 | height: 144 * 1, 10 | scale: 1, 11 | tint: '#96a06e', 12 | }, 13 | powerButton: { 14 | moveDistance: 0.114, 15 | moveSpeed: 1.3, 16 | powerIndicatorColor: 0xff0000, 17 | }, 18 | buttons: { 19 | firstRepeatTime: 400, 20 | repeatTime: 0.04, 21 | }, 22 | volumeController: { 23 | sensitivity: 0.01, 24 | maxAngle: 120, 25 | hideTime: 1200, 26 | }, 27 | rotation: { 28 | rotationCursorEnabled: true, 29 | rotationDragEnabled: true, 30 | debugRotationCursorEnabled: true, 31 | debugRotationDragEnabled:true, 32 | returnTime: 5000, 33 | cursorRotationSpeed: 0.2, 34 | dragRotationSpeed: 4, 35 | mobileDragRotationSpeed: 8, 36 | standardLerpSpeed: 0.05, 37 | slowLerpSpeed: 0.01, 38 | fastLerpSpeed: 0.07, 39 | zoomThresholdToDisableRotation: 2.3, 40 | }, 41 | intro: { 42 | enabled: true, 43 | speed: 5, 44 | rotationX: -20, 45 | }, 46 | } 47 | 48 | const GAME_BOY_BUTTONS_CONFIG = { 49 | [BUTTON_TYPE.A]: { 50 | moveDistance: 0.055, 51 | moveSpeed: 0.5, 52 | keyRepeat: false, 53 | keyCode: ['KeyX', 'Space'], 54 | }, 55 | [BUTTON_TYPE.B]: { 56 | moveDistance: 0.055, 57 | moveSpeed: 0.5, 58 | keyRepeat: false, 59 | keyCode: ['KeyZ'], 60 | }, 61 | [BUTTON_TYPE.Select]: { 62 | moveDistance: 0.039, 63 | moveSpeed: 0.5, 64 | keyRepeat: false, 65 | }, 66 | [BUTTON_TYPE.Start]: { 67 | moveDistance: 0.039, 68 | moveSpeed: 0.5, 69 | keyRepeat: false, 70 | keyCode: ['Enter'], 71 | }, 72 | [BUTTON_TYPE.CrossLeft]: { 73 | rotateAxis: 'y', 74 | rotateAngle: -8, 75 | moveSpeed: 1, 76 | keyRepeat: true, 77 | keyCode: ['ArrowLeft', 'KeyA'], 78 | }, 79 | [BUTTON_TYPE.CrossRight]: { 80 | rotateAxis: 'y', 81 | rotateAngle: 8, 82 | moveSpeed: 1, 83 | keyRepeat: true, 84 | keyCode: ['ArrowRight', 'KeyD'], 85 | }, 86 | [BUTTON_TYPE.CrossUp]: { 87 | rotateAxis: 'x', 88 | rotateAngle: -8, 89 | moveSpeed: 1, 90 | keyRepeat: false, 91 | keyCode: ['ArrowUp', 'KeyW'], 92 | }, 93 | [BUTTON_TYPE.CrossDown]: { 94 | rotateAxis: 'x', 95 | rotateAngle: 8, 96 | moveSpeed: 1, 97 | keyRepeat: true, 98 | keyCode: ['ArrowDown', 'KeyS'], 99 | }, 100 | } 101 | 102 | const GAME_BOY_BUTTON_PART_BY_TYPE = { 103 | [GAME_BOY_PART_TYPE.ButtonA]: BUTTON_TYPE.A, 104 | [GAME_BOY_PART_TYPE.ButtonB]: BUTTON_TYPE.B, 105 | [GAME_BOY_PART_TYPE.ButtonStart]: BUTTON_TYPE.Start, 106 | [GAME_BOY_PART_TYPE.ButtonSelect]: BUTTON_TYPE.Select, 107 | [GAME_BOY_PART_TYPE.ButtonCrossLeft]: BUTTON_TYPE.CrossLeft, 108 | [GAME_BOY_PART_TYPE.ButtonCrossRight]: BUTTON_TYPE.CrossRight, 109 | [GAME_BOY_PART_TYPE.ButtonCrossUp]: BUTTON_TYPE.CrossUp, 110 | [GAME_BOY_PART_TYPE.ButtonCrossDown]: BUTTON_TYPE.CrossDown, 111 | } 112 | 113 | const CROSS_BUTTONS = [ 114 | BUTTON_TYPE.CrossLeft, 115 | BUTTON_TYPE.CrossRight, 116 | BUTTON_TYPE.CrossUp, 117 | BUTTON_TYPE.CrossDown, 118 | ] 119 | 120 | export { 121 | GAME_BOY_BUTTONS_CONFIG, 122 | GAME_BOY_CONFIG, 123 | GAME_BOY_BUTTON_PART_BY_TYPE, 124 | CROSS_BUTTONS, 125 | }; 126 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy/data/game-boy-data.js: -------------------------------------------------------------------------------- 1 | const GAME_BOY_PART_TYPE = { 2 | Body: 'body', 3 | ButtonA: 'button-a', 4 | ButtonB: 'button-b', 5 | ButtonCrossLeft: 'button-cross-left', 6 | ButtonCrossRight: 'button-cross-right', 7 | ButtonCrossUp: 'button-cross-up', 8 | ButtonCrossDown: 'button-cross-down', 9 | ButtonSelect: 'button-select', 10 | ButtonStart: 'button-start', 11 | PowerButton: 'power-button', 12 | PowerButtonFrame: 'power-button-frame', 13 | PowerIndicator: 'power-indicator', 14 | Screen: 'screen', 15 | VolumeControl: 'volume-control', 16 | CartridgePocket: 'cartridge-pocket', 17 | } 18 | 19 | const GAME_BOY_ACTIVE_PARTS = [ 20 | GAME_BOY_PART_TYPE.ButtonA, 21 | GAME_BOY_PART_TYPE.ButtonB, 22 | GAME_BOY_PART_TYPE.ButtonCrossLeft, 23 | GAME_BOY_PART_TYPE.ButtonCrossRight, 24 | GAME_BOY_PART_TYPE.ButtonCrossUp, 25 | GAME_BOY_PART_TYPE.ButtonCrossDown, 26 | GAME_BOY_PART_TYPE.ButtonSelect, 27 | GAME_BOY_PART_TYPE.ButtonStart, 28 | GAME_BOY_PART_TYPE.PowerButton, 29 | GAME_BOY_PART_TYPE.PowerButtonFrame, 30 | GAME_BOY_PART_TYPE.VolumeControl, 31 | ] 32 | 33 | const GAME_BOY_DRAGGABLE_PARTS = [ 34 | GAME_BOY_PART_TYPE.Body, 35 | GAME_BOY_PART_TYPE.PowerIndicator, 36 | GAME_BOY_PART_TYPE.CartridgePocket, 37 | ] 38 | 39 | const GAME_BOY_CROSS_PARTS = [ 40 | GAME_BOY_PART_TYPE.ButtonCrossLeft, 41 | GAME_BOY_PART_TYPE.ButtonCrossRight, 42 | GAME_BOY_PART_TYPE.ButtonCrossUp, 43 | GAME_BOY_PART_TYPE.ButtonCrossDown, 44 | ] 45 | 46 | const BUTTON_TYPE = { 47 | A: 'A', 48 | B: 'B', 49 | Start: 'START', 50 | Select: 'SELECT', 51 | CrossLeft: 'CROSS_LEFT', 52 | CrossRight: 'CROSS_RIGHT', 53 | CrossUp: 'CROSS_UP', 54 | CrossDown: 'CROSS_DOWN', 55 | } 56 | 57 | const POWER_STATE = { 58 | On: 'ON', 59 | Off: 'OFF', 60 | } 61 | 62 | const CARTRIDGE_STATE = { 63 | Inserted: 'INSERTED', 64 | NotInserted: 'NOT_INSERTED', 65 | } 66 | 67 | export { 68 | GAME_BOY_PART_TYPE, 69 | GAME_BOY_ACTIVE_PARTS, 70 | GAME_BOY_CROSS_PARTS, 71 | BUTTON_TYPE, 72 | GAME_BOY_DRAGGABLE_PARTS, 73 | POWER_STATE, 74 | CARTRIDGE_STATE, 75 | }; 76 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy/game-boy-audio/game-boy-audio-config.js: -------------------------------------------------------------------------------- 1 | import { GAME_BOY_SOUND_TYPE } from "./game-boy-audio-data"; 2 | 3 | const GAME_BOY_SOUNDS_CONFIG = { 4 | [GAME_BOY_SOUND_TYPE.GameBoyLoad]: { 5 | fileName: 'game-boy-load', 6 | repeat: false, 7 | }, 8 | [GAME_BOY_SOUND_TYPE.ZeldaIntro]: { 9 | fileName: 'zelda-intro-sound', 10 | repeat: false, 11 | }, 12 | [GAME_BOY_SOUND_TYPE.TetrisMusic]: { 13 | fileName: 'tetris-music', 14 | repeat: true, 15 | }, 16 | [GAME_BOY_SOUND_TYPE.MoveSide]: { 17 | fileName: 'move-side', 18 | repeat: false, 19 | }, 20 | [GAME_BOY_SOUND_TYPE.RotateShape]: { 21 | fileName: 'rotate-shape', 22 | repeat: false, 23 | }, 24 | [GAME_BOY_SOUND_TYPE.ShapeFall]: { 25 | fileName: 'shape-fall', 26 | repeat: false, 27 | }, 28 | [GAME_BOY_SOUND_TYPE.LineClear]: { 29 | fileName: 'line-clear', 30 | repeat: false, 31 | }, 32 | [GAME_BOY_SOUND_TYPE.TetrisPause]: { 33 | fileName: 'tetris-pause', 34 | repeat: false, 35 | }, 36 | [GAME_BOY_SOUND_TYPE.TetrisGameOver]: { 37 | fileName: 'tetris-game-over', 38 | repeat: false, 39 | }, 40 | [GAME_BOY_SOUND_TYPE.TetrisGameOverFinal]: { 41 | fileName: 'tetris-game-over-final', 42 | repeat: false, 43 | }, 44 | 45 | // Space Invaders 46 | [GAME_BOY_SOUND_TYPE.PlayerShoot]: { 47 | fileName: 'player-shoot', 48 | repeat: false, 49 | }, 50 | [GAME_BOY_SOUND_TYPE.EnemyKilled]: { 51 | fileName: 'enemy-killed', 52 | repeat: false, 53 | }, 54 | [GAME_BOY_SOUND_TYPE.PlayerKilled]: { 55 | fileName: 'player-killed', 56 | repeat: false, 57 | }, 58 | } 59 | 60 | export { GAME_BOY_SOUNDS_CONFIG }; 61 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy/game-boy-audio/game-boy-audio-data.js: -------------------------------------------------------------------------------- 1 | const GAME_BOY_SOUND_TYPE = { 2 | GameBoyLoad: 'GAME_BOY_LOAD', 3 | ZeldaIntro: 'ZELDA_INTRO', 4 | 5 | // Tetris 6 | TetrisMusic: 'TETRIS_MUSIC', 7 | MoveSide: 'MOVE_SIDE', 8 | RotateShape: 'ROTATE_SHAPE', 9 | ShapeFall: 'SHAPE_FALL', 10 | LineClear: 'LineClear', 11 | TetrisPause: 'TetrisPause', 12 | TetrisGameOver: 'TetrisGameOver', 13 | TetrisGameOverFinal: 'TetrisGameOverFinal', 14 | 15 | // Space Invaders 16 | PlayerShoot: 'PLAYER_SHOOT', 17 | EnemyKilled: 'ENEMY_KILLED', 18 | PlayerKilled: 'PLAYER_KILLED', 19 | } 20 | 21 | export { GAME_BOY_SOUND_TYPE }; 22 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy/game-boy-audio/game-boy-audio.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { GAME_BOY_SOUND_TYPE } from './game-boy-audio-data'; 3 | import { GAME_BOY_SOUNDS_CONFIG } from './game-boy-audio-config'; 4 | import { SOUNDS_CONFIG } from '../../../../core/configs/sounds-config'; 5 | import { PositionalAudioHelper } from 'three/addons/helpers/PositionalAudioHelper.js'; 6 | import Loader from '../../../../core/loader'; 7 | 8 | export default class GameBoyAudio { 9 | constructor(audioListener, audioGroup) { 10 | 11 | this._audioListener = audioListener; 12 | this._audioGroup = audioGroup; 13 | 14 | this._globalVolume = SOUNDS_CONFIG.masterVolume; 15 | this._gameBoyVolume = SOUNDS_CONFIG.gameBoyVolume; 16 | this._isSoundsEnabled = true; 17 | this._isGameBoyEnabled = false; 18 | this.sounds = {}; 19 | 20 | this._initSounds(); 21 | 22 | GameBoyAudio.instance = this; 23 | } 24 | 25 | _initSounds() { 26 | for (const value in GAME_BOY_SOUND_TYPE) { 27 | const soundType = GAME_BOY_SOUND_TYPE[value]; 28 | this._initSound(soundType); 29 | } 30 | 31 | // const sound = this.sounds[GAME_BOY_SOUND_TYPE.GameBoyLoad]; 32 | // const audioHelper = new PositionalAudioHelper(sound, 1); 33 | // this._audioGroup.add(audioHelper); 34 | } 35 | 36 | _initSound(soundType) { 37 | const config = GAME_BOY_SOUNDS_CONFIG[soundType]; 38 | 39 | const sound = new THREE.PositionalAudio(this._audioListener); 40 | this._audioGroup.add(sound); 41 | 42 | this.sounds[soundType] = sound; 43 | 44 | sound.setRefDistance(10); 45 | sound.setDirectionalCone(130, 180, 0.2); 46 | 47 | sound.setVolume(this._globalVolume * this._gameBoyVolume); 48 | sound.setLoop(config.repeat); 49 | 50 | Loader.events.on('onAudioLoaded', () => { 51 | sound.setBuffer(Loader.assets[config.fileName]); 52 | }); 53 | } 54 | 55 | _updateVolume() { 56 | if (this._isSoundsEnabled && this._isGameBoyEnabled) { 57 | for (const value in GAME_BOY_SOUND_TYPE) { 58 | const soundType = GAME_BOY_SOUND_TYPE[value]; 59 | const sound = this.sounds[soundType]; 60 | sound.setVolume(this._globalVolume * this._gameBoyVolume); 61 | } 62 | } 63 | } 64 | 65 | _setVolumeZero() { 66 | for (const value in GAME_BOY_SOUND_TYPE) { 67 | const soundType = GAME_BOY_SOUND_TYPE[value]; 68 | const sound = this.sounds[soundType]; 69 | sound.setVolume(0); 70 | } 71 | } 72 | 73 | _stopAllSounds() { 74 | for (const value in GAME_BOY_SOUND_TYPE) { 75 | const soundType = GAME_BOY_SOUND_TYPE[value]; 76 | const sound = this.sounds[soundType]; 77 | 78 | if (sound.isPlaying) { 79 | sound.stop(); 80 | } 81 | } 82 | } 83 | 84 | static playSound(type) { 85 | const sound = GameBoyAudio.instance.sounds[type]; 86 | 87 | if (sound.isPlaying) { 88 | sound.stop(); 89 | } 90 | 91 | sound.play(); 92 | } 93 | 94 | static switchSound(type) { 95 | const sound = GameBoyAudio.instance.sounds[type]; 96 | 97 | if (sound.isPlaying) { 98 | sound.stop(); 99 | } else { 100 | sound.play(); 101 | } 102 | } 103 | 104 | static stopSound(type) { 105 | const sound = GameBoyAudio.instance.sounds[type]; 106 | 107 | if (sound.isPlaying) { 108 | sound.stop(); 109 | } 110 | } 111 | 112 | static changeGlobalVolume(volume) { 113 | GameBoyAudio.instance._globalVolume = volume; 114 | GameBoyAudio.instance._updateVolume(); 115 | } 116 | 117 | static changeGameBoyVolume(volume) { 118 | GameBoyAudio.instance._gameBoyVolume = volume; 119 | GameBoyAudio.instance._updateVolume(); 120 | } 121 | 122 | static enableSound() { 123 | GameBoyAudio.instance._isSoundsEnabled = true; 124 | GameBoyAudio.instance._updateVolume(); 125 | } 126 | 127 | static disableSound() { 128 | GameBoyAudio.instance._isSoundsEnabled = false; 129 | GameBoyAudio.instance._setVolumeZero(); 130 | } 131 | 132 | static onTurnOnGameBoy() { 133 | GameBoyAudio.instance._isGameBoyEnabled = true; 134 | GameBoyAudio.instance._updateVolume(); 135 | } 136 | 137 | static onTurnOffGameBoy() { 138 | GameBoyAudio.instance._isGameBoyEnabled = false; 139 | GameBoyAudio.instance._setVolumeZero(); 140 | GameBoyAudio.instance._stopAllSounds(); 141 | } 142 | } 143 | 144 | GameBoyAudio.instance = null; 145 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy/mix-texture-bitmap-shaders/mix-texture-bitmap-fragment.glsl: -------------------------------------------------------------------------------- 1 | uniform sampler2D uVideoTexture; 2 | uniform sampler2D uBitmapTexture; 3 | uniform sampler2D uTexture; 4 | 5 | varying vec2 vUv; 6 | 7 | void main() 8 | { 9 | vec3 videoColor = texture2D(uVideoTexture, vUv).rgb; 10 | vec4 bitmapColor = texture2D(uBitmapTexture, vUv).rgba; 11 | vec4 textureColor = texture2D(uTexture, vUv).rgba; 12 | 13 | vec3 mixVideo = mix(videoColor.rgb, bitmapColor.rgb, bitmapColor.a); 14 | vec3 mixColor = mix(mixVideo.rgb, textureColor.rgb, textureColor.a); 15 | 16 | gl_FragColor = vec4(mixColor, 1.0); 17 | } 18 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy/mix-texture-bitmap-shaders/mix-texture-bitmap-vertex.glsl: -------------------------------------------------------------------------------- 1 | varying vec2 vUv; 2 | 3 | void main() 4 | { 5 | vec4 modelPosition = modelMatrix * vec4(position, 1.0); 6 | vec4 viewPosition = viewMatrix * modelPosition; 7 | vec4 projectionPosition = projectionMatrix * viewPosition; 8 | gl_Position = projectionPosition; 9 | 10 | vUv = uv; 11 | } 12 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy/mix-texture-color-shaders/mix-texture-color-fragment.glsl: -------------------------------------------------------------------------------- 1 | uniform sampler2D uTexture; 2 | uniform vec3 uColor; 3 | uniform float uMixPercent; 4 | 5 | varying vec2 vUv; 6 | 7 | void main() 8 | { 9 | vec3 textureColor = texture2D(uTexture, vUv).rgb; 10 | vec3 mixColor = mix(textureColor, uColor, uMixPercent); 11 | 12 | gl_FragColor = vec4(mixColor, 1.0); 13 | } 14 | -------------------------------------------------------------------------------- /src/scene/game-boy-scene/game-boy/mix-texture-color-shaders/mix-texture-color-vertex.glsl: -------------------------------------------------------------------------------- 1 | varying vec2 vUv; 2 | 3 | void main() 4 | { 5 | vec4 modelPosition = modelMatrix * vec4(position, 1.0); 6 | vec4 viewPosition = viewMatrix * modelPosition; 7 | vec4 projectionPosition = projectionMatrix * viewPosition; 8 | gl_Position = projectionPosition; 9 | 10 | vUv = uv; 11 | } 12 | -------------------------------------------------------------------------------- /src/scene/raycaster-controller.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | export default class RaycasterController { 4 | constructor(camera) { 5 | 6 | this._camera = camera; 7 | 8 | this._raycaster = null; 9 | this._meshes = []; 10 | 11 | this._init(); 12 | } 13 | 14 | getRaycaster() { 15 | return this._raycaster; 16 | } 17 | 18 | checkIntersection(x, y) { 19 | const mousePositionX = (x / window.innerWidth) * 2 - 1; 20 | const mousePositionY = -(y / window.innerHeight) * 2 + 1; 21 | const mousePosition = new THREE.Vector2(mousePositionX, mousePositionY); 22 | 23 | this._raycaster.setFromCamera(mousePosition, this._camera); 24 | const intersects = this._raycaster.intersectObjects(this._meshes); 25 | 26 | let intersectedObject = null; 27 | 28 | if (intersects.length > 0) { 29 | intersectedObject = intersects[0]; 30 | } 31 | 32 | return intersectedObject; 33 | } 34 | 35 | addMeshes(meshes) { 36 | this._meshes = [...this._meshes, ...meshes]; 37 | } 38 | 39 | _init() { 40 | this._raycaster = new THREE.Raycaster(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/scene/scene3d.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import RaycasterController from './raycaster-controller'; 3 | import { MessageDispatcher } from 'black-engine'; 4 | import GameBoyScene from './game-boy-scene/game-boy-scene'; 5 | 6 | export default class Scene3D extends THREE.Group { 7 | constructor(data) { 8 | super(); 9 | 10 | this.events = new MessageDispatcher(); 11 | 12 | this._data = data, 13 | this._scene = data.scene, 14 | this._camera = data.camera, 15 | 16 | this._raycasterController = null; 17 | this._gameBoyScene = null; 18 | 19 | this._init(); 20 | } 21 | 22 | update(dt) { 23 | this._gameBoyScene.update(dt); 24 | } 25 | 26 | onPointerMove(x, y) { 27 | this._gameBoyScene.onPointerMove(x, y); 28 | } 29 | 30 | onPointerDown(x, y) { 31 | this._gameBoyScene.onPointerDown(x, y); 32 | } 33 | 34 | onPointerUp(x, y) { 35 | this._gameBoyScene.onPointerUp(x, y); 36 | } 37 | 38 | onPointerLeave() { 39 | this._gameBoyScene.onPointerLeave(); 40 | } 41 | 42 | onWheelScroll(delta) { 43 | this._gameBoyScene.onWheelScroll(delta); 44 | } 45 | 46 | onSoundChanged() { 47 | this._gameBoyScene.onSoundChanged(); 48 | } 49 | 50 | _init() { 51 | this._initRaycaster(); 52 | this._initGameBoy(); 53 | this._initSignals(); 54 | } 55 | 56 | _initRaycaster() { 57 | this._raycasterController = new RaycasterController(this._camera); 58 | } 59 | 60 | _initGameBoy() { 61 | const gameBoyScene = this._gameBoyScene = new GameBoyScene(this._data, this._raycasterController); 62 | this.add(gameBoyScene); 63 | } 64 | 65 | _initSignals() { 66 | this._gameBoyScene.events.on('fpsMeterChanged', () => this.events.post('fpsMeterChanged')); 67 | this._gameBoyScene.events.on('onSoundsEnabledChanged', () => this.events.post('onSoundsEnabledChanged')); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'AlarmClock'; 3 | src: url('/fonts/alarm-clock.ttf'); 4 | font-weight: normal; 5 | font-style: normal; 6 | } 7 | 8 | * 9 | { 10 | margin: 0; 11 | padding: 0; 12 | } 13 | 14 | html, 15 | body 16 | { 17 | width: 100%; 18 | height: 100%; 19 | margin: 0; 20 | padding: 0; 21 | overflow: hidden; 22 | } 23 | 24 | body { -webkit-touch-callout: none !important; } 25 | a { -webkit-user-select: none !important; } 26 | div { -webkit-user-select: none !important; } 27 | 28 | .webgl 29 | { 30 | position: fixed; 31 | top: 0; 32 | left: 0; 33 | outline: none; 34 | } 35 | 36 | #container { 37 | width: 100%; 38 | height: 100%; 39 | margin: 0; 40 | padding: 0; 41 | background-color: black; 42 | touch-action: none; 43 | user-select: none; 44 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 45 | cursor: auto; 46 | } 47 | 48 | .loading-percent 49 | { 50 | position: absolute; 51 | top: 50%; 52 | width: 100%; 53 | margin-top: -25px; 54 | text-align: center; 55 | vertical-align: bottom; 56 | color: #ffffff; 57 | font-size: 26px; 58 | font-family: sans-serif; 59 | } 60 | 61 | .loading-percent.ended 62 | { 63 | opacity: 0; 64 | transition: opacity 0.3s ease-in-out; 65 | } 66 | 67 | .credits 68 | { 69 | position: absolute; 70 | bottom: 15px; 71 | right: 15px; 72 | color: #000000; 73 | font-size: 16px; 74 | font-family: sans-serif; 75 | opacity: 0.5; 76 | } 77 | 78 | .hide 79 | { 80 | opacity: 0; 81 | transition: opacity 0.3s ease-in-out; 82 | } 83 | 84 | .show 85 | { 86 | opacity: 1; 87 | transition: opacity 0.3s ease-in-out; 88 | } 89 | 90 | .intro-text 91 | { 92 | position: absolute; 93 | bottom: 40px; 94 | left: 50%; 95 | color: #000000; 96 | font-size: 30px; 97 | font-family: sans-serif; 98 | transform: translateX(-50%); 99 | opacity: 0.5; 100 | width: 200px; 101 | } 102 | 103 | .intro-text.hide 104 | { 105 | opacity: 0; 106 | transition: opacity 0.3s ease-in-out; 107 | } 108 | 109 | .intro-text.fastHide 110 | { 111 | display: none; 112 | } 113 | 114 | .rotate-to-landscape 115 | { 116 | position: absolute; 117 | bottom: 45px; 118 | left: 50%; 119 | color: #000000; 120 | font-size: 22px; 121 | font-family: sans-serif; 122 | transform: translateX(-50%); 123 | text-align: center; 124 | background-color: #ffffff; 125 | border-radius: 5px; 126 | padding: 8px; 127 | opacity: 0.6; 128 | width: 200px; 129 | display: none; 130 | } 131 | 132 | .rotate-to-landscape.show 133 | { 134 | display: block; 135 | } 136 | 137 | .rotate-to-landscape.hide 138 | { 139 | display: none; 140 | } 141 | 142 | .keyboard-icon 143 | { 144 | position: absolute; 145 | bottom: 45px; 146 | right: 15px; 147 | width: 90px; 148 | content: url(/assets/other/keyboard-icon.png); 149 | opacity: 0.5; 150 | cursor: pointer; 151 | } 152 | 153 | .keyboard-icon.hide 154 | { 155 | display: none; 156 | } 157 | 158 | .keyboard-shortcuts 159 | { 160 | position: absolute; 161 | bottom: 103px; 162 | right: 15px; 163 | width: 175px; 164 | color: #000000; 165 | font-size: 16px; 166 | font-family: sans-serif; 167 | opacity: 0.5; 168 | background-color: #ffffff; 169 | border-radius: 5px; 170 | padding: 10px 28px; 171 | display: none; 172 | opacity: 0; 173 | } 174 | 175 | .keyboard-shortcuts.hide 176 | { 177 | opacity: 0; 178 | transition: opacity 0.3s ease-in-out; 179 | } 180 | 181 | .keyboard-shortcuts.show 182 | { 183 | opacity: 0.5; 184 | transition: opacity 0.3s ease-in-out; 185 | } 186 | 187 | .keyboard-shortcuts.fastShow 188 | { 189 | display: block; 190 | } 191 | 192 | .copyrights 193 | { 194 | position: absolute; 195 | bottom: 6px; 196 | left: 50%; 197 | color: #000000; 198 | font-size: 9px; 199 | font-family: sans-serif; 200 | transform: translateX(-50%); 201 | opacity: 0.5; 202 | text-align: center; 203 | display: none; 204 | width: 650px; 205 | } 206 | 207 | .copyrights.show 208 | { 209 | display: block; 210 | } 211 | -------------------------------------------------------------------------------- /src/ui/overlay.js: -------------------------------------------------------------------------------- 1 | import { Black, DisplayObject, Sprite } from "black-engine"; 2 | 3 | export default class Overlay extends DisplayObject { 4 | constructor() { 5 | super(); 6 | 7 | this._view = null; 8 | 9 | this.touchable = true; 10 | } 11 | 12 | onAdded() { 13 | this._initView(); 14 | this._initSignals(); 15 | 16 | Black.stage.on('resize', () => this._onResize()); 17 | this._onResize(); 18 | } 19 | 20 | _initView() { 21 | const view = this._view = new Sprite('other/overlay'); 22 | this.add(view); 23 | 24 | view.alpha = 0; 25 | view.touchable = true; 26 | } 27 | 28 | _initSignals() { 29 | this._view.on('pointerDown', (msg, pointer) => { 30 | if (pointer.button === 0) { 31 | this.post('onPointerDown', pointer.x, pointer.y); 32 | } 33 | }); 34 | 35 | this._view.on('pointerUp', (msg, pointer) => { 36 | if (pointer.button === 0) { 37 | this.post('onPointerUp', pointer.x, pointer.y); 38 | } 39 | }); 40 | 41 | this._view.on('pointerMove', (msg, pointer) => { 42 | this.post('onPointerMove', pointer.x, pointer.y); 43 | }); 44 | 45 | Black.engine.containerElement.addEventListener("wheel", event => { 46 | const delta = Math.sign(event.deltaY); 47 | this.post('onWheelScroll', delta); 48 | }); 49 | } 50 | 51 | _onResize() { 52 | const bounds = Black.stage.bounds; 53 | 54 | this._view.x = bounds.left; 55 | this._view.y = bounds.top; 56 | 57 | const overlaySize = 10; 58 | this._view.scaleX = bounds.width / overlaySize; 59 | this._view.scaleY = bounds.height / overlaySize; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/ui/sound-icon.js: -------------------------------------------------------------------------------- 1 | import { DisplayObject, Sprite } from "black-engine"; 2 | import DEBUG_CONFIG from "../core/configs/debug-config"; 3 | import { SOUNDS_CONFIG } from "../core/configs/sounds-config"; 4 | 5 | export default class SoundIcon extends DisplayObject { 6 | constructor() { 7 | super(); 8 | 9 | this._view = null; 10 | 11 | this.touchable = true; 12 | } 13 | 14 | updateTexture() { 15 | this._view.textureName = this._getTexture(); 16 | } 17 | 18 | onAdded() { 19 | this._initView(); 20 | this._initSignals(); 21 | 22 | if (DEBUG_CONFIG.withoutUIMode) { 23 | this.visible = false; 24 | } 25 | } 26 | 27 | _initView() { 28 | const view = this._view = new Sprite(this._getTexture()); 29 | this.add(view); 30 | 31 | view.alignAnchor(); 32 | view.touchable = true; 33 | 34 | view.scale = 0.4; 35 | } 36 | 37 | _getTexture() { 38 | return SOUNDS_CONFIG.enabled ? 'other/sound-icon' : 'other/sound-icon-mute'; 39 | } 40 | 41 | _initSignals() { 42 | this._view.on('pointerDown', () => { 43 | SOUNDS_CONFIG.enabled = !SOUNDS_CONFIG.enabled; 44 | this.updateTexture(); 45 | this.post('onSoundChanged'); 46 | }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/ui/ui.js: -------------------------------------------------------------------------------- 1 | import { Black, DisplayObject, Message } from "black-engine"; 2 | import Overlay from "./overlay"; 3 | import SoundIcon from "./sound-icon"; 4 | 5 | export default class UI extends DisplayObject { 6 | constructor() { 7 | super(); 8 | 9 | this._overlay = null; 10 | this._soundIcon = null; 11 | 12 | this.touchable = true; 13 | } 14 | 15 | updateSoundIcon() { 16 | this._soundIcon.updateTexture(); 17 | } 18 | 19 | onAdded() { 20 | this._initOverlay(); 21 | this._initSoundIcon(); 22 | this._initSignals(); 23 | 24 | this.stage.on(Message.RESIZE, this._handleResize, this); 25 | this._handleResize(); 26 | } 27 | 28 | _initOverlay() { 29 | const overlay = this._overlay = new Overlay(); 30 | this.add(overlay); 31 | } 32 | 33 | _initSoundIcon() { 34 | const soundIcon = this._soundIcon = new SoundIcon(); 35 | this.add(soundIcon); 36 | } 37 | 38 | _initSignals() { 39 | this._overlay.on('onPointerMove', (msg, x, y) => this.post('onPointerMove', x, y)); 40 | this._overlay.on('onPointerDown', (msg, x, y) => this.post('onPointerDown', x, y)); 41 | this._overlay.on('onPointerUp', (msg, x, y) => this.post('onPointerUp', x, y)); 42 | this._overlay.on('onWheelScroll', (msg, delta) => this.post('onWheelScroll', delta)); 43 | this._soundIcon.on('onSoundChanged', () => this.post('onSoundChanged')); 44 | } 45 | 46 | _handleResize() { 47 | const bounds = Black.stage.bounds; 48 | 49 | this._soundIcon.x = bounds.left + 50; 50 | this._soundIcon.y = bounds.top + 50; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import glsl from 'vite-plugin-glsl'; 2 | 3 | const isCodeSandbox = 'SANDBOX_URL' in process.env || 'CODESANDBOX_HOST' in process.env 4 | 5 | export default { 6 | publicDir: './public/', 7 | base: './', 8 | server: 9 | { 10 | host: true, 11 | open: !isCodeSandbox // Open if it's not a CodeSandbox 12 | }, 13 | build: 14 | { 15 | outDir: './dist', 16 | emptyOutDir: true, 17 | sourcemap: true, 18 | }, 19 | plugins: [glsl()], 20 | } 21 | --------------------------------------------------------------------------------