├── .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 | 
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 | 
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 |
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 |
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 |
232 | fileLists
233 |
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 |
--------------------------------------------------------------------------------