all features implemented
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user