From 85c112580863d1301b96824c085b9cce53065e83 Mon Sep 17 00:00:00 2001 From: Jonathan Carifio Date: Fri, 13 Oct 2023 01:11:11 -0400 Subject: [PATCH 01/14] Add methods to spell slot status to obtain levels that satisfy various properties. --- Spellbook/SpellSlotStatus.swift | 53 ++++++++++++++++++++++++++------- Spellbook/Util.swift | 3 ++ 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/Spellbook/SpellSlotStatus.swift b/Spellbook/SpellSlotStatus.swift index d7451feb..3ebdf6d1 100644 --- a/Spellbook/SpellSlotStatus.swift +++ b/Spellbook/SpellSlotStatus.swift @@ -15,7 +15,7 @@ class SpellSlotStatus { private static let totalSlotsKey = "totalSlots" private static let usedSlotsKey = "usedSlotsKey" - + init(totalSlots: [Int], usedSlots: [Int]) { self.totalSlots = totalSlots self.usedSlots = usedSlots @@ -37,6 +37,9 @@ class SpellSlotStatus { func getUsedSlots(level: Int) -> Int { return usedSlots[level - 1] } func getAvailableSlots(level: Int) -> Int { return totalSlots[level - 1] - usedSlots[level - 1] } + func hasSlots(level: Int) -> Bool { return getTotalSlots(level: level) > 0 } + func hasAvailableSlots(level: Int) -> Bool { return getAvailableSlots(level: level) > 0 } + func setTotalSlots(level: Int, slots: Int) { totalSlots[level - 1] = slots if (slots < usedSlots[level - 1]) { @@ -48,37 +51,65 @@ class SpellSlotStatus { let used = totalSlots[level - 1] - slots usedSlots[level - 1] = max(0, used) } - + func setUsedSlots(level: Int, slots: Int) { usedSlots[level - 1] = min(slots, totalSlots[level - 1]) } - + func regainAllSlots() { usedSlots = usedSlots.map { _ in 0 } } - + func useSlot(level: Int) { usedSlots[level - 1] = min(usedSlots[level - 1] + 1, totalSlots[level - 1]) } - + func gainSlot(level: Int) { usedSlots[level - 1] = max(usedSlots[level - 1] - 1, 0) } - - func maxLevelWithSlots() -> Int { - for level in Spellbook.MAX_SPELL_LEVEL...1 { - if (self.getTotalSlots(level: level) > 0) { + + func levelWithCondition(condition: Predicate, range: ClosedRange) -> Int { + for level in range { + if condition(level) { return level } } return 0 } - + + func minLevelWithCondition(condition: Predicate) -> Int { + return levelWithCondition(condition: condition, range: Spellbook.MIN_SPELL_LEVEL...Spellbook.MAX_SPELL_LEVEL) + } + + func maxLevelWithCondition(condition: Predicate) -> Int { + return levelWithCondition(condition: condition, range: Spellbook.MAX_SPELL_LEVEL...Spellbook.MIN_SPELL_LEVEL) + } + + func minLevelWithSlots() -> Int { + return minLevelWithCondition(condition: self.hasSlots) + } + + func maxLevelWithSlots() -> Int { + return maxLevelWithCondition(condition: self.hasSlots) + } + + func minLevelWithAvailableSlots() -> Int { + return minLevelWithCondition(condition: self.hasAvailableSlots) + } + + func maxLevelWithAvailableSlots() -> Int { + return maxLevelWithCondition(condition: self.hasAvailableSlots) + } + + func nextAvailableSlotLevel(baseLevel: Int) -> Int { + return levelWithCondition(condition: self.hasAvailableSlots, range: baseLevel...Spellbook.MAX_SPELL_LEVEL) + } + func toSION() -> SION { var sion: SION = [:] sion[SpellSlotStatus.totalSlotsKey].array = totalSlots.map { SION($0) } sion[SpellSlotStatus.usedSlotsKey].array = usedSlots.map { SION($0) } return sion } - + } diff --git a/Spellbook/Util.swift b/Spellbook/Util.swift index 6a390549..301a28a8 100644 --- a/Spellbook/Util.swift +++ b/Spellbook/Util.swift @@ -1,3 +1,6 @@ +// There isn't a proper Predicate type until iOS 17 +typealias Predicate = (T) -> Bool + func yn_to_bool(yn: String) throws -> Bool { if yn == "no" { return false From d788f38bb406eb1f52602f9b82b81a7dcb4b2976 Mon Sep 17 00:00:00 2001 From: Jonathan Carifio Date: Fri, 13 Oct 2023 01:32:26 -0400 Subject: [PATCH 02/14] Add action and reducer for casting a spell of a given level. --- Spellbook/AppReducer.swift | 3 +++ Spellbook/SpellbookActions.swift | 4 ++++ Spellbook/SpellbookReducers.swift | 7 +++++++ 3 files changed, 14 insertions(+) diff --git a/Spellbook/AppReducer.swift b/Spellbook/AppReducer.swift index b36b315f..9031b1fd 100644 --- a/Spellbook/AppReducer.swift +++ b/Spellbook/AppReducer.swift @@ -168,6 +168,9 @@ func specificActionReducer(action: Action, state: inout SpellbookAppState) -> Sp case let action as SaveSettingsAction: return saveSettingsReducer(action: action, state: &state) + case let action as CastSpellAction: + return castSpellReducer(action: action, state: state) + // If we somehow get here, just do nothing default: return state diff --git a/Spellbook/SpellbookActions.swift b/Spellbook/SpellbookActions.swift index 88a49c05..7d831003 100644 --- a/Spellbook/SpellbookActions.swift +++ b/Spellbook/SpellbookActions.swift @@ -229,3 +229,7 @@ struct RegainAllSlotsAction: Action {} // TODO: Is there a better way to do this? struct MarkAllSpellsCleanAction: Action {} + +struct CastSpellAction: Action { + let level: Int +} diff --git a/Spellbook/SpellbookReducers.swift b/Spellbook/SpellbookReducers.swift index eb3ba6aa..1d69f4ab 100644 --- a/Spellbook/SpellbookReducers.swift +++ b/Spellbook/SpellbookReducers.swift @@ -351,3 +351,10 @@ func saveSettingsReducer(action: SaveSettingsAction, state: inout SpellbookAppSt SerializationUtils.saveSettings(state.settings) return state } + +func castSpellReducer(action: CastSpellAction, state: SpellbookAppState) -> SpellbookAppState { + guard let profile = state.profile else { return state } + let status = profile.spellSlotStatus + status.useSlot(level: action.level) + return state +} From 1968f5a91679ba1ab101d835074cef7eff7cc997 Mon Sep 17 00:00:00 2001 From: Jonathan Carifio Date: Sat, 14 Oct 2023 01:26:28 -0400 Subject: [PATCH 03/14] Add cast button to spell window controller. --- Spellbook/Base.lproj/Main.storyboard | 41 +++++++++++++++++---------- Spellbook/SpellWindowController.swift | 3 ++ 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/Spellbook/Base.lproj/Main.storyboard b/Spellbook/Base.lproj/Main.storyboard index d2cd761a..42e2c8a1 100644 --- a/Spellbook/Base.lproj/Main.storyboard +++ b/Spellbook/Base.lproj/Main.storyboard @@ -501,6 +501,14 @@ + @@ -513,6 +521,7 @@ + @@ -540,6 +549,7 @@ + @@ -580,6 +590,7 @@ + @@ -1253,7 +1264,7 @@ - + @@ -1324,14 +1335,14 @@ - + - + @@ -1406,7 +1417,7 @@ @@ -1691,7 +1702,7 @@ - + @@ -1749,7 +1760,7 @@ - + @@ -1839,7 +1850,7 @@ - + @@ -1898,7 +1909,7 @@ - + @@ -1985,7 +1996,7 @@ - + @@ -2136,7 +2147,7 @@ - + @@ -2258,7 +2269,7 @@ - + @@ -2372,7 +2383,7 @@ - + @@ -2482,7 +2493,7 @@ - + @@ -2626,7 +2637,7 @@ - + @@ -2765,7 +2776,7 @@ - + diff --git a/Spellbook/SpellWindowController.swift b/Spellbook/SpellWindowController.swift index 5c542cf1..0a6e7e5a 100644 --- a/Spellbook/SpellWindowController.swift +++ b/Spellbook/SpellWindowController.swift @@ -60,6 +60,9 @@ class SpellWindowController: UIViewController { @IBOutlet weak var preparedButton: ToggleButton! @IBOutlet weak var knownButton: ToggleButton! + // The cast button + @IBOutlet weak var castButton: UIButton! + @IBOutlet weak var backgroundView: UIImageView! // Spacing constraints for the spacing between the components/materials/royalties/duration labels From b9672ec09eb9cfbf11880d393966db891c79f2b6 Mon Sep 17 00:00:00 2001 From: Jonathan Carifio Date: Sat, 14 Oct 2023 01:27:37 -0400 Subject: [PATCH 04/14] Add filter to text field chooser delegate. Make base class more general and add iterable specialization. --- Spellbook/TextFieldChooserDelegate.swift | 52 ++++++++++++++++++------ 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/Spellbook/TextFieldChooserDelegate.swift b/Spellbook/TextFieldChooserDelegate.swift index bd7ea866..ac47e094 100644 --- a/Spellbook/TextFieldChooserDelegate.swift +++ b/Spellbook/TextFieldChooserDelegate.swift @@ -10,31 +10,34 @@ import ReSwift import UIKit import CoreActionSheetPicker -class TextFieldChooserDelegate: NSObject, UITextFieldDelegate { +class TextFieldChooserDelegate: NSObject, UITextFieldDelegate { typealias ActionCreator = (T) -> A typealias ItemProvider = () -> T typealias StringGetter = (T) -> String typealias StringConstructor = (String) -> T + typealias ItemFilter = (T) -> Bool let title: String let main = Controllers.mainController let itemProvider: ItemProvider - let pickerData: [String] + let items: [T] let actionCreator: ActionCreator let nameGetter: StringGetter let textSetter: StringGetter let nameConstructor: StringConstructor + let itemFilter: ItemFilter? - init(itemProvider: @escaping ItemProvider, actionCreator: @escaping ActionCreator, nameGetter: @escaping StringGetter, textSetter: @escaping StringGetter, nameConstructor: @escaping StringConstructor, title: String) { + init(items: [T], title: String, itemProvider: @escaping ItemProvider, actionCreator: @escaping ActionCreator, nameGetter: @escaping StringGetter, textSetter: @escaping StringGetter, nameConstructor: @escaping StringConstructor, itemFilter: ItemFilter? = nil) { + self.items = items self.itemProvider = itemProvider self.actionCreator = actionCreator self.nameGetter = nameGetter self.textSetter = textSetter self.nameConstructor = nameConstructor + self.itemFilter = itemFilter self.title = title - pickerData = T.allCases.map({ nameGetter($0) }) } @@ -47,7 +50,14 @@ class TextFieldChooserDelegate: NSObject, // Get the index of the selected option let selectedItem = self.itemProvider() - let selectedIndex = T.allCases.firstIndex(of: selectedItem) as! Int + let selectedIndex: Int = items.firstIndex(of: selectedItem) ?? 0 + + var itemsToUse = self.items + if (self.itemFilter != nil) { + itemsToUse = self.items.filter(self.itemFilter!) + } + let pickerData = itemsToUse.map(self.nameGetter) + // Create the action sheet picker let actionSheetPicker = ActionSheetStringPicker(title: title, @@ -70,26 +80,43 @@ class TextFieldChooserDelegate: NSObject, } +class TextFieldIterableChooserDelegate: TextFieldChooserDelegate { + + init(title: String, itemProvider: @escaping ItemProvider, actionCreator: @escaping ActionCreator, nameGetter: @escaping StringGetter, textSetter: @escaping StringGetter, nameConstructor: @escaping StringConstructor, itemFilter: ItemFilter? = nil) { + let items = T.allCases.map({ $0 }) + super.init(items: items, + title: title, + itemProvider: itemProvider, + actionCreator: actionCreator, + nameGetter: nameGetter, + textSetter: textSetter, + nameConstructor: nameConstructor, + itemFilter: itemFilter) + } + +} + // Specific cases for types that implement NameConstructible and Unit protocols // These didn't seem worth their own files, since all that needs to be overwritten is the constructor // because the name getter and constructor come directly from the protocol -class NameConstructibleChooserDelegate: TextFieldChooserDelegate { +class NameConstructibleChooserDelegate: TextFieldIterableChooserDelegate { init(itemProvider: @escaping ItemProvider, actionCreator: @escaping ActionCreator, title: String) { - super.init(itemProvider: itemProvider, + super.init(title: title, + itemProvider: itemProvider, actionCreator: actionCreator, nameGetter: { $0.displayName }, textSetter: { $0.displayName }, - nameConstructor: { return N.fromName($0) }, - title: title) + nameConstructor: { return N.fromName($0) }) } } -class UnitChooserDelegate : TextFieldChooserDelegate { +class UnitChooserDelegate : TextFieldIterableChooserDelegate { init(itemProvider: @escaping ItemProvider, actionCreator: @escaping ActionCreator, title: String) { - super.init(itemProvider: itemProvider, + super.init(title: title, + itemProvider: itemProvider, actionCreator: actionCreator, nameGetter: { $0.pluralName }, textSetter: SizeUtils.unitTextGetter(U.self), @@ -99,7 +126,6 @@ class UnitChooserDelegate : TextFieldChooserDelegate { } catch { return U.defaultUnit } - }, - title: title) + }) } } From 3155899b350173758f284bb2f84dbcf10071140d Mon Sep 17 00:00:00 2001 From: Jonathan Carifio Date: Mon, 16 Oct 2023 01:09:40 -0400 Subject: [PATCH 05/14] More work on setting up spell casting dialogs. --- Spellbook/AppDelegate.swift | 2 +- Spellbook/Base.lproj/Main.storyboard | 86 +++++++++++++++++++++++ Spellbook/DeletionPromptController.swift | 8 +-- Spellbook/HigherLevelSlotController.swift | 67 ++++++++++++++++++ Spellbook/SpellWindowController.swift | 9 +++ Spellbook/SpellbookActions.swift | 6 ++ Spellbook/SpellbookMiddleware.swift | 13 ++++ Spellbook/TextFieldChooserDelegate.swift | 14 ++-- Spellbook/Util.swift | 28 ++++++++ 9 files changed, 221 insertions(+), 12 deletions(-) create mode 100644 Spellbook/HigherLevelSlotController.swift diff --git a/Spellbook/AppDelegate.swift b/Spellbook/AppDelegate.swift index 4424842e..f9fbc1a8 100644 --- a/Spellbook/AppDelegate.swift +++ b/Spellbook/AppDelegate.swift @@ -10,7 +10,7 @@ import UIKit import ReSwift typealias SpellbookStore = Store -let store = SpellbookStore(reducer: appReducer, state: nil, middleware: [switchProfileMiddleware, switchProfileByNameMiddleware, createProfileMiddleware, saveProfileMiddleware, saveCurrentProfileMiddleware, deleteProfileMiddleware, deleteProfileByNameMiddleware]) +let store = SpellbookStore(reducer: appReducer, state: nil, middleware: [switchProfileMiddleware, switchProfileByNameMiddleware, createProfileMiddleware, saveProfileMiddleware, saveCurrentProfileMiddleware, deleteProfileMiddleware, deleteProfileByNameMiddleware, makeToastMiddleware]) @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { diff --git a/Spellbook/Base.lproj/Main.storyboard b/Spellbook/Base.lproj/Main.storyboard index 42e2c8a1..dc084a7a 100644 --- a/Spellbook/Base.lproj/Main.storyboard +++ b/Spellbook/Base.lproj/Main.storyboard @@ -769,6 +769,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Spellbook/DeletionPromptController.swift b/Spellbook/DeletionPromptController.swift index c2f3e28c..c89ab80a 100644 --- a/Spellbook/DeletionPromptController.swift +++ b/Spellbook/DeletionPromptController.swift @@ -24,14 +24,12 @@ class DeletionPromptController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - - // Set the text + deleteMessage.text = "Are you sure you want to delete \(name ?? "")?" - + // Set the layout //setLayout() - - // Set the button functions + noButton.addTarget(self, action: #selector(noButtonPressed), for: UIControl.Event.touchUpInside) yesButton.addTarget(self, action: #selector(yesButtonPressed), for: UIControl.Event.touchUpInside) diff --git a/Spellbook/HigherLevelSlotController.swift b/Spellbook/HigherLevelSlotController.swift new file mode 100644 index 00000000..0a606d49 --- /dev/null +++ b/Spellbook/HigherLevelSlotController.swift @@ -0,0 +1,67 @@ +// +// HigherLevelSlotController.swift +// Spellbook +// +// Created by Mac Pro on 10/14/23. +// Copyright © 2023 Jonathan Carifio. All rights reserved. +// + +import UIKit + +import ReSwift + +class HigherLevelSlotController: UIViewController { + + @IBOutlet weak var slotLevelChooser: UITextField! + @IBOutlet weak var cancelButton: UIButton! + @IBOutlet weak var castButton: UIButton! + + var spell: Spell? + + override func viewDidLoad() { + super.viewDidLoad() + + cancelButton.addTarget(self, action: #selector(cancelButtonPressed), for: UIControl.Event.touchUpInside) + castButton.addTarget(self, action: #selector(castButtonPressed), for: UIControl.Event.touchUpInside) + + guard let spell = self.spell else { return } + guard let profile = store.state.profile else { return } + let status = profile.spellSlotStatus + let baseLevel = spell.level + let maxLevel = status.maxLevelWithSlots() + let range = baseLevel...maxLevel + + // TODO: It's kind of gross to need to use this dummy type + // It feels like a refactor of the delegate is necessary + let textDelegate = TextFieldChooserDelegate( + items: Array(range), + title: "Select Slot Level", + itemProvider: { + status.minLevelWithCondition(condition: { level in + return status.hasAvailableSlots(level: level) && level >= baseLevel + }) + }, + nameGetter: ordinal, + textSetter: ordinal, + nameConstructor: { valueFrom(ordinal: $0) ?? 0 }) + + slotLevelChooser.delegate = textDelegate + } + + @objc func cancelButtonPressed() { + self.dismiss(animated: true, completion: nil) + } + + @objc func castButtonPressed() { + guard let spell = self.spell else { return } + if let text = slotLevelChooser.text { + if let level = valueFrom(ordinal: text) { + store.dispatch(CastSpellAction(level: level)) + store.dispatch(ToastAction(message: "\(spell.name) was cast at level \(level)")) + } + } + + self.dismiss(animated: true, completion: nil) + } + +} diff --git a/Spellbook/SpellWindowController.swift b/Spellbook/SpellWindowController.swift index 0a6e7e5a..6bf47a51 100644 --- a/Spellbook/SpellWindowController.swift +++ b/Spellbook/SpellWindowController.swift @@ -107,6 +107,8 @@ class SpellWindowController: UIViewController { store.dispatch(TogglePropertyAction(spell: self.spell, property: .Known)) }) + castButton.addTarget(self, action: #selector(self.onCastClicked), for: UIControl.Event.touchUpInside) + // Set the content view to fill the screen contentView.frame = UIScreen.main.bounds @@ -221,6 +223,13 @@ class SpellWindowController: UIViewController { func locationText(_ s: Spell) -> String { return s.locations.map { $0.key.code.uppercased() + " " + String($0.value) }.joined(separator: ", ") } + + @objc func onCastClicked() { + guard let profile = store.state.profile else { return } + let level = spell.level + let status = profile.spellSlotStatus + + } } diff --git a/Spellbook/SpellbookActions.swift b/Spellbook/SpellbookActions.swift index 7d831003..b0835662 100644 --- a/Spellbook/SpellbookActions.swift +++ b/Spellbook/SpellbookActions.swift @@ -8,6 +8,8 @@ import ReSwift +struct GenericSpellbookAction: Action {} + struct SortFieldAction: Action { let sortField: SortField let level: Int @@ -233,3 +235,7 @@ struct MarkAllSpellsCleanAction: Action {} struct CastSpellAction: Action { let level: Int } + +struct ToastAction: Action { + let message: String +} diff --git a/Spellbook/SpellbookMiddleware.swift b/Spellbook/SpellbookMiddleware.swift index ec70276a..8688e2cb 100644 --- a/Spellbook/SpellbookMiddleware.swift +++ b/Spellbook/SpellbookMiddleware.swift @@ -152,3 +152,16 @@ let deleteProfileByNameMiddleware: AppMiddleware = { } } } + +let makeToastMiddleware: AppMiddleware = { + dispatch, getState in + return { next in + return { action in + guard let toastAction = action as? ToastAction else { + next(action) + return + } + Toast.makeToast(toastAction.message) + } + } +} diff --git a/Spellbook/TextFieldChooserDelegate.swift b/Spellbook/TextFieldChooserDelegate.swift index ac47e094..83ade282 100644 --- a/Spellbook/TextFieldChooserDelegate.swift +++ b/Spellbook/TextFieldChooserDelegate.swift @@ -23,13 +23,13 @@ class TextFieldChooserDelegate: NSObject, UITextFieldDel let itemProvider: ItemProvider let items: [T] - let actionCreator: ActionCreator + let actionCreator: ActionCreator? let nameGetter: StringGetter let textSetter: StringGetter let nameConstructor: StringConstructor let itemFilter: ItemFilter? - init(items: [T], title: String, itemProvider: @escaping ItemProvider, actionCreator: @escaping ActionCreator, nameGetter: @escaping StringGetter, textSetter: @escaping StringGetter, nameConstructor: @escaping StringConstructor, itemFilter: ItemFilter? = nil) { + init(items: [T], title: String, itemProvider: @escaping ItemProvider, actionCreator: ActionCreator? = nil, nameGetter: @escaping StringGetter, textSetter: @escaping StringGetter, nameConstructor: @escaping StringConstructor, itemFilter: ItemFilter? = nil) { self.items = items self.itemProvider = itemProvider self.actionCreator = actionCreator @@ -67,7 +67,9 @@ class TextFieldChooserDelegate: NSObject, UITextFieldDel picker, index, value in let valueStr = value as! String let item = self.nameConstructor(valueStr) - store.dispatch(self.actionCreator(item)) + if let creator = self.actionCreator { + store.dispatch(creator(item)) + } sender.text = self.textSetter(item) sender.endEditing(true) return @@ -82,7 +84,7 @@ class TextFieldChooserDelegate: NSObject, UITextFieldDel class TextFieldIterableChooserDelegate: TextFieldChooserDelegate { - init(title: String, itemProvider: @escaping ItemProvider, actionCreator: @escaping ActionCreator, nameGetter: @escaping StringGetter, textSetter: @escaping StringGetter, nameConstructor: @escaping StringConstructor, itemFilter: ItemFilter? = nil) { + init(title: String, itemProvider: @escaping ItemProvider, actionCreator: ActionCreator? = nil, nameGetter: @escaping StringGetter, textSetter: @escaping StringGetter, nameConstructor: @escaping StringConstructor, itemFilter: ItemFilter? = nil) { let items = T.allCases.map({ $0 }) super.init(items: items, title: title, @@ -103,7 +105,7 @@ class TextFieldIterableChooserDelegate: Te class NameConstructibleChooserDelegate: TextFieldIterableChooserDelegate { - init(itemProvider: @escaping ItemProvider, actionCreator: @escaping ActionCreator, title: String) { + init(itemProvider: @escaping ItemProvider, actionCreator: ActionCreator? = nil, title: String) { super.init(title: title, itemProvider: itemProvider, actionCreator: actionCreator, @@ -114,7 +116,7 @@ class NameConstructibleChooserDelegate: TextField } class UnitChooserDelegate : TextFieldIterableChooserDelegate { - init(itemProvider: @escaping ItemProvider, actionCreator: @escaping ActionCreator, title: String) { + init(itemProvider: @escaping ItemProvider, actionCreator: ActionCreator? = nil, title: String) { super.init(title: title, itemProvider: itemProvider, actionCreator: actionCreator, diff --git a/Spellbook/Util.swift b/Spellbook/Util.swift index 301a28a8..88fa68e6 100644 --- a/Spellbook/Util.swift +++ b/Spellbook/Util.swift @@ -91,3 +91,31 @@ func complement(items: [T]) -> [T] { func arrayDifference(array arr: [T], remove: [T]) -> [T] { return arr.filter { !remove.contains($0) } } + +func ordinal(number: Int) -> String { + switch number { + case 1: + return "1st" + case 2: + return "2nd" + case 3: + return "3rd" + default: + return "\(number)th" + } +} + +func valueFrom(ordinal: String) -> Int? { + if ordinal == "1st" { + return 1 + } else if ordinal == "2nd" { + return 2 + } else if ordinal == "3rd" { + return 3 + } else if ordinal.hasSuffix("th") { + let numberString = ordinal.dropLast(2) + return Int(numberString) + } else { + return nil + } +} From a400ce55b0de9526fe426fc4c678b4604b208fde Mon Sep 17 00:00:00 2001 From: Jonathan Carifio Date: Wed, 25 Oct 2023 01:41:07 -0400 Subject: [PATCH 06/14] Add confirm upcasting controller. More progress on casting functionality. --- Spellbook.xcodeproj/project.pbxproj | 8 ++ Spellbook/Base.lproj/Main.storyboard | 78 ++++++++++++++++++- .../ConfirmNextAvailableCastController.swift | 54 +++++++++++++ Spellbook/DeletionPromptController.swift | 11 --- Spellbook/HigherLevelSlotController.swift | 2 +- Spellbook/SpellSlotStatus.swift | 6 +- Spellbook/SpellWindowController.swift | 35 +++++++++ Spellbook/Toast.swift | 9 ++- 8 files changed, 185 insertions(+), 18 deletions(-) create mode 100644 Spellbook/ConfirmNextAvailableCastController.swift diff --git a/Spellbook.xcodeproj/project.pbxproj b/Spellbook.xcodeproj/project.pbxproj index 4aef47d6..6cd84cdb 100644 --- a/Spellbook.xcodeproj/project.pbxproj +++ b/Spellbook.xcodeproj/project.pbxproj @@ -138,6 +138,8 @@ 8ED0B9382431392700A085F4 /* NameDisplayable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8ED0B9372431392700A085F4 /* NameDisplayable.swift */; }; 8EEA28FD297C23D8004971AD /* SpellFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EEA28FC297C23D8004971AD /* SpellFilter.swift */; }; 8EEAFCCA2645FB4C00A05D01 /* LegacyProfileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EEAFCC92645FB4C00A05D01 /* LegacyProfileTests.swift */; }; + 8EEDC3E22ADA69170045D002 /* HigherLevelSlotController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EEDC3E12ADA69170045D002 /* HigherLevelSlotController.swift */; }; + 8EEDC3E42AE739A60045D002 /* ConfirmNextAvailableCastController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EEDC3E32AE739A50045D002 /* ConfirmNextAvailableCastController.swift */; }; 8EF6EF0B264668F60059BCE8 /* Version.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EF6EF0A264668F60059BCE8 /* Version.swift */; }; 8EF6EF0D26485A2E0059BCE8 /* VersionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EF6EF0C26485A2E0059BCE8 /* VersionTests.swift */; }; /* End PBXBuildFile section */ @@ -295,6 +297,8 @@ 8ED0B9372431392700A085F4 /* NameDisplayable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NameDisplayable.swift; sourceTree = ""; }; 8EEA28FC297C23D8004971AD /* SpellFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpellFilter.swift; sourceTree = ""; }; 8EEAFCC92645FB4C00A05D01 /* LegacyProfileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyProfileTests.swift; sourceTree = ""; }; + 8EEDC3E12ADA69170045D002 /* HigherLevelSlotController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HigherLevelSlotController.swift; sourceTree = ""; }; + 8EEDC3E32AE739A50045D002 /* ConfirmNextAvailableCastController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmNextAvailableCastController.swift; sourceTree = ""; }; 8EF6EF0A264668F60059BCE8 /* Version.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Version.swift; sourceTree = ""; }; 8EF6EF0C26485A2E0059BCE8 /* VersionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -359,6 +363,7 @@ isa = PBXGroup; children = ( 397EE96C21ACA86400B116B0 /* Main.storyboard */, + 8EEDC3E12ADA69170045D002 /* HigherLevelSlotController.swift */, 8E07AE0C2A0F708700C0487B /* ImportCharacterController.swift */, 8EB3692129405EAB00856738 /* SpellbookReducers.swift */, 8EB3691D293F123900856738 /* AppReducer.swift */, @@ -384,6 +389,7 @@ 8E6148D7241991C100842A18 /* Constants.swift */, 8E6148C3241991C000842A18 /* Controllers.swift */, 8E6148A5241991BF00842A18 /* DeletionPromptController.swift */, + 8EEDC3E32AE739A50045D002 /* ConfirmNextAvailableCastController.swift */, 8E61489E241991BE00842A18 /* Duration.swift */, 8E6148DE241991C100842A18 /* EnumMap.swift */, 8E6148C5241991C000842A18 /* EnumUtilities.swift */, @@ -772,9 +778,11 @@ 8E6E2B8228E9FE4A00AA6483 /* SpellSlotsController.swift in Sources */, 8EB3691E293F123900856738 /* AppReducer.swift in Sources */, 8E6148F8241991C200842A18 /* StringUtils.swift in Sources */, + 8EEDC3E22ADA69170045D002 /* HigherLevelSlotController.swift in Sources */, 8E614927241991C200842A18 /* SpellBuilder.swift in Sources */, 8E02F76224345F6200A3050C /* YesNo.swift in Sources */, 8E07AE0D2A0F708700C0487B /* ImportCharacterController.swift in Sources */, + 8EEDC3E42AE739A60045D002 /* ConfirmNextAvailableCastController.swift in Sources */, 8EEA28FD297C23D8004971AD /* SpellFilter.swift in Sources */, 8E614916241991C200842A18 /* SideMenuController.swift in Sources */, 8E74FA5A29EBD7D900E13B91 /* SpellSlotsManagerCell.swift in Sources */, diff --git a/Spellbook/Base.lproj/Main.storyboard b/Spellbook/Base.lproj/Main.storyboard index dc084a7a..be1376df 100644 --- a/Spellbook/Base.lproj/Main.storyboard +++ b/Spellbook/Base.lproj/Main.storyboard @@ -16,6 +16,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -508,6 +576,14 @@ + + + + + + + + @@ -772,7 +848,7 @@ - + diff --git a/Spellbook/ConfirmNextAvailableCastController.swift b/Spellbook/ConfirmNextAvailableCastController.swift new file mode 100644 index 00000000..f04f8c5e --- /dev/null +++ b/Spellbook/ConfirmNextAvailableCastController.swift @@ -0,0 +1,54 @@ +// +// ConfirmNextAvailableCastController.swift +// Spellbook +// +// Created by Mac Pro on 10/23/23. +// Copyright © 2023 Jonathan Carifio. All rights reserved. +// + +import UIKit + +class ConfirmNextAvailableCastController: UIViewController { + + @IBOutlet weak var availableCastMessage: UILabel! + @IBOutlet weak var yesButton: UIButton! + @IBOutlet weak var noButton: UIButton! + + var spell: Spell? + var level: Int = 0 + + override func viewDidLoad() { + super.viewDidLoad() + + // We should never be here without an assigned profile + let name = store.state.profile?.name ?? "" + let baseLevel = spell?.level ?? 0 + + availableCastMessage.text = "\(name) has no slots of level \(baseLevel) remaining. Would you like to use a slot of level \(level)?" + + noButton.addTarget(self, action: #selector(noButtonPressed), for: UIControl.Event.touchUpInside) + yesButton.addTarget(self, action: #selector(yesButtonPressed), for: UIControl.Event.touchUpInside) + + // Do any additional setup after loading the view. + } + + @objc func noButtonPressed() { + self.dismiss(animated: true, completion: nil) + } + + @objc func yesButtonPressed() { + self.dismiss(animated: true, completion: { () -> Void in + + if self.level == 0 { + return + } + if let spell = self.spell { + store.dispatch(CastSpellAction(level: self.level)) + store.dispatch(ToastAction(message: "\(spell.name) was cast at level \(self.level)")) + } + }) + } + +} + + diff --git a/Spellbook/DeletionPromptController.swift b/Spellbook/DeletionPromptController.swift index c89ab80a..930232c7 100644 --- a/Spellbook/DeletionPromptController.swift +++ b/Spellbook/DeletionPromptController.swift @@ -93,16 +93,5 @@ class DeletionPromptController: UIViewController { self.main!.openCharacterCreationDialog(mustComplete: creationNecessary) }) } - - - /* - // MARK: - Navigation - - // In a storyboard-based application, you will often want to do a little preparation before navigation - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - // Get the new view controller using segue.destination. - // Pass the selected object to the new view controller. - } - */ } diff --git a/Spellbook/HigherLevelSlotController.swift b/Spellbook/HigherLevelSlotController.swift index 0a606d49..f6760b77 100644 --- a/Spellbook/HigherLevelSlotController.swift +++ b/Spellbook/HigherLevelSlotController.swift @@ -37,7 +37,7 @@ class HigherLevelSlotController: UIViewController { items: Array(range), title: "Select Slot Level", itemProvider: { - status.minLevelWithCondition(condition: { level in + return status.minLevelWithCondition(condition: { level in return status.hasAvailableSlots(level: level) && level >= baseLevel }) }, diff --git a/Spellbook/SpellSlotStatus.swift b/Spellbook/SpellSlotStatus.swift index 3ebdf6d1..9ce985f5 100644 --- a/Spellbook/SpellSlotStatus.swift +++ b/Spellbook/SpellSlotStatus.swift @@ -68,7 +68,7 @@ class SpellSlotStatus { usedSlots[level - 1] = max(usedSlots[level - 1] - 1, 0) } - func levelWithCondition(condition: Predicate, range: ClosedRange) -> Int { + func levelWithCondition(condition: Predicate, range: SeqInt) -> Int where SeqInt.Iterator.Element == Int { for level in range { if condition(level) { return level @@ -78,11 +78,11 @@ class SpellSlotStatus { } func minLevelWithCondition(condition: Predicate) -> Int { - return levelWithCondition(condition: condition, range: Spellbook.MIN_SPELL_LEVEL...Spellbook.MAX_SPELL_LEVEL) + return levelWithCondition(condition: condition, range: 1...Spellbook.MAX_SPELL_LEVEL) } func maxLevelWithCondition(condition: Predicate) -> Int { - return levelWithCondition(condition: condition, range: Spellbook.MAX_SPELL_LEVEL...Spellbook.MIN_SPELL_LEVEL) + return levelWithCondition(condition: condition, range: (1...Spellbook.MAX_SPELL_LEVEL).reversed()) } func minLevelWithSlots() -> Int { diff --git a/Spellbook/SpellWindowController.swift b/Spellbook/SpellWindowController.swift index 6bf47a51..8bc7b93c 100644 --- a/Spellbook/SpellWindowController.swift +++ b/Spellbook/SpellWindowController.swift @@ -146,6 +146,9 @@ class SpellWindowController: UIViewController { var needsLayoutUpdate = false + // Don't show the cast button for cantrips + castButton.isHidden = spell.level == 0 + // Set the text on the name label spellNameLabel.text = spell.name @@ -228,7 +231,39 @@ class SpellWindowController: UIViewController { guard let profile = store.state.profile else { return } let level = spell.level let status = profile.spellSlotStatus + var message: String? + if level > status.maxLevelWithSlots() { + message = "\(profile.name) has no slots of level \(level) or above!" + } else if level > status.maxLevelWithAvailableSlots() { + message = "\(profile.name) has no available slots of level \(level) or above!" + } else if !spell.higherLevel.isEmpty { + let controller = storyboard?.instantiateViewController(withIdentifier: "higherLevelSlotDialog") as! HigherLevelSlotController + controller.spell = spell + let popupHeight = 0.33 * SizeUtils.screenHeight + let popupWidth = 0.75 * SizeUtils.screenWidth + let height = min(popupHeight, CGFloat(320)) + let width = min(popupWidth, CGFloat(370)) + let popupVC = PopupViewController(contentController: controller, popupWidth: width, popupHeight: height) + self.present(popupVC, animated: true, completion: nil) + } else if status.getAvailableSlots(level: level) == 0 { + let levelToUse = status.nextAvailableSlotLevel(baseLevel: level) + let controller = storyboard?.instantiateViewController(withIdentifier: "confirmNextAvailableCast") as! ConfirmNextAvailableCastController + controller.spell = spell + controller.level = levelToUse + let popupHeight = 0.33 * SizeUtils.screenHeight + let popupWidth = 0.75 * SizeUtils.screenWidth + let height = min(popupHeight, CGFloat(250)) + let width = min(popupWidth, CGFloat(370)) + let popupVC = PopupViewController(contentController: controller, popupWidth: width, popupHeight: height) + self.present(popupVC, animated: true, completion: nil) + } else { + store.dispatch(CastSpellAction(level: level)) + message = "\(spell.name) was cast" + } + if let toast = message { + self.view.makeToast(toast, duration: Constants.toastDuration) + } } } diff --git a/Spellbook/Toast.swift b/Spellbook/Toast.swift index 388dd120..1d436d32 100644 --- a/Spellbook/Toast.swift +++ b/Spellbook/Toast.swift @@ -11,10 +11,15 @@ import Toast class Toast { - // We make our Toast messages through the main ViewController of the app + static func makeToast(_ message: String, duration: TimeInterval = Constants.toastDuration, controller: UIViewController) { + controller.view.makeToast(message, duration: duration) + } + + // By default, we make our Toast messages through the main ViewController of the app // which is the SWRevealController // If we ever change this, we only need to change it here static func makeToast(_ message: String, duration: TimeInterval = Constants.toastDuration) { - Controllers.revealController.view.makeToast(message, duration: duration) + makeToast(message, duration: duration, controller: Controllers.revealController) } + } From 8d885ecc44e7dfe7c62532e936c4fbebaf18660c Mon Sep 17 00:00:00 2001 From: Jonathan Carifio Date: Sat, 28 Oct 2023 19:14:46 -0400 Subject: [PATCH 07/14] Finish basic setup of confirm upcasting controller. --- Spellbook/Base.lproj/Main.storyboard | 51 +++++++++++-------- .../ConfirmNextAvailableCastController.swift | 8 ++- Spellbook/HigherLevelSlotController.swift | 29 ++++++----- Spellbook/SpellWindowController.swift | 10 ++-- Spellbook/SpellbookMiddleware.swift | 1 + 5 files changed, 59 insertions(+), 40 deletions(-) diff --git a/Spellbook/Base.lproj/Main.storyboard b/Spellbook/Base.lproj/Main.storyboard index be1376df..247d0f9e 100644 --- a/Spellbook/Base.lproj/Main.storyboard +++ b/Spellbook/Base.lproj/Main.storyboard @@ -19,7 +19,7 @@ - + @@ -27,14 +27,8 @@ - - @@ -82,7 +83,7 @@ - + @@ -848,12 +849,12 @@ - + - + @@ -873,6 +874,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Spellbook/HigherLevelSlotController.swift b/Spellbook/HigherLevelSlotController.swift index 7d142ea2..5d1fb948 100644 --- a/Spellbook/HigherLevelSlotController.swift +++ b/Spellbook/HigherLevelSlotController.swift @@ -11,7 +11,7 @@ import UIKit import ReSwift class HigherLevelSlotController: UIViewController { - + @IBOutlet weak var slotLevelChooser: UITextField! @IBOutlet weak var cancelButton: UIButton! @IBOutlet weak var castButton: UIButton! @@ -22,7 +22,7 @@ class HigherLevelSlotController: UIViewController { super.viewDidLoad() cancelButton.addTarget(self, action: #selector(cancelButtonPressed), for: UIControl.Event.touchUpInside) - castButton.addTarget(self, action: #selector(castButtonPressed), for: UIControl.Event.touchUpInside) + //castButton.addTarget(self, action: #selector(castButtonPressed), for: UIControl.Event.touchUpInside) guard let spell = self.spell else { return } guard let profile = store.state.profile else { return } @@ -52,17 +52,17 @@ class HigherLevelSlotController: UIViewController { self.dismiss(animated: true, completion: nil) } - @objc func castButtonPressed() { - guard let spell = self.spell else { return } - if let text = slotLevelChooser.text { - if let level = valueFrom(ordinal: text) { - store.dispatch(CastSpellAction(level: level)) - let message = "\(spell.name) was cast at level \(level)" - Toast.makeToast(message, controller: self.parent ?? self) - } - } - - self.dismiss(animated: true, completion: nil) - } +// @objc func castButtonPressed() { +// guard let spell = self.spell else { return } +// if let text = slotLevelChooser.text { +// if let level = valueFrom(ordinal: text) { +// store.dispatch(CastSpellAction(level: level)) +// let message = "\(spell.name) was cast at level \(level)" +// Toast.makeToast(message, controller: self.parent ?? self) +// } +// } +// +// self.dismiss(animated: true, completion: nil) +// } } From bcb39bfc186feb9c4bbadb5a9458729a9bb10e3e Mon Sep 17 00:00:00 2001 From: Jonathan Carifio Date: Mon, 30 Oct 2023 01:05:03 -0400 Subject: [PATCH 09/14] Higher level controller layout seems good, but delegate isn't working yet. --- Spellbook/Base.lproj/Main.storyboard | 20 ++++++--- Spellbook/HigherLevelSlotController.swift | 50 +++++++++++------------ 2 files changed, 39 insertions(+), 31 deletions(-) diff --git a/Spellbook/Base.lproj/Main.storyboard b/Spellbook/Base.lproj/Main.storyboard index 7da99eaf..4c8ac632 100644 --- a/Spellbook/Base.lproj/Main.storyboard +++ b/Spellbook/Base.lproj/Main.storyboard @@ -1146,6 +1146,9 @@ + + + @@ -1161,6 +1164,12 @@ + + + + + + - - - - - - + @@ -1200,9 +1204,13 @@ + + + + diff --git a/Spellbook/HigherLevelSlotController.swift b/Spellbook/HigherLevelSlotController.swift index 5d1fb948..c126a29e 100644 --- a/Spellbook/HigherLevelSlotController.swift +++ b/Spellbook/HigherLevelSlotController.swift @@ -33,36 +33,36 @@ class HigherLevelSlotController: UIViewController { // TODO: It's kind of gross to need to use this dummy type // It feels like a refactor of the delegate is necessary -// let textDelegate = TextFieldChooserDelegate( -// items: Array(range), -// title: "Select Slot Level", -// itemProvider: { -// () in return status.minLevelWithCondition(condition: { level in -// return status.hasAvailableSlots(level: level) && level >= baseLevel -// }) -// }, -// nameGetter: ordinal, -// textSetter: ordinal, -// nameConstructor: { valueFrom(ordinal: $0) ?? 0 }) -// -// slotLevelChooser.delegate = textDelegate + let textDelegate = TextFieldChooserDelegate( + items: Array(range), + title: "Select Slot Level", + itemProvider: { + () in return status.minLevelWithCondition(condition: { level in + return status.hasAvailableSlots(level: level) && level >= baseLevel + }) + }, + nameGetter: ordinal, + textSetter: ordinal, + nameConstructor: { valueFrom(ordinal: $0) ?? 0 }) + + slotLevelChooser.delegate = textDelegate } @objc func cancelButtonPressed() { self.dismiss(animated: true, completion: nil) } -// @objc func castButtonPressed() { -// guard let spell = self.spell else { return } -// if let text = slotLevelChooser.text { -// if let level = valueFrom(ordinal: text) { -// store.dispatch(CastSpellAction(level: level)) -// let message = "\(spell.name) was cast at level \(level)" -// Toast.makeToast(message, controller: self.parent ?? self) -// } -// } -// -// self.dismiss(animated: true, completion: nil) -// } + @objc func castButtonPressed() { + guard let spell = self.spell else { return } + if let text = slotLevelChooser.text { + if let level = valueFrom(ordinal: text) { + store.dispatch(CastSpellAction(level: level)) + let message = "\(spell.name) was cast at level \(level)" + Toast.makeToast(message, controller: self.parent ?? self) + } + } + + self.dismiss(animated: true, completion: nil) + } } From ee00042634a1743982164b5ad0f737985af0eee5 Mon Sep 17 00:00:00 2001 From: Jonathan Carifio Date: Thu, 2 Nov 2023 00:47:15 -0400 Subject: [PATCH 10/14] More work on various spellcasting UI elements. --- Spellbook/Base.lproj/Main.storyboard | 8 +++++--- Spellbook/HigherLevelSlotController.swift | 13 ++++++++----- Spellbook/SpellWindowController.swift | 4 ++-- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/Spellbook/Base.lproj/Main.storyboard b/Spellbook/Base.lproj/Main.storyboard index 4c8ac632..f5174cee 100644 --- a/Spellbook/Base.lproj/Main.storyboard +++ b/Spellbook/Base.lproj/Main.storyboard @@ -571,8 +571,10 @@ -