mirror of
https://github.com/cmclark00/mintris.git
synced 2025-05-18 05:15:20 +01:00
adding gamepad support
This commit is contained in:
parent
86424eac32
commit
0ac25eb3a9
5 changed files with 796 additions and 5 deletions
|
@ -31,8 +31,15 @@ import android.util.Log
|
|||
import android.view.KeyEvent
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.MotionEvent
|
||||
import com.mintris.game.GamepadController
|
||||
import android.view.InputDevice
|
||||
import android.widget.Toast
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.IntentFilter
|
||||
import android.app.AlertDialog
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
class MainActivity : AppCompatActivity(), GamepadController.GamepadConnectionListener {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "MainActivity"
|
||||
|
@ -66,6 +73,24 @@ class MainActivity : AppCompatActivity() {
|
|||
// Activity result launcher for high score entry
|
||||
private lateinit var highScoreEntryLauncher: ActivityResultLauncher<Intent>
|
||||
|
||||
// Gamepad controller
|
||||
private lateinit var gamepadController: GamepadController
|
||||
|
||||
// Input device change receiver
|
||||
private val inputDeviceReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
when (intent.action) {
|
||||
Intent.ACTION_CONFIGURATION_CHANGED -> {
|
||||
// A gamepad might have connected or disconnected
|
||||
checkGamepadConnections()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Track connected gamepads to detect changes
|
||||
private val connectedGamepads = mutableSetOf<String>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
// Register activity result launcher for high score entry
|
||||
highScoreEntryLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
|
@ -93,6 +118,10 @@ class MainActivity : AppCompatActivity() {
|
|||
themeSelector = binding.themeSelector
|
||||
blockSkinSelector = binding.blockSkinSelector
|
||||
|
||||
// Initialize gamepad controller
|
||||
gamepadController = GamepadController(gameView)
|
||||
gamepadController.setGamepadConnectionListener(this)
|
||||
|
||||
// Set up touch event forwarding
|
||||
binding.touchInterceptor?.setOnTouchListener { _, event ->
|
||||
if (gameView.visibility == View.VISIBLE && !gameView.isPaused && !gameView.isGameOver()) {
|
||||
|
@ -231,8 +260,35 @@ class MainActivity : AppCompatActivity() {
|
|||
updateUI(score, level, lines)
|
||||
}
|
||||
|
||||
gameView.onGameOver = { score ->
|
||||
showGameOver(score)
|
||||
gameView.onGameOver = { finalScore ->
|
||||
// Pause music on game over
|
||||
gameMusic.pause()
|
||||
|
||||
// Update high scores
|
||||
val timePlayedMs = System.currentTimeMillis() - gameStartTime
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
|
||||
val currentDate = dateFormat.format(Date())
|
||||
|
||||
// Track if this is a high score
|
||||
val isHighScore = highScoreManager.isHighScore(finalScore)
|
||||
|
||||
// Show game over screen
|
||||
showGameOver(finalScore)
|
||||
|
||||
// Save player stats to track game history
|
||||
statsManager.updateSessionStats(finalScore, gameBoard.lines, piecesPlaced, timePlayedMs, currentLevel)
|
||||
|
||||
// Handle progression - XP earned, potential level up
|
||||
val xpGained = progressionManager.calculateGameXP(finalScore, gameBoard.lines, currentLevel, timePlayedMs, statsManager.getSessionTetrises(), 0)
|
||||
val newRewards = progressionManager.addXP(xpGained)
|
||||
|
||||
// Show progression screen if player earned XP
|
||||
if (xpGained > 0) {
|
||||
// Delay showing progression screen for a moment
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
showProgressionScreen(xpGained, newRewards)
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
|
||||
gameView.onLineClear = { lineCount ->
|
||||
|
@ -577,12 +633,83 @@ class MainActivity : AppCompatActivity() {
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a new game
|
||||
*/
|
||||
private fun startGame() {
|
||||
// Set initial game state
|
||||
currentScore = 0
|
||||
currentLevel = selectedLevel
|
||||
piecesPlaced = 0
|
||||
gameStartTime = System.currentTimeMillis()
|
||||
|
||||
// Update UI to show initial values
|
||||
binding.scoreText.text = "$currentScore"
|
||||
binding.currentLevelText.text = "$currentLevel"
|
||||
binding.linesText.text = "0"
|
||||
|
||||
// Reset game view and game board
|
||||
gameView.reset()
|
||||
|
||||
// Configure callbacks
|
||||
gameView.onGameStateChanged = { score, level, lines ->
|
||||
currentScore = score
|
||||
currentLevel = level
|
||||
|
||||
binding.scoreText.text = "$score"
|
||||
binding.currentLevelText.text = "$level"
|
||||
binding.linesText.text = "$lines"
|
||||
}
|
||||
|
||||
gameView.onGameOver = { finalScore ->
|
||||
// Pause music on game over
|
||||
gameMusic.pause()
|
||||
|
||||
// Update high scores
|
||||
val timePlayedMs = System.currentTimeMillis() - gameStartTime
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
|
||||
val currentDate = dateFormat.format(Date())
|
||||
|
||||
// Track if this is a high score
|
||||
val isHighScore = highScoreManager.isHighScore(finalScore)
|
||||
|
||||
// Show game over screen
|
||||
showGameOver(finalScore)
|
||||
|
||||
// Save player stats to track game history
|
||||
statsManager.updateSessionStats(finalScore, gameBoard.lines, piecesPlaced, timePlayedMs, currentLevel)
|
||||
|
||||
// Handle progression - XP earned, potential level up
|
||||
val xpGained = progressionManager.calculateGameXP(finalScore, gameBoard.lines, currentLevel, timePlayedMs, statsManager.getSessionTetrises(), 0)
|
||||
val newRewards = progressionManager.addXP(xpGained)
|
||||
|
||||
// Show progression screen if player earned XP
|
||||
if (xpGained > 0) {
|
||||
// Delay showing progression screen for a moment
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
showProgressionScreen(xpGained, newRewards)
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
|
||||
// Connect line clear callback to gamepad rumble
|
||||
gameView.onLineClear = { lineCount ->
|
||||
// Vibrate phone
|
||||
gameHaptics.vibrateForLineClear(lineCount)
|
||||
|
||||
// Vibrate gamepad if connected
|
||||
gamepadController.vibrateForLineClear(lineCount)
|
||||
}
|
||||
|
||||
// Start the game
|
||||
gameView.start()
|
||||
gameMusic.setEnabled(isMusicEnabled)
|
||||
|
||||
// Start background music if enabled
|
||||
if (isMusicEnabled) {
|
||||
gameMusic.start()
|
||||
}
|
||||
|
||||
gameStartTime = System.currentTimeMillis()
|
||||
piecesPlaced = 0
|
||||
statsManager.startNewSession()
|
||||
|
@ -612,10 +739,25 @@ class MainActivity : AppCompatActivity() {
|
|||
gameView.pause()
|
||||
gameMusic.pause()
|
||||
}
|
||||
|
||||
// Unregister broadcast receiver
|
||||
try {
|
||||
unregisterReceiver(inputDeviceReceiver)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
// Receiver wasn't registered
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
// Register for input device changes
|
||||
val filter = IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED)
|
||||
registerReceiver(inputDeviceReceiver, filter)
|
||||
|
||||
// Check for already connected gamepads
|
||||
checkGamepadConnections()
|
||||
|
||||
// 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()
|
||||
|
@ -783,10 +925,99 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
/**
|
||||
* Completely block the hardware back button during gameplay
|
||||
* GamepadConnectionListener implementation
|
||||
*/
|
||||
override fun onGamepadConnected(gamepadName: String) {
|
||||
runOnUiThread {
|
||||
Toast.makeText(this, "Gamepad connected: $gamepadName", Toast.LENGTH_SHORT).show()
|
||||
// Provide haptic feedback for gamepad connection
|
||||
gameHaptics.vibrateForPieceLock()
|
||||
|
||||
// Show gamepad help if not shown before
|
||||
if (!hasSeenGamepadHelp()) {
|
||||
showGamepadHelpDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onGamepadDisconnected(gamepadName: String) {
|
||||
runOnUiThread {
|
||||
Toast.makeText(this, "Gamepad disconnected: $gamepadName", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show gamepad help dialog
|
||||
*/
|
||||
private fun showGamepadHelpDialog() {
|
||||
val dialogView = layoutInflater.inflate(R.layout.gamepad_help_dialog, null)
|
||||
val dialog = android.app.AlertDialog.Builder(this, android.R.style.Theme_DeviceDefault_Dialog_NoActionBar)
|
||||
.setView(dialogView)
|
||||
.setCancelable(true)
|
||||
.create()
|
||||
|
||||
// Set up dismiss button
|
||||
dialogView.findViewById<View>(R.id.gamepad_help_dismiss_button).setOnClickListener {
|
||||
dialog.dismiss()
|
||||
markGamepadHelpSeen()
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has seen the gamepad help
|
||||
*/
|
||||
private fun hasSeenGamepadHelp(): Boolean {
|
||||
val prefs = getSharedPreferences("com.mintris.preferences", Context.MODE_PRIVATE)
|
||||
return prefs.getBoolean("has_seen_gamepad_help", false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark that user has seen the gamepad help
|
||||
*/
|
||||
private fun markGamepadHelpSeen() {
|
||||
val prefs = getSharedPreferences("com.mintris.preferences", Context.MODE_PRIVATE)
|
||||
prefs.edit().putBoolean("has_seen_gamepad_help", true).apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for connected/disconnected gamepads
|
||||
*/
|
||||
private fun checkGamepadConnections() {
|
||||
val currentGamepads = GamepadController.getConnectedGamepadsInfo().toSet()
|
||||
|
||||
// Find newly connected gamepads
|
||||
currentGamepads.filter { it !in connectedGamepads }.forEach { gamepadName ->
|
||||
onGamepadConnected(gamepadName)
|
||||
}
|
||||
|
||||
// Find disconnected gamepads
|
||||
connectedGamepads.filter { it !in currentGamepads }.forEach { gamepadName ->
|
||||
onGamepadDisconnected(gamepadName)
|
||||
}
|
||||
|
||||
// Update the stored list of connected gamepads
|
||||
connectedGamepads.clear()
|
||||
connectedGamepads.addAll(currentGamepads)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle key events for both device controls and gamepads
|
||||
*/
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||
// If back button is pressed
|
||||
if (event == null) return super.onKeyDown(keyCode, event)
|
||||
|
||||
// First check if it's a gamepad input
|
||||
if (GamepadController.isGamepad(event.device)) {
|
||||
// If gamepad input was handled by the controller, consume the event
|
||||
if (gamepadController.handleKeyEvent(keyCode, event)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// If not handled as gamepad, handle as regular key event
|
||||
// Handle back button
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
// Handle back button press as a pause action during gameplay
|
||||
if (gameView.visibility == View.VISIBLE && !gameView.isPaused && !gameView.isGameOver()) {
|
||||
|
@ -807,9 +1038,24 @@ class MainActivity : AppCompatActivity() {
|
|||
return true // Consume the event
|
||||
}
|
||||
}
|
||||
|
||||
return super.onKeyDown(keyCode, event)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle motion events for gamepad analog sticks
|
||||
*/
|
||||
override fun onGenericMotionEvent(event: MotionEvent): Boolean {
|
||||
// Check if it's a gamepad motion event (analog sticks)
|
||||
if ((event.source and InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK) {
|
||||
if (gamepadController.handleMotionEvent(event)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return super.onGenericMotionEvent(event)
|
||||
}
|
||||
|
||||
private fun showProgressionScreen(xpGained: Long, newRewards: List<String>) {
|
||||
// Apply theme before showing the screen
|
||||
progressionScreen.applyTheme(currentTheme)
|
||||
|
|
|
@ -1413,4 +1413,71 @@ class GameView @JvmOverloads constructor(
|
|||
gameOverBlocksSpeed[i] *= 1.03f
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the game is active (running, not paused, not game over)
|
||||
*/
|
||||
fun isActive(): Boolean {
|
||||
return isRunning && !isPaused && !gameBoard.isGameOver
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the current piece left (for gamepad/keyboard support)
|
||||
*/
|
||||
fun moveLeft() {
|
||||
if (!isActive()) return
|
||||
gameBoard.moveLeft()
|
||||
gameHaptics?.vibrateForPieceMove()
|
||||
invalidate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the current piece right (for gamepad/keyboard support)
|
||||
*/
|
||||
fun moveRight() {
|
||||
if (!isActive()) return
|
||||
gameBoard.moveRight()
|
||||
gameHaptics?.vibrateForPieceMove()
|
||||
invalidate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotate the current piece (for gamepad/keyboard support)
|
||||
*/
|
||||
fun rotate() {
|
||||
if (!isActive()) return
|
||||
gameBoard.rotate()
|
||||
gameHaptics?.vibrateForPieceMove()
|
||||
invalidate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a soft drop (move down faster) (for gamepad/keyboard support)
|
||||
*/
|
||||
fun softDrop() {
|
||||
if (!isActive()) return
|
||||
gameBoard.softDrop()
|
||||
gameHaptics?.vibrateForPieceMove()
|
||||
invalidate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a hard drop (instant drop) (for gamepad/keyboard support)
|
||||
*/
|
||||
fun hardDrop() {
|
||||
if (!isActive()) return
|
||||
gameBoard.hardDrop()
|
||||
// Hard drop haptic feedback is handled by the game board via onPieceLock
|
||||
invalidate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Hold the current piece (for gamepad/keyboard support)
|
||||
*/
|
||||
fun holdPiece() {
|
||||
if (!isActive()) return
|
||||
gameBoard.holdPiece()
|
||||
gameHaptics?.vibrateForPieceMove()
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
|
372
app/src/main/java/com/mintris/game/GamepadController.kt
Normal file
372
app/src/main/java/com/mintris/game/GamepadController.kt
Normal file
|
@ -0,0 +1,372 @@
|
|||
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
|
||||
|
||||
/**
|
||||
* 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.25f
|
||||
|
||||
// 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
|
||||
|
||||
// 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
|
||||
|
||||
// Callback interfaces
|
||||
interface GamepadConnectionListener {
|
||||
fun onGamepadConnected(gamepadName: String)
|
||||
fun onGamepadDisconnected(gamepadName: String)
|
||||
}
|
||||
|
||||
// Listener for gamepad connection
|
||||
private var connectionListener: GamepadConnectionListener? = null
|
||||
|
||||
// Currently active gamepad for rumble
|
||||
private var activeGamepad: InputDevice? = null
|
||||
|
||||
/**
|
||||
* Set a listener for gamepad connection events
|
||||
*/
|
||||
fun setGamepadConnectionListener(listener: GamepadConnectionListener) {
|
||||
connectionListener = 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
|
||||
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()
|
||||
|
||||
when (event.action) {
|
||||
KeyEvent.ACTION_DOWN -> {
|
||||
when (keyCode) {
|
||||
// D-pad and analog movement
|
||||
KeyEvent.KEYCODE_DPAD_LEFT -> {
|
||||
if (!isMovingLeft && currentTime - lastMoveTime > MOVE_COOLDOWN_MS) {
|
||||
gameView.moveLeft()
|
||||
vibrateForPieceMove()
|
||||
lastMoveTime = currentTime
|
||||
isMovingLeft = true
|
||||
return true
|
||||
}
|
||||
}
|
||||
KeyEvent.KEYCODE_DPAD_RIGHT -> {
|
||||
if (!isMovingRight && currentTime - lastMoveTime > MOVE_COOLDOWN_MS) {
|
||||
gameView.moveRight()
|
||||
vibrateForPieceMove()
|
||||
lastMoveTime = currentTime
|
||||
isMovingRight = true
|
||||
return true
|
||||
}
|
||||
}
|
||||
KeyEvent.KEYCODE_DPAD_DOWN -> {
|
||||
if (!isMovingDown && currentTime - lastMoveTime > MOVE_COOLDOWN_MS) {
|
||||
gameView.softDrop()
|
||||
vibrateForPieceMove()
|
||||
lastMoveTime = currentTime
|
||||
isMovingDown = true
|
||||
return true
|
||||
}
|
||||
}
|
||||
KeyEvent.KEYCODE_DPAD_UP -> {
|
||||
if (currentTime - lastHardDropTime > HARD_DROP_COOLDOWN_MS) {
|
||||
gameView.hardDrop()
|
||||
vibrateForHardDrop()
|
||||
lastHardDropTime = currentTime
|
||||
return true
|
||||
}
|
||||
}
|
||||
// Rotation buttons - supporting multiple buttons for different controllers
|
||||
KeyEvent.KEYCODE_BUTTON_A,
|
||||
KeyEvent.KEYCODE_BUTTON_X,
|
||||
KeyEvent.KEYCODE_BUTTON_B -> {
|
||||
if (currentTime - lastRotationTime > ROTATION_COOLDOWN_MS) {
|
||||
gameView.rotate()
|
||||
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
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_DPAD_RIGHT -> {
|
||||
isMovingRight = false
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_DPAD_DOWN -> {
|
||||
isMovingDown = false
|
||||
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 && currentTime - lastMoveTime > MOVE_COOLDOWN_MS) {
|
||||
gameView.moveRight()
|
||||
vibrateForPieceMove()
|
||||
lastMoveTime = currentTime
|
||||
isMovingRight = true
|
||||
isMovingLeft = false
|
||||
return true
|
||||
} else if (axisX < 0 && !isMovingLeft && currentTime - lastMoveTime > MOVE_COOLDOWN_MS) {
|
||||
gameView.moveLeft()
|
||||
vibrateForPieceMove()
|
||||
lastMoveTime = currentTime
|
||||
isMovingLeft = true
|
||||
isMovingRight = false
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
// Reset horizontal movement flags when stick returns to center
|
||||
isMovingLeft = false
|
||||
isMovingRight = false
|
||||
}
|
||||
|
||||
if (Math.abs(axisY) > STICK_DEADZONE) {
|
||||
if (axisY > 0 && !isMovingDown && currentTime - lastMoveTime > MOVE_COOLDOWN_MS) {
|
||||
gameView.softDrop()
|
||||
vibrateForPieceMove()
|
||||
lastMoveTime = currentTime
|
||||
isMovingDown = true
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
// Reset vertical movement flag when stick returns to center
|
||||
isMovingDown = false
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
7
app/src/main/res/drawable/dialog_background.xml
Normal file
7
app/src/main/res/drawable/dialog_background.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="#AA000000" />
|
||||
<corners android:radius="16dp" />
|
||||
<stroke android:width="2dp" android:color="#FFFFFF" />
|
||||
</shape>
|
99
app/src/main/res/layout/gamepad_help_dialog.xml
Normal file
99
app/src/main/res/layout/gamepad_help_dialog.xml
Normal file
|
@ -0,0 +1,99 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp"
|
||||
android:background="@drawable/dialog_background">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Gamepad Controls"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="#FFFFFF"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:gravity="center"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Movement"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="#FFFFFF"
|
||||
android:layout_marginBottom="8dp"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="D-Pad Left/Right or Left Stick: Move piece"
|
||||
android:textSize="16sp"
|
||||
android:textColor="#FFFFFF"
|
||||
android:layout_marginBottom="4dp"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="D-Pad Down or Left Stick Down: Soft drop"
|
||||
android:textSize="16sp"
|
||||
android:textColor="#FFFFFF"
|
||||
android:layout_marginBottom="4dp"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="D-Pad Up: Hard drop"
|
||||
android:textSize="16sp"
|
||||
android:textColor="#FFFFFF"/>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Actions"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="#FFFFFF"
|
||||
android:layout_marginBottom="8dp"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="A/B/X or Right Stick: Rotate piece"
|
||||
android:textSize="16sp"
|
||||
android:textColor="#FFFFFF"
|
||||
android:layout_marginBottom="4dp"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Y, L1, or R1: Hold piece"
|
||||
android:textSize="16sp"
|
||||
android:textColor="#FFFFFF"/>
|
||||
</LinearLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/gamepad_help_dismiss_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Got it!"
|
||||
android:layout_gravity="center"
|
||||
android:paddingLeft="24dp"
|
||||
android:paddingRight="24dp"/>
|
||||
|
||||
</LinearLayout>
|
Loading…
Add table
Add a link
Reference in a new issue