6 Commits

Author SHA1 Message Date
61a81d4d91 1.1.0 2025-12-23 21:39:59 -07:00
5dbce64bed Merge branch 'main' of ssh://git.atri.dad:69/atridad/MagicCounter 2025-12-23 11:47:11 -07:00
b6b56ac01a Fixed a few issues 2025-12-23 11:47:06 -07:00
5f556074d4 Upload files to "/" 2025-12-11 21:55:20 +00:00
c11e4f1b47 Delete logo.png 2025-12-11 21:54:49 +00:00
5e815353cb Added drag and drop to Android 2025-12-08 12:55:55 -07:00
28 changed files with 371 additions and 92 deletions

BIN
MagicCounter.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -1,3 +1,5 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
@@ -13,8 +15,8 @@ android {
applicationId = "com.atridad.magiccounter"
minSdk = 31
targetSdk = 36
versionCode = 2
versionName = "1.2.0"
versionCode = 3
versionName = "1.3.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -34,9 +36,7 @@ android {
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
kotlin { compilerOptions { jvmTarget.set(JvmTarget.JVM_17) } }
java {
toolchain {

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

View File

@@ -15,6 +15,20 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.PointerInputScope
import androidx.compose.ui.input.pointer.positionChange
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.zIndex
import java.util.Collections
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.withTimeout
import kotlinx.coroutines.CancellationException
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
@@ -29,12 +43,17 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.Button
import androidx.compose.material3.AlertDialog
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
@@ -78,6 +97,14 @@ fun GameScreen(
// State for stop game confirmation
val showStopConfirm = remember { mutableStateOf(false) }
// Local list for drag-and-drop reordering
val uiPlayers = remember { mutableStateListOf<PlayerState>() }
LaunchedEffect(state.players) {
uiPlayers.clear()
uiPlayers.addAll(state.players)
}
// Initialize
state.players.forEach { p ->
@@ -100,7 +127,7 @@ fun GameScreen(
}
fun snapshotState(): GameState = state.copy(
players = state.players.map { p ->
players = uiPlayers.map { p ->
PlayerState(
id = p.id,
name = p.name,
@@ -167,19 +194,75 @@ fun GameScreen(
gameLocked.value = true
}
val displayPlayers = state.players.sortedBy { eliminated[it.id] == true }
val listState = rememberLazyListState()
var draggingItemIndex by remember { mutableStateOf<Int?>(null) }
var draggingItemOffset by remember { mutableFloatStateOf(0f) }
val haptics = LocalHapticFeedback.current
LazyColumn(
state = listState,
modifier = Modifier.weight(1f),
contentPadding = PaddingValues(4.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
itemsIndexed(displayPlayers, key = { _, item -> item.id }) { index, player ->
itemsIndexed(uiPlayers, key = { _, item -> item.id }) { index, player ->
val accent = seatAccentColor(index, MaterialTheme.colorScheme)
val perPlayerCommander: Map<Int, Int> = commanderDamages[player.id]?.toMap() ?: emptyMap()
PlayerCard(
val isDragging = index == draggingItemIndex
val currentItemIndex by rememberUpdatedState(index)
Box(
modifier = Modifier
.then(if (isDragging) Modifier else Modifier.animateItem())
.zIndex(if (isDragging) 1f else 0f)
.graphicsLayer {
translationY = if (isDragging) draggingItemOffset else 0f
scaleX = if (isDragging) 1.05f else 1f
scaleY = if (isDragging) 1.05f else 1f
shadowElevation = if (isDragging) 8.dp.toPx() else 0f
}
.pointerInput(player.id) {
detectDragGesturesAfterShortPress(
onDragStart = {
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
draggingItemIndex = currentItemIndex
draggingItemOffset = 0f
},
onDrag = { change, dragAmount ->
change.consume()
draggingItemOffset += dragAmount.y
val currentIndex = currentItemIndex
val currentInfo = listState.layoutInfo.visibleItemsInfo.find { it.index == currentIndex }
if (currentInfo != null) {
val currentCenter = currentInfo.offset + currentInfo.size / 2 + draggingItemOffset
val targetItem = listState.layoutInfo.visibleItemsInfo.find {
it.index != currentIndex &&
currentCenter > it.offset &&
currentCenter < (it.offset + it.size)
}
if (targetItem != null) {
val targetIndex = targetItem.index
if (currentIndex < uiPlayers.size && targetIndex < uiPlayers.size) {
Collections.swap(uiPlayers, currentIndex, targetIndex)
draggingItemIndex = targetIndex
draggingItemOffset -= (targetItem.offset - currentInfo.offset)
}
}
}
},
onDragEnd = {
draggingItemIndex = null
draggingItemOffset = 0f
onProgress?.invoke(snapshotState())
}
)
}
) {
PlayerCard(
player = player,
gameState = state,
opponents = state.players.map { it.id }.filter { it != player.id },
life = lifeTotals[player.id] ?: state.startingLife,
onLifeChange = { new ->
@@ -218,6 +301,7 @@ fun GameScreen(
}
}
)
}
}
}
@@ -272,7 +356,6 @@ private fun seatAccentColor(index: Int, scheme: androidx.compose.material3.Color
@Composable
private fun PlayerCard(
player: PlayerState,
gameState: GameState,
opponents: List<Int>,
life: Int,
onLifeChange: (Int) -> Unit,
@@ -427,4 +510,67 @@ private fun CounterIconButton(icon: ImageVector, contentDescription: String, ena
}
}
suspend fun PointerInputScope.detectDragGesturesAfterShortPress(
onDragStart: (Offset) -> Unit = { },
onDragEnd: () -> Unit = { },
onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
) {
while (true) {
val down = awaitPointerEventScope {
awaitFirstDown(requireUnconsumed = false)
}
var dragStarted = false
try {
withTimeout(200) {
awaitPointerEventScope {
val downId = down.id
while (true) {
val event = awaitPointerEvent()
val change = event.changes.firstOrNull { it.id == downId }
if (change == null || !change.pressed) {
throw CancellationException("Up")
}
val positionChange = change.position - down.position
if (positionChange.getDistance() > viewConfiguration.touchSlop) {
throw CancellationException("Moved")
}
}
}
}
} catch (_: TimeoutCancellationException) {
dragStarted = true
} catch (_: CancellationException) {
}
if (dragStarted) {
onDragStart(down.position)
try {
awaitPointerEventScope {
val downId = down.id
while (true) {
val event = awaitPointerEvent()
val change = event.changes.firstOrNull { it.id == downId }
if (change == null || !change.pressed) {
break
}
val delta = change.positionChange()
if (delta != Offset.Zero) {
change.consume()
onDrag(change, delta)
}
}
}
} finally {
onDragEnd()
}
}
}
}

View File

@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
android:height="108dp"
android:width="108dp"
android:viewportHeight="108"
android:viewportWidth="108"
xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z"/>
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
</vector>

View File

@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@android:color/transparent" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@android:color/transparent" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#000000</color>
</resources>

View File

@@ -1,5 +1,5 @@
[versions]
agp = "8.12.1"
agp = "8.12.3"
kotlin = "2.0.21"
coreKtx = "1.10.1"
junit = "4.13.2"

View File

@@ -411,11 +411,12 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2;
CURRENT_PROJECT_VERSION = 4;
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = MagicCounter;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@@ -446,11 +447,12 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2;
CURRENT_PROJECT_VERSION = 4;
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = MagicCounter;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;

View File

@@ -7,6 +7,7 @@
import SwiftUI
import UniformTypeIdentifiers
import Combine
/**
* Main game screen displaying the grid of players.
@@ -19,75 +20,65 @@ struct GameView: View {
@State private var showingStopConfirmation = false
@State private var selectedPlayerForCommander: PlayerState?
@State private var draggedPlayer: PlayerState?
@State private var elapsedTime: TimeInterval = 0
let match: MatchRecord
init(match: MatchRecord) {
self.match = match
self._gameState = State(initialValue: match.state)
}
var body: some View {
ZStack {
// Background
LinearGradient(colors: [Color.black, Color.indigo.opacity(0.5)], startPoint: .top, endPoint: .bottom)
.ignoresSafeArea()
VStack(spacing: 0) {
// Custom Toolbar
HStack {
Button(action: { gameManager.activeMatch = nil }) {
Image(systemName: "xmark")
.font(.system(size: 16, weight: .bold))
.foregroundColor(.white)
.padding(10)
.background(.ultraThinMaterial)
.clipShape(Circle())
}
Spacer()
if gameState.stopped {
Text("Game Stopped")
.font(.headline)
.foregroundStyle(.red)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(.ultraThinMaterial)
.cornerRadius(8)
} else if let winner = gameState.winner {
Text("Winner: \(winner.name)")
.font(.headline)
.foregroundStyle(.green)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(.ultraThinMaterial)
.cornerRadius(8)
} else {
Text("Magic Counter")
.font(.headline)
.foregroundStyle(.white)
}
Spacer()
if !gameState.stopped && gameState.winner == nil {
Button(action: { showingStopConfirmation = true }) {
Image(systemName: "stop.fill")
.font(.system(size: 16, weight: .bold))
.foregroundColor(.white)
.padding(10)
.background(.ultraThinMaterial)
.clipShape(Circle())
NavigationStack {
GeometryReader { geometry in
let columns = [GridItem(.adaptive(minimum: 320), spacing: 24)]
ScrollView {
VStack(alignment: .leading, spacing: 24) {
// Custom Header
HStack(alignment: .center, spacing: 12) {
Text(match.name)
.font(.largeTitle.bold())
.lineLimit(1)
.truncationMode(.tail)
.foregroundStyle(.white)
if !gameState.stopped && gameState.winner == nil {
Text(timeString(from: elapsedTime))
.font(.subheadline.bold())
.monospacedDigit()
.foregroundStyle(.green)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(.green.opacity(0.2))
.clipShape(Capsule())
} else {
Text(timeString(from: match.lastUpdated.timeIntervalSince(match.startedAt)))
.font(.subheadline.bold())
.monospacedDigit()
.foregroundStyle(.secondary)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(.secondary.opacity(0.2))
.clipShape(Capsule())
}
Spacer()
}
} else {
Color.clear.frame(width: 40, height: 40)
}
}
.padding()
.background(.ultraThinMaterial)
// Players Grid
GeometryReader { geometry in
let columns = [GridItem(.adaptive(minimum: 320), spacing: 24)]
ScrollView {
.padding(.horizontal, 24)
.padding(.top, 16)
if gameState.stopped {
Text("Game Stopped")
.font(.headline)
.foregroundStyle(.red)
.padding(.horizontal, 24)
} else if let winner = gameState.winner {
Text("Winner: \(winner.name)")
.font(.headline)
.foregroundStyle(.green)
.padding(.horizontal, 24)
}
LazyVGrid(columns: columns, spacing: 24) {
ForEach(gameState.players) { player in
PlayerCell(
@@ -106,10 +97,33 @@ struct GameView: View {
.onDrop(of: [.text], delegate: PlayerDropDelegate(item: player, items: $gameState.players, draggedItem: $draggedPlayer))
}
}
.padding(24)
.padding(.horizontal, 24)
.padding(.bottom, 24)
}
}
}
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button(action: { gameManager.activeMatch = nil }) {
Image(systemName: "xmark")
}
}
ToolbarItem(placement: .topBarTrailing) {
if !gameState.stopped && gameState.winner == nil {
Button(action: { showingStopConfirmation = true }) {
Image(systemName: "stop.fill")
}
}
}
}
}
.onAppear {
updateElapsedTime()
}
.onReceive(Timer.publish(every: 1, on: .main, in: .common).autoconnect()) { _ in
updateElapsedTime()
}
.alert("Stop Game?", isPresented: $showingStopConfirmation) {
Button("Cancel", role: .cancel) { }
@@ -138,6 +152,25 @@ struct GameView: View {
}
}
private func updateElapsedTime() {
if !gameState.stopped && gameState.winner == nil {
elapsedTime = Date().timeIntervalSince(match.startedAt)
} else {
elapsedTime = match.lastUpdated.timeIntervalSince(match.startedAt)
}
}
private func timeString(from timeInterval: TimeInterval) -> String {
let hours = Int(timeInterval) / 3600
let minutes = Int(timeInterval) / 60 % 60
let seconds = Int(timeInterval) % 60
if hours > 0 {
return String(format: "%02i:%02i:%02i", hours, minutes, seconds)
} else {
return String(format: "%02i:%02i", minutes, seconds)
}
}
private func updatePlayer(player: PlayerState) {
if gameState.stopped || gameState.winner != nil { return }
@@ -310,6 +343,12 @@ struct CommanderDamageView: View {
let gameState: GameState
let onUpdate: (PlayerState) -> Void
init(targetPlayer: PlayerState, gameState: GameState, onUpdate: @escaping (PlayerState) -> Void) {
self.targetPlayer = targetPlayer
self.gameState = gameState
self.onUpdate = onUpdate
}
var body: some View {
NavigationStack {
List {
@@ -324,6 +363,7 @@ struct CommanderDamageView: View {
color: .secondary,
action: { adjustCommanderDamage(attackerId: attacker.id, by: -1) }
)
.buttonStyle(.borderless)
Text("\(targetPlayer.commanderDamages[attacker.id] ?? 0)")
.font(.title.bold())
@@ -335,6 +375,7 @@ struct CommanderDamageView: View {
color: .primary,
action: { adjustCommanderDamage(attackerId: attacker.id, by: 1) }
)
.buttonStyle(.borderless)
}
}
.padding(.vertical, 8)
@@ -352,14 +393,28 @@ struct CommanderDamageView: View {
private func adjustCommanderDamage(attackerId: Int, by amount: Int) {
Haptics.play(.light)
var newPlayer = targetPlayer
var damages = newPlayer.commanderDamages
let current = damages[attackerId] ?? 0
damages[attackerId] = max(0, current + amount)
newPlayer.commanderDamages = damages
onUpdate(newPlayer)
let wasEliminated = targetPlayer.isEliminated
if newPlayer.isEliminated && !targetPlayer.isEliminated {
// Get current damage value
let current = targetPlayer.commanderDamages[attackerId] ?? 0
let newDamage = max(0, current + amount)
// Only proceed if there's an actual change
guard newDamage != current else { return }
// Update commander damages
var updatedPlayer = targetPlayer
var damages = updatedPlayer.commanderDamages
damages[attackerId] = newDamage
updatedPlayer.commanderDamages = damages
// Adjust life total by the damage change
updatedPlayer.life -= amount
// Notify parent to update
onUpdate(updatedPlayer)
if updatedPlayer.isEliminated && !wasEliminated {
Haptics.notification(.error)
}
}
@@ -390,4 +445,4 @@ struct PlayerDropDelegate: DropDelegate {
func dropUpdated(info: DropInfo) -> DropProposal? {
return DropProposal(operation: .move)
}
}
}

BIN
logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB