Compare commits

..

No commits in common. "main" and "0.4" have entirely different histories.
main ... 0.4

63 changed files with 2832 additions and 6771 deletions

131
README.md
View file

@ -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
### Core Gameplay
- Classic block-stacking mechanics
- Classic Tetris mechanics
- 7-bag randomizer for piece distribution
- Ghost piece preview
- Hard drop and soft drop
@ -29,7 +29,7 @@ The game features a comprehensive scoring system:
- Single line: 40 points
- Double: 100 points
- Triple: 300 points
- Quad (4 lines): 1200 points
- Tetris (4 lines): 1200 points
#### Multipliers
@ -48,15 +48,15 @@ The game features a comprehensive scoring system:
- 4 combos: 2.5x
- 5+ combos: 3.0x
3. **Back-to-Back Quad**
- 50% bonus (1.5x) for consecutive quad clears
- Resets if a non-quad clear is performed
3. **Back-to-Back Tetris**
- 50% bonus (1.5x) for consecutive Tetris clears
- Resets if a non-Tetris clear is performed
4. **Perfect Clear**
- 2x for single line
- 3x for double
- 4x for triple
- 5x for quad
- 5x for Tetris
- Awarded when clearing lines without leaving blocks
5. **All Clear**
@ -102,124 +102,11 @@ The game features a comprehensive scoring system:
- Follows Material Design guidelines
- Implements high score persistence using SharedPreferences
## Project Improvements and Best Practices
### Performance Optimizations
The codebase includes several performance optimizations:
1. **Release Build Configuration**
- Minification enabled to reduce APK size
- Resource shrinking to remove unused resources
- ProGuard rules to optimize while preserving critical classes
2. **Memory Management**
- Proper lifecycle handling to prevent memory leaks
- Resource cleanup through `releaseResources()` methods
- Efficient bitmap handling with reuse when possible
3. **Rendering Efficiency**
- Custom view invalidation limited to areas that need updating
- Hardware acceleration for canvas operations
- Bitmap caching for frequently used graphics
### Code Organization
The codebase follows good architecture practices:
1. **Package Structure**
- `model`: Data classes and game logic
- `game`: Core gameplay implementation
- `ui`: User interface components
- `audio`: Sound and music management
- `accessibility`: Accessibility helpers
2. **Responsibility Separation**
- `GameLifecycleManager`: Handles lifecycle events
- `GameUIManager`: Manages UI state and updates
- `GameAccessibilityHelper`: Improves accessibility features
- `GamepadController`: Manages gamepad input
### Google Play Compliance
The app meets Google Play standards:
1. **Manifest Configuration**
- Proper permissions declaration
- Screen orientation handling
- Full backup rules for user data
2. **Accessibility Support**
- Content descriptions for UI elements
- Color contrast considerations
- Screen reader compatibility
3. **Shortcuts and Deep Links**
- App shortcuts for quick actions
- Proper intent handling
### Usage Examples
#### Lifecycle Management
```kotlin
// In your activity
private lateinit var lifecycleManager: GameLifecycleManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleManager = GameLifecycleManager(this)
}
override fun onPause() {
super.onPause()
lifecycleManager.onPause(gameView, gameMusic, statsManager, highScoreManager)
}
override fun onResume() {
super.onResume()
lifecycleManager.onResume(gameView, gameMusic, isMusicEnabled)
}
override fun onDestroy() {
lifecycleManager.onDestroy(gameView, gameMusic, statsManager, highScoreManager)
super.onDestroy()
}
```
#### Accessibility Implementation
```kotlin
// In your activity
private lateinit var accessibilityHelper: GameAccessibilityHelper
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
accessibilityHelper = GameAccessibilityHelper(this)
// Setup accessibility descriptions for controls
accessibilityHelper.setupAccessibilityDescriptions(
leftButton, rightButton, rotateButton, dropButton,
holdButton, pauseButton, gameView, holdPieceView, nextPieceView
)
}
// When level changes
private fun onLevelUp(newLevel: Int) {
accessibilityHelper.announceLevelUp(gameView, newLevel)
}
// When game ends
private fun onGameOver(score: Long) {
accessibilityHelper.announceGameOver(gameView, score)
}
```
## Building from Source
1. Clone the repository:
```bash
git clone https://github.com/cmclark00/pixelmintdrop.git
git clone https://github.com/cmclark00/mintris.git
```
2. Open the project in Android Studio

View file

@ -4,13 +4,13 @@ plugins {
}
android {
namespace "com.pixelmintdrop"
compileSdk 35
namespace 'com.mintris'
compileSdk 34
defaultConfig {
applicationId "com.pixelmintdrop"
applicationId "com.mintris"
minSdk 30
targetSdk 35
targetSdk 34
versionCode 1
versionName "1.0"
@ -19,15 +19,13 @@ android {
buildTypes {
release {
minifyEnabled true
shrinkResources true
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
buildFeatures {
viewBinding true
dataBinding true
}
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 {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.12.0'

View file

@ -6,40 +6,7 @@
# http://developer.android.com/guide/developing/tools/proguard.html
# Keep models intact
-keep class com.pixelmintdrop.model.** { *; }
# Keep game classes intact to prevent issues
-keep class com.pixelmintdrop.game.** { *; }
# Preserve critical classes that might be used through reflection
-keep class com.pixelmintdrop.audio.GameMusic { *; }
-keep class com.pixelmintdrop.ui.** { *; }
# Keep all public methods in the MainActivity
-keepclassmembers class com.pixelmintdrop.MainActivity {
public *;
}
# Keep serializable and parcelable classes for proper game state saving
-keepnames class * implements java.io.Serializable
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
!static !transient <fields>;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
# Preserve line number information for debugging stack traces
-keepattributes SourceFile,LineNumberTable
# Keep Gson usage intact
-keep class com.google.gson.** { *; }
-keep class * implements com.google.gson.TypeAdapterFactory
-keep class * implements com.google.gson.JsonSerializer
-keep class * implements com.google.gson.JsonDeserializer
-keep class com.mintris.model.** { *; }
# Uncomment this to preserve the line number information for
# debugging stack traces.

View file

@ -7,46 +7,38 @@
<application
android:allowBackup="true"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.pixelmintdrop">
android:theme="@style/Theme.Mintris">
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.pixelmintdrop.NoActionBar"
android:screenOrientation="portrait"
android:theme="@style/Theme.Mintris.NoActionBar"
android:immersive="true"
android:resizeableActivity="false"
android:excludeFromRecents="false"
android:screenOrientation="portrait"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode">
android:excludeFromRecents="false">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity>
<activity
android:name=".HighScoreEntryActivity"
android:theme="@style/Theme.AppCompat.NoActionBar"
android:exported="false"
android:screenOrientation="portrait" />
android:exported="false" />
<activity
android:name=".HighScoresActivity"
android:exported="false"
android:screenOrientation="portrait"
android:theme="@style/Theme.AppCompat.NoActionBar" />
<activity
android:name=".StatsActivity"
android:exported="false"
android:screenOrientation="portrait"
android:theme="@style/Theme.AppCompat.NoActionBar" />
</application>
</manifest>

View file

@ -1,15 +1,17 @@
package com.pixelmintdrop
package com.mintris
import android.app.Activity
import android.os.Bundle
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import com.pixelmintdrop.databinding.HighScoreEntryBinding
import com.pixelmintdrop.model.HighScore
import com.pixelmintdrop.model.HighScoreManager
import com.pixelmintdrop.model.PlayerProgressionManager
import com.mintris.databinding.HighScoreEntryBinding
import com.mintris.model.HighScore
import com.mintris.model.HighScoreManager
import com.mintris.model.PlayerProgressionManager
import android.graphics.Color
import android.view.KeyEvent
import android.view.InputDevice
class HighScoreEntryActivity : AppCompatActivity() {
private lateinit var binding: HighScoreEntryBinding
@ -37,55 +39,20 @@ class HighScoreEntryActivity : AppCompatActivity() {
binding.scoreText.text = "Score: $score"
binding.saveButton.setOnClickListener {
saveScore()
}
}
private fun saveScore() {
// Only allow saving once
if (!hasSaved) {
val name = binding.nameInput.text.toString().trim()
if (name.isNotEmpty()) {
hasSaved = true
val highScore = HighScore(name, score, 1)
highScoreManager.addHighScore(highScore)
// Set result and finish
setResult(Activity.RESULT_OK)
finish()
}
}
}
// Override onKeyDown to handle gamepad buttons
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
// Check if it's a gamepad input
if (event != null && isGamepadDevice(event.device)) {
when (keyCode) {
KeyEvent.KEYCODE_BUTTON_A,
KeyEvent.KEYCODE_DPAD_CENTER -> {
// A button saves the score
saveScore()
return true
}
KeyEvent.KEYCODE_BUTTON_B -> {
// B button cancels
setResult(Activity.RESULT_CANCELED)
// Only allow saving once
if (!hasSaved) {
val name = binding.nameInput.text.toString().trim()
if (name.isNotEmpty()) {
hasSaved = true
val highScore = HighScore(name, score, 1)
highScoreManager.addHighScore(highScore)
// Set result and finish
setResult(Activity.RESULT_OK)
finish()
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

View file

@ -1,16 +1,17 @@
package com.pixelmintdrop
package com.mintris
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import com.pixelmintdrop.databinding.HighScoresBinding
import com.pixelmintdrop.model.HighScoreAdapter
import com.pixelmintdrop.model.HighScoreManager
import com.pixelmintdrop.model.PlayerProgressionManager
import androidx.recyclerview.widget.RecyclerView
import com.mintris.databinding.HighScoresBinding
import com.mintris.model.HighScoreAdapter
import com.mintris.model.HighScoreManager
import com.mintris.model.PlayerProgressionManager
import android.graphics.Color
import android.util.Log
import android.view.KeyEvent
import android.view.InputDevice
class HighScoresActivity : AppCompatActivity() {
private lateinit var binding: HighScoresBinding
@ -112,29 +113,4 @@ class HighScoresActivity : AppCompatActivity() {
Log.e("HighScoresActivity", "Error in onResume", e)
}
}
// Handle gamepad buttons
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
// Check if it's a gamepad input
if (event != null && isGamepadDevice(event.device)) {
when (keyCode) {
KeyEvent.KEYCODE_BUTTON_B,
KeyEvent.KEYCODE_BACK -> {
// B button or Back button returns to previous screen
finish()
return true
}
}
}
return super.onKeyDown(keyCode, event)
}
// Helper function to check if the device is a gamepad
private fun isGamepadDevice(device: InputDevice?): Boolean {
if (device == null) return false
val sources = device.sources
return (sources and InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD ||
(sources and InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK
}
}

View file

@ -0,0 +1,758 @@
package com.mintris
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.view.View
import android.view.HapticFeedbackConstants
import androidx.appcompat.app.AppCompatActivity
import com.mintris.databinding.ActivityMainBinding
import com.mintris.game.GameHaptics
import com.mintris.game.GameView
import com.mintris.game.TitleScreen
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 com.mintris.ui.ThemeSelector
import com.mintris.ui.BlockSkinSelector
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.util.Log
import android.view.KeyEvent
class MainActivity : AppCompatActivity() {
companion object {
private const val TAG = "MainActivity"
}
// 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
private lateinit var themeSelector: ThemeSelector
private lateinit var blockSkinSelector: BlockSkinSelector
// 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)
themeSelector = binding.themeSelector
blockSkinSelector = binding.blockSkinSelector
// Set up progression screen
progressionScreen = binding.progressionScreen
progressionScreen.visibility = View.GONE
progressionScreen.onContinue = {
progressionScreen.visibility = View.GONE
binding.gameOverContainer.visibility = View.VISIBLE
}
// Load and apply theme preference
currentTheme = progressionManager.getSelectedTheme()
applyTheme(currentTheme)
// Load and apply block skin preference
gameView.setBlockSkin(progressionManager.getSelectedBlockSkin())
// Update block skin selector with current selection
blockSkinSelector.updateBlockSkins(
progressionManager.getUnlockedBlocks(),
gameView.getCurrentBlockSkin(),
progressionManager.getPlayerLevel()
)
// Set up game view
gameView.setGameBoard(gameBoard)
gameView.setHaptics(gameHaptics)
// Set up theme selector
themeSelector.onThemeSelected = { themeId: String ->
// 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 block skin selector
blockSkinSelector.onBlockSkinSelected = { skinId: String ->
// Apply the new block skin
gameView.setBlockSkin(skinId)
// Save the selection
progressionManager.setSelectedBlockSkin(skinId)
// Provide haptic feedback
gameHaptics.vibrateForPieceLock()
}
// 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 hold piece preview
binding.holdPieceView.setGameView(gameView)
gameBoard.onPieceLock = {
binding.holdPieceView.invalidate()
}
gameBoard.onPieceMove = {
binding.holdPieceView.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 ->
Log.d(TAG, "Received line clear callback: $lineCount lines")
// Use enhanced haptic feedback for line clears
if (isSoundEnabled) {
Log.d(TAG, "Sound is enabled, triggering haptic feedback")
try {
gameHaptics.vibrateForLineClear(lineCount)
Log.d(TAG, "Haptic feedback triggered successfully")
} catch (e: Exception) {
Log.e(TAG, "Error triggering haptic feedback", e)
}
} else {
Log.d(TAG, "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
showProgressionScreen(xpGained, newRewards)
// 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()
// Update block skin selector
blockSkinSelector.updateBlockSkins(
progressionManager.getUnlockedBlocks(),
gameView.getCurrentBlockSkin(),
progressionManager.getPlayerLevel()
)
}
/**
* 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) {
currentTheme = themeId
val themeColor = 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
}
// Get background color for the theme
val backgroundColor = 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
}
// Apply background color to root view
binding.root.setBackgroundColor(backgroundColor)
// Apply theme color to title screen
titleScreen.setThemeColor(themeColor)
titleScreen.setBackgroundColor(backgroundColor)
// Apply theme color to game over screen
binding.gameOverContainer.setBackgroundColor(backgroundColor)
binding.gameOverText.setTextColor(themeColor)
binding.scoreText.setTextColor(themeColor)
binding.currentLevelText.setTextColor(themeColor)
binding.linesText.setTextColor(themeColor)
binding.comboText.setTextColor(themeColor)
binding.playAgainButton.setTextColor(themeColor)
binding.playAgainButton.setBackgroundResource(android.R.color.transparent)
binding.playAgainButton.setTextSize(24f)
// Apply theme to progression screen (it will handle its own colors)
progressionScreen.applyTheme(themeId)
// Apply theme color to game view
gameView.setThemeColor(themeColor)
gameView.setBackgroundColor(backgroundColor)
// Save theme preference
progressionManager.setSelectedTheme(themeId)
}
/**
* 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)
}
private fun showProgressionScreen(xpGained: Long, newRewards: List<String>) {
// Apply theme before showing the screen
progressionScreen.applyTheme(currentTheme)
// Show the progression screen
binding.gameOverContainer.visibility = View.GONE
progressionScreen.visibility = View.VISIBLE
// Display progression data
progressionScreen.showProgress(progressionManager, xpGained, newRewards, currentTheme)
}
}

View file

@ -1,11 +1,13 @@
package com.pixelmintdrop
package com.mintris
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.pixelmintdrop.databinding.ActivityStatsBinding
import com.pixelmintdrop.model.StatsManager
import com.pixelmintdrop.model.PlayerProgressionManager
import com.mintris.databinding.ActivityStatsBinding
import com.mintris.model.StatsManager
import com.mintris.model.PlayerProgressionManager
import android.graphics.Color
import java.text.SimpleDateFormat
import java.util.*
@ -78,7 +80,7 @@ class StatsActivity : AppCompatActivity() {
binding.totalSinglesText.setTextColor(textColor)
binding.totalDoublesText.setTextColor(textColor)
binding.totalTriplesText.setTextColor(textColor)
binding.totalQuadsText.setTextColor(textColor)
binding.totalTetrisesText.setTextColor(textColor)
binding.maxLevelText.setTextColor(textColor)
binding.maxScoreText.setTextColor(textColor)
binding.maxLinesText.setTextColor(textColor)
@ -116,7 +118,7 @@ class StatsActivity : AppCompatActivity() {
binding.totalSinglesText.text = getString(R.string.singles, statsManager.getTotalSingles())
binding.totalDoublesText.text = getString(R.string.doubles, statsManager.getTotalDoubles())
binding.totalTriplesText.text = getString(R.string.triples, statsManager.getTotalTriples())
binding.totalQuadsText.text = getString(R.string.quads, statsManager.getTotalQuads())
binding.totalTetrisesText.text = getString(R.string.tetrises, statsManager.getTotalTetrises())
// Update best performance stats
binding.maxLevelText.text = getString(R.string.max_level, statsManager.getMaxLevel())

View file

@ -1,22 +1,20 @@
package com.pixelmintdrop.audio
package com.mintris.audio
import android.content.Context
import android.media.MediaPlayer
import android.media.AudioAttributes
import android.os.Build
import android.util.Log
import com.pixelmintdrop.R
import com.mintris.R
class GameMusic(private val context: Context) {
private var mediaPlayer: MediaPlayer? = null
private var gameOverPlayer: MediaPlayer? = null
private var isEnabled = true
private var isPrepared = false
init {
try {
setupMediaPlayer()
setupGameOverPlayer()
} catch (e: Exception) {
Log.e("GameMusic", "Error initializing: ${e.message}")
}
@ -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() {
if (isEnabled && mediaPlayer != null && isPrepared) {
try {
@ -148,25 +103,14 @@ class GameMusic(private val context: Context) {
fun isEnabled(): Boolean = isEnabled
/**
* Releases all media player resources to prevent memory leaks
*/
fun releaseResources() {
fun release() {
try {
Log.d("GameMusic", "Releasing MediaPlayer resources")
Log.d("GameMusic", "Releasing MediaPlayer")
mediaPlayer?.release()
gameOverPlayer?.release()
mediaPlayer = null
gameOverPlayer = null
isPrepared = false
} 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()
}
}

View file

@ -0,0 +1,94 @@
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
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
}
// 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
// More lines = longer and stronger vibration
val duration = when(lineCount) {
1 -> 50L // Single line: short vibration
2 -> 80L // Double line: slightly longer
3 -> 120L // Triple line: even longer
4 -> 200L // Tetris: longest vibration
else -> 50L
}
val amplitude = when(lineCount) {
1 -> 80 // Single line: mild vibration (80/255)
2 -> 120 // Double line: medium vibration (120/255)
3 -> 180 // Triple line: strong vibration (180/255)
4 -> 255 // Tetris: maximum vibration (255/255)
else -> 80
}
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 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)
}
}
}

View file

@ -1,4 +1,4 @@
package com.pixelmintdrop.game
package com.mintris.game
import android.content.Context
import android.graphics.BlurMaskFilter
@ -7,7 +7,8 @@ import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.View
import com.pixelmintdrop.model.GameBoard
import com.mintris.model.GameBoard
import com.mintris.model.Tetromino
import kotlin.math.min
/**

View file

@ -0,0 +1,99 @@
package com.mintris.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 Tetromino 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?.let {
it.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 + 2),
canvas.height.toFloat() / (height + 2)
)
// Center the piece in the preview area
val previewLeft = (canvas.width - width * previewBlockSize) / 2
val previewTop = (canvas.height - height * previewBlockSize) / 2
// 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 * previewBlockSize + previewBlockSize,
previewTop + height * previewBlockSize + previewBlockSize,
glowPaint
)
for (y in 0 until height) {
for (x in 0 until width) {
if (piece.isBlockAt(x, y)) {
val left = previewLeft + x * previewBlockSize
val top = previewTop + y * previewBlockSize
val right = left + previewBlockSize
val bottom = top + previewBlockSize
// Draw block with subtle glow
val rect = RectF(left + 1, top + 1, right - 1, bottom - 1)
canvas.drawRect(rect, blockPaint)
// Draw subtle border glow
val glowRect = RectF(left, top, right, bottom)
canvas.drawRect(glowRect, glowPaint)
}
}
}
}
}
}
}

View file

@ -1,4 +1,4 @@
package com.pixelmintdrop.game
package com.mintris.game
import android.content.Context
import android.graphics.Canvas
@ -10,9 +10,9 @@ import android.view.MotionEvent
import android.view.View
import java.util.Random
import android.util.Log
import com.pixelmintdrop.model.HighScoreManager
import com.pixelmintdrop.model.HighScore
import com.pixelmintdrop.model.PlayerProgressionManager
import com.mintris.model.HighScoreManager
import com.mintris.model.HighScore
import com.mintris.model.PlayerProgressionManager
import kotlin.math.abs
import androidx.core.graphics.withTranslation
import androidx.core.graphics.withScale
@ -33,7 +33,7 @@ class TitleScreen @JvmOverloads constructor(
private val random = Random()
private var width = 0
private var height = 0
private val piecesToAdd = mutableListOf<FallingPiece>()
private val tetrominosToAdd = mutableListOf<Tetromino>()
private val highScoreManager = HighScoreManager(context) // Pre-allocate HighScoreManager
// Touch handling variables
@ -50,8 +50,8 @@ class TitleScreen @JvmOverloads constructor(
private var themeColor = Color.WHITE
private var backgroundColor = Color.BLACK
// Define piece shapes (I, O, T, S, Z, J, L)
private val pieceShapes = arrayOf(
// Define tetromino shapes (I, O, T, S, Z, J, L)
private val tetrominoShapes = arrayOf(
// I
arrayOf(
intArrayOf(0, 0, 0, 0),
@ -96,8 +96,8 @@ class TitleScreen @JvmOverloads constructor(
)
)
// FallingPiece class to represent falling pieces
private class FallingPiece(
// Tetromino class to represent falling pieces
private class Tetromino(
var x: Float,
var y: Float,
val shape: Array<IntArray>,
@ -106,7 +106,7 @@ class TitleScreen @JvmOverloads constructor(
val rotation: Int = 0
)
private val pieces = mutableListOf<FallingPiece>()
private val tetrominos = mutableListOf<Tetromino>()
init {
// Title text settings
@ -138,14 +138,14 @@ class TitleScreen @JvmOverloads constructor(
alpha = 200
}
// General paint settings for pieces
// General paint settings for tetrominos
paint.apply {
color = themeColor
style = Paint.Style.FILL
isAntiAlias = true
}
// Glow paint settings for pieces
// Glow paint settings for tetrominos
glowPaint.apply {
color = themeColor
style = Paint.Style.FILL
@ -159,26 +159,26 @@ class TitleScreen @JvmOverloads constructor(
width = w
height = h
// Clear existing pieces
pieces.clear()
// Clear existing tetrominos
tetrominos.clear()
// Initialize some pieces
// Initialize some tetrominos
repeat(20) {
val piece = createRandomPiece()
pieces.add(piece)
val tetromino = createRandomTetromino()
tetrominos.add(tetromino)
}
}
private fun createRandomPiece(): FallingPiece {
private fun createRandomTetromino(): Tetromino {
val x = random.nextFloat() * (width - 150) + 50 // Keep away from edges
val y = -cellSize * 4 - (random.nextFloat() * height / 2)
val shapeIndex = random.nextInt(pieceShapes.size)
val shape = pieceShapes[shapeIndex]
val shapeIndex = random.nextInt(tetrominoShapes.size)
val shape = tetrominoShapes[shapeIndex]
val speed = 1f + random.nextFloat() * 2f
val scale = 0.8f + random.nextFloat() * 0.4f
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) {
@ -188,37 +188,37 @@ class TitleScreen @JvmOverloads constructor(
// Draw background using the current background color
canvas.drawColor(backgroundColor)
// Add any pending pieces
pieces.addAll(piecesToAdd)
piecesToAdd.clear()
// Add any pending tetrominos
tetrominos.addAll(tetrominosToAdd)
tetrominosToAdd.clear()
// Update and draw falling pieces
val piecesToRemove = mutableListOf<FallingPiece>()
// Update and draw falling tetrominos
val tetrominosToRemove = mutableListOf<Tetromino>()
for (piece in pieces) {
piece.y += piece.speed
for (tetromino in tetrominos) {
tetromino.y += tetromino.speed
// Remove pieces that have fallen off the screen
if (piece.y > height) {
piecesToRemove.add(piece)
piecesToAdd.add(createRandomPiece())
// Remove tetrominos that have fallen off the screen
if (tetromino.y > height) {
tetrominosToRemove.add(tetromino)
tetrominosToAdd.add(createRandomTetromino())
} else {
try {
// Draw the piece
for (y in 0 until piece.shape.size) {
for (x in 0 until piece.shape.size) {
if (piece.shape[y][x] == 1) {
// Draw the tetromino
for (y in 0 until tetromino.shape.size) {
for (x in 0 until tetromino.shape.size) {
if (tetromino.shape[y][x] == 1) {
val left = x * cellSize
val top = y * cellSize
val right = left + cellSize
val bottom = top + cellSize
// Draw block with glow effect
canvas.withTranslation(piece.x, piece.y) {
withScale(piece.scale, piece.scale) {
withRotation(piece.rotation.toFloat(),
piece.shape.size * cellSize / 2,
piece.shape.size * cellSize / 2) {
canvas.withTranslation(tetromino.x, tetromino.y) {
withScale(tetromino.scale, tetromino.scale) {
withRotation(tetromino.rotation.toFloat(),
tetromino.shape.size * cellSize / 2,
tetromino.shape.size * cellSize / 2) {
// Draw glow
canvas.drawRect(left - 8f, top - 8f, right + 8f, bottom + 8f, glowPaint)
// Draw block
@ -230,22 +230,21 @@ class TitleScreen @JvmOverloads constructor(
}
}
} catch (e: Exception) {
Log.e("TitleScreen", "Error drawing piece", e)
Log.e("TitleScreen", "Error drawing tetromino", e)
}
}
}
// Remove pieces that fell off the screen
pieces.removeAll(piecesToRemove)
// Remove tetrominos that fell off the screen
tetrominos.removeAll(tetrominosToRemove)
// Draw title
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
val highScores: List<HighScore> = highScoreManager.getHighScores()
val highScoreY = height * 0.5f
var lastHighScoreY = highScoreY
if (highScores.isNotEmpty()) {
// Calculate the starting X position to center the entire block of scores
val maxScoreWidth = highScorePaint.measureText("99. PLAYER: 999999")
@ -253,7 +252,6 @@ class TitleScreen @JvmOverloads constructor(
highScores.forEachIndexed { index: Int, score: HighScore ->
val y = highScoreY + (index * 80f)
lastHighScoreY = y // Track the last high score's Y position
// Pad the rank number to ensure alignment
val rank = (index + 1).toString().padStart(2, ' ')
// Pad the name to ensure score alignment
@ -262,15 +260,8 @@ class TitleScreen @JvmOverloads constructor(
}
}
// Draw "touch to start" prompt below the high scores
val promptY = if (resources.configuration.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE) {
// In landscape mode, position below the last high score with some padding
lastHighScoreY + 100f
} else {
// In portrait mode, use the original position
height * 0.7f
}
canvas.drawText("touch to start", width / 2f, promptY, promptPaint)
// Draw "touch to start" prompt
canvas.drawText("touch to start", width / 2f, height * 0.7f, promptPaint)
// Request another frame
invalidate()
@ -292,10 +283,10 @@ class TitleScreen @JvmOverloads constructor(
val deltaX = event.x - lastTouchX
val deltaY = event.y - lastTouchY
// Update piece positions
for (piece in pieces) {
piece.x += deltaX * 0.5f
piece.y += deltaY * 0.5f
// Update tetromino positions
for (tetromino in tetrominos) {
tetromino.x += deltaX * 0.5f
tetromino.y += deltaY * 0.5f
}
lastTouchX = event.x

View file

@ -0,0 +1,688 @@
package com.mintris.model
import android.util.Log
/**
* Represents the game board (grid) and manages game state
*/
class GameBoard(
val width: Int = 10,
val height: Int = 20
) {
companion object {
private const val TAG = "GameBoard"
}
// 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
private var isPlayerSoftDrop = false // Track if the drop is player-initiated
// 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
// Store the last cleared lines
private val lastClearedLines = mutableListOf<Int>()
// Add spawn protection variables
private var pieceSpawnTime = 0L
private val spawnGracePeriod = 250L // Changed from 150ms to 250ms
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.entries.toTypedArray())
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() {
Log.d(TAG, "spawnPiece() started - current states: isHardDropInProgress=$isHardDropInProgress, isPieceLocking=$isPieceLocking")
currentPiece = nextPiece
spawnNextPiece()
// Center the piece horizontally
currentPiece?.apply {
x = (width - getWidth()) / 2
y = 0
Log.d(TAG, "spawnPiece() - new piece spawned at position (${x},${y}), type=${type}")
// Set the spawn time for the grace period
pieceSpawnTime = System.currentTimeMillis()
// Check if the piece can be placed (Game Over condition)
if (!canMove(0, 0)) {
isGameOver = true
Log.d(TAG, "spawnPiece() - Game Over condition detected")
}
}
}
/**
* 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
// Only add soft drop points if it's a player-initiated drop
if (isPlayerSoftDrop) {
score += 1
}
onPieceMove?.invoke()
true
} else {
// Check if we're within the spawn grace period
val currentTime = System.currentTimeMillis()
if (currentTime - pieceSpawnTime < spawnGracePeriod) {
Log.d(TAG, "moveDown() - not locking piece due to spawn grace period (${currentTime - pieceSpawnTime}ms < ${spawnGracePeriod}ms)")
return false
}
lockPiece()
false
}
}
/**
* Player-initiated soft drop
*/
fun softDrop() {
isPlayerSoftDrop = true
moveDown()
isPlayerSoftDrop = false
}
/**
* Hard drop the current piece
*/
fun hardDrop() {
if (isHardDropInProgress || isPieceLocking) {
Log.d(TAG, "hardDrop() called but blocked: isHardDropInProgress=$isHardDropInProgress, isPieceLocking=$isPieceLocking")
return // Prevent multiple hard drops
}
// Check if we're within the spawn grace period
val currentTime = System.currentTimeMillis()
if (currentTime - pieceSpawnTime < spawnGracePeriod) {
Log.d(TAG, "hardDrop() - blocked due to spawn grace period (${currentTime - pieceSpawnTime}ms < ${spawnGracePeriod}ms)")
return
}
Log.d(TAG, "hardDrop() started - setting isHardDropInProgress=true")
isHardDropInProgress = true
val piece = currentPiece ?: return
// Count how many cells the piece will drop
var dropDistance = 0
while (canMove(0, dropDistance + 1)) {
dropDistance++
}
Log.d(TAG, "hardDrop() - piece will drop $dropDistance cells, position before: (${piece.x},${piece.y})")
// Move piece down until it can't move anymore
while (canMove(0, 1)) {
piece.y++
onPieceMove?.invoke()
}
Log.d(TAG, "hardDrop() - piece final position: (${piece.x},${piece.y})")
// Add hard drop points (2 points per cell)
score += dropDistance * 2
// 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) {
Log.d(TAG, "lockPiece() called but blocked: isPieceLocking=$isPieceLocking")
return // Prevent recursive locking
}
Log.d(TAG, "lockPiece() started - setting isPieceLocking=true, current isHardDropInProgress=$isHardDropInProgress")
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()
// IMPORTANT: Reset the hard drop flag before spawning a new piece
// This prevents the immediate hard drop of the next piece
if (isHardDropInProgress) {
Log.d(TAG, "lockPiece() - resetting isHardDropInProgress=false BEFORE spawning new piece")
isHardDropInProgress = false
}
// Log piece position before spawning new piece
Log.d(TAG, "lockPiece() - about to spawn new piece at y=${piece.y}, isHardDropInProgress=$isHardDropInProgress")
// Spawn new piece immediately
spawnPiece()
// Allow holding piece again after locking
canHold = true
// Reset locking state
isPieceLocking = false
Log.d(TAG, "lockPiece() completed - reset flags: isPieceLocking=false, isHardDropInProgress=$isHardDropInProgress")
}
/**
* 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--
}
// Store the last cleared lines
lastClearedLines.clear()
lastClearedLines.addAll(linesToClear)
// If lines were cleared, calculate score in background and trigger callback
if (shiftAmount > 0) {
// Log line clear
Log.d(TAG, "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 {
// Call the line clear callback with the cleared line count
try {
Log.d(TAG, "Triggering onLineClear callback with $shiftAmount lines")
val clearedLines = getLastClearedLines()
onLineClear?.invoke(shiftAmount, clearedLines)
Log.d(TAG, "onLineClear callback completed successfully")
} catch (e: Exception) {
Log.e(TAG, "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
/**
* Get the list of lines that were most recently cleared
*/
private fun getLastClearedLines(): List<Int> {
return lastClearedLines.toList()
}
/**
* Update the game state (called by game loop)
*/
fun update() {
if (!isGameOver) {
moveDown()
}
}
}

View file

@ -1,4 +1,4 @@
package com.pixelmintdrop.model
package com.mintris.model
data class HighScore(
val name: String,

View file

@ -1,4 +1,4 @@
package com.pixelmintdrop.model
package com.mintris.model
import android.graphics.Color
import android.view.LayoutInflater
@ -6,7 +6,7 @@ import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.pixelmintdrop.R
import com.mintris.R
class HighScoreAdapter : RecyclerView.Adapter<HighScoreAdapter.HighScoreViewHolder>() {
private var highScores: List<HighScore> = emptyList()

View file

@ -1,4 +1,4 @@
package com.pixelmintdrop.model
package com.mintris.model
import android.content.Context
import android.content.SharedPreferences
@ -12,7 +12,7 @@ class HighScoreManager(private val context: Context) {
private val type: Type = object : TypeToken<List<HighScore>>() {}.type
companion object {
private const val PREFS_NAME = "pixelmintdrop_highscores"
private const val PREFS_NAME = "mintris_highscores"
private const val KEY_HIGHSCORES = "highscores"
private const val MAX_HIGHSCORES = 5
}

View file

@ -1,7 +1,8 @@
package com.pixelmintdrop.model
package com.mintris.model
import android.content.Context
import android.content.SharedPreferences
import com.mintris.R
import kotlin.math.pow
import kotlin.math.roundToInt
import kotlin.math.min
@ -51,11 +52,10 @@ class PlayerProgressionManager(context: Context) {
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()
// Add default block skin if nothing is unlocked
if (unlockedBlocks.isEmpty()) {
unlockedBlocks.add("block_skin_1")
}
}
/**
@ -93,31 +93,24 @@ class PlayerProgressionManager(context: Context) {
/**
* Calculate XP from a game session based on score, lines, level, etc.
*/
fun calculateGameXP(
score: Int,
lines: Int,
level: Int,
timePlayedMs: Long,
quadCount: Int,
perfectClearCount: Int
): Long {
// Calculate base XP from score
val scoreXP = score * BASE_SCORE_XP * level
// Calculate XP from lines cleared
val linesXP = lines * BASE_LINES_XP * level
// Calculate quad bonus
val quadBonus = quadCount * BASE_QUAD_BONUS * level
// Calculate perfect clear bonus
val perfectClearBonus = perfectClearCount * BASE_PERFECT_CLEAR_BONUS * level
// Calculate time bonus (convert ms to seconds)
val timeBonus = (timePlayedMs / 1000.0) * BASE_TIME_XP * level
// Sum all XP components
return (scoreXP + linesXP + quadBonus + perfectClearBonus + timeBonus).toLong()
fun calculateGameXP(score: Int, lines: Int, level: Int, gameTime: Long,
tetrisCount: Int, perfectClearCount: Int): Long {
// Base XP from score with level multiplier (capped at level 10)
val cappedLevel = min(level, 10)
val scoreXP = (score * (1 + LEVEL_MULTIPLIER * cappedLevel)).toLong()
// XP from lines cleared (reduced for higher levels)
val linesXP = lines * XP_PER_LINE * (1 - (level - 1) * 0.05).coerceAtLeast(0.5)
// XP from special moves (reduced for higher levels)
val tetrisBonus = tetrisCount * TETRIS_XP_BONUS * (1 - (level - 1) * 0.05).coerceAtLeast(0.5)
val perfectClearBonus = perfectClearCount * PERFECT_CLEAR_XP_BONUS * (1 - (level - 1) * 0.05).coerceAtLeast(0.5)
// Time bonus (reduced for longer games)
val timeBonus = (gameTime / 60000) * TIME_XP_PER_MINUTE * (1 - (gameTime / 3600000) * 0.1).coerceAtLeast(0.5)
// Calculate total XP
return (scoreXP + linesXP + tetrisBonus + perfectClearBonus + timeBonus).toLong()
}
/**
@ -189,27 +182,11 @@ class PlayerProgressionManager(context: Context) {
}
}
// Check for block skin unlocks (start from skin 2 at level 7)
when (level) {
7 -> {
if (unlockedBlocks.add("block_skin_2")) {
newRewards.add("Unlocked Neon Block Skin!")
}
}
14 -> {
if (unlockedBlocks.add("block_skin_3")) {
newRewards.add("Unlocked Retro Block Skin!")
}
}
21 -> {
if (unlockedBlocks.add("block_skin_4")) {
newRewards.add("Unlocked Minimalist Block Skin!")
}
}
28 -> {
if (unlockedBlocks.add("block_skin_5")) {
newRewards.add("Unlocked Galaxy Block Skin!")
}
// Check for block skin unlocks
if (level % 7 == 0 && level <= 35) {
val blockSkin = "block_skin_${level / 7}"
if (unlockedBlocks.add(blockSkin)) {
newRewards.add("Unlocked New Block Skin!")
}
}
@ -228,12 +205,13 @@ class PlayerProgressionManager(context: Context) {
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")
// Check block skin unlocks
for (i in 1..5) {
val requiredLevel = i * 7
if (playerLevel >= requiredLevel) {
unlockedBlocks.add("block_skin_$i")
}
}
// Save any newly unlocked items
saveProgress()
@ -291,14 +269,14 @@ class PlayerProgressionManager(context: Context) {
// Add default theme
unlockedThemes.add(THEME_CLASSIC)
// Add default block skin (Level 1)
// Add default block skin
unlockedBlocks.add("block_skin_1")
saveProgress()
}
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_XP = "player_xp"
private const val KEY_TOTAL_XP_EARNED = "total_xp_earned"
@ -334,12 +312,6 @@ class PlayerProgressionManager(context: Context) {
THEME_MINIMALIST to 20,
THEME_GALAXY to 25
)
private const val BASE_SCORE_XP = 0.1 // XP per score point
private const val BASE_LINES_XP = 100.0 // XP per line cleared
private const val BASE_QUAD_BONUS = 500.0 // Bonus XP for clearing 4 lines at once
private const val BASE_PERFECT_CLEAR_BONUS = 1000.0 // Bonus XP for perfect clear
private const val BASE_TIME_XP = 1.0 // XP per second played
}
/**

View 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"
}
}

View file

@ -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 {
I, J, L, O, S, T, Z

View file

@ -0,0 +1,308 @@
package com.mintris.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.mintris.R
import com.mintris.model.PlayerProgressionManager
/**
* 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
private var selectedSkin: String = "block_skin_1"
// Block skin cards
private val skinCards = mutableMapOf<String, CardView>()
// 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
skinsGrid.removeAllViews()
skinCards.clear()
// Update selected skin
selectedSkin = currentSkin
// Get all possible skins and their details
val allSkins = getBlockSkins()
// Add skin cards to the grid
allSkins.forEach { (skinId, skinInfo) ->
val isUnlocked = unlockedSkins.contains(skinId) || playerLevel >= skinInfo.unlockLevel
val isSelected = skinId == selectedSkin
val skinCard = createBlockSkinCard(skinId, skinInfo, isUnlocked, isSelected)
skinCards[skinId] = skinCard
skinsGrid.addView(skinCard)
}
}
/**
* Set the selected skin with a visual effect
*/
fun setSelectedSkin(skinId: String) {
if (skinId == selectedSkin) return
// Update previously selected card
skinCards[selectedSkin]?.let { prevCard ->
prevCard.cardElevation = 2f
// Reset any special styling
prevCard.background = null
prevCard.setCardBackgroundColor(getBlockSkins()[selectedSkin]?.backgroundColor ?: Color.BLACK)
}
// Update visual state of newly selected card
skinCards[skinId]?.let { card ->
card.cardElevation = 12f
// Flash animation for selection feedback
val flashColor = Color.WHITE
val originalColor = getBlockSkins()[skinId]?.backgroundColor ?: Color.BLACK
// 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()
// Add special border to selected card
val gradientDrawable = android.graphics.drawable.GradientDrawable().apply {
setColor(originalColor)
setStroke(6, Color.WHITE) // Thicker border
cornerRadius = 12f
}
card.background = gradientDrawable
}
// Update selected skin
selectedSkin = skinId
}
/**
* 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)
// Add more noticeable visual indicator for selected skin
if (isSelected) {
setContentPadding(4, 4, 4, 4)
// Create a gradient drawable for the border
val gradientDrawable = android.graphics.drawable.GradientDrawable().apply {
setColor(skinInfo.backgroundColor)
setStroke(6, Color.WHITE) // Thicker border
cornerRadius = 12f
}
background = gradientDrawable
// Add glow effect via elevation
cardElevation = 12f
}
// Set card dimensions
val cardSize = resources.getDimensionPixelSize(R.dimen.theme_card_size)
layoutParams = GridLayout.LayoutParams().apply {
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
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 {
// Only trigger callback if this isn't already the selected skin
if (skinId != selectedSkin) {
// Update UI for selection
setSelectedSkin(skinId)
// Notify listener
onBlockSkinSelected?.invoke(skinId)
}
}
}
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
)
)
}
}

View file

@ -1,4 +1,4 @@
package com.pixelmintdrop.ui
package com.mintris.ui
import android.content.Context
import android.graphics.Canvas

View file

@ -1,4 +1,4 @@
package com.pixelmintdrop.ui
package com.mintris.ui
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
@ -12,8 +12,8 @@ import android.view.animation.OvershootInterpolator
import android.widget.LinearLayout
import android.widget.TextView
import androidx.cardview.widget.CardView
import com.pixelmintdrop.R
import com.pixelmintdrop.model.PlayerProgressionManager
import com.mintris.R
import com.mintris.model.PlayerProgressionManager
/**
* Screen that displays player progression, XP gain, and unlocked rewards
@ -304,11 +304,4 @@ class ProgressionScreen @JvmOverloads constructor(
card?.setCardBackgroundColor(backgroundColor)
}
}
/**
* Public method to handle continue action via gamepad
*/
fun performContinue() {
continueButton.performClick()
}
}

View file

@ -1,4 +1,4 @@
package com.pixelmintdrop.ui
package com.mintris.ui
import android.content.Context
import android.graphics.Color
@ -9,10 +9,8 @@ import android.widget.FrameLayout
import android.widget.GridLayout
import android.widget.TextView
import androidx.cardview.widget.CardView
import com.pixelmintdrop.R
import com.pixelmintdrop.model.PlayerProgressionManager
import android.graphics.drawable.GradientDrawable
import android.util.Log
import com.mintris.R
import com.mintris.model.PlayerProgressionManager
/**
* UI component for selecting game themes
@ -29,19 +27,11 @@ class ThemeSelector @JvmOverloads constructor(
// Callback when a theme is selected
var onThemeSelected: ((String) -> Unit)? = null
// Currently selected theme (persisted)
// Currently selected theme
private var selectedTheme: String = PlayerProgressionManager.THEME_CLASSIC
// Theme cards map (themeId -> CardView)
// Theme cards
private val themeCards = mutableMapOf<String, CardView>()
// Ordered list of theme IDs for navigation
private val themeIdList = mutableListOf<String>()
// Currently focused theme ID (for gamepad navigation within the selector)
private var focusedThemeId: String? = null
// Index of the currently focused theme in themeIdList
private var focusedIndex: Int = -1
// Flag indicating if the entire selector component has focus from the main menu
private var hasComponentFocus: Boolean = false
init {
// Inflate the layout
@ -56,47 +46,25 @@ class ThemeSelector @JvmOverloads constructor(
* Update the theme selector with unlocked themes
*/
fun updateThemes(unlockedThemes: Set<String>, currentTheme: String) {
// Clear existing theme cards and ID list
// Clear existing theme cards
themesGrid.removeAllViews()
themeCards.clear()
themeIdList.clear()
// Update selected theme
selectedTheme = currentTheme
focusedThemeId = currentTheme // Initially focus the selected theme
focusedIndex = -1 // Reset index
// Get all possible themes and their details, sorted for consistent order
val allThemes = getThemes().entries.sortedWith(compareBy({ it.value.unlockLevel }, { it.value.displayName })).associate { it.key to it.value }
// Get all possible themes and their details
val allThemes = getThemes()
// Add theme cards to the grid and build the ID list
// Add theme cards to the grid
allThemes.forEach { (themeId, themeInfo) ->
val isUnlocked = unlockedThemes.contains(themeId)
val isSelected = themeId == selectedTheme
// Only add unlocked themes to the navigable list
if (isUnlocked) {
themeIdList.add(themeId)
}
val themeCard = createThemeCard(themeId, themeInfo, isUnlocked, isSelected)
themeCards[themeId] = themeCard
themesGrid.addView(themeCard)
// Update focused index if this is the currently selected/focused theme
if (isUnlocked && themeId == focusedThemeId) {
focusedIndex = themeIdList.indexOf(themeId)
}
}
// Ensure focus index is valid if the previously focused theme is no longer available/unlocked
if (focusedIndex == -1 && themeIdList.isNotEmpty()) {
focusedIndex = 0
focusedThemeId = themeIdList[0]
}
// Apply initial focus highlight if the component has focus
highlightFocusedCard()
}
/**
@ -118,6 +86,20 @@ class ThemeSelector @JvmOverloads constructor(
// Set card background color based on theme
setCardBackgroundColor(themeInfo.primaryColor)
// Add more noticeable visual indicator for selected theme
if (isSelected) {
setContentPadding(4, 4, 4, 4)
// Create a gradient drawable for the border
val gradientDrawable = android.graphics.drawable.GradientDrawable().apply {
setColor(themeInfo.primaryColor)
setStroke(6, Color.WHITE) // Thicker border
cornerRadius = 12f
}
background = gradientDrawable
// Add glow effect via elevation
cardElevation = 12f
}
// Set card dimensions
val cardSize = resources.getDimensionPixelSize(R.dimen.theme_card_size)
layoutParams = GridLayout.LayoutParams().apply {
@ -212,11 +194,38 @@ class ThemeSelector @JvmOverloads constructor(
// Set up click listener only for unlocked themes
if (isUnlocked) {
card.setOnClickListener {
// Clicking directly selects the theme
Log.d("ThemeSelector", "Theme card clicked: $themeId (isUnlocked=$isUnlocked)")
focusedThemeId = themeId
focusedIndex = themeIdList.indexOf(themeId)
confirmSelection() // Directly confirm click selection
// Only trigger callback if this isn't already the selected theme
if (themeId != selectedTheme) {
// Update previously selected card
themeCards[selectedTheme]?.let { prevCard ->
prevCard.cardElevation = 2f
// Reset any special styling
prevCard.background = null
prevCard.setCardBackgroundColor(getThemes()[selectedTheme]?.primaryColor ?: Color.BLACK)
}
// Update visual state of newly selected card
card.cardElevation = 12f
// Flash animation for selection feedback
val flashColor = Color.WHITE
val originalColor = themeInfo.primaryColor
// Create animator for flash effect
val flashAnimator = android.animation.ValueAnimator.ofArgb(flashColor, originalColor)
flashAnimator.duration = 300 // 300ms
flashAnimator.addUpdateListener { animator ->
val color = animator.animatedValue as Int
card.setCardBackgroundColor(color)
}
flashAnimator.start()
// Update selected theme
selectedTheme = themeId
// Notify listener
onThemeSelected?.invoke(themeId)
}
}
}
@ -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
}
}
}

View file

@ -1,4 +1,4 @@
package com.pixelmintdrop.ui
package com.mintris.ui
import android.animation.ValueAnimator
import android.content.Context
@ -9,6 +9,8 @@ import android.graphics.RectF
import android.util.AttributeSet
import android.view.View
import android.view.animation.AccelerateDecelerateInterpolator
import androidx.core.content.ContextCompat
import com.mintris.R
/**
* Custom progress bar for displaying player XP with animation capabilities

File diff suppressed because it is too large Load diff

View file

@ -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
)
}
}

View file

@ -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"
}
}
}

View file

@ -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)
}
}
}

View file

@ -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
}
}
}

View file

@ -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
}
}

View file

@ -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)
}
}
}
}
}
}

View file

@ -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())
}
}

View file

@ -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()
}
}
}

View file

@ -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)
)
}
}
}

View file

@ -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()
}
}

View file

@ -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
}

View file

@ -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
}
}
}

View file

@ -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
}

View file

@ -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>

View file

@ -5,7 +5,7 @@
android:viewportWidth="108"
android:viewportHeight="108">
<!-- T-piece -->
<!-- T-Tetromino -->
<path
android:fillColor="#FFFFFF"
android:pathData="M36,36h12v12h-12z" />
@ -19,7 +19,7 @@
android:fillColor="#FFFFFF"
android:pathData="M48,48h12v12h-12z" />
<!-- L-piece -->
<!-- L-Tetromino -->
<path
android:fillColor="#FFFFFF"
android:pathData="M36,60h12v12h-12z" />

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -19,7 +19,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.pixelmintdrop.game.GameView
<com.mintris.game.GameView
android:id="@+id/gameView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
@ -33,7 +33,7 @@
</FrameLayout>
<!-- Title Screen -->
<com.pixelmintdrop.game.TitleScreen
<com.mintris.game.TitleScreen
android:id="@+id/titleScreen"
android:layout_width="0dp"
android:layout_height="0dp"
@ -116,7 +116,7 @@
android:fontFamily="sans-serif"
android:layout_marginBottom="4dp" />
<com.pixelmintdrop.game.NextPieceView
<com.mintris.game.NextPieceView
android:id="@+id/nextPieceView"
android:layout_width="80dp"
android:layout_height="80dp" />
@ -136,7 +136,7 @@
</LinearLayout>
<!-- Hold Piece Preview -->
<com.pixelmintdrop.game.HoldPieceView
<com.mintris.game.HoldPieceView
android:id="@+id/holdPieceView"
android:layout_width="60dp"
android:layout_height="60dp"
@ -266,7 +266,7 @@
android:fontFamily="sans-serif" />
<TextView
android:id="@+id/sessionQuadsText"
android:id="@+id/sessionTetrisesText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
@ -289,7 +289,7 @@
</LinearLayout>
<!-- Player Progression Screen -->
<com.pixelmintdrop.ui.ProgressionScreen
<com.mintris.ui.ProgressionScreen
android:id="@+id/progressionScreen"
android:layout_width="match_parent"
android:layout_height="match_parent"
@ -305,6 +305,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone">
@ -313,7 +314,6 @@
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:layout_marginTop="32dp"
android:layout_marginBottom="32dp">
<TextView
@ -328,7 +328,7 @@
android:layout_marginEnd="16dp"
android:textAllCaps="false" />
<com.pixelmintdrop.ui.LevelBadge
<com.mintris.ui.LevelBadge
android:id="@+id/pauseLevelBadge"
android:layout_width="48dp"
android:layout_height="48dp" />
@ -336,21 +336,17 @@
</LinearLayout>
<ScrollView
android:id="@+id/pauseMenuScrollView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:scrollbars="none"
android:overScrollMode="never"
android:fillViewport="false">
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
android:paddingTop="16dp"
android:paddingBottom="32dp">
android:paddingTop="16dp">
<Button
android:id="@+id/pauseStartButton"
@ -416,13 +412,13 @@
android:fontFamily="sans-serif"
android:textAllCaps="false" />
<!-- Level Selection -->
<LinearLayout
android:id="@+id/levelSelectorContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:orientation="vertical"
android:gravity="center"
android:layout_marginTop="16dp">
android:gravity="center">
<TextView
android:id="@+id/selectLevelText"
@ -476,7 +472,22 @@
</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" />
<!-- Block Skin Selector -->
<com.mintris.ui.BlockSkinSelector
android:id="@+id/blockSkinSelector"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginBottom="16dp" />
<Button
android:id="@+id/settingsButton"
android:layout_width="200dp"
@ -490,12 +501,11 @@
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:gravity="center_vertical"
android:layout_marginTop="16dp">
<TextView
@ -507,148 +517,18 @@
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
android:layout_marginEnd="16dp"
android:textAllCaps="false" />
android:textAllCaps="false"
android:layout_marginEnd="16dp" />
<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" />
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/toggle_music"
android:padding="12dp"
android:src="@drawable/ic_volume_up" />
</LinearLayout>
<!-- Customization Button -->
<Button
android:id="@+id/customizationButton"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@color/transparent"
android:text="@string/customization"
android:textColor="@color/white"
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
android:textAllCaps="false"
android:singleLine="true" />
</LinearLayout>
</ScrollView>
</LinearLayout>
<!-- Customization Menu overlay -->
<LinearLayout
android:id="@+id/customizationContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
android:orientation="vertical"
android:visibility="gone">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:layout_marginTop="32dp"
android:layout_marginBottom="32dp">
<TextView
android:id="@+id/customizationTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/customization"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
android:layout_marginEnd="16dp"
android:textAllCaps="false"
android:singleLine="true" />
<com.pixelmintdrop.ui.LevelBadge
android:id="@+id/customizationLevelBadge"
android:layout_width="48dp"
android:layout_height="48dp" />
</LinearLayout>
<ScrollView
android:id="@+id/customizationMenuScrollView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:scrollbars="none"
android:overScrollMode="never"
android:fillViewport="false">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
android:paddingTop="16dp"
android:paddingBottom="32dp">
<!-- Theme Selector -->
<com.pixelmintdrop.ui.ThemeSelector
android:id="@+id/customizationThemeSelector"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp" />
<!-- Block Skin Selector -->
<com.pixelmintdrop.ui.BlockSkinSelector
android:id="@+id/customizationBlockSkinSelector"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@drawable/menu_item_background"
android:padding="16dp" />
<!-- Random Mode Toggle -->
<LinearLayout
android:id="@+id/randomModeContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@drawable/menu_item_background"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="16dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/random_mode"
android:textSize="18sp"
android:textColor="@android:color/white" />
<Switch
android:id="@+id/randomModeSwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:scaleX="1.5"
android:scaleY="1.5"
android:thumbTint="@color/switch_thumb_color"
android:trackTint="@color/switch_track_color" />
</LinearLayout>
<!-- Back Button -->
<Button
android:id="@+id/customizationBackButton"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:background="@color/transparent"
android:text="@string/back"
android:textColor="@color/white"
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
android:textAllCaps="false" />
</LinearLayout>
</ScrollView>
</LinearLayout>

View file

@ -101,7 +101,7 @@
android:layout_marginBottom="4dp"/>
<TextView
android:id="@+id/totalQuadsText"
android:id="@+id/totalTetrisesText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"

View file

@ -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>

View file

@ -30,7 +30,7 @@
android:gravity="center"
android:layout_marginBottom="24dp" />
<com.pixelmintdrop.ui.XPProgressBar
<com.mintris.ui.XPProgressBar
android:id="@+id/xp_progress_bar"
android:layout_width="match_parent"
android:layout_height="50dp"

Binary file not shown.

View file

@ -1,10 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="black">#000000</color>
<color name="white">#FFFFFF</color>
<color name="gray_dark">#222222</color>
<color name="gray_light">#CCCCCC</color>
<color name="transparent">#00000000</color>
<color name="switch_track_color">#33FFFFFF</color>
<color name="switch_thumb_color">#FFFFFF</color>
</resources>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Pixel Mint Drop</string>
<string name="app_name">mintris</string>
<string name="game_over">game over</string>
<string name="score">score</string>
<string name="level">level</string>
@ -23,7 +23,6 @@
<!-- Stats Screen -->
<string name="lifetime_stats">lifetime stats</string>
<string name="general_stats">general stats</string>
<string name="best_performance">best performance</string>
<string name="total_games">total games: %d</string>
<string name="total_score">total score: %d</string>
@ -44,26 +43,7 @@
<string name="singles">singles: %d</string>
<string name="doubles">doubles: %d</string>
<string name="triples">triples: %d</string>
<string name="quads">quads: %d</string>
<string name="tetrises">tetrises: %d</string>
<string name="reset_stats">reset stats</string>
<string name="music">music</string>
<string name="customization">Customization</string>
<string name="random_mode">Random Mode</string>
<!-- Shortcuts -->
<string name="shortcut_new_game_short_label">Play</string>
<string name="shortcut_new_game_long_label">Start New Game</string>
<string name="shortcut_high_scores_short_label">Scores</string>
<string name="shortcut_high_scores_long_label">View High Scores</string>
<!-- Accessibility -->
<string name="accessibility_rotate_piece">Rotate piece</string>
<string name="accessibility_move_left">Move piece left</string>
<string name="accessibility_move_right">Move piece right</string>
<string name="accessibility_drop_piece">Drop piece</string>
<string name="accessibility_hold_piece">Hold current piece</string>
<string name="accessibility_game_board">Game board</string>
<string name="accessibility_next_piece">Next piece preview</string>
<string name="accessibility_held_piece">Held piece</string>
<string name="accessibility_pause_game">Pause game</string>
</resources>

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- 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="colorPrimaryDark">@color/black</item>
<item name="colorAccent">@color/white</item>
@ -11,7 +11,7 @@
</style>
<!-- No action bar theme -->
<style name="Theme.pixelmintdrop.NoActionBar">
<style name="Theme.Mintris.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:windowFullscreen">true</item>

View file

@ -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>

View file

@ -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>

View file

@ -1,2 +1,2 @@
rootProject.name = "pixelmintdrop"
rootProject.name = "Mintris"
include ':app'

12
tatus
View file

@ -1,8 +1,8 @@
app/src/main/java/com/pixelmintdrop/MainActivity.kt
app/src/main/java/com/pixelmintdrop/audio/GameMusic.kt
app/src/main/java/com/pixelmintdrop/model/PlayerProgressionManager.kt
app/src/main/java/com/pixelmintdrop/ui/ProgressionScreen.kt
app/src/main/java/com/pixelmintdrop/ui/ThemeSelector.kt
app/src/main/java/com/pixelmintdrop/ui/XPProgressBar.kt
app/src/main/java/com/mintris/MainActivity.kt
app/src/main/java/com/mintris/audio/GameMusic.kt
app/src/main/java/com/mintris/model/PlayerProgressionManager.kt
app/src/main/java/com/mintris/ui/ProgressionScreen.kt
app/src/main/java/com/mintris/ui/ThemeSelector.kt
app/src/main/java/com/mintris/ui/XPProgressBar.kt
app/src/main/res/drawable/rounded_button.xml
app/src/main/res/layout/progression_screen.xml