mirror of
https://github.com/cmclark00/mintris.git
synced 2025-05-18 01:45:21 +01:00
Compare commits
6 commits
809ae33e5e
...
1c57c438ce
Author | SHA1 | Date | |
---|---|---|---|
|
1c57c438ce | ||
|
a47d83d905 | ||
|
5861644883 | ||
|
7614cef7e5 | ||
|
9ab9b53407 | ||
|
8661fd8a80 |
9 changed files with 904 additions and 178 deletions
|
@ -2,6 +2,8 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<!-- Add permission to handle system gestures if needed on some devices -->
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
|
@ -14,7 +16,10 @@
|
|||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.Mintris.NoActionBar">
|
||||
android:theme="@style/Theme.Mintris.NoActionBar"
|
||||
android:immersive="true"
|
||||
android:resizeableActivity="false"
|
||||
android:excludeFromRecents="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
|
|
|
@ -1,40 +1,41 @@
|
|||
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 android.view.HapticFeedbackConstants
|
||||
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 com.mintris.ui.ThemeSelector
|
||||
import com.mintris.ui.BlockSkinSelector
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import android.graphics.Color
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import android.graphics.Rect
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "MainActivity"
|
||||
}
|
||||
|
||||
// UI components
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private lateinit var gameView: GameView
|
||||
|
@ -46,6 +47,8 @@ class MainActivity : AppCompatActivity() {
|
|||
private lateinit var statsManager: StatsManager
|
||||
private lateinit var progressionManager: PlayerProgressionManager
|
||||
private lateinit var progressionScreen: ProgressionScreen
|
||||
private lateinit var themeSelector: ThemeSelector
|
||||
private lateinit var blockSkinSelector: BlockSkinSelector
|
||||
|
||||
// Game state
|
||||
private var isSoundEnabled = true
|
||||
|
@ -73,6 +76,9 @@ class MainActivity : AppCompatActivity() {
|
|||
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)
|
||||
|
@ -82,11 +88,16 @@ class MainActivity : AppCompatActivity() {
|
|||
highScoreManager = HighScoreManager(this)
|
||||
statsManager = StatsManager(this)
|
||||
progressionManager = PlayerProgressionManager(this)
|
||||
themeSelector = binding.themeSelector
|
||||
blockSkinSelector = binding.blockSkinSelector
|
||||
|
||||
// Load and apply theme preference
|
||||
currentTheme = loadThemePreference()
|
||||
currentTheme = progressionManager.getSelectedTheme()
|
||||
applyTheme(currentTheme)
|
||||
|
||||
// Load and apply block skin preference
|
||||
gameView.setBlockSkin(progressionManager.getSelectedBlockSkin())
|
||||
|
||||
// Set up game view
|
||||
gameView.setGameBoard(gameBoard)
|
||||
gameView.setHaptics(gameHaptics)
|
||||
|
@ -100,8 +111,7 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
// Set up theme selector
|
||||
val themeSelector = binding.themeSelector
|
||||
themeSelector.onThemeSelected = { themeId ->
|
||||
themeSelector.onThemeSelected = { themeId: String ->
|
||||
// Apply the new theme
|
||||
applyTheme(themeId)
|
||||
|
||||
|
@ -114,6 +124,18 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
// Set up block skin selector
|
||||
blockSkinSelector.onBlockSkinSelected = { skinId: String ->
|
||||
// Apply the new block skin
|
||||
gameView.setBlockSkin(skinId)
|
||||
|
||||
// Save the selection
|
||||
progressionManager.setSelectedBlockSkin(skinId)
|
||||
|
||||
// Provide haptic feedback
|
||||
gameHaptics.vibrateForPieceLock()
|
||||
}
|
||||
|
||||
// Set up title screen
|
||||
titleScreen.onStartGame = {
|
||||
titleScreen.visibility = View.GONE
|
||||
|
@ -160,18 +182,18 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
gameView.onLineClear = { lineCount ->
|
||||
android.util.Log.d("MainActivity", "Received line clear callback: $lineCount lines")
|
||||
Log.d(TAG, "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")
|
||||
Log.d(TAG, "Sound is enabled, triggering haptic feedback")
|
||||
try {
|
||||
gameHaptics.vibrateForLineClear(lineCount)
|
||||
android.util.Log.d("MainActivity", "Haptic feedback triggered successfully")
|
||||
Log.d(TAG, "Haptic feedback triggered successfully")
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("MainActivity", "Error triggering haptic feedback", e)
|
||||
Log.e(TAG, "Error triggering haptic feedback", e)
|
||||
}
|
||||
} else {
|
||||
android.util.Log.d("MainActivity", "Sound is disabled, skipping haptic feedback")
|
||||
Log.d(TAG, "Sound is disabled, skipping haptic feedback")
|
||||
}
|
||||
// Record line clear in stats
|
||||
statsManager.recordLineClear(lineCount)
|
||||
|
@ -424,6 +446,13 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
// Update theme selector
|
||||
updateThemeSelector()
|
||||
|
||||
// Update block skin selector
|
||||
blockSkinSelector.updateBlockSkins(
|
||||
progressionManager.getUnlockedBlocks(),
|
||||
gameView.getCurrentBlockSkin(),
|
||||
progressionManager.getPlayerLevel()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -562,7 +591,7 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
// Save the selected theme
|
||||
currentTheme = themeId
|
||||
saveThemePreference(themeId)
|
||||
progressionManager.setSelectedTheme(themeId)
|
||||
|
||||
// Apply theme to title screen if it's visible
|
||||
if (titleScreen.visibility == View.VISIBLE) {
|
||||
|
@ -616,22 +645,6 @@ class MainActivity : AppCompatActivity() {
|
|||
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
|
||||
*/
|
||||
|
@ -646,4 +659,83 @@ class MainActivity : AppCompatActivity() {
|
|||
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)
|
||||
}
|
||||
}
|
|
@ -7,15 +7,66 @@ 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) {
|
||||
private val vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "GameHaptics"
|
||||
}
|
||||
|
||||
// Vibrator service
|
||||
private val vibrator: Vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
val vibratorManager = context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
|
||||
vibratorManager.defaultVibrator
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||
}
|
||||
|
||||
// Vibrate for line clear (more intense for more lines)
|
||||
fun vibrateForLineClear(lineCount: Int) {
|
||||
Log.d(TAG, "Attempting to vibrate for $lineCount lines")
|
||||
|
||||
// Only proceed if the device has a vibrator and it's available
|
||||
if (!vibrator.hasVibrator()) return
|
||||
|
||||
// Scale duration and amplitude based on line count
|
||||
// More lines = longer and stronger vibration
|
||||
val duration = when(lineCount) {
|
||||
1 -> 50L // Single line: short vibration
|
||||
2 -> 80L // Double line: slightly longer
|
||||
3 -> 120L // Triple line: even longer
|
||||
4 -> 200L // Tetris: longest vibration
|
||||
else -> 50L
|
||||
}
|
||||
|
||||
val amplitude = when(lineCount) {
|
||||
1 -> 80 // Single line: mild vibration (80/255)
|
||||
2 -> 120 // Double line: medium vibration (120/255)
|
||||
3 -> 180 // Triple line: strong vibration (180/255)
|
||||
4 -> 255 // Tetris: maximum vibration (255/255)
|
||||
else -> 80
|
||||
}
|
||||
|
||||
Log.d(TAG, "Vibration parameters - Duration: ${duration}ms, Amplitude: $amplitude")
|
||||
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
vibrator.vibrate(VibrationEffect.createOneShot(duration, amplitude))
|
||||
Log.d(TAG, "Vibration triggered successfully")
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
vibrator.vibrate(duration)
|
||||
Log.w(TAG, "Device does not support vibration effects (Android < 8.0)")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error triggering vibration", e)
|
||||
}
|
||||
}
|
||||
|
||||
fun performHapticFeedback(view: View, feedbackType: Int) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
|
@ -26,40 +77,6 @@ class GameHaptics(private val context: Context) {
|
|||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
|
|
@ -12,12 +12,12 @@ import android.os.Build
|
|||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
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 android.view.Display
|
||||
import com.mintris.model.GameBoard
|
||||
import com.mintris.model.Tetromino
|
||||
import com.mintris.model.TetrominoType
|
||||
|
@ -33,13 +33,18 @@ class GameView @JvmOverloads constructor(
|
|||
defStyleAttr: Int = 0
|
||||
) : View(context, attrs, defStyleAttr) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "GameView"
|
||||
}
|
||||
|
||||
// Game board model
|
||||
private var gameBoard = GameBoard()
|
||||
private var gameHaptics: GameHaptics? = null
|
||||
|
||||
// Game state
|
||||
private var isRunning = false
|
||||
private var isPaused = false
|
||||
var isPaused = false // Changed from private to public to allow access from MainActivity
|
||||
private var score = 0
|
||||
|
||||
// Callbacks
|
||||
var onNextPieceChanged: (() -> Unit)? = null
|
||||
|
@ -128,15 +133,25 @@ class GameView @JvmOverloads constructor(
|
|||
private var lastTapTime = 0L
|
||||
private var lastRotationTime = 0L
|
||||
private var lastMoveTime = 0L
|
||||
private var minSwipeVelocity = 800 // Minimum velocity for swipe to be considered a hard drop
|
||||
private var lastHardDropTime = 0L // Track when the last hard drop occurred
|
||||
private val hardDropCooldown = 250L // Reduced from 500ms to 250ms
|
||||
private var touchFreezeUntil = 0L // Time until which touch events should be ignored
|
||||
private val pieceLockFreezeTime = 300L // Time to freeze touch events after piece locks
|
||||
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 = 1.5f // Threshold for direction lock 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
|
||||
|
||||
// Block skin
|
||||
private var currentBlockSkin: String = "block_skin_1"
|
||||
private val blockSkinPaints = mutableMapOf<String, Paint>()
|
||||
|
||||
private enum class Direction {
|
||||
HORIZONTAL, VERTICAL
|
||||
}
|
||||
|
@ -160,19 +175,24 @@ class GameView @JvmOverloads constructor(
|
|||
|
||||
// Connect our callbacks to the GameBoard
|
||||
gameBoard.onPieceMove = { onPieceMove?.invoke() }
|
||||
gameBoard.onPieceLock = { onPieceLock?.invoke() }
|
||||
gameBoard.onPieceLock = {
|
||||
// Freeze touch events for a brief period after a piece locks
|
||||
touchFreezeUntil = System.currentTimeMillis() + pieceLockFreezeTime
|
||||
Log.d(TAG, "Piece locked - freezing touch events until ${touchFreezeUntil}")
|
||||
onPieceLock?.invoke()
|
||||
}
|
||||
gameBoard.onLineClear = { lineCount, clearedLines ->
|
||||
android.util.Log.d("GameView", "Received line clear from GameBoard: $lineCount lines")
|
||||
Log.d(TAG, "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")
|
||||
Log.d(TAG, "Found ${linesToPulse.size} lines to pulse")
|
||||
startPulseAnimation(lineCount)
|
||||
android.util.Log.d("GameView", "Forwarded line clear callback")
|
||||
Log.d(TAG, "Forwarded line clear callback")
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("GameView", "Error forwarding line clear callback", e)
|
||||
Log.e(TAG, "Error forwarding line clear callback", e)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -181,7 +201,11 @@ class GameView @JvmOverloads constructor(
|
|||
|
||||
// Set better frame rate using modern APIs
|
||||
val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
|
||||
val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY)
|
||||
val display = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
displayManager.getDisplay(Display.DEFAULT_DISPLAY)
|
||||
} else {
|
||||
displayManager.displays.firstOrNull()
|
||||
}
|
||||
display?.let { disp ->
|
||||
val refreshRate = disp.refreshRate
|
||||
// Set game loop interval based on refresh rate, but don't go faster than the base interval
|
||||
|
@ -195,8 +219,64 @@ class GameView @JvmOverloads constructor(
|
|||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
setSystemGestureExclusionRects(listOf(Rect(0, 0, width, height)))
|
||||
}
|
||||
|
||||
// Initialize block skin paints
|
||||
initializeBlockSkinPaints()
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize paints for different block skins
|
||||
*/
|
||||
private fun initializeBlockSkinPaints() {
|
||||
// Classic skin
|
||||
blockSkinPaints["block_skin_1"] = Paint().apply {
|
||||
color = Color.WHITE
|
||||
isAntiAlias = true
|
||||
}
|
||||
|
||||
// Neon skin
|
||||
blockSkinPaints["block_skin_2"] = Paint().apply {
|
||||
color = Color.parseColor("#FF00FF")
|
||||
isAntiAlias = true
|
||||
maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.OUTER)
|
||||
}
|
||||
|
||||
// Retro skin
|
||||
blockSkinPaints["block_skin_3"] = Paint().apply {
|
||||
color = Color.parseColor("#FF5A5F")
|
||||
isAntiAlias = true
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = 2f
|
||||
}
|
||||
|
||||
// Minimalist skin
|
||||
blockSkinPaints["block_skin_4"] = Paint().apply {
|
||||
color = Color.BLACK
|
||||
isAntiAlias = true
|
||||
style = Paint.Style.FILL
|
||||
}
|
||||
|
||||
// Galaxy skin
|
||||
blockSkinPaints["block_skin_5"] = Paint().apply {
|
||||
color = Color.parseColor("#66FCF1")
|
||||
isAntiAlias = true
|
||||
maskFilter = BlurMaskFilter(12f, BlurMaskFilter.Blur.OUTER)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current block skin
|
||||
*/
|
||||
fun setBlockSkin(skinId: String) {
|
||||
currentBlockSkin = skinId
|
||||
invalidate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current block skin
|
||||
*/
|
||||
fun getCurrentBlockSkin(): String = currentBlockSkin
|
||||
|
||||
/**
|
||||
* Start the game
|
||||
*/
|
||||
|
@ -240,8 +320,8 @@ class GameView @JvmOverloads constructor(
|
|||
return
|
||||
}
|
||||
|
||||
// Move the current tetromino down automatically
|
||||
gameBoard.moveDown()
|
||||
// Update the game state
|
||||
gameBoard.update()
|
||||
|
||||
// Update UI with current game state
|
||||
onGameStateChanged?.invoke(gameBoard.score, gameBoard.level, gameBoard.lines)
|
||||
|
@ -286,7 +366,7 @@ class GameView @JvmOverloads constructor(
|
|||
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")
|
||||
Log.d(TAG, "Board dimensions: width=$width, height=$height, blockSize=$blockSize, boardLeft=$boardLeft, boardTop=$boardTop, totalHeight=$totalHeight")
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
|
@ -508,20 +588,68 @@ class GameView @JvmOverloads constructor(
|
|||
// 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)
|
||||
// Get the current block skin paint
|
||||
val paint = blockSkinPaints[currentBlockSkin] ?: blockSkinPaints["block_skin_1"]!!
|
||||
|
||||
// Draw block
|
||||
blockPaint.apply {
|
||||
color = if (isGhost) Color.argb(30, 255, 255, 255) else Color.WHITE
|
||||
alpha = if (isGhost) 30 else 255
|
||||
// Create a clone of the paint to avoid modifying the original
|
||||
val blockPaint = Paint(paint)
|
||||
|
||||
// Special handling for neon skin
|
||||
if (currentBlockSkin == "block_skin_2") {
|
||||
// Stronger outer glow for neon skin
|
||||
blockGlowPaint.color = if (isGhost) Color.argb(30, 255, 0, 255) else Color.parseColor("#FF00FF")
|
||||
blockGlowPaint.maskFilter = BlurMaskFilter(16f, BlurMaskFilter.Blur.OUTER)
|
||||
canvas.drawRect(left - 4f, top - 4f, right + 4f, bottom + 4f, blockGlowPaint)
|
||||
|
||||
// For neon, use semi-translucent fill with strong glowing edges
|
||||
blockPaint.style = Paint.Style.FILL_AND_STROKE
|
||||
blockPaint.strokeWidth = 2f
|
||||
blockPaint.maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.NORMAL)
|
||||
|
||||
if (isGhost) {
|
||||
blockPaint.color = Color.argb(30, 255, 0, 255)
|
||||
blockPaint.alpha = 30
|
||||
} else {
|
||||
blockPaint.color = Color.parseColor("#66004D") // Darker magenta fill
|
||||
blockPaint.alpha = 170 // More opaque to be more visible
|
||||
}
|
||||
|
||||
// Draw block with neon effect
|
||||
canvas.drawRect(left, top, right, bottom, blockPaint)
|
||||
|
||||
// Draw a brighter border for better visibility
|
||||
val borderPaint = Paint().apply {
|
||||
color = Color.parseColor("#FF00FF")
|
||||
style = Paint.Style.STROKE
|
||||
strokeWidth = 3f
|
||||
alpha = 255
|
||||
isAntiAlias = true
|
||||
maskFilter = BlurMaskFilter(6f, BlurMaskFilter.Blur.NORMAL)
|
||||
}
|
||||
canvas.drawRect(left, top, right, bottom, borderPaint)
|
||||
|
||||
// Inner glow for neon blocks - brighter than before
|
||||
glowPaint.color = if (isGhost) Color.argb(10, 255, 0, 255) else Color.parseColor("#FF00FF")
|
||||
glowPaint.alpha = if (isGhost) 10 else 100 // More visible inner glow
|
||||
glowPaint.style = Paint.Style.STROKE
|
||||
glowPaint.strokeWidth = 2f
|
||||
glowPaint.maskFilter = BlurMaskFilter(4f, BlurMaskFilter.Blur.NORMAL)
|
||||
canvas.drawRect(left + 4f, top + 4f, right - 4f, bottom - 4f, glowPaint)
|
||||
} else {
|
||||
// Standard rendering for other skins
|
||||
// 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 with current skin
|
||||
blockPaint.color = if (isGhost) Color.argb(30, 255, 255, 255) else blockPaint.color
|
||||
blockPaint.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)
|
||||
}
|
||||
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) {
|
||||
|
@ -578,6 +706,13 @@ class GameView @JvmOverloads constructor(
|
|||
return true
|
||||
}
|
||||
|
||||
// Ignore touch events during the freeze period after a piece locks
|
||||
val currentTime = System.currentTimeMillis()
|
||||
if (currentTime < touchFreezeUntil) {
|
||||
Log.d(TAG, "Ignoring touch event - freeze active for ${touchFreezeUntil - currentTime}ms more")
|
||||
return true
|
||||
}
|
||||
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
// Record start of touch
|
||||
|
@ -612,12 +747,14 @@ class GameView @JvmOverloads constructor(
|
|||
|
||||
// Check if movement exceeds threshold
|
||||
if (absDeltaX > blockSize * minMovementThreshold || absDeltaY > blockSize * minMovementThreshold) {
|
||||
// Determine dominant direction
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -640,7 +777,7 @@ class GameView @JvmOverloads constructor(
|
|||
}
|
||||
Direction.VERTICAL -> {
|
||||
if (deltaY > blockSize * minMovementThreshold) {
|
||||
gameBoard.moveDown()
|
||||
gameBoard.softDrop()
|
||||
lastTouchY = event.y
|
||||
if (currentTime - lastMoveTime >= moveCooldown) {
|
||||
gameHaptics?.vibrateForPieceMove()
|
||||
|
@ -660,17 +797,34 @@ class GameView @JvmOverloads constructor(
|
|||
val moveTime = System.currentTimeMillis() - lastTapTime
|
||||
val deltaY = event.y - startY
|
||||
val deltaX = event.x - startX
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
// If the movement was fast and downward, treat as hard drop
|
||||
if (moveTime > 0 && deltaY > blockSize * 0.5f && (deltaY / moveTime) * 1000 > minSwipeVelocity) {
|
||||
gameBoard.hardDrop()
|
||||
invalidate()
|
||||
// Check if this might have been a hard drop gesture
|
||||
val isVerticalSwipe = moveTime > 0 &&
|
||||
deltaY > blockSize * minHardDropDistance &&
|
||||
(deltaY / moveTime) * 1000 > minSwipeVelocity &&
|
||||
abs(deltaX) < abs(deltaY) * 0.3f
|
||||
|
||||
// Check cooldown separately for better logging
|
||||
val isCooldownActive = currentTime - lastHardDropTime <= hardDropCooldown
|
||||
|
||||
if (isVerticalSwipe) {
|
||||
if (isCooldownActive) {
|
||||
// Log when we're blocking a hard drop due to cooldown
|
||||
Log.d("GameView", "Hard drop blocked by cooldown - time since last: ${currentTime - lastHardDropTime}ms, cooldown: ${hardDropCooldown}ms")
|
||||
} else {
|
||||
// Process the hard drop
|
||||
Log.d("GameView", "Hard drop detected - deltaY: $deltaY, velocity: ${(deltaY / moveTime) * 1000}, ratio: ${abs(deltaX) / abs(deltaY)}")
|
||||
gameBoard.hardDrop()
|
||||
lastHardDropTime = currentTime // Update the last hard drop time
|
||||
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) {
|
||||
Log.d("GameView", "Rotation detected")
|
||||
gameBoard.rotate()
|
||||
lastRotationTime = currentTime
|
||||
invalidate()
|
||||
|
@ -730,17 +884,17 @@ class GameView @JvmOverloads constructor(
|
|||
gameBoard.onPieceMove = { onPieceMove?.invoke() }
|
||||
gameBoard.onPieceLock = { onPieceLock?.invoke() }
|
||||
gameBoard.onLineClear = { lineCount, clearedLines ->
|
||||
android.util.Log.d("GameView", "Received line clear from GameBoard: $lineCount lines")
|
||||
Log.d(TAG, "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")
|
||||
Log.d(TAG, "Found ${linesToPulse.size} lines to pulse")
|
||||
startPulseAnimation(lineCount)
|
||||
android.util.Log.d("GameView", "Forwarded line clear callback")
|
||||
Log.d(TAG, "Forwarded line clear callback")
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("GameView", "Error forwarding line clear callback", e)
|
||||
Log.e(TAG, "Error forwarding line clear callback", e)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -776,7 +930,7 @@ class GameView @JvmOverloads constructor(
|
|||
* Start the pulse animation for line clear
|
||||
*/
|
||||
private fun startPulseAnimation(lineCount: Int) {
|
||||
android.util.Log.d("GameView", "Starting pulse animation for $lineCount lines")
|
||||
Log.d(TAG, "Starting pulse animation for $lineCount lines")
|
||||
|
||||
// Cancel any existing animation
|
||||
pulseAnimator?.cancel()
|
||||
|
@ -795,7 +949,7 @@ class GameView @JvmOverloads constructor(
|
|||
pulseAlpha = animation.animatedValue as Float
|
||||
isPulsing = true
|
||||
invalidate()
|
||||
android.util.Log.d("GameView", "Pulse animation update: alpha = $pulseAlpha")
|
||||
Log.d(TAG, "Pulse animation update: alpha = $pulseAlpha")
|
||||
}
|
||||
addListener(object : android.animation.AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: android.animation.Animator) {
|
||||
|
@ -803,7 +957,7 @@ class GameView @JvmOverloads constructor(
|
|||
pulseAlpha = 0f
|
||||
linesToPulse.clear()
|
||||
invalidate()
|
||||
android.util.Log.d("GameView", "Pulse animation ended")
|
||||
Log.d(TAG, "Pulse animation ended")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package com.mintris.model
|
||||
|
||||
import kotlin.random.Random
|
||||
import android.util.Log
|
||||
|
||||
/**
|
||||
* Represents the game board (grid) and manages game state
|
||||
|
@ -9,6 +9,10 @@ class GameBoard(
|
|||
val width: Int = 10,
|
||||
val height: Int = 20
|
||||
) {
|
||||
companion object {
|
||||
private const val TAG = "GameBoard"
|
||||
}
|
||||
|
||||
// Board grid to track locked pieces
|
||||
// True = occupied, False = empty
|
||||
private val grid = Array(height) { BooleanArray(width) { false } }
|
||||
|
@ -34,6 +38,7 @@ class GameBoard(
|
|||
var isGameOver = false
|
||||
var isHardDropInProgress = false // Make public
|
||||
var isPieceLocking = false // Make public
|
||||
private var isPlayerSoftDrop = false // Track if the drop is player-initiated
|
||||
|
||||
// Scoring state
|
||||
private var combo = 0
|
||||
|
@ -55,6 +60,13 @@ class GameBoard(
|
|||
var onNextPieceChanged: (() -> Unit)? = null
|
||||
var onLineClear: ((Int, List<Int>) -> Unit)? = null
|
||||
|
||||
// Store the last cleared lines
|
||||
private val lastClearedLines = mutableListOf<Int>()
|
||||
|
||||
// Add spawn protection variables
|
||||
private var pieceSpawnTime = 0L
|
||||
private val spawnGracePeriod = 250L // Changed from 150ms to 250ms
|
||||
|
||||
init {
|
||||
spawnNextPiece()
|
||||
spawnPiece()
|
||||
|
@ -66,7 +78,7 @@ class GameBoard(
|
|||
private fun spawnNextPiece() {
|
||||
// If bag is empty, refill it with all piece types
|
||||
if (bag.isEmpty()) {
|
||||
bag.addAll(TetrominoType.values())
|
||||
bag.addAll(TetrominoType.entries.toTypedArray())
|
||||
bag.shuffle()
|
||||
}
|
||||
|
||||
|
@ -114,6 +126,8 @@ class GameBoard(
|
|||
* Spawns the current tetromino at the top of the board
|
||||
*/
|
||||
fun spawnPiece() {
|
||||
Log.d(TAG, "spawnPiece() started - current states: isHardDropInProgress=$isHardDropInProgress, isPieceLocking=$isPieceLocking")
|
||||
|
||||
currentPiece = nextPiece
|
||||
spawnNextPiece()
|
||||
|
||||
|
@ -122,9 +136,15 @@ class GameBoard(
|
|||
x = (width - getWidth()) / 2
|
||||
y = 0
|
||||
|
||||
Log.d(TAG, "spawnPiece() - new piece spawned at position (${x},${y}), type=${type}")
|
||||
|
||||
// Set the spawn time for the grace period
|
||||
pieceSpawnTime = System.currentTimeMillis()
|
||||
|
||||
// Check if the piece can be placed (Game Over condition)
|
||||
if (!canMove(0, 0)) {
|
||||
isGameOver = true
|
||||
Log.d(TAG, "spawnPiece() - Game Over condition detected")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -158,29 +178,73 @@ class GameBoard(
|
|||
|
||||
return if (canMove(0, 1)) {
|
||||
currentPiece?.y = currentPiece?.y?.plus(1) ?: 0
|
||||
// Only add soft drop points if it's a player-initiated drop
|
||||
if (isPlayerSoftDrop) {
|
||||
score += 1
|
||||
}
|
||||
onPieceMove?.invoke()
|
||||
true
|
||||
} else {
|
||||
// Check if we're within the spawn grace period
|
||||
val currentTime = System.currentTimeMillis()
|
||||
if (currentTime - pieceSpawnTime < spawnGracePeriod) {
|
||||
Log.d(TAG, "moveDown() - not locking piece due to spawn grace period (${currentTime - pieceSpawnTime}ms < ${spawnGracePeriod}ms)")
|
||||
return false
|
||||
}
|
||||
|
||||
lockPiece()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Player-initiated soft drop
|
||||
*/
|
||||
fun softDrop() {
|
||||
isPlayerSoftDrop = true
|
||||
moveDown()
|
||||
isPlayerSoftDrop = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Hard drop the current piece
|
||||
*/
|
||||
fun hardDrop() {
|
||||
if (isHardDropInProgress || isPieceLocking) return // Prevent multiple hard drops
|
||||
if (isHardDropInProgress || isPieceLocking) {
|
||||
Log.d(TAG, "hardDrop() called but blocked: isHardDropInProgress=$isHardDropInProgress, isPieceLocking=$isPieceLocking")
|
||||
return // Prevent multiple hard drops
|
||||
}
|
||||
|
||||
// Check if we're within the spawn grace period
|
||||
val currentTime = System.currentTimeMillis()
|
||||
if (currentTime - pieceSpawnTime < spawnGracePeriod) {
|
||||
Log.d(TAG, "hardDrop() - blocked due to spawn grace period (${currentTime - pieceSpawnTime}ms < ${spawnGracePeriod}ms)")
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(TAG, "hardDrop() started - setting isHardDropInProgress=true")
|
||||
isHardDropInProgress = true
|
||||
val piece = currentPiece ?: return
|
||||
|
||||
// Count how many cells the piece will drop
|
||||
var dropDistance = 0
|
||||
while (canMove(0, dropDistance + 1)) {
|
||||
dropDistance++
|
||||
}
|
||||
|
||||
Log.d(TAG, "hardDrop() - piece will drop $dropDistance cells, position before: (${piece.x},${piece.y})")
|
||||
|
||||
// Move piece down until it can't move anymore
|
||||
while (canMove(0, 1)) {
|
||||
piece.y++
|
||||
onPieceMove?.invoke()
|
||||
}
|
||||
|
||||
Log.d(TAG, "hardDrop() - piece final position: (${piece.x},${piece.y})")
|
||||
|
||||
// Add hard drop points (2 points per cell)
|
||||
score += dropDistance * 2
|
||||
|
||||
// Lock the piece immediately
|
||||
lockPiece()
|
||||
}
|
||||
|
@ -268,7 +332,12 @@ class GameBoard(
|
|||
* Lock the current piece in place
|
||||
*/
|
||||
private fun lockPiece() {
|
||||
if (isPieceLocking) return // Prevent recursive locking
|
||||
if (isPieceLocking) {
|
||||
Log.d(TAG, "lockPiece() called but blocked: isPieceLocking=$isPieceLocking")
|
||||
return // Prevent recursive locking
|
||||
}
|
||||
|
||||
Log.d(TAG, "lockPiece() started - setting isPieceLocking=true, current isHardDropInProgress=$isHardDropInProgress")
|
||||
isPieceLocking = true
|
||||
|
||||
val piece = currentPiece ?: return
|
||||
|
@ -294,15 +363,25 @@ class GameBoard(
|
|||
// Find and clear lines immediately
|
||||
findAndClearLines()
|
||||
|
||||
// IMPORTANT: Reset the hard drop flag before spawning a new piece
|
||||
// This prevents the immediate hard drop of the next piece
|
||||
if (isHardDropInProgress) {
|
||||
Log.d(TAG, "lockPiece() - resetting isHardDropInProgress=false BEFORE spawning new piece")
|
||||
isHardDropInProgress = false
|
||||
}
|
||||
|
||||
// Log piece position before spawning new piece
|
||||
Log.d(TAG, "lockPiece() - about to spawn new piece at y=${piece.y}, isHardDropInProgress=$isHardDropInProgress")
|
||||
|
||||
// Spawn new piece immediately
|
||||
spawnPiece()
|
||||
|
||||
// Allow holding piece again after locking
|
||||
canHold = true
|
||||
|
||||
// Reset both states after everything is done
|
||||
// Reset locking state
|
||||
isPieceLocking = false
|
||||
isHardDropInProgress = false
|
||||
Log.d(TAG, "lockPiece() completed - reset flags: isPieceLocking=false, isHardDropInProgress=$isHardDropInProgress")
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -326,18 +405,26 @@ class GameBoard(
|
|||
y--
|
||||
}
|
||||
|
||||
// Store the last cleared lines
|
||||
lastClearedLines.clear()
|
||||
lastClearedLines.addAll(linesToClear)
|
||||
|
||||
// If lines were cleared, calculate score in background and trigger callback
|
||||
if (shiftAmount > 0) {
|
||||
android.util.Log.d("GameBoard", "Lines cleared: $shiftAmount")
|
||||
// Log line clear
|
||||
Log.d(TAG, "Lines cleared: $shiftAmount")
|
||||
|
||||
// Trigger line clear callback on main thread with the lines that were cleared
|
||||
val mainHandler = android.os.Handler(android.os.Looper.getMainLooper())
|
||||
mainHandler.post {
|
||||
android.util.Log.d("GameBoard", "Triggering onLineClear callback with $shiftAmount lines")
|
||||
// Call the line clear callback with the cleared line count
|
||||
try {
|
||||
onLineClear?.invoke(shiftAmount, linesToClear) // Pass the lines that were cleared
|
||||
android.util.Log.d("GameBoard", "onLineClear callback completed successfully")
|
||||
Log.d(TAG, "Triggering onLineClear callback with $shiftAmount lines")
|
||||
val clearedLines = getLastClearedLines()
|
||||
onLineClear?.invoke(shiftAmount, clearedLines)
|
||||
Log.d(TAG, "onLineClear callback completed successfully")
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("GameBoard", "Error in onLineClear callback", e)
|
||||
Log.e(TAG, "Error in onLineClear callback", e)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -582,4 +669,20 @@ class GameBoard(
|
|||
* Get the current combo count
|
||||
*/
|
||||
fun getCombo(): Int = combo
|
||||
|
||||
/**
|
||||
* Get the list of lines that were most recently cleared
|
||||
*/
|
||||
private fun getLastClearedLines(): List<Int> {
|
||||
return lastClearedLines.toList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the game state (called by game loop)
|
||||
*/
|
||||
fun update() {
|
||||
if (!isGameOver) {
|
||||
moveDown()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,21 @@ 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)
|
||||
}
|
||||
|
||||
// Add default block skin if nothing is unlocked
|
||||
if (unlockedBlocks.isEmpty()) {
|
||||
unlockedBlocks.add("block_skin_1")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -65,7 +67,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()
|
||||
}
|
||||
|
@ -179,30 +180,6 @@ class PlayerProgressionManager(context: Context) {
|
|||
}
|
||||
}
|
||||
|
||||
// Check for power unlocks
|
||||
when (level) {
|
||||
8 -> {
|
||||
if (unlockedPowers.add(POWER_FREEZE_TIME)) {
|
||||
newRewards.add("Unlocked Freeze Time Power!")
|
||||
}
|
||||
}
|
||||
12 -> {
|
||||
if (unlockedPowers.add(POWER_BLOCK_SWAP)) {
|
||||
newRewards.add("Unlocked Block Swap Power!")
|
||||
}
|
||||
}
|
||||
18 -> {
|
||||
if (unlockedPowers.add(POWER_SAFE_LANDING)) {
|
||||
newRewards.add("Unlocked Safe Landing Power!")
|
||||
}
|
||||
}
|
||||
30 -> {
|
||||
if (unlockedPowers.add(POWER_PERFECT_CLEAR)) {
|
||||
newRewards.add("Unlocked Perfect Clear Power!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for block skin unlocks
|
||||
if (level % 7 == 0 && level <= 35) {
|
||||
val blockSkin = "block_skin_${level / 7}"
|
||||
|
@ -214,11 +191,38 @@ class PlayerProgressionManager(context: Context) {
|
|||
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
|
||||
for (i in 1..5) {
|
||||
val requiredLevel = i * 7
|
||||
if (playerLevel >= requiredLevel) {
|
||||
unlockedBlocks.add("block_skin_$i")
|
||||
}
|
||||
}
|
||||
|
||||
// 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 +231,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 +241,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,12 +262,14 @@ class PlayerProgressionManager(context: Context) {
|
|||
|
||||
unlockedThemes.clear()
|
||||
unlockedBlocks.clear()
|
||||
unlockedPowers.clear()
|
||||
unlockedBadges.clear()
|
||||
|
||||
// Add default theme
|
||||
unlockedThemes.add(THEME_CLASSIC)
|
||||
|
||||
// Add default block skin
|
||||
unlockedBlocks.add("block_skin_1")
|
||||
|
||||
saveProgress()
|
||||
}
|
||||
|
||||
|
@ -282,8 +280,9 @@ class PlayerProgressionManager(context: Context) {
|
|||
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_BLOCK_SKIN = "selected_block_skin"
|
||||
private const val KEY_SELECTED_THEME = "selected_theme"
|
||||
|
||||
// XP curve parameters
|
||||
private const val BASE_XP = 4000.0 // Base XP for level 1 (reduced from 5000)
|
||||
|
@ -313,20 +312,6 @@ class PlayerProgressionManager(context: Context) {
|
|||
THEME_MINIMALIST to 20,
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -337,9 +322,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).apply()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
308
app/src/main/java/com/mintris/ui/BlockSkinSelector.kt
Normal file
308
app/src/main/java/com/mintris/ui/BlockSkinSelector.kt
Normal file
|
@ -0,0 +1,308 @@
|
|||
package com.mintris.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.GridLayout
|
||||
import android.widget.TextView
|
||||
import androidx.cardview.widget.CardView
|
||||
import com.mintris.R
|
||||
import com.mintris.model.PlayerProgressionManager
|
||||
|
||||
/**
|
||||
* UI component for selecting block skins
|
||||
*/
|
||||
class BlockSkinSelector @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : FrameLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
private val skinsGrid: GridLayout
|
||||
private val availableSkinsLabel: TextView
|
||||
|
||||
// Callback when a block skin is selected
|
||||
var onBlockSkinSelected: ((String) -> Unit)? = null
|
||||
|
||||
// Currently selected block skin
|
||||
private var selectedSkin: String = "block_skin_1"
|
||||
|
||||
// Block skin cards
|
||||
private val skinCards = mutableMapOf<String, CardView>()
|
||||
|
||||
// Player level for determining what should be unlocked
|
||||
private var playerLevel: Int = 1
|
||||
|
||||
init {
|
||||
// Inflate the layout
|
||||
LayoutInflater.from(context).inflate(R.layout.block_skin_selector, this, true)
|
||||
|
||||
// Get references to views
|
||||
skinsGrid = findViewById(R.id.skins_grid)
|
||||
availableSkinsLabel = findViewById(R.id.available_skins_label)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the block skin selector with unlocked skins
|
||||
*/
|
||||
fun updateBlockSkins(unlockedSkins: Set<String>, currentSkin: String, playerLevel: Int = 1) {
|
||||
// Store player level
|
||||
this.playerLevel = playerLevel
|
||||
|
||||
// Clear existing skin cards
|
||||
skinsGrid.removeAllViews()
|
||||
skinCards.clear()
|
||||
|
||||
// Update selected skin
|
||||
selectedSkin = currentSkin
|
||||
|
||||
// Get all possible skins and their details
|
||||
val allSkins = getBlockSkins()
|
||||
|
||||
// Add skin cards to the grid
|
||||
allSkins.forEach { (skinId, skinInfo) ->
|
||||
val isUnlocked = unlockedSkins.contains(skinId) || playerLevel >= skinInfo.unlockLevel
|
||||
val isSelected = skinId == selectedSkin
|
||||
|
||||
val skinCard = createBlockSkinCard(skinId, skinInfo, isUnlocked, isSelected)
|
||||
skinCards[skinId] = skinCard
|
||||
skinsGrid.addView(skinCard)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the selected skin with a visual effect
|
||||
*/
|
||||
fun setSelectedSkin(skinId: String) {
|
||||
if (skinId == selectedSkin) return
|
||||
|
||||
// Update previously selected card
|
||||
skinCards[selectedSkin]?.let { prevCard ->
|
||||
prevCard.cardElevation = 2f
|
||||
// Reset any special styling
|
||||
prevCard.background = null
|
||||
prevCard.setCardBackgroundColor(getBlockSkins()[selectedSkin]?.backgroundColor ?: Color.BLACK)
|
||||
}
|
||||
|
||||
// Update visual state of newly selected card
|
||||
skinCards[skinId]?.let { card ->
|
||||
card.cardElevation = 12f
|
||||
|
||||
// Flash animation for selection feedback
|
||||
val flashColor = Color.WHITE
|
||||
val originalColor = getBlockSkins()[skinId]?.backgroundColor ?: Color.BLACK
|
||||
|
||||
// Create animator for flash effect
|
||||
val flashAnimator = android.animation.ValueAnimator.ofArgb(flashColor, originalColor)
|
||||
flashAnimator.duration = 300 // 300ms
|
||||
flashAnimator.addUpdateListener { animator ->
|
||||
val color = animator.animatedValue as Int
|
||||
card.setCardBackgroundColor(color)
|
||||
}
|
||||
flashAnimator.start()
|
||||
|
||||
// Add special border to selected card
|
||||
val gradientDrawable = android.graphics.drawable.GradientDrawable().apply {
|
||||
setColor(originalColor)
|
||||
setStroke(6, Color.WHITE) // Thicker border
|
||||
cornerRadius = 12f
|
||||
}
|
||||
card.background = gradientDrawable
|
||||
}
|
||||
|
||||
// Update selected skin
|
||||
selectedSkin = skinId
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a card for a block skin
|
||||
*/
|
||||
private fun createBlockSkinCard(
|
||||
skinId: String,
|
||||
skinInfo: BlockSkinInfo,
|
||||
isUnlocked: Boolean,
|
||||
isSelected: Boolean
|
||||
): CardView {
|
||||
// Create the card
|
||||
val card = CardView(context).apply {
|
||||
id = View.generateViewId()
|
||||
radius = 12f
|
||||
cardElevation = if (isSelected) 8f else 2f
|
||||
useCompatPadding = true
|
||||
|
||||
// Set card background color based on skin
|
||||
setCardBackgroundColor(skinInfo.backgroundColor)
|
||||
|
||||
// Add more noticeable visual indicator for selected skin
|
||||
if (isSelected) {
|
||||
setContentPadding(4, 4, 4, 4)
|
||||
// Create a gradient drawable for the border
|
||||
val gradientDrawable = android.graphics.drawable.GradientDrawable().apply {
|
||||
setColor(skinInfo.backgroundColor)
|
||||
setStroke(6, Color.WHITE) // Thicker border
|
||||
cornerRadius = 12f
|
||||
}
|
||||
background = gradientDrawable
|
||||
// Add glow effect via elevation
|
||||
cardElevation = 12f
|
||||
}
|
||||
|
||||
// Set card dimensions
|
||||
val cardSize = resources.getDimensionPixelSize(R.dimen.theme_card_size)
|
||||
layoutParams = GridLayout.LayoutParams().apply {
|
||||
width = cardSize
|
||||
height = cardSize
|
||||
columnSpec = GridLayout.spec(GridLayout.UNDEFINED, 1f)
|
||||
rowSpec = GridLayout.spec(GridLayout.UNDEFINED, 1f)
|
||||
setMargins(8, 8, 8, 8)
|
||||
}
|
||||
|
||||
// Apply locked/selected state visuals
|
||||
alpha = if (isUnlocked) 1.0f else 0.5f
|
||||
}
|
||||
|
||||
// Create block skin content container
|
||||
val container = FrameLayout(context).apply {
|
||||
layoutParams = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
}
|
||||
|
||||
// Create the block skin preview
|
||||
val blockSkinPreview = View(context).apply {
|
||||
layoutParams = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
|
||||
// Set the background color
|
||||
setBackgroundColor(skinInfo.backgroundColor)
|
||||
}
|
||||
|
||||
// Add a label with the skin name
|
||||
val skinLabel = TextView(context).apply {
|
||||
text = skinInfo.displayName
|
||||
setTextColor(skinInfo.textColor)
|
||||
textSize = 14f
|
||||
textAlignment = View.TEXT_ALIGNMENT_CENTER
|
||||
|
||||
// Position at the bottom of the card
|
||||
layoutParams = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.WRAP_CONTENT
|
||||
).apply {
|
||||
gravity = android.view.Gravity.BOTTOM or android.view.Gravity.CENTER_HORIZONTAL
|
||||
setMargins(4, 4, 4, 8)
|
||||
}
|
||||
}
|
||||
|
||||
// Add level requirement for locked skins
|
||||
val levelRequirement = TextView(context).apply {
|
||||
text = "Level ${skinInfo.unlockLevel}"
|
||||
setTextColor(Color.WHITE)
|
||||
textSize = 12f
|
||||
textAlignment = View.TEXT_ALIGNMENT_CENTER
|
||||
visibility = if (isUnlocked) View.GONE else View.VISIBLE
|
||||
|
||||
// Position at the center of the card
|
||||
layoutParams = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.WRAP_CONTENT,
|
||||
FrameLayout.LayoutParams.WRAP_CONTENT
|
||||
).apply {
|
||||
gravity = android.view.Gravity.CENTER
|
||||
}
|
||||
// Make text bold and more visible for better readability
|
||||
typeface = android.graphics.Typeface.DEFAULT_BOLD
|
||||
setShadowLayer(3f, 1f, 1f, Color.BLACK)
|
||||
}
|
||||
|
||||
// Add a lock icon if the skin is locked
|
||||
val lockOverlay = View(context).apply {
|
||||
layoutParams = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
|
||||
// Add lock icon or visual indicator
|
||||
setBackgroundResource(R.drawable.lock_overlay)
|
||||
visibility = if (isUnlocked) View.GONE else View.VISIBLE
|
||||
}
|
||||
|
||||
// Add all elements to container
|
||||
container.addView(blockSkinPreview)
|
||||
container.addView(skinLabel)
|
||||
container.addView(lockOverlay)
|
||||
container.addView(levelRequirement)
|
||||
|
||||
// Add container to card
|
||||
card.addView(container)
|
||||
|
||||
// Set up click listener only for unlocked skins
|
||||
if (isUnlocked) {
|
||||
card.setOnClickListener {
|
||||
// Only trigger callback if this isn't already the selected skin
|
||||
if (skinId != selectedSkin) {
|
||||
// Update UI for selection
|
||||
setSelectedSkin(skinId)
|
||||
|
||||
// Notify listener
|
||||
onBlockSkinSelected?.invoke(skinId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return card
|
||||
}
|
||||
|
||||
/**
|
||||
* Data class for block skin information
|
||||
*/
|
||||
data class BlockSkinInfo(
|
||||
val displayName: String,
|
||||
val backgroundColor: Int,
|
||||
val textColor: Int,
|
||||
val unlockLevel: Int
|
||||
)
|
||||
|
||||
/**
|
||||
* Get all available block skins with their details
|
||||
*/
|
||||
private fun getBlockSkins(): Map<String, BlockSkinInfo> {
|
||||
return mapOf(
|
||||
"block_skin_1" to BlockSkinInfo(
|
||||
displayName = "Classic",
|
||||
backgroundColor = Color.BLACK,
|
||||
textColor = Color.WHITE,
|
||||
unlockLevel = 1
|
||||
),
|
||||
"block_skin_2" to BlockSkinInfo(
|
||||
displayName = "Neon",
|
||||
backgroundColor = Color.parseColor("#0D0221"),
|
||||
textColor = Color.parseColor("#FF00FF"),
|
||||
unlockLevel = 7
|
||||
),
|
||||
"block_skin_3" to BlockSkinInfo(
|
||||
displayName = "Retro",
|
||||
backgroundColor = Color.parseColor("#3F2832"),
|
||||
textColor = Color.parseColor("#FF5A5F"),
|
||||
unlockLevel = 14
|
||||
),
|
||||
"block_skin_4" to BlockSkinInfo(
|
||||
displayName = "Minimalist",
|
||||
backgroundColor = Color.WHITE,
|
||||
textColor = Color.BLACK,
|
||||
unlockLevel = 21
|
||||
),
|
||||
"block_skin_5" to BlockSkinInfo(
|
||||
displayName = "Galaxy",
|
||||
backgroundColor = Color.parseColor("#0B0C10"),
|
||||
textColor = Color.parseColor("#66FCF1"),
|
||||
unlockLevel = 28
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -282,13 +282,15 @@
|
|||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1">
|
||||
android:layout_weight="1"
|
||||
android:fillViewport="true">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center">
|
||||
android:gravity="center"
|
||||
android:paddingTop="16dp">
|
||||
|
||||
<Button
|
||||
android:id="@+id/pauseStartButton"
|
||||
|
@ -399,6 +401,14 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<!-- Block Skin Selector -->
|
||||
<com.mintris.ui.BlockSkinSelector
|
||||
android:id="@+id/blockSkinSelector"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/settingsButton"
|
||||
|
|
27
app/src/main/res/layout/block_skin_selector.xml
Normal file
27
app/src/main/res/layout/block_skin_selector.xml
Normal file
|
@ -0,0 +1,27 @@
|
|||
<?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="18sp"
|
||||
android:textStyle="bold"
|
||||
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>
|
Loading…
Add table
Add a link
Reference in a new issue