mirror of
https://github.com/cmclark00/mintris.git
synced 2025-05-18 11:05:19 +01:00
Compare commits
57 commits
Author | SHA1 | Date | |
---|---|---|---|
|
7babaeca50 | ||
|
e26c6ebd8c | ||
|
df9957580e | ||
|
22fd887037 | ||
|
38163c33a3 | ||
|
f5f135ff27 | ||
|
5cf8aec02a | ||
|
6958a864c1 | ||
|
915e207aca | ||
|
3cd6080177 | ||
|
fad3e0863c | ||
|
1f400238e5 | ||
|
b60c106a08 | ||
|
5faa780c62 | ||
|
b2a9c40539 | ||
|
0b78bf7833 | ||
|
419cd8cbdb | ||
|
71a8efff91 | ||
|
a8f095cf42 | ||
|
b739d0ec3a | ||
|
240f16be82 | ||
|
05a504bb21 | ||
|
7d7090d7ea | ||
|
7e4423efce | ||
|
c4f103ae1e | ||
|
dbaebb8b60 | ||
|
08c9f8a1ce | ||
|
f4f40c4c34 | ||
|
779fa8eab1 | ||
|
36559eac4c | ||
|
0ac25eb3a9 | ||
|
86424eac32 | ||
|
b481fb4e80 | ||
|
94e8d313c2 | ||
|
7dccad8d12 | ||
|
292ea656f8 | ||
|
ce19427cca | ||
|
03ff049bef | ||
|
e23d33e2e2 | ||
|
53c46c9864 | ||
|
47c9bbddec | ||
|
103a21d9b7 | ||
|
68e8cb160f | ||
|
42b9bcfab4 | ||
|
1980f15a46 | ||
|
c6a4339931 | ||
|
83935d35a8 | ||
|
d0700202b7 | ||
|
af0082a6db | ||
|
ebff618fa4 | ||
|
7cdc9988cb | ||
|
2774703df5 | ||
|
1c57c438ce | ||
|
a47d83d905 | ||
|
5861644883 | ||
|
7614cef7e5 | ||
|
9ab9b53407 |
65 changed files with 7917 additions and 2898 deletions
131
README.md
131
README.md
|
@ -1,11 +1,11 @@
|
|||
# Mintris
|
||||
# pixelmintdrop
|
||||
|
||||
A modern Tetris implementation for Android, featuring smooth animations, responsive controls, and a beautiful minimalist design.
|
||||
A modern block-stacking puzzle game for Android, featuring smooth animations, responsive controls, and a beautiful minimalist design.
|
||||
|
||||
## Features
|
||||
|
||||
### Core Gameplay
|
||||
- Classic Tetris mechanics
|
||||
- Classic block-stacking mechanics
|
||||
- 7-bag randomizer for piece distribution
|
||||
- Ghost piece preview
|
||||
- Hard drop and soft drop
|
||||
|
@ -29,7 +29,7 @@ The game features a comprehensive scoring system:
|
|||
- Single line: 40 points
|
||||
- Double: 100 points
|
||||
- Triple: 300 points
|
||||
- Tetris (4 lines): 1200 points
|
||||
- Quad (4 lines): 1200 points
|
||||
|
||||
#### Multipliers
|
||||
|
||||
|
@ -48,15 +48,15 @@ The game features a comprehensive scoring system:
|
|||
- 4 combos: 2.5x
|
||||
- 5+ combos: 3.0x
|
||||
|
||||
3. **Back-to-Back Tetris**
|
||||
- 50% bonus (1.5x) for consecutive Tetris clears
|
||||
- Resets if a non-Tetris clear is performed
|
||||
3. **Back-to-Back Quad**
|
||||
- 50% bonus (1.5x) for consecutive quad clears
|
||||
- Resets if a non-quad clear is performed
|
||||
|
||||
4. **Perfect Clear**
|
||||
- 2x for single line
|
||||
- 3x for double
|
||||
- 4x for triple
|
||||
- 5x for Tetris
|
||||
- 5x for quad
|
||||
- Awarded when clearing lines without leaving blocks
|
||||
|
||||
5. **All Clear**
|
||||
|
@ -102,11 +102,124 @@ The game features a comprehensive scoring system:
|
|||
- Follows Material Design guidelines
|
||||
- Implements high score persistence using SharedPreferences
|
||||
|
||||
## Project Improvements and Best Practices
|
||||
|
||||
### Performance Optimizations
|
||||
|
||||
The codebase includes several performance optimizations:
|
||||
|
||||
1. **Release Build Configuration**
|
||||
- Minification enabled to reduce APK size
|
||||
- Resource shrinking to remove unused resources
|
||||
- ProGuard rules to optimize while preserving critical classes
|
||||
|
||||
2. **Memory Management**
|
||||
- Proper lifecycle handling to prevent memory leaks
|
||||
- Resource cleanup through `releaseResources()` methods
|
||||
- Efficient bitmap handling with reuse when possible
|
||||
|
||||
3. **Rendering Efficiency**
|
||||
- Custom view invalidation limited to areas that need updating
|
||||
- Hardware acceleration for canvas operations
|
||||
- Bitmap caching for frequently used graphics
|
||||
|
||||
### Code Organization
|
||||
|
||||
The codebase follows good architecture practices:
|
||||
|
||||
1. **Package Structure**
|
||||
- `model`: Data classes and game logic
|
||||
- `game`: Core gameplay implementation
|
||||
- `ui`: User interface components
|
||||
- `audio`: Sound and music management
|
||||
- `accessibility`: Accessibility helpers
|
||||
|
||||
2. **Responsibility Separation**
|
||||
- `GameLifecycleManager`: Handles lifecycle events
|
||||
- `GameUIManager`: Manages UI state and updates
|
||||
- `GameAccessibilityHelper`: Improves accessibility features
|
||||
- `GamepadController`: Manages gamepad input
|
||||
|
||||
### Google Play Compliance
|
||||
|
||||
The app meets Google Play standards:
|
||||
|
||||
1. **Manifest Configuration**
|
||||
- Proper permissions declaration
|
||||
- Screen orientation handling
|
||||
- Full backup rules for user data
|
||||
|
||||
2. **Accessibility Support**
|
||||
- Content descriptions for UI elements
|
||||
- Color contrast considerations
|
||||
- Screen reader compatibility
|
||||
|
||||
3. **Shortcuts and Deep Links**
|
||||
- App shortcuts for quick actions
|
||||
- Proper intent handling
|
||||
|
||||
### Usage Examples
|
||||
|
||||
#### Lifecycle Management
|
||||
|
||||
```kotlin
|
||||
// In your activity
|
||||
private lateinit var lifecycleManager: GameLifecycleManager
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
lifecycleManager = GameLifecycleManager(this)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
lifecycleManager.onPause(gameView, gameMusic, statsManager, highScoreManager)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
lifecycleManager.onResume(gameView, gameMusic, isMusicEnabled)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
lifecycleManager.onDestroy(gameView, gameMusic, statsManager, highScoreManager)
|
||||
super.onDestroy()
|
||||
}
|
||||
```
|
||||
|
||||
#### Accessibility Implementation
|
||||
|
||||
```kotlin
|
||||
// In your activity
|
||||
private lateinit var accessibilityHelper: GameAccessibilityHelper
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
accessibilityHelper = GameAccessibilityHelper(this)
|
||||
|
||||
// Setup accessibility descriptions for controls
|
||||
accessibilityHelper.setupAccessibilityDescriptions(
|
||||
leftButton, rightButton, rotateButton, dropButton,
|
||||
holdButton, pauseButton, gameView, holdPieceView, nextPieceView
|
||||
)
|
||||
}
|
||||
|
||||
// When level changes
|
||||
private fun onLevelUp(newLevel: Int) {
|
||||
accessibilityHelper.announceLevelUp(gameView, newLevel)
|
||||
}
|
||||
|
||||
// When game ends
|
||||
private fun onGameOver(score: Long) {
|
||||
accessibilityHelper.announceGameOver(gameView, score)
|
||||
}
|
||||
```
|
||||
|
||||
## Building from Source
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone https://github.com/cmclark00/mintris.git
|
||||
git clone https://github.com/cmclark00/pixelmintdrop.git
|
||||
```
|
||||
|
||||
2. Open the project in Android Studio
|
||||
|
|
|
@ -4,13 +4,13 @@ plugins {
|
|||
}
|
||||
|
||||
android {
|
||||
namespace 'com.mintris'
|
||||
compileSdk 34
|
||||
namespace "com.pixelmintdrop"
|
||||
compileSdk 35
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.mintris"
|
||||
applicationId "com.pixelmintdrop"
|
||||
minSdk 30
|
||||
targetSdk 34
|
||||
targetSdk 35
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
|
||||
|
@ -19,13 +19,15 @@ android {
|
|||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
dataBinding true
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
|
@ -38,6 +40,15 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
// Enable strict mode for debug builds
|
||||
android.applicationVariants.all { variant ->
|
||||
if (variant.buildType.name == "debug") {
|
||||
variant.mergedFlavor.manifestPlaceholders = [enableStrictMode: "true"]
|
||||
} else {
|
||||
variant.mergedFlavor.manifestPlaceholders = [enableStrictMode: "false"]
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
|
|
35
app/proguard-rules.pro
vendored
35
app/proguard-rules.pro
vendored
|
@ -6,7 +6,40 @@
|
|||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Keep models intact
|
||||
-keep class com.mintris.model.** { *; }
|
||||
-keep class com.pixelmintdrop.model.** { *; }
|
||||
|
||||
# Keep game classes intact to prevent issues
|
||||
-keep class com.pixelmintdrop.game.** { *; }
|
||||
|
||||
# Preserve critical classes that might be used through reflection
|
||||
-keep class com.pixelmintdrop.audio.GameMusic { *; }
|
||||
-keep class com.pixelmintdrop.ui.** { *; }
|
||||
|
||||
# Keep all public methods in the MainActivity
|
||||
-keepclassmembers class com.pixelmintdrop.MainActivity {
|
||||
public *;
|
||||
}
|
||||
|
||||
# Keep serializable and parcelable classes for proper game state saving
|
||||
-keepnames class * implements java.io.Serializable
|
||||
-keepclassmembers class * implements java.io.Serializable {
|
||||
static final long serialVersionUID;
|
||||
private static final java.io.ObjectStreamField[] serialPersistentFields;
|
||||
!static !transient <fields>;
|
||||
private void writeObject(java.io.ObjectOutputStream);
|
||||
private void readObject(java.io.ObjectInputStream);
|
||||
java.lang.Object writeReplace();
|
||||
java.lang.Object readResolve();
|
||||
}
|
||||
|
||||
# Preserve line number information for debugging stack traces
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# Keep Gson usage intact
|
||||
-keep class com.google.gson.** { *; }
|
||||
-keep class * implements com.google.gson.TypeAdapterFactory
|
||||
-keep class * implements com.google.gson.JsonSerializer
|
||||
-keep class * implements com.google.gson.JsonDeserializer
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
|
|
|
@ -7,38 +7,46 @@
|
|||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Mintris">
|
||||
android:theme="@style/Theme.pixelmintdrop">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.Mintris.NoActionBar"
|
||||
android:theme="@style/Theme.pixelmintdrop.NoActionBar"
|
||||
android:immersive="true"
|
||||
android:resizeableActivity="false"
|
||||
android:excludeFromRecents="false">
|
||||
android:excludeFromRecents="false"
|
||||
android:screenOrientation="portrait"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".HighScoreEntryActivity"
|
||||
android:theme="@style/Theme.AppCompat.NoActionBar"
|
||||
android:exported="false" />
|
||||
android:exported="false"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".HighScoresActivity"
|
||||
android:exported="false"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.AppCompat.NoActionBar" />
|
||||
|
||||
<activity
|
||||
android:name=".StatsActivity"
|
||||
android:exported="false"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.AppCompat.NoActionBar" />
|
||||
</application>
|
||||
</manifest>
|
|
@ -1,733 +0,0 @@
|
|||
package com.mintris
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.VibrationEffect
|
||||
import android.os.Vibrator
|
||||
import android.os.VibratorManager
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.mintris.databinding.ActivityMainBinding
|
||||
import com.mintris.game.GameHaptics
|
||||
import com.mintris.game.GameView
|
||||
import com.mintris.game.NextPieceView
|
||||
import com.mintris.game.TitleScreen
|
||||
import android.view.HapticFeedbackConstants
|
||||
import com.mintris.model.GameBoard
|
||||
import com.mintris.audio.GameMusic
|
||||
import com.mintris.model.HighScoreManager
|
||||
import com.mintris.model.PlayerProgressionManager
|
||||
import com.mintris.model.StatsManager
|
||||
import com.mintris.ui.ProgressionScreen
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import android.graphics.Color
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import android.graphics.Rect
|
||||
import android.view.KeyEvent
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
// UI components
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private lateinit var gameView: GameView
|
||||
private lateinit var gameHaptics: GameHaptics
|
||||
private lateinit var gameBoard: GameBoard
|
||||
private lateinit var gameMusic: GameMusic
|
||||
private lateinit var titleScreen: TitleScreen
|
||||
private lateinit var highScoreManager: HighScoreManager
|
||||
private lateinit var statsManager: StatsManager
|
||||
private lateinit var progressionManager: PlayerProgressionManager
|
||||
private lateinit var progressionScreen: ProgressionScreen
|
||||
|
||||
// Game state
|
||||
private var isSoundEnabled = true
|
||||
private var isMusicEnabled = true
|
||||
private var selectedLevel = 1
|
||||
private val maxLevel = 20
|
||||
private var currentScore = 0
|
||||
private var currentLevel = 1
|
||||
private var gameStartTime: Long = 0
|
||||
private var piecesPlaced: Int = 0
|
||||
private var currentTheme = PlayerProgressionManager.THEME_CLASSIC
|
||||
|
||||
// Activity result launcher for high score entry
|
||||
private lateinit var highScoreEntryLauncher: ActivityResultLauncher<Intent>
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
// Register activity result launcher for high score entry
|
||||
highScoreEntryLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
// No matter what the result is, we just show the game over container
|
||||
progressionScreen.visibility = View.GONE
|
||||
binding.gameOverContainer.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
// Disable Android back gesture to prevent accidental app exits
|
||||
disableAndroidBackGesture()
|
||||
|
||||
// Initialize game components
|
||||
gameBoard = GameBoard()
|
||||
gameHaptics = GameHaptics(this)
|
||||
gameView = binding.gameView
|
||||
titleScreen = binding.titleScreen
|
||||
gameMusic = GameMusic(this)
|
||||
highScoreManager = HighScoreManager(this)
|
||||
statsManager = StatsManager(this)
|
||||
progressionManager = PlayerProgressionManager(this)
|
||||
|
||||
// Load and apply theme preference
|
||||
currentTheme = loadThemePreference()
|
||||
applyTheme(currentTheme)
|
||||
|
||||
// Set up game view
|
||||
gameView.setGameBoard(gameBoard)
|
||||
gameView.setHaptics(gameHaptics)
|
||||
|
||||
// Set up progression screen
|
||||
progressionScreen = binding.progressionScreen
|
||||
progressionScreen.visibility = View.GONE
|
||||
progressionScreen.onContinue = {
|
||||
progressionScreen.visibility = View.GONE
|
||||
binding.gameOverContainer.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
// Set up theme selector
|
||||
val themeSelector = binding.themeSelector
|
||||
themeSelector.onThemeSelected = { themeId ->
|
||||
// Apply the new theme
|
||||
applyTheme(themeId)
|
||||
|
||||
// Provide haptic feedback as a cue that the theme changed
|
||||
gameHaptics.vibrateForPieceLock()
|
||||
|
||||
// Refresh the pause menu to immediately show theme changes
|
||||
if (binding.pauseContainer.visibility == View.VISIBLE) {
|
||||
showPauseMenu()
|
||||
}
|
||||
}
|
||||
|
||||
// Set up title screen
|
||||
titleScreen.onStartGame = {
|
||||
titleScreen.visibility = View.GONE
|
||||
gameView.visibility = View.VISIBLE
|
||||
binding.gameControlsContainer.visibility = View.VISIBLE
|
||||
startGame()
|
||||
}
|
||||
|
||||
// Initially hide the game view and show title screen
|
||||
gameView.visibility = View.GONE
|
||||
binding.gameControlsContainer.visibility = View.GONE
|
||||
titleScreen.visibility = View.VISIBLE
|
||||
|
||||
// Set up pause button to show settings menu
|
||||
binding.pauseButton.setOnClickListener {
|
||||
gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY)
|
||||
gameView.pause()
|
||||
gameMusic.pause()
|
||||
showPauseMenu()
|
||||
binding.pauseStartButton.visibility = View.GONE
|
||||
binding.resumeButton.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
// Set up next piece preview
|
||||
binding.nextPieceView.setGameView(gameView)
|
||||
gameBoard.onNextPieceChanged = {
|
||||
binding.nextPieceView.invalidate()
|
||||
}
|
||||
|
||||
// Set up music toggle
|
||||
binding.musicToggle.setOnClickListener {
|
||||
isMusicEnabled = !isMusicEnabled
|
||||
gameMusic.setEnabled(isMusicEnabled)
|
||||
updateMusicToggleUI()
|
||||
}
|
||||
|
||||
// Set up callbacks
|
||||
gameView.onGameStateChanged = { score, level, lines ->
|
||||
updateUI(score, level, lines)
|
||||
}
|
||||
|
||||
gameView.onGameOver = { score ->
|
||||
showGameOver(score)
|
||||
}
|
||||
|
||||
gameView.onLineClear = { lineCount ->
|
||||
android.util.Log.d("MainActivity", "Received line clear callback: $lineCount lines")
|
||||
// Use enhanced haptic feedback for line clears
|
||||
if (isSoundEnabled) {
|
||||
android.util.Log.d("MainActivity", "Sound is enabled, triggering haptic feedback")
|
||||
try {
|
||||
gameHaptics.vibrateForLineClear(lineCount)
|
||||
android.util.Log.d("MainActivity", "Haptic feedback triggered successfully")
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("MainActivity", "Error triggering haptic feedback", e)
|
||||
}
|
||||
} else {
|
||||
android.util.Log.d("MainActivity", "Sound is disabled, skipping haptic feedback")
|
||||
}
|
||||
// Record line clear in stats
|
||||
statsManager.recordLineClear(lineCount)
|
||||
}
|
||||
|
||||
// Add callbacks for piece movement and locking
|
||||
gameView.onPieceMove = {
|
||||
if (isSoundEnabled) {
|
||||
gameHaptics.vibrateForPieceMove()
|
||||
}
|
||||
}
|
||||
|
||||
gameView.onPieceLock = {
|
||||
if (isSoundEnabled) {
|
||||
gameHaptics.vibrateForPieceLock()
|
||||
}
|
||||
piecesPlaced++
|
||||
}
|
||||
|
||||
// Set up button click listeners with haptic feedback
|
||||
binding.playAgainButton.setOnClickListener {
|
||||
gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY)
|
||||
hideGameOver()
|
||||
gameView.reset()
|
||||
startGame()
|
||||
}
|
||||
|
||||
binding.resumeButton.setOnClickListener {
|
||||
gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY)
|
||||
hidePauseMenu()
|
||||
resumeGame()
|
||||
}
|
||||
|
||||
binding.settingsButton.setOnClickListener {
|
||||
gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY)
|
||||
toggleSound()
|
||||
}
|
||||
|
||||
// Set up pause menu buttons
|
||||
binding.pauseStartButton.setOnClickListener {
|
||||
gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY)
|
||||
hidePauseMenu()
|
||||
gameView.reset()
|
||||
startGame()
|
||||
}
|
||||
|
||||
binding.pauseRestartButton.setOnClickListener {
|
||||
gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY)
|
||||
hidePauseMenu()
|
||||
gameView.reset()
|
||||
startGame()
|
||||
}
|
||||
|
||||
binding.highScoresButton.setOnClickListener {
|
||||
gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY)
|
||||
showHighScores()
|
||||
}
|
||||
|
||||
binding.pauseLevelUpButton.setOnClickListener {
|
||||
gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY)
|
||||
if (selectedLevel < maxLevel) {
|
||||
selectedLevel++
|
||||
updateLevelSelector()
|
||||
}
|
||||
}
|
||||
|
||||
binding.pauseLevelDownButton.setOnClickListener {
|
||||
gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY)
|
||||
if (selectedLevel > 1) {
|
||||
selectedLevel--
|
||||
updateLevelSelector()
|
||||
}
|
||||
}
|
||||
|
||||
// Set up stats button
|
||||
binding.statsButton.setOnClickListener {
|
||||
gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY)
|
||||
val intent = Intent(this, StatsActivity::class.java)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
// Initialize level selector
|
||||
updateLevelSelector()
|
||||
|
||||
// Enable edge-to-edge display
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
window.setDecorFitsSystemWindows(false)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update UI with current game state
|
||||
*/
|
||||
private fun updateUI(score: Int, level: Int, lines: Int) {
|
||||
binding.scoreText.text = score.toString()
|
||||
binding.currentLevelText.text = level.toString()
|
||||
binding.linesText.text = lines.toString()
|
||||
binding.comboText.text = gameBoard.getCombo().toString()
|
||||
|
||||
// Update current level for stats
|
||||
currentLevel = level
|
||||
|
||||
// Force redraw of next piece preview
|
||||
binding.nextPieceView.invalidate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Show game over screen
|
||||
*/
|
||||
private fun showGameOver(score: Int) {
|
||||
val gameTime = System.currentTimeMillis() - gameStartTime
|
||||
|
||||
// Update session stats
|
||||
statsManager.updateSessionStats(
|
||||
score = score,
|
||||
lines = gameBoard.lines,
|
||||
pieces = piecesPlaced,
|
||||
time = gameTime,
|
||||
level = currentLevel
|
||||
)
|
||||
|
||||
// Calculate XP earned
|
||||
val xpGained = progressionManager.calculateGameXP(
|
||||
score = score,
|
||||
lines = gameBoard.lines,
|
||||
level = currentLevel,
|
||||
gameTime = gameTime,
|
||||
tetrisCount = statsManager.getSessionTetrises(),
|
||||
perfectClearCount = 0 // Implement perfect clear tracking if needed
|
||||
)
|
||||
|
||||
// Add XP and check for rewards
|
||||
val newRewards = progressionManager.addXP(xpGained)
|
||||
|
||||
// End session and save stats
|
||||
statsManager.endSession()
|
||||
|
||||
// Update session stats display
|
||||
val timeFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
|
||||
timeFormat.timeZone = TimeZone.getTimeZone("UTC")
|
||||
|
||||
binding.sessionScoreText.text = getString(R.string.session_score, score)
|
||||
binding.sessionLinesText.text = getString(R.string.session_lines, gameBoard.lines)
|
||||
binding.sessionPiecesText.text = getString(R.string.session_pieces, piecesPlaced)
|
||||
binding.sessionTimeText.text = getString(R.string.session_time, timeFormat.format(gameTime))
|
||||
binding.sessionLevelText.text = getString(R.string.session_level, currentLevel)
|
||||
|
||||
// Update session line clear stats
|
||||
binding.sessionSinglesText.text = getString(R.string.singles, statsManager.getSessionSingles())
|
||||
binding.sessionDoublesText.text = getString(R.string.doubles, statsManager.getSessionDoubles())
|
||||
binding.sessionTriplesText.text = getString(R.string.triples, statsManager.getSessionTriples())
|
||||
binding.sessionTetrisesText.text = getString(R.string.tetrises, statsManager.getSessionTetrises())
|
||||
|
||||
// Flag to track if high score screen will be shown
|
||||
var showingHighScore = false
|
||||
|
||||
// Show progression screen first with XP animation
|
||||
binding.gameOverContainer.visibility = View.GONE
|
||||
progressionScreen.visibility = View.VISIBLE
|
||||
progressionScreen.applyTheme(currentTheme)
|
||||
progressionScreen.showProgress(progressionManager, xpGained, newRewards, currentTheme)
|
||||
|
||||
// Override the continue button behavior if high score needs to be shown
|
||||
val originalOnContinue = progressionScreen.onContinue
|
||||
|
||||
progressionScreen.onContinue = {
|
||||
// If this is a high score, show high score entry screen
|
||||
if (highScoreManager.isHighScore(score)) {
|
||||
showingHighScore = true
|
||||
showHighScoreEntry(score)
|
||||
} else {
|
||||
// Just show game over screen normally
|
||||
progressionScreen.visibility = View.GONE
|
||||
binding.gameOverContainer.visibility = View.VISIBLE
|
||||
|
||||
// Update theme selector if new themes were unlocked
|
||||
if (newRewards.any { it.contains("Theme") }) {
|
||||
updateThemeSelector()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Vibrate to indicate game over
|
||||
vibrate(VibrationEffect.EFFECT_DOUBLE_CLICK)
|
||||
}
|
||||
|
||||
/**
|
||||
* Show high score entry screen
|
||||
*/
|
||||
private fun showHighScoreEntry(score: Int) {
|
||||
val intent = Intent(this, HighScoreEntryActivity::class.java).apply {
|
||||
putExtra("score", score)
|
||||
putExtra("level", currentLevel)
|
||||
}
|
||||
// Use the launcher instead of startActivity
|
||||
highScoreEntryLauncher.launch(intent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide game over screen
|
||||
*/
|
||||
private fun hideGameOver() {
|
||||
binding.gameOverContainer.visibility = View.GONE
|
||||
progressionScreen.visibility = View.GONE
|
||||
}
|
||||
|
||||
/**
|
||||
* Show settings menu
|
||||
*/
|
||||
private fun showPauseMenu() {
|
||||
binding.pauseContainer.visibility = View.VISIBLE
|
||||
binding.pauseStartButton.visibility = View.VISIBLE
|
||||
binding.resumeButton.visibility = View.GONE
|
||||
|
||||
// Update level badge
|
||||
binding.pauseLevelBadge.setLevel(progressionManager.getPlayerLevel())
|
||||
binding.pauseLevelBadge.setThemeColor(getThemeColor(currentTheme))
|
||||
|
||||
// Get theme color
|
||||
val textColor = getThemeColor(currentTheme)
|
||||
|
||||
// Apply theme color to pause container background
|
||||
val backgroundColor = when (currentTheme) {
|
||||
PlayerProgressionManager.THEME_CLASSIC -> Color.BLACK
|
||||
PlayerProgressionManager.THEME_NEON -> Color.parseColor("#0D0221")
|
||||
PlayerProgressionManager.THEME_MONOCHROME -> Color.parseColor("#1A1A1A")
|
||||
PlayerProgressionManager.THEME_RETRO -> Color.parseColor("#3F2832")
|
||||
PlayerProgressionManager.THEME_MINIMALIST -> Color.WHITE
|
||||
PlayerProgressionManager.THEME_GALAXY -> Color.parseColor("#0B0C10")
|
||||
else -> Color.BLACK
|
||||
}
|
||||
binding.pauseContainer.setBackgroundColor(backgroundColor)
|
||||
|
||||
// Apply theme colors to buttons
|
||||
binding.pauseStartButton.setTextColor(textColor)
|
||||
binding.pauseRestartButton.setTextColor(textColor)
|
||||
binding.resumeButton.setTextColor(textColor)
|
||||
binding.highScoresButton.setTextColor(textColor)
|
||||
binding.statsButton.setTextColor(textColor)
|
||||
binding.pauseLevelText.setTextColor(textColor)
|
||||
binding.pauseLevelUpButton.setTextColor(textColor)
|
||||
binding.pauseLevelDownButton.setTextColor(textColor)
|
||||
binding.settingsButton.setTextColor(textColor)
|
||||
binding.musicToggle.setColorFilter(textColor)
|
||||
|
||||
// Apply theme colors to text elements
|
||||
binding.settingsTitle.setTextColor(textColor)
|
||||
binding.selectLevelText.setTextColor(textColor)
|
||||
binding.musicText.setTextColor(textColor)
|
||||
|
||||
// Update theme selector
|
||||
updateThemeSelector()
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide settings menu
|
||||
*/
|
||||
private fun hidePauseMenu() {
|
||||
binding.pauseContainer.visibility = View.GONE
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle sound on/off
|
||||
*/
|
||||
private fun toggleSound() {
|
||||
isSoundEnabled = !isSoundEnabled
|
||||
binding.settingsButton.text = getString(
|
||||
if (isSoundEnabled) R.string.sound_on else R.string.sound_off
|
||||
)
|
||||
|
||||
// Vibrate to provide feedback
|
||||
vibrate(VibrationEffect.EFFECT_CLICK)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the level selector display
|
||||
*/
|
||||
private fun updateLevelSelector() {
|
||||
binding.pauseLevelText.text = selectedLevel.toString()
|
||||
gameBoard.updateLevel(selectedLevel)
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger device vibration with predefined effect
|
||||
*/
|
||||
private fun vibrate(effectId: Int) {
|
||||
val vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||
vibrator.vibrate(VibrationEffect.createPredefined(effectId))
|
||||
}
|
||||
|
||||
private fun updateMusicToggleUI() {
|
||||
binding.musicToggle.setImageResource(
|
||||
if (isMusicEnabled) R.drawable.ic_volume_up
|
||||
else R.drawable.ic_volume_off
|
||||
)
|
||||
}
|
||||
|
||||
private fun startGame() {
|
||||
gameView.start()
|
||||
gameMusic.setEnabled(isMusicEnabled)
|
||||
if (isMusicEnabled) {
|
||||
gameMusic.start()
|
||||
}
|
||||
gameStartTime = System.currentTimeMillis()
|
||||
piecesPlaced = 0
|
||||
statsManager.startNewSession()
|
||||
progressionManager.startNewSession()
|
||||
gameBoard.updateLevel(selectedLevel)
|
||||
}
|
||||
|
||||
private fun restartGame() {
|
||||
gameBoard.reset()
|
||||
gameView.visibility = View.VISIBLE
|
||||
gameView.start()
|
||||
showPauseMenu()
|
||||
}
|
||||
|
||||
private fun resumeGame() {
|
||||
gameView.resume()
|
||||
if (isMusicEnabled) {
|
||||
gameMusic.resume()
|
||||
}
|
||||
// Force a redraw to ensure pieces aren't frozen
|
||||
gameView.invalidate()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
if (gameView.visibility == View.VISIBLE) {
|
||||
gameView.pause()
|
||||
gameMusic.pause()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
// 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()
|
||||
}
|
||||
|
||||
// Update theme selector with available themes when pause screen appears
|
||||
updateThemeSelector()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
gameMusic.release()
|
||||
}
|
||||
|
||||
/**
|
||||
* Show title screen (for game restart)
|
||||
*/
|
||||
private fun showTitleScreen() {
|
||||
gameView.reset()
|
||||
gameView.visibility = View.GONE
|
||||
binding.gameControlsContainer.visibility = View.GONE
|
||||
binding.gameOverContainer.visibility = View.GONE
|
||||
binding.pauseContainer.visibility = View.GONE
|
||||
titleScreen.visibility = View.VISIBLE
|
||||
titleScreen.applyTheme(currentTheme)
|
||||
}
|
||||
|
||||
/**
|
||||
* Show high scores
|
||||
*/
|
||||
private fun showHighScores() {
|
||||
val intent = Intent(this, HighScoresActivity::class.java)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the theme selector with unlocked themes
|
||||
*/
|
||||
private fun updateThemeSelector() {
|
||||
binding.themeSelector.updateThemes(
|
||||
unlockedThemes = progressionManager.getUnlockedThemes(),
|
||||
currentTheme = currentTheme
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a theme to the game
|
||||
*/
|
||||
private fun applyTheme(themeId: String) {
|
||||
// Only apply if the theme is unlocked
|
||||
if (!progressionManager.isThemeUnlocked(themeId)) return
|
||||
|
||||
// Save the selected theme
|
||||
currentTheme = themeId
|
||||
saveThemePreference(themeId)
|
||||
|
||||
// Apply theme to title screen if it's visible
|
||||
if (titleScreen.visibility == View.VISIBLE) {
|
||||
titleScreen.applyTheme(themeId)
|
||||
}
|
||||
|
||||
// Apply theme colors based on theme ID
|
||||
when (themeId) {
|
||||
PlayerProgressionManager.THEME_CLASSIC -> {
|
||||
// Default black theme
|
||||
binding.root.setBackgroundColor(Color.BLACK)
|
||||
}
|
||||
PlayerProgressionManager.THEME_NEON -> {
|
||||
// Neon theme with dark purple background
|
||||
binding.root.setBackgroundColor(Color.parseColor("#0D0221"))
|
||||
}
|
||||
PlayerProgressionManager.THEME_MONOCHROME -> {
|
||||
// Monochrome dark gray
|
||||
binding.root.setBackgroundColor(Color.parseColor("#1A1A1A"))
|
||||
}
|
||||
PlayerProgressionManager.THEME_RETRO -> {
|
||||
// Retro arcade theme
|
||||
binding.root.setBackgroundColor(Color.parseColor("#3F2832"))
|
||||
}
|
||||
PlayerProgressionManager.THEME_MINIMALIST -> {
|
||||
// Minimalist white theme
|
||||
binding.root.setBackgroundColor(Color.WHITE)
|
||||
|
||||
// Update text colors for visibility
|
||||
binding.scoreText.setTextColor(Color.BLACK)
|
||||
binding.currentLevelText.setTextColor(Color.BLACK)
|
||||
binding.linesText.setTextColor(Color.BLACK)
|
||||
binding.comboText.setTextColor(Color.BLACK)
|
||||
}
|
||||
PlayerProgressionManager.THEME_GALAXY -> {
|
||||
// Galaxy dark blue theme
|
||||
binding.root.setBackgroundColor(Color.parseColor("#0B0C10"))
|
||||
}
|
||||
}
|
||||
|
||||
// Apply theme to progression screen if it's visible and initialized
|
||||
if (::progressionScreen.isInitialized && progressionScreen.visibility == View.VISIBLE) {
|
||||
progressionScreen.applyTheme(themeId)
|
||||
}
|
||||
|
||||
// Apply theme color to the stats button
|
||||
val textColor = getThemeColor(currentTheme)
|
||||
binding.statsButton.setTextColor(textColor)
|
||||
|
||||
// Update the game view to apply theme
|
||||
gameView.invalidate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the selected theme in preferences
|
||||
*/
|
||||
private fun saveThemePreference(themeId: String) {
|
||||
val prefs = getSharedPreferences("mintris_settings", Context.MODE_PRIVATE)
|
||||
prefs.edit().putString("selected_theme", themeId).apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the saved theme preference
|
||||
*/
|
||||
private fun loadThemePreference(): String {
|
||||
val prefs = getSharedPreferences("mintris_settings", Context.MODE_PRIVATE)
|
||||
return prefs.getString("selected_theme", PlayerProgressionManager.THEME_CLASSIC) ?: PlayerProgressionManager.THEME_CLASSIC
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the appropriate color for the current theme
|
||||
*/
|
||||
private fun getThemeColor(themeId: String): Int {
|
||||
return when (themeId) {
|
||||
PlayerProgressionManager.THEME_CLASSIC -> Color.WHITE
|
||||
PlayerProgressionManager.THEME_NEON -> Color.parseColor("#FF00FF")
|
||||
PlayerProgressionManager.THEME_MONOCHROME -> Color.LTGRAY
|
||||
PlayerProgressionManager.THEME_RETRO -> Color.parseColor("#FF5A5F")
|
||||
PlayerProgressionManager.THEME_MINIMALIST -> Color.BLACK
|
||||
PlayerProgressionManager.THEME_GALAXY -> Color.parseColor("#66FCF1")
|
||||
else -> Color.WHITE
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the Android system back gesture to prevent accidental exits
|
||||
*/
|
||||
private fun disableAndroidBackGesture() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
// Set the entire window to be excluded from the system gesture areas
|
||||
window.decorView.post {
|
||||
// Create a list of rectangles representing the edges of the screen to exclude from system gestures
|
||||
val gestureInsets = window.decorView.rootWindowInsets?.systemGestureInsets
|
||||
if (gestureInsets != null) {
|
||||
val leftEdge = Rect(0, 0, 50, window.decorView.height)
|
||||
val rightEdge = Rect(window.decorView.width - 50, 0, window.decorView.width, window.decorView.height)
|
||||
val bottomEdge = Rect(0, window.decorView.height - 50, window.decorView.width, window.decorView.height)
|
||||
|
||||
window.decorView.systemGestureExclusionRects = listOf(leftEdge, rightEdge, bottomEdge)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add an on back pressed callback to handle back button/gesture
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
onBackPressedDispatcher.addCallback(this, object : androidx.activity.OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
// If we're playing the game, handle it as a pause action instead of exiting
|
||||
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
|
||||
resumeGame()
|
||||
} else if (binding.gameOverContainer.visibility == View.VISIBLE) {
|
||||
// If game over is showing, go back to title
|
||||
hideGameOver()
|
||||
showTitleScreen()
|
||||
} else if (titleScreen.visibility == View.VISIBLE) {
|
||||
// If title screen is showing, allow normal back behavior (exit app)
|
||||
isEnabled = false
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
}
|
||||
})
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
// For Android 11 (R) to Android 12 (S), use the WindowInsetsController to disable gestures
|
||||
window.insetsController?.systemBarsBehavior =
|
||||
android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Completely block the hardware back button during gameplay
|
||||
*/
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
||||
// If back button is pressed
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
// Handle back button press as a pause action during gameplay
|
||||
if (gameView.visibility == View.VISIBLE && !gameView.isPaused && !gameView.isGameOver()) {
|
||||
gameView.pause()
|
||||
gameMusic.pause()
|
||||
showPauseMenu()
|
||||
binding.pauseStartButton.visibility = View.GONE
|
||||
binding.resumeButton.visibility = View.VISIBLE
|
||||
return true // Consume the event
|
||||
} else if (binding.pauseContainer.visibility == View.VISIBLE) {
|
||||
// If pause menu is showing, handle as a resume
|
||||
resumeGame()
|
||||
return true // Consume the event
|
||||
} else if (binding.gameOverContainer.visibility == View.VISIBLE) {
|
||||
// If game over is showing, go back to title
|
||||
hideGameOver()
|
||||
showTitleScreen()
|
||||
return true // Consume the event
|
||||
}
|
||||
}
|
||||
return super.onKeyDown(keyCode, event)
|
||||
}
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
package com.mintris.game
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.VibrationEffect
|
||||
import android.os.Vibrator
|
||||
import android.os.VibratorManager
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.View
|
||||
|
||||
class GameHaptics(private val context: Context) {
|
||||
private val vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
val vibratorManager = context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
|
||||
vibratorManager.defaultVibrator
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||
}
|
||||
|
||||
fun performHapticFeedback(view: View, feedbackType: Int) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
view.performHapticFeedback(HapticFeedbackConstants.CONFIRM)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
view.performHapticFeedback(feedbackType)
|
||||
}
|
||||
}
|
||||
|
||||
fun vibrateForLineClear(lineCount: Int) {
|
||||
android.util.Log.d("GameHaptics", "Attempting to vibrate for $lineCount lines")
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val duration = when (lineCount) {
|
||||
4 -> 200L // Tetris - doubled from 100L
|
||||
3 -> 160L // Triples - doubled from 80L
|
||||
2 -> 120L // Doubles - doubled from 60L
|
||||
1 -> 80L // Singles - doubled from 40L
|
||||
else -> 0L
|
||||
}
|
||||
|
||||
val amplitude = when (lineCount) {
|
||||
4 -> 255 // Full amplitude for Tetris
|
||||
3 -> 230 // 90% amplitude for triples
|
||||
2 -> 180 // 70% amplitude for doubles
|
||||
1 -> 128 // 50% amplitude for singles
|
||||
else -> 0
|
||||
}
|
||||
|
||||
android.util.Log.d("GameHaptics", "Vibration parameters - Duration: ${duration}ms, Amplitude: $amplitude")
|
||||
if (duration > 0 && amplitude > 0) {
|
||||
try {
|
||||
val vibrationEffect = VibrationEffect.createOneShot(duration, amplitude)
|
||||
vibrator.vibrate(vibrationEffect)
|
||||
android.util.Log.d("GameHaptics", "Vibration triggered successfully")
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("GameHaptics", "Error triggering vibration", e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
android.util.Log.w("GameHaptics", "Device does not support vibration effects (Android < 8.0)")
|
||||
}
|
||||
}
|
||||
|
||||
fun vibrateForPieceLock() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val vibrationEffect = VibrationEffect.createOneShot(50L, VibrationEffect.DEFAULT_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)
|
||||
vibrator.vibrate(vibrationEffect)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,821 +0,0 @@
|
|||
package com.mintris.game
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.graphics.BlurMaskFilter
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.animation.LinearInterpolator
|
||||
import android.view.WindowManager
|
||||
import android.view.Display
|
||||
import android.hardware.display.DisplayManager
|
||||
import com.mintris.model.GameBoard
|
||||
import com.mintris.model.Tetromino
|
||||
import com.mintris.model.TetrominoType
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* GameView that renders the Tetris game and handles touch input
|
||||
*/
|
||||
class GameView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : View(context, attrs, defStyleAttr) {
|
||||
|
||||
// Game board model
|
||||
private var gameBoard = GameBoard()
|
||||
private var gameHaptics: GameHaptics? = null
|
||||
|
||||
// Game state
|
||||
private var isRunning = false
|
||||
var isPaused = false // Changed from private to public to allow access from MainActivity
|
||||
private var score = 0
|
||||
|
||||
// Callbacks
|
||||
var onNextPieceChanged: (() -> Unit)? = null
|
||||
|
||||
// Rendering
|
||||
private val blockPaint = Paint().apply {
|
||||
color = Color.WHITE
|
||||
isAntiAlias = true
|
||||
}
|
||||
|
||||
private val ghostBlockPaint = Paint().apply {
|
||||
color = Color.WHITE
|
||||
alpha = 80 // 30% opacity
|
||||
isAntiAlias = true
|
||||
}
|
||||
|
||||
private val gridPaint = Paint().apply {
|
||||
color = Color.parseColor("#222222") // Very dark gray
|
||||
alpha = 20 // Reduced from 40 to be more subtle
|
||||
isAntiAlias = true
|
||||
strokeWidth = 1f
|
||||
style = Paint.Style.STROKE
|
||||
maskFilter = null // Ensure no blur effect on grid lines
|
||||
}
|
||||
|
||||
private val glowPaint = Paint().apply {
|
||||
color = Color.WHITE
|
||||
alpha = 40 // Reduced from 80 for more subtlety
|
||||
isAntiAlias = true
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = 1.5f
|
||||
maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.OUTER)
|
||||
}
|
||||
|
||||
private val blockGlowPaint = Paint().apply {
|
||||
color = Color.WHITE
|
||||
alpha = 60
|
||||
isAntiAlias = true
|
||||
style = Paint.Style.FILL
|
||||
maskFilter = BlurMaskFilter(12f, BlurMaskFilter.Blur.OUTER)
|
||||
}
|
||||
|
||||
private val borderGlowPaint = Paint().apply {
|
||||
color = Color.WHITE
|
||||
alpha = 60
|
||||
isAntiAlias = true
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = 2f
|
||||
maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.OUTER)
|
||||
}
|
||||
|
||||
// Add a new paint for the pulse effect
|
||||
private val pulsePaint = Paint().apply {
|
||||
color = Color.CYAN
|
||||
alpha = 255
|
||||
isAntiAlias = true
|
||||
style = Paint.Style.FILL
|
||||
maskFilter = BlurMaskFilter(32f, BlurMaskFilter.Blur.OUTER) // Increased from 16f to 32f
|
||||
}
|
||||
|
||||
// Pre-allocate paint objects to avoid GC
|
||||
private val tmpPaint = Paint()
|
||||
|
||||
// Calculate block size based on view dimensions and board size
|
||||
private var blockSize = 0f
|
||||
private var boardLeft = 0f
|
||||
private var boardTop = 0f
|
||||
|
||||
// Game loop handler and runnable
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private val gameLoopRunnable = object : Runnable {
|
||||
override fun run() {
|
||||
if (isRunning && !isPaused) {
|
||||
update()
|
||||
invalidate()
|
||||
handler.postDelayed(this, gameBoard.dropInterval)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Touch parameters
|
||||
private var lastTouchX = 0f
|
||||
private var lastTouchY = 0f
|
||||
private var startX = 0f
|
||||
private var startY = 0f
|
||||
private var lastTapTime = 0L
|
||||
private var lastRotationTime = 0L
|
||||
private var lastMoveTime = 0L
|
||||
private var minSwipeVelocity = 1200 // Increased from 800 to require more deliberate swipes
|
||||
private val maxTapMovement = 20f // Maximum movement allowed for a tap (in pixels)
|
||||
private val minTapTime = 100L // Minimum time for a tap (in milliseconds)
|
||||
private val rotationCooldown = 150L // Minimum time between rotations (in milliseconds)
|
||||
private val moveCooldown = 50L // Minimum time between move haptics (in milliseconds)
|
||||
private var lockedDirection: Direction? = null // Track the locked movement direction
|
||||
private val minMovementThreshold = 0.75f // Minimum movement threshold relative to block size
|
||||
private val directionLockThreshold = 2.5f // Increased from 1.5f to make direction locking more aggressive
|
||||
private val isStrictDirectionLock = true // Enable strict direction locking to prevent diagonal inputs
|
||||
private val minHardDropDistance = 1.5f // Minimum distance (in blocks) for hard drop gesture
|
||||
|
||||
private enum class Direction {
|
||||
HORIZONTAL, VERTICAL
|
||||
}
|
||||
|
||||
// Callback for game events
|
||||
var onGameStateChanged: ((score: Int, level: Int, lines: Int) -> Unit)? = null
|
||||
var onGameOver: ((score: Int) -> Unit)? = null
|
||||
var onLineClear: ((Int) -> Unit)? = null // New callback for line clear events
|
||||
var onPieceMove: (() -> Unit)? = null // New callback for piece movement
|
||||
var onPieceLock: (() -> Unit)? = null // New callback for piece locking
|
||||
|
||||
// Animation state
|
||||
private var pulseAnimator: ValueAnimator? = null
|
||||
private var pulseAlpha = 0f
|
||||
private var isPulsing = false
|
||||
private var linesToPulse = mutableListOf<Int>() // Track which lines are being cleared
|
||||
|
||||
init {
|
||||
// Start with paused state
|
||||
pause()
|
||||
|
||||
// Connect our callbacks to the GameBoard
|
||||
gameBoard.onPieceMove = { onPieceMove?.invoke() }
|
||||
gameBoard.onPieceLock = { onPieceLock?.invoke() }
|
||||
gameBoard.onLineClear = { lineCount, clearedLines ->
|
||||
android.util.Log.d("GameView", "Received line clear from GameBoard: $lineCount lines")
|
||||
try {
|
||||
onLineClear?.invoke(lineCount)
|
||||
// Use the lines that were cleared directly
|
||||
linesToPulse.clear()
|
||||
linesToPulse.addAll(clearedLines)
|
||||
android.util.Log.d("GameView", "Found ${linesToPulse.size} lines to pulse")
|
||||
startPulseAnimation(lineCount)
|
||||
android.util.Log.d("GameView", "Forwarded line clear callback")
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("GameView", "Error forwarding line clear callback", e)
|
||||
}
|
||||
}
|
||||
|
||||
// Force hardware acceleration - This is critical for performance
|
||||
setLayerType(LAYER_TYPE_HARDWARE, null)
|
||||
|
||||
// Set better frame rate using modern APIs
|
||||
val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
|
||||
val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY)
|
||||
display?.let { disp ->
|
||||
val refreshRate = disp.refreshRate
|
||||
// Set game loop interval based on refresh rate, but don't go faster than the base interval
|
||||
val targetFps = refreshRate.toInt()
|
||||
if (targetFps > 0) {
|
||||
gameBoard.dropInterval = gameBoard.dropInterval.coerceAtMost(1000L / targetFps)
|
||||
}
|
||||
}
|
||||
|
||||
// Enable edge-to-edge rendering
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
setSystemGestureExclusionRects(listOf(Rect(0, 0, width, height)))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the game
|
||||
*/
|
||||
fun start() {
|
||||
isPaused = false
|
||||
isRunning = true
|
||||
gameBoard.startGame() // Add this line to ensure a new piece is spawned
|
||||
handler.post(gameLoopRunnable)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause the game
|
||||
*/
|
||||
fun pause() {
|
||||
isPaused = true
|
||||
handler.removeCallbacks(gameLoopRunnable)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the game
|
||||
*/
|
||||
fun reset() {
|
||||
isRunning = false
|
||||
isPaused = true
|
||||
gameBoard.reset()
|
||||
gameBoard.startGame() // Add this line to ensure a new piece is spawned
|
||||
handler.removeCallbacks(gameLoopRunnable)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update game state (called on game loop)
|
||||
*/
|
||||
private fun update() {
|
||||
if (gameBoard.isGameOver) {
|
||||
isRunning = false
|
||||
isPaused = true
|
||||
onGameOver?.invoke(gameBoard.score)
|
||||
return
|
||||
}
|
||||
|
||||
// Move the current tetromino down automatically
|
||||
gameBoard.moveDown()
|
||||
|
||||
// Update UI with current game state
|
||||
onGameStateChanged?.invoke(gameBoard.score, gameBoard.level, gameBoard.lines)
|
||||
}
|
||||
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||
super.onSizeChanged(w, h, oldw, oldh)
|
||||
|
||||
// Force hardware acceleration - Critical for performance
|
||||
setLayerType(LAYER_TYPE_HARDWARE, null)
|
||||
|
||||
// Update gesture exclusion rect for edge-to-edge rendering
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
setSystemGestureExclusionRects(listOf(Rect(0, 0, w, h)))
|
||||
}
|
||||
|
||||
calculateDimensions(w, h)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate dimensions for the board and blocks based on view size
|
||||
*/
|
||||
private fun calculateDimensions(width: Int, height: Int) {
|
||||
// Calculate block size based on available space
|
||||
val horizontalBlocks = gameBoard.width
|
||||
val verticalBlocks = gameBoard.height
|
||||
|
||||
// Account for all glow effects and borders
|
||||
val borderPadding = 16f // Padding for border glow effects
|
||||
|
||||
// Calculate block size to fit the height exactly, accounting for all padding
|
||||
blockSize = (height.toFloat() - (borderPadding * 2)) / verticalBlocks
|
||||
|
||||
// Calculate total board width
|
||||
val totalBoardWidth = blockSize * horizontalBlocks
|
||||
|
||||
// Center horizontally
|
||||
boardLeft = (width - totalBoardWidth) / 2
|
||||
boardTop = borderPadding // Start with border padding from top
|
||||
|
||||
// Calculate the total height needed for the board
|
||||
val totalHeight = blockSize * verticalBlocks
|
||||
|
||||
// Log dimensions for debugging
|
||||
android.util.Log.d("GameView", "Board dimensions: width=$width, height=$height, blockSize=$blockSize, boardLeft=$boardLeft, boardTop=$boardTop, totalHeight=$totalHeight")
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
// Skip drawing if paused or game over - faster return
|
||||
if (isPaused || gameBoard.isGameOver) {
|
||||
super.onDraw(canvas)
|
||||
return
|
||||
}
|
||||
|
||||
// Set hardware layer type during draw for better performance
|
||||
val wasHardwareAccelerated = isHardwareAccelerated
|
||||
if (!wasHardwareAccelerated) {
|
||||
setLayerType(LAYER_TYPE_HARDWARE, null)
|
||||
}
|
||||
|
||||
super.onDraw(canvas)
|
||||
|
||||
// Draw background (already black from theme)
|
||||
|
||||
// Draw board border glow
|
||||
drawBoardBorder(canvas)
|
||||
|
||||
// Draw grid (very subtle)
|
||||
drawGrid(canvas)
|
||||
|
||||
// Draw locked pieces
|
||||
drawLockedBlocks(canvas)
|
||||
|
||||
if (!gameBoard.isGameOver && isRunning) {
|
||||
// Draw ghost piece (landing preview)
|
||||
drawGhostPiece(canvas)
|
||||
|
||||
// Draw active piece
|
||||
drawActivePiece(canvas)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw glowing border around the playable area
|
||||
*/
|
||||
private fun drawBoardBorder(canvas: Canvas) {
|
||||
val left = boardLeft
|
||||
val top = boardTop
|
||||
val right = boardLeft + gameBoard.width * blockSize
|
||||
val bottom = boardTop + gameBoard.height * blockSize
|
||||
|
||||
val rect = RectF(left, top, right, bottom)
|
||||
|
||||
// Draw base border with increased glow
|
||||
borderGlowPaint.apply {
|
||||
alpha = 80 // Increased from 60
|
||||
maskFilter = BlurMaskFilter(16f, BlurMaskFilter.Blur.OUTER) // Increased from 8f
|
||||
}
|
||||
canvas.drawRect(rect, borderGlowPaint)
|
||||
|
||||
// Draw pulsing border if animation is active
|
||||
if (isPulsing) {
|
||||
val pulseBorderPaint = Paint().apply {
|
||||
color = Color.WHITE
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = 6f + (16f * pulseAlpha) // Increased from 4f+12f to 6f+16f
|
||||
alpha = (255 * pulseAlpha).toInt()
|
||||
isAntiAlias = true
|
||||
maskFilter = BlurMaskFilter(32f * (1f + pulseAlpha), BlurMaskFilter.Blur.OUTER) // Increased from 24f to 32f
|
||||
}
|
||||
// Draw the border with a slight inset to prevent edge artifacts
|
||||
val inset = 1f
|
||||
canvas.drawRect(
|
||||
left + inset,
|
||||
top + inset,
|
||||
right - inset,
|
||||
bottom - inset,
|
||||
pulseBorderPaint
|
||||
)
|
||||
|
||||
// Add an additional outer glow for more dramatic effect
|
||||
val outerGlowPaint = Paint().apply {
|
||||
color = Color.WHITE
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = 2f
|
||||
alpha = (128 * pulseAlpha).toInt()
|
||||
isAntiAlias = true
|
||||
maskFilter = BlurMaskFilter(48f * (1f + pulseAlpha), BlurMaskFilter.Blur.OUTER)
|
||||
}
|
||||
canvas.drawRect(
|
||||
left - 4f,
|
||||
top - 4f,
|
||||
right + 4f,
|
||||
bottom + 4f,
|
||||
outerGlowPaint
|
||||
)
|
||||
|
||||
// Add extra bright glow for side borders during line clear
|
||||
val sideGlowPaint = Paint().apply {
|
||||
color = Color.WHITE
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = 8f + (24f * pulseAlpha) // Thicker stroke for side borders
|
||||
alpha = (255 * pulseAlpha).toInt()
|
||||
isAntiAlias = true
|
||||
maskFilter = BlurMaskFilter(64f * (1f + pulseAlpha), BlurMaskFilter.Blur.OUTER) // Larger blur for side borders
|
||||
}
|
||||
|
||||
// Draw left border with extra glow
|
||||
canvas.drawLine(
|
||||
left + inset,
|
||||
top + inset,
|
||||
left + inset,
|
||||
bottom - inset,
|
||||
sideGlowPaint
|
||||
)
|
||||
|
||||
// Draw right border with extra glow
|
||||
canvas.drawLine(
|
||||
right - inset,
|
||||
top + inset,
|
||||
right - inset,
|
||||
bottom - inset,
|
||||
sideGlowPaint
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the grid lines (very subtle)
|
||||
*/
|
||||
private fun drawGrid(canvas: Canvas) {
|
||||
// Save the canvas state to prevent any effects from affecting the grid
|
||||
canvas.save()
|
||||
|
||||
// Draw vertical grid lines
|
||||
for (x in 0..gameBoard.width) {
|
||||
val xPos = boardLeft + x * blockSize
|
||||
canvas.drawLine(
|
||||
xPos, boardTop,
|
||||
xPos, boardTop + gameBoard.height * blockSize,
|
||||
gridPaint
|
||||
)
|
||||
}
|
||||
|
||||
// Draw horizontal grid lines
|
||||
for (y in 0..gameBoard.height) {
|
||||
val yPos = boardTop + y * blockSize
|
||||
canvas.drawLine(
|
||||
boardLeft, yPos,
|
||||
boardLeft + gameBoard.width * blockSize, yPos,
|
||||
gridPaint
|
||||
)
|
||||
}
|
||||
|
||||
// Restore the canvas state
|
||||
canvas.restore()
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the locked blocks on the board
|
||||
*/
|
||||
private fun drawLockedBlocks(canvas: Canvas) {
|
||||
for (y in 0 until gameBoard.height) {
|
||||
for (x in 0 until gameBoard.width) {
|
||||
if (gameBoard.isOccupied(x, y)) {
|
||||
drawBlock(canvas, x, y, false, y in linesToPulse)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the currently active tetromino
|
||||
*/
|
||||
private fun drawActivePiece(canvas: Canvas) {
|
||||
val piece = gameBoard.getCurrentPiece() ?: return
|
||||
|
||||
for (y in 0 until piece.getHeight()) {
|
||||
for (x in 0 until piece.getWidth()) {
|
||||
if (piece.isBlockAt(x, y)) {
|
||||
val boardX = piece.x + x
|
||||
val boardY = piece.y + y
|
||||
|
||||
// Draw piece regardless of vertical position
|
||||
if (boardX >= 0 && boardX < gameBoard.width) {
|
||||
drawBlock(canvas, boardX, boardY, false, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the ghost piece (landing preview)
|
||||
*/
|
||||
private fun drawGhostPiece(canvas: Canvas) {
|
||||
val piece = gameBoard.getCurrentPiece() ?: return
|
||||
val ghostY = gameBoard.getGhostY()
|
||||
|
||||
for (y in 0 until piece.getHeight()) {
|
||||
for (x in 0 until piece.getWidth()) {
|
||||
if (piece.isBlockAt(x, y)) {
|
||||
val boardX = piece.x + x
|
||||
val boardY = ghostY + y
|
||||
|
||||
// Draw ghost piece regardless of vertical position
|
||||
if (boardX >= 0 && boardX < gameBoard.width) {
|
||||
drawBlock(canvas, boardX, boardY, true, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a single tetris block at the given grid position
|
||||
*/
|
||||
private fun drawBlock(canvas: Canvas, x: Int, y: Int, isGhost: Boolean, isPulsingLine: Boolean) {
|
||||
val left = boardLeft + x * blockSize
|
||||
val top = boardTop + y * blockSize
|
||||
val right = left + blockSize
|
||||
val bottom = top + blockSize
|
||||
|
||||
// Save canvas state before drawing block effects
|
||||
canvas.save()
|
||||
|
||||
// Draw outer glow
|
||||
blockGlowPaint.color = if (isGhost) Color.argb(30, 255, 255, 255) else Color.WHITE
|
||||
canvas.drawRect(left - 2f, top - 2f, right + 2f, bottom + 2f, blockGlowPaint)
|
||||
|
||||
// Draw block
|
||||
blockPaint.apply {
|
||||
color = if (isGhost) Color.argb(30, 255, 255, 255) else Color.WHITE
|
||||
alpha = if (isGhost) 30 else 255
|
||||
}
|
||||
canvas.drawRect(left, top, right, bottom, blockPaint)
|
||||
|
||||
// Draw inner glow
|
||||
glowPaint.color = if (isGhost) Color.argb(30, 255, 255, 255) else Color.WHITE
|
||||
canvas.drawRect(left + 1f, top + 1f, right - 1f, bottom - 1f, glowPaint)
|
||||
|
||||
// Draw pulse effect if animation is active and this is a pulsing line
|
||||
if (isPulsing && isPulsingLine) {
|
||||
val pulseBlockPaint = Paint().apply {
|
||||
color = Color.WHITE
|
||||
alpha = (255 * pulseAlpha).toInt()
|
||||
isAntiAlias = true
|
||||
style = Paint.Style.FILL
|
||||
maskFilter = BlurMaskFilter(40f * (1f + pulseAlpha), BlurMaskFilter.Blur.OUTER)
|
||||
}
|
||||
canvas.drawRect(left - 16f, top - 16f, right + 16f, bottom + 16f, pulseBlockPaint)
|
||||
}
|
||||
|
||||
// Restore canvas state after drawing block effects
|
||||
canvas.restore()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given board position is part of the current piece
|
||||
*/
|
||||
private fun isPositionInPiece(boardX: Int, boardY: Int, piece: Tetromino): Boolean {
|
||||
for (y in 0 until piece.getHeight()) {
|
||||
for (x in 0 until piece.getWidth()) {
|
||||
if (piece.isBlockAt(x, y)) {
|
||||
val pieceX = piece.x + x
|
||||
val pieceY = piece.y + y
|
||||
if (pieceX == boardX && pieceY == boardY) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Get color for tetromino type
|
||||
*/
|
||||
private fun getTetrominoColor(type: TetrominoType): Int {
|
||||
return when (type) {
|
||||
TetrominoType.I -> Color.CYAN
|
||||
TetrominoType.J -> Color.BLUE
|
||||
TetrominoType.L -> Color.rgb(255, 165, 0) // Orange
|
||||
TetrominoType.O -> Color.YELLOW
|
||||
TetrominoType.S -> Color.GREEN
|
||||
TetrominoType.T -> Color.MAGENTA
|
||||
TetrominoType.Z -> Color.RED
|
||||
}
|
||||
}
|
||||
|
||||
// Custom touch event handling
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
if (!isRunning || isPaused || gameBoard.isGameOver) {
|
||||
return true
|
||||
}
|
||||
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
// Record start of touch
|
||||
startX = event.x
|
||||
startY = event.y
|
||||
lastTouchX = event.x
|
||||
lastTouchY = event.y
|
||||
lockedDirection = null // Reset direction lock
|
||||
|
||||
// Check for double tap (rotate)
|
||||
val currentTime = System.currentTimeMillis()
|
||||
if (currentTime - lastTapTime < 200) { // Reduced from 250ms for faster response
|
||||
// Double tap detected, rotate the piece
|
||||
if (currentTime - lastRotationTime >= rotationCooldown) {
|
||||
gameBoard.rotate()
|
||||
lastRotationTime = currentTime
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
lastTapTime = currentTime
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
val deltaX = event.x - lastTouchX
|
||||
val deltaY = event.y - lastTouchY
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
// Determine movement direction if not locked
|
||||
if (lockedDirection == null) {
|
||||
val absDeltaX = abs(deltaX)
|
||||
val absDeltaY = abs(deltaY)
|
||||
|
||||
// Check if movement exceeds threshold
|
||||
if (absDeltaX > blockSize * minMovementThreshold || absDeltaY > blockSize * minMovementThreshold) {
|
||||
// Determine dominant direction with stricter criteria
|
||||
if (absDeltaX > absDeltaY * directionLockThreshold) {
|
||||
lockedDirection = Direction.HORIZONTAL
|
||||
} else if (absDeltaY > absDeltaX * directionLockThreshold) {
|
||||
lockedDirection = Direction.VERTICAL
|
||||
}
|
||||
// If strict direction lock is enabled and we couldn't determine a clear direction, don't set one
|
||||
// This prevents diagonal movements from being recognized
|
||||
}
|
||||
}
|
||||
|
||||
// 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.moveDown()
|
||||
lastTouchY = event.y
|
||||
if (currentTime - lastMoveTime >= moveCooldown) {
|
||||
gameHaptics?.vibrateForPieceMove()
|
||||
lastMoveTime = currentTime
|
||||
}
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
null -> {
|
||||
// No direction lock yet, don't process movement
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP -> {
|
||||
// Calculate movement speed for potential fling detection
|
||||
val moveTime = System.currentTimeMillis() - lastTapTime
|
||||
val deltaY = event.y - startY
|
||||
val deltaX = event.x - startX
|
||||
|
||||
// Only allow hard drops with a deliberate downward swipe
|
||||
// Requires: predominantly vertical movement, minimum distance, and minimum velocity
|
||||
if (moveTime > 0 &&
|
||||
deltaY > blockSize * minHardDropDistance && // Require longer swipe for hard drop
|
||||
(deltaY / moveTime) * 1000 > minSwipeVelocity &&
|
||||
abs(deltaX) < abs(deltaY) * 0.3f) { // Require more purely vertical movement (reduced from 0.5f to 0.3f)
|
||||
gameBoard.hardDrop()
|
||||
invalidate()
|
||||
} else if (moveTime < minTapTime &&
|
||||
abs(deltaY) < maxTapMovement &&
|
||||
abs(deltaX) < maxTapMovement) {
|
||||
// Quick tap with minimal movement (rotation)
|
||||
val currentTime = System.currentTimeMillis()
|
||||
if (currentTime - lastRotationTime >= rotationCooldown) {
|
||||
gameBoard.rotate()
|
||||
lastRotationTime = currentTime
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
// Reset direction lock
|
||||
lockedDirection = null
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current score
|
||||
*/
|
||||
fun getScore(): Int = gameBoard.score
|
||||
|
||||
/**
|
||||
* Get the current level
|
||||
*/
|
||||
fun getLevel(): Int = gameBoard.level
|
||||
|
||||
/**
|
||||
* Get the number of lines cleared
|
||||
*/
|
||||
fun getLines(): Int = gameBoard.lines
|
||||
|
||||
/**
|
||||
* Check if the game is over
|
||||
*/
|
||||
fun isGameOver(): Boolean = gameBoard.isGameOver
|
||||
|
||||
/**
|
||||
* Get the next piece that will be spawned
|
||||
*/
|
||||
fun getNextPiece(): Tetromino? {
|
||||
return gameBoard.getNextPiece()
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up resources when view is detached
|
||||
*/
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
handler.removeCallbacks(gameLoopRunnable)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the game board for this view
|
||||
*/
|
||||
fun setGameBoard(board: GameBoard) {
|
||||
gameBoard = board
|
||||
|
||||
// Reconnect callbacks to the new board
|
||||
gameBoard.onPieceMove = { onPieceMove?.invoke() }
|
||||
gameBoard.onPieceLock = { onPieceLock?.invoke() }
|
||||
gameBoard.onLineClear = { lineCount, clearedLines ->
|
||||
android.util.Log.d("GameView", "Received line clear from GameBoard: $lineCount lines")
|
||||
try {
|
||||
onLineClear?.invoke(lineCount)
|
||||
// Use the lines that were cleared directly
|
||||
linesToPulse.clear()
|
||||
linesToPulse.addAll(clearedLines)
|
||||
android.util.Log.d("GameView", "Found ${linesToPulse.size} lines to pulse")
|
||||
startPulseAnimation(lineCount)
|
||||
android.util.Log.d("GameView", "Forwarded line clear callback")
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("GameView", "Error forwarding line clear callback", e)
|
||||
}
|
||||
}
|
||||
|
||||
invalidate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the haptics handler for this view
|
||||
*/
|
||||
fun setHaptics(haptics: GameHaptics) {
|
||||
gameHaptics = haptics
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume the game
|
||||
*/
|
||||
fun resume() {
|
||||
if (!isRunning) {
|
||||
isRunning = true
|
||||
}
|
||||
isPaused = false
|
||||
|
||||
// Restart the game loop immediately
|
||||
handler.removeCallbacks(gameLoopRunnable)
|
||||
handler.post(gameLoopRunnable)
|
||||
|
||||
// Force an update to ensure pieces move immediately
|
||||
update()
|
||||
invalidate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the pulse animation for line clear
|
||||
*/
|
||||
private fun startPulseAnimation(lineCount: Int) {
|
||||
android.util.Log.d("GameView", "Starting pulse animation for $lineCount lines")
|
||||
|
||||
// Cancel any existing animation
|
||||
pulseAnimator?.cancel()
|
||||
|
||||
// Create new animation
|
||||
pulseAnimator = ValueAnimator.ofFloat(0f, 1f, 0f).apply {
|
||||
duration = when (lineCount) {
|
||||
4 -> 2000L // Tetris - longer duration
|
||||
3 -> 1600L // Triples
|
||||
2 -> 1200L // Doubles
|
||||
1 -> 1000L // Singles
|
||||
else -> 1000L
|
||||
}
|
||||
interpolator = LinearInterpolator()
|
||||
addUpdateListener { animation ->
|
||||
pulseAlpha = animation.animatedValue as Float
|
||||
isPulsing = true
|
||||
invalidate()
|
||||
android.util.Log.d("GameView", "Pulse animation update: alpha = $pulseAlpha")
|
||||
}
|
||||
addListener(object : android.animation.AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: android.animation.Animator) {
|
||||
isPulsing = false
|
||||
pulseAlpha = 0f
|
||||
linesToPulse.clear()
|
||||
invalidate()
|
||||
android.util.Log.d("GameView", "Pulse animation ended")
|
||||
}
|
||||
})
|
||||
}
|
||||
pulseAnimator?.start()
|
||||
}
|
||||
}
|
|
@ -1,585 +0,0 @@
|
|||
package com.mintris.model
|
||||
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
* Represents the game board (grid) and manages game state
|
||||
*/
|
||||
class GameBoard(
|
||||
val width: Int = 10,
|
||||
val height: Int = 20
|
||||
) {
|
||||
// Board grid to track locked pieces
|
||||
// True = occupied, False = empty
|
||||
private val grid = Array(height) { BooleanArray(width) { false } }
|
||||
|
||||
// Current active tetromino
|
||||
private var currentPiece: Tetromino? = null
|
||||
|
||||
// Next tetromino to be played
|
||||
private var nextPiece: Tetromino? = null
|
||||
|
||||
// Hold piece
|
||||
private var holdPiece: Tetromino? = null
|
||||
private var canHold = true
|
||||
|
||||
// 7-bag randomizer
|
||||
private val bag = mutableListOf<TetrominoType>()
|
||||
|
||||
// Game state
|
||||
var score = 0
|
||||
var level = 1
|
||||
var startingLevel = 1 // Add this line to track the starting level
|
||||
var lines = 0
|
||||
var isGameOver = false
|
||||
var isHardDropInProgress = false // Make public
|
||||
var isPieceLocking = false // Make public
|
||||
|
||||
// Scoring state
|
||||
private var combo = 0
|
||||
private var lastClearWasTetris = false
|
||||
private var lastClearWasPerfect = false
|
||||
private var lastClearWasAllClear = false
|
||||
private var lastPieceClearedLines = false // Track if the last piece placed cleared lines
|
||||
|
||||
// Animation state
|
||||
var linesToClear = mutableListOf<Int>()
|
||||
var isLineClearAnimationInProgress = false
|
||||
|
||||
// Initial game speed (milliseconds per drop)
|
||||
var dropInterval = 1000L
|
||||
|
||||
// Callbacks for game events
|
||||
var onPieceMove: (() -> Unit)? = null
|
||||
var onPieceLock: (() -> Unit)? = null
|
||||
var onNextPieceChanged: (() -> Unit)? = null
|
||||
var onLineClear: ((Int, List<Int>) -> Unit)? = null
|
||||
|
||||
init {
|
||||
spawnNextPiece()
|
||||
spawnPiece()
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the next tetromino piece using 7-bag randomizer
|
||||
*/
|
||||
private fun spawnNextPiece() {
|
||||
// If bag is empty, refill it with all piece types
|
||||
if (bag.isEmpty()) {
|
||||
bag.addAll(TetrominoType.values())
|
||||
bag.shuffle()
|
||||
}
|
||||
|
||||
// Take the next piece from the bag
|
||||
nextPiece = Tetromino(bag.removeAt(0))
|
||||
onNextPieceChanged?.invoke()
|
||||
}
|
||||
|
||||
/**
|
||||
* Hold the current piece
|
||||
*/
|
||||
fun holdPiece() {
|
||||
if (!canHold) return
|
||||
|
||||
val current = currentPiece
|
||||
if (holdPiece == null) {
|
||||
// If no piece is held, hold current piece and spawn new one
|
||||
holdPiece = current
|
||||
spawnNextPiece()
|
||||
spawnPiece()
|
||||
} else {
|
||||
// Swap current piece with held piece
|
||||
currentPiece = holdPiece
|
||||
holdPiece = current
|
||||
// Reset position of swapped piece
|
||||
currentPiece?.apply {
|
||||
x = (width - getWidth()) / 2
|
||||
y = 0
|
||||
}
|
||||
}
|
||||
canHold = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently held piece
|
||||
*/
|
||||
fun getHoldPiece(): Tetromino? = holdPiece
|
||||
|
||||
/**
|
||||
* Get the next piece that will be spawned
|
||||
*/
|
||||
fun getNextPiece(): Tetromino? = nextPiece
|
||||
|
||||
/**
|
||||
* Spawns the current tetromino at the top of the board
|
||||
*/
|
||||
fun spawnPiece() {
|
||||
currentPiece = nextPiece
|
||||
spawnNextPiece()
|
||||
|
||||
// Center the piece horizontally
|
||||
currentPiece?.apply {
|
||||
x = (width - getWidth()) / 2
|
||||
y = 0
|
||||
|
||||
// Check if the piece can be placed (Game Over condition)
|
||||
if (!canMove(0, 0)) {
|
||||
isGameOver = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the current piece left
|
||||
*/
|
||||
fun moveLeft() {
|
||||
if (canMove(-1, 0)) {
|
||||
currentPiece?.x = currentPiece?.x?.minus(1) ?: 0
|
||||
onPieceMove?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the current piece right
|
||||
*/
|
||||
fun moveRight() {
|
||||
if (canMove(1, 0)) {
|
||||
currentPiece?.x = currentPiece?.x?.plus(1) ?: 0
|
||||
onPieceMove?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the current piece down (soft drop)
|
||||
*/
|
||||
fun moveDown(): Boolean {
|
||||
// Don't allow movement if a hard drop is in progress or piece is locking
|
||||
if (isHardDropInProgress || isPieceLocking) return false
|
||||
|
||||
return if (canMove(0, 1)) {
|
||||
currentPiece?.y = currentPiece?.y?.plus(1) ?: 0
|
||||
onPieceMove?.invoke()
|
||||
true
|
||||
} else {
|
||||
lockPiece()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hard drop the current piece
|
||||
*/
|
||||
fun hardDrop() {
|
||||
if (isHardDropInProgress || isPieceLocking) return // Prevent multiple hard drops
|
||||
|
||||
isHardDropInProgress = true
|
||||
val piece = currentPiece ?: return
|
||||
|
||||
// Move piece down until it can't move anymore
|
||||
while (canMove(0, 1)) {
|
||||
piece.y++
|
||||
onPieceMove?.invoke()
|
||||
}
|
||||
|
||||
// Lock the piece immediately
|
||||
lockPiece()
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotate the current piece clockwise
|
||||
*/
|
||||
fun rotate() {
|
||||
currentPiece?.let {
|
||||
// Save current rotation
|
||||
val originalX = it.x
|
||||
val originalY = it.y
|
||||
|
||||
// Try to rotate
|
||||
it.rotateClockwise()
|
||||
|
||||
// Wall kick logic - try to move the piece if rotation causes collision
|
||||
if (!canMove(0, 0)) {
|
||||
// Try to move left
|
||||
if (canMove(-1, 0)) {
|
||||
it.x--
|
||||
}
|
||||
// Try to move right
|
||||
else if (canMove(1, 0)) {
|
||||
it.x++
|
||||
}
|
||||
// Try to move 2 spaces (for I piece)
|
||||
else if (canMove(-2, 0)) {
|
||||
it.x -= 2
|
||||
}
|
||||
else if (canMove(2, 0)) {
|
||||
it.x += 2
|
||||
}
|
||||
// Try to move up for floor kicks
|
||||
else if (canMove(0, -1)) {
|
||||
it.y--
|
||||
}
|
||||
// Revert if can't find a valid position
|
||||
else {
|
||||
it.rotateCounterClockwise()
|
||||
it.x = originalX
|
||||
it.y = originalY
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current piece can move to the given position
|
||||
*/
|
||||
fun canMove(deltaX: Int, deltaY: Int): Boolean {
|
||||
val piece = currentPiece ?: return false
|
||||
|
||||
val newX = piece.x + deltaX
|
||||
val newY = piece.y + deltaY
|
||||
|
||||
for (y in 0 until piece.getHeight()) {
|
||||
for (x in 0 until piece.getWidth()) {
|
||||
if (piece.isBlockAt(x, y)) {
|
||||
val boardX = newX + x
|
||||
val boardY = newY + y
|
||||
|
||||
// Check if the position is outside the board horizontally
|
||||
if (boardX < 0 || boardX >= width) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if the position is below the board
|
||||
if (boardY >= height) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if the position is already occupied (but not if it's above the board)
|
||||
if (boardY >= 0 && grid[boardY][boardX]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock the current piece in place
|
||||
*/
|
||||
private fun lockPiece() {
|
||||
if (isPieceLocking) return // Prevent recursive locking
|
||||
isPieceLocking = true
|
||||
|
||||
val piece = currentPiece ?: return
|
||||
|
||||
// Add the piece to the grid
|
||||
for (y in 0 until piece.getHeight()) {
|
||||
for (x in 0 until piece.getWidth()) {
|
||||
if (piece.isBlockAt(x, y)) {
|
||||
val boardX = piece.x + x
|
||||
val boardY = piece.y + y
|
||||
|
||||
// Only add to grid if within bounds
|
||||
if (boardY >= 0 && boardY < height && boardX >= 0 && boardX < width) {
|
||||
grid[boardY][boardX] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger the piece lock vibration
|
||||
onPieceLock?.invoke()
|
||||
|
||||
// Find and clear lines immediately
|
||||
findAndClearLines()
|
||||
|
||||
// Spawn new piece immediately
|
||||
spawnPiece()
|
||||
|
||||
// Allow holding piece again after locking
|
||||
canHold = true
|
||||
|
||||
// Reset both states after everything is done
|
||||
isPieceLocking = false
|
||||
isHardDropInProgress = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Find and clear completed lines immediately
|
||||
*/
|
||||
private fun findAndClearLines() {
|
||||
// Quick scan for completed lines
|
||||
var shiftAmount = 0
|
||||
var y = height - 1
|
||||
val linesToClear = mutableListOf<Int>()
|
||||
|
||||
while (y >= 0) {
|
||||
if (grid[y].all { it }) {
|
||||
// Line is full, add to lines to clear
|
||||
linesToClear.add(y)
|
||||
shiftAmount++
|
||||
} else if (shiftAmount > 0) {
|
||||
// Shift this row down by shiftAmount
|
||||
System.arraycopy(grid[y], 0, grid[y + shiftAmount], 0, width)
|
||||
}
|
||||
y--
|
||||
}
|
||||
|
||||
// If lines were cleared, calculate score in background and trigger callback
|
||||
if (shiftAmount > 0) {
|
||||
android.util.Log.d("GameBoard", "Lines cleared: $shiftAmount")
|
||||
// Trigger line clear callback on main thread with the lines that were cleared
|
||||
val mainHandler = android.os.Handler(android.os.Looper.getMainLooper())
|
||||
mainHandler.post {
|
||||
android.util.Log.d("GameBoard", "Triggering onLineClear callback with $shiftAmount lines")
|
||||
try {
|
||||
onLineClear?.invoke(shiftAmount, linesToClear) // Pass the lines that were cleared
|
||||
android.util.Log.d("GameBoard", "onLineClear callback completed successfully")
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("GameBoard", "Error in onLineClear callback", e)
|
||||
}
|
||||
}
|
||||
|
||||
// Clear top rows after callback
|
||||
for (y in 0 until shiftAmount) {
|
||||
java.util.Arrays.fill(grid[y], false)
|
||||
}
|
||||
|
||||
Thread {
|
||||
calculateScore(shiftAmount)
|
||||
}.start()
|
||||
}
|
||||
|
||||
// Update combo based on whether this piece cleared lines
|
||||
if (shiftAmount > 0) {
|
||||
if (lastPieceClearedLines) {
|
||||
combo++
|
||||
} else {
|
||||
combo = 1 // Start new combo
|
||||
}
|
||||
} else {
|
||||
combo = 0 // Reset combo if no lines cleared
|
||||
}
|
||||
lastPieceClearedLines = shiftAmount > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate score for cleared lines
|
||||
*/
|
||||
private fun calculateScore(clearedLines: Int) {
|
||||
// Pre-calculated score multipliers for better performance
|
||||
val baseScore = when (clearedLines) {
|
||||
1 -> 40
|
||||
2 -> 100
|
||||
3 -> 300
|
||||
4 -> 1200
|
||||
else -> 0
|
||||
}
|
||||
|
||||
// Check for perfect clear (no blocks left)
|
||||
val isPerfectClear = !grid.any { row -> row.any { it } }
|
||||
|
||||
// Check for all clear (no blocks in playfield)
|
||||
val isAllClear = !grid.any { row -> row.any { it } } &&
|
||||
currentPiece == null &&
|
||||
nextPiece == null
|
||||
|
||||
// Calculate combo multiplier
|
||||
val comboMultiplier = if (combo > 0) {
|
||||
when (combo) {
|
||||
1 -> 1.0
|
||||
2 -> 1.5
|
||||
3 -> 2.0
|
||||
4 -> 2.5
|
||||
else -> 3.0
|
||||
}
|
||||
} else 1.0
|
||||
|
||||
// Calculate back-to-back Tetris bonus
|
||||
val backToBackMultiplier = if (clearedLines == 4 && lastClearWasTetris) 1.5 else 1.0
|
||||
|
||||
// Calculate perfect clear bonus
|
||||
val perfectClearMultiplier = if (isPerfectClear) {
|
||||
when (clearedLines) {
|
||||
1 -> 2.0
|
||||
2 -> 3.0
|
||||
3 -> 4.0
|
||||
4 -> 5.0
|
||||
else -> 1.0
|
||||
}
|
||||
} else 1.0
|
||||
|
||||
// Calculate all clear bonus
|
||||
val allClearMultiplier = if (isAllClear) 2.0 else 1.0
|
||||
|
||||
// Calculate T-Spin bonus
|
||||
val tSpinMultiplier = if (isTSpin()) {
|
||||
when (clearedLines) {
|
||||
1 -> 2.0
|
||||
2 -> 4.0
|
||||
3 -> 6.0
|
||||
else -> 1.0
|
||||
}
|
||||
} else 1.0
|
||||
|
||||
// Calculate final score with all multipliers
|
||||
val finalScore = (baseScore * level * comboMultiplier *
|
||||
backToBackMultiplier * perfectClearMultiplier *
|
||||
allClearMultiplier * tSpinMultiplier).toInt()
|
||||
|
||||
// Update score on main thread
|
||||
Thread {
|
||||
score += finalScore
|
||||
}.start()
|
||||
|
||||
// Update line clear state
|
||||
lastClearWasTetris = clearedLines == 4
|
||||
lastClearWasPerfect = isPerfectClear
|
||||
lastClearWasAllClear = isAllClear
|
||||
|
||||
// Update lines cleared and level
|
||||
lines += clearedLines
|
||||
// Calculate level based on lines cleared, but ensure it's never below the starting level
|
||||
level = Math.max((lines / 10) + 1, startingLevel)
|
||||
|
||||
// Update game speed based on level (NES formula)
|
||||
dropInterval = (1000 * Math.pow(0.8, (level - 1).toDouble())).toLong()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the last move was a T-Spin
|
||||
*/
|
||||
private fun isTSpin(): Boolean {
|
||||
val piece = currentPiece ?: return false
|
||||
if (piece.type != TetrominoType.T) return false
|
||||
|
||||
// Count occupied corners around the T piece
|
||||
var occupiedCorners = 0
|
||||
val centerX = piece.x + 1
|
||||
val centerY = piece.y + 1
|
||||
|
||||
// Check all four corners
|
||||
if (isOccupied(centerX - 1, centerY - 1)) occupiedCorners++
|
||||
if (isOccupied(centerX + 1, centerY - 1)) occupiedCorners++
|
||||
if (isOccupied(centerX - 1, centerY + 1)) occupiedCorners++
|
||||
if (isOccupied(centerX + 1, centerY + 1)) occupiedCorners++
|
||||
|
||||
// T-Spin requires at least 3 occupied corners
|
||||
return occupiedCorners >= 3
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ghost piece position (preview of where piece will land)
|
||||
*/
|
||||
fun getGhostY(): Int {
|
||||
val piece = currentPiece ?: return 0
|
||||
var ghostY = piece.y
|
||||
|
||||
// Find how far the piece can move down
|
||||
while (true) {
|
||||
if (canMove(0, ghostY - piece.y + 1)) {
|
||||
ghostY++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure ghostY doesn't exceed the board height
|
||||
return ghostY.coerceAtMost(height - 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current tetromino
|
||||
*/
|
||||
fun getCurrentPiece(): Tetromino? = currentPiece
|
||||
|
||||
/**
|
||||
* Check if a cell in the grid is occupied
|
||||
*/
|
||||
fun isOccupied(x: Int, y: Int): Boolean {
|
||||
return if (x in 0 until width && y in 0 until height) {
|
||||
grid[y][x]
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a line is completely filled
|
||||
*/
|
||||
fun isLineFull(y: Int): Boolean {
|
||||
return if (y in 0 until height) {
|
||||
grid[y].all { it }
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current level and adjust game parameters
|
||||
*/
|
||||
fun updateLevel(newLevel: Int) {
|
||||
level = newLevel.coerceIn(1, 20)
|
||||
startingLevel = level // Store the starting level
|
||||
// Update game speed based on level (NES formula)
|
||||
dropInterval = (1000 * Math.pow(0.8, (level - 1).toDouble())).toLong()
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a new game
|
||||
*/
|
||||
fun startGame() {
|
||||
reset()
|
||||
// Initialize pieces
|
||||
spawnNextPiece()
|
||||
spawnPiece()
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the game board
|
||||
*/
|
||||
fun reset() {
|
||||
// Clear the grid
|
||||
for (y in 0 until height) {
|
||||
for (x in 0 until width) {
|
||||
grid[y][x] = false
|
||||
}
|
||||
}
|
||||
|
||||
// Reset game state
|
||||
score = 0
|
||||
level = startingLevel // Use starting level instead of resetting to 1
|
||||
lines = 0
|
||||
isGameOver = false
|
||||
dropInterval = (1000 * Math.pow(0.8, (level - 1).toDouble())).toLong() // Set speed based on current level
|
||||
|
||||
// Reset scoring state
|
||||
combo = 0
|
||||
lastClearWasTetris = false
|
||||
lastClearWasPerfect = false
|
||||
lastClearWasAllClear = false
|
||||
lastPieceClearedLines = false
|
||||
|
||||
// Reset piece state
|
||||
holdPiece = null
|
||||
canHold = true
|
||||
bag.clear()
|
||||
|
||||
// Clear current and next pieces
|
||||
currentPiece = null
|
||||
nextPiece = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear completed lines and move blocks down (legacy method, kept for reference)
|
||||
*/
|
||||
private fun clearLines(): Int {
|
||||
return linesToClear.size // Return the number of lines that will be cleared
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current combo count
|
||||
*/
|
||||
fun getCombo(): Int = combo
|
||||
}
|
|
@ -1,196 +0,0 @@
|
|||
package com.mintris.model
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
|
||||
class StatsManager(context: Context) {
|
||||
private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
// Lifetime stats
|
||||
private var totalGames: Int = 0
|
||||
private var totalScore: Long = 0
|
||||
private var totalLines: Int = 0
|
||||
private var totalPieces: Int = 0
|
||||
private var totalTime: Long = 0
|
||||
private var maxLevel: Int = 0
|
||||
private var maxScore: Int = 0
|
||||
private var maxLines: Int = 0
|
||||
|
||||
// Line clear stats (lifetime)
|
||||
private var totalSingles: Int = 0
|
||||
private var totalDoubles: Int = 0
|
||||
private var totalTriples: Int = 0
|
||||
private var totalTetrises: Int = 0
|
||||
|
||||
// Session stats
|
||||
private var sessionScore: Int = 0
|
||||
private var sessionLines: Int = 0
|
||||
private var sessionPieces: Int = 0
|
||||
private var sessionTime: Long = 0
|
||||
private var sessionLevel: Int = 0
|
||||
|
||||
// Line clear stats (session)
|
||||
private var sessionSingles: Int = 0
|
||||
private var sessionDoubles: Int = 0
|
||||
private var sessionTriples: Int = 0
|
||||
private var sessionTetrises: Int = 0
|
||||
|
||||
init {
|
||||
loadStats()
|
||||
}
|
||||
|
||||
private fun loadStats() {
|
||||
totalGames = prefs.getInt(KEY_TOTAL_GAMES, 0)
|
||||
totalScore = prefs.getLong(KEY_TOTAL_SCORE, 0)
|
||||
totalLines = prefs.getInt(KEY_TOTAL_LINES, 0)
|
||||
totalPieces = prefs.getInt(KEY_TOTAL_PIECES, 0)
|
||||
totalTime = prefs.getLong(KEY_TOTAL_TIME, 0)
|
||||
maxLevel = prefs.getInt(KEY_MAX_LEVEL, 0)
|
||||
maxScore = prefs.getInt(KEY_MAX_SCORE, 0)
|
||||
maxLines = prefs.getInt(KEY_MAX_LINES, 0)
|
||||
|
||||
// Load line clear stats
|
||||
totalSingles = prefs.getInt(KEY_TOTAL_SINGLES, 0)
|
||||
totalDoubles = prefs.getInt(KEY_TOTAL_DOUBLES, 0)
|
||||
totalTriples = prefs.getInt(KEY_TOTAL_TRIPLES, 0)
|
||||
totalTetrises = prefs.getInt(KEY_TOTAL_TETRISES, 0)
|
||||
}
|
||||
|
||||
private fun saveStats() {
|
||||
prefs.edit()
|
||||
.putInt(KEY_TOTAL_GAMES, totalGames)
|
||||
.putLong(KEY_TOTAL_SCORE, totalScore)
|
||||
.putInt(KEY_TOTAL_LINES, totalLines)
|
||||
.putInt(KEY_TOTAL_PIECES, totalPieces)
|
||||
.putLong(KEY_TOTAL_TIME, totalTime)
|
||||
.putInt(KEY_MAX_LEVEL, maxLevel)
|
||||
.putInt(KEY_MAX_SCORE, maxScore)
|
||||
.putInt(KEY_MAX_LINES, maxLines)
|
||||
.putInt(KEY_TOTAL_SINGLES, totalSingles)
|
||||
.putInt(KEY_TOTAL_DOUBLES, totalDoubles)
|
||||
.putInt(KEY_TOTAL_TRIPLES, totalTriples)
|
||||
.putInt(KEY_TOTAL_TETRISES, totalTetrises)
|
||||
.apply()
|
||||
}
|
||||
|
||||
fun startNewSession() {
|
||||
sessionScore = 0
|
||||
sessionLines = 0
|
||||
sessionPieces = 0
|
||||
sessionTime = 0
|
||||
sessionLevel = 0
|
||||
sessionSingles = 0
|
||||
sessionDoubles = 0
|
||||
sessionTriples = 0
|
||||
sessionTetrises = 0
|
||||
}
|
||||
|
||||
fun updateSessionStats(score: Int, lines: Int, pieces: Int, time: Long, level: Int) {
|
||||
sessionScore = score
|
||||
sessionLines = lines
|
||||
sessionPieces = pieces
|
||||
sessionTime = time
|
||||
sessionLevel = level
|
||||
}
|
||||
|
||||
fun recordLineClear(lineCount: Int) {
|
||||
when (lineCount) {
|
||||
1 -> {
|
||||
sessionSingles++
|
||||
totalSingles++
|
||||
}
|
||||
2 -> {
|
||||
sessionDoubles++
|
||||
totalDoubles++
|
||||
}
|
||||
3 -> {
|
||||
sessionTriples++
|
||||
totalTriples++
|
||||
}
|
||||
4 -> {
|
||||
sessionTetrises++
|
||||
totalTetrises++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun endSession() {
|
||||
totalGames++
|
||||
totalScore += sessionScore
|
||||
totalLines += sessionLines
|
||||
totalPieces += sessionPieces
|
||||
totalTime += sessionTime
|
||||
|
||||
if (sessionLevel > maxLevel) maxLevel = sessionLevel
|
||||
if (sessionScore > maxScore) maxScore = sessionScore
|
||||
if (sessionLines > maxLines) maxLines = sessionLines
|
||||
|
||||
saveStats()
|
||||
}
|
||||
|
||||
// Getters for lifetime stats
|
||||
fun getTotalGames(): Int = totalGames
|
||||
fun getTotalScore(): Long = totalScore
|
||||
fun getTotalLines(): Int = totalLines
|
||||
fun getTotalPieces(): Int = totalPieces
|
||||
fun getTotalTime(): Long = totalTime
|
||||
fun getMaxLevel(): Int = maxLevel
|
||||
fun getMaxScore(): Int = maxScore
|
||||
fun getMaxLines(): Int = maxLines
|
||||
|
||||
// Getters for line clear stats (lifetime)
|
||||
fun getTotalSingles(): Int = totalSingles
|
||||
fun getTotalDoubles(): Int = totalDoubles
|
||||
fun getTotalTriples(): Int = totalTriples
|
||||
fun getTotalTetrises(): Int = totalTetrises
|
||||
|
||||
// Getters for session stats
|
||||
fun getSessionScore(): Int = sessionScore
|
||||
fun getSessionLines(): Int = sessionLines
|
||||
fun getSessionPieces(): Int = sessionPieces
|
||||
fun getSessionTime(): Long = sessionTime
|
||||
fun getSessionLevel(): Int = sessionLevel
|
||||
|
||||
// Getters for line clear stats (session)
|
||||
fun getSessionSingles(): Int = sessionSingles
|
||||
fun getSessionDoubles(): Int = sessionDoubles
|
||||
fun getSessionTriples(): Int = sessionTriples
|
||||
fun getSessionTetrises(): Int = sessionTetrises
|
||||
|
||||
fun resetStats() {
|
||||
// Reset all lifetime stats
|
||||
totalGames = 0
|
||||
totalScore = 0
|
||||
totalLines = 0
|
||||
totalPieces = 0
|
||||
totalTime = 0
|
||||
maxLevel = 0
|
||||
maxScore = 0
|
||||
maxLines = 0
|
||||
|
||||
// Reset line clear stats
|
||||
totalSingles = 0
|
||||
totalDoubles = 0
|
||||
totalTriples = 0
|
||||
totalTetrises = 0
|
||||
|
||||
// Save the reset stats
|
||||
saveStats()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREFS_NAME = "mintris_stats"
|
||||
private const val KEY_TOTAL_GAMES = "total_games"
|
||||
private const val KEY_TOTAL_SCORE = "total_score"
|
||||
private const val KEY_TOTAL_LINES = "total_lines"
|
||||
private const val KEY_TOTAL_PIECES = "total_pieces"
|
||||
private const val KEY_TOTAL_TIME = "total_time"
|
||||
private const val KEY_MAX_LEVEL = "max_level"
|
||||
private const val KEY_MAX_SCORE = "max_score"
|
||||
private const val KEY_MAX_LINES = "max_lines"
|
||||
private const val KEY_TOTAL_SINGLES = "total_singles"
|
||||
private const val KEY_TOTAL_DOUBLES = "total_doubles"
|
||||
private const val KEY_TOTAL_TRIPLES = "total_triples"
|
||||
private const val KEY_TOTAL_TETRISES = "total_tetrises"
|
||||
}
|
||||
}
|
|
@ -1,17 +1,15 @@
|
|||
package com.mintris
|
||||
package com.pixelmintdrop
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Bundle
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.mintris.databinding.HighScoreEntryBinding
|
||||
import com.mintris.model.HighScore
|
||||
import com.mintris.model.HighScoreManager
|
||||
import com.mintris.model.PlayerProgressionManager
|
||||
import com.pixelmintdrop.databinding.HighScoreEntryBinding
|
||||
import com.pixelmintdrop.model.HighScore
|
||||
import com.pixelmintdrop.model.HighScoreManager
|
||||
import com.pixelmintdrop.model.PlayerProgressionManager
|
||||
import android.graphics.Color
|
||||
import android.view.KeyEvent
|
||||
import android.view.InputDevice
|
||||
|
||||
class HighScoreEntryActivity : AppCompatActivity() {
|
||||
private lateinit var binding: HighScoreEntryBinding
|
||||
|
@ -39,20 +37,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
|
||||
|
@ -66,8 +99,7 @@ class HighScoreEntryActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
private fun loadThemePreference(): String {
|
||||
val prefs = getSharedPreferences("mintris_settings", MODE_PRIVATE)
|
||||
return prefs.getString("selected_theme", PlayerProgressionManager.THEME_CLASSIC) ?: PlayerProgressionManager.THEME_CLASSIC
|
||||
return progressionManager.getSelectedTheme()
|
||||
}
|
||||
|
||||
private fun applyTheme(themeId: String) {
|
|
@ -1,17 +1,16 @@
|
|||
package com.mintris
|
||||
package com.pixelmintdrop
|
||||
|
||||
import android.os.Bundle
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.mintris.databinding.HighScoresBinding
|
||||
import com.mintris.model.HighScoreAdapter
|
||||
import com.mintris.model.HighScoreManager
|
||||
import com.mintris.model.PlayerProgressionManager
|
||||
import com.pixelmintdrop.databinding.HighScoresBinding
|
||||
import com.pixelmintdrop.model.HighScoreAdapter
|
||||
import com.pixelmintdrop.model.HighScoreManager
|
||||
import com.pixelmintdrop.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
|
||||
|
@ -58,8 +57,7 @@ class HighScoresActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
private fun loadThemePreference(): String {
|
||||
val prefs = getSharedPreferences("mintris_settings", MODE_PRIVATE)
|
||||
return prefs.getString("selected_theme", PlayerProgressionManager.THEME_CLASSIC) ?: PlayerProgressionManager.THEME_CLASSIC
|
||||
return progressionManager.getSelectedTheme()
|
||||
}
|
||||
|
||||
private fun applyTheme(themeId: String) {
|
||||
|
@ -114,4 +112,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
|
||||
}
|
||||
}
|
2041
app/src/main/java/com/pixelmintdrop/MainActivity.kt
Normal file
2041
app/src/main/java/com/pixelmintdrop/MainActivity.kt
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,13 +1,11 @@
|
|||
package com.mintris
|
||||
package com.pixelmintdrop
|
||||
|
||||
import android.os.Bundle
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.mintris.databinding.ActivityStatsBinding
|
||||
import com.mintris.model.StatsManager
|
||||
import com.mintris.model.PlayerProgressionManager
|
||||
import com.pixelmintdrop.databinding.ActivityStatsBinding
|
||||
import com.pixelmintdrop.model.StatsManager
|
||||
import com.pixelmintdrop.model.PlayerProgressionManager
|
||||
import android.graphics.Color
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
@ -44,8 +42,7 @@ class StatsActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
private fun loadThemePreference(): String {
|
||||
val prefs = getSharedPreferences("mintris_settings", MODE_PRIVATE)
|
||||
return prefs.getString("selected_theme", PlayerProgressionManager.THEME_CLASSIC) ?: PlayerProgressionManager.THEME_CLASSIC
|
||||
return progressionManager.getSelectedTheme()
|
||||
}
|
||||
|
||||
private fun applyTheme(themeId: String) {
|
||||
|
@ -81,7 +78,7 @@ class StatsActivity : AppCompatActivity() {
|
|||
binding.totalSinglesText.setTextColor(textColor)
|
||||
binding.totalDoublesText.setTextColor(textColor)
|
||||
binding.totalTriplesText.setTextColor(textColor)
|
||||
binding.totalTetrisesText.setTextColor(textColor)
|
||||
binding.totalQuadsText.setTextColor(textColor)
|
||||
binding.maxLevelText.setTextColor(textColor)
|
||||
binding.maxScoreText.setTextColor(textColor)
|
||||
binding.maxLinesText.setTextColor(textColor)
|
||||
|
@ -119,7 +116,7 @@ class StatsActivity : AppCompatActivity() {
|
|||
binding.totalSinglesText.text = getString(R.string.singles, statsManager.getTotalSingles())
|
||||
binding.totalDoublesText.text = getString(R.string.doubles, statsManager.getTotalDoubles())
|
||||
binding.totalTriplesText.text = getString(R.string.triples, statsManager.getTotalTriples())
|
||||
binding.totalTetrisesText.text = getString(R.string.tetrises, statsManager.getTotalTetrises())
|
||||
binding.totalQuadsText.text = getString(R.string.quads, statsManager.getTotalQuads())
|
||||
|
||||
// Update best performance stats
|
||||
binding.maxLevelText.text = getString(R.string.max_level, statsManager.getMaxLevel())
|
19
app/src/main/java/com/pixelmintdrop/ThemeManager.kt
Normal file
19
app/src/main/java/com/pixelmintdrop/ThemeManager.kt
Normal file
|
@ -0,0 +1,19 @@
|
|||
package com.pixelmintdrop
|
||||
|
||||
import android.graphics.Color
|
||||
|
||||
object ThemeManager {
|
||||
data class ThemeColors(
|
||||
val background: Int,
|
||||
val text: Int,
|
||||
val accent: Int
|
||||
)
|
||||
|
||||
fun getThemeColors(): ThemeColors {
|
||||
return ThemeColors(
|
||||
background = Color.BLACK,
|
||||
text = Color.WHITE,
|
||||
accent = Color.WHITE
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
package com.pixelmintdrop.accessibility
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.view.View
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
import android.view.accessibility.AccessibilityManager
|
||||
import android.widget.ImageButton
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.ViewCompat
|
||||
import com.pixelmintdrop.R
|
||||
import com.pixelmintdrop.game.GameView
|
||||
import com.pixelmintdrop.model.GamePieceType
|
||||
|
||||
/**
|
||||
* Helper class to improve the game's accessibility for users with visual impairments
|
||||
* or other accessibility needs.
|
||||
*/
|
||||
class GameAccessibilityHelper(private val context: Context) {
|
||||
|
||||
private val accessibilityManager = context.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
|
||||
|
||||
/**
|
||||
* Sets up accessibility content descriptions for game controls
|
||||
*/
|
||||
fun setupAccessibilityDescriptions(
|
||||
leftButton: ImageButton?,
|
||||
rightButton: ImageButton?,
|
||||
rotateButton: ImageButton?,
|
||||
dropButton: ImageButton?,
|
||||
holdButton: ImageButton?,
|
||||
pauseButton: ImageButton?,
|
||||
gameView: GameView?,
|
||||
holdPieceView: View?,
|
||||
nextPieceView: View?
|
||||
) {
|
||||
// Set content descriptions for all control buttons
|
||||
leftButton?.contentDescription = context.getString(R.string.accessibility_move_left)
|
||||
rightButton?.contentDescription = context.getString(R.string.accessibility_move_right)
|
||||
rotateButton?.contentDescription = context.getString(R.string.accessibility_rotate_piece)
|
||||
dropButton?.contentDescription = context.getString(R.string.accessibility_drop_piece)
|
||||
holdButton?.contentDescription = context.getString(R.string.accessibility_hold_piece)
|
||||
pauseButton?.contentDescription = context.getString(R.string.accessibility_pause_game)
|
||||
|
||||
// Set content descriptions for game views
|
||||
gameView?.contentDescription = context.getString(R.string.accessibility_game_board)
|
||||
holdPieceView?.contentDescription = context.getString(R.string.accessibility_held_piece)
|
||||
nextPieceView?.contentDescription = context.getString(R.string.accessibility_next_piece)
|
||||
|
||||
// Add accessibility delegate to the game view for custom events
|
||||
gameView?.let {
|
||||
ViewCompat.setAccessibilityDelegate(it, object : androidx.core.view.AccessibilityDelegateCompat() {
|
||||
override fun onPopulateAccessibilityEvent(host: View, event: AccessibilityEvent) {
|
||||
super.onPopulateAccessibilityEvent(host, event)
|
||||
|
||||
// Add custom content to accessibility events if needed
|
||||
if (event.eventType == AccessibilityEvent.TYPE_VIEW_SELECTED) {
|
||||
// Example: announce the current piece type
|
||||
event.text.add("Current piece: ${getCurrentPieceDescription(it)}")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Announces important game events via accessibility services
|
||||
*/
|
||||
fun announceGameEvent(view: View, message: String) {
|
||||
if (accessibilityManager.isEnabled) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
view.announceForAccessibility(message)
|
||||
} else {
|
||||
// For older Android versions
|
||||
val event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_ANNOUNCEMENT)
|
||||
event.text.add(message)
|
||||
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates score and level information to be more accessible
|
||||
*/
|
||||
fun updateGameStatusAccessibility(
|
||||
scoreTextView: TextView?,
|
||||
levelTextView: TextView?,
|
||||
linesTextView: TextView?,
|
||||
score: Long,
|
||||
level: Int,
|
||||
lines: Int
|
||||
) {
|
||||
// Make sure score is accessible to screen readers with proper formatting
|
||||
scoreTextView?.let {
|
||||
it.contentDescription = "Score: $score points"
|
||||
}
|
||||
|
||||
levelTextView?.let {
|
||||
it.contentDescription = "Level: $level"
|
||||
}
|
||||
|
||||
linesTextView?.let {
|
||||
it.contentDescription = "Lines cleared: $lines"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a description of the current Tetromino piece for accessibility announcements
|
||||
*/
|
||||
private fun getCurrentPieceDescription(gameView: GameView): String {
|
||||
val pieceType = gameView.getCurrentPieceType() ?: return "No piece"
|
||||
|
||||
return when (pieceType) {
|
||||
GamePieceType.I -> "I piece, long bar"
|
||||
GamePieceType.J -> "J piece, hook shape pointing left"
|
||||
GamePieceType.L -> "L piece, hook shape pointing right"
|
||||
GamePieceType.O -> "O piece, square shape"
|
||||
GamePieceType.S -> "S piece, zigzag shape"
|
||||
GamePieceType.T -> "T piece, T shape"
|
||||
GamePieceType.Z -> "Z piece, reverse zigzag shape"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Announce the game over event with final score
|
||||
*/
|
||||
fun announceGameOver(view: View, score: Long) {
|
||||
announceGameEvent(view, "Game over. Final score: $score points")
|
||||
}
|
||||
|
||||
/**
|
||||
* Announce level up
|
||||
*/
|
||||
fun announceLevelUp(view: View, newLevel: Int) {
|
||||
announceGameEvent(view, "Level up! Now at level $newLevel")
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the piece name for accessibility announcements
|
||||
*/
|
||||
private fun getPieceName(type: GamePieceType): String {
|
||||
return when (type) {
|
||||
GamePieceType.I -> "I piece"
|
||||
GamePieceType.J -> "J piece"
|
||||
GamePieceType.L -> "L piece"
|
||||
GamePieceType.O -> "O piece"
|
||||
GamePieceType.S -> "S piece"
|
||||
GamePieceType.T -> "T piece"
|
||||
GamePieceType.Z -> "Z piece"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,20 +1,22 @@
|
|||
package com.mintris.audio
|
||||
package com.pixelmintdrop.audio
|
||||
|
||||
import android.content.Context
|
||||
import android.media.MediaPlayer
|
||||
import android.media.AudioAttributes
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import com.mintris.R
|
||||
import com.pixelmintdrop.R
|
||||
|
||||
class GameMusic(private val context: Context) {
|
||||
private var mediaPlayer: MediaPlayer? = null
|
||||
private var gameOverPlayer: MediaPlayer? = null
|
||||
private var isEnabled = true
|
||||
private var isPrepared = false
|
||||
|
||||
init {
|
||||
try {
|
||||
setupMediaPlayer()
|
||||
setupGameOverPlayer()
|
||||
} catch (e: Exception) {
|
||||
Log.e("GameMusic", "Error initializing: ${e.message}")
|
||||
}
|
||||
|
@ -46,6 +48,49 @@ class GameMusic(private val context: Context) {
|
|||
}
|
||||
}
|
||||
|
||||
private fun setupGameOverPlayer() {
|
||||
try {
|
||||
Log.d("GameMusic", "Setting up GameOver MediaPlayer")
|
||||
gameOverPlayer = MediaPlayer.create(context, R.raw.game_over).apply {
|
||||
setVolume(1.0f, 1.0f) // Increased from 0.7f to 1.0f for maximum volume
|
||||
|
||||
// Set audio attributes for better performance
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
setAudioAttributes(
|
||||
AudioAttributes.Builder()
|
||||
.setUsage(AudioAttributes.USAGE_GAME)
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
Log.d("GameMusic", "GameOver MediaPlayer setup complete")
|
||||
} catch (e: Exception) {
|
||||
Log.e("GameMusic", "Error setting up GameOver MediaPlayer", e)
|
||||
gameOverPlayer = null
|
||||
}
|
||||
}
|
||||
|
||||
fun playGameOver() {
|
||||
if (isEnabled && gameOverPlayer != null) {
|
||||
try {
|
||||
Log.d("GameMusic", "Playing game over sound")
|
||||
// Temporarily lower background music volume
|
||||
mediaPlayer?.setVolume(0.2f, 0.2f)
|
||||
|
||||
// Play game over sound
|
||||
gameOverPlayer?.start()
|
||||
|
||||
// Restore background music volume after a delay
|
||||
gameOverPlayer?.setOnCompletionListener {
|
||||
mediaPlayer?.setVolume(0.5f, 0.5f)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("GameMusic", "Error playing game over sound: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun start() {
|
||||
if (isEnabled && mediaPlayer != null && isPrepared) {
|
||||
try {
|
||||
|
@ -103,14 +148,25 @@ class GameMusic(private val context: Context) {
|
|||
|
||||
fun isEnabled(): Boolean = isEnabled
|
||||
|
||||
fun release() {
|
||||
/**
|
||||
* Releases all media player resources to prevent memory leaks
|
||||
*/
|
||||
fun releaseResources() {
|
||||
try {
|
||||
Log.d("GameMusic", "Releasing MediaPlayer")
|
||||
Log.d("GameMusic", "Releasing MediaPlayer resources")
|
||||
mediaPlayer?.release()
|
||||
gameOverPlayer?.release()
|
||||
mediaPlayer = null
|
||||
gameOverPlayer = null
|
||||
isPrepared = false
|
||||
} catch (e: Exception) {
|
||||
Log.e("GameMusic", "Error releasing music: ${e.message}")
|
||||
Log.e("GameMusic", "Error releasing music resources: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
// Keeping old method for backward compatibility
|
||||
@Deprecated("Use releaseResources() instead", ReplaceWith("releaseResources()"))
|
||||
fun release() {
|
||||
releaseResources()
|
||||
}
|
||||
}
|
136
app/src/main/java/com/pixelmintdrop/game/GameHaptics.kt
Normal file
136
app/src/main/java/com/pixelmintdrop/game/GameHaptics.kt
Normal file
|
@ -0,0 +1,136 @@
|
|||
package com.pixelmintdrop.game
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.VibrationEffect
|
||||
import android.os.Vibrator
|
||||
import android.os.VibratorManager
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.View
|
||||
import android.util.Log
|
||||
|
||||
/**
|
||||
* Handles haptic feedback for game events
|
||||
*/
|
||||
class GameHaptics(private val context: Context) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "GameHaptics"
|
||||
}
|
||||
|
||||
// Vibrator service
|
||||
private val vibrator: Vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
val vibratorManager = context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
|
||||
vibratorManager.defaultVibrator
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||
}
|
||||
|
||||
// Track if gamepad is connected
|
||||
private var isGamepadConnected = false
|
||||
|
||||
// Set gamepad connection state
|
||||
fun setGamepadConnected(connected: Boolean) {
|
||||
isGamepadConnected = connected
|
||||
}
|
||||
|
||||
// Get vibration multiplier based on input method
|
||||
private fun getVibrationMultiplier(): Float {
|
||||
return if (isGamepadConnected) 1.5f else 1.0f
|
||||
}
|
||||
|
||||
// Vibrate for line clear (more intense for more lines)
|
||||
fun vibrateForLineClear(lineCount: Int) {
|
||||
Log.d(TAG, "Attempting to vibrate for $lineCount lines")
|
||||
|
||||
// Only proceed if the device has a vibrator and it's available
|
||||
if (!vibrator.hasVibrator()) return
|
||||
|
||||
// Scale duration and amplitude based on line count and input method
|
||||
val multiplier = getVibrationMultiplier()
|
||||
val duration = when(lineCount) {
|
||||
1 -> (50L * multiplier).toLong() // Single line: short vibration
|
||||
2 -> (80L * multiplier).toLong() // Double line: slightly longer
|
||||
3 -> (120L * multiplier).toLong() // Triple line: even longer
|
||||
4 -> (200L * multiplier).toLong() // Quad: longest vibration
|
||||
else -> (50L * multiplier).toLong()
|
||||
}
|
||||
|
||||
val amplitude = when(lineCount) {
|
||||
1 -> (80 * multiplier).toInt().coerceAtMost(255) // Single line: mild vibration
|
||||
2 -> (120 * multiplier).toInt().coerceAtMost(255) // Double line: medium vibration
|
||||
3 -> (180 * multiplier).toInt().coerceAtMost(255) // Triple line: strong vibration
|
||||
4 -> 255 // Quad: maximum vibration
|
||||
else -> (80 * multiplier).toInt().coerceAtMost(255)
|
||||
}
|
||||
|
||||
Log.d(TAG, "Vibration parameters - Duration: ${duration}ms, Amplitude: $amplitude")
|
||||
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
vibrator.vibrate(VibrationEffect.createOneShot(duration, amplitude))
|
||||
Log.d(TAG, "Vibration triggered successfully")
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
vibrator.vibrate(duration)
|
||||
Log.w(TAG, "Device does not support vibration effects (Android < 8.0)")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error triggering vibration", e)
|
||||
}
|
||||
}
|
||||
|
||||
fun performHapticFeedback(view: View, feedbackType: Int) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
view.performHapticFeedback(HapticFeedbackConstants.CONFIRM)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
view.performHapticFeedback(feedbackType)
|
||||
}
|
||||
}
|
||||
|
||||
fun vibrateForPieceLock() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val multiplier = getVibrationMultiplier()
|
||||
val duration = (50L * multiplier).toLong()
|
||||
val amplitude = (VibrationEffect.DEFAULT_AMPLITUDE * multiplier).toInt().coerceAtMost(255)
|
||||
val vibrationEffect = VibrationEffect.createOneShot(duration, amplitude)
|
||||
vibrator.vibrate(vibrationEffect)
|
||||
}
|
||||
}
|
||||
|
||||
fun vibrateForPieceMove() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val multiplier = getVibrationMultiplier()
|
||||
val duration = (20L * multiplier).toLong()
|
||||
val amplitude = (VibrationEffect.DEFAULT_AMPLITUDE * 0.3 * multiplier).toInt().coerceAtLeast(1).coerceAtMost(255)
|
||||
val vibrationEffect = VibrationEffect.createOneShot(duration, amplitude)
|
||||
vibrator.vibrate(vibrationEffect)
|
||||
}
|
||||
}
|
||||
|
||||
fun vibrateForGameOver() {
|
||||
Log.d(TAG, "Attempting to vibrate for game over")
|
||||
|
||||
// Only proceed if the device has a vibrator and it's available
|
||||
if (!vibrator.hasVibrator()) return
|
||||
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val multiplier = getVibrationMultiplier()
|
||||
val duration = (300L * multiplier).toLong()
|
||||
val amplitude = (VibrationEffect.DEFAULT_AMPLITUDE * multiplier).toInt().coerceAtMost(255)
|
||||
val vibrationEffect = VibrationEffect.createOneShot(duration, amplitude)
|
||||
vibrator.vibrate(vibrationEffect)
|
||||
Log.d(TAG, "Game over vibration triggered successfully")
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
vibrator.vibrate(300L)
|
||||
Log.w(TAG, "Device does not support vibration effects (Android < 8.0)")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error triggering game over vibration", e)
|
||||
}
|
||||
}
|
||||
}
|
154
app/src/main/java/com/pixelmintdrop/game/GameLifecycleManager.kt
Normal file
154
app/src/main/java/com/pixelmintdrop/game/GameLifecycleManager.kt
Normal file
|
@ -0,0 +1,154 @@
|
|||
package com.pixelmintdrop.game
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import com.google.gson.Gson
|
||||
import com.pixelmintdrop.audio.GameMusic
|
||||
import com.pixelmintdrop.model.GameBoard
|
||||
import com.pixelmintdrop.model.HighScoreManager
|
||||
import com.pixelmintdrop.model.StatsManager
|
||||
|
||||
/**
|
||||
* Handles the game's lifecycle events to ensure proper resource management
|
||||
* and state saving/restoration.
|
||||
*/
|
||||
class GameLifecycleManager(private val context: Context) {
|
||||
private val sharedPreferences: SharedPreferences =
|
||||
context.getSharedPreferences("com.com.pixelmintgames.pixelmintdrop.game_state", Context.MODE_PRIVATE)
|
||||
private val gson = Gson()
|
||||
|
||||
/**
|
||||
* Save the current game state when the app is paused
|
||||
*/
|
||||
fun saveGameState(
|
||||
gameBoard: GameBoard,
|
||||
currentScore: Long,
|
||||
currentLevel: Int,
|
||||
piecesPlaced: Int,
|
||||
gameStartTime: Long
|
||||
) {
|
||||
val editor = sharedPreferences.edit()
|
||||
|
||||
// Save primitive data
|
||||
editor.putLong("current_score", currentScore)
|
||||
editor.putInt("current_level", currentLevel)
|
||||
editor.putInt("pieces_placed", piecesPlaced)
|
||||
editor.putLong("game_start_time", gameStartTime)
|
||||
|
||||
// Save complex GameBoard state - we don't serialize the GameBoard directly
|
||||
// Instead we save key properties that can be used to recreate the board state
|
||||
try {
|
||||
editor.putInt("board_level", gameBoard.level)
|
||||
editor.putInt("board_lines", gameBoard.lines)
|
||||
editor.putInt("board_score", gameBoard.score)
|
||||
} catch (e: Exception) {
|
||||
// If serialization fails, just clear the saved state
|
||||
editor.remove("board_level")
|
||||
editor.remove("board_lines")
|
||||
editor.remove("board_score")
|
||||
}
|
||||
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the saved game state when the app is resumed
|
||||
* Returns a bundle with the restored state or null if no state exists
|
||||
*/
|
||||
fun restoreGameState(): Bundle? {
|
||||
// Check if we have a saved state
|
||||
if (!sharedPreferences.contains("current_score")) {
|
||||
return null
|
||||
}
|
||||
|
||||
val bundle = Bundle()
|
||||
|
||||
// Restore primitive data
|
||||
bundle.putLong("current_score", sharedPreferences.getLong("current_score", 0))
|
||||
bundle.putInt("current_level", sharedPreferences.getInt("current_level", 1))
|
||||
bundle.putInt("pieces_placed", sharedPreferences.getInt("pieces_placed", 0))
|
||||
bundle.putLong("game_start_time", sharedPreferences.getLong("game_start_time", 0))
|
||||
|
||||
// We don't try to deserialize the GameBoard, as it's too complex
|
||||
// Instead, we'll create a new game board and apply saved properties later
|
||||
bundle.putInt("board_level", sharedPreferences.getInt("board_level", 1))
|
||||
bundle.putInt("board_lines", sharedPreferences.getInt("board_lines", 0))
|
||||
bundle.putInt("board_score", sharedPreferences.getInt("board_score", 0))
|
||||
|
||||
return bundle
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the saved game state
|
||||
*/
|
||||
fun clearGameState() {
|
||||
sharedPreferences.edit().clear().apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle activity pause
|
||||
*/
|
||||
fun onPause(
|
||||
gameView: GameView?,
|
||||
gameMusic: GameMusic?,
|
||||
statsManager: StatsManager?,
|
||||
highScoreManager: HighScoreManager?
|
||||
) {
|
||||
// Pause the game view
|
||||
gameView?.let {
|
||||
if (!it.isPaused && !it.isGameOver()) {
|
||||
it.pause()
|
||||
}
|
||||
}
|
||||
|
||||
// Pause music
|
||||
gameMusic?.pause()
|
||||
|
||||
// Save any pending stats and scores - these methods must be made public in their respective classes
|
||||
// or this functionality should be removed if those methods can't be accessed
|
||||
try {
|
||||
statsManager?.let { /* Call public save method if available */ }
|
||||
highScoreManager?.let { /* Call public save method if available */ }
|
||||
} catch (e: Exception) {
|
||||
// Log error but continue
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle activity resume
|
||||
*/
|
||||
fun onResume(
|
||||
gameView: GameView?,
|
||||
gameMusic: GameMusic?,
|
||||
isMusicEnabled: Boolean
|
||||
) {
|
||||
// Resume music if enabled
|
||||
if (isMusicEnabled) {
|
||||
gameMusic?.resume()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle activity destroy
|
||||
*/
|
||||
fun onDestroy(
|
||||
gameView: GameView?,
|
||||
gameMusic: GameMusic?,
|
||||
statsManager: StatsManager?,
|
||||
highScoreManager: HighScoreManager?
|
||||
) {
|
||||
// Release resources
|
||||
gameView?.releaseResources()
|
||||
gameMusic?.releaseResources()
|
||||
|
||||
// Save any pending data - these methods must be made public in their respective classes
|
||||
// or this functionality should be removed if those methods can't be accessed
|
||||
try {
|
||||
statsManager?.let { /* Call public save method if available */ }
|
||||
highScoreManager?.let { /* Call public save method if available */ }
|
||||
} catch (e: Exception) {
|
||||
// Log error but continue
|
||||
}
|
||||
}
|
||||
}
|
1237
app/src/main/java/com/pixelmintdrop/game/GameView.kt
Normal file
1237
app/src/main/java/com/pixelmintdrop/game/GameView.kt
Normal file
File diff suppressed because it is too large
Load diff
510
app/src/main/java/com/pixelmintdrop/game/GamepadController.kt
Normal file
510
app/src/main/java/com/pixelmintdrop/game/GamepadController.kt
Normal file
|
@ -0,0 +1,510 @@
|
|||
package com.pixelmintdrop.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.os.Handler
|
||||
import android.os.Looper
|
||||
|
||||
/**
|
||||
* GamepadController handles gamepad input for the pixelmintdrop game.
|
||||
* Supports multiple gamepad types including:
|
||||
* - Microsoft Xbox controllers
|
||||
* - Sony PlayStation controllers
|
||||
* - Nintendo Switch controllers
|
||||
* - Backbone controllers
|
||||
*/
|
||||
class GamepadController(
|
||||
private val gameView: GameView
|
||||
) {
|
||||
companion object {
|
||||
private const val TAG = "GamepadController"
|
||||
|
||||
// Deadzone for analog sticks (normalized value 0.0-1.0)
|
||||
private const val STICK_DEADZONE = 0.30f
|
||||
|
||||
// Cooldown times for responsive input without repeating too quickly
|
||||
private const val MOVE_COOLDOWN_MS = 100L
|
||||
private const val ROTATION_COOLDOWN_MS = 150L
|
||||
private const val HARD_DROP_COOLDOWN_MS = 200L
|
||||
private const val HOLD_COOLDOWN_MS = 250L
|
||||
|
||||
// Continuous movement repeat delay
|
||||
private const val CONTINUOUS_MOVEMENT_DELAY_MS = 100L
|
||||
|
||||
// Rumble patterns
|
||||
private const val RUMBLE_MOVE_DURATION_MS = 20L
|
||||
private const val RUMBLE_ROTATE_DURATION_MS = 30L
|
||||
private const val RUMBLE_HARD_DROP_DURATION_MS = 100L
|
||||
private const val RUMBLE_LINE_CLEAR_DURATION_MS = 150L
|
||||
|
||||
// Check if device is a gamepad
|
||||
fun isGamepad(device: InputDevice?): Boolean {
|
||||
if (device == null) return false
|
||||
|
||||
// Check for gamepad via input device sources
|
||||
val sources = device.sources
|
||||
return (sources and InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD ||
|
||||
(sources and InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK
|
||||
}
|
||||
|
||||
// Get a list of all connected gamepads
|
||||
fun getGamepads(): List<InputDevice> {
|
||||
val gamepads = mutableListOf<InputDevice>()
|
||||
val deviceIds = InputDevice.getDeviceIds()
|
||||
|
||||
for (deviceId in deviceIds) {
|
||||
val device = InputDevice.getDevice(deviceId)
|
||||
if (device != null && isGamepad(device)) {
|
||||
gamepads.add(device)
|
||||
}
|
||||
}
|
||||
|
||||
return gamepads
|
||||
}
|
||||
|
||||
// Check if any gamepad is connected
|
||||
fun isGamepadConnected(): Boolean {
|
||||
return getGamepads().isNotEmpty()
|
||||
}
|
||||
|
||||
// Get the name of the first connected gamepad
|
||||
fun getConnectedGamepadName(): String? {
|
||||
val gamepads = getGamepads()
|
||||
if (gamepads.isEmpty()) return null
|
||||
return gamepads.first().name
|
||||
}
|
||||
|
||||
// Get information about all connected gamepads
|
||||
fun getConnectedGamepadsInfo(): List<String> {
|
||||
return getGamepads().map { it.name }
|
||||
}
|
||||
|
||||
// Check if device supports vibration
|
||||
fun supportsVibration(device: InputDevice?): Boolean {
|
||||
if (device == null) return false
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return false
|
||||
|
||||
return device.vibratorManager?.vibratorIds?.isNotEmpty() ?: false
|
||||
}
|
||||
}
|
||||
|
||||
// Timestamps for cooldowns
|
||||
private var lastMoveTime = 0L
|
||||
private var lastRotationTime = 0L
|
||||
private var lastHardDropTime = 0L
|
||||
private var lastHoldTime = 0L
|
||||
|
||||
// Track current directional input state
|
||||
private var isMovingLeft = false
|
||||
private var isMovingRight = false
|
||||
private var isMovingDown = false
|
||||
|
||||
// Handler for continuous movement
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private val moveLeftRunnable = object : Runnable {
|
||||
override fun run() {
|
||||
if (isMovingLeft && gameView.isActive()) {
|
||||
gameView.moveLeft()
|
||||
vibrateForPieceMove()
|
||||
handler.postDelayed(this, CONTINUOUS_MOVEMENT_DELAY_MS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val moveRightRunnable = object : Runnable {
|
||||
override fun run() {
|
||||
if (isMovingRight && gameView.isActive()) {
|
||||
gameView.moveRight()
|
||||
vibrateForPieceMove()
|
||||
handler.postDelayed(this, CONTINUOUS_MOVEMENT_DELAY_MS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val moveDownRunnable = object : Runnable {
|
||||
override fun run() {
|
||||
if (isMovingDown && gameView.isActive()) {
|
||||
gameView.softDrop()
|
||||
vibrateForPieceMove()
|
||||
handler.postDelayed(this, CONTINUOUS_MOVEMENT_DELAY_MS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Callback interfaces
|
||||
interface GamepadConnectionListener {
|
||||
fun onGamepadConnected(gamepadName: String)
|
||||
fun onGamepadDisconnected(gamepadName: String)
|
||||
}
|
||||
|
||||
interface GamepadMenuListener {
|
||||
fun onPauseRequested()
|
||||
}
|
||||
|
||||
interface GamepadNavigationListener {
|
||||
fun onMenuUp()
|
||||
fun onMenuDown()
|
||||
fun onMenuSelect()
|
||||
fun onMenuLeft()
|
||||
fun onMenuRight()
|
||||
}
|
||||
|
||||
// Listeners
|
||||
private var connectionListener: GamepadConnectionListener? = null
|
||||
private var menuListener: GamepadMenuListener? = null
|
||||
private var navigationListener: GamepadNavigationListener? = null
|
||||
|
||||
// Currently active gamepad for rumble
|
||||
private var activeGamepad: InputDevice? = null
|
||||
|
||||
/**
|
||||
* Set a listener for gamepad connection events
|
||||
*/
|
||||
fun setGamepadConnectionListener(listener: GamepadConnectionListener) {
|
||||
connectionListener = listener
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a listener for gamepad menu events (pause/start button)
|
||||
*/
|
||||
fun setGamepadMenuListener(listener: GamepadMenuListener) {
|
||||
menuListener = listener
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a listener for gamepad navigation events
|
||||
*/
|
||||
fun setGamepadNavigationListener(listener: GamepadNavigationListener) {
|
||||
navigationListener = listener
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to check for newly connected gamepads.
|
||||
* Call this periodically from the activity to detect connection changes.
|
||||
*/
|
||||
fun checkForGamepadChanges(context: Context) {
|
||||
// Implementation would track previous and current gamepads
|
||||
// and notify through the connectionListener
|
||||
// This would be called from the activity's onResume or via a handler
|
||||
}
|
||||
|
||||
/**
|
||||
* Vibrate the gamepad if supported
|
||||
*/
|
||||
fun vibrateGamepad(durationMs: Long, amplitude: Int) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return
|
||||
|
||||
val gamepad = activeGamepad ?: return
|
||||
|
||||
if (supportsVibration(gamepad)) {
|
||||
try {
|
||||
val vibrator = gamepad.vibratorManager
|
||||
val vibratorIds = vibrator.vibratorIds
|
||||
|
||||
if (vibratorIds.isNotEmpty()) {
|
||||
val effect = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
VibrationEffect.createOneShot(durationMs, amplitude)
|
||||
} else {
|
||||
// For older devices, fall back to a simple vibration
|
||||
VibrationEffect.createOneShot(durationMs, VibrationEffect.DEFAULT_AMPLITUDE)
|
||||
}
|
||||
|
||||
// Create combined vibration for Android S+
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
val combinedVibration = android.os.CombinedVibration.createParallel(effect)
|
||||
vibrator.vibrate(combinedVibration)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error vibrating gamepad", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vibrate for piece movement
|
||||
*/
|
||||
fun vibrateForPieceMove() {
|
||||
vibrateGamepad(RUMBLE_MOVE_DURATION_MS, 50)
|
||||
}
|
||||
|
||||
/**
|
||||
* Vibrate for piece rotation
|
||||
*/
|
||||
fun vibrateForPieceRotation() {
|
||||
vibrateGamepad(RUMBLE_ROTATE_DURATION_MS, 80)
|
||||
}
|
||||
|
||||
/**
|
||||
* Vibrate for hard drop
|
||||
*/
|
||||
fun vibrateForHardDrop() {
|
||||
vibrateGamepad(RUMBLE_HARD_DROP_DURATION_MS, 150)
|
||||
}
|
||||
|
||||
/**
|
||||
* Vibrate for line clear
|
||||
*/
|
||||
fun vibrateForLineClear(lineCount: Int) {
|
||||
val amplitude = when (lineCount) {
|
||||
1 -> 100
|
||||
2 -> 150
|
||||
3 -> 200
|
||||
else -> 255 // For quad (4 lines)
|
||||
}
|
||||
vibrateGamepad(RUMBLE_LINE_CLEAR_DURATION_MS, amplitude)
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a key event from a gamepad
|
||||
* @return true if the event was handled, false otherwise
|
||||
*/
|
||||
fun handleKeyEvent(keyCode: Int, event: KeyEvent): Boolean {
|
||||
// Skip if game is not active but handle menu navigation
|
||||
if (!gameView.isActive()) {
|
||||
// Handle menu navigation even when game is not active
|
||||
if (event.action == KeyEvent.ACTION_DOWN) {
|
||||
when (keyCode) {
|
||||
KeyEvent.KEYCODE_DPAD_UP -> {
|
||||
navigationListener?.onMenuUp()
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_DPAD_DOWN -> {
|
||||
navigationListener?.onMenuDown()
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_DPAD_LEFT -> {
|
||||
navigationListener?.onMenuLeft()
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_DPAD_RIGHT -> {
|
||||
navigationListener?.onMenuRight()
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_BUTTON_A,
|
||||
KeyEvent.KEYCODE_DPAD_CENTER -> {
|
||||
navigationListener?.onMenuSelect()
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_BUTTON_B -> {
|
||||
// B button can be used to go back/cancel
|
||||
menuListener?.onPauseRequested()
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle menu/start button for pause menu
|
||||
if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_BUTTON_START) {
|
||||
menuListener?.onPauseRequested()
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
val device = event.device
|
||||
if (!isGamepad(device)) return false
|
||||
|
||||
// Update active gamepad for rumble
|
||||
activeGamepad = device
|
||||
|
||||
val currentTime = SystemClock.uptimeMillis()
|
||||
|
||||
when (event.action) {
|
||||
KeyEvent.ACTION_DOWN -> {
|
||||
when (keyCode) {
|
||||
// D-pad and analog movement
|
||||
KeyEvent.KEYCODE_DPAD_LEFT -> {
|
||||
if (!isMovingLeft) {
|
||||
gameView.moveLeft()
|
||||
vibrateForPieceMove()
|
||||
lastMoveTime = currentTime
|
||||
isMovingLeft = true
|
||||
// Start continuous movement after initial input
|
||||
handler.postDelayed(moveLeftRunnable, CONTINUOUS_MOVEMENT_DELAY_MS)
|
||||
return true
|
||||
}
|
||||
}
|
||||
KeyEvent.KEYCODE_DPAD_RIGHT -> {
|
||||
if (!isMovingRight) {
|
||||
gameView.moveRight()
|
||||
vibrateForPieceMove()
|
||||
lastMoveTime = currentTime
|
||||
isMovingRight = true
|
||||
// Start continuous movement after initial input
|
||||
handler.postDelayed(moveRightRunnable, CONTINUOUS_MOVEMENT_DELAY_MS)
|
||||
return true
|
||||
}
|
||||
}
|
||||
KeyEvent.KEYCODE_DPAD_DOWN -> {
|
||||
if (!isMovingDown) {
|
||||
gameView.softDrop()
|
||||
vibrateForPieceMove()
|
||||
lastMoveTime = currentTime
|
||||
isMovingDown = true
|
||||
// Start continuous movement after initial input
|
||||
handler.postDelayed(moveDownRunnable, CONTINUOUS_MOVEMENT_DELAY_MS)
|
||||
return true
|
||||
}
|
||||
}
|
||||
KeyEvent.KEYCODE_DPAD_UP -> {
|
||||
if (currentTime - lastHardDropTime > HARD_DROP_COOLDOWN_MS) {
|
||||
gameView.hardDrop()
|
||||
vibrateForHardDrop()
|
||||
lastHardDropTime = currentTime
|
||||
return true
|
||||
}
|
||||
}
|
||||
// Start button (pause)
|
||||
KeyEvent.KEYCODE_BUTTON_START -> {
|
||||
menuListener?.onPauseRequested()
|
||||
return true
|
||||
}
|
||||
// Rotation buttons - supporting multiple buttons for different controllers
|
||||
KeyEvent.KEYCODE_BUTTON_A,
|
||||
KeyEvent.KEYCODE_BUTTON_X -> {
|
||||
if (currentTime - lastRotationTime > ROTATION_COOLDOWN_MS) {
|
||||
gameView.rotate()
|
||||
vibrateForPieceRotation()
|
||||
lastRotationTime = currentTime
|
||||
return true
|
||||
}
|
||||
}
|
||||
KeyEvent.KEYCODE_BUTTON_B -> {
|
||||
if (currentTime - lastRotationTime > ROTATION_COOLDOWN_MS) {
|
||||
gameView.rotateCounterClockwise()
|
||||
vibrateForPieceRotation()
|
||||
lastRotationTime = currentTime
|
||||
return true
|
||||
}
|
||||
}
|
||||
// Hold piece buttons
|
||||
KeyEvent.KEYCODE_BUTTON_Y,
|
||||
KeyEvent.KEYCODE_BUTTON_L1,
|
||||
KeyEvent.KEYCODE_BUTTON_R1 -> {
|
||||
if (currentTime - lastHoldTime > HOLD_COOLDOWN_MS) {
|
||||
gameView.holdPiece()
|
||||
vibrateForPieceRotation()
|
||||
lastHoldTime = currentTime
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyEvent.ACTION_UP -> {
|
||||
when (keyCode) {
|
||||
KeyEvent.KEYCODE_DPAD_LEFT -> {
|
||||
isMovingLeft = false
|
||||
handler.removeCallbacks(moveLeftRunnable)
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_DPAD_RIGHT -> {
|
||||
isMovingRight = false
|
||||
handler.removeCallbacks(moveRightRunnable)
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_DPAD_DOWN -> {
|
||||
isMovingDown = false
|
||||
handler.removeCallbacks(moveDownRunnable)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Process generic motion events (for analog sticks)
|
||||
* @return true if the event was handled, false otherwise
|
||||
*/
|
||||
fun handleMotionEvent(event: MotionEvent): Boolean {
|
||||
// Skip if game is not active
|
||||
if (!gameView.isActive()) return false
|
||||
|
||||
val device = event.device
|
||||
if (!isGamepad(device)) return false
|
||||
|
||||
// Update active gamepad for rumble
|
||||
activeGamepad = device
|
||||
|
||||
val currentTime = SystemClock.uptimeMillis()
|
||||
|
||||
// Process left analog stick
|
||||
val axisX = event.getAxisValue(MotionEvent.AXIS_X)
|
||||
val axisY = event.getAxisValue(MotionEvent.AXIS_Y)
|
||||
|
||||
// Apply deadzone
|
||||
if (Math.abs(axisX) > STICK_DEADZONE) {
|
||||
if (axisX > 0 && !isMovingRight) {
|
||||
gameView.moveRight()
|
||||
vibrateForPieceMove()
|
||||
lastMoveTime = currentTime
|
||||
isMovingRight = true
|
||||
isMovingLeft = false
|
||||
|
||||
// Start continuous movement after initial input
|
||||
handler.removeCallbacks(moveLeftRunnable)
|
||||
handler.postDelayed(moveRightRunnable, CONTINUOUS_MOVEMENT_DELAY_MS)
|
||||
return true
|
||||
} else if (axisX < 0 && !isMovingLeft) {
|
||||
gameView.moveLeft()
|
||||
vibrateForPieceMove()
|
||||
lastMoveTime = currentTime
|
||||
isMovingLeft = true
|
||||
isMovingRight = false
|
||||
|
||||
// Start continuous movement after initial input
|
||||
handler.removeCallbacks(moveRightRunnable)
|
||||
handler.postDelayed(moveLeftRunnable, CONTINUOUS_MOVEMENT_DELAY_MS)
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
// Reset horizontal movement flags when stick returns to center
|
||||
isMovingLeft = false
|
||||
isMovingRight = false
|
||||
handler.removeCallbacks(moveLeftRunnable)
|
||||
handler.removeCallbacks(moveRightRunnable)
|
||||
}
|
||||
|
||||
if (Math.abs(axisY) > STICK_DEADZONE) {
|
||||
if (axisY > 0 && !isMovingDown) {
|
||||
gameView.softDrop()
|
||||
vibrateForPieceMove()
|
||||
lastMoveTime = currentTime
|
||||
isMovingDown = true
|
||||
|
||||
// Start continuous movement after initial input
|
||||
handler.postDelayed(moveDownRunnable, CONTINUOUS_MOVEMENT_DELAY_MS)
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
// Reset vertical movement flag when stick returns to center
|
||||
isMovingDown = false
|
||||
handler.removeCallbacks(moveDownRunnable)
|
||||
}
|
||||
|
||||
// Check right analog stick for rotation
|
||||
val axisZ = event.getAxisValue(MotionEvent.AXIS_Z)
|
||||
val axisRZ = event.getAxisValue(MotionEvent.AXIS_RZ)
|
||||
|
||||
if (Math.abs(axisZ) > STICK_DEADZONE || Math.abs(axisRZ) > STICK_DEADZONE) {
|
||||
if (currentTime - lastRotationTime > ROTATION_COOLDOWN_MS) {
|
||||
gameView.rotate()
|
||||
vibrateForPieceRotation()
|
||||
lastRotationTime = currentTime
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
|
@ -1,53 +1,70 @@
|
|||
package com.mintris.game
|
||||
package com.pixelmintdrop.game
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.BlurMaskFilter
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.RectF
|
||||
import android.graphics.BlurMaskFilter
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import com.pixelmintdrop.model.GameBoard
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* Custom view to display the next Tetromino piece
|
||||
* View that displays the currently held piece
|
||||
*/
|
||||
class NextPieceView @JvmOverloads constructor(
|
||||
class HoldPieceView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : View(context, attrs, defStyleAttr) {
|
||||
|
||||
private var gameView: GameView? = null
|
||||
|
||||
private var gameBoard: GameBoard? = null
|
||||
|
||||
// Rendering
|
||||
private val blockPaint = Paint().apply {
|
||||
color = Color.WHITE
|
||||
isAntiAlias = true
|
||||
style = Paint.Style.FILL
|
||||
}
|
||||
|
||||
|
||||
private val glowPaint = Paint().apply {
|
||||
color = Color.WHITE
|
||||
alpha = 30
|
||||
alpha = 40
|
||||
isAntiAlias = true
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = 1.5f
|
||||
maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.OUTER)
|
||||
}
|
||||
|
||||
|
||||
private val blockGlowPaint = Paint().apply {
|
||||
color = Color.WHITE
|
||||
alpha = 60
|
||||
isAntiAlias = true
|
||||
style = Paint.Style.FILL
|
||||
maskFilter = BlurMaskFilter(12f, BlurMaskFilter.Blur.OUTER)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the game view to get the next piece from
|
||||
* Set the game view reference
|
||||
*/
|
||||
fun setGameView(gameView: GameView) {
|
||||
this.gameView = gameView
|
||||
fun setGameView(view: GameView) {
|
||||
gameView = view
|
||||
gameBoard = view.getGameBoard()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the game board reference
|
||||
*/
|
||||
private fun getGameBoard(): GameBoard? = gameBoard
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
// Get the next piece from game view
|
||||
gameView?.let {
|
||||
it.getNextPiece()?.let { piece ->
|
||||
// Get the held piece from game board
|
||||
gameBoard?.let {
|
||||
it.getHoldPiece()?.let { piece ->
|
||||
val width = piece.getWidth()
|
||||
val height = piece.getHeight()
|
||||
|
||||
|
@ -75,6 +92,7 @@ class NextPieceView @JvmOverloads constructor(
|
|||
glowPaint
|
||||
)
|
||||
|
||||
// Draw the held piece
|
||||
for (y in 0 until height) {
|
||||
for (x in 0 until width) {
|
||||
if (piece.isBlockAt(x, y)) {
|
||||
|
@ -83,13 +101,29 @@ class NextPieceView @JvmOverloads constructor(
|
|||
val right = left + previewBlockSize
|
||||
val bottom = top + previewBlockSize
|
||||
|
||||
// Draw block with subtle glow
|
||||
val rect = RectF(left + 1, top + 1, right - 1, bottom - 1)
|
||||
canvas.drawRect(rect, blockPaint)
|
||||
// Draw outer glow
|
||||
blockGlowPaint.color = Color.WHITE
|
||||
canvas.drawRect(
|
||||
left - 2f,
|
||||
top - 2f,
|
||||
right + 2f,
|
||||
bottom + 2f,
|
||||
blockGlowPaint
|
||||
)
|
||||
|
||||
// Draw subtle border glow
|
||||
val glowRect = RectF(left, top, right, bottom)
|
||||
canvas.drawRect(glowRect, glowPaint)
|
||||
// Draw block
|
||||
blockPaint.color = Color.WHITE
|
||||
canvas.drawRect(left, top, right, bottom, blockPaint)
|
||||
|
||||
// Draw inner glow
|
||||
glowPaint.color = Color.WHITE
|
||||
canvas.drawRect(
|
||||
left + 1f,
|
||||
top + 1f,
|
||||
right - 1f,
|
||||
bottom - 1f,
|
||||
glowPaint
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
97
app/src/main/java/com/pixelmintdrop/game/NextPieceView.kt
Normal file
97
app/src/main/java/com/pixelmintdrop/game/NextPieceView.kt
Normal file
|
@ -0,0 +1,97 @@
|
|||
package com.pixelmintdrop.game
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.RectF
|
||||
import android.graphics.BlurMaskFilter
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* Custom view to display the next game piece
|
||||
*/
|
||||
class NextPieceView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : View(context, attrs, defStyleAttr) {
|
||||
|
||||
private var gameView: GameView? = null
|
||||
|
||||
// Rendering
|
||||
private val blockPaint = Paint().apply {
|
||||
color = Color.WHITE
|
||||
isAntiAlias = true
|
||||
}
|
||||
|
||||
private val glowPaint = Paint().apply {
|
||||
color = Color.WHITE
|
||||
alpha = 30
|
||||
isAntiAlias = true
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = 1.5f
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the game view to get the next piece from
|
||||
*/
|
||||
fun setGameView(gameView: GameView) {
|
||||
this.gameView = gameView
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
// Get the next piece from game view
|
||||
gameView?.getNextPiece()?.let { piece ->
|
||||
val width = piece.getWidth()
|
||||
val height = piece.getHeight()
|
||||
|
||||
// Calculate block size for the preview (smaller than main board)
|
||||
val previewBlockSize = min(
|
||||
canvas.width.toFloat() / (width.toFloat() + 2f),
|
||||
canvas.height.toFloat() / (height.toFloat() + 2f)
|
||||
)
|
||||
|
||||
// Center the piece in the preview area
|
||||
val previewLeft = (canvas.width.toFloat() - width.toFloat() * previewBlockSize) / 2f
|
||||
val previewTop = (canvas.height.toFloat() - height.toFloat() * previewBlockSize) / 2f
|
||||
|
||||
// Draw subtle background glow
|
||||
val glowPaint = Paint().apply {
|
||||
color = Color.WHITE
|
||||
alpha = 10
|
||||
maskFilter = BlurMaskFilter(previewBlockSize * 0.5f, BlurMaskFilter.Blur.OUTER)
|
||||
}
|
||||
canvas.drawRect(
|
||||
previewLeft - previewBlockSize,
|
||||
previewTop - previewBlockSize,
|
||||
previewLeft + width.toFloat() * previewBlockSize + previewBlockSize,
|
||||
previewTop + height.toFloat() * previewBlockSize + previewBlockSize,
|
||||
glowPaint
|
||||
)
|
||||
|
||||
for (y in 0 until height) {
|
||||
for (x in 0 until width) {
|
||||
if (piece.isBlockAt(x, y)) {
|
||||
val left = previewLeft + x.toFloat() * previewBlockSize
|
||||
val top = previewTop + y.toFloat() * previewBlockSize
|
||||
val right = left + previewBlockSize
|
||||
val bottom = top + previewBlockSize
|
||||
|
||||
// Draw block with subtle glow
|
||||
val rect = RectF(left + 1f, top + 1f, right - 1f, bottom - 1f)
|
||||
canvas.drawRect(rect, blockPaint)
|
||||
|
||||
// Draw subtle border glow
|
||||
val glowRect = RectF(left, top, right, bottom)
|
||||
canvas.drawRect(glowRect, glowPaint)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package com.mintris.game
|
||||
package com.pixelmintdrop.game
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
|
@ -10,9 +10,9 @@ import android.view.MotionEvent
|
|||
import android.view.View
|
||||
import java.util.Random
|
||||
import android.util.Log
|
||||
import com.mintris.model.HighScoreManager
|
||||
import com.mintris.model.HighScore
|
||||
import com.mintris.model.PlayerProgressionManager
|
||||
import com.pixelmintdrop.model.HighScoreManager
|
||||
import com.pixelmintdrop.model.HighScore
|
||||
import com.pixelmintdrop.model.PlayerProgressionManager
|
||||
import kotlin.math.abs
|
||||
import androidx.core.graphics.withTranslation
|
||||
import androidx.core.graphics.withScale
|
||||
|
@ -33,7 +33,7 @@ class TitleScreen @JvmOverloads constructor(
|
|||
private val random = Random()
|
||||
private var width = 0
|
||||
private var height = 0
|
||||
private val tetrominosToAdd = mutableListOf<Tetromino>()
|
||||
private val piecesToAdd = mutableListOf<FallingPiece>()
|
||||
private val highScoreManager = HighScoreManager(context) // Pre-allocate HighScoreManager
|
||||
|
||||
// Touch handling variables
|
||||
|
@ -46,11 +46,12 @@ class TitleScreen @JvmOverloads constructor(
|
|||
// Callback for when the user touches the screen
|
||||
var onStartGame: (() -> Unit)? = null
|
||||
|
||||
// Theme color
|
||||
// Theme color and background color
|
||||
private var themeColor = Color.WHITE
|
||||
private var backgroundColor = Color.BLACK
|
||||
|
||||
// Define tetromino shapes (I, O, T, S, Z, J, L)
|
||||
private val tetrominoShapes = arrayOf(
|
||||
// Define piece shapes (I, O, T, S, Z, J, L)
|
||||
private val pieceShapes = arrayOf(
|
||||
// I
|
||||
arrayOf(
|
||||
intArrayOf(0, 0, 0, 0),
|
||||
|
@ -95,8 +96,8 @@ class TitleScreen @JvmOverloads constructor(
|
|||
)
|
||||
)
|
||||
|
||||
// Tetromino class to represent falling pieces
|
||||
private class Tetromino(
|
||||
// FallingPiece class to represent falling pieces
|
||||
private class FallingPiece(
|
||||
var x: Float,
|
||||
var y: Float,
|
||||
val shape: Array<IntArray>,
|
||||
|
@ -105,12 +106,12 @@ class TitleScreen @JvmOverloads constructor(
|
|||
val rotation: Int = 0
|
||||
)
|
||||
|
||||
private val tetrominos = mutableListOf<Tetromino>()
|
||||
private val pieces = mutableListOf<FallingPiece>()
|
||||
|
||||
init {
|
||||
// Title text settings
|
||||
titlePaint.apply {
|
||||
color = Color.WHITE
|
||||
color = themeColor
|
||||
textSize = 120f
|
||||
textAlign = Paint.Align.CENTER
|
||||
typeface = Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD)
|
||||
|
@ -119,7 +120,7 @@ class TitleScreen @JvmOverloads constructor(
|
|||
|
||||
// "Touch to start" text settings
|
||||
promptPaint.apply {
|
||||
color = Color.WHITE
|
||||
color = themeColor
|
||||
textSize = 50f
|
||||
textAlign = Paint.Align.CENTER
|
||||
typeface = Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL)
|
||||
|
@ -129,7 +130,7 @@ class TitleScreen @JvmOverloads constructor(
|
|||
|
||||
// High scores text settings
|
||||
highScorePaint.apply {
|
||||
color = Color.WHITE
|
||||
color = themeColor
|
||||
textSize = 70f
|
||||
textAlign = Paint.Align.LEFT // Changed to LEFT alignment
|
||||
typeface = Typeface.create(Typeface.MONOSPACE, Typeface.NORMAL) // Changed to monospace
|
||||
|
@ -137,16 +138,16 @@ class TitleScreen @JvmOverloads constructor(
|
|||
alpha = 200
|
||||
}
|
||||
|
||||
// General paint settings for tetrominos (white)
|
||||
// General paint settings for pieces
|
||||
paint.apply {
|
||||
color = Color.WHITE
|
||||
color = themeColor
|
||||
style = Paint.Style.FILL
|
||||
isAntiAlias = true
|
||||
}
|
||||
|
||||
// Glow paint settings for tetrominos
|
||||
// Glow paint settings for pieces
|
||||
glowPaint.apply {
|
||||
color = Color.WHITE
|
||||
color = themeColor
|
||||
style = Paint.Style.FILL
|
||||
isAntiAlias = true
|
||||
alpha = 60
|
||||
|
@ -158,66 +159,66 @@ class TitleScreen @JvmOverloads constructor(
|
|||
width = w
|
||||
height = h
|
||||
|
||||
// Clear existing tetrominos
|
||||
tetrominos.clear()
|
||||
// Clear existing pieces
|
||||
pieces.clear()
|
||||
|
||||
// Initialize some tetrominos
|
||||
// Initialize some pieces
|
||||
repeat(20) {
|
||||
val tetromino = createRandomTetromino()
|
||||
tetrominos.add(tetromino)
|
||||
val piece = createRandomPiece()
|
||||
pieces.add(piece)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createRandomTetromino(): Tetromino {
|
||||
private fun createRandomPiece(): FallingPiece {
|
||||
val x = random.nextFloat() * (width - 150) + 50 // Keep away from edges
|
||||
val y = -cellSize * 4 - (random.nextFloat() * height / 2)
|
||||
val shapeIndex = random.nextInt(tetrominoShapes.size)
|
||||
val shape = tetrominoShapes[shapeIndex]
|
||||
val shapeIndex = random.nextInt(pieceShapes.size)
|
||||
val shape = pieceShapes[shapeIndex]
|
||||
val speed = 1f + random.nextFloat() * 2f
|
||||
val scale = 0.8f + random.nextFloat() * 0.4f
|
||||
val rotation = random.nextInt(4) * 90
|
||||
|
||||
return Tetromino(x, y, shape, speed, scale, rotation)
|
||||
return FallingPiece(x, y, shape, speed, scale, rotation)
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
try {
|
||||
super.onDraw(canvas)
|
||||
|
||||
// Draw background
|
||||
canvas.drawColor(Color.BLACK)
|
||||
// Draw background using the current background color
|
||||
canvas.drawColor(backgroundColor)
|
||||
|
||||
// Add any pending tetrominos
|
||||
tetrominos.addAll(tetrominosToAdd)
|
||||
tetrominosToAdd.clear()
|
||||
// Add any pending pieces
|
||||
pieces.addAll(piecesToAdd)
|
||||
piecesToAdd.clear()
|
||||
|
||||
// Update and draw falling tetrominos
|
||||
val tetrominosToRemove = mutableListOf<Tetromino>()
|
||||
// Update and draw falling pieces
|
||||
val piecesToRemove = mutableListOf<FallingPiece>()
|
||||
|
||||
for (tetromino in tetrominos) {
|
||||
tetromino.y += tetromino.speed
|
||||
for (piece in pieces) {
|
||||
piece.y += piece.speed
|
||||
|
||||
// Remove tetrominos that have fallen off the screen
|
||||
if (tetromino.y > height) {
|
||||
tetrominosToRemove.add(tetromino)
|
||||
tetrominosToAdd.add(createRandomTetromino())
|
||||
// Remove pieces that have fallen off the screen
|
||||
if (piece.y > height) {
|
||||
piecesToRemove.add(piece)
|
||||
piecesToAdd.add(createRandomPiece())
|
||||
} else {
|
||||
try {
|
||||
// Draw the tetromino
|
||||
for (y in 0 until tetromino.shape.size) {
|
||||
for (x in 0 until tetromino.shape.size) {
|
||||
if (tetromino.shape[y][x] == 1) {
|
||||
// Draw the piece
|
||||
for (y in 0 until piece.shape.size) {
|
||||
for (x in 0 until piece.shape.size) {
|
||||
if (piece.shape[y][x] == 1) {
|
||||
val left = x * cellSize
|
||||
val top = y * cellSize
|
||||
val right = left + cellSize
|
||||
val bottom = top + cellSize
|
||||
|
||||
// Draw block with glow effect
|
||||
canvas.withTranslation(tetromino.x, tetromino.y) {
|
||||
withScale(tetromino.scale, tetromino.scale) {
|
||||
withRotation(tetromino.rotation.toFloat(),
|
||||
tetromino.shape.size * cellSize / 2,
|
||||
tetromino.shape.size * cellSize / 2) {
|
||||
canvas.withTranslation(piece.x, piece.y) {
|
||||
withScale(piece.scale, piece.scale) {
|
||||
withRotation(piece.rotation.toFloat(),
|
||||
piece.shape.size * cellSize / 2,
|
||||
piece.shape.size * cellSize / 2) {
|
||||
// Draw glow
|
||||
canvas.drawRect(left - 8f, top - 8f, right + 8f, bottom + 8f, glowPaint)
|
||||
// Draw block
|
||||
|
@ -229,21 +230,22 @@ class TitleScreen @JvmOverloads constructor(
|
|||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("TitleScreen", "Error drawing tetromino", e)
|
||||
Log.e("TitleScreen", "Error drawing piece", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove tetrominos that fell off the screen
|
||||
tetrominos.removeAll(tetrominosToRemove)
|
||||
// Remove pieces that fell off the screen
|
||||
pieces.removeAll(piecesToRemove)
|
||||
|
||||
// Draw title
|
||||
val titleY = height * 0.4f
|
||||
canvas.drawText("mintris", width / 2f, titleY, titlePaint)
|
||||
canvas.drawText("Pixel Mint Drop", width / 2f, titleY, titlePaint)
|
||||
|
||||
// Draw high scores using pre-allocated manager
|
||||
val highScores: List<HighScore> = highScoreManager.getHighScores()
|
||||
val highScoreY = height * 0.5f
|
||||
var lastHighScoreY = highScoreY
|
||||
if (highScores.isNotEmpty()) {
|
||||
// Calculate the starting X position to center the entire block of scores
|
||||
val maxScoreWidth = highScorePaint.measureText("99. PLAYER: 999999")
|
||||
|
@ -251,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
|
||||
|
@ -259,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()
|
||||
|
@ -282,10 +292,10 @@ class TitleScreen @JvmOverloads constructor(
|
|||
val deltaX = event.x - lastTouchX
|
||||
val deltaY = event.y - lastTouchY
|
||||
|
||||
// Update tetromino positions
|
||||
for (tetromino in tetrominos) {
|
||||
tetromino.x += deltaX * 0.5f
|
||||
tetromino.y += deltaY * 0.5f
|
||||
// Update piece positions
|
||||
for (piece in pieces) {
|
||||
piece.x += deltaX * 0.5f
|
||||
piece.y += deltaY * 0.5f
|
||||
}
|
||||
|
||||
lastTouchX = event.x
|
||||
|
@ -340,7 +350,7 @@ class TitleScreen @JvmOverloads constructor(
|
|||
glowPaint.color = themeColor
|
||||
|
||||
// Update background color
|
||||
setBackgroundColor(when (themeId) {
|
||||
backgroundColor = when (themeId) {
|
||||
PlayerProgressionManager.THEME_CLASSIC -> Color.BLACK
|
||||
PlayerProgressionManager.THEME_NEON -> Color.parseColor("#0D0221")
|
||||
PlayerProgressionManager.THEME_MONOCHROME -> Color.parseColor("#1A1A1A")
|
||||
|
@ -348,8 +358,29 @@ class TitleScreen @JvmOverloads constructor(
|
|||
PlayerProgressionManager.THEME_MINIMALIST -> Color.WHITE
|
||||
PlayerProgressionManager.THEME_GALAXY -> Color.parseColor("#0B0C10")
|
||||
else -> Color.BLACK
|
||||
})
|
||||
}
|
||||
|
||||
invalidate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the theme color for the title screen
|
||||
*/
|
||||
fun setThemeColor(color: Int) {
|
||||
themeColor = color
|
||||
titlePaint.color = color
|
||||
promptPaint.color = color
|
||||
highScorePaint.color = color
|
||||
paint.color = color
|
||||
glowPaint.color = color
|
||||
invalidate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the background color for the title screen
|
||||
*/
|
||||
override fun setBackgroundColor(color: Int) {
|
||||
backgroundColor = color
|
||||
invalidate()
|
||||
}
|
||||
}
|
199
app/src/main/java/com/pixelmintdrop/model/GameBoard.kt
Normal file
199
app/src/main/java/com/pixelmintdrop/model/GameBoard.kt
Normal file
|
@ -0,0 +1,199 @@
|
|||
package com.pixelmintdrop.model
|
||||
|
||||
import android.util.Log
|
||||
|
||||
/**
|
||||
* Represents the game board and manages piece placement and collision detection
|
||||
*/
|
||||
class GameBoard {
|
||||
companion object {
|
||||
const val WIDTH = 10
|
||||
const val HEIGHT = 20
|
||||
const val INITIAL_DROP_INTERVAL = 1000L // 1 second
|
||||
const val MIN_DROP_INTERVAL = 100L // 0.1 seconds
|
||||
}
|
||||
|
||||
// Board state
|
||||
private val board = Array(HEIGHT) { Array(WIDTH) { false } }
|
||||
private var currentPiece: GamePiece? = null
|
||||
private var nextPiece: GamePiece? = null
|
||||
|
||||
// Game state
|
||||
var dropInterval = INITIAL_DROP_INTERVAL
|
||||
private set
|
||||
|
||||
val width: Int = WIDTH
|
||||
val height: Int = HEIGHT
|
||||
|
||||
fun reset() {
|
||||
// Clear the board
|
||||
for (y in 0 until HEIGHT) {
|
||||
for (x in 0 until WIDTH) {
|
||||
board[y][x] = false
|
||||
}
|
||||
}
|
||||
|
||||
// Reset pieces
|
||||
currentPiece = null
|
||||
nextPiece = null
|
||||
|
||||
// Reset drop interval
|
||||
dropInterval = INITIAL_DROP_INTERVAL
|
||||
}
|
||||
|
||||
fun spawnNewPiece(): GamePiece {
|
||||
// Get the next piece or create a new one if none exists
|
||||
currentPiece = nextPiece ?: createRandomPiece()
|
||||
nextPiece = createRandomPiece()
|
||||
|
||||
// Position the piece at the top center of the board
|
||||
currentPiece?.let { piece ->
|
||||
piece.x = (WIDTH - piece.getWidth()) / 2
|
||||
piece.y = 0
|
||||
}
|
||||
|
||||
return currentPiece!!
|
||||
}
|
||||
|
||||
fun getNextPiece(): GamePiece? = nextPiece
|
||||
|
||||
fun getCurrentPiece(): GamePiece? = currentPiece
|
||||
|
||||
fun hasBlockAt(x: Int, y: Int): Boolean {
|
||||
if (x < 0 || x >= WIDTH || y < 0 || y >= HEIGHT) return false
|
||||
return board[y][x]
|
||||
}
|
||||
|
||||
fun wouldCollide(piece: GamePiece, newX: Int, newY: Int): Boolean {
|
||||
val width = piece.getWidth()
|
||||
val height = piece.getHeight()
|
||||
|
||||
for (y in 0 until height) {
|
||||
for (x in 0 until width) {
|
||||
if (piece.isBlockAt(x, y)) {
|
||||
val boardX = newX + x
|
||||
val boardY = newY + y
|
||||
|
||||
// Check board boundaries
|
||||
if (boardX < 0 || boardX >= WIDTH || boardY >= HEIGHT) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check collision with placed blocks
|
||||
if (boardY >= 0 && board[boardY][boardX]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
fun movePiece(piece: GamePiece, dx: Int, dy: Int): Boolean {
|
||||
val newX = piece.x + dx
|
||||
val newY = piece.y + dy
|
||||
|
||||
if (!wouldCollide(piece, newX, newY)) {
|
||||
piece.x = newX
|
||||
piece.y = newY
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
fun rotatePiece(piece: GamePiece): Boolean {
|
||||
// Save current state
|
||||
val originalRotation = piece.rotation
|
||||
|
||||
// Try to rotate
|
||||
piece.rotate()
|
||||
|
||||
// Check if new position is valid
|
||||
if (wouldCollide(piece, piece.x, piece.y)) {
|
||||
// If not valid, try wall kicks
|
||||
val kicks = arrayOf(
|
||||
Pair(1, 0), // Try moving right
|
||||
Pair(-1, 0), // Try moving left
|
||||
Pair(0, -1), // Try moving up
|
||||
Pair(2, 0), // Try moving right 2
|
||||
Pair(-2, 0) // Try moving left 2
|
||||
)
|
||||
|
||||
for ((kickX, kickY) in kicks) {
|
||||
if (!wouldCollide(piece, piece.x + kickX, piece.y + kickY)) {
|
||||
piece.x += kickX
|
||||
piece.y += kickY
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// If no wall kick worked, revert rotation
|
||||
piece.rotation = originalRotation
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun lockPiece(piece: GamePiece) {
|
||||
val width = piece.getWidth()
|
||||
val height = piece.getHeight()
|
||||
|
||||
// Place the piece on the board
|
||||
for (y in 0 until height) {
|
||||
for (x in 0 until width) {
|
||||
if (piece.isBlockAt(x, y)) {
|
||||
val boardX = piece.x + x
|
||||
val boardY = piece.y + y
|
||||
if (boardY >= 0 && boardY < HEIGHT && boardX >= 0 && boardX < WIDTH) {
|
||||
board[boardY][boardX] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear any completed lines
|
||||
clearLines()
|
||||
}
|
||||
|
||||
private fun clearLines() {
|
||||
var linesCleared = 0
|
||||
var y = HEIGHT - 1
|
||||
|
||||
while (y >= 0) {
|
||||
if (isLineFull(y)) {
|
||||
// Move all lines above down
|
||||
for (moveY in y downTo 1) {
|
||||
for (x in 0 until WIDTH) {
|
||||
board[moveY][x] = board[moveY - 1][x]
|
||||
}
|
||||
}
|
||||
// Clear top line
|
||||
for (x in 0 until WIDTH) {
|
||||
board[0][x] = false
|
||||
}
|
||||
linesCleared++
|
||||
} else {
|
||||
y--
|
||||
}
|
||||
}
|
||||
|
||||
// Increase speed based on lines cleared
|
||||
if (linesCleared > 0) {
|
||||
dropInterval = (dropInterval * 0.95).toLong().coerceAtLeast(MIN_DROP_INTERVAL)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isLineFull(y: Int): Boolean {
|
||||
for (x in 0 until WIDTH) {
|
||||
if (!board[y][x]) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun createRandomPiece(): GamePiece {
|
||||
return GamePiece(GamePieceType.values().random())
|
||||
}
|
||||
}
|
48
app/src/main/java/com/pixelmintdrop/model/GamePiece.kt
Normal file
48
app/src/main/java/com/pixelmintdrop/model/GamePiece.kt
Normal file
|
@ -0,0 +1,48 @@
|
|||
package com.pixelmintdrop.model
|
||||
|
||||
/**
|
||||
* Represents a game piece with its type, position, and rotation
|
||||
*/
|
||||
class GamePiece(val type: GamePieceType) {
|
||||
var x: Int = 0
|
||||
var y: Int = 0
|
||||
var rotation: Int = 0
|
||||
private set
|
||||
|
||||
private val blocks: Array<Array<Boolean>> = type.getBlocks()
|
||||
|
||||
fun getWidth(): Int = blocks[0].size
|
||||
|
||||
fun getHeight(): Int = blocks.size
|
||||
|
||||
fun isBlockAt(x: Int, y: Int): Boolean {
|
||||
if (x < 0 || x >= getWidth() || y < 0 || y >= getHeight()) return false
|
||||
return blocks[y][x]
|
||||
}
|
||||
|
||||
fun rotate() {
|
||||
rotation = (rotation + 1) % 4
|
||||
rotateBlocks()
|
||||
}
|
||||
|
||||
private fun rotateBlocks() {
|
||||
val width = getWidth()
|
||||
val height = getHeight()
|
||||
val rotated = Array(width) { Array(height) { false } }
|
||||
|
||||
for (y in 0 until height) {
|
||||
for (x in 0 until width) {
|
||||
when (rotation) {
|
||||
1 -> rotated[x][height - 1 - y] = blocks[y][x]
|
||||
2 -> rotated[height - 1 - y][width - 1 - x] = blocks[y][x]
|
||||
3 -> rotated[width - 1 - x][y] = blocks[y][x]
|
||||
else -> rotated[y][x] = blocks[y][x]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
blocks.indices.forEach { i ->
|
||||
blocks[i] = rotated[i].copyOf()
|
||||
}
|
||||
}
|
||||
}
|
48
app/src/main/java/com/pixelmintdrop/model/GamePieceType.kt
Normal file
48
app/src/main/java/com/pixelmintdrop/model/GamePieceType.kt
Normal file
|
@ -0,0 +1,48 @@
|
|||
package com.pixelmintdrop.model
|
||||
|
||||
/**
|
||||
* Represents the different types of game pieces
|
||||
*/
|
||||
enum class GamePieceType {
|
||||
I, J, L, O, S, T, Z;
|
||||
|
||||
fun getBlocks(): Array<Array<Boolean>> {
|
||||
return when (this) {
|
||||
I -> arrayOf(
|
||||
arrayOf(false, false, false, false),
|
||||
arrayOf(true, true, true, true),
|
||||
arrayOf(false, false, false, false),
|
||||
arrayOf(false, false, false, false)
|
||||
)
|
||||
J -> arrayOf(
|
||||
arrayOf(true, false, false),
|
||||
arrayOf(true, true, true),
|
||||
arrayOf(false, false, false)
|
||||
)
|
||||
L -> arrayOf(
|
||||
arrayOf(false, false, true),
|
||||
arrayOf(true, true, true),
|
||||
arrayOf(false, false, false)
|
||||
)
|
||||
O -> arrayOf(
|
||||
arrayOf(true, true),
|
||||
arrayOf(true, true)
|
||||
)
|
||||
S -> arrayOf(
|
||||
arrayOf(false, true, true),
|
||||
arrayOf(true, true, false),
|
||||
arrayOf(false, false, false)
|
||||
)
|
||||
T -> arrayOf(
|
||||
arrayOf(false, true, false),
|
||||
arrayOf(true, true, true),
|
||||
arrayOf(false, false, false)
|
||||
)
|
||||
Z -> arrayOf(
|
||||
arrayOf(true, true, false),
|
||||
arrayOf(false, true, true),
|
||||
arrayOf(false, false, false)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package com.mintris.model
|
||||
package com.pixelmintdrop.model
|
||||
|
||||
data class HighScore(
|
||||
val name: String,
|
|
@ -1,4 +1,4 @@
|
|||
package com.mintris.model
|
||||
package com.pixelmintdrop.model
|
||||
|
||||
import android.graphics.Color
|
||||
import android.view.LayoutInflater
|
||||
|
@ -6,7 +6,7 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.mintris.R
|
||||
import com.pixelmintdrop.R
|
||||
|
||||
class HighScoreAdapter : RecyclerView.Adapter<HighScoreAdapter.HighScoreViewHolder>() {
|
||||
private var highScores: List<HighScore> = emptyList()
|
|
@ -1,4 +1,4 @@
|
|||
package com.mintris.model
|
||||
package com.pixelmintdrop.model
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
|
@ -12,7 +12,7 @@ class HighScoreManager(private val context: Context) {
|
|||
private val type: Type = object : TypeToken<List<HighScore>>() {}.type
|
||||
|
||||
companion object {
|
||||
private const val PREFS_NAME = "mintris_highscores"
|
||||
private const val PREFS_NAME = "pixelmintdrop_highscores"
|
||||
private const val KEY_HIGHSCORES = "highscores"
|
||||
private const val MAX_HIGHSCORES = 5
|
||||
}
|
|
@ -1,10 +1,10 @@
|
|||
package com.mintris.model
|
||||
package com.pixelmintdrop.model
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import com.mintris.R
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* Manages player progression, experience points, and unlockable rewards
|
||||
|
@ -20,7 +20,6 @@ class PlayerProgressionManager(context: Context) {
|
|||
// Track unlocked rewards
|
||||
private val unlockedThemes = mutableSetOf<String>()
|
||||
private val unlockedBlocks = mutableSetOf<String>()
|
||||
private val unlockedPowers = mutableSetOf<String>()
|
||||
private val unlockedBadges = mutableSetOf<String>()
|
||||
|
||||
// XP gained in the current session
|
||||
|
@ -41,18 +40,22 @@ class PlayerProgressionManager(context: Context) {
|
|||
// Load unlocked rewards
|
||||
val themesSet = prefs.getStringSet(KEY_UNLOCKED_THEMES, setOf()) ?: setOf()
|
||||
val blocksSet = prefs.getStringSet(KEY_UNLOCKED_BLOCKS, setOf()) ?: setOf()
|
||||
val powersSet = prefs.getStringSet(KEY_UNLOCKED_POWERS, setOf()) ?: setOf()
|
||||
val badgesSet = prefs.getStringSet(KEY_UNLOCKED_BADGES, setOf()) ?: setOf()
|
||||
|
||||
unlockedThemes.addAll(themesSet)
|
||||
unlockedBlocks.addAll(blocksSet)
|
||||
unlockedPowers.addAll(powersSet)
|
||||
unlockedBadges.addAll(badgesSet)
|
||||
|
||||
// Add default theme if nothing is unlocked
|
||||
if (unlockedThemes.isEmpty()) {
|
||||
unlockedThemes.add(THEME_CLASSIC)
|
||||
}
|
||||
|
||||
// Always ensure default block skin is present (Level 1)
|
||||
unlockedBlocks.add("block_skin_1")
|
||||
|
||||
// Explicitly check and save all unlocks for the current level on load
|
||||
checkAllUnlocksForCurrentLevel()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -65,7 +68,6 @@ class PlayerProgressionManager(context: Context) {
|
|||
.putLong(KEY_TOTAL_XP_EARNED, totalXPEarned)
|
||||
.putStringSet(KEY_UNLOCKED_THEMES, unlockedThemes)
|
||||
.putStringSet(KEY_UNLOCKED_BLOCKS, unlockedBlocks)
|
||||
.putStringSet(KEY_UNLOCKED_POWERS, unlockedPowers)
|
||||
.putStringSet(KEY_UNLOCKED_BADGES, unlockedBadges)
|
||||
.apply()
|
||||
}
|
||||
|
@ -91,23 +93,31 @@ class PlayerProgressionManager(context: Context) {
|
|||
/**
|
||||
* Calculate XP from a game session based on score, lines, level, etc.
|
||||
*/
|
||||
fun calculateGameXP(score: Int, lines: Int, level: Int, gameTime: Long,
|
||||
tetrisCount: Int, perfectClearCount: Int): Long {
|
||||
// Base XP from score with level multiplier
|
||||
val scoreXP = (score * (1 + LEVEL_MULTIPLIER * level)).toLong()
|
||||
|
||||
// XP from lines cleared
|
||||
val linesXP = lines * XP_PER_LINE
|
||||
|
||||
// XP from special moves
|
||||
val tetrisBonus = tetrisCount * TETRIS_XP_BONUS
|
||||
val perfectClearBonus = perfectClearCount * PERFECT_CLEAR_XP_BONUS
|
||||
|
||||
// Time bonus (to reward longer gameplay)
|
||||
val timeBonus = (gameTime / 60000) * TIME_XP_PER_MINUTE // XP per minute played
|
||||
|
||||
// Calculate total XP
|
||||
return scoreXP + linesXP + tetrisBonus + perfectClearBonus + timeBonus
|
||||
fun calculateGameXP(
|
||||
score: Int,
|
||||
lines: Int,
|
||||
level: Int,
|
||||
timePlayedMs: Long,
|
||||
quadCount: Int,
|
||||
perfectClearCount: Int
|
||||
): Long {
|
||||
// Calculate base XP from score
|
||||
val scoreXP = score * BASE_SCORE_XP * level
|
||||
|
||||
// Calculate XP from lines cleared
|
||||
val linesXP = lines * BASE_LINES_XP * level
|
||||
|
||||
// Calculate quad bonus
|
||||
val quadBonus = quadCount * BASE_QUAD_BONUS * level
|
||||
|
||||
// Calculate perfect clear bonus
|
||||
val perfectClearBonus = perfectClearCount * BASE_PERFECT_CLEAR_BONUS * level
|
||||
|
||||
// Calculate time bonus (convert ms to seconds)
|
||||
val timeBonus = (timePlayedMs / 1000.0) * BASE_TIME_XP * level
|
||||
|
||||
// Sum all XP components
|
||||
return (scoreXP + linesXP + quadBonus + perfectClearBonus + timeBonus).toLong()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -179,46 +189,64 @@ class PlayerProgressionManager(context: Context) {
|
|||
}
|
||||
}
|
||||
|
||||
// Check for power unlocks
|
||||
// Check for block skin unlocks (start from skin 2 at level 7)
|
||||
when (level) {
|
||||
8 -> {
|
||||
if (unlockedPowers.add(POWER_FREEZE_TIME)) {
|
||||
newRewards.add("Unlocked Freeze Time Power!")
|
||||
7 -> {
|
||||
if (unlockedBlocks.add("block_skin_2")) {
|
||||
newRewards.add("Unlocked Neon Block Skin!")
|
||||
}
|
||||
}
|
||||
12 -> {
|
||||
if (unlockedPowers.add(POWER_BLOCK_SWAP)) {
|
||||
newRewards.add("Unlocked Block Swap Power!")
|
||||
14 -> {
|
||||
if (unlockedBlocks.add("block_skin_3")) {
|
||||
newRewards.add("Unlocked Retro Block Skin!")
|
||||
}
|
||||
}
|
||||
18 -> {
|
||||
if (unlockedPowers.add(POWER_SAFE_LANDING)) {
|
||||
newRewards.add("Unlocked Safe Landing Power!")
|
||||
21 -> {
|
||||
if (unlockedBlocks.add("block_skin_4")) {
|
||||
newRewards.add("Unlocked Minimalist Block Skin!")
|
||||
}
|
||||
}
|
||||
30 -> {
|
||||
if (unlockedPowers.add(POWER_PERFECT_CLEAR)) {
|
||||
newRewards.add("Unlocked Perfect Clear Power!")
|
||||
28 -> {
|
||||
if (unlockedBlocks.add("block_skin_5")) {
|
||||
newRewards.add("Unlocked Galaxy Block Skin!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for block skin unlocks
|
||||
if (level % 7 == 0 && level <= 35) {
|
||||
val blockSkin = "block_skin_${level / 7}"
|
||||
if (unlockedBlocks.add(blockSkin)) {
|
||||
newRewards.add("Unlocked New Block Skin!")
|
||||
}
|
||||
}
|
||||
|
||||
return newRewards
|
||||
}
|
||||
|
||||
/**
|
||||
* Check and unlock any rewards the player should have based on their current level
|
||||
* This ensures players don't miss unlocks if they level up multiple times at once
|
||||
*/
|
||||
private fun checkAllUnlocksForCurrentLevel() {
|
||||
// Check theme unlocks
|
||||
if (playerLevel >= 5) unlockedThemes.add(THEME_NEON)
|
||||
if (playerLevel >= 10) unlockedThemes.add(THEME_MONOCHROME)
|
||||
if (playerLevel >= 15) unlockedThemes.add(THEME_RETRO)
|
||||
if (playerLevel >= 20) unlockedThemes.add(THEME_MINIMALIST)
|
||||
if (playerLevel >= 25) unlockedThemes.add(THEME_GALAXY)
|
||||
|
||||
// Check block skin unlocks (start from skin 2 at level 7)
|
||||
// Skin 1 is default (added in loadProgress)
|
||||
if (playerLevel >= 7) unlockedBlocks.add("block_skin_2")
|
||||
if (playerLevel >= 14) unlockedBlocks.add("block_skin_3")
|
||||
if (playerLevel >= 21) unlockedBlocks.add("block_skin_4")
|
||||
if (playerLevel >= 28) unlockedBlocks.add("block_skin_5")
|
||||
|
||||
// Save any newly unlocked items
|
||||
saveProgress()
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a new progression session
|
||||
*/
|
||||
fun startNewSession() {
|
||||
sessionXPGained = 0
|
||||
|
||||
// Ensure all appropriate unlocks are available
|
||||
checkAllUnlocksForCurrentLevel()
|
||||
}
|
||||
|
||||
// Getters
|
||||
|
@ -227,7 +255,6 @@ class PlayerProgressionManager(context: Context) {
|
|||
fun getXPForNextLevel(): Long = calculateXPForLevel(playerLevel)
|
||||
fun getSessionXPGained(): Long = sessionXPGained
|
||||
fun getUnlockedThemes(): Set<String> = unlockedThemes.toSet()
|
||||
fun getUnlockedPowers(): Set<String> = unlockedPowers.toSet()
|
||||
fun getUnlockedBlocks(): Set<String> = unlockedBlocks.toSet()
|
||||
fun getUnlockedBadges(): Set<String> = unlockedBadges.toSet()
|
||||
|
||||
|
@ -238,13 +265,6 @@ class PlayerProgressionManager(context: Context) {
|
|||
return unlockedThemes.contains(themeId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a specific power is unlocked
|
||||
*/
|
||||
fun isPowerUnlocked(powerId: String): Boolean {
|
||||
return unlockedPowers.contains(powerId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Award a badge to the player
|
||||
*/
|
||||
|
@ -266,37 +286,38 @@ class PlayerProgressionManager(context: Context) {
|
|||
|
||||
unlockedThemes.clear()
|
||||
unlockedBlocks.clear()
|
||||
unlockedPowers.clear()
|
||||
unlockedBadges.clear()
|
||||
|
||||
// Add default theme
|
||||
unlockedThemes.add(THEME_CLASSIC)
|
||||
|
||||
// Add default block skin (Level 1)
|
||||
unlockedBlocks.add("block_skin_1")
|
||||
|
||||
saveProgress()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREFS_NAME = "mintris_progression"
|
||||
private const val PREFS_NAME = "pixelmintdrop_progression"
|
||||
private const val KEY_PLAYER_LEVEL = "player_level"
|
||||
private const val KEY_PLAYER_XP = "player_xp"
|
||||
private const val KEY_TOTAL_XP_EARNED = "total_xp_earned"
|
||||
private const val KEY_UNLOCKED_THEMES = "unlocked_themes"
|
||||
private const val KEY_UNLOCKED_BLOCKS = "unlocked_blocks"
|
||||
private const val KEY_UNLOCKED_POWERS = "unlocked_powers"
|
||||
private const val KEY_UNLOCKED_BADGES = "unlocked_badges"
|
||||
private const val KEY_SELECTED_THEME = "selected_theme"
|
||||
private const val KEY_SELECTED_BLOCK_SKIN = "selected_block_skin"
|
||||
|
||||
// XP curve parameters
|
||||
private const val BASE_XP = 4000.0 // Base XP for level 1 (reduced from 5000)
|
||||
private const val XP_CURVE_FACTOR = 1.9 // Exponential factor for XP curve (reduced from 2.2)
|
||||
// XP constants
|
||||
private const val BASE_XP = 3000L
|
||||
private const val XP_CURVE_FACTOR = 2.0
|
||||
private const val LEVEL_MULTIPLIER = 0.03
|
||||
private const val XP_PER_LINE = 40L
|
||||
private const val TETRIS_XP_BONUS = 150L
|
||||
private const val PERFECT_CLEAR_XP_BONUS = 300L
|
||||
private const val TIME_XP_PER_MINUTE = 20L
|
||||
|
||||
// XP calculation constants
|
||||
private const val LEVEL_MULTIPLIER = 0.15 // 15% bonus per level (increased from 10%)
|
||||
private const val XP_PER_LINE = 15L // Increased from 10
|
||||
private const val TETRIS_XP_BONUS = 75L // Increased from 50
|
||||
private const val PERFECT_CLEAR_XP_BONUS = 250L // Increased from 200
|
||||
private const val TIME_XP_PER_MINUTE = 8L // Increased from 5
|
||||
|
||||
// Theme IDs with required levels
|
||||
// Theme constants
|
||||
const val THEME_CLASSIC = "theme_classic"
|
||||
const val THEME_NEON = "theme_neon"
|
||||
const val THEME_MONOCHROME = "theme_monochrome"
|
||||
|
@ -314,19 +335,11 @@ class PlayerProgressionManager(context: Context) {
|
|||
THEME_GALAXY to 25
|
||||
)
|
||||
|
||||
// Power IDs
|
||||
const val POWER_FREEZE_TIME = "power_freeze_time"
|
||||
const val POWER_BLOCK_SWAP = "power_block_swap"
|
||||
const val POWER_SAFE_LANDING = "power_safe_landing"
|
||||
const val POWER_PERFECT_CLEAR = "power_perfect_clear"
|
||||
|
||||
// Map of powers to required levels
|
||||
val POWER_REQUIRED_LEVELS = mapOf(
|
||||
POWER_FREEZE_TIME to 8,
|
||||
POWER_BLOCK_SWAP to 12,
|
||||
POWER_SAFE_LANDING to 18,
|
||||
POWER_PERFECT_CLEAR to 30
|
||||
)
|
||||
private const val BASE_SCORE_XP = 0.1 // XP per score point
|
||||
private const val BASE_LINES_XP = 100.0 // XP per line cleared
|
||||
private const val BASE_QUAD_BONUS = 500.0 // Bonus XP for clearing 4 lines at once
|
||||
private const val BASE_PERFECT_CLEAR_BONUS = 1000.0 // Bonus XP for perfect clear
|
||||
private const val BASE_TIME_XP = 1.0 // XP per second played
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -337,9 +350,34 @@ class PlayerProgressionManager(context: Context) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get the required level for a specific power
|
||||
* Set the selected block skin
|
||||
*/
|
||||
fun getRequiredLevelForPower(powerId: String): Int {
|
||||
return POWER_REQUIRED_LEVELS[powerId] ?: 1
|
||||
fun setSelectedBlockSkin(skinId: String) {
|
||||
if (unlockedBlocks.contains(skinId)) {
|
||||
prefs.edit().putString(KEY_SELECTED_BLOCK_SKIN, skinId).commit()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the selected block skin
|
||||
*/
|
||||
fun getSelectedBlockSkin(): String {
|
||||
return prefs.getString(KEY_SELECTED_BLOCK_SKIN, "block_skin_1") ?: "block_skin_1"
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the selected theme
|
||||
*/
|
||||
fun setSelectedTheme(themeId: String) {
|
||||
if (unlockedThemes.contains(themeId)) {
|
||||
prefs.edit().putString(KEY_SELECTED_THEME, themeId).apply()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the selected theme
|
||||
*/
|
||||
fun getSelectedTheme(): String {
|
||||
return prefs.getString(KEY_SELECTED_THEME, THEME_CLASSIC) ?: THEME_CLASSIC
|
||||
}
|
||||
}
|
78
app/src/main/java/com/pixelmintdrop/model/StatsManager.kt
Normal file
78
app/src/main/java/com/pixelmintdrop/model/StatsManager.kt
Normal file
|
@ -0,0 +1,78 @@
|
|||
package com.pixelmintdrop.model
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
|
||||
/**
|
||||
* Manages game statistics and high scores
|
||||
*/
|
||||
class StatsManager(context: Context) {
|
||||
companion object {
|
||||
private const val PREFS_NAME = "game_stats"
|
||||
private const val KEY_HIGH_SCORE = "high_score"
|
||||
private const val KEY_TOTAL_GAMES = "total_games"
|
||||
private const val KEY_TOTAL_LINES = "total_lines"
|
||||
private const val KEY_TOTAL_QUADS = "total_quads"
|
||||
private const val KEY_TOTAL_PERFECT_CLEARS = "total_perfect_clears"
|
||||
private const val KEY_SESSION_SCORE = "session_score"
|
||||
private const val KEY_SESSION_LINES = "session_lines"
|
||||
private const val KEY_SESSION_QUADS = "session_quads"
|
||||
private const val KEY_SESSION_PERFECT_CLEARS = "session_perfect_clears"
|
||||
}
|
||||
|
||||
private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
// Session stats
|
||||
private var sessionScore = 0
|
||||
private var sessionLines = 0
|
||||
private var sessionQuads = 0
|
||||
private var sessionPerfectClears = 0
|
||||
|
||||
fun getHighScore(): Int = prefs.getInt(KEY_HIGH_SCORE, 0)
|
||||
fun getTotalGames(): Int = prefs.getInt(KEY_TOTAL_GAMES, 0)
|
||||
fun getTotalLines(): Int = prefs.getInt(KEY_TOTAL_LINES, 0)
|
||||
fun getTotalQuads(): Int = prefs.getInt(KEY_TOTAL_QUADS, 0)
|
||||
fun getTotalPerfectClears(): Int = prefs.getInt(KEY_TOTAL_PERFECT_CLEARS, 0)
|
||||
|
||||
fun getSessionScore(): Int = sessionScore
|
||||
fun getSessionLines(): Int = sessionLines
|
||||
fun getSessionQuads(): Int = sessionQuads
|
||||
fun getSessionPerfectClears(): Int = sessionPerfectClears
|
||||
|
||||
fun updateStats(score: Int, lines: Int, isQuad: Boolean, isPerfectClear: Boolean) {
|
||||
// Update session stats
|
||||
sessionScore += score
|
||||
sessionLines += lines
|
||||
if (isQuad) sessionQuads++
|
||||
if (isPerfectClear) sessionPerfectClears++
|
||||
|
||||
// Update high score if needed
|
||||
if (sessionScore > getHighScore()) {
|
||||
prefs.edit().putInt(KEY_HIGH_SCORE, sessionScore).apply()
|
||||
}
|
||||
|
||||
// Update total stats
|
||||
prefs.edit()
|
||||
.putInt(KEY_TOTAL_LINES, getTotalLines() + lines)
|
||||
.putInt(KEY_TOTAL_QUADS, getTotalQuads() + if (isQuad) 1 else 0)
|
||||
.putInt(KEY_TOTAL_PERFECT_CLEARS, getTotalPerfectClears() + if (isPerfectClear) 1 else 0)
|
||||
.apply()
|
||||
}
|
||||
|
||||
fun startNewGame() {
|
||||
// Increment total games counter
|
||||
prefs.edit().putInt(KEY_TOTAL_GAMES, getTotalGames() + 1).apply()
|
||||
}
|
||||
|
||||
fun resetSession() {
|
||||
sessionScore = 0
|
||||
sessionLines = 0
|
||||
sessionQuads = 0
|
||||
sessionPerfectClears = 0
|
||||
}
|
||||
|
||||
fun resetAllStats() {
|
||||
prefs.edit().clear().apply()
|
||||
resetSession()
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
package com.mintris.model
|
||||
package com.pixelmintdrop.model
|
||||
|
||||
/**
|
||||
* Represents a Tetris piece (Tetromino)
|
||||
* Represents a game piece (Tetromino)
|
||||
*/
|
||||
enum class TetrominoType {
|
||||
I, J, L, O, S, T, Z
|
52
app/src/main/java/com/pixelmintdrop/theme/ThemeManager.kt
Normal file
52
app/src/main/java/com/pixelmintdrop/theme/ThemeManager.kt
Normal file
|
@ -0,0 +1,52 @@
|
|||
package com.pixelmintdrop.theme
|
||||
|
||||
import android.graphics.Color
|
||||
|
||||
object ThemeManager {
|
||||
// Theme colors
|
||||
const val COLOR_CLASSIC_BACKGROUND = Color.BLACK
|
||||
const val COLOR_CLASSIC_FOREGROUND = Color.WHITE
|
||||
const val COLOR_CLASSIC_ACCENT = Color.CYAN
|
||||
|
||||
const val COLOR_NEON_BACKGROUND = 0xFF0D0221.toInt()
|
||||
const val COLOR_NEON_FOREGROUND = 0xFFFF00FF.toInt()
|
||||
const val COLOR_NEON_ACCENT = 0xFF00FFFF.toInt()
|
||||
|
||||
const val COLOR_MONOCHROME_BACKGROUND = 0xFF1A1A1A.toInt()
|
||||
const val COLOR_MONOCHROME_FOREGROUND = Color.LTGRAY
|
||||
const val COLOR_MONOCHROME_ACCENT = Color.WHITE
|
||||
|
||||
const val COLOR_RETRO_BACKGROUND = 0xFF3F2832.toInt()
|
||||
const val COLOR_RETRO_FOREGROUND = 0xFFFF5A5F.toInt()
|
||||
const val COLOR_RETRO_ACCENT = 0xFFFFB400.toInt()
|
||||
|
||||
const val COLOR_MINIMALIST_BACKGROUND = Color.WHITE
|
||||
const val COLOR_MINIMALIST_FOREGROUND = Color.BLACK
|
||||
const val COLOR_MINIMALIST_ACCENT = Color.DKGRAY
|
||||
|
||||
const val COLOR_GALAXY_BACKGROUND = 0xFF0B0C10.toInt()
|
||||
const val COLOR_GALAXY_FOREGROUND = 0xFF66FCF1.toInt()
|
||||
const val COLOR_GALAXY_ACCENT = 0xFF45A29E.toInt()
|
||||
|
||||
// Block colors for each piece type
|
||||
const val COLOR_I_PIECE = 0xFF00F0F0.toInt()
|
||||
const val COLOR_J_PIECE = 0xFF0000F0.toInt()
|
||||
const val COLOR_L_PIECE = 0xFFF0A000.toInt()
|
||||
const val COLOR_O_PIECE = 0xFFF0F000.toInt()
|
||||
const val COLOR_S_PIECE = 0xFF00F000.toInt()
|
||||
const val COLOR_T_PIECE = 0xFFA000F0.toInt()
|
||||
const val COLOR_Z_PIECE = 0xFFF00000.toInt()
|
||||
|
||||
// Ghost piece colors
|
||||
const val COLOR_GHOST_PIECE = 0x40FFFFFF
|
||||
const val COLOR_GHOST_PIECE_GLOW = 0x20FFFFFF
|
||||
|
||||
// Grid colors
|
||||
const val COLOR_GRID_LINE = 0x20FFFFFF
|
||||
const val COLOR_GRID_BORDER = 0x40FFFFFF
|
||||
|
||||
// Effect colors
|
||||
const val COLOR_LINE_CLEAR_FLASH = 0x80FFFFFF
|
||||
const val COLOR_PERFECT_CLEAR_FLASH = 0xFFFFD700.toInt()
|
||||
const val COLOR_COMBO_FLASH = 0x60FFFFFF
|
||||
}
|
431
app/src/main/java/com/pixelmintdrop/ui/BlockSkinSelector.kt
Normal file
431
app/src/main/java/com/pixelmintdrop/ui/BlockSkinSelector.kt
Normal file
|
@ -0,0 +1,431 @@
|
|||
package com.pixelmintdrop.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.GridLayout
|
||||
import android.widget.TextView
|
||||
import androidx.cardview.widget.CardView
|
||||
import com.pixelmintdrop.R
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
|
||||
/**
|
||||
* UI component for selecting block skins
|
||||
*/
|
||||
class BlockSkinSelector @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : FrameLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
private val skinsGrid: GridLayout
|
||||
private val availableSkinsLabel: TextView
|
||||
|
||||
// Callback when a block skin is selected
|
||||
var onBlockSkinSelected: ((String) -> Unit)? = null
|
||||
|
||||
// Currently selected block skin (persisted)
|
||||
private var selectedSkin: String = "block_skin_1"
|
||||
|
||||
// Block skin cards map (skinId -> CardView)
|
||||
private val skinCards = mutableMapOf<String, CardView>()
|
||||
// Ordered list of skin IDs for navigation
|
||||
private val skinIdList = mutableListOf<String>()
|
||||
// Currently focused skin ID (for gamepad navigation within the selector)
|
||||
private var focusedSkinId: String? = null
|
||||
// Index of the currently focused skin in skinIdList
|
||||
private var focusedIndex: Int = -1
|
||||
// Flag indicating if the entire selector component has focus from the main menu
|
||||
private var hasComponentFocus: Boolean = false
|
||||
|
||||
// Player level for determining what should be unlocked
|
||||
private var playerLevel: Int = 1
|
||||
|
||||
init {
|
||||
// Inflate the layout
|
||||
LayoutInflater.from(context).inflate(R.layout.block_skin_selector, this, true)
|
||||
|
||||
// Get references to views
|
||||
skinsGrid = findViewById(R.id.skins_grid)
|
||||
availableSkinsLabel = findViewById(R.id.available_skins_label)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the block skin selector with unlocked skins
|
||||
*/
|
||||
fun updateBlockSkins(unlockedSkins: Set<String>, currentSkin: String, playerLevel: Int = 1) {
|
||||
// Store player level
|
||||
this.playerLevel = playerLevel
|
||||
|
||||
// Clear existing skin cards and ID list
|
||||
skinsGrid.removeAllViews()
|
||||
skinCards.clear()
|
||||
skinIdList.clear()
|
||||
|
||||
// Update selected skin and initial focus
|
||||
selectedSkin = currentSkin
|
||||
focusedSkinId = currentSkin
|
||||
focusedIndex = -1 // Reset index
|
||||
|
||||
// Get all possible skins and their details, sorted for consistent order
|
||||
val allSkins = getBlockSkins().entries.sortedWith(compareBy({ it.value.unlockLevel }, { it.value.displayName })).associate { it.key to it.value }
|
||||
|
||||
// Add skin cards to the grid and build ID list
|
||||
allSkins.forEach { (skinId, skinInfo) ->
|
||||
val isEffectivelyUnlocked = unlockedSkins.contains(skinId) || playerLevel >= skinInfo.unlockLevel
|
||||
val isSelected = skinId == selectedSkin
|
||||
|
||||
// Only add unlocked skins to the navigable list
|
||||
if (isEffectivelyUnlocked) {
|
||||
skinIdList.add(skinId)
|
||||
}
|
||||
|
||||
val skinCard = createBlockSkinCard(skinId, skinInfo, isEffectivelyUnlocked, isSelected)
|
||||
skinCards[skinId] = skinCard
|
||||
skinsGrid.addView(skinCard)
|
||||
|
||||
// Update focused index if this is the currently selected/focused skin
|
||||
if (isEffectivelyUnlocked && skinId == focusedSkinId) {
|
||||
focusedIndex = skinIdList.indexOf(skinId)
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure focus index is valid if the previously focused skin is no longer available/unlocked
|
||||
if (focusedIndex == -1 && skinIdList.isNotEmpty()) {
|
||||
focusedIndex = 0
|
||||
focusedSkinId = skinIdList[0]
|
||||
}
|
||||
|
||||
// Apply initial focus highlight if the component has focus
|
||||
highlightFocusedCard()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a card for a block skin
|
||||
*/
|
||||
private fun createBlockSkinCard(
|
||||
skinId: String,
|
||||
skinInfo: BlockSkinInfo,
|
||||
isUnlocked: Boolean,
|
||||
isSelected: Boolean
|
||||
): CardView {
|
||||
// Create the card
|
||||
val card = CardView(context).apply {
|
||||
id = View.generateViewId()
|
||||
radius = 12f
|
||||
cardElevation = if (isSelected) 8f else 2f
|
||||
useCompatPadding = true
|
||||
|
||||
// Set card background color based on skin
|
||||
setCardBackgroundColor(skinInfo.backgroundColor)
|
||||
|
||||
// Set card dimensions
|
||||
val cardSize = resources.getDimensionPixelSize(R.dimen.theme_card_size)
|
||||
layoutParams = GridLayout.LayoutParams().apply {
|
||||
width = cardSize
|
||||
height = cardSize
|
||||
columnSpec = GridLayout.spec(GridLayout.UNDEFINED, 1f)
|
||||
rowSpec = GridLayout.spec(GridLayout.UNDEFINED, 1f)
|
||||
setMargins(8, 8, 8, 8)
|
||||
}
|
||||
|
||||
// Apply locked/selected state visuals (only opacity here)
|
||||
alpha = if (isUnlocked) 1.0f else 0.5f
|
||||
}
|
||||
|
||||
// Create block skin content container
|
||||
val container = FrameLayout(context).apply {
|
||||
layoutParams = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
}
|
||||
|
||||
// Create the block skin preview
|
||||
val blockSkinPreview = View(context).apply {
|
||||
layoutParams = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
|
||||
// Set the background color
|
||||
setBackgroundColor(skinInfo.backgroundColor)
|
||||
}
|
||||
|
||||
// Add a label with the skin name
|
||||
val skinLabel = TextView(context).apply {
|
||||
text = skinInfo.displayName
|
||||
setTextColor(skinInfo.textColor)
|
||||
textSize = 14f
|
||||
textAlignment = View.TEXT_ALIGNMENT_CENTER
|
||||
|
||||
// Position at the bottom of the card
|
||||
layoutParams = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.WRAP_CONTENT
|
||||
).apply {
|
||||
gravity = android.view.Gravity.BOTTOM or android.view.Gravity.CENTER_HORIZONTAL
|
||||
setMargins(4, 4, 4, 8)
|
||||
}
|
||||
}
|
||||
|
||||
// Add level requirement for locked skins
|
||||
val levelRequirement = TextView(context).apply {
|
||||
text = "Level ${skinInfo.unlockLevel}"
|
||||
setTextColor(Color.WHITE)
|
||||
textSize = 12f
|
||||
textAlignment = View.TEXT_ALIGNMENT_CENTER
|
||||
visibility = if (isUnlocked) View.GONE else View.VISIBLE
|
||||
|
||||
// Position at the center of the card
|
||||
layoutParams = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.WRAP_CONTENT,
|
||||
FrameLayout.LayoutParams.WRAP_CONTENT
|
||||
).apply {
|
||||
gravity = android.view.Gravity.CENTER
|
||||
}
|
||||
// Make text bold and more visible for better readability
|
||||
typeface = android.graphics.Typeface.DEFAULT_BOLD
|
||||
setShadowLayer(3f, 1f, 1f, Color.BLACK)
|
||||
}
|
||||
|
||||
// Add a lock icon if the skin is locked
|
||||
val lockOverlay = View(context).apply {
|
||||
layoutParams = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
|
||||
// Add lock icon or visual indicator
|
||||
setBackgroundResource(R.drawable.lock_overlay)
|
||||
visibility = if (isUnlocked) View.GONE else View.VISIBLE
|
||||
}
|
||||
|
||||
// Add all elements to container
|
||||
container.addView(blockSkinPreview)
|
||||
container.addView(skinLabel)
|
||||
container.addView(lockOverlay)
|
||||
container.addView(levelRequirement)
|
||||
|
||||
// Add container to card
|
||||
card.addView(container)
|
||||
|
||||
// Set up click listener only for unlocked skins
|
||||
if (isUnlocked) {
|
||||
card.setOnClickListener {
|
||||
// Clicking directly selects the skin
|
||||
focusedSkinId = skinId
|
||||
focusedIndex = skinIdList.indexOf(skinId)
|
||||
confirmSelection() // Directly confirm click selection
|
||||
}
|
||||
}
|
||||
|
||||
return card
|
||||
}
|
||||
|
||||
/**
|
||||
* Data class for block skin information
|
||||
*/
|
||||
data class BlockSkinInfo(
|
||||
val displayName: String,
|
||||
val backgroundColor: Int,
|
||||
val textColor: Int,
|
||||
val unlockLevel: Int
|
||||
)
|
||||
|
||||
/**
|
||||
* Get all available block skins with their details
|
||||
*/
|
||||
private fun getBlockSkins(): Map<String, BlockSkinInfo> {
|
||||
return mapOf(
|
||||
"block_skin_1" to BlockSkinInfo(
|
||||
displayName = "Classic",
|
||||
backgroundColor = Color.BLACK,
|
||||
textColor = Color.WHITE,
|
||||
unlockLevel = 1
|
||||
),
|
||||
"block_skin_2" to BlockSkinInfo(
|
||||
displayName = "Neon",
|
||||
backgroundColor = Color.parseColor("#0D0221"),
|
||||
textColor = Color.parseColor("#FF00FF"),
|
||||
unlockLevel = 7
|
||||
),
|
||||
"block_skin_3" to BlockSkinInfo(
|
||||
displayName = "Retro",
|
||||
backgroundColor = Color.parseColor("#3F2832"),
|
||||
textColor = Color.parseColor("#FF5A5F"),
|
||||
unlockLevel = 14
|
||||
),
|
||||
"block_skin_4" to BlockSkinInfo(
|
||||
displayName = "Minimalist",
|
||||
backgroundColor = Color.WHITE,
|
||||
textColor = Color.BLACK,
|
||||
unlockLevel = 21
|
||||
),
|
||||
"block_skin_5" to BlockSkinInfo(
|
||||
displayName = "Galaxy",
|
||||
backgroundColor = Color.parseColor("#0B0C10"),
|
||||
textColor = Color.parseColor("#66FCF1"),
|
||||
unlockLevel = 28
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the entire component has focus (from the parent menu).
|
||||
*/
|
||||
fun setHasFocus(hasFocus: Boolean) {
|
||||
hasComponentFocus = hasFocus
|
||||
highlightFocusedCard() // Re-apply highlights
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the internal focus to the next available skin.
|
||||
*/
|
||||
fun focusNextItem() {
|
||||
if (!hasComponentFocus || skinIdList.isEmpty()) return
|
||||
|
||||
focusedIndex = (focusedIndex + 1) % skinIdList.size
|
||||
focusedSkinId = skinIdList[focusedIndex]
|
||||
highlightFocusedCard()
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the internal focus to the previous available skin.
|
||||
*/
|
||||
fun focusPreviousItem() {
|
||||
if (!hasComponentFocus || skinIdList.isEmpty()) return
|
||||
|
||||
focusedIndex = (focusedIndex - 1 + skinIdList.size) % skinIdList.size
|
||||
focusedSkinId = skinIdList[focusedIndex]
|
||||
highlightFocusedCard()
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirms the currently focused skin as the selected skin.
|
||||
* Triggers the onBlockSkinSelected callback.
|
||||
*/
|
||||
fun confirmSelection() {
|
||||
if (focusedSkinId == null || focusedSkinId == selectedSkin) {
|
||||
return // No change needed
|
||||
}
|
||||
|
||||
// Update the selected skin
|
||||
val newlySelectedSkin = focusedSkinId!!
|
||||
selectedSkin = newlySelectedSkin
|
||||
|
||||
// Update visual states
|
||||
highlightFocusedCard()
|
||||
|
||||
// Trigger the callback
|
||||
onBlockSkinSelected?.invoke(selectedSkin)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the visual highlight state of the skin cards based on
|
||||
* selection and internal focus.
|
||||
*/
|
||||
private fun highlightFocusedCard() {
|
||||
if (skinCards.isEmpty()) return
|
||||
|
||||
val focusColor = Color.YELLOW // Color for focused-but-not-selected
|
||||
val selectedColor = Color.WHITE // Color for selected (might be focused or not)
|
||||
|
||||
skinCards.forEach { (skinId, card) ->
|
||||
val skinInfo = getBlockSkins()[skinId] ?: return@forEach
|
||||
// Check unlock status based on the navigable list derived from level/unlocks
|
||||
val isUnlocked = skinIdList.contains(skinId)
|
||||
|
||||
if (!isUnlocked) {
|
||||
// Keep locked skins visually distinct
|
||||
card.alpha = 0.5f
|
||||
card.cardElevation = 2f
|
||||
card.background = null
|
||||
card.setCardBackgroundColor(skinInfo.backgroundColor)
|
||||
card.scaleX = 1.0f // Reset scale
|
||||
card.scaleY = 1.0f
|
||||
return@forEach
|
||||
}
|
||||
|
||||
// Reset unlocked cards first
|
||||
card.alpha = 1.0f
|
||||
card.cardElevation = 4f
|
||||
card.background = null
|
||||
card.setCardBackgroundColor(skinInfo.backgroundColor)
|
||||
card.scaleX = 1.0f
|
||||
card.scaleY = 1.0f
|
||||
|
||||
val isSelected = (skinId == selectedSkin)
|
||||
val isFocused = (hasComponentFocus && skinId == focusedSkinId)
|
||||
|
||||
var borderColor = Color.TRANSPARENT
|
||||
var borderWidth = 0
|
||||
var elevation = 4f
|
||||
var scale = 1.0f
|
||||
|
||||
if (isSelected) {
|
||||
borderColor = selectedColor
|
||||
borderWidth = 6 // Thick border for selected
|
||||
elevation = 12f // Higher elevation for selected
|
||||
}
|
||||
|
||||
if (isFocused) {
|
||||
// Focused item gets a distinct border (unless it's also selected)
|
||||
if (!isSelected) {
|
||||
borderColor = focusColor
|
||||
borderWidth = 4 // Slightly thinner border for focused
|
||||
}
|
||||
elevation = 12f // Use high elevation for focus too
|
||||
scale = 1.1f // Scale up the focused item
|
||||
}
|
||||
|
||||
// Apply scale
|
||||
card.scaleX = scale
|
||||
card.scaleY = scale
|
||||
|
||||
// Apply border and elevation
|
||||
if (borderWidth > 0) {
|
||||
val gradientDrawable = GradientDrawable().apply {
|
||||
setColor(skinInfo.backgroundColor) // Use skin's background for the fill
|
||||
setStroke(borderWidth, borderColor)
|
||||
cornerRadius = 12f
|
||||
}
|
||||
card.background = gradientDrawable
|
||||
} else {
|
||||
card.background = null // Ensure no border if not selected/focused
|
||||
}
|
||||
card.cardElevation = elevation
|
||||
}
|
||||
}
|
||||
|
||||
// Keep selectNextBlockSkin temporarily for compatibility, but it shouldn't be called by MainActivity anymore
|
||||
fun selectNextBlockSkin() {
|
||||
val allSkins = getBlockSkins().keys.toList()
|
||||
val currentIndex = allSkins.indexOf(selectedSkin)
|
||||
if (currentIndex == -1) return
|
||||
|
||||
var nextIndex = (currentIndex + 1) % allSkins.size
|
||||
while (nextIndex != currentIndex) {
|
||||
val nextSkin = allSkins[nextIndex]
|
||||
val skinInfo = getBlockSkins()[nextSkin] ?: continue
|
||||
val isEffectivelyUnlocked = skinCards[nextSkin]?.alpha == 1.0f // Basic check based on alpha
|
||||
|| playerLevel >= skinInfo.unlockLevel
|
||||
|
||||
if (isEffectivelyUnlocked) {
|
||||
// This method now just sets the internal focus and confirms
|
||||
focusedSkinId = nextSkin
|
||||
focusedIndex = skinIdList.indexOf(nextSkin) // Update index based on navigable list
|
||||
if (focusedIndex == -1) { // If not found in navigable list, reset focus
|
||||
focusedIndex = 0
|
||||
focusedSkinId = if (skinIdList.isNotEmpty()) skinIdList[0] else null
|
||||
}
|
||||
confirmSelection() // Confirm the selection
|
||||
return
|
||||
}
|
||||
nextIndex = (nextIndex + 1) % allSkins.size
|
||||
}
|
||||
}
|
||||
}
|
187
app/src/main/java/com/pixelmintdrop/ui/GameUIManager.kt
Normal file
187
app/src/main/java/com/pixelmintdrop/ui/GameUIManager.kt
Normal file
|
@ -0,0 +1,187 @@
|
|||
package com.pixelmintdrop.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.view.View
|
||||
import com.pixelmintdrop.databinding.ActivityMainBinding
|
||||
import com.pixelmintdrop.model.PlayerProgressionManager
|
||||
|
||||
/**
|
||||
* Handles UI updates and state management for the game interface
|
||||
* to reduce the responsibility of the MainActivity.
|
||||
*/
|
||||
class GameUIManager(
|
||||
private val context: Context,
|
||||
private val binding: ActivityMainBinding
|
||||
) {
|
||||
// UI state
|
||||
private var currentScore = 0L
|
||||
private var currentLevel = 1
|
||||
private var currentLines = 0
|
||||
|
||||
// Theme management
|
||||
private var currentTheme = PlayerProgressionManager.THEME_CLASSIC
|
||||
|
||||
/**
|
||||
* Update the game state UI elements
|
||||
*/
|
||||
fun updateGameStateUI(score: Long, level: Int, lines: Int) {
|
||||
// Update cached values
|
||||
currentScore = score
|
||||
currentLevel = level
|
||||
currentLines = lines
|
||||
|
||||
// Update UI
|
||||
binding.scoreText.text = score.toString()
|
||||
binding.currentLevelText.text = level.toString()
|
||||
binding.linesText.text = lines.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the game over UI
|
||||
*/
|
||||
fun showGameOver(finalScore: Long) {
|
||||
// Hide game UI elements
|
||||
binding.gameControlsContainer.visibility = View.GONE
|
||||
binding.holdPieceView.visibility = View.GONE
|
||||
binding.nextPieceView.visibility = View.GONE
|
||||
binding.pauseButton.visibility = View.GONE
|
||||
binding.leftControlsPanel?.visibility = View.GONE
|
||||
binding.rightControlsPanel?.visibility = View.GONE
|
||||
|
||||
// Show game over container
|
||||
binding.gameOverContainer.visibility = View.VISIBLE
|
||||
binding.sessionScoreText.text = "Score: $finalScore"
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the game over UI
|
||||
*/
|
||||
fun hideGameOver() {
|
||||
binding.gameOverContainer.visibility = View.GONE
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the pause menu
|
||||
*/
|
||||
fun showPauseMenu() {
|
||||
binding.pauseContainer.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the pause menu
|
||||
*/
|
||||
fun hidePauseMenu() {
|
||||
binding.pauseContainer.visibility = View.GONE
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the music toggle UI based on current state
|
||||
*/
|
||||
fun updateMusicToggleUI(isMusicEnabled: Boolean) {
|
||||
// This assumes there's a musicToggle view in the layout
|
||||
// Modify as needed based on the actual UI
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the level selector display
|
||||
*/
|
||||
fun updateLevelSelector(selectedLevel: Int) {
|
||||
// Assuming pauseLevelText is part of the LevelBadge component
|
||||
// This may need to be adapted based on how the level badge works
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the game elements (views, controls)
|
||||
*/
|
||||
fun showGameElements() {
|
||||
binding.gameView.visibility = View.VISIBLE
|
||||
binding.gameControlsContainer.visibility = View.VISIBLE
|
||||
binding.holdPieceView.visibility = View.VISIBLE
|
||||
binding.nextPieceView.visibility = View.VISIBLE
|
||||
binding.pauseButton.visibility = View.VISIBLE
|
||||
|
||||
// These might not exist in the actual layout
|
||||
// binding.leftControlsPanel?.visibility = View.VISIBLE
|
||||
// binding.rightControlsPanel?.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the game elements
|
||||
*/
|
||||
fun hideGameElements() {
|
||||
binding.gameControlsContainer.visibility = View.GONE
|
||||
binding.holdPieceView.visibility = View.GONE
|
||||
binding.nextPieceView.visibility = View.GONE
|
||||
binding.pauseButton.visibility = View.GONE
|
||||
|
||||
// These might not exist in the actual layout
|
||||
// binding.leftControlsPanel?.visibility = View.GONE
|
||||
// binding.rightControlsPanel?.visibility = View.GONE
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a theme to the game UI
|
||||
*/
|
||||
fun applyTheme(themeId: String) {
|
||||
currentTheme = themeId
|
||||
|
||||
// Set background color based on theme
|
||||
val backgroundColor = getThemeBackgroundColor(themeId)
|
||||
binding.root.setBackgroundColor(backgroundColor)
|
||||
|
||||
// Update title screen theme if it has a setTheme method
|
||||
// binding.titleScreen.setTheme(themeId)
|
||||
|
||||
// Set text colors based on theme
|
||||
val isDarkTheme = backgroundColor == Color.BLACK ||
|
||||
Color.red(backgroundColor) < 50 &&
|
||||
Color.green(backgroundColor) < 50 &&
|
||||
Color.blue(backgroundColor) < 50
|
||||
|
||||
val textColor = if (isDarkTheme) Color.WHITE else Color.BLACK
|
||||
updateTextColors(textColor)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update all text colors in the UI
|
||||
*/
|
||||
private fun updateTextColors(color: Int) {
|
||||
binding.scoreText.setTextColor(color)
|
||||
binding.currentLevelText.setTextColor(color)
|
||||
binding.linesText.setTextColor(color)
|
||||
|
||||
// Other text views might not exist or have different IDs
|
||||
// Adapt based on the actual layout
|
||||
}
|
||||
|
||||
/**
|
||||
* Get background color based on theme ID
|
||||
*/
|
||||
private fun getThemeBackgroundColor(themeId: String): Int {
|
||||
return when (themeId) {
|
||||
PlayerProgressionManager.THEME_CLASSIC -> Color.BLACK
|
||||
PlayerProgressionManager.THEME_NEON -> Color.parseColor("#0D0221")
|
||||
PlayerProgressionManager.THEME_MONOCHROME -> Color.parseColor("#1A1A1A")
|
||||
PlayerProgressionManager.THEME_RETRO -> Color.parseColor("#3F2832")
|
||||
PlayerProgressionManager.THEME_MINIMALIST -> Color.WHITE
|
||||
PlayerProgressionManager.THEME_GALAXY -> Color.parseColor("#0B0C10")
|
||||
else -> Color.BLACK
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current score
|
||||
*/
|
||||
fun getCurrentScore(): Long = currentScore
|
||||
|
||||
/**
|
||||
* Get the current level
|
||||
*/
|
||||
fun getCurrentLevel(): Int = currentLevel
|
||||
|
||||
/**
|
||||
* Get the current lines cleared
|
||||
*/
|
||||
fun getCurrentLines(): Int = currentLines
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package com.mintris.ui
|
||||
package com.pixelmintdrop.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
|
@ -1,4 +1,4 @@
|
|||
package com.mintris.ui
|
||||
package com.pixelmintdrop.ui
|
||||
|
||||
import android.animation.AnimatorSet
|
||||
import android.animation.ObjectAnimator
|
||||
|
@ -12,8 +12,8 @@ import android.view.animation.OvershootInterpolator
|
|||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.cardview.widget.CardView
|
||||
import com.mintris.R
|
||||
import com.mintris.model.PlayerProgressionManager
|
||||
import com.pixelmintdrop.R
|
||||
import com.pixelmintdrop.model.PlayerProgressionManager
|
||||
|
||||
/**
|
||||
* Screen that displays player progression, XP gain, and unlocked rewards
|
||||
|
@ -31,6 +31,9 @@ class ProgressionScreen @JvmOverloads constructor(
|
|||
private val rewardsContainer: LinearLayout
|
||||
private val continueButton: TextView
|
||||
|
||||
// Current theme
|
||||
private var currentTheme: String = PlayerProgressionManager.THEME_CLASSIC
|
||||
|
||||
// Callback for when the player dismisses the screen
|
||||
var onContinue: (() -> Unit)? = null
|
||||
|
||||
|
@ -62,6 +65,9 @@ class ProgressionScreen @JvmOverloads constructor(
|
|||
newRewards: List<String>,
|
||||
themeId: String = PlayerProgressionManager.THEME_CLASSIC
|
||||
) {
|
||||
// Update current theme
|
||||
currentTheme = themeId
|
||||
|
||||
// Hide rewards container initially if there are no new rewards
|
||||
rewardsContainer.visibility = if (newRewards.isEmpty()) View.GONE else View.INVISIBLE
|
||||
|
||||
|
@ -74,20 +80,39 @@ class ProgressionScreen @JvmOverloads constructor(
|
|||
playerLevelText.text = "Player Level: $playerLevel"
|
||||
xpGainText.text = "+$xpGained XP"
|
||||
|
||||
// Begin animation sequence
|
||||
xpProgressBar.setXPValues(playerLevel, currentXP, xpForNextLevel)
|
||||
// Update level up text visibility
|
||||
val progressionTitle = findViewById<TextView>(R.id.progression_title)
|
||||
progressionTitle.visibility = if (newRewards.any { it.contains("Level") }) View.VISIBLE else View.GONE
|
||||
|
||||
// Animate XP gain text entrance
|
||||
val xpTextAnimator = ObjectAnimator.ofFloat(xpGainText, "alpha", 0f, 1f).apply {
|
||||
duration = 500
|
||||
// Start with initial animations
|
||||
AnimatorSet().apply {
|
||||
// Fade in the XP gain text
|
||||
val xpTextAnimator = ObjectAnimator.ofFloat(xpGainText, "alpha", 0f, 1f).apply {
|
||||
duration = 800
|
||||
interpolator = AccelerateDecelerateInterpolator()
|
||||
}
|
||||
|
||||
// Set up the XP progress bar animation sequence
|
||||
val xpBarAnimator = ObjectAnimator.ofFloat(xpProgressBar, "alpha", 0f, 1f).apply {
|
||||
duration = 800
|
||||
interpolator = AccelerateDecelerateInterpolator()
|
||||
}
|
||||
|
||||
// Play animations in sequence
|
||||
play(xpTextAnimator)
|
||||
play(xpBarAnimator).after(xpTextAnimator)
|
||||
start()
|
||||
}
|
||||
|
||||
// Schedule animation for the XP bar after text appears
|
||||
// Set initial progress bar state
|
||||
xpProgressBar.setXPValues(playerLevel, currentXP - xpGained, xpForNextLevel)
|
||||
|
||||
// Animate the XP gain after a short delay
|
||||
postDelayed({
|
||||
xpProgressBar.animateXPGain(xpGained, playerLevel, currentXP, xpForNextLevel)
|
||||
}, 600)
|
||||
}, 1000) // Increased delay to 1 second for better visual flow
|
||||
|
||||
// If there are new rewards, show them with animation
|
||||
// If there are new rewards, show them with animation after XP bar animation
|
||||
if (newRewards.isNotEmpty()) {
|
||||
// Create reward cards
|
||||
rewardsContainer.removeAllViews()
|
||||
|
@ -113,18 +138,12 @@ class ProgressionScreen @JvmOverloads constructor(
|
|||
card.animate()
|
||||
.alpha(1f)
|
||||
.translationY(0f)
|
||||
.setDuration(400)
|
||||
.setStartDelay((i * 150).toLong())
|
||||
.setDuration(600) // Increased duration for smoother animation
|
||||
.setStartDelay((i * 200).toLong()) // Increased delay between cards
|
||||
.setInterpolator(OvershootInterpolator())
|
||||
.start()
|
||||
}
|
||||
}, 2000) // Wait for XP bar animation to finish
|
||||
}
|
||||
|
||||
// Start with initial animations
|
||||
AnimatorSet().apply {
|
||||
play(xpTextAnimator)
|
||||
start()
|
||||
}, 2500) // Increased delay to wait for XP bar animation to finish
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -137,8 +156,17 @@ class ProgressionScreen @JvmOverloads constructor(
|
|||
cardElevation = 4f
|
||||
useCompatPadding = true
|
||||
|
||||
// Default background color - will be adjusted based on theme
|
||||
setCardBackgroundColor(Color.BLACK)
|
||||
// Set background color based on current theme
|
||||
val backgroundColor = when (currentTheme) {
|
||||
PlayerProgressionManager.THEME_CLASSIC -> Color.BLACK
|
||||
PlayerProgressionManager.THEME_NEON -> Color.parseColor("#0D0221")
|
||||
PlayerProgressionManager.THEME_MONOCHROME -> Color.parseColor("#1A1A1A")
|
||||
PlayerProgressionManager.THEME_RETRO -> Color.parseColor("#3F2832")
|
||||
PlayerProgressionManager.THEME_MINIMALIST -> Color.WHITE
|
||||
PlayerProgressionManager.THEME_GALAXY -> Color.parseColor("#0B0C10")
|
||||
else -> Color.BLACK
|
||||
}
|
||||
setCardBackgroundColor(backgroundColor)
|
||||
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
|
@ -167,6 +195,8 @@ class ProgressionScreen @JvmOverloads constructor(
|
|||
* Apply the current theme to the progression screen
|
||||
*/
|
||||
fun applyTheme(themeId: String) {
|
||||
currentTheme = themeId
|
||||
|
||||
// Get reference to the title text
|
||||
val progressionTitle = findViewById<TextView>(R.id.progression_title)
|
||||
val rewardsTitle = findViewById<TextView>(R.id.rewards_title)
|
||||
|
@ -248,10 +278,10 @@ class ProgressionScreen @JvmOverloads constructor(
|
|||
}
|
||||
}
|
||||
|
||||
// Set theme color on XP progress bar
|
||||
// Update XP progress bar theme color
|
||||
xpProgressBar.setThemeColor(xpThemeColor)
|
||||
|
||||
// Update card colors for any existing reward cards
|
||||
// Update reward card colors
|
||||
updateRewardCardColors(themeId)
|
||||
}
|
||||
|
||||
|
@ -259,8 +289,7 @@ class ProgressionScreen @JvmOverloads constructor(
|
|||
* Update colors of existing reward cards to match the theme
|
||||
*/
|
||||
private fun updateRewardCardColors(themeId: String) {
|
||||
// Color for card backgrounds based on theme
|
||||
val cardBackgroundColor = when (themeId) {
|
||||
val backgroundColor = when (themeId) {
|
||||
PlayerProgressionManager.THEME_CLASSIC -> Color.BLACK
|
||||
PlayerProgressionManager.THEME_NEON -> Color.parseColor("#0D0221")
|
||||
PlayerProgressionManager.THEME_MONOCHROME -> Color.parseColor("#1A1A1A")
|
||||
|
@ -270,28 +299,16 @@ class ProgressionScreen @JvmOverloads constructor(
|
|||
else -> Color.BLACK
|
||||
}
|
||||
|
||||
// Text color for rewards based on theme
|
||||
val rewardTextColor = when (themeId) {
|
||||
PlayerProgressionManager.THEME_CLASSIC -> Color.WHITE
|
||||
PlayerProgressionManager.THEME_NEON -> Color.parseColor("#FF00FF")
|
||||
PlayerProgressionManager.THEME_MONOCHROME -> Color.LTGRAY
|
||||
PlayerProgressionManager.THEME_RETRO -> Color.parseColor("#FF5A5F")
|
||||
PlayerProgressionManager.THEME_MINIMALIST -> Color.BLACK
|
||||
PlayerProgressionManager.THEME_GALAXY -> Color.parseColor("#66FCF1")
|
||||
else -> Color.WHITE
|
||||
}
|
||||
|
||||
// Update each card in the rewards container
|
||||
for (i in 0 until rewardsContainer.childCount) {
|
||||
val card = rewardsContainer.getChildAt(i) as? CardView
|
||||
card?.let {
|
||||
it.setCardBackgroundColor(cardBackgroundColor)
|
||||
|
||||
// Update text color in the card
|
||||
if (it.childCount > 0 && it.getChildAt(0) is TextView) {
|
||||
(it.getChildAt(0) as TextView).setTextColor(rewardTextColor)
|
||||
}
|
||||
}
|
||||
card?.setCardBackgroundColor(backgroundColor)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Public method to handle continue action via gamepad
|
||||
*/
|
||||
fun performContinue() {
|
||||
continueButton.performClick()
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package com.mintris.ui
|
||||
package com.pixelmintdrop.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
|
@ -9,8 +9,10 @@ import android.widget.FrameLayout
|
|||
import android.widget.GridLayout
|
||||
import android.widget.TextView
|
||||
import androidx.cardview.widget.CardView
|
||||
import com.mintris.R
|
||||
import com.mintris.model.PlayerProgressionManager
|
||||
import com.pixelmintdrop.R
|
||||
import com.pixelmintdrop.model.PlayerProgressionManager
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.util.Log
|
||||
|
||||
/**
|
||||
* UI component for selecting game themes
|
||||
|
@ -27,11 +29,19 @@ class ThemeSelector @JvmOverloads constructor(
|
|||
// Callback when a theme is selected
|
||||
var onThemeSelected: ((String) -> Unit)? = null
|
||||
|
||||
// Currently selected theme
|
||||
// Currently selected theme (persisted)
|
||||
private var selectedTheme: String = PlayerProgressionManager.THEME_CLASSIC
|
||||
|
||||
// Theme cards
|
||||
// Theme cards map (themeId -> CardView)
|
||||
private val themeCards = mutableMapOf<String, CardView>()
|
||||
// Ordered list of theme IDs for navigation
|
||||
private val themeIdList = mutableListOf<String>()
|
||||
// Currently focused theme ID (for gamepad navigation within the selector)
|
||||
private var focusedThemeId: String? = null
|
||||
// Index of the currently focused theme in themeIdList
|
||||
private var focusedIndex: Int = -1
|
||||
// Flag indicating if the entire selector component has focus from the main menu
|
||||
private var hasComponentFocus: Boolean = false
|
||||
|
||||
init {
|
||||
// Inflate the layout
|
||||
|
@ -46,25 +56,47 @@ class ThemeSelector @JvmOverloads constructor(
|
|||
* Update the theme selector with unlocked themes
|
||||
*/
|
||||
fun updateThemes(unlockedThemes: Set<String>, currentTheme: String) {
|
||||
// Clear existing theme cards
|
||||
// Clear existing theme cards and ID list
|
||||
themesGrid.removeAllViews()
|
||||
themeCards.clear()
|
||||
themeIdList.clear()
|
||||
|
||||
// Update selected theme
|
||||
selectedTheme = currentTheme
|
||||
focusedThemeId = currentTheme // Initially focus the selected theme
|
||||
focusedIndex = -1 // Reset index
|
||||
|
||||
// Get all possible themes and their details
|
||||
val allThemes = getThemes()
|
||||
// Get all possible themes and their details, sorted for consistent order
|
||||
val allThemes = getThemes().entries.sortedWith(compareBy({ it.value.unlockLevel }, { it.value.displayName })).associate { it.key to it.value }
|
||||
|
||||
// Add theme cards to the grid
|
||||
// Add theme cards to the grid and build the ID list
|
||||
allThemes.forEach { (themeId, themeInfo) ->
|
||||
val isUnlocked = unlockedThemes.contains(themeId)
|
||||
val isSelected = themeId == selectedTheme
|
||||
|
||||
// Only add unlocked themes to the navigable list
|
||||
if (isUnlocked) {
|
||||
themeIdList.add(themeId)
|
||||
}
|
||||
|
||||
val themeCard = createThemeCard(themeId, themeInfo, isUnlocked, isSelected)
|
||||
themeCards[themeId] = themeCard
|
||||
themesGrid.addView(themeCard)
|
||||
|
||||
// Update focused index if this is the currently selected/focused theme
|
||||
if (isUnlocked && themeId == focusedThemeId) {
|
||||
focusedIndex = themeIdList.indexOf(themeId)
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure focus index is valid if the previously focused theme is no longer available/unlocked
|
||||
if (focusedIndex == -1 && themeIdList.isNotEmpty()) {
|
||||
focusedIndex = 0
|
||||
focusedThemeId = themeIdList[0]
|
||||
}
|
||||
|
||||
// Apply initial focus highlight if the component has focus
|
||||
highlightFocusedCard()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -86,20 +118,6 @@ class ThemeSelector @JvmOverloads constructor(
|
|||
// Set card background color based on theme
|
||||
setCardBackgroundColor(themeInfo.primaryColor)
|
||||
|
||||
// Add more noticeable visual indicator for selected theme
|
||||
if (isSelected) {
|
||||
setContentPadding(4, 4, 4, 4)
|
||||
// Create a gradient drawable for the border
|
||||
val gradientDrawable = android.graphics.drawable.GradientDrawable().apply {
|
||||
setColor(themeInfo.primaryColor)
|
||||
setStroke(6, Color.WHITE) // Thicker border
|
||||
cornerRadius = 12f
|
||||
}
|
||||
background = gradientDrawable
|
||||
// Add glow effect via elevation
|
||||
cardElevation = 12f
|
||||
}
|
||||
|
||||
// Set card dimensions
|
||||
val cardSize = resources.getDimensionPixelSize(R.dimen.theme_card_size)
|
||||
layoutParams = GridLayout.LayoutParams().apply {
|
||||
|
@ -194,38 +212,11 @@ class ThemeSelector @JvmOverloads constructor(
|
|||
// Set up click listener only for unlocked themes
|
||||
if (isUnlocked) {
|
||||
card.setOnClickListener {
|
||||
// Only trigger callback if this isn't already the selected theme
|
||||
if (themeId != selectedTheme) {
|
||||
// Update previously selected card
|
||||
themeCards[selectedTheme]?.let { prevCard ->
|
||||
prevCard.cardElevation = 2f
|
||||
// Reset any special styling
|
||||
prevCard.background = null
|
||||
prevCard.setCardBackgroundColor(getThemes()[selectedTheme]?.primaryColor ?: Color.BLACK)
|
||||
}
|
||||
|
||||
// Update visual state of newly selected card
|
||||
card.cardElevation = 12f
|
||||
|
||||
// Flash animation for selection feedback
|
||||
val flashColor = Color.WHITE
|
||||
val originalColor = themeInfo.primaryColor
|
||||
|
||||
// Create animator for flash effect
|
||||
val flashAnimator = android.animation.ValueAnimator.ofArgb(flashColor, originalColor)
|
||||
flashAnimator.duration = 300 // 300ms
|
||||
flashAnimator.addUpdateListener { animator ->
|
||||
val color = animator.animatedValue as Int
|
||||
card.setCardBackgroundColor(color)
|
||||
}
|
||||
flashAnimator.start()
|
||||
|
||||
// Update selected theme
|
||||
selectedTheme = themeId
|
||||
|
||||
// Notify listener
|
||||
onThemeSelected?.invoke(themeId)
|
||||
}
|
||||
// Clicking directly selects the theme
|
||||
Log.d("ThemeSelector", "Theme card clicked: $themeId (isUnlocked=$isUnlocked)")
|
||||
focusedThemeId = themeId
|
||||
focusedIndex = themeIdList.indexOf(themeId)
|
||||
confirmSelection() // Directly confirm click selection
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -292,4 +283,132 @@ class ThemeSelector @JvmOverloads constructor(
|
|||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the entire component has focus (from the parent menu).
|
||||
* Controls the outer border visibility.
|
||||
*/
|
||||
fun setHasFocus(hasFocus: Boolean) {
|
||||
hasComponentFocus = hasFocus
|
||||
// Update visual state based on component focus
|
||||
highlightFocusedCard() // Re-apply highlights
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the internal focus to the next available theme.
|
||||
*/
|
||||
fun focusNextItem() {
|
||||
if (!hasComponentFocus || themeIdList.isEmpty()) return // Only navigate if component has focus
|
||||
|
||||
focusedIndex = (focusedIndex + 1) % themeIdList.size
|
||||
focusedThemeId = themeIdList[focusedIndex]
|
||||
highlightFocusedCard()
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the internal focus to the previous available theme.
|
||||
*/
|
||||
fun focusPreviousItem() {
|
||||
if (!hasComponentFocus || themeIdList.isEmpty()) return // Only navigate if component has focus
|
||||
|
||||
focusedIndex = (focusedIndex - 1 + themeIdList.size) % themeIdList.size
|
||||
focusedThemeId = themeIdList[focusedIndex]
|
||||
highlightFocusedCard()
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirms the currently focused theme as the selected theme.
|
||||
* Triggers the onThemeSelected callback.
|
||||
*/
|
||||
fun confirmSelection() {
|
||||
Log.d("ThemeSelector", "confirmSelection called. Focused theme: $focusedThemeId")
|
||||
if (focusedThemeId == null || focusedThemeId == selectedTheme) {
|
||||
// No change needed if nothing is focused,
|
||||
// or the focused item is already selected
|
||||
return
|
||||
}
|
||||
|
||||
// Update the selected theme
|
||||
selectedTheme = focusedThemeId!!
|
||||
|
||||
// Update visual states for all cards
|
||||
highlightFocusedCard() // This will now mark the new theme as selected
|
||||
|
||||
// Trigger the callback
|
||||
onThemeSelected?.invoke(selectedTheme)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the visual highlight state of the theme cards based on
|
||||
* selection and internal focus.
|
||||
*/
|
||||
private fun highlightFocusedCard() {
|
||||
if (themeCards.isEmpty()) return
|
||||
|
||||
val focusColor = Color.YELLOW // Color for the focused-but-not-selected item
|
||||
val selectedColor = Color.WHITE // Color for the selected item (might be focused or not)
|
||||
|
||||
themeCards.forEach { (themeId, card) ->
|
||||
val themeInfo = getThemes()[themeId] ?: return@forEach
|
||||
val isUnlocked = themeIdList.contains(themeId) // Check if it's in the navigable list
|
||||
|
||||
if (!isUnlocked) {
|
||||
// Keep locked themes visually distinct
|
||||
card.alpha = 0.5f
|
||||
card.cardElevation = 2f
|
||||
card.background = null // Remove any border/background
|
||||
card.setCardBackgroundColor(themeInfo.primaryColor)
|
||||
return@forEach // Skip further styling for locked themes
|
||||
}
|
||||
|
||||
// Reset unlocked cards first
|
||||
card.alpha = 1.0f
|
||||
card.cardElevation = 4f // Default elevation for unlocked cards
|
||||
card.background = null
|
||||
card.setCardBackgroundColor(themeInfo.primaryColor)
|
||||
card.scaleX = 1.0f
|
||||
card.scaleY = 1.0f
|
||||
|
||||
val isSelected = (themeId == selectedTheme)
|
||||
val isFocused = (hasComponentFocus && themeId == focusedThemeId)
|
||||
|
||||
var borderColor = Color.TRANSPARENT
|
||||
var borderWidth = 0
|
||||
var elevation = 4f
|
||||
var scale = 1.0f
|
||||
|
||||
if (isSelected) {
|
||||
borderColor = selectedColor
|
||||
borderWidth = 6 // Thick border for selected
|
||||
elevation = 12f // Higher elevation for selected
|
||||
}
|
||||
|
||||
if (isFocused) {
|
||||
// Focused item gets a distinct border (unless it's also selected)
|
||||
if (!isSelected) {
|
||||
borderColor = focusColor
|
||||
borderWidth = 4 // Slightly thinner border for focused
|
||||
}
|
||||
elevation = 12f // Use high elevation for focus too
|
||||
scale = 1.1f // Scale up the focused item
|
||||
}
|
||||
|
||||
// Apply scale
|
||||
card.scaleX = scale
|
||||
card.scaleY = scale
|
||||
|
||||
// Apply border and elevation
|
||||
if (borderWidth > 0) {
|
||||
val gradientDrawable = GradientDrawable().apply {
|
||||
setColor(themeInfo.primaryColor)
|
||||
setStroke(borderWidth, borderColor)
|
||||
cornerRadius = 12f // Keep consistent corner radius
|
||||
}
|
||||
card.background = gradientDrawable
|
||||
} else {
|
||||
card.background = null // Ensure no border if not selected/focused
|
||||
}
|
||||
card.cardElevation = elevation
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package com.mintris.ui
|
||||
package com.pixelmintdrop.ui
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
|
@ -9,8 +9,6 @@ import android.graphics.RectF
|
|||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.mintris.R
|
||||
|
||||
/**
|
||||
* Custom progress bar for displaying player XP with animation capabilities
|
||||
|
@ -99,6 +97,8 @@ class XPProgressBar @JvmOverloads constructor(
|
|||
*/
|
||||
fun setThemeColor(color: Int) {
|
||||
themeColor = color
|
||||
progressPaint.color = color
|
||||
textPaint.color = color
|
||||
levelBadgePaint.color = color
|
||||
invalidate()
|
||||
}
|
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>
|
|
@ -5,7 +5,7 @@
|
|||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
|
||||
<!-- T-Tetromino -->
|
||||
<!-- T-piece -->
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M36,36h12v12h-12z" />
|
||||
|
@ -19,7 +19,7 @@
|
|||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M48,48h12v12h-12z" />
|
||||
|
||||
<!-- L-Tetromino -->
|
||||
<!-- L-piece -->
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M36,60h12v12h-12z" />
|
||||
|
|
11
app/src/main/res/drawable/ic_leaderboard.xml
Normal file
11
app/src/main/res/drawable/ic_leaderboard.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M7.5,21H2V9h5.5V21zM14.75,3h-5.5v18h5.5V3zM22,11h-5.5v10H22V11z"/>
|
||||
</vector>
|
11
app/src/main/res/drawable/ic_play.xml
Normal file
11
app/src/main/res/drawable/ic_play.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M8,5v14l11,-7z"/>
|
||||
</vector>
|
9
app/src/main/res/drawable/menu_item_background.xml
Normal file
9
app/src/main/res/drawable/menu_item_background.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">
|
||||
<corners android:radius="8dp" />
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="#33FFFFFF" />
|
||||
<solid android:color="#1AFFFFFF" />
|
||||
</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>
|
6
app/src/main/res/drawable/menu_section_background.xml
Normal file
6
app/src/main/res/drawable/menu_section_background.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="#80000000" />
|
||||
<corners android:radius="16dp" />
|
||||
</shape>
|
8
app/src/main/res/drawable/preview_border.xml
Normal file
8
app/src/main/res/drawable/preview_border.xml
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?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="#FFFFFF" />
|
||||
<solid android:color="#00000000" />
|
||||
</shape>
|
750
app/src/main/res/layout-land/activity_main.xml
Normal file
750
app/src/main/res/layout-land/activity_main.xml
Normal file
|
@ -0,0 +1,750 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black"
|
||||
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="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="1:2"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.pixelmintdrop.game.GameView
|
||||
android:id="@+id/gameView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<!-- Glowing Border -->
|
||||
<View
|
||||
android:id="@+id/glowBorder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/glow_border"
|
||||
android:clickable="false"
|
||||
android:focusable="false" />
|
||||
</FrameLayout>
|
||||
|
||||
<!-- Left Side Controls Panel - Overlay -->
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/leftControlsPanel"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintWidth_percent="0.25"
|
||||
android:clickable="false"
|
||||
android:focusable="false"
|
||||
android:elevation="1dp">
|
||||
|
||||
<!-- Hold Piece View -->
|
||||
<TextView
|
||||
android:id="@+id/holdLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="HOLD"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="24dp"/>
|
||||
|
||||
<com.pixelmintdrop.game.HoldPieceView
|
||||
android:id="@+id/holdPieceView"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="80dp"
|
||||
android:layout_marginTop="8dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/holdLabel" />
|
||||
|
||||
<!-- Pause Button -->
|
||||
<ImageButton
|
||||
android:id="@+id/pauseButton"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/settings"
|
||||
android:padding="12dp"
|
||||
android:src="@drawable/ic_pause"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginBottom="24dp" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<!-- Right Side Controls Panel - Overlay -->
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/rightControlsPanel"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintWidth_percent="0.25"
|
||||
android:clickable="false"
|
||||
android:focusable="false"
|
||||
android:elevation="1dp">
|
||||
|
||||
<!-- Next Piece Preview -->
|
||||
<TextView
|
||||
android:id="@+id/nextLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="NEXT"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="24dp"/>
|
||||
|
||||
<com.pixelmintdrop.game.NextPieceView
|
||||
android:id="@+id/nextPieceView"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="80dp"
|
||||
android:layout_marginTop="8dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/nextLabel" />
|
||||
|
||||
<!-- HUD Container - Score, Level, Lines -->
|
||||
<LinearLayout
|
||||
android:id="@+id/hudContainer"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/nextPieceView">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/scoreText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
tools:text="score: 0" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/currentLevelText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
tools:text="level: 1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/linesText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
tools:text="lines: 0" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/comboText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
tools:text="combo: 0" />
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<!-- Title Screen -->
|
||||
<com.pixelmintdrop.game.TitleScreen
|
||||
android:id="@+id/titleScreen"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<!-- Progression Screen -->
|
||||
<com.pixelmintdrop.ui.ProgressionScreen
|
||||
android:id="@+id/progressionScreen"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<!-- Theme Selector -->
|
||||
<com.pixelmintdrop.ui.ThemeSelector
|
||||
android:id="@+id/themeSelector"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<!-- Block Skin Selector -->
|
||||
<com.pixelmintdrop.ui.BlockSkinSelector
|
||||
android:id="@+id/blockSkinSelector"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<!-- Pause Container -->
|
||||
<LinearLayout
|
||||
android:id="@+id/pauseContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<!-- Scrollable content for pause menu -->
|
||||
<ScrollView
|
||||
android:id="@+id/pauseMenuScrollView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scrollbars="none"
|
||||
android:overScrollMode="never"
|
||||
android:fillViewport="true">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<Button
|
||||
android:id="@+id/pauseStartButton"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/transparent"
|
||||
android:text="@string/start"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
android:textAllCaps="false" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/resumeButton"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@color/transparent"
|
||||
android:text="@string/resume"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
android:textAllCaps="false" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/pauseRestartButton"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@color/transparent"
|
||||
android:text="@string/restart"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
android:textAllCaps="false" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/highScoresButton"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@color/transparent"
|
||||
android:text="@string/high_scores"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
android:textAllCaps="false" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/statsButton"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@color/transparent"
|
||||
android:text="@string/stats"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
android:textAllCaps="false" />
|
||||
|
||||
<!-- Level Selection -->
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:layout_marginTop="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/selectLevelText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/select_level"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="24sp"
|
||||
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:layout_marginTop="8dp">
|
||||
|
||||
<Button
|
||||
android:id="@+id/pauseLevelDownButton"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="@color/transparent"
|
||||
android:text="-"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pauseLevelText"
|
||||
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:layout_marginHorizontal="16dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/pauseLevelUpButton"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="@color/transparent"
|
||||
android:text="+"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Sound Toggle -->
|
||||
<Button
|
||||
android:id="@+id/settingsButton"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@color/transparent"
|
||||
android:text="@string/sound_on"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
android:textAllCaps="false" />
|
||||
|
||||
<!-- Music Toggle -->
|
||||
<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="24sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:textAllCaps="false" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/musicToggle"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="@color/transparent"
|
||||
android:src="@drawable/ic_volume_up"
|
||||
android:contentDescription="@string/music" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Customization Button -->
|
||||
<Button
|
||||
android:id="@+id/customizationButton"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@color/transparent"
|
||||
android:text="@string/customization"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
android:textAllCaps="false"
|
||||
android:singleLine="true"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Game Over overlay -->
|
||||
<LinearLayout
|
||||
android:id="@+id/gameOverContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/gameOverText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/game_over"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="36sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/session_stats"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="28sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
android:layout_marginTop="24dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginTop="8dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginEnd="24dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sessionScoreText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sessionLinesText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sessionPiecesText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sessionTimeText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sessionLevelText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/line_clears"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="28sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
android:layout_marginTop="16dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginEnd="24dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sessionSinglesText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sessionDoublesText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sessionTriplesText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sessionQuadsText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/playAgainButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:text="@string/play"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- This linear layout is hidden in landscape mode as we're using the panels instead -->
|
||||
<LinearLayout
|
||||
android:id="@+id/gameControlsContainer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Customization Menu overlay -->
|
||||
<LinearLayout
|
||||
android:id="@+id/customizationContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center"
|
||||
android:layout_marginTop="32dp"
|
||||
android:layout_marginBottom="32dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/customizationTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/customization"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:textAllCaps="false"
|
||||
android:singleLine="true" />
|
||||
|
||||
<com.pixelmintdrop.ui.LevelBadge
|
||||
android:id="@+id/customizationLevelBadge"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/customizationMenuScrollView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:scrollbars="none"
|
||||
android:overScrollMode="never"
|
||||
android:fillViewport="true"
|
||||
android:paddingBottom="32dp"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="32dp">
|
||||
|
||||
<!-- Theme Selector -->
|
||||
<com.pixelmintdrop.ui.ThemeSelector
|
||||
android:id="@+id/customizationThemeSelector"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true" />
|
||||
|
||||
<!-- Block Skin Selector -->
|
||||
<com.pixelmintdrop.ui.BlockSkinSelector
|
||||
android:id="@+id/customizationBlockSkinSelector"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true" />
|
||||
|
||||
<!-- Random Mode Toggle -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:padding="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:background="@drawable/menu_section_background"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/random_mode"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif" />
|
||||
|
||||
<Switch
|
||||
android:id="@+id/randomModeSwitch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Back Button -->
|
||||
<Button
|
||||
android:id="@+id/customizationBackButton"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
android:background="@color/transparent"
|
||||
android:text="@string/back"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
android:textAllCaps="false"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
194
app/src/main/res/layout-land/activity_stats.xml
Normal file
194
app/src/main/res/layout-land/activity_stats.xml
Normal file
|
@ -0,0 +1,194 @@
|
|||
<?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="match_parent"
|
||||
android:background="@color/black"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/lifetime_stats"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="28sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginTop="16dp"/>
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="16dp">
|
||||
|
||||
<!-- Left Column -->
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/general_stats"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="16dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/totalGamesText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp"
|
||||
android:layout_marginBottom="8dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/totalScoreText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp"
|
||||
android:layout_marginBottom="8dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/totalLinesText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp"
|
||||
android:layout_marginBottom="8dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/totalPiecesText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp"
|
||||
android:layout_marginBottom="8dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/totalTimeText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp"
|
||||
android:layout_marginBottom="24dp"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/line_clears"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="16dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/totalSinglesText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp"
|
||||
android:layout_marginBottom="4dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/totalDoublesText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp"
|
||||
android:layout_marginBottom="4dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/totalTriplesText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp"
|
||||
android:layout_marginBottom="4dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/totalQuadsText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp"/>
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Right Column -->
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/best_performance"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="16dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/maxLevelText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp"
|
||||
android:layout_marginBottom="8dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/maxScoreText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp"
|
||||
android:layout_marginBottom="8dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/maxLinesText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center"
|
||||
android:padding="8dp">
|
||||
|
||||
<Button
|
||||
android:id="@+id/backButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/back"
|
||||
android:textColor="@color/white"
|
||||
android:background="@color/transparent"
|
||||
android:layout_marginEnd="16dp"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/resetStatsButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/reset_stats"
|
||||
android:textColor="@color/white"
|
||||
android:background="@color/transparent"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
51
app/src/main/res/layout-land/high_score_entry.xml
Normal file
51
app/src/main/res/layout-land/high_score_entry.xml
Normal file
|
@ -0,0 +1,51 @@
|
|||
<?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="match_parent"
|
||||
android:background="@color/black"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/new_high_score"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="28sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="monospace"
|
||||
android:layout_marginBottom="24dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/scoreText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="24sp"
|
||||
android:fontFamily="monospace"
|
||||
android:layout_marginBottom="24dp"/>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/nameInput"
|
||||
android:layout_width="280dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/edit_text_background"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="22sp"
|
||||
android:fontFamily="monospace"
|
||||
android:gravity="center"
|
||||
android:inputType="text"
|
||||
android:maxLength="10"
|
||||
android:layout_marginBottom="24dp"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/saveButton"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/transparent"
|
||||
android:text="@string/save"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="20sp"
|
||||
android:fontFamily="monospace"/>
|
||||
</LinearLayout>
|
38
app/src/main/res/layout-land/high_scores.xml
Normal file
38
app/src/main/res/layout-land/high_scores.xml
Normal file
|
@ -0,0 +1,38 @@
|
|||
<?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="match_parent"
|
||||
android:background="@color/black"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/high_scores"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="28sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="monospace"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginBottom="16dp"/>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/highScoresList"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginHorizontal="64dp"
|
||||
android:layout_marginBottom="16dp"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/backButton"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:background="@color/transparent"
|
||||
android:text="@string/back"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp"
|
||||
android:fontFamily="monospace"/>
|
||||
</LinearLayout>
|
|
@ -19,7 +19,7 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.mintris.game.GameView
|
||||
<com.pixelmintdrop.game.GameView
|
||||
android:id="@+id/gameView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
@ -33,7 +33,7 @@
|
|||
</FrameLayout>
|
||||
|
||||
<!-- Title Screen -->
|
||||
<com.mintris.game.TitleScreen
|
||||
<com.pixelmintdrop.game.TitleScreen
|
||||
android:id="@+id/titleScreen"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
|
@ -62,45 +62,65 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="24sp"
|
||||
android:fontFamily="sans-serif-light"
|
||||
tools:text="Score: 0" />
|
||||
android:textSize="32sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
tools:text="score: 0" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/currentLevelText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="24sp"
|
||||
android:fontFamily="sans-serif-light"
|
||||
tools:text="Level: 1" />
|
||||
android:textSize="32sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
tools:text="level: 1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/linesText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="24sp"
|
||||
android:fontFamily="sans-serif-light"
|
||||
tools:text="Lines: 0" />
|
||||
android:textSize="32sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
tools:text="lines: 0" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/comboText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="24sp"
|
||||
android:fontFamily="sans-serif-light"
|
||||
tools:text="Combo: 0" />
|
||||
android:textSize="32sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
tools:text="combo: 0" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Next Piece Preview -->
|
||||
<com.mintris.game.NextPieceView
|
||||
android:id="@+id/nextPieceView"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="80dp"
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:layout_gravity="end" />
|
||||
android:layout_gravity="end"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="NEXT"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
android:layout_marginBottom="4dp" />
|
||||
|
||||
<com.pixelmintdrop.game.NextPieceView
|
||||
android:id="@+id/nextPieceView"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="80dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Settings button -->
|
||||
<ImageButton
|
||||
|
@ -115,6 +135,16 @@
|
|||
android:src="@drawable/ic_pause" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Hold Piece Preview -->
|
||||
<com.pixelmintdrop.game.HoldPieceView
|
||||
android:id="@+id/holdPieceView"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="60dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<!-- Game Over overlay -->
|
||||
<LinearLayout
|
||||
android:id="@+id/gameOverContainer"
|
||||
|
@ -131,16 +161,18 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:text="@string/game_over"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold" />
|
||||
android:textSize="36sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/session_stats"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="20sp"
|
||||
android:textSize="28sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
android:layout_marginTop="24dp" />
|
||||
|
||||
<TextView
|
||||
|
@ -149,7 +181,9 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp" />
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sessionLinesText"
|
||||
|
@ -157,7 +191,9 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp" />
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sessionPiecesText"
|
||||
|
@ -165,7 +201,9 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp" />
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sessionTimeText"
|
||||
|
@ -173,7 +211,9 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp" />
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sessionLevelText"
|
||||
|
@ -181,15 +221,18 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp" />
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/line_clears"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="20sp"
|
||||
android:textSize="28sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
android:layout_marginTop="16dp" />
|
||||
|
||||
<TextView
|
||||
|
@ -198,7 +241,9 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp" />
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sessionDoublesText"
|
||||
|
@ -206,7 +251,9 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp" />
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sessionTriplesText"
|
||||
|
@ -214,15 +261,19 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp" />
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sessionTetrisesText"
|
||||
android:id="@+id/sessionQuadsText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp" />
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/playAgainButton"
|
||||
|
@ -231,11 +282,14 @@
|
|||
android:layout_marginTop="32dp"
|
||||
android:background="@color/transparent"
|
||||
android:text="@string/play"
|
||||
android:textColor="@color/white" />
|
||||
android:textColor="@color/white"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Player Progression Screen -->
|
||||
<com.mintris.ui.ProgressionScreen
|
||||
<com.pixelmintdrop.ui.ProgressionScreen
|
||||
android:id="@+id/progressionScreen"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
@ -251,7 +305,6 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
|
@ -260,6 +313,7 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center"
|
||||
android:layout_marginTop="32dp"
|
||||
android:layout_marginBottom="32dp">
|
||||
|
||||
<TextView
|
||||
|
@ -270,9 +324,11 @@
|
|||
android:textColor="@color/white"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginEnd="16dp" />
|
||||
android:fontFamily="sans-serif"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:textAllCaps="false" />
|
||||
|
||||
<com.mintris.ui.LevelBadge
|
||||
<com.pixelmintdrop.ui.LevelBadge
|
||||
android:id="@+id/pauseLevelBadge"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp" />
|
||||
|
@ -280,15 +336,21 @@
|
|||
</LinearLayout>
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/pauseMenuScrollView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1">
|
||||
android:layout_weight="1"
|
||||
android:scrollbars="none"
|
||||
android:overScrollMode="never"
|
||||
android:fillViewport="false">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center">
|
||||
android:gravity="center"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="32dp">
|
||||
|
||||
<Button
|
||||
android:id="@+id/pauseStartButton"
|
||||
|
@ -297,7 +359,10 @@
|
|||
android:background="@color/transparent"
|
||||
android:text="@string/start"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp" />
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
android:textAllCaps="false" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/resumeButton"
|
||||
|
@ -307,7 +372,10 @@
|
|||
android:background="@color/transparent"
|
||||
android:text="@string/resume"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp" />
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
android:textAllCaps="false" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/pauseRestartButton"
|
||||
|
@ -317,7 +385,10 @@
|
|||
android:background="@color/transparent"
|
||||
android:text="@string/restart"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp" />
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
android:textAllCaps="false" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/highScoresButton"
|
||||
|
@ -327,7 +398,10 @@
|
|||
android:background="@color/transparent"
|
||||
android:text="@string/high_scores"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp" />
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
android:textAllCaps="false" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/statsButton"
|
||||
|
@ -337,15 +411,18 @@
|
|||
android:background="@color/transparent"
|
||||
android:text="@string/stats"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp" />
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
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"
|
||||
|
@ -353,8 +430,10 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:text="@string/select_level"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
android:textAllCaps="false" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -369,7 +448,9 @@
|
|||
android:background="@color/transparent"
|
||||
android:text="−"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="24sp" />
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/pauseLevelText"
|
||||
|
@ -379,7 +460,8 @@
|
|||
android:text="1"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold" />
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/pauseLevelUpButton"
|
||||
|
@ -388,18 +470,13 @@
|
|||
android:background="@color/transparent"
|
||||
android:text="+"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="24sp" />
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Theme Selector -->
|
||||
<com.mintris.ui.ThemeSelector
|
||||
android:id="@+id/themeSelector"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<!-- Sound Toggle -->
|
||||
<Button
|
||||
android:id="@+id/settingsButton"
|
||||
android:layout_width="200dp"
|
||||
|
@ -408,13 +485,17 @@
|
|||
android:background="@color/transparent"
|
||||
android:text="@string/sound_on"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp" />
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
android:textAllCaps="false" />
|
||||
|
||||
<!-- Music Toggle -->
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:gravity="center"
|
||||
android:layout_marginTop="16dp">
|
||||
|
||||
<TextView
|
||||
|
@ -423,18 +504,151 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:text="@string/music"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp"
|
||||
android:layout_marginEnd="16dp" />
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:textAllCaps="false" />
|
||||
|
||||
<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" />
|
||||
android:background="@color/transparent"
|
||||
android:src="@drawable/ic_volume_up"
|
||||
android:contentDescription="@string/music" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Customization Button -->
|
||||
<Button
|
||||
android:id="@+id/customizationButton"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@color/transparent"
|
||||
android:text="@string/customization"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
android:textAllCaps="false"
|
||||
android:singleLine="true" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Customization Menu overlay -->
|
||||
<LinearLayout
|
||||
android:id="@+id/customizationContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/black"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center"
|
||||
android:layout_marginTop="32dp"
|
||||
android:layout_marginBottom="32dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/customizationTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/customization"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:textAllCaps="false"
|
||||
android:singleLine="true" />
|
||||
|
||||
<com.pixelmintdrop.ui.LevelBadge
|
||||
android:id="@+id/customizationLevelBadge"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/customizationMenuScrollView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:scrollbars="none"
|
||||
android:overScrollMode="never"
|
||||
android:fillViewport="false">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="32dp">
|
||||
|
||||
<!-- Theme Selector -->
|
||||
<com.pixelmintdrop.ui.ThemeSelector
|
||||
android:id="@+id/customizationThemeSelector"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<!-- Block Skin Selector -->
|
||||
<com.pixelmintdrop.ui.BlockSkinSelector
|
||||
android:id="@+id/customizationBlockSkinSelector"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@drawable/menu_item_background"
|
||||
android:padding="16dp" />
|
||||
|
||||
<!-- Random Mode Toggle -->
|
||||
<LinearLayout
|
||||
android:id="@+id/randomModeContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@drawable/menu_item_background"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/random_mode"
|
||||
android:textSize="18sp"
|
||||
android:textColor="@android:color/white" />
|
||||
|
||||
<Switch
|
||||
android:id="@+id/randomModeSwitch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:scaleX="1.5"
|
||||
android:scaleY="1.5"
|
||||
android:thumbTint="@color/switch_thumb_color"
|
||||
android:trackTint="@color/switch_track_color" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Back Button -->
|
||||
<Button
|
||||
android:id="@+id/customizationBackButton"
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
android:background="@color/transparent"
|
||||
android:text="@string/back"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
android:textAllCaps="false" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</LinearLayout>
|
||||
|
|
|
@ -101,7 +101,7 @@
|
|||
android:layout_marginBottom="4dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/totalTetrisesText"
|
||||
android:id="@+id/totalQuadsText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/white"
|
||||
|
|
28
app/src/main/res/layout/block_skin_selector.xml
Normal file
28
app/src/main/res/layout/block_skin_selector.xml
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?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="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/available_skins_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="block skins"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
android:gravity="center"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<GridLayout
|
||||
android:id="@+id/skins_grid"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:columnCount="3"
|
||||
android:alignmentMode="alignMargins"
|
||||
android:useDefaultMargins="true" />
|
||||
|
||||
</LinearLayout>
|
122
app/src/main/res/layout/gamepad_help_dialog.xml
Normal file
122
app/src/main/res/layout/gamepad_help_dialog.xml
Normal file
|
@ -0,0 +1,122 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fillViewport="true"
|
||||
android:scrollbars="none">
|
||||
|
||||
<LinearLayout
|
||||
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"
|
||||
android:layout_marginBottom="4dp"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Start Button: Pause game"
|
||||
android:textSize="16sp"
|
||||
android:textColor="#FFFFFF"
|
||||
android:layout_marginBottom="4dp"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="B Button: Back/Cancel"
|
||||
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>
|
||||
</ScrollView>
|
|
@ -3,16 +3,16 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp"
|
||||
android:background="@color/black">
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/progression_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="LEVEL UP"
|
||||
android:textSize="24sp"
|
||||
android:text="level up"
|
||||
android:textSize="48sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
android:textColor="@color/white"
|
||||
android:gravity="center"
|
||||
android:layout_marginTop="16dp"
|
||||
|
@ -22,13 +22,15 @@
|
|||
android:id="@+id/player_level_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Player Level: 1"
|
||||
android:textSize="20sp"
|
||||
android:text="player level: 1"
|
||||
android:textSize="36sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
android:textColor="@color/white"
|
||||
android:gravity="center"
|
||||
android:layout_marginBottom="24dp" />
|
||||
|
||||
<com.mintris.ui.XPProgressBar
|
||||
<com.pixelmintdrop.ui.XPProgressBar
|
||||
android:id="@+id/xp_progress_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="50dp"
|
||||
|
@ -38,10 +40,11 @@
|
|||
android:id="@+id/xp_gain_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="+0 XP"
|
||||
android:textSize="22sp"
|
||||
android:textColor="#50C878"
|
||||
android:text="+0 xp"
|
||||
android:textSize="42sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
android:textColor="#50C878"
|
||||
android:gravity="center"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="24dp" />
|
||||
|
@ -50,9 +53,10 @@
|
|||
android:id="@+id/rewards_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="REWARDS UNLOCKED"
|
||||
android:textSize="20sp"
|
||||
android:text="rewards unlocked"
|
||||
android:textSize="36sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
android:textColor="#FFD700"
|
||||
android:gravity="center"
|
||||
android:layout_marginTop="8dp"
|
||||
|
@ -76,8 +80,10 @@
|
|||
android:id="@+id/continue_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="CONTINUE"
|
||||
android:textSize="18sp"
|
||||
android:text="continue"
|
||||
android:textSize="32sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
|
|
|
@ -9,10 +9,11 @@
|
|||
android:id="@+id/available_themes_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="AVAILABLE THEMES"
|
||||
android:text="available themes"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="18sp"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
android:gravity="center"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
|
|
BIN
app/src/main/res/raw/game_over.mp3
Normal file
BIN
app/src/main/res/raw/game_over.mp3
Normal file
Binary file not shown.
|
@ -1,8 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="black">#000000</color>
|
||||
<color name="white">#FFFFFF</color>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
<color name="gray_dark">#222222</color>
|
||||
<color name="gray_light">#CCCCCC</color>
|
||||
<color name="transparent">#00000000</color>
|
||||
<color name="switch_track_color">#33FFFFFF</color>
|
||||
<color name="switch_thumb_color">#FFFFFF</color>
|
||||
</resources>
|
|
@ -1,49 +1,69 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Mintris</string>
|
||||
<string name="game_over">Game Over</string>
|
||||
<string name="score">Score</string>
|
||||
<string name="level">Level</string>
|
||||
<string name="lines">Lines</string>
|
||||
<string name="next">Next</string>
|
||||
<string name="play">Play Again</string>
|
||||
<string name="resume">Resume</string>
|
||||
<string name="pause">PAUSE</string>
|
||||
<string name="settings">Settings</string>
|
||||
<string name="start">Start Game</string>
|
||||
<string name="restart">Restart</string>
|
||||
<string name="select_level">Select Level</string>
|
||||
<string name="sound_on">Sound: On</string>
|
||||
<string name="sound_off">Sound: Off</string>
|
||||
<string name="toggle_music">Toggle music</string>
|
||||
<string name="high_scores">High Scores</string>
|
||||
<string name="new_high_score">New High Score!</string>
|
||||
<string name="save">Save</string>
|
||||
<string name="back">Back</string>
|
||||
<string name="app_name">Pixel Mint Drop</string>
|
||||
<string name="game_over">game over</string>
|
||||
<string name="score">score</string>
|
||||
<string name="level">level</string>
|
||||
<string name="lines">lines</string>
|
||||
<string name="next">next</string>
|
||||
<string name="play">play again</string>
|
||||
<string name="resume">resume</string>
|
||||
<string name="pause">pause</string>
|
||||
<string name="settings">settings</string>
|
||||
<string name="start">start game</string>
|
||||
<string name="restart">restart</string>
|
||||
<string name="select_level">select level</string>
|
||||
<string name="sound_on">sound: on</string>
|
||||
<string name="sound_off">sound: off</string>
|
||||
<string name="toggle_music">toggle music</string>
|
||||
<string name="high_scores">high scores</string>
|
||||
<string name="new_high_score">new high score!</string>
|
||||
<string name="save">save</string>
|
||||
<string name="back">back</string>
|
||||
|
||||
<!-- Stats Screen -->
|
||||
<string name="lifetime_stats">Lifetime Stats</string>
|
||||
<string name="best_performance">Best Performance</string>
|
||||
<string name="total_games">Total Games: %d</string>
|
||||
<string name="total_score">Total Score: %d</string>
|
||||
<string name="total_lines">Total Lines: %d</string>
|
||||
<string name="total_pieces">Total Pieces: %d</string>
|
||||
<string name="total_time">Total Time: %s</string>
|
||||
<string name="max_level">Max Level: %d</string>
|
||||
<string name="max_score">Max Score: %d</string>
|
||||
<string name="max_lines">Max Lines: %d</string>
|
||||
<string name="stats">Stats</string>
|
||||
<string name="session_stats">Session Stats</string>
|
||||
<string name="session_score">Score: %d</string>
|
||||
<string name="session_lines">Lines: %d</string>
|
||||
<string name="session_pieces">Pieces: %d</string>
|
||||
<string name="session_time">Time: %s</string>
|
||||
<string name="session_level">Level: %d</string>
|
||||
<string name="line_clears">Line Clears</string>
|
||||
<string name="singles">Singles: %d</string>
|
||||
<string name="doubles">Doubles: %d</string>
|
||||
<string name="triples">Triples: %d</string>
|
||||
<string name="tetrises">Tetrises: %d</string>
|
||||
<string name="reset_stats">Reset Stats</string>
|
||||
<string name="music">Music</string>
|
||||
<string name="lifetime_stats">lifetime stats</string>
|
||||
<string name="general_stats">general stats</string>
|
||||
<string name="best_performance">best performance</string>
|
||||
<string name="total_games">total games: %d</string>
|
||||
<string name="total_score">total score: %d</string>
|
||||
<string name="total_lines">total lines: %d</string>
|
||||
<string name="total_pieces">total pieces: %d</string>
|
||||
<string name="total_time">total time: %s</string>
|
||||
<string name="max_level">max level: %d</string>
|
||||
<string name="max_score">max score: %d</string>
|
||||
<string name="max_lines">max lines: %d</string>
|
||||
<string name="stats">stats</string>
|
||||
<string name="session_stats">session stats</string>
|
||||
<string name="session_score">score: %d</string>
|
||||
<string name="session_lines">lines: %d</string>
|
||||
<string name="session_pieces">pieces: %d</string>
|
||||
<string name="session_time">time: %s</string>
|
||||
<string name="session_level">level: %d</string>
|
||||
<string name="line_clears">line clears</string>
|
||||
<string name="singles">singles: %d</string>
|
||||
<string name="doubles">doubles: %d</string>
|
||||
<string name="triples">triples: %d</string>
|
||||
<string name="quads">quads: %d</string>
|
||||
<string name="reset_stats">reset stats</string>
|
||||
<string name="music">music</string>
|
||||
<string name="customization">Customization</string>
|
||||
<string name="random_mode">Random Mode</string>
|
||||
|
||||
<!-- Shortcuts -->
|
||||
<string name="shortcut_new_game_short_label">Play</string>
|
||||
<string name="shortcut_new_game_long_label">Start New Game</string>
|
||||
<string name="shortcut_high_scores_short_label">Scores</string>
|
||||
<string name="shortcut_high_scores_long_label">View High Scores</string>
|
||||
|
||||
<!-- Accessibility -->
|
||||
<string name="accessibility_rotate_piece">Rotate piece</string>
|
||||
<string name="accessibility_move_left">Move piece left</string>
|
||||
<string name="accessibility_move_right">Move piece right</string>
|
||||
<string name="accessibility_drop_piece">Drop piece</string>
|
||||
<string name="accessibility_hold_piece">Hold current piece</string>
|
||||
<string name="accessibility_game_board">Game board</string>
|
||||
<string name="accessibility_next_piece">Next piece preview</string>
|
||||
<string name="accessibility_held_piece">Held piece</string>
|
||||
<string name="accessibility_pause_game">Pause game</string>
|
||||
</resources>
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Base application theme -->
|
||||
<style name="Theme.Mintris" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<style name="Theme.pixelmintdrop" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<item name="colorPrimary">@color/white</item>
|
||||
<item name="colorPrimaryDark">@color/black</item>
|
||||
<item name="colorAccent">@color/white</item>
|
||||
|
@ -11,7 +11,7 @@
|
|||
</style>
|
||||
|
||||
<!-- No action bar theme -->
|
||||
<style name="Theme.Mintris.NoActionBar">
|
||||
<style name="Theme.pixelmintdrop.NoActionBar">
|
||||
<item name="windowActionBar">false</item>
|
||||
<item name="windowNoTitle">true</item>
|
||||
<item name="android:windowFullscreen">true</item>
|
||||
|
|
13
app/src/main/res/xml/backup_rules.xml
Normal file
13
app/src/main/res/xml/backup_rules.xml
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<full-backup-content>
|
||||
<!-- Backup all shared preferences -->
|
||||
<include domain="sharedpref" path="."/>
|
||||
|
||||
<!-- Backup specific files -->
|
||||
<include domain="file" path="highscores.json"/>
|
||||
<include domain="file" path="stats.json"/>
|
||||
<include domain="file" path="progression.json"/>
|
||||
|
||||
<!-- Exclude any sensitive information if present -->
|
||||
<exclude domain="sharedpref" path="sensitive_data.xml"/>
|
||||
</full-backup-content>
|
30
app/src/main/res/xml/shortcuts.xml
Normal file
30
app/src/main/res/xml/shortcuts.xml
Normal file
|
@ -0,0 +1,30 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<shortcut
|
||||
android:shortcutId="new_game"
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/ic_play"
|
||||
android:shortcutShortLabel="@string/shortcut_new_game_short_label"
|
||||
android:shortcutLongLabel="@string/shortcut_new_game_long_label">
|
||||
<intent
|
||||
android:action="android.intent.action.VIEW"
|
||||
android:targetPackage="com.pixelmintdrop"
|
||||
android:targetClass="com.pixelmintdrop.MainActivity">
|
||||
<extra android:name="action" android:value="new_game" />
|
||||
</intent>
|
||||
<categories android:name="android.shortcut.conversation" />
|
||||
</shortcut>
|
||||
|
||||
<shortcut
|
||||
android:shortcutId="high_scores"
|
||||
android:enabled="true"
|
||||
android:icon="@drawable/ic_leaderboard"
|
||||
android:shortcutShortLabel="@string/shortcut_high_scores_short_label"
|
||||
android:shortcutLongLabel="@string/shortcut_high_scores_long_label">
|
||||
<intent
|
||||
android:action="android.intent.action.VIEW"
|
||||
android:targetPackage="com.pixelmintdrop"
|
||||
android:targetClass="com.pixelmintdrop.HighScoresActivity" />
|
||||
<categories android:name="android.shortcut.conversation" />
|
||||
</shortcut>
|
||||
</shortcuts>
|
|
@ -1,2 +1,2 @@
|
|||
rootProject.name = "Mintris"
|
||||
rootProject.name = "pixelmintdrop"
|
||||
include ':app'
|
12
tatus
12
tatus
|
@ -1,8 +1,8 @@
|
|||
app/src/main/java/com/mintris/MainActivity.kt
|
||||
app/src/main/java/com/mintris/audio/GameMusic.kt
|
||||
app/src/main/java/com/mintris/model/PlayerProgressionManager.kt
|
||||
app/src/main/java/com/mintris/ui/ProgressionScreen.kt
|
||||
app/src/main/java/com/mintris/ui/ThemeSelector.kt
|
||||
app/src/main/java/com/mintris/ui/XPProgressBar.kt
|
||||
app/src/main/java/com/pixelmintdrop/MainActivity.kt
|
||||
app/src/main/java/com/pixelmintdrop/audio/GameMusic.kt
|
||||
app/src/main/java/com/pixelmintdrop/model/PlayerProgressionManager.kt
|
||||
app/src/main/java/com/pixelmintdrop/ui/ProgressionScreen.kt
|
||||
app/src/main/java/com/pixelmintdrop/ui/ThemeSelector.kt
|
||||
app/src/main/java/com/pixelmintdrop/ui/XPProgressBar.kt
|
||||
app/src/main/res/drawable/rounded_button.xml
|
||||
app/src/main/res/layout/progression_screen.xml
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue