Fix: Correct namespace and applicationId in app build.gradle

This commit is contained in:
cmclark00 2025-04-01 07:51:52 -04:00
parent 5cf8aec02a
commit 5ace9d7fc5
25 changed files with 4 additions and 4 deletions

View file

@ -0,0 +1,136 @@
package com.mintris.game
import android.content.Context
import android.os.Build
import android.os.VibrationEffect
import android.os.Vibrator
import android.os.VibratorManager
import android.view.HapticFeedbackConstants
import android.view.View
import android.util.Log
/**
* Handles haptic feedback for game events
*/
class GameHaptics(private val context: Context) {
companion object {
private const val TAG = "GameHaptics"
}
// Vibrator service
private val vibrator: Vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val vibratorManager = context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
vibratorManager.defaultVibrator
} else {
@Suppress("DEPRECATION")
context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
}
// Track if gamepad is connected
private var isGamepadConnected = false
// Set gamepad connection state
fun setGamepadConnected(connected: Boolean) {
isGamepadConnected = connected
}
// Get vibration multiplier based on input method
private fun getVibrationMultiplier(): Float {
return if (isGamepadConnected) 1.5f else 1.0f
}
// Vibrate for line clear (more intense for more lines)
fun vibrateForLineClear(lineCount: Int) {
Log.d(TAG, "Attempting to vibrate for $lineCount lines")
// Only proceed if the device has a vibrator and it's available
if (!vibrator.hasVibrator()) return
// Scale duration and amplitude based on line count and input method
val multiplier = getVibrationMultiplier()
val duration = when(lineCount) {
1 -> (50L * multiplier).toLong() // Single line: short vibration
2 -> (80L * multiplier).toLong() // Double line: slightly longer
3 -> (120L * multiplier).toLong() // Triple line: even longer
4 -> (200L * multiplier).toLong() // Tetris: longest vibration
else -> (50L * multiplier).toLong()
}
val amplitude = when(lineCount) {
1 -> (80 * multiplier).toInt().coerceAtMost(255) // Single line: mild vibration
2 -> (120 * multiplier).toInt().coerceAtMost(255) // Double line: medium vibration
3 -> (180 * multiplier).toInt().coerceAtMost(255) // Triple line: strong vibration
4 -> 255 // Tetris: maximum vibration
else -> (80 * multiplier).toInt().coerceAtMost(255)
}
Log.d(TAG, "Vibration parameters - Duration: ${duration}ms, Amplitude: $amplitude")
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
vibrator.vibrate(VibrationEffect.createOneShot(duration, amplitude))
Log.d(TAG, "Vibration triggered successfully")
} else {
@Suppress("DEPRECATION")
vibrator.vibrate(duration)
Log.w(TAG, "Device does not support vibration effects (Android < 8.0)")
}
} catch (e: Exception) {
Log.e(TAG, "Error triggering vibration", e)
}
}
fun performHapticFeedback(view: View, feedbackType: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
view.performHapticFeedback(HapticFeedbackConstants.CONFIRM)
} else {
@Suppress("DEPRECATION")
view.performHapticFeedback(feedbackType)
}
}
fun vibrateForPieceLock() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val multiplier = getVibrationMultiplier()
val duration = (50L * multiplier).toLong()
val amplitude = (VibrationEffect.DEFAULT_AMPLITUDE * multiplier).toInt().coerceAtMost(255)
val vibrationEffect = VibrationEffect.createOneShot(duration, amplitude)
vibrator.vibrate(vibrationEffect)
}
}
fun vibrateForPieceMove() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val multiplier = getVibrationMultiplier()
val duration = (20L * multiplier).toLong()
val amplitude = (VibrationEffect.DEFAULT_AMPLITUDE * 0.3 * multiplier).toInt().coerceAtLeast(1).coerceAtMost(255)
val vibrationEffect = VibrationEffect.createOneShot(duration, amplitude)
vibrator.vibrate(vibrationEffect)
}
}
fun vibrateForGameOver() {
Log.d(TAG, "Attempting to vibrate for game over")
// Only proceed if the device has a vibrator and it's available
if (!vibrator.hasVibrator()) return
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val multiplier = getVibrationMultiplier()
val duration = (300L * multiplier).toLong()
val amplitude = (VibrationEffect.DEFAULT_AMPLITUDE * multiplier).toInt().coerceAtMost(255)
val vibrationEffect = VibrationEffect.createOneShot(duration, amplitude)
vibrator.vibrate(vibrationEffect)
Log.d(TAG, "Game over vibration triggered successfully")
} else {
@Suppress("DEPRECATION")
vibrator.vibrate(300L)
Log.w(TAG, "Device does not support vibration effects (Android < 8.0)")
}
} catch (e: Exception) {
Log.e(TAG, "Error triggering game over vibration", e)
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,512 @@
package com.mintris.game
import android.os.SystemClock
import android.view.InputDevice
import android.view.KeyEvent
import android.view.MotionEvent
import android.util.Log
import android.content.Context
import android.os.Build
import android.os.VibrationEffect
import android.view.InputDevice.MotionRange
import android.os.Vibrator
import android.os.Handler
import android.os.Looper
/**
* GamepadController handles gamepad input for the Mintris game.
* Supports multiple gamepad types including:
* - Microsoft Xbox controllers
* - Sony PlayStation controllers
* - Nintendo Switch controllers
* - Backbone controllers
*/
class GamepadController(
private val gameView: GameView
) {
companion object {
private const val TAG = "GamepadController"
// Deadzone for analog sticks (normalized value 0.0-1.0)
private const val STICK_DEADZONE = 0.30f
// Cooldown times for responsive input without repeating too quickly
private const val MOVE_COOLDOWN_MS = 100L
private const val ROTATION_COOLDOWN_MS = 150L
private const val HARD_DROP_COOLDOWN_MS = 200L
private const val HOLD_COOLDOWN_MS = 250L
// Continuous movement repeat delay
private const val CONTINUOUS_MOVEMENT_DELAY_MS = 100L
// Rumble patterns
private const val RUMBLE_MOVE_DURATION_MS = 20L
private const val RUMBLE_ROTATE_DURATION_MS = 30L
private const val RUMBLE_HARD_DROP_DURATION_MS = 100L
private const val RUMBLE_LINE_CLEAR_DURATION_MS = 150L
// Check if device is a gamepad
fun isGamepad(device: InputDevice?): Boolean {
if (device == null) return false
// Check for gamepad via input device sources
val sources = device.sources
return (sources and InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD ||
(sources and InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK
}
// Get a list of all connected gamepads
fun getGamepads(): List<InputDevice> {
val gamepads = mutableListOf<InputDevice>()
val deviceIds = InputDevice.getDeviceIds()
for (deviceId in deviceIds) {
val device = InputDevice.getDevice(deviceId)
if (device != null && isGamepad(device)) {
gamepads.add(device)
}
}
return gamepads
}
// Check if any gamepad is connected
fun isGamepadConnected(): Boolean {
return getGamepads().isNotEmpty()
}
// Get the name of the first connected gamepad
fun getConnectedGamepadName(): String? {
val gamepads = getGamepads()
if (gamepads.isEmpty()) return null
return gamepads.first().name
}
// Get information about all connected gamepads
fun getConnectedGamepadsInfo(): List<String> {
return getGamepads().map { it.name }
}
// Check if device supports vibration
fun supportsVibration(device: InputDevice?): Boolean {
if (device == null) return false
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return false
return device.vibratorManager?.vibratorIds?.isNotEmpty() ?: false
}
}
// Timestamps for cooldowns
private var lastMoveTime = 0L
private var lastRotationTime = 0L
private var lastHardDropTime = 0L
private var lastHoldTime = 0L
// Track current directional input state
private var isMovingLeft = false
private var isMovingRight = false
private var isMovingDown = false
// Handler for continuous movement
private val handler = Handler(Looper.getMainLooper())
private val moveLeftRunnable = object : Runnable {
override fun run() {
if (isMovingLeft && gameView.isActive()) {
gameView.moveLeft()
vibrateForPieceMove()
handler.postDelayed(this, CONTINUOUS_MOVEMENT_DELAY_MS)
}
}
}
private val moveRightRunnable = object : Runnable {
override fun run() {
if (isMovingRight && gameView.isActive()) {
gameView.moveRight()
vibrateForPieceMove()
handler.postDelayed(this, CONTINUOUS_MOVEMENT_DELAY_MS)
}
}
}
private val moveDownRunnable = object : Runnable {
override fun run() {
if (isMovingDown && gameView.isActive()) {
gameView.softDrop()
vibrateForPieceMove()
handler.postDelayed(this, CONTINUOUS_MOVEMENT_DELAY_MS)
}
}
}
// Callback interfaces
interface GamepadConnectionListener {
fun onGamepadConnected(gamepadName: String)
fun onGamepadDisconnected(gamepadName: String)
}
interface GamepadMenuListener {
fun onPauseRequested()
}
interface GamepadNavigationListener {
fun onMenuUp()
fun onMenuDown()
fun onMenuSelect()
fun onMenuLeft()
fun onMenuRight()
}
// Listeners
private var connectionListener: GamepadConnectionListener? = null
private var menuListener: GamepadMenuListener? = null
private var navigationListener: GamepadNavigationListener? = null
// Currently active gamepad for rumble
private var activeGamepad: InputDevice? = null
/**
* Set a listener for gamepad connection events
*/
fun setGamepadConnectionListener(listener: GamepadConnectionListener) {
connectionListener = listener
}
/**
* Set a listener for gamepad menu events (pause/start button)
*/
fun setGamepadMenuListener(listener: GamepadMenuListener) {
menuListener = listener
}
/**
* Set a listener for gamepad navigation events
*/
fun setGamepadNavigationListener(listener: GamepadNavigationListener) {
navigationListener = listener
}
/**
* Function to check for newly connected gamepads.
* Call this periodically from the activity to detect connection changes.
*/
fun checkForGamepadChanges(context: Context) {
// Implementation would track previous and current gamepads
// and notify through the connectionListener
// This would be called from the activity's onResume or via a handler
}
/**
* Vibrate the gamepad if supported
*/
fun vibrateGamepad(durationMs: Long, amplitude: Int) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return
val gamepad = activeGamepad ?: return
if (supportsVibration(gamepad)) {
try {
val vibrator = gamepad.vibratorManager
val vibratorIds = vibrator.vibratorIds
if (vibratorIds.isNotEmpty()) {
val effect = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
VibrationEffect.createOneShot(durationMs, amplitude)
} else {
// For older devices, fall back to a simple vibration
VibrationEffect.createOneShot(durationMs, VibrationEffect.DEFAULT_AMPLITUDE)
}
// Create combined vibration for Android S+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val combinedVibration = android.os.CombinedVibration.createParallel(effect)
vibrator.vibrate(combinedVibration)
}
}
} catch (e: Exception) {
Log.e(TAG, "Error vibrating gamepad", e)
}
}
}
/**
* Vibrate for piece movement
*/
fun vibrateForPieceMove() {
vibrateGamepad(RUMBLE_MOVE_DURATION_MS, 50)
}
/**
* Vibrate for piece rotation
*/
fun vibrateForPieceRotation() {
vibrateGamepad(RUMBLE_ROTATE_DURATION_MS, 80)
}
/**
* Vibrate for hard drop
*/
fun vibrateForHardDrop() {
vibrateGamepad(RUMBLE_HARD_DROP_DURATION_MS, 150)
}
/**
* Vibrate for line clear
*/
fun vibrateForLineClear(lineCount: Int) {
val amplitude = when (lineCount) {
1 -> 100
2 -> 150
3 -> 200
else -> 255 // For tetris (4 lines)
}
vibrateGamepad(RUMBLE_LINE_CLEAR_DURATION_MS, amplitude)
}
/**
* Process a key event from a gamepad
* @return true if the event was handled, false otherwise
*/
fun handleKeyEvent(keyCode: Int, event: KeyEvent): Boolean {
// Skip if game is not active but handle menu navigation
if (!gameView.isActive()) {
// Handle menu navigation even when game is not active
if (event.action == KeyEvent.ACTION_DOWN) {
when (keyCode) {
KeyEvent.KEYCODE_DPAD_UP -> {
navigationListener?.onMenuUp()
return true
}
KeyEvent.KEYCODE_DPAD_DOWN -> {
navigationListener?.onMenuDown()
return true
}
KeyEvent.KEYCODE_DPAD_LEFT -> {
navigationListener?.onMenuLeft()
return true
}
KeyEvent.KEYCODE_DPAD_RIGHT -> {
navigationListener?.onMenuRight()
return true
}
KeyEvent.KEYCODE_BUTTON_A,
KeyEvent.KEYCODE_DPAD_CENTER -> {
navigationListener?.onMenuSelect()
return true
}
KeyEvent.KEYCODE_BUTTON_B -> {
// B button can be used to go back/cancel
menuListener?.onPauseRequested()
return true
}
}
}
// Handle menu/start button for pause menu
if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_BUTTON_START) {
menuListener?.onPauseRequested()
return true
}
return false
}
val device = event.device
if (!isGamepad(device)) return false
// Update active gamepad for rumble
activeGamepad = device
val currentTime = SystemClock.uptimeMillis()
when (event.action) {
KeyEvent.ACTION_DOWN -> {
when (keyCode) {
// D-pad and analog movement
KeyEvent.KEYCODE_DPAD_LEFT -> {
if (!isMovingLeft) {
gameView.moveLeft()
vibrateForPieceMove()
lastMoveTime = currentTime
isMovingLeft = true
// Start continuous movement after initial input
handler.postDelayed(moveLeftRunnable, CONTINUOUS_MOVEMENT_DELAY_MS)
return true
}
}
KeyEvent.KEYCODE_DPAD_RIGHT -> {
if (!isMovingRight) {
gameView.moveRight()
vibrateForPieceMove()
lastMoveTime = currentTime
isMovingRight = true
// Start continuous movement after initial input
handler.postDelayed(moveRightRunnable, CONTINUOUS_MOVEMENT_DELAY_MS)
return true
}
}
KeyEvent.KEYCODE_DPAD_DOWN -> {
if (!isMovingDown) {
gameView.softDrop()
vibrateForPieceMove()
lastMoveTime = currentTime
isMovingDown = true
// Start continuous movement after initial input
handler.postDelayed(moveDownRunnable, CONTINUOUS_MOVEMENT_DELAY_MS)
return true
}
}
KeyEvent.KEYCODE_DPAD_UP -> {
if (currentTime - lastHardDropTime > HARD_DROP_COOLDOWN_MS) {
gameView.hardDrop()
vibrateForHardDrop()
lastHardDropTime = currentTime
return true
}
}
// Start button (pause)
KeyEvent.KEYCODE_BUTTON_START -> {
menuListener?.onPauseRequested()
return true
}
// Rotation buttons - supporting multiple buttons for different controllers
KeyEvent.KEYCODE_BUTTON_A,
KeyEvent.KEYCODE_BUTTON_X -> {
if (currentTime - lastRotationTime > ROTATION_COOLDOWN_MS) {
gameView.rotate()
vibrateForPieceRotation()
lastRotationTime = currentTime
return true
}
}
KeyEvent.KEYCODE_BUTTON_B -> {
if (currentTime - lastRotationTime > ROTATION_COOLDOWN_MS) {
gameView.rotateCounterClockwise()
vibrateForPieceRotation()
lastRotationTime = currentTime
return true
}
}
// Hold piece buttons
KeyEvent.KEYCODE_BUTTON_Y,
KeyEvent.KEYCODE_BUTTON_L1,
KeyEvent.KEYCODE_BUTTON_R1 -> {
if (currentTime - lastHoldTime > HOLD_COOLDOWN_MS) {
gameView.holdPiece()
vibrateForPieceRotation()
lastHoldTime = currentTime
return true
}
}
}
}
KeyEvent.ACTION_UP -> {
when (keyCode) {
KeyEvent.KEYCODE_DPAD_LEFT -> {
isMovingLeft = false
handler.removeCallbacks(moveLeftRunnable)
return true
}
KeyEvent.KEYCODE_DPAD_RIGHT -> {
isMovingRight = false
handler.removeCallbacks(moveRightRunnable)
return true
}
KeyEvent.KEYCODE_DPAD_DOWN -> {
isMovingDown = false
handler.removeCallbacks(moveDownRunnable)
return true
}
}
}
}
return false
}
/**
* Process generic motion events (for analog sticks)
* @return true if the event was handled, false otherwise
*/
fun handleMotionEvent(event: MotionEvent): Boolean {
// Skip if game is not active
if (!gameView.isActive()) return false
val device = event.device
if (!isGamepad(device)) return false
// Update active gamepad for rumble
activeGamepad = device
val currentTime = SystemClock.uptimeMillis()
// Process left analog stick
val axisX = event.getAxisValue(MotionEvent.AXIS_X)
val axisY = event.getAxisValue(MotionEvent.AXIS_Y)
// Apply deadzone
if (Math.abs(axisX) > STICK_DEADZONE) {
if (axisX > 0 && !isMovingRight) {
gameView.moveRight()
vibrateForPieceMove()
lastMoveTime = currentTime
isMovingRight = true
isMovingLeft = false
// Start continuous movement after initial input
handler.removeCallbacks(moveLeftRunnable)
handler.postDelayed(moveRightRunnable, CONTINUOUS_MOVEMENT_DELAY_MS)
return true
} else if (axisX < 0 && !isMovingLeft) {
gameView.moveLeft()
vibrateForPieceMove()
lastMoveTime = currentTime
isMovingLeft = true
isMovingRight = false
// Start continuous movement after initial input
handler.removeCallbacks(moveRightRunnable)
handler.postDelayed(moveLeftRunnable, CONTINUOUS_MOVEMENT_DELAY_MS)
return true
}
} else {
// Reset horizontal movement flags when stick returns to center
isMovingLeft = false
isMovingRight = false
handler.removeCallbacks(moveLeftRunnable)
handler.removeCallbacks(moveRightRunnable)
}
if (Math.abs(axisY) > STICK_DEADZONE) {
if (axisY > 0 && !isMovingDown) {
gameView.softDrop()
vibrateForPieceMove()
lastMoveTime = currentTime
isMovingDown = true
// Start continuous movement after initial input
handler.postDelayed(moveDownRunnable, CONTINUOUS_MOVEMENT_DELAY_MS)
return true
}
} else {
// Reset vertical movement flag when stick returns to center
isMovingDown = false
handler.removeCallbacks(moveDownRunnable)
}
// Check right analog stick for rotation
val axisZ = event.getAxisValue(MotionEvent.AXIS_Z)
val axisRZ = event.getAxisValue(MotionEvent.AXIS_RZ)
if (Math.abs(axisZ) > STICK_DEADZONE || Math.abs(axisRZ) > STICK_DEADZONE) {
if (currentTime - lastRotationTime > ROTATION_COOLDOWN_MS) {
gameView.rotate()
vibrateForPieceRotation()
lastRotationTime = currentTime
return true
}
}
return false
}
}

View file

@ -0,0 +1,134 @@
package com.mintris.game
import android.content.Context
import android.graphics.BlurMaskFilter
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
import com.mintris.model.GameBoard
import com.mintris.model.Tetromino
import kotlin.math.min
/**
* View that displays the currently held piece
*/
class HoldPieceView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private var gameView: GameView? = null
private var gameBoard: GameBoard? = null
// Rendering
private val blockPaint = Paint().apply {
color = Color.WHITE
isAntiAlias = true
style = Paint.Style.FILL
}
private val glowPaint = Paint().apply {
color = Color.WHITE
alpha = 40
isAntiAlias = true
style = Paint.Style.STROKE
strokeWidth = 1.5f
maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.OUTER)
}
private val blockGlowPaint = Paint().apply {
color = Color.WHITE
alpha = 60
isAntiAlias = true
style = Paint.Style.FILL
maskFilter = BlurMaskFilter(12f, BlurMaskFilter.Blur.OUTER)
}
/**
* Set the game view reference
*/
fun setGameView(view: GameView) {
gameView = view
gameBoard = view.getGameBoard()
}
/**
* Get the game board reference
*/
private fun getGameBoard(): GameBoard? = gameBoard
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// Get the held piece from game board
gameBoard?.let {
it.getHoldPiece()?.let { piece ->
val width = piece.getWidth()
val height = piece.getHeight()
// Calculate block size for the preview (smaller than main board)
val previewBlockSize = min(
canvas.width.toFloat() / (width + 2),
canvas.height.toFloat() / (height + 2)
)
// Center the piece in the preview area
val previewLeft = (canvas.width - width * previewBlockSize) / 2
val previewTop = (canvas.height - height * previewBlockSize) / 2
// Draw subtle background glow
val glowPaint = Paint().apply {
color = Color.WHITE
alpha = 10
maskFilter = BlurMaskFilter(previewBlockSize * 0.5f, BlurMaskFilter.Blur.OUTER)
}
canvas.drawRect(
previewLeft - previewBlockSize,
previewTop - previewBlockSize,
previewLeft + width * previewBlockSize + previewBlockSize,
previewTop + height * previewBlockSize + previewBlockSize,
glowPaint
)
// Draw the held piece
for (y in 0 until height) {
for (x in 0 until width) {
if (piece.isBlockAt(x, y)) {
val left = previewLeft + x * previewBlockSize
val top = previewTop + y * previewBlockSize
val right = left + previewBlockSize
val bottom = top + previewBlockSize
// Draw outer glow
blockGlowPaint.color = Color.WHITE
canvas.drawRect(
left - 2f,
top - 2f,
right + 2f,
bottom + 2f,
blockGlowPaint
)
// Draw block
blockPaint.color = Color.WHITE
canvas.drawRect(left, top, right, bottom, blockPaint)
// Draw inner glow
glowPaint.color = Color.WHITE
canvas.drawRect(
left + 1f,
top + 1f,
right - 1f,
bottom - 1f,
glowPaint
)
}
}
}
}
}
}
}

View file

@ -0,0 +1,99 @@
package com.mintris.game
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.graphics.BlurMaskFilter
import android.util.AttributeSet
import android.view.View
import kotlin.math.min
/**
* Custom view to display the next Tetromino piece
*/
class NextPieceView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private var gameView: GameView? = null
// Rendering
private val blockPaint = Paint().apply {
color = Color.WHITE
isAntiAlias = true
}
private val glowPaint = Paint().apply {
color = Color.WHITE
alpha = 30
isAntiAlias = true
style = Paint.Style.STROKE
strokeWidth = 1.5f
}
/**
* Set the game view to get the next piece from
*/
fun setGameView(gameView: GameView) {
this.gameView = gameView
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// Get the next piece from game view
gameView?.let {
it.getNextPiece()?.let { piece ->
val width = piece.getWidth()
val height = piece.getHeight()
// Calculate block size for the preview (smaller than main board)
val previewBlockSize = min(
canvas.width.toFloat() / (width + 2),
canvas.height.toFloat() / (height + 2)
)
// Center the piece in the preview area
val previewLeft = (canvas.width - width * previewBlockSize) / 2
val previewTop = (canvas.height - height * previewBlockSize) / 2
// Draw subtle background glow
val glowPaint = Paint().apply {
color = Color.WHITE
alpha = 10
maskFilter = BlurMaskFilter(previewBlockSize * 0.5f, BlurMaskFilter.Blur.OUTER)
}
canvas.drawRect(
previewLeft - previewBlockSize,
previewTop - previewBlockSize,
previewLeft + width * previewBlockSize + previewBlockSize,
previewTop + height * previewBlockSize + previewBlockSize,
glowPaint
)
for (y in 0 until height) {
for (x in 0 until width) {
if (piece.isBlockAt(x, y)) {
val left = previewLeft + x * previewBlockSize
val top = previewTop + y * previewBlockSize
val right = left + previewBlockSize
val bottom = top + previewBlockSize
// Draw block with subtle glow
val rect = RectF(left + 1, top + 1, right - 1, bottom - 1)
canvas.drawRect(rect, blockPaint)
// Draw subtle border glow
val glowRect = RectF(left, top, right, bottom)
canvas.drawRect(glowRect, glowPaint)
}
}
}
}
}
}
}

View file

@ -0,0 +1,386 @@
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
import com.mintris.model.HighScoreManager
import com.mintris.model.HighScore
import com.mintris.model.PlayerProgressionManager
import kotlin.math.abs
import androidx.core.graphics.withTranslation
import androidx.core.graphics.withScale
import androidx.core.graphics.withRotation
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 highScorePaint = Paint()
private val cellSize = 30f
private val random = Random()
private var width = 0
private var height = 0
private val tetrominosToAdd = mutableListOf<Tetromino>()
private val highScoreManager = HighScoreManager(context) // Pre-allocate HighScoreManager
// Touch handling variables
private var startX = 0f
private var startY = 0f
private var lastTouchX = 0f
private var lastTouchY = 0f
private val maxTapMovement = 20f // Maximum movement allowed for a tap (in pixels)
// Callback for when the user touches the screen
var onStartGame: (() -> Unit)? = null
// Theme color and background color
private var themeColor = Color.WHITE
private var backgroundColor = Color.BLACK
// 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<IntArray>,
val speed: Float,
val scale: Float,
val rotation: Int = 0
)
private val tetrominos = mutableListOf<Tetromino>()
init {
// Title text settings
titlePaint.apply {
color = themeColor
textSize = 120f
textAlign = Paint.Align.CENTER
typeface = Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD)
isAntiAlias = true
}
// "Touch to start" text settings
promptPaint.apply {
color = themeColor
textSize = 50f
textAlign = Paint.Align.CENTER
typeface = Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL)
isAntiAlias = true
alpha = 180
}
// High scores text settings
highScorePaint.apply {
color = themeColor
textSize = 70f
textAlign = Paint.Align.LEFT // Changed to LEFT alignment
typeface = Typeface.create(Typeface.MONOSPACE, Typeface.NORMAL) // Changed to monospace
isAntiAlias = true
alpha = 200
}
// General paint settings for tetrominos
paint.apply {
color = themeColor
style = Paint.Style.FILL
isAntiAlias = true
}
// Glow paint settings for tetrominos
glowPaint.apply {
color = themeColor
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 using the current background color
canvas.drawColor(backgroundColor)
// Add any pending tetrominos
tetrominos.addAll(tetrominosToAdd)
tetrominosToAdd.clear()
// Update and draw falling tetrominos
val tetrominosToRemove = mutableListOf<Tetromino>()
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 {
// Draw the tetromino
for (y in 0 until tetromino.shape.size) {
for (x in 0 until tetromino.shape.size) {
if (tetromino.shape[y][x] == 1) {
val left = x * cellSize
val top = y * cellSize
val right = left + cellSize
val bottom = top + cellSize
// Draw block with glow effect
canvas.withTranslation(tetromino.x, tetromino.y) {
withScale(tetromino.scale, tetromino.scale) {
withRotation(tetromino.rotation.toFloat(),
tetromino.shape.size * cellSize / 2,
tetromino.shape.size * cellSize / 2) {
// Draw glow
canvas.drawRect(left - 8f, top - 8f, right + 8f, bottom + 8f, glowPaint)
// Draw block
canvas.drawRect(left, top, right, bottom, paint)
}
}
}
}
}
}
} 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 high scores using pre-allocated manager
val highScores: List<HighScore> = highScoreManager.getHighScores()
val highScoreY = height * 0.5f
var lastHighScoreY = highScoreY
if (highScores.isNotEmpty()) {
// Calculate the starting X position to center the entire block of scores
val maxScoreWidth = highScorePaint.measureText("99. PLAYER: 999999")
val startX = (width - maxScoreWidth) / 2
highScores.forEachIndexed { index: Int, score: HighScore ->
val y = highScoreY + (index * 80f)
lastHighScoreY = y // Track the last high score's Y position
// Pad the rank number to ensure alignment
val rank = (index + 1).toString().padStart(2, ' ')
// Pad the name to ensure score alignment
val paddedName = score.name.padEnd(8, ' ')
canvas.drawText("$rank. $paddedName ${score.score}", startX, y, highScorePaint)
}
}
// Draw "touch to start" prompt below the high scores
val promptY = if (resources.configuration.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE) {
// In landscape mode, position below the last high score with some padding
lastHighScoreY + 100f
} else {
// In portrait mode, use the original position
height * 0.7f
}
canvas.drawText("touch to start", width / 2f, promptY, promptPaint)
// Request another frame
invalidate()
} catch (e: Exception) {
Log.e("TitleScreen", "Error in onDraw", e)
}
}
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
startX = event.x
startY = event.y
lastTouchX = event.x
lastTouchY = event.y
return true
}
MotionEvent.ACTION_MOVE -> {
val deltaX = event.x - lastTouchX
val deltaY = event.y - lastTouchY
// Update tetromino positions
for (tetromino in tetrominos) {
tetromino.x += deltaX * 0.5f
tetromino.y += deltaY * 0.5f
}
lastTouchX = event.x
lastTouchY = event.y
invalidate()
return true
}
MotionEvent.ACTION_UP -> {
val deltaX = event.x - startX
val deltaY = event.y - startY
// If the movement was minimal, treat as a tap
if (abs(deltaX) < maxTapMovement && abs(deltaY) < maxTapMovement) {
performClick()
}
return true
}
}
return super.onTouchEvent(event)
}
override fun performClick(): Boolean {
// Call the superclass's performClick
super.performClick()
// Handle the click event
onStartGame?.invoke()
return true
}
/**
* Apply a theme to the title screen
*/
fun applyTheme(themeId: String) {
// Get theme color based on theme ID
themeColor = when (themeId) {
PlayerProgressionManager.THEME_CLASSIC -> Color.WHITE
PlayerProgressionManager.THEME_NEON -> Color.parseColor("#FF00FF")
PlayerProgressionManager.THEME_MONOCHROME -> Color.LTGRAY
PlayerProgressionManager.THEME_RETRO -> Color.parseColor("#FF5A5F")
PlayerProgressionManager.THEME_MINIMALIST -> Color.BLACK
PlayerProgressionManager.THEME_GALAXY -> Color.parseColor("#66FCF1")
else -> Color.WHITE
}
// Update paint colors
titlePaint.color = themeColor
promptPaint.color = themeColor
highScorePaint.color = themeColor
paint.color = themeColor
glowPaint.color = themeColor
// Update background color
backgroundColor = when (themeId) {
PlayerProgressionManager.THEME_CLASSIC -> Color.BLACK
PlayerProgressionManager.THEME_NEON -> Color.parseColor("#0D0221")
PlayerProgressionManager.THEME_MONOCHROME -> Color.parseColor("#1A1A1A")
PlayerProgressionManager.THEME_RETRO -> Color.parseColor("#3F2832")
PlayerProgressionManager.THEME_MINIMALIST -> Color.WHITE
PlayerProgressionManager.THEME_GALAXY -> Color.parseColor("#0B0C10")
else -> Color.BLACK
}
invalidate()
}
/**
* Set the theme color for the title screen
*/
fun setThemeColor(color: Int) {
themeColor = color
titlePaint.color = color
promptPaint.color = color
highScorePaint.color = color
paint.color = color
glowPaint.color = color
invalidate()
}
/**
* Set the background color for the title screen
*/
override fun setBackgroundColor(color: Int) {
backgroundColor = color
invalidate()
}
}