refactor into smaller units
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}())
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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<RerollMode>(
|
||||
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())
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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
|
||||
}())
|
||||
}
|
||||
+33
-411
@@ -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<RerollMode>) -> some View {
|
||||
let actionBinding = Binding<RerollMode>(
|
||||
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") {
|
||||
|
||||
Reference in New Issue
Block a user