1.1.0
This commit is contained in:
@@ -411,11 +411,12 @@
|
|||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
|
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 3;
|
CURRENT_PROJECT_VERSION = 4;
|
||||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = MagicCounter;
|
INFOPLIST_KEY_CFBundleDisplayName = MagicCounter;
|
||||||
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games";
|
||||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
@@ -446,11 +447,12 @@
|
|||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
|
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 3;
|
CURRENT_PROJECT_VERSION = 4;
|
||||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = MagicCounter;
|
INFOPLIST_KEY_CFBundleDisplayName = MagicCounter;
|
||||||
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games";
|
||||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
|
|||||||
Binary file not shown.
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UniformTypeIdentifiers
|
import UniformTypeIdentifiers
|
||||||
|
import Combine
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main game screen displaying the grid of players.
|
* Main game screen displaying the grid of players.
|
||||||
@@ -19,75 +20,65 @@ struct GameView: View {
|
|||||||
@State private var showingStopConfirmation = false
|
@State private var showingStopConfirmation = false
|
||||||
@State private var selectedPlayerForCommander: PlayerState?
|
@State private var selectedPlayerForCommander: PlayerState?
|
||||||
@State private var draggedPlayer: PlayerState?
|
@State private var draggedPlayer: PlayerState?
|
||||||
|
@State private var elapsedTime: TimeInterval = 0
|
||||||
|
let match: MatchRecord
|
||||||
|
|
||||||
init(match: MatchRecord) {
|
init(match: MatchRecord) {
|
||||||
|
self.match = match
|
||||||
self._gameState = State(initialValue: match.state)
|
self._gameState = State(initialValue: match.state)
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
NavigationStack {
|
||||||
// Background
|
GeometryReader { geometry in
|
||||||
LinearGradient(colors: [Color.black, Color.indigo.opacity(0.5)], startPoint: .top, endPoint: .bottom)
|
let columns = [GridItem(.adaptive(minimum: 320), spacing: 24)]
|
||||||
.ignoresSafeArea()
|
ScrollView {
|
||||||
|
VStack(alignment: .leading, spacing: 24) {
|
||||||
VStack(spacing: 0) {
|
// Custom Header
|
||||||
// Custom Toolbar
|
HStack(alignment: .center, spacing: 12) {
|
||||||
HStack {
|
Text(match.name)
|
||||||
Button(action: { gameManager.activeMatch = nil }) {
|
.font(.largeTitle.bold())
|
||||||
Image(systemName: "xmark")
|
.lineLimit(1)
|
||||||
.font(.system(size: 16, weight: .bold))
|
.truncationMode(.tail)
|
||||||
.foregroundColor(.white)
|
.foregroundStyle(.white)
|
||||||
.padding(10)
|
|
||||||
.background(.ultraThinMaterial)
|
if !gameState.stopped && gameState.winner == nil {
|
||||||
.clipShape(Circle())
|
Text(timeString(from: elapsedTime))
|
||||||
}
|
.font(.subheadline.bold())
|
||||||
|
.monospacedDigit()
|
||||||
Spacer()
|
.foregroundStyle(.green)
|
||||||
|
.padding(.horizontal, 8)
|
||||||
if gameState.stopped {
|
.padding(.vertical, 4)
|
||||||
Text("Game Stopped")
|
.background(.green.opacity(0.2))
|
||||||
.font(.headline)
|
.clipShape(Capsule())
|
||||||
.foregroundStyle(.red)
|
} else {
|
||||||
.padding(.horizontal, 12)
|
Text(timeString(from: match.lastUpdated.timeIntervalSince(match.startedAt)))
|
||||||
.padding(.vertical, 6)
|
.font(.subheadline.bold())
|
||||||
.background(.ultraThinMaterial)
|
.monospacedDigit()
|
||||||
.cornerRadius(8)
|
.foregroundStyle(.secondary)
|
||||||
} else if let winner = gameState.winner {
|
.padding(.horizontal, 8)
|
||||||
Text("Winner: \(winner.name)")
|
.padding(.vertical, 4)
|
||||||
.font(.headline)
|
.background(.secondary.opacity(0.2))
|
||||||
.foregroundStyle(.green)
|
.clipShape(Capsule())
|
||||||
.padding(.horizontal, 12)
|
}
|
||||||
.padding(.vertical, 6)
|
|
||||||
.background(.ultraThinMaterial)
|
Spacer()
|
||||||
.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())
|
|
||||||
}
|
}
|
||||||
} else {
|
.padding(.horizontal, 24)
|
||||||
Color.clear.frame(width: 40, height: 40)
|
.padding(.top, 16)
|
||||||
}
|
|
||||||
}
|
if gameState.stopped {
|
||||||
.padding()
|
Text("Game Stopped")
|
||||||
.background(.ultraThinMaterial)
|
.font(.headline)
|
||||||
|
.foregroundStyle(.red)
|
||||||
// Players Grid
|
.padding(.horizontal, 24)
|
||||||
GeometryReader { geometry in
|
} else if let winner = gameState.winner {
|
||||||
let columns = [GridItem(.adaptive(minimum: 320), spacing: 24)]
|
Text("Winner: \(winner.name)")
|
||||||
ScrollView {
|
.font(.headline)
|
||||||
|
.foregroundStyle(.green)
|
||||||
|
.padding(.horizontal, 24)
|
||||||
|
}
|
||||||
|
|
||||||
LazyVGrid(columns: columns, spacing: 24) {
|
LazyVGrid(columns: columns, spacing: 24) {
|
||||||
ForEach(gameState.players) { player in
|
ForEach(gameState.players) { player in
|
||||||
PlayerCell(
|
PlayerCell(
|
||||||
@@ -106,10 +97,33 @@ struct GameView: View {
|
|||||||
.onDrop(of: [.text], delegate: PlayerDropDelegate(item: player, items: $gameState.players, draggedItem: $draggedPlayer))
|
.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) {
|
.alert("Stop Game?", isPresented: $showingStopConfirmation) {
|
||||||
Button("Cancel", role: .cancel) { }
|
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) {
|
private func updatePlayer(player: PlayerState) {
|
||||||
if gameState.stopped || gameState.winner != nil { return }
|
if gameState.stopped || gameState.winner != nil { return }
|
||||||
|
|
||||||
@@ -306,12 +339,12 @@ struct PlayerCell: View {
|
|||||||
*/
|
*/
|
||||||
struct CommanderDamageView: View {
|
struct CommanderDamageView: View {
|
||||||
@Environment(\.dismiss) var dismiss
|
@Environment(\.dismiss) var dismiss
|
||||||
@State private var targetPlayer: PlayerState
|
let targetPlayer: PlayerState
|
||||||
let gameState: GameState
|
let gameState: GameState
|
||||||
let onUpdate: (PlayerState) -> Void
|
let onUpdate: (PlayerState) -> Void
|
||||||
|
|
||||||
init(targetPlayer: PlayerState, gameState: GameState, onUpdate: @escaping (PlayerState) -> Void) {
|
init(targetPlayer: PlayerState, gameState: GameState, onUpdate: @escaping (PlayerState) -> Void) {
|
||||||
self._targetPlayer = State(initialValue: targetPlayer)
|
self.targetPlayer = targetPlayer
|
||||||
self.gameState = gameState
|
self.gameState = gameState
|
||||||
self.onUpdate = onUpdate
|
self.onUpdate = onUpdate
|
||||||
}
|
}
|
||||||
@@ -330,6 +363,7 @@ struct CommanderDamageView: View {
|
|||||||
color: .secondary,
|
color: .secondary,
|
||||||
action: { adjustCommanderDamage(attackerId: attacker.id, by: -1) }
|
action: { adjustCommanderDamage(attackerId: attacker.id, by: -1) }
|
||||||
)
|
)
|
||||||
|
.buttonStyle(.borderless)
|
||||||
|
|
||||||
Text("\(targetPlayer.commanderDamages[attacker.id] ?? 0)")
|
Text("\(targetPlayer.commanderDamages[attacker.id] ?? 0)")
|
||||||
.font(.title.bold())
|
.font(.title.bold())
|
||||||
@@ -341,6 +375,7 @@ struct CommanderDamageView: View {
|
|||||||
color: .primary,
|
color: .primary,
|
||||||
action: { adjustCommanderDamage(attackerId: attacker.id, by: 1) }
|
action: { adjustCommanderDamage(attackerId: attacker.id, by: 1) }
|
||||||
)
|
)
|
||||||
|
.buttonStyle(.borderless)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
@@ -359,20 +394,27 @@ struct CommanderDamageView: View {
|
|||||||
private func adjustCommanderDamage(attackerId: Int, by amount: Int) {
|
private func adjustCommanderDamage(attackerId: Int, by amount: Int) {
|
||||||
Haptics.play(.light)
|
Haptics.play(.light)
|
||||||
let wasEliminated = targetPlayer.isEliminated
|
let wasEliminated = targetPlayer.isEliminated
|
||||||
var newPlayer = targetPlayer
|
|
||||||
var damages = newPlayer.commanderDamages
|
|
||||||
let current = damages[attackerId] ?? 0
|
|
||||||
let newDamage = max(0, current + amount)
|
|
||||||
let damageChange = newDamage - current
|
|
||||||
damages[attackerId] = newDamage
|
|
||||||
newPlayer.commanderDamages = damages
|
|
||||||
// Commander damage also reduces life total
|
|
||||||
newPlayer.life -= damageChange
|
|
||||||
// Update local state so the view refreshes
|
|
||||||
targetPlayer = newPlayer
|
|
||||||
onUpdate(newPlayer)
|
|
||||||
|
|
||||||
if newPlayer.isEliminated && !wasEliminated {
|
// 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)
|
Haptics.notification(.error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -403,4 +445,4 @@ struct PlayerDropDelegate: DropDelegate {
|
|||||||
func dropUpdated(info: DropInfo) -> DropProposal? {
|
func dropUpdated(info: DropInfo) -> DropProposal? {
|
||||||
return DropProposal(operation: .move)
|
return DropProposal(operation: .move)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user