mirror of
https://github.com/cmclark00/mintris.git
synced 2025-05-18 06:15:20 +01:00
Compare commits
9 commits
b481fb4e80
...
7e4423efce
Author | SHA1 | Date | |
---|---|---|---|
|
7e4423efce | ||
|
c4f103ae1e | ||
|
dbaebb8b60 | ||
|
08c9f8a1ce | ||
|
f4f40c4c34 | ||
|
779fa8eab1 | ||
|
36559eac4c | ||
|
0ac25eb3a9 | ||
|
86424eac32 |
12 changed files with 1407 additions and 201 deletions
|
@ -12,6 +12,8 @@ import com.mintris.model.HighScore
|
||||||
import com.mintris.model.HighScoreManager
|
import com.mintris.model.HighScoreManager
|
||||||
import com.mintris.model.PlayerProgressionManager
|
import com.mintris.model.PlayerProgressionManager
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import android.view.InputDevice
|
||||||
|
|
||||||
class HighScoreEntryActivity : AppCompatActivity() {
|
class HighScoreEntryActivity : AppCompatActivity() {
|
||||||
private lateinit var binding: HighScoreEntryBinding
|
private lateinit var binding: HighScoreEntryBinding
|
||||||
|
@ -39,6 +41,11 @@ class HighScoreEntryActivity : AppCompatActivity() {
|
||||||
binding.scoreText.text = "Score: $score"
|
binding.scoreText.text = "Score: $score"
|
||||||
|
|
||||||
binding.saveButton.setOnClickListener {
|
binding.saveButton.setOnClickListener {
|
||||||
|
saveScore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveScore() {
|
||||||
// Only allow saving once
|
// Only allow saving once
|
||||||
if (!hasSaved) {
|
if (!hasSaved) {
|
||||||
val name = binding.nameInput.text.toString().trim()
|
val name = binding.nameInput.text.toString().trim()
|
||||||
|
@ -53,6 +60,36 @@ class HighScoreEntryActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Override onKeyDown to handle gamepad buttons
|
||||||
|
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||||
|
// Check if it's a gamepad input
|
||||||
|
if (event != null && isGamepadDevice(event.device)) {
|
||||||
|
when (keyCode) {
|
||||||
|
KeyEvent.KEYCODE_BUTTON_A,
|
||||||
|
KeyEvent.KEYCODE_DPAD_CENTER -> {
|
||||||
|
// A button saves the score
|
||||||
|
saveScore()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_BUTTON_B -> {
|
||||||
|
// B button cancels
|
||||||
|
setResult(Activity.RESULT_CANCELED)
|
||||||
|
finish()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.onKeyDown(keyCode, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to check if the device is a gamepad
|
||||||
|
private fun isGamepadDevice(device: InputDevice?): Boolean {
|
||||||
|
if (device == null) return false
|
||||||
|
val sources = device.sources
|
||||||
|
return (sources and InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD ||
|
||||||
|
(sources and InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent accidental back button press from causing issues
|
// Prevent accidental back button press from causing issues
|
||||||
|
|
|
@ -12,6 +12,8 @@ import com.mintris.model.HighScoreManager
|
||||||
import com.mintris.model.PlayerProgressionManager
|
import com.mintris.model.PlayerProgressionManager
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import android.view.InputDevice
|
||||||
|
|
||||||
class HighScoresActivity : AppCompatActivity() {
|
class HighScoresActivity : AppCompatActivity() {
|
||||||
private lateinit var binding: HighScoresBinding
|
private lateinit var binding: HighScoresBinding
|
||||||
|
@ -113,4 +115,29 @@ class HighScoresActivity : AppCompatActivity() {
|
||||||
Log.e("HighScoresActivity", "Error in onResume", e)
|
Log.e("HighScoresActivity", "Error in onResume", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle gamepad buttons
|
||||||
|
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||||
|
// Check if it's a gamepad input
|
||||||
|
if (event != null && isGamepadDevice(event.device)) {
|
||||||
|
when (keyCode) {
|
||||||
|
KeyEvent.KEYCODE_BUTTON_B,
|
||||||
|
KeyEvent.KEYCODE_BACK -> {
|
||||||
|
// B button or Back button returns to previous screen
|
||||||
|
finish()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.onKeyDown(keyCode, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to check if the device is a gamepad
|
||||||
|
private fun isGamepadDevice(device: InputDevice?): Boolean {
|
||||||
|
if (device == null) return false
|
||||||
|
val sources = device.sources
|
||||||
|
return (sources and InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD ||
|
||||||
|
(sources and InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -31,8 +31,23 @@ import android.util.Log
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
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
|
||||||
|
import android.graphics.drawable.ColorDrawable
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.ScrollView
|
||||||
|
import android.widget.ImageButton
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity(),
|
||||||
|
GamepadController.GamepadConnectionListener,
|
||||||
|
GamepadController.GamepadMenuListener,
|
||||||
|
GamepadController.GamepadNavigationListener {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "MainActivity"
|
private const val TAG = "MainActivity"
|
||||||
|
@ -51,6 +66,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
private lateinit var progressionScreen: ProgressionScreen
|
private lateinit var progressionScreen: ProgressionScreen
|
||||||
private lateinit var themeSelector: ThemeSelector
|
private lateinit var themeSelector: ThemeSelector
|
||||||
private lateinit var blockSkinSelector: BlockSkinSelector
|
private lateinit var blockSkinSelector: BlockSkinSelector
|
||||||
|
private var pauseMenuScrollView: ScrollView? = null
|
||||||
|
|
||||||
// Game state
|
// Game state
|
||||||
private var isSoundEnabled = true
|
private var isSoundEnabled = true
|
||||||
|
@ -66,6 +82,28 @@ class MainActivity : AppCompatActivity() {
|
||||||
// Activity result launcher for high score entry
|
// Activity result launcher for high score entry
|
||||||
private lateinit var highScoreEntryLauncher: ActivityResultLauncher<Intent>
|
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>()
|
||||||
|
|
||||||
|
// Track currently selected menu item in pause menu for gamepad navigation
|
||||||
|
private var currentMenuSelection = 0
|
||||||
|
private val pauseMenuItems = mutableListOf<View>()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
// Register activity result launcher for high score entry
|
// Register activity result launcher for high score entry
|
||||||
highScoreEntryLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
highScoreEntryLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||||
|
@ -92,6 +130,23 @@ class MainActivity : AppCompatActivity() {
|
||||||
progressionManager = PlayerProgressionManager(this)
|
progressionManager = PlayerProgressionManager(this)
|
||||||
themeSelector = binding.themeSelector
|
themeSelector = binding.themeSelector
|
||||||
blockSkinSelector = binding.blockSkinSelector
|
blockSkinSelector = binding.blockSkinSelector
|
||||||
|
pauseMenuScrollView = binding.pauseMenuScrollView
|
||||||
|
|
||||||
|
// Initialize gamepad controller
|
||||||
|
gamepadController = GamepadController(gameView)
|
||||||
|
gamepadController.setGamepadConnectionListener(this)
|
||||||
|
gamepadController.setGamepadMenuListener(this)
|
||||||
|
gamepadController.setGamepadNavigationListener(this)
|
||||||
|
|
||||||
|
// Set up touch event forwarding
|
||||||
|
binding.touchInterceptor?.setOnTouchListener { _, event ->
|
||||||
|
if (gameView.visibility == View.VISIBLE && !gameView.isPaused && !gameView.isGameOver()) {
|
||||||
|
gameView.onTouchEvent(event)
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Set up progression screen
|
// Set up progression screen
|
||||||
progressionScreen = binding.progressionScreen
|
progressionScreen = binding.progressionScreen
|
||||||
|
@ -221,8 +276,44 @@ class MainActivity : AppCompatActivity() {
|
||||||
updateUI(score, level, lines)
|
updateUI(score, level, lines)
|
||||||
}
|
}
|
||||||
|
|
||||||
gameView.onGameOver = { score ->
|
// Track pieces placed using callback
|
||||||
showGameOver(score)
|
gameBoard.onPieceLock = {
|
||||||
|
// Increment pieces placed counter
|
||||||
|
piecesPlaced++
|
||||||
|
|
||||||
|
// Provide haptic feedback
|
||||||
|
gameHaptics.vibrateForPieceLock()
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ->
|
gameView.onLineClear = { lineCount ->
|
||||||
|
@ -250,13 +341,6 @@ class MainActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
gameView.onPieceLock = {
|
|
||||||
if (isSoundEnabled) {
|
|
||||||
gameHaptics.vibrateForPieceLock()
|
|
||||||
}
|
|
||||||
piecesPlaced++
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up button click listeners with haptic feedback
|
// Set up button click listeners with haptic feedback
|
||||||
binding.playAgainButton.setOnClickListener {
|
binding.playAgainButton.setOnClickListener {
|
||||||
gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY)
|
gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY)
|
||||||
|
@ -326,6 +410,9 @@ class MainActivity : AppCompatActivity() {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
window.setDecorFitsSystemWindows(false)
|
window.setDecorFitsSystemWindows(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize pause menu items for gamepad navigation
|
||||||
|
initPauseMenuNavigation()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -404,6 +491,12 @@ class MainActivity : AppCompatActivity() {
|
||||||
Log.d("MainActivity", "Triggering game over animation")
|
Log.d("MainActivity", "Triggering game over animation")
|
||||||
gameView.startGameOverAnimation()
|
gameView.startGameOverAnimation()
|
||||||
|
|
||||||
|
// Hide game UI elements in landscape mode
|
||||||
|
if (resources.configuration.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE) {
|
||||||
|
binding.leftControlsPanel?.visibility = View.GONE
|
||||||
|
binding.rightControlsPanel?.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
// Wait a moment before showing progression screen to let animation be visible
|
// Wait a moment before showing progression screen to let animation be visible
|
||||||
Handler(Looper.getMainLooper()).postDelayed({
|
Handler(Looper.getMainLooper()).postDelayed({
|
||||||
// Show progression screen first with XP animation
|
// Show progression screen first with XP animation
|
||||||
|
@ -522,6 +615,15 @@ class MainActivity : AppCompatActivity() {
|
||||||
gameView.getCurrentBlockSkin(),
|
gameView.getCurrentBlockSkin(),
|
||||||
progressionManager.getPlayerLevel()
|
progressionManager.getPlayerLevel()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Initialize pause menu navigation
|
||||||
|
initPauseMenuNavigation()
|
||||||
|
|
||||||
|
// Reset and highlight first selectable menu item
|
||||||
|
if (pauseMenuItems.isNotEmpty()) {
|
||||||
|
currentMenuSelection = if (binding.resumeButton.visibility == View.VISIBLE) 0 else 1
|
||||||
|
highlightMenuItem(currentMenuSelection)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -567,14 +669,110 @@ class MainActivity : AppCompatActivity() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a new game
|
||||||
|
*/
|
||||||
private fun startGame() {
|
private fun startGame() {
|
||||||
gameView.start()
|
// Reset pieces placed counter
|
||||||
gameMusic.setEnabled(isMusicEnabled)
|
piecesPlaced = 0
|
||||||
|
|
||||||
|
// Set initial game state
|
||||||
|
currentScore = 0
|
||||||
|
currentLevel = selectedLevel
|
||||||
|
gameStartTime = System.currentTimeMillis()
|
||||||
|
|
||||||
|
// Update UI to show initial values
|
||||||
|
binding.scoreText.text = "$currentScore"
|
||||||
|
binding.currentLevelText.text = "$currentLevel"
|
||||||
|
binding.linesText.text = "0"
|
||||||
|
binding.comboText.text = "0"
|
||||||
|
|
||||||
|
// Reset game view and game board
|
||||||
|
gameBoard.reset()
|
||||||
|
gameView.reset()
|
||||||
|
|
||||||
|
// Show game elements
|
||||||
|
gameView.visibility = View.VISIBLE
|
||||||
|
binding.gameControlsContainer.visibility = View.VISIBLE
|
||||||
|
binding.gameOverContainer.visibility = View.GONE
|
||||||
|
binding.pauseContainer.visibility = View.GONE
|
||||||
|
titleScreen.visibility = View.GONE
|
||||||
|
progressionScreen.visibility = View.GONE
|
||||||
|
|
||||||
|
// Show game UI elements in landscape mode
|
||||||
|
if (resources.configuration.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE) {
|
||||||
|
binding.leftControlsPanel?.visibility = View.VISIBLE
|
||||||
|
binding.rightControlsPanel?.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure callbacks
|
||||||
|
gameView.onGameStateChanged = { score, level, lines ->
|
||||||
|
currentScore = score
|
||||||
|
currentLevel = level
|
||||||
|
|
||||||
|
binding.scoreText.text = "$score"
|
||||||
|
binding.currentLevelText.text = "$level"
|
||||||
|
binding.linesText.text = "$lines"
|
||||||
|
binding.comboText.text = gameBoard.getCombo().toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
// Record line clear in stats
|
||||||
|
statsManager.recordLineClear(lineCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset pause button state
|
||||||
|
binding.pauseStartButton.visibility = View.VISIBLE
|
||||||
|
binding.resumeButton.visibility = View.GONE
|
||||||
|
|
||||||
|
// Start background music if enabled
|
||||||
if (isMusicEnabled) {
|
if (isMusicEnabled) {
|
||||||
gameMusic.start()
|
gameMusic.start()
|
||||||
}
|
}
|
||||||
gameStartTime = System.currentTimeMillis()
|
|
||||||
piecesPlaced = 0
|
// Start the game
|
||||||
|
gameView.start()
|
||||||
|
gameMusic.setEnabled(isMusicEnabled)
|
||||||
|
|
||||||
|
// Reset session stats
|
||||||
statsManager.startNewSession()
|
statsManager.startNewSession()
|
||||||
progressionManager.startNewSession()
|
progressionManager.startNewSession()
|
||||||
gameBoard.updateLevel(selectedLevel)
|
gameBoard.updateLevel(selectedLevel)
|
||||||
|
@ -602,10 +800,25 @@ class MainActivity : AppCompatActivity() {
|
||||||
gameView.pause()
|
gameView.pause()
|
||||||
gameMusic.pause()
|
gameMusic.pause()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unregister broadcast receiver
|
||||||
|
try {
|
||||||
|
unregisterReceiver(inputDeviceReceiver)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
// Receiver wasn't registered
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.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 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) {
|
if (titleScreen.visibility == View.GONE && gameView.visibility == View.VISIBLE && binding.gameOverContainer.visibility == View.GONE && binding.pauseContainer.visibility == View.GONE) {
|
||||||
resumeGame()
|
resumeGame()
|
||||||
|
@ -773,10 +986,110 @@ 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()
|
||||||
|
// Set gamepad connected state in haptics
|
||||||
|
gameHaptics.setGamepadConnected(true)
|
||||||
|
// 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()
|
||||||
|
// Set gamepad disconnected state in haptics
|
||||||
|
gameHaptics.setGamepadConnected(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
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)) {
|
||||||
|
// Handle title screen start with gamepad
|
||||||
|
if (titleScreen.visibility == View.VISIBLE &&
|
||||||
|
(keyCode == KeyEvent.KEYCODE_BUTTON_A || keyCode == KeyEvent.KEYCODE_BUTTON_START)) {
|
||||||
|
titleScreen.performClick()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
||||||
// Handle back button press as a pause action during gameplay
|
// Handle back button press as a pause action during gameplay
|
||||||
if (gameView.visibility == View.VISIBLE && !gameView.isPaused && !gameView.isGameOver()) {
|
if (gameView.visibility == View.VISIBLE && !gameView.isPaused && !gameView.isGameOver()) {
|
||||||
|
@ -797,9 +1110,24 @@ class MainActivity : AppCompatActivity() {
|
||||||
return true // Consume the event
|
return true // Consume the event
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.onKeyDown(keyCode, 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>) {
|
private fun showProgressionScreen(xpGained: Long, newRewards: List<String>) {
|
||||||
// Apply theme before showing the screen
|
// Apply theme before showing the screen
|
||||||
progressionScreen.applyTheme(currentTheme)
|
progressionScreen.applyTheme(currentTheme)
|
||||||
|
@ -811,4 +1139,160 @@ class MainActivity : AppCompatActivity() {
|
||||||
// Display progression data
|
// Display progression data
|
||||||
progressionScreen.showProgress(progressionManager, xpGained, newRewards, currentTheme)
|
progressionScreen.showProgress(progressionManager, xpGained, newRewards, currentTheme)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements GamepadMenuListener to handle start button press
|
||||||
|
*/
|
||||||
|
override fun onPauseRequested() {
|
||||||
|
runOnUiThread {
|
||||||
|
if (gameView.visibility == View.VISIBLE && !gameView.isPaused && !gameView.isGameOver()) {
|
||||||
|
gameView.pause()
|
||||||
|
gameMusic.pause()
|
||||||
|
showPauseMenu()
|
||||||
|
binding.pauseStartButton.visibility = View.GONE
|
||||||
|
binding.resumeButton.visibility = View.VISIBLE
|
||||||
|
} else if (binding.pauseContainer.visibility == View.VISIBLE) {
|
||||||
|
// If pause menu is showing, handle as a resume
|
||||||
|
hidePauseMenu()
|
||||||
|
resumeGame()
|
||||||
|
} else if (titleScreen.visibility == View.VISIBLE) {
|
||||||
|
// If title screen is showing, start the game
|
||||||
|
titleScreen.performClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements GamepadNavigationListener to handle menu navigation
|
||||||
|
*/
|
||||||
|
override fun onMenuUp() {
|
||||||
|
runOnUiThread {
|
||||||
|
if (binding.pauseContainer.visibility == View.VISIBLE) {
|
||||||
|
moveMenuSelectionUp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMenuDown() {
|
||||||
|
runOnUiThread {
|
||||||
|
if (binding.pauseContainer.visibility == View.VISIBLE) {
|
||||||
|
moveMenuSelectionDown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMenuSelect() {
|
||||||
|
runOnUiThread {
|
||||||
|
if (binding.pauseContainer.visibility == View.VISIBLE) {
|
||||||
|
activateSelectedMenuItem()
|
||||||
|
} else if (titleScreen.visibility == View.VISIBLE) {
|
||||||
|
titleScreen.performClick()
|
||||||
|
} else if (progressionScreen.visibility == View.VISIBLE) {
|
||||||
|
// Handle continue in progression screen
|
||||||
|
progressionScreen.performContinue()
|
||||||
|
} else if (binding.gameOverContainer.visibility == View.VISIBLE) {
|
||||||
|
// Handle play again in game over screen
|
||||||
|
binding.playAgainButton.performClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize pause menu items for gamepad navigation
|
||||||
|
*/
|
||||||
|
private fun initPauseMenuNavigation() {
|
||||||
|
pauseMenuItems.clear()
|
||||||
|
pauseMenuItems.add(binding.resumeButton)
|
||||||
|
pauseMenuItems.add(binding.pauseStartButton)
|
||||||
|
pauseMenuItems.add(binding.pauseRestartButton)
|
||||||
|
pauseMenuItems.add(binding.highScoresButton)
|
||||||
|
pauseMenuItems.add(binding.statsButton)
|
||||||
|
pauseMenuItems.add(binding.pauseLevelUpButton)
|
||||||
|
pauseMenuItems.add(binding.pauseLevelDownButton)
|
||||||
|
pauseMenuItems.add(binding.settingsButton)
|
||||||
|
pauseMenuItems.add(binding.musicToggle)
|
||||||
|
|
||||||
|
// Add theme selector if present
|
||||||
|
val themeSelector = binding.themeSelector
|
||||||
|
if (themeSelector.visibility == View.VISIBLE) {
|
||||||
|
pauseMenuItems.add(themeSelector)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add block skin selector if present
|
||||||
|
val blockSkinSelector = binding.blockSkinSelector
|
||||||
|
if (blockSkinSelector.visibility == View.VISIBLE) {
|
||||||
|
pauseMenuItems.add(blockSkinSelector)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Highlight the currently selected menu item
|
||||||
|
*/
|
||||||
|
private fun highlightMenuItem(index: Int) {
|
||||||
|
// Reset all items to normal state
|
||||||
|
pauseMenuItems.forEachIndexed { i, item ->
|
||||||
|
item.alpha = if (i == index) 1.0f else 0.7f
|
||||||
|
item.scaleX = if (i == index) 1.2f else 1.0f
|
||||||
|
item.scaleY = if (i == index) 1.2f else 1.0f
|
||||||
|
// Add or remove border based on selection
|
||||||
|
if (item is Button) {
|
||||||
|
item.background = if (i == index) {
|
||||||
|
ContextCompat.getDrawable(this, R.drawable.menu_item_selected)
|
||||||
|
} else {
|
||||||
|
ColorDrawable(Color.TRANSPARENT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move menu selection up
|
||||||
|
*/
|
||||||
|
private fun moveMenuSelectionUp() {
|
||||||
|
if (pauseMenuItems.isEmpty()) return
|
||||||
|
currentMenuSelection = (currentMenuSelection - 1 + pauseMenuItems.size) % pauseMenuItems.size
|
||||||
|
highlightMenuItem(currentMenuSelection)
|
||||||
|
scrollToSelectedItem()
|
||||||
|
gameHaptics.vibrateForPieceMove()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move menu selection down
|
||||||
|
*/
|
||||||
|
private fun moveMenuSelectionDown() {
|
||||||
|
if (pauseMenuItems.isEmpty()) return
|
||||||
|
currentMenuSelection = (currentMenuSelection + 1) % pauseMenuItems.size
|
||||||
|
highlightMenuItem(currentMenuSelection)
|
||||||
|
scrollToSelectedItem()
|
||||||
|
gameHaptics.vibrateForPieceMove()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scrollToSelectedItem() {
|
||||||
|
if (pauseMenuItems.isEmpty()) return
|
||||||
|
|
||||||
|
val selectedItem = pauseMenuItems[currentMenuSelection]
|
||||||
|
val scrollView = pauseMenuScrollView ?: return
|
||||||
|
|
||||||
|
// Calculate the item's position relative to the scroll view
|
||||||
|
val itemTop = selectedItem.top
|
||||||
|
val itemBottom = selectedItem.bottom
|
||||||
|
val scrollViewHeight = scrollView.height
|
||||||
|
|
||||||
|
// If the item is partially or fully below the visible area, scroll down
|
||||||
|
if (itemBottom > scrollViewHeight) {
|
||||||
|
scrollView.smoothScrollTo(0, itemBottom - scrollViewHeight)
|
||||||
|
}
|
||||||
|
// If the item is partially or fully above the visible area, scroll up
|
||||||
|
else if (itemTop < 0) {
|
||||||
|
scrollView.smoothScrollTo(0, itemTop)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activate the currently selected menu item
|
||||||
|
*/
|
||||||
|
private fun activateSelectedMenuItem() {
|
||||||
|
if (pauseMenuItems.isEmpty() || currentMenuSelection < 0 || currentMenuSelection >= pauseMenuItems.size) return
|
||||||
|
pauseMenuItems[currentMenuSelection].performClick()
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -27,6 +27,19 @@ class GameHaptics(private val context: Context) {
|
||||||
context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
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)
|
// Vibrate for line clear (more intense for more lines)
|
||||||
fun vibrateForLineClear(lineCount: Int) {
|
fun vibrateForLineClear(lineCount: Int) {
|
||||||
Log.d(TAG, "Attempting to vibrate for $lineCount lines")
|
Log.d(TAG, "Attempting to vibrate for $lineCount lines")
|
||||||
|
@ -34,22 +47,22 @@ class GameHaptics(private val context: Context) {
|
||||||
// Only proceed if the device has a vibrator and it's available
|
// Only proceed if the device has a vibrator and it's available
|
||||||
if (!vibrator.hasVibrator()) return
|
if (!vibrator.hasVibrator()) return
|
||||||
|
|
||||||
// Scale duration and amplitude based on line count
|
// Scale duration and amplitude based on line count and input method
|
||||||
// More lines = longer and stronger vibration
|
val multiplier = getVibrationMultiplier()
|
||||||
val duration = when(lineCount) {
|
val duration = when(lineCount) {
|
||||||
1 -> 50L // Single line: short vibration
|
1 -> (50L * multiplier).toLong() // Single line: short vibration
|
||||||
2 -> 80L // Double line: slightly longer
|
2 -> (80L * multiplier).toLong() // Double line: slightly longer
|
||||||
3 -> 120L // Triple line: even longer
|
3 -> (120L * multiplier).toLong() // Triple line: even longer
|
||||||
4 -> 200L // Tetris: longest vibration
|
4 -> (200L * multiplier).toLong() // Tetris: longest vibration
|
||||||
else -> 50L
|
else -> (50L * multiplier).toLong()
|
||||||
}
|
}
|
||||||
|
|
||||||
val amplitude = when(lineCount) {
|
val amplitude = when(lineCount) {
|
||||||
1 -> 80 // Single line: mild vibration (80/255)
|
1 -> (80 * multiplier).toInt().coerceAtMost(255) // Single line: mild vibration
|
||||||
2 -> 120 // Double line: medium vibration (120/255)
|
2 -> (120 * multiplier).toInt().coerceAtMost(255) // Double line: medium vibration
|
||||||
3 -> 180 // Triple line: strong vibration (180/255)
|
3 -> (180 * multiplier).toInt().coerceAtMost(255) // Triple line: strong vibration
|
||||||
4 -> 255 // Tetris: maximum vibration (255/255)
|
4 -> 255 // Tetris: maximum vibration
|
||||||
else -> 80
|
else -> (80 * multiplier).toInt().coerceAtMost(255)
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "Vibration parameters - Duration: ${duration}ms, Amplitude: $amplitude")
|
Log.d(TAG, "Vibration parameters - Duration: ${duration}ms, Amplitude: $amplitude")
|
||||||
|
@ -79,15 +92,20 @@ class GameHaptics(private val context: Context) {
|
||||||
|
|
||||||
fun vibrateForPieceLock() {
|
fun vibrateForPieceLock() {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
val vibrationEffect = VibrationEffect.createOneShot(50L, VibrationEffect.DEFAULT_AMPLITUDE)
|
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)
|
vibrator.vibrate(vibrationEffect)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun vibrateForPieceMove() {
|
fun vibrateForPieceMove() {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
val amplitude = (VibrationEffect.DEFAULT_AMPLITUDE * 0.3).toInt().coerceAtLeast(1)
|
val multiplier = getVibrationMultiplier()
|
||||||
val vibrationEffect = VibrationEffect.createOneShot(20L, amplitude)
|
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)
|
vibrator.vibrate(vibrationEffect)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -100,8 +118,10 @@ class GameHaptics(private val context: Context) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
// Create a strong, long vibration effect
|
val multiplier = getVibrationMultiplier()
|
||||||
val vibrationEffect = VibrationEffect.createOneShot(300L, VibrationEffect.DEFAULT_AMPLITUDE)
|
val duration = (300L * multiplier).toLong()
|
||||||
|
val amplitude = (VibrationEffect.DEFAULT_AMPLITUDE * multiplier).toInt().coerceAtMost(255)
|
||||||
|
val vibrationEffect = VibrationEffect.createOneShot(duration, amplitude)
|
||||||
vibrator.vibrate(vibrationEffect)
|
vibrator.vibrate(vibrationEffect)
|
||||||
Log.d(TAG, "Game over vibration triggered successfully")
|
Log.d(TAG, "Game over vibration triggered successfully")
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1019,16 +1019,6 @@ class GameView @JvmOverloads constructor(
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define the game board boundaries
|
|
||||||
val boardRight = boardLeft + (gameBoard.width * blockSize)
|
|
||||||
val boardBottom = boardTop + (gameBoard.height * blockSize)
|
|
||||||
|
|
||||||
// Determine if touch is on left side, right side, or within board area
|
|
||||||
val isLeftSide = event.x < boardLeft
|
|
||||||
val isRightSide = event.x > boardRight
|
|
||||||
val isWithinBoard = event.x >= boardLeft && event.x <= boardRight &&
|
|
||||||
event.y >= boardTop && event.y <= boardBottom
|
|
||||||
|
|
||||||
when (event.action) {
|
when (event.action) {
|
||||||
MotionEvent.ACTION_DOWN -> {
|
MotionEvent.ACTION_DOWN -> {
|
||||||
startX = event.x
|
startX = event.x
|
||||||
|
@ -1061,50 +1051,6 @@ class GameView @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special handling for landscape mode - side controls
|
|
||||||
if (isLeftSide) {
|
|
||||||
// Left side controls - move left
|
|
||||||
if (deltaY < -blockSize * minMovementThreshold) {
|
|
||||||
// Swipe up on left side - rotate
|
|
||||||
if (currentTime - lastRotationTime >= rotationCooldown) {
|
|
||||||
gameBoard.rotate()
|
|
||||||
lastRotationTime = currentTime
|
|
||||||
gameHaptics?.vibrateForPieceMove()
|
|
||||||
invalidate()
|
|
||||||
}
|
|
||||||
} else if (deltaY > blockSize * minMovementThreshold) {
|
|
||||||
// Swipe down on left side - move left
|
|
||||||
gameBoard.moveLeft()
|
|
||||||
lastTouchY = event.y
|
|
||||||
if (currentTime - lastMoveTime >= moveCooldown) {
|
|
||||||
gameHaptics?.vibrateForPieceMove()
|
|
||||||
lastMoveTime = currentTime
|
|
||||||
}
|
|
||||||
invalidate()
|
|
||||||
}
|
|
||||||
} else if (isRightSide) {
|
|
||||||
// Right side controls - move right
|
|
||||||
if (deltaY < -blockSize * minMovementThreshold) {
|
|
||||||
// Swipe up on right side - hold piece
|
|
||||||
if (currentTime - lastHoldTime >= holdCooldown) {
|
|
||||||
gameBoard.holdPiece()
|
|
||||||
lastHoldTime = currentTime
|
|
||||||
gameHaptics?.vibrateForPieceMove()
|
|
||||||
invalidate()
|
|
||||||
}
|
|
||||||
} else if (deltaY > blockSize * minMovementThreshold) {
|
|
||||||
// Swipe down on right side - move right
|
|
||||||
gameBoard.moveRight()
|
|
||||||
lastTouchY = event.y
|
|
||||||
if (currentTime - lastMoveTime >= moveCooldown) {
|
|
||||||
gameHaptics?.vibrateForPieceMove()
|
|
||||||
lastMoveTime = currentTime
|
|
||||||
}
|
|
||||||
invalidate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Standard touch controls for main board area or portrait mode
|
|
||||||
else {
|
|
||||||
// Handle movement based on locked direction
|
// Handle movement based on locked direction
|
||||||
when (lockedDirection) {
|
when (lockedDirection) {
|
||||||
Direction.HORIZONTAL -> {
|
Direction.HORIZONTAL -> {
|
||||||
|
@ -1138,30 +1084,18 @@ class GameView @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
MotionEvent.ACTION_UP -> {
|
MotionEvent.ACTION_UP -> {
|
||||||
val deltaX = event.x - startX
|
val deltaX = event.x - startX
|
||||||
val deltaY = event.y - startY
|
val deltaY = event.y - startY
|
||||||
val moveTime = currentTime - lastTapTime
|
val moveTime = currentTime - lastTapTime
|
||||||
|
|
||||||
// Special handling for taps on game board or sides
|
// Handle taps for rotation
|
||||||
if (moveTime < minTapTime * 1.5 &&
|
if (moveTime < minTapTime * 1.5 &&
|
||||||
abs(deltaY) < maxTapMovement * 1.5 &&
|
abs(deltaY) < maxTapMovement * 1.5 &&
|
||||||
abs(deltaX) < maxTapMovement * 1.5) {
|
abs(deltaX) < maxTapMovement * 1.5) {
|
||||||
|
|
||||||
if (isLeftSide) {
|
if (currentTime - lastRotationTime >= rotationCooldown) {
|
||||||
// Tap on left side - move left
|
|
||||||
gameBoard.moveLeft()
|
|
||||||
gameHaptics?.vibrateForPieceMove()
|
|
||||||
invalidate()
|
|
||||||
} else if (isRightSide) {
|
|
||||||
// Tap on right side - move right
|
|
||||||
gameBoard.moveRight()
|
|
||||||
gameHaptics?.vibrateForPieceMove()
|
|
||||||
invalidate()
|
|
||||||
} else if (isWithinBoard && currentTime - lastRotationTime >= rotationCooldown) {
|
|
||||||
// Tap on board - rotate
|
|
||||||
gameBoard.rotate()
|
gameBoard.rotate()
|
||||||
lastRotationTime = currentTime
|
lastRotationTime = currentTime
|
||||||
gameHaptics?.vibrateForPieceMove()
|
gameHaptics?.vibrateForPieceMove()
|
||||||
|
@ -1170,15 +1104,11 @@ class GameView @JvmOverloads constructor(
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Long swipe handling for hard drops and holds
|
// Handle gestures
|
||||||
// Check for hold gesture (swipe up)
|
// Check for hold gesture (swipe up)
|
||||||
if (deltaY < -blockSize * minHoldDistance &&
|
if (deltaY < -blockSize * minHoldDistance &&
|
||||||
abs(deltaX) / abs(deltaY) < 0.5f) {
|
abs(deltaX) / abs(deltaY) < 0.5f) {
|
||||||
if (currentTime - lastHoldTime < holdCooldown) {
|
if (currentTime - lastHoldTime >= holdCooldown) {
|
||||||
Log.d(TAG, "Hold blocked by cooldown - time since last: ${currentTime - lastHoldTime}ms, cooldown: ${holdCooldown}ms")
|
|
||||||
} else {
|
|
||||||
// Process the hold
|
|
||||||
Log.d(TAG, "Hold detected - deltaY: $deltaY, ratio: ${abs(deltaX) / abs(deltaY)}")
|
|
||||||
gameBoard.holdPiece()
|
gameBoard.holdPiece()
|
||||||
lastHoldTime = currentTime
|
lastHoldTime = currentTime
|
||||||
gameHaptics?.vibrateForPieceMove()
|
gameHaptics?.vibrateForPieceMove()
|
||||||
|
@ -1189,11 +1119,7 @@ class GameView @JvmOverloads constructor(
|
||||||
else if (deltaY > blockSize * minHardDropDistance &&
|
else if (deltaY > blockSize * minHardDropDistance &&
|
||||||
abs(deltaX) / abs(deltaY) < 0.5f &&
|
abs(deltaX) / abs(deltaY) < 0.5f &&
|
||||||
(deltaY / moveTime) * 1000 > minSwipeVelocity) {
|
(deltaY / moveTime) * 1000 > minSwipeVelocity) {
|
||||||
if (currentTime - lastHardDropTime < hardDropCooldown) {
|
if (currentTime - lastHardDropTime >= hardDropCooldown) {
|
||||||
Log.d(TAG, "Hard drop blocked by cooldown - time since last: ${currentTime - lastHardDropTime}ms, cooldown: ${hardDropCooldown}ms")
|
|
||||||
} else {
|
|
||||||
// Process the hard drop
|
|
||||||
Log.d(TAG, "Hard drop detected - deltaY: $deltaY, velocity: ${(deltaY / moveTime) * 1000}, ratio: ${abs(deltaX) / abs(deltaY)}")
|
|
||||||
gameBoard.hardDrop()
|
gameBoard.hardDrop()
|
||||||
lastHardDropTime = currentTime
|
lastHardDropTime = currentTime
|
||||||
invalidate()
|
invalidate()
|
||||||
|
@ -1487,4 +1413,71 @@ class GameView @JvmOverloads constructor(
|
||||||
gameOverBlocksSpeed[i] *= 1.03f
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
495
app/src/main/java/com/mintris/game/GamepadController.kt
Normal file
495
app/src/main/java/com/mintris/game/GamepadController.kt
Normal file
|
@ -0,0 +1,495 @@
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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_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,
|
||||||
|
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
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -245,6 +245,7 @@ class TitleScreen @JvmOverloads constructor(
|
||||||
// Draw high scores using pre-allocated manager
|
// Draw high scores using pre-allocated manager
|
||||||
val highScores: List<HighScore> = highScoreManager.getHighScores()
|
val highScores: List<HighScore> = highScoreManager.getHighScores()
|
||||||
val highScoreY = height * 0.5f
|
val highScoreY = height * 0.5f
|
||||||
|
var lastHighScoreY = highScoreY
|
||||||
if (highScores.isNotEmpty()) {
|
if (highScores.isNotEmpty()) {
|
||||||
// Calculate the starting X position to center the entire block of scores
|
// Calculate the starting X position to center the entire block of scores
|
||||||
val maxScoreWidth = highScorePaint.measureText("99. PLAYER: 999999")
|
val maxScoreWidth = highScorePaint.measureText("99. PLAYER: 999999")
|
||||||
|
@ -252,6 +253,7 @@ class TitleScreen @JvmOverloads constructor(
|
||||||
|
|
||||||
highScores.forEachIndexed { index: Int, score: HighScore ->
|
highScores.forEachIndexed { index: Int, score: HighScore ->
|
||||||
val y = highScoreY + (index * 80f)
|
val y = highScoreY + (index * 80f)
|
||||||
|
lastHighScoreY = y // Track the last high score's Y position
|
||||||
// Pad the rank number to ensure alignment
|
// Pad the rank number to ensure alignment
|
||||||
val rank = (index + 1).toString().padStart(2, ' ')
|
val rank = (index + 1).toString().padStart(2, ' ')
|
||||||
// Pad the name to ensure score alignment
|
// Pad the name to ensure score alignment
|
||||||
|
@ -260,8 +262,15 @@ class TitleScreen @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw "touch to start" prompt
|
// Draw "touch to start" prompt below the high scores
|
||||||
canvas.drawText("touch to start", width / 2f, height * 0.7f, promptPaint)
|
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
|
// Request another frame
|
||||||
invalidate()
|
invalidate()
|
||||||
|
|
|
@ -304,4 +304,11 @@ class ProgressionScreen @JvmOverloads constructor(
|
||||||
card?.setCardBackgroundColor(backgroundColor)
|
card?.setCardBackgroundColor(backgroundColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public method to handle continue action via gamepad
|
||||||
|
*/
|
||||||
|
fun performContinue() {
|
||||||
|
continueButton.performClick()
|
||||||
|
}
|
||||||
}
|
}
|
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>
|
9
app/src/main/res/drawable/menu_item_selected.xml
Normal file
9
app/src/main/res/drawable/menu_item_selected.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<stroke
|
||||||
|
android:width="2dp"
|
||||||
|
android:color="@color/white" />
|
||||||
|
<corners android:radius="8dp" />
|
||||||
|
<solid android:color="@android:color/transparent" />
|
||||||
|
</shape>
|
|
@ -8,16 +8,24 @@
|
||||||
android:fitsSystemWindows="true"
|
android:fitsSystemWindows="true"
|
||||||
tools:context=".MainActivity">
|
tools:context=".MainActivity">
|
||||||
|
|
||||||
|
<!-- Full Screen Touch Interceptor -->
|
||||||
|
<View
|
||||||
|
android:id="@+id/touchInterceptor"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true" />
|
||||||
|
|
||||||
<!-- Game Container with Glow - Centered -->
|
<!-- Game Container with Glow - Centered -->
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:id="@+id/gameContainer"
|
android:id="@+id/gameContainer"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_margin="12dp"
|
android:layout_margin="0dp"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintDimensionRatio="1:2"
|
app:layout_constraintDimensionRatio="1:2"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/rightControlsPanel"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@+id/leftControlsPanel"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent">
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
<com.mintris.game.GameView
|
<com.mintris.game.GameView
|
||||||
|
@ -30,10 +38,12 @@
|
||||||
android:id="@+id/glowBorder"
|
android:id="@+id/glowBorder"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="@drawable/glow_border" />
|
android:background="@drawable/glow_border"
|
||||||
|
android:clickable="false"
|
||||||
|
android:focusable="false" />
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
<!-- Left Side Controls Panel -->
|
<!-- Left Side Controls Panel - Overlay -->
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/leftControlsPanel"
|
android:id="@+id/leftControlsPanel"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
@ -42,7 +52,10 @@
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintWidth_percent="0.25">
|
app:layout_constraintWidth_percent="0.25"
|
||||||
|
android:clickable="false"
|
||||||
|
android:focusable="false"
|
||||||
|
android:elevation="1dp">
|
||||||
|
|
||||||
<!-- Hold Piece View -->
|
<!-- Hold Piece View -->
|
||||||
<TextView
|
<TextView
|
||||||
|
@ -83,7 +96,7 @@
|
||||||
android:layout_marginBottom="24dp" />
|
android:layout_marginBottom="24dp" />
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
<!-- Right Side Controls Panel -->
|
<!-- Right Side Controls Panel - Overlay -->
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/rightControlsPanel"
|
android:id="@+id/rightControlsPanel"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
@ -92,7 +105,10 @@
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintWidth_percent="0.25">
|
app:layout_constraintWidth_percent="0.25"
|
||||||
|
android:clickable="false"
|
||||||
|
android:focusable="false"
|
||||||
|
android:elevation="1dp">
|
||||||
|
|
||||||
<!-- Next Piece Preview -->
|
<!-- Next Piece Preview -->
|
||||||
<TextView
|
<TextView
|
||||||
|
@ -226,8 +242,12 @@
|
||||||
|
|
||||||
<!-- Scrollable content for pause menu -->
|
<!-- Scrollable content for pause menu -->
|
||||||
<ScrollView
|
<ScrollView
|
||||||
|
android:id="@+id/pauseMenuScrollView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent"
|
||||||
|
android:scrollbars="none"
|
||||||
|
android:overScrollMode="never"
|
||||||
|
android:fillViewport="true">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -300,13 +320,13 @@
|
||||||
android:fontFamily="sans-serif"
|
android:fontFamily="sans-serif"
|
||||||
android:textAllCaps="false" />
|
android:textAllCaps="false" />
|
||||||
|
|
||||||
|
<!-- Level Selection -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/levelSelectorContainer"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="32dp"
|
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:gravity="center">
|
android:gravity="center"
|
||||||
|
android:layout_marginTop="16dp">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/selectLevelText"
|
android:id="@+id/selectLevelText"
|
||||||
|
@ -314,15 +334,15 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/select_level"
|
android:text="@string/select_level"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:textSize="24sp"
|
android:textSize="16sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
android:fontFamily="sans-serif"
|
android:fontFamily="sans-serif" />
|
||||||
android:textAllCaps="false" />
|
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center"
|
||||||
android:layout_marginTop="8dp">
|
android:layout_marginTop="8dp">
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
@ -330,7 +350,7 @@
|
||||||
android:layout_width="48dp"
|
android:layout_width="48dp"
|
||||||
android:layout_height="48dp"
|
android:layout_height="48dp"
|
||||||
android:background="@color/transparent"
|
android:background="@color/transparent"
|
||||||
android:text="−"
|
android:text="-"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:textSize="24sp"
|
android:textSize="24sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
|
@ -338,14 +358,13 @@
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/pauseLevelText"
|
android:id="@+id/pauseLevelText"
|
||||||
android:layout_width="48dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="48dp"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center"
|
|
||||||
android:text="1"
|
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:textSize="24sp"
|
android:textSize="24sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
android:fontFamily="sans-serif" />
|
android:fontFamily="sans-serif"
|
||||||
|
android:layout_marginHorizontal="16dp" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/pauseLevelUpButton"
|
android:id="@+id/pauseLevelUpButton"
|
||||||
|
@ -373,9 +392,38 @@
|
||||||
android:id="@+id/inPauseBlockSkinSelector"
|
android:id="@+id/inPauseBlockSkinSelector"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="24dp"
|
android:layout_marginTop="16dp"
|
||||||
android:layout_marginBottom="16dp" />
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
|
<!-- Sound Settings -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_marginTop="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/musicText"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/music"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:fontFamily="sans-serif" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/musicToggle"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:background="@color/transparent"
|
||||||
|
android:contentDescription="@string/toggle_music"
|
||||||
|
android:src="@drawable/ic_volume_up" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Sound Toggle -->
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/settingsButton"
|
android:id="@+id/settingsButton"
|
||||||
android:layout_width="200dp"
|
android:layout_width="200dp"
|
||||||
|
@ -388,35 +436,6 @@
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
android:fontFamily="sans-serif"
|
android:fontFamily="sans-serif"
|
||||||
android:textAllCaps="false" />
|
android:textAllCaps="false" />
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:layout_marginTop="16dp">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/musicText"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/music"
|
|
||||||
android:textColor="@color/white"
|
|
||||||
android:textSize="24sp"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:fontFamily="sans-serif"
|
|
||||||
android:textAllCaps="false"
|
|
||||||
android:layout_marginEnd="16dp" />
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/musicToggle"
|
|
||||||
android:layout_width="48dp"
|
|
||||||
android:layout_height="48dp"
|
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"
|
|
||||||
android:contentDescription="@string/toggle_music"
|
|
||||||
android:padding="12dp"
|
|
||||||
android:src="@drawable/ic_volume_up" />
|
|
||||||
</LinearLayout>
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
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