mirror of
https://github.com/cmclark00/mintris.git
synced 2025-05-18 20:05:20 +01:00
Compare commits
No commits in common. "main" and "0.2" have entirely different histories.
65 changed files with 2898 additions and 7917 deletions
131
README.md
131
README.md
|
@ -1,11 +1,11 @@
|
||||||
# pixelmintdrop
|
# Mintris
|
||||||
|
|
||||||
A modern block-stacking puzzle game for Android, featuring smooth animations, responsive controls, and a beautiful minimalist design.
|
A modern Tetris implementation for Android, featuring smooth animations, responsive controls, and a beautiful minimalist design.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
### Core Gameplay
|
### Core Gameplay
|
||||||
- Classic block-stacking mechanics
|
- Classic Tetris mechanics
|
||||||
- 7-bag randomizer for piece distribution
|
- 7-bag randomizer for piece distribution
|
||||||
- Ghost piece preview
|
- Ghost piece preview
|
||||||
- Hard drop and soft drop
|
- Hard drop and soft drop
|
||||||
|
@ -29,7 +29,7 @@ The game features a comprehensive scoring system:
|
||||||
- Single line: 40 points
|
- Single line: 40 points
|
||||||
- Double: 100 points
|
- Double: 100 points
|
||||||
- Triple: 300 points
|
- Triple: 300 points
|
||||||
- Quad (4 lines): 1200 points
|
- Tetris (4 lines): 1200 points
|
||||||
|
|
||||||
#### Multipliers
|
#### Multipliers
|
||||||
|
|
||||||
|
@ -48,15 +48,15 @@ The game features a comprehensive scoring system:
|
||||||
- 4 combos: 2.5x
|
- 4 combos: 2.5x
|
||||||
- 5+ combos: 3.0x
|
- 5+ combos: 3.0x
|
||||||
|
|
||||||
3. **Back-to-Back Quad**
|
3. **Back-to-Back Tetris**
|
||||||
- 50% bonus (1.5x) for consecutive quad clears
|
- 50% bonus (1.5x) for consecutive Tetris clears
|
||||||
- Resets if a non-quad clear is performed
|
- Resets if a non-Tetris clear is performed
|
||||||
|
|
||||||
4. **Perfect Clear**
|
4. **Perfect Clear**
|
||||||
- 2x for single line
|
- 2x for single line
|
||||||
- 3x for double
|
- 3x for double
|
||||||
- 4x for triple
|
- 4x for triple
|
||||||
- 5x for quad
|
- 5x for Tetris
|
||||||
- Awarded when clearing lines without leaving blocks
|
- Awarded when clearing lines without leaving blocks
|
||||||
|
|
||||||
5. **All Clear**
|
5. **All Clear**
|
||||||
|
@ -102,124 +102,11 @@ The game features a comprehensive scoring system:
|
||||||
- Follows Material Design guidelines
|
- Follows Material Design guidelines
|
||||||
- Implements high score persistence using SharedPreferences
|
- 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
|
## Building from Source
|
||||||
|
|
||||||
1. Clone the repository:
|
1. Clone the repository:
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/cmclark00/pixelmintdrop.git
|
git clone https://github.com/cmclark00/mintris.git
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Open the project in Android Studio
|
2. Open the project in Android Studio
|
||||||
|
|
|
@ -4,13 +4,13 @@ plugins {
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace "com.pixelmintdrop"
|
namespace 'com.mintris'
|
||||||
compileSdk 35
|
compileSdk 34
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.pixelmintdrop"
|
applicationId "com.mintris"
|
||||||
minSdk 30
|
minSdk 30
|
||||||
targetSdk 35
|
targetSdk 34
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName "1.0"
|
versionName "1.0"
|
||||||
|
|
||||||
|
@ -19,15 +19,13 @@ android {
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
minifyEnabled true
|
minifyEnabled false
|
||||||
shrinkResources true
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding true
|
viewBinding true
|
||||||
dataBinding true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
|
@ -40,15 +38,6 @@ 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 {
|
dependencies {
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
implementation 'androidx.core:core-ktx:1.12.0'
|
implementation 'androidx.core:core-ktx:1.12.0'
|
||||||
|
|
35
app/proguard-rules.pro
vendored
35
app/proguard-rules.pro
vendored
|
@ -6,40 +6,7 @@
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
# Keep models intact
|
# Keep models intact
|
||||||
-keep class com.pixelmintdrop.model.** { *; }
|
-keep class com.mintris.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
|
# Uncomment this to preserve the line number information for
|
||||||
# debugging stack traces.
|
# debugging stack traces.
|
||||||
|
|
|
@ -7,46 +7,38 @@
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.pixelmintdrop">
|
android:theme="@style/Theme.Mintris">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@style/Theme.pixelmintdrop.NoActionBar"
|
android:screenOrientation="portrait"
|
||||||
|
android:theme="@style/Theme.Mintris.NoActionBar"
|
||||||
android:immersive="true"
|
android:immersive="true"
|
||||||
android:resizeableActivity="false"
|
android:resizeableActivity="false"
|
||||||
android:excludeFromRecents="false"
|
android:excludeFromRecents="false">
|
||||||
android:screenOrientation="portrait"
|
|
||||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<meta-data
|
|
||||||
android:name="android.app.shortcuts"
|
|
||||||
android:resource="@xml/shortcuts" />
|
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".HighScoreEntryActivity"
|
android:name=".HighScoreEntryActivity"
|
||||||
android:theme="@style/Theme.AppCompat.NoActionBar"
|
android:theme="@style/Theme.AppCompat.NoActionBar"
|
||||||
android:exported="false"
|
android:exported="false" />
|
||||||
android:screenOrientation="portrait" />
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".HighScoresActivity"
|
android:name=".HighScoresActivity"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:screenOrientation="portrait"
|
|
||||||
android:theme="@style/Theme.AppCompat.NoActionBar" />
|
android:theme="@style/Theme.AppCompat.NoActionBar" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".StatsActivity"
|
android:name=".StatsActivity"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:screenOrientation="portrait"
|
|
||||||
android:theme="@style/Theme.AppCompat.NoActionBar" />
|
android:theme="@style/Theme.AppCompat.NoActionBar" />
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
|
@ -1,15 +1,17 @@
|
||||||
package com.pixelmintdrop
|
package com.mintris
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.os.Bundle
|
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 androidx.appcompat.app.AppCompatActivity
|
||||||
import com.pixelmintdrop.databinding.HighScoreEntryBinding
|
import com.mintris.databinding.HighScoreEntryBinding
|
||||||
import com.pixelmintdrop.model.HighScore
|
import com.mintris.model.HighScore
|
||||||
import com.pixelmintdrop.model.HighScoreManager
|
import com.mintris.model.HighScoreManager
|
||||||
import com.pixelmintdrop.model.PlayerProgressionManager
|
import com.mintris.model.PlayerProgressionManager
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.view.KeyEvent
|
|
||||||
import android.view.InputDevice
|
|
||||||
|
|
||||||
class HighScoreEntryActivity : AppCompatActivity() {
|
class HighScoreEntryActivity : AppCompatActivity() {
|
||||||
private lateinit var binding: HighScoreEntryBinding
|
private lateinit var binding: HighScoreEntryBinding
|
||||||
|
@ -37,11 +39,6 @@ class HighScoreEntryActivity : AppCompatActivity() {
|
||||||
binding.scoreText.text = "Score: $score"
|
binding.scoreText.text = "Score: $score"
|
||||||
|
|
||||||
binding.saveButton.setOnClickListener {
|
binding.saveButton.setOnClickListener {
|
||||||
saveScore()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun saveScore() {
|
|
||||||
// Only allow saving once
|
// Only allow saving once
|
||||||
if (!hasSaved) {
|
if (!hasSaved) {
|
||||||
val name = binding.nameInput.text.toString().trim()
|
val name = binding.nameInput.text.toString().trim()
|
||||||
|
@ -56,36 +53,6 @@ class HighScoreEntryActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Override onKeyDown to handle gamepad buttons
|
|
||||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
|
||||||
// Check if it's a gamepad input
|
|
||||||
if (event != null && isGamepadDevice(event.device)) {
|
|
||||||
when (keyCode) {
|
|
||||||
KeyEvent.KEYCODE_BUTTON_A,
|
|
||||||
KeyEvent.KEYCODE_DPAD_CENTER -> {
|
|
||||||
// A button saves the score
|
|
||||||
saveScore()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
KeyEvent.KEYCODE_BUTTON_B -> {
|
|
||||||
// B button cancels
|
|
||||||
setResult(Activity.RESULT_CANCELED)
|
|
||||||
finish()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.onKeyDown(keyCode, event)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to check if the device is a gamepad
|
|
||||||
private fun isGamepadDevice(device: InputDevice?): Boolean {
|
|
||||||
if (device == null) return false
|
|
||||||
val sources = device.sources
|
|
||||||
return (sources and InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD ||
|
|
||||||
(sources and InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent accidental back button press from causing issues
|
// Prevent accidental back button press from causing issues
|
||||||
|
@ -99,7 +66,8 @@ class HighScoreEntryActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadThemePreference(): String {
|
private fun loadThemePreference(): String {
|
||||||
return progressionManager.getSelectedTheme()
|
val prefs = getSharedPreferences("mintris_settings", MODE_PRIVATE)
|
||||||
|
return prefs.getString("selected_theme", PlayerProgressionManager.THEME_CLASSIC) ?: PlayerProgressionManager.THEME_CLASSIC
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun applyTheme(themeId: String) {
|
private fun applyTheme(themeId: String) {
|
|
@ -1,16 +1,17 @@
|
||||||
package com.pixelmintdrop
|
package com.mintris
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.pixelmintdrop.databinding.HighScoresBinding
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.pixelmintdrop.model.HighScoreAdapter
|
import com.mintris.databinding.HighScoresBinding
|
||||||
import com.pixelmintdrop.model.HighScoreManager
|
import com.mintris.model.HighScoreAdapter
|
||||||
import com.pixelmintdrop.model.PlayerProgressionManager
|
import com.mintris.model.HighScoreManager
|
||||||
|
import com.mintris.model.PlayerProgressionManager
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.KeyEvent
|
|
||||||
import android.view.InputDevice
|
|
||||||
|
|
||||||
class HighScoresActivity : AppCompatActivity() {
|
class HighScoresActivity : AppCompatActivity() {
|
||||||
private lateinit var binding: HighScoresBinding
|
private lateinit var binding: HighScoresBinding
|
||||||
|
@ -57,7 +58,8 @@ class HighScoresActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadThemePreference(): String {
|
private fun loadThemePreference(): String {
|
||||||
return progressionManager.getSelectedTheme()
|
val prefs = getSharedPreferences("mintris_settings", MODE_PRIVATE)
|
||||||
|
return prefs.getString("selected_theme", PlayerProgressionManager.THEME_CLASSIC) ?: PlayerProgressionManager.THEME_CLASSIC
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun applyTheme(themeId: String) {
|
private fun applyTheme(themeId: String) {
|
||||||
|
@ -112,29 +114,4 @@ class HighScoresActivity : AppCompatActivity() {
|
||||||
Log.e("HighScoresActivity", "Error in onResume", e)
|
Log.e("HighScoresActivity", "Error in onResume", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle gamepad buttons
|
|
||||||
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
|
|
||||||
// Check if it's a gamepad input
|
|
||||||
if (event != null && isGamepadDevice(event.device)) {
|
|
||||||
when (keyCode) {
|
|
||||||
KeyEvent.KEYCODE_BUTTON_B,
|
|
||||||
KeyEvent.KEYCODE_BACK -> {
|
|
||||||
// B button or Back button returns to previous screen
|
|
||||||
finish()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.onKeyDown(keyCode, event)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to check if the device is a gamepad
|
|
||||||
private fun isGamepadDevice(device: InputDevice?): Boolean {
|
|
||||||
if (device == null) return false
|
|
||||||
val sources = device.sources
|
|
||||||
return (sources and InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD ||
|
|
||||||
(sources and InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK
|
|
||||||
}
|
|
||||||
}
|
}
|
733
app/src/main/java/com/mintris/MainActivity.kt
Normal file
733
app/src/main/java/com/mintris/MainActivity.kt
Normal file
|
@ -0,0 +1,733 @@
|
||||||
|
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,11 +1,13 @@
|
||||||
package com.pixelmintdrop
|
package com.mintris
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.pixelmintdrop.databinding.ActivityStatsBinding
|
import com.mintris.databinding.ActivityStatsBinding
|
||||||
import com.pixelmintdrop.model.StatsManager
|
import com.mintris.model.StatsManager
|
||||||
import com.pixelmintdrop.model.PlayerProgressionManager
|
import com.mintris.model.PlayerProgressionManager
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
@ -42,7 +44,8 @@ class StatsActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadThemePreference(): String {
|
private fun loadThemePreference(): String {
|
||||||
return progressionManager.getSelectedTheme()
|
val prefs = getSharedPreferences("mintris_settings", MODE_PRIVATE)
|
||||||
|
return prefs.getString("selected_theme", PlayerProgressionManager.THEME_CLASSIC) ?: PlayerProgressionManager.THEME_CLASSIC
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun applyTheme(themeId: String) {
|
private fun applyTheme(themeId: String) {
|
||||||
|
@ -78,7 +81,7 @@ class StatsActivity : AppCompatActivity() {
|
||||||
binding.totalSinglesText.setTextColor(textColor)
|
binding.totalSinglesText.setTextColor(textColor)
|
||||||
binding.totalDoublesText.setTextColor(textColor)
|
binding.totalDoublesText.setTextColor(textColor)
|
||||||
binding.totalTriplesText.setTextColor(textColor)
|
binding.totalTriplesText.setTextColor(textColor)
|
||||||
binding.totalQuadsText.setTextColor(textColor)
|
binding.totalTetrisesText.setTextColor(textColor)
|
||||||
binding.maxLevelText.setTextColor(textColor)
|
binding.maxLevelText.setTextColor(textColor)
|
||||||
binding.maxScoreText.setTextColor(textColor)
|
binding.maxScoreText.setTextColor(textColor)
|
||||||
binding.maxLinesText.setTextColor(textColor)
|
binding.maxLinesText.setTextColor(textColor)
|
||||||
|
@ -116,7 +119,7 @@ class StatsActivity : AppCompatActivity() {
|
||||||
binding.totalSinglesText.text = getString(R.string.singles, statsManager.getTotalSingles())
|
binding.totalSinglesText.text = getString(R.string.singles, statsManager.getTotalSingles())
|
||||||
binding.totalDoublesText.text = getString(R.string.doubles, statsManager.getTotalDoubles())
|
binding.totalDoublesText.text = getString(R.string.doubles, statsManager.getTotalDoubles())
|
||||||
binding.totalTriplesText.text = getString(R.string.triples, statsManager.getTotalTriples())
|
binding.totalTriplesText.text = getString(R.string.triples, statsManager.getTotalTriples())
|
||||||
binding.totalQuadsText.text = getString(R.string.quads, statsManager.getTotalQuads())
|
binding.totalTetrisesText.text = getString(R.string.tetrises, statsManager.getTotalTetrises())
|
||||||
|
|
||||||
// Update best performance stats
|
// Update best performance stats
|
||||||
binding.maxLevelText.text = getString(R.string.max_level, statsManager.getMaxLevel())
|
binding.maxLevelText.text = getString(R.string.max_level, statsManager.getMaxLevel())
|
|
@ -1,22 +1,20 @@
|
||||||
package com.pixelmintdrop.audio
|
package com.mintris.audio
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.media.MediaPlayer
|
import android.media.MediaPlayer
|
||||||
import android.media.AudioAttributes
|
import android.media.AudioAttributes
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.pixelmintdrop.R
|
import com.mintris.R
|
||||||
|
|
||||||
class GameMusic(private val context: Context) {
|
class GameMusic(private val context: Context) {
|
||||||
private var mediaPlayer: MediaPlayer? = null
|
private var mediaPlayer: MediaPlayer? = null
|
||||||
private var gameOverPlayer: MediaPlayer? = null
|
|
||||||
private var isEnabled = true
|
private var isEnabled = true
|
||||||
private var isPrepared = false
|
private var isPrepared = false
|
||||||
|
|
||||||
init {
|
init {
|
||||||
try {
|
try {
|
||||||
setupMediaPlayer()
|
setupMediaPlayer()
|
||||||
setupGameOverPlayer()
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("GameMusic", "Error initializing: ${e.message}")
|
Log.e("GameMusic", "Error initializing: ${e.message}")
|
||||||
}
|
}
|
||||||
|
@ -48,49 +46,6 @@ 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() {
|
fun start() {
|
||||||
if (isEnabled && mediaPlayer != null && isPrepared) {
|
if (isEnabled && mediaPlayer != null && isPrepared) {
|
||||||
try {
|
try {
|
||||||
|
@ -148,25 +103,14 @@ class GameMusic(private val context: Context) {
|
||||||
|
|
||||||
fun isEnabled(): Boolean = isEnabled
|
fun isEnabled(): Boolean = isEnabled
|
||||||
|
|
||||||
/**
|
fun release() {
|
||||||
* Releases all media player resources to prevent memory leaks
|
|
||||||
*/
|
|
||||||
fun releaseResources() {
|
|
||||||
try {
|
try {
|
||||||
Log.d("GameMusic", "Releasing MediaPlayer resources")
|
Log.d("GameMusic", "Releasing MediaPlayer")
|
||||||
mediaPlayer?.release()
|
mediaPlayer?.release()
|
||||||
gameOverPlayer?.release()
|
|
||||||
mediaPlayer = null
|
mediaPlayer = null
|
||||||
gameOverPlayer = null
|
|
||||||
isPrepared = false
|
isPrepared = false
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("GameMusic", "Error releasing music resources: ${e.message}")
|
Log.e("GameMusic", "Error releasing music: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keeping old method for backward compatibility
|
|
||||||
@Deprecated("Use releaseResources() instead", ReplaceWith("releaseResources()"))
|
|
||||||
fun release() {
|
|
||||||
releaseResources()
|
|
||||||
}
|
|
||||||
}
|
}
|
77
app/src/main/java/com/mintris/game/GameHaptics.kt
Normal file
77
app/src/main/java/com/mintris/game/GameHaptics.kt
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
821
app/src/main/java/com/mintris/game/GameView.kt
Normal file
821
app/src/main/java/com/mintris/game/GameView.kt
Normal file
|
@ -0,0 +1,821 @@
|
||||||
|
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,70 +1,53 @@
|
||||||
package com.pixelmintdrop.game
|
package com.mintris.game
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.BlurMaskFilter
|
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.Paint
|
import android.graphics.Paint
|
||||||
|
import android.graphics.RectF
|
||||||
|
import android.graphics.BlurMaskFilter
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import com.pixelmintdrop.model.GameBoard
|
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* View that displays the currently held piece
|
* Custom view to display the next Tetromino piece
|
||||||
*/
|
*/
|
||||||
class HoldPieceView @JvmOverloads constructor(
|
class NextPieceView @JvmOverloads constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
attrs: AttributeSet? = null,
|
attrs: AttributeSet? = null,
|
||||||
defStyleAttr: Int = 0
|
defStyleAttr: Int = 0
|
||||||
) : View(context, attrs, defStyleAttr) {
|
) : View(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
private var gameView: GameView? = null
|
private var gameView: GameView? = null
|
||||||
private var gameBoard: GameBoard? = null
|
|
||||||
|
|
||||||
// Rendering
|
// Rendering
|
||||||
private val blockPaint = Paint().apply {
|
private val blockPaint = Paint().apply {
|
||||||
color = Color.WHITE
|
color = Color.WHITE
|
||||||
isAntiAlias = true
|
isAntiAlias = true
|
||||||
style = Paint.Style.FILL
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val glowPaint = Paint().apply {
|
private val glowPaint = Paint().apply {
|
||||||
color = Color.WHITE
|
color = Color.WHITE
|
||||||
alpha = 40
|
alpha = 30
|
||||||
isAntiAlias = true
|
isAntiAlias = true
|
||||||
style = Paint.Style.STROKE
|
style = Paint.Style.STROKE
|
||||||
strokeWidth = 1.5f
|
strokeWidth = 1.5f
|
||||||
maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.OUTER)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val blockGlowPaint = Paint().apply {
|
|
||||||
color = Color.WHITE
|
|
||||||
alpha = 60
|
|
||||||
isAntiAlias = true
|
|
||||||
style = Paint.Style.FILL
|
|
||||||
maskFilter = BlurMaskFilter(12f, BlurMaskFilter.Blur.OUTER)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the game view reference
|
* Set the game view to get the next piece from
|
||||||
*/
|
*/
|
||||||
fun setGameView(view: GameView) {
|
fun setGameView(gameView: GameView) {
|
||||||
gameView = view
|
this.gameView = gameView
|
||||||
gameBoard = view.getGameBoard()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the game board reference
|
|
||||||
*/
|
|
||||||
private fun getGameBoard(): GameBoard? = gameBoard
|
|
||||||
|
|
||||||
override fun onDraw(canvas: Canvas) {
|
override fun onDraw(canvas: Canvas) {
|
||||||
super.onDraw(canvas)
|
super.onDraw(canvas)
|
||||||
|
|
||||||
// Get the held piece from game board
|
// Get the next piece from game view
|
||||||
gameBoard?.let {
|
gameView?.let {
|
||||||
it.getHoldPiece()?.let { piece ->
|
it.getNextPiece()?.let { piece ->
|
||||||
val width = piece.getWidth()
|
val width = piece.getWidth()
|
||||||
val height = piece.getHeight()
|
val height = piece.getHeight()
|
||||||
|
|
||||||
|
@ -92,7 +75,6 @@ class HoldPieceView @JvmOverloads constructor(
|
||||||
glowPaint
|
glowPaint
|
||||||
)
|
)
|
||||||
|
|
||||||
// Draw the held piece
|
|
||||||
for (y in 0 until height) {
|
for (y in 0 until height) {
|
||||||
for (x in 0 until width) {
|
for (x in 0 until width) {
|
||||||
if (piece.isBlockAt(x, y)) {
|
if (piece.isBlockAt(x, y)) {
|
||||||
|
@ -101,29 +83,13 @@ class HoldPieceView @JvmOverloads constructor(
|
||||||
val right = left + previewBlockSize
|
val right = left + previewBlockSize
|
||||||
val bottom = top + previewBlockSize
|
val bottom = top + previewBlockSize
|
||||||
|
|
||||||
// Draw outer glow
|
// Draw block with subtle glow
|
||||||
blockGlowPaint.color = Color.WHITE
|
val rect = RectF(left + 1, top + 1, right - 1, bottom - 1)
|
||||||
canvas.drawRect(
|
canvas.drawRect(rect, blockPaint)
|
||||||
left - 2f,
|
|
||||||
top - 2f,
|
|
||||||
right + 2f,
|
|
||||||
bottom + 2f,
|
|
||||||
blockGlowPaint
|
|
||||||
)
|
|
||||||
|
|
||||||
// Draw block
|
// Draw subtle border glow
|
||||||
blockPaint.color = Color.WHITE
|
val glowRect = RectF(left, top, right, bottom)
|
||||||
canvas.drawRect(left, top, right, bottom, blockPaint)
|
canvas.drawRect(glowRect, glowPaint)
|
||||||
|
|
||||||
// Draw inner glow
|
|
||||||
glowPaint.color = Color.WHITE
|
|
||||||
canvas.drawRect(
|
|
||||||
left + 1f,
|
|
||||||
top + 1f,
|
|
||||||
right - 1f,
|
|
||||||
bottom - 1f,
|
|
||||||
glowPaint
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package com.pixelmintdrop.game
|
package com.mintris.game
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
|
@ -10,9 +10,9 @@ import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import java.util.Random
|
import java.util.Random
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.pixelmintdrop.model.HighScoreManager
|
import com.mintris.model.HighScoreManager
|
||||||
import com.pixelmintdrop.model.HighScore
|
import com.mintris.model.HighScore
|
||||||
import com.pixelmintdrop.model.PlayerProgressionManager
|
import com.mintris.model.PlayerProgressionManager
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import androidx.core.graphics.withTranslation
|
import androidx.core.graphics.withTranslation
|
||||||
import androidx.core.graphics.withScale
|
import androidx.core.graphics.withScale
|
||||||
|
@ -33,7 +33,7 @@ class TitleScreen @JvmOverloads constructor(
|
||||||
private val random = Random()
|
private val random = Random()
|
||||||
private var width = 0
|
private var width = 0
|
||||||
private var height = 0
|
private var height = 0
|
||||||
private val piecesToAdd = mutableListOf<FallingPiece>()
|
private val tetrominosToAdd = mutableListOf<Tetromino>()
|
||||||
private val highScoreManager = HighScoreManager(context) // Pre-allocate HighScoreManager
|
private val highScoreManager = HighScoreManager(context) // Pre-allocate HighScoreManager
|
||||||
|
|
||||||
// Touch handling variables
|
// Touch handling variables
|
||||||
|
@ -46,12 +46,11 @@ class TitleScreen @JvmOverloads constructor(
|
||||||
// Callback for when the user touches the screen
|
// Callback for when the user touches the screen
|
||||||
var onStartGame: (() -> Unit)? = null
|
var onStartGame: (() -> Unit)? = null
|
||||||
|
|
||||||
// Theme color and background color
|
// Theme color
|
||||||
private var themeColor = Color.WHITE
|
private var themeColor = Color.WHITE
|
||||||
private var backgroundColor = Color.BLACK
|
|
||||||
|
|
||||||
// Define piece shapes (I, O, T, S, Z, J, L)
|
// Define tetromino shapes (I, O, T, S, Z, J, L)
|
||||||
private val pieceShapes = arrayOf(
|
private val tetrominoShapes = arrayOf(
|
||||||
// I
|
// I
|
||||||
arrayOf(
|
arrayOf(
|
||||||
intArrayOf(0, 0, 0, 0),
|
intArrayOf(0, 0, 0, 0),
|
||||||
|
@ -96,8 +95,8 @@ class TitleScreen @JvmOverloads constructor(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
// FallingPiece class to represent falling pieces
|
// Tetromino class to represent falling pieces
|
||||||
private class FallingPiece(
|
private class Tetromino(
|
||||||
var x: Float,
|
var x: Float,
|
||||||
var y: Float,
|
var y: Float,
|
||||||
val shape: Array<IntArray>,
|
val shape: Array<IntArray>,
|
||||||
|
@ -106,12 +105,12 @@ class TitleScreen @JvmOverloads constructor(
|
||||||
val rotation: Int = 0
|
val rotation: Int = 0
|
||||||
)
|
)
|
||||||
|
|
||||||
private val pieces = mutableListOf<FallingPiece>()
|
private val tetrominos = mutableListOf<Tetromino>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// Title text settings
|
// Title text settings
|
||||||
titlePaint.apply {
|
titlePaint.apply {
|
||||||
color = themeColor
|
color = Color.WHITE
|
||||||
textSize = 120f
|
textSize = 120f
|
||||||
textAlign = Paint.Align.CENTER
|
textAlign = Paint.Align.CENTER
|
||||||
typeface = Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD)
|
typeface = Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD)
|
||||||
|
@ -120,7 +119,7 @@ class TitleScreen @JvmOverloads constructor(
|
||||||
|
|
||||||
// "Touch to start" text settings
|
// "Touch to start" text settings
|
||||||
promptPaint.apply {
|
promptPaint.apply {
|
||||||
color = themeColor
|
color = Color.WHITE
|
||||||
textSize = 50f
|
textSize = 50f
|
||||||
textAlign = Paint.Align.CENTER
|
textAlign = Paint.Align.CENTER
|
||||||
typeface = Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL)
|
typeface = Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL)
|
||||||
|
@ -130,7 +129,7 @@ class TitleScreen @JvmOverloads constructor(
|
||||||
|
|
||||||
// High scores text settings
|
// High scores text settings
|
||||||
highScorePaint.apply {
|
highScorePaint.apply {
|
||||||
color = themeColor
|
color = Color.WHITE
|
||||||
textSize = 70f
|
textSize = 70f
|
||||||
textAlign = Paint.Align.LEFT // Changed to LEFT alignment
|
textAlign = Paint.Align.LEFT // Changed to LEFT alignment
|
||||||
typeface = Typeface.create(Typeface.MONOSPACE, Typeface.NORMAL) // Changed to monospace
|
typeface = Typeface.create(Typeface.MONOSPACE, Typeface.NORMAL) // Changed to monospace
|
||||||
|
@ -138,16 +137,16 @@ class TitleScreen @JvmOverloads constructor(
|
||||||
alpha = 200
|
alpha = 200
|
||||||
}
|
}
|
||||||
|
|
||||||
// General paint settings for pieces
|
// General paint settings for tetrominos (white)
|
||||||
paint.apply {
|
paint.apply {
|
||||||
color = themeColor
|
color = Color.WHITE
|
||||||
style = Paint.Style.FILL
|
style = Paint.Style.FILL
|
||||||
isAntiAlias = true
|
isAntiAlias = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Glow paint settings for pieces
|
// Glow paint settings for tetrominos
|
||||||
glowPaint.apply {
|
glowPaint.apply {
|
||||||
color = themeColor
|
color = Color.WHITE
|
||||||
style = Paint.Style.FILL
|
style = Paint.Style.FILL
|
||||||
isAntiAlias = true
|
isAntiAlias = true
|
||||||
alpha = 60
|
alpha = 60
|
||||||
|
@ -159,66 +158,66 @@ class TitleScreen @JvmOverloads constructor(
|
||||||
width = w
|
width = w
|
||||||
height = h
|
height = h
|
||||||
|
|
||||||
// Clear existing pieces
|
// Clear existing tetrominos
|
||||||
pieces.clear()
|
tetrominos.clear()
|
||||||
|
|
||||||
// Initialize some pieces
|
// Initialize some tetrominos
|
||||||
repeat(20) {
|
repeat(20) {
|
||||||
val piece = createRandomPiece()
|
val tetromino = createRandomTetromino()
|
||||||
pieces.add(piece)
|
tetrominos.add(tetromino)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createRandomPiece(): FallingPiece {
|
private fun createRandomTetromino(): Tetromino {
|
||||||
val x = random.nextFloat() * (width - 150) + 50 // Keep away from edges
|
val x = random.nextFloat() * (width - 150) + 50 // Keep away from edges
|
||||||
val y = -cellSize * 4 - (random.nextFloat() * height / 2)
|
val y = -cellSize * 4 - (random.nextFloat() * height / 2)
|
||||||
val shapeIndex = random.nextInt(pieceShapes.size)
|
val shapeIndex = random.nextInt(tetrominoShapes.size)
|
||||||
val shape = pieceShapes[shapeIndex]
|
val shape = tetrominoShapes[shapeIndex]
|
||||||
val speed = 1f + random.nextFloat() * 2f
|
val speed = 1f + random.nextFloat() * 2f
|
||||||
val scale = 0.8f + random.nextFloat() * 0.4f
|
val scale = 0.8f + random.nextFloat() * 0.4f
|
||||||
val rotation = random.nextInt(4) * 90
|
val rotation = random.nextInt(4) * 90
|
||||||
|
|
||||||
return FallingPiece(x, y, shape, speed, scale, rotation)
|
return Tetromino(x, y, shape, speed, scale, rotation)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDraw(canvas: Canvas) {
|
override fun onDraw(canvas: Canvas) {
|
||||||
try {
|
try {
|
||||||
super.onDraw(canvas)
|
super.onDraw(canvas)
|
||||||
|
|
||||||
// Draw background using the current background color
|
// Draw background
|
||||||
canvas.drawColor(backgroundColor)
|
canvas.drawColor(Color.BLACK)
|
||||||
|
|
||||||
// Add any pending pieces
|
// Add any pending tetrominos
|
||||||
pieces.addAll(piecesToAdd)
|
tetrominos.addAll(tetrominosToAdd)
|
||||||
piecesToAdd.clear()
|
tetrominosToAdd.clear()
|
||||||
|
|
||||||
// Update and draw falling pieces
|
// Update and draw falling tetrominos
|
||||||
val piecesToRemove = mutableListOf<FallingPiece>()
|
val tetrominosToRemove = mutableListOf<Tetromino>()
|
||||||
|
|
||||||
for (piece in pieces) {
|
for (tetromino in tetrominos) {
|
||||||
piece.y += piece.speed
|
tetromino.y += tetromino.speed
|
||||||
|
|
||||||
// Remove pieces that have fallen off the screen
|
// Remove tetrominos that have fallen off the screen
|
||||||
if (piece.y > height) {
|
if (tetromino.y > height) {
|
||||||
piecesToRemove.add(piece)
|
tetrominosToRemove.add(tetromino)
|
||||||
piecesToAdd.add(createRandomPiece())
|
tetrominosToAdd.add(createRandomTetromino())
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
// Draw the piece
|
// Draw the tetromino
|
||||||
for (y in 0 until piece.shape.size) {
|
for (y in 0 until tetromino.shape.size) {
|
||||||
for (x in 0 until piece.shape.size) {
|
for (x in 0 until tetromino.shape.size) {
|
||||||
if (piece.shape[y][x] == 1) {
|
if (tetromino.shape[y][x] == 1) {
|
||||||
val left = x * cellSize
|
val left = x * cellSize
|
||||||
val top = y * cellSize
|
val top = y * cellSize
|
||||||
val right = left + cellSize
|
val right = left + cellSize
|
||||||
val bottom = top + cellSize
|
val bottom = top + cellSize
|
||||||
|
|
||||||
// Draw block with glow effect
|
// Draw block with glow effect
|
||||||
canvas.withTranslation(piece.x, piece.y) {
|
canvas.withTranslation(tetromino.x, tetromino.y) {
|
||||||
withScale(piece.scale, piece.scale) {
|
withScale(tetromino.scale, tetromino.scale) {
|
||||||
withRotation(piece.rotation.toFloat(),
|
withRotation(tetromino.rotation.toFloat(),
|
||||||
piece.shape.size * cellSize / 2,
|
tetromino.shape.size * cellSize / 2,
|
||||||
piece.shape.size * cellSize / 2) {
|
tetromino.shape.size * cellSize / 2) {
|
||||||
// Draw glow
|
// Draw glow
|
||||||
canvas.drawRect(left - 8f, top - 8f, right + 8f, bottom + 8f, glowPaint)
|
canvas.drawRect(left - 8f, top - 8f, right + 8f, bottom + 8f, glowPaint)
|
||||||
// Draw block
|
// Draw block
|
||||||
|
@ -230,22 +229,21 @@ class TitleScreen @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("TitleScreen", "Error drawing piece", e)
|
Log.e("TitleScreen", "Error drawing tetromino", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove pieces that fell off the screen
|
// Remove tetrominos that fell off the screen
|
||||||
pieces.removeAll(piecesToRemove)
|
tetrominos.removeAll(tetrominosToRemove)
|
||||||
|
|
||||||
// Draw title
|
// Draw title
|
||||||
val titleY = height * 0.4f
|
val titleY = height * 0.4f
|
||||||
canvas.drawText("Pixel Mint Drop", width / 2f, titleY, titlePaint)
|
canvas.drawText("mintris", width / 2f, titleY, titlePaint)
|
||||||
|
|
||||||
// Draw high scores using pre-allocated manager
|
// Draw high scores using pre-allocated manager
|
||||||
val highScores: List<HighScore> = highScoreManager.getHighScores()
|
val highScores: List<HighScore> = highScoreManager.getHighScores()
|
||||||
val highScoreY = height * 0.5f
|
val highScoreY = height * 0.5f
|
||||||
var lastHighScoreY = highScoreY
|
|
||||||
if (highScores.isNotEmpty()) {
|
if (highScores.isNotEmpty()) {
|
||||||
// Calculate the starting X position to center the entire block of scores
|
// Calculate the starting X position to center the entire block of scores
|
||||||
val maxScoreWidth = highScorePaint.measureText("99. PLAYER: 999999")
|
val maxScoreWidth = highScorePaint.measureText("99. PLAYER: 999999")
|
||||||
|
@ -253,7 +251,6 @@ class TitleScreen @JvmOverloads constructor(
|
||||||
|
|
||||||
highScores.forEachIndexed { index: Int, score: HighScore ->
|
highScores.forEachIndexed { index: Int, score: HighScore ->
|
||||||
val y = highScoreY + (index * 80f)
|
val y = highScoreY + (index * 80f)
|
||||||
lastHighScoreY = y // Track the last high score's Y position
|
|
||||||
// Pad the rank number to ensure alignment
|
// Pad the rank number to ensure alignment
|
||||||
val rank = (index + 1).toString().padStart(2, ' ')
|
val rank = (index + 1).toString().padStart(2, ' ')
|
||||||
// Pad the name to ensure score alignment
|
// Pad the name to ensure score alignment
|
||||||
|
@ -262,15 +259,8 @@ class TitleScreen @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw "touch to start" prompt below the high scores
|
// Draw "touch to start" prompt
|
||||||
val promptY = if (resources.configuration.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE) {
|
canvas.drawText("touch to start", width / 2f, height * 0.7f, promptPaint)
|
||||||
// In landscape mode, position below the last high score with some padding
|
|
||||||
lastHighScoreY + 100f
|
|
||||||
} else {
|
|
||||||
// In portrait mode, use the original position
|
|
||||||
height * 0.7f
|
|
||||||
}
|
|
||||||
canvas.drawText("touch to start", width / 2f, promptY, promptPaint)
|
|
||||||
|
|
||||||
// Request another frame
|
// Request another frame
|
||||||
invalidate()
|
invalidate()
|
||||||
|
@ -292,10 +282,10 @@ class TitleScreen @JvmOverloads constructor(
|
||||||
val deltaX = event.x - lastTouchX
|
val deltaX = event.x - lastTouchX
|
||||||
val deltaY = event.y - lastTouchY
|
val deltaY = event.y - lastTouchY
|
||||||
|
|
||||||
// Update piece positions
|
// Update tetromino positions
|
||||||
for (piece in pieces) {
|
for (tetromino in tetrominos) {
|
||||||
piece.x += deltaX * 0.5f
|
tetromino.x += deltaX * 0.5f
|
||||||
piece.y += deltaY * 0.5f
|
tetromino.y += deltaY * 0.5f
|
||||||
}
|
}
|
||||||
|
|
||||||
lastTouchX = event.x
|
lastTouchX = event.x
|
||||||
|
@ -350,7 +340,7 @@ class TitleScreen @JvmOverloads constructor(
|
||||||
glowPaint.color = themeColor
|
glowPaint.color = themeColor
|
||||||
|
|
||||||
// Update background color
|
// Update background color
|
||||||
backgroundColor = when (themeId) {
|
setBackgroundColor(when (themeId) {
|
||||||
PlayerProgressionManager.THEME_CLASSIC -> Color.BLACK
|
PlayerProgressionManager.THEME_CLASSIC -> Color.BLACK
|
||||||
PlayerProgressionManager.THEME_NEON -> Color.parseColor("#0D0221")
|
PlayerProgressionManager.THEME_NEON -> Color.parseColor("#0D0221")
|
||||||
PlayerProgressionManager.THEME_MONOCHROME -> Color.parseColor("#1A1A1A")
|
PlayerProgressionManager.THEME_MONOCHROME -> Color.parseColor("#1A1A1A")
|
||||||
|
@ -358,29 +348,8 @@ class TitleScreen @JvmOverloads constructor(
|
||||||
PlayerProgressionManager.THEME_MINIMALIST -> Color.WHITE
|
PlayerProgressionManager.THEME_MINIMALIST -> Color.WHITE
|
||||||
PlayerProgressionManager.THEME_GALAXY -> Color.parseColor("#0B0C10")
|
PlayerProgressionManager.THEME_GALAXY -> Color.parseColor("#0B0C10")
|
||||||
else -> Color.BLACK
|
else -> Color.BLACK
|
||||||
}
|
})
|
||||||
|
|
||||||
invalidate()
|
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
585
app/src/main/java/com/mintris/model/GameBoard.kt
Normal file
585
app/src/main/java/com/mintris/model/GameBoard.kt
Normal file
|
@ -0,0 +1,585 @@
|
||||||
|
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,4 +1,4 @@
|
||||||
package com.pixelmintdrop.model
|
package com.mintris.model
|
||||||
|
|
||||||
data class HighScore(
|
data class HighScore(
|
||||||
val name: String,
|
val name: String,
|
|
@ -1,4 +1,4 @@
|
||||||
package com.pixelmintdrop.model
|
package com.mintris.model
|
||||||
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
@ -6,7 +6,7 @@ import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.pixelmintdrop.R
|
import com.mintris.R
|
||||||
|
|
||||||
class HighScoreAdapter : RecyclerView.Adapter<HighScoreAdapter.HighScoreViewHolder>() {
|
class HighScoreAdapter : RecyclerView.Adapter<HighScoreAdapter.HighScoreViewHolder>() {
|
||||||
private var highScores: List<HighScore> = emptyList()
|
private var highScores: List<HighScore> = emptyList()
|
|
@ -1,4 +1,4 @@
|
||||||
package com.pixelmintdrop.model
|
package com.mintris.model
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
@ -12,7 +12,7 @@ class HighScoreManager(private val context: Context) {
|
||||||
private val type: Type = object : TypeToken<List<HighScore>>() {}.type
|
private val type: Type = object : TypeToken<List<HighScore>>() {}.type
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val PREFS_NAME = "pixelmintdrop_highscores"
|
private const val PREFS_NAME = "mintris_highscores"
|
||||||
private const val KEY_HIGHSCORES = "highscores"
|
private const val KEY_HIGHSCORES = "highscores"
|
||||||
private const val MAX_HIGHSCORES = 5
|
private const val MAX_HIGHSCORES = 5
|
||||||
}
|
}
|
|
@ -1,10 +1,10 @@
|
||||||
package com.pixelmintdrop.model
|
package com.mintris.model
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
import com.mintris.R
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import kotlin.math.min
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages player progression, experience points, and unlockable rewards
|
* Manages player progression, experience points, and unlockable rewards
|
||||||
|
@ -20,6 +20,7 @@ class PlayerProgressionManager(context: Context) {
|
||||||
// Track unlocked rewards
|
// Track unlocked rewards
|
||||||
private val unlockedThemes = mutableSetOf<String>()
|
private val unlockedThemes = mutableSetOf<String>()
|
||||||
private val unlockedBlocks = mutableSetOf<String>()
|
private val unlockedBlocks = mutableSetOf<String>()
|
||||||
|
private val unlockedPowers = mutableSetOf<String>()
|
||||||
private val unlockedBadges = mutableSetOf<String>()
|
private val unlockedBadges = mutableSetOf<String>()
|
||||||
|
|
||||||
// XP gained in the current session
|
// XP gained in the current session
|
||||||
|
@ -40,22 +41,18 @@ class PlayerProgressionManager(context: Context) {
|
||||||
// Load unlocked rewards
|
// Load unlocked rewards
|
||||||
val themesSet = prefs.getStringSet(KEY_UNLOCKED_THEMES, setOf()) ?: setOf()
|
val themesSet = prefs.getStringSet(KEY_UNLOCKED_THEMES, setOf()) ?: setOf()
|
||||||
val blocksSet = prefs.getStringSet(KEY_UNLOCKED_BLOCKS, 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()
|
val badgesSet = prefs.getStringSet(KEY_UNLOCKED_BADGES, setOf()) ?: setOf()
|
||||||
|
|
||||||
unlockedThemes.addAll(themesSet)
|
unlockedThemes.addAll(themesSet)
|
||||||
unlockedBlocks.addAll(blocksSet)
|
unlockedBlocks.addAll(blocksSet)
|
||||||
|
unlockedPowers.addAll(powersSet)
|
||||||
unlockedBadges.addAll(badgesSet)
|
unlockedBadges.addAll(badgesSet)
|
||||||
|
|
||||||
// Add default theme if nothing is unlocked
|
// Add default theme if nothing is unlocked
|
||||||
if (unlockedThemes.isEmpty()) {
|
if (unlockedThemes.isEmpty()) {
|
||||||
unlockedThemes.add(THEME_CLASSIC)
|
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -68,6 +65,7 @@ class PlayerProgressionManager(context: Context) {
|
||||||
.putLong(KEY_TOTAL_XP_EARNED, totalXPEarned)
|
.putLong(KEY_TOTAL_XP_EARNED, totalXPEarned)
|
||||||
.putStringSet(KEY_UNLOCKED_THEMES, unlockedThemes)
|
.putStringSet(KEY_UNLOCKED_THEMES, unlockedThemes)
|
||||||
.putStringSet(KEY_UNLOCKED_BLOCKS, unlockedBlocks)
|
.putStringSet(KEY_UNLOCKED_BLOCKS, unlockedBlocks)
|
||||||
|
.putStringSet(KEY_UNLOCKED_POWERS, unlockedPowers)
|
||||||
.putStringSet(KEY_UNLOCKED_BADGES, unlockedBadges)
|
.putStringSet(KEY_UNLOCKED_BADGES, unlockedBadges)
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
@ -93,31 +91,23 @@ class PlayerProgressionManager(context: Context) {
|
||||||
/**
|
/**
|
||||||
* Calculate XP from a game session based on score, lines, level, etc.
|
* Calculate XP from a game session based on score, lines, level, etc.
|
||||||
*/
|
*/
|
||||||
fun calculateGameXP(
|
fun calculateGameXP(score: Int, lines: Int, level: Int, gameTime: Long,
|
||||||
score: Int,
|
tetrisCount: Int, perfectClearCount: Int): Long {
|
||||||
lines: Int,
|
// Base XP from score with level multiplier
|
||||||
level: Int,
|
val scoreXP = (score * (1 + LEVEL_MULTIPLIER * level)).toLong()
|
||||||
timePlayedMs: Long,
|
|
||||||
quadCount: Int,
|
|
||||||
perfectClearCount: Int
|
|
||||||
): Long {
|
|
||||||
// Calculate base XP from score
|
|
||||||
val scoreXP = score * BASE_SCORE_XP * level
|
|
||||||
|
|
||||||
// Calculate XP from lines cleared
|
// XP from lines cleared
|
||||||
val linesXP = lines * BASE_LINES_XP * level
|
val linesXP = lines * XP_PER_LINE
|
||||||
|
|
||||||
// Calculate quad bonus
|
// XP from special moves
|
||||||
val quadBonus = quadCount * BASE_QUAD_BONUS * level
|
val tetrisBonus = tetrisCount * TETRIS_XP_BONUS
|
||||||
|
val perfectClearBonus = perfectClearCount * PERFECT_CLEAR_XP_BONUS
|
||||||
|
|
||||||
// Calculate perfect clear bonus
|
// Time bonus (to reward longer gameplay)
|
||||||
val perfectClearBonus = perfectClearCount * BASE_PERFECT_CLEAR_BONUS * level
|
val timeBonus = (gameTime / 60000) * TIME_XP_PER_MINUTE // XP per minute played
|
||||||
|
|
||||||
// Calculate time bonus (convert ms to seconds)
|
// Calculate total XP
|
||||||
val timeBonus = (timePlayedMs / 1000.0) * BASE_TIME_XP * level
|
return scoreXP + linesXP + tetrisBonus + perfectClearBonus + timeBonus
|
||||||
|
|
||||||
// Sum all XP components
|
|
||||||
return (scoreXP + linesXP + quadBonus + perfectClearBonus + timeBonus).toLong()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -189,64 +179,46 @@ class PlayerProgressionManager(context: Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for block skin unlocks (start from skin 2 at level 7)
|
// Check for power unlocks
|
||||||
when (level) {
|
when (level) {
|
||||||
7 -> {
|
8 -> {
|
||||||
if (unlockedBlocks.add("block_skin_2")) {
|
if (unlockedPowers.add(POWER_FREEZE_TIME)) {
|
||||||
newRewards.add("Unlocked Neon Block Skin!")
|
newRewards.add("Unlocked Freeze Time Power!")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
14 -> {
|
12 -> {
|
||||||
if (unlockedBlocks.add("block_skin_3")) {
|
if (unlockedPowers.add(POWER_BLOCK_SWAP)) {
|
||||||
newRewards.add("Unlocked Retro Block Skin!")
|
newRewards.add("Unlocked Block Swap Power!")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
21 -> {
|
18 -> {
|
||||||
if (unlockedBlocks.add("block_skin_4")) {
|
if (unlockedPowers.add(POWER_SAFE_LANDING)) {
|
||||||
newRewards.add("Unlocked Minimalist Block Skin!")
|
newRewards.add("Unlocked Safe Landing Power!")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
28 -> {
|
30 -> {
|
||||||
if (unlockedBlocks.add("block_skin_5")) {
|
if (unlockedPowers.add(POWER_PERFECT_CLEAR)) {
|
||||||
newRewards.add("Unlocked Galaxy Block Skin!")
|
newRewards.add("Unlocked Perfect Clear Power!")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
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
|
* Start a new progression session
|
||||||
*/
|
*/
|
||||||
fun startNewSession() {
|
fun startNewSession() {
|
||||||
sessionXPGained = 0
|
sessionXPGained = 0
|
||||||
|
|
||||||
// Ensure all appropriate unlocks are available
|
|
||||||
checkAllUnlocksForCurrentLevel()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
|
@ -255,6 +227,7 @@ class PlayerProgressionManager(context: Context) {
|
||||||
fun getXPForNextLevel(): Long = calculateXPForLevel(playerLevel)
|
fun getXPForNextLevel(): Long = calculateXPForLevel(playerLevel)
|
||||||
fun getSessionXPGained(): Long = sessionXPGained
|
fun getSessionXPGained(): Long = sessionXPGained
|
||||||
fun getUnlockedThemes(): Set<String> = unlockedThemes.toSet()
|
fun getUnlockedThemes(): Set<String> = unlockedThemes.toSet()
|
||||||
|
fun getUnlockedPowers(): Set<String> = unlockedPowers.toSet()
|
||||||
fun getUnlockedBlocks(): Set<String> = unlockedBlocks.toSet()
|
fun getUnlockedBlocks(): Set<String> = unlockedBlocks.toSet()
|
||||||
fun getUnlockedBadges(): Set<String> = unlockedBadges.toSet()
|
fun getUnlockedBadges(): Set<String> = unlockedBadges.toSet()
|
||||||
|
|
||||||
|
@ -265,6 +238,13 @@ class PlayerProgressionManager(context: Context) {
|
||||||
return unlockedThemes.contains(themeId)
|
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
|
* Award a badge to the player
|
||||||
*/
|
*/
|
||||||
|
@ -286,38 +266,37 @@ class PlayerProgressionManager(context: Context) {
|
||||||
|
|
||||||
unlockedThemes.clear()
|
unlockedThemes.clear()
|
||||||
unlockedBlocks.clear()
|
unlockedBlocks.clear()
|
||||||
|
unlockedPowers.clear()
|
||||||
unlockedBadges.clear()
|
unlockedBadges.clear()
|
||||||
|
|
||||||
// Add default theme
|
// Add default theme
|
||||||
unlockedThemes.add(THEME_CLASSIC)
|
unlockedThemes.add(THEME_CLASSIC)
|
||||||
|
|
||||||
// Add default block skin (Level 1)
|
|
||||||
unlockedBlocks.add("block_skin_1")
|
|
||||||
|
|
||||||
saveProgress()
|
saveProgress()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val PREFS_NAME = "pixelmintdrop_progression"
|
private const val PREFS_NAME = "mintris_progression"
|
||||||
private const val KEY_PLAYER_LEVEL = "player_level"
|
private const val KEY_PLAYER_LEVEL = "player_level"
|
||||||
private const val KEY_PLAYER_XP = "player_xp"
|
private const val KEY_PLAYER_XP = "player_xp"
|
||||||
private const val KEY_TOTAL_XP_EARNED = "total_xp_earned"
|
private const val KEY_TOTAL_XP_EARNED = "total_xp_earned"
|
||||||
private const val KEY_UNLOCKED_THEMES = "unlocked_themes"
|
private const val KEY_UNLOCKED_THEMES = "unlocked_themes"
|
||||||
private const val KEY_UNLOCKED_BLOCKS = "unlocked_blocks"
|
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_UNLOCKED_BADGES = "unlocked_badges"
|
||||||
private const val KEY_SELECTED_THEME = "selected_theme"
|
|
||||||
private const val KEY_SELECTED_BLOCK_SKIN = "selected_block_skin"
|
|
||||||
|
|
||||||
// XP constants
|
// XP curve parameters
|
||||||
private const val BASE_XP = 3000L
|
private const val BASE_XP = 4000.0 // Base XP for level 1 (reduced from 5000)
|
||||||
private const val XP_CURVE_FACTOR = 2.0
|
private const val XP_CURVE_FACTOR = 1.9 // Exponential factor for XP curve (reduced from 2.2)
|
||||||
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
|
|
||||||
|
|
||||||
// Theme constants
|
// 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
|
||||||
const val THEME_CLASSIC = "theme_classic"
|
const val THEME_CLASSIC = "theme_classic"
|
||||||
const val THEME_NEON = "theme_neon"
|
const val THEME_NEON = "theme_neon"
|
||||||
const val THEME_MONOCHROME = "theme_monochrome"
|
const val THEME_MONOCHROME = "theme_monochrome"
|
||||||
|
@ -335,11 +314,19 @@ class PlayerProgressionManager(context: Context) {
|
||||||
THEME_GALAXY to 25
|
THEME_GALAXY to 25
|
||||||
)
|
)
|
||||||
|
|
||||||
private const val BASE_SCORE_XP = 0.1 // XP per score point
|
// Power IDs
|
||||||
private const val BASE_LINES_XP = 100.0 // XP per line cleared
|
const val POWER_FREEZE_TIME = "power_freeze_time"
|
||||||
private const val BASE_QUAD_BONUS = 500.0 // Bonus XP for clearing 4 lines at once
|
const val POWER_BLOCK_SWAP = "power_block_swap"
|
||||||
private const val BASE_PERFECT_CLEAR_BONUS = 1000.0 // Bonus XP for perfect clear
|
const val POWER_SAFE_LANDING = "power_safe_landing"
|
||||||
private const val BASE_TIME_XP = 1.0 // XP per second played
|
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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -350,34 +337,9 @@ class PlayerProgressionManager(context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the selected block skin
|
* Get the required level for a specific power
|
||||||
*/
|
*/
|
||||||
fun setSelectedBlockSkin(skinId: String) {
|
fun getRequiredLevelForPower(powerId: String): Int {
|
||||||
if (unlockedBlocks.contains(skinId)) {
|
return POWER_REQUIRED_LEVELS[powerId] ?: 1
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
196
app/src/main/java/com/mintris/model/StatsManager.kt
Normal file
196
app/src/main/java/com/mintris/model/StatsManager.kt
Normal file
|
@ -0,0 +1,196 @@
|
||||||
|
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,7 +1,7 @@
|
||||||
package com.pixelmintdrop.model
|
package com.mintris.model
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a game piece (Tetromino)
|
* Represents a Tetris piece (Tetromino)
|
||||||
*/
|
*/
|
||||||
enum class TetrominoType {
|
enum class TetrominoType {
|
||||||
I, J, L, O, S, T, Z
|
I, J, L, O, S, T, Z
|
|
@ -1,4 +1,4 @@
|
||||||
package com.pixelmintdrop.ui
|
package com.mintris.ui
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
|
@ -1,4 +1,4 @@
|
||||||
package com.pixelmintdrop.ui
|
package com.mintris.ui
|
||||||
|
|
||||||
import android.animation.AnimatorSet
|
import android.animation.AnimatorSet
|
||||||
import android.animation.ObjectAnimator
|
import android.animation.ObjectAnimator
|
||||||
|
@ -12,8 +12,8 @@ import android.view.animation.OvershootInterpolator
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.cardview.widget.CardView
|
import androidx.cardview.widget.CardView
|
||||||
import com.pixelmintdrop.R
|
import com.mintris.R
|
||||||
import com.pixelmintdrop.model.PlayerProgressionManager
|
import com.mintris.model.PlayerProgressionManager
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Screen that displays player progression, XP gain, and unlocked rewards
|
* Screen that displays player progression, XP gain, and unlocked rewards
|
||||||
|
@ -31,9 +31,6 @@ class ProgressionScreen @JvmOverloads constructor(
|
||||||
private val rewardsContainer: LinearLayout
|
private val rewardsContainer: LinearLayout
|
||||||
private val continueButton: TextView
|
private val continueButton: TextView
|
||||||
|
|
||||||
// Current theme
|
|
||||||
private var currentTheme: String = PlayerProgressionManager.THEME_CLASSIC
|
|
||||||
|
|
||||||
// Callback for when the player dismisses the screen
|
// Callback for when the player dismisses the screen
|
||||||
var onContinue: (() -> Unit)? = null
|
var onContinue: (() -> Unit)? = null
|
||||||
|
|
||||||
|
@ -65,9 +62,6 @@ class ProgressionScreen @JvmOverloads constructor(
|
||||||
newRewards: List<String>,
|
newRewards: List<String>,
|
||||||
themeId: String = PlayerProgressionManager.THEME_CLASSIC
|
themeId: String = PlayerProgressionManager.THEME_CLASSIC
|
||||||
) {
|
) {
|
||||||
// Update current theme
|
|
||||||
currentTheme = themeId
|
|
||||||
|
|
||||||
// Hide rewards container initially if there are no new rewards
|
// Hide rewards container initially if there are no new rewards
|
||||||
rewardsContainer.visibility = if (newRewards.isEmpty()) View.GONE else View.INVISIBLE
|
rewardsContainer.visibility = if (newRewards.isEmpty()) View.GONE else View.INVISIBLE
|
||||||
|
|
||||||
|
@ -80,39 +74,20 @@ class ProgressionScreen @JvmOverloads constructor(
|
||||||
playerLevelText.text = "Player Level: $playerLevel"
|
playerLevelText.text = "Player Level: $playerLevel"
|
||||||
xpGainText.text = "+$xpGained XP"
|
xpGainText.text = "+$xpGained XP"
|
||||||
|
|
||||||
// Update level up text visibility
|
// Begin animation sequence
|
||||||
val progressionTitle = findViewById<TextView>(R.id.progression_title)
|
xpProgressBar.setXPValues(playerLevel, currentXP, xpForNextLevel)
|
||||||
progressionTitle.visibility = if (newRewards.any { it.contains("Level") }) View.VISIBLE else View.GONE
|
|
||||||
|
|
||||||
// Start with initial animations
|
// Animate XP gain text entrance
|
||||||
AnimatorSet().apply {
|
|
||||||
// Fade in the XP gain text
|
|
||||||
val xpTextAnimator = ObjectAnimator.ofFloat(xpGainText, "alpha", 0f, 1f).apply {
|
val xpTextAnimator = ObjectAnimator.ofFloat(xpGainText, "alpha", 0f, 1f).apply {
|
||||||
duration = 800
|
duration = 500
|
||||||
interpolator = AccelerateDecelerateInterpolator()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up the XP progress bar animation sequence
|
// Schedule animation for the XP bar after text appears
|
||||||
val xpBarAnimator = ObjectAnimator.ofFloat(xpProgressBar, "alpha", 0f, 1f).apply {
|
|
||||||
duration = 800
|
|
||||||
interpolator = AccelerateDecelerateInterpolator()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Play animations in sequence
|
|
||||||
play(xpTextAnimator)
|
|
||||||
play(xpBarAnimator).after(xpTextAnimator)
|
|
||||||
start()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set initial progress bar state
|
|
||||||
xpProgressBar.setXPValues(playerLevel, currentXP - xpGained, xpForNextLevel)
|
|
||||||
|
|
||||||
// Animate the XP gain after a short delay
|
|
||||||
postDelayed({
|
postDelayed({
|
||||||
xpProgressBar.animateXPGain(xpGained, playerLevel, currentXP, xpForNextLevel)
|
xpProgressBar.animateXPGain(xpGained, playerLevel, currentXP, xpForNextLevel)
|
||||||
}, 1000) // Increased delay to 1 second for better visual flow
|
}, 600)
|
||||||
|
|
||||||
// If there are new rewards, show them with animation after XP bar animation
|
// If there are new rewards, show them with animation
|
||||||
if (newRewards.isNotEmpty()) {
|
if (newRewards.isNotEmpty()) {
|
||||||
// Create reward cards
|
// Create reward cards
|
||||||
rewardsContainer.removeAllViews()
|
rewardsContainer.removeAllViews()
|
||||||
|
@ -138,12 +113,18 @@ class ProgressionScreen @JvmOverloads constructor(
|
||||||
card.animate()
|
card.animate()
|
||||||
.alpha(1f)
|
.alpha(1f)
|
||||||
.translationY(0f)
|
.translationY(0f)
|
||||||
.setDuration(600) // Increased duration for smoother animation
|
.setDuration(400)
|
||||||
.setStartDelay((i * 200).toLong()) // Increased delay between cards
|
.setStartDelay((i * 150).toLong())
|
||||||
.setInterpolator(OvershootInterpolator())
|
.setInterpolator(OvershootInterpolator())
|
||||||
.start()
|
.start()
|
||||||
}
|
}
|
||||||
}, 2500) // Increased delay to wait for XP bar animation to finish
|
}, 2000) // Wait for XP bar animation to finish
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start with initial animations
|
||||||
|
AnimatorSet().apply {
|
||||||
|
play(xpTextAnimator)
|
||||||
|
start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,17 +137,8 @@ class ProgressionScreen @JvmOverloads constructor(
|
||||||
cardElevation = 4f
|
cardElevation = 4f
|
||||||
useCompatPadding = true
|
useCompatPadding = true
|
||||||
|
|
||||||
// Set background color based on current theme
|
// Default background color - will be adjusted based on theme
|
||||||
val backgroundColor = when (currentTheme) {
|
setCardBackgroundColor(Color.BLACK)
|
||||||
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(
|
layoutParams = LinearLayout.LayoutParams(
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
@ -195,8 +167,6 @@ class ProgressionScreen @JvmOverloads constructor(
|
||||||
* Apply the current theme to the progression screen
|
* Apply the current theme to the progression screen
|
||||||
*/
|
*/
|
||||||
fun applyTheme(themeId: String) {
|
fun applyTheme(themeId: String) {
|
||||||
currentTheme = themeId
|
|
||||||
|
|
||||||
// Get reference to the title text
|
// Get reference to the title text
|
||||||
val progressionTitle = findViewById<TextView>(R.id.progression_title)
|
val progressionTitle = findViewById<TextView>(R.id.progression_title)
|
||||||
val rewardsTitle = findViewById<TextView>(R.id.rewards_title)
|
val rewardsTitle = findViewById<TextView>(R.id.rewards_title)
|
||||||
|
@ -278,10 +248,10 @@ class ProgressionScreen @JvmOverloads constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update XP progress bar theme color
|
// Set theme color on XP progress bar
|
||||||
xpProgressBar.setThemeColor(xpThemeColor)
|
xpProgressBar.setThemeColor(xpThemeColor)
|
||||||
|
|
||||||
// Update reward card colors
|
// Update card colors for any existing reward cards
|
||||||
updateRewardCardColors(themeId)
|
updateRewardCardColors(themeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -289,7 +259,8 @@ class ProgressionScreen @JvmOverloads constructor(
|
||||||
* Update colors of existing reward cards to match the theme
|
* Update colors of existing reward cards to match the theme
|
||||||
*/
|
*/
|
||||||
private fun updateRewardCardColors(themeId: String) {
|
private fun updateRewardCardColors(themeId: String) {
|
||||||
val backgroundColor = when (themeId) {
|
// Color for card backgrounds based on theme
|
||||||
|
val cardBackgroundColor = when (themeId) {
|
||||||
PlayerProgressionManager.THEME_CLASSIC -> Color.BLACK
|
PlayerProgressionManager.THEME_CLASSIC -> Color.BLACK
|
||||||
PlayerProgressionManager.THEME_NEON -> Color.parseColor("#0D0221")
|
PlayerProgressionManager.THEME_NEON -> Color.parseColor("#0D0221")
|
||||||
PlayerProgressionManager.THEME_MONOCHROME -> Color.parseColor("#1A1A1A")
|
PlayerProgressionManager.THEME_MONOCHROME -> Color.parseColor("#1A1A1A")
|
||||||
|
@ -299,16 +270,28 @@ class ProgressionScreen @JvmOverloads constructor(
|
||||||
else -> Color.BLACK
|
else -> Color.BLACK
|
||||||
}
|
}
|
||||||
|
|
||||||
for (i in 0 until rewardsContainer.childCount) {
|
// Text color for rewards based on theme
|
||||||
val card = rewardsContainer.getChildAt(i) as? CardView
|
val rewardTextColor = when (themeId) {
|
||||||
card?.setCardBackgroundColor(backgroundColor)
|
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
|
||||||
* Public method to handle continue action via gamepad
|
for (i in 0 until rewardsContainer.childCount) {
|
||||||
*/
|
val card = rewardsContainer.getChildAt(i) as? CardView
|
||||||
fun performContinue() {
|
card?.let {
|
||||||
continueButton.performClick()
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package com.pixelmintdrop.ui
|
package com.mintris.ui
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
@ -9,10 +9,8 @@ import android.widget.FrameLayout
|
||||||
import android.widget.GridLayout
|
import android.widget.GridLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.cardview.widget.CardView
|
import androidx.cardview.widget.CardView
|
||||||
import com.pixelmintdrop.R
|
import com.mintris.R
|
||||||
import com.pixelmintdrop.model.PlayerProgressionManager
|
import com.mintris.model.PlayerProgressionManager
|
||||||
import android.graphics.drawable.GradientDrawable
|
|
||||||
import android.util.Log
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UI component for selecting game themes
|
* UI component for selecting game themes
|
||||||
|
@ -29,19 +27,11 @@ class ThemeSelector @JvmOverloads constructor(
|
||||||
// Callback when a theme is selected
|
// Callback when a theme is selected
|
||||||
var onThemeSelected: ((String) -> Unit)? = null
|
var onThemeSelected: ((String) -> Unit)? = null
|
||||||
|
|
||||||
// Currently selected theme (persisted)
|
// Currently selected theme
|
||||||
private var selectedTheme: String = PlayerProgressionManager.THEME_CLASSIC
|
private var selectedTheme: String = PlayerProgressionManager.THEME_CLASSIC
|
||||||
|
|
||||||
// Theme cards map (themeId -> CardView)
|
// Theme cards
|
||||||
private val themeCards = mutableMapOf<String, 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 {
|
init {
|
||||||
// Inflate the layout
|
// Inflate the layout
|
||||||
|
@ -56,49 +46,27 @@ class ThemeSelector @JvmOverloads constructor(
|
||||||
* Update the theme selector with unlocked themes
|
* Update the theme selector with unlocked themes
|
||||||
*/
|
*/
|
||||||
fun updateThemes(unlockedThemes: Set<String>, currentTheme: String) {
|
fun updateThemes(unlockedThemes: Set<String>, currentTheme: String) {
|
||||||
// Clear existing theme cards and ID list
|
// Clear existing theme cards
|
||||||
themesGrid.removeAllViews()
|
themesGrid.removeAllViews()
|
||||||
themeCards.clear()
|
themeCards.clear()
|
||||||
themeIdList.clear()
|
|
||||||
|
|
||||||
// Update selected theme
|
// Update selected theme
|
||||||
selectedTheme = currentTheme
|
selectedTheme = currentTheme
|
||||||
focusedThemeId = currentTheme // Initially focus the selected theme
|
|
||||||
focusedIndex = -1 // Reset index
|
|
||||||
|
|
||||||
// Get all possible themes and their details, sorted for consistent order
|
// Get all possible themes and their details
|
||||||
val allThemes = getThemes().entries.sortedWith(compareBy({ it.value.unlockLevel }, { it.value.displayName })).associate { it.key to it.value }
|
val allThemes = getThemes()
|
||||||
|
|
||||||
// Add theme cards to the grid and build the ID list
|
// Add theme cards to the grid
|
||||||
allThemes.forEach { (themeId, themeInfo) ->
|
allThemes.forEach { (themeId, themeInfo) ->
|
||||||
val isUnlocked = unlockedThemes.contains(themeId)
|
val isUnlocked = unlockedThemes.contains(themeId)
|
||||||
val isSelected = themeId == selectedTheme
|
val isSelected = themeId == selectedTheme
|
||||||
|
|
||||||
// Only add unlocked themes to the navigable list
|
|
||||||
if (isUnlocked) {
|
|
||||||
themeIdList.add(themeId)
|
|
||||||
}
|
|
||||||
|
|
||||||
val themeCard = createThemeCard(themeId, themeInfo, isUnlocked, isSelected)
|
val themeCard = createThemeCard(themeId, themeInfo, isUnlocked, isSelected)
|
||||||
themeCards[themeId] = themeCard
|
themeCards[themeId] = themeCard
|
||||||
themesGrid.addView(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()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a card for a theme
|
* Create a card for a theme
|
||||||
*/
|
*/
|
||||||
|
@ -118,6 +86,20 @@ class ThemeSelector @JvmOverloads constructor(
|
||||||
// Set card background color based on theme
|
// Set card background color based on theme
|
||||||
setCardBackgroundColor(themeInfo.primaryColor)
|
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
|
// Set card dimensions
|
||||||
val cardSize = resources.getDimensionPixelSize(R.dimen.theme_card_size)
|
val cardSize = resources.getDimensionPixelSize(R.dimen.theme_card_size)
|
||||||
layoutParams = GridLayout.LayoutParams().apply {
|
layoutParams = GridLayout.LayoutParams().apply {
|
||||||
|
@ -212,11 +194,38 @@ class ThemeSelector @JvmOverloads constructor(
|
||||||
// Set up click listener only for unlocked themes
|
// Set up click listener only for unlocked themes
|
||||||
if (isUnlocked) {
|
if (isUnlocked) {
|
||||||
card.setOnClickListener {
|
card.setOnClickListener {
|
||||||
// Clicking directly selects the theme
|
// Only trigger callback if this isn't already the selected theme
|
||||||
Log.d("ThemeSelector", "Theme card clicked: $themeId (isUnlocked=$isUnlocked)")
|
if (themeId != selectedTheme) {
|
||||||
focusedThemeId = themeId
|
// Update previously selected card
|
||||||
focusedIndex = themeIdList.indexOf(themeId)
|
themeCards[selectedTheme]?.let { prevCard ->
|
||||||
confirmSelection() // Directly confirm click selection
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -283,132 +292,4 @@ 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.pixelmintdrop.ui
|
package com.mintris.ui
|
||||||
|
|
||||||
import android.animation.ValueAnimator
|
import android.animation.ValueAnimator
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
@ -9,6 +9,8 @@ import android.graphics.RectF
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.animation.AccelerateDecelerateInterpolator
|
import android.view.animation.AccelerateDecelerateInterpolator
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.mintris.R
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom progress bar for displaying player XP with animation capabilities
|
* Custom progress bar for displaying player XP with animation capabilities
|
||||||
|
@ -97,8 +99,6 @@ class XPProgressBar @JvmOverloads constructor(
|
||||||
*/
|
*/
|
||||||
fun setThemeColor(color: Int) {
|
fun setThemeColor(color: Int) {
|
||||||
themeColor = color
|
themeColor = color
|
||||||
progressPaint.color = color
|
|
||||||
textPaint.color = color
|
|
||||||
levelBadgePaint.color = color
|
levelBadgePaint.color = color
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
File diff suppressed because it is too large
Load diff
|
@ -1,19 +0,0 @@
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,152 +0,0 @@
|
||||||
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,136 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,154 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,510 +0,0 @@
|
||||||
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,97 +0,0 @@
|
||||||
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,199 +0,0 @@
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,48 +0,0 @@
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,48 +0,0 @@
|
||||||
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,78 +0,0 @@
|
||||||
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,52 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
|
@ -1,431 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,187 +0,0 @@
|
||||||
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,7 +0,0 @@
|
||||||
<?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:viewportWidth="108"
|
||||||
android:viewportHeight="108">
|
android:viewportHeight="108">
|
||||||
|
|
||||||
<!-- T-piece -->
|
<!-- T-Tetromino -->
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FFFFFF"
|
android:fillColor="#FFFFFF"
|
||||||
android:pathData="M36,36h12v12h-12z" />
|
android:pathData="M36,36h12v12h-12z" />
|
||||||
|
@ -19,7 +19,7 @@
|
||||||
android:fillColor="#FFFFFF"
|
android:fillColor="#FFFFFF"
|
||||||
android:pathData="M48,48h12v12h-12z" />
|
android:pathData="M48,48h12v12h-12z" />
|
||||||
|
|
||||||
<!-- L-piece -->
|
<!-- L-Tetromino -->
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FFFFFF"
|
android:fillColor="#FFFFFF"
|
||||||
android:pathData="M36,60h12v12h-12z" />
|
android:pathData="M36,60h12v12h-12z" />
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
<?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>
|
|
|
@ -1,11 +0,0 @@
|
||||||
<?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>
|
|
|
@ -1,9 +0,0 @@
|
||||||
<?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>
|
|
|
@ -1,9 +0,0 @@
|
||||||
<?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>
|
|
|
@ -1,6 +0,0 @@
|
||||||
<?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>
|
|
|
@ -1,8 +0,0 @@
|
||||||
<?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>
|
|
|
@ -1,750 +0,0 @@
|
||||||
<?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>
|
|
|
@ -1,194 +0,0 @@
|
||||||
<?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>
|
|
|
@ -1,51 +0,0 @@
|
||||||
<?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>
|
|
|
@ -1,38 +0,0 @@
|
||||||
<?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_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent">
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
<com.pixelmintdrop.game.GameView
|
<com.mintris.game.GameView
|
||||||
android:id="@+id/gameView"
|
android:id="@+id/gameView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent" />
|
android:layout_height="match_parent" />
|
||||||
|
@ -33,7 +33,7 @@
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
<!-- Title Screen -->
|
<!-- Title Screen -->
|
||||||
<com.pixelmintdrop.game.TitleScreen
|
<com.mintris.game.TitleScreen
|
||||||
android:id="@+id/titleScreen"
|
android:id="@+id/titleScreen"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
|
@ -62,65 +62,45 @@
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:textSize="32sp"
|
android:textSize="24sp"
|
||||||
android:textStyle="bold"
|
android:fontFamily="sans-serif-light"
|
||||||
android:fontFamily="sans-serif"
|
tools:text="Score: 0" />
|
||||||
tools:text="score: 0" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/currentLevelText"
|
android:id="@+id/currentLevelText"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:textSize="32sp"
|
android:textSize="24sp"
|
||||||
android:textStyle="bold"
|
android:fontFamily="sans-serif-light"
|
||||||
android:fontFamily="sans-serif"
|
tools:text="Level: 1" />
|
||||||
tools:text="level: 1" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/linesText"
|
android:id="@+id/linesText"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:textSize="32sp"
|
android:textSize="24sp"
|
||||||
android:textStyle="bold"
|
android:fontFamily="sans-serif-light"
|
||||||
android:fontFamily="sans-serif"
|
tools:text="Lines: 0" />
|
||||||
tools:text="lines: 0" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/comboText"
|
android:id="@+id/comboText"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:textSize="32sp"
|
android:textSize="24sp"
|
||||||
android:textStyle="bold"
|
android:fontFamily="sans-serif-light"
|
||||||
android:fontFamily="sans-serif"
|
tools:text="Combo: 0" />
|
||||||
tools:text="combo: 0" />
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<!-- Next Piece Preview -->
|
<!-- Next Piece Preview -->
|
||||||
<LinearLayout
|
<com.mintris.game.NextPieceView
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_margin="16dp"
|
|
||||||
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:id="@+id/nextPieceView"
|
||||||
android:layout_width="80dp"
|
android:layout_width="80dp"
|
||||||
android:layout_height="80dp" />
|
android:layout_height="80dp"
|
||||||
</LinearLayout>
|
android:layout_margin="16dp"
|
||||||
|
android:layout_gravity="end" />
|
||||||
|
|
||||||
<!-- Settings button -->
|
<!-- Settings button -->
|
||||||
<ImageButton
|
<ImageButton
|
||||||
|
@ -135,16 +115,6 @@
|
||||||
android:src="@drawable/ic_pause" />
|
android:src="@drawable/ic_pause" />
|
||||||
</LinearLayout>
|
</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 -->
|
<!-- Game Over overlay -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/gameOverContainer"
|
android:id="@+id/gameOverContainer"
|
||||||
|
@ -161,18 +131,16 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/game_over"
|
android:text="@string/game_over"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:textSize="36sp"
|
android:textSize="24sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold" />
|
||||||
android:fontFamily="sans-serif" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/session_stats"
|
android:text="@string/session_stats"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:textSize="28sp"
|
android:textSize="20sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
android:fontFamily="sans-serif"
|
|
||||||
android:layout_marginTop="24dp" />
|
android:layout_marginTop="24dp" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
@ -181,9 +149,7 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:textSize="24sp"
|
android:textSize="18sp" />
|
||||||
android:textStyle="bold"
|
|
||||||
android:fontFamily="sans-serif" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/sessionLinesText"
|
android:id="@+id/sessionLinesText"
|
||||||
|
@ -191,9 +157,7 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="4dp"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:textSize="24sp"
|
android:textSize="18sp" />
|
||||||
android:textStyle="bold"
|
|
||||||
android:fontFamily="sans-serif" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/sessionPiecesText"
|
android:id="@+id/sessionPiecesText"
|
||||||
|
@ -201,9 +165,7 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="4dp"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:textSize="24sp"
|
android:textSize="18sp" />
|
||||||
android:textStyle="bold"
|
|
||||||
android:fontFamily="sans-serif" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/sessionTimeText"
|
android:id="@+id/sessionTimeText"
|
||||||
|
@ -211,9 +173,7 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="4dp"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:textSize="24sp"
|
android:textSize="18sp" />
|
||||||
android:textStyle="bold"
|
|
||||||
android:fontFamily="sans-serif" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/sessionLevelText"
|
android:id="@+id/sessionLevelText"
|
||||||
|
@ -221,18 +181,15 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="4dp"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:textSize="24sp"
|
android:textSize="18sp" />
|
||||||
android:textStyle="bold"
|
|
||||||
android:fontFamily="sans-serif" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/line_clears"
|
android:text="@string/line_clears"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:textSize="28sp"
|
android:textSize="20sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
android:fontFamily="sans-serif"
|
|
||||||
android:layout_marginTop="16dp" />
|
android:layout_marginTop="16dp" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
@ -241,9 +198,7 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="4dp"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:textSize="24sp"
|
android:textSize="18sp" />
|
||||||
android:textStyle="bold"
|
|
||||||
android:fontFamily="sans-serif" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/sessionDoublesText"
|
android:id="@+id/sessionDoublesText"
|
||||||
|
@ -251,9 +206,7 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="4dp"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:textSize="24sp"
|
android:textSize="18sp" />
|
||||||
android:textStyle="bold"
|
|
||||||
android:fontFamily="sans-serif" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/sessionTriplesText"
|
android:id="@+id/sessionTriplesText"
|
||||||
|
@ -261,19 +214,15 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="4dp"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:textSize="24sp"
|
android:textSize="18sp" />
|
||||||
android:textStyle="bold"
|
|
||||||
android:fontFamily="sans-serif" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/sessionQuadsText"
|
android:id="@+id/sessionTetrisesText"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="4dp"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:textSize="24sp"
|
android:textSize="18sp" />
|
||||||
android:textStyle="bold"
|
|
||||||
android:fontFamily="sans-serif" />
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/playAgainButton"
|
android:id="@+id/playAgainButton"
|
||||||
|
@ -282,14 +231,11 @@
|
||||||
android:layout_marginTop="32dp"
|
android:layout_marginTop="32dp"
|
||||||
android:background="@color/transparent"
|
android:background="@color/transparent"
|
||||||
android:text="@string/play"
|
android:text="@string/play"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white" />
|
||||||
android:textSize="24sp"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:fontFamily="sans-serif" />
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<!-- Player Progression Screen -->
|
<!-- Player Progression Screen -->
|
||||||
<com.pixelmintdrop.ui.ProgressionScreen
|
<com.mintris.ui.ProgressionScreen
|
||||||
android:id="@+id/progressionScreen"
|
android:id="@+id/progressionScreen"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
@ -305,6 +251,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="@color/black"
|
android:background="@color/black"
|
||||||
|
android:gravity="center"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:visibility="gone">
|
android:visibility="gone">
|
||||||
|
|
||||||
|
@ -313,7 +260,6 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:layout_marginTop="32dp"
|
|
||||||
android:layout_marginBottom="32dp">
|
android:layout_marginBottom="32dp">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
@ -324,11 +270,9 @@
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:textSize="24sp"
|
android:textSize="24sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
android:fontFamily="sans-serif"
|
android:layout_marginEnd="16dp" />
|
||||||
android:layout_marginEnd="16dp"
|
|
||||||
android:textAllCaps="false" />
|
|
||||||
|
|
||||||
<com.pixelmintdrop.ui.LevelBadge
|
<com.mintris.ui.LevelBadge
|
||||||
android:id="@+id/pauseLevelBadge"
|
android:id="@+id/pauseLevelBadge"
|
||||||
android:layout_width="48dp"
|
android:layout_width="48dp"
|
||||||
android:layout_height="48dp" />
|
android:layout_height="48dp" />
|
||||||
|
@ -336,21 +280,15 @@
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
android:id="@+id/pauseMenuScrollView"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1">
|
||||||
android:scrollbars="none"
|
|
||||||
android:overScrollMode="never"
|
|
||||||
android:fillViewport="false">
|
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:gravity="center"
|
android:gravity="center">
|
||||||
android:paddingTop="16dp"
|
|
||||||
android:paddingBottom="32dp">
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/pauseStartButton"
|
android:id="@+id/pauseStartButton"
|
||||||
|
@ -359,10 +297,7 @@
|
||||||
android:background="@color/transparent"
|
android:background="@color/transparent"
|
||||||
android:text="@string/start"
|
android:text="@string/start"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:textSize="24sp"
|
android:textSize="18sp" />
|
||||||
android:textStyle="bold"
|
|
||||||
android:fontFamily="sans-serif"
|
|
||||||
android:textAllCaps="false" />
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/resumeButton"
|
android:id="@+id/resumeButton"
|
||||||
|
@ -372,10 +307,7 @@
|
||||||
android:background="@color/transparent"
|
android:background="@color/transparent"
|
||||||
android:text="@string/resume"
|
android:text="@string/resume"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:textSize="24sp"
|
android:textSize="18sp" />
|
||||||
android:textStyle="bold"
|
|
||||||
android:fontFamily="sans-serif"
|
|
||||||
android:textAllCaps="false" />
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/pauseRestartButton"
|
android:id="@+id/pauseRestartButton"
|
||||||
|
@ -385,10 +317,7 @@
|
||||||
android:background="@color/transparent"
|
android:background="@color/transparent"
|
||||||
android:text="@string/restart"
|
android:text="@string/restart"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:textSize="24sp"
|
android:textSize="18sp" />
|
||||||
android:textStyle="bold"
|
|
||||||
android:fontFamily="sans-serif"
|
|
||||||
android:textAllCaps="false" />
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/highScoresButton"
|
android:id="@+id/highScoresButton"
|
||||||
|
@ -398,10 +327,7 @@
|
||||||
android:background="@color/transparent"
|
android:background="@color/transparent"
|
||||||
android:text="@string/high_scores"
|
android:text="@string/high_scores"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:textSize="24sp"
|
android:textSize="18sp" />
|
||||||
android:textStyle="bold"
|
|
||||||
android:fontFamily="sans-serif"
|
|
||||||
android:textAllCaps="false" />
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/statsButton"
|
android:id="@+id/statsButton"
|
||||||
|
@ -411,18 +337,15 @@
|
||||||
android:background="@color/transparent"
|
android:background="@color/transparent"
|
||||||
android:text="@string/stats"
|
android:text="@string/stats"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:textSize="24sp"
|
android:textSize="18sp" />
|
||||||
android:textStyle="bold"
|
|
||||||
android:fontFamily="sans-serif"
|
|
||||||
android:textAllCaps="false" />
|
|
||||||
|
|
||||||
<!-- Level Selection -->
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
android:id="@+id/levelSelectorContainer"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="32dp"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:gravity="center"
|
android:gravity="center">
|
||||||
android:layout_marginTop="16dp">
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/selectLevelText"
|
android:id="@+id/selectLevelText"
|
||||||
|
@ -430,10 +353,8 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/select_level"
|
android:text="@string/select_level"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:textSize="24sp"
|
android:textSize="16sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold" />
|
||||||
android:fontFamily="sans-serif"
|
|
||||||
android:textAllCaps="false" />
|
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
@ -448,9 +369,7 @@
|
||||||
android:background="@color/transparent"
|
android:background="@color/transparent"
|
||||||
android:text="−"
|
android:text="−"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:textSize="24sp"
|
android:textSize="24sp" />
|
||||||
android:textStyle="bold"
|
|
||||||
android:fontFamily="sans-serif" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/pauseLevelText"
|
android:id="@+id/pauseLevelText"
|
||||||
|
@ -460,8 +379,7 @@
|
||||||
android:text="1"
|
android:text="1"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:textSize="24sp"
|
android:textSize="24sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold" />
|
||||||
android:fontFamily="sans-serif" />
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/pauseLevelUpButton"
|
android:id="@+id/pauseLevelUpButton"
|
||||||
|
@ -470,13 +388,18 @@
|
||||||
android:background="@color/transparent"
|
android:background="@color/transparent"
|
||||||
android:text="+"
|
android:text="+"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:textSize="24sp"
|
android:textSize="24sp" />
|
||||||
android:textStyle="bold"
|
|
||||||
android:fontFamily="sans-serif" />
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<!-- Sound Toggle -->
|
<!-- 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" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/settingsButton"
|
android:id="@+id/settingsButton"
|
||||||
android:layout_width="200dp"
|
android:layout_width="200dp"
|
||||||
|
@ -485,17 +408,13 @@
|
||||||
android:background="@color/transparent"
|
android:background="@color/transparent"
|
||||||
android:text="@string/sound_on"
|
android:text="@string/sound_on"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:textSize="24sp"
|
android:textSize="18sp" />
|
||||||
android:textStyle="bold"
|
|
||||||
android:fontFamily="sans-serif"
|
|
||||||
android:textAllCaps="false" />
|
|
||||||
|
|
||||||
<!-- Music Toggle -->
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:gravity="center"
|
android:gravity="center_vertical"
|
||||||
android:layout_marginTop="16dp">
|
android:layout_marginTop="16dp">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
@ -504,151 +423,18 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/music"
|
android:text="@string/music"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:textSize="24sp"
|
android:textSize="18sp"
|
||||||
android:textStyle="bold"
|
android:layout_marginEnd="16dp" />
|
||||||
android:fontFamily="sans-serif"
|
|
||||||
android:layout_marginEnd="16dp"
|
|
||||||
android:textAllCaps="false" />
|
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/musicToggle"
|
android:id="@+id/musicToggle"
|
||||||
android:layout_width="48dp"
|
android:layout_width="48dp"
|
||||||
android:layout_height="48dp"
|
android:layout_height="48dp"
|
||||||
android:background="@color/transparent"
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
android:src="@drawable/ic_volume_up"
|
android:contentDescription="@string/toggle_music"
|
||||||
android:contentDescription="@string/music" />
|
android:padding="12dp"
|
||||||
|
android:src="@drawable/ic_volume_up" />
|
||||||
</LinearLayout>
|
</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>
|
</LinearLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
|
@ -101,7 +101,7 @@
|
||||||
android:layout_marginBottom="4dp"/>
|
android:layout_marginBottom="4dp"/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/totalQuadsText"
|
android:id="@+id/totalTetrisesText"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
<?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>
|
|
|
@ -1,122 +0,0 @@
|
||||||
<?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_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:padding="16dp">
|
android:padding="16dp"
|
||||||
|
android:background="@color/black">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/progression_title"
|
android:id="@+id/progression_title"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="level up"
|
android:text="LEVEL UP"
|
||||||
android:textSize="48sp"
|
android:textSize="24sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
android:fontFamily="sans-serif"
|
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:layout_marginTop="16dp"
|
android:layout_marginTop="16dp"
|
||||||
|
@ -22,15 +22,13 @@
|
||||||
android:id="@+id/player_level_text"
|
android:id="@+id/player_level_text"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="player level: 1"
|
android:text="Player Level: 1"
|
||||||
android:textSize="36sp"
|
android:textSize="20sp"
|
||||||
android:textStyle="bold"
|
|
||||||
android:fontFamily="sans-serif"
|
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:layout_marginBottom="24dp" />
|
android:layout_marginBottom="24dp" />
|
||||||
|
|
||||||
<com.pixelmintdrop.ui.XPProgressBar
|
<com.mintris.ui.XPProgressBar
|
||||||
android:id="@+id/xp_progress_bar"
|
android:id="@+id/xp_progress_bar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="50dp"
|
android:layout_height="50dp"
|
||||||
|
@ -40,11 +38,10 @@
|
||||||
android:id="@+id/xp_gain_text"
|
android:id="@+id/xp_gain_text"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="+0 xp"
|
android:text="+0 XP"
|
||||||
android:textSize="42sp"
|
android:textSize="22sp"
|
||||||
android:textStyle="bold"
|
|
||||||
android:fontFamily="sans-serif"
|
|
||||||
android:textColor="#50C878"
|
android:textColor="#50C878"
|
||||||
|
android:textStyle="bold"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
android:layout_marginBottom="24dp" />
|
android:layout_marginBottom="24dp" />
|
||||||
|
@ -53,10 +50,9 @@
|
||||||
android:id="@+id/rewards_title"
|
android:id="@+id/rewards_title"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="rewards unlocked"
|
android:text="REWARDS UNLOCKED"
|
||||||
android:textSize="36sp"
|
android:textSize="20sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
android:fontFamily="sans-serif"
|
|
||||||
android:textColor="#FFD700"
|
android:textColor="#FFD700"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
|
@ -80,10 +76,8 @@
|
||||||
android:id="@+id/continue_button"
|
android:id="@+id/continue_button"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="continue"
|
android:text="CONTINUE"
|
||||||
android:textSize="32sp"
|
android:textSize="18sp"
|
||||||
android:textStyle="bold"
|
|
||||||
android:fontFamily="sans-serif"
|
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:layout_marginTop="16dp"
|
android:layout_marginTop="16dp"
|
||||||
android:layout_marginBottom="16dp"
|
android:layout_marginBottom="16dp"
|
||||||
|
|
|
@ -9,11 +9,10 @@
|
||||||
android:id="@+id/available_themes_label"
|
android:id="@+id/available_themes_label"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="available themes"
|
android:text="AVAILABLE THEMES"
|
||||||
android:textColor="@android:color/white"
|
android:textColor="@android:color/white"
|
||||||
android:textSize="24sp"
|
android:textSize="18sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
android:fontFamily="sans-serif"
|
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:layout_marginBottom="16dp" />
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
|
|
Binary file not shown.
|
@ -1,10 +1,8 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<color name="black">#FF000000</color>
|
<color name="black">#000000</color>
|
||||||
<color name="white">#FFFFFFFF</color>
|
<color name="white">#FFFFFF</color>
|
||||||
<color name="gray_dark">#222222</color>
|
<color name="gray_dark">#222222</color>
|
||||||
<color name="gray_light">#CCCCCC</color>
|
<color name="gray_light">#CCCCCC</color>
|
||||||
<color name="transparent">#00000000</color>
|
<color name="transparent">#00000000</color>
|
||||||
<color name="switch_track_color">#33FFFFFF</color>
|
|
||||||
<color name="switch_thumb_color">#FFFFFF</color>
|
|
||||||
</resources>
|
</resources>
|
|
@ -1,69 +1,49 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Pixel Mint Drop</string>
|
<string name="app_name">Mintris</string>
|
||||||
<string name="game_over">game over</string>
|
<string name="game_over">Game Over</string>
|
||||||
<string name="score">score</string>
|
<string name="score">Score</string>
|
||||||
<string name="level">level</string>
|
<string name="level">Level</string>
|
||||||
<string name="lines">lines</string>
|
<string name="lines">Lines</string>
|
||||||
<string name="next">next</string>
|
<string name="next">Next</string>
|
||||||
<string name="play">play again</string>
|
<string name="play">Play Again</string>
|
||||||
<string name="resume">resume</string>
|
<string name="resume">Resume</string>
|
||||||
<string name="pause">pause</string>
|
<string name="pause">PAUSE</string>
|
||||||
<string name="settings">settings</string>
|
<string name="settings">Settings</string>
|
||||||
<string name="start">start game</string>
|
<string name="start">Start Game</string>
|
||||||
<string name="restart">restart</string>
|
<string name="restart">Restart</string>
|
||||||
<string name="select_level">select level</string>
|
<string name="select_level">Select Level</string>
|
||||||
<string name="sound_on">sound: on</string>
|
<string name="sound_on">Sound: On</string>
|
||||||
<string name="sound_off">sound: off</string>
|
<string name="sound_off">Sound: Off</string>
|
||||||
<string name="toggle_music">toggle music</string>
|
<string name="toggle_music">Toggle music</string>
|
||||||
<string name="high_scores">high scores</string>
|
<string name="high_scores">High Scores</string>
|
||||||
<string name="new_high_score">new high score!</string>
|
<string name="new_high_score">New High Score!</string>
|
||||||
<string name="save">save</string>
|
<string name="save">Save</string>
|
||||||
<string name="back">back</string>
|
<string name="back">Back</string>
|
||||||
|
|
||||||
<!-- Stats Screen -->
|
<!-- Stats Screen -->
|
||||||
<string name="lifetime_stats">lifetime stats</string>
|
<string name="lifetime_stats">Lifetime Stats</string>
|
||||||
<string name="general_stats">general stats</string>
|
<string name="best_performance">Best Performance</string>
|
||||||
<string name="best_performance">best performance</string>
|
<string name="total_games">Total Games: %d</string>
|
||||||
<string name="total_games">total games: %d</string>
|
<string name="total_score">Total Score: %d</string>
|
||||||
<string name="total_score">total score: %d</string>
|
<string name="total_lines">Total Lines: %d</string>
|
||||||
<string name="total_lines">total lines: %d</string>
|
<string name="total_pieces">Total Pieces: %d</string>
|
||||||
<string name="total_pieces">total pieces: %d</string>
|
<string name="total_time">Total Time: %s</string>
|
||||||
<string name="total_time">total time: %s</string>
|
<string name="max_level">Max Level: %d</string>
|
||||||
<string name="max_level">max level: %d</string>
|
<string name="max_score">Max Score: %d</string>
|
||||||
<string name="max_score">max score: %d</string>
|
<string name="max_lines">Max Lines: %d</string>
|
||||||
<string name="max_lines">max lines: %d</string>
|
<string name="stats">Stats</string>
|
||||||
<string name="stats">stats</string>
|
<string name="session_stats">Session Stats</string>
|
||||||
<string name="session_stats">session stats</string>
|
<string name="session_score">Score: %d</string>
|
||||||
<string name="session_score">score: %d</string>
|
<string name="session_lines">Lines: %d</string>
|
||||||
<string name="session_lines">lines: %d</string>
|
<string name="session_pieces">Pieces: %d</string>
|
||||||
<string name="session_pieces">pieces: %d</string>
|
<string name="session_time">Time: %s</string>
|
||||||
<string name="session_time">time: %s</string>
|
<string name="session_level">Level: %d</string>
|
||||||
<string name="session_level">level: %d</string>
|
<string name="line_clears">Line Clears</string>
|
||||||
<string name="line_clears">line clears</string>
|
<string name="singles">Singles: %d</string>
|
||||||
<string name="singles">singles: %d</string>
|
<string name="doubles">Doubles: %d</string>
|
||||||
<string name="doubles">doubles: %d</string>
|
<string name="triples">Triples: %d</string>
|
||||||
<string name="triples">triples: %d</string>
|
<string name="tetrises">Tetrises: %d</string>
|
||||||
<string name="quads">quads: %d</string>
|
<string name="reset_stats">Reset Stats</string>
|
||||||
<string name="reset_stats">reset stats</string>
|
<string name="music">Music</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>
|
</resources>
|
|
@ -1,7 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<!-- Base application theme -->
|
<!-- Base application theme -->
|
||||||
<style name="Theme.pixelmintdrop" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
<style name="Theme.Mintris" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||||
<item name="colorPrimary">@color/white</item>
|
<item name="colorPrimary">@color/white</item>
|
||||||
<item name="colorPrimaryDark">@color/black</item>
|
<item name="colorPrimaryDark">@color/black</item>
|
||||||
<item name="colorAccent">@color/white</item>
|
<item name="colorAccent">@color/white</item>
|
||||||
|
@ -11,7 +11,7 @@
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<!-- No action bar theme -->
|
<!-- No action bar theme -->
|
||||||
<style name="Theme.pixelmintdrop.NoActionBar">
|
<style name="Theme.Mintris.NoActionBar">
|
||||||
<item name="windowActionBar">false</item>
|
<item name="windowActionBar">false</item>
|
||||||
<item name="windowNoTitle">true</item>
|
<item name="windowNoTitle">true</item>
|
||||||
<item name="android:windowFullscreen">true</item>
|
<item name="android:windowFullscreen">true</item>
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
<?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>
|
|
|
@ -1,30 +0,0 @@
|
||||||
<?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 = "pixelmintdrop"
|
rootProject.name = "Mintris"
|
||||||
include ':app'
|
include ':app'
|
12
tatus
12
tatus
|
@ -1,8 +1,8 @@
|
||||||
app/src/main/java/com/pixelmintdrop/MainActivity.kt
|
app/src/main/java/com/mintris/MainActivity.kt
|
||||||
app/src/main/java/com/pixelmintdrop/audio/GameMusic.kt
|
app/src/main/java/com/mintris/audio/GameMusic.kt
|
||||||
app/src/main/java/com/pixelmintdrop/model/PlayerProgressionManager.kt
|
app/src/main/java/com/mintris/model/PlayerProgressionManager.kt
|
||||||
app/src/main/java/com/pixelmintdrop/ui/ProgressionScreen.kt
|
app/src/main/java/com/mintris/ui/ProgressionScreen.kt
|
||||||
app/src/main/java/com/pixelmintdrop/ui/ThemeSelector.kt
|
app/src/main/java/com/mintris/ui/ThemeSelector.kt
|
||||||
app/src/main/java/com/pixelmintdrop/ui/XPProgressBar.kt
|
app/src/main/java/com/mintris/ui/XPProgressBar.kt
|
||||||
app/src/main/res/drawable/rounded_button.xml
|
app/src/main/res/drawable/rounded_button.xml
|
||||||
app/src/main/res/layout/progression_screen.xml
|
app/src/main/res/layout/progression_screen.xml
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue