all features implemented

This commit is contained in:
Rory Hinnen
2026-09-07 15:27:33 -05:00
parent ac4d4a670a
commit 2a6130ba8e
64 changed files with 691 additions and 340 deletions
+1
View File
@@ -0,0 +1 @@
@_exported import DieHammerEngine
+34
View File
@@ -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")
]
}
}
+460
View File
@@ -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
}
}