mirror of
https://github.com/cmclark00/mintris.git
synced 2025-05-18 20:05:20 +01:00
Fix: Correct namespace and applicationId in app build.gradle
This commit is contained in:
parent
5cf8aec02a
commit
5ace9d7fc5
25 changed files with 4 additions and 4 deletions
136
app/src/main/java/com/pixelmintdrop/game/GameHaptics.kt
Normal file
136
app/src/main/java/com/pixelmintdrop/game/GameHaptics.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
1511
app/src/main/java/com/pixelmintdrop/game/GameView.kt
Normal file
1511
app/src/main/java/com/pixelmintdrop/game/GameView.kt
Normal file
File diff suppressed because it is too large
Load diff
512
app/src/main/java/com/pixelmintdrop/game/GamepadController.kt
Normal file
512
app/src/main/java/com/pixelmintdrop/game/GamepadController.kt
Normal 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
|
||||
}
|
||||
}
|
134
app/src/main/java/com/pixelmintdrop/game/HoldPieceView.kt
Normal file
134
app/src/main/java/com/pixelmintdrop/game/HoldPieceView.kt
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
99
app/src/main/java/com/pixelmintdrop/game/NextPieceView.kt
Normal file
99
app/src/main/java/com/pixelmintdrop/game/NextPieceView.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
386
app/src/main/java/com/pixelmintdrop/game/TitleScreen.kt
Normal file
386
app/src/main/java/com/pixelmintdrop/game/TitleScreen.kt
Normal 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()
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue