Compare commits

..

57 commits
0.2 ... main

Author SHA1 Message Date
cmclark00
7babaeca50 Update game piece handling and rename tetris to quad 2025-04-01 05:31:52 -04:00
cmclark00
e26c6ebd8c Remove Tetris references and rename to generic game pieces 2025-04-01 05:18:47 -04:00
cmclark00
df9957580e Add perfect clear tracking to StatsManager and fix XP calculation 2025-04-01 05:12:04 -04:00
cmclark00
22fd887037 Change references from tetris to quad for 4-line clears 2025-04-01 05:09:02 -04:00
cmclark00
38163c33a3 Fix namespace and applicationId in build.gradle to match package structure 2025-04-01 05:06:38 -04:00
cmclark00
f5f135ff27 Improve code quality, performance, and Google Play compliance 2025-03-31 21:08:37 -04:00
cmclark00
5cf8aec02a Fix: Add piece placement tracking to update session stats correctly 2025-03-31 20:30:06 -04:00
cmclark00
6958a864c1 Enhance block skin unlock logic and ensure default skin is always present. Update UI component checks for selection confirmation in BlockSkinSelector and ThemeSelector with added logging for better debugging. 2025-03-31 20:17:40 -04:00
cmclark00
915e207aca Fix hold piece view not showing after starting new game 2025-03-31 17:27:02 -04:00
cmclark00
3cd6080177 Hide all game UI elements on progression and game over screens 2025-03-31 17:20:16 -04:00
cmclark00
fad3e0863c Fix compilation errors with unresolved references 2025-03-31 17:15:58 -04:00
cmclark00
1f400238e5 Hide game controls on progression screen and game over 2025-03-31 17:12:22 -04:00
cmclark00
b60c106a08 Fix block skin changes in random mode with improved logging and UI updates 2025-03-31 17:08:16 -04:00
cmclark00
5faa780c62 Fix random mode to change themes and block skins every 10 lines/level up 2025-03-31 17:07:28 -04:00
cmclark00
b2a9c40539 Add random mode toggle and fix resource linking issues 2025-03-31 16:37:57 -04:00
cmclark00
0b78bf7833 feat: add B button support for exiting customization menu 2025-03-31 16:10:44 -04:00
cmclark00
419cd8cbdb fix: make customization button text stay on one line in portrait mode 2025-03-31 16:05:01 -04:00
cmclark00
71a8efff91 Fix customization menu navigation and layout issues: - Fix D-pad navigation between theme and block skin selectors - Prevent word wrapping in customization title - Add ThemeManager for theme color management - Improve menu item highlighting and focus handling 2025-03-31 16:02:30 -04:00
cmclark00
a8f095cf42 Add counterclockwise rotation support for gamepad controls
- Implemented rotateCounterClockwise method in GameBoard to handle piece rotation.
- Updated GameView to call rotateCounterClockwise and added haptic feedback.
- Modified GamepadController to support rotation with button B.
2025-03-31 15:20:54 -04:00
cmclark00
b739d0ec3a Make gamepad controls dialog scrollable and add missing controls 2025-03-31 13:58:30 -04:00
cmclark00
240f16be82 Fix pause menu scrolling and gamepad navigation: - Remove fillViewport from ScrollView to allow proper scrolling - Add explicit scrollToSelectedItem calls in menu navigation - Ensure selected item is always visible when navigating with gamepad 2025-03-31 13:46:44 -04:00
cmclark00
05a504bb21 Reordered pause menu items and fixed music icon resource 2025-03-31 13:42:09 -04:00
cmclark00
7d7090d7ea more gamepad support 2025-03-31 13:38:38 -04:00
cmclark00
7e4423efce Add hollow border to selected menu items and fix scroll view in landscape mode 2025-03-31 08:48:33 -04:00
cmclark00
c4f103ae1e Enhanced haptic feedback for gamepad users: - Added gamepad connection tracking - Increased vibration intensity by 50% when using gamepad - Adjusted vibration durations and amplitudes for better feedback 2025-03-31 08:40:54 -04:00
cmclark00
dbaebb8b60 Update MainActivity.kt 2025-03-31 08:30:59 -04:00
cmclark00
08c9f8a1ce Add full gamepad support to all screens including pause menu, progression screen, high scores, and game over 2025-03-31 08:01:47 -04:00
cmclark00
f4f40c4c34 Implement full gamepad menu support and title screen start 2025-03-31 07:52:01 -04:00
cmclark00
779fa8eab1 Adjust gamepad D-pad sensitivity by increasing continuous movement delay 2025-03-31 07:49:39 -04:00
Corey
36559eac4c Implemented continuous D-pad movement and Start button pause menu functionality 2025-03-31 07:35:58 -04:00
Corey
0ac25eb3a9 adding gamepad support 2025-03-31 04:52:11 -04:00
Corey
86424eac32 Update game layout and functionality in MainActivity and GameView 2025-03-31 04:05:50 -04:00
Corey
b481fb4e80 Enhance landscape support: Add click handlers for theme and block skin selectors, improve UI layout in landscape mode 2025-03-31 03:46:05 -04:00
cmclark00
94e8d313c2 Add enhanced game over animation with falling blocks, screen shake, and dynamic text effects 2025-03-30 19:39:35 -04:00
cmclark00
7dccad8d12 fix: prevent crash in game over animation by checking for zero blur radius 2025-03-30 19:14:16 -04:00
cmclark00
292ea656f8 Enhanced game over experience: Added louder game over sound, improved game over animation, and added haptic feedback 2025-03-30 19:02:27 -04:00
cmclark00
ce19427cca fix: hold piece now correctly uses next piece preview on first hold 2025-03-30 18:15:07 -04:00
cmclark00
03ff049bef feat: improve ghost piece visibility with three-layer design for better accessibility 2025-03-30 15:54:36 -04:00
cmclark00
e23d33e2e2 feat: improve touch controls - increase tap sensitivity for rotation and make hard drop more distinct 2025-03-30 15:45:19 -04:00
cmclark00
53c46c9864 feat: implement hold piece functionality with swipe up gesture 2025-03-29 23:20:49 -04:00
cmclark00
47c9bbddec feat: change hold piece to swipe up gesture and improve movement sensitivity 2025-03-29 23:20:14 -04:00
cmclark00
103a21d9b7 fix: update text styles and sizes across app to match mintris theme 2025-03-29 02:21:15 -04:00
cmclark00
68e8cb160f fix: theme colors and UI improvements 2025-03-29 01:59:14 -04:00
cmclark00
42b9bcfab4 fix: progression screen theme colors and XP bar styling 2025-03-29 01:58:53 -04:00
cmclark00
1980f15a46 Adjust XP progression and improve animations: - Increased XP requirements and curve for slower leveling - Enhanced XP gain animations with smoother transitions - Improved visual feedback for level progression 2025-03-29 00:04:44 -04:00
cmclark00
c6a4339931 Fix: Update theme loading to use PlayerProgressionManager consistently across all activities 2025-03-28 20:47:37 -04:00
cmclark00
83935d35a8 Fix: Replace minOf with min in PlayerProgressionManager 2025-03-28 20:43:40 -04:00
cmclark00
d0700202b7 Update game mechanics and haptic feedback implementation 2025-03-28 20:41:03 -04:00
cmclark00
af0082a6db refactor: modernize codebase - Use KTX extension functions for system services - Update performClick handling in touch events - Modernize back gesture handling with KTX - Improve vibrator service initialization 2025-03-28 20:21:25 -04:00
cmclark00
ebff618fa4 fix: balance progression system - Increased base XP requirements and curve - Added diminishing returns for higher levels - Reduced XP rewards for special moves - Capped level multiplier at level 10 - Added time-based XP reduction 2025-03-28 20:19:17 -04:00
cmclark00
7cdc9988cb fix: enhance block skins to match their respective themes - Classic: Clean white blocks with subtle glow - Neon: Strong glow effects with magenta colors - Retro: Pixelated look with highlights and shadows - Minimalist: Clean black blocks with subtle borders - Galaxy: Cosmic effects with gradient and sparkles 2025-03-28 20:17:44 -04:00
cmclark00
2774703df5 fix: persist block skin selection through app restarts 2025-03-28 19:36:14 -04:00
cmclark00
1c57c438ce Fix auto-drop issue for pieces near top of board, add spawn grace period and hard drop cooldown 2025-03-28 18:59:13 -04:00
cmclark00
a47d83d905 Improve neon block skin and enhance BlockSkinSelector. Made neon blocks more visible with semi-translucent fill and stronger borders, and improved BlockSkinSelector with instant visual feedback on selection. 2025-03-28 16:11:30 -04:00
cmclark00
5861644883 Added block skin selection feature and removed powerups 2025-03-28 15:45:20 -04:00
cmclark00
7614cef7e5 Add scoring for soft and hard drops, remove automatic falling points 2025-03-28 14:35:20 -04:00
cmclark00
9ab9b53407 Fix menu shifting issue and code cleanup. Fix menu shifting by adding fillViewport and padding to ScrollView. Added missing getLastClearedLines method. Improved code quality with proper logging constants and removed unused imports. 2025-03-28 12:33:42 -04:00
65 changed files with 7917 additions and 2898 deletions

131
README.md
View file

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

View file

@ -4,13 +4,13 @@ plugins {
}
android {
namespace 'com.mintris'
compileSdk 34
namespace "com.pixelmintdrop"
compileSdk 35
defaultConfig {
applicationId "com.mintris"
applicationId "com.pixelmintdrop"
minSdk 30
targetSdk 34
targetSdk 35
versionCode 1
versionName "1.0"
@ -19,13 +19,15 @@ android {
buildTypes {
release {
minifyEnabled false
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
buildFeatures {
viewBinding true
dataBinding true
}
compileOptions {
@ -38,6 +40,15 @@ android {
}
}
// Enable strict mode for debug builds
android.applicationVariants.all { variant ->
if (variant.buildType.name == "debug") {
variant.mergedFlavor.manifestPlaceholders = [enableStrictMode: "true"]
} else {
variant.mergedFlavor.manifestPlaceholders = [enableStrictMode: "false"]
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.12.0'

View file

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

View file

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

View file

@ -1,733 +0,0 @@
package com.mintris
import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.os.VibrationEffect
import android.os.Vibrator
import android.os.VibratorManager
import android.view.View
import android.widget.Button
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import com.mintris.databinding.ActivityMainBinding
import com.mintris.game.GameHaptics
import com.mintris.game.GameView
import com.mintris.game.NextPieceView
import com.mintris.game.TitleScreen
import android.view.HapticFeedbackConstants
import com.mintris.model.GameBoard
import com.mintris.audio.GameMusic
import com.mintris.model.HighScoreManager
import com.mintris.model.PlayerProgressionManager
import com.mintris.model.StatsManager
import com.mintris.ui.ProgressionScreen
import java.text.SimpleDateFormat
import java.util.*
import android.graphics.Color
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import android.graphics.Rect
import android.view.KeyEvent
class MainActivity : AppCompatActivity() {
// UI components
private lateinit var binding: ActivityMainBinding
private lateinit var gameView: GameView
private lateinit var gameHaptics: GameHaptics
private lateinit var gameBoard: GameBoard
private lateinit var gameMusic: GameMusic
private lateinit var titleScreen: TitleScreen
private lateinit var highScoreManager: HighScoreManager
private lateinit var statsManager: StatsManager
private lateinit var progressionManager: PlayerProgressionManager
private lateinit var progressionScreen: ProgressionScreen
// Game state
private var isSoundEnabled = true
private var isMusicEnabled = true
private var selectedLevel = 1
private val maxLevel = 20
private var currentScore = 0
private var currentLevel = 1
private var gameStartTime: Long = 0
private var piecesPlaced: Int = 0
private var currentTheme = PlayerProgressionManager.THEME_CLASSIC
// Activity result launcher for high score entry
private lateinit var highScoreEntryLauncher: ActivityResultLauncher<Intent>
override fun onCreate(savedInstanceState: Bundle?) {
// Register activity result launcher for high score entry
highScoreEntryLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
// No matter what the result is, we just show the game over container
progressionScreen.visibility = View.GONE
binding.gameOverContainer.visibility = View.VISIBLE
}
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// Disable Android back gesture to prevent accidental app exits
disableAndroidBackGesture()
// Initialize game components
gameBoard = GameBoard()
gameHaptics = GameHaptics(this)
gameView = binding.gameView
titleScreen = binding.titleScreen
gameMusic = GameMusic(this)
highScoreManager = HighScoreManager(this)
statsManager = StatsManager(this)
progressionManager = PlayerProgressionManager(this)
// Load and apply theme preference
currentTheme = loadThemePreference()
applyTheme(currentTheme)
// Set up game view
gameView.setGameBoard(gameBoard)
gameView.setHaptics(gameHaptics)
// Set up progression screen
progressionScreen = binding.progressionScreen
progressionScreen.visibility = View.GONE
progressionScreen.onContinue = {
progressionScreen.visibility = View.GONE
binding.gameOverContainer.visibility = View.VISIBLE
}
// Set up theme selector
val themeSelector = binding.themeSelector
themeSelector.onThemeSelected = { themeId ->
// Apply the new theme
applyTheme(themeId)
// Provide haptic feedback as a cue that the theme changed
gameHaptics.vibrateForPieceLock()
// Refresh the pause menu to immediately show theme changes
if (binding.pauseContainer.visibility == View.VISIBLE) {
showPauseMenu()
}
}
// Set up title screen
titleScreen.onStartGame = {
titleScreen.visibility = View.GONE
gameView.visibility = View.VISIBLE
binding.gameControlsContainer.visibility = View.VISIBLE
startGame()
}
// Initially hide the game view and show title screen
gameView.visibility = View.GONE
binding.gameControlsContainer.visibility = View.GONE
titleScreen.visibility = View.VISIBLE
// Set up pause button to show settings menu
binding.pauseButton.setOnClickListener {
gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY)
gameView.pause()
gameMusic.pause()
showPauseMenu()
binding.pauseStartButton.visibility = View.GONE
binding.resumeButton.visibility = View.VISIBLE
}
// Set up next piece preview
binding.nextPieceView.setGameView(gameView)
gameBoard.onNextPieceChanged = {
binding.nextPieceView.invalidate()
}
// Set up music toggle
binding.musicToggle.setOnClickListener {
isMusicEnabled = !isMusicEnabled
gameMusic.setEnabled(isMusicEnabled)
updateMusicToggleUI()
}
// Set up callbacks
gameView.onGameStateChanged = { score, level, lines ->
updateUI(score, level, lines)
}
gameView.onGameOver = { score ->
showGameOver(score)
}
gameView.onLineClear = { lineCount ->
android.util.Log.d("MainActivity", "Received line clear callback: $lineCount lines")
// Use enhanced haptic feedback for line clears
if (isSoundEnabled) {
android.util.Log.d("MainActivity", "Sound is enabled, triggering haptic feedback")
try {
gameHaptics.vibrateForLineClear(lineCount)
android.util.Log.d("MainActivity", "Haptic feedback triggered successfully")
} catch (e: Exception) {
android.util.Log.e("MainActivity", "Error triggering haptic feedback", e)
}
} else {
android.util.Log.d("MainActivity", "Sound is disabled, skipping haptic feedback")
}
// Record line clear in stats
statsManager.recordLineClear(lineCount)
}
// Add callbacks for piece movement and locking
gameView.onPieceMove = {
if (isSoundEnabled) {
gameHaptics.vibrateForPieceMove()
}
}
gameView.onPieceLock = {
if (isSoundEnabled) {
gameHaptics.vibrateForPieceLock()
}
piecesPlaced++
}
// Set up button click listeners with haptic feedback
binding.playAgainButton.setOnClickListener {
gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY)
hideGameOver()
gameView.reset()
startGame()
}
binding.resumeButton.setOnClickListener {
gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY)
hidePauseMenu()
resumeGame()
}
binding.settingsButton.setOnClickListener {
gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY)
toggleSound()
}
// Set up pause menu buttons
binding.pauseStartButton.setOnClickListener {
gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY)
hidePauseMenu()
gameView.reset()
startGame()
}
binding.pauseRestartButton.setOnClickListener {
gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY)
hidePauseMenu()
gameView.reset()
startGame()
}
binding.highScoresButton.setOnClickListener {
gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY)
showHighScores()
}
binding.pauseLevelUpButton.setOnClickListener {
gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY)
if (selectedLevel < maxLevel) {
selectedLevel++
updateLevelSelector()
}
}
binding.pauseLevelDownButton.setOnClickListener {
gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY)
if (selectedLevel > 1) {
selectedLevel--
updateLevelSelector()
}
}
// Set up stats button
binding.statsButton.setOnClickListener {
gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY)
val intent = Intent(this, StatsActivity::class.java)
startActivity(intent)
}
// Initialize level selector
updateLevelSelector()
// Enable edge-to-edge display
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.setDecorFitsSystemWindows(false)
}
}
/**
* Update UI with current game state
*/
private fun updateUI(score: Int, level: Int, lines: Int) {
binding.scoreText.text = score.toString()
binding.currentLevelText.text = level.toString()
binding.linesText.text = lines.toString()
binding.comboText.text = gameBoard.getCombo().toString()
// Update current level for stats
currentLevel = level
// Force redraw of next piece preview
binding.nextPieceView.invalidate()
}
/**
* Show game over screen
*/
private fun showGameOver(score: Int) {
val gameTime = System.currentTimeMillis() - gameStartTime
// Update session stats
statsManager.updateSessionStats(
score = score,
lines = gameBoard.lines,
pieces = piecesPlaced,
time = gameTime,
level = currentLevel
)
// Calculate XP earned
val xpGained = progressionManager.calculateGameXP(
score = score,
lines = gameBoard.lines,
level = currentLevel,
gameTime = gameTime,
tetrisCount = statsManager.getSessionTetrises(),
perfectClearCount = 0 // Implement perfect clear tracking if needed
)
// Add XP and check for rewards
val newRewards = progressionManager.addXP(xpGained)
// End session and save stats
statsManager.endSession()
// Update session stats display
val timeFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
timeFormat.timeZone = TimeZone.getTimeZone("UTC")
binding.sessionScoreText.text = getString(R.string.session_score, score)
binding.sessionLinesText.text = getString(R.string.session_lines, gameBoard.lines)
binding.sessionPiecesText.text = getString(R.string.session_pieces, piecesPlaced)
binding.sessionTimeText.text = getString(R.string.session_time, timeFormat.format(gameTime))
binding.sessionLevelText.text = getString(R.string.session_level, currentLevel)
// Update session line clear stats
binding.sessionSinglesText.text = getString(R.string.singles, statsManager.getSessionSingles())
binding.sessionDoublesText.text = getString(R.string.doubles, statsManager.getSessionDoubles())
binding.sessionTriplesText.text = getString(R.string.triples, statsManager.getSessionTriples())
binding.sessionTetrisesText.text = getString(R.string.tetrises, statsManager.getSessionTetrises())
// Flag to track if high score screen will be shown
var showingHighScore = false
// Show progression screen first with XP animation
binding.gameOverContainer.visibility = View.GONE
progressionScreen.visibility = View.VISIBLE
progressionScreen.applyTheme(currentTheme)
progressionScreen.showProgress(progressionManager, xpGained, newRewards, currentTheme)
// Override the continue button behavior if high score needs to be shown
val originalOnContinue = progressionScreen.onContinue
progressionScreen.onContinue = {
// If this is a high score, show high score entry screen
if (highScoreManager.isHighScore(score)) {
showingHighScore = true
showHighScoreEntry(score)
} else {
// Just show game over screen normally
progressionScreen.visibility = View.GONE
binding.gameOverContainer.visibility = View.VISIBLE
// Update theme selector if new themes were unlocked
if (newRewards.any { it.contains("Theme") }) {
updateThemeSelector()
}
}
}
// Vibrate to indicate game over
vibrate(VibrationEffect.EFFECT_DOUBLE_CLICK)
}
/**
* Show high score entry screen
*/
private fun showHighScoreEntry(score: Int) {
val intent = Intent(this, HighScoreEntryActivity::class.java).apply {
putExtra("score", score)
putExtra("level", currentLevel)
}
// Use the launcher instead of startActivity
highScoreEntryLauncher.launch(intent)
}
/**
* Hide game over screen
*/
private fun hideGameOver() {
binding.gameOverContainer.visibility = View.GONE
progressionScreen.visibility = View.GONE
}
/**
* Show settings menu
*/
private fun showPauseMenu() {
binding.pauseContainer.visibility = View.VISIBLE
binding.pauseStartButton.visibility = View.VISIBLE
binding.resumeButton.visibility = View.GONE
// Update level badge
binding.pauseLevelBadge.setLevel(progressionManager.getPlayerLevel())
binding.pauseLevelBadge.setThemeColor(getThemeColor(currentTheme))
// Get theme color
val textColor = getThemeColor(currentTheme)
// Apply theme color to pause container background
val backgroundColor = when (currentTheme) {
PlayerProgressionManager.THEME_CLASSIC -> Color.BLACK
PlayerProgressionManager.THEME_NEON -> Color.parseColor("#0D0221")
PlayerProgressionManager.THEME_MONOCHROME -> Color.parseColor("#1A1A1A")
PlayerProgressionManager.THEME_RETRO -> Color.parseColor("#3F2832")
PlayerProgressionManager.THEME_MINIMALIST -> Color.WHITE
PlayerProgressionManager.THEME_GALAXY -> Color.parseColor("#0B0C10")
else -> Color.BLACK
}
binding.pauseContainer.setBackgroundColor(backgroundColor)
// Apply theme colors to buttons
binding.pauseStartButton.setTextColor(textColor)
binding.pauseRestartButton.setTextColor(textColor)
binding.resumeButton.setTextColor(textColor)
binding.highScoresButton.setTextColor(textColor)
binding.statsButton.setTextColor(textColor)
binding.pauseLevelText.setTextColor(textColor)
binding.pauseLevelUpButton.setTextColor(textColor)
binding.pauseLevelDownButton.setTextColor(textColor)
binding.settingsButton.setTextColor(textColor)
binding.musicToggle.setColorFilter(textColor)
// Apply theme colors to text elements
binding.settingsTitle.setTextColor(textColor)
binding.selectLevelText.setTextColor(textColor)
binding.musicText.setTextColor(textColor)
// Update theme selector
updateThemeSelector()
}
/**
* Hide settings menu
*/
private fun hidePauseMenu() {
binding.pauseContainer.visibility = View.GONE
}
/**
* Toggle sound on/off
*/
private fun toggleSound() {
isSoundEnabled = !isSoundEnabled
binding.settingsButton.text = getString(
if (isSoundEnabled) R.string.sound_on else R.string.sound_off
)
// Vibrate to provide feedback
vibrate(VibrationEffect.EFFECT_CLICK)
}
/**
* Update the level selector display
*/
private fun updateLevelSelector() {
binding.pauseLevelText.text = selectedLevel.toString()
gameBoard.updateLevel(selectedLevel)
}
/**
* Trigger device vibration with predefined effect
*/
private fun vibrate(effectId: Int) {
val vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
vibrator.vibrate(VibrationEffect.createPredefined(effectId))
}
private fun updateMusicToggleUI() {
binding.musicToggle.setImageResource(
if (isMusicEnabled) R.drawable.ic_volume_up
else R.drawable.ic_volume_off
)
}
private fun startGame() {
gameView.start()
gameMusic.setEnabled(isMusicEnabled)
if (isMusicEnabled) {
gameMusic.start()
}
gameStartTime = System.currentTimeMillis()
piecesPlaced = 0
statsManager.startNewSession()
progressionManager.startNewSession()
gameBoard.updateLevel(selectedLevel)
}
private fun restartGame() {
gameBoard.reset()
gameView.visibility = View.VISIBLE
gameView.start()
showPauseMenu()
}
private fun resumeGame() {
gameView.resume()
if (isMusicEnabled) {
gameMusic.resume()
}
// Force a redraw to ensure pieces aren't frozen
gameView.invalidate()
}
override fun onPause() {
super.onPause()
if (gameView.visibility == View.VISIBLE) {
gameView.pause()
gameMusic.pause()
}
}
override fun onResume() {
super.onResume()
// If we're on the title screen, don't auto-resume the game
if (titleScreen.visibility == View.GONE && gameView.visibility == View.VISIBLE && binding.gameOverContainer.visibility == View.GONE && binding.pauseContainer.visibility == View.GONE) {
resumeGame()
}
// Update theme selector with available themes when pause screen appears
updateThemeSelector()
}
override fun onDestroy() {
super.onDestroy()
gameMusic.release()
}
/**
* Show title screen (for game restart)
*/
private fun showTitleScreen() {
gameView.reset()
gameView.visibility = View.GONE
binding.gameControlsContainer.visibility = View.GONE
binding.gameOverContainer.visibility = View.GONE
binding.pauseContainer.visibility = View.GONE
titleScreen.visibility = View.VISIBLE
titleScreen.applyTheme(currentTheme)
}
/**
* Show high scores
*/
private fun showHighScores() {
val intent = Intent(this, HighScoresActivity::class.java)
startActivity(intent)
}
/**
* Update the theme selector with unlocked themes
*/
private fun updateThemeSelector() {
binding.themeSelector.updateThemes(
unlockedThemes = progressionManager.getUnlockedThemes(),
currentTheme = currentTheme
)
}
/**
* Apply a theme to the game
*/
private fun applyTheme(themeId: String) {
// Only apply if the theme is unlocked
if (!progressionManager.isThemeUnlocked(themeId)) return
// Save the selected theme
currentTheme = themeId
saveThemePreference(themeId)
// Apply theme to title screen if it's visible
if (titleScreen.visibility == View.VISIBLE) {
titleScreen.applyTheme(themeId)
}
// Apply theme colors based on theme ID
when (themeId) {
PlayerProgressionManager.THEME_CLASSIC -> {
// Default black theme
binding.root.setBackgroundColor(Color.BLACK)
}
PlayerProgressionManager.THEME_NEON -> {
// Neon theme with dark purple background
binding.root.setBackgroundColor(Color.parseColor("#0D0221"))
}
PlayerProgressionManager.THEME_MONOCHROME -> {
// Monochrome dark gray
binding.root.setBackgroundColor(Color.parseColor("#1A1A1A"))
}
PlayerProgressionManager.THEME_RETRO -> {
// Retro arcade theme
binding.root.setBackgroundColor(Color.parseColor("#3F2832"))
}
PlayerProgressionManager.THEME_MINIMALIST -> {
// Minimalist white theme
binding.root.setBackgroundColor(Color.WHITE)
// Update text colors for visibility
binding.scoreText.setTextColor(Color.BLACK)
binding.currentLevelText.setTextColor(Color.BLACK)
binding.linesText.setTextColor(Color.BLACK)
binding.comboText.setTextColor(Color.BLACK)
}
PlayerProgressionManager.THEME_GALAXY -> {
// Galaxy dark blue theme
binding.root.setBackgroundColor(Color.parseColor("#0B0C10"))
}
}
// Apply theme to progression screen if it's visible and initialized
if (::progressionScreen.isInitialized && progressionScreen.visibility == View.VISIBLE) {
progressionScreen.applyTheme(themeId)
}
// Apply theme color to the stats button
val textColor = getThemeColor(currentTheme)
binding.statsButton.setTextColor(textColor)
// Update the game view to apply theme
gameView.invalidate()
}
/**
* Save the selected theme in preferences
*/
private fun saveThemePreference(themeId: String) {
val prefs = getSharedPreferences("mintris_settings", Context.MODE_PRIVATE)
prefs.edit().putString("selected_theme", themeId).apply()
}
/**
* Load the saved theme preference
*/
private fun loadThemePreference(): String {
val prefs = getSharedPreferences("mintris_settings", Context.MODE_PRIVATE)
return prefs.getString("selected_theme", PlayerProgressionManager.THEME_CLASSIC) ?: PlayerProgressionManager.THEME_CLASSIC
}
/**
* Get the appropriate color for the current theme
*/
private fun getThemeColor(themeId: String): Int {
return when (themeId) {
PlayerProgressionManager.THEME_CLASSIC -> Color.WHITE
PlayerProgressionManager.THEME_NEON -> Color.parseColor("#FF00FF")
PlayerProgressionManager.THEME_MONOCHROME -> Color.LTGRAY
PlayerProgressionManager.THEME_RETRO -> Color.parseColor("#FF5A5F")
PlayerProgressionManager.THEME_MINIMALIST -> Color.BLACK
PlayerProgressionManager.THEME_GALAXY -> Color.parseColor("#66FCF1")
else -> Color.WHITE
}
}
/**
* Disables the Android system back gesture to prevent accidental exits
*/
private fun disableAndroidBackGesture() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Set the entire window to be excluded from the system gesture areas
window.decorView.post {
// Create a list of rectangles representing the edges of the screen to exclude from system gestures
val gestureInsets = window.decorView.rootWindowInsets?.systemGestureInsets
if (gestureInsets != null) {
val leftEdge = Rect(0, 0, 50, window.decorView.height)
val rightEdge = Rect(window.decorView.width - 50, 0, window.decorView.width, window.decorView.height)
val bottomEdge = Rect(0, window.decorView.height - 50, window.decorView.width, window.decorView.height)
window.decorView.systemGestureExclusionRects = listOf(leftEdge, rightEdge, bottomEdge)
}
}
}
// Add an on back pressed callback to handle back button/gesture
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
onBackPressedDispatcher.addCallback(this, object : androidx.activity.OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
// If we're playing the game, handle it as a pause action instead of exiting
if (gameView.visibility == View.VISIBLE && !gameView.isPaused && !gameView.isGameOver()) {
gameView.pause()
gameMusic.pause()
showPauseMenu()
binding.pauseStartButton.visibility = View.GONE
binding.resumeButton.visibility = View.VISIBLE
} else if (binding.pauseContainer.visibility == View.VISIBLE) {
// If pause menu is showing, handle as a resume
resumeGame()
} else if (binding.gameOverContainer.visibility == View.VISIBLE) {
// If game over is showing, go back to title
hideGameOver()
showTitleScreen()
} else if (titleScreen.visibility == View.VISIBLE) {
// If title screen is showing, allow normal back behavior (exit app)
isEnabled = false
onBackPressedDispatcher.onBackPressed()
}
}
})
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// For Android 11 (R) to Android 12 (S), use the WindowInsetsController to disable gestures
window.insetsController?.systemBarsBehavior =
android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
}
/**
* Completely block the hardware back button during gameplay
*/
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
// If back button is pressed
if (keyCode == KeyEvent.KEYCODE_BACK) {
// Handle back button press as a pause action during gameplay
if (gameView.visibility == View.VISIBLE && !gameView.isPaused && !gameView.isGameOver()) {
gameView.pause()
gameMusic.pause()
showPauseMenu()
binding.pauseStartButton.visibility = View.GONE
binding.resumeButton.visibility = View.VISIBLE
return true // Consume the event
} else if (binding.pauseContainer.visibility == View.VISIBLE) {
// If pause menu is showing, handle as a resume
resumeGame()
return true // Consume the event
} else if (binding.gameOverContainer.visibility == View.VISIBLE) {
// If game over is showing, go back to title
hideGameOver()
showTitleScreen()
return true // Consume the event
}
}
return super.onKeyDown(keyCode, event)
}
}

View file

@ -1,77 +0,0 @@
package com.mintris.game
import android.content.Context
import android.os.Build
import android.os.VibrationEffect
import android.os.Vibrator
import android.os.VibratorManager
import android.view.HapticFeedbackConstants
import android.view.View
class GameHaptics(private val context: Context) {
private val vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val vibratorManager = context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
vibratorManager.defaultVibrator
} else {
@Suppress("DEPRECATION")
context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
}
fun performHapticFeedback(view: View, feedbackType: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
view.performHapticFeedback(HapticFeedbackConstants.CONFIRM)
} else {
@Suppress("DEPRECATION")
view.performHapticFeedback(feedbackType)
}
}
fun vibrateForLineClear(lineCount: Int) {
android.util.Log.d("GameHaptics", "Attempting to vibrate for $lineCount lines")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val duration = when (lineCount) {
4 -> 200L // Tetris - doubled from 100L
3 -> 160L // Triples - doubled from 80L
2 -> 120L // Doubles - doubled from 60L
1 -> 80L // Singles - doubled from 40L
else -> 0L
}
val amplitude = when (lineCount) {
4 -> 255 // Full amplitude for Tetris
3 -> 230 // 90% amplitude for triples
2 -> 180 // 70% amplitude for doubles
1 -> 128 // 50% amplitude for singles
else -> 0
}
android.util.Log.d("GameHaptics", "Vibration parameters - Duration: ${duration}ms, Amplitude: $amplitude")
if (duration > 0 && amplitude > 0) {
try {
val vibrationEffect = VibrationEffect.createOneShot(duration, amplitude)
vibrator.vibrate(vibrationEffect)
android.util.Log.d("GameHaptics", "Vibration triggered successfully")
} catch (e: Exception) {
android.util.Log.e("GameHaptics", "Error triggering vibration", e)
}
}
} else {
android.util.Log.w("GameHaptics", "Device does not support vibration effects (Android < 8.0)")
}
}
fun vibrateForPieceLock() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val vibrationEffect = VibrationEffect.createOneShot(50L, VibrationEffect.DEFAULT_AMPLITUDE)
vibrator.vibrate(vibrationEffect)
}
}
fun vibrateForPieceMove() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val amplitude = (VibrationEffect.DEFAULT_AMPLITUDE * 0.3).toInt().coerceAtLeast(1)
val vibrationEffect = VibrationEffect.createOneShot(20L, amplitude)
vibrator.vibrate(vibrationEffect)
}
}
}

View file

@ -1,821 +0,0 @@
package com.mintris.game
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.BlurMaskFilter
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.animation.LinearInterpolator
import android.view.WindowManager
import android.view.Display
import android.hardware.display.DisplayManager
import com.mintris.model.GameBoard
import com.mintris.model.Tetromino
import com.mintris.model.TetrominoType
import kotlin.math.abs
import kotlin.math.min
/**
* GameView that renders the Tetris game and handles touch input
*/
class GameView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
// Game board model
private var gameBoard = GameBoard()
private var gameHaptics: GameHaptics? = null
// Game state
private var isRunning = false
var isPaused = false // Changed from private to public to allow access from MainActivity
private var score = 0
// Callbacks
var onNextPieceChanged: (() -> Unit)? = null
// Rendering
private val blockPaint = Paint().apply {
color = Color.WHITE
isAntiAlias = true
}
private val ghostBlockPaint = Paint().apply {
color = Color.WHITE
alpha = 80 // 30% opacity
isAntiAlias = true
}
private val gridPaint = Paint().apply {
color = Color.parseColor("#222222") // Very dark gray
alpha = 20 // Reduced from 40 to be more subtle
isAntiAlias = true
strokeWidth = 1f
style = Paint.Style.STROKE
maskFilter = null // Ensure no blur effect on grid lines
}
private val glowPaint = Paint().apply {
color = Color.WHITE
alpha = 40 // Reduced from 80 for more subtlety
isAntiAlias = true
style = Paint.Style.STROKE
strokeWidth = 1.5f
maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.OUTER)
}
private val blockGlowPaint = Paint().apply {
color = Color.WHITE
alpha = 60
isAntiAlias = true
style = Paint.Style.FILL
maskFilter = BlurMaskFilter(12f, BlurMaskFilter.Blur.OUTER)
}
private val borderGlowPaint = Paint().apply {
color = Color.WHITE
alpha = 60
isAntiAlias = true
style = Paint.Style.STROKE
strokeWidth = 2f
maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.OUTER)
}
// Add a new paint for the pulse effect
private val pulsePaint = Paint().apply {
color = Color.CYAN
alpha = 255
isAntiAlias = true
style = Paint.Style.FILL
maskFilter = BlurMaskFilter(32f, BlurMaskFilter.Blur.OUTER) // Increased from 16f to 32f
}
// Pre-allocate paint objects to avoid GC
private val tmpPaint = Paint()
// Calculate block size based on view dimensions and board size
private var blockSize = 0f
private var boardLeft = 0f
private var boardTop = 0f
// Game loop handler and runnable
private val handler = Handler(Looper.getMainLooper())
private val gameLoopRunnable = object : Runnable {
override fun run() {
if (isRunning && !isPaused) {
update()
invalidate()
handler.postDelayed(this, gameBoard.dropInterval)
}
}
}
// Touch parameters
private var lastTouchX = 0f
private var lastTouchY = 0f
private var startX = 0f
private var startY = 0f
private var lastTapTime = 0L
private var lastRotationTime = 0L
private var lastMoveTime = 0L
private var minSwipeVelocity = 1200 // Increased from 800 to require more deliberate swipes
private val maxTapMovement = 20f // Maximum movement allowed for a tap (in pixels)
private val minTapTime = 100L // Minimum time for a tap (in milliseconds)
private val rotationCooldown = 150L // Minimum time between rotations (in milliseconds)
private val moveCooldown = 50L // Minimum time between move haptics (in milliseconds)
private var lockedDirection: Direction? = null // Track the locked movement direction
private val minMovementThreshold = 0.75f // Minimum movement threshold relative to block size
private val directionLockThreshold = 2.5f // Increased from 1.5f to make direction locking more aggressive
private val isStrictDirectionLock = true // Enable strict direction locking to prevent diagonal inputs
private val minHardDropDistance = 1.5f // Minimum distance (in blocks) for hard drop gesture
private enum class Direction {
HORIZONTAL, VERTICAL
}
// Callback for game events
var onGameStateChanged: ((score: Int, level: Int, lines: Int) -> Unit)? = null
var onGameOver: ((score: Int) -> Unit)? = null
var onLineClear: ((Int) -> Unit)? = null // New callback for line clear events
var onPieceMove: (() -> Unit)? = null // New callback for piece movement
var onPieceLock: (() -> Unit)? = null // New callback for piece locking
// Animation state
private var pulseAnimator: ValueAnimator? = null
private var pulseAlpha = 0f
private var isPulsing = false
private var linesToPulse = mutableListOf<Int>() // Track which lines are being cleared
init {
// Start with paused state
pause()
// Connect our callbacks to the GameBoard
gameBoard.onPieceMove = { onPieceMove?.invoke() }
gameBoard.onPieceLock = { onPieceLock?.invoke() }
gameBoard.onLineClear = { lineCount, clearedLines ->
android.util.Log.d("GameView", "Received line clear from GameBoard: $lineCount lines")
try {
onLineClear?.invoke(lineCount)
// Use the lines that were cleared directly
linesToPulse.clear()
linesToPulse.addAll(clearedLines)
android.util.Log.d("GameView", "Found ${linesToPulse.size} lines to pulse")
startPulseAnimation(lineCount)
android.util.Log.d("GameView", "Forwarded line clear callback")
} catch (e: Exception) {
android.util.Log.e("GameView", "Error forwarding line clear callback", e)
}
}
// Force hardware acceleration - This is critical for performance
setLayerType(LAYER_TYPE_HARDWARE, null)
// Set better frame rate using modern APIs
val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY)
display?.let { disp ->
val refreshRate = disp.refreshRate
// Set game loop interval based on refresh rate, but don't go faster than the base interval
val targetFps = refreshRate.toInt()
if (targetFps > 0) {
gameBoard.dropInterval = gameBoard.dropInterval.coerceAtMost(1000L / targetFps)
}
}
// Enable edge-to-edge rendering
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
setSystemGestureExclusionRects(listOf(Rect(0, 0, width, height)))
}
}
/**
* Start the game
*/
fun start() {
isPaused = false
isRunning = true
gameBoard.startGame() // Add this line to ensure a new piece is spawned
handler.post(gameLoopRunnable)
invalidate()
}
/**
* Pause the game
*/
fun pause() {
isPaused = true
handler.removeCallbacks(gameLoopRunnable)
invalidate()
}
/**
* Reset the game
*/
fun reset() {
isRunning = false
isPaused = true
gameBoard.reset()
gameBoard.startGame() // Add this line to ensure a new piece is spawned
handler.removeCallbacks(gameLoopRunnable)
invalidate()
}
/**
* Update game state (called on game loop)
*/
private fun update() {
if (gameBoard.isGameOver) {
isRunning = false
isPaused = true
onGameOver?.invoke(gameBoard.score)
return
}
// Move the current tetromino down automatically
gameBoard.moveDown()
// Update UI with current game state
onGameStateChanged?.invoke(gameBoard.score, gameBoard.level, gameBoard.lines)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
// Force hardware acceleration - Critical for performance
setLayerType(LAYER_TYPE_HARDWARE, null)
// Update gesture exclusion rect for edge-to-edge rendering
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
setSystemGestureExclusionRects(listOf(Rect(0, 0, w, h)))
}
calculateDimensions(w, h)
}
/**
* Calculate dimensions for the board and blocks based on view size
*/
private fun calculateDimensions(width: Int, height: Int) {
// Calculate block size based on available space
val horizontalBlocks = gameBoard.width
val verticalBlocks = gameBoard.height
// Account for all glow effects and borders
val borderPadding = 16f // Padding for border glow effects
// Calculate block size to fit the height exactly, accounting for all padding
blockSize = (height.toFloat() - (borderPadding * 2)) / verticalBlocks
// Calculate total board width
val totalBoardWidth = blockSize * horizontalBlocks
// Center horizontally
boardLeft = (width - totalBoardWidth) / 2
boardTop = borderPadding // Start with border padding from top
// Calculate the total height needed for the board
val totalHeight = blockSize * verticalBlocks
// Log dimensions for debugging
android.util.Log.d("GameView", "Board dimensions: width=$width, height=$height, blockSize=$blockSize, boardLeft=$boardLeft, boardTop=$boardTop, totalHeight=$totalHeight")
}
override fun onDraw(canvas: Canvas) {
// Skip drawing if paused or game over - faster return
if (isPaused || gameBoard.isGameOver) {
super.onDraw(canvas)
return
}
// Set hardware layer type during draw for better performance
val wasHardwareAccelerated = isHardwareAccelerated
if (!wasHardwareAccelerated) {
setLayerType(LAYER_TYPE_HARDWARE, null)
}
super.onDraw(canvas)
// Draw background (already black from theme)
// Draw board border glow
drawBoardBorder(canvas)
// Draw grid (very subtle)
drawGrid(canvas)
// Draw locked pieces
drawLockedBlocks(canvas)
if (!gameBoard.isGameOver && isRunning) {
// Draw ghost piece (landing preview)
drawGhostPiece(canvas)
// Draw active piece
drawActivePiece(canvas)
}
}
/**
* Draw glowing border around the playable area
*/
private fun drawBoardBorder(canvas: Canvas) {
val left = boardLeft
val top = boardTop
val right = boardLeft + gameBoard.width * blockSize
val bottom = boardTop + gameBoard.height * blockSize
val rect = RectF(left, top, right, bottom)
// Draw base border with increased glow
borderGlowPaint.apply {
alpha = 80 // Increased from 60
maskFilter = BlurMaskFilter(16f, BlurMaskFilter.Blur.OUTER) // Increased from 8f
}
canvas.drawRect(rect, borderGlowPaint)
// Draw pulsing border if animation is active
if (isPulsing) {
val pulseBorderPaint = Paint().apply {
color = Color.WHITE
style = Paint.Style.STROKE
strokeWidth = 6f + (16f * pulseAlpha) // Increased from 4f+12f to 6f+16f
alpha = (255 * pulseAlpha).toInt()
isAntiAlias = true
maskFilter = BlurMaskFilter(32f * (1f + pulseAlpha), BlurMaskFilter.Blur.OUTER) // Increased from 24f to 32f
}
// Draw the border with a slight inset to prevent edge artifacts
val inset = 1f
canvas.drawRect(
left + inset,
top + inset,
right - inset,
bottom - inset,
pulseBorderPaint
)
// Add an additional outer glow for more dramatic effect
val outerGlowPaint = Paint().apply {
color = Color.WHITE
style = Paint.Style.STROKE
strokeWidth = 2f
alpha = (128 * pulseAlpha).toInt()
isAntiAlias = true
maskFilter = BlurMaskFilter(48f * (1f + pulseAlpha), BlurMaskFilter.Blur.OUTER)
}
canvas.drawRect(
left - 4f,
top - 4f,
right + 4f,
bottom + 4f,
outerGlowPaint
)
// Add extra bright glow for side borders during line clear
val sideGlowPaint = Paint().apply {
color = Color.WHITE
style = Paint.Style.STROKE
strokeWidth = 8f + (24f * pulseAlpha) // Thicker stroke for side borders
alpha = (255 * pulseAlpha).toInt()
isAntiAlias = true
maskFilter = BlurMaskFilter(64f * (1f + pulseAlpha), BlurMaskFilter.Blur.OUTER) // Larger blur for side borders
}
// Draw left border with extra glow
canvas.drawLine(
left + inset,
top + inset,
left + inset,
bottom - inset,
sideGlowPaint
)
// Draw right border with extra glow
canvas.drawLine(
right - inset,
top + inset,
right - inset,
bottom - inset,
sideGlowPaint
)
}
}
/**
* Draw the grid lines (very subtle)
*/
private fun drawGrid(canvas: Canvas) {
// Save the canvas state to prevent any effects from affecting the grid
canvas.save()
// Draw vertical grid lines
for (x in 0..gameBoard.width) {
val xPos = boardLeft + x * blockSize
canvas.drawLine(
xPos, boardTop,
xPos, boardTop + gameBoard.height * blockSize,
gridPaint
)
}
// Draw horizontal grid lines
for (y in 0..gameBoard.height) {
val yPos = boardTop + y * blockSize
canvas.drawLine(
boardLeft, yPos,
boardLeft + gameBoard.width * blockSize, yPos,
gridPaint
)
}
// Restore the canvas state
canvas.restore()
}
/**
* Draw the locked blocks on the board
*/
private fun drawLockedBlocks(canvas: Canvas) {
for (y in 0 until gameBoard.height) {
for (x in 0 until gameBoard.width) {
if (gameBoard.isOccupied(x, y)) {
drawBlock(canvas, x, y, false, y in linesToPulse)
}
}
}
}
/**
* Draw the currently active tetromino
*/
private fun drawActivePiece(canvas: Canvas) {
val piece = gameBoard.getCurrentPiece() ?: return
for (y in 0 until piece.getHeight()) {
for (x in 0 until piece.getWidth()) {
if (piece.isBlockAt(x, y)) {
val boardX = piece.x + x
val boardY = piece.y + y
// Draw piece regardless of vertical position
if (boardX >= 0 && boardX < gameBoard.width) {
drawBlock(canvas, boardX, boardY, false, false)
}
}
}
}
}
/**
* Draw the ghost piece (landing preview)
*/
private fun drawGhostPiece(canvas: Canvas) {
val piece = gameBoard.getCurrentPiece() ?: return
val ghostY = gameBoard.getGhostY()
for (y in 0 until piece.getHeight()) {
for (x in 0 until piece.getWidth()) {
if (piece.isBlockAt(x, y)) {
val boardX = piece.x + x
val boardY = ghostY + y
// Draw ghost piece regardless of vertical position
if (boardX >= 0 && boardX < gameBoard.width) {
drawBlock(canvas, boardX, boardY, true, false)
}
}
}
}
}
/**
* Draw a single tetris block at the given grid position
*/
private fun drawBlock(canvas: Canvas, x: Int, y: Int, isGhost: Boolean, isPulsingLine: Boolean) {
val left = boardLeft + x * blockSize
val top = boardTop + y * blockSize
val right = left + blockSize
val bottom = top + blockSize
// Save canvas state before drawing block effects
canvas.save()
// Draw outer glow
blockGlowPaint.color = if (isGhost) Color.argb(30, 255, 255, 255) else Color.WHITE
canvas.drawRect(left - 2f, top - 2f, right + 2f, bottom + 2f, blockGlowPaint)
// Draw block
blockPaint.apply {
color = if (isGhost) Color.argb(30, 255, 255, 255) else Color.WHITE
alpha = if (isGhost) 30 else 255
}
canvas.drawRect(left, top, right, bottom, blockPaint)
// Draw inner glow
glowPaint.color = if (isGhost) Color.argb(30, 255, 255, 255) else Color.WHITE
canvas.drawRect(left + 1f, top + 1f, right - 1f, bottom - 1f, glowPaint)
// Draw pulse effect if animation is active and this is a pulsing line
if (isPulsing && isPulsingLine) {
val pulseBlockPaint = Paint().apply {
color = Color.WHITE
alpha = (255 * pulseAlpha).toInt()
isAntiAlias = true
style = Paint.Style.FILL
maskFilter = BlurMaskFilter(40f * (1f + pulseAlpha), BlurMaskFilter.Blur.OUTER)
}
canvas.drawRect(left - 16f, top - 16f, right + 16f, bottom + 16f, pulseBlockPaint)
}
// Restore canvas state after drawing block effects
canvas.restore()
}
/**
* Check if the given board position is part of the current piece
*/
private fun isPositionInPiece(boardX: Int, boardY: Int, piece: Tetromino): Boolean {
for (y in 0 until piece.getHeight()) {
for (x in 0 until piece.getWidth()) {
if (piece.isBlockAt(x, y)) {
val pieceX = piece.x + x
val pieceY = piece.y + y
if (pieceX == boardX && pieceY == boardY) {
return true
}
}
}
}
return false
}
/**
* Get color for tetromino type
*/
private fun getTetrominoColor(type: TetrominoType): Int {
return when (type) {
TetrominoType.I -> Color.CYAN
TetrominoType.J -> Color.BLUE
TetrominoType.L -> Color.rgb(255, 165, 0) // Orange
TetrominoType.O -> Color.YELLOW
TetrominoType.S -> Color.GREEN
TetrominoType.T -> Color.MAGENTA
TetrominoType.Z -> Color.RED
}
}
// Custom touch event handling
override fun onTouchEvent(event: MotionEvent): Boolean {
if (!isRunning || isPaused || gameBoard.isGameOver) {
return true
}
when (event.action) {
MotionEvent.ACTION_DOWN -> {
// Record start of touch
startX = event.x
startY = event.y
lastTouchX = event.x
lastTouchY = event.y
lockedDirection = null // Reset direction lock
// Check for double tap (rotate)
val currentTime = System.currentTimeMillis()
if (currentTime - lastTapTime < 200) { // Reduced from 250ms for faster response
// Double tap detected, rotate the piece
if (currentTime - lastRotationTime >= rotationCooldown) {
gameBoard.rotate()
lastRotationTime = currentTime
invalidate()
}
}
lastTapTime = currentTime
}
MotionEvent.ACTION_MOVE -> {
val deltaX = event.x - lastTouchX
val deltaY = event.y - lastTouchY
val currentTime = System.currentTimeMillis()
// Determine movement direction if not locked
if (lockedDirection == null) {
val absDeltaX = abs(deltaX)
val absDeltaY = abs(deltaY)
// Check if movement exceeds threshold
if (absDeltaX > blockSize * minMovementThreshold || absDeltaY > blockSize * minMovementThreshold) {
// Determine dominant direction with stricter criteria
if (absDeltaX > absDeltaY * directionLockThreshold) {
lockedDirection = Direction.HORIZONTAL
} else if (absDeltaY > absDeltaX * directionLockThreshold) {
lockedDirection = Direction.VERTICAL
}
// If strict direction lock is enabled and we couldn't determine a clear direction, don't set one
// This prevents diagonal movements from being recognized
}
}
// Handle movement based on locked direction
when (lockedDirection) {
Direction.HORIZONTAL -> {
if (abs(deltaX) > blockSize * minMovementThreshold) {
if (deltaX > 0) {
gameBoard.moveRight()
} else {
gameBoard.moveLeft()
}
lastTouchX = event.x
if (currentTime - lastMoveTime >= moveCooldown) {
gameHaptics?.vibrateForPieceMove()
lastMoveTime = currentTime
}
invalidate()
}
}
Direction.VERTICAL -> {
if (deltaY > blockSize * minMovementThreshold) {
gameBoard.moveDown()
lastTouchY = event.y
if (currentTime - lastMoveTime >= moveCooldown) {
gameHaptics?.vibrateForPieceMove()
lastMoveTime = currentTime
}
invalidate()
}
}
null -> {
// No direction lock yet, don't process movement
}
}
}
MotionEvent.ACTION_UP -> {
// Calculate movement speed for potential fling detection
val moveTime = System.currentTimeMillis() - lastTapTime
val deltaY = event.y - startY
val deltaX = event.x - startX
// Only allow hard drops with a deliberate downward swipe
// Requires: predominantly vertical movement, minimum distance, and minimum velocity
if (moveTime > 0 &&
deltaY > blockSize * minHardDropDistance && // Require longer swipe for hard drop
(deltaY / moveTime) * 1000 > minSwipeVelocity &&
abs(deltaX) < abs(deltaY) * 0.3f) { // Require more purely vertical movement (reduced from 0.5f to 0.3f)
gameBoard.hardDrop()
invalidate()
} else if (moveTime < minTapTime &&
abs(deltaY) < maxTapMovement &&
abs(deltaX) < maxTapMovement) {
// Quick tap with minimal movement (rotation)
val currentTime = System.currentTimeMillis()
if (currentTime - lastRotationTime >= rotationCooldown) {
gameBoard.rotate()
lastRotationTime = currentTime
invalidate()
}
}
// Reset direction lock
lockedDirection = null
}
}
return true
}
/**
* Get the current score
*/
fun getScore(): Int = gameBoard.score
/**
* Get the current level
*/
fun getLevel(): Int = gameBoard.level
/**
* Get the number of lines cleared
*/
fun getLines(): Int = gameBoard.lines
/**
* Check if the game is over
*/
fun isGameOver(): Boolean = gameBoard.isGameOver
/**
* Get the next piece that will be spawned
*/
fun getNextPiece(): Tetromino? {
return gameBoard.getNextPiece()
}
/**
* Clean up resources when view is detached
*/
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
handler.removeCallbacks(gameLoopRunnable)
}
/**
* Set the game board for this view
*/
fun setGameBoard(board: GameBoard) {
gameBoard = board
// Reconnect callbacks to the new board
gameBoard.onPieceMove = { onPieceMove?.invoke() }
gameBoard.onPieceLock = { onPieceLock?.invoke() }
gameBoard.onLineClear = { lineCount, clearedLines ->
android.util.Log.d("GameView", "Received line clear from GameBoard: $lineCount lines")
try {
onLineClear?.invoke(lineCount)
// Use the lines that were cleared directly
linesToPulse.clear()
linesToPulse.addAll(clearedLines)
android.util.Log.d("GameView", "Found ${linesToPulse.size} lines to pulse")
startPulseAnimation(lineCount)
android.util.Log.d("GameView", "Forwarded line clear callback")
} catch (e: Exception) {
android.util.Log.e("GameView", "Error forwarding line clear callback", e)
}
}
invalidate()
}
/**
* Set the haptics handler for this view
*/
fun setHaptics(haptics: GameHaptics) {
gameHaptics = haptics
}
/**
* Resume the game
*/
fun resume() {
if (!isRunning) {
isRunning = true
}
isPaused = false
// Restart the game loop immediately
handler.removeCallbacks(gameLoopRunnable)
handler.post(gameLoopRunnable)
// Force an update to ensure pieces move immediately
update()
invalidate()
}
/**
* Start the pulse animation for line clear
*/
private fun startPulseAnimation(lineCount: Int) {
android.util.Log.d("GameView", "Starting pulse animation for $lineCount lines")
// Cancel any existing animation
pulseAnimator?.cancel()
// Create new animation
pulseAnimator = ValueAnimator.ofFloat(0f, 1f, 0f).apply {
duration = when (lineCount) {
4 -> 2000L // Tetris - longer duration
3 -> 1600L // Triples
2 -> 1200L // Doubles
1 -> 1000L // Singles
else -> 1000L
}
interpolator = LinearInterpolator()
addUpdateListener { animation ->
pulseAlpha = animation.animatedValue as Float
isPulsing = true
invalidate()
android.util.Log.d("GameView", "Pulse animation update: alpha = $pulseAlpha")
}
addListener(object : android.animation.AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: android.animation.Animator) {
isPulsing = false
pulseAlpha = 0f
linesToPulse.clear()
invalidate()
android.util.Log.d("GameView", "Pulse animation ended")
}
})
}
pulseAnimator?.start()
}
}

