Compare commits

...

29 Commits

Author SHA1 Message Date
c52a604f0f add README 2025-05-21 10:21:03 +02:00
8e9a089d34 meta 2025-05-20 17:01:59 +02:00
d5893eb8ef fix changeKey 2025-04-08 00:31:52 +02:00
1e006d46b9 little more bumpScale 2024-10-05 15:23:18 +02:00
0f84f90e05 tweak space theme 2024-10-05 15:22:15 +02:00
4287edab71 rename TetraControls 2024-10-03 22:26:37 +02:00
5c2eaca35a remove import 2024-10-03 00:38:10 +02:00
e7dc780173 T class 2024-10-03 00:37:53 +02:00
9721b311eb move mino materials to InstancedMino.prototype 2024-10-03 00:31:12 +02:00
d3f6cf9b71 refactoring 2024-10-03 00:18:42 +02:00
5b058a58b3 load mino retro texture if needed 2024-10-02 22:38:37 +02:00
9fca05ae6e loading... 2024-10-02 14:48:19 +02:00
1a7628bb42 hide screen on loading 2024-10-02 02:17:23 +02:00
bc4ba54c0a spread queues 2024-10-01 21:02:18 +02:00
653befdc02 retro material tweak 2024-10-01 20:50:28 +02:00
1b3f837bf0 dev menu 2024-10-01 20:39:17 +02:00
375d47397e reset 2024-10-01 19:34:02 +02:00
efb6482238 lightsteelblue 2024-10-01 19:31:31 +02:00
7fd3c04a2d rename gui to menu 2024-10-01 18:39:26 +02:00
1e42c2160f textured mino edge 2024-10-01 11:35:41 +02:00
74bf8521fb tweaks 2024-10-01 11:03:37 +02:00
fef08f64e8 tweaks 2024-10-01 10:59:43 +02:00
3c8fc95e23 texture on all faces 2024-10-01 01:57:38 +02:00
f7b7b74e01 move rotationPoint4Used test to rotation 2024-10-01 00:49:38 +02:00
af9e0c481a locked texture 2024-10-01 00:35:06 +02:00
90eb3247e0 new 3d loading animation 2024-09-30 21:23:44 +02:00
2a25dbe4b0 new 3d loading animation 2024-09-30 21:21:50 +02:00
d9397c4bcb textured ghost 2024-09-30 21:21:25 +02:00
ca93423bf8 hide vortex hole 2024-09-30 21:18:38 +02:00
14 changed files with 403 additions and 553 deletions

5
README.md Normal file
View File

@ -0,0 +1,5 @@
# teTra
Falling blocks web game made with three.js librairy
![screenshot](https://git.malingrey.fr/adrien/teTra/raw/branch/master/thumbnail.png)

74
app.js
View File

@ -1,11 +1,12 @@
import * as THREE from 'three' import * as THREE from 'three'
import { scheduler } from './jsm/scheduler.js' import { scheduler } from './jsm/scheduler.js'
import { TRANSLATION, ROTATION, environment, Mino, Playfield, HoldQueue, NextQueue } from './jsm/Tetrominoes.js' import { TRANSLATION, ROTATION, environment, InstancedMino, Mino, Playfield, HoldQueue, NextQueue } from './jsm/Tetrominoes.js'
import { Settings } from './jsm/Settings.js' import Settings from './jsm/Settings.js'
import { Stats } from './jsm/Stats.js' import { Stats } from './jsm/Stats.js'
import { TetraGUI } from './jsm/TetraGUI.js' import { Menu } from './jsm/Menu.js'
import { TetraControls } from './jsm/TetraControls.js' import CameraControls from './jsm/CameraControls.js'
import { TetraScene } from './jsm/TetraScene.js' import { TetraScene } from './jsm/TetraScene.js'
import * as FPS from 'three/addons/libs/stats.module.js'
HTMLElement.prototype.addNewChild = function (tag, properties) { HTMLElement.prototype.addNewChild = function (tag, properties) {
@ -25,11 +26,11 @@ let game = {
start: function() { start: function() {
stats.init() stats.init()
gui.startButton.hide() menu.startButton.hide()
gui.settings.close() menu.stats.show()
gui.stats.show() menu.settings.close()
Mino.meshes.clear() Mino.instances.clear()
nextQueue.init() nextQueue.init()
holdQueue.piece = undefined holdQueue.piece = undefined
@ -49,11 +50,11 @@ let game = {
document.onkeydown = onkeydown document.onkeydown = onkeydown
document.onkeyup = onkeyup document.onkeyup = onkeyup
window.onblur = game.pause window.onblur = game.pause
gui.settings.domElement.onclick = game.pause menu.settings.domElement.onclick = game.pause
document.body.classList.remove("pause") document.body.classList.remove("pause")
gui.resumeButton.hide() menu.resumeButton.hide()
gui.pauseButton.show() menu.pauseButton.show()
stats.clock.start() stats.clock.start()
stats.clock.elapsedTime = stats.elapsedTime stats.clock.elapsedTime = stats.elapsedTime
@ -61,7 +62,7 @@ let game = {
if (settings.musicVolume) scene.music.play() if (settings.musicVolume) scene.music.play()
if (playfield.piece) { if (playfield.piece) {
scheduler.setInterval(game.fall, stats.fallPeriod) scheduler.resetInterval(game.fall, stats.fallPeriod)
} else { } else {
this.generate() this.generate()
} }
@ -73,7 +74,7 @@ let game = {
playfield.piece.onLockDown = game.lockDown playfield.piece.onLockDown = game.lockDown
if (playfield.piece.canMove(TRANSLATION.NONE)) { if (playfield.piece.canMove(TRANSLATION.NONE)) {
scheduler.setInterval(game.fall, stats.fallPeriod) scheduler.resetInterval(game.fall, stats.fallPeriod)
} else { } else {
game.over() // block out game.over() // block out
} }
@ -106,7 +107,7 @@ let game = {
}, },
pause: function() { pause: function() {
gui.settings.domElement.onclick = null menu.settings.domElement.onclick = null
stats.elapsedTime = stats.clock.elapsedTime stats.elapsedTime = stats.clock.elapsedTime
stats.clock.stop() stats.clock.stop()
@ -123,8 +124,8 @@ let game = {
pauseSpan.onfocus = game.resume pauseSpan.onfocus = game.resume
document.body.classList.add("pause") document.body.classList.add("pause")
gui.pauseButton.hide() menu.pauseButton.hide()
gui.resumeButton.show() menu.resumeButton.show()
}, },
over: function() { over: function() {
@ -133,15 +134,15 @@ let game = {
document.onkeydown = null document.onkeydown = null
window.onblur = null window.onblur = null
renderer.domElement.onfocus = null renderer.domElement.onfocus = null
gui.settings.domElement.onfocus = null menu.settings.domElement.onfocus = null
game.playing = false game.playing = false
scene.music.pause() scene.music.pause()
stats.clock.stop() stats.clock.stop()
messagesSpan.addNewChild("div", { className: "show-level-animation", innerHTML: `<h1>GAME<br/>OVER</h1>` }) messagesSpan.addNewChild("div", { className: "show-level-animation", innerHTML: `<h1>GAME<br/>OVER</h1>` })
gui.pauseButton.hide() menu.pauseButton.hide()
gui.startButton.name("Rejouer") menu.startButton.name("Rejouer")
gui.startButton.show() menu.startButton.show()
}, },
} }
@ -215,8 +216,8 @@ function onkeydown(event) {
actionsQueue.unshift(action) actionsQueue.unshift(action)
scheduler.clearTimeout(repeat) scheduler.clearTimeout(repeat)
scheduler.clearInterval(autorepeat) scheduler.clearInterval(autorepeat)
if (action == playerActions.softDrop) scheduler.setInterval(autorepeat, settings.fallPeriod / 20) if (action == playerActions.softDrop) scheduler.resetInterval(autorepeat, settings.fallPeriod / 20)
else scheduler.setTimeout(repeat, settings.dasDelay) else scheduler.resetTimeout(repeat, settings.dasDelay)
} }
} }
} }
@ -225,7 +226,7 @@ function onkeydown(event) {
function repeat() { function repeat() {
if (actionsQueue.length) { if (actionsQueue.length) {
actionsQueue[0]() actionsQueue[0]()
scheduler.setInterval(autorepeat, settings.arrDelay) scheduler.resetInterval(autorepeat, settings.arrDelay)
} }
} }
@ -277,8 +278,8 @@ renderer.domElement.tabIndex = 1
let loadingManager = new THREE.LoadingManager( let loadingManager = new THREE.LoadingManager(
function() { function() {
loaddingCircle.style.display = "none" loadingDiv.style.display = "none"
gui.startButton.show() menu.startButton.show()
renderer.setAnimationLoop(animate) renderer.setAnimationLoop(animate)
}, },
function (url, itemsLoaded, itemsTotal) { function (url, itemsLoaded, itemsTotal) {
@ -290,15 +291,16 @@ let loadingManager = new THREE.LoadingManager(
) )
loadingManager.onStart = function (url, itemsLoaded, itemsTotal) { loadingManager.onStart = function (url, itemsLoaded, itemsTotal) {
loadingPercent.innerText = "0%" loadingPercent.innerText = "0%"
loaddingCircle.style.display = "block" loadingDiv.style.display = "flex"
} }
const stats = new Stats() const stats = new Stats()
const settings = new Settings() const settings = new Settings()
const scene = new TetraScene(settings, loadingManager) const scene = new TetraScene(settings, loadingManager)
const controls = new TetraControls(scene.camera, renderer.domElement) const controls = new CameraControls(scene.camera, renderer.domElement)
scene.add(Mino.meshes) const minoes = new InstancedMino()
scene.add(minoes)
const holdQueue = new HoldQueue() const holdQueue = new HoldQueue()
scene.add(holdQueue) scene.add(holdQueue)
const playfield = new Playfield(loadingManager) const playfield = new Playfield(loadingManager)
@ -306,8 +308,14 @@ scene.add(playfield)
const nextQueue = new NextQueue() const nextQueue = new NextQueue()
scene.add(nextQueue) scene.add(nextQueue)
const gui = new TetraGUI(game, settings, stats, scene, controls, playfield) const menu = new Menu(game, settings, stats, scene, minoes, playfield)
gui.load() menu.load()
let fps
if (window.location.search.includes("fps")) {
let fps = new FPS.default()
document.body.appendChild(fps.dom)
}
messagesSpan.onanimationend = function (event) { messagesSpan.onanimationend = function (event) {
event.target.remove() event.target.remove()
@ -321,13 +329,13 @@ function animate() {
scene.updateMatrixWorld() scene.updateMatrixWorld()
scene.update(delta) scene.update(delta)
playfield.update(delta) playfield.update(delta)
Mino.meshes.update() minoes.update()
controls.update() controls.update()
renderer.render(scene, scene.camera) renderer.render(scene, scene.camera)
environment.camera.update(renderer, scene) environment.camera.update(renderer, scene)
gui.update() fps?.update()
} }
window.addEventListener("resize", () => { window.addEventListener("resize", () => {
@ -337,7 +345,7 @@ window.addEventListener("resize", () => {
}) })
window.onbeforeunload = function (event) { window.onbeforeunload = function (event) {
gui.save() menu.save()
localStorage["teTraHighScore"] = stats.highScore localStorage["teTraHighScore"] = stats.highScore
return !game.playing return !game.playing
} }

View File

@ -1,279 +1,70 @@
@-webkit-keyframes outerRotate1 { #loadingDiv {
0% { position: absolute;
transform: translate(-50%, -50%) rotate(0); top: 0;
} left: 0;
display: flex;
100% { flex-flow: column;
transform: translate(-50%, -50%) rotate(360deg); justify-content: center;
} box-sizing: border-box;
font-family: "Open Sans", sans-serif;
font-size: 1.4rem;
color: lightsteelblue;
text-align: center;
width: 100vw;
height: 100vh;
padding: 30vh;
background-color: black;
z-index: 1;
} }
@-moz-keyframes outerRotate1 { .scene {
0% { width: 200px;
transform: translate(-50%, -50%) rotate(0); height: 200px;
} margin: 0 auto;
perspective: 200px;
100% { font-size: 40px;
transform: translate(-50%, -50%) rotate(360deg);
}
} }
@-o-keyframes outerRotate1 { .tetromino {
0% { position: relative;
transform: translate(-50%, -50%) rotate(0); top: 2em;
} left: 2em;
width: 1em;
100% { height: 1em;
transform: translate(-50%, -50%) rotate(360deg); transform-style: preserve-3d;
} transform: translateZ(0.5em);
animation: spinCube 5s infinite ease-in-out;
} }
@keyframes outerRotate1 { @keyframes spinCube {
0% { 0% { transform: translateZ(0.5em) rotateX( 0deg) rotateY( 0deg); }
transform: translate(-50%, -50%) rotate(0); 100% { transform: translateZ(0.5em) rotateX(360deg) rotateY(360deg); }
}
100% {
transform: translate(-50%, -50%) rotate(360deg);
}
} }
@-webkit-keyframes outerRotate2 { .mino {
0% { width: 1em;
transform: translate(-50%, -50%) rotate(0); height: 1em;
} position: absolute;
transform-style: preserve-3d;
100% {
transform: translate(-50%, -50%) rotate(-360deg);
}
} }
@-moz-keyframes outerRotate2 { .T.tetromino .first.mino { top: -0.5em; left: -1em; }
0% { .T.tetromino .second.mino { top: -0.5em; left: 0em; }
transform: translate(-50%, -50%) rotate(0); .T.tetromino .third.mino { top: -0.5em; left: 1em; }
} .T.tetromino .fourth.mino { top: 0.5em; left: 0em; }
100% { .face {
transform: translate(-50%, -50%) rotate(-360deg); position: absolute;
} width: 1em;
height: 1em;
padding: 0;
background: hsla(240, 100%, 0%, 0.4);
border: 1px solid hsla(240, 100%, 70%, 0.6);
} }
@-o-keyframes outerRotate2 { .front.face { transform: rotateY( 0deg) translateZ(0.5em); }
0% { .right.face { transform: rotateY( 90deg) translateZ(0.5em); }
transform: translate(-50%, -50%) rotate(0); .back.face { transform: rotateY(180deg) translateZ(0.5em); }
} .left.face { transform: rotateY(-90deg) translateZ(0.5em); }
.top.face { transform: rotateX( 90deg) translateZ(0.5em); }
100% { .bottom.face { transform: rotateX(-90deg) translateZ(0.5em); }
transform: translate(-50%, -50%) rotate(-360deg);
}
}
@keyframes outerRotate2 {
0% {
transform: translate(-50%, -50%) rotate(0);
}
100% {
transform: translate(-50%, -50%) rotate(-360deg);
}
}
@-webkit-keyframes textColour {
0% {
color: #fff;
}
100% {
color: #3BB2D0;
}
}
@-moz-keyframes textColour {
0% {
color: #fff;
}
100% {
color: #3BB2D0;
}
}
@-o-keyframes textColour {
0% {
color: #fff;
}
100% {
color: #3BB2D0;
}
}
@keyframes textColour {
0% {
color: #fff;
}
100% {
color: #3BB2D0;
}
}
body {
background-color: #222;
}
#loaddingCircle {
margin: 0;
padding: 0;
width: 100vw;
height: 100vh;
cursor: progress;
}
.e-loadholder {
position: absolute;
top: 50%;
left: 50%;
-webkit-transform: translate(-51%, -50%);
-moz-transform: translate(-51%, -50%);
-ms-transform: translate(-51%, -50%);
-o-transform: translate(-51%, -50%);
transform: translate(-51%, -50%);
width: 240px;
height: 240px;
border: 5px solid #1B5F70;
border-radius: 120px;
box-sizing: border-box;
}
.e-loadholder:after {
position: absolute;
top: 50%;
left: 50%;
-webkit-transform: translate(-51%, -50%);
-moz-transform: translate(-51%, -50%);
-ms-transform: translate(-51%, -50%);
-o-transform: translate(-51%, -50%);
transform: translate(-51%, -50%);
content: " ";
display: block;
background: #222;
transform-origin: center;
z-index: 0;
}
.e-loadholder:after {
width: 100px;
height: 200%;
-webkit-animation: outerRotate2 30s infinite linear;
-moz-animation: outerRotate2 30s infinite linear;
-o-animation: outerRotate2 30s infinite linear;
animation: outerRotate2 30s infinite linear;
}
.e-loadholder .m-loader {
position: absolute;
top: 50%;
left: 50%;
-webkit-transform: translate(-51%, -50%);
-moz-transform: translate(-51%, -50%);
-ms-transform: translate(-51%, -50%);
-o-transform: translate(-51%, -50%);
transform: translate(-51%, -50%);
width: 200px;
height: 200px;
color: #888;
text-align: center;
border: 5px solid #2a93ae;
border-radius: 100px;
box-sizing: border-box;
z-index: 20;
text-transform: uppercase;
}
.e-loadholder .m-loader:after {
position: absolute;
top: 50%;
left: 50%;
-webkit-transform: translate(-51%, -50%);
-moz-transform: translate(-51%, -50%);
-ms-transform: translate(-51%, -50%);
-o-transform: translate(-51%, -50%);
transform: translate(-51%, -50%);
content: " ";
display: block;
background: #222;
transform-origin: center;
z-index: -1;
}
.e-loadholder .m-loader:after {
width: 100px;
height: 106%;
-webkit-animation: outerRotate1 15s infinite linear;
-moz-animation: outerRotate1 15s infinite linear;
-o-animation: outerRotate1 15s infinite linear;
animation: outerRotate1 15s infinite linear;
}
.e-loadholder .m-loader .e-text {
font-family: "Open Sans", sans-serif;
font-size: 10px;
font-size: 1rem;
position: absolute;
top: 50%;
left: 50%;
-webkit-transform: translate(-51%, -50%);
-moz-transform: translate(-51%, -50%);
-ms-transform: translate(-51%, -50%);
-o-transform: translate(-51%, -50%);
transform: translate(-51%, -50%);
-webkit-animation: textColour 1s alternate linear infinite;
-moz-animation: textColour 1s alternate linear infinite;
-o-animation: textColour 1s alternate linear infinite;
animation: textColour 1s alternate linear infinite;
display: flex;
flex-direction: column;
justify-content: center;
width: 140px;
height: 140px;
text-align: center;
border: 5px solid #3bb2d0;
border-radius: 70px;
box-sizing: border-box;
z-index: 20;
}
.e-loadholder .m-loader .e-text:before, .e-loadholder .m-loader .e-text:after {
position: absolute;
top: 50%;
left: 50%;
-webkit-transform: translate(-51%, -50%);
-moz-transform: translate(-51%, -50%);
-ms-transform: translate(-51%, -50%);
-o-transform: translate(-51%, -50%);
transform: translate(-51%, -50%);
content: " ";
display: block;
background: #222;
transform-origin: center;
z-index: -1;
}
.e-loadholder .m-loader .e-text:before {
width: 110%;
height: 40px;
-webkit-animation: outerRotate2 3.5s infinite linear;
-moz-animation: outerRotate2 3.5s infinite linear;
-o-animation: outerRotate2 3.5s infinite linear;
animation: outerRotate2 3.5s infinite linear;
}
.e-loadholder .m-loader .e-text:after {
width: 40px;
height: 110%;
-webkit-animation: outerRotate1 8s infinite linear;
-moz-animation: outerRotate1 8s infinite linear;
-o-animation: outerRotate1 8s infinite linear;
animation: outerRotate1 8s infinite linear;
}

View File

@ -9,14 +9,16 @@ body {
span { span {
position: absolute; position: absolute;
top: 0;
left: 0;
} }
.lil-gui { .lil-menu {
--background-color: rgba(33, 37, 41, 30%); --background-color: rgba(33, 37, 41, 30%);
--width: 200px; --width: 200px;
} }
@supports (backdrop-filter: blur()) { @supports (backdrop-filter: blur()) {
.lil-gui { .lil-menu {
backdrop-filter: blur(15px); backdrop-filter: blur(15px);
} }
} }
@ -27,11 +29,11 @@ span {
left: 15px; left: 15px;
} }
.lil-gui.root > .title { .lil-menu.root > .title {
font-size: 1.5em; font-size: 1.5em;
} }
.lil-gui .controller.disabled { .lil-menu .controller.disabled {
opacity: .8; opacity: .8;
} }
@ -171,10 +173,10 @@ h1 {
} }
.pause #pauseSpan { .pause #pauseSpan {
display: flex; position: absolute;
position:absolute;
top: 0; top: 0;
left: 0; left: 0;
display: flex;
filter: blur(2px); filter: blur(2px);
width: 100%; width: 100%;
height: 100%; height: 100%;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

BIN
images/edges.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 792 B

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -8,6 +8,15 @@
<link rel="icon" href="favicon.ico"> <link rel="icon" href="favicon.ico">
<link rel="stylesheet" href="css/style.css"> <link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="css/loading.css"> <link rel="stylesheet" href="css/loading.css">
<meta property="og:title" content="ᵀᴱTᴿᴬ"/>
<meta property="og:type" content="game"/>
<meta property="og:url" content="https://adrien.malingrey.fr/jeux/tetra/"/>
<meta property="og:image" content="https://adrien.malingrey.fr/jeux/tetra/thumbnail.png"/>
<meta property="og:image:width" content="250"/>
<meta property="og:image:height" content="250"/>
<meta property="og:description" content="Des blocs qui tombent en 3D"/>
<meta property="og:locale" content="fr_FR"/>
<meta property="og:site_name" content="adrien.malingrey.fr"/>
<script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script> <script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script>
<script type="importmap"> <script type="importmap">
{ {
@ -20,16 +29,48 @@
</head> </head>
<body> <body>
<div id="loaddingCircle"> <span id="loadingDiv">
<div class="e-loadholder"> <div class="scene">
<div class="m-loader"> <div class="T tetromino">
<span class="e-text"> <div class="first mino">
<div class="front face"></div>
<div class="back face"></div>
<div class="left face"></div>
<div class="right face"></div>
<div class="top face"></div>
<div class="bottom face"></div>
</div>
<div class="second mino">
<div class="front face"></div>
<div class="back face"></div>
<div class="left face"></div>
<div class="right face"></div>
<div class="top face"></div>
<div class="bottom face"></div>
</div>
<div class="third mino">
<div class="front face"></div>
<div class="back face"></div>
<div class="left face"></div>
<div class="right face"></div>
<div class="top face"></div>
<div class="bottom face"></div>
</div>
<div class="fourth mino">
<div class="front face"></div>
<div class="back face"></div>
<div class="left face"></div>
<div class="right face"></div>
<div class="top face"></div>
<div class="bottom face"></div>
</div>
</div>
</div>
<div>
<div>Chargement</div> <div>Chargement</div>
<div id="loadingPercent">0%</div> <div id="loadingPercent">0%</div>
</span> </div>
</div> </span>
</div>
</div>
<span id="messagesSpan"></span> <span id="messagesSpan"></span>
<span id="pauseSpan" tabindex="1">II</span> <span id="pauseSpan" tabindex="1">II</span>
<audio id="music" src="audio/benevolence.m4a" loop></audio> <audio id="music" src="audio/benevolence.m4a" loop></audio>

View File

@ -1,8 +1,7 @@
import { OrbitControls } from 'three/addons/controls/OrbitControls.js' import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
class TetraControls extends OrbitControls { export default class CameraControls extends OrbitControls {
constructor(camera, domElement) { constructor(camera, domElement) {
super(camera, domElement) super(camera, domElement)
this.autoRotate this.autoRotate
@ -10,7 +9,7 @@ class TetraControls extends OrbitControls {
this.dampingFactor = 0.04 this.dampingFactor = 0.04
this.maxDistance = 21 this.maxDistance = 21
this.keys = {} this.keys = {}
this.minPolarAngle = 1 this.minPolarAngle = 1.05
this.maxPolarAngle = 2.1 this.maxPolarAngle = 2.1
this.minAzimuthAngle = 0.9 - Math.PI / 2 this.minAzimuthAngle = 0.9 - Math.PI / 2
this.maxAzimuthAngle = 2.14 - Math.PI / 2 this.maxAzimuthAngle = 2.14 - Math.PI / 2
@ -19,6 +18,4 @@ class TetraControls extends OrbitControls {
this.addEventListener("start", () => domElement.style.cursor = "grabbing") this.addEventListener("start", () => domElement.style.cursor = "grabbing")
this.addEventListener("end", () => domElement.style.cursor = "grab") this.addEventListener("end", () => domElement.style.cursor = "grab")
} }
} }
export { TetraControls }

View File

@ -1,18 +1,17 @@
import * as THREE from 'three' import * as THREE from 'three'
import { GUI } from 'three/addons/libs/lil-gui.module.min.js' import { GUI } from 'three/addons/libs/lil-gui.module.min.js'
import * as FPS from 'three/addons/libs/stats.module.js' import { environment } from './Tetrominoes.js'
import { Mino, environment } from './Tetrominoes.js'
export class TetraGUI extends GUI { export class Menu extends GUI {
constructor(game, settings, stats, scene, controls, playfield) { constructor(game, settings, stats, scene, minoes, playfield) {
super({title: "ᵀᴱTᴿᴬ"}) super({title: "ᵀᴱTᴿᴬ"})
this.startButton = this.add(game, "start").name("Jouer").hide() this.startButton = this.add(game, "start").name("Jouer").hide()
this.pauseButton = this.add(game, "pause").name("Pause").hide() this.pauseButton = this.add(game, "pause").name("Pause").hide()
this.resumeButton = this.add(game, "resume").name("Reprendre").hide() this.resumeButton = this.add(game, "resume").name("Reprendre").hide()
this.stats = this.addFolder("Stats").hide() this.stats = this.addFolder("Statistiques").hide()
this.stats.add(stats, "time").name("Temps").disable().listen() this.stats.add(stats, "time").name("Temps").disable().listen()
this.stats.add(stats, "score").name("Score").disable().listen() this.stats.add(stats, "score").name("Score").disable().listen()
this.stats.add(stats, "highScore").name("Meilleur score").disable().listen() this.stats.add(stats, "highScore").name("Meilleur score").disable().listen()
@ -24,44 +23,41 @@ export class TetraGUI extends GUI {
this.stats.add(stats, "maxCombo").name("Combos max").disable().listen() this.stats.add(stats, "maxCombo").name("Combos max").disable().listen()
this.stats.add(stats, "maxB2B").name("BàB max").disable().listen() this.stats.add(stats, "maxB2B").name("BàB max").disable().listen()
this.settings = this.addFolder("Options").open() this.settings = this.addFolder("Options")
this.settings.add(settings, "startLevel").name("Niveau initial").min(1).max(15).step(1) this.settings.add(settings, "startLevel").name("Niveau initial").min(1).max(15).step(1)
this.settings.add(settings, "theme", ["Plasma", "Espace", "Rétro"]).name("Thème").onChange(theme => { this.settings.add(settings, "theme", ["Plasma", "Espace", "Rétro"]).name("Thème").onChange(theme => {
scene.theme = theme scene.theme = theme
Mino.meshes.material = Mino.materials[theme] minoes.theme = theme
if (theme == "Rétro") { if (theme == "Rétro") {
playfield.edge.visible = false playfield.edge.visible = false
playfield.retroEdge.visible = true playfield.retroEdge.visible = true
Mino.meshes.resetColor()
Mino.meshes.update = Mino.meshes.updateOffset
music.src = "audio/Tetris_MkVaffQuasi_Ultimix_OC_ReMix.mp3" music.src = "audio/Tetris_MkVaffQuasi_Ultimix_OC_ReMix.mp3"
} else { } else {
playfield.edge.visible = true playfield.edge.visible = true
playfield.retroEdge.visible = false playfield.retroEdge.visible = false
Mino.meshes.update = Mino.meshes.updateColor
music.src = "audio/benevolence.m4a" music.src = "audio/benevolence.m4a"
} }
if (dev) changeMaterial()
}) })
this.settings.key = this.settings.addFolder("Commandes").open() this.settings.key = this.settings.addFolder("Commandes").open()
let moveLeftKeyController = this.settings.key.add(settings.key, "moveLeft").name('Gauche') let moveLeftKeyController = this.settings.key.add(settings.key, "moveLeft").name('Gauche')
moveLeftKeyController.domElement.onclick = this.changeKey.bind(moveLeftKeyController) moveLeftKeyController.domElement.onclick = this.changeKey(moveLeftKeyController)
let moveRightKeyController = this.settings.key.add(settings.key, "moveRight").name('Droite') let moveRightKeyController = this.settings.key.add(settings.key, "moveRight").name('Droite')
moveRightKeyController.domElement.onclick = this.changeKey.bind(moveRightKeyController) moveRightKeyController.domElement.onclick = this.changeKey(moveRightKeyController)
let rotateCWKeyController = this.settings.key.add(settings.key, "rotateCW").name('Rotation horaire') let rotateCWKeyController = this.settings.key.add(settings.key, "rotateCW").name('Rotation horaire')
rotateCWKeyController.domElement.onclick = this.changeKey.bind(rotateCWKeyController) rotateCWKeyController.domElement.onclick = this.changeKey(rotateCWKeyController)
let rotateCCWKeyController = this.settings.key.add(settings.key, "rotateCCW").name('anti-horaire') let rotateCCWKeyController = this.settings.key.add(settings.key, "rotateCCW").name('anti-horaire')
rotateCCWKeyController.domElement.onclick = this.changeKey.bind(rotateCCWKeyController) rotateCCWKeyController.domElement.onclick = this.changeKey(rotateCCWKeyController)
let softDropKeyController = this.settings.key.add(settings.key, "softDrop").name('Chute lente') let softDropKeyController = this.settings.key.add(settings.key, "softDrop").name('Chute lente')
softDropKeyController.domElement.onclick = this.changeKey.bind(softDropKeyController) softDropKeyController.domElement.onclick = this.changeKey(softDropKeyController)
let hardDropKeyController = this.settings.key.add(settings.key, "hardDrop").name('Chute rapide') let hardDropKeyController = this.settings.key.add(settings.key, "hardDrop").name('Chute rapide')
hardDropKeyController.domElement.onclick = this.changeKey.bind(hardDropKeyController) hardDropKeyController.domElement.onclick = this.changeKey(hardDropKeyController)
let holdKeyController = this.settings.key.add(settings.key, "hold").name('Garder') let holdKeyController = this.settings.key.add(settings.key, "hold").name('Garder')
holdKeyController.domElement.onclick = this.changeKey.bind(holdKeyController) holdKeyController.domElement.onclick = this.changeKey(holdKeyController)
let pauseKeyController = this.settings.key.add(settings.key, "pause").name('Pause') let pauseKeyController = this.settings.key.add(settings.key, "pause").name('Pause')
pauseKeyController.domElement.onclick = this.changeKey.bind(pauseKeyController) pauseKeyController.domElement.onclick = this.changeKey(pauseKeyController)
this.settings.delay = this.settings.addFolder("Répétition automatique").open() this.settings.delay = this.settings.addFolder("Répétition automatique").open()
this.settings.delay.add(settings,"arrDelay").name("ARR (ms)").min(2).max(200).step(1); this.settings.delay.add(settings,"arrDelay").name("ARR (ms)").min(2).max(200).step(1);
@ -76,15 +72,72 @@ export class TetraGUI extends GUI {
scene.tetrisSound.setVolume(volume/100) scene.tetrisSound.setVolume(volume/100)
scene.hardDropSound.setVolume(volume/100) scene.hardDropSound.setVolume(volume/100)
}) })
let material
function changeMaterial() {
material?.destroy()
material = dev.addFolder("minoes material")
material.add(minoes.material, "constructor", ["MeshBasicMaterial", "MeshStandardMaterial", "MeshPhysicalMaterial"]).listen().onChange(type => {
switch(type) {
case "MeshBasicMaterial":
minoes.material = new THREE.MeshBasicMaterial({
envMap: environment,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.5,
reflectivity: 0.9,
})
break
case "MeshStandardMaterial":
minoes.material = new THREE.MeshStandardMaterial({
envMap: environment,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.7,
roughness: 0.48,
metalness: 0.67,
})
break
case "MeshPhysicalMaterial":
minoes.material = new THREE.MeshPhysicalMaterial({
envMap: environment,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.7,
roughness: 0.5,
ior: 1.8,
metalness: 0.9,
transmission: 1,
})
break
}
minoes.update = minoes.updateColor
changeMaterial()
})
this.dev = window.location.search.includes("dev") let minoMaterial = minoes.material instanceof Array ? minoes.material[0] : minoes.material
if (this.dev) { if ("opacity" in minoMaterial) material.add(minoMaterial, "opacity" ).min(0).max(1)
let dev = this.addFolder("dev") if ("reflectivity" in minoMaterial) material.add(minoMaterial, "reflectivity" ).min(0).max(1)
if ("roughness" in minoMaterial) material.add(minoMaterial, "roughness" ).min(0).max(1)
if ("bumpScale" in minoMaterial) material.add(minoMaterial, "bumpScale" ).min(0).max(5)
if ("metalness" in minoMaterial) material.add(minoMaterial, "metalness" ).min(0).max(1)
if ("attenuationDistance" in minoMaterial) material.add(minoMaterial, "attenuationDistance").min(0)
if ("ior" in minoMaterial) material.add(minoMaterial, "ior" ).min(1).max(2)
if ("sheen" in minoMaterial) material.add(minoMaterial, "sheen" ).min(0).max(1)
if ("sheenRoughness" in minoMaterial) material.add(minoMaterial, "sheenRoughness" ).min(0).max(1)
if ("specularIntensity" in minoMaterial) material.add(minoMaterial, "specularIntensity" ).min(0).max(1)
if ("thickness" in minoMaterial) material.add(minoMaterial, "thickness" ).min(0).max(5)
if ("transmission" in minoMaterial) material.add(minoMaterial, "transmission" ).min(0).max(1)
}
let dev
if (window.location.search.includes("dev")) {
dev = this.addFolder("dev")
let cameraPosition = dev.addFolder("camera").close() let cameraPosition = dev.addFolder("camera").close()
cameraPosition.add(scene.camera.position, "x") cameraPosition.add(scene.camera.position, "x").listen()
cameraPosition.add(scene.camera.position, "y") cameraPosition.add(scene.camera.position, "y").listen()
cameraPosition.add(scene.camera.position, "z") cameraPosition.add(scene.camera.position, "z").listen()
cameraPosition.add(scene.camera, "fov", 0, 200).onChange(() => scene.camera.updateProjectionMatrix()) cameraPosition.add(scene.camera, "fov", 0, 200).onChange(() => scene.camera.updateProjectionMatrix()).listen()
let light = dev.addFolder("lights intensity").close() let light = dev.addFolder("lights intensity").close()
light.add(scene.ambientLight, "intensity").name("ambient").min(0).max(20).listen() light.add(scene.ambientLight, "intensity").name("ambient").min(0).max(20).listen()
@ -99,72 +152,8 @@ export class TetraGUI extends GUI {
vortex.add(scene.vortex.darkCylinder.material, "opacity").name("dark").min(0).max(1) vortex.add(scene.vortex.darkCylinder.material, "opacity").name("dark").min(0).max(1)
vortex.add(scene.vortex.colorFullCylinder.material, "opacity").name("colorFull").min(0).max(1) vortex.add(scene.vortex.colorFullCylinder.material, "opacity").name("colorFull").min(0).max(1)
let material changeMaterial(minoes.material.constructor.name)
function changeMaterial(type) {
material?.destroy()
material = dev.addFolder("minoes material")
material.add(Mino.meshes.material, "constructor", ["MeshBasicMaterial", "MeshStandardMaterial", "MeshPhysicalMaterial"]).name("type").onChange(changeMaterial)
switch(type) {
case "MeshBasicMaterial":
Mino.meshes.material = new THREE.MeshBasicMaterial({
envMap: environment,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.5,
reflectivity: 0.9,
})
break
case "MeshStandardMaterial":
Mino.meshes.material = new THREE.MeshStandardMaterial({
envMap: environment,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.7,
roughness: 0.48,
metalness: 0.67,
})
break
case "MeshPhysicalMaterial":
Mino.meshes.material = new THREE.MeshPhysicalMaterial({
envMap: environment,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.7,
roughness: 0.5,
ior: 1.8,
metalness: 0.9,
transmission: 1,
})
break
}
if ("opacity" in Mino.meshes.material) material.add(Mino.meshes.material, "opacity" ).min(0).max(1).listen()
if ("reflectivity" in Mino.meshes.material) material.add(Mino.meshes.material, "reflectivity" ).min(0).max(1).listen()
if ("roughness" in Mino.meshes.material) material.add(Mino.meshes.material, "roughness" ).min(0).max(1).listen()
if ("metalness" in Mino.meshes.material) material.add(Mino.meshes.material, "metalness" ).min(0).max(1).listen()
if ("attenuationDistance" in Mino.meshes.material) material.add(Mino.meshes.material, "attenuationDistance").min(0).listen()
if ("ior" in Mino.meshes.material) material.add(Mino.meshes.material, "ior" ).min(1).max(2).listen()
if ("sheen" in Mino.meshes.material) material.add(Mino.meshes.material, "sheen" ).min(0).max(1).listen()
if ("sheenRoughness" in Mino.meshes.material) material.add(Mino.meshes.material, "sheenRoughness" ).min(0).max(1).listen()
if ("specularIntensity" in Mino.meshes.material) material.add(Mino.meshes.material, "specularIntensity" ).min(0).max(1).listen()
if ("thickness" in Mino.meshes.material) material.add(Mino.meshes.material, "thickness" ).min(0).max(5).listen()
if ("transmission" in Mino.meshes.material) material.add(Mino.meshes.material, "transmission" ).min(0).max(1).listen()
}
changeMaterial(this.materialType)
material.close() material.close()
controls.addEventListener("change", () => cameraPosition.controllersRecursive().forEach((control) => {
control.updateDisplay()
}))
}
if (window.location.search.includes("fps")) {
let fps = new FPS.default()
document.body.appendChild(fps.dom)
this.update = function() {
fps.update()
}
} }
} }
@ -178,8 +167,8 @@ export class TetraGUI extends GUI {
localStorage["teTraSettings"] = JSON.stringify(this.settings.save()) localStorage["teTraSettings"] = JSON.stringify(this.settings.save())
} }
changeKey() { changeKey(settings) {
let controller = this let controller = settings
let input = controller.domElement.getElementsByTagName("input")[0] let input = controller.domElement.getElementsByTagName("input")[0]
input.select() input.select()
input.onkeydown = function (event) { input.onkeydown = function (event) {
@ -187,6 +176,4 @@ export class TetraGUI extends GUI {
input.blur() input.blur()
} }
} }
update() {}
} }

View File

@ -27,7 +27,7 @@ let friendyKeyRenamer = new Proxy({
} }
}) })
class Settings { export default class Settings {
constructor() { constructor() {
this.startLevel = 1 this.startLevel = 1
@ -79,7 +79,4 @@ class Settings {
this.theme = "Plasma" this.theme = "Plasma"
} }
} }
export { Settings }

View File

@ -5,7 +5,7 @@ import { TileMaterial } from './TileMaterial.js'
Array.prototype.pick = function () { return this.splice(Math.floor(Math.random() * this.length), 1)[0] } Array.prototype.pick = function () { return this.splice(Math.floor(Math.random() * this.length), 1)[0] }
let P = (x, y, z = 0) => new THREE.Vector3(x, y, z) let P = (x, y, z=0) => new THREE.Vector3(x, y, z)
const GRAVITY = -30 const GRAVITY = -30
@ -20,7 +20,7 @@ const COLORS = {
LOCKING: 0xffffff, LOCKING: 0xffffff,
GHOST: 0x99a9b2, GHOST: 0x99a9b2,
EDGE: 0x88abe0, EDGE: 0x88abe0,
RETRO: 0xffffff, RETRO: 0xd0d4c1,
} }
const TRANSLATION = { const TRANSLATION = {
@ -69,102 +69,8 @@ const sideMaterial = new THREE.MeshStandardMaterial({
}) })
class InstancedMino extends THREE.InstancedMesh { export class InstancedMino extends THREE.InstancedMesh {
constructor(geometry, material, count) { constructor() {
super(geometry, material, count)
this.instances = new Set()
this.count = 0
this.offsets = new Uint8Array(2*count)
this.update = this.updateColor
}
add(instance) {
this.instances.add(instance)
}
delete(instance) {
this.instances.delete(instance)
}
clear() {
this.instances.clear()
}
setOffsetAt(index, offset) {
this.offsets[index * 2] = offset
}
resetColor() {
this.instanceColor = null
}
updateColor() {
this.count = 0
this.instances.forEach(mino => {
if (mino.parent?.visible) {
this.setMatrixAt(this.count, mino.matrixWorld)
this.setColorAt(this.count, mino.color)
this.count++
}
})
if (this.count) {
this.instanceMatrix.needsUpdate = true
this.instanceColor.needsUpdate = true
}
}
updateOffset() {
this.count = 0
this.instances.forEach(mino => {
if (mino.parent?.visible) {
this.setMatrixAt(this.count, mino.matrixWorld)
this.setOffsetAt(this.count, mino.offset)
this.count++
}
})
if (this.count) {
this.instanceMatrix.needsUpdate = true
this.geometry.setAttribute('offset', new THREE.InstancedBufferAttribute(this.offsets, 2))
}
}
}
class Mino extends THREE.Object3D {
static materials = {
Plasma: new THREE.MeshStandardMaterial({
envMap: environment,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.8,
roughness: 0.48,
metalness: 0.67,
}),
Espace: new THREE.MeshStandardMaterial({
envMap: environment,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.8,
roughness: 0.1,
metalness: 0.99,
}),
Rétro: [sideMaterial, sideMaterial, sideMaterial, sideMaterial, sideMaterial, sideMaterial]
}
static {
new THREE.TextureLoader().load("images/sprites.png", (texture) => {
this.materials.Rétro[0] = this.materials.Rétro[2] = new TileMaterial({
color: 0xd0d4c1,
map: texture,
bumpMap: texture,
bumpScale: 1.5,
roughness: 0.25,
metalness: 0.8,
transparent: true,
}, 8, 8)
})
}
static meshes
static {
let minoFaceShape = new THREE.Shape() let minoFaceShape = new THREE.Shape()
minoFaceShape.moveTo(.1, .1) minoFaceShape.moveTo(.1, .1)
minoFaceShape.lineTo(.1, .9) minoFaceShape.lineTo(.1, .9)
@ -180,18 +86,119 @@ class Mino extends THREE.Object3D {
bevelOffset: 0, bevelOffset: 0,
bevelSegments: 1 bevelSegments: 1
} }
let minoGeometry = new THREE.ExtrudeGeometry(minoFaceShape, minoExtrudeSettings) const geometry = new THREE.ExtrudeGeometry(minoFaceShape, minoExtrudeSettings)
this.meshes = new InstancedMino(minoGeometry, this.materials.Plasma, 2*ROWS*COLUMNS) super(geometry, undefined, 2*ROWS*COLUMNS)
this.offsets = new Uint8Array(2*this.count)
} }
set theme(theme) {
if (theme == "Rétro") {
this.resetColor()
this.update = this.updateOffset
if (this.materials["Rétro"]) {
this.material = this.materials["Rétro"]
} else {
this.materials["Rétro"] = []
const loadingManager = new THREE.LoadingManager(() => this.material = this.materials["Rétro"])
new THREE.TextureLoader(loadingManager).load("images/sprites.png", (texture) => {
this.materials.Rétro[0] = this.materials.Rétro[2] = new TileMaterial({
color: COLORS.RETRO,
map: texture,
bumpMap: texture,
bumpScale: 1.5,
roughness: 0.25,
metalness: 0.9,
transparent: true,
}, 8, 8)
})
new THREE.TextureLoader(loadingManager).load("images/edges.png", (texture) => {
this.materials.Rétro[1] = this.materials.Rétro[3] = this.materials.Rétro[4] = this.materials.Rétro[5] = new TileMaterial({
color: COLORS.RETRO,
map: texture,
bumpMap: texture,
bumpScale: 1.5,
roughness: 0.25,
metalness: 0.9,
transparent: true,
}, 1, 1)
})
}
} else {
this.update = this.updateColor
this.material = this.materials[theme]
}
}
setOffsetAt(index, offset) {
this.offsets[2*index] = offset.x
this.offsets[2*index + 1] = offset.y
}
resetColor() {
this.instanceColor = null
}
updateColor() {
this.count = 0
Mino.instances.forEach(mino => {
if (mino.parent?.visible) {
this.setMatrixAt(this.count, mino.matrixWorld)
this.setColorAt(this.count, mino.color)
this.count++
}
})
if (this.count) {
this.instanceMatrix.needsUpdate = true
this.instanceColor.needsUpdate = true
}
}
updateOffset() {
this.count = 0
Mino.instances.forEach(mino => {
if (mino.parent?.visible) {
this.setMatrixAt(this.count, mino.matrixWorld)
this.setOffsetAt(this.count, mino.offset)
this.count++
}
})
if (this.count) {
this.instanceMatrix.needsUpdate = true
this.geometry.setAttribute('offset', new THREE.InstancedBufferAttribute(this.offsets, 2))
}
}
}
InstancedMino.prototype.materials = {
Plasma: new THREE.MeshStandardMaterial({
envMap: environment,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.7,
roughness: 0.6,
metalness: 1,
}),
Espace: new THREE.MeshStandardMaterial({
envMap: environment,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.8,
roughness: 0.1,
metalness: 0.99,
})
}
class Mino extends THREE.Object3D {
static instances = new Set()
constructor(color, offset) { constructor(color, offset) {
super() super()
this.color = color this.color = color
this.offset = offset this.offset = offset
this.velocity = P(50 - 100 * Math.random(), 50 - 100 * Math.random(), 50 - 100 * Math.random()) this.velocity = P(50 - 100 * Math.random(), 60 - 100 * Math.random(), 50 - 100 * Math.random())
this.rotationAngle = P(Math.random(), Math.random(), Math.random()).normalize() this.rotationAngle = P(Math.random(), Math.random(), Math.random()).normalize()
this.angularVelocity = 5 - 10 * Math.random() this.angularVelocity = 5 - 10 * Math.random()
this.constructor.meshes.add(this) this.constructor.instances.add(this)
} }
explode(delta) { explode(delta) {
@ -207,7 +214,7 @@ class Mino extends THREE.Object3D {
} }
dispose() { dispose() {
this.constructor.meshes.delete(this) this.constructor.instances.delete(this)
} }
} }
@ -222,6 +229,7 @@ class Tetromino extends THREE.Group {
constructor(position) { constructor(position) {
super() super()
if (position) this.position.copy(position) if (position) this.position.copy(position)
this.offset = this.offset.clone()
this.minoesPosition[FACING.NORTH].forEach(() => this.add(new Mino(this.freeColor, this.offset))) this.minoesPosition[FACING.NORTH].forEach(() => this.add(new Mino(this.freeColor, this.offset)))
this.facing = FACING.NORTH this.facing = FACING.NORTH
this.rotatedLast = false this.rotatedLast = false
@ -242,8 +250,10 @@ class Tetromino extends THREE.Group {
set locking(locking) { set locking(locking) {
if (locking) { if (locking) {
this.color = this.lockingColor this.color = this.lockingColor
this.offset.y = 2
} else { } else {
this.color = this.freeColor this.color = this.freeColor
this.offset.y = 0
} }
} }
@ -256,13 +266,12 @@ class Tetromino extends THREE.Group {
return this.minoesPosition[facing].every(minoPosition => this.parent?.cellIsEmpty(minoPosition.clone().add(testPosition))) return this.minoesPosition[facing].every(minoPosition => this.parent?.cellIsEmpty(minoPosition.clone().add(testPosition)))
} }
move(translation, rotatedFacing, rotationPoint) { move(translation, rotatedFacing) {
if (this.canMove(translation, rotatedFacing)) { if (this.canMove(translation, rotatedFacing)) {
this.position.add(translation) this.position.add(translation)
this.rotatedLast = rotatedFacing this.rotatedLast = rotatedFacing
if (rotatedFacing != undefined) { if (rotatedFacing != undefined) {
this.facing = rotatedFacing this.facing = rotatedFacing
if (rotationPoint == 4) this.rotationPoint4Used = true
} }
if (this.canMove(TRANSLATION.DOWN)) { if (this.canMove(TRANSLATION.DOWN)) {
this.locking = false this.locking = false
@ -277,16 +286,19 @@ class Tetromino extends THREE.Group {
} else if (translation == TRANSLATION.DOWN) { } else if (translation == TRANSLATION.DOWN) {
this.locked = true this.locked = true
if (!scheduler.timeoutTasks.has(this.onLockDown)) if (!scheduler.timeoutTasks.has(this.onLockDown))
scheduler.setTimeout(this.onLockDown, this.lockDelay) scheduler.resetTimeout(this.onLockDown, this.lockDelay)
} }
} }
rotate(rotation) { rotate(rotation) {
let testFacing = (this.facing + rotation) % 4 let testFacing = (this.facing + rotation) % 4
return this.srs[this.facing][rotation].some( return this.srs[this.facing][rotation].some((translation, rotationPoint) => {
(translation, rotationPoint) => this.move(translation, testFacing, rotationPoint) if (this.move(translation, testFacing)) {
) if (rotationPoint == 4) this.rotationPoint4Used = true
return true
}
})
} }
get tSpin() { get tSpin() {
@ -295,7 +307,7 @@ class Tetromino extends THREE.Group {
} }
Tetromino.prototype.lockingColor = new THREE.Color(COLORS.LOCKING) Tetromino.prototype.lockingColor = new THREE.Color(COLORS.LOCKING)
// Super Rotation System // Super Rotation System
// freedom of movement = srs[this.parent.piece.facing][rotation] // freedom of movement = srs[this.facing][rotation]
Tetromino.prototype.srs = [ Tetromino.prototype.srs = [
{ [ROTATION.CW]: [P(0, 0), P(-1, 0), P(-1, 1), P(0, -2), P(-1, -2)], [ROTATION.CCW]: [P(0, 0), P(1, 0), P(1, 1), P(0, -2), P(1, -2)] }, { [ROTATION.CW]: [P(0, 0), P(-1, 0), P(-1, 1), P(0, -2), P(-1, -2)], [ROTATION.CCW]: [P(0, 0), P(1, 0), P(1, 1), P(0, -2), P(1, -2)] },
{ [ROTATION.CW]: [P(0, 0), P(1, 0), P(1, -1), P(0, 2), P(1, 2)], [ROTATION.CCW]: [P(0, 0), P(1, 0), P(1, -1), P(0, 2), P(1, 2)] }, { [ROTATION.CW]: [P(0, 0), P(1, 0), P(1, -1), P(0, 2), P(1, 2)], [ROTATION.CCW]: [P(0, 0), P(1, 0), P(1, -1), P(0, 2), P(1, 2)] },
@ -309,7 +321,7 @@ class Ghost extends Tetromino {
copy(piece) { copy(piece) {
this.position.copy(piece.position) this.position.copy(piece.position)
this.minoesPosition = piece.minoesPosition this.minoesPosition = piece.minoesPosition
//this.children.forEach(mino => mino.offset = piece.offset) this.children.forEach(mino => {mino.offset = piece.ghostOffset})
this.facing = piece.facing this.facing = piece.facing
this.visible = true this.visible = true
while (this.canMove(TRANSLATION.DOWN)) this.position.y-- while (this.canMove(TRANSLATION.DOWN)) this.position.y--
@ -319,7 +331,7 @@ Ghost.prototype.freeColor = new THREE.Color(COLORS.GHOST)
Ghost.prototype.minoesPosition = [ Ghost.prototype.minoesPosition = [
[P(0, 0, 0), P(0, 0, 0), P(0, 0, 0), P(0, 0, 0)], [P(0, 0, 0), P(0, 0, 0), P(0, 0, 0), P(0, 0, 0)],
] ]
Ghost.prototype.offset = 0 Ghost.prototype.offset = P(0, 1)
class I extends Tetromino { } class I extends Tetromino { }
@ -336,7 +348,8 @@ I.prototype.srs = [
{ [ROTATION.CW]: [P(0, 0), P(1, 0), P(-2, 0), P(1, -2), P(-2, 1)], [ROTATION.CCW]: [P(0, 0), P(-2, 0), P(1, 0), P(-2, -1), P(1, 2)] }, { [ROTATION.CW]: [P(0, 0), P(1, 0), P(-2, 0), P(1, -2), P(-2, 1)], [ROTATION.CCW]: [P(0, 0), P(-2, 0), P(1, 0), P(-2, -1), P(1, 2)] },
] ]
I.prototype.freeColor = new THREE.Color(COLORS.I) I.prototype.freeColor = new THREE.Color(COLORS.I)
I.prototype.offset = 1 I.prototype.offset = P(0, 0)
I.prototype.ghostOffset = P(0, 1)
class J extends Tetromino { } class J extends Tetromino { }
J.prototype.minoesPosition = [ J.prototype.minoesPosition = [
@ -346,7 +359,8 @@ J.prototype.minoesPosition = [
[P(0, 1), P(-1, -1), P(0, 0), P(0, -1)], [P(0, 1), P(-1, -1), P(0, 0), P(0, -1)],
] ]
J.prototype.freeColor = new THREE.Color(COLORS.J) J.prototype.freeColor = new THREE.Color(COLORS.J)
J.prototype.offset = 2 J.prototype.offset = P(1, 0)
J.prototype.ghostOffset = P(1, 1)
class L extends Tetromino { class L extends Tetromino {
} }
@ -357,7 +371,8 @@ L.prototype.minoesPosition = [
[P(0, 1), P(0, 0), P(0, -1), P(-1, 1)], [P(0, 1), P(0, 0), P(0, -1), P(-1, 1)],
] ]
L.prototype.freeColor = new THREE.Color(COLORS.L) L.prototype.freeColor = new THREE.Color(COLORS.L)
L.prototype.offset = 3 L.prototype.offset = P(2, 0)
L.prototype.ghostOffset = P(2, 1)
class O extends Tetromino { } class O extends Tetromino { }
O.prototype.minoesPosition = [ O.prototype.minoesPosition = [
@ -367,7 +382,8 @@ O.prototype.srs = [
{ [ROTATION.CW]: [], [ROTATION.CCW]: [] } { [ROTATION.CW]: [], [ROTATION.CCW]: [] }
] ]
O.prototype.freeColor = new THREE.Color(COLORS.O) O.prototype.freeColor = new THREE.Color(COLORS.O)
O.prototype.offset = 4 O.prototype.offset = P(3, 0)
O.prototype.ghostOffset = P(3, 1)
class S extends Tetromino { } class S extends Tetromino { }
S.prototype.minoesPosition = [ S.prototype.minoesPosition = [
@ -377,7 +393,8 @@ S.prototype.minoesPosition = [
[P(-1, 1), P(0, 0), P(-1, 0), P(0, -1)], [P(-1, 1), P(0, 0), P(-1, 0), P(0, -1)],
] ]
S.prototype.freeColor = new THREE.Color(COLORS.S) S.prototype.freeColor = new THREE.Color(COLORS.S)
S.prototype.offset = 5 S.prototype.offset = P(4, 0)
S.prototype.ghostOffset = P(4, 1)
class T extends Tetromino { class T extends Tetromino {
get tSpin() { get tSpin() {
@ -405,7 +422,8 @@ T.prototype.tSlots = [
[P(-1, -1), P(-1, 1), P(1, 1), P(1, -1)], [P(-1, -1), P(-1, 1), P(1, 1), P(1, -1)],
] ]
T.prototype.freeColor = new THREE.Color(COLORS.T) T.prototype.freeColor = new THREE.Color(COLORS.T)
T.prototype.offset = 6 T.prototype.offset = P(5, 0)
T.prototype.ghostOffset = P(5, 1)
class Z extends Tetromino { } class Z extends Tetromino { }
Z.prototype.minoesPosition = [ Z.prototype.minoesPosition = [
@ -415,7 +433,8 @@ Z.prototype.minoesPosition = [
[P(0, 1), P(-1, 0), P(0, 0), P(-1, -1)] [P(0, 1), P(-1, 0), P(0, 0), P(-1, -1)]
] ]
Z.prototype.freeColor = new THREE.Color(COLORS.Z) Z.prototype.freeColor = new THREE.Color(COLORS.Z)
Z.prototype.offset = 7 Z.prototype.offset = P(6, 0)
Z.prototype.ghostOffset = P(6, 1)
class Playfield extends THREE.Group { class Playfield extends THREE.Group {
@ -457,20 +476,20 @@ class Playfield extends THREE.Group {
.lineTo(COLUMNS, 0) .lineTo(COLUMNS, 0)
.lineTo(COLUMNS, SKYLINE) .lineTo(COLUMNS, SKYLINE)
.lineTo(COLUMNS + 1, SKYLINE) .lineTo(COLUMNS + 1, SKYLINE)
.lineTo(COLUMNS + 1, -.5) .lineTo(COLUMNS + 1, -1/3)
.lineTo(-1, -.5) .lineTo(-1, -1/3)
.moveTo(-1, SKYLINE) .moveTo(-1, SKYLINE)
const retroEdgeTexture = new THREE.TextureLoader(loadingManager).load("images/edge.png", (texture) => { const retroEdgeTexture = new THREE.TextureLoader(loadingManager).load("images/edge.png", (texture) => {
texture.wrapS = THREE.RepeatWrapping texture.wrapS = THREE.RepeatWrapping
texture.wrapT = THREE.RepeatWrapping texture.wrapT = THREE.RepeatWrapping
}) })
const retroEdgeMaterial = new THREE.MeshStandardMaterial({ const retroEdgeMaterial = new THREE.MeshStandardMaterial({
color: 0xd0d4c1, color: COLORS.RETRO,
map: retroEdgeTexture, map: retroEdgeTexture,
bumpMap: retroEdgeTexture, bumpMap: retroEdgeTexture,
bumpScale: 0.3, bumpScale: 1.5,
roughness: 0.25, roughness: 0.25,
metalness: 0.8, metalness: 0.9,
}) })
this.retroEdge = new THREE.Mesh( this.retroEdge = new THREE.Mesh(
new THREE.ExtrudeGeometry(retroEdgeShape, { new THREE.ExtrudeGeometry(retroEdgeShape, {
@ -487,7 +506,7 @@ class Playfield extends THREE.Group {
metalness: 0.9, metalness: 0.9,
}) })
) )
back.position.set(COLUMNS/2, SKYLINE/2, 0) back.position.set(COLUMNS/2, SKYLINE/2)
this.retroEdge.add(back) this.retroEdge.add(back)
this.retroEdge.visible = false this.retroEdge.visible = false
this.add(this.retroEdge) this.add(this.retroEdge)
@ -587,7 +606,7 @@ class Playfield extends THREE.Group {
class HoldQueue extends THREE.Group { class HoldQueue extends THREE.Group {
constructor() { constructor() {
super() super()
this.position.set(-4, SKYLINE - 2) this.position.set(-5, SKYLINE - 2)
} }
set piece(piece) { set piece(piece) {
@ -611,7 +630,7 @@ class HoldQueue extends THREE.Group {
class NextQueue extends THREE.Group { class NextQueue extends THREE.Group {
constructor() { constructor() {
super() super()
this.position.set(13, SKYLINE - 2) this.position.set(14, SKYLINE - 2)
} }
init() { init() {

View File

@ -55,7 +55,7 @@ export class Vortex extends THREE.Group {
texture.repeat.set(2, 2) texture.repeat.set(2, 2)
this.colorFullCylinder.material.map = texture this.colorFullCylinder.material.map = texture
}) })
this.colorFullCylinder.material.opacity = 0.7 this.colorFullCylinder.material.opacity = 0.5
this.globalRotation = 0.028 this.globalRotation = 0.028
this.darkTextureRotation = 0.005 this.darkTextureRotation = 0.005
@ -70,10 +70,10 @@ export class Vortex extends THREE.Group {
new THREE.TextureLoader(this.loadingManager).load("./images/dark.jpg", texture => { new THREE.TextureLoader(this.loadingManager).load("./images/dark.jpg", texture => {
texture.wrapS = THREE.RepeatWrapping texture.wrapS = THREE.RepeatWrapping
texture.wrapT = THREE.MirroredRepeatWrapping texture.wrapT = THREE.MirroredRepeatWrapping
texture.repeat.set(2, 2) texture.repeat.set(2, 4)
this.darkCylinder.material.map = texture this.darkCylinder.material.map = texture
}) })
this.darkCylinder.material.opacity = 0.05 this.darkCylinder.material.opacity = 0.08
new THREE.TextureLoader(this.loadingManager).load("./images/colorfull.jpg", texture => { new THREE.TextureLoader(this.loadingManager).load("./images/colorfull.jpg", texture => {
texture.wrapS = THREE.RepeatWrapping texture.wrapS = THREE.RepeatWrapping
@ -81,7 +81,7 @@ export class Vortex extends THREE.Group {
texture.repeat.set(2, 2) texture.repeat.set(2, 2)
this.colorFullCylinder.material.map = texture this.colorFullCylinder.material.map = texture
}) })
this.colorFullCylinder.material.opacity = 0.14 this.colorFullCylinder.material.opacity = 0.15
this.globalRotation = 0.028 this.globalRotation = 0.028
this.darkTextureRotation = 0.006 this.darkTextureRotation = 0.006

View File

@ -15,6 +15,11 @@ class Scheduler {
} }
} }
resetInterval(func, delay, ...args) {
this.clearInterval(func)
this.setInterval(func, delay, ...args)
}
setTimeout(func, delay, ...args) { setTimeout(func, delay, ...args) {
this.timeoutTasks.set(func, window.setTimeout(func, delay, ...args)) this.timeoutTasks.set(func, window.setTimeout(func, delay, ...args))
} }
@ -33,6 +38,4 @@ class Scheduler {
} }
const scheduler = new Scheduler() export const scheduler = new Scheduler()
export { scheduler }