import * as THREE from 'three'
import { scheduler } from './jsm/scheduler.js'
import { TRANSLATION, ROTATION, environment, 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 { TetraScene } from './jsm/TetraScene.js'
HTMLElement.prototype.addNewChild = function (tag, properties) {
let child = document.createElement(tag)
for (let key in properties) {
child[key] = properties[key]
}
this.appendChild(child)
}
/* Game logic */
let game = {
playing: false,
start: function() {
stats.init()
gui.startButton.hide()
gui.settings.close()
gui.stats.show()
Mino.meshes.clear()
nextQueue.init()
holdQueue.piece = undefined
holdQueue.clear()
playfield.init()
scene.music.currentTime = 0
this.playing = true
stats.clock.start()
stats.level = settings.startLevel
this.resume()
},
resume: function() {
document.onkeydown = onkeydown
document.onkeyup = onkeyup
window.onblur = game.pause
gui.settings.domElement.onclick = game.pause
document.body.classList.remove("pause")
gui.resumeButton.hide()
gui.pauseButton.show()
stats.clock.start()
stats.clock.elapsedTime = stats.elapsedTime
if (settings.musicVolume) scene.music.play()
if (playfield.piece) {
scheduler.setInterval(game.fall, stats.fallPeriod)
} else {
this.generate()
}
},
generate: function(nextPiece=nextQueue.shift()) {
nextPiece.lockDelay = stats.lockDelay
playfield.piece = nextPiece
playfield.piece.onLockDown = game.lockDown
if (playfield.piece.canMove(TRANSLATION.NONE)) {
scheduler.setInterval(game.fall, stats.fallPeriod)
} else {
game.over() // block out
}
},
fall: function() {
playfield.piece.move(TRANSLATION.DOWN)
},
lockDown: function() {
scheduler.clearTimeout(game.lockDown)
scheduler.clearInterval(game.fall)
if (playfield.lock(playfield.piece)) {
let tSpin = playfield.piece.tSpin
let nbClearedLines = playfield.clearLines()
stats.lockDown(nbClearedLines, tSpin)
if (settings.sfxVolume) {
if (nbClearedLines == 4 || (tSpin && nbClearedLines)) {
playSound(scene.tetrisSound, stats.combo)
} else if (nbClearedLines || tSpin) {
playSound(scene.lineClearSound, stats.combo)
}
}
game.generate()
} else {
game.over() // lock out
}
},
pause: function() {
gui.settings.domElement.onclick = null
stats.elapsedTime = stats.clock.elapsedTime
stats.clock.stop()
scheduler.clearInterval(game.fall)
scheduler.clearTimeout(game.lockDown)
scheduler.clearTimeout(repeat)
scheduler.clearInterval(autorepeat)
scene.music.pause()
document.onkeydown = resumeOnKeyDown
document.onkeyup = null
window.onblur = null
pauseSpan.onfocus = game.resume
document.body.classList.add("pause")
gui.pauseButton.hide()
gui.resumeButton.show()
},
over: function() {
playfield.piece.locking = false
document.onkeydown = null
window.onblur = null
renderer.domElement.onfocus = null
gui.settings.domElement.onfocus = null
game.playing = false
scene.music.pause()
stats.clock.stop()
messagesSpan.addNewChild("div", { className: "show-level-animation", innerHTML: `
GAME
OVER
` })
gui.pauseButton.hide()
gui.startButton.name("Rejouer")
gui.startButton.show()
},
}
function playSound(sound, note=0) {
sound.stop()
sound.currentTime = 0
sound.playbackRate = Math.pow(5/4, note)
sound.play()
}
/* Handle player inputs */
let playerActions = {
moveLeft: () => playfield.piece.move(TRANSLATION.LEFT),
moveRight: () => playfield.piece.move(TRANSLATION.RIGHT),
rotateCW: () => playfield.piece.rotate(ROTATION.CW),
rotateCCW: () => playfield.piece.rotate(ROTATION.CCW),
softDrop: function () {
if (playfield.piece.move(TRANSLATION.DOWN)) stats.score++
},
hardDrop: function () {
scheduler.clearTimeout(game.lockDown)
if (settings.sfxVolume) {
scene.hardDropSound.stop()
scene.hardDropSound.play()
}
while (playfield.piece.move(TRANSLATION.DOWN)) stats.score += 2
game.lockDown()
playfield.hardDropAnimation.reset()
playfield.hardDropAnimation.play()
},
hold: function () {
if (playfield.piece.holdEnabled) {
scheduler.clearInterval(game.fall)
scheduler.clearTimeout(game.lockDown)
let heldpiece = holdQueue.piece
holdQueue.piece = playfield.piece
game.generate(heldpiece)
}
},
pause: game.pause,
}
const REPEATABLE_ACTIONS = [
playerActions.moveLeft,
playerActions.moveRight,
playerActions.softDrop
]
let pressedKeys = new Set()
let actionsQueue = []
function onkeydown(event) {
let key = event.key
if (key in settings.action) {
event.preventDefault()
if (!pressedKeys.has(key)) {
pressedKeys.add(key)
let action = playerActions[settings.action[key]]
action()
if (REPEATABLE_ACTIONS.includes(action)) {
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)
}
}
}
}
function repeat() {
if (actionsQueue.length) {
actionsQueue[0]()
scheduler.setInterval(autorepeat, settings.arrDelay)
}
}
function autorepeat() {
if (actionsQueue.length) {
actionsQueue[0]()
} else {
scheduler.clearInterval(autorepeat)
}
}
function onkeyup(event) {
let key = event.key
if (key in settings.action) {
event.preventDefault()
pressedKeys.delete(key)
let action = playerActions[settings.action[key]]
if (actionsQueue.includes(action)) {
actionsQueue.splice(actionsQueue.indexOf(action), 1)
if (!actionsQueue.length) {
scheduler.clearTimeout(repeat)
scheduler.clearInterval(autorepeat)
}
}
}
}
function resumeOnKeyDown(event) {
let key = event.key
if(playerActions[settings.action[key]] == playerActions.pause) {
event.preventDefault()
game.resume()
}
}
/* Scene */
const renderer = new THREE.WebGLRenderer({
powerPreference: "high-performance",
antialias: true,
stencil: false
})
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.setClearColor(0x000000, 10)
renderer.toneMapping = THREE.ACESFilmicToneMapping
document.body.appendChild(renderer.domElement)
renderer.domElement.tabIndex = 1
let loadingManager = new THREE.LoadingManager(
function() {
loadingDiv.style.display = "none"
gui.startButton.show()
renderer.setAnimationLoop(animate)
},
function (url, itemsLoaded, itemsTotal) {
loadingPercent.innerText = Math.floor(100 * itemsLoaded / itemsTotal) + '%'
},
function (url) {
loadingPercent.innerText = "Erreur"
}
)
loadingManager.onStart = function (url, itemsLoaded, itemsTotal) {
loadingPercent.innerText = "0%"
loadingDiv.style.display = "block"
}
const stats = new Stats()
const settings = new Settings()
const scene = new TetraScene(settings, loadingManager)
const controls = new TetraControls(scene.camera, renderer.domElement)
scene.add(Mino.meshes)
const holdQueue = new HoldQueue()
scene.add(holdQueue)
const playfield = new Playfield(loadingManager)
scene.add(playfield)
const nextQueue = new NextQueue()
scene.add(nextQueue)
const gui = new TetraGUI(game, settings, stats, scene, controls, playfield)
gui.load()
messagesSpan.onanimationend = function (event) {
event.target.remove()
}
const clock = new THREE.Clock()
function animate() {
const delta = clock.getDelta()
scene.updateMatrixWorld()
scene.update(delta)
playfield.update(delta)
Mino.meshes.update()
controls.update()
renderer.render(scene, scene.camera)
environment.camera.update(renderer, scene)
gui.update()
}
window.addEventListener("resize", () => {
renderer.setSize(window.innerWidth, window.innerHeight)
scene.camera.aspect = window.innerWidth / window.innerHeight
scene.camera.updateProjectionMatrix()
})
window.onbeforeunload = function (event) {
gui.save()
localStorage["teTraHighScore"] = stats.highScore
return !game.playing
}