// // PortraitView.swift // Bunch O' Dice // // Created by Rory Hinnen on 22/6/18. // import SwiftUI 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")) } } } @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) 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(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) } } .padding() .background(Color.black) .clipShape(RoundedRectangle(cornerRadius: 14)) diePool() stateView() 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()} } .font(.system(size: controlSize)) .foregroundColor(.primary) } } .sheet(isPresented: $sheet.isShowing, content: sheetContent)} } #Preview("Hit") { MainView().environmentObject(Roll()) } #Preview("Wound") { MainView().environmentObject({ let r = Roll() r.state = .wound return r }()) } #Preview("Armour") { MainView().environmentObject({ let r = Roll() r.state = .armour return r }()) }