View file

@ -1,585 +0,0 @@
package com.mintris.model
import kotlin.random.Random
/**
* Represents the game board (grid) and manages game state
*/
class GameBoard(
val width: Int = 10,
val height: Int = 20
) {
// Board grid to track locked pieces
// True = occupied, False = empty
private val grid = Array(height) { BooleanArray(width) { false } }
// Current active tetromino
private var currentPiece: Tetromino? = null
// Next tetromino to be played
private var nextPiece: Tetromino? = null
// Hold piece
private var holdPiece: Tetromino? = null
private var canHold = true
// 7-bag randomizer
private val bag = mutableListOf<TetrominoType>()
// Game state
var score = 0
var level = 1
var startingLevel = 1 // Add this line to track the starting level
var lines = 0
var isGameOver = false
var isHardDropInProgress = false // Make public
var isPieceLocking = false // Make public
// Scoring state
private var combo = 0
private var lastClearWasTetris = false
private var lastClearWasPerfect = false
private var lastClearWasAllClear = false
private var lastPieceClearedLines = false // Track if the last piece placed cleared lines
// Animation state
var linesToClear = mutableListOf<Int>()
var isLineClearAnimationInProgress = false
// Initial game speed (milliseconds per drop)
var dropInterval = 1000L
// Callbacks for game events
var onPieceMove: (() -> Unit)? = null
var onPieceLock: (() -> Unit)? = null
var onNextPieceChanged: (() -> Unit)? = null
var onLineClear: ((Int, List<Int>) -> Unit)? = null
init {
spawnNextPiece()
spawnPiece()
}
/**
* Generates the next tetromino piece using 7-bag randomizer
*/
private fun spawnNextPiece() {
// If bag is empty, refill it with all piece types
if (bag.isEmpty()) {
bag.addAll(TetrominoType.values())
bag.shuffle()
}
// Take the next piece from the bag
nextPiece = Tetromino(bag.removeAt(0))
onNextPieceChanged?.invoke()
}
/**
* Hold the current piece
*/
fun holdPiece() {
if (!canHold) return
val current = currentPiece
if (holdPiece == null) {
// If no piece is held, hold current piece and spawn new one
holdPiece = current
spawnNextPiece()
spawnPiece()
} else {
// Swap current piece with held piece
currentPiece = holdPiece
holdPiece = current
// Reset position of swapped piece
currentPiece?.apply {
x = (width - getWidth()) / 2
y = 0
}
}
canHold = false
}
/**
* Get the currently held piece
*/
fun getHoldPiece(): Tetromino? = holdPiece
/**
* Get the next piece that will be spawned
*/
fun getNextPiece(): Tetromino? = nextPiece
/**
* Spawns the current tetromino at the top of the board
*/
fun spawnPiece() {
currentPiece = nextPiece
spawnNextPiece()
// Center the piece horizontally
currentPiece?.apply {
x = (width - getWidth()) / 2
y = 0
// Check if the piece can be placed (Game Over condition)
if (!canMove(0, 0)) {
isGameOver = true
}
}
}
/**
* Move the current piece left
*/
fun moveLeft() {
if (canMove(-1, 0)) {
currentPiece?.x = currentPiece?.x?.minus(1) ?: 0
onPieceMove?.invoke()
}
}
/**
* Move the current piece right
*/
fun moveRight() {
if (canMove(1, 0)) {
currentPiece?.x = currentPiece?.x?.plus(1) ?: 0
onPieceMove?.invoke()
}
}
/**
* Move the current piece down (soft drop)
*/
fun moveDown(): Boolean {
// Don't allow movement if a hard drop is in progress or piece is locking
if (isHardDropInProgress || isPieceLocking) return false
return if (canMove(0, 1)) {
currentPiece?.y = currentPiece?.y?.plus(1) ?: 0
onPieceMove?.invoke()
true
} else {
lockPiece()
false
}
}
/**
* Hard drop the current piece
*/
fun hardDrop() {
if (isHardDropInProgress || isPieceLocking) return // Prevent multiple hard drops
isHardDropInProgress = true
val piece = currentPiece ?: return
// Move piece down until it can't move anymore
while (canMove(0, 1)) {
piece.y++
onPieceMove?.invoke()
}
// Lock the piece immediately
lockPiece()
}
/**
* Rotate the current piece clockwise
*/
fun rotate() {
currentPiece?.let {
// Save current rotation
val originalX = it.x
val originalY = it.y
// Try to rotate
it.rotateClockwise()
// Wall kick logic - try to move the piece if rotation causes collision
if (!canMove(0, 0)) {
// Try to move left
if (canMove(-1, 0)) {
it.x--
}
// Try to move right
else if (canMove(1, 0)) {
it.x++
}
// Try to move 2 spaces (for I piece)
else if (canMove(-2, 0)) {
it.x -= 2
}
else if (canMove(2, 0)) {
it.x += 2
}
// Try to move up for floor kicks
else if (canMove(0, -1)) {
it.y--
}
// Revert if can't find a valid position
else {
it.rotateCounterClockwise()
it.x = originalX
it.y = originalY
}
}
}
}
/**
* Check if the current piece can move to the given position
*/
fun canMove(deltaX: Int, deltaY: Int): Boolean {
val piece = currentPiece ?: return false
val newX = piece.x + deltaX
val newY = piece.y + deltaY
for (y in 0 until piece.getHeight()) {
for (x in 0 until piece.getWidth()) {
if (piece.isBlockAt(x, y)) {
val boardX = newX + x
val boardY = newY + y
// Check if the position is outside the board horizontally
if (boardX < 0 || boardX >= width) {
return false
}
// Check if the position is below the board
if (boardY >= height) {
return false
}
// Check if the position is already occupied (but not if it's above the board)
if (boardY >= 0 && grid[boardY][boardX]) {
return false
}
}
}
}
return true
}
/**
* Lock the current piece in place
*/
private fun lockPiece() {
if (isPieceLocking) return // Prevent recursive locking
isPieceLocking = true
val piece = currentPiece ?: return
// Add the piece to the grid
for (y in 0 until piece.getHeight()) {
for (x in 0 until piece.getWidth()) {
if (piece.isBlockAt(x, y)) {
val boardX = piece.x + x
val boardY = piece.y + y
// Only add to grid if within bounds
if (boardY >= 0 && boardY < height && boardX >= 0 && boardX < width) {
grid[boardY][boardX] = true
}
}
}
}
// Trigger the piece lock vibration
onPieceLock?.invoke()
// Find and clear lines immediately
findAndClearLines()
// Spawn new piece immediately
spawnPiece()
// Allow holding piece again after locking
canHold = true
// Reset both states after everything is done
isPieceLocking = false
isHardDropInProgress = false
}
/**
* Find and clear completed lines immediately
*/
private fun findAndClearLines() {
// Quick scan for completed lines
var shiftAmount = 0
var y = height - 1
val linesToClear = mutableListOf<Int>()
while (y >= 0) {
if (grid[y].all { it }) {
// Line is full, add to lines to clear
linesToClear.add(y)
shiftAmount++
} else if (shiftAmount > 0) {
// Shift this row down by shiftAmount
System.arraycopy(grid[y], 0, grid[y + shiftAmount], 0, width)
}
y--
}
// If lines were cleared, calculate score in background and trigger callback
if (shiftAmount > 0) {
android.util.Log.d("GameBoard", "Lines cleared: $shiftAmount")
// Trigger line clear callback on main thread with the lines that were cleared
val mainHandler = android.os.Handler(android.os.Looper.getMainLooper())
mainHandler.post {
android.util.Log.d("GameBoard", "Triggering onLineClear callback with $shiftAmount lines")
try {
onLineClear?.invoke(shiftAmount, linesToClear) // Pass the lines that were cleared
android.util.Log.d("GameBoard", "onLineClear callback completed successfully")
} catch (e: Exception) {
android.util.Log.e("GameBoard", "Error in onLineClear callback", e)
}
}
// Clear top rows after callback
for (y in 0 until shiftAmount) {
java.util.Arrays.fill(grid[y], false)
}
Thread {
calculateScore(shiftAmount)
}.start()
}
// Update combo based on whether this piece cleared lines
if (shiftAmount > 0) {
if (lastPieceClearedLines) {
combo++
} else {
combo = 1 // Start new combo
}
} else {
combo = 0 // Reset combo if no lines cleared
}
lastPieceClearedLines = shiftAmount > 0
}
/**
* Calculate score for cleared lines
*/
private fun calculateScore(clearedLines: Int) {
// Pre-calculated score multipliers for better performance
val baseScore = when (clearedLines) {
1 -> 40
2 -> 100
3 -> 300
4 -> 1200
else -> 0
}
// Check for perfect clear (no blocks left)
val isPerfectClear = !grid.any { row -> row.any { it } }
// Check for all clear (no blocks in playfield)
val isAllClear = !grid.any { row -> row.any { it } } &&
currentPiece == null &&
nextPiece == null
// Calculate combo multiplier
val comboMultiplier = if (combo > 0) {
when (combo) {
1 -> 1.0
2 -> 1.5
3 -> 2.0
4 -> 2.5
else -> 3.0
}
} else 1.0
// Calculate back-to-back Tetris bonus
val backToBackMultiplier = if (clearedLines == 4 && lastClearWasTetris) 1.5 else 1.0
// Calculate perfect clear bonus
val perfectClearMultiplier = if (isPerfectClear) {
when (clearedLines) {
1 -> 2.0
2 -> 3.0
3 -> 4.0
4 -> 5.0
else -> 1.0
}
} else 1.0
// Calculate all clear bonus
val allClearMultiplier = if (isAllClear) 2.0 else 1.0
// Calculate T-Spin bonus
val tSpinMultiplier = if (isTSpin()) {
when (clearedLines) {
1 -> 2.0
2 -> 4.0
3 -> 6.0
else -> 1.0
}
} else 1.0
// Calculate final score with all multipliers
val finalScore = (baseScore * level * comboMultiplier *
backToBackMultiplier * perfectClearMultiplier *
allClearMultiplier * tSpinMultiplier).toInt()
// Update score on main thread
Thread {
score += finalScore
}.start()
// Update line clear state
lastClearWasTetris = clearedLines == 4
lastClearWasPerfect = isPerfectClear
lastClearWasAllClear = isAllClear
// Update lines cleared and level
lines += clearedLines
// Calculate level based on lines cleared, but ensure it's never below the starting level
level = Math.max((lines / 10) + 1, startingLevel)
// Update game speed based on level (NES formula)
dropInterval = (1000 * Math.pow(0.8, (level - 1).toDouble())).toLong()
}
/**
* Check if the last move was a T-Spin
*/
private fun isTSpin(): Boolean {
val piece = currentPiece ?: return false
if (piece.type != TetrominoType.T) return false
// Count occupied corners around the T piece
var occupiedCorners = 0
val centerX = piece.x + 1
val centerY = piece.y + 1
// Check all four corners
if (isOccupied(centerX - 1, centerY - 1)) occupiedCorners++
if (isOccupied(centerX + 1, centerY - 1)) occupiedCorners++
if (isOccupied(centerX - 1, centerY + 1)) occupiedCorners++
if (isOccupied(centerX + 1, centerY + 1)) occupiedCorners++
// T-Spin requires at least 3 occupied corners
return occupiedCorners >= 3
}
/**
* Get the ghost piece position (preview of where piece will land)
*/
fun getGhostY(): Int {
val piece = currentPiece ?: return 0
var ghostY = piece.y
// Find how far the piece can move down
while (true) {
if (canMove(0, ghostY - piece.y + 1)) {
ghostY++
} else {
break
}
}
// Ensure ghostY doesn't exceed the board height
return ghostY.coerceAtMost(height - 1)
}
/**
* Get the current tetromino
*/
fun getCurrentPiece(): Tetromino? = currentPiece
/**
* Check if a cell in the grid is occupied
*/
fun isOccupied(x: Int, y: Int): Boolean {
return if (x in 0 until width && y in 0 until height) {
grid[y][x]
} else {
false
}
}
/**
* Check if a line is completely filled
*/
fun isLineFull(y: Int): Boolean {
return if (y in 0 until height) {
grid[y].all { it }
} else {
false
}
}
/**
* Update the current level and adjust game parameters
*/
fun updateLevel(newLevel: Int) {
level = newLevel.coerceIn(1, 20)
startingLevel = level // Store the starting level
// Update game speed based on level (NES formula)
dropInterval = (1000 * Math.pow(0.8, (level - 1).toDouble())).toLong()
}
/**
* Start a new game
*/
fun startGame() {
reset()
// Initialize pieces
spawnNextPiece()
spawnPiece()
}
/**
* Reset the game board
*/
fun reset() {
// Clear the grid
for (y in 0 until height) {
for (x in 0 until width) {
grid[y][x] = false
}
}
// Reset game state
score = 0
level = startingLevel // Use starting level instead of resetting to 1
lines = 0
isGameOver = false
dropInterval = (1000 * Math.pow(0.8, (level - 1).toDouble())).toLong() // Set speed based on current level
// Reset scoring state
combo = 0
lastClearWasTetris = false
lastClearWasPerfect = false
lastClearWasAllClear = false
lastPieceClearedLines = false
// Reset piece state
holdPiece = null
canHold = true
bag.clear()
// Clear current and next pieces
currentPiece = null
nextPiece = null
}
/**
* Clear completed lines and move blocks down (legacy method, kept for reference)
*/
private fun clearLines(): Int {
return linesToClear.size // Return the number of lines that will be cleared
}
/**
* Get the current combo count
*/
fun getCombo(): Int = combo
}

View file

@ -1,196 +0,0 @@
package com.mintris.model
import android.content.Context
import android.content.SharedPreferences
class StatsManager(context: Context) {
private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
// Lifetime stats
private var totalGames: Int = 0
private var totalScore: Long = 0
private var totalLines: Int = 0
private var totalPieces: Int = 0
private var totalTime: Long = 0
private var maxLevel: Int = 0
private var maxScore: Int = 0
private var maxLines: Int = 0
// Line clear stats (lifetime)
private var totalSingles: Int = 0
private var totalDoubles: Int = 0
private var totalTriples: Int = 0
private var totalTetrises: Int = 0
// Session stats
private var sessionScore: Int = 0
private var sessionLines: Int = 0
private var sessionPieces: Int = 0
private var sessionTime: Long = 0
private var sessionLevel: Int = 0
// Line clear stats (session)
private var sessionSingles: Int = 0
private var sessionDoubles: Int = 0
private var sessionTriples: Int = 0
private var sessionTetrises: Int = 0
init {
loadStats()
}
private fun loadStats() {
totalGames = prefs.getInt(KEY_TOTAL_GAMES, 0)
totalScore = prefs.getLong(KEY_TOTAL_SCORE, 0)
totalLines = prefs.getInt(KEY_TOTAL_LINES, 0)
totalPieces = prefs.getInt(KEY_TOTAL_PIECES, 0)
totalTime = prefs.getLong(KEY_TOTAL_TIME, 0)
maxLevel = prefs.getInt(KEY_MAX_LEVEL, 0)
maxScore = prefs.getInt(KEY_MAX_SCORE, 0)
maxLines = prefs.getInt(KEY_MAX_LINES, 0)
// Load line clear stats
totalSingles = prefs.getInt(KEY_TOTAL_SINGLES, 0)
totalDoubles = prefs.getInt(KEY_TOTAL_DOUBLES, 0)
totalTriples = prefs.getInt(KEY_TOTAL_TRIPLES, 0)
totalTetrises = prefs.getInt(KEY_TOTAL_TETRISES, 0)
}
private fun saveStats() {
prefs.edit()
.putInt(KEY_TOTAL_GAMES, totalGames)
.putLong(KEY_TOTAL_SCORE, totalScore)
.putInt(KEY_TOTAL_LINES, totalLines)
.putInt(KEY_TOTAL_PIECES, totalPieces)
.putLong(KEY_TOTAL_TIME, totalTime)
.putInt(KEY_MAX_LEVEL, maxLevel)
.putInt(KEY_MAX_SCORE, maxScore)
.putInt(KEY_MAX_LINES, maxLines)
.putInt(KEY_TOTAL_SINGLES, totalSingles)
.putInt(KEY_TOTAL_DOUBLES, totalDoubles)
.putInt(KEY_TOTAL_TRIPLES, totalTriples)
.putInt(KEY_TOTAL_TETRISES, totalTetrises)
.apply()
}
fun startNewSession() {
sessionScore = 0
sessionLines = 0
sessionPieces = 0
sessionTime = 0
sessionLevel = 0
sessionSingles = 0
sessionDoubles = 0
sessionTriples = 0
sessionTetrises = 0
}
fun updateSessionStats(score: Int, lines: Int, pieces: Int, time: Long, level: Int) {
sessionScore = score
sessionLines = lines
sessionPieces = pieces
sessionTime = time
sessionLevel = level
}
fun recordLineClear(lineCount: Int) {
when (lineCount) {
1 -> {
sessionSingles++
totalSingles++
}
2 -> {
sessionDoubles++
totalDoubles++
}
3 -> {
sessionTriples++
totalTriples++
}
4 -> {
sessionTetrises++
totalTetrises++
}
}
}
fun endSession() {
totalGames++
totalScore += sessionScore
totalLines += sessionLines
totalPieces += sessionPieces
totalTime += sessionTime
if (sessionLevel > maxLevel) maxLevel = sessionLevel
if (sessionScore > maxScore) maxScore = sessionScore
if (sessionLines > maxLines) maxLines = sessionLines
saveStats()
}
// Getters for lifetime stats
fun getTotalGames(): Int = totalGames
fun getTotalScore(): Long = totalScore
fun getTotalLines(): Int = totalLines
fun getTotalPieces(): Int = totalPieces
fun getTotalTime(): Long = totalTime
fun getMaxLevel(): Int = maxLevel
fun getMaxScore(): Int = maxScore
fun getMaxLines(): Int = maxLines
// Getters for line clear stats (lifetime)
fun getTotalSingles(): Int = totalSingles
fun getTotalDoubles(): Int = totalDoubles
fun getTotalTriples(): Int = totalTriples
fun getTotalTetrises(): Int = totalTetrises
// Getters for session stats
fun getSessionScore(): Int = sessionScore
fun getSessionLines(): Int = sessionLines
fun getSessionPieces(): Int = sessionPieces
fun getSessionTime(): Long = sessionTime
fun getSessionLevel(): Int = sessionLevel
// Getters for line clear stats (session)
fun getSessionSingles(): Int = sessionSingles
fun getSessionDoubles(): Int = sessionDoubles
fun getSessionTriples(): Int = sessionTriples
fun getSessionTetrises(): Int = sessionTetrises
fun resetStats() {
// Reset all lifetime stats
totalGames = 0
totalScore = 0
totalLines = 0
totalPieces = 0
totalTime = 0
maxLevel = 0
maxScore = 0
maxLines = 0
// Reset line clear stats
totalSingles = 0
totalDoubles = 0
totalTriples = 0
totalTetrises = 0
// Save the reset stats
saveStats()
}
companion object {
private const val PREFS_NAME = "mintris_stats"
private const val KEY_TOTAL_GAMES = "total_games"
private const val KEY_TOTAL_SCORE = "total_score"
private const val KEY_TOTAL_LINES = "total_lines"
private const val KEY_TOTAL_PIECES = "total_pieces"
private const val KEY_TOTAL_TIME = "total_time"
private const val KEY_MAX_LEVEL = "max_level"
private const val KEY_MAX_SCORE = "max_score"
private const val KEY_MAX_LINES = "max_lines"
private const val KEY_TOTAL_SINGLES = "total_singles"
private const val KEY_TOTAL_DOUBLES = "total_doubles"
private const val KEY_TOTAL_TRIPLES = "total_triples"
private const val KEY_TOTAL_TETRISES = "total_tetrises"
}
}

View file

@ -1,17 +1,15 @@
package com.mintris
package com.pixelmintdrop
import android.app.Activity
import android.os.Bundle
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import com.mintris.databinding.HighScoreEntryBinding
import com.mintris.model.HighScore
import com.mintris.model.HighScoreManager
import com.mintris.model.PlayerProgressionManager
import com.pixelmintdrop.databinding.HighScoreEntryBinding
import com.pixelmintdrop.model.HighScore
import com.pixelmintdrop.model.HighScoreManager
import com.pixelmintdrop.model.PlayerProgressionManager
import android.graphics.Color
import android.view.KeyEvent
import android.view.InputDevice
class HighScoreEntryActivity : AppCompatActivity() {
private lateinit var binding: HighScoreEntryBinding
@ -39,6 +37,11 @@ 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()
@ -53,6 +56,36 @@ class HighScoreEntryActivity : AppCompatActivity() {
}
}
}
// Override onKeyDown to handle gamepad buttons
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
// Check if it's a gamepad input
if (event != null && isGamepadDevice(event.device)) {
when (keyCode) {
KeyEvent.KEYCODE_BUTTON_A,
KeyEvent.KEYCODE_DPAD_CENTER -> {
// A button saves the score
saveScore()
return true
}
KeyEvent.KEYCODE_BUTTON_B -> {
// B button cancels
setResult(Activity.RESULT_CANCELED)
finish()
return true
}
}
}
return super.onKeyDown(keyCode, event)
}
// Helper function to check if the device is a gamepad
private fun isGamepadDevice(device: InputDevice?): Boolean {
if (device == null) return false
val sources = device.sources
return (sources and InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD ||
(sources and InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK
}
// Prevent accidental back button press from causing issues
@ -66,8 +99,7 @@ class HighScoreEntryActivity : AppCompatActivity() {
}
private fun loadThemePreference(): String {
val prefs = getSharedPreferences("mintris_settings", MODE_PRIVATE)
return prefs.getString("selected_theme", PlayerProgressionManager.THEME_CLASSIC) ?: PlayerProgressionManager.THEME_CLASSIC
return progressionManager.getSelectedTheme()
}
private fun applyTheme(themeId: String) {

View file

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

File diff suppressed because it is too large Load diff

View file

@ -1,13 +1,11 @@
package com.mintris
package com.pixelmintdrop
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.mintris.databinding.ActivityStatsBinding
import com.mintris.model.StatsManager
import com.mintris.model.PlayerProgressionManager
import com.pixelmintdrop.databinding.ActivityStatsBinding
import com.pixelmintdrop.model.StatsManager
import com.pixelmintdrop.model.PlayerProgressionManager
import android.graphics.Color
import java.text.SimpleDateFormat
import java.util.*
@ -44,8 +42,7 @@ class StatsActivity : AppCompatActivity() {
}
private fun loadThemePreference(): String {
val prefs = getSharedPreferences("mintris_settings", MODE_PRIVATE)
return prefs.getString("selected_theme", PlayerProgressionManager.THEME_CLASSIC) ?: PlayerProgressionManager.THEME_CLASSIC
return progressionManager.getSelectedTheme()
}
private fun applyTheme(themeId: String) {
@ -81,7 +78,7 @@ class StatsActivity : AppCompatActivity() {
binding.totalSinglesText.setTextColor(textColor)
binding.totalDoublesText.setTextColor(textColor)
binding.totalTriplesText.setTextColor(textColor)
binding.totalTetrisesText.setTextColor(textColor)
binding.totalQuadsText.setTextColor(textColor)
binding.maxLevelText.setTextColor(textColor)
binding.maxScoreText.setTextColor(textColor)
binding.maxLinesText.setTextColor(textColor)
@ -119,7 +116,7 @@ class StatsActivity : AppCompatActivity() {
binding.totalSinglesText.text = getString(R.string.singles, statsManager.getTotalSingles())
binding.totalDoublesText.text = getString(R.string.doubles, statsManager.getTotalDoubles())
binding.totalTriplesText.text = getString(R.string.triples, statsManager.getTotalTriples())
binding.totalTetrisesText.text = getString(R.string.tetrises, statsManager.getTotalTetrises())
binding.totalQuadsText.text = getString(R.string.quads, statsManager.getTotalQuads())
// Update best performance stats
binding.maxLevelText.text = getString(R.string.max_level, statsManager.getMaxLevel())

View file

@ -0,0 +1,19 @@
package com.pixelmintdrop
import android.graphics.Color
object ThemeManager {
data class ThemeColors(
val background: Int,
val text: Int,
val accent: Int
)
fun getThemeColors(): ThemeColors {
return ThemeColors(
background = Color.BLACK,
text = Color.WHITE,
accent = Color.WHITE
)
}
}

View file

@ -0,0 +1,152 @@
package com.pixelmintdrop.accessibility
import android.content.Context
import android.os.Build
import android.view.View
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityManager
import android.widget.ImageButton
import android.widget.TextView
import androidx.core.view.ViewCompat
import com.pixelmintdrop.R
import com.pixelmintdrop.game.GameView
import com.pixelmintdrop.model.GamePieceType
/**
* Helper class to improve the game's accessibility for users with visual impairments
* or other accessibility needs.
*/
class GameAccessibilityHelper(private val context: Context) {
private val accessibilityManager = context.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
/**
* Sets up accessibility content descriptions for game controls
*/
fun setupAccessibilityDescriptions(
leftButton: ImageButton?,
rightButton: ImageButton?,
rotateButton: ImageButton?,
dropButton: ImageButton?,
holdButton: ImageButton?,
pauseButton: ImageButton?,
gameView: GameView?,
holdPieceView: View?,
nextPieceView: View?
) {
// Set content descriptions for all control buttons
leftButton?.contentDescription = context.getString(R.string.accessibility_move_left)
rightButton?.contentDescription = context.getString(R.string.accessibility_move_right)
rotateButton?.contentDescription = context.getString(R.string.accessibility_rotate_piece)
dropButton?.contentDescription = context.getString(R.string.accessibility_drop_piece)
holdButton?.contentDescription = context.getString(R.string.accessibility_hold_piece)
pauseButton?.contentDescription = context.getString(R.string.accessibility_pause_game)
// Set content descriptions for game views
gameView?.contentDescription = context.getString(R.string.accessibility_game_board)
holdPieceView?.contentDescription = context.getString(R.string.accessibility_held_piece)
nextPieceView?.contentDescription = context.getString(R.string.accessibility_next_piece)
// Add accessibility delegate to the game view for custom events
gameView?.let {
ViewCompat.setAccessibilityDelegate(it, object : androidx.core.view.AccessibilityDelegateCompat() {
override fun onPopulateAccessibilityEvent(host: View, event: AccessibilityEvent) {
super.onPopulateAccessibilityEvent(host, event)
// Add custom content to accessibility events if needed
if (event.eventType == AccessibilityEvent.TYPE_VIEW_SELECTED) {
// Example: announce the current piece type
event.text.add("Current piece: ${getCurrentPieceDescription(it)}")
}
}
})
}
}
/**
* Announces important game events via accessibility services
*/
fun announceGameEvent(view: View, message: String) {
if (accessibilityManager.isEnabled) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
view.announceForAccessibility(message)
} else {
// For older Android versions
val event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_ANNOUNCEMENT)
event.text.add(message)
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT)
}
}
}
/**
* Updates score and level information to be more accessible
*/
fun updateGameStatusAccessibility(
scoreTextView: TextView?,
levelTextView: TextView?,
linesTextView: TextView?,
score: Long,
level: Int,
lines: Int
) {
// Make sure score is accessible to screen readers with proper formatting
scoreTextView?.let {
it.contentDescription = "Score: $score points"
}
levelTextView?.let {
it.contentDescription = "Level: $level"
}
linesTextView?.let {
it.contentDescription = "Lines cleared: $lines"
}
}
/**
* Get a description of the current Tetromino piece for accessibility announcements
*/
private fun getCurrentPieceDescription(gameView: GameView): String {
val pieceType = gameView.getCurrentPieceType() ?: return "No piece"
return when (pieceType) {
GamePieceType.I -> "I piece, long bar"
GamePieceType.J -> "J piece, hook shape pointing left"
GamePieceType.L -> "L piece, hook shape pointing right"
GamePieceType.O -> "O piece, square shape"
GamePieceType.S -> "S piece, zigzag shape"
GamePieceType.T -> "T piece, T shape"
GamePieceType.Z -> "Z piece, reverse zigzag shape"
}
}
/**
* Announce the game over event with final score
*/
fun announceGameOver(view: View, score: Long) {
announceGameEvent(view, "Game over. Final score: $score points")
}
/**
* Announce level up
*/
fun announceLevelUp(view: View, newLevel: Int) {
announceGameEvent(view, "Level up! Now at level $newLevel")
}
/**
* Get the piece name for accessibility announcements
*/
private fun getPieceName(type: GamePieceType): String {
return when (type) {
GamePieceType.I -> "I piece"
GamePieceType.J -> "J piece"
GamePieceType.L -> "L piece"
GamePieceType.O -> "O piece"
GamePieceType.S -> "S piece"
GamePieceType.T -> "T piece"
GamePieceType.Z -> "Z piece"
}
}
}

View file

@ -1,20 +1,22 @@
package com.mintris.audio
package com.pixelmintdrop.audio
import android.content.Context
import android.media.MediaPlayer
import android.media.AudioAttributes
import android.os.Build
import android.util.Log
import com.mintris.R
import com.pixelmintdrop.R
class GameMusic(private val context: Context) {
private var mediaPlayer: MediaPlayer? = null
private var gameOverPlayer: MediaPlayer? = null
private var isEnabled = true
private var isPrepared = false
init {
try {
setupMediaPlayer()
setupGameOverPlayer()
} catch (e: Exception) {
Log.e("GameMusic", "Error initializing: ${e.message}")
}
@ -46,6 +48,49 @@ class GameMusic(private val context: Context) {
}
}
private fun setupGameOverPlayer() {
try {
Log.d("GameMusic", "Setting up GameOver MediaPlayer")
gameOverPlayer = MediaPlayer.create(context, R.raw.game_over).apply {
setVolume(1.0f, 1.0f) // Increased from 0.7f to 1.0f for maximum volume
// Set audio attributes for better performance
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
setAudioAttributes(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_GAME)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.build()
)
}
}
Log.d("GameMusic", "GameOver MediaPlayer setup complete")
} catch (e: Exception) {
Log.e("GameMusic", "Error setting up GameOver MediaPlayer", e)
gameOverPlayer = null
}
}
fun playGameOver() {
if (isEnabled && gameOverPlayer != null) {
try {
Log.d("GameMusic", "Playing game over sound")
// Temporarily lower background music volume
mediaPlayer?.setVolume(0.2f, 0.2f)
// Play game over sound
gameOverPlayer?.start()
// Restore background music volume after a delay
gameOverPlayer?.setOnCompletionListener {
mediaPlayer?.setVolume(0.5f, 0.5f)
}
} catch (e: Exception) {
Log.e("GameMusic", "Error playing game over sound: ${e.message}")
}
}
}
fun start() {
if (isEnabled && mediaPlayer != null && isPrepared) {
try {
@ -103,14 +148,25 @@ class GameMusic(private val context: Context) {
fun isEnabled(): Boolean = isEnabled
fun release() {
/**
* Releases all media player resources to prevent memory leaks
*/
fun releaseResources() {
try {
Log.d("GameMusic", "Releasing MediaPlayer")
Log.d("GameMusic", "Releasing MediaPlayer resources")
mediaPlayer?.release()
gameOverPlayer?.release()
mediaPlayer = null
gameOverPlayer = null
isPrepared = false
} catch (e: Exception) {
Log.e("GameMusic", "Error releasing music: ${e.message}")
Log.e("GameMusic", "Error releasing music resources: ${e.message}")
}
}
// Keeping old method for backward compatibility
@Deprecated("Use releaseResources() instead", ReplaceWith("releaseResources()"))
fun release() {
releaseResources()
}
}

View file

@ -0,0 +1,136 @@
package com.pixelmintdrop.game
import android.content.Context
import android.os.Build
import android.os.VibrationEffect
import android.os.Vibrator
import android.os.VibratorManager
import android.view.HapticFeedbackConstants
import android.view.View
import android.util.Log
/**
* Handles haptic feedback for game events
*/
class GameHaptics(private val context: Context) {
companion object {
private const val TAG = "GameHaptics"
}
// Vibrator service
private val vibrator: Vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val vibratorManager = context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
vibratorManager.defaultVibrator
} else {
@Suppress("DEPRECATION")
context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
}
// Track if gamepad is connected
private var isGamepadConnected = false
// Set gamepad connection state
fun setGamepadConnected(connected: Boolean) {
isGamepadConnected = connected
}
// Get vibration multiplier based on input method
private fun getVibrationMultiplier(): Float {
return if (isGamepadConnected) 1.5f else 1.0f
}
// Vibrate for line clear (more intense for more lines)
fun vibrateForLineClear(lineCount: Int) {
Log.d(TAG, "Attempting to vibrate for $lineCount lines")
// Only proceed if the device has a vibrator and it's available
if (!vibrator.hasVibrator()) return
// Scale duration and amplitude based on line count and input method
val multiplier = getVibrationMultiplier()
val duration = when(lineCount) {
1 -> (50L * multiplier).toLong() // Single line: short vibration
2 -> (80L * multiplier).toLong() // Double line: slightly longer
3 -> (120L * multiplier).toLong() // Triple line: even longer
4 -> (200L * multiplier).toLong() // Quad: longest vibration
else -> (50L * multiplier).toLong()
}
val amplitude = when(lineCount) {
1 -> (80 * multiplier).toInt().coerceAtMost(255) // Single line: mild vibration
2 -> (120 * multiplier).toInt().coerceAtMost(255) // Double line: medium vibration
3 -> (180 * multiplier).toInt().coerceAtMost(255) // Triple line: strong vibration
4 -> 255 // Quad: maximum vibration
else -> (80 * multiplier).toInt().coerceAtMost(255)
}
Log.d(TAG, "Vibration parameters - Duration: ${duration}ms, Amplitude: $amplitude")
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
vibrator.vibrate(VibrationEffect.createOneShot(duration, amplitude))
Log.d(TAG, "Vibration triggered successfully")
} else {
@Suppress("DEPRECATION")
vibrator.vibrate(duration)
Log.w(TAG, "Device does not support vibration effects (Android < 8.0)")
}
} catch (e: Exception) {
Log.e(TAG, "Error triggering vibration", e)
}
}
fun performHapticFeedback(view: View, feedbackType: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
view.performHapticFeedback(HapticFeedbackConstants.CONFIRM)
} else {
@Suppress("DEPRECATION")
view.performHapticFeedback(feedbackType)
}
}
fun vibrateForPieceLock() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val multiplier = getVibrationMultiplier()
val duration = (50L * multiplier).toLong()
val amplitude = (VibrationEffect.DEFAULT_AMPLITUDE * multiplier).toInt().coerceAtMost(255)
val vibrationEffect = VibrationEffect.createOneShot(duration, amplitude)
vibrator.vibrate(vibrationEffect)
}
}
fun vibrateForPieceMove() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val multiplier = getVibrationMultiplier()
val duration = (20L * multiplier).toLong()
val amplitude = (VibrationEffect.DEFAULT_AMPLITUDE * 0.3 * multiplier).toInt().coerceAtLeast(1).coerceAtMost(255)
val vibrationEffect = VibrationEffect.createOneShot(duration, amplitude)
vibrator.vibrate(vibrationEffect)
}
}
fun vibrateForGameOver() {
Log.d(TAG, "Attempting to vibrate for game over")
// Only proceed if the device has a vibrator and it's available
if (!vibrator.hasVibrator()) return
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val multiplier = getVibrationMultiplier()
val duration = (300L * multiplier).toLong()
val amplitude = (VibrationEffect.DEFAULT_AMPLITUDE * multiplier).toInt().coerceAtMost(255)
val vibrationEffect = VibrationEffect.createOneShot(duration, amplitude)
vibrator.vibrate(vibrationEffect)
Log.d(TAG, "Game over vibration triggered successfully")
} else {
@Suppress("DEPRECATION")
vibrator.vibrate(300L)
Log.w(TAG, "Device does not support vibration effects (Android < 8.0)")
}
} catch (e: Exception) {
Log.e(TAG, "Error triggering game over vibration", e)
}
}
}

View file

@ -0,0 +1,154 @@
package com.pixelmintdrop.game
import android.content.Context
import android.content.SharedPreferences
import android.os.Bundle
import com.google.gson.Gson
import com.pixelmintdrop.audio.GameMusic
import com.pixelmintdrop.model.GameBoard
import com.pixelmintdrop.model.HighScoreManager
import com.pixelmintdrop.model.StatsManager
/**
* Handles the game's lifecycle events to ensure proper resource management
* and state saving/restoration.
*/
class GameLifecycleManager(private val context: Context) {
private val sharedPreferences: SharedPreferences =
context.getSharedPreferences("com.com.pixelmintgames.pixelmintdrop.game_state", Context.MODE_PRIVATE)
private val gson = Gson()
/**
* Save the current game state when the app is paused
*/
fun saveGameState(
gameBoard: GameBoard,
currentScore: Long,
currentLevel: Int,
piecesPlaced: Int,
gameStartTime: Long
) {
val editor = sharedPreferences.edit()
// Save primitive data
editor.putLong("current_score", currentScore)
editor.putInt("current_level", currentLevel)
editor.putInt("pieces_placed", piecesPlaced)
editor.putLong("game_start_time", gameStartTime)
// Save complex GameBoard state - we don't serialize the GameBoard directly
// Instead we save key properties that can be used to recreate the board state
try {
editor.putInt("board_level", gameBoard.level)
editor.putInt("board_lines", gameBoard.lines)
editor.putInt("board_score", gameBoard.score)
} catch (e: Exception) {
// If serialization fails, just clear the saved state
editor.remove("board_level")
editor.remove("board_lines")
editor.remove("board_score")
}
editor.apply()
}
/**
* Restore the saved game state when the app is resumed
* Returns a bundle with the restored state or null if no state exists
*/
fun restoreGameState(): Bundle? {
// Check if we have a saved state
if (!sharedPreferences.contains("current_score")) {
return null
}
val bundle = Bundle()
// Restore primitive data
bundle.putLong("current_score", sharedPreferences.getLong("current_score", 0))
bundle.putInt("current_level", sharedPreferences.getInt("current_level", 1))
bundle.putInt("pieces_placed", sharedPreferences.getInt("pieces_placed", 0))
bundle.putLong("game_start_time", sharedPreferences.getLong("game_start_time", 0))
// We don't try to deserialize the GameBoard, as it's too complex
// Instead, we'll create a new game board and apply saved properties later
bundle.putInt("board_level", sharedPreferences.getInt("board_level", 1))
bundle.putInt("board_lines", sharedPreferences.getInt("board_lines", 0))
bundle.putInt("board_score", sharedPreferences.getInt("board_score", 0))
return bundle
}
/**
* Clears the saved game state
*/
fun clearGameState() {
sharedPreferences.edit().clear().apply()
}
/**
* Handle activity pause
*/
fun onPause(
gameView: GameView?,
gameMusic: GameMusic?,
statsManager: StatsManager?,
highScoreManager: HighScoreManager?
) {
// Pause the game view
gameView?.let {
if (!it.isPaused && !it.isGameOver()) {
it.pause()
}
}
// Pause music
gameMusic?.pause()
// Save any pending stats and scores - these methods must be made public in their respective classes
// or this functionality should be removed if those methods can't be accessed
try {
statsManager?.let { /* Call public save method if available */ }
highScoreManager?.let { /* Call public save method if available */ }
} catch (e: Exception) {
// Log error but continue
}
}
/**
* Handle activity resume
*/
fun onResume(
gameView: GameView?,
gameMusic: GameMusic?,
isMusicEnabled: Boolean
) {
// Resume music if enabled
if (isMusicEnabled) {
gameMusic?.resume()
}
}
/**
* Handle activity destroy
*/
fun onDestroy(
gameView: GameView?,
gameMusic: GameMusic?,
statsManager: StatsManager?,
highScoreManager: HighScoreManager?
) {
// Release resources
gameView?.releaseResources()
gameMusic?.releaseResources()
// Save any pending data - these methods must be made public in their respective classes
// or this functionality should be removed if those methods can't be accessed
try {
statsManager?.let { /* Call public save method if available */ }
highScoreManager?.let { /* Call public save method if available */ }
} catch (e: Exception) {
// Log error but continue
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,510 @@
package com.pixelmintdrop.game
import android.os.SystemClock
import android.view.InputDevice
import android.view.KeyEvent
import android.view.MotionEvent
import android.util.Log
import android.content.Context
import android.os.Build
import android.os.VibrationEffect
import android.os.Handler
import android.os.Looper
/**
* GamepadController handles gamepad input for the pixelmintdrop game.
* Supports multiple gamepad types including:
* - Microsoft Xbox controllers
* - Sony PlayStation controllers
* - Nintendo Switch controllers
* - Backbone controllers
*/
class GamepadController(
private val gameView: GameView
) {
companion object {
private const val TAG = "GamepadController"
// Deadzone for analog sticks (normalized value 0.0-1.0)
private const val STICK_DEADZONE = 0.30f
// Cooldown times for responsive input without repeating too quickly
private const val MOVE_COOLDOWN_MS = 100L
private const val ROTATION_COOLDOWN_MS = 150L
private const val HARD_DROP_COOLDOWN_MS = 200L
private const val HOLD_COOLDOWN_MS = 250L
// Continuous movement repeat delay
private const val CONTINUOUS_MOVEMENT_DELAY_MS = 100L
// Rumble patterns
private const val RUMBLE_MOVE_DURATION_MS = 20L
private const val RUMBLE_ROTATE_DURATION_MS = 30L
private const val RUMBLE_HARD_DROP_DURATION_MS = 100L
private const val RUMBLE_LINE_CLEAR_DURATION_MS = 150L
// Check if device is a gamepad
fun isGamepad(device: InputDevice?): Boolean {
if (device == null) return false
// Check for gamepad via input device sources
val sources = device.sources
return (sources and InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD ||
(sources and InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK
}
// Get a list of all connected gamepads
fun getGamepads(): List<InputDevice> {
val gamepads = mutableListOf<InputDevice>()
val deviceIds = InputDevice.getDeviceIds()
for (deviceId in deviceIds) {
val device = InputDevice.getDevice(deviceId)
if (device != null && isGamepad(device)) {
gamepads.add(device)
}
}
return gamepads
}
// Check if any gamepad is connected
fun isGamepadConnected(): Boolean {
return getGamepads().isNotEmpty()
}
// Get the name of the first connected gamepad
fun getConnectedGamepadName(): String? {
val gamepads = getGamepads()
if (gamepads.isEmpty()) return null
return gamepads.first().name
}
// Get information about all connected gamepads
fun getConnectedGamepadsInfo(): List<String> {
return getGamepads().map { it.name }
}
// Check if device supports vibration
fun supportsVibration(device: InputDevice?): Boolean {
if (device == null) return false
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return false
return device.vibratorManager?.vibratorIds?.isNotEmpty() ?: false
}
}
// Timestamps for cooldowns
private var lastMoveTime = 0L
private var lastRotationTime = 0L
private var lastHardDropTime = 0L
private var lastHoldTime = 0L
// Track current directional input state
private var isMovingLeft = false
private var isMovingRight = false
private var isMovingDown = false
// Handler for continuous movement
private val handler = Handler(Looper.getMainLooper())
private val moveLeftRunnable = object : Runnable {
override fun run() {
if (isMovingLeft && gameView.isActive()) {
gameView.moveLeft()
vibrateForPieceMove()
handler.postDelayed(this, CONTINUOUS_MOVEMENT_DELAY_MS)
}
}
}
private val moveRightRunnable = object : Runnable {
override fun run() {
if (isMovingRight && gameView.isActive()) {
gameView.moveRight()
vibrateForPieceMove()
handler.postDelayed(this, CONTINUOUS_MOVEMENT_DELAY_MS)
}
}
}
private val moveDownRunnable = object : Runnable {
override fun run() {
if (isMovingDown && gameView.isActive()) {
gameView.softDrop()
vibrateForPieceMove()
handler.postDelayed(this, CONTINUOUS_MOVEMENT_DELAY_MS)
}
}
}
// Callback interfaces
interface GamepadConnectionListener {
fun onGamepadConnected(gamepadName: String)
fun onGamepadDisconnected(gamepadName: String)
}
interface GamepadMenuListener {
fun onPauseRequested()
}
interface GamepadNavigationListener {
fun onMenuUp()
fun onMenuDown()
fun onMenuSelect()
fun onMenuLeft()
fun onMenuRight()
}
// Listeners
private var connectionListener: GamepadConnectionListener? = null
private var menuListener: GamepadMenuListener? = null
private var navigationListener: GamepadNavigationListener? = null
// Currently active gamepad for rumble
private var activeGamepad: InputDevice? = null
/**
* Set a listener for gamepad connection events
*/
fun setGamepadConnectionListener(listener: GamepadConnectionListener) {
connectionListener = listener
}
/**
* Set a listener for gamepad menu events (pause/start button)
*/
fun setGamepadMenuListener(listener: GamepadMenuListener) {
menuListener = listener
}
/**
* Set a listener for gamepad navigation events
*/
fun setGamepadNavigationListener(listener: GamepadNavigationListener) {
navigationListener = listener
}
/**
* Function to check for newly connected gamepads.
* Call this periodically from the activity to detect connection changes.
*/
fun checkForGamepadChanges(context: Context) {
// Implementation would track previous and current gamepads
// and notify through the connectionListener
// This would be called from the activity's onResume or via a handler
}
/**
* Vibrate the gamepad if supported
*/
fun vibrateGamepad(durationMs: Long, amplitude: Int) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return
val gamepad = activeGamepad ?: return
if (supportsVibration(gamepad)) {
try {
val vibrator = gamepad.vibratorManager
val vibratorIds = vibrator.vibratorIds
if (vibratorIds.isNotEmpty()) {
val effect = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
VibrationEffect.createOneShot(durationMs, amplitude)
} else {
// For older devices, fall back to a simple vibration
VibrationEffect.createOneShot(durationMs, VibrationEffect.DEFAULT_AMPLITUDE)
}
// Create combined vibration for Android S+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val combinedVibration = android.os.CombinedVibration.createParallel(effect)
vibrator.vibrate(combinedVibration)
}
}
} catch (e: Exception) {
Log.e(TAG, "Error vibrating gamepad", e)
}
}
}
/**
* Vibrate for piece movement
*/
fun vibrateForPieceMove() {
vibrateGamepad(RUMBLE_MOVE_DURATION_MS, 50)
}
/**
* Vibrate for piece rotation
*/
fun vibrateForPieceRotation() {
vibrateGamepad(RUMBLE_ROTATE_DURATION_MS, 80)
}
/**
* Vibrate for hard drop
*/
fun vibrateForHardDrop() {
vibrateGamepad(RUMBLE_HARD_DROP_DURATION_MS, 150)
}
/**
* Vibrate for line clear
*/
fun vibrateForLineClear(lineCount: Int) {
val amplitude = when (lineCount) {
1 -> 100
2 -> 150
3 -> 200
else -> 255 // For quad (4 lines)
}
vibrateGamepad(RUMBLE_LINE_CLEAR_DURATION_MS, amplitude)
}
/**
* Process a key event from a gamepad
* @return true if the event was handled, false otherwise
*/
fun handleKeyEvent(keyCode: Int, event: KeyEvent): Boolean {
// Skip if game is not active but handle menu navigation
if (!gameView.isActive()) {
// Handle menu navigation even when game is not active
if (event.action == KeyEvent.ACTION_DOWN) {
when (keyCode) {
KeyEvent.KEYCODE_DPAD_UP -> {
navigationListener?.onMenuUp()
return true
}
KeyEvent.KEYCODE_DPAD_DOWN -> {
navigationListener?.onMenuDown()
return true
}
KeyEvent.KEYCODE_DPAD_LEFT -> {
navigationListener?.onMenuLeft()
return true
}
KeyEvent.KEYCODE_DPAD_RIGHT -> {
navigationListener?.onMenuRight()
return true
}
KeyEvent.KEYCODE_BUTTON_A,
KeyEvent.KEYCODE_DPAD_CENTER -> {
navigationListener?.onMenuSelect()
return true
}
KeyEvent.KEYCODE_BUTTON_B -> {
// B button can be used to go back/cancel
menuListener?.onPauseRequested()
return true
}
}
}
// Handle menu/start button for pause menu
if (event.action == KeyEvent.ACTION_DOWN && keyCode == KeyEvent.KEYCODE_BUTTON_START) {
menuListener?.onPauseRequested()
return true
}
return false
}
val device = event.device
if (!isGamepad(device)) return false
// Update active gamepad for rumble
activeGamepad = device
val currentTime = SystemClock.uptimeMillis()
when (event.action) {
KeyEvent.ACTION_DOWN -> {
when (keyCode) {
// D-pad and analog movement
KeyEvent.KEYCODE_DPAD_LEFT -> {
if (!isMovingLeft) {
gameView.moveLeft()
vibrateForPieceMove()
lastMoveTime = currentTime
isMovingLeft = true
// Start continuous movement after initial input
handler.postDelayed(moveLeftRunnable, CONTINUOUS_MOVEMENT_DELAY_MS)
return true
}
}
KeyEvent.KEYCODE_DPAD_RIGHT -> {
if (!isMovingRight) {
gameView.moveRight()
vibrateForPieceMove()
lastMoveTime = currentTime
isMovingRight = true
// Start continuous movement after initial input
handler.postDelayed(moveRightRunnable, CONTINUOUS_MOVEMENT_DELAY_MS)
return true
}
}
KeyEvent.KEYCODE_DPAD_DOWN -> {
if (!isMovingDown) {
gameView.softDrop()
vibrateForPieceMove()
lastMoveTime = currentTime
isMovingDown = true
// Start continuous movement after initial input
handler.postDelayed(moveDownRunnable, CONTINUOUS_MOVEMENT_DELAY_MS)
return true
}
}
KeyEvent.KEYCODE_DPAD_UP -> {
if (currentTime - lastHardDropTime > HARD_DROP_COOLDOWN_MS) {
gameView.hardDrop()
vibrateForHardDrop()
lastHardDropTime = currentTime
return true
}
}
// Start button (pause)
KeyEvent.KEYCODE_BUTTON_START -> {
menuListener?.onPauseRequested()
return true
}
// Rotation buttons - supporting multiple buttons for different controllers
KeyEvent.KEYCODE_BUTTON_A,
KeyEvent.KEYCODE_BUTTON_X -> {
if (currentTime - lastRotationTime > ROTATION_COOLDOWN_MS) {
gameView.rotate()
vibrateForPieceRotation()
lastRotationTime = currentTime
return true
}
}
KeyEvent.KEYCODE_BUTTON_B -> {
if (currentTime - lastRotationTime > ROTATION_COOLDOWN_MS) {
gameView.rotateCounterClockwise()
vibrateForPieceRotation()
lastRotationTime = currentTime
return true
}
}
// Hold piece buttons
KeyEvent.KEYCODE_BUTTON_Y,
KeyEvent.KEYCODE_BUTTON_L1,
KeyEvent.KEYCODE_BUTTON_R1 -> {
if (currentTime - lastHoldTime > HOLD_COOLDOWN_MS) {
gameView.holdPiece()
vibrateForPieceRotation()
lastHoldTime = currentTime
return true
}
}
}
}
KeyEvent.ACTION_UP -> {
when (keyCode) {
KeyEvent.KEYCODE_DPAD_LEFT -> {
isMovingLeft = false
handler.removeCallbacks(moveLeftRunnable)
return true
}
KeyEvent.KEYCODE_DPAD_RIGHT -> {
isMovingRight = false
handler.removeCallbacks(moveRightRunnable)
return true
}
KeyEvent.KEYCODE_DPAD_DOWN -> {
isMovingDown = false
handler.removeCallbacks(moveDownRunnable)
return true
}
}
}
}
return false
}
/**
* Process generic motion events (for analog sticks)
* @return true if the event was handled, false otherwise
*/
fun handleMotionEvent(event: MotionEvent): Boolean {
// Skip if game is not active
if (!gameView.isActive()) return false
val device = event.device
if (!isGamepad(device)) return false
// Update active gamepad for rumble
activeGamepad = device
val currentTime = SystemClock.uptimeMillis()
// Process left analog stick
val axisX = event.getAxisValue(MotionEvent.AXIS_X)
val axisY = event.getAxisValue(MotionEvent.AXIS_Y)
// Apply deadzone
if (Math.abs(axisX) > STICK_DEADZONE) {
if (axisX > 0 && !isMovingRight) {
gameView.moveRight()
vibrateForPieceMove()
lastMoveTime = currentTime
isMovingRight = true
isMovingLeft = false
// Start continuous movement after initial input
handler.removeCallbacks(moveLeftRunnable)
handler.postDelayed(moveRightRunnable, CONTINUOUS_MOVEMENT_DELAY_MS)
return true
} else if (axisX < 0 && !isMovingLeft) {
gameView.moveLeft()
vibrateForPieceMove()
lastMoveTime = currentTime
isMovingLeft = true
isMovingRight = false
// Start continuous movement after initial input
handler.removeCallbacks(moveRightRunnable)
handler.postDelayed(moveLeftRunnable, CONTINUOUS_MOVEMENT_DELAY_MS)
return true
}
} else {
// Reset horizontal movement flags when stick returns to center
isMovingLeft = false
isMovingRight = false
handler.removeCallbacks(moveLeftRunnable)
handler.removeCallbacks(moveRightRunnable)
}
if (Math.abs(axisY) > STICK_DEADZONE) {
if (axisY > 0 && !isMovingDown) {
gameView.softDrop()
vibrateForPieceMove()
lastMoveTime = currentTime
isMovingDown = true
// Start continuous movement after initial input
handler.postDelayed(moveDownRunnable, CONTINUOUS_MOVEMENT_DELAY_MS)
return true
}
} else {
// Reset vertical movement flag when stick returns to center
isMovingDown = false
handler.removeCallbacks(moveDownRunnable)
}
// Check right analog stick for rotation
val axisZ = event.getAxisValue(MotionEvent.AXIS_Z)
val axisRZ = event.getAxisValue(MotionEvent.AXIS_RZ)
if (Math.abs(axisZ) > STICK_DEADZONE || Math.abs(axisRZ) > STICK_DEADZONE) {
if (currentTime - lastRotationTime > ROTATION_COOLDOWN_MS) {
gameView.rotate()
vibrateForPieceRotation()
lastRotationTime = currentTime
return true
}
}
return false
}
}

View file

@ -1,53 +1,70 @@
package com.mintris.game
package com.pixelmintdrop.game
import android.content.Context
import android.graphics.BlurMaskFilter
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.graphics.BlurMaskFilter
import android.util.AttributeSet
import android.view.View
import com.pixelmintdrop.model.GameBoard
import kotlin.math.min
/**
* Custom view to display the next Tetromino piece
* View that displays the currently held piece
*/
class NextPieceView @JvmOverloads constructor(
class HoldPieceView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private var gameView: GameView? = null
private var gameBoard: GameBoard? = null
// Rendering
private val blockPaint = Paint().apply {
color = Color.WHITE
isAntiAlias = true
style = Paint.Style.FILL
}
private val glowPaint = Paint().apply {
color = Color.WHITE
alpha = 30
alpha = 40
isAntiAlias = true
style = Paint.Style.STROKE
strokeWidth = 1.5f
maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.OUTER)
}
private val blockGlowPaint = Paint().apply {
color = Color.WHITE
alpha = 60
isAntiAlias = true
style = Paint.Style.FILL
maskFilter = BlurMaskFilter(12f, BlurMaskFilter.Blur.OUTER)
}
/**
* Set the game view to get the next piece from
* Set the game view reference
*/
fun setGameView(gameView: GameView) {
this.gameView = gameView
fun setGameView(view: GameView) {
gameView = view
gameBoard = view.getGameBoard()
}
/**
* Get the game board reference
*/
private fun getGameBoard(): GameBoard? = gameBoard
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// Get the next piece from game view
gameView?.let {
it.getNextPiece()?.let { piece ->
// Get the held piece from game board
gameBoard?.let {
it.getHoldPiece()?.let { piece ->
val width = piece.getWidth()
val height = piece.getHeight()
@ -75,6 +92,7 @@ class NextPieceView @JvmOverloads constructor(
glowPaint
)
// Draw the held piece
for (y in 0 until height) {
for (x in 0 until width) {
if (piece.isBlockAt(x, y)) {
@ -83,13 +101,29 @@ class NextPieceView @JvmOverloads constructor(
val right = left + previewBlockSize
val bottom = top + previewBlockSize
// Draw block with subtle glow
val rect = RectF(left + 1, top + 1, right - 1, bottom - 1)
canvas.drawRect(rect, blockPaint)
// Draw outer glow
blockGlowPaint.color = Color.WHITE
canvas.drawRect(
left - 2f,
top - 2f,
right + 2f,
bottom + 2f,
blockGlowPaint
)
// Draw subtle border glow
val glowRect = RectF(left, top, right, bottom)
canvas.drawRect(glowRect, glowPaint)
// Draw block
blockPaint.color = Color.WHITE
canvas.drawRect(left, top, right, bottom, blockPaint)
// Draw inner glow
glowPaint.color = Color.WHITE
canvas.drawRect(
left + 1f,
top + 1f,
right - 1f,
bottom - 1f,
glowPaint
)
}
}
}

View file

@ -0,0 +1,97 @@
package com.pixelmintdrop.game
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.graphics.BlurMaskFilter
import android.util.AttributeSet
import android.view.View
import kotlin.math.min
/**
* Custom view to display the next game piece
*/
class NextPieceView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private var gameView: GameView? = null
// Rendering
private val blockPaint = Paint().apply {
color = Color.WHITE
isAntiAlias = true
}
private val glowPaint = Paint().apply {
color = Color.WHITE
alpha = 30
isAntiAlias = true
style = Paint.Style.STROKE
strokeWidth = 1.5f
}
/**
* Set the game view to get the next piece from
*/
fun setGameView(gameView: GameView) {
this.gameView = gameView
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// Get the next piece from game view
gameView?.getNextPiece()?.let { piece ->
val width = piece.getWidth()
val height = piece.getHeight()
// Calculate block size for the preview (smaller than main board)
val previewBlockSize = min(
canvas.width.toFloat() / (width.toFloat() + 2f),
canvas.height.toFloat() / (height.toFloat() + 2f)
)
// Center the piece in the preview area
val previewLeft = (canvas.width.toFloat() - width.toFloat() * previewBlockSize) / 2f
val previewTop = (canvas.height.toFloat() - height.toFloat() * previewBlockSize) / 2f
// Draw subtle background glow
val glowPaint = Paint().apply {
color = Color.WHITE
alpha = 10
maskFilter = BlurMaskFilter(previewBlockSize * 0.5f, BlurMaskFilter.Blur.OUTER)
}
canvas.drawRect(
previewLeft - previewBlockSize,
previewTop - previewBlockSize,
previewLeft + width.toFloat() * previewBlockSize + previewBlockSize,
previewTop + height.toFloat() * previewBlockSize + previewBlockSize,
glowPaint
)
for (y in 0 until height) {
for (x in 0 until width) {
if (piece.isBlockAt(x, y)) {
val left = previewLeft + x.toFloat() * previewBlockSize
val top = previewTop + y.toFloat() * previewBlockSize
val right = left + previewBlockSize
val bottom = top + previewBlockSize
// Draw block with subtle glow
val rect = RectF(left + 1f, top + 1f, right - 1f, bottom - 1f)
canvas.drawRect(rect, blockPaint)
// Draw subtle border glow
val glowRect = RectF(left, top, right, bottom)
canvas.drawRect(glowRect, glowPaint)
}
}
}
}
}
}

View file

@ -1,4 +1,4 @@
package com.mintris.game
package com.pixelmintdrop.game
import android.content.Context
import android.graphics.Canvas
@ -10,9 +10,9 @@ import android.view.MotionEvent
import android.view.View
import java.util.Random
import android.util.Log
import com.mintris.model.HighScoreManager
import com.mintris.model.HighScore
import com.mintris.model.PlayerProgressionManager
import com.pixelmintdrop.model.HighScoreManager
import com.pixelmintdrop.model.HighScore
import com.pixelmintdrop.model.PlayerProgressionManager
import kotlin.math.abs
import androidx.core.graphics.withTranslation
import androidx.core.graphics.withScale
@ -33,7 +33,7 @@ class TitleScreen @JvmOverloads constructor(
private val random = Random()
private var width = 0
private var height = 0
private val tetrominosToAdd = mutableListOf<Tetromino>()
private val piecesToAdd = mutableListOf<FallingPiece>()
private val highScoreManager = HighScoreManager(context) // Pre-allocate HighScoreManager
// Touch handling variables
@ -46,11 +46,12 @@ class TitleScreen @JvmOverloads constructor(
// Callback for when the user touches the screen
var onStartGame: (() -> Unit)? = null
// Theme color
// Theme color and background color
private var themeColor = Color.WHITE
private var backgroundColor = Color.BLACK
// Define tetromino shapes (I, O, T, S, Z, J, L)
private val tetrominoShapes = arrayOf(
// Define piece shapes (I, O, T, S, Z, J, L)
private val pieceShapes = arrayOf(
// I
arrayOf(
intArrayOf(0, 0, 0, 0),
@ -95,8 +96,8 @@ class TitleScreen @JvmOverloads constructor(
)
)
// Tetromino class to represent falling pieces
private class Tetromino(
// FallingPiece class to represent falling pieces
private class FallingPiece(
var x: Float,
var y: Float,
val shape: Array<IntArray>,
@ -105,12 +106,12 @@ class TitleScreen @JvmOverloads constructor(
val rotation: Int = 0
)
private val tetrominos = mutableListOf<Tetromino>()
private val pieces = mutableListOf<FallingPiece>()
init {
// Title text settings
titlePaint.apply {
color = Color.WHITE
color = themeColor
textSize = 120f
textAlign = Paint.Align.CENTER
typeface = Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD)
@ -119,7 +120,7 @@ class TitleScreen @JvmOverloads constructor(
// "Touch to start" text settings
promptPaint.apply {
color = Color.WHITE
color = themeColor
textSize = 50f
textAlign = Paint.Align.CENTER
typeface = Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL)
@ -129,7 +130,7 @@ class TitleScreen @JvmOverloads constructor(
// High scores text settings
highScorePaint.apply {
color = Color.WHITE
color = themeColor
textSize = 70f
textAlign = Paint.Align.LEFT // Changed to LEFT alignment
typeface = Typeface.create(Typeface.MONOSPACE, Typeface.NORMAL) // Changed to monospace
@ -137,16 +138,16 @@ class TitleScreen @JvmOverloads constructor(
alpha = 200
}
// General paint settings for tetrominos (white)
// General paint settings for pieces
paint.apply {
color = Color.WHITE
color = themeColor
style = Paint.Style.FILL
isAntiAlias = true
}
// Glow paint settings for tetrominos
// Glow paint settings for pieces
glowPaint.apply {
color = Color.WHITE
color = themeColor
style = Paint.Style.FILL
isAntiAlias = true
alpha = 60
@ -158,66 +159,66 @@ class TitleScreen @JvmOverloads constructor(
width = w
height = h
// Clear existing tetrominos
tetrominos.clear()
// Clear existing pieces
pieces.clear()
// Initialize some tetrominos
// Initialize some pieces
repeat(20) {
val tetromino = createRandomTetromino()
tetrominos.add(tetromino)
val piece = createRandomPiece()
pieces.add(piece)
}
}
private fun createRandomTetromino(): Tetromino {
private fun createRandomPiece(): FallingPiece {
val x = random.nextFloat() * (width - 150) + 50 // Keep away from edges
val y = -cellSize * 4 - (random.nextFloat() * height / 2)
val shapeIndex = random.nextInt(tetrominoShapes.size)
val shape = tetrominoShapes[shapeIndex]
val shapeIndex = random.nextInt(pieceShapes.size)
val shape = pieceShapes[shapeIndex]
val speed = 1f + random.nextFloat() * 2f
val scale = 0.8f + random.nextFloat() * 0.4f
val rotation = random.nextInt(4) * 90
return Tetromino(x, y, shape, speed, scale, rotation)
return FallingPiece(x, y, shape, speed, scale, rotation)
}
override fun onDraw(canvas: Canvas) {
try {
super.onDraw(canvas)
// Draw background
canvas.drawColor(Color.BLACK)
// Draw background using the current background color
canvas.drawColor(backgroundColor)
// Add any pending tetrominos
tetrominos.addAll(tetrominosToAdd)
tetrominosToAdd.clear()
// Add any pending pieces
pieces.addAll(piecesToAdd)
piecesToAdd.clear()
// Update and draw falling tetrominos
val tetrominosToRemove = mutableListOf<Tetromino>()
// Update and draw falling pieces
val piecesToRemove = mutableListOf<FallingPiece>()
for (tetromino in tetrominos) {
tetromino.y += tetromino.speed
for (piece in pieces) {
piece.y += piece.speed
// Remove tetrominos that have fallen off the screen
if (tetromino.y > height) {
tetrominosToRemove.add(tetromino)
tetrominosToAdd.add(createRandomTetromino())
// Remove pieces that have fallen off the screen
if (piece.y > height) {
piecesToRemove.add(piece)
piecesToAdd.add(createRandomPiece())
} else {
try {
// Draw the tetromino
for (y in 0 until tetromino.shape.size) {
for (x in 0 until tetromino.shape.size) {
if (tetromino.shape[y][x] == 1) {
// Draw the piece
for (y in 0 until piece.shape.size) {
for (x in 0 until piece.shape.size) {
if (piece.shape[y][x] == 1) {
val left = x * cellSize
val top = y * cellSize
val right = left + cellSize
val bottom = top + cellSize
// Draw block with glow effect
canvas.withTranslation(tetromino.x, tetromino.y) {
withScale(tetromino.scale, tetromino.scale) {
withRotation(tetromino.rotation.toFloat(),
tetromino.shape.size * cellSize / 2,
tetromino.shape.size * cellSize / 2) {
canvas.withTranslation(piece.x, piece.y) {
withScale(piece.scale, piece.scale) {
withRotation(piece.rotation.toFloat(),
piece.shape.size * cellSize / 2,
piece.shape.size * cellSize / 2) {
// Draw glow
canvas.drawRect(left - 8f, top - 8f, right + 8f, bottom + 8f, glowPaint)
// Draw block
@ -229,21 +230,22 @@ class TitleScreen @JvmOverloads constructor(
}
}
} catch (e: Exception) {
Log.e("TitleScreen", "Error drawing tetromino", e)
Log.e("TitleScreen", "Error drawing piece", e)
}
}
}
// Remove tetrominos that fell off the screen
tetrominos.removeAll(tetrominosToRemove)
// Remove pieces that fell off the screen
pieces.removeAll(piecesToRemove)
// Draw title
val titleY = height * 0.4f
canvas.drawText("mintris", width / 2f, titleY, titlePaint)
canvas.drawText("Pixel Mint Drop", width / 2f, titleY, titlePaint)
// Draw high scores using pre-allocated manager
val highScores: List<HighScore> = highScoreManager.getHighScores()
val highScoreY = height * 0.5f
var lastHighScoreY = highScoreY
if (highScores.isNotEmpty()) {
// Calculate the starting X position to center the entire block of scores
val maxScoreWidth = highScorePaint.measureText("99. PLAYER: 999999")
@ -251,6 +253,7 @@ class TitleScreen @JvmOverloads constructor(
highScores.forEachIndexed { index: Int, score: HighScore ->
val y = highScoreY + (index * 80f)
lastHighScoreY = y // Track the last high score's Y position
// Pad the rank number to ensure alignment
val rank = (index + 1).toString().padStart(2, ' ')
// Pad the name to ensure score alignment
@ -259,8 +262,15 @@ class TitleScreen @JvmOverloads constructor(
}
}
// Draw "touch to start" prompt
canvas.drawText("touch to start", width / 2f, height * 0.7f, promptPaint)
// Draw "touch to start" prompt below the high scores
val promptY = if (resources.configuration.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE) {
// In landscape mode, position below the last high score with some padding
lastHighScoreY + 100f
} else {
// In portrait mode, use the original position
height * 0.7f
}
canvas.drawText("touch to start", width / 2f, promptY, promptPaint)
// Request another frame
invalidate()
@ -282,10 +292,10 @@ class TitleScreen @JvmOverloads constructor(
val deltaX = event.x - lastTouchX
val deltaY = event.y - lastTouchY
// Update tetromino positions
for (tetromino in tetrominos) {
tetromino.x += deltaX * 0.5f
tetromino.y += deltaY * 0.5f
// Update piece positions
for (piece in pieces) {
piece.x += deltaX * 0.5f
piece.y += deltaY * 0.5f
}
lastTouchX = event.x
@ -340,7 +350,7 @@ class TitleScreen @JvmOverloads constructor(
glowPaint.color = themeColor
// Update background color
setBackgroundColor(when (themeId) {
backgroundColor = when (themeId) {
PlayerProgressionManager.THEME_CLASSIC -> Color.BLACK
PlayerProgressionManager.THEME_NEON -> Color.parseColor("#0D0221")
PlayerProgressionManager.THEME_MONOCHROME -> Color.parseColor("#1A1A1A")
@ -348,8 +358,29 @@ class TitleScreen @JvmOverloads constructor(
PlayerProgressionManager.THEME_MINIMALIST -> Color.WHITE
PlayerProgressionManager.THEME_GALAXY -> Color.parseColor("#0B0C10")
else -> Color.BLACK
})
}
invalidate()
}
/**
* Set the theme color for the title screen
*/
fun setThemeColor(color: Int) {
themeColor = color
titlePaint.color = color
promptPaint.color = color
highScorePaint.color = color
paint.color = color
glowPaint.color = color
invalidate()
}
/**
* Set the background color for the title screen
*/
override fun setBackgroundColor(color: Int) {
backgroundColor = color
invalidate()
}
}

View file

@ -0,0 +1,199 @@
package com.pixelmintdrop.model
import android.util.Log
/**
* Represents the game board and manages piece placement and collision detection
*/
class GameBoard {
companion object {
const val WIDTH = 10
const val HEIGHT = 20
const val INITIAL_DROP_INTERVAL = 1000L // 1 second
const val MIN_DROP_INTERVAL = 100L // 0.1 seconds
}
// Board state
private val board = Array(HEIGHT) { Array(WIDTH) { false } }
private var currentPiece: GamePiece? = null
private var nextPiece: GamePiece? = null
// Game state
var dropInterval = INITIAL_DROP_INTERVAL
private set
val width: Int = WIDTH
val height: Int = HEIGHT
fun reset() {
// Clear the board
for (y in 0 until HEIGHT) {
for (x in 0 until WIDTH) {
board[y][x] = false
}
}
// Reset pieces
currentPiece = null
nextPiece = null
// Reset drop interval
dropInterval = INITIAL_DROP_INTERVAL
}
fun spawnNewPiece(): GamePiece {
// Get the next piece or create a new one if none exists
currentPiece = nextPiece ?: createRandomPiece()
nextPiece = createRandomPiece()
// Position the piece at the top center of the board
currentPiece?.let { piece ->
piece.x = (WIDTH - piece.getWidth()) / 2
piece.y = 0
}
return currentPiece!!
}
fun getNextPiece(): GamePiece? = nextPiece
fun getCurrentPiece(): GamePiece? = currentPiece
fun hasBlockAt(x: Int, y: Int): Boolean {
if (x < 0 || x >= WIDTH || y < 0 || y >= HEIGHT) return false
return board[y][x]
}
fun wouldCollide(piece: GamePiece, newX: Int, newY: Int): Boolean {
val width = piece.getWidth()
val height = piece.getHeight()
for (y in 0 until height) {
for (x in 0 until width) {
if (piece.isBlockAt(x, y)) {
val boardX = newX + x
val boardY = newY + y
// Check board boundaries
if (boardX < 0 || boardX >= WIDTH || boardY >= HEIGHT) {
return true
}
// Check collision with placed blocks
if (boardY >= 0 && board[boardY][boardX]) {
return true
}
}
}
}
return false
}
fun movePiece(piece: GamePiece, dx: Int, dy: Int): Boolean {
val newX = piece.x + dx
val newY = piece.y + dy
if (!wouldCollide(piece, newX, newY)) {
piece.x = newX
piece.y = newY
return true
}
return false
}
fun rotatePiece(piece: GamePiece): Boolean {
// Save current state
val originalRotation = piece.rotation
// Try to rotate
piece.rotate()
// Check if new position is valid
if (wouldCollide(piece, piece.x, piece.y)) {
// If not valid, try wall kicks
val kicks = arrayOf(
Pair(1, 0), // Try moving right
Pair(-1, 0), // Try moving left
Pair(0, -1), // Try moving up
Pair(2, 0), // Try moving right 2
Pair(-2, 0) // Try moving left 2
)
for ((kickX, kickY) in kicks) {
if (!wouldCollide(piece, piece.x + kickX, piece.y + kickY)) {
piece.x += kickX
piece.y += kickY
return true
}
}
// If no wall kick worked, revert rotation
piece.rotation = originalRotation
return false
}
return true
}
fun lockPiece(piece: GamePiece) {
val width = piece.getWidth()
val height = piece.getHeight()
// Place the piece on the board
for (y in 0 until height) {
for (x in 0 until width) {
if (piece.isBlockAt(x, y)) {
val boardX = piece.x + x
val boardY = piece.y + y
if (boardY >= 0 && boardY < HEIGHT && boardX >= 0 && boardX < WIDTH) {
board[boardY][boardX] = true
}
}
}
}
// Clear any completed lines
clearLines()
}
private fun clearLines() {
var linesCleared = 0
var y = HEIGHT - 1
while (y >= 0) {
if (isLineFull(y)) {
// Move all lines above down
for (moveY in y downTo 1) {
for (x in 0 until WIDTH) {
board[moveY][x] = board[moveY - 1][x]
}
}
// Clear top line
for (x in 0 until WIDTH) {
board[0][x] = false
}
linesCleared++
} else {
y--
}
}
// Increase speed based on lines cleared
if (linesCleared > 0) {
dropInterval = (dropInterval * 0.95).toLong().coerceAtLeast(MIN_DROP_INTERVAL)
}
}
private fun isLineFull(y: Int): Boolean {
for (x in 0 until WIDTH) {
if (!board[y][x]) return false
}
return true
}
private fun createRandomPiece(): GamePiece {
return GamePiece(GamePieceType.values().random())
}
}

View file

@ -0,0 +1,48 @@
package com.pixelmintdrop.model
/**
* Represents a game piece with its type, position, and rotation
*/
class GamePiece(val type: GamePieceType) {
var x: Int = 0
var y: Int = 0
var rotation: Int = 0
private set
private val blocks: Array<Array<Boolean>> = type.getBlocks()
fun getWidth(): Int = blocks[0].size
fun getHeight(): Int = blocks.size
fun isBlockAt(x: Int, y: Int): Boolean {
if (x < 0 || x >= getWidth() || y < 0 || y >= getHeight()) return false
return blocks[y][x]
}
fun rotate() {
rotation = (rotation + 1) % 4
rotateBlocks()
}
private fun rotateBlocks() {
val width = getWidth()
val height = getHeight()
val rotated = Array(width) { Array(height) { false } }
for (y in 0 until height) {
for (x in 0 until width) {
when (rotation) {
1 -> rotated[x][height - 1 - y] = blocks[y][x]
2 -> rotated[height - 1 - y][width - 1 - x] = blocks[y][x]
3 -> rotated[width - 1 - x][y] = blocks[y][x]
else -> rotated[y][x] = blocks[y][x]
}
}
}
blocks.indices.forEach { i ->
blocks[i] = rotated[i].copyOf()
}
}
}

View file

@ -0,0 +1,48 @@
package com.pixelmintdrop.model
/**
* Represents the different types of game pieces
*/
enum class GamePieceType {
I, J, L, O, S, T, Z;
fun getBlocks(): Array<Array<Boolean>> {
return when (this) {
I -> arrayOf(
arrayOf(false, false, false, false),
arrayOf(true, true, true, true),
arrayOf(false, false, false, false),
arrayOf(false, false, false, false)
)
J -> arrayOf(
arrayOf(true, false, false),
arrayOf(true, true, true),
arrayOf(false, false, false)
)
L -> arrayOf(
arrayOf(false, false, true),
arrayOf(true, true, true),
arrayOf(false, false, false)
)
O -> arrayOf(
arrayOf(true, true),
arrayOf(true, true)
)
S -> arrayOf(
arrayOf(false, true, true),
arrayOf(true, true, false),
arrayOf(false, false, false)
)
T -> arrayOf(
arrayOf(false, true, false),
arrayOf(true, true, true),
arrayOf(false, false, false)
)
Z -> arrayOf(
arrayOf(true, true, false),
arrayOf(false, true, true),
arrayOf(false, false, false)
)
}
}
}

View file

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

View file

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

View file

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

View file

@ -1,10 +1,10 @@
package com.mintris.model
package com.pixelmintdrop.model
import android.content.Context
import android.content.SharedPreferences
import com.mintris.R
import kotlin.math.pow
import kotlin.math.roundToInt
import kotlin.math.min
/**
* Manages player progression, experience points, and unlockable rewards
@ -20,7 +20,6 @@ class PlayerProgressionManager(context: Context) {
// Track unlocked rewards
private val unlockedThemes = mutableSetOf<String>()
private val unlockedBlocks = mutableSetOf<String>()
private val unlockedPowers = mutableSetOf<String>()
private val unlockedBadges = mutableSetOf<String>()
// XP gained in the current session
@ -41,18 +40,22 @@ class PlayerProgressionManager(context: Context) {
// Load unlocked rewards
val themesSet = prefs.getStringSet(KEY_UNLOCKED_THEMES, setOf()) ?: setOf()
val blocksSet = prefs.getStringSet(KEY_UNLOCKED_BLOCKS, setOf()) ?: setOf()
val powersSet = prefs.getStringSet(KEY_UNLOCKED_POWERS, setOf()) ?: setOf()
val badgesSet = prefs.getStringSet(KEY_UNLOCKED_BADGES, setOf()) ?: setOf()
unlockedThemes.addAll(themesSet)
unlockedBlocks.addAll(blocksSet)
unlockedPowers.addAll(powersSet)
unlockedBadges.addAll(badgesSet)
// Add default theme if nothing is unlocked
if (unlockedThemes.isEmpty()) {
unlockedThemes.add(THEME_CLASSIC)
}
// Always ensure default block skin is present (Level 1)
unlockedBlocks.add("block_skin_1")
// Explicitly check and save all unlocks for the current level on load
checkAllUnlocksForCurrentLevel()
}
/**
@ -65,7 +68,6 @@ class PlayerProgressionManager(context: Context) {
.putLong(KEY_TOTAL_XP_EARNED, totalXPEarned)
.putStringSet(KEY_UNLOCKED_THEMES, unlockedThemes)
.putStringSet(KEY_UNLOCKED_BLOCKS, unlockedBlocks)
.putStringSet(KEY_UNLOCKED_POWERS, unlockedPowers)
.putStringSet(KEY_UNLOCKED_BADGES, unlockedBadges)
.apply()
}
@ -91,23 +93,31 @@ class PlayerProgressionManager(context: Context) {
/**
* Calculate XP from a game session based on score, lines, level, etc.
*/
fun calculateGameXP(score: Int, lines: Int, level: Int, gameTime: Long,
tetrisCount: Int, perfectClearCount: Int): Long {
// Base XP from score with level multiplier
val scoreXP = (score * (1 + LEVEL_MULTIPLIER * level)).toLong()
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
// XP from lines cleared
val linesXP = lines * XP_PER_LINE
// Calculate XP from lines cleared
val linesXP = lines * BASE_LINES_XP * level
// XP from special moves
val tetrisBonus = tetrisCount * TETRIS_XP_BONUS
val perfectClearBonus = perfectClearCount * PERFECT_CLEAR_XP_BONUS
// Calculate quad bonus
val quadBonus = quadCount * BASE_QUAD_BONUS * level
// Time bonus (to reward longer gameplay)
val timeBonus = (gameTime / 60000) * TIME_XP_PER_MINUTE // XP per minute played
// Calculate perfect clear bonus
val perfectClearBonus = perfectClearCount * BASE_PERFECT_CLEAR_BONUS * level
// Calculate total XP
return scoreXP + linesXP + tetrisBonus + perfectClearBonus + timeBonus
// Calculate time bonus (convert ms to seconds)
val timeBonus = (timePlayedMs / 1000.0) * BASE_TIME_XP * level
// Sum all XP components
return (scoreXP + linesXP + quadBonus + perfectClearBonus + timeBonus).toLong()
}
/**
@ -179,46 +189,64 @@ class PlayerProgressionManager(context: Context) {
}
}
// Check for power unlocks
// Check for block skin unlocks (start from skin 2 at level 7)
when (level) {
8 -> {
if (unlockedPowers.add(POWER_FREEZE_TIME)) {
newRewards.add("Unlocked Freeze Time Power!")
7 -> {
if (unlockedBlocks.add("block_skin_2")) {
newRewards.add("Unlocked Neon Block Skin!")
}
}
12 -> {
if (unlockedPowers.add(POWER_BLOCK_SWAP)) {
newRewards.add("Unlocked Block Swap Power!")
14 -> {
if (unlockedBlocks.add("block_skin_3")) {
newRewards.add("Unlocked Retro Block Skin!")
}
}
18 -> {
if (unlockedPowers.add(POWER_SAFE_LANDING)) {
newRewards.add("Unlocked Safe Landing Power!")
21 -> {
if (unlockedBlocks.add("block_skin_4")) {
newRewards.add("Unlocked Minimalist Block Skin!")
}
}
30 -> {
if (unlockedPowers.add(POWER_PERFECT_CLEAR)) {
newRewards.add("Unlocked Perfect Clear Power!")
28 -> {
if (unlockedBlocks.add("block_skin_5")) {
newRewards.add("Unlocked Galaxy Block Skin!")
}
}
}
// Check for block skin unlocks
if (level % 7 == 0 && level <= 35) {
val blockSkin = "block_skin_${level / 7}"
if (unlockedBlocks.add(blockSkin)) {
newRewards.add("Unlocked New Block Skin!")
}
}
return newRewards
}
/**
* Check and unlock any rewards the player should have based on their current level
* This ensures players don't miss unlocks if they level up multiple times at once
*/
private fun checkAllUnlocksForCurrentLevel() {
// Check theme unlocks
if (playerLevel >= 5) unlockedThemes.add(THEME_NEON)
if (playerLevel >= 10) unlockedThemes.add(THEME_MONOCHROME)
if (playerLevel >= 15) unlockedThemes.add(THEME_RETRO)
if (playerLevel >= 20) unlockedThemes.add(THEME_MINIMALIST)
if (playerLevel >= 25) unlockedThemes.add(THEME_GALAXY)
// Check block skin unlocks (start from skin 2 at level 7)
// Skin 1 is default (added in loadProgress)
if (playerLevel >= 7) unlockedBlocks.add("block_skin_2")
if (playerLevel >= 14) unlockedBlocks.add("block_skin_3")
if (playerLevel >= 21) unlockedBlocks.add("block_skin_4")
if (playerLevel >= 28) unlockedBlocks.add("block_skin_5")
// Save any newly unlocked items
saveProgress()
}
/**
* Start a new progression session
*/
fun startNewSession() {
sessionXPGained = 0
// Ensure all appropriate unlocks are available
checkAllUnlocksForCurrentLevel()
}
// Getters
@ -227,7 +255,6 @@ class PlayerProgressionManager(context: Context) {
fun getXPForNextLevel(): Long = calculateXPForLevel(playerLevel)
fun getSessionXPGained(): Long = sessionXPGained
fun getUnlockedThemes(): Set<String> = unlockedThemes.toSet()
fun getUnlockedPowers(): Set<String> = unlockedPowers.toSet()
fun getUnlockedBlocks(): Set<String> = unlockedBlocks.toSet()
fun getUnlockedBadges(): Set<String> = unlockedBadges.toSet()
@ -238,13 +265,6 @@ class PlayerProgressionManager(context: Context) {
return unlockedThemes.contains(themeId)
}
/**
* Check if a specific power is unlocked
*/
fun isPowerUnlocked(powerId: String): Boolean {
return unlockedPowers.contains(powerId)
}
/**
* Award a badge to the player
*/
@ -266,37 +286,38 @@ class PlayerProgressionManager(context: Context) {
unlockedThemes.clear()
unlockedBlocks.clear()
unlockedPowers.clear()
unlockedBadges.clear()
// Add default theme
unlockedThemes.add(THEME_CLASSIC)
// Add default block skin (Level 1)
unlockedBlocks.add("block_skin_1")
saveProgress()
}
companion object {
private const val PREFS_NAME = "mintris_progression"
private const val PREFS_NAME = "pixelmintdrop_progression"
private const val KEY_PLAYER_LEVEL = "player_level"
private const val KEY_PLAYER_XP = "player_xp"
private const val KEY_TOTAL_XP_EARNED = "total_xp_earned"
private const val KEY_UNLOCKED_THEMES = "unlocked_themes"
private const val KEY_UNLOCKED_BLOCKS = "unlocked_blocks"
private const val KEY_UNLOCKED_POWERS = "unlocked_powers"
private const val KEY_UNLOCKED_BADGES = "unlocked_badges"
private const val KEY_SELECTED_THEME = "selected_theme"
private const val KEY_SELECTED_BLOCK_SKIN = "selected_block_skin"
// XP curve parameters
private const val BASE_XP = 4000.0 // Base XP for level 1 (reduced from 5000)
private const val XP_CURVE_FACTOR = 1.9 // Exponential factor for XP curve (reduced from 2.2)
// XP constants
private const val BASE_XP = 3000L
private const val XP_CURVE_FACTOR = 2.0
private const val LEVEL_MULTIPLIER = 0.03
private const val XP_PER_LINE = 40L
private const val TETRIS_XP_BONUS = 150L
private const val PERFECT_CLEAR_XP_BONUS = 300L
private const val TIME_XP_PER_MINUTE = 20L
// XP calculation constants
private const val LEVEL_MULTIPLIER = 0.15 // 15% bonus per level (increased from 10%)
private const val XP_PER_LINE = 15L // Increased from 10
private const val TETRIS_XP_BONUS = 75L // Increased from 50
private const val PERFECT_CLEAR_XP_BONUS = 250L // Increased from 200
private const val TIME_XP_PER_MINUTE = 8L // Increased from 5
// Theme IDs with required levels
// Theme constants
const val THEME_CLASSIC = "theme_classic"
const val THEME_NEON = "theme_neon"
const val THEME_MONOCHROME = "theme_monochrome"
@ -314,19 +335,11 @@ class PlayerProgressionManager(context: Context) {
THEME_GALAXY to 25
)
// Power IDs
const val POWER_FREEZE_TIME = "power_freeze_time"
const val POWER_BLOCK_SWAP = "power_block_swap"
const val POWER_SAFE_LANDING = "power_safe_landing"
const val POWER_PERFECT_CLEAR = "power_perfect_clear"
// Map of powers to required levels
val POWER_REQUIRED_LEVELS = mapOf(
POWER_FREEZE_TIME to 8,
POWER_BLOCK_SWAP to 12,
POWER_SAFE_LANDING to 18,
POWER_PERFECT_CLEAR to 30
)
private const val BASE_SCORE_XP = 0.1 // XP per score point
private const val BASE_LINES_XP = 100.0 // XP per line cleared
private const val BASE_QUAD_BONUS = 500.0 // Bonus XP for clearing 4 lines at once
private const val BASE_PERFECT_CLEAR_BONUS = 1000.0 // Bonus XP for perfect clear
private const val BASE_TIME_XP = 1.0 // XP per second played
}
/**
@ -337,9 +350,34 @@ class PlayerProgressionManager(context: Context) {
}
/**
* Get the required level for a specific power
* Set the selected block skin
*/
fun getRequiredLevelForPower(powerId: String): Int {
return POWER_REQUIRED_LEVELS[powerId] ?: 1
fun setSelectedBlockSkin(skinId: String) {
if (unlockedBlocks.contains(skinId)) {
prefs.edit().putString(KEY_SELECTED_BLOCK_SKIN, skinId).commit()
}
}
/**
* Get the selected block skin
*/
fun getSelectedBlockSkin(): String {
return prefs.getString(KEY_SELECTED_BLOCK_SKIN, "block_skin_1") ?: "block_skin_1"
}
/**
* Set the selected theme
*/
fun setSelectedTheme(themeId: String) {
if (unlockedThemes.contains(themeId)) {
prefs.edit().putString(KEY_SELECTED_THEME, themeId).apply()
}
}
/**
* Get the selected theme
*/
fun getSelectedTheme(): String {
return prefs.getString(KEY_SELECTED_THEME, THEME_CLASSIC) ?: THEME_CLASSIC
}
}

View file

@ -0,0 +1,78 @@
package com.pixelmintdrop.model
import android.content.Context
import android.content.SharedPreferences
/**
* Manages game statistics and high scores
*/
class StatsManager(context: Context) {
companion object {
private const val PREFS_NAME = "game_stats"
private const val KEY_HIGH_SCORE = "high_score"
private const val KEY_TOTAL_GAMES = "total_games"
private const val KEY_TOTAL_LINES = "total_lines"
private const val KEY_TOTAL_QUADS = "total_quads"
private const val KEY_TOTAL_PERFECT_CLEARS = "total_perfect_clears"
private const val KEY_SESSION_SCORE = "session_score"
private const val KEY_SESSION_LINES = "session_lines"
private const val KEY_SESSION_QUADS = "session_quads"
private const val KEY_SESSION_PERFECT_CLEARS = "session_perfect_clears"
}
private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
// Session stats
private var sessionScore = 0
private var sessionLines = 0
private var sessionQuads = 0
private var sessionPerfectClears = 0
fun getHighScore(): Int = prefs.getInt(KEY_HIGH_SCORE, 0)
fun getTotalGames(): Int = prefs.getInt(KEY_TOTAL_GAMES, 0)
fun getTotalLines(): Int = prefs.getInt(KEY_TOTAL_LINES, 0)
fun getTotalQuads(): Int = prefs.getInt(KEY_TOTAL_QUADS, 0)
fun getTotalPerfectClears(): Int = prefs.getInt(KEY_TOTAL_PERFECT_CLEARS, 0)
fun getSessionScore(): Int = sessionScore
fun getSessionLines(): Int = sessionLines
fun getSessionQuads(): Int = sessionQuads
fun getSessionPerfectClears(): Int = sessionPerfectClears
fun updateStats(score: Int, lines: Int, isQuad: Boolean, isPerfectClear: Boolean) {
// Update session stats
sessionScore += score
sessionLines += lines
if (isQuad) sessionQuads++
if (isPerfectClear) sessionPerfectClears++
// Update high score if needed
if (sessionScore > getHighScore()) {
prefs.edit().putInt(KEY_HIGH_SCORE, sessionScore).apply()
}
// Update total stats
prefs.edit()
.putInt(KEY_TOTAL_LINES, getTotalLines() + lines)
.putInt(KEY_TOTAL_QUADS, getTotalQuads() + if (isQuad) 1 else 0)
.putInt(KEY_TOTAL_PERFECT_CLEARS, getTotalPerfectClears() + if (isPerfectClear) 1 else 0)
.apply()
}
fun startNewGame() {
// Increment total games counter
prefs.edit().putInt(KEY_TOTAL_GAMES, getTotalGames() + 1).apply()
}
fun resetSession() {
sessionScore = 0
sessionLines = 0
sessionQuads = 0
sessionPerfectClears = 0
}
fun resetAllStats() {
prefs.edit().clear().apply()
resetSession()
}
}

View file

@ -1,7 +1,7 @@
package com.mintris.model
package com.pixelmintdrop.model
/**
* Represents a Tetris piece (Tetromino)
* Represents a game piece (Tetromino)
*/
enum class TetrominoType {
I, J, L, O, S, T, Z

View file

@ -0,0 +1,52 @@
package com.pixelmintdrop.theme
import android.graphics.Color
object ThemeManager {
// Theme colors
const val COLOR_CLASSIC_BACKGROUND = Color.BLACK
const val COLOR_CLASSIC_FOREGROUND = Color.WHITE
const val COLOR_CLASSIC_ACCENT = Color.CYAN
const val COLOR_NEON_BACKGROUND = 0xFF0D0221.toInt()
const val COLOR_NEON_FOREGROUND = 0xFFFF00FF.toInt()
const val COLOR_NEON_ACCENT = 0xFF00FFFF.toInt()
const val COLOR_MONOCHROME_BACKGROUND = 0xFF1A1A1A.toInt()
const val COLOR_MONOCHROME_FOREGROUND = Color.LTGRAY
const val COLOR_MONOCHROME_ACCENT = Color.WHITE
const val COLOR_RETRO_BACKGROUND = 0xFF3F2832.toInt()
const val COLOR_RETRO_FOREGROUND = 0xFFFF5A5F.toInt()
const val COLOR_RETRO_ACCENT = 0xFFFFB400.toInt()
const val COLOR_MINIMALIST_BACKGROUND = Color.WHITE
const val COLOR_MINIMALIST_FOREGROUND = Color.BLACK
const val COLOR_MINIMALIST_ACCENT = Color.DKGRAY
const val COLOR_GALAXY_BACKGROUND = 0xFF0B0C10.toInt()
const val COLOR_GALAXY_FOREGROUND = 0xFF66FCF1.toInt()
const val COLOR_GALAXY_ACCENT = 0xFF45A29E.toInt()
// Block colors for each piece type
const val COLOR_I_PIECE = 0xFF00F0F0.toInt()
const val COLOR_J_PIECE = 0xFF0000F0.toInt()
const val COLOR_L_PIECE = 0xFFF0A000.toInt()
const val COLOR_O_PIECE = 0xFFF0F000.toInt()
const val COLOR_S_PIECE = 0xFF00F000.toInt()
const val COLOR_T_PIECE = 0xFFA000F0.toInt()
const val COLOR_Z_PIECE = 0xFFF00000.toInt()
// Ghost piece colors
const val COLOR_GHOST_PIECE = 0x40FFFFFF
const val COLOR_GHOST_PIECE_GLOW = 0x20FFFFFF
// Grid colors
const val COLOR_GRID_LINE = 0x20FFFFFF
const val COLOR_GRID_BORDER = 0x40FFFFFF
// Effect colors
const val COLOR_LINE_CLEAR_FLASH = 0x80FFFFFF
const val COLOR_PERFECT_CLEAR_FLASH = 0xFFFFD700.toInt()
const val COLOR_COMBO_FLASH = 0x60FFFFFF
}

View file

@ -0,0 +1,431 @@
package com.pixelmintdrop.ui
import android.content.Context
import android.graphics.Color
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.FrameLayout
import android.widget.GridLayout
import android.widget.TextView
import androidx.cardview.widget.CardView
import com.pixelmintdrop.R
import android.graphics.drawable.GradientDrawable
/**
* UI component for selecting block skins
*/
class BlockSkinSelector @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
private val skinsGrid: GridLayout
private val availableSkinsLabel: TextView
// Callback when a block skin is selected
var onBlockSkinSelected: ((String) -> Unit)? = null
// Currently selected block skin (persisted)
private var selectedSkin: String = "block_skin_1"
// Block skin cards map (skinId -> CardView)
private val skinCards = mutableMapOf<String, CardView>()
// Ordered list of skin IDs for navigation
private val skinIdList = mutableListOf<String>()
// Currently focused skin ID (for gamepad navigation within the selector)
private var focusedSkinId: String? = null
// Index of the currently focused skin in skinIdList
private var focusedIndex: Int = -1
// Flag indicating if the entire selector component has focus from the main menu
private var hasComponentFocus: Boolean = false
// Player level for determining what should be unlocked
private var playerLevel: Int = 1
init {
// Inflate the layout
LayoutInflater.from(context).inflate(R.layout.block_skin_selector, this, true)
// Get references to views
skinsGrid = findViewById(R.id.skins_grid)
availableSkinsLabel = findViewById(R.id.available_skins_label)
}
/**
* Update the block skin selector with unlocked skins
*/
fun updateBlockSkins(unlockedSkins: Set<String>, currentSkin: String, playerLevel: Int = 1) {
// Store player level
this.playerLevel = playerLevel
// Clear existing skin cards and ID list
skinsGrid.removeAllViews()
skinCards.clear()
skinIdList.clear()
// Update selected skin and initial focus
selectedSkin = currentSkin
focusedSkinId = currentSkin
focusedIndex = -1 // Reset index
// Get all possible skins and their details, sorted for consistent order
val allSkins = getBlockSkins().entries.sortedWith(compareBy({ it.value.unlockLevel }, { it.value.displayName })).associate { it.key to it.value }
// Add skin cards to the grid and build ID list
allSkins.forEach { (skinId, skinInfo) ->
val isEffectivelyUnlocked = unlockedSkins.contains(skinId) || playerLevel >= skinInfo.unlockLevel
val isSelected = skinId == selectedSkin
// Only add unlocked skins to the navigable list
if (isEffectivelyUnlocked) {
skinIdList.add(skinId)
}
val skinCard = createBlockSkinCard(skinId, skinInfo, isEffectivelyUnlocked, isSelected)
skinCards[skinId] = skinCard
skinsGrid.addView(skinCard)
// Update focused index if this is the currently selected/focused skin
if (isEffectivelyUnlocked && skinId == focusedSkinId) {
focusedIndex = skinIdList.indexOf(skinId)
}
}
// Ensure focus index is valid if the previously focused skin is no longer available/unlocked
if (focusedIndex == -1 && skinIdList.isNotEmpty()) {
focusedIndex = 0
focusedSkinId = skinIdList[0]
}
// Apply initial focus highlight if the component has focus
highlightFocusedCard()
}
/**
* Create a card for a block skin
*/
private fun createBlockSkinCard(
skinId: String,
skinInfo: BlockSkinInfo,
isUnlocked: Boolean,
isSelected: Boolean
): CardView {
// Create the card
val card = CardView(context).apply {
id = View.generateViewId()
radius = 12f
cardElevation = if (isSelected) 8f else 2f
useCompatPadding = true
// Set card background color based on skin
setCardBackgroundColor(skinInfo.backgroundColor)
// Set card dimensions
val cardSize = resources.getDimensionPixelSize(R.dimen.theme_card_size)
layoutParams = GridLayout.LayoutParams().apply {
width = cardSize
height = cardSize
columnSpec = GridLayout.spec(GridLayout.UNDEFINED, 1f)
rowSpec = GridLayout.spec(GridLayout.UNDEFINED, 1f)
setMargins(8, 8, 8, 8)
}
// Apply locked/selected state visuals (only opacity here)
alpha = if (isUnlocked) 1.0f else 0.5f
}
// Create block skin content container
val container = FrameLayout(context).apply {
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
}
// Create the block skin preview
val blockSkinPreview = View(context).apply {
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
// Set the background color
setBackgroundColor(skinInfo.backgroundColor)
}
// Add a label with the skin name
val skinLabel = TextView(context).apply {
text = skinInfo.displayName
setTextColor(skinInfo.textColor)
textSize = 14f
textAlignment = View.TEXT_ALIGNMENT_CENTER
// Position at the bottom of the card
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT
).apply {
gravity = android.view.Gravity.BOTTOM or android.view.Gravity.CENTER_HORIZONTAL
setMargins(4, 4, 4, 8)
}
}
// Add level requirement for locked skins
val levelRequirement = TextView(context).apply {
text = "Level ${skinInfo.unlockLevel}"
setTextColor(Color.WHITE)
textSize = 12f
textAlignment = View.TEXT_ALIGNMENT_CENTER
visibility = if (isUnlocked) View.GONE else View.VISIBLE
// Position at the center of the card
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT
).apply {
gravity = android.view.Gravity.CENTER
}
// Make text bold and more visible for better readability
typeface = android.graphics.Typeface.DEFAULT_BOLD
setShadowLayer(3f, 1f, 1f, Color.BLACK)
}
// Add a lock icon if the skin is locked
val lockOverlay = View(context).apply {
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
// Add lock icon or visual indicator
setBackgroundResource(R.drawable.lock_overlay)
visibility = if (isUnlocked) View.GONE else View.VISIBLE
}
// Add all elements to container
container.addView(blockSkinPreview)
container.addView(skinLabel)
container.addView(lockOverlay)
container.addView(levelRequirement)
// Add container to card
card.addView(container)
// Set up click listener only for unlocked skins
if (isUnlocked) {
card.setOnClickListener {
// Clicking directly selects the skin
focusedSkinId = skinId
focusedIndex = skinIdList.indexOf(skinId)
confirmSelection() // Directly confirm click selection
}
}
return card
}
/**
* Data class for block skin information
*/
data class BlockSkinInfo(
val displayName: String,
val backgroundColor: Int,
val textColor: Int,
val unlockLevel: Int
)
/**
* Get all available block skins with their details
*/
private fun getBlockSkins(): Map<String, BlockSkinInfo> {
return mapOf(
"block_skin_1" to BlockSkinInfo(
displayName = "Classic",
backgroundColor = Color.BLACK,
textColor = Color.WHITE,
unlockLevel = 1
),
"block_skin_2" to BlockSkinInfo(
displayName = "Neon",
backgroundColor = Color.parseColor("#0D0221"),
textColor = Color.parseColor("#FF00FF"),
unlockLevel = 7
),
"block_skin_3" to BlockSkinInfo(
displayName = "Retro",
backgroundColor = Color.parseColor("#3F2832"),
textColor = Color.parseColor("#FF5A5F"),
unlockLevel = 14
),
"block_skin_4" to BlockSkinInfo(
displayName = "Minimalist",
backgroundColor = Color.WHITE,
textColor = Color.BLACK,
unlockLevel = 21
),
"block_skin_5" to BlockSkinInfo(
displayName = "Galaxy",
backgroundColor = Color.parseColor("#0B0C10"),
textColor = Color.parseColor("#66FCF1"),
unlockLevel = 28
)
)
}
/**
* Sets whether the entire component has focus (from the parent menu).
*/
fun setHasFocus(hasFocus: Boolean) {
hasComponentFocus = hasFocus
highlightFocusedCard() // Re-apply highlights
}
/**
* Moves the internal focus to the next available skin.
*/
fun focusNextItem() {
if (!hasComponentFocus || skinIdList.isEmpty()) return
focusedIndex = (focusedIndex + 1) % skinIdList.size
focusedSkinId = skinIdList[focusedIndex]
highlightFocusedCard()
}
/**
* Moves the internal focus to the previous available skin.
*/
fun focusPreviousItem() {
if (!hasComponentFocus || skinIdList.isEmpty()) return
focusedIndex = (focusedIndex - 1 + skinIdList.size) % skinIdList.size
focusedSkinId = skinIdList[focusedIndex]
highlightFocusedCard()
}
/**
* Confirms the currently focused skin as the selected skin.
* Triggers the onBlockSkinSelected callback.
*/
fun confirmSelection() {
if (focusedSkinId == null || focusedSkinId == selectedSkin) {
return // No change needed
}
// Update the selected skin
val newlySelectedSkin = focusedSkinId!!
selectedSkin = newlySelectedSkin
// Update visual states
highlightFocusedCard()
// Trigger the callback
onBlockSkinSelected?.invoke(selectedSkin)
}
/**
* Updates the visual highlight state of the skin cards based on
* selection and internal focus.
*/
private fun highlightFocusedCard() {
if (skinCards.isEmpty()) return
val focusColor = Color.YELLOW // Color for focused-but-not-selected
val selectedColor = Color.WHITE // Color for selected (might be focused or not)
skinCards.forEach { (skinId, card) ->
val skinInfo = getBlockSkins()[skinId] ?: return@forEach
// Check unlock status based on the navigable list derived from level/unlocks
val isUnlocked = skinIdList.contains(skinId)
if (!isUnlocked) {
// Keep locked skins visually distinct
card.alpha = 0.5f
card.cardElevation = 2f
card.background = null
card.setCardBackgroundColor(skinInfo.backgroundColor)
card.scaleX = 1.0f // Reset scale
card.scaleY = 1.0f
return@forEach
}
// Reset unlocked cards first
card.alpha = 1.0f
card.cardElevation = 4f
card.background = null
card.setCardBackgroundColor(skinInfo.backgroundColor)
card.scaleX = 1.0f
card.scaleY = 1.0f
val isSelected = (skinId == selectedSkin)
val isFocused = (hasComponentFocus && skinId == focusedSkinId)
var borderColor = Color.TRANSPARENT
var borderWidth = 0
var elevation = 4f
var scale = 1.0f
if (isSelected) {
borderColor = selectedColor
borderWidth = 6 // Thick border for selected
elevation = 12f // Higher elevation for selected
}
if (isFocused) {
// Focused item gets a distinct border (unless it's also selected)
if (!isSelected) {
borderColor = focusColor
borderWidth = 4 // Slightly thinner border for focused
}
elevation = 12f // Use high elevation for focus too
scale = 1.1f // Scale up the focused item
}
// Apply scale
card.scaleX = scale
card.scaleY = scale
// Apply border and elevation
if (borderWidth > 0) {
val gradientDrawable = GradientDrawable().apply {
setColor(skinInfo.backgroundColor) // Use skin's background for the fill
setStroke(borderWidth, borderColor)
cornerRadius = 12f
}
card.background = gradientDrawable
} else {
card.background = null // Ensure no border if not selected/focused
}
card.cardElevation = elevation
}
}
// Keep selectNextBlockSkin temporarily for compatibility, but it shouldn't be called by MainActivity anymore
fun selectNextBlockSkin() {
val allSkins = getBlockSkins().keys.toList()
val currentIndex = allSkins.indexOf(selectedSkin)
if (currentIndex == -1) return
var nextIndex = (currentIndex + 1) % allSkins.size
while (nextIndex != currentIndex) {
val nextSkin = allSkins[nextIndex]
val skinInfo = getBlockSkins()[nextSkin] ?: continue
val isEffectivelyUnlocked = skinCards[nextSkin]?.alpha == 1.0f // Basic check based on alpha
|| playerLevel >= skinInfo.unlockLevel
if (isEffectivelyUnlocked) {
// This method now just sets the internal focus and confirms
focusedSkinId = nextSkin
focusedIndex = skinIdList.indexOf(nextSkin) // Update index based on navigable list
if (focusedIndex == -1) { // If not found in navigable list, reset focus
focusedIndex = 0
focusedSkinId = if (skinIdList.isNotEmpty()) skinIdList[0] else null
}
confirmSelection() // Confirm the selection
return
}
nextIndex = (nextIndex + 1) % allSkins.size
}
}
}

View file

@ -0,0 +1,187 @@
package com.pixelmintdrop.ui
import android.content.Context
import android.graphics.Color
import android.view.View
import com.pixelmintdrop.databinding.ActivityMainBinding
import com.pixelmintdrop.model.PlayerProgressionManager
/**
* Handles UI updates and state management for the game interface
* to reduce the responsibility of the MainActivity.
*/
class GameUIManager(
private val context: Context,
private val binding: ActivityMainBinding
) {
// UI state
private var currentScore = 0L
private var currentLevel = 1
private var currentLines = 0
// Theme management
private var currentTheme = PlayerProgressionManager.THEME_CLASSIC
/**
* Update the game state UI elements
*/
fun updateGameStateUI(score: Long, level: Int, lines: Int) {
// Update cached values
currentScore = score
currentLevel = level
currentLines = lines
// Update UI
binding.scoreText.text = score.toString()
binding.currentLevelText.text = level.toString()
binding.linesText.text = lines.toString()
}
/**
* Show the game over UI
*/
fun showGameOver(finalScore: Long) {
// Hide game UI elements
binding.gameControlsContainer.visibility = View.GONE
binding.holdPieceView.visibility = View.GONE
binding.nextPieceView.visibility = View.GONE
binding.pauseButton.visibility = View.GONE
binding.leftControlsPanel?.visibility = View.GONE
binding.rightControlsPanel?.visibility = View.GONE
// Show game over container
binding.gameOverContainer.visibility = View.VISIBLE
binding.sessionScoreText.text = "Score: $finalScore"
}
/**
* Hide the game over UI
*/
fun hideGameOver() {
binding.gameOverContainer.visibility = View.GONE
}
/**
* Show the pause menu
*/
fun showPauseMenu() {
binding.pauseContainer.visibility = View.VISIBLE
}
/**
* Hide the pause menu
*/
fun hidePauseMenu() {
binding.pauseContainer.visibility = View.GONE
}
/**
* Update the music toggle UI based on current state
*/
fun updateMusicToggleUI(isMusicEnabled: Boolean) {
// This assumes there's a musicToggle view in the layout
// Modify as needed based on the actual UI
}
/**
* Update the level selector display
*/
fun updateLevelSelector(selectedLevel: Int) {
// Assuming pauseLevelText is part of the LevelBadge component
// This may need to be adapted based on how the level badge works
}
/**
* Show the game elements (views, controls)
*/
fun showGameElements() {
binding.gameView.visibility = View.VISIBLE
binding.gameControlsContainer.visibility = View.VISIBLE
binding.holdPieceView.visibility = View.VISIBLE
binding.nextPieceView.visibility = View.VISIBLE
binding.pauseButton.visibility = View.VISIBLE
// These might not exist in the actual layout
// binding.leftControlsPanel?.visibility = View.VISIBLE
// binding.rightControlsPanel?.visibility = View.VISIBLE
}
/**
* Hide the game elements
*/
fun hideGameElements() {
binding.gameControlsContainer.visibility = View.GONE
binding.holdPieceView.visibility = View.GONE
binding.nextPieceView.visibility = View.GONE
binding.pauseButton.visibility = View.GONE
// These might not exist in the actual layout
// binding.leftControlsPanel?.visibility = View.GONE
// binding.rightControlsPanel?.visibility = View.GONE
}
/**
* Apply a theme to the game UI
*/
fun applyTheme(themeId: String) {
currentTheme = themeId
// Set background color based on theme
val backgroundColor = getThemeBackgroundColor(themeId)
binding.root.setBackgroundColor(backgroundColor)
// Update title screen theme if it has a setTheme method
// binding.titleScreen.setTheme(themeId)
// Set text colors based on theme
val isDarkTheme = backgroundColor == Color.BLACK ||
Color.red(backgroundColor) < 50 &&
Color.green(backgroundColor) < 50 &&
Color.blue(backgroundColor) < 50
val textColor = if (isDarkTheme) Color.WHITE else Color.BLACK
updateTextColors(textColor)
}
/**
* Update all text colors in the UI
*/
private fun updateTextColors(color: Int) {
binding.scoreText.setTextColor(color)
binding.currentLevelText.setTextColor(color)
binding.linesText.setTextColor(color)
// Other text views might not exist or have different IDs
// Adapt based on the actual layout
}
/**
* Get background color based on theme ID
*/
private fun getThemeBackgroundColor(themeId: String): Int {
return when (themeId) {
PlayerProgressionManager.THEME_CLASSIC -> Color.BLACK
PlayerProgressionManager.THEME_NEON -> Color.parseColor("#0D0221")
PlayerProgressionManager.THEME_MONOCHROME -> Color.parseColor("#1A1A1A")
PlayerProgressionManager.THEME_RETRO -> Color.parseColor("#3F2832")
PlayerProgressionManager.THEME_MINIMALIST -> Color.WHITE
PlayerProgressionManager.THEME_GALAXY -> Color.parseColor("#0B0C10")
else -> Color.BLACK
}
}
/**
* Get the current score
*/
fun getCurrentScore(): Long = currentScore
/**
* Get the current level
*/
fun getCurrentLevel(): Int = currentLevel
/**
* Get the current lines cleared
*/
fun getCurrentLines(): Int = currentLines
}

View file

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

View file

@ -1,4 +1,4 @@
package com.mintris.ui
package com.pixelmintdrop.ui
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
@ -12,8 +12,8 @@ import android.view.animation.OvershootInterpolator
import android.widget.LinearLayout
import android.widget.TextView
import androidx.cardview.widget.CardView
import com.mintris.R
import com.mintris.model.PlayerProgressionManager
import com.pixelmintdrop.R
import com.pixelmintdrop.model.PlayerProgressionManager
/**
* Screen that displays player progression, XP gain, and unlocked rewards
@ -31,6 +31,9 @@ class ProgressionScreen @JvmOverloads constructor(
private val rewardsContainer: LinearLayout
private val continueButton: TextView
// Current theme
private var currentTheme: String = PlayerProgressionManager.THEME_CLASSIC
// Callback for when the player dismisses the screen
var onContinue: (() -> Unit)? = null
@ -62,6 +65,9 @@ class ProgressionScreen @JvmOverloads constructor(
newRewards: List<String>,
themeId: String = PlayerProgressionManager.THEME_CLASSIC
) {
// Update current theme
currentTheme = themeId
// Hide rewards container initially if there are no new rewards
rewardsContainer.visibility = if (newRewards.isEmpty()) View.GONE else View.INVISIBLE
@ -74,20 +80,39 @@ class ProgressionScreen @JvmOverloads constructor(
playerLevelText.text = "Player Level: $playerLevel"
xpGainText.text = "+$xpGained XP"
// Begin animation sequence
xpProgressBar.setXPValues(playerLevel, currentXP, xpForNextLevel)
// Update level up text visibility
val progressionTitle = findViewById<TextView>(R.id.progression_title)
progressionTitle.visibility = if (newRewards.any { it.contains("Level") }) View.VISIBLE else View.GONE
// Animate XP gain text entrance
// Start with initial animations
AnimatorSet().apply {
// Fade in the XP gain text
val xpTextAnimator = ObjectAnimator.ofFloat(xpGainText, "alpha", 0f, 1f).apply {
duration = 500
duration = 800
interpolator = AccelerateDecelerateInterpolator()
}
// Schedule animation for the XP bar after text appears
// Set up the XP progress bar animation sequence
val xpBarAnimator = ObjectAnimator.ofFloat(xpProgressBar, "alpha", 0f, 1f).apply {
duration = 800
interpolator = AccelerateDecelerateInterpolator()
}
// Play animations in sequence
play(xpTextAnimator)
play(xpBarAnimator).after(xpTextAnimator)
start()
}
// Set initial progress bar state
xpProgressBar.setXPValues(playerLevel, currentXP - xpGained, xpForNextLevel)
// Animate the XP gain after a short delay
postDelayed({
xpProgressBar.animateXPGain(xpGained, playerLevel, currentXP, xpForNextLevel)
}, 600)
}, 1000) // Increased delay to 1 second for better visual flow
// If there are new rewards, show them with animation
// If there are new rewards, show them with animation after XP bar animation
if (newRewards.isNotEmpty()) {
// Create reward cards
rewardsContainer.removeAllViews()
@ -113,18 +138,12 @@ class ProgressionScreen @JvmOverloads constructor(
card.animate()
.alpha(1f)
.translationY(0f)
.setDuration(400)
.setStartDelay((i * 150).toLong())
.setDuration(600) // Increased duration for smoother animation
.setStartDelay((i * 200).toLong()) // Increased delay between cards
.setInterpolator(OvershootInterpolator())
.start()
}
}, 2000) // Wait for XP bar animation to finish
}
// Start with initial animations
AnimatorSet().apply {
play(xpTextAnimator)
start()
}, 2500) // Increased delay to wait for XP bar animation to finish
}
}
@ -137,8 +156,17 @@ class ProgressionScreen @JvmOverloads constructor(
cardElevation = 4f
useCompatPadding = true
// Default background color - will be adjusted based on theme
setCardBackgroundColor(Color.BLACK)
// Set background color based on current theme
val backgroundColor = when (currentTheme) {
PlayerProgressionManager.THEME_CLASSIC -> Color.BLACK
PlayerProgressionManager.THEME_NEON -> Color.parseColor("#0D0221")
PlayerProgressionManager.THEME_MONOCHROME -> Color.parseColor("#1A1A1A")
PlayerProgressionManager.THEME_RETRO -> Color.parseColor("#3F2832")
PlayerProgressionManager.THEME_MINIMALIST -> Color.WHITE
PlayerProgressionManager.THEME_GALAXY -> Color.parseColor("#0B0C10")
else -> Color.BLACK
}
setCardBackgroundColor(backgroundColor)
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
@ -167,6 +195,8 @@ class ProgressionScreen @JvmOverloads constructor(
* Apply the current theme to the progression screen
*/
fun applyTheme(themeId: String) {
currentTheme = themeId
// Get reference to the title text
val progressionTitle = findViewById<TextView>(R.id.progression_title)
val rewardsTitle = findViewById<TextView>(R.id.rewards_title)
@ -248,10 +278,10 @@ class ProgressionScreen @JvmOverloads constructor(
}
}
// Set theme color on XP progress bar
// Update XP progress bar theme color
xpProgressBar.setThemeColor(xpThemeColor)
// Update card colors for any existing reward cards
// Update reward card colors
updateRewardCardColors(themeId)
}
@ -259,8 +289,7 @@ class ProgressionScreen @JvmOverloads constructor(
* Update colors of existing reward cards to match the theme
*/
private fun updateRewardCardColors(themeId: String) {
// Color for card backgrounds based on theme
val cardBackgroundColor = when (themeId) {
val backgroundColor = when (themeId) {
PlayerProgressionManager.THEME_CLASSIC -> Color.BLACK
PlayerProgressionManager.THEME_NEON -> Color.parseColor("#0D0221")
PlayerProgressionManager.THEME_MONOCHROME -> Color.parseColor("#1A1A1A")
@ -270,28 +299,16 @@ class ProgressionScreen @JvmOverloads constructor(
else -> Color.BLACK
}
// Text color for rewards based on theme
val rewardTextColor = when (themeId) {
PlayerProgressionManager.THEME_CLASSIC -> Color.WHITE
PlayerProgressionManager.THEME_NEON -> Color.parseColor("#FF00FF")
PlayerProgressionManager.THEME_MONOCHROME -> Color.LTGRAY
PlayerProgressionManager.THEME_RETRO -> Color.parseColor("#FF5A5F")
PlayerProgressionManager.THEME_MINIMALIST -> Color.BLACK
PlayerProgressionManager.THEME_GALAXY -> Color.parseColor("#66FCF1")
else -> Color.WHITE
}
// Update each card in the rewards container
for (i in 0 until rewardsContainer.childCount) {
val card = rewardsContainer.getChildAt(i) as? CardView
card?.let {
it.setCardBackgroundColor(cardBackgroundColor)
card?.setCardBackgroundColor(backgroundColor)
}
}
// Update text color in the card
if (it.childCount > 0 && it.getChildAt(0) is TextView) {
(it.getChildAt(0) as TextView).setTextColor(rewardTextColor)
}
}
}
/**
* Public method to handle continue action via gamepad
*/
fun performContinue() {
continueButton.performClick()
}
}

View file

@ -1,4 +1,4 @@
package com.mintris.ui
package com.pixelmintdrop.ui
import android.content.Context
import android.graphics.Color
@ -9,8 +9,10 @@ import android.widget.FrameLayout
import android.widget.GridLayout
import android.widget.TextView
import androidx.cardview.widget.CardView
import com.mintris.R
import com.mintris.model.PlayerProgressionManager
import com.pixelmintdrop.R
import com.pixelmintdrop.model.PlayerProgressionManager
import android.graphics.drawable.GradientDrawable
import android.util.Log
/**
* UI component for selecting game themes
@ -27,11 +29,19 @@ class ThemeSelector @JvmOverloads constructor(
// Callback when a theme is selected
var onThemeSelected: ((String) -> Unit)? = null
// Currently selected theme
// Currently selected theme (persisted)
private var selectedTheme: String = PlayerProgressionManager.THEME_CLASSIC
// Theme cards
// Theme cards map (themeId -> CardView)
private val themeCards = mutableMapOf<String, CardView>()
// Ordered list of theme IDs for navigation
private val themeIdList = mutableListOf<String>()
// Currently focused theme ID (for gamepad navigation within the selector)
private var focusedThemeId: String? = null
// Index of the currently focused theme in themeIdList
private var focusedIndex: Int = -1
// Flag indicating if the entire selector component has focus from the main menu
private var hasComponentFocus: Boolean = false
init {
// Inflate the layout
@ -46,27 +56,49 @@ class ThemeSelector @JvmOverloads constructor(
* Update the theme selector with unlocked themes
*/
fun updateThemes(unlockedThemes: Set<String>, currentTheme: String) {
// Clear existing theme cards
// Clear existing theme cards and ID list
themesGrid.removeAllViews()
themeCards.clear()
themeIdList.clear()
// Update selected theme
selectedTheme = currentTheme
focusedThemeId = currentTheme // Initially focus the selected theme
focusedIndex = -1 // Reset index
// Get all possible themes and their details
val allThemes = getThemes()
// Get all possible themes and their details, sorted for consistent order
val allThemes = getThemes().entries.sortedWith(compareBy({ it.value.unlockLevel }, { it.value.displayName })).associate { it.key to it.value }
// Add theme cards to the grid
// Add theme cards to the grid and build the ID list
allThemes.forEach { (themeId, themeInfo) ->
val isUnlocked = unlockedThemes.contains(themeId)
val isSelected = themeId == selectedTheme
// Only add unlocked themes to the navigable list
if (isUnlocked) {
themeIdList.add(themeId)
}
val themeCard = createThemeCard(themeId, themeInfo, isUnlocked, isSelected)
themeCards[themeId] = themeCard
themesGrid.addView(themeCard)
// Update focused index if this is the currently selected/focused theme
if (isUnlocked && themeId == focusedThemeId) {
focusedIndex = themeIdList.indexOf(themeId)
}
}
// Ensure focus index is valid if the previously focused theme is no longer available/unlocked
if (focusedIndex == -1 && themeIdList.isNotEmpty()) {
focusedIndex = 0
focusedThemeId = themeIdList[0]
}
// Apply initial focus highlight if the component has focus
highlightFocusedCard()
}
/**
* Create a card for a theme
*/
@ -86,20 +118,6 @@ class ThemeSelector @JvmOverloads constructor(
// Set card background color based on theme
setCardBackgroundColor(themeInfo.primaryColor)
// Add more noticeable visual indicator for selected theme
if (isSelected) {
setContentPadding(4, 4, 4, 4)
// Create a gradient drawable for the border
val gradientDrawable = android.graphics.drawable.GradientDrawable().apply {
setColor(themeInfo.primaryColor)
setStroke(6, Color.WHITE) // Thicker border
cornerRadius = 12f
}
background = gradientDrawable
// Add glow effect via elevation
cardElevation = 12f
}
// Set card dimensions
val cardSize = resources.getDimensionPixelSize(R.dimen.theme_card_size)
layoutParams = GridLayout.LayoutParams().apply {
@ -194,38 +212,11 @@ class ThemeSelector @JvmOverloads constructor(
// Set up click listener only for unlocked themes
if (isUnlocked) {
card.setOnClickListener {
// Only trigger callback if this isn't already the selected theme
if (themeId != selectedTheme) {
// Update previously selected card
themeCards[selectedTheme]?.let { prevCard ->
prevCard.cardElevation = 2f
// Reset any special styling
prevCard.background = null
prevCard.setCardBackgroundColor(getThemes()[selectedTheme]?.primaryColor ?: Color.BLACK)
}
// Update visual state of newly selected card
card.cardElevation = 12f
// Flash animation for selection feedback
val flashColor = Color.WHITE
val originalColor = themeInfo.primaryColor
// Create animator for flash effect
val flashAnimator = android.animation.ValueAnimator.ofArgb(flashColor, originalColor)
flashAnimator.duration = 300 // 300ms
flashAnimator.addUpdateListener { animator ->
val color = animator.animatedValue as Int
card.setCardBackgroundColor(color)
}
flashAnimator.start()
// Update selected theme
selectedTheme = themeId
// Notify listener
onThemeSelected?.invoke(themeId)
}
// Clicking directly selects the theme
Log.d("ThemeSelector", "Theme card clicked: $themeId (isUnlocked=$isUnlocked)")
focusedThemeId = themeId
focusedIndex = themeIdList.indexOf(themeId)
confirmSelection() // Directly confirm click selection
}
}
@ -292,4 +283,132 @@ class ThemeSelector @JvmOverloads constructor(
)
)
}
/**
* Sets whether the entire component has focus (from the parent menu).
* Controls the outer border visibility.
*/
fun setHasFocus(hasFocus: Boolean) {
hasComponentFocus = hasFocus
// Update visual state based on component focus
highlightFocusedCard() // Re-apply highlights
}
/**
* Moves the internal focus to the next available theme.
*/
fun focusNextItem() {
if (!hasComponentFocus || themeIdList.isEmpty()) return // Only navigate if component has focus
focusedIndex = (focusedIndex + 1) % themeIdList.size
focusedThemeId = themeIdList[focusedIndex]
highlightFocusedCard()
}
/**
* Moves the internal focus to the previous available theme.
*/
fun focusPreviousItem() {
if (!hasComponentFocus || themeIdList.isEmpty()) return // Only navigate if component has focus
focusedIndex = (focusedIndex - 1 + themeIdList.size) % themeIdList.size
focusedThemeId = themeIdList[focusedIndex]
highlightFocusedCard()
}
/**
* Confirms the currently focused theme as the selected theme.
* Triggers the onThemeSelected callback.
*/
fun confirmSelection() {
Log.d("ThemeSelector", "confirmSelection called. Focused theme: $focusedThemeId")
if (focusedThemeId == null || focusedThemeId == selectedTheme) {
// No change needed if nothing is focused,
// or the focused item is already selected
return
}
// Update the selected theme
selectedTheme = focusedThemeId!!
// Update visual states for all cards
highlightFocusedCard() // This will now mark the new theme as selected
// Trigger the callback
onThemeSelected?.invoke(selectedTheme)
}
/**
* Updates the visual highlight state of the theme cards based on
* selection and internal focus.
*/
private fun highlightFocusedCard() {
if (themeCards.isEmpty()) return
val focusColor = Color.YELLOW // Color for the focused-but-not-selected item
val selectedColor = Color.WHITE // Color for the selected item (might be focused or not)
themeCards.forEach { (themeId, card) ->
val themeInfo = getThemes()[themeId] ?: return@forEach
val isUnlocked = themeIdList.contains(themeId) // Check if it's in the navigable list
if (!isUnlocked) {
// Keep locked themes visually distinct
card.alpha = 0.5f
card.cardElevation = 2f
card.background = null // Remove any border/background
card.setCardBackgroundColor(themeInfo.primaryColor)
return@forEach // Skip further styling for locked themes
}
// Reset unlocked cards first
card.alpha = 1.0f
card.cardElevation = 4f // Default elevation for unlocked cards
card.background = null
card.setCardBackgroundColor(themeInfo.primaryColor)
card.scaleX = 1.0f
card.scaleY = 1.0f
val isSelected = (themeId == selectedTheme)
val isFocused = (hasComponentFocus && themeId == focusedThemeId)
var borderColor = Color.TRANSPARENT
var borderWidth = 0
var elevation = 4f
var scale = 1.0f
if (isSelected) {
borderColor = selectedColor
borderWidth = 6 // Thick border for selected
elevation = 12f // Higher elevation for selected
}
if (isFocused) {
// Focused item gets a distinct border (unless it's also selected)
if (!isSelected) {
borderColor = focusColor
borderWidth = 4 // Slightly thinner border for focused
}
elevation = 12f // Use high elevation for focus too
scale = 1.1f // Scale up the focused item
}
// Apply scale
card.scaleX = scale
card.scaleY = scale
// Apply border and elevation
if (borderWidth > 0) {
val gradientDrawable = GradientDrawable().apply {
setColor(themeInfo.primaryColor)
setStroke(borderWidth, borderColor)
cornerRadius = 12f // Keep consistent corner radius
}
card.background = gradientDrawable
} else {
card.background = null // Ensure no border if not selected/focused
}
card.cardElevation = elevation
}
}
}

View file

@ -1,4 +1,4 @@
package com.mintris.ui
package com.pixelmintdrop.ui
import android.animation.ValueAnimator
import android.content.Context
@ -9,8 +9,6 @@ import android.graphics.RectF
import android.util.AttributeSet
import android.view.View
import android.view.animation.AccelerateDecelerateInterpolator
import androidx.core.content.ContextCompat
import com.mintris.R
/**
* Custom progress bar for displaying player XP with animation capabilities
@ -99,6 +97,8 @@ class XPProgressBar @JvmOverloads constructor(
*/
fun setThemeColor(color: Int) {
themeColor = color
progressPaint.color = color
textPaint.color = color
levelBadgePaint.color = color
invalidate()
}

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#AA000000" />
<corners android:radius="16dp" />
<stroke android:width="2dp" android:color="#FFFFFF" />
</shape>

View file

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

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M7.5,21H2V9h5.5V21zM14.75,3h-5.5v18h5.5V3zM22,11h-5.5v10H22V11z"/>
</vector>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M8,5v14l11,-7z"/>
</vector>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="8dp" />
<stroke
android:width="1dp"
android:color="#33FFFFFF" />
<solid android:color="#1AFFFFFF" />
</shape>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<stroke
android:width="2dp"
android:color="@color/white" />
<corners android:radius="8dp" />
<solid android:color="@android:color/transparent" />
</shape>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#80000000" />
<corners android:radius="16dp" />
</shape>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<stroke
android:width="2dp"
android:color="#FFFFFF" />
<solid android:color="#00000000" />
</shape>

View file

@ -0,0 +1,750 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
android:fitsSystemWindows="true"
tools:context=".MainActivity">
<!-- Full Screen Touch Interceptor -->
<View
android:id="@+id/touchInterceptor"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true"
android:focusable="true" />
<!-- Game Container with Glow - Centered -->
<FrameLayout
android:id="@+id/gameContainer"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="1:2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.pixelmintdrop.game.GameView
android:id="@+id/gameView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!-- Glowing Border -->
<View
android:id="@+id/glowBorder"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/glow_border"
android:clickable="false"
android:focusable="false" />
</FrameLayout>
<!-- Left Side Controls Panel - Overlay -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/leftControlsPanel"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="0.25"
android:clickable="false"
android:focusable="false"
android:elevation="1dp">
<!-- Hold Piece View -->
<TextView
android:id="@+id/holdLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="HOLD"
android:textColor="@color/white"
android:textSize="16sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="24dp"/>
<com.pixelmintdrop.game.HoldPieceView
android:id="@+id/holdPieceView"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_marginTop="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/holdLabel" />
<!-- Pause Button -->
<ImageButton
android:id="@+id/pauseButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/settings"
android:padding="12dp"
android:src="@drawable/ic_pause"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginBottom="24dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- Right Side Controls Panel - Overlay -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/rightControlsPanel"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="0.25"
android:clickable="false"
android:focusable="false"
android:elevation="1dp">
<!-- Next Piece Preview -->
<TextView
android:id="@+id/nextLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="NEXT"
android:textColor="@color/white"
android:textSize="16sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="24dp"/>
<com.pixelmintdrop.game.NextPieceView
android:id="@+id/nextPieceView"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_marginTop="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/nextLabel" />
<!-- HUD Container - Score, Level, Lines -->
<LinearLayout
android:id="@+id/hudContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/nextPieceView">
<TextView
android:id="@+id/scoreText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
tools:text="score: 0" />
<TextView
android:id="@+id/currentLevelText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
tools:text="level: 1" />
<TextView
android:id="@+id/linesText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
tools:text="lines: 0" />
<TextView
android:id="@+id/comboText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
tools:text="combo: 0" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- Title Screen -->
<com.pixelmintdrop.game.TitleScreen
android:id="@+id/titleScreen"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- Progression Screen -->
<com.pixelmintdrop.ui.ProgressionScreen
android:id="@+id/progressionScreen"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- Theme Selector -->
<com.pixelmintdrop.ui.ThemeSelector
android:id="@+id/themeSelector"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- Block Skin Selector -->
<com.pixelmintdrop.ui.BlockSkinSelector
android:id="@+id/blockSkinSelector"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- Pause Container -->
<LinearLayout
android:id="@+id/pauseContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone">
<!-- Scrollable content for pause menu -->
<ScrollView
android:id="@+id/pauseMenuScrollView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="none"
android:overScrollMode="never"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:padding="16dp">
<Button
android:id="@+id/pauseStartButton"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:background="@color/transparent"
android:text="@string/start"
android:textColor="@color/white"
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
android:textAllCaps="false" />
<Button
android:id="@+id/resumeButton"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@color/transparent"
android:text="@string/resume"
android:textColor="@color/white"
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
android:textAllCaps="false" />
<Button
android:id="@+id/pauseRestartButton"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@color/transparent"
android:text="@string/restart"
android:textColor="@color/white"
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
android:textAllCaps="false" />
<Button
android:id="@+id/highScoresButton"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@color/transparent"
android:text="@string/high_scores"
android:textColor="@color/white"
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
android:textAllCaps="false" />
<Button
android:id="@+id/statsButton"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@color/transparent"
android:text="@string/stats"
android:textColor="@color/white"
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
android:textAllCaps="false" />
<!-- Level Selection -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
android:layout_marginTop="16dp">
<TextView
android:id="@+id/selectLevelText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/select_level"
android:textColor="@color/white"
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
android:textAllCaps="false" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="8dp">
<Button
android:id="@+id/pauseLevelDownButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@color/transparent"
android:text="-"
android:textColor="@color/white"
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="sans-serif" />
<TextView
android:id="@+id/pauseLevelText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
android:layout_marginHorizontal="16dp" />
<Button
android:id="@+id/pauseLevelUpButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@color/transparent"
android:text="+"
android:textColor="@color/white"
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="sans-serif" />
</LinearLayout>
</LinearLayout>
<!-- Sound Toggle -->
<Button
android:id="@+id/settingsButton"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@color/transparent"
android:text="@string/sound_on"
android:textColor="@color/white"
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
android:textAllCaps="false" />
<!-- Music Toggle -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:layout_marginTop="16dp">
<TextView
android:id="@+id/musicText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/music"
android:textColor="@color/white"
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
android:layout_marginEnd="16dp"
android:textAllCaps="false" />
<ImageButton
android:id="@+id/musicToggle"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@color/transparent"
android:src="@drawable/ic_volume_up"
android:contentDescription="@string/music" />
</LinearLayout>
<!-- Customization Button -->
<Button
android:id="@+id/customizationButton"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@color/transparent"
android:text="@string/customization"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
android:textAllCaps="false"
android:singleLine="true"
android:focusable="true"
android:focusableInTouchMode="true" />
</LinearLayout>
</ScrollView>
</LinearLayout>
<!-- Game Over overlay -->
<LinearLayout
android:id="@+id/gameOverContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone">
<TextView
android:id="@+id/gameOverText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/game_over"
android:textColor="@color/white"
android:textSize="36sp"
android:textStyle="bold"
android:fontFamily="sans-serif" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/session_stats"
android:textColor="@color/white"
android:textSize="28sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
android:layout_marginTop="24dp" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="8dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginEnd="24dp">
<TextView
android:id="@+id/sessionScoreText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"
android:fontFamily="sans-serif" />
<TextView
android:id="@+id/sessionLinesText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"
android:fontFamily="sans-serif" />
<TextView
android:id="@+id/sessionPiecesText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"
android:fontFamily="sans-serif" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/sessionTimeText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"
android:fontFamily="sans-serif" />
<TextView
android:id="@+id/sessionLevelText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"
android:fontFamily="sans-serif" />
</LinearLayout>
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/line_clears"
android:textColor="@color/white"
android:textSize="28sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
android:layout_marginTop="16dp" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginEnd="24dp">
<TextView
android:id="@+id/sessionSinglesText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"
android:fontFamily="sans-serif" />
<TextView
android:id="@+id/sessionDoublesText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"
android:fontFamily="sans-serif" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/sessionTriplesText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"
android:fontFamily="sans-serif" />
<TextView
android:id="@+id/sessionQuadsText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"
android:fontFamily="sans-serif" />
</LinearLayout>
</LinearLayout>
<Button
android:id="@+id/playAgainButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/play"
android:textColor="@color/white"
android:textSize="18sp" />
</LinearLayout>
<!-- This linear layout is hidden in landscape mode as we're using the panels instead -->
<LinearLayout
android:id="@+id/gameControlsContainer"
android:layout_width="0dp"
android:layout_height="0dp"
android:orientation="vertical"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent">
</LinearLayout>
<!-- Customization Menu overlay -->
<LinearLayout
android:id="@+id/customizationContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
android:orientation="vertical"
android:visibility="gone">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:layout_marginTop="32dp"
android:layout_marginBottom="32dp">
<TextView
android:id="@+id/customizationTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/customization"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
android:layout_marginEnd="16dp"
android:textAllCaps="false"
android:singleLine="true" />
<com.pixelmintdrop.ui.LevelBadge
android:id="@+id/customizationLevelBadge"
android:layout_width="48dp"
android:layout_height="48dp" />
</LinearLayout>
<ScrollView
android:id="@+id/customizationMenuScrollView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:scrollbars="none"
android:overScrollMode="never"
android:fillViewport="true"
android:paddingBottom="32dp"
android:focusable="true"
android:focusableInTouchMode="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
android:paddingTop="16dp"
android:paddingBottom="32dp">
<!-- Theme Selector -->
<com.pixelmintdrop.ui.ThemeSelector
android:id="@+id/customizationThemeSelector"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="true"
android:focusableInTouchMode="true" />
<!-- Block Skin Selector -->
<com.pixelmintdrop.ui.BlockSkinSelector
android:id="@+id/customizationBlockSkinSelector"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="true"
android:focusableInTouchMode="true" />
<!-- Random Mode Toggle -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:padding="16dp"
android:layout_marginBottom="16dp"
android:background="@drawable/menu_section_background"
android:focusable="true"
android:focusableInTouchMode="true">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/random_mode"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold"
android:fontFamily="sans-serif" />
<Switch
android:id="@+id/randomModeSwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:focusable="true"
android:focusableInTouchMode="true" />
</LinearLayout>
<!-- Back Button -->
<Button
android:id="@+id/customizationBackButton"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:background="@color/transparent"
android:text="@string/back"
android:textColor="@color/white"
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
android:textAllCaps="false"
android:focusable="true"
android:focusableInTouchMode="true" />
</LinearLayout>
</ScrollView>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,194 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/lifetime_stats"
android:textColor="@color/white"
android:textSize="28sp"
android:textStyle="bold"
android:layout_gravity="center"
android:layout_marginTop="16dp"/>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp">
<!-- Left Column -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:padding="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/general_stats"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"
android:layout_marginBottom="16dp"/>
<TextView
android:id="@+id/totalGamesText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="18sp"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/totalScoreText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="18sp"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/totalLinesText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="18sp"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/totalPiecesText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="18sp"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/totalTimeText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="18sp"
android:layout_marginBottom="24dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/line_clears"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"
android:layout_marginBottom="16dp"/>
<TextView
android:id="@+id/totalSinglesText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="18sp"
android:layout_marginBottom="4dp"/>
<TextView
android:id="@+id/totalDoublesText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="18sp"
android:layout_marginBottom="4dp"/>
<TextView
android:id="@+id/totalTriplesText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="18sp"
android:layout_marginBottom="4dp"/>
<TextView
android:id="@+id/totalQuadsText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="18sp"/>
</LinearLayout>
<!-- Right Column -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:padding="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/best_performance"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"
android:layout_marginBottom="16dp"/>
<TextView
android:id="@+id/maxLevelText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="18sp"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/maxScoreText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="18sp"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/maxLinesText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="18sp"/>
</LinearLayout>
</LinearLayout>
</ScrollView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:padding="8dp">
<Button
android:id="@+id/backButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/back"
android:textColor="@color/white"
android:background="@color/transparent"
android:layout_marginEnd="16dp"/>
<Button
android:id="@+id/resetStatsButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/reset_stats"
android:textColor="@color/white"
android:background="@color/transparent"/>
</LinearLayout>
</LinearLayout>

View file

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
android:gravity="center"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/new_high_score"
android:textColor="@color/white"
android:textSize="28sp"
android:textStyle="bold"
android:fontFamily="monospace"
android:layout_marginBottom="24dp"/>
<TextView
android:id="@+id/scoreText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="24sp"
android:fontFamily="monospace"
android:layout_marginBottom="24dp"/>
<EditText
android:id="@+id/nameInput"
android:layout_width="280dp"
android:layout_height="wrap_content"
android:background="@drawable/edit_text_background"
android:textColor="@color/white"
android:textSize="22sp"
android:fontFamily="monospace"
android:gravity="center"
android:inputType="text"
android:maxLength="10"
android:layout_marginBottom="24dp"/>
<Button
android:id="@+id/saveButton"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:background="@color/transparent"
android:text="@string/save"
android:textColor="@color/white"
android:textSize="20sp"
android:fontFamily="monospace"/>
</LinearLayout>

View file

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/high_scores"
android:textColor="@color/white"
android:textSize="28sp"
android:textStyle="bold"
android:fontFamily="monospace"
android:layout_gravity="center"
android:layout_marginBottom="16dp"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/highScoresList"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginHorizontal="64dp"
android:layout_marginBottom="16dp"/>
<Button
android:id="@+id/backButton"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="@color/transparent"
android:text="@string/back"
android:textColor="@color/white"
android:textSize="18sp"
android:fontFamily="monospace"/>
</LinearLayout>

View file

@ -19,7 +19,7 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.mintris.game.GameView
<com.pixelmintdrop.game.GameView
android:id="@+id/gameView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
@ -33,7 +33,7 @@
</FrameLayout>
<!-- Title Screen -->
<com.mintris.game.TitleScreen
<com.pixelmintdrop.game.TitleScreen
android:id="@+id/titleScreen"
android:layout_width="0dp"
android:layout_height="0dp"
@ -62,45 +62,65 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="24sp"
android:fontFamily="sans-serif-light"
tools:text="Score: 0" />
android:textSize="32sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
tools:text="score: 0" />
<TextView
android:id="@+id/currentLevelText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="24sp"
android:fontFamily="sans-serif-light"
tools:text="Level: 1" />
android:textSize="32sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
tools:text="level: 1" />
<TextView
android:id="@+id/linesText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="24sp"
android:fontFamily="sans-serif-light"
tools:text="Lines: 0" />
android:textSize="32sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
tools:text="lines: 0" />
<TextView
android:id="@+id/comboText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
android:textSize="24sp"
android:fontFamily="sans-serif-light"
tools:text="Combo: 0" />
android:textSize="32sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
tools:text="combo: 0" />
</LinearLayout>
<!-- Next Piece Preview -->
<com.mintris.game.NextPieceView
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:layout_gravity="end"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="NEXT"
android:textColor="@color/white"
android:textSize="16sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
android:layout_marginBottom="4dp" />
<com.pixelmintdrop.game.NextPieceView
android:id="@+id/nextPieceView"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_margin="16dp"
android:layout_gravity="end" />
android:layout_height="80dp" />
</LinearLayout>
<!-- Settings button -->
<ImageButton
@ -115,6 +135,16 @@
android:src="@drawable/ic_pause" />
</LinearLayout>
<!-- Hold Piece Preview -->
<com.pixelmintdrop.game.HoldPieceView
android:id="@+id/holdPieceView"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- Game Over overlay -->
<LinearLayout
android:id="@+id/gameOverContainer"
@ -131,16 +161,18 @@
android:layout_height="wrap_content"
android:text="@string/game_over"
android:textColor="@color/white"
android:textSize="24sp"
android:textStyle="bold" />
android:textSize="36sp"
android:textStyle="bold"
android:fontFamily="sans-serif" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/session_stats"
android:textColor="@color/white"
android:textSize="20sp"
android:textSize="28sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
android:layout_marginTop="24dp" />
<TextView
@ -149,7 +181,9 @@
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textColor="@color/white"
android:textSize="18sp" />
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="sans-serif" />
<TextView
android:id="@+id/sessionLinesText"
@ -157,7 +191,9 @@
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textColor="@color/white"
android:textSize="18sp" />
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="sans-serif" />
<TextView
android:id="@+id/sessionPiecesText"
@ -165,7 +201,9 @@
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textColor="@color/white"
android:textSize="18sp" />
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="sans-serif" />
<TextView
android:id="@+id/sessionTimeText"
@ -173,7 +211,9 @@
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textColor="@color/white"
android:textSize="18sp" />
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="sans-serif" />
<TextView
android:id="@+id/sessionLevelText"
@ -181,15 +221,18 @@
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textColor="@color/white"
android:textSize="18sp" />
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="sans-serif" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/line_clears"
android:textColor="@color/white"
android:textSize="20sp"
android:textSize="28sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
android:layout_marginTop="16dp" />
<TextView
@ -198,7 +241,9 @@
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textColor="@color/white"
android:textSize="18sp" />
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="sans-serif" />
<TextView
android:id="@+id/sessionDoublesText"
@ -206,7 +251,9 @@
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textColor="@color/white"
android:textSize="18sp" />
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="sans-serif" />
<TextView
android:id="@+id/sessionTriplesText"
@ -214,15 +261,19 @@
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textColor="@color/white"
android:textSize="18sp" />
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="sans-serif" />
<TextView
android:id="@+id/sessionTetrisesText"
android:id="@+id/sessionQuadsText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textColor="@color/white"
android:textSize="18sp" />
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="sans-serif" />
<Button
android:id="@+id/playAgainButton"
@ -231,11 +282,14 @@
android:layout_marginTop="32dp"
android:background="@color/transparent"
android:text="@string/play"
android:textColor="@color/white" />
android:textColor="@color/white"
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="sans-serif" />
</LinearLayout>
<!-- Player Progression Screen -->
<com.mintris.ui.ProgressionScreen
<com.pixelmintdrop.ui.ProgressionScreen
android:id="@+id/progressionScreen"
android:layout_width="match_parent"
android:layout_height="match_parent"
@ -251,7 +305,6 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone">
@ -260,6 +313,7 @@
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:layout_marginTop="32dp"
android:layout_marginBottom="32dp">
<TextView
@ -270,9 +324,11 @@
android:textColor="@color/white"
android:textSize="24sp"
android:textStyle="bold"
android:layout_marginEnd="16dp" />
android:fontFamily="sans-serif"
android:layout_marginEnd="16dp"
android:textAllCaps="false" />
<com.mintris.ui.LevelBadge
<com.pixelmintdrop.ui.LevelBadge
android:id="@+id/pauseLevelBadge"
android:layout_width="48dp"
android:layout_height="48dp" />
@ -280,15 +336,21 @@
</LinearLayout>
<ScrollView
android:id="@+id/pauseMenuScrollView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
android:layout_weight="1"
android:scrollbars="none"
android:overScrollMode="never"
android:fillViewport="false">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center">
android:gravity="center"
android:paddingTop="16dp"
android:paddingBottom="32dp">
<Button
android:id="@+id/pauseStartButton"
@ -297,7 +359,10 @@
android:background="@color/transparent"
android:text="@string/start"
android:textColor="@color/white"
android:textSize="18sp" />
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
android:textAllCaps="false" />
<Button
android:id="@+id/resumeButton"
@ -307,7 +372,10 @@
android:background="@color/transparent"
android:text="@string/resume"
android:textColor="@color/white"
android:textSize="18sp" />
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
android:textAllCaps="false" />
<Button
android:id="@+id/pauseRestartButton"
@ -317,7 +385,10 @@
android:background="@color/transparent"
android:text="@string/restart"
android:textColor="@color/white"
android:textSize="18sp" />
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
android:textAllCaps="false" />
<Button
android:id="@+id/highScoresButton"
@ -327,7 +398,10 @@
android:background="@color/transparent"
android:text="@string/high_scores"
android:textColor="@color/white"
android:textSize="18sp" />
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
android:textAllCaps="false" />
<Button
android:id="@+id/statsButton"
@ -337,15 +411,18 @@
android:background="@color/transparent"
android:text="@string/stats"
android:textColor="@color/white"
android:textSize="18sp" />
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
android:textAllCaps="false" />
<!-- Level Selection -->
<LinearLayout
android:id="@+id/levelSelectorContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:orientation="vertical"
android:gravity="center">
android:gravity="center"
android:layout_marginTop="16dp">
<TextView
android:id="@+id/selectLevelText"
@ -353,8 +430,10 @@
android:layout_height="wrap_content"
android:text="@string/select_level"
android:textColor="@color/white"
android:textSize="16sp"
android:textStyle="bold" />
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
android:textAllCaps="false" />
<LinearLayout
android:layout_width="wrap_content"
@ -369,7 +448,9 @@
android:background="@color/transparent"
android:text=""
android:textColor="@color/white"
android:textSize="24sp" />
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="sans-serif" />
<TextView
android:id="@+id/pauseLevelText"
@ -379,7 +460,8 @@
android:text="1"
android:textColor="@color/white"
android:textSize="24sp"
android:textStyle="bold" />
android:textStyle="bold"
android:fontFamily="sans-serif" />
<Button
android:id="@+id/pauseLevelUpButton"
@ -388,18 +470,13 @@
android:background="@color/transparent"
android:text="+"
android:textColor="@color/white"
android:textSize="24sp" />
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="sans-serif" />
</LinearLayout>
</LinearLayout>
<!-- Theme Selector -->
<com.mintris.ui.ThemeSelector
android:id="@+id/themeSelector"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginBottom="16dp" />
<!-- Sound Toggle -->
<Button
android:id="@+id/settingsButton"
android:layout_width="200dp"
@ -408,13 +485,17 @@
android:background="@color/transparent"
android:text="@string/sound_on"
android:textColor="@color/white"
android:textSize="18sp" />
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
android:textAllCaps="false" />
<!-- Music Toggle -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:gravity="center"
android:layout_marginTop="16dp">
<TextView
@ -423,18 +504,151 @@
android:layout_height="wrap_content"
android:text="@string/music"
android:textColor="@color/white"
android:textSize="18sp"
android:layout_marginEnd="16dp" />
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
android:layout_marginEnd="16dp"
android:textAllCaps="false" />
<ImageButton
android:id="@+id/musicToggle"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/toggle_music"
android:padding="12dp"
android:src="@drawable/ic_volume_up" />
android:background="@color/transparent"
android:src="@drawable/ic_volume_up"
android:contentDescription="@string/music" />
</LinearLayout>
<!-- Customization Button -->
<Button
android:id="@+id/customizationButton"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@color/transparent"
android:text="@string/customization"
android:textColor="@color/white"
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
android:textAllCaps="false"
android:singleLine="true" />
</LinearLayout>
</ScrollView>
</LinearLayout>
<!-- Customization Menu overlay -->
<LinearLayout
android:id="@+id/customizationContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
android:orientation="vertical"
android:visibility="gone">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:layout_marginTop="32dp"
android:layout_marginBottom="32dp">
<TextView
android:id="@+id/customizationTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/customization"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
android:layout_marginEnd="16dp"
android:textAllCaps="false"
android:singleLine="true" />
<com.pixelmintdrop.ui.LevelBadge
android:id="@+id/customizationLevelBadge"
android:layout_width="48dp"
android:layout_height="48dp" />
</LinearLayout>
<ScrollView
android:id="@+id/customizationMenuScrollView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:scrollbars="none"
android:overScrollMode="never"
android:fillViewport="false">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
android:paddingTop="16dp"
android:paddingBottom="32dp">
<!-- Theme Selector -->
<com.pixelmintdrop.ui.ThemeSelector
android:id="@+id/customizationThemeSelector"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp" />
<!-- Block Skin Selector -->
<com.pixelmintdrop.ui.BlockSkinSelector
android:id="@+id/customizationBlockSkinSelector"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@drawable/menu_item_background"
android:padding="16dp" />
<!-- Random Mode Toggle -->
<LinearLayout
android:id="@+id/randomModeContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@drawable/menu_item_background"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="16dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/random_mode"
android:textSize="18sp"
android:textColor="@android:color/white" />
<Switch
android:id="@+id/randomModeSwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:scaleX="1.5"
android:scaleY="1.5"
android:thumbTint="@color/switch_thumb_color"
android:trackTint="@color/switch_track_color" />
</LinearLayout>
<!-- Back Button -->
<Button
android:id="@+id/customizationBackButton"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:background="@color/transparent"
android:text="@string/back"
android:textColor="@color/white"
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
android:textAllCaps="false" />
</LinearLayout>
</ScrollView>
</LinearLayout>

View file

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

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/available_skins_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="block skins"
android:textColor="@android:color/white"
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
android:gravity="center"
android:layout_marginBottom="16dp" />
<GridLayout
android:id="@+id/skins_grid"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:columnCount="3"
android:alignmentMode="alignMargins"
android:useDefaultMargins="true" />
</LinearLayout>

View file

@ -0,0 +1,122 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fillViewport="true"
android:scrollbars="none">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp"
android:background="@drawable/dialog_background">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Gamepad Controls"
android:textSize="24sp"
android:textStyle="bold"
android:textColor="#FFFFFF"
android:layout_marginBottom="16dp"
android:gravity="center"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Movement"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="#FFFFFF"
android:layout_marginBottom="8dp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="D-Pad Left/Right or Left Stick: Move piece"
android:textSize="16sp"
android:textColor="#FFFFFF"
android:layout_marginBottom="4dp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="D-Pad Down or Left Stick Down: Soft drop"
android:textSize="16sp"
android:textColor="#FFFFFF"
android:layout_marginBottom="4dp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="D-Pad Up: Hard drop"
android:textSize="16sp"
android:textColor="#FFFFFF"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Actions"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="#FFFFFF"
android:layout_marginBottom="8dp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="A/B/X or Right Stick: Rotate piece"
android:textSize="16sp"
android:textColor="#FFFFFF"
android:layout_marginBottom="4dp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Y, L1, or R1: Hold piece"
android:textSize="16sp"
android:textColor="#FFFFFF"
android:layout_marginBottom="4dp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Start Button: Pause game"
android:textSize="16sp"
android:textColor="#FFFFFF"
android:layout_marginBottom="4dp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="B Button: Back/Cancel"
android:textSize="16sp"
android:textColor="#FFFFFF"/>
</LinearLayout>
<Button
android:id="@+id/gamepad_help_dismiss_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Got it!"
android:layout_gravity="center"
android:paddingLeft="24dp"
android:paddingRight="24dp"/>
</LinearLayout>
</ScrollView>

View file

@ -3,16 +3,16 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp"
android:background="@color/black">
android:padding="16dp">
<TextView
android:id="@+id/progression_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="LEVEL UP"
android:textSize="24sp"
android:text="level up"
android:textSize="48sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
android:textColor="@color/white"
android:gravity="center"
android:layout_marginTop="16dp"
@ -22,13 +22,15 @@
android:id="@+id/player_level_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Player Level: 1"
android:textSize="20sp"
android:text="player level: 1"
android:textSize="36sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
android:textColor="@color/white"
android:gravity="center"
android:layout_marginBottom="24dp" />
<com.mintris.ui.XPProgressBar
<com.pixelmintdrop.ui.XPProgressBar
android:id="@+id/xp_progress_bar"
android:layout_width="match_parent"
android:layout_height="50dp"
@ -38,10 +40,11 @@
android:id="@+id/xp_gain_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="+0 XP"
android:textSize="22sp"
android:textColor="#50C878"
android:text="+0 xp"
android:textSize="42sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
android:textColor="#50C878"
android:gravity="center"
android:layout_marginTop="8dp"
android:layout_marginBottom="24dp" />
@ -50,9 +53,10 @@
android:id="@+id/rewards_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="REWARDS UNLOCKED"
android:textSize="20sp"
android:text="rewards unlocked"
android:textSize="36sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
android:textColor="#FFD700"
android:gravity="center"
android:layout_marginTop="8dp"
@ -76,8 +80,10 @@
android:id="@+id/continue_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="CONTINUE"
android:textSize="18sp"
android:text="continue"
android:textSize="32sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
android:layout_gravity="center"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"

View file

@ -9,10 +9,11 @@
android:id="@+id/available_themes_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="AVAILABLE THEMES"
android:text="available themes"
android:textColor="@android:color/white"
android:textSize="18sp"
android:textSize="24sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
android:gravity="center"
android:layout_marginBottom="16dp" />

Binary file not shown.

View file

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

View file

@ -1,49 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Mintris</string>
<string name="game_over">Game Over</string>
<string name="score">Score</string>
<string name="level">Level</string>
<string name="lines">Lines</string>
<string name="next">Next</string>
<string name="play">Play Again</string>
<string name="resume">Resume</string>
<string name="pause">PAUSE</string>
<string name="settings">Settings</string>
<string name="start">Start Game</string>
<string name="restart">Restart</string>
<string name="select_level">Select Level</string>
<string name="sound_on">Sound: On</string>
<string name="sound_off">Sound: Off</string>
<string name="toggle_music">Toggle music</string>
<string name="high_scores">High Scores</string>
<string name="new_high_score">New High Score!</string>
<string name="save">Save</string>
<string name="back">Back</string>
<string name="app_name">Pixel Mint Drop</string>
<string name="game_over">game over</string>
<string name="score">score</string>
<string name="level">level</string>
<string name="lines">lines</string>
<string name="next">next</string>
<string name="play">play again</string>
<string name="resume">resume</string>
<string name="pause">pause</string>
<string name="settings">settings</string>
<string name="start">start game</string>
<string name="restart">restart</string>
<string name="select_level">select level</string>
<string name="sound_on">sound: on</string>
<string name="sound_off">sound: off</string>
<string name="toggle_music">toggle music</string>
<string name="high_scores">high scores</string>
<string name="new_high_score">new high score!</string>
<string name="save">save</string>
<string name="back">back</string>
<!-- Stats Screen -->
<string name="lifetime_stats">Lifetime Stats</string>
<string name="best_performance">Best Performance</string>
<string name="total_games">Total Games: %d</string>
<string name="total_score">Total Score: %d</string>
<string name="total_lines">Total Lines: %d</string>
<string name="total_pieces">Total Pieces: %d</string>
<string name="total_time">Total Time: %s</string>
<string name="max_level">Max Level: %d</string>
<string name="max_score">Max Score: %d</string>
<string name="max_lines">Max Lines: %d</string>
<string name="stats">Stats</string>
<string name="session_stats">Session Stats</string>
<string name="session_score">Score: %d</string>
<string name="session_lines">Lines: %d</string>
<string name="session_pieces">Pieces: %d</string>
<string name="session_time">Time: %s</string>
<string name="session_level">Level: %d</string>
<string name="line_clears">Line Clears</string>
<string name="singles">Singles: %d</string>
<string name="doubles">Doubles: %d</string>
<string name="triples">Triples: %d</string>
<string name="tetrises">Tetrises: %d</string>
<string name="reset_stats">Reset Stats</string>
<string name="music">Music</string>
<string name="lifetime_stats">lifetime stats</string>
<string name="general_stats">general stats</string>
<string name="best_performance">best performance</string>
<string name="total_games">total games: %d</string>
<string name="total_score">total score: %d</string>
<string name="total_lines">total lines: %d</string>
<string name="total_pieces">total pieces: %d</string>
<string name="total_time">total time: %s</string>
<string name="max_level">max level: %d</string>
<string name="max_score">max score: %d</string>
<string name="max_lines">max lines: %d</string>
<string name="stats">stats</string>
<string name="session_stats">session stats</string>
<string name="session_score">score: %d</string>
<string name="session_lines">lines: %d</string>
<string name="session_pieces">pieces: %d</string>
<string name="session_time">time: %s</string>
<string name="session_level">level: %d</string>
<string name="line_clears">line clears</string>
<string name="singles">singles: %d</string>
<string name="doubles">doubles: %d</string>
<string name="triples">triples: %d</string>
<string name="quads">quads: %d</string>
<string name="reset_stats">reset stats</string>
<string name="music">music</string>
<string name="customization">Customization</string>
<string name="random_mode">Random Mode</string>
<!-- Shortcuts -->
<string name="shortcut_new_game_short_label">Play</string>
<string name="shortcut_new_game_long_label">Start New Game</string>
<string name="shortcut_high_scores_short_label">Scores</string>
<string name="shortcut_high_scores_long_label">View High Scores</string>
<!-- Accessibility -->
<string name="accessibility_rotate_piece">Rotate piece</string>
<string name="accessibility_move_left">Move piece left</string>
<string name="accessibility_move_right">Move piece right</string>
<string name="accessibility_drop_piece">Drop piece</string>
<string name="accessibility_hold_piece">Hold current piece</string>
<string name="accessibility_game_board">Game board</string>
<string name="accessibility_next_piece">Next piece preview</string>
<string name="accessibility_held_piece">Held piece</string>
<string name="accessibility_pause_game">Pause game</string>
</resources>

View file

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

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<!-- Backup all shared preferences -->
<include domain="sharedpref" path="."/>
<!-- Backup specific files -->
<include domain="file" path="highscores.json"/>
<include domain="file" path="stats.json"/>
<include domain="file" path="progression.json"/>
<!-- Exclude any sensitive information if present -->
<exclude domain="sharedpref" path="sensitive_data.xml"/>
</full-backup-content>

View file

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<shortcut
android:shortcutId="new_game"
android:enabled="true"
android:icon="@drawable/ic_play"
android:shortcutShortLabel="@string/shortcut_new_game_short_label"
android:shortcutLongLabel="@string/shortcut_new_game_long_label">
<intent
android:action="android.intent.action.VIEW"
android:targetPackage="com.pixelmintdrop"
android:targetClass="com.pixelmintdrop.MainActivity">
<extra android:name="action" android:value="new_game" />
</intent>
<categories android:name="android.shortcut.conversation" />
</shortcut>
<shortcut
android:shortcutId="high_scores"
android:enabled="true"
android:icon="@drawable/ic_leaderboard"
android:shortcutShortLabel="@string/shortcut_high_scores_short_label"
android:shortcutLongLabel="@string/shortcut_high_scores_long_label">
<intent
android:action="android.intent.action.VIEW"
android:targetPackage="com.pixelmintdrop"
android:targetClass="com.pixelmintdrop.HighScoresActivity" />
<categories android:name="android.shortcut.conversation" />
</shortcut>
</shortcuts>

View file

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

12
tatus
View file

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