mirror of
https://github.com/cmclark00/mintris.git
synced 2025-05-17 20:35:19 +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.PlayerProgressionManager
|
||||
import android.graphics.Color
|
||||
import android.view.KeyEvent
|
||||
import android.view.InputDevice
|
||||
|
||||
class HighScoreEntryActivity : AppCompatActivity() {
|
||||
private lateinit var binding: HighScoreEntryBinding
|
||||
|
@ -39,20 +41,55 @@ class HighScoreEntryActivity : AppCompatActivity() {
|
|||
binding.scoreText.text = "Score: $score"
|
||||
|
||||
binding.saveButton.setOnClickListener {
|
||||
// Only allow saving once
|
||||
if (!hasSaved) {
|
||||
val name = binding.nameInput.text.toString().trim()
|
||||
if (name.isNotEmpty()) {
|
||||
hasSaved = true
|
||||
val highScore = HighScore(name, score, 1)
|
||||
highScoreManager.addHighScore(highScore)
|
||||
|
||||
// Set result and finish
|
||||
setResult(Activity.RESULT_OK)
|
||||
saveScore()
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveScore() {
|
||||
// Only allow saving once
|
||||
if (!hasSaved) {
|
||||
val name = binding.nameInput.text.toString().trim()
|
||||
if (name.isNotEmpty()) {
|
||||
hasSaved = true
|
||||
val highScore = HighScore(name, score, 1)
|
||||
highScoreManager.addHighScore(highScore)
|
||||
|
||||
// Set result and finish
|
||||
setResult(Activity.RESULT_OK)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
|
|
@ -12,6 +12,8 @@ import com.mintris.model.HighScoreManager
|
|||
import com.mintris.model.PlayerProgressionManager
|
||||
import android.graphics.Color
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import android.view.InputDevice
|
||||
|
||||
class HighScoresActivity : AppCompatActivity() {
|
||||
private lateinit var binding: HighScoresBinding
|
||||
|
@ -113,4 +115,29 @@ class HighScoresActivity : AppCompatActivity() {
|
|||
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.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
|
||||
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 {
|
||||
private const val TAG = "MainActivity"
|
||||
|
@ -51,6 +66,7 @@ class MainActivity : AppCompatActivity() {
|
|||
private lateinit var progressionScreen: ProgressionScreen
|
||||
private lateinit var themeSelector: ThemeSelector
|
||||
private lateinit var blockSkinSelector: BlockSkinSelector
|
||||
private var pauseMenuScrollView: ScrollView? = null
|
||||
|
||||
// Game state
|
||||
private var isSoundEnabled = true
|
||||
|
@ -65,6 +81,28 @@ 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>()
|
||||
|
||||
// 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?) {
|
||||
// Register activity result launcher for high score entry
|
||||
|
@ -92,6 +130,23 @@ class MainActivity : AppCompatActivity() {
|
|||
progressionManager = PlayerProgressionManager(this)
|
||||
themeSelector = binding.themeSelector
|
||||
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
|
||||
progressionScreen = binding.progressionScreen
|
||||
|
@ -221,8 +276,44 @@ class MainActivity : AppCompatActivity() {
|
|||
updateUI(score, level, lines)
|
||||
}
|
||||
|
||||
gameView.onGameOver = { score ->
|
||||
showGameOver(score)
|
||||
// Track pieces placed using callback
|
||||
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 ->
|
||||
|
@ -250,13 +341,6 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
gameView.onPieceLock = {
|
||||
if (isSoundEnabled) {
|
||||
gameHaptics.vibrateForPieceLock()
|
||||
}
|
||||
piecesPlaced++
|
||||
}
|
||||
|
||||
// Set up button click listeners with haptic feedback
|
||||
binding.playAgainButton.setOnClickListener {
|
||||
gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY)
|
||||
|
@ -326,6 +410,9 @@ class MainActivity : AppCompatActivity() {
|
|||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
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")
|
||||
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
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
// Show progression screen first with XP animation
|
||||
|
@ -507,7 +600,7 @@ class MainActivity : AppCompatActivity() {
|
|||
unlockedThemes = progressionManager.getUnlockedThemes(),
|
||||
currentTheme = currentTheme
|
||||
)
|
||||
|
||||
|
||||
// Update block skin selector - handle both standard and landscape versions
|
||||
blockSkinSelector.updateBlockSkins(
|
||||
progressionManager.getUnlockedBlocks(),
|
||||
|
@ -522,6 +615,15 @@ class MainActivity : AppCompatActivity() {
|
|||
gameView.getCurrentBlockSkin(),
|
||||
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() {
|
||||
gameView.start()
|
||||
gameMusic.setEnabled(isMusicEnabled)
|
||||
// Reset pieces placed counter
|
||||
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) {
|
||||
gameMusic.start()
|
||||
}
|
||||
gameStartTime = System.currentTimeMillis()
|
||||
piecesPlaced = 0
|
||||
|
||||
// Start the game
|
||||
gameView.start()
|
||||
gameMusic.setEnabled(isMusicEnabled)
|
||||
|
||||
// Reset session stats
|
||||
statsManager.startNewSession()
|
||||
progressionManager.startNewSession()
|
||||
gameBoard.updateLevel(selectedLevel)
|
||||
|
@ -602,10 +800,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()
|
||||
|
@ -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 {
|
||||
// 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) {
|
||||
// Handle back button press as a pause action during gameplay
|
||||
if (gameView.visibility == View.VISIBLE && !gameView.isPaused && !gameView.isGameOver()) {
|
||||
|
@ -797,9 +1110,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)
|
||||
|
@ -811,4 +1139,160 @@ class MainActivity : AppCompatActivity() {
|
|||
// Display progression data
|
||||
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
|
||||
}
|
||||
|
||||
// Track if gamepad is connected
|
||||
private var isGamepadConnected = false
|
||||
|
||||
// Set gamepad connection state
|
||||
fun setGamepadConnected(connected: Boolean) {
|
||||
isGamepadConnected = connected
|
||||
}
|
||||
|
||||
// Get vibration multiplier based on input method
|
||||
private fun getVibrationMultiplier(): Float {
|
||||
return if (isGamepadConnected) 1.5f else 1.0f
|
||||
}
|
||||
|
||||
// Vibrate for line clear (more intense for more lines)
|
||||
fun vibrateForLineClear(lineCount: Int) {
|
||||
Log.d(TAG, "Attempting to vibrate for $lineCount lines")
|
||||
|
@ -34,22 +47,22 @@ class GameHaptics(private val context: Context) {
|
|||
// Only proceed if the device has a vibrator and it's available
|
||||
if (!vibrator.hasVibrator()) return
|
||||
|
||||
// Scale duration and amplitude based on line count
|
||||
// More lines = longer and stronger vibration
|
||||
// Scale duration and amplitude based on line count and input method
|
||||
val multiplier = getVibrationMultiplier()
|
||||
val duration = when(lineCount) {
|
||||
1 -> 50L // Single line: short vibration
|
||||
2 -> 80L // Double line: slightly longer
|
||||
3 -> 120L // Triple line: even longer
|
||||
4 -> 200L // Tetris: longest vibration
|
||||
else -> 50L
|
||||
1 -> (50L * multiplier).toLong() // Single line: short vibration
|
||||
2 -> (80L * multiplier).toLong() // Double line: slightly longer
|
||||
3 -> (120L * multiplier).toLong() // Triple line: even longer
|
||||
4 -> (200L * multiplier).toLong() // Tetris: longest vibration
|
||||
else -> (50L * multiplier).toLong()
|
||||
}
|
||||
|
||||
val amplitude = when(lineCount) {
|
||||
1 -> 80 // Single line: mild vibration (80/255)
|
||||
2 -> 120 // Double line: medium vibration (120/255)
|
||||
3 -> 180 // Triple line: strong vibration (180/255)
|
||||
4 -> 255 // Tetris: maximum vibration (255/255)
|
||||
else -> 80
|
||||
1 -> (80 * multiplier).toInt().coerceAtMost(255) // Single line: mild vibration
|
||||
2 -> (120 * multiplier).toInt().coerceAtMost(255) // Double line: medium vibration
|
||||
3 -> (180 * multiplier).toInt().coerceAtMost(255) // Triple line: strong vibration
|
||||
4 -> 255 // Tetris: maximum vibration
|
||||
else -> (80 * multiplier).toInt().coerceAtMost(255)
|
||||
}
|
||||
|
||||
Log.d(TAG, "Vibration parameters - Duration: ${duration}ms, Amplitude: $amplitude")
|
||||
|
@ -79,15 +92,20 @@ class GameHaptics(private val context: Context) {
|
|||
|
||||
fun vibrateForPieceLock() {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
fun vibrateForPieceMove() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val amplitude = (VibrationEffect.DEFAULT_AMPLITUDE * 0.3).toInt().coerceAtLeast(1)
|
||||
val vibrationEffect = VibrationEffect.createOneShot(20L, amplitude)
|
||||
val multiplier = getVibrationMultiplier()
|
||||
val duration = (20L * multiplier).toLong()
|
||||
val amplitude = (VibrationEffect.DEFAULT_AMPLITUDE * 0.3 * multiplier).toInt().coerceAtLeast(1).coerceAtMost(255)
|
||||
val vibrationEffect = VibrationEffect.createOneShot(duration, amplitude)
|
||||
vibrator.vibrate(vibrationEffect)
|
||||
}
|
||||
}
|
||||
|
@ -100,8 +118,10 @@ class GameHaptics(private val context: Context) {
|
|||
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
// Create a strong, long vibration effect
|
||||
val vibrationEffect = VibrationEffect.createOneShot(300L, VibrationEffect.DEFAULT_AMPLITUDE)
|
||||
val multiplier = getVibrationMultiplier()
|
||||
val duration = (300L * multiplier).toLong()
|
||||
val amplitude = (VibrationEffect.DEFAULT_AMPLITUDE * multiplier).toInt().coerceAtMost(255)
|
||||
val vibrationEffect = VibrationEffect.createOneShot(duration, amplitude)
|
||||
vibrator.vibrate(vibrationEffect)
|
||||
Log.d(TAG, "Game over vibration triggered successfully")
|
||||
} else {
|
||||
|
|
|
@ -1019,16 +1019,6 @@ class GameView @JvmOverloads constructor(
|
|||
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) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
startX = event.x
|
||||
|
@ -1061,81 +1051,36 @@ 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()
|
||||
// Handle movement based on locked direction
|
||||
when (lockedDirection) {
|
||||
Direction.HORIZONTAL -> {
|
||||
if (abs(deltaX) > blockSize * minMovementThreshold) {
|
||||
if (deltaX > 0) {
|
||||
gameBoard.moveRight()
|
||||
} else {
|
||||
gameBoard.moveLeft()
|
||||
}
|
||||
lastTouchX = event.x
|
||||
if (currentTime - lastMoveTime >= moveCooldown) {
|
||||
gameHaptics?.vibrateForPieceMove()
|
||||
lastMoveTime = currentTime
|
||||
}
|
||||
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()
|
||||
Direction.VERTICAL -> {
|
||||
if (deltaY > blockSize * minMovementThreshold) {
|
||||
gameBoard.softDrop()
|
||||
lastTouchY = event.y
|
||||
if (currentTime - lastMoveTime >= moveCooldown) {
|
||||
gameHaptics?.vibrateForPieceMove()
|
||||
lastMoveTime = currentTime
|
||||
}
|
||||
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
|
||||
when (lockedDirection) {
|
||||
Direction.HORIZONTAL -> {
|
||||
if (abs(deltaX) > blockSize * minMovementThreshold) {
|
||||
if (deltaX > 0) {
|
||||
gameBoard.moveRight()
|
||||
} else {
|
||||
gameBoard.moveLeft()
|
||||
}
|
||||
lastTouchX = event.x
|
||||
if (currentTime - lastMoveTime >= moveCooldown) {
|
||||
gameHaptics?.vibrateForPieceMove()
|
||||
lastMoveTime = currentTime
|
||||
}
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
Direction.VERTICAL -> {
|
||||
if (deltaY > blockSize * minMovementThreshold) {
|
||||
gameBoard.softDrop()
|
||||
lastTouchY = event.y
|
||||
if (currentTime - lastMoveTime >= moveCooldown) {
|
||||
gameHaptics?.vibrateForPieceMove()
|
||||
lastMoveTime = currentTime
|
||||
}
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
null -> {
|
||||
// No direction lock yet, don't process movement
|
||||
}
|
||||
null -> {
|
||||
// No direction lock yet, don't process movement
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1145,23 +1090,12 @@ class GameView @JvmOverloads constructor(
|
|||
val deltaY = event.y - startY
|
||||
val moveTime = currentTime - lastTapTime
|
||||
|
||||
// Special handling for taps on game board or sides
|
||||
// Handle taps for rotation
|
||||
if (moveTime < minTapTime * 1.5 &&
|
||||
abs(deltaY) < maxTapMovement * 1.5 &&
|
||||
abs(deltaX) < maxTapMovement * 1.5) {
|
||||
|
||||
if (isLeftSide) {
|
||||
// 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
|
||||
if (currentTime - lastRotationTime >= rotationCooldown) {
|
||||
gameBoard.rotate()
|
||||
lastRotationTime = currentTime
|
||||
gameHaptics?.vibrateForPieceMove()
|
||||
|
@ -1170,15 +1104,11 @@ class GameView @JvmOverloads constructor(
|
|||
return true
|
||||
}
|
||||
|
||||
// Long swipe handling for hard drops and holds
|
||||
// Handle gestures
|
||||
// Check for hold gesture (swipe up)
|
||||
if (deltaY < -blockSize * minHoldDistance &&
|
||||
abs(deltaX) / abs(deltaY) < 0.5f) {
|
||||
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)}")
|
||||
if (currentTime - lastHoldTime >= holdCooldown) {
|
||||
gameBoard.holdPiece()
|
||||
lastHoldTime = currentTime
|
||||
gameHaptics?.vibrateForPieceMove()
|
||||
|
@ -1189,11 +1119,7 @@ class GameView @JvmOverloads constructor(
|
|||
else if (deltaY > blockSize * minHardDropDistance &&
|
||||
abs(deltaX) / abs(deltaY) < 0.5f &&
|
||||
(deltaY / moveTime) * 1000 > minSwipeVelocity) {
|
||||
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)}")
|
||||
if (currentTime - lastHardDropTime >= hardDropCooldown) {
|
||||
gameBoard.hardDrop()
|
||||
lastHardDropTime = currentTime
|
||||
invalidate()
|
||||
|
@ -1487,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()
|
||||
}
|
||||
}
|
||||
|
|
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
|
||||
val highScores: List<HighScore> = highScoreManager.getHighScores()
|
||||
val highScoreY = height * 0.5f
|
||||
var lastHighScoreY = highScoreY
|
||||
if (highScores.isNotEmpty()) {
|
||||
// Calculate the starting X position to center the entire block of scores
|
||||
val maxScoreWidth = highScorePaint.measureText("99. PLAYER: 999999")
|
||||
|
@ -252,6 +253,7 @@ class TitleScreen @JvmOverloads constructor(
|
|||
|
||||
highScores.forEachIndexed { index: Int, score: HighScore ->
|
||||
val y = highScoreY + (index * 80f)
|
||||
lastHighScoreY = y // Track the last high score's Y position
|
||||
// Pad the rank number to ensure alignment
|
||||
val rank = (index + 1).toString().padStart(2, ' ')
|
||||
// Pad the name to ensure score alignment
|
||||
|
@ -260,8 +262,15 @@ class TitleScreen @JvmOverloads constructor(
|
|||
}
|
||||
}
|
||||
|
||||
// Draw "touch to start" prompt
|
||||
canvas.drawText("touch to start", width / 2f, height * 0.7f, promptPaint)
|
||||
// Draw "touch to start" prompt below the high scores
|
||||
val promptY = if (resources.configuration.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE) {
|
||||
// In landscape mode, position below the last high score with some padding
|
||||
lastHighScoreY + 100f
|
||||
} else {
|
||||
// In portrait mode, use the original position
|
||||
height * 0.7f
|
||||
}
|
||||
canvas.drawText("touch to start", width / 2f, promptY, promptPaint)
|
||||
|
||||
// Request another frame
|
||||
invalidate()
|
||||
|
|
|
@ -304,4 +304,11 @@ class ProgressionScreen @JvmOverloads constructor(
|
|||
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"
|
||||
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 -->
|
||||
<FrameLayout
|
||||
android:id="@+id/gameContainer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="12dp"
|
||||
android:layout_margin="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="1:2"
|
||||
app:layout_constraintEnd_toStartOf="@+id/rightControlsPanel"
|
||||
app:layout_constraintStart_toEndOf="@+id/leftControlsPanel"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.mintris.game.GameView
|
||||
|
@ -30,10 +38,12 @@
|
|||
android:id="@+id/glowBorder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/glow_border" />
|
||||
android:background="@drawable/glow_border"
|
||||
android:clickable="false"
|
||||
android:focusable="false" />
|
||||
</FrameLayout>
|
||||
|
||||
<!-- Left Side Controls Panel -->
|
||||
<!-- Left Side Controls Panel - Overlay -->
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/leftControlsPanel"
|
||||
android:layout_width="0dp"
|
||||
|
@ -42,7 +52,10 @@
|
|||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="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 -->
|
||||
<TextView
|
||||
|
@ -83,7 +96,7 @@
|
|||
android:layout_marginBottom="24dp" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<!-- Right Side Controls Panel -->
|
||||
<!-- Right Side Controls Panel - Overlay -->
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/rightControlsPanel"
|
||||
android:layout_width="0dp"
|
||||
|
@ -92,7 +105,10 @@
|
|||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="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 -->
|
||||
<TextView
|
||||
|
@ -226,8 +242,12 @@
|
|||
|
||||
<!-- Scrollable content for pause menu -->
|
||||
<ScrollView
|
||||
android:id="@+id/pauseMenuScrollView"
|
||||
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
|
||||
android:layout_width="match_parent"
|
||||
|
@ -300,13 +320,13 @@
|
|||
android:fontFamily="sans-serif"
|
||||
android:textAllCaps="false" />
|
||||
|
||||
<!-- Level Selection -->
|
||||
<LinearLayout
|
||||
android:id="@+id/levelSelectorContainer"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center">
|
||||
android:gravity="center"
|
||||
android:layout_marginTop="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/selectLevelText"
|
||||
|
@ -314,15 +334,15 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:text="@string/select_level"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="24sp"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
android:textAllCaps="false" />
|
||||
android:fontFamily="sans-serif" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center"
|
||||
android:layout_marginTop="8dp">
|
||||
|
||||
<Button
|
||||
|
@ -330,7 +350,7 @@
|
|||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="@color/transparent"
|
||||
android:text="−"
|
||||
android:text="-"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
|
@ -338,14 +358,13 @@
|
|||
|
||||
<TextView
|
||||
android:id="@+id/pauseLevelText"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:gravity="center"
|
||||
android:text="1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif" />
|
||||
android:fontFamily="sans-serif"
|
||||
android:layout_marginHorizontal="16dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/pauseLevelUpButton"
|
||||
|
@ -367,15 +386,44 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
|
||||
<!-- Block Skin Selector -->
|
||||
<com.mintris.ui.BlockSkinSelector
|
||||
android:id="@+id/inPauseBlockSkinSelector"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginTop="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
|
||||
android:id="@+id/settingsButton"
|
||||
android:layout_width="200dp"
|
||||
|
@ -388,35 +436,6 @@
|
|||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
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>
|
||||
</ScrollView>
|
||||
</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