mirror of
https://github.com/cmclark00/mintris.git
synced 2025-05-18 12:15:21 +01:00
Fix board positioning and block alignment issues. Ensure pieces reach bottom and columns have equal width.
This commit is contained in:
parent
5016b6a2f3
commit
7ba18e1a4a
2 changed files with 192 additions and 33 deletions
|
@ -62,6 +62,7 @@ class GameView @JvmOverloads constructor(
|
|||
isAntiAlias = true
|
||||
strokeWidth = 1f
|
||||
style = Paint.Style.STROKE
|
||||
maskFilter = null // Ensure no blur effect on grid lines
|
||||
}
|
||||
|
||||
private val glowPaint = Paint().apply {
|
||||
|
@ -90,6 +91,15 @@ class GameView @JvmOverloads constructor(
|
|||
maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.OUTER)
|
||||
}
|
||||
|
||||
// Add a new paint for the pulse effect
|
||||
private val pulsePaint = Paint().apply {
|
||||
color = Color.CYAN
|
||||
alpha = 255
|
||||
isAntiAlias = true
|
||||
style = Paint.Style.FILL
|
||||
maskFilter = BlurMaskFilter(32f, BlurMaskFilter.Blur.OUTER) // Increased from 16f to 32f
|
||||
}
|
||||
|
||||
// Pre-allocate paint objects to avoid GC
|
||||
private val tmpPaint = Paint()
|
||||
|
||||
|
@ -131,6 +141,12 @@ class GameView @JvmOverloads constructor(
|
|||
var onPieceMove: (() -> Unit)? = null // New callback for piece movement
|
||||
var onPieceLock: (() -> Unit)? = null // New callback for piece locking
|
||||
|
||||
// Animation state
|
||||
private var pulseAnimator: ValueAnimator? = null
|
||||
private var pulseAlpha = 0f
|
||||
private var isPulsing = false
|
||||
private var linesToPulse = mutableListOf<Int>() // Track which lines are being cleared
|
||||
|
||||
init {
|
||||
// Start with paused state
|
||||
pause()
|
||||
|
@ -138,10 +154,15 @@ class GameView @JvmOverloads constructor(
|
|||
// Connect our callbacks to the GameBoard
|
||||
gameBoard.onPieceMove = { onPieceMove?.invoke() }
|
||||
gameBoard.onPieceLock = { onPieceLock?.invoke() }
|
||||
gameBoard.onLineClear = { lineCount ->
|
||||
gameBoard.onLineClear = { lineCount, clearedLines ->
|
||||
android.util.Log.d("GameView", "Received line clear from GameBoard: $lineCount lines")
|
||||
try {
|
||||
onLineClear?.invoke(lineCount)
|
||||
// Use the lines that were cleared directly
|
||||
linesToPulse.clear()
|
||||
linesToPulse.addAll(clearedLines)
|
||||
android.util.Log.d("GameView", "Found ${linesToPulse.size} lines to pulse")
|
||||
startPulseAnimation(lineCount)
|
||||
android.util.Log.d("GameView", "Forwarded line clear callback")
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("GameView", "Error forwarding line clear callback", e)
|
||||
|
@ -241,15 +262,24 @@ class GameView @JvmOverloads constructor(
|
|||
val horizontalBlocks = gameBoard.width
|
||||
val verticalBlocks = gameBoard.height
|
||||
|
||||
// Calculate block size to fit within the view
|
||||
blockSize = min(
|
||||
width.toFloat() / horizontalBlocks,
|
||||
height.toFloat() / verticalBlocks
|
||||
)
|
||||
// Account for all glow effects and borders
|
||||
val borderPadding = 16f // Padding for border glow effects
|
||||
|
||||
// Center horizontally and align to bottom
|
||||
boardLeft = (width - (blockSize * horizontalBlocks)) / 2
|
||||
boardTop = height - (blockSize * verticalBlocks) // Align to bottom
|
||||
// Calculate block size to fit the height exactly, accounting for all padding
|
||||
blockSize = (height.toFloat() - (borderPadding * 2)) / verticalBlocks
|
||||
|
||||
// Calculate total board width
|
||||
val totalBoardWidth = blockSize * horizontalBlocks
|
||||
|
||||
// Center horizontally
|
||||
boardLeft = (width - totalBoardWidth) / 2
|
||||
boardTop = borderPadding // Start with border padding from top
|
||||
|
||||
// Calculate the total height needed for the board
|
||||
val totalHeight = blockSize * verticalBlocks
|
||||
|
||||
// Log dimensions for debugging
|
||||
android.util.Log.d("GameView", "Board dimensions: width=$width, height=$height, blockSize=$blockSize, boardLeft=$boardLeft, boardTop=$boardTop, totalHeight=$totalHeight")
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
|
@ -297,13 +327,60 @@ class GameView @JvmOverloads constructor(
|
|||
val bottom = boardTop + gameBoard.height * blockSize
|
||||
|
||||
val rect = RectF(left, top, right, bottom)
|
||||
|
||||
// Draw base border with increased glow
|
||||
borderGlowPaint.apply {
|
||||
alpha = 80 // Increased from 60
|
||||
maskFilter = BlurMaskFilter(16f, BlurMaskFilter.Blur.OUTER) // Increased from 8f
|
||||
}
|
||||
canvas.drawRect(rect, borderGlowPaint)
|
||||
|
||||
// Draw pulsing border if animation is active
|
||||
if (isPulsing) {
|
||||
val pulseBorderPaint = Paint().apply {
|
||||
color = Color.WHITE
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = 6f + (16f * pulseAlpha) // Increased from 4f+12f to 6f+16f
|
||||
alpha = (255 * pulseAlpha).toInt()
|
||||
isAntiAlias = true
|
||||
maskFilter = BlurMaskFilter(32f * (1f + pulseAlpha), BlurMaskFilter.Blur.OUTER) // Increased from 24f to 32f
|
||||
}
|
||||
// Draw the border with a slight inset to prevent edge artifacts
|
||||
val inset = 1f
|
||||
canvas.drawRect(
|
||||
left + inset,
|
||||
top + inset,
|
||||
right - inset,
|
||||
bottom - inset,
|
||||
pulseBorderPaint
|
||||
)
|
||||
|
||||
// Add an additional outer glow for more dramatic effect
|
||||
val outerGlowPaint = Paint().apply {
|
||||
color = Color.WHITE
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = 2f
|
||||
alpha = (128 * pulseAlpha).toInt()
|
||||
isAntiAlias = true
|
||||
maskFilter = BlurMaskFilter(48f * (1f + pulseAlpha), BlurMaskFilter.Blur.OUTER)
|
||||
}
|
||||
canvas.drawRect(
|
||||
left - 4f,
|
||||
top - 4f,
|
||||
right + 4f,
|
||||
bottom + 4f,
|
||||
outerGlowPaint
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the grid lines (very subtle)
|
||||
*/
|
||||
private fun drawGrid(canvas: Canvas) {
|
||||
// Save the canvas state to prevent any effects from affecting the grid
|
||||
canvas.save()
|
||||
|
||||
// Draw vertical grid lines
|
||||
for (x in 0..gameBoard.width) {
|
||||
val xPos = boardLeft + x * blockSize
|
||||
|
@ -323,6 +400,9 @@ class GameView @JvmOverloads constructor(
|
|||
gridPaint
|
||||
)
|
||||
}
|
||||
|
||||
// Restore the canvas state
|
||||
canvas.restore()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -332,7 +412,7 @@ class GameView @JvmOverloads constructor(
|
|||
for (y in 0 until gameBoard.height) {
|
||||
for (x in 0 until gameBoard.width) {
|
||||
if (gameBoard.isOccupied(x, y)) {
|
||||
drawBlock(canvas, x, y, false)
|
||||
drawBlock(canvas, x, y, false, y in linesToPulse)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -350,10 +430,9 @@ class GameView @JvmOverloads constructor(
|
|||
val boardX = piece.x + x
|
||||
val boardY = piece.y + y
|
||||
|
||||
// Only draw if within bounds and visible on screen
|
||||
if (boardY >= 0 && boardY < gameBoard.height &&
|
||||
boardX >= 0 && boardX < gameBoard.width) {
|
||||
drawBlock(canvas, boardX, boardY, false)
|
||||
// Draw piece regardless of vertical position
|
||||
if (boardX >= 0 && boardX < gameBoard.width) {
|
||||
drawBlock(canvas, boardX, boardY, false, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -373,10 +452,9 @@ class GameView @JvmOverloads constructor(
|
|||
val boardX = piece.x + x
|
||||
val boardY = ghostY + y
|
||||
|
||||
// Only draw if within bounds and visible on screen
|
||||
if (boardY >= 0 && boardY < gameBoard.height &&
|
||||
boardX >= 0 && boardX < gameBoard.width) {
|
||||
drawBlock(canvas, boardX, boardY, true)
|
||||
// Draw ghost piece regardless of vertical position
|
||||
if (boardX >= 0 && boardX < gameBoard.width) {
|
||||
drawBlock(canvas, boardX, boardY, true, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -386,12 +464,15 @@ class GameView @JvmOverloads constructor(
|
|||
/**
|
||||
* Draw a single tetris block at the given grid position
|
||||
*/
|
||||
private fun drawBlock(canvas: Canvas, x: Int, y: Int, isGhost: Boolean) {
|
||||
private fun drawBlock(canvas: Canvas, x: Int, y: Int, isGhost: Boolean, isPulsingLine: Boolean) {
|
||||
val left = boardLeft + x * blockSize
|
||||
val top = boardTop + y * blockSize
|
||||
val right = left + blockSize
|
||||
val bottom = top + blockSize
|
||||
|
||||
// Save canvas state before drawing block effects
|
||||
canvas.save()
|
||||
|
||||
// Draw outer glow
|
||||
blockGlowPaint.color = if (isGhost) Color.argb(30, 255, 255, 255) else Color.WHITE
|
||||
canvas.drawRect(left - 2f, top - 2f, right + 2f, bottom + 2f, blockGlowPaint)
|
||||
|
@ -406,6 +487,21 @@ class GameView @JvmOverloads constructor(
|
|||
// Draw inner glow
|
||||
glowPaint.color = if (isGhost) Color.argb(30, 255, 255, 255) else Color.WHITE
|
||||
canvas.drawRect(left + 1f, top + 1f, right - 1f, bottom - 1f, glowPaint)
|
||||
|
||||
// Draw pulse effect if animation is active and this is a pulsing line
|
||||
if (isPulsing && isPulsingLine) {
|
||||
val pulseBlockPaint = Paint().apply {
|
||||
color = Color.WHITE
|
||||
alpha = (255 * pulseAlpha).toInt()
|
||||
isAntiAlias = true
|
||||
style = Paint.Style.FILL
|
||||
maskFilter = BlurMaskFilter(40f * (1f + pulseAlpha), BlurMaskFilter.Blur.OUTER)
|
||||
}
|
||||
canvas.drawRect(left - 16f, top - 16f, right + 16f, bottom + 16f, pulseBlockPaint)
|
||||
}
|
||||
|
||||
// Restore canvas state after drawing block effects
|
||||
canvas.restore()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -573,10 +669,15 @@ class GameView @JvmOverloads constructor(
|
|||
// Reconnect callbacks to the new board
|
||||
gameBoard.onPieceMove = { onPieceMove?.invoke() }
|
||||
gameBoard.onPieceLock = { onPieceLock?.invoke() }
|
||||
gameBoard.onLineClear = { lineCount ->
|
||||
gameBoard.onLineClear = { lineCount, clearedLines ->
|
||||
android.util.Log.d("GameView", "Received line clear from GameBoard: $lineCount lines")
|
||||
try {
|
||||
onLineClear?.invoke(lineCount)
|
||||
// Use the lines that were cleared directly
|
||||
linesToPulse.clear()
|
||||
linesToPulse.addAll(clearedLines)
|
||||
android.util.Log.d("GameView", "Found ${linesToPulse.size} lines to pulse")
|
||||
startPulseAnimation(lineCount)
|
||||
android.util.Log.d("GameView", "Forwarded line clear callback")
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("GameView", "Error forwarding line clear callback", e)
|
||||
|
@ -610,4 +711,42 @@ class GameView @JvmOverloads constructor(
|
|||
update()
|
||||
invalidate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the pulse animation for line clear
|
||||
*/
|
||||
private fun startPulseAnimation(lineCount: Int) {
|
||||
android.util.Log.d("GameView", "Starting pulse animation for $lineCount lines")
|
||||
|
||||
// Cancel any existing animation
|
||||
pulseAnimator?.cancel()
|
||||
|
||||
// Create new animation
|
||||
pulseAnimator = ValueAnimator.ofFloat(0f, 1f, 0f).apply {
|
||||
duration = when (lineCount) {
|
||||
4 -> 2000L // Tetris - longer duration
|
||||
3 -> 1600L // Triples
|
||||
2 -> 1200L // Doubles
|
||||
1 -> 1000L // Singles
|
||||
else -> 1000L
|
||||
}
|
||||
interpolator = LinearInterpolator()
|
||||
addUpdateListener { animation ->
|
||||
pulseAlpha = animation.animatedValue as Float
|
||||
isPulsing = true
|
||||
invalidate()
|
||||
android.util.Log.d("GameView", "Pulse animation update: alpha = $pulseAlpha")
|
||||
}
|
||||
addListener(object : android.animation.AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: android.animation.Animator) {
|
||||
isPulsing = false
|
||||
pulseAlpha = 0f
|
||||
linesToPulse.clear()
|
||||
invalidate()
|
||||
android.util.Log.d("GameView", "Pulse animation ended")
|
||||
}
|
||||
})
|
||||
}
|
||||
pulseAnimator?.start()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ class GameBoard(
|
|||
var onPieceMove: (() -> Unit)? = null
|
||||
var onPieceLock: (() -> Unit)? = null
|
||||
var onNextPieceChanged: (() -> Unit)? = null
|
||||
var onLineClear: ((Int) -> Unit)? = null
|
||||
var onLineClear: ((Int, List<Int>) -> Unit)? = null
|
||||
|
||||
init {
|
||||
spawnNextPiece()
|
||||
|
@ -226,8 +226,13 @@ class GameBoard(
|
|||
val boardX = newX + x
|
||||
val boardY = newY + y
|
||||
|
||||
// Check if the position is outside the board
|
||||
if (boardX < 0 || boardX >= width || boardY >= height) {
|
||||
// Check if the position is outside the board horizontally
|
||||
if (boardX < 0 || boardX >= width) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if the position is below the board
|
||||
if (boardY >= height) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -283,10 +288,12 @@ class GameBoard(
|
|||
// Quick scan for completed lines
|
||||
var shiftAmount = 0
|
||||
var y = height - 1
|
||||
val linesToClear = mutableListOf<Int>()
|
||||
|
||||
while (y >= 0) {
|
||||
if (grid[y].all { it }) {
|
||||
// Line is full, increment shift amount
|
||||
// Line is full, add to lines to clear
|
||||
linesToClear.add(y)
|
||||
shiftAmount++
|
||||
} else if (shiftAmount > 0) {
|
||||
// Shift this row down by shiftAmount
|
||||
|
@ -295,26 +302,26 @@ class GameBoard(
|
|||
y--
|
||||
}
|
||||
|
||||
// Clear top rows
|
||||
for (y in 0 until shiftAmount) {
|
||||
java.util.Arrays.fill(grid[y], false)
|
||||
}
|
||||
|
||||
// If lines were cleared, calculate score in background and trigger callback
|
||||
if (shiftAmount > 0) {
|
||||
android.util.Log.d("GameBoard", "Lines cleared: $shiftAmount")
|
||||
// Trigger line clear callback on main thread
|
||||
// Trigger line clear callback on main thread with the lines that were cleared
|
||||
val mainHandler = android.os.Handler(android.os.Looper.getMainLooper())
|
||||
mainHandler.post {
|
||||
android.util.Log.d("GameBoard", "Triggering onLineClear callback with $shiftAmount lines")
|
||||
try {
|
||||
onLineClear?.invoke(shiftAmount)
|
||||
onLineClear?.invoke(shiftAmount, linesToClear) // Pass the lines that were cleared
|
||||
android.util.Log.d("GameBoard", "onLineClear callback completed successfully")
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("GameBoard", "Error in onLineClear callback", e)
|
||||
}
|
||||
}
|
||||
|
||||
// Clear top rows after callback
|
||||
for (y in 0 until shiftAmount) {
|
||||
java.util.Arrays.fill(grid[y], false)
|
||||
}
|
||||
|
||||
Thread {
|
||||
calculateScore(shiftAmount)
|
||||
}.start()
|
||||
|
@ -448,7 +455,8 @@ class GameBoard(
|
|||
}
|
||||
}
|
||||
|
||||
return ghostY
|
||||
// Ensure ghostY doesn't exceed the board height
|
||||
return ghostY.coerceAtMost(height - 1)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -467,6 +475,17 @@ class GameBoard(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a line is completely filled
|
||||
*/
|
||||
fun isLineFull(y: Int): Boolean {
|
||||
return if (y in 0 until height) {
|
||||
grid[y].all { it }
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current level and adjust game parameters
|
||||
*/
|
||||
|
@ -499,9 +518,10 @@ class GameBoard(
|
|||
|
||||
// Reset game state
|
||||
score = 0
|
||||
level = 1
|
||||
lines = 0
|
||||
isGameOver = false
|
||||
dropInterval = (1000 * Math.pow(0.8, (level - 1).toDouble())).toLong()
|
||||
dropInterval = 1000L // Reset to level 1 speed
|
||||
|
||||
// Reset scoring state
|
||||
combo = 0
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue