This commit is contained in:
Lucas Lima 2020-09-16 20:20:32 -03:00
parent 3f2844d730
commit 98f9cd94db
No known key found for this signature in database
GPG key ID: 049CCC5A365B00D2
33 changed files with 415 additions and 60 deletions

View file

@ -8,6 +8,7 @@ import android.text.format.DateUtils
import android.view.View
import android.view.WindowManager
import android.widget.FrameLayout
import android.widget.Toast
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.TooltipCompat
@ -58,7 +59,6 @@ import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import java.util.Locale
class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.OnDismissListener {
private val billingManager: IBillingManager by inject()
@ -188,6 +188,13 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
currentSaveId = it
}
)
tips.observe(
this@GameActivity,
{
tipsCounter.text = it.toString()
}
)
}
override fun onBackPressed() {
@ -248,8 +255,47 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
)
}
private fun refreshShortcutIcon(isEndGame: Boolean = false) {
if (isEndGame || instantAppManager.isEnabled(this)) {
private fun disableShortcutIcon(hide: Boolean = false) {
tipsCounter.visibility = View.GONE
shortcutIcon.apply {
visibility = if (hide) View.GONE else View.VISIBLE
isClickable = false
animate().alpha(0.3f).start()
}
}
private fun refreshTipShortcutIcon() {
val tipsAmount = gameViewModel.getTips()
tipsCounter.apply {
visibility = View.VISIBLE
text = tipsAmount.toString()
}
shortcutIcon.apply {
TooltipCompat.setTooltipText(this, getString(R.string.help))
setImageResource(R.drawable.tip)
setColorFilter(minesCount.currentTextColor)
visibility = View.VISIBLE
animate().alpha(1.0f).start()
setOnClickListener {
lifecycleScope.launch {
analyticsManager.sentEvent(Analytics.UseTip)
if (tipsAmount > 0) {
if (!gameViewModel.revealRandomMine()) {
Toast.makeText(applicationContext, R.string.cant_do_it_now, Toast.LENGTH_SHORT).show()
}
} else {
Toast.makeText(applicationContext, R.string.cant_do_it_now, Toast.LENGTH_SHORT).show()
}
}
}
}
}
private fun refreshEndGameShortcutIcon(victory: Boolean = false, isOldGame: Boolean) {
if ((isOldGame || !victory) && !instantAppManager.isEnabled(this)) {
shortcutIcon.apply {
TooltipCompat.setTooltipText(this, getString(R.string.new_game))
setImageResource(R.drawable.retry)
@ -284,6 +330,7 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
}
}
tipsCounter.visibility = View.GONE
shortcutIcon.apply {
when (status) {
is Status.Over, is Status.Running -> {
@ -324,7 +371,7 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
}
override fun onDrawerClosed(drawerView: View) {
if (hasNoOtherFocusedDialog()) {
if (hasNoOtherFocusedDialog() && hasActiveGameFragment()) {
gameViewModel.resumeGame()
}
@ -435,13 +482,6 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
private fun loadGameFragment() {
supportFragmentManager.apply {
findFragmentByTag(LevelFragment.TAG)?.let { it ->
beginTransaction().apply {
remove(it)
commitAllowingStateLoss()
}
}
findFragmentByTag(TutorialLevelFragment.TAG)?.let { it ->
beginTransaction().apply {
remove(it)
@ -451,7 +491,7 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
if (findFragmentByTag(LevelFragment.TAG) == null) {
beginTransaction().apply {
replace(R.id.levelContainer, LevelFragment())
replace(R.id.levelContainer, LevelFragment(), LevelFragment.TAG)
setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
commitAllowingStateLoss()
}
@ -460,6 +500,8 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
}
private fun loadGameTutorial() {
disableShortcutIcon(true)
supportFragmentManager.apply {
findFragmentById(R.id.levelContainer)?.let { it ->
beginTransaction().apply {
@ -556,7 +598,8 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
victory,
score?.rightMines ?: 0,
score?.totalMines ?: 0,
currentGameStatus.time
currentGameStatus.time,
if (victory) 1 else 0
).apply {
showAllowingStateLoss(supportFragmentManager, EndGameDialogFragment.TAG)
}
@ -597,25 +640,28 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
when (event) {
Event.ResumeGame -> {
status = Status.Running
refreshShortcutIcon()
refreshTipShortcutIcon()
}
Event.StartNewGame -> {
status = Status.PreGame
refreshShortcutIcon()
disableShortcutIcon()
refreshAds()
}
Event.Resume, Event.Running -> {
status = Status.Running
gameViewModel.runClock()
refreshShortcutIcon()
refreshTipShortcutIcon()
keepScreenOn(true)
}
Event.StartTutorial -> {
status = Status.PreGame
gameViewModel.stopClock()
disableShortcutIcon(true)
loadGameTutorial()
}
Event.FinishTutorial -> {
gameViewModel.startNewGame(Difficulty.Beginner)
disableShortcutIcon()
loadGameFragment()
status = Status.Over(0, Score(4, 4, 25))
analyticsManager.sentEvent(Analytics.TutorialCompleted)
@ -631,12 +677,14 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
)
status = Status.Over(currentTime, score)
gameViewModel.stopClock()
gameViewModel.revealAllEmptyAreas()
gameViewModel.showAllEmptyAreas()
gameViewModel.victory()
refreshShortcutIcon(true)
refreshEndGameShortcutIcon(true, isResuming)
keepScreenOn(false)
if (!isResuming) {
gameViewModel.addNewTip()
waitAndShowEndGameDialog(
victory = true,
await = false
@ -651,7 +699,7 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
totalArea
)
status = Status.Over(currentTime, score)
refreshShortcutIcon(true)
refreshEndGameShortcutIcon(false, isResuming)
keepScreenOn(false)
gameViewModel.stopClock()
@ -711,6 +759,10 @@ class GameActivity : ThematicActivity(R.layout.activity_game), DialogInterface.O
} == 0
}
private fun hasActiveGameFragment(): Boolean {
return supportFragmentManager.findFragmentByTag(LevelFragment.TAG) != null
}
override fun onDismiss(dialog: DialogInterface?) {
gameViewModel.run {
refreshUserPreferences()

View file

@ -5,6 +5,7 @@ import dev.lucasnlm.antimine.common.level.di.LevelModule
import dev.lucasnlm.antimine.core.analytics.IAnalyticsManager
import dev.lucasnlm.antimine.core.analytics.models.Analytics
import dev.lucasnlm.antimine.core.di.CommonModule
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
import dev.lucasnlm.antimine.di.AppModule
import dev.lucasnlm.antimine.di.ViewModelModule
import dev.lucasnlm.external.IAdsManager
@ -14,6 +15,7 @@ import org.koin.core.context.startKoin
open class MainApplication : MultiDexApplication() {
private val analyticsManager: IAnalyticsManager by inject()
private val preferencesRepository: IPreferencesRepository by inject()
private val adsManager: IAdsManager by inject()
@ -29,6 +31,10 @@ open class MainApplication : MultiDexApplication() {
sentEvent(Analytics.Open)
}
if (BuildConfig.FLAVOR == "foss") {
preferencesRepository.setPremiumFeatures(true)
}
adsManager.start(applicationContext)
}
}

View file

@ -25,9 +25,9 @@ val ViewModelModule = module {
viewModel { TextViewModel(get()) }
viewModel { ThemeViewModel(get(), get(), get(), get()) }
viewModel {
GameViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get())
GameViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get())
}
viewModel {
TutorialViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get())
TutorialViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get())
}
}

View file

@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
@ -12,11 +13,8 @@ import androidx.fragment.app.FragmentManager
import androidx.lifecycle.lifecycleScope
import dev.lucasnlm.antimine.R
import dev.lucasnlm.antimine.common.level.viewmodel.GameViewModel
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
import dev.lucasnlm.antimine.level.viewmodel.EndGameDialogEvent
import dev.lucasnlm.antimine.level.viewmodel.EndGameDialogViewModel
import dev.lucasnlm.external.Ads
import dev.lucasnlm.external.IAdsManager
import dev.lucasnlm.external.IInstantAppManager
import kotlinx.coroutines.flow.collect
import org.koin.android.ext.android.inject
@ -25,9 +23,6 @@ import org.koin.androidx.viewmodel.ext.android.viewModel
class EndGameDialogFragment : AppCompatDialogFragment() {
private val instantAppManager: IInstantAppManager by inject()
private val adsManager: IAdsManager by inject()
private val preferencesRepository: IPreferencesRepository by inject()
private val endGameViewModel by viewModel<EndGameDialogViewModel>()
private val gameViewModel by sharedViewModel<GameViewModel>()
@ -40,7 +35,8 @@ class EndGameDialogFragment : AppCompatDialogFragment() {
isVictory = if (getInt(DIALOG_TOTAL_MINES, 0) > 0) getBoolean(DIALOG_IS_VICTORY) else null,
time = getLong(DIALOG_TIME, 0L),
rightMines = getInt(DIALOG_RIGHT_MINES, 0),
totalMines = getInt(DIALOG_TOTAL_MINES, 0)
totalMines = getInt(DIALOG_TOTAL_MINES, 0),
received = getInt(DIALOG_RECEIVED, -1)
)
)
}
@ -72,6 +68,15 @@ class EndGameDialogFragment : AppCompatDialogFragment() {
}
}
findViewById<TextView>(R.id.received_message).apply {
if (state.received > 0 && state.isVictory == true) {
visibility = View.VISIBLE
text = getString(R.string.you_have_received, state.received)
} else {
visibility = View.GONE
}
}
if (state.isVictory == true) {
if (!instantAppManager.isEnabled(context)) {
setNeutralButton(R.string.share) { _, _ ->
@ -95,20 +100,27 @@ class EndGameDialogFragment : AppCompatDialogFragment() {
}.create()
companion object {
fun newInstance(victory: Boolean, rightMines: Int, totalMines: Int, time: Long) =
EndGameDialogFragment().apply {
arguments = Bundle().apply {
putBoolean(DIALOG_IS_VICTORY, victory)
putInt(DIALOG_RIGHT_MINES, rightMines)
putInt(DIALOG_TOTAL_MINES, totalMines)
putLong(DIALOG_TIME, time)
}
fun newInstance(
victory: Boolean,
rightMines: Int,
totalMines: Int,
time: Long,
received: Int,
) = EndGameDialogFragment().apply {
arguments = Bundle().apply {
putBoolean(DIALOG_IS_VICTORY, victory)
putInt(DIALOG_RIGHT_MINES, rightMines)
putInt(DIALOG_TOTAL_MINES, totalMines)
putInt(DIALOG_RECEIVED, received)
putLong(DIALOG_TIME, time)
}
}
const val DIALOG_IS_VICTORY = "dialog_state"
private const val DIALOG_TIME = "dialog_time"
private const val DIALOG_RIGHT_MINES = "dialog_right_mines"
private const val DIALOG_TOTAL_MINES = "dialog_total_mines"
private const val DIALOG_RECEIVED = "dialog_received"
val TAG = EndGameDialogFragment::class.simpleName!!
}

View file

@ -8,6 +8,7 @@ sealed class EndGameDialogEvent {
val time: Long,
val rightMines: Int,
val totalMines: Int,
val received: Int,
) : EndGameDialogEvent()
data class ChangeEmoji(

View file

@ -7,4 +7,5 @@ data class EndGameDialogState(
val title: String,
val message: String,
val isVictory: Boolean?,
val received: Int
)

View file

@ -70,6 +70,7 @@ class EndGameDialogViewModel(
"",
"",
false,
0
)
override suspend fun mapEventToState(event: EndGameDialogEvent) = flow {
@ -80,7 +81,8 @@ class EndGameDialogViewModel(
titleEmoji = randomVictoryEmoji(),
title = context.getString(R.string.you_won),
message = messageTo(event.time, event.isVictory),
isVictory = true
isVictory = true,
received = event.received
)
}
false -> {
@ -88,7 +90,8 @@ class EndGameDialogViewModel(
titleEmoji = randomGameOverEmoji(),
title = context.getString(R.string.you_lost),
message = messageTo(event.time, event.isVictory),
isVictory = false
isVictory = false,
received = event.received
)
}
null -> {
@ -96,7 +99,8 @@ class EndGameDialogViewModel(
titleEmoji = randomNeutralEmoji(),
title = context.getString(R.string.new_game),
message = context.getString(R.string.new_game_request),
isVictory = false
isVictory = false,
received = event.received
)
}
}

View file

@ -27,15 +27,42 @@ class ThemeAdapter(
private val themes: List<AppTheme> = themeViewModel.singleState().themes
private val minefield = listOf(
Area(0, 0, 0, 1, hasMine = false, mistake = false, mark = Mark.None, isCovered = false),
Area(1, 1, 0, 0, hasMine = true, mistake = false, mark = Mark.None, isCovered = false),
Area(2, 2, 0, 0, hasMine = true, mistake = false, mark = Mark.None, isCovered = true),
Area(3, 0, 1, 2, hasMine = false, mistake = false, mark = Mark.None, isCovered = false),
Area(4, 1, 1, 3, hasMine = false, mistake = false, mark = Mark.None, isCovered = false),
Area(5, 2, 1, 3, hasMine = true, mistake = false, mark = Mark.Flag, isCovered = true),
Area(6, 0, 2, 0, hasMine = true, mistake = false, mark = Mark.Question, isCovered = true),
Area(7, 1, 2, 4, hasMine = false, mistake = false, mark = Mark.None, isCovered = false),
Area(8, 2, 2, 0, hasMine = false, mistake = false, mark = Mark.None, isCovered = true),
Area(
0, 0, 0, 1,
hasMine = false, mistake = false, mark = Mark.None, isCovered = false
),
Area(
1, 1, 0, 0,
hasMine = true, mistake = false, mark = Mark.None, isCovered = false
),
Area(
2, 2, 0, 0,
hasMine = true, mistake = false, mark = Mark.None, isCovered = true
),
Area(
3, 0, 1, 2,
hasMine = false, mistake = false, mark = Mark.None, isCovered = false
),
Area(
4, 1, 1, 3,
hasMine = false, mistake = false, mark = Mark.None, isCovered = false
),
Area(
5, 2, 1, 3,
hasMine = true, mistake = false, mark = Mark.Flag, isCovered = true
),
Area(
6, 0, 2, 0,
hasMine = true, mistake = false, mark = Mark.Question, isCovered = true
),
Area(
7, 1, 2, 4,
hasMine = false, mistake = false, mark = Mark.None, isCovered = false
),
Area(
8, 2, 2, 0,
hasMine = false, mistake = false, mark = Mark.None, isCovered = true, revealed = true
),
)
init {

View file

@ -7,6 +7,7 @@ import dev.lucasnlm.antimine.common.level.repository.IDimensionRepository
import dev.lucasnlm.antimine.common.level.repository.IMinefieldRepository
import dev.lucasnlm.antimine.common.level.repository.ISavesRepository
import dev.lucasnlm.antimine.common.level.repository.IStatsRepository
import dev.lucasnlm.antimine.common.level.repository.ITipRepository
import dev.lucasnlm.antimine.common.level.utils.Clock
import dev.lucasnlm.antimine.common.level.utils.IHapticFeedbackManager
import dev.lucasnlm.antimine.common.level.viewmodel.GameViewModel
@ -29,6 +30,7 @@ class TutorialViewModel(
minefieldRepository: IMinefieldRepository,
analyticsManager: IAnalyticsManager,
playGamesManager: IPlayGamesManager,
tipRepository: ITipRepository,
private val clock: Clock,
private val context: Context,
private val hapticFeedbackManager: IHapticFeedbackManager,
@ -44,6 +46,7 @@ class TutorialViewModel(
minefieldRepository,
analyticsManager,
playGamesManager,
tipRepository,
clock,
) {
val tutorialState = MutableStateFlow(

View file

@ -0,0 +1,10 @@
<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="M12,3c-0.46,0 -0.93,0.04 -1.4,0.14C7.84,3.67 5.64,5.9 5.12,8.66c-0.48,2.61 0.48,5.01 2.22,6.56C7.77,15.6 8,16.13 8,16.69V19c0,1.1 0.9,2 2,2h0.28c0.35,0.6 0.98,1 1.72,1s1.38,-0.4 1.72,-1H14c1.1,0 2,-0.9 2,-2v-2.31c0,-0.55 0.22,-1.09 0.64,-1.46C18.09,13.95 19,12.08 19,10C19,6.13 15.87,3 12,3zM12.5,14h-1v-2.59L9.67,9.59l0.71,-0.71L12,10.5l1.62,-1.62l0.71,0.71l-1.83,1.83V14zM13.5,19c-0.01,0 -0.02,-0.01 -0.03,-0.01V19h-2.94v-0.01c-0.01,0 -0.02,0.01 -0.03,0.01c-0.28,0 -0.5,-0.22 -0.5,-0.5c0,-0.28 0.22,-0.5 0.5,-0.5c0.01,0 0.02,0.01 0.03,0.01V18h2.94v0.01c0.01,0 0.02,-0.01 0.03,-0.01c0.28,0 0.5,0.22 0.5,0.5C14,18.78 13.78,19 13.5,19zM13.5,17h-3c-0.28,0 -0.5,-0.22 -0.5,-0.5c0,-0.28 0.22,-0.5 0.5,-0.5h3c0.28,0 0.5,0.22 0.5,0.5C14,16.78 13.78,17 13.5,17z" />
</vector>

View file

@ -90,6 +90,20 @@
app:srcCompat="@drawable/retry"
tools:alpha="1.0"/>
<TextView
android:id="@+id/tipsCounter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="41dp"
android:textSize="12sp"
android:textStyle="bold"
android:visibility="gone"
tools:text="100"
tools:visibility="visible"
ads:layout_constraintBottom_toBottomOf="@+id/shortcutIcon"
ads:layout_constraintEnd_toEndOf="@+id/shortcutIcon"
ads:layout_constraintTop_toTopOf="parent" />
<FrameLayout
android:id="@+id/levelContainer"
android:layout_width="0dp"

View file

@ -50,4 +50,19 @@
app:layout_constraintTop_toBottomOf="@+id/title"
tools:text="You did 5/12 in 34 seconds." />
<TextView
android:id="@+id/received_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/you_have_received"
android:layout_marginTop="4dp"
android:textSize="14sp"
android:textAlignment="center"
android:gravity="center"
app:drawableEndCompat="@drawable/tip"
app:drawableTint="@color/accent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/subtitle"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -61,6 +61,14 @@ class MockPreferencesRepository : IPreferencesRepository {
override fun showSupport(): Boolean = true
override fun getTips(): Int = 0
override fun setTips(tips: Int) { }
override fun getExtraTips(): Int = 5
override fun setExtraTips(tips: Int) { }
override fun useFlagAssistant(): Boolean = false
override fun useHapticFeedback(): Boolean = true

View file

@ -48,7 +48,8 @@ class ThemeViewModelTest : IntentViewModelTest() {
toolbarMine = R.drawable.mine,
mine = R.drawable.mine,
mineExploded = R.drawable.mine_exploded_red,
mineLow = R.drawable.mine_low
mineLow = R.drawable.mine_low,
revealed = R.drawable.mine_revealed,
)
)
@ -81,7 +82,8 @@ class ThemeViewModelTest : IntentViewModelTest() {
toolbarMine = R.drawable.mine_low,
mine = R.drawable.mine,
mineExploded = R.drawable.mine_exploded_white,
mineLow = R.drawable.mine_low
mineLow = R.drawable.mine_low,
revealed = R.drawable.mine_revealed,
)
)
@ -114,7 +116,8 @@ class ThemeViewModelTest : IntentViewModelTest() {
toolbarMine = R.drawable.mine_low,
mine = R.drawable.mine,
mineExploded = R.drawable.mine_exploded_white,
mineLow = R.drawable.mine_low
mineLow = R.drawable.mine_low,
revealed = R.drawable.mine_revealed,
)
)

View file

@ -212,6 +212,15 @@ class GameController {
}
}
fun revealRandomMine(): Boolean {
val result: Boolean
field = MinefieldHandler(field.toMutableList(), false).run {
result = revealRandomMine()
result()
}
return result
}
fun hasAnyMineExploded(): Boolean = mines().firstOrNull { it.mistake } != null
fun hasFlaggedAllMines(): Boolean = rightFlags() == minefield.mines

View file

@ -18,7 +18,7 @@ import dev.lucasnlm.antimine.common.level.database.models.Stats
Save::class,
Stats::class
],
version = 6,
version = 7,
exportSchema = false
)
@TypeConverters(

View file

@ -5,9 +5,11 @@ import dev.lucasnlm.antimine.common.level.database.AppDataBase
import dev.lucasnlm.antimine.common.level.repository.IMinefieldRepository
import dev.lucasnlm.antimine.common.level.repository.ISavesRepository
import dev.lucasnlm.antimine.common.level.repository.IStatsRepository
import dev.lucasnlm.antimine.common.level.repository.ITipRepository
import dev.lucasnlm.antimine.common.level.repository.MinefieldRepository
import dev.lucasnlm.antimine.common.level.repository.SavesRepository
import dev.lucasnlm.antimine.common.level.repository.StatsRepository
import dev.lucasnlm.antimine.common.level.repository.TipRepository
import dev.lucasnlm.antimine.common.level.utils.Clock
import dev.lucasnlm.antimine.common.level.utils.HapticFeedbackManager
import dev.lucasnlm.antimine.common.level.utils.IHapticFeedbackManager
@ -48,4 +50,8 @@ val LevelModule = module {
single {
HapticFeedbackManager(get())
} bind IHapticFeedbackManager::class
single {
TipRepository(get())
} bind ITipRepository::class
}

View file

@ -22,6 +22,13 @@ class MinefieldHandler(
.forEach { field[it.id] = it.copy(isCovered = false) }
}
fun revealRandomMine(): Boolean {
return field.filter { it.hasMine && it.mark.isNone() && !it.revealed }.shuffled().firstOrNull()?.run {
field[this.id] = this.copy(revealed = true)
true
} ?: false
}
fun turnOffAllHighlighted() {
field.filter { it.highlighted }
.forEach { field[it.id] = it.copy(highlighted = false) }

View file

@ -10,4 +10,5 @@ data class Area(
val isCovered: Boolean = true,
val mark: Mark = Mark.None,
val highlighted: Boolean = false,
val revealed: Boolean = false,
)

View file

@ -0,0 +1,50 @@
package dev.lucasnlm.antimine.common.level.repository
import dev.lucasnlm.antimine.core.preferences.IPreferencesRepository
interface ITipRepository {
fun setExtraTips(amount: Int)
fun removeTip(): Boolean
fun increaseTip()
fun getTotalTips(): Int
}
class TipRepository(
private val preferencesRepository: IPreferencesRepository
) : ITipRepository {
override fun setExtraTips(amount: Int) {
preferencesRepository.setExtraTips(amount)
}
override fun removeTip(): Boolean {
val tips = preferencesRepository.getTips()
val extra = preferencesRepository.getExtraTips()
return when {
tips >= 1 -> {
preferencesRepository.setTips(tips - 1)
true
}
preferencesRepository.getExtraTips() >= 1 -> {
preferencesRepository.setExtraTips(extra - 1)
true
}
else -> {
false
}
}
}
override fun increaseTip() {
val newValue = (preferencesRepository.getTips() + 1).coerceAtMost(MAX_TIPS).coerceAtLeast(0)
preferencesRepository.setTips(newValue)
}
override fun getTotalTips(): Int {
return preferencesRepository.getExtraTips() + preferencesRepository.getTips()
}
companion object {
const val MAX_TIPS = 5
}
}

View file

@ -79,7 +79,21 @@ fun Area.paintOnCanvas(
)
question?.draw(canvas)
}
else -> {}
else -> {
if (revealed) {
val padding = minePadding ?: context.resources.getDimension(R.dimen.mine_padding).toInt()
val revealedDrawable = ContextCompat.getDrawable(context, theme.assets.revealed)
revealedDrawable?.setBounds(
rectF.left.toInt() + padding,
rectF.top.toInt() + padding,
rectF.right.toInt() - padding,
rectF.bottom.toInt() - padding
)
revealedDrawable?.draw(canvas)
}
}
}
} else {
if (isAmbientMode) {

View file

@ -15,6 +15,7 @@ import dev.lucasnlm.antimine.common.level.repository.IDimensionRepository
import dev.lucasnlm.antimine.common.level.repository.IMinefieldRepository
import dev.lucasnlm.antimine.common.level.repository.ISavesRepository
import dev.lucasnlm.antimine.common.level.repository.IStatsRepository
import dev.lucasnlm.antimine.common.level.repository.ITipRepository
import dev.lucasnlm.antimine.common.level.utils.Clock
import dev.lucasnlm.antimine.common.level.utils.IHapticFeedbackManager
import dev.lucasnlm.antimine.core.analytics.IAnalyticsManager
@ -46,6 +47,7 @@ open class GameViewModel(
private val minefieldRepository: IMinefieldRepository,
private val analyticsManager: IAnalyticsManager,
private val playGamesManager: IPlayGamesManager,
private val tipRepository: ITipRepository,
private val clock: Clock,
) : ViewModel() {
val eventObserver = MutableLiveData<Event>()
@ -62,6 +64,7 @@ open class GameViewModel(
val difficulty = MutableLiveData<Difficulty>()
val levelSetup = MutableLiveData<Minefield>()
val saveId = MutableLiveData<Long>()
val tips = MutableLiveData(tipRepository.getTotalTips())
fun startNewGame(newDifficulty: Difficulty = currentDifficulty): Minefield {
clock.reset()
@ -359,7 +362,22 @@ open class GameViewModel(
clock.stop()
}
fun revealAllEmptyAreas() = gameController.revealAllEmptyAreas()
fun showAllEmptyAreas() {
gameController.revealAllEmptyAreas()
}
fun revealRandomMine(): Boolean {
return if (gameController.revealRandomMine()) {
if (tipRepository.removeTip()) {
refreshField()
}
tips.postValue(tipRepository.getTotalTips())
true
} else {
false
}
}
fun explosionDelay() = if (preferencesRepository.useAnimations()) 750L else 0L
@ -408,6 +426,14 @@ open class GameViewModel(
}
}
fun addNewTip() {
tipRepository.increaseTip()
}
fun getTips(): Int {
return tipRepository.getTotalTips()
}
fun victory() {
gameController.run {
analyticsManager.sentEvent(

View file

@ -119,5 +119,7 @@ sealed class Analytics(
object TapRatingRequest : Analytics("Rating Request")
object UseTip : Analytics("Use Tip")
class TapGameReset(resign: Boolean) : Analytics("Game reset", mapOf("Resign" to resign.toString()))
}

View file

@ -43,6 +43,11 @@ interface IPreferencesRepository {
fun setShowSupport(show: Boolean)
fun showSupport(): Boolean
fun getTips(): Int
fun setTips(tips: Int)
fun getExtraTips(): Int
fun setExtraTips(tips: Int)
fun useFlagAssistant(): Boolean
fun useHapticFeedback(): Boolean
fun areaSizeMultiplier(): Int

View file

@ -186,6 +186,22 @@ class PreferencesRepository(
return preferencesManager.getBoolean(PREFERENCE_SHOW_SUPPORT, true)
}
override fun getTips(): Int {
return preferencesManager.getInt(PREFERENCE_TIPS, 5)
}
override fun setTips(tips: Int) {
preferencesManager.putInt(PREFERENCE_TIPS, tips)
}
override fun getExtraTips(): Int {
return preferencesManager.getInt(PREFERENCE_EXTRA_TIPS, 0)
}
override fun setExtraTips(tips: Int) {
preferencesManager.putInt(PREFERENCE_EXTRA_TIPS, tips)
}
private companion object {
private const val PREFERENCE_VIBRATION = "preference_vibration"
private const val PREFERENCE_ASSISTANT = "preference_assistant"
@ -209,5 +225,7 @@ class PreferencesRepository(
private const val PREFERENCE_REQUEST_RATING = "preference_request_rating"
private const val PREFERENCE_PREMIUM_FEATURES = "preference_premium_features"
private const val PREFERENCE_SHOW_SUPPORT = "preference_show_support"
private const val PREFERENCE_TIPS = "preference_current_tips"
private const val PREFERENCE_EXTRA_TIPS = "preference_extra_tips"
}
}

View file

@ -10,4 +10,5 @@ data class Assets(
@DrawableRes val mine: Int,
@DrawableRes val mineExploded: Int,
@DrawableRes val mineLow: Int,
@DrawableRes val revealed: Int,
)

View file

@ -58,7 +58,8 @@ class ThemeRepository(
toolbarMine = R.drawable.mine,
mine = R.drawable.mine,
mineExploded = R.drawable.mine_exploded_red,
mineLow = R.drawable.mine_low
mineLow = R.drawable.mine_low,
revealed = R.drawable.mine_revealed
)
private fun fromDefaultPalette(context: Context) =

View file

@ -36,6 +36,7 @@ object Themes {
mine = R.drawable.mine,
mineExploded = R.drawable.mine_exploded_red,
mineLow = R.drawable.mine_low,
revealed = R.drawable.mine_revealed_white,
)
)
@ -69,6 +70,7 @@ object Themes {
mine = R.drawable.mine,
mineExploded = R.drawable.mine_exploded_white,
mineLow = R.drawable.mine_low,
revealed = R.drawable.mine_revealed_white,
)
)
@ -102,6 +104,7 @@ object Themes {
mine = R.drawable.mine_low,
mineExploded = R.drawable.mine_low,
mineLow = R.drawable.mine_low,
revealed = R.drawable.mine_revealed_white,
)
)
@ -139,6 +142,7 @@ object Themes {
mine = R.drawable.mine,
mineExploded = R.drawable.mine_exploded_white,
mineLow = R.drawable.mine_low,
revealed = R.drawable.mine_revealed_white,
)
),
AppTheme(
@ -171,6 +175,7 @@ object Themes {
mine = R.drawable.mine,
mineExploded = R.drawable.mine_exploded_red,
mineLow = R.drawable.mine_low,
revealed = R.drawable.mine_revealed_white,
)
),
AppTheme(
@ -203,6 +208,7 @@ object Themes {
mine = R.drawable.mine,
mineExploded = R.drawable.mine_exploded_red,
mineLow = R.drawable.mine_low,
revealed = R.drawable.mine_revealed_white,
)
),
AppTheme(
@ -235,6 +241,7 @@ object Themes {
mine = R.drawable.mine,
mineExploded = R.drawable.mine_exploded_red,
mineLow = R.drawable.mine_low,
revealed = R.drawable.mine_revealed_white,
)
),
AppTheme(
@ -267,6 +274,7 @@ object Themes {
mine = R.drawable.mine_white,
mineExploded = R.drawable.mine_white,
mineLow = R.drawable.mine_low,
revealed = R.drawable.mine_revealed_black,
)
),
AppTheme(
@ -299,6 +307,7 @@ object Themes {
mine = R.drawable.mine_pink,
mineExploded = R.drawable.mine_pink_exploded,
mineLow = R.drawable.mine_low,
revealed = R.drawable.mine_revealed_white,
)
),
AppTheme(
@ -331,6 +340,7 @@ object Themes {
mine = R.drawable.mine_pink,
mineExploded = R.drawable.mine_pink_exploded,
mineLow = R.drawable.mine_low,
revealed = R.drawable.mine_revealed_white,
)
),
AppTheme(
@ -362,7 +372,8 @@ object Themes {
toolbarMine = R.drawable.mine_low,
mine = R.drawable.mine,
mineExploded = R.drawable.mine_pink_exploded,
mineLow = R.drawable.mine_low
mineLow = R.drawable.mine_low,
revealed = R.drawable.mine_revealed_white,
)
),
AppTheme(
@ -394,7 +405,8 @@ object Themes {
toolbarMine = R.drawable.mine_low,
mine = R.drawable.mine,
mineExploded = R.drawable.mine_pink_exploded,
mineLow = R.drawable.mine_low
mineLow = R.drawable.mine_low,
revealed = R.drawable.mine_revealed_white,
)
),
AppTheme(
@ -427,6 +439,7 @@ object Themes {
mine = R.drawable.mine,
mineExploded = R.drawable.mine_pink_exploded,
mineLow = R.drawable.mine_low,
revealed = R.drawable.mine_revealed_white,
)
),
AppTheme(
@ -458,7 +471,8 @@ object Themes {
toolbarMine = R.drawable.mine_low,
mine = R.drawable.mine,
mineExploded = R.drawable.mine_pink_exploded,
mineLow = R.drawable.mine_low
mineLow = R.drawable.mine_low,
revealed = R.drawable.mine_revealed_white,
)
),
AppTheme(
@ -491,6 +505,7 @@ object Themes {
mine = R.drawable.mine_white,
mineExploded = R.drawable.mine_white,
mineLow = R.drawable.mine_low,
revealed = R.drawable.mine_revealed_black,
)
),
AppTheme(
@ -522,7 +537,8 @@ object Themes {
toolbarMine = R.drawable.mine_low,
mine = R.drawable.mine_white,
mineExploded = R.drawable.mine_white,
mineLow = R.drawable.mine_low
mineLow = R.drawable.mine_low,
revealed = R.drawable.mine_revealed_black,
)
),
AppTheme(
@ -555,6 +571,7 @@ object Themes {
mine = R.drawable.mine_white,
mineExploded = R.drawable.mine_white,
mineLow = R.drawable.mine_low,
revealed = R.drawable.mine_revealed_black,
)
),
AppTheme(
@ -587,6 +604,7 @@ object Themes {
mine = R.drawable.mine,
mineExploded = R.drawable.mine_exploded_red,
mineLow = R.drawable.mine_low,
revealed = R.drawable.mine_revealed_black,
)
),
AppTheme(
@ -619,6 +637,7 @@ object Themes {
mine = R.drawable.mine,
mineExploded = R.drawable.mine_exploded_red,
mineLow = R.drawable.mine_low,
revealed = R.drawable.mine_revealed_black,
)
),
AppTheme(
@ -645,12 +664,13 @@ object Themes {
),
assets = Assets(
wrongFlag = R.drawable.red_flag,
flag = R.drawable.flag,
flag = R.drawable.flag_black,
questionMark = R.drawable.question,
toolbarMine = R.drawable.mine,
mine = R.drawable.mine,
mineExploded = R.drawable.mine_exploded_red,
mineLow = R.drawable.mine_low,
revealed = R.drawable.mine_revealed_black,
)
)
)

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:fillColor="#33000000"
android:pathData="M331.722,124.091l-33.743,-67.204l-83.901,0l-33.84,65.28l-80.22,0l-37.443,65.714l37.443,67.649l-37.443,66.12l37.42,67.746l80.243,0l39.239,65.717l75.084,0l37.161,-66.643l79.759,-0.019l37.942,-66.801l-40.501,-65.64l40.501,-68.129l-38.02,-63.79z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:fillColor="#33ffffff"
android:pathData="M331.722,124.091l-33.743,-67.204l-83.901,0l-33.84,65.28l-80.22,0l-37.443,65.714l37.443,67.649l-37.443,66.12l37.42,67.746l80.243,0l39.239,65.717l75.084,0l37.161,-66.643l79.759,-0.019l37.942,-66.801l-40.501,-65.64l40.501,-68.129l-38.02,-63.79z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:fillColor="#33000000"
android:pathData="M331.722,124.091l-33.743,-67.204l-83.901,0l-33.84,65.28l-80.22,0l-37.443,65.714l37.443,67.649l-37.443,66.12l37.42,67.746l80.243,0l39.239,65.717l75.084,0l37.161,-66.643l79.759,-0.019l37.942,-66.801l-40.501,-65.64l40.501,-68.129l-38.02,-63.79z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:fillColor="#33ffffff"
android:pathData="M331.722,124.091l-33.743,-67.204l-83.901,0l-33.84,65.28l-80.22,0l-37.443,65.714l37.443,67.649l-37.443,66.12l37.42,67.746l80.243,0l39.239,65.717l75.084,0l37.161,-66.643l79.759,-0.019l37.942,-66.801l-40.501,-65.64l40.501,-68.129l-38.02,-63.79z" />
</vector>

View file

@ -78,6 +78,8 @@
<string name="flag_tile">Flag</string>
<string name="retry">Retry</string>
<string name="empty">Empty</string>
<string name="cant_do_it_now">Impossible to do that now</string>
<string name="you_have_received">You have received: +%1$d</string>
<string name="unknown_error">Unknown error.</string>
<string name="leaderboards">Leaderboards</string>
<string name="cancel">Cancel</string>
@ -116,4 +118,5 @@
<string name="flag_placed">Flag placed!</string>
<string name="flag_removed">Flag removed!</string>
<string name="remove_ad">Remove Ads</string>
<string name="help">Help</string>
</resources>