diff --git a/app/src/main/java/com/mintris/MainActivity.kt b/app/src/main/java/com/mintris/MainActivity.kt index 50915da..5d0c01b 100644 --- a/app/src/main/java/com/mintris/MainActivity.kt +++ b/app/src/main/java/com/mintris/MainActivity.kt @@ -15,6 +15,7 @@ import com.mintris.databinding.ActivityMainBinding import com.mintris.game.GameHaptics import com.mintris.game.GameView import com.mintris.game.NextPieceView +import com.mintris.game.TitleScreen import android.view.HapticFeedbackConstants import com.mintris.model.GameBoard import com.mintris.audio.GameMusic @@ -27,6 +28,7 @@ class MainActivity : AppCompatActivity() { private lateinit var gameHaptics: GameHaptics private lateinit var gameBoard: GameBoard private lateinit var gameMusic: GameMusic + private lateinit var titleScreen: TitleScreen // Game state private var isSoundEnabled = true @@ -43,12 +45,36 @@ class MainActivity : AppCompatActivity() { gameBoard = GameBoard() gameHaptics = GameHaptics(this) gameView = binding.gameView + titleScreen = binding.titleScreen gameMusic = GameMusic(this) // Set up game view gameView.setGameBoard(gameBoard) gameView.setHaptics(gameHaptics) + // Set up title screen + titleScreen.onStartGame = { + titleScreen.visibility = View.GONE + gameView.visibility = View.VISIBLE + binding.gameControlsContainer.visibility = View.VISIBLE + startGame() + } + + // Initially hide the game view and show title screen + gameView.visibility = View.GONE + binding.gameControlsContainer.visibility = View.GONE + titleScreen.visibility = View.VISIBLE + + // Set up pause button to show settings menu + binding.pauseButton.setOnClickListener { + gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY) + gameView.pause() + gameMusic.pause() + showPauseMenu() + binding.pauseStartButton.visibility = View.GONE + binding.resumeButton.visibility = View.VISIBLE + } + // Set up next piece preview binding.nextPieceView.setGameView(gameView) gameBoard.onNextPieceChanged = { @@ -62,9 +88,6 @@ class MainActivity : AppCompatActivity() { updateMusicToggleUI() } - // Start game immediately - startGame() - // Set up callbacks gameView.onGameStateChanged = { score, level, lines -> updateUI(score, level, lines) @@ -99,7 +122,7 @@ class MainActivity : AppCompatActivity() { gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY) hideGameOver() gameView.reset() - gameView.start() + startGame() } binding.resumeButton.setOnClickListener { @@ -118,14 +141,14 @@ class MainActivity : AppCompatActivity() { gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY) hidePauseMenu() gameView.reset() - gameView.start() + startGame() } binding.pauseRestartButton.setOnClickListener { gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY) hidePauseMenu() gameView.reset() - gameView.start() + startGame() } binding.pauseLevelUpButton.setOnClickListener { @@ -250,50 +273,27 @@ class MainActivity : AppCompatActivity() { showPauseMenu() } - private fun pauseGame() { - gameView.pause() - gameMusic.pause() - } - private fun resumeGame() { gameView.resume() if (isMusicEnabled) { - gameMusic.start() + gameMusic.resume() } + // Force a redraw to ensure pieces aren't frozen + gameView.invalidate() } override fun onPause() { super.onPause() - if (!gameView.isGameOver()) { - pauseGame() - showPauseMenu() - binding.pauseStartButton.visibility = View.GONE - binding.resumeButton.visibility = View.VISIBLE - } - } - - @Deprecated("Deprecated in Java") - override fun onBackPressed() { - if (binding.gameOverContainer.visibility == View.VISIBLE) { - hideGameOver() - gameView.reset() - return - } - - if (binding.pauseContainer.visibility == View.GONE) { + if (gameView.visibility == View.VISIBLE) { gameView.pause() - showPauseMenu() - binding.pauseStartButton.visibility = View.GONE - binding.resumeButton.visibility = View.VISIBLE - } else { - hidePauseMenu() - resumeGame() + gameMusic.pause() } } override fun onResume() { super.onResume() - if (!gameView.isGameOver()) { + // If we're on the title screen, don't auto-resume the game + if (titleScreen.visibility == View.GONE && gameView.visibility == View.VISIBLE && binding.gameOverContainer.visibility == View.GONE && binding.pauseContainer.visibility == View.GONE) { resumeGame() } } @@ -302,4 +302,16 @@ class MainActivity : AppCompatActivity() { super.onDestroy() gameMusic.release() } + + /** + * Show title screen (for game restart) + */ + private fun showTitleScreen() { + gameView.reset() + gameView.visibility = View.GONE + binding.gameControlsContainer.visibility = View.GONE + binding.gameOverContainer.visibility = View.GONE + binding.pauseContainer.visibility = View.GONE + titleScreen.visibility = View.VISIBLE + } } \ No newline at end of file diff --git a/app/src/main/java/com/mintris/audio/GameMusic.kt b/app/src/main/java/com/mintris/audio/GameMusic.kt index 6b96257..b455666 100644 --- a/app/src/main/java/com/mintris/audio/GameMusic.kt +++ b/app/src/main/java/com/mintris/audio/GameMusic.kt @@ -59,6 +59,17 @@ class GameMusic(private val context: Context) { } } + fun resume() { + try { + Log.d("GameMusic", "Resuming music playback") + if (isEnabled && mediaPlayer?.isPlaying != true) { + mediaPlayer?.start() + } + } catch (e: Exception) { + Log.e("GameMusic", "Error resuming music", e) + } + } + fun stop() { try { Log.d("GameMusic", "Stopping music playback") diff --git a/app/src/main/java/com/mintris/game/GameView.kt b/app/src/main/java/com/mintris/game/GameView.kt index be53364..b9f06e7 100644 --- a/app/src/main/java/com/mintris/game/GameView.kt +++ b/app/src/main/java/com/mintris/game/GameView.kt @@ -664,9 +664,15 @@ class GameView @JvmOverloads constructor( fun resume() { if (!isRunning) { isRunning = true - handler.post(gameLoopRunnable) } isPaused = false + + // Restart the game loop immediately + handler.removeCallbacks(gameLoopRunnable) + handler.post(gameLoopRunnable) + + // Force an update to ensure pieces move immediately + update() invalidate() } } \ No newline at end of file diff --git a/app/src/main/java/com/mintris/game/TitleScreen.kt b/app/src/main/java/com/mintris/game/TitleScreen.kt new file mode 100644 index 0000000..0c87891 --- /dev/null +++ b/app/src/main/java/com/mintris/game/TitleScreen.kt @@ -0,0 +1,259 @@ +package com.mintris.game + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Typeface +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View +import java.util.Random +import android.util.Log + +class TitleScreen @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) { + + private val paint = Paint() + private val glowPaint = Paint() + private val titlePaint = Paint() + private val promptPaint = Paint() + private val cellSize = 30f + private val random = Random() + private var width = 0 + private var height = 0 + private val tetrominosToAdd = mutableListOf() + + // Callback for when the user touches the screen + var onStartGame: (() -> Unit)? = null + + // Define tetromino shapes (I, O, T, S, Z, J, L) + private val tetrominoShapes = arrayOf( + // I + arrayOf( + intArrayOf(0, 0, 0, 0), + intArrayOf(1, 1, 1, 1), + intArrayOf(0, 0, 0, 0), + intArrayOf(0, 0, 0, 0) + ), + // O + arrayOf( + intArrayOf(1, 1), + intArrayOf(1, 1) + ), + // T + arrayOf( + intArrayOf(0, 1, 0), + intArrayOf(1, 1, 1), + intArrayOf(0, 0, 0) + ), + // S + arrayOf( + intArrayOf(0, 1, 1), + intArrayOf(1, 1, 0), + intArrayOf(0, 0, 0) + ), + // Z + arrayOf( + intArrayOf(1, 1, 0), + intArrayOf(0, 1, 1), + intArrayOf(0, 0, 0) + ), + // J + arrayOf( + intArrayOf(1, 0, 0), + intArrayOf(1, 1, 1), + intArrayOf(0, 0, 0) + ), + // L + arrayOf( + intArrayOf(0, 0, 1), + intArrayOf(1, 1, 1), + intArrayOf(0, 0, 0) + ) + ) + + // Tetromino class to represent falling pieces + private class Tetromino( + var x: Float, + var y: Float, + val shape: Array, + val speed: Float, + val scale: Float, + val rotation: Int = 0 + ) + + private val tetrominos = mutableListOf() + + init { + // Title text settings + titlePaint.apply { + color = Color.WHITE + textSize = 120f + textAlign = Paint.Align.CENTER + typeface = Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD) + isAntiAlias = true + } + + // "Touch to start" text settings + promptPaint.apply { + color = Color.WHITE + textSize = 40f + textAlign = Paint.Align.CENTER + typeface = Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL) + isAntiAlias = true + alpha = 180 + } + + // General paint settings for tetrominos (white) + paint.apply { + color = Color.WHITE + style = Paint.Style.FILL + isAntiAlias = true + } + + // Glow paint settings for tetrominos + glowPaint.apply { + color = Color.WHITE + style = Paint.Style.FILL + isAntiAlias = true + alpha = 60 + } + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + width = w + height = h + + // Clear existing tetrominos + tetrominos.clear() + + // Initialize some tetrominos + repeat(20) { + val tetromino = createRandomTetromino() + tetrominos.add(tetromino) + } + } + + private fun createRandomTetromino(): Tetromino { + val x = random.nextFloat() * (width - 150) + 50 // Keep away from edges + val y = -cellSize * 4 - (random.nextFloat() * height / 2) + val shapeIndex = random.nextInt(tetrominoShapes.size) + val shape = tetrominoShapes[shapeIndex] + val speed = 1f + random.nextFloat() * 2f + val scale = 0.8f + random.nextFloat() * 0.4f + val rotation = random.nextInt(4) * 90 + + return Tetromino(x, y, shape, speed, scale, rotation) + } + + override fun onDraw(canvas: Canvas) { + try { + super.onDraw(canvas) + + // Draw background + canvas.drawColor(Color.BLACK) + + // Add any pending tetrominos + tetrominos.addAll(tetrominosToAdd) + tetrominosToAdd.clear() + + // Update and draw falling tetrominos + val tetrominosToRemove = mutableListOf() + + for (tetromino in tetrominos) { + tetromino.y += tetromino.speed + + // Remove tetrominos that have fallen off the screen + if (tetromino.y > height) { + tetrominosToRemove.add(tetromino) + tetrominosToAdd.add(createRandomTetromino()) + } else { + try { + // Save canvas state before rotation + canvas.save() + + // Translate to the tetromino's position + canvas.translate(tetromino.x, tetromino.y) + + // Scale according to the tetromino's scale factor + canvas.scale(tetromino.scale, tetromino.scale) + + // Rotate around the center of the tetromino + val centerX = tetromino.shape.size * cellSize / 2 + val centerY = tetromino.shape.size * cellSize / 2 + canvas.rotate(tetromino.rotation.toFloat(), centerX, centerY) + + // Draw the tetromino + for (row in tetromino.shape.indices) { + for (col in 0 until tetromino.shape[row].size) { + if (tetromino.shape[row][col] == 1) { + // Draw larger glow effect + glowPaint.alpha = 30 + canvas.drawRect( + col * cellSize - 8, + row * cellSize - 8, + (col + 1) * cellSize + 8, + (row + 1) * cellSize + 8, + glowPaint + ) + + // Draw medium glow + glowPaint.alpha = 60 + canvas.drawRect( + col * cellSize - 4, + row * cellSize - 4, + (col + 1) * cellSize + 4, + (row + 1) * cellSize + 4, + glowPaint + ) + + // Draw main block + canvas.drawRect( + col * cellSize, + row * cellSize, + (col + 1) * cellSize, + (row + 1) * cellSize, + paint + ) + } + } + } + + // Restore canvas state + canvas.restore() + } catch (e: Exception) { + Log.e("TitleScreen", "Error drawing tetromino", e) + } + } + } + + // Remove tetrominos that fell off the screen + tetrominos.removeAll(tetrominosToRemove) + + // Draw title + val titleY = height * 0.4f + canvas.drawText("mintris", width / 2f, titleY, titlePaint) + + // Draw "touch to start" prompt + canvas.drawText("touch to start", width / 2f, height * 0.6f, promptPaint) + + // Request another frame + invalidate() + } catch (e: Exception) { + Log.e("TitleScreen", "Error in onDraw", e) + } + } + + override fun onTouchEvent(event: MotionEvent): Boolean { + if (event.action == MotionEvent.ACTION_DOWN) { + onStartGame?.invoke() + return true + } + return super.onTouchEvent(event) + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_pause.xml b/app/src/main/res/drawable/ic_pause.xml new file mode 100644 index 0000000..31e80fc --- /dev/null +++ b/app/src/main/res/drawable/ic_pause.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 03a19ea..f18eabe 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -31,54 +31,81 @@ android:layout_height="match_parent" android:background="@drawable/glow_border" /> + + + - - + + + android:layout_margin="16dp" + android:layout_gravity="end" + android:orientation="vertical"> - + - + + + + + + + + + + - - -