update view, still work to do

This commit is contained in:
Rory Hinnen
2026-09-08 17:49:59 -05:00
parent 241a753879
commit b1d4094f78
9 changed files with 201 additions and 204 deletions
+3 -7
View File
@@ -18,7 +18,6 @@
981AD0D927C30E32005A6492 /* Bunch_O__DiceUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 981AD0D827C30E32005A6492 /* Bunch_O__DiceUITests.swift */; }; 981AD0D927C30E32005A6492 /* Bunch_O__DiceUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 981AD0D827C30E32005A6492 /* Bunch_O__DiceUITests.swift */; };
981AD0DB27C30E32005A6492 /* Bunch_O__DiceUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 981AD0DA27C30E32005A6492 /* Bunch_O__DiceUITestsLaunchTests.swift */; }; 981AD0DB27C30E32005A6492 /* Bunch_O__DiceUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 981AD0DA27C30E32005A6492 /* Bunch_O__DiceUITestsLaunchTests.swift */; };
981AD0E927C31208005A6492 /* Dice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 981AD0E827C31208005A6492 /* Dice.swift */; }; 981AD0E927C31208005A6492 /* Dice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 981AD0E827C31208005A6492 /* Dice.swift */; };
98225DE12A3527160019DE8E /* dice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98225DE02A3527160019DE8E /* dice.swift */; };
98325A2E2839BE4B0006DF4B /* RollState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98325A2D2839BE4B0006DF4B /* RollState.swift */; }; 98325A2E2839BE4B0006DF4B /* RollState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98325A2D2839BE4B0006DF4B /* RollState.swift */; };
98745CA62870CC670031166A /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98745CA52870CC670031166A /* SettingsView.swift */; }; 98745CA62870CC670031166A /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98745CA52870CC670031166A /* SettingsView.swift */; };
988B150F2854E9170082C2C5 /* HistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 988B150E2854E9160082C2C5 /* HistoryView.swift */; }; 988B150F2854E9170082C2C5 /* HistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 988B150E2854E9160082C2C5 /* HistoryView.swift */; };
@@ -60,7 +59,6 @@
981AD0D827C30E32005A6492 /* Bunch_O__DiceUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bunch_O__DiceUITests.swift; sourceTree = "<group>"; }; 981AD0D827C30E32005A6492 /* Bunch_O__DiceUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bunch_O__DiceUITests.swift; sourceTree = "<group>"; };
981AD0DA27C30E32005A6492 /* Bunch_O__DiceUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bunch_O__DiceUITestsLaunchTests.swift; sourceTree = "<group>"; }; 981AD0DA27C30E32005A6492 /* Bunch_O__DiceUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bunch_O__DiceUITestsLaunchTests.swift; sourceTree = "<group>"; };
981AD0E827C31208005A6492 /* Dice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dice.swift; sourceTree = "<group>"; }; 981AD0E827C31208005A6492 /* Dice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dice.swift; sourceTree = "<group>"; };
98225DE02A3527160019DE8E /* dice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dice.swift; sourceTree = "<group>"; };
98325A2D2839BE4B0006DF4B /* RollState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RollState.swift; sourceTree = "<group>"; }; 98325A2D2839BE4B0006DF4B /* RollState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RollState.swift; sourceTree = "<group>"; };
98745CA52870CC670031166A /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; }; 98745CA52870CC670031166A /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
988997FF285814AB00B07E09 /* DieHammer.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = DieHammer.plist; sourceTree = SOURCE_ROOT; }; 988997FF285814AB00B07E09 /* DieHammer.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = DieHammer.plist; sourceTree = SOURCE_ROOT; };
@@ -132,9 +130,6 @@
7692E8422BD55CDE005BDD76 /* PrivacyInfo.xcprivacy */, 7692E8422BD55CDE005BDD76 /* PrivacyInfo.xcprivacy */,
988997FF285814AB00B07E09 /* DieHammer.plist */, 988997FF285814AB00B07E09 /* DieHammer.plist */,
981AD0BD27C30E31005A6492 /* DieHammerApp.swift */, 981AD0BD27C30E31005A6492 /* DieHammerApp.swift */,
981AD0BF27C30E31005A6492 /* ContentView.swift */,
988B1518285566AD0082C2C5 /* OnboardingView.swift */,
980EF453285E2BAD002FE8A0 /* MainView.swift */,
981AD0EC27C3356D005A6492 /* Views */, 981AD0EC27C3356D005A6492 /* Views */,
981AD0E727C311DA005A6492 /* Model */, 981AD0E727C311DA005A6492 /* Model */,
98E534D2285653E700D5ABB4 /* Utilities */, 98E534D2285653E700D5ABB4 /* Utilities */,
@@ -186,7 +181,9 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
98D0CD192856E3850047DC1B /* Modals */, 98D0CD192856E3850047DC1B /* Modals */,
98225DE02A3527160019DE8E /* dice.swift */, 988B1518285566AD0082C2C5 /* OnboardingView.swift */,
981AD0BF27C30E31005A6492 /* ContentView.swift */,
980EF453285E2BAD002FE8A0 /* MainView.swift */,
); );
path = Views; path = Views;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -349,7 +346,6 @@
981AD0E927C31208005A6492 /* Dice.swift in Sources */, 981AD0E927C31208005A6492 /* Dice.swift in Sources */,
98F001D5284F86F1004F5140 /* AddToPool.swift in Sources */, 98F001D5284F86F1004F5140 /* AddToPool.swift in Sources */,
988B150F2854E9170082C2C5 /* HistoryView.swift in Sources */, 988B150F2854E9170082C2C5 /* HistoryView.swift in Sources */,
98225DE12A3527160019DE8E /* dice.swift in Sources */,
981AD0BE27C30E31005A6492 /* DieHammerApp.swift in Sources */, 981AD0BE27C30E31005A6492 /* DieHammerApp.swift in Sources */,
980EF454285E2BAD002FE8A0 /* MainView.swift in Sources */, 980EF454285E2BAD002FE8A0 /* MainView.swift in Sources */,
98325A2E2839BE4B0006DF4B /* RollState.swift in Sources */, 98325A2E2839BE4B0006DF4B /* RollState.swift in Sources */,
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "2700" LastUpgradeVersion = "2700"
version = "1.8"> version = "1.7">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
buildImplicitDependencies = "YES" buildImplicitDependencies = "YES"
@@ -27,11 +27,10 @@
buildConfiguration = "Debug" buildConfiguration = "Debug"
selectedDebuggerIdentifier = "" selectedDebuggerIdentifier = ""
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn" selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
shouldUseLaunchSchemeArgsEnv = "YES" shouldUseLaunchSchemeArgsEnv = "YES">
shouldAutocreateTestPlan = "NO">
<TestPlans> <TestPlans>
<TestPlanReference <TestPlanReference
reference = "container:Bunch O' Dice.xctestplan" reference = "container:Bunch O&apos; Dice.xctestplan"
default = "YES"> default = "YES">
</TestPlanReference> </TestPlanReference>
</TestPlans> </TestPlans>
+33 -14
View File
@@ -151,6 +151,11 @@ class Roll: ObservableObject {
@Published var woundHasRerolled = false @Published var woundHasRerolled = false
@Published var saveHasRerolled = 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 blowupUsed = false
@Published var torrent = false @Published var torrent = false
@Published var psychic = false @Published var psychic = false
@@ -209,17 +214,7 @@ class Roll: ObservableObject {
case .hit: case .hit:
return hitCutoff return hitCutoff
case .wound: case .wound:
if Float(self.strength) / Float(self.toughness) >= 2.0 { return Dice.DiceType(rawValue: woundCutoff) ?? .four
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: case .armour:
return Dice.DiceType(rawValue: self.armourSave) ?? .one return Dice.DiceType(rawValue: self.armourSave) ?? .one
} }
@@ -376,6 +371,7 @@ class Roll: ObservableObject {
self.total = max(0, hit - autoWoundsCarried) + sustained self.total = max(0, hit - autoWoundsCarried) + sustained
self.state = .wound self.state = .wound
woundHasRerolled = false woundHasRerolled = false
preRerollDice = nil
case .wound: case .wound:
let mortals = critWoundCount let mortals = critWoundCount
mortalWoundsTotal = mortals mortalWoundsTotal = mortals
@@ -383,10 +379,12 @@ class Roll: ObservableObject {
autoWoundsCarried = 0 autoWoundsCarried = 0
self.state = .armour self.state = .armour
saveHasRerolled = false saveHasRerolled = false
preRerollDice = nil
case .armour: case .armour:
self.total = save self.total = save
self.state = .hit self.state = .hit
hitHasRerolled = false hitHasRerolled = false
preRerollDice = nil
blowupUsed = false blowupUsed = false
mortalWoundsTotal = 0 mortalWoundsTotal = 0
} }
@@ -397,6 +395,7 @@ class Roll: ObservableObject {
func resetPool() { func resetPool() {
dice.roll(howMany: total) dice.roll(howMany: total)
hitHasRerolled = false hitHasRerolled = false
preRerollDice = nil
blowupUsed = false blowupUsed = false
} }
@@ -414,8 +413,6 @@ class Roll: ObservableObject {
} }
func applyReroll() { func applyReroll() {
guard !currentHasRerolled else { return }
let mode: RerollMode let mode: RerollMode
let cutoff: Int let cutoff: Int
@@ -431,9 +428,19 @@ class Roll: ObservableObject {
cutoff = invulnerable ? armourSave : (armourSave + armourPenetration) 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 { switch mode {
case .none: case .none:
return break
case .ones: case .ones:
dice.reroll(dice: .one) dice.reroll(dice: .one)
case .failed: case .failed:
@@ -449,6 +456,18 @@ class Roll: ObservableObject {
} }
} }
// 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) { func add(howMuch: Int) {
var newTotal: Int = 0 var newTotal: Int = 0
newTotal = self.total + howMuch newTotal = self.total + howMuch
+6
View File
@@ -31,3 +31,9 @@ extension CaseIterable where Self: Equatable {
self = allCases[nextIndex == allCases.endIndex ? allCases.startIndex : nextIndex] self = allCases[nextIndex == allCases.endIndex ? allCases.startIndex : nextIndex]
} }
} }
extension View {
func disabledWithOpacity(_ condition: Bool) -> some View {
self.disabled(condition).opacity(condition ? 0.4 : 1.0)
}
}
@@ -53,6 +53,49 @@ struct MainView: View {
} }
} }
@ViewBuilder private func rerollPicker(mode: Binding<RerollMode>) -> some View {
let actionBinding = Binding<RerollMode>(
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 { @ViewBuilder func buttons(color: CGColor, label: String, value: Int) -> some View {
let frameSize = 120.0 let frameSize = 120.0
let fontSize = 35.0 let fontSize = 35.0
@@ -79,25 +122,13 @@ struct MainView: View {
VStack { VStack {
HStack(alignment: .top) { HStack(alignment: .top) {
Spacer() Spacer()
VStack { statStepper(
Text("Armour") top: "Armour",
Group { value: (current.armourPenetration > 0 ? "+" : "") + String(current.armourPenetration),
Text("▲") bottom: "Penetration",
.gesture(TapGesture() .onEnded { increment: { current.addPenetration() },
current.addPenetration() decrement: { current.losePenetration() }
}) )
Text(
(current.armourPenetration > 0 ?
"+" : "") +
String(current.armourPenetration))
Text("▼")
.gesture(TapGesture() .onEnded {
current.losePenetration()
})
}
.font(.system(size: controlSize))
Text("Penetration")
}
Spacer() Spacer()
VStack { VStack {
Text("Invulnerable") Text("Invulnerable")
@@ -106,21 +137,7 @@ struct MainView: View {
} }
Spacer() Spacer()
} }
HStack { rerollPicker(mode: $current.saveRerollMode)
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 { if current.mortalWoundsTotal > 0 {
HStack { HStack {
Text("Mortal wounds: \(current.mortalWoundsTotal) (bypassed saves)") Text("Mortal wounds: \(current.mortalWoundsTotal) (bypassed saves)")
@@ -149,74 +166,32 @@ struct MainView: View {
} }
HStack(alignment: .top) { HStack(alignment: .top) {
Spacer() Spacer()
VStack { statStepper(
Text("Attack") top: "Attack",
Group { value: String(current.strength),
Text("▲") bottom: "Strength",
.gesture(TapGesture() .onEnded { increment: { current.addStrength() },
current.addStrength() decrement: { current.loseStrength() }
}) )
Text(String(current.strength))
Text("▼")
.gesture(TapGesture() .onEnded {
current.loseStrength()
})
}.font(.system(size: controlSize))
Text("Strength")
}
Spacer() Spacer()
VStack { statStepper(
Text("Toughness") top: "Toughness",
Group { value: String(current.toughness),
Text("▲") bottom: " ",
.gesture(TapGesture() .onEnded { increment: { current.addToughness() },
current.addToughness() decrement: { current.loseToughness() }
}) )
Text(String(current.toughness))
Text("▼")
.gesture(TapGesture() .onEnded {
current.loseToughness()
})
}.font(.system(size: controlSize))
Text(" ")
}
Spacer() Spacer()
VStack { statStepper(
Text("Wound") top: "Wound",
.multilineTextAlignment(.center) value: (current.woundModifier.rawValue > 0 ? "+" : "") + String(current.woundModifier.rawValue),
Group { bottom: "Modifier",
Text("▲") increment: { current.woundModifier = current.woundModifier.add() },
.gesture(TapGesture() .onEnded { decrement: { current.woundModifier = current.woundModifier.remove() }
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() Spacer()
} }
HStack { rerollPicker(mode: $current.woundRerollMode)
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) { VStack(spacing: 8) {
HStack { HStack {
Toggle("Devastating Wounds", isOn: $current.devastatingWounds) Toggle("Devastating Wounds", isOn: $current.devastatingWounds)
@@ -240,26 +215,14 @@ struct MainView: View {
VStack { VStack {
HStack(alignment: .top) { HStack(alignment: .top) {
Spacer() Spacer()
VStack { statStepper(
Text("Hit") top: "Hit",
.multilineTextAlignment(.center) value: (current.modifier.rawValue > 0 ? "+" : "") + String(current.modifier.rawValue),
Group { bottom: "Modifier",
Text("▲") increment: { current.modifier = current.modifier.add() },
.gesture(TapGesture() .onEnded { decrement: { current.modifier = current.modifier.remove() }
current.modifier = current.modifier.add() )
}) .disabledWithOpacity(current.torrent || current.psychic)
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() Spacer()
VStack { VStack {
Text("Blow Up") Text("Blow Up")
@@ -276,45 +239,38 @@ struct MainView: View {
.padding() .padding()
.font(.system(size: controlSize)) .font(.system(size: controlSize))
} }
.disabled(current.blowupUsed || current.torrent) .disabledWithOpacity(current.blowupUsed || current.torrent)
.opacity((current.blowupUsed || current.torrent) ? 0.4 : 1.0)
Spacer() Spacer()
} }
rerollPicker(mode: $current.hitRerollMode)
.disabledWithOpacity(current.torrent)
HStack { 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() Spacer()
Toggle("Psychic", isOn: $current.psychic) VStack {
.disabled(current.torrent) Text("Torrent")
.opacity(current.torrent ? 0.4 : 1.0) 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) .padding(.horizontal)
VStack(spacing: 8) { VStack(spacing: 8) {
HStack { HStack {
Toggle("Lethal Hits", isOn: $current.lethalHits)
Spacer()
Toggle("Sust. Hits", isOn: $current.sustainedHits) Toggle("Sust. Hits", isOn: $current.sustainedHits)
} .fixedSize()
if current.sustainedHits { Spacer()
HStack { if current.sustainedHits {
Text("X:") Text("X:")
Spacer()
Text("▲") Text("▲")
.gesture(TapGesture().onEnded { .gesture(TapGesture().onEnded {
if current.sustainedHitsX < 6 { current.sustainedHitsX += 1 } if current.sustainedHitsX < 6 { current.sustainedHitsX += 1 }
@@ -329,8 +285,9 @@ struct MainView: View {
} }
HStack { HStack {
Toggle("Anti-X", isOn: $current.antiX) Toggle("Anti-X", isOn: $current.antiX)
.fixedSize()
Spacer()
if current.antiX { if current.antiX {
Spacer()
Text("▲") Text("▲")
.gesture(TapGesture().onEnded { .gesture(TapGesture().onEnded {
if current.antiXThreshold < 6 { current.antiXThreshold += 1 } if current.antiXThreshold < 6 { current.antiXThreshold += 1 }
-20
View File
@@ -1,20 +0,0 @@
//
// 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()
}
}
+51 -11
View File
@@ -227,20 +227,30 @@ struct RerollTests {
#expect(roll.hitHasRerolled == true) #expect(roll.hitHasRerolled == true)
} }
@Test("applyReroll: guard blocks second call") @Test("applyReroll: re-applying restores snapshot and rerolls with new mode")
func applyRerollRespectsFlagAlreadySet() { func applyRerollChangeModeRestoresSnapshot() {
let roll = Roll() let roll = Roll()
roll.state = .hit roll.state = .hit
roll.hitCutoff = .four
roll.hitRerollMode = .ones
roll.dice.one = 3; roll.dice.six = 2
roll.total = 5
// First reroll: reroll the 3 ones
roll.applyReroll()
#expect(roll.hitHasRerolled == true)
#expect(roll.preRerollDice != nil)
let afterFirst = roll.dice.one + roll.dice.two + roll.dice.three +
roll.dice.four + roll.dice.five + roll.dice.six
#expect(afterFirst == 5)
// Switch to 'all' mode — should restore snapshot then reroll all
roll.hitRerollMode = .all roll.hitRerollMode = .all
roll.hitHasRerolled = true // simulate already used roll.applyReroll()
roll.dice.one = 5 // known state let afterSecond = roll.dice.one + roll.dice.two + roll.dice.three +
roll.applyReroll() // should be blocked by guard roll.dice.four + roll.dice.five + roll.dice.six
#expect(roll.dice.one == 5) #expect(afterSecond == 5)
#expect(roll.dice.two == 0) #expect(roll.hitHasRerolled == true)
#expect(roll.dice.three == 0)
#expect(roll.dice.four == 0)
#expect(roll.dice.five == 0)
#expect(roll.dice.six == 0)
} }
@Test("applyReroll: only sets flag for the active phase") @Test("applyReroll: only sets flag for the active phase")
@@ -269,6 +279,36 @@ struct RerollTests {
#expect(roll.dice.six >= 2) // dice above cutoff are untouched; rerolls may also produce sixes #expect(roll.dice.six >= 2) // dice above cutoff are untouched; rerolls may also produce sixes
} }
@Test("undoReroll: restores dice and clears flag")
func undoRerollRestoresDice() {
let roll = Roll()
roll.state = .hit
roll.hitCutoff = .four
roll.hitRerollMode = .ones
roll.dice.one = 3; roll.dice.six = 2
roll.total = 5
roll.applyReroll()
#expect(roll.hitHasRerolled == true)
roll.undoReroll()
#expect(roll.hitHasRerolled == false)
#expect(roll.preRerollDice == nil)
// Dice should be restored to original state
#expect(roll.dice.one == 3)
#expect(roll.dice.six == 2)
}
@Test("undoReroll: no-op when no reroll has been applied")
func undoRerollNoop() {
let roll = Roll()
roll.state = .hit
roll.dice.one = 3
roll.undoReroll() // nothing to undo
#expect(roll.dice.one == 3)
#expect(roll.hitHasRerolled == false)
}
@Test("resetPool: clears hitHasRerolled flag") @Test("resetPool: clears hitHasRerolled flag")
func resetPoolClearsFlag() { func resetPoolClearsFlag() {
let roll = Roll() let roll = Roll()