all features implemented
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 547 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "40.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "60.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "58.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "87.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "80.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "120.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "120.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"filename" : "180.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"filename" : "20.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "40.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "29.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "58.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "40.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "80.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "76.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"filename" : "152.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"filename" : "167.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "83.5x83.5"
|
||||
},
|
||||
{
|
||||
"filename" : "1024.png",
|
||||
"idiom" : "ios-marketing",
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "armor.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 45 KiB |
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "five.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "four.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 112 KiB |
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "nine.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 547 KiB |
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "1024.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "wound.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 55 KiB |
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "six.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 42 KiB |
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "three.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 120 KiB |
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "two.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 134 KiB |
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// ContentView.swift
|
||||
// Bunch O' Dice
|
||||
//
|
||||
// Created by Rory Hinnen on 22/2/20.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
@EnvironmentObject var current: Roll
|
||||
@State var preferredColorScheme: ColorScheme? = nil
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
switch current.currentPage {
|
||||
case "OnboardingView":
|
||||
OnboardingView()
|
||||
default:
|
||||
MainView()
|
||||
}
|
||||
}
|
||||
.preferredColorScheme(current.appearance.type)
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContentView()
|
||||
.environmentObject(Roll())
|
||||
.previewInterfaceOrientation(.portrait)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// Bunch_O__DiceApp.swift
|
||||
// Bunch O' Dice
|
||||
//
|
||||
// Created by Rory Hinnen on 22/2/20.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct Bunch_O__DiceApp: App {
|
||||
@StateObject var state = Roll()
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
.environmentObject(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,509 @@
|
||||
//
|
||||
// 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 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()
|
||||
VStack {
|
||||
Text("Armour")
|
||||
Group {
|
||||
Text("▲")
|
||||
.gesture(TapGesture() .onEnded {
|
||||
current.addPenetration()
|
||||
})
|
||||
Text(
|
||||
(current.armourPenetration > 0 ?
|
||||
"+" : "") +
|
||||
String(current.armourPenetration))
|
||||
Text("▼")
|
||||
.gesture(TapGesture() .onEnded {
|
||||
current.losePenetration()
|
||||
})
|
||||
}
|
||||
.font(.system(size: controlSize))
|
||||
Text("Penetration")
|
||||
}
|
||||
Spacer()
|
||||
VStack {
|
||||
Text("Invulnerable")
|
||||
Toggle("", isOn: $current.invulnerable).labelsHidden()
|
||||
.padding()
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
HStack {
|
||||
Text("Reroll")
|
||||
Picker("", selection: $current.saveRerollMode) {
|
||||
ForEach(RerollMode.allCases, id: \.self) { mode in
|
||||
Text(mode.label).tag(mode)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
Button("🎲") {
|
||||
current.applyReroll()
|
||||
}
|
||||
.disabled(current.saveRerollMode == .none || current.saveHasRerolled)
|
||||
.font(.system(size: controlSize))
|
||||
}
|
||||
.padding(.horizontal)
|
||||
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()
|
||||
VStack {
|
||||
Text("Attack")
|
||||
Group {
|
||||
Text("▲")
|
||||
.gesture(TapGesture() .onEnded {
|
||||
current.addStrength()
|
||||
})
|
||||
Text(String(current.strength))
|
||||
Text("▼")
|
||||
.gesture(TapGesture() .onEnded {
|
||||
current.loseStrength()
|
||||
})
|
||||
}.font(.system(size: controlSize))
|
||||
Text("Strength")
|
||||
}
|
||||
Spacer()
|
||||
VStack {
|
||||
Text("Toughness")
|
||||
Group {
|
||||
Text("▲")
|
||||
.gesture(TapGesture() .onEnded {
|
||||
current.addToughness()
|
||||
})
|
||||
Text(String(current.toughness))
|
||||
Text("▼")
|
||||
.gesture(TapGesture() .onEnded {
|
||||
current.loseToughness()
|
||||
})
|
||||
}.font(.system(size: controlSize))
|
||||
Text(" ")
|
||||
}
|
||||
Spacer()
|
||||
VStack {
|
||||
Text("Wound")
|
||||
.multilineTextAlignment(.center)
|
||||
Group {
|
||||
Text("▲")
|
||||
.gesture(TapGesture() .onEnded {
|
||||
current.woundModifier = current.woundModifier.add()
|
||||
})
|
||||
Text(
|
||||
(current.woundModifier.rawValue > 0 ?
|
||||
"+" : "") +
|
||||
String(current.woundModifier.rawValue))
|
||||
Text("▼")
|
||||
.gesture(TapGesture() .onEnded {
|
||||
current.woundModifier = current.woundModifier.remove()
|
||||
})
|
||||
}.font(.system(size: controlSize))
|
||||
Text("Modifier")
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
HStack {
|
||||
Text("Reroll")
|
||||
Picker("", selection: $current.woundRerollMode) {
|
||||
ForEach(RerollMode.allCases, id: \.self) { mode in
|
||||
Text(mode.label).tag(mode)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
Button("🎲") {
|
||||
current.applyReroll()
|
||||
}
|
||||
.disabled(current.woundRerollMode == .none || current.woundHasRerolled)
|
||||
.font(.system(size: controlSize))
|
||||
}
|
||||
.padding(.horizontal)
|
||||
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()
|
||||
VStack {
|
||||
Text("Hit")
|
||||
.multilineTextAlignment(.center)
|
||||
Group {
|
||||
Text("▲")
|
||||
.gesture(TapGesture() .onEnded {
|
||||
current.modifier = current.modifier.add()
|
||||
})
|
||||
Text((current.modifier.rawValue > 0 ? "+" : "") +
|
||||
String(current.modifier.rawValue))
|
||||
Text("▼")
|
||||
.gesture(TapGesture() .onEnded {
|
||||
current.modifier = current.modifier.remove()
|
||||
})
|
||||
}
|
||||
.font(.system(size: controlSize))
|
||||
Text("Modifier")
|
||||
}
|
||||
.disabled(current.torrent || current.psychic)
|
||||
.opacity((current.torrent || current.psychic) ? 0.4 : 1.0)
|
||||
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))
|
||||
}
|
||||
.disabled(current.blowupUsed || current.torrent)
|
||||
.opacity((current.blowupUsed || current.torrent) ? 0.4 : 1.0)
|
||||
Spacer()
|
||||
}
|
||||
HStack {
|
||||
Text("Reroll")
|
||||
Picker("", selection: $current.hitRerollMode) {
|
||||
ForEach(RerollMode.allCases, id: \.self) { mode in
|
||||
Text(mode.label).tag(mode)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
Button("🎲") {
|
||||
current.applyReroll()
|
||||
}
|
||||
.disabled(current.hitRerollMode == .none || current.hitHasRerolled)
|
||||
.font(.system(size: controlSize))
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.disabled(current.torrent)
|
||||
.opacity(current.torrent ? 0.4 : 1.0)
|
||||
HStack {
|
||||
Toggle("Torrent", isOn: $current.torrent)
|
||||
Spacer()
|
||||
Toggle("Psychic", isOn: $current.psychic)
|
||||
.disabled(current.torrent)
|
||||
.opacity(current.torrent ? 0.4 : 1.0)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
VStack(spacing: 8) {
|
||||
HStack {
|
||||
Toggle("Lethal Hits", isOn: $current.lethalHits)
|
||||
Spacer()
|
||||
Toggle("Sust. Hits", isOn: $current.sustainedHits)
|
||||
}
|
||||
if current.sustainedHits {
|
||||
HStack {
|
||||
Text("X:")
|
||||
Spacer()
|
||||
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)
|
||||
if current.antiX {
|
||||
Spacer()
|
||||
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()
|
||||
}.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
|
||||
}())
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
@_exported import DieHammerEngine
|
||||
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// OnboardingModel.swift
|
||||
// Bunch O' Dice
|
||||
//
|
||||
// Created by Rory Hinnen on 22/6/11.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct DTCourse: Identifiable {
|
||||
var id = UUID()
|
||||
var title: String
|
||||
var subtitle: String
|
||||
var desc: String
|
||||
var imageName: String = ""
|
||||
}
|
||||
|
||||
extension DTCourse {
|
||||
static let title = "DieHammer"
|
||||
static var pages: [DTCourse] {
|
||||
[
|
||||
DTCourse(title: "DieHammer", subtitle: "A chaos manager for Warhammer or 40k", desc: "Welcome! This app will help you manage simple rolls to hit, wound, or save for Warhammer and Warhammer:40,000. Let's walk through the interface to help you understand how to use it.", imageName: "one"),
|
||||
DTCourse(title: "State", subtitle: "Types of Rolls", desc: "You can make three kinds of rolls: Hit, Wound or Armor Save. The top of the screen shows you what roll you are making. You can change the roll at any time by tapping the icon.", imageName: "two"),
|
||||
DTCourse(title: "The Pool", subtitle: "Rolling the Dice", desc: "The pool shows how many dice you are going to roll. Tap on the pool to change the amount.", imageName: "three"),
|
||||
DTCourse(title: "The Result", subtitle: "How bad is it, Doc?", desc: "Results will count up the number of dice that apply to the roll. We always count damage, so the Armour Save total shows how many wounds to apply. Tapping on results will save the current roll, and move on to the next.", imageName: "four"),
|
||||
DTCourse(title: "Re-Rolling", subtitle: "Ooo...yeah.", desc: "You can re-roll at any time by tapping the re-roll icon", imageName: "five"),
|
||||
DTCourse(title: "Hit Settings", subtitle: "Sweet, sweet modifiers", desc: "Tap the die that relates to your hit value, and adjust a hit modifier if you have one. If you have a special rule for Blowing up a hit, tapping the icon will add the new rolls.", imageName: "six"),
|
||||
DTCourse(title: "Wound", subtitle: "Make them bleed!", desc: "Set the Attackers Strength and the Defenders strength to calculate the new Wound cutoff. Results are always updated in real time.", imageName: "seven"),
|
||||
DTCourse(title: "Armour Save", subtitle: "This is your last chance!", desc: "Amour Save will show how many dice in the roll fail, counting up the number of wounds to distribute.", imageName: "eight"),
|
||||
DTCourse(title: "History & Settings", subtitle: "Wait, what happened?", desc: "You can always pull up your history to see the old rolls you've made. You can also add a roll to the history at any time.", imageName: "nine"),
|
||||
DTCourse(title: title, subtitle: "Have Fun!", desc: "", imageName: "one")
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,460 @@
|
||||
//
|
||||
// State.swift
|
||||
// Bunch O' Dice
|
||||
//
|
||||
// Created by Rory Hinnen on 22/5/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
// MARK: Extensions
|
||||
extension Int {
|
||||
func digits() -> [Int] {
|
||||
var digits: [Int] = []
|
||||
var num = self
|
||||
|
||||
digits.append(num % 10)
|
||||
|
||||
repeat {
|
||||
num = num / 10
|
||||
digits.append(num % 10)
|
||||
} while num >= 10
|
||||
while digits.count < 3 {
|
||||
digits.append(0)
|
||||
}
|
||||
return digits
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Enums
|
||||
enum RollState: Int {
|
||||
case hit
|
||||
case wound
|
||||
case armour
|
||||
|
||||
func print() -> String {
|
||||
switch self {
|
||||
case .hit:
|
||||
return "⚔️"
|
||||
case .wound:
|
||||
return "🩸"
|
||||
case .armour:
|
||||
return "🛡"
|
||||
}
|
||||
}
|
||||
|
||||
func text() -> String {
|
||||
switch self {
|
||||
case .hit:
|
||||
return "Hit"
|
||||
case .wound:
|
||||
return "Wound"
|
||||
case .armour:
|
||||
return "Damage"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Appearance: Int, CaseIterable, Identifiable {
|
||||
case light, dark, automatic
|
||||
|
||||
var id: Int { self.rawValue }
|
||||
|
||||
var name: String {
|
||||
switch self {
|
||||
case .light: return "Light"
|
||||
case .dark: return "Dark"
|
||||
case .automatic: return "Automatic"
|
||||
}
|
||||
}
|
||||
|
||||
var type: ColorScheme? {
|
||||
switch self {
|
||||
case .light: return ColorScheme.light
|
||||
case .dark: return ColorScheme.dark
|
||||
case .automatic: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum RerollMode: Int, CaseIterable {
|
||||
case none, ones, failed, all
|
||||
|
||||
var label: String {
|
||||
switch self {
|
||||
case .none: return "None"
|
||||
case .ones: return "1s"
|
||||
case .failed: return "Failed"
|
||||
case .all: return "All"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Structs
|
||||
struct Event: Identifiable {
|
||||
let id: UUID
|
||||
var total: Int
|
||||
var pool: Dice
|
||||
var state: RollState
|
||||
|
||||
var poolTotal: Int {
|
||||
get {
|
||||
return pool.one + pool.two + pool.three + pool.four + pool.five + pool.six
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
id = UUID()
|
||||
total = 0
|
||||
pool = Dice()
|
||||
state = .hit
|
||||
}
|
||||
|
||||
init(total: Int, pool: Dice, state: RollState) {
|
||||
self.id = UUID()
|
||||
self.total = total
|
||||
self.pool = pool
|
||||
self.state = state
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Classes
|
||||
class Roll: ObservableObject {
|
||||
//Published
|
||||
@Published var dice = Dice()
|
||||
@Published var total = 0
|
||||
@Published var state = RollState.hit
|
||||
@Published var history = [Event()]
|
||||
@Published var appearance = Appearance.automatic
|
||||
@Published var firstView = true
|
||||
|
||||
@Published var hitCutoff = Dice.DiceType.four
|
||||
@Published var modifier = Dice.RollModifiers.none
|
||||
|
||||
@Published var strength = 7
|
||||
@Published var toughness = 5
|
||||
@Published var woundModifier = Dice.RollModifiers.none
|
||||
@Published var mortal = false
|
||||
|
||||
@Published var armourPenetration = 0
|
||||
@Published var armourSave = 3
|
||||
@Published var invulnerable = false
|
||||
|
||||
@Published var currentPage: String
|
||||
|
||||
@Published var hitRerollMode = RerollMode.none
|
||||
@Published var woundRerollMode = RerollMode.none
|
||||
@Published var saveRerollMode = RerollMode.none
|
||||
|
||||
@Published var hitHasRerolled = false
|
||||
@Published var woundHasRerolled = false
|
||||
@Published var saveHasRerolled = false
|
||||
|
||||
@Published var blowupUsed = false
|
||||
@Published var torrent = false
|
||||
@Published var psychic = false
|
||||
|
||||
@Published var sustainedHits = false
|
||||
@Published var sustainedHitsX = 1
|
||||
@Published var lethalHits = false
|
||||
@Published var devastatingWounds = false
|
||||
@Published var antiX = false
|
||||
@Published var antiXThreshold = 6
|
||||
|
||||
@Published var autoWoundsCarried = 0
|
||||
@Published var mortalWoundsTotal = 0
|
||||
|
||||
// calculated variables
|
||||
var totalDigits: [Int] {
|
||||
get {
|
||||
return total.digits()
|
||||
}
|
||||
}
|
||||
|
||||
var result: Int {
|
||||
get
|
||||
{
|
||||
switch self.state {
|
||||
case .hit:
|
||||
return hit
|
||||
case .wound:
|
||||
return wound
|
||||
case .armour:
|
||||
return save
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var woundCutoff: Int {
|
||||
get
|
||||
{
|
||||
if Float(self.strength) / Float(self.toughness) >= 2.0 {
|
||||
return 2
|
||||
} else if Float(self.strength) / Float(self.toughness) > 1.0 {
|
||||
return 3
|
||||
} else if Float(self.strength) / Float(self.toughness) == 1 {
|
||||
return 4
|
||||
} else if Float(self.strength) / Float(self.toughness) <= 0.5 {
|
||||
return 6
|
||||
} else {
|
||||
return 5
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var cutoff: Dice.DiceType {
|
||||
get {
|
||||
switch self.state {
|
||||
case .hit:
|
||||
return hitCutoff
|
||||
case .wound:
|
||||
if Float(self.strength) / Float(self.toughness) >= 2.0 {
|
||||
return .two
|
||||
} else if Float(self.strength) / Float(self.toughness) > 1.0 {
|
||||
return .three
|
||||
} else if Float(self.strength) / Float(self.toughness) == 1 {
|
||||
return .four
|
||||
} else if Float(self.strength) / Float(self.toughness) <= 0.5 {
|
||||
return .six
|
||||
} else {
|
||||
return .five
|
||||
}
|
||||
case .armour:
|
||||
return Dice.DiceType(rawValue: self.armourSave) ?? .one
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hit: Int {
|
||||
get {
|
||||
if torrent { return total }
|
||||
let mod = psychic ? Dice.RollModifiers.none.rawValue : modifier.rawValue
|
||||
return self.dice.hit(cutoff: self.hitCutoff.rawValue, modifier: mod)
|
||||
}
|
||||
}
|
||||
|
||||
var wound: Int {
|
||||
get {
|
||||
let cutoff = self.mortal ? 2 : self.woundCutoff
|
||||
return self.dice.wound(cutoff: cutoff, modifier: woundModifier.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
var save: Int {
|
||||
get {
|
||||
return self.dice.armour(save: self.armourSave, armourPenetration: self.armourPenetration, invulnerable: self.invulnerable)
|
||||
}
|
||||
}
|
||||
|
||||
var critHitCount: Int { dice.six }
|
||||
|
||||
var antiXCount: Int {
|
||||
guard antiX else { return 0 }
|
||||
var count = 0
|
||||
for face in antiXThreshold...6 {
|
||||
if let dt = Dice.DiceType(rawValue: face) { count += dice.value(which: dt) }
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
var critWoundCount: Int { devastatingWounds ? dice.six : 0 }
|
||||
|
||||
// Deduplicated count of hit-phase crits that auto-wound (Lethal Hits and/or Anti-X).
|
||||
// Anti-X is always the superset when its threshold <= 6, so it takes precedence.
|
||||
// Gated off during Torrent since there are no hit dice to count natural rolls on.
|
||||
var autoWoundCrits: Int {
|
||||
guard !torrent else { return 0 }
|
||||
if antiX { return antiXCount }
|
||||
if lethalHits { return dice.six }
|
||||
return 0
|
||||
}
|
||||
|
||||
var currentRerollMode: RerollMode {
|
||||
switch state {
|
||||
case .hit: return hitRerollMode
|
||||
case .wound: return woundRerollMode
|
||||
case .armour: return saveRerollMode
|
||||
}
|
||||
}
|
||||
|
||||
var currentHasRerolled: Bool {
|
||||
switch state {
|
||||
case .hit: return hitHasRerolled
|
||||
case .wound: return woundHasRerolled
|
||||
case .armour: return saveHasRerolled
|
||||
}
|
||||
}
|
||||
|
||||
// init
|
||||
init() {
|
||||
if !UserDefaults.standard.bool(forKey: "didLaunchBefore") {
|
||||
UserDefaults.standard.set(true, forKey: "didLaunchBefore")
|
||||
currentPage = "OnboardingView"
|
||||
} else {
|
||||
currentPage = "Bunch"
|
||||
}
|
||||
}
|
||||
|
||||
// modifier controllers
|
||||
// MARK: wound controllers
|
||||
func addStrength() {
|
||||
self.strength += 1
|
||||
}
|
||||
|
||||
func loseStrength() {
|
||||
if self.strength > 1 {
|
||||
self.strength -= 1
|
||||
}
|
||||
}
|
||||
|
||||
func addToughness() {
|
||||
self.toughness += 1
|
||||
}
|
||||
|
||||
func loseToughness() {
|
||||
if self.toughness > 1 {
|
||||
self.toughness -= 1
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: armour controllers
|
||||
func addArmour() {
|
||||
if self.armourSave < 6 {
|
||||
self.armourSave += 1
|
||||
}
|
||||
}
|
||||
|
||||
func loseArmour() {
|
||||
if self.armourSave > 1 {
|
||||
self.armourSave -= 1
|
||||
}
|
||||
}
|
||||
|
||||
func addPenetration() {
|
||||
self.armourPenetration += 1
|
||||
}
|
||||
|
||||
func losePenetration() {
|
||||
self.armourPenetration -= 1
|
||||
}
|
||||
|
||||
// MARK: total pool
|
||||
func incrementDigits(place: Int) {
|
||||
if totalDigits[place] < 9 {
|
||||
self.total += Int(pow(10.0, Float(place)))
|
||||
}
|
||||
}
|
||||
|
||||
func decrementDigits(place: Int) {
|
||||
if totalDigits[place] > 0 {
|
||||
self.total -= Int(pow(10.0, Float(place)))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: utility function
|
||||
func incrementState() {
|
||||
var x = 0
|
||||
switch self.state {
|
||||
case .hit:
|
||||
x = self.hit
|
||||
case .wound:
|
||||
x = self.wound
|
||||
case .armour:
|
||||
x = self.armourSave
|
||||
}
|
||||
if self.history[0].total == 0 {
|
||||
self.history[0] = Event(total: x, pool: self.dice, state: self.state)
|
||||
} else {
|
||||
self.history.insert(Event(total: x, pool: self.dice, state: self.state), at: 0)
|
||||
}
|
||||
switch self.state {
|
||||
case .hit:
|
||||
let crits = autoWoundCrits
|
||||
let sustained = (sustainedHits && !torrent) ? dice.six * sustainedHitsX : 0
|
||||
autoWoundsCarried = min(crits, hit)
|
||||
self.total = max(0, hit - autoWoundsCarried) + sustained
|
||||
self.state = .wound
|
||||
woundHasRerolled = false
|
||||
case .wound:
|
||||
let mortals = critWoundCount
|
||||
mortalWoundsTotal = mortals
|
||||
self.total = max(0, wound + autoWoundsCarried - mortals)
|
||||
autoWoundsCarried = 0
|
||||
self.state = .armour
|
||||
saveHasRerolled = false
|
||||
case .armour:
|
||||
self.total = save
|
||||
self.state = .hit
|
||||
hitHasRerolled = false
|
||||
blowupUsed = false
|
||||
mortalWoundsTotal = 0
|
||||
}
|
||||
self.dice.roll(howMany: self.total)
|
||||
}
|
||||
|
||||
// MARK: extra
|
||||
func resetPool() {
|
||||
dice.roll(howMany: total)
|
||||
hitHasRerolled = false
|
||||
blowupUsed = false
|
||||
}
|
||||
|
||||
func applyBlowup(face: Dice.DiceType) {
|
||||
guard !blowupUsed else { return }
|
||||
switch face {
|
||||
case .six:
|
||||
dice.blowup(dice: .six)
|
||||
case .five:
|
||||
dice.blowup(dice: .five)
|
||||
default:
|
||||
break
|
||||
}
|
||||
blowupUsed = true
|
||||
}
|
||||
|
||||
func applyReroll() {
|
||||
guard !currentHasRerolled else { return }
|
||||
|
||||
let mode: RerollMode
|
||||
let cutoff: Int
|
||||
|
||||
switch state {
|
||||
case .hit:
|
||||
mode = hitRerollMode
|
||||
cutoff = hitCutoff.rawValue
|
||||
case .wound:
|
||||
mode = woundRerollMode
|
||||
cutoff = woundCutoff
|
||||
case .armour:
|
||||
mode = saveRerollMode
|
||||
cutoff = invulnerable ? armourSave : (armourSave + armourPenetration)
|
||||
}
|
||||
|
||||
switch mode {
|
||||
case .none:
|
||||
return
|
||||
case .ones:
|
||||
dice.reroll(dice: .one)
|
||||
case .failed:
|
||||
dice.rerollBelow(cutoff: cutoff)
|
||||
case .all:
|
||||
dice.roll(howMany: total)
|
||||
}
|
||||
|
||||
switch state {
|
||||
case .hit: hitHasRerolled = true
|
||||
case .wound: woundHasRerolled = true
|
||||
case .armour: saveHasRerolled = true
|
||||
}
|
||||
}
|
||||
|
||||
func add(howMuch: Int) {
|
||||
var newTotal: Int = 0
|
||||
newTotal = self.total + howMuch
|
||||
if newTotal < 0 {
|
||||
newTotal = 0
|
||||
}
|
||||
self.total = newTotal
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
//
|
||||
// OnboardingView.swift
|
||||
// Bunch O' Dice
|
||||
//
|
||||
// Created by Rory Hinnen on 22/6/11.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct OnboardingView: View {
|
||||
private let courses = DTCourse.pages
|
||||
@State private var selection = 0
|
||||
@EnvironmentObject var current: Roll
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
TabView(selection: $selection) {
|
||||
ForEach(courses.indices, id: \.self) { idx in
|
||||
VStack(spacing: 0) {
|
||||
Spacer()
|
||||
Text(courses[idx].title)
|
||||
.font(.largeTitle)
|
||||
Rectangle()
|
||||
.fill(Color.secondary)
|
||||
.frame(height: 1)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
|
||||
Image(courses[idx].imageName)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
|
||||
Text(courses[idx].subtitle)
|
||||
|
||||
Text(courses[idx].desc)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
}.padding()
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.tag(idx)
|
||||
}
|
||||
}
|
||||
.tabViewStyle(.page(indexDisplayMode: .never))
|
||||
HStack {
|
||||
Spacer()
|
||||
Button {
|
||||
withAnimation {
|
||||
if selection < courses.count - 1 {
|
||||
selection += 1
|
||||
} else {
|
||||
current.currentPage = "Bunch"
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
if selection == courses.count - 1 {
|
||||
Image(systemName: "checkmark.circle")
|
||||
.font(.largeTitle)
|
||||
} else {
|
||||
Image(systemName: "chevron.right.circle")
|
||||
.font(.largeTitle)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct OnboardingView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
OnboardingView()
|
||||
.environmentObject(Roll())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>CA92.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// ViewUtiles.swift
|
||||
// Bunch O' Dice
|
||||
//
|
||||
// Created by Rory Hinnen on 22/6/12.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
class SheetState<State>: ObservableObject {
|
||||
@Published var isShowing = false
|
||||
@Published var state: State? {
|
||||
didSet { isShowing = state != nil }
|
||||
}
|
||||
}
|
||||
|
||||
class SettingsSheet: SheetState<SettingsSheet.State> {
|
||||
enum State {
|
||||
case settings, history
|
||||
}
|
||||
}
|
||||
|
||||
extension CaseIterable where Self: Equatable {
|
||||
mutating func next() {
|
||||
let allCases = Self.allCases
|
||||
// just a sanity check, as the possibility of a enum case to not be
|
||||
// present in `allCases` is quite low
|
||||
guard let selfIndex = allCases.firstIndex(of: self) else { return }
|
||||
let nextIndex = Self.allCases.index(after: selfIndex)
|
||||
self = allCases[nextIndex == allCases.endIndex ? allCases.startIndex : nextIndex]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
//
|
||||
// Die.swift
|
||||
// Bunch O' Dice
|
||||
//
|
||||
// Created by Rory Hinnen on 22/6/18.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct Die: View {
|
||||
@EnvironmentObject var current: Roll
|
||||
|
||||
@State var whichDie: Dice.DiceType
|
||||
|
||||
var body: some View {
|
||||
/* let size = 25.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
|
||||
|
||||
HStack{
|
||||
Text(String(current.dice.value(which: whichDie)))
|
||||
.foregroundColor(current.state == .armour ?
|
||||
displayColorArmorText : displayColorHit)
|
||||
Image(systemName: "die.face." + String(whichDie.rawValue) + ".fill")
|
||||
.foregroundColor(current.state == .armour ?
|
||||
displayColorArmor : displayColorHit)
|
||||
|
||||
}
|
||||
.font(.system(size: size))
|
||||
.padding(2)
|
||||
.gesture(TapGesture().onEnded {
|
||||
if current.state == .hit {
|
||||
if whichDie.rawValue != 1 {
|
||||
current.hitCutoff = whichDie }
|
||||
} else if current.state == .armour {
|
||||
current.armourSave = Int(whichDie.rawValue)
|
||||
}
|
||||
})
|
||||
} */
|
||||
Text("")
|
||||
}
|
||||
|
||||
struct Die_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Die(whichDie: Dice.DiceType.one)
|
||||
.environmentObject(Roll())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
//
|
||||
// Die.swift
|
||||
// Bunch O' Dice
|
||||
//
|
||||
// Created by Rory Hinnen on 22/2/20.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SingleDie: View {
|
||||
@EnvironmentObject var current: Roll
|
||||
@State var whichDie: Dice.DiceType
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text(String(current.dice.value(which: whichDie)))
|
||||
Group {
|
||||
if (current.state != .armour) && (whichDie.rawValue >= current.cutoff.rawValue) {
|
||||
Text(whichDie.print())
|
||||
.foregroundColor(Color.red)
|
||||
} else if (current.state == .armour) && (whichDie.rawValue < current.cutoff.rawValue) {
|
||||
Text(whichDie.print())
|
||||
.foregroundColor(Color.red)
|
||||
} else {
|
||||
Text(whichDie.print())
|
||||
.foregroundColor(Color.black)
|
||||
.background(Color.white)
|
||||
}
|
||||
}
|
||||
.font(.system(size: 45))
|
||||
}
|
||||
.font(/*@START_MENU_TOKEN@*/.title/*@END_MENU_TOKEN@*/)
|
||||
.padding(.horizontal, 5.0)
|
||||
.gesture(TapGesture().onEnded {
|
||||
if current.state == .hit {
|
||||
current.hitCutoff = whichDie
|
||||
}
|
||||
else if current.state == .armour {
|
||||
current.armourSave = Int(whichDie.rawValue)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct Die_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Die(whichDie: .one)
|
||||
.environmentObject(Roll())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
//
|
||||
// SwiftUIView.swift
|
||||
// Bunch O' Dice
|
||||
//
|
||||
// Created by Rory Martin Hinnen on 6/7/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ShowPoolLandscape: View {
|
||||
@EnvironmentObject var current: Roll
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Spacer()
|
||||
ForEach(Dice.DiceType.allCases, id: \.rawValue) { i in
|
||||
SingleDie(whichDie: i)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ShowPool_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ShowPoolLandscape()
|
||||
.environmentObject(Roll())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
//
|
||||
// IncrementDie.swift
|
||||
// Bunch O' Dice
|
||||
//
|
||||
// Created by Rory Martin Hinnen on 6/7/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct AddToPool: View {
|
||||
@EnvironmentObject var current: Roll
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Spacer()
|
||||
Text(String(current.total) + " 🎲")
|
||||
.font(.system(size: 40))
|
||||
.padding(15)
|
||||
HStack {
|
||||
ForEach(Array(stride(from: 2, through: 0, by: -1)), id: \.self) { place in
|
||||
VStack{
|
||||
Text("▲")
|
||||
.gesture(TapGesture().onEnded({
|
||||
current.incrementDigits(place: place)
|
||||
}))
|
||||
Text(String(current.totalDigits[place]))
|
||||
.padding(4)
|
||||
.border(.black, width: 1.0)
|
||||
Text("▼")
|
||||
.gesture(TapGesture().onEnded({
|
||||
current.decrementDigits(place: place)
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
.font(.system(size: 45))
|
||||
.padding(10)
|
||||
Button("Clear", action: {current.total = 0})
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct IncrementDie_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
AddToPool()
|
||||
.environmentObject(Roll())
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// HistoryView.swift
|
||||
// Bunch O' Dice
|
||||
//
|
||||
// Created by Rory Hinnen on 22/6/11.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct HistoryView: View {
|
||||
@EnvironmentObject var current: Roll
|
||||
@Binding var isShowing: Bool
|
||||
|
||||
var body: some View {
|
||||
VStack{
|
||||
List(current.history) {event in
|
||||
Text("\(event.poolTotal) Dice Rolled, \(event.total) \(event.state.print())")
|
||||
}
|
||||
Spacer()
|
||||
Button("Dismiss", action: {isShowing.toggle()})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct HistoryView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
HistoryView(isShowing: .constant(false))
|
||||
.environmentObject(Roll())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
//
|
||||
// SettingsView.swift
|
||||
// Bunch O' Dice
|
||||
//
|
||||
// Created by Rory Hinnen on 22/7/2.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsView: View {
|
||||
@EnvironmentObject var current: Roll
|
||||
@Binding var isShowing: Bool
|
||||
|
||||
var body: some View {
|
||||
VStack{
|
||||
VStack{
|
||||
Text("DieHammer")
|
||||
.font(.title)
|
||||
Text("A roll management tool for Warhammer & Warhammer:40,000")
|
||||
.font(.subheadline)
|
||||
}.padding()
|
||||
Group{
|
||||
VStack{
|
||||
Text("Theme")
|
||||
Button(String(current.appearance.name)) {current.appearance.next()}
|
||||
}
|
||||
Button("Tutorial") {current.currentPage = "OnboardingView"}
|
||||
.padding()
|
||||
}
|
||||
Spacer()
|
||||
Button("Dismiss", action: {isShowing = false})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SettingsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SettingsView(isShowing: .constant(true))
|
||||
.environmentObject(Roll())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
//
|
||||
// DieHorizontal.swift
|
||||
// Bunch O' Dice
|
||||
//
|
||||
// Created by Rory Hinnen on 22/6/12.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct DieHorizontal: View {
|
||||
@EnvironmentObject var current: Roll
|
||||
@State var whichDie: Dice.DiceType
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text(String(current.dice.value(which: whichDie)))
|
||||
.offset(x: 0.0, y: 2.0)
|
||||
Group {
|
||||
if (current.state != .armour) && (whichDie.rawValue >= current.cutoff.rawValue) {
|
||||
Text(whichDie.print())
|
||||
.foregroundColor(Color.red)
|
||||
} else if (current.state == .armour) && (whichDie.rawValue < current.cutoff.rawValue) {
|
||||
Text(whichDie.print())
|
||||
.foregroundColor(Color.red)
|
||||
} else {
|
||||
Text(whichDie.print())
|
||||
.foregroundColor(Color.black)
|
||||
}
|
||||
}
|
||||
.font(.system(size: 40))
|
||||
}
|
||||
.font(.system(size: 25))
|
||||
.padding(.horizontal, 5.0)
|
||||
|
||||
.gesture(TapGesture().onEnded {
|
||||
if current.state == .hit {
|
||||
current.hitCutoff = whichDie
|
||||
}
|
||||
else if current.state == .armour {
|
||||
current.armourSave = Int(whichDie.rawValue)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct DieHorizontal_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VStack {
|
||||
DieHorizontal(whichDie: .four).environmentObject(Roll())
|
||||
DieHorizontal(whichDie: .three).environmentObject(Roll())
|
||||
}
|
||||
}
|
||||
//.environmentObject(Roll())
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// ShowPoolHorizontal.swift
|
||||
// Bunch O' Dice
|
||||
//
|
||||
// Created by Rory Hinnen on 22/6/12.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ShowPoolHorizontal: View {
|
||||
@EnvironmentObject var current: Roll
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .trailing) {
|
||||
ForEach(Dice.DiceType.allCases.reversed(), id: \.rawValue) { i in
|
||||
DieHorizontal(whichDie: i)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ShowPoolHorizontal_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ShowPoolHorizontal().environmentObject(Roll())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// dice.swift
|
||||
// Bunch O' Dice
|
||||
//
|
||||
// Created by Rory Hinnen on 6/10/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct dice: View {
|
||||
var body: some View {
|
||||
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
||||
}
|
||||
}
|
||||
|
||||
struct dice_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
dice()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
//
|
||||
// PortraitScreen.swift
|
||||
// Bunch O' Dice
|
||||
//
|
||||
// Created by Rory Hinnen on 22/6/11.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct PortraitScreen: View {
|
||||
@EnvironmentObject var current: Roll
|
||||
var frameSize: CGFloat // = CGFloat(120)
|
||||
let fontSize: Double // = 35.0
|
||||
let blurSize: Double // = 11.0
|
||||
|
||||
var body: some View {
|
||||
Group{
|
||||
VStack {
|
||||
HStack {
|
||||
Spacer()
|
||||
Pool(frameSize: frameSize, fontSize: fontSize, blurSize: blurSize)
|
||||
Spacer()
|
||||
Total(frameSize: frameSize, fontSize: fontSize, blurSize: blurSize)
|
||||
Spacer()
|
||||
}
|
||||
ShowPoolLandscape()
|
||||
Group {
|
||||
switch current.state {
|
||||
case .wound:
|
||||
WoundCalculator()
|
||||
case .armour:
|
||||
ArmourCalculator()
|
||||
default:
|
||||
HitCalculator()
|
||||
}
|
||||
}
|
||||
}.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PortraitScreen_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
PortraitScreen(frameSize: 120, fontSize: 35, blurSize: 11)
|
||||
.environmentObject(Roll())
|
||||
}
|
||||
}
|
||||