Files
diehammer/DieHammer/Model/RollState.swift
T

480 lines
12 KiB
Swift

//
// 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
// Snapshot of dice before the current phase's reroll was applied.
// Restored when the user switches reroll modes, so the new mode
// always applies to the original un-rerolled results.
@Published var preRerollDice: Dice? = nil
@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 + mortalWoundsTotal
}
}
}
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:
return Dice.DiceType(rawValue: woundCutoff) ?? .four
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
preRerollDice = nil
case .wound:
let mortals = critWoundCount
mortalWoundsTotal = mortals
self.total = max(0, wound + autoWoundsCarried - mortals)
autoWoundsCarried = 0
self.state = .armour
saveHasRerolled = false
preRerollDice = nil
case .armour:
self.total = save + mortalWoundsTotal
self.state = .hit
hitHasRerolled = false
preRerollDice = nil
blowupUsed = false
mortalWoundsTotal = 0
}
self.dice.roll(howMany: self.total)
}
// MARK: extra
func resetPool() {
dice.roll(howMany: total)
hitHasRerolled = false
preRerollDice = nil
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() {
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)
}
guard mode != .none else { return }
// If a reroll was already applied this phase, restore the original dice first
// so the new mode always operates on the un-rerolled results.
if let snapshot = preRerollDice {
dice = snapshot
} else {
preRerollDice = dice
}
switch mode {
case .none:
break
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
}
}
// Restores dice to pre-reroll state when the user selects None.
func undoReroll() {
guard let snapshot = preRerollDice else { return }
dice = snapshot
preRerollDice = nil
switch state {
case .hit: hitHasRerolled = false
case .wound: woundHasRerolled = false
case .armour: saveHasRerolled = false
}
}
func add(howMuch: Int) {
var newTotal: Int = 0
newTotal = self.total + howMuch
if newTotal < 0 {
newTotal = 0
}
self.total = newTotal
}
}