273 lines
6.7 KiB
Plaintext
273 lines
6.7 KiB
Plaintext
var draw = use('draw2d')
|
||
var input = use('input')
|
||
var config = use('config')
|
||
var color = use('color')
|
||
var random = use('random')
|
||
|
||
prosperon.camera.transform.pos = [0,0]
|
||
|
||
// Board constants
|
||
var COLS = 10, ROWS = 20
|
||
var TILE = 6 // each cell is 6x6
|
||
|
||
// Board storage (2D), each cell is either 0 or a [r,g,b,a] color
|
||
var board = []
|
||
|
||
// Gravity timing
|
||
var baseGravity = 0.8 // seconds between drops at level 0
|
||
var gravityTimer = 0
|
||
|
||
// Current piece & position
|
||
var piece = null
|
||
var pieceX = 0
|
||
var pieceY = 0
|
||
|
||
// Next piece
|
||
var nextPiece = null
|
||
|
||
// Score/lines/level
|
||
var score = 0
|
||
var linesCleared = 0
|
||
var level = 0
|
||
|
||
// Rotation lock to prevent spinning with W
|
||
var rotateHeld = false
|
||
var gameOver = false
|
||
|
||
// Horizontal movement gating
|
||
var hMoveTimer = 0
|
||
var hDelay = 0.2 // delay before repeated moves begin
|
||
var hRepeat = 0.05 // time between repeated moves
|
||
var prevLeft = false
|
||
var prevRight = false
|
||
|
||
// Tetrimino definitions
|
||
var SHAPES = {
|
||
I: { color:[0,1,1,1], blocks:[[0,0],[1,0],[2,0],[3,0]] },
|
||
O: { color:[1,1,0,1], blocks:[[0,0],[1,0],[0,1],[1,1]] },
|
||
T: { color:[1,0,1,1], blocks:[[0,0],[1,0],[2,0],[1,1]] },
|
||
S: { color:[0,1,0,1], blocks:[[1,0],[2,0],[0,1],[1,1]] },
|
||
Z: { color:[1,0,0,1], blocks:[[0,0],[1,0],[1,1],[2,1]] },
|
||
J: { color:[0,0,1,1], blocks:[[0,0],[0,1],[1,1],[2,1]] },
|
||
L: { color:[1,0.5,0,1], blocks:[[2,0],[0,1],[1,1],[2,1]] }
|
||
}
|
||
var shapeKeys = array(SHAPES)
|
||
|
||
// Initialize board with empty (0)
|
||
function initBoard() {
|
||
board = []
|
||
for (var r=0; r<ROWS; r++) {
|
||
var row = []
|
||
for (var c=0; c<COLS; c++) row.push(0)
|
||
board.push(row)
|
||
}
|
||
}
|
||
initBoard()
|
||
|
||
function randomShape() {
|
||
var key = shapeKeys[floor(random.random()*shapeKeys.length)]
|
||
// Make a copy of the shape’s blocks
|
||
return {
|
||
type: key,
|
||
color: SHAPES[key].color,
|
||
blocks: array(SHAPES[key].blocks, b => [b[0], b[1]])
|
||
}
|
||
}
|
||
|
||
function spawnPiece() {
|
||
piece = nextPiece || randomShape()
|
||
nextPiece = randomShape()
|
||
pieceX = 3
|
||
pieceY = 0
|
||
// Collision on spawn => game over
|
||
if (collides(pieceX, pieceY, piece.blocks)) gameOver = true
|
||
}
|
||
|
||
function collides(px, py, blocks) {
|
||
for (var i=0; i<blocks.length; i++) {
|
||
var x = px + blocks[i][0]
|
||
var y = py + blocks[i][1]
|
||
if (x<0 || x>=COLS || y<0 || y>=ROWS) return true
|
||
if (y>=0 && board[y][x]) return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
// Lock piece into board
|
||
function lockPiece() {
|
||
for (var i=0; i<piece.blocks.length; i++) {
|
||
var x = pieceX + piece.blocks[i][0]
|
||
var y = pieceY + piece.blocks[i][1]
|
||
if (y>=0) board[y][x] = piece.color
|
||
}
|
||
}
|
||
|
||
// Rotate 90° clockwise
|
||
function rotate(blocks) {
|
||
// (x,y) => (y,-x)
|
||
for (var i=0; i<blocks.length; i++) {
|
||
var x = blocks[i][0]
|
||
var y = blocks[i][1]
|
||
blocks[i][0] = y
|
||
blocks[i][1] = -x
|
||
}
|
||
}
|
||
|
||
function clearLines() {
|
||
var lines = 0
|
||
for (var r=ROWS-1; r>=0;) {
|
||
if (every(board[r], cell => cell)) {
|
||
lines++
|
||
// remove row
|
||
board.splice(r,1)
|
||
// add empty row on top
|
||
var newRow = []
|
||
for (var c=0; c<COLS; c++) newRow.push(0)
|
||
board.unshift(newRow)
|
||
} else {
|
||
r--
|
||
}
|
||
}
|
||
// Score
|
||
if (lines==1) score += 100
|
||
else if (lines==2) score += 300
|
||
else if (lines==3) score += 500
|
||
else if (lines==4) score += 800
|
||
linesCleared += lines
|
||
level = floor(linesCleared/10)
|
||
}
|
||
|
||
function placePiece() {
|
||
lockPiece()
|
||
clearLines()
|
||
spawnPiece()
|
||
}
|
||
|
||
// Hard drop
|
||
function hardDrop() {
|
||
while(!collides(pieceX, pieceY+1, piece.blocks)) pieceY++
|
||
placePiece()
|
||
}
|
||
|
||
spawnPiece()
|
||
|
||
this.update = function(dt) {
|
||
if (gameOver) return
|
||
|
||
// ======= Horizontal Movement Gate =======
|
||
var leftPressed = input.keyboard.down('a')
|
||
var rightPressed = input.keyboard.down('d')
|
||
var horizontalMove = 0
|
||
|
||
// If user just pressed A, move once & start gating
|
||
if (leftPressed && !prevLeft) {
|
||
horizontalMove = -1
|
||
hMoveTimer = hDelay
|
||
}
|
||
// If user is holding A & the timer is up, move again, then reset timer to repeat
|
||
else if (leftPressed && hMoveTimer <= 0) {
|
||
horizontalMove = -1
|
||
hMoveTimer = hRepeat
|
||
}
|
||
|
||
// Same logic for D
|
||
if (rightPressed && !prevRight) {
|
||
horizontalMove = 1
|
||
hMoveTimer = hDelay
|
||
} else if (rightPressed && hMoveTimer <= 0) {
|
||
horizontalMove = 1
|
||
hMoveTimer = hRepeat
|
||
}
|
||
|
||
// Move horizontally if it doesn't collide
|
||
if (horizontalMove < 0 && !collides(pieceX-1, pieceY, piece.blocks)) pieceX--
|
||
else if (horizontalMove > 0 && !collides(pieceX+1, pieceY, piece.blocks)) pieceX++
|
||
|
||
// If neither A nor D is pressed, reset the timer so next press is immediate
|
||
if (!leftPressed && !rightPressed) {
|
||
hMoveTimer = 0
|
||
}
|
||
|
||
// Decrement horizontal timer
|
||
hMoveTimer -= dt
|
||
prevLeft = leftPressed
|
||
prevRight = rightPressed
|
||
// ======= End Horizontal Movement Gate =======
|
||
|
||
// Rotate with W (once per press, no spinning)
|
||
if (input.keyboard.down('w')) {
|
||
if (!rotateHeld) {
|
||
rotateHeld = true
|
||
var test = array(piece.blocks, b => [b[0], b[1]])
|
||
rotate(test)
|
||
if (!collides(pieceX, pieceY, test)) piece.blocks = test
|
||
}
|
||
} else {
|
||
rotateHeld = false
|
||
}
|
||
|
||
// Soft drop if S is held (accelerates gravity)
|
||
var fallSpeed = input.keyboard.down('s') ? 10 : 1
|
||
|
||
// Gravity
|
||
gravityTimer += dt * fallSpeed
|
||
var dropInterval = max(0.1, baseGravity - level*0.05)
|
||
if (gravityTimer >= dropInterval) {
|
||
gravityTimer = 0
|
||
if (!collides(pieceX, pieceY+1, piece.blocks)) {
|
||
pieceY++
|
||
} else {
|
||
placePiece()
|
||
}
|
||
}
|
||
|
||
// Hard drop if space is held
|
||
if (input.keyboard.down('space')) {
|
||
// hardDrop()
|
||
}
|
||
}
|
||
|
||
this.hud = function() {
|
||
// Clear screen
|
||
draw.rectangle({x:0, y:0, width:config.width, height:config.height}, [0,0,0,1])
|
||
|
||
// Draw board
|
||
for (var r=0; r<ROWS; r++) {
|
||
for (var c=0; c<COLS; c++) {
|
||
var cell = board[r][c]
|
||
if (!cell) continue
|
||
draw.rectangle({x:c*TILE, y:(ROWS-1-r)*TILE, width:TILE, height:TILE}, cell)
|
||
}
|
||
}
|
||
|
||
// Draw falling piece
|
||
if (!gameOver && piece) {
|
||
for (var i=0; i<piece.blocks.length; i++) {
|
||
var x = pieceX + piece.blocks[i][0]
|
||
var y = pieceY + piece.blocks[i][1]
|
||
draw.rectangle({x:x*TILE, y:(ROWS-1-y)*TILE, width:TILE, height:TILE}, piece.color)
|
||
}
|
||
}
|
||
|
||
// Next piece window
|
||
draw.text("Next", {x:70, y:5, width:50, height:10}, null, 0, color.white)
|
||
if (nextPiece) {
|
||
for (var i=0; i<nextPiece.blocks.length; i++) {
|
||
var nx = nextPiece.blocks[i][0]
|
||
var ny = nextPiece.blocks[i][1]
|
||
var dx = 12 + nx
|
||
var dy = 16 - ny
|
||
draw.rectangle({x:dx*TILE, y:(ROWS-1-dy)*TILE, width:TILE, height:TILE}, nextPiece.color)
|
||
}
|
||
}
|
||
|
||
// Score & Level
|
||
var info = "Score: " + score + "\nLines: " + linesCleared + "\nLevel: " + level
|
||
draw.text(info, {x:70, y:30, width:90, height:50}, null, 0, color.white)
|
||
|
||
if (gameOver) {
|
||
draw.text("GAME OVER", {x:10, y:config.height*0.5-5, width:config.width-20, height:20}, null, 0, color.red)
|
||
}
|
||
}
|
||
|