Compare commits

...

43 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
ae8dcb7077 ᵀᴱTᴿᴬ 2024-09-30 14:03:47 +02:00
6ed614d536 MeshStandardMaterial on Plasma 2024-09-27 03:10:23 +02:00
8c5b704b3c MeshPhysicalMaterial on Plasma 2024-09-27 03:06:36 +02:00
1b0f1c07d2 camera init position 2024-09-27 00:52:19 +02:00
c8eb029987 another leak fixed! 2024-09-27 00:47:23 +02:00
825fbca97b fix a leak 2024-09-26 23:15:15 +02:00
ce94604fc0 split fps & dev 2024-09-26 23:15:08 +02:00
07daa4a9cf small fixes 2024-09-26 21:30:12 +02:00
abf562fd89 themes fixes 2024-09-26 13:52:41 +02:00
cae3dc9af5 themed directionnal light position 2024-09-26 11:53:58 +02:00
a75329f985 fix color on theme change 2024-09-26 11:53:31 +02:00
31eca05faf two faces 2024-09-26 09:02:08 +02:00
7f6795109b RETRO THEME 2024-09-26 01:30:31 +02:00
8ed998f255 upatre 2024-09-25 15:24:28 +02:00
20 changed files with 796 additions and 714 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)

86
app.js
View File

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

Binary file not shown.

70
css/loading.css Normal file
View File

@ -0,0 +1,70 @@
#loadingDiv {
position: absolute;
top: 0;
left: 0;
display: flex;
flex-flow: column;
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;
}
.scene {
width: 200px;
height: 200px;
margin: 0 auto;
perspective: 200px;
font-size: 40px;
}
.tetromino {
position: relative;
top: 2em;
left: 2em;
width: 1em;
height: 1em;
transform-style: preserve-3d;
transform: translateZ(0.5em);
animation: spinCube 5s infinite ease-in-out;
}
@keyframes spinCube {
0% { transform: translateZ(0.5em) rotateX( 0deg) rotateY( 0deg); }
100% { transform: translateZ(0.5em) rotateX(360deg) rotateY(360deg); }
}
.mino {
width: 1em;
height: 1em;
position: absolute;
transform-style: preserve-3d;
}
.T.tetromino .first.mino { top: -0.5em; left: -1em; }
.T.tetromino .second.mino { top: -0.5em; left: 0em; }
.T.tetromino .third.mino { top: -0.5em; left: 1em; }
.T.tetromino .fourth.mino { top: 0.5em; left: 0em; }
.face {
position: absolute;
width: 1em;
height: 1em;
padding: 0;
background: hsla(240, 100%, 0%, 0.4);
border: 1px solid hsla(240, 100%, 70%, 0.6);
}
.front.face { transform: rotateY( 0deg) translateZ(0.5em); }
.right.face { transform: rotateY( 90deg) translateZ(0.5em); }
.back.face { transform: rotateY(180deg) translateZ(0.5em); }
.left.face { transform: rotateY(-90deg) translateZ(0.5em); }
.top.face { transform: rotateX( 90deg) translateZ(0.5em); }
.bottom.face { transform: rotateX(-90deg) translateZ(0.5em); }

View File

@ -9,14 +9,16 @@ body {
span {
position: absolute;
top: 0;
left: 0;
}
.lil-gui {
.lil-menu {
--background-color: rgba(33, 37, 41, 30%);
--width: 200px;
}
@supports (backdrop-filter: blur()) {
.lil-gui {
.lil-menu {
backdrop-filter: blur(15px);
}
}
@ -27,7 +29,11 @@ span {
left: 15px;
}
.lil-gui .controller.disabled {
.lil-menu.root > .title {
font-size: 1.5em;
}
.lil-menu .controller.disabled {
opacity: .8;
}
@ -51,6 +57,7 @@ canvas {
#messagesSpan div {
opacity: 0;
overflow: hidden;
user-select: none;
}
h1 {
@ -166,11 +173,10 @@ h1 {
}
.pause #pauseSpan {
display: flex;
position:absolute;
display: flex;
position: absolute;
top: 0;
left: 0;
display: flex;
filter: blur(2px);
width: 100%;
height: 100%;
@ -181,4 +187,5 @@ h1 {
font-size: 20vh;
font-weight: 800;
letter-spacing: .1em;
user-select: none;
}

BIN
images/edge.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

BIN
images/edges.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

BIN
images/sprites.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -4,37 +4,78 @@
<head>
<meta charset="utf-8" />
<title>teTra</title>
<title>ᵀᴱTᴿᴬ</title>
<link rel="icon" href="favicon.ico">
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="loading.css">
<link rel="stylesheet" href="css/style.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 type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.155/build/three.module.js?module",
"three/addons/": "https://unpkg.com/three@0.155/examples/jsm/"
"three": "https://unpkg.com/three@0.169/build/three.module.js?module",
"three/addons/": "https://unpkg.com/three@0.169/examples/jsm/"
}
}
</script>
</head>
<body>
<div id="loaddingCircle">
<div class="e-loadholder">
<div class="m-loader">
<span class="e-text">
<span id="loadingDiv">
<div class="scene">
<div class="T tetromino">
<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 id="loadingPercent">0%</div>
</span>
</div>
</div>
</div>
</div>
</span>
<span id="messagesSpan"></span>
<span id="pauseSpan" tabindex="1">II</span>
<audio id="music" src="audio/benevolence.m4a" loop></audio>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ENjdO4Dr2bkBIFxQpeoTz1HIcje39Wm4jDKdf19U8gI4ddQ3GYNS7NTKfAdVQSZe" crossorigin="anonymous"></script>
<script type="module" src="app.js"></script>
<script>navigator?.serviceWorker.register('./jsm/service-worker.js')</script>
<script>navigator?.serviceWorker?.register('./jsm/service-worker.js')</script>
</body>
</html>

View File

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

179
jsm/Menu.js Normal file
View File

@ -0,0 +1,179 @@
import * as THREE from 'three'
import { GUI } from 'three/addons/libs/lil-gui.module.min.js'
import { environment } from './Tetrominoes.js'
export class Menu extends GUI {
constructor(game, settings, stats, scene, minoes, playfield) {
super({title: "ᵀᴱTᴿᴬ"})
this.startButton = this.add(game, "start").name("Jouer").hide()
this.pauseButton = this.add(game, "pause").name("Pause").hide()
this.resumeButton = this.add(game, "resume").name("Reprendre").hide()
this.stats = this.addFolder("Statistiques").hide()
this.stats.add(stats, "time").name("Temps").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, "level").name("Niveau").disable().listen()
this.stats.add(stats, "totalClearedLines").name("Lignes").disable().listen()
this.stats.add(stats, "goal").name("Objectif").disable().listen()
this.stats.add(stats, "nbTetra").name("teTras").disable().listen()
this.stats.add(stats, "nbTSpin").name("Pirouettes").disable().listen()
this.stats.add(stats, "maxCombo").name("Combos max").disable().listen()
this.stats.add(stats, "maxB2B").name("BàB max").disable().listen()
this.settings = this.addFolder("Options")
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 => {
scene.theme = theme
minoes.theme = theme
if (theme == "Rétro") {
playfield.edge.visible = false
playfield.retroEdge.visible = true
music.src = "audio/Tetris_MkVaffQuasi_Ultimix_OC_ReMix.mp3"
} else {
playfield.edge.visible = true
playfield.retroEdge.visible = false
music.src = "audio/benevolence.m4a"
}
if (dev) changeMaterial()
})
this.settings.key = this.settings.addFolder("Commandes").open()
let moveLeftKeyController = this.settings.key.add(settings.key, "moveLeft").name('Gauche')
moveLeftKeyController.domElement.onclick = this.changeKey(moveLeftKeyController)
let moveRightKeyController = this.settings.key.add(settings.key, "moveRight").name('Droite')
moveRightKeyController.domElement.onclick = this.changeKey(moveRightKeyController)
let rotateCWKeyController = this.settings.key.add(settings.key, "rotateCW").name('Rotation horaire')
rotateCWKeyController.domElement.onclick = this.changeKey(rotateCWKeyController)
let rotateCCWKeyController = this.settings.key.add(settings.key, "rotateCCW").name('anti-horaire')
rotateCCWKeyController.domElement.onclick = this.changeKey(rotateCCWKeyController)
let softDropKeyController = this.settings.key.add(settings.key, "softDrop").name('Chute lente')
softDropKeyController.domElement.onclick = this.changeKey(softDropKeyController)
let hardDropKeyController = this.settings.key.add(settings.key, "hardDrop").name('Chute rapide')
hardDropKeyController.domElement.onclick = this.changeKey(hardDropKeyController)
let holdKeyController = this.settings.key.add(settings.key, "hold").name('Garder')
holdKeyController.domElement.onclick = this.changeKey(holdKeyController)
let pauseKeyController = this.settings.key.add(settings.key, "pause").name('Pause')
pauseKeyController.domElement.onclick = this.changeKey(pauseKeyController)
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,"dasDelay").name("DAS (ms)").min(100).max(500).step(5);
this.settings.volume = this.settings.addFolder("Volume").open()
this.settings.volume.add(settings,"musicVolume").name("Musique").min(0).max(100).step(1).onChange(volume => {
scene.music.volume = settings.musicVolume / 100
})
this.settings.volume.add(settings,"sfxVolume").name("Effets").min(0).max(100).step(1).onChange(volume => {
scene.lineClearSound.setVolume(volume/100)
scene.tetrisSound.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()
})
let minoMaterial = minoes.material instanceof Array ? minoes.material[0] : minoes.material
if ("opacity" in minoMaterial) material.add(minoMaterial, "opacity" ).min(0).max(1)
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()
cameraPosition.add(scene.camera.position, "x").listen()
cameraPosition.add(scene.camera.position, "y").listen()
cameraPosition.add(scene.camera.position, "z").listen()
cameraPosition.add(scene.camera, "fov", 0, 200).onChange(() => scene.camera.updateProjectionMatrix()).listen()
let light = dev.addFolder("lights intensity").close()
light.add(scene.ambientLight, "intensity").name("ambient").min(0).max(20).listen()
light.add(scene.directionalLight, "intensity").name("directional").min(0).max(20).listen()
let directionalLightPosition = dev.addFolder("directionalLight.position").close()
directionalLightPosition.add(scene.directionalLight.position, "x").listen()
directionalLightPosition.add(scene.directionalLight.position, "y").listen()
directionalLightPosition.add(scene.directionalLight.position, "z").listen()
let vortex = dev.addFolder("vortex opacity").close()
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)
changeMaterial(minoes.material.constructor.name)
material.close()
}
}
load() {
if (localStorage["teTraSettings"]) {
this.settings.load(JSON.parse(localStorage["teTraSettings"]))
}
}
save() {
localStorage["teTraSettings"] = JSON.stringify(this.settings.save())
}
changeKey(settings) {
let controller = settings
let input = controller.domElement.getElementsByTagName("input")[0]
input.select()
input.onkeydown = function (event) {
controller.setValue(event.key)
input.blur()
}
}
}

View File

@ -27,7 +27,7 @@ let friendyKeyRenamer = new Proxy({
}
})
class Settings {
export default class Settings {
constructor() {
this.startLevel = 1
@ -76,8 +76,7 @@ class Settings {
this.musicVolume = 50
this.sfxVolume = 50
this.theme = "Plasma"
}
}
export { Settings }
}

View File

@ -1,5 +1,5 @@
import { Clock } from 'three'
import { T_SPIN } from './gamelogic.js'
import { T_SPIN } from './Tetrominoes.js'
// score = AWARDED_LINE_CLEARS[tSpin][nbClearedLines]

View File

@ -1,248 +0,0 @@
import * as THREE from 'three'
import { GUI } from 'three/addons/libs/lil-gui.module.min.js'
import * as FPS from 'three/addons/libs/stats.module.js'
import { Mino, environnement } from './gamelogic.js'
export class TetraGUI extends GUI {
constructor(game, settings, stats, scene, controls) {
super({title: "teTra"})
this.startButton = this.add(game, "start").name("Jouer").hide()
this.pauseButton = this.add(game, "pause").name("Pause").hide()
this.resumeButton = this.add(game, "resume").name("Reprendre").hide()
this.stats = this.addFolder("Stats").hide()
this.stats.add(stats, "time").name("Temps").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, "level").name("Niveau").disable().listen()
this.stats.add(stats, "totalClearedLines").name("Lignes").disable().listen()
this.stats.add(stats, "goal").name("Objectif").disable().listen()
this.stats.add(stats, "nbTetra").name("teTras").disable().listen()
this.stats.add(stats, "nbTSpin").name("Pirouettes").disable().listen()
this.stats.add(stats, "maxCombo").name("Combos max").disable().listen()
this.stats.add(stats, "maxB2B").name("BàB max").disable().listen()
this.settings = this.addFolder("Options").open()
this.settings.add(settings, "startLevel").name("Niveau initial").min(1).max(15).step(1)
this.settings.add(scene.vortex, "background", ["Plasma", "Espace"]).name("Fond").onChange(background => {
const loadingManager = new THREE.LoadingManager()
let darkTexture, colorfullTexture
switch (background) {
case "Plasma":
darkTexture = new THREE.TextureLoader(loadingManager).load("./images/plasma.jpg", texture => {
texture.wrapS = THREE.RepeatWrapping
texture.wrapT = THREE.MirroredRepeatWrapping
texture.repeat.set(1, 1)
})
colorfullTexture = new THREE.TextureLoader(loadingManager).load("./images/plasma2.jpg", texture => {
texture.wrapS = THREE.RepeatWrapping
texture.wrapT = THREE.MirroredRepeatWrapping
texture.repeat.set(2, 1)
})
loadingManager.onLoad = function() {
scene.vortex.darkCylinder.material.map = darkTexture
scene.vortex.darkCylinder.material.opacity = 0.17
scene.vortex.colorFullCylinder.material.map = colorfullTexture
scene.vortex.colorFullCylinder.material.opacity = 0.7
scene.vortex.globalRotation = 0.028
scene.vortex.darkTextureRotation = 0.005
scene.vortex.darkMoveForward = 0.009
scene.vortex.colorFullTextureRotation = 0.006
scene.vortex.colorFullMoveForward = 0.025
scene.ambientLight.intensity = 0.6
scene.directionalLight.intensity = 5
Mino.mesh.material.opacity = 0.7
Mino.mesh.material.roughness = 0.48
Mino.mesh.material.metalness = 0.9
}
break
case "Espace":
darkTexture = new THREE.TextureLoader(loadingManager).load("./images/dark.jpg", texture => {
texture.wrapS = THREE.RepeatWrapping
texture.wrapT = THREE.MirroredRepeatWrapping
texture.repeat.set(2, 2)
})
colorfullTexture = new THREE.TextureLoader(loadingManager).load("./images/colorfull.jpg", texture => {
texture.wrapS = THREE.RepeatWrapping
texture.wrapT = THREE.MirroredRepeatWrapping
texture.repeat.set(2, 2)
})
loadingManager.onLoad = function() {
scene.vortex.darkCylinder.material.map = darkTexture
scene.vortex.darkCylinder.material.opacity = 0.05
scene.vortex.colorFullCylinder.material.map = colorfullTexture
scene.vortex.colorFullCylinder.material.opacity = 0.25
scene.vortex.globalRotation = 0.028
scene.vortex.darkTextureRotation = 0.006
scene.vortex.darkMoveForward = 0.03
scene.vortex.colorFullTextureRotation = 0.006
scene.vortex.colorFullMoveForward = 0.012
scene.ambientLight.intensity = 20
scene.directionalLight.intensity = 10
//Mino.mesh.material.opacity = 0.6
//Mino.mesh.material.roughness = 0.08
//Mino.mesh.material.metalness = 0.98
}
break
}
})
this.settings.key = this.settings.addFolder("Commandes").open()
let moveLeftKeyController = this.settings.key.add(settings.key, "moveLeft").name('Gauche')
moveLeftKeyController.domElement.onclick = this.changeKey.bind(moveLeftKeyController)
let moveRightKeyController = this.settings.key.add(settings.key, "moveRight").name('Droite')
moveRightKeyController.domElement.onclick = this.changeKey.bind(moveRightKeyController)
let rotateCWKeyController = this.settings.key.add(settings.key, "rotateCW").name('Rotation horaire')
rotateCWKeyController.domElement.onclick = this.changeKey.bind(rotateCWKeyController)
let rotateCCWKeyController = this.settings.key.add(settings.key, "rotateCCW").name('anti-horaire')
rotateCCWKeyController.domElement.onclick = this.changeKey.bind(rotateCCWKeyController)
let softDropKeyController = this.settings.key.add(settings.key, "softDrop").name('Chute lente')
softDropKeyController.domElement.onclick = this.changeKey.bind(softDropKeyController)
let hardDropKeyController = this.settings.key.add(settings.key, "hardDrop").name('Chute rapide')
hardDropKeyController.domElement.onclick = this.changeKey.bind(hardDropKeyController)
let holdKeyController = this.settings.key.add(settings.key, "hold").name('Garder')
holdKeyController.domElement.onclick = this.changeKey.bind(holdKeyController)
let pauseKeyController = this.settings.key.add(settings.key, "pause").name('Pause')
pauseKeyController.domElement.onclick = this.changeKey.bind(pauseKeyController)
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,"dasDelay").name("DAS (ms)").min(100).max(500).step(5);
this.settings.volume = this.settings.addFolder("Volume").open()
this.settings.volume.add(settings,"musicVolume").name("Musique").min(0).max(100).step(1).onChange(volume => {
scene.music.volume = settings.musicVolume / 100
})
this.settings.volume.add(settings,"sfxVolume").name("Effets").min(0).max(100).step(1).onChange(volume => {
scene.lineClearSound.setVolume(volume/100)
scene.tetrisSound.setVolume(volume/100)
scene.hardDropSound.setVolume(volume/100)
})
this.dev = window.location.search.includes("dev")
if (this.dev) {
let dev = this.addFolder("dev")
let cameraPosition = dev.addFolder("camera").close()
cameraPosition.add(scene.camera.position, "x")
cameraPosition.add(scene.camera.position, "y")
cameraPosition.add(scene.camera.position, "z")
cameraPosition.add(scene.camera, "fov", 0, 200).onChange(() => scene.camera.updateProjectionMatrix())
let light = dev.addFolder("lights intensity").close()
light.add(scene.ambientLight, "intensity").name("ambient").min(0).max(20)
light.add(scene.directionalLight, "intensity").name("directional").min(0).max(20)
let directionalLightPosition = dev.addFolder("directionalLight.position").close()
directionalLightPosition.add(scene.directionalLight.position, "x")
directionalLightPosition.add(scene.directionalLight.position, "y")
directionalLightPosition.add(scene.directionalLight.position, "z")
let vortex = dev.addFolder("vortex opacity").close()
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)
let material
function changeMaterial(type) {
material?.destroy()
material = dev.addFolder("minoes material")
material.add(Mino.mesh.material, "constructor", ["MeshBasicMaterial", "MeshStandardMaterial", "MeshPhysicalMaterial"]).name("type").onChange(changeMaterial)
switch(type) {
case "MeshBasicMaterial":
Mino.mesh.material = new THREE.MeshBasicMaterial({
envMap: environnement,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.5,
reflectivity: 0.9,
})
break
case "MeshStandardMaterial":
Mino.mesh.material = new THREE.MeshStandardMaterial({
envMap: environnement,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.7,
roughness: 0.48,
metalness: 0.67,
})
break
case "MeshPhysicalMaterial":
Mino.mesh.material = new THREE.MeshPhysicalMaterial({
envMap: environnement,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.6,
roughness: 0.5,
metalness: 0.67,
attenuationDistance: 0.5,
ior: 2,
sheen: 0,
sheenRoughness: 1,
specularIntensity: 1,
thickness: 5,
transmission: 1,
})
break
}
if ("opacity" in Mino.mesh.material) material.add(Mino.mesh.material, "opacity").min(0).max(1)
if ("reflectivity" in Mino.mesh.material) material.add(Mino.mesh.material, "reflectivity").min(0).max(1)
if ("roughness" in Mino.mesh.material) material.add(Mino.mesh.material, "roughness").min(0).max(1)
if ("metalness" in Mino.mesh.material) material.add(Mino.mesh.material, "metalness").min(0).max(1)
if ("attenuationDistance" in Mino.mesh.material) material.add(Mino.mesh.material, "attenuationDistance").min(0)
if ("ior" in Mino.mesh.material) material.add(Mino.mesh.material, "ior").min(1).max(2)
if ("sheen" in Mino.mesh.material) material.add(Mino.mesh.material, "sheen").min(0).max(1)
if ("sheenRoughness" in Mino.mesh.material) material.add(Mino.mesh.material, "sheenRoughness").min(0).max(1)
if ("specularIntensity" in Mino.mesh.material) material.add(Mino.mesh.material, "specularIntensity").min(0).max(1)
if ("thickness" in Mino.mesh.material) material.add(Mino.mesh.material, "thickness").min(0).max(5)
if ("transmission" in Mino.mesh.material) material.add(Mino.mesh.material, "transmission").min(0).max(1)
}
changeMaterial(this.materialType)
material.close()
let fps = new FPS.default()
document.body.appendChild(fps.dom)
this.update = function() {
fps.update()
}
controls.addEventListener("change", () => cameraPosition.controllersRecursive().forEach((control) => {
control.updateDisplay()
}))
}
this.load()
}
load() {
if (localStorage["teTraSettings"]) {
this.settings.load(JSON.parse(localStorage["teTraSettings"]))
}
}
save() {
localStorage["teTraSettings"] = JSON.stringify(this.settings.save())
}
changeKey() {
let controller = this
let input = controller.domElement.getElementsByTagName("input")[0]
input.select()
input.onkeydown = function (event) {
controller.setValue(event.key)
input.blur()
}
}
update() {}
}

View File

@ -7,7 +7,7 @@ export class TetraScene extends THREE.Scene {
super()
this.camera = new THREE.PerspectiveCamera(100, window.innerWidth / window.innerHeight, 0.1, 1000)
this.camera.position.set(5, 3, 12)
this.camera.position.set(5, 4, 12)
this.vortex = new Vortex(loadingManager)
this.add(this.vortex)
@ -16,9 +16,9 @@ export class TetraScene extends THREE.Scene {
this.add(this.ambientLight)
this.directionalLight = new THREE.DirectionalLight(0xffffff, 5)
this.directionalLight.position.set(5, -20, 20)
this.add(this.directionalLight)
this.theme = settings.theme
/* Sounds */
this.music = music
@ -30,20 +30,41 @@ export class TetraScene extends THREE.Scene {
this.lineClearSound = new THREE.Audio(listener)
audioLoader.load('audio/line-clear.ogg', function( buffer ) {
this.lineClearSound.setBuffer(buffer)
this.lineClearSound.setVolume(settings.sfxVolume/100)
}.bind(this))
this.tetrisSound = new THREE.Audio(listener)
audioLoader.load('audio/tetris.ogg', function( buffer ) {
this.tetrisSound.setBuffer(buffer)
this.lineClearSound.setVolume(settings.sfxVolume/100)
this.tetrisSound.setVolume(settings.sfxVolume/100)
this.hardDropSound.setVolume(settings.sfxVolume/100)
}.bind(this))
this.hardDropSound = new THREE.Audio(listener)
audioLoader.load('audio/hard-drop.wav', function( buffer ) {
this.hardDropSound.setBuffer(buffer)
this.hardDropSound.setVolume(settings.sfxVolume/100)
}.bind(this))
}
set theme(theme) {
switch (theme) {
case "Plasma":
this.ambientLight.intensity = 0.6
this.directionalLight.intensity = 5
this.directionalLight.position.set(5, -20, 20)
break
case "Espace":
this.ambientLight.intensity = 20
this.directionalLight.intensity = 10
this.directionalLight.position.set(5, -20, 20)
break
case "Rétro":
this.ambientLight.intensity = 1
this.directionalLight.intensity = 10
this.directionalLight.position.set(19, 120, 200)
break
}
this.vortex.theme = theme
}
update(delta) {
this.vortex.update(delta)
}

View File

@ -1,10 +1,11 @@
import * as THREE from 'three'
import { scheduler } from './scheduler.js'
import { TileMaterial } from './TileMaterial.js'
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
@ -16,9 +17,10 @@ const COLORS = {
S: 0xC8FBA8,
T: 0xedb2ff,
Z: 0xffb8c5,
LOCKING: "white",
LOCKING: 0xffffff,
GHOST: 0x99a9b2,
EDGE: 0x88abe0
EDGE: 0x88abe0,
RETRO: 0xd0d4c1,
}
const TRANSLATION = {
@ -54,52 +56,21 @@ const COLUMNS = 10
const envRenderTarget = new THREE.WebGLCubeRenderTarget(256)
const environnement = envRenderTarget.texture
environnement.type = THREE.HalfFloatType
environnement.camera = new THREE.CubeCamera(1, 1000, envRenderTarget)
environnement.camera.position.set(5, 10, 0)
const environment = envRenderTarget.texture
environment.type = THREE.HalfFloatType
environment.camera = new THREE.CubeCamera(1, 1000, envRenderTarget)
environment.camera.position.set(5, 10, 0)
class InstancedMino extends THREE.InstancedMesh {
constructor(geometry, material, count) {
super(geometry, material, count)
this.instances = new Set()
this.count = 0
}
add(instance) {
this.instances.add(instance)
}
delete(instance) {
this.instances.delete(instance)
}
clear() {
this.instances.clear()
}
update() {
this.count = 0
this.instances.forEach(mino => {
if (mino.parent?.visible) {
this.setColorAt(this.count, mino.color)
this.setMatrixAt(this.count, mino.matrixWorld)
this.count++
}
})
if (this.count) {
this.instanceColor.needsUpdate = true
this.instanceMatrix.needsUpdate = true
}
}
}
const sideMaterial = new THREE.MeshStandardMaterial({
color: 0x222222,
roughness: 0.8,
metalness: 0.8,
})
class Mino extends THREE.Object3D {
static instances = new Set()
static mesh
static {
export class InstancedMino extends THREE.InstancedMesh {
constructor() {
let minoFaceShape = new THREE.Shape()
minoFaceShape.moveTo(.1, .1)
minoFaceShape.lineTo(.1, .9)
@ -115,25 +86,119 @@ class Mino extends THREE.Object3D {
bevelOffset: 0,
bevelSegments: 1
}
let minoGeometry = new THREE.ExtrudeGeometry(minoFaceShape, minoExtrudeSettings)
let minoMaterial = new THREE.MeshStandardMaterial({
envMap: environnement,
side: THREE.DoubleSide,
transparent: true,
opacity: 0.7,
roughness: 0.48,
metalness: 0.67,
})
this.mesh = new InstancedMino(minoGeometry, minoMaterial, 2*ROWS*COLUMNS)
const geometry = new THREE.ExtrudeGeometry(minoFaceShape, minoExtrudeSettings)
super(geometry, undefined, 2*ROWS*COLUMNS)
this.offsets = new Uint8Array(2*this.count)
}
constructor(color) {
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) {
super()
this.color = color
this.velocity = P(50 - 100 * Math.random(), 50 - 100 * Math.random(), 50 - 100 * Math.random())
this.offset = offset
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.angularVelocity = 5 - 10 * Math.random()
this.constructor.mesh.add(this)
this.constructor.instances.add(this)
}
explode(delta) {
@ -142,14 +207,14 @@ class Mino extends THREE.Object3D {
this.rotateOnWorldAxis(this.rotationAngle, delta * this.angularVelocity)
if (Math.sqrt(this.position.x * this.position.x + this.position.z * this.position.z) > 40 || this.position.y < -50) {
this.dispose()
return false
} else {
return true
} else {
return false
}
}
dispose() {
this.constructor.mesh.delete(this)
this.constructor.instances.delete(this)
}
}
@ -164,7 +229,8 @@ class Tetromino extends THREE.Group {
constructor(position) {
super()
if (position) this.position.copy(position)
this.minoesPosition[FACING.NORTH].forEach(() => this.add(new Mino(this.freeColor)))
this.offset = this.offset.clone()
this.minoesPosition[FACING.NORTH].forEach(() => this.add(new Mino(this.freeColor, this.offset)))
this.facing = FACING.NORTH
this.rotatedLast = false
this.rotationPoint4Used = false
@ -184,8 +250,10 @@ class Tetromino extends THREE.Group {
set locking(locking) {
if (locking) {
this.color = this.lockingColor
this.offset.y = 2
} else {
this.color = this.freeColor
this.offset.y = 0
}
}
@ -195,20 +263,19 @@ class Tetromino extends THREE.Group {
canMove(translation, facing=this.facing) {
let testPosition = this.position.clone().add(translation)
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)) {
this.position.add(translation)
this.rotatedLast = rotatedFacing
if (rotatedFacing != undefined) {
this.facing = rotatedFacing
if (rotationPoint == 4) this.rotationPoint4Used = true
}
if (this.canMove(TRANSLATION.DOWN)) {
this.locking = false
this.parent.ghost.copy(this)
this.parent?.ghost.copy(this)
scheduler.clearTimeout(this.onLockDown)
} else {
scheduler.resetTimeout(this.onLockDown, this.lockDelay)
@ -219,16 +286,19 @@ class Tetromino extends THREE.Group {
} else if (translation == TRANSLATION.DOWN) {
this.locked = true
if (!scheduler.timeoutTasks.has(this.onLockDown))
scheduler.setTimeout(this.onLockDown, this.lockDelay)
scheduler.resetTimeout(this.onLockDown, this.lockDelay)
}
}
rotate(rotation) {
let testFacing = (this.facing + rotation) % 4
return this.srs[this.facing][rotation].some(
(translation, rotationPoint) => this.move(translation, testFacing, rotationPoint)
)
return this.srs[this.facing][rotation].some((translation, rotationPoint) => {
if (this.move(translation, testFacing)) {
if (rotationPoint == 4) this.rotationPoint4Used = true
return true
}
})
}
get tSpin() {
@ -237,7 +307,7 @@ class Tetromino extends THREE.Group {
}
Tetromino.prototype.lockingColor = new THREE.Color(COLORS.LOCKING)
// Super Rotation System
// freedom of movement = srs[this.parent.piece.facing][rotation]
// freedom of movement = srs[this.facing][rotation]
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)] },
@ -251,6 +321,7 @@ class Ghost extends Tetromino {
copy(piece) {
this.position.copy(piece.position)
this.minoesPosition = piece.minoesPosition
this.children.forEach(mino => {mino.offset = piece.ghostOffset})
this.facing = piece.facing
this.visible = true
while (this.canMove(TRANSLATION.DOWN)) this.position.y--
@ -260,6 +331,7 @@ Ghost.prototype.freeColor = new THREE.Color(COLORS.GHOST)
Ghost.prototype.minoesPosition = [
[P(0, 0, 0), P(0, 0, 0), P(0, 0, 0), P(0, 0, 0)],
]
Ghost.prototype.offset = P(0, 1)
class I extends Tetromino { }
@ -276,6 +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)] },
]
I.prototype.freeColor = new THREE.Color(COLORS.I)
I.prototype.offset = P(0, 0)
I.prototype.ghostOffset = P(0, 1)
class J extends Tetromino { }
J.prototype.minoesPosition = [
@ -285,8 +359,11 @@ J.prototype.minoesPosition = [
[P(0, 1), P(-1, -1), P(0, 0), P(0, -1)],
]
J.prototype.freeColor = new THREE.Color(COLORS.J)
J.prototype.offset = P(1, 0)
J.prototype.ghostOffset = P(1, 1)
class L extends Tetromino { }
class L extends Tetromino {
}
L.prototype.minoesPosition = [
[P(-1, 0), P(0, 0), P(1, 0), P(1, 1)],
[P(0, 1), P(0, 0), P(0, -1), P(1, -1)],
@ -294,6 +371,8 @@ L.prototype.minoesPosition = [
[P(0, 1), P(0, 0), P(0, -1), P(-1, 1)],
]
L.prototype.freeColor = new THREE.Color(COLORS.L)
L.prototype.offset = P(2, 0)
L.prototype.ghostOffset = P(2, 1)
class O extends Tetromino { }
O.prototype.minoesPosition = [
@ -303,6 +382,8 @@ O.prototype.srs = [
{ [ROTATION.CW]: [], [ROTATION.CCW]: [] }
]
O.prototype.freeColor = new THREE.Color(COLORS.O)
O.prototype.offset = P(3, 0)
O.prototype.ghostOffset = P(3, 1)
class S extends Tetromino { }
S.prototype.minoesPosition = [
@ -312,6 +393,8 @@ S.prototype.minoesPosition = [
[P(-1, 1), P(0, 0), P(-1, 0), P(0, -1)],
]
S.prototype.freeColor = new THREE.Color(COLORS.S)
S.prototype.offset = P(4, 0)
S.prototype.ghostOffset = P(4, 1)
class T extends Tetromino {
get tSpin() {
@ -339,6 +422,8 @@ T.prototype.tSlots = [
[P(-1, -1), P(-1, 1), P(1, 1), P(1, -1)],
]
T.prototype.freeColor = new THREE.Color(COLORS.T)
T.prototype.offset = P(5, 0)
T.prototype.ghostOffset = P(5, 1)
class Z extends Tetromino { }
Z.prototype.minoesPosition = [
@ -348,18 +433,20 @@ Z.prototype.minoesPosition = [
[P(0, 1), P(-1, 0), P(0, 0), P(-1, -1)]
]
Z.prototype.freeColor = new THREE.Color(COLORS.Z)
Z.prototype.offset = P(6, 0)
Z.prototype.ghostOffset = P(6, 1)
class Playfield extends THREE.Group {
constructor() {
constructor(loadingManager) {
super()
//this.visible = false
const edgeMaterial = new THREE.MeshStandardMaterial({
color: COLORS.EDGE,
envMap: environnement,
envMap: environment,
transparent: true,
opacity: 0.2,
opacity: 0.3,
roughness: 0.1,
metalness: 0.67,
})
@ -373,14 +460,56 @@ class Playfield extends THREE.Group {
.lineTo(COLUMNS + .3, -.3)
.lineTo(-.3, -.3)
.moveTo(-.3, SKYLINE)
const edge = new THREE.Mesh(
this.edge = new THREE.Mesh(
new THREE.ExtrudeGeometry(edgeShape, {
depth: 1,
bevelEnabled: false,
}),
edgeMaterial
)
this.add(edge)
this.add(this.edge)
const retroEdgeShape = new THREE.Shape()
.moveTo(-1, SKYLINE)
.lineTo(0, SKYLINE)
.lineTo(0, 0)
.lineTo(COLUMNS, 0)
.lineTo(COLUMNS, SKYLINE)
.lineTo(COLUMNS + 1, SKYLINE)
.lineTo(COLUMNS + 1, -1/3)
.lineTo(-1, -1/3)
.moveTo(-1, SKYLINE)
const retroEdgeTexture = new THREE.TextureLoader(loadingManager).load("images/edge.png", (texture) => {
texture.wrapS = THREE.RepeatWrapping
texture.wrapT = THREE.RepeatWrapping
})
const retroEdgeMaterial = new THREE.MeshStandardMaterial({
color: COLORS.RETRO,
map: retroEdgeTexture,
bumpMap: retroEdgeTexture,
bumpScale: 1.5,
roughness: 0.25,
metalness: 0.9,
})
this.retroEdge = new THREE.Mesh(
new THREE.ExtrudeGeometry(retroEdgeShape, {
depth: 1,
bevelEnabled: false,
}),
[retroEdgeMaterial, sideMaterial, sideMaterial, sideMaterial, sideMaterial, sideMaterial],
)
const back = new THREE.Mesh(
new THREE.PlaneGeometry(COLUMNS, SKYLINE),
new THREE.MeshStandardMaterial({
color: 0xc5d0a1,
roughness: 0.9,
metalness: 0.9,
})
)
back.position.set(COLUMNS/2, SKYLINE/2)
this.retroEdge.add(back)
this.retroEdge.visible = false
this.add(this.retroEdge)
const positionKF = new THREE.VectorKeyframeTrack('.position', [0, 1, 2], [0, 0, 0, 0, -0.2, 0, 0, 0, 0])
const clip = new THREE.AnimationClip('HardDrop', 3, [positionKF])
@ -403,7 +532,7 @@ class Playfield extends THREE.Group {
this.ghost.visible = false
this.add(this.ghost)
this.visible = true
// this.visible = true
}
cellIsEmpty(p) {
@ -414,6 +543,7 @@ class Playfield extends THREE.Group {
set piece(piece) {
if (piece) {
this.remove(this.piece)
this.add(piece)
piece.position.set(4, SKYLINE)
this.ghost.copy(piece)
@ -459,7 +589,10 @@ class Playfield extends THREE.Group {
updateFreedMinoes(delta) {
this.freedMinoes.forEach(mino => {
if (mino.explode(delta)) this.freedMinoes.delete(this)
if (mino.explode(delta)) {
this.remove(mino)
this.freedMinoes.delete(mino)
}
})
}
@ -473,11 +606,12 @@ class Playfield extends THREE.Group {
class HoldQueue extends THREE.Group {
constructor() {
super()
this.position.set(-4, SKYLINE - 2)
this.position.set(-5, SKYLINE - 2)
}
set piece(piece) {
if(piece) {
this.remove(this.piece)
piece.holdEnabled = false
piece.locking = false
piece.position.set(0, 0)
@ -496,7 +630,7 @@ class HoldQueue extends THREE.Group {
class NextQueue extends THREE.Group {
constructor() {
super()
this.position.set(13, SKYLINE - 2)
this.position.set(14, SKYLINE - 2)
}
init() {
@ -506,6 +640,7 @@ class NextQueue extends THREE.Group {
shift() {
let fistPiece = this.children.shift()
this.remove(fistPiece)
this.add(new Tetromino.random())
this.positions.forEach((position, i) => this.children[i].position.copy(position))
return fistPiece
@ -515,4 +650,4 @@ class NextQueue extends THREE.Group {
NextQueue.prototype.positions = [P(0, 0), P(0, -3), P(0, -6), P(0, -9), P(0, -12), P(0, -15), P(0, -18)]
export { T_SPIN, FACING, TRANSLATION, ROTATION, COLORS, environnement, Mino, Tetromino, Playfield, HoldQueue, NextQueue }
export { T_SPIN, FACING, TRANSLATION, ROTATION, COLORS, environment, Mino, Tetromino, Playfield, HoldQueue, NextQueue }

93
jsm/TileMaterial.js Normal file
View File

@ -0,0 +1,93 @@
import { MeshStandardMaterial, Texture, Vector2 } from 'three';
export class TileMaterial extends MeshStandardMaterial {
constructor(params, tileSizeX, tileSizeY) {
super(params);
this.tileSize = { value: new Vector2(tileSizeX / this.map.image.width, tileSizeY / this.map.image.height) };
}
onBeforeCompile(shader) {
shader.uniforms.tileSize = this.tileSize
shader.vertexShader = shader.vertexShader.replace(
`void main() {`,
`varying vec2 vUv;
varying vec2 vOffset;
attribute vec2 offset;
void main() {
vUv = uv;
vOffset = offset;
gl_Position = projectionMatrix * modelViewMatrix * instanceMatrix * vec4(position, 1.0);`
)
shader.fragmentShader = `varying vec2 vUv;
varying vec2 vOffset;
//uniform sampler2D map;
uniform vec2 tileSize;
varying vec2 vBumpMapUvOffset;
` + shader.fragmentShader
shader.fragmentShader = shader.fragmentShader.replace(
`#include <map_fragment>`,
`#ifdef USE_MAP
vec4 sampledDiffuseColor = texture2D(map, vUv * tileSize + vOffset * tileSize);
#ifdef DECODE_VIDEO_TEXTURE
// use inline sRGB decode until browsers properly support SRGB8_ALPHA8 with video textures (#26516)
sampledDiffuseColor = vec4( mix( pow( sampledDiffuseColor.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), sampledDiffuseColor.rgb * 0.0773993808, vec3( lessThanEqual( sampledDiffuseColor.rgb, vec3( 0.04045 ) ) ) ), sampledDiffuseColor.w );
#endif
diffuseColor *= sampledDiffuseColor;
#endif`
)
shader.fragmentShader = shader.fragmentShader.replace(
`#include <bumpmap_pars_fragment>`,
`#ifdef USE_BUMPMAP
uniform sampler2D bumpMap;
uniform float bumpScale;
// Bump Mapping Unparametrized Surfaces on the GPU by Morten S. Mikkelsen
// https://mmikk.github.io/papers3d/mm_sfgrad_bump.pdf
// Evaluate the derivative of the height w.r.t. screen-space using forward differencing (listing 2)
vec2 dHdxy_fwd() {
vec2 vBumpMapUvOffset = vBumpMapUv * tileSize + vOffset * tileSize;
vec2 dSTdx = dFdx( vBumpMapUvOffset );
vec2 dSTdy = dFdy( vBumpMapUvOffset );
float Hll = bumpScale * texture2D( bumpMap, vBumpMapUvOffset ).x;
float dBx = bumpScale * texture2D( bumpMap, vBumpMapUvOffset + dSTdx ).x - Hll;
float dBy = bumpScale * texture2D( bumpMap, vBumpMapUvOffset + dSTdy ).x - Hll;
return vec2( dBx, dBy );
}
vec3 perturbNormalArb( vec3 surf_pos, vec3 surf_norm, vec2 dHdxy, float faceDirection ) {
// normalize is done to ensure that the bump map looks the same regardless of the texture's scale
vec3 vSigmaX = normalize( dFdx( surf_pos.xyz ) );
vec3 vSigmaY = normalize( dFdy( surf_pos.xyz ) );
vec3 vN = surf_norm; // normalized
vec3 R1 = cross( vSigmaY, vN );
vec3 R2 = cross( vN, vSigmaX );
float fDet = dot( vSigmaX, R1 ) * faceDirection;
vec3 vGrad = sign( fDet ) * ( dHdxy.x * R1 + dHdxy.y * R2 );
return normalize( abs( fDet ) * surf_norm - vGrad );
}
#endif`
)
}
}

View File

@ -5,6 +5,8 @@ export class Vortex extends THREE.Group {
constructor(loadingManager) {
super()
this.loadingManager = loadingManager
this.globalRotation = 0.028
this.darkTextureRotation = 0.006
@ -13,21 +15,13 @@ export class Vortex extends THREE.Group {
this.colorFullTextureRotation = 0.006
this.colorFullMoveForward = 0.025
const commonCylinderGeometry = new THREE.CylinderGeometry(35, 35, 500, 12, 1, true)
this.background = "Plasma"
const commonCylinderGeometry = new THREE.CylinderGeometry(35, 35, 1000, 12, 1, true)
this.darkCylinder = new THREE.Mesh(
commonCylinderGeometry,
new THREE.MeshLambertMaterial({
side: THREE.BackSide,
map: new THREE.TextureLoader(loadingManager).load("./images/plasma.jpg", (texture) => {
texture.wrapS = THREE.RepeatWrapping
texture.wrapT = THREE.MirroredRepeatWrapping
texture.repeat.set(1, 1)
}),
blending: THREE.AdditiveBlending,
opacity: 0.17
})
)
this.add(this.darkCylinder)
@ -36,27 +30,87 @@ export class Vortex extends THREE.Group {
commonCylinderGeometry,
new THREE.MeshBasicMaterial({
side: THREE.BackSide,
map: new THREE.TextureLoader(loadingManager).load("./images/plasma2.jpg", (texture) => {
texture.wrapS = THREE.RepeatWrapping
texture.wrapT = THREE.MirroredRepeatWrapping
texture.repeat.set(2, 1)
}),
blending: THREE.AdditiveBlending,
opacity: 0.7
})
)
this.add(this.colorFullCylinder)
this.position.set(5, 10, -10)
this.position.set(5, 100, -10)
}
set theme(theme) {
switch (theme) {
case "Plasma":
new THREE.TextureLoader(this.loadingManager).load("./images/plasma.jpg", texture => {
texture.wrapS = THREE.RepeatWrapping
texture.wrapT = THREE.MirroredRepeatWrapping
texture.repeat.set(1, 2)
this.darkCylinder.material.map = texture
})
this.darkCylinder.material.opacity = 0.17
new THREE.TextureLoader(this.loadingManager).load("./images/plasma2.jpg", texture => {
texture.wrapS = THREE.RepeatWrapping
texture.wrapT = THREE.MirroredRepeatWrapping
texture.repeat.set(2, 2)
this.colorFullCylinder.material.map = texture
})
this.colorFullCylinder.material.opacity = 0.5
this.globalRotation = 0.028
this.darkTextureRotation = 0.005
this.darkMoveForward = 0.009
this.colorFullTextureRotation = 0.006
this.colorFullMoveForward = 0.025
this.visible = true
break
case "Espace":
new THREE.TextureLoader(this.loadingManager).load("./images/dark.jpg", texture => {
texture.wrapS = THREE.RepeatWrapping
texture.wrapT = THREE.MirroredRepeatWrapping
texture.repeat.set(2, 4)
this.darkCylinder.material.map = texture
})
this.darkCylinder.material.opacity = 0.08
new THREE.TextureLoader(this.loadingManager).load("./images/colorfull.jpg", texture => {
texture.wrapS = THREE.RepeatWrapping
texture.wrapT = THREE.MirroredRepeatWrapping
texture.repeat.set(2, 2)
this.colorFullCylinder.material.map = texture
})
this.colorFullCylinder.material.opacity = 0.15
this.globalRotation = 0.028
this.darkTextureRotation = 0.006
this.darkMoveForward = 0.03
this.colorFullTextureRotation = 0.006
this.colorFullMoveForward = 0.012
this.visible = true
break
case "Rétro":
this.visible = false
break
}
}
update(delta) {
this.rotation.y += this.globalRotation * delta
this.darkCylinder.material.map.offset.y += this.darkMoveForward * delta
this.darkCylinder.material.map.offset.x += this.darkTextureRotation * delta
this.colorFullCylinder.material.map.offset.y += this.colorFullMoveForward * delta
this.colorFullCylinder.material.map.offset.x += this.colorFullTextureRotation * delta
if (this.visible) {
this.rotation.y += this.globalRotation * delta
if (this.darkCylinder.material.map) {
this.darkCylinder.material.map.offset.y += this.darkMoveForward * delta
this.darkCylinder.material.map.offset.x += this.darkTextureRotation * delta
}
if (this.colorFullCylinder.material.map) {
this.colorFullCylinder.material.map.offset.y += this.colorFullMoveForward * delta
this.colorFullCylinder.material.map.offset.x += this.colorFullTextureRotation * delta
}
}
}
}

View File

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

View File

@ -1,279 +0,0 @@
@-webkit-keyframes outerRotate1 {
0% {
transform: translate(-50%, -50%) rotate(0);
}
100% {
transform: translate(-50%, -50%) rotate(360deg);
}
}
@-moz-keyframes outerRotate1 {
0% {
transform: translate(-50%, -50%) rotate(0);
}
100% {
transform: translate(-50%, -50%) rotate(360deg);
}
}
@-o-keyframes outerRotate1 {
0% {
transform: translate(-50%, -50%) rotate(0);
}
100% {
transform: translate(-50%, -50%) rotate(360deg);
}
}
@keyframes outerRotate1 {
0% {
transform: translate(-50%, -50%) rotate(0);
}
100% {
transform: translate(-50%, -50%) rotate(360deg);
}
}
@-webkit-keyframes outerRotate2 {
0% {
transform: translate(-50%, -50%) rotate(0);
}
100% {
transform: translate(-50%, -50%) rotate(-360deg);
}
}
@-moz-keyframes outerRotate2 {
0% {
transform: translate(-50%, -50%) rotate(0);
}
100% {
transform: translate(-50%, -50%) rotate(-360deg);
}
}
@-o-keyframes outerRotate2 {
0% {
transform: translate(-50%, -50%) rotate(0);
}
100% {
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;
}