diff --git a/DieHammer.xcodeproj/project.pbxproj b/DieHammer.xcodeproj/project.pbxproj index c889815..3744413 100644 --- a/DieHammer.xcodeproj/project.pbxproj +++ b/DieHammer.xcodeproj/project.pbxproj @@ -7,6 +7,16 @@ objects = { /* Begin PBXBuildFile section */ + 7619A1543055ABA300E6F277 /* RerollPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7619A1533055ABA300E6F277 /* RerollPickerView.swift */; }; + 7619A1563055ABAF00E6F277 /* HitCalculatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7619A1553055ABAF00E6F277 /* HitCalculatorView.swift */; }; + 7619A1583055ABB400E6F277 /* WoundCalculatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7619A1573055ABB400E6F277 /* WoundCalculatorView.swift */; }; + 7619A15A3055ABB600E6F277 /* ArmourCalculatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7619A1593055ABB600E6F277 /* ArmourCalculatorView.swift */; }; + 7619A15C3055AF2400E6F277 /* StatePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7619A15B3055AF2400E6F277 /* StatePickerView.swift */; }; + 7619A15E3055AF2A00E6F277 /* RollSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7619A15D3055AF2A00E6F277 /* RollSummaryView.swift */; }; + 7619A1603055AF3100E6F277 /* DieView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7619A15F3055AF3100E6F277 /* DieView.swift */; }; + 7619A1623055AF3300E6F277 /* DiePoolView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7619A1613055AF3300E6F277 /* DiePoolView.swift */; }; + 7619A1643055AF3900E6F277 /* InfoAreaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7619A1633055AF3900E6F277 /* InfoAreaView.swift */; }; + 7619A1663055AF3D00E6F277 /* ButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7619A1653055AF3D00E6F277 /* ButtonView.swift */; }; 7692E8432BD55CDF005BDD76 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 7692E8422BD55CDE005BDD76 /* PrivacyInfo.xcprivacy */; }; 76AB19823050A4B500BF7B9D /* DieHammerEngine in Frameworks */ = {isa = PBXBuildFile; productRef = 76AB19813050A4B500BF7B9D /* DieHammerEngine */; }; 980EF454285E2BAD002FE8A0 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 980EF453285E2BAD002FE8A0 /* MainView.swift */; }; @@ -45,6 +55,16 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 7619A1533055ABA300E6F277 /* RerollPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RerollPickerView.swift; sourceTree = ""; }; + 7619A1553055ABAF00E6F277 /* HitCalculatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HitCalculatorView.swift; sourceTree = ""; }; + 7619A1573055ABB400E6F277 /* WoundCalculatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WoundCalculatorView.swift; sourceTree = ""; }; + 7619A1593055ABB600E6F277 /* ArmourCalculatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArmourCalculatorView.swift; sourceTree = ""; }; + 7619A15B3055AF2400E6F277 /* StatePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatePickerView.swift; sourceTree = ""; }; + 7619A15D3055AF2A00E6F277 /* RollSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RollSummaryView.swift; sourceTree = ""; }; + 7619A15F3055AF3100E6F277 /* DieView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DieView.swift; sourceTree = ""; }; + 7619A1613055AF3300E6F277 /* DiePoolView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiePoolView.swift; sourceTree = ""; }; + 7619A1633055AF3900E6F277 /* InfoAreaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoAreaView.swift; sourceTree = ""; }; + 7619A1653055AF3D00E6F277 /* ButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonView.swift; sourceTree = ""; }; 768C2F3A304F04E6002FEA9A /* Goals.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = Goals.md; sourceTree = ""; }; 7692E8422BD55CDE005BDD76 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 980EF453285E2BAD002FE8A0 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; @@ -95,6 +115,23 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 7619A1523055AA4500E6F277 /* Elements */ = { + isa = PBXGroup; + children = ( + 7619A1533055ABA300E6F277 /* RerollPickerView.swift */, + 7619A1553055ABAF00E6F277 /* HitCalculatorView.swift */, + 7619A1573055ABB400E6F277 /* WoundCalculatorView.swift */, + 7619A1593055ABB600E6F277 /* ArmourCalculatorView.swift */, + 7619A15B3055AF2400E6F277 /* StatePickerView.swift */, + 7619A15D3055AF2A00E6F277 /* RollSummaryView.swift */, + 7619A15F3055AF3100E6F277 /* DieView.swift */, + 7619A1613055AF3300E6F277 /* DiePoolView.swift */, + 7619A1633055AF3900E6F277 /* InfoAreaView.swift */, + 7619A1653055AF3D00E6F277 /* ButtonView.swift */, + ); + path = Elements; + sourceTree = ""; + }; 76AB19803050A4B500BF7B9D /* Frameworks */ = { isa = PBXGroup; children = ( @@ -180,6 +217,7 @@ 981AD0EC27C3356D005A6492 /* Views */ = { isa = PBXGroup; children = ( + 7619A1523055AA4500E6F277 /* Elements */, 98D0CD192856E3850047DC1B /* Modals */, 988B1518285566AD0082C2C5 /* OnboardingView.swift */, 981AD0BF27C30E31005A6492 /* ContentView.swift */, @@ -341,16 +379,26 @@ buildActionMask = 2147483647; files = ( 98E534D42856540A00D5ABB4 /* ViewUtils.swift in Sources */, + 7619A1643055AF3900E6F277 /* InfoAreaView.swift in Sources */, + 7619A15A3055ABB600E6F277 /* ArmourCalculatorView.swift in Sources */, 981AD0C027C30E31005A6492 /* ContentView.swift in Sources */, 988B1519285566AD0082C2C5 /* OnboardingView.swift in Sources */, 981AD0E927C31208005A6492 /* Dice.swift in Sources */, + 7619A1603055AF3100E6F277 /* DieView.swift in Sources */, + 7619A1583055ABB400E6F277 /* WoundCalculatorView.swift in Sources */, 98F001D5284F86F1004F5140 /* AddToPool.swift in Sources */, 988B150F2854E9170082C2C5 /* HistoryView.swift in Sources */, + 7619A1543055ABA300E6F277 /* RerollPickerView.swift in Sources */, 981AD0BE27C30E31005A6492 /* DieHammerApp.swift in Sources */, + 7619A15E3055AF2A00E6F277 /* RollSummaryView.swift in Sources */, + 7619A1623055AF3300E6F277 /* DiePoolView.swift in Sources */, + 7619A15C3055AF2400E6F277 /* StatePickerView.swift in Sources */, 980EF454285E2BAD002FE8A0 /* MainView.swift in Sources */, + 7619A1563055ABAF00E6F277 /* HitCalculatorView.swift in Sources */, 98325A2E2839BE4B0006DF4B /* RollState.swift in Sources */, 988B1517285565610082C2C5 /* Onboarding.swift in Sources */, 98745CA62870CC670031166A /* SettingsView.swift in Sources */, + 7619A1663055AF3D00E6F277 /* ButtonView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -528,7 +576,7 @@ INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait; - IPHONEOS_DEPLOYMENT_TARGET = "$(RECOMMENDED_IPHONEOS_DEPLOYMENT_TARGET)"; + IPHONEOS_DEPLOYMENT_TARGET = 26.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -560,7 +608,7 @@ INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait; - IPHONEOS_DEPLOYMENT_TARGET = "$(RECOMMENDED_IPHONEOS_DEPLOYMENT_TARGET)"; + IPHONEOS_DEPLOYMENT_TARGET = 26.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/DieHammer/Model/RollState.swift b/DieHammer/Model/RollState.swift index 6887cb0..1dcae81 100644 --- a/DieHammer/Model/RollState.swift +++ b/DieHammer/Model/RollState.swift @@ -186,7 +186,7 @@ class Roll: ObservableObject { case .wound: return wound case .armour: - return save + return save + mortalWoundsTotal } } } @@ -381,7 +381,7 @@ class Roll: ObservableObject { saveHasRerolled = false preRerollDice = nil case .armour: - self.total = save + self.total = save + mortalWoundsTotal self.state = .hit hitHasRerolled = false preRerollDice = nil diff --git a/DieHammer/Utilities/ViewUtils.swift b/DieHammer/Utilities/ViewUtils.swift index 09ada7d..ebd3d6f 100644 --- a/DieHammer/Utilities/ViewUtils.swift +++ b/DieHammer/Utilities/ViewUtils.swift @@ -37,3 +37,42 @@ extension View { self.disabled(condition).opacity(condition ? 0.4 : 1.0) } } + +struct ToggleChip: View { + let label: String + @Binding var isOn: Bool + + var body: some View { + Button(action: { isOn.toggle() }) { + Text(label) + .fontWeight(isOn ? .semibold : .regular) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .foregroundColor(isOn ? Color(UIColor.systemBackground) : .primary) + .background(isOn ? Color.primary : Color.clear) + .clipShape(Capsule()) + .overlay(Capsule().stroke(Color.primary, lineWidth: 1)) + } + } +} + +struct StatStepper: View { + let top: String + let value: String + let bottom: String + let increment: () -> Void + let decrement: () -> Void + + var body: some View { + VStack { + Text(top) + Group { + Text("▲").gesture(TapGesture().onEnded { increment() }) + Text(value) + Text("▼").gesture(TapGesture().onEnded { decrement() }) + } + .font(.system(size: 30)) + Text(bottom) + } + } +} diff --git a/DieHammer/Views/Elements/ArmourCalculatorView.swift b/DieHammer/Views/Elements/ArmourCalculatorView.swift new file mode 100644 index 0000000..34a4537 --- /dev/null +++ b/DieHammer/Views/Elements/ArmourCalculatorView.swift @@ -0,0 +1,39 @@ +// +// ArmourCalculatorView.swift +// Bunch O' Dice +// + +import SwiftUI + +struct ArmourCalculatorView: View { + @EnvironmentObject var current: Roll + + var body: some View { + VStack { + HStack(alignment: .top) { + Spacer() + StatStepper( + top: "Armour", + value: (current.armourPenetration > 0 ? "+" : "") + String(current.armourPenetration), + bottom: "Penetration", + increment: { current.addPenetration() }, + decrement: { current.losePenetration() } + ) + Spacer() + ToggleChip(label: "Invulnerable", isOn: $current.invulnerable) + Spacer() + } + RerollPickerView(mode: $current.saveRerollMode) + } + .padding() + } +} + +#Preview { + ArmourCalculatorView() + .environmentObject({ + let r = Roll() + r.state = .armour + return r + }()) +} diff --git a/DieHammer/Views/Elements/ButtonView.swift b/DieHammer/Views/Elements/ButtonView.swift new file mode 100644 index 0000000..e07edb3 --- /dev/null +++ b/DieHammer/Views/Elements/ButtonView.swift @@ -0,0 +1,41 @@ +// +// ButtonView.swift +// Bunch O' Dice +// + +import SwiftUI + +struct ButtonView: View { + let color: CGColor + let label: String + let value: Int + + var body: some View { + let frameSize = 120.0 + let fontSize = 35.0 + let blurSize = 5.0 + + ZStack { + Circle() + .fill(Color(color)) + .frame(width: frameSize, height: frameSize) + .blur(radius: blurSize) + VStack { + Text(String(value)) + .fontWeight(.black) + Text(label) + .font(.system(size: 20)) + } + } + .foregroundColor(.primary) + .font(.system(size: fontSize)) + } +} + +#Preview { + ButtonView( + color: CGColor(red: 1.0, green: 0.3, blue: 0.3, alpha: 1.0), + label: "Hits", + value: 12 + ) +} diff --git a/DieHammer/Views/Elements/DiePoolView.swift b/DieHammer/Views/Elements/DiePoolView.swift new file mode 100644 index 0000000..2b60d24 --- /dev/null +++ b/DieHammer/Views/Elements/DiePoolView.swift @@ -0,0 +1,21 @@ +// +// DiePoolView.swift +// Bunch O' Dice +// + +import SwiftUI + +struct DiePoolView: View { + var body: some View { + HStack(spacing: 0) { + ForEach(Dice.DiceType.allCases, id: \.rawValue) { i in + DieView(whichDie: i) + } + } + } +} + +#Preview { + DiePoolView() + .environmentObject(Roll()) +} diff --git a/DieHammer/Views/Elements/DieView.swift b/DieHammer/Views/Elements/DieView.swift new file mode 100644 index 0000000..84a56e1 --- /dev/null +++ b/DieHammer/Views/Elements/DieView.swift @@ -0,0 +1,43 @@ +// +// DieView.swift +// Bunch O' Dice +// + +import SwiftUI + +struct DieView: View { + let whichDie: Dice.DiceType + @EnvironmentObject var current: Roll + + var body: some View { + let size = 22.0 + let displayColorHit = (whichDie.rawValue >= current.cutoff.rawValue) ? Color.red : Color.primary + let displayColorArmor = (whichDie.rawValue <= current.cutoff.rawValue) ? Color.red : Color.primary + let displayColorArmorText = (whichDie.rawValue < current.cutoff.rawValue) ? Color.red : Color.primary + + VStack(spacing: 2) { + Text(String(current.dice.value(which: whichDie))) + .foregroundColor(current.state == .armour ? displayColorArmorText : displayColorHit) + .frame(minWidth: 30, alignment: .center) + Image(systemName: "die.face." + String(whichDie.rawValue) + ".fill") + .foregroundColor(current.state == .armour ? displayColorArmor : displayColorHit) + } + .font(.system(size: size)) + .padding(4) + .frame(maxWidth: .infinity) + .gesture(TapGesture().onEnded { + if current.state == .hit { + if whichDie.rawValue != 1 { + current.hitCutoff = whichDie + } + } else if current.state == .armour { + current.armourSave = Int(whichDie.rawValue) + } + }) + } +} + +#Preview { + DieView(whichDie: .four) + .environmentObject(Roll()) +} diff --git a/DieHammer/Views/Elements/HitCalculatorView.swift b/DieHammer/Views/Elements/HitCalculatorView.swift new file mode 100644 index 0000000..9064097 --- /dev/null +++ b/DieHammer/Views/Elements/HitCalculatorView.swift @@ -0,0 +1,97 @@ +// +// HitCalculatorView.swift +// Bunch O' Dice +// + +import SwiftUI + +struct HitCalculatorView: View { + @EnvironmentObject var current: Roll + private let controlSize = 30.0 + + var body: some View { + VStack { + HStack(alignment: .top) { + Spacer() + StatStepper( + top: "Hit", + value: (current.modifier.rawValue > 0 ? "+" : "") + String(current.modifier.rawValue), + bottom: "Modifier", + increment: { current.modifier = current.modifier.add() }, + decrement: { current.modifier = current.modifier.remove() } + ) + .disabledWithOpacity(current.torrent || current.psychic) + Spacer() + VStack { + Text("Blow Up") + VStack { + Button(action: { current.applyBlowup(face: .six) }) { + Image(systemName: "die.face.6") + .foregroundColor(.primary) + } + Button(action: { current.applyBlowup(face: .five) }) { + Image(systemName: "die.face.5") + .foregroundColor(.primary) + } + } + .padding() + .font(.system(size: controlSize)) + } + .disabledWithOpacity(current.blowupUsed || current.torrent) + Spacer() + } + RerollPickerView(mode: $current.hitRerollMode) + .disabledWithOpacity(current.torrent) + HStack(spacing: 6) { + ToggleChip(label: "Torrent", isOn: $current.torrent) + ToggleChip(label: "Psychic", isOn: $current.psychic) + .disabledWithOpacity(current.torrent) + ToggleChip(label: "Lethal Hits", isOn: $current.lethalHits) + } + .padding(.horizontal) + VStack(spacing: 8) { + HStack { + ToggleChip(label: "Sust. Hits", isOn: $current.sustainedHits) + Spacer() + if current.sustainedHits { + Text("X:") + Text("▲") + .gesture(TapGesture().onEnded { + if current.sustainedHitsX < 6 { current.sustainedHitsX += 1 } + }) + Text(String(current.sustainedHitsX)) + .frame(minWidth: 30, alignment: .center) + Text("▼") + .gesture(TapGesture().onEnded { + if current.sustainedHitsX > 1 { current.sustainedHitsX -= 1 } + }) + } + } + HStack { + ToggleChip(label: "Anti-X", isOn: $current.antiX) + Spacer() + if current.antiX { + Text("▲") + .gesture(TapGesture().onEnded { + if current.antiXThreshold < 6 { current.antiXThreshold += 1 } + }) + Text("\(current.antiXThreshold)+") + .frame(minWidth: 34, alignment: .center) + Text("▼") + .gesture(TapGesture().onEnded { + if current.antiXThreshold > 2 { current.antiXThreshold -= 1 } + }) + } + } + } + .font(.system(size: 18)) + .padding(.horizontal) + } + .padding() + } +} + +#Preview { + HitCalculatorView() + .environmentObject(Roll()) +} diff --git a/DieHammer/Views/Elements/InfoAreaView.swift b/DieHammer/Views/Elements/InfoAreaView.swift new file mode 100644 index 0000000..71daeec --- /dev/null +++ b/DieHammer/Views/Elements/InfoAreaView.swift @@ -0,0 +1,49 @@ +// +// InfoAreaView.swift +// Bunch O' Dice +// + +import SwiftUI + +struct InfoAreaView: View { + @EnvironmentObject var current: Roll + + var body: some View { + VStack(alignment: .leading, spacing: 6) { + switch current.state { + case .hit: + if current.lethalHits || current.sustainedHits { + Text("Crits (6s): \(current.critHitCount)") + .fontWeight(.semibold) + .foregroundColor(.orange) + } + if current.antiX { + Text("Anti-X (\(current.antiXThreshold)+): \(current.antiXCount)") + .fontWeight(.semibold) + .foregroundColor(.orange) + } + case .wound: + if current.autoWoundsCarried > 0 { + Text("Auto-wounds carried: \(current.autoWoundsCarried) (skip wound roll)") + .fontWeight(.semibold) + .foregroundColor(.orange) + } + if current.devastatingWounds { + Text("Mortals (6s): \(current.critWoundCount)") + .fontWeight(.semibold) + .foregroundColor(.orange) + } + case .armour: + EmptyView() + } + } + .font(.system(size: 18)) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal) + } +} + +#Preview { + InfoAreaView() + .environmentObject(Roll()) +} diff --git a/DieHammer/Views/Elements/RerollPickerView.swift b/DieHammer/Views/Elements/RerollPickerView.swift new file mode 100644 index 0000000..44f5c2b --- /dev/null +++ b/DieHammer/Views/Elements/RerollPickerView.swift @@ -0,0 +1,53 @@ +// +// RerollPickerView.swift +// Bunch O' Dice +// + +import SwiftUI + +@available(iOS 26.0, *) +struct RerollPickerView: View { + @EnvironmentObject var current: Roll + @Binding var mode: RerollMode + + var body: some View { + let actionBinding = Binding( + get: { mode }, + set: { newValue in + mode = newValue + if newValue != .none { + current.applyReroll() + } else { + current.undoReroll() + } + } + ) + HStack(spacing: 8) { + Text("Reroll") + HStack(spacing: 6) { + ForEach(RerollMode.allCases, id: \.self) { m in + let isSelected = mode == m + Button(action: { actionBinding.wrappedValue = m }) { + Text(m.label) + .fontWeight(isSelected ? .semibold : .regular) + .padding(.horizontal, 12) + .padding(.vertical, 6) + .foregroundColor(isSelected ? Color(UIColor.systemBackground) : .primary) + .background(isSelected ? Color.green : Color.gray) + .clipShape(Capsule()) + .overlay(Capsule().stroke(Color.primary, lineWidth: 1)) + .glassEffect() + } + } + } + } + .padding(.horizontal) + } +} + +#Preview { + if #available(iOS 26.0, *) { + RerollPickerView(mode: .constant(.none)) + .environmentObject(Roll()) + } +} diff --git a/DieHammer/Views/Elements/RollSummaryView.swift b/DieHammer/Views/Elements/RollSummaryView.swift new file mode 100644 index 0000000..5f1f607 --- /dev/null +++ b/DieHammer/Views/Elements/RollSummaryView.swift @@ -0,0 +1,62 @@ +// +// RollSummaryView.swift +// Bunch O' Dice +// + +import SwiftUI + +struct RollSummaryView: View { + @EnvironmentObject var current: Roll + @State private var showFillView = false + + var body: some View { + HStack(spacing: 12) { + Button(action: { showFillView.toggle() }) { + VStack(spacing: 4) { + Text("Dice Pool") + .font(.caption) + Text(String(current.total)) + .font(.system(size: 28, weight: .black)) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 12) + .background(Color.white) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .foregroundColor(.black) + } + .sheet(isPresented: $showFillView, + onDismiss: { current.resetPool() }) { + VStack { + AddToPool() + Button("Accept", action: { showFillView.toggle() }) + .padding(20) + } + } + Button(action: current.incrementState) { + VStack(spacing: 4) { + Text(current.state.text()) + .font(.caption) + Text(String(current.result)) + .font(.system(size: 28, weight: .black)) + if current.state == .armour && current.mortalWoundsTotal > 0 { + Text("mortal: \(current.mortalWoundsTotal)") + .font(.caption) + } + } + .frame(maxWidth: .infinity) + .padding(.vertical, 12) + .background(Color.white) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .foregroundColor(.black) + } + } + .padding() + .background(Color.black) + .clipShape(RoundedRectangle(cornerRadius: 14)) + } +} + +#Preview { + RollSummaryView() + .environmentObject(Roll()) +} diff --git a/DieHammer/Views/Elements/StatePickerView.swift b/DieHammer/Views/Elements/StatePickerView.swift new file mode 100644 index 0000000..592dd26 --- /dev/null +++ b/DieHammer/Views/Elements/StatePickerView.swift @@ -0,0 +1,50 @@ +// +// StatePickerView.swift +// Bunch O' Dice +// + +import SwiftUI + +struct StatePickerView: View { + @EnvironmentObject var current: Roll + private let controlSize = 30.0 + + var body: some View { + HStack { + Button(action: { current.state = .hit }) { + ZStack { + if current.state == .hit { circle() } + Text(RollState.hit.print()) + } + } + .glassEffect() + Button(action: { current.state = .wound }) { + ZStack { + if current.state == .wound { circle() } + Text(RollState.wound.print()) + } + } + .glassEffect() + Button(action: { current.state = .armour }) { + ZStack { + if current.state == .armour { circle() } + Text(RollState.armour.print()) + } + } + .glassEffect() + } + .font(.system(size: 60)) + } + + @ViewBuilder private func circle() -> some View { + Circle() + .fill(Color.gray) + .blur(radius: 2.0) + .frame(width: controlSize * 2.5, height: controlSize * 2.5, alignment: .center) + } +} + +#Preview { + StatePickerView() + .environmentObject(Roll()) +} diff --git a/DieHammer/Views/Elements/WoundCalculatorView.swift b/DieHammer/Views/Elements/WoundCalculatorView.swift new file mode 100644 index 0000000..3a8010b --- /dev/null +++ b/DieHammer/Views/Elements/WoundCalculatorView.swift @@ -0,0 +1,58 @@ +// +// WoundCalculatorView.swift +// Bunch O' Dice +// + +import SwiftUI + +struct WoundCalculatorView: View { + @EnvironmentObject var current: Roll + + var body: some View { + VStack { + HStack(alignment: .top) { + Spacer() + StatStepper( + top: "Attack", + value: String(current.strength), + bottom: "Strength", + increment: { current.addStrength() }, + decrement: { current.loseStrength() } + ) + Spacer() + StatStepper( + top: "Toughness", + value: String(current.toughness), + bottom: " ", + increment: { current.addToughness() }, + decrement: { current.loseToughness() } + ) + Spacer() + StatStepper( + top: "Wound", + value: (current.woundModifier.rawValue > 0 ? "+" : "") + String(current.woundModifier.rawValue), + bottom: "Modifier", + increment: { current.woundModifier = current.woundModifier.add() }, + decrement: { current.woundModifier = current.woundModifier.remove() } + ) + Spacer() + } + RerollPickerView(mode: $current.woundRerollMode) + HStack { + ToggleChip(label: "Devastating Wounds", isOn: $current.devastatingWounds) + } + .font(.system(size: 18)) + .padding(.horizontal) + } + .padding() + } +} + +#Preview { + WoundCalculatorView() + .environmentObject({ + let r = Roll() + r.state = .wound + return r + }()) +} diff --git a/DieHammer/Views/MainView.swift b/DieHammer/Views/MainView.swift index 8ce9bc5..625bddd 100644 --- a/DieHammer/Views/MainView.swift +++ b/DieHammer/Views/MainView.swift @@ -11,439 +11,61 @@ struct MainView: View { enum MenuView: Int { case history, add, settings - + func icon() -> Image { switch self { - case .history: - return(Image(systemName: "folder")) - case .settings: - return(Image(systemName: "square.and.pencil")) - case .add: - return(Image(systemName: "folder.badge.plus")) + case .history: return Image(systemName: "folder") + case .settings: return Image(systemName: "square.and.pencil") + case .add: return Image(systemName: "folder.badge.plus") } } } - - @ViewBuilder private func myCircle() -> some View { - Circle() - .fill(Color.gray) - .blur(radius: 2.0) - .frame(width: controlSize*2.5, height: controlSize*2.5, alignment: .center) - } - - @ViewBuilder private func stateView() -> some View { - switch current.state { - case .wound: - woundCalculator(controlSize: controlSize) - case .armour: - armourCalculator(controlSize: controlSize) - default: - hitCalculator(controlSize: controlSize) - } - } - - @ViewBuilder private func sheetContent() -> some View { - switch sheet.state { - case .settings: - SettingsView(isShowing: $sheet.isShowing) - case .history: - HistoryView(isShowing: $sheet.isShowing) - default: - EmptyView() - } - } - - @ViewBuilder private func rerollPicker(mode: Binding) -> some View { - let actionBinding = Binding( - get: { mode.wrappedValue }, - set: { newValue in - mode.wrappedValue = newValue - if newValue != .none { - current.applyReroll() - } else { - current.undoReroll() - } - } - ) - HStack { - Text("Reroll") - Picker("", selection: actionBinding) { - ForEach(RerollMode.allCases, id: \.self) { m in - Text(m.label).tag(m) - } - } - .pickerStyle(.segmented) - } - .padding(.horizontal) - } - - @ViewBuilder private func statStepper( - top: String, - value: String, - bottom: String, - increment: @escaping () -> Void, - decrement: @escaping () -> Void - ) -> some View { - VStack { - Text(top) - Group { - Text("▲").gesture(TapGesture().onEnded { increment() }) - Text(value) - Text("▼").gesture(TapGesture().onEnded { decrement() }) - } - .font(.system(size: controlSize)) - Text(bottom) - } - } - @ViewBuilder func buttons(color: CGColor, label: String, value: Int) -> some View { - let frameSize = 120.0 - let fontSize = 35.0 - let blurSize = 5.0 - - ZStack { - Circle() - .fill(Color(color)) - .frame(width: frameSize, height: frameSize) - .blur(radius: blurSize) - VStack{ - Text(String(value)) - .fontWeight(.black) - Text(label) - .font(.system(size: 20)) - } - } - .foregroundColor(.primary) - .font(.system(size: fontSize)) - } - - //MARK: State Views - @ViewBuilder func armourCalculator(controlSize: Double) -> some View { - VStack { - HStack(alignment: .top) { - Spacer() - statStepper( - top: "Armour", - value: (current.armourPenetration > 0 ? "+" : "") + String(current.armourPenetration), - bottom: "Penetration", - increment: { current.addPenetration() }, - decrement: { current.losePenetration() } - ) - Spacer() - VStack { - Text("Invulnerable") - Toggle("", isOn: $current.invulnerable).labelsHidden() - .padding() - } - Spacer() - } - rerollPicker(mode: $current.saveRerollMode) - if current.mortalWoundsTotal > 0 { - HStack { - Text("Mortal wounds: \(current.mortalWoundsTotal) (bypassed saves)") - .fontWeight(.semibold) - .foregroundColor(.orange) - Spacer() - } - .font(.system(size: 18)) - .padding(.horizontal) - } - } - .padding() - } - - @ViewBuilder func woundCalculator(controlSize: Double) -> some View { - VStack { - if current.autoWoundsCarried > 0 { - HStack { - Text("Auto-wounds carried: \(current.autoWoundsCarried) (skip wound roll)") - .fontWeight(.semibold) - .foregroundColor(.orange) - Spacer() - } - .font(.system(size: 18)) - .padding(.horizontal) - } - HStack(alignment: .top) { - Spacer() - statStepper( - top: "Attack", - value: String(current.strength), - bottom: "Strength", - increment: { current.addStrength() }, - decrement: { current.loseStrength() } - ) - Spacer() - statStepper( - top: "Toughness", - value: String(current.toughness), - bottom: " ", - increment: { current.addToughness() }, - decrement: { current.loseToughness() } - ) - Spacer() - statStepper( - top: "Wound", - value: (current.woundModifier.rawValue > 0 ? "+" : "") + String(current.woundModifier.rawValue), - bottom: "Modifier", - increment: { current.woundModifier = current.woundModifier.add() }, - decrement: { current.woundModifier = current.woundModifier.remove() } - ) - Spacer() - } - rerollPicker(mode: $current.woundRerollMode) - VStack(spacing: 8) { - HStack { - Toggle("Devastating Wounds", isOn: $current.devastatingWounds) - } - if current.devastatingWounds { - HStack { - Text("Mortals (6s): \(current.critWoundCount)") - .fontWeight(.semibold) - .foregroundColor(.orange) - Spacer() - } - } - } - .font(.system(size: 18)) - .padding(.horizontal) - } - .padding() - } - - @ViewBuilder func hitCalculator(controlSize: Double) -> some View { - VStack { - HStack(alignment: .top) { - Spacer() - statStepper( - top: "Hit", - value: (current.modifier.rawValue > 0 ? "+" : "") + String(current.modifier.rawValue), - bottom: "Modifier", - increment: { current.modifier = current.modifier.add() }, - decrement: { current.modifier = current.modifier.remove() } - ) - .disabledWithOpacity(current.torrent || current.psychic) - Spacer() - VStack { - Text("Blow Up") - VStack { - Button(action: { current.applyBlowup(face: .six) }) { - Image(systemName: "die.face.6") - .foregroundColor(.primary) - } - Button(action: { current.applyBlowup(face: .five) }) { - Image(systemName: "die.face.5") - .foregroundColor(.primary) - } - } - .padding() - .font(.system(size: controlSize)) - } - .disabledWithOpacity(current.blowupUsed || current.torrent) - Spacer() - } - rerollPicker(mode: $current.hitRerollMode) - .disabledWithOpacity(current.torrent) - HStack { - Spacer() - VStack { - Text("Torrent") - Toggle("", isOn: $current.torrent).labelsHidden() - } - Spacer() - VStack { - Text("Psychic") - Toggle("", isOn: $current.psychic).labelsHidden() - .disabledWithOpacity(current.torrent) - } - Spacer() - VStack { - Text("Lethal Hits") - Toggle("", isOn: $current.lethalHits).labelsHidden() - } - Spacer() - } - .padding(.horizontal) - VStack(spacing: 8) { - HStack { - Toggle("Sust. Hits", isOn: $current.sustainedHits) - .fixedSize() - Spacer() - if current.sustainedHits { - Text("X:") - Text("▲") - .gesture(TapGesture().onEnded { - if current.sustainedHitsX < 6 { current.sustainedHitsX += 1 } - }) - Text(String(current.sustainedHitsX)) - .frame(minWidth: 30, alignment: .center) - Text("▼") - .gesture(TapGesture().onEnded { - if current.sustainedHitsX > 1 { current.sustainedHitsX -= 1 } - }) - } - } - HStack { - Toggle("Anti-X", isOn: $current.antiX) - .fixedSize() - Spacer() - if current.antiX { - Text("▲") - .gesture(TapGesture().onEnded { - if current.antiXThreshold < 6 { current.antiXThreshold += 1 } - }) - Text("\(current.antiXThreshold)+") - .frame(minWidth: 34, alignment: .center) - Text("▼") - .gesture(TapGesture().onEnded { - if current.antiXThreshold > 2 { current.antiXThreshold -= 1 } - }) - } - } - if current.lethalHits || current.sustainedHits || current.antiX { - HStack { - if current.lethalHits || current.sustainedHits { - Text("Crits (6s): \(current.critHitCount)") - .fontWeight(.semibold) - } - Spacer() - if current.antiX { - Text("Anti-X (\(current.antiXThreshold)+): \(current.antiXCount)") - .fontWeight(.semibold) - } - } - .foregroundColor(.orange) - } - } - .font(.system(size: 18)) - .padding(.horizontal) - } - .padding() - } - - //MARK: die views - @ViewBuilder func die(whichDie: Dice.DiceType) -> some View { - let size = 22.0 - let displayColorHit = (whichDie.rawValue >= current.cutoff.rawValue) ? Color.red : Color.primary - let displayColorArmor = (whichDie.rawValue <= current.cutoff.rawValue) ? Color.red : Color.primary - let displayColorArmorText = (whichDie.rawValue < current.cutoff.rawValue) ? Color.red : Color.primary - - VStack(spacing: 2) { - Text(String(current.dice.value(which: whichDie))) - .foregroundColor(current.state == .armour ? - displayColorArmorText : displayColorHit) - .frame(minWidth: 30, alignment: .center) - Image(systemName: "die.face." + String(whichDie.rawValue) + ".fill") - .foregroundColor(current.state == .armour ? - displayColorArmor : displayColorHit) - } - .font(.system(size: size)) - .padding(4) - .frame(maxWidth: .infinity) - .gesture(TapGesture().onEnded { - if current.state == .hit { - if whichDie.rawValue != 1 { - current.hitCutoff = whichDie } - } else if current.state == .armour { - current.armourSave = Int(whichDie.rawValue) - } - }) - } - - @ViewBuilder func diePool() -> some View { - HStack(spacing: 0) { - ForEach(Dice.DiceType.allCases, id: \.rawValue) { i in - die(whichDie: i) - } - } - } - - //MARK: Var declarations @EnvironmentObject var current: Roll @ObservedObject var sheet = SettingsSheet() - @State var showFillView = false - - let padSize = 60.0 - let controlSize = 30.0 - let red = CGColor(red: 1.0, green: 0.3, blue: 0.3, alpha: 1.0) - let blue = CGColor(red: 0.3, green: 0.3, blue: 1.0, alpha: 1.0) - + + private let controlSize = 30.0 + var body: some View { - VStack{ - HStack{ - Button(action: {current.state = .hit}) { - ZStack{ - if current.state == .hit {myCircle()} - Text(RollState.hit.print())}} - Button(action: {current.state = .wound}) { - ZStack{ - if current.state == .wound {myCircle()} - Text(RollState.wound.print())}} - Button(action: {current.state = .armour}) { - ZStack{ - if current.state == .armour {myCircle()} - Text(RollState.armour.print())}} - }.font(.system(size: 60)) + VStack { + StatePickerView() VStack(spacing: 12) { - HStack(spacing: 12) { - Button(action: { showFillView.toggle() }) { - VStack(spacing: 4) { - Text("Dice Pool") - .font(.caption) - Text(String(current.total)) - .font(.system(size: 28, weight: .black)) - } - .frame(maxWidth: .infinity) - .padding(.vertical, 12) - .background(Color.white) - .clipShape(RoundedRectangle(cornerRadius: 10)) - .foregroundColor(.black) - } - .sheet(isPresented: $showFillView, - onDismiss: { current.resetPool() }) { - VStack { - AddToPool() - Button("Accept", action: {showFillView.toggle()}) - .padding(20) - } - } - Button(action: current.incrementState) { - VStack(spacing: 4) { - Text(current.state.text()) - .font(.caption) - Text(String(current.result)) - .font(.system(size: 28, weight: .black)) - } - .frame(maxWidth: .infinity) - .padding(.vertical, 12) - .background(Color.white) - .clipShape(RoundedRectangle(cornerRadius: 10)) - .foregroundColor(.black) - } + RollSummaryView() + DiePoolView() + switch current.state { + case .wound: WoundCalculatorView() + case .armour: ArmourCalculatorView() + default: HitCalculatorView() } - .padding() - .background(Color.black) - .clipShape(RoundedRectangle(cornerRadius: 14)) - diePool() - stateView() + InfoAreaView() Spacer() }.padding() } .toolbar { ToolbarItemGroup(placement: .bottomBar) { HStack { - Button(action: {self.sheet.state = .history}) { - MenuView.history.icon()} - Button(action: {current.history.insert(Event(total: current.result, pool: current.dice, state: current.state), at: 0)}) { - MenuView.add.icon()} - Button(action: {self.sheet.state = .settings}) { - MenuView.settings.icon()} + Button(action: { sheet.state = .history }) { + MenuView.history.icon() + } + Button(action: { current.history.insert(Event(total: current.result, pool: current.dice, state: current.state), at: 0) }) { + MenuView.add.icon() + } + Button(action: { sheet.state = .settings }) { + MenuView.settings.icon() + } } .font(.system(size: controlSize)) .foregroundColor(.primary) } } - .sheet(isPresented: $sheet.isShowing, content: sheetContent)} + .sheet(isPresented: $sheet.isShowing) { + switch sheet.state { + case .settings: SettingsView(isShowing: $sheet.isShowing) + case .history: HistoryView(isShowing: $sheet.isShowing) + default: EmptyView() + } + } + } } #Preview("Hit") {