Add 'iOS/' from commit '938f0fb75860d3637b998bdd0c27dcffd9fc9451'

git-subtree-dir: iOS
git-subtree-mainline: c4bb775af4
git-subtree-split: 938f0fb758
This commit is contained in:
Tom Hicks
2025-06-30 12:55:22 -07:00
135 changed files with 7654 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 521 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 667 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1014 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "section-divider.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "section-divider@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "section-divider@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1001 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1,20 @@
//
// Color+Hex.swift
// MonsterCards
//
// Created by Tom Hicks on 1/17/21.
//
import Foundation
import SwiftUI
extension Color {
init(hex: UInt, alpha: Double = 1) {
self.init(
.sRGB,
red: Double((hex >> 16) & 0xff) / 255,
green: Double((hex >> 8) & 0xff) / 255,
blue: Double((hex >> 0) & 0xff) / 255,
opacity: alpha)
}
}

View File

@@ -0,0 +1,111 @@
//
// MonsterImportHelper.swift
// MonsterCards
//
// Created by Tom Hicks on 4/3/21.
//
import Foundation
extension MonsterViewModel {
func maybeAddSense(_ name: String, _ distance: Int) {
if (distance > 0) {
senses.append(StringViewModel("\(name): \(distance) ft."))
}
}
}
class MonsterImportHelper {
static func import5ESBMonster(_ monsterDTO: MonsterDTO) -> MonsterViewModel {
let monster = MonsterViewModel()
monster.name = monsterDTO.name
monster.size = monsterDTO.size
monster.type = monsterDTO.type
monster.subType = monsterDTO.tag
monster.alignment = monsterDTO.alignment
monster.hitDice = Int64(monsterDTO.hitDice)
monster.armorType = ArmorType(rawValue: monsterDTO.armorName) ?? .none
monster.hasShield = monsterDTO.shieldBonus != 0
monster.naturalArmorBonus = Int64(monsterDTO.natArmorBonus)
monster.customArmor = monsterDTO.otherArmorDesc
monster.walkSpeed = Int64(monsterDTO.speed)
monster.burrowSpeed = Int64(monsterDTO.burrowSpeed)
monster.climbSpeed = Int64(monsterDTO.climbSpeed)
monster.flySpeed = Int64(monsterDTO.flySpeed)
monster.swimSpeed = Int64(monsterDTO.swimSpeed)
monster.canHover = monsterDTO.hover
monster.hasCustomHP = monsterDTO.customHP
monster.customHP = monsterDTO.hpText
monster.hasCustomSpeed = monsterDTO.customSpeed
monster.customSpeed = monsterDTO.speedDesc
monster.strengthScore = Int64(monsterDTO.strPoints)
monster.dexterityScore = Int64(monsterDTO.dexPoints)
monster.constitutionScore = Int64(monsterDTO.conPoints)
monster.intelligenceScore = Int64(monsterDTO.intPoints)
monster.wisdomScore = Int64(monsterDTO.wisPoints)
monster.charismaScore = Int64(monsterDTO.chaPoints)
monster.isBlind = monsterDTO.blind
monster.maybeAddSense("blindsight", monsterDTO.blindsight)
monster.maybeAddSense("darkvision", monsterDTO.darkvision)
monster.maybeAddSense("tremorsense", monsterDTO.tremorsense)
monster.maybeAddSense("turesight", monsterDTO.truesight)
monster.telepathy = Int64(monsterDTO.telepathy)
monster.challengeRating = ChallengeRating(rawValue: monsterDTO.cr) ?? ChallengeRating.one
monster.customChallengeRating = monsterDTO.customCr
monster.customProficiencyBonus = Int64(monsterDTO.customProf)
// TODO: Think about adding legendary properties isLegendary, legendariesDescription, isLair, lairDescription, lairDescriptionEnd, isRegional, regionalDescription, regionalDescriptionEnd
monster.abilities = monsterDTO.abilities.map({AbilityViewModel($0.name, $0.desc)})
monster.actions = monsterDTO.actions.map({AbilityViewModel($0.name, $0.desc)})
monster.reactions = monsterDTO.reactions.map({AbilityViewModel($0.name, $0.desc)})
monster.legendaryActions = monsterDTO.legendaries.map({AbilityViewModel($0.name, $0.desc)})
monster.lairActions = monsterDTO.lairs.map({AbilityViewModel($0.name, $0.desc)})
monster.regionalActions = monsterDTO.regionals.map({AbilityViewModel($0.name, $0.desc)})
monsterDTO.sthrows.forEach({
switch $0.name {
case "str":
monster.strengthSavingThrowProficiency = .proficient
case "dex":
monster.dexteritySavingThrowProficiency = .proficient
case "con":
monster.constitutionSavingThrowProficiency = .proficient
case "int":
monster.intelligenceSavingThrowProficiency = .proficient
case "wis":
monster.wisdomSavingThrowProficiency = .proficient
case "cha":
monster.charismaSavingThrowProficiency = .proficient
default:
break
}
})
monster.skills = monsterDTO.skills.map({
// TODO: consider using a lookup table to make fixing missing stats easier
SkillViewModel(
$0.name,
AbilityScore(rawValue: $0.stat) ?? .dexterity,
$0.note == " (ex)" ? .expertise : .proficient)
})
monster.damageImmunities = monsterDTO.damageTypes
.filter({$0.type == "i" || $0.type == " (Immune)"})
.map({StringViewModel($0.name)})
monster.damageResistances = monsterDTO.damageTypes
.filter({$0.type == "r" || $0.type == " (Resistant)"})
.map({StringViewModel($0.name)})
monster.damageVulnerabilities = monsterDTO.damageTypes
.filter({$0.type == "v" || $0.type == " (Vulnerable)"})
.map({StringViewModel($0.name)})
monster.conditionImmunities = monsterDTO.conditions.map({StringViewModel($0.name)})
monster.languages = monsterDTO.languages
.map({
LanguageViewModel($0.name, $0.speaks)
})
monster.understandsBut = monsterDTO.understandsBut
// TODO: add shortName or nickname
// monster.shortName = monsterDTO.shortName
// TODO: look into what goes in specialdamage and damage
return monster
}
}

View File

@@ -0,0 +1,50 @@
//
// StringHelper.swift
// MonsterCards
//
// Created by Tom Hicks on 3/21/21.
//
import Foundation
class StringHelper {
static func oxfordJoin(
_ strings: [String],
_ separator: String = ", ",
_ lastSeparator: String = ", and ",
_ onlySeparator: String = " and "
) -> String {
let numStrings = strings.count
if (numStrings < 1) {
return "";
} else if (numStrings == 2) {
return strings[0] + onlySeparator + strings[1]
} else {
var joined = ""
var index = 0
let lastIndex = numStrings - 1
strings.forEach {
if index > 0 && index < lastIndex {
joined.append(separator)
} else if (index > 0 && index >= lastIndex) {
joined.append(lastSeparator)
}
joined.append($0)
index = index + 1
}
return joined
}
}
static func safeContainsCaseInsensitive(_ str: String?, _ match: String) -> Bool {
guard let str = str else { return false }
return str.localizedCaseInsensitiveContains(match)
}
static func safeEqualsIgnoreCase(_ str: String?, _ match: String) -> Bool {
guard let str = str else { return false }
return str.lowercased() == match.lowercased()
}
}

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "section-divider.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "section-divider@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "section-divider@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1001 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

131
iOS/MonsterCards/Info.plist Normal file
View File

@@ -0,0 +1,131 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeIconFiles</key>
<array/>
<key>CFBundleTypeName</key>
<string>Monster Data</string>
<key>LSHandlerRank</key>
<string>Owner</string>
<key>LSItemContentTypes</key>
<array>
<string>com.majinnaibu.MonsterCards.Monster</string>
</array>
</dict>
</array>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIconName</key>
<string>AppIcon</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<true/>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
</array>
<key>UILaunchScreen</key>
<dict/>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportsDocumentBrowser</key>
<true/>
<key>UIUserInterfaceStyle</key>
<string>Light</string>
<key>UTExportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
<string>public.content</string>
</array>
<key>UTTypeDescription</key>
<string>Monster data file</string>
<key>UTTypeIconFiles</key>
<array/>
<key>UTTypeIdentifier</key>
<string>com.majinnaibu.MonsterCards.Monster</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>monster</string>
</array>
<key>public.mime-type</key>
<array>
<string>text/vnd.monstercards.monster</string>
</array>
</dict>
</dict>
</array>
<key>UTImportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
<string>public.content</string>
</array>
<key>UTTypeDescription</key>
<string>Monster data file</string>
<key>UTTypeIconFiles</key>
<array/>
<key>UTTypeIdentifier</key>
<string>com.majinnaibu.MonsterCards.Monster</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>monster</string>
</array>
<key>public.mime-type</key>
<array>
<string>text/vnd.monstercards.monster</string>
</array>
</dict>
</dict>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,116 @@
//
// AbilityViewModel.swift
// MonsterCards
//
// Created by Tom Hicks on 3/25/21.
//
import Foundation
public class AbilityViewModel: NSObject, ObservableObject, Identifiable, NSSecureCoding {
public static var supportsSecureCoding = true
public func encode(with coder: NSCoder) {
coder.encode(self.name, forKey: "name")
coder.encode(self.abilityDescription, forKey: "abilityDescription")
}
public required init?(coder: NSCoder) {
self.name = coder.decodeObject(of: NSString.self, forKey: "name")! as String
self.abilityDescription = coder.decodeObject(of: NSString.self, forKey: "abilityDescription")! as String
}
@Published public var name: String
@Published public var abilityDescription: String
public init(_ name: String = "", _ abilityDescription: String = "") {
self.name = name
self.abilityDescription = abilityDescription
}
public var fullText: String {
get {
return String(format: "___%@:___ %@", name, abilityDescription)
}
}
func renderedText(_ monster: MonsterViewModel) -> String {
let strSave = monster.strengthModifier + monster.proficiencyBonus + 8
let dexSave = monster.dexterityModifier + monster.proficiencyBonus + 8
let conSave = monster.constitutionModifier + monster.proficiencyBonus + 8
let intSave = monster.intelligenceModifier + monster.proficiencyBonus + 8
let wisSave = monster.wisdomModifier + monster.proficiencyBonus + 8
let chaSave = monster.charismaModifier + monster.proficiencyBonus + 8
let strAttack = monster.strengthModifier + monster.proficiencyBonus
let dexAttack = monster.dexterityModifier + monster.proficiencyBonus
let conAttack = monster.constitutionModifier + monster.proficiencyBonus
let intAttack = monster.intelligenceModifier + monster.proficiencyBonus
let wisAttack = monster.wisdomModifier + monster.proficiencyBonus
let chaAttack = monster.charismaModifier + monster.proficiencyBonus
// TODO: find the other options and implement them [WIS], [WIS STAT], [WIS DMG], [WIS STAT 1d12]
let finalText = fullText
.replacingOccurrences(of: "[STR SAVE]", with: String(strSave))
.replacingOccurrences(of: "[DEX SAVE]", with: String(dexSave))
.replacingOccurrences(of: "[CON SAVE]", with: String(conSave))
.replacingOccurrences(of: "[INT SAVE]", with: String(intSave))
.replacingOccurrences(of: "[WIS SAVE]", with: String(wisSave))
.replacingOccurrences(of: "[CHA SAVE]", with: String(chaSave))
.replacingOccurrences(of: "[STR ATK]", with: String(strAttack))
.replacingOccurrences(of: "[DEX ATK]", with: String(dexAttack))
.replacingOccurrences(of: "[CON ATK]", with: String(conAttack))
.replacingOccurrences(of: "[INT ATK]", with: String(intAttack))
.replacingOccurrences(of: "[WIS ATK]", with: String(wisAttack))
.replacingOccurrences(of: "[CHA ATK]", with: String(chaAttack))
return finalText
}
}
extension AbilityViewModel: Comparable {
public static func < (lhs: AbilityViewModel, rhs: AbilityViewModel) -> Bool {
lhs.name < rhs.name
}
public static func == (lhs: AbilityViewModel, rhs: AbilityViewModel) -> Bool {
lhs.name == rhs.name &&
lhs.abilityDescription == rhs.abilityDescription
}
}
// TODO: figure out how to add this to the set of known transformers so it will work with transformer set to NSSecureUnarchiveFromDataTransformerName
@objc(AbilityViewModelValueTransformer)
public final class AbilityViewModelValueTransformer: ValueTransformer {
override public class func transformedValueClass() -> AnyClass {
return NSArray.self
}
override public class func allowsReverseTransformation() -> Bool {
return true
}
override public func transformedValue(_ value: Any?) -> Any? {
guard let language = value as? NSArray else { return nil }
do {
let data = try NSKeyedArchiver.archivedData(withRootObject: language, requiringSecureCoding: true)
return data
} catch {
assertionFailure("Failed to transform `AbilityViewModel` to `Data`")
return nil
}
}
override public func reverseTransformedValue(_ value: Any?) -> Any? {
guard let data = value as? NSData else { return nil }
do {
let language = try NSKeyedUnarchiver.unarchivedArrayOfObjects(ofClass: AbilityViewModel.self, from: data as Data)
return language
} catch {
assertionFailure("Failed to transform `Data` to `AbilityViewModel`")
return nil
}
}
}

View File

@@ -0,0 +1,46 @@
//
// ChallengeRatingViewModel.swift
// MonsterCards
//
// Created by Tom Hicks on 3/24/21.
//
import Foundation
class ChallengeRatingViewModel: ObservableObject/*, Comparable*/, Identifiable {
func encode(with coder: NSCoder) {
coder.encode(self.rating.rawValue, forKey: "rating")
}
static func == (lhs: ChallengeRatingViewModel, rhs: ChallengeRatingViewModel) -> Bool {
lhs.rating == rhs.rating &&
lhs.customText == rhs.customText &&
lhs.customProficiencyBonus == rhs.customProficiencyBonus
}
@Published var rating: ChallengeRating
@Published var customText: String
@Published var customProficiencyBonus: Int64
init(
_ rating: ChallengeRating = .one,
_ customText: String = "",
_ customProficiencyBonus: Int64 = 0
) {
self.rating = rating
self.customText = customText
self.customProficiencyBonus = customProficiencyBonus
}
init(
_ rating: String = ChallengeRating.one.rawValue,
_ customText: String = "",
_ customProficiencyBonus: Int64 = 0
) {
self.rating = ChallengeRating(rawValue: rating) ?? .one
self.customText = customText
self.customProficiencyBonus = customProficiencyBonus
}
}

View File

@@ -0,0 +1,42 @@
//
// DamageTypeDTO.swift
// MonsterCards
//
// Created by Tom Hicks on 3/28/21.
//
import Foundation
struct DamageTypeDTO {
var name: String
var note: String
var type: String
}
private enum DamageTypeDTOCodingKeys: String, CodingKey {
case name = "name"
case note = "note"
case type = "type"
}
extension DamageTypeDTO: Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: DamageTypeDTOCodingKeys.self)
self.name = (try? container.decode(String.self, forKey: .name)) ?? ""
self.note = (try? container.decode(String.self, forKey: .note)) ?? ""
self.type = (try? container.decode(String.self, forKey: .type)) ?? ""
}
}
extension DamageTypeDTO: Encodable {
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: DamageTypeDTOCodingKeys.self)
try container.encode(self.name, forKey: .name)
try container.encode(self.note, forKey: .note)
try container.encode(self.type, forKey: .type)
}
}

View File

@@ -0,0 +1,73 @@
//
// AbilityScore.swift
// MonsterCards
//
// Created by Tom Hicks on 1/18/21.
//
import Foundation
enum AbilityScore: String, CaseIterable, Identifiable {
case strength = "strength"
case dexterity = "dexterity"
case constitution = "constitution"
case intelligence = "intelligence"
case wisdom = "wisdom"
case charisma = "charisma"
var id: AbilityScore { self }
var displayName: String {
switch self {
case .strength:
return "Strength"
case .dexterity:
return "Dexterity"
case .constitution:
return "Constitution"
case .intelligence:
return "Intelligence"
case .wisdom:
return "Wisdom"
case .charisma:
return "Charisma"
}
}
var shortDisplayName: String {
switch self {
case .strength:
return "STR"
case .dexterity:
return "DEX"
case .constitution:
return "CON"
case .intelligence:
return "INT"
case .wisdom:
return "WIS"
case .charisma:
return "CHA"
}
}
init?(rawValue: String) {
var match: AbilityScore? = nil
let raw = rawValue.lowercased()
for abilityScore in AbilityScore.allCases {
if (abilityScore.rawValue.lowercased() == raw) {
match = abilityScore
}
if (abilityScore.shortDisplayName.lowercased() == raw) {
match = abilityScore
}
}
if (match == nil) {
return nil
} else {
self = match!
}
}
}

View File

@@ -0,0 +1,27 @@
//
// AdvantageType.swift
// MonsterCards
//
// Created by Tom Hicks on 1/17/21.
//
import Foundation
enum AdvantageType: String, CaseIterable, Identifiable {
case none = "none"
case advantage = "advantage"
case disadvantage = "disadvantage"
var id: AdvantageType { self }
var displayName: String {
switch self {
case .none:
return "None"
case .advantage:
return "Advantage"
case .disadvantage:
return "Disadvantage"
}
}
}

View File

@@ -0,0 +1,50 @@
//
// ArmorType.swift
// MonsterCards
//
// Created by Tom Hicks on 3/21/21.
//
import Foundation
enum ArmorType: String, CaseIterable, Identifiable {
case none = "none"
case naturalArmor = "natural armor"
case mageArmor = "mage armor"
case padded = "padded"
case leather = "leather"
case studdedLeather = "studded"
case hide = "hide"
case chainShirt = "chain shirt"
case scaleMail = "scale mail"
case breastplate = "breastplate"
case halfPlate = "half plate"
case ringMail = "ring mail"
case chainMail = "chain mail"
case splintMail = "splint"
case plateMail = "plate"
case other = "other"
var id: ArmorType { self }
var displayName: String {
switch self {
case .none: return "None"
case .naturalArmor: return "Natural Armor"
case .mageArmor: return "Mage Armor"
case .padded: return "Padded"
case .leather: return "Leather"
case .studdedLeather: return "Studded Leather"
case .hide: return "Hide"
case .chainShirt: return "Chain Shirt"
case .scaleMail: return "Scale Mail"
case .breastplate: return "Breastplate"
case .halfPlate: return "Half Plate"
case .ringMail: return "Ring Mail"
case .chainMail: return "Chain Mail"
case .splintMail: return "Splint Mail"
case .plateMail: return "Plate Mail"
case .other: return "Other"
}
}
}

View File

@@ -0,0 +1,123 @@
//
// ChallengeRating.swift
// MonsterCards
//
// Created by Tom Hicks on 3/21/21.
//
import Foundation
enum ChallengeRating: String, CaseIterable, Identifiable {
case custom = "Custom"
case zero = "0"
case oneEighth = "1/8"
case oneQuarter = "1/4"
case oneHalf = "1/2"
case one = "1"
case two = "2"
case three = "3"
case four = "4"
case five = "5"
case six = "6"
case seven = "7"
case eight = "8"
case nine = "9"
case ten = "10"
case eleven = "11"
case twelve = "12"
case thirteen = "13"
case fourteen = "14"
case fifteen = "15"
case sixteen = "16"
case seventeen = "17"
case eighteen = "18"
case nineteen = "19"
case twenty = "20"
case twentyOne = "21"
case twentyTwo = "22"
case twentyThree = "23"
case twentyFour = "24"
case twentyFive = "25"
case twentySix = "26"
case twentySeven = "27"
case twentyEight = "28"
case twentyNine = "29"
case thirty = "30"
var id: ChallengeRating { self }
var displayName: String {
switch(self) {
case .custom:
return "Custom"
case .zero:
return "0 (10 XP)"
case .oneEighth:
return "1/8 (25 XP)"
case .oneQuarter:
return "1/4 (50 XP)"
case .oneHalf:
return "1/2 (100 XP)"
case .one:
return "1 (200 XP)"
case .two:
return "2 (450 XP)"
case .three:
return "3 (700 XP)"
case .four:
return "4 (1,100 XP)"
case .five:
return "5 (1,800 XP)"
case .six:
return "6 (2,300 XP)"
case .seven:
return "7 (2,900 XP)"
case .eight:
return "8 (3,900 XP)"
case .nine:
return "9 (5,000 XP)"
case .ten:
return "10 (5,900 XP)"
case .eleven:
return "11 (7,200 XP)"
case .twelve:
return "12 (8,400 XP)"
case .thirteen:
return "13 (10,000 XP)"
case .fourteen:
return "14 (11,500 XP)"
case .fifteen:
return "15 (13,000 XP)"
case .sixteen:
return "16 (15,000 XP)"
case .seventeen:
return "17 (18,000 XP)"
case .eighteen:
return "18 (20,000 XP)"
case .nineteen:
return "19 (22,000 XP)"
case .twenty:
return "20 (25,000 XP)"
case .twentyOne:
return "21 (33,000 XP)"
case .twentyTwo:
return "22 (41,000 XP)"
case .twentyThree:
return "23 (50,000 XP)"
case .twentyFour:
return "24 (62,000 XP)"
case .twentyFive:
return "25 (75,000 XP)"
case .twentySix:
return "26 (90,000 XP)"
case .twentySeven:
return "27 (105,000 XP)"
case .twentyEight:
return "28 (120,000 XP)"
case .twentyNine:
return "29 (135,000 XP)"
case .thirty:
return "30 (155,000 XP)"
}
}
}

View File

@@ -0,0 +1,27 @@
//
// ProficiencyType.swift
// MonsterCards
//
// Created by Tom Hicks on 1/17/21.
//
import Foundation
enum ProficiencyType: String, CaseIterable, Identifiable {
case none = "none"
case proficient = "proficient"
case expertise = "expertise"
var id: ProficiencyType { self }
var displayName: String {
switch self {
case .none:
return "None"
case .proficient:
return "Proficient"
case .expertise:
return "Expertise"
}
}
}

View File

@@ -0,0 +1,52 @@
//
// SizeType.swift
// MonsterCards
//
// Created by Tom Hicks on 3/21/21.
//
import Foundation
enum SizeType: String, CaseIterable, Identifiable {
case tiny = "tiny"
case small = "small"
case medium = "medium"
case large = "large"
case huge = "huge"
case gargantuan = "gargantuan"
var id: SizeType { self }
var displayName: String {
switch self {
case .tiny: return "Tiny"
case .small: return "Small"
case .medium: return "Medium"
case .large: return "Large"
case .huge: return "Huge"
case .gargantuan: return "gargantuan"
}
}
init?(rawValue: String) {
var match: SizeType? = nil
for size in SizeType.allCases {
if (size.rawValue == rawValue) {
match = size
}
}
for size in SizeType.allCases {
if (size.rawValue.lowercased() == rawValue.lowercased()) {
match = size
}
}
if (match == nil) {
return nil
} else {
self = match!
}
}
}

View File

@@ -0,0 +1,35 @@
//
// LanguageDTO.swift
// MonsterCards
//
// Created by Tom Hicks on 3/28/21.
//
import Foundation
struct LanguageDTO {
var name: String
var speaks: Bool
}
private enum LanguageDTOCodingKeys: String, CodingKey {
case name = "name"
case speaks = "speaks"
}
extension LanguageDTO: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: LanguageDTOCodingKeys.self)
self.name = (try? container.decode(String.self, forKey: .name)) ?? ""
self.speaks = (try? container.decode(Bool.self, forKey: .speaks)) ?? false
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: LanguageDTOCodingKeys.self)
try container.encode(self.name, forKey: .name)
try container.encode(self.speaks, forKey: .speaks)
}
}

View File

@@ -0,0 +1,79 @@
//
// LanguageViewModel.swift
// MonsterCards
//
// Created by Tom Hicks on 3/24/21.
//
import Foundation
// TODO: split this into separate Model and ViewModel classes later.
public class LanguageViewModel : NSObject, ObservableObject, Comparable, Identifiable, NSSecureCoding {
public static var supportsSecureCoding = true
public func encode(with coder: NSCoder) {
coder.encode(self.name, forKey: "name")
coder.encode(self.speaks, forKey: "speaks")
}
public required init?(coder: NSCoder) {
self.name = coder.decodeObject(of: NSString.self, forKey: "name")! as String
self.speaks = coder.decodeBool(forKey: "speaks")
}
public static func < (lhs: LanguageViewModel, rhs: LanguageViewModel) -> Bool {
lhs.name < rhs.name
}
public static func == (lhs: LanguageViewModel, rhs: LanguageViewModel) -> Bool {
lhs.name == rhs.name &&
lhs.speaks == rhs.speaks
}
@Published var name: String
@Published var speaks: Bool
public init(
_ name: String = "",
_ speaks: Bool = true
) {
self.name = name
self.speaks = speaks
}
}
// TODO: figure out how to add this to the set of known transformers so it will work with transformer set to NSSecureUnarchiveFromDataTransformerName
@objc(LanguageViewModelValueTransformer)
public final class LanguageViewModelValueTransformer: ValueTransformer {
override public class func transformedValueClass() -> AnyClass {
return NSArray.self
}
override public class func allowsReverseTransformation() -> Bool {
return true
}
override public func transformedValue(_ value: Any?) -> Any? {
guard let language = value as? NSArray else { return nil }
do {
let data = try NSKeyedArchiver.archivedData(withRootObject: language, requiringSecureCoding: true)
return data
} catch {
assertionFailure("Failed to transform `LanguageViewModel` to `Data`")
return nil
}
}
override public func reverseTransformedValue(_ value: Any?) -> Any? {
guard let data = value as? NSData else { return nil }
do {
let language = try NSKeyedUnarchiver.unarchivedArrayOfObjects(ofClass: LanguageViewModel.self, from: data as Data)
return language
} catch {
assertionFailure("Failed to transform `Data` to `LanguageViewModel`")
return nil
}
}
}

View File

@@ -0,0 +1,161 @@
//
// Monster+CoreDataClass.swift
// MonsterCards
//
// Created by Tom Hicks on 1/15/21.
//
//
import Foundation
import CoreData
@objc(Monster)
public class Monster: NSManagedObject {
convenience init(context: NSManagedObjectContext, name: String, size: String, type: String, subtype: String, alignment: String) {
self.init(context:context)
self.name = name;
self.size = size;
self.type = type;
self.subtype = subtype;
self.alignment = alignment;
}
// MARK: Armor
var armorTypeEnum: ArmorType {
get {
return ArmorType.init(rawValue: armorType ?? "none") ?? .none
}
set {
armorType = newValue.rawValue
}
}
// MARK: Challenge Rating / Proficiency Bonus
var challengeRatingEnum: ChallengeRating {
get {
return ChallengeRating.init(rawValue: challengeRating ?? "1") ?? .one
}
set {
challengeRating = newValue.rawValue
}
}
// MARK: Saving Throws
var strengthSavingThrowProficiencyEnum: ProficiencyType {
get {
return ProficiencyType.init(rawValue: strengthSavingThrowProficiency ?? "") ?? .none
}
set {
strengthSavingThrowProficiency = newValue.rawValue
}
}
var strengthSavingThrowAdvantageEnum: AdvantageType {
get {
return AdvantageType.init(rawValue: strengthSavingThrowAdvantage ?? "") ?? .none
}
set {
strengthSavingThrowAdvantage = newValue.rawValue
}
}
var dexteritySavingThrowProficiencyEnum: ProficiencyType {
get {
return ProficiencyType.init(rawValue: dexteritySavingThrowProficiency ?? "") ?? .none
}
set {
dexteritySavingThrowProficiency = newValue.rawValue
}
}
var dexteritySavingThrowAdvantageEnum: AdvantageType {
get {
return AdvantageType.init(rawValue: dexteritySavingThrowAdvantage ?? "") ?? .none
}
set {
dexteritySavingThrowAdvantage = newValue.rawValue
}
}
var constitutionSavingThrowProficiencyEnum: ProficiencyType {
get {
return ProficiencyType.init(rawValue: constitutionSavingThrowProficiency ?? "") ?? .none
}
set {
constitutionSavingThrowProficiency = newValue.rawValue
}
}
var constitutionSavingThrowAdvantageEnum: AdvantageType {
get {
return AdvantageType.init(rawValue: constitutionSavingThrowAdvantage ?? "") ?? .none
}
set {
constitutionSavingThrowAdvantage = newValue.rawValue
}
}
var intelligenceSavingThrowProficiencyEnum: ProficiencyType {
get {
return ProficiencyType.init(rawValue: intelligenceSavingThrowProficiency ?? "") ?? .none
}
set {
intelligenceSavingThrowProficiency = newValue.rawValue
}
}
var intelligenceSavingThrowAdvantageEnum: AdvantageType {
get {
return AdvantageType.init(rawValue: intelligenceSavingThrowAdvantage ?? "") ?? .none
}
set {
intelligenceSavingThrowAdvantage = newValue.rawValue
}
}
var wisdomSavingThrowProficiencyEnum: ProficiencyType {
get {
return ProficiencyType.init(rawValue: wisdomSavingThrowProficiency ?? "") ?? .none
}
set {
wisdomSavingThrowProficiency = newValue.rawValue
}
}
var wisdomSavingThrowAdvantageEnum: AdvantageType {
get {
return AdvantageType.init(rawValue: wisdomSavingThrowAdvantage ?? "") ?? .none
}
set {
wisdomSavingThrowAdvantage = newValue.rawValue
}
}
var charismaSavingThrowProficiencyEnum: ProficiencyType {
get {
return ProficiencyType.init(rawValue: charismaSavingThrowProficiency ?? "") ?? .none
}
set {
charismaSavingThrowProficiency = newValue.rawValue
}
}
var charismaSavingThrowAdvantageEnum: AdvantageType {
get {
return AdvantageType.init(rawValue: charismaSavingThrowAdvantage ?? "") ?? .none
}
set {
charismaSavingThrowAdvantage = newValue.rawValue
}
}
// MARK: End
}

View File

@@ -0,0 +1,353 @@
//
// MonsterDTO.swift
// MonsterCards
//
// Created by Tom Hicks on 3/28/21.
//
import Foundation
struct MonsterDTO {
var name: String
var type: String
var alignment: String
var size: String
var hitDice: Int
var armorName: String
var otherArmorDesc: String
var shieldBonus: Int
var natArmorBonus: Int
var speed: Int
var burrowSpeed: Int
var climbSpeed: Int
var flySpeed: Int
var hover: Bool
var swimSpeed: Int
var speedDesc: String
var customSpeed: Bool
var strPoints: Int
var dexPoints: Int
var conPoints: Int
var intPoints: Int
var wisPoints: Int
var chaPoints: Int
var cr: String
var customCr: String
var customProf: Int
var hpText: String
var sthrows: [SavingThrowDTO]
var skills: [SkillDTO]
var actions: [TraitDTO]
var legendaryDescription: String
var legendaries: [TraitDTO]
var reactions: [TraitDTO]// TODO: verify this
var abilities: [TraitDTO]
var damageTypes: [DamageTypeDTO]
var conditions: [DamageTypeDTO] // TODO: figure this out
var languages: [LanguageDTO]
var telepathy: Int
var understandsBut: String
var blindsight: Int
var blind: Bool
var darkvision: Int
var tremorsense: Int
var truesight: Int
var tag: String
var customHP: Bool
var isLegendary: Bool
var isLair: Bool
var lairDescription: String
var lairDescriptionEnd: String
var isRegional: Bool
var regionalDescription: String
var regionalDescriptionEnd: String
// var properties: [???] // TODO: figure this out
var lairs: [TraitDTO]
var regionals: [TraitDTO]
var specialDamage: [DamageTypeDTO]
var shortName: String
var doubleColumns: Bool
var separationPoint: Int
var damage: [DamageTypeDTO] // TODO: figure this out
init() {
self.abilities = []
self.actions = []
self.alignment = ""
self.armorName = ""
self.blind = false
self.blindsight = 0
self.burrowSpeed = 0
self.chaPoints = 0
self.climbSpeed = 0
self.conPoints = 0
self.conditions = []
self.cr = ""
self.customCr = ""
self.customHP = false
self.customProf = 0
self.customSpeed = false
self.damage = []
self.damageTypes = []
self.darkvision = 0
self.dexPoints = 0
self.doubleColumns = false
self.flySpeed = 0
self.hitDice = 0
self.hover = false
self.hpText = ""
self.intPoints = 0
self.isLair = false
self.isLegendary = false
self.isRegional = false
self.lairs = []
self.languages = []
self.legendaries = []
self.lairDescription = ""
self.legendaryDescription = ""
self.lairDescriptionEnd = ""
self.name = ""
self.natArmorBonus = 0
self.otherArmorDesc = ""
self.reactions = []
self.regionals = []
self.regionalDescription = ""
self.regionalDescriptionEnd = ""
self.size = ""
self.speed = 0
self.skills = []
self.sthrows = []
self.strPoints = 0
self.swimSpeed = 0
self.speedDesc = ""
self.shortName = ""
self.specialDamage = []
self.shieldBonus = 0
self.separationPoint = 0
self.type = ""
self.tag = ""
self.telepathy = 0
self.truesight = 0
self.tremorsense = 0
self.understandsBut = ""
self.wisPoints = 0
}
}
enum MonsterDTOCodingKeys: String, CodingKey {
case name = "name"
case type = "type"
case alignment = "alignment"
case size = "size"
case hitDice = "hitDice"
case armorName = "armorName"
case otherArmorDesc = "otherArmorDesc"
case shieldBonus = "shieldBonus"
case natArmorBonus = "natArmorBonus"
case speed = "speed"
case burrowSpeed = "burrowSpeed"
case climbSpeed = "climbSpeed"
case flySpeed = "flySpeed"
case hover = "hover"
case swimSpeed = "swimSpeed"
case speedDesc = "speedDesc"
case customSpeed = "customSpeed"
case strPoints = "strPoints"
case dexPoints = "dexPoints"
case conPoints = "conPoints"
case intPoints = "intPoints"
case wisPoints = "wisPoints"
case chaPoints = "chaPoints"
case cr = "cr"
case customCr = "customCr"
case customProf = "customProf"
case hpText = "hpText"
case sthrows = "sthrows"
case skills = "skills"
case actions = "actions"
case legendaryDescription = "legendaryDescription"
case legendaries = "legendaries"
case reactions = "reactions"
case abilities = "abilities"
case damageTypes = "damageTypes"
case conditions = "conditions"
case languages = "languages"
case telepathy = "telepathy"
case understandsBut = "understandsBut"
case blindsight = "blindsight"
case blind = "blind"
case darkvision = "darkvision"
case tremorsense = "tremorsense"
case truesight = "truesight"
case tag = "tag"
case customHP = "customHP"
case isLegendary = "isLegendary"
case isLair = "isLair"
case lairDescription = "lairDescription"
case lairDescriptionEnd = "lairDescriptionEnd"
case isRegional = "isRegional"
case regionalDescription = "regionalDescription"
case regionalDescriptionEnd = "regionalDescriptionEnd"
case properties = "properties"
case lairs = "lairs"
case regionals = "regionals"
case specialDamage = "specialDamage"
case shortName = "shortName"
case doubleColumns = "doubleColumns"
case separationPoint = "separationPoint"
case damage = "damage"
}
func readInt(_ container: KeyedDecodingContainer<MonsterDTOCodingKeys>, _ key: MonsterDTOCodingKeys, _ defaultValue: Int = 0) -> Int {
let readInt = try? container.decode(Int.self, forKey: key)
let readString = try? container.decode(String.self, forKey: key)
if (readInt != nil) {
return readInt!
}
if (readString != nil) {
return Int(readString!) ?? defaultValue
}
return defaultValue;
}
func readString(_ container: KeyedDecodingContainer<MonsterDTOCodingKeys>, _ key: MonsterDTOCodingKeys, _ defaultValue: String = "") -> String {
return (try? container.decode(String.self, forKey: key)) ?? defaultValue
}
extension MonsterDTO: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: MonsterDTOCodingKeys.self)
self.name = readString(container, .name, "Imported Monster")
self.type = readString(container, .type, "")
self.alignment = readString(container, .alignment, "")
self.size = readString(container, .size, "")
self.hitDice = readInt(container, .hitDice, 0)
self.armorName = readString(container, .armorName, "")
self.otherArmorDesc = readString(container, .otherArmorDesc, "")
self.shieldBonus = readInt(container, .shieldBonus, 0)
self.natArmorBonus = readInt(container, .natArmorBonus, 0)
self.speed = readInt(container, .speed, 0)
self.burrowSpeed = readInt(container, .burrowSpeed, 0)
self.climbSpeed = readInt(container, .climbSpeed, 0)
self.flySpeed = readInt(container, .flySpeed, 0)
self.hover = (try? container.decode(Bool.self, forKey: .hover)) ?? false
self.swimSpeed = readInt(container, .swimSpeed, 0)
self.speedDesc = readString(container, .speedDesc, "")
self.customSpeed = (try? container.decode(Bool.self, forKey: .customSpeed)) ?? false
self.strPoints = readInt(container, .strPoints, 0)
self.dexPoints = readInt(container, .dexPoints, 0)
self.conPoints = readInt(container, .conPoints, 0)
self.intPoints = readInt(container, .intPoints, 0)
self.wisPoints = readInt(container, .wisPoints, 0)
self.chaPoints = readInt(container, .chaPoints, 0)
self.cr = readString(container, .cr, "")
self.customCr = readString(container, .customCr, "")
self.customProf = readInt(container, .customProf, 0)
self.hpText = readString(container, .hpText, "")
self.legendaryDescription = readString(container, .legendaryDescription, "")
self.understandsBut = readString(container, .understandsBut, "")
self.tag = readString(container, .tag, "")
self.lairDescription = readString(container, .lairDescription, "")
self.lairDescriptionEnd = readString(container, .lairDescriptionEnd, "")
self.regionalDescription = readString(container, .regionalDescription, "")
self.regionalDescriptionEnd = readString(container, .regionalDescriptionEnd, "")
self.shortName = readString(container, .shortName, "")
self.telepathy = readInt(container, .telepathy, 0)
self.blindsight = readInt(container, .blindsight, 0)
self.darkvision = readInt(container, .darkvision, 0)
self.tremorsense = readInt(container, .tremorsense, 0)
self.truesight = readInt(container, .truesight, 0)
self.separationPoint = readInt(container, .separationPoint, 0)
self.blind = (try? container.decode(Bool.self, forKey: .blind)) ?? false
self.customHP = (try? container.decode(Bool.self, forKey: .customHP)) ?? false
self.isLegendary = (try? container.decode(Bool.self, forKey: .isLegendary)) ?? false
self.isLair = (try? container.decode(Bool.self, forKey: .isLair)) ?? false
self.isRegional = (try? container.decode(Bool.self, forKey: .isRegional)) ?? false
self.doubleColumns = (try? container.decode(Bool.self, forKey: .doubleColumns)) ?? false
self.lairs = (try? container.decode([TraitDTO].self, forKey: .lairs)) ?? []
self.regionals = (try? container.decode([TraitDTO].self, forKey: .regionals)) ?? []
self.specialDamage = (try? container.decode([DamageTypeDTO].self, forKey: .specialDamage)) ?? []
self.damage = (try? container.decode([DamageTypeDTO].self, forKey: .damage)) ?? []
self.conditions = (try? container.decode([DamageTypeDTO].self, forKey: .conditions)) ?? []
self.sthrows = (try? container.decode([SavingThrowDTO].self, forKey: .sthrows)) ?? []
self.skills = (try? container.decode([SkillDTO].self, forKey: .skills)) ?? []
self.actions = (try? container.decode([TraitDTO].self, forKey: .actions)) ?? []
self.legendaries = (try? container.decode([TraitDTO].self, forKey: .legendaries)) ?? []
self.reactions = (try? container.decode([TraitDTO].self, forKey: .reactions)) ?? []
self.abilities = (try? container.decode([TraitDTO].self, forKey: .abilities)) ?? []
self.damageTypes = (try? container.decode([DamageTypeDTO].self, forKey: .damageTypes)) ?? []
self.languages = (try? container.decode([LanguageDTO].self, forKey: .languages)) ?? []
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: MonsterDTOCodingKeys.self)
try container.encode(self.name, forKey: .name)
try container.encode(self.type, forKey: .type)
try container.encode(self.alignment, forKey: .alignment)
try container.encode(self.size, forKey: .size)
try container.encode(self.hitDice, forKey: .hitDice)
try container.encode(self.armorName, forKey: .armorName)
try container.encode(self.otherArmorDesc, forKey: .otherArmorDesc)
try container.encode(self.shieldBonus, forKey: .shieldBonus)
try container.encode(self.natArmorBonus, forKey: .natArmorBonus)
try container.encode(self.speed, forKey: .speed)
try container.encode(self.burrowSpeed, forKey: .burrowSpeed)
try container.encode(self.climbSpeed, forKey: .climbSpeed)
try container.encode(self.flySpeed, forKey: .flySpeed)
try container.encode(self.hover, forKey: .hover)
try container.encode(self.swimSpeed, forKey: .swimSpeed)
try container.encode(self.speedDesc, forKey: .speedDesc)
try container.encode(self.customSpeed, forKey: .customSpeed)
try container.encode(self.strPoints, forKey: .strPoints)
try container.encode(self.dexPoints, forKey: .dexPoints)
try container.encode(self.conPoints, forKey: .conPoints)
try container.encode(self.intPoints, forKey: .intPoints)
try container.encode(self.wisPoints, forKey: .wisPoints)
try container.encode(self.chaPoints, forKey: .chaPoints)
try container.encode(self.cr, forKey: .cr)
try container.encode(self.customCr, forKey: .customCr)
try container.encode(self.customProf, forKey: .customProf)
try container.encode(self.hpText, forKey: .hpText)
try container.encode(self.sthrows, forKey: .sthrows)
try container.encode(self.skills, forKey: .skills)
try container.encode(self.actions, forKey: .actions)
try container.encode(self.legendaryDescription, forKey: .legendaryDescription)
try container.encode(self.legendaries, forKey: .legendaries)
try container.encode(self.reactions, forKey: .reactions)
try container.encode(self.abilities, forKey: .abilities)
try container.encode(self.damageTypes, forKey: .damageTypes)
try container.encode(self.conditions, forKey: .conditions)
try container.encode(self.languages, forKey: .languages)
try container.encode(self.telepathy, forKey: .telepathy)
try container.encode(self.understandsBut, forKey: .understandsBut)
try container.encode(self.blindsight, forKey: .blindsight)
try container.encode(self.blind, forKey: .blind)
try container.encode(self.darkvision, forKey: .darkvision)
try container.encode(self.tremorsense, forKey: .tremorsense)
try container.encode(self.truesight, forKey: .truesight)
try container.encode(self.tag, forKey: .tag)
try container.encode(self.customHP, forKey: .customHP)
try container.encode(self.isLegendary, forKey: .isLegendary)
try container.encode(self.isLair, forKey: .isLair)
try container.encode(self.lairDescription, forKey: .lairDescription)
try container.encode(self.lairDescriptionEnd, forKey: .lairDescriptionEnd)
try container.encode(self.isRegional, forKey: .isRegional)
try container.encode(self.regionalDescription, forKey: .regionalDescription)
try container.encode(self.regionalDescriptionEnd, forKey: .regionalDescriptionEnd)
try container.encode([] as [TraitDTO], forKey: .properties)
try container.encode(self.lairs, forKey: .lairs)
try container.encode(self.regionals, forKey: .regionals)
try container.encode(self.specialDamage, forKey: .specialDamage)
try container.encode(self.shortName, forKey: .shortName)
try container.encode(self.doubleColumns, forKey: .doubleColumns)
try container.encode(self.separationPoint, forKey: .separationPoint)
try container.encode(self.damage, forKey: .damage)
}
}

View File

@@ -0,0 +1,37 @@
//
// Document.swift
// MonsterCards
//
// Created by Tom Hicks on 4/7/21.
//
import UIKit
import SceneKit
class MonsterDocument: UIDocument {
var monsterDTO: MonsterDTO?
var error: Error?
override func contents(forType typeName: String) throws -> Any {
let encoder = JSONEncoder()
do {
let data = try encoder.encode(monsterDTO)
return data
} catch {
return Data()
}
}
override func load(fromContents contents: Any, ofType typeName: String?) throws {
let decoder = JSONDecoder()
do {
let data = contents as! Data
monsterDTO = try decoder.decode(MonsterDTO.self, from: data)
} catch {
monsterDTO = MonsterDTO()
}
}
}

View File

@@ -0,0 +1,223 @@
//
// MonsterViewModel+CoreData.swift
// MonsterCards
//
// Created by Tom Hicks on 4/7/21.
//
import Foundation
import CoreData
extension MonsterViewModel {
convenience init(_ rawMonster: Monster?) {
self.init()
// Call the copy constructor
if (rawMonster != nil) {
self.copyFromMonster(monster: rawMonster!)
}
}
func copyFromMonster(monster: Monster) {
self.name = monster.name ?? ""
self.size = monster.size ?? ""
self.type = monster.type ?? ""
self.subType = monster.subtype ?? ""
self.alignment = monster.alignment ?? ""
self.hitDice = monster.hitDice
self.hasCustomHP = monster.hasCustomHP
self.customHP = monster.customHP ?? ""
self.armorType = monster.armorTypeEnum
self.hasShield = monster.hasShield
self.naturalArmorBonus = monster.naturalArmorBonus
self.customArmor = monster.customArmor ?? ""
self.walkSpeed = monster.walkSpeed
self.burrowSpeed = monster.burrowSpeed
self.climbSpeed = monster.climbSpeed
self.flySpeed = monster.flySpeed
self.canHover = monster.canHover
self.swimSpeed = monster.swimSpeed
self.hasCustomSpeed = monster.hasCustomSpeed
self.customSpeed = monster.customSpeed ?? ""
self.strengthScore = monster.strengthScore
self.strengthSavingThrowAdvantage = monster.strengthSavingThrowAdvantageEnum
self.strengthSavingThrowProficiency = monster.strengthSavingThrowProficiencyEnum
self.dexterityScore = monster.dexterityScore
self.dexteritySavingThrowAdvantage = monster.dexteritySavingThrowAdvantageEnum
self.dexteritySavingThrowProficiency = monster.dexteritySavingThrowProficiencyEnum
self.constitutionScore = monster.constitutionScore
self.constitutionSavingThrowAdvantage = monster.constitutionSavingThrowAdvantageEnum
self.constitutionSavingThrowProficiency = monster.constitutionSavingThrowProficiencyEnum
self.intelligenceScore = monster.intelligenceScore
self.intelligenceSavingThrowAdvantage = monster.intelligenceSavingThrowAdvantageEnum
self.intelligenceSavingThrowProficiency = monster.intelligenceSavingThrowProficiencyEnum
self.wisdomScore = monster.wisdomScore
self.wisdomSavingThrowAdvantage = monster.wisdomSavingThrowAdvantageEnum
self.wisdomSavingThrowProficiency = monster.wisdomSavingThrowProficiencyEnum
self.charismaScore = monster.charismaScore
self.charismaSavingThrowAdvantage = monster.charismaSavingThrowAdvantageEnum
self.charismaSavingThrowProficiency = monster.charismaSavingThrowProficiencyEnum
self.telepathy = monster.telepathy
self.understandsBut = monster.understandsBut ?? ""
self.challengeRating = monster.challengeRatingEnum
self.customChallengeRating = monster.customChallengeRating ?? ""
self.customProficiencyBonus = monster.customProficiencyBonus
self.isBlind = monster.isBlind
self.skills = (monster.skills?.allObjects.map {
let skill = $0 as! Skill
return SkillViewModel(
skill.name ?? "",
AbilityScore(rawValue: skill.abilityScoreName ?? "") ?? .dexterity,
ProficiencyType(rawValue: skill.proficiency ?? "") ?? .none,
AdvantageType(rawValue: skill.advantage ?? "") ?? .none
)
})!.sorted()
// self.name = rawSkill!.name ?? ""
// self.abilityScore = AbilityScore(rawValue: rawSkill!.abilityScoreName ?? "") ?? .strength
// self.proficiency = ProficiencyType(rawValue: rawSkill!.proficiency ?? "") ?? .none
// self.advantage = AdvantageType(rawValue: rawSkill!.advantage ?? "") ?? .none
self.damageImmunities = (monster.damageImmunities ?? [])
.map {StringViewModel($0)}
.sorted()
self.damageResistances = (monster.damageResistances ?? [])
.map {StringViewModel($0)}
.sorted()
self.damageVulnerabilities = (monster.damageVulnerabilities ?? [])
.map {StringViewModel($0)}
.sorted()
self.conditionImmunities = (monster.conditionImmunities ?? [])
.map {StringViewModel($0)}
.sorted()
self.senses = (monster.senses ?? [])
.map {StringViewModel($0)}
.sorted()
self.languages = (monster.languages ?? [])
.map {LanguageViewModel($0.name, $0.speaks)}
.sorted()
// These are manually sorted in the UI
self.abilities = (monster.abilities ?? [])
.map {AbilityViewModel($0.name, $0.abilityDescription)}
self.actions = (monster.actions ?? [])
.map {AbilityViewModel($0.name, $0.abilityDescription)}
self.legendaryActions = (monster.legendaryActions ?? [])
.map {AbilityViewModel($0.name, $0.abilityDescription)}
self.lairActions = (monster.lairActions ?? [])
.map {AbilityViewModel($0.name, $0.abilityDescription)}
self.regionalActions = (monster.regionalActions ?? [])
.map {AbilityViewModel($0.name, $0.abilityDescription)}
self.reactions = (monster.reactions ?? [])
.map {AbilityViewModel($0.name, $0.abilityDescription)}
// Private fields
self.shieldBonus = Int(monster.shieldBonus)
self.otherArmorDescription = monster.otherArmorDescription ?? ""
}
func copyToMonster(monster: Monster) {
monster.name = name
monster.size = size
monster.type = type
monster.subtype = subType
monster.alignment = alignment
monster.hitDice = hitDice
monster.hasCustomHP = hasCustomHP
monster.customHP = customHP
monster.armorTypeEnum = armorType
monster.hasShield = hasShield
monster.naturalArmorBonus = naturalArmorBonus
monster.customArmor = customArmor
monster.walkSpeed = walkSpeed
monster.burrowSpeed = burrowSpeed
monster.climbSpeed = climbSpeed
monster.flySpeed = flySpeed
monster.canHover = canHover
monster.swimSpeed = swimSpeed
monster.hasCustomSpeed = hasCustomSpeed
monster.customSpeed = customSpeed
monster.strengthScore = strengthScore
monster.strengthSavingThrowAdvantageEnum = strengthSavingThrowAdvantage
monster.strengthSavingThrowProficiencyEnum = strengthSavingThrowProficiency
monster.dexterityScore = dexterityScore
monster.dexteritySavingThrowAdvantageEnum = dexteritySavingThrowAdvantage
monster.dexteritySavingThrowProficiencyEnum = dexteritySavingThrowProficiency
monster.constitutionScore = constitutionScore
monster.constitutionSavingThrowAdvantageEnum = constitutionSavingThrowAdvantage
monster.constitutionSavingThrowProficiencyEnum = constitutionSavingThrowProficiency
monster.intelligenceScore = intelligenceScore
monster.intelligenceSavingThrowAdvantageEnum = intelligenceSavingThrowAdvantage
monster.intelligenceSavingThrowProficiencyEnum = intelligenceSavingThrowProficiency
monster.wisdomScore = wisdomScore
monster.wisdomSavingThrowAdvantageEnum = wisdomSavingThrowAdvantage
monster.wisdomSavingThrowProficiencyEnum = wisdomSavingThrowProficiency
monster.charismaScore = charismaScore
monster.charismaSavingThrowAdvantageEnum = charismaSavingThrowAdvantage
monster.charismaSavingThrowProficiencyEnum = charismaSavingThrowProficiency
monster.telepathy = telepathy
monster.understandsBut = understandsBut
monster.challengeRatingEnum = challengeRating
monster.customChallengeRating = customChallengeRating
monster.customProficiencyBonus = customProficiencyBonus
monster.isBlind = isBlind
// Remove missing skills from raw monster
monster.skills?.forEach {s in
let skill = s as! Skill
let skillVM = skills.first { $0.isEqualTo(rawSkill: skill) }
if (skillVM != nil) {
skillVM!.copyToSkill(skill: skill)
} else {
monster.removeFromSkills(skill)
}
}
// Add new skills to raw monster
skills.forEach {skillVM in
if (!(monster.skills?.contains(
where: {
skillVM.isEqualTo(rawSkill: $0 as? Skill)
}) ?? true)){
monster.addToSkills(skillVM.buildRawSkill(context: monster.managedObjectContext))
}
}
monster.conditionImmunities = conditionImmunities.map {$0.name}
monster.damageImmunities = damageImmunities.map {$0.name}
monster.damageResistances = damageResistances.map {$0.name}
monster.damageVulnerabilities = damageVulnerabilities.map {$0.name}
monster.senses = senses.map {$0.name}
// This is necessary so core data sees the language objects as changed. Without it they won't be persisted.
monster.languages = languages.map {LanguageViewModel($0.name, $0.speaks)}
monster.abilities = abilities.map {AbilityViewModel($0.name, $0.abilityDescription)}
monster.actions = actions.map {AbilityViewModel($0.name, $0.abilityDescription)}
monster.legendaryActions = legendaryActions.map {AbilityViewModel($0.name, $0.abilityDescription)}
monster.lairActions = lairActions.map {AbilityViewModel($0.name, $0.abilityDescription)}
monster.regionalActions = regionalActions.map {AbilityViewModel($0.name, $0.abilityDescription)}
monster.reactions = reactions.map {AbilityViewModel($0.name, $0.abilityDescription)}
monster.shieldBonus = Int64(shieldBonus)
monster.otherArmorDescription = otherArmorDescription
}
}

View File

@@ -0,0 +1,706 @@
//
// MonsterViewModel.swift
// MonsterCards
//
// Created by Tom Hicks on 1/18/21.
//
import Foundation
import CoreData
class MonsterViewModel: ObservableObject {
// TODO: Determine whether to prefer Int or Int64 for these fields and switch as many as possible to the winner.
@Published var name: String
@Published var size: String
@Published var type: String
@Published var subType: String
@Published var alignment: String
@Published var hitDice: Int64
@Published var hasCustomHP: Bool
@Published var customHP: String
@Published var armorType: ArmorType
@Published var hasShield: Bool {
didSet { shieldBonus = hasShield ? 2 : 0 }
}
@Published var naturalArmorBonus: Int64
@Published var customArmor: String
@Published var walkSpeed: Int64
@Published var burrowSpeed: Int64
@Published var climbSpeed: Int64
@Published var flySpeed: Int64
@Published var canHover: Bool
@Published var swimSpeed: Int64
@Published var hasCustomSpeed: Bool
@Published var customSpeed: String
@Published var strengthScore: Int64
@Published var dexterityScore: Int64
@Published var constitutionScore: Int64
@Published var intelligenceScore: Int64
@Published var wisdomScore: Int64
@Published var charismaScore: Int64
@Published var strengthSavingThrowProficiency: ProficiencyType
@Published var strengthSavingThrowAdvantage: AdvantageType
@Published var dexteritySavingThrowProficiency: ProficiencyType
@Published var dexteritySavingThrowAdvantage: AdvantageType
@Published var constitutionSavingThrowProficiency: ProficiencyType
@Published var constitutionSavingThrowAdvantage: AdvantageType
@Published var intelligenceSavingThrowProficiency: ProficiencyType
@Published var intelligenceSavingThrowAdvantage: AdvantageType
@Published var wisdomSavingThrowProficiency: ProficiencyType
@Published var wisdomSavingThrowAdvantage: AdvantageType
@Published var charismaSavingThrowProficiency: ProficiencyType
@Published var charismaSavingThrowAdvantage: AdvantageType
@Published var skills: [SkillViewModel]
@Published var damageImmunities: [StringViewModel]
@Published var damageResistances: [StringViewModel]
@Published var damageVulnerabilities: [StringViewModel]
@Published var conditionImmunities: [StringViewModel]
@Published var senses: [StringViewModel]
@Published var languages: [LanguageViewModel]
@Published var telepathy: Int64
@Published var understandsBut: String
@Published var challengeRating: ChallengeRating
@Published var customChallengeRating: String
@Published var customProficiencyBonus: Int64
@Published var abilities: [AbilityViewModel]
@Published var actions: [AbilityViewModel]
@Published var legendaryActions: [AbilityViewModel]
@Published var lairActions: [AbilityViewModel]
@Published var regionalActions: [AbilityViewModel]
@Published var reactions: [AbilityViewModel]
@Published var isBlind: Bool
@Published var shieldBonus: Int
@Published var otherArmorDescription: String
let kBaseArmorClassUnarmored = 10;
let kBaseArmorClassMageArmor = 13;
let kBaseArmorClassPadded = 11;
let kBaseArmorClassLeather = 11;
let kBaseArmorClassStudded = 12;
let kBaseArmorClassHide = 12;
let kBaseArmorClassChainShirt = 13;
let kBaseArmorClassScaleMail = 14;
let kBaseArmorClassBreastplate = 14;
let kBaseArmorClassHalfPlate = 15;
let kBaseArmorClassRingMail = 14;
let kBaseArmorClassChainMail = 16;
let kBaseArmorClassSplintMail = 17;
let kBaseArmorClassPlate = 18;
init() {
self.name = ""
self.size = ""
self.type = ""
self.subType = ""
self.alignment = ""
self.hitDice = 0
self.hasCustomHP = false
self.customHP = ""
self.armorType = .none
self.hasShield = false
self.naturalArmorBonus = 0
self.customArmor = ""
self.walkSpeed = 0
self.burrowSpeed = 0
self.climbSpeed = 0
self.flySpeed = 0
self.canHover = false
self.swimSpeed = 0
self.hasCustomSpeed = false
self.customSpeed = ""
self.strengthScore = 10
self.strengthSavingThrowAdvantage = .none
self.strengthSavingThrowProficiency = .none
self.dexterityScore = 10
self.dexteritySavingThrowAdvantage = .none
self.dexteritySavingThrowProficiency = .none
self.constitutionScore = 10
self.constitutionSavingThrowAdvantage = .none
self.constitutionSavingThrowProficiency = .none
self.intelligenceScore = 10
self.intelligenceSavingThrowAdvantage = .none
self.intelligenceSavingThrowProficiency = .none
self.wisdomScore = 10
self.wisdomSavingThrowAdvantage = .none
self.wisdomSavingThrowProficiency = .none
self.charismaScore = 10
self.charismaSavingThrowAdvantage = .none
self.charismaSavingThrowProficiency = .none
self.skills = []
self.damageImmunities = []
self.damageResistances = []
self.damageVulnerabilities = []
self.conditionImmunities = []
self.senses = []
self.languages = []
self.telepathy = 0
self.understandsBut = ""
self.challengeRating = .one
self.customChallengeRating = ""
self.customProficiencyBonus = 0
self.abilities = []
self.actions = []
self.legendaryActions = []
self.lairActions = []
self.regionalActions = []
self.reactions = []
self.isBlind = false
// Private properties
self.shieldBonus = 0
self.otherArmorDescription = ""
}
// MARK: Basic Info
var meta: String {
get {
// size type (subtype) alignment
var parts: [String] = []
if (!(self.size.isEmpty)) {
parts.append(self.size)
}
if (!(self.type.isEmpty)) {
parts.append(self.type)
}
if (!(self.subType.isEmpty)) {
parts.append(String.init(format: "(%@)", arguments: [self.subType]))
}
if (!(self.alignment.isEmpty)) {
parts.append(self.alignment)
}
return parts.joined(separator: " ")
}
}
var sizeEnum: SizeType {
get {
return SizeType.init(rawValue: size) ?? .medium
}
set {
size = newValue.rawValue
}
}
var hitPoints: String {
get {
if (hasCustomHP) {
return customHP;
} else {
let dieSize = Double(MonsterViewModel.hitDieForSize(sizeEnum))
let conMod = Double(constitutionModifier)
// let level1HP = Double(dieSize + conMod)
let level1HP = Double(dieSize/2.0 + conMod)
let extraLevels = Double(hitDice - 1)
let levelNHP = (dieSize + 1.0) / 2.0 + conMod
let extraLevelsHP = extraLevels * levelNHP
let hpTotal = Int(ceil(level1HP + extraLevelsHP))
let conBonus = Int(conMod) * Int(hitDice)
return String(format: "%d (%dd%d%+d)", hpTotal, hitDice, Int(dieSize), conBonus)
}
}
}
var speed: String {
get {
if (hasCustomSpeed) {
return customSpeed
} else {
var parts: [String] = []
if (walkSpeed > 0) {
parts.append("\(walkSpeed) ft.")
}
if (burrowSpeed > 0) {
parts.append("burrow \(burrowSpeed) ft.")
}
if (climbSpeed > 0) {
parts.append("climb \(climbSpeed) ft.")
}
if (flySpeed > 0) {
parts.append("fly \(flySpeed) ft.\(canHover ? " (hover)": "")")
}
if (swimSpeed > 0) {
parts.append("swim \(swimSpeed) ft.")
}
return parts.joined(separator: ", ")
}
}
}
class func hitDieForSize(_ size: SizeType) -> Int {
switch size {
case .tiny: return 4
case .small: return 6
case .medium: return 8
case .large: return 10
case .huge: return 12
case .gargantuan: return 20
}
}
// MARK: Ability Scores
class func abilityModifierForScore(_ score: Int) -> Int {
return Int(floor(Double((score - 10)) / 2.0))
}
func abilityModifierForAbilityScore(_ abilityScore: AbilityScore) -> Int {
switch abilityScore {
case .strength:
return strengthModifier;
case .dexterity:
return dexterityModifier
case .constitution:
return constitutionModifier
case .intelligence:
return intelligenceModifier
case .wisdom:
return wisdomModifier
case .charisma:
return charismaModifier
}
}
var strengthModifier: Int {
get {
return MonsterViewModel.abilityModifierForScore(Int(strengthScore))
}
}
var dexterityModifier: Int {
get {
return MonsterViewModel.abilityModifierForScore(Int(dexterityScore))
}
}
var constitutionModifier: Int {
get {
return MonsterViewModel.abilityModifierForScore(Int(constitutionScore))
}
}
var intelligenceModifier: Int {
get {
return MonsterViewModel.abilityModifierForScore(Int(intelligenceScore))
}
}
var wisdomModifier: Int {
get {
return MonsterViewModel.abilityModifierForScore(Int(wisdomScore))
}
}
var charismaModifier: Int {
get {
return MonsterViewModel.abilityModifierForScore(Int(charismaScore))
}
}
// MARK: Armor
var armorClassDescription: String {
get {
var armorClassTotal = 0
if (armorType == ArmorType.none) {
// 10 + dexMod + 2 for shieldBonus "15" or "17 (shield)"
armorClassTotal = kBaseArmorClassUnarmored + dexterityModifier + Int(shieldBonus)
return "\(armorClassTotal)\(hasShield ? " (shield)" : "")"
} else if (armorType == .naturalArmor) {
// 10 + dexMod + naturalArmorBonus + 2 for shieldBonus "16 (natural armor)" or "18 (natural armor, shield)"
armorClassTotal = kBaseArmorClassUnarmored + dexterityModifier + Int(naturalArmorBonus) + Int(shieldBonus)
return "\(armorClassTotal) (natural armor\(hasShield ? " (shield)" : ""))"
} else if (armorType == .mageArmor) {
// 10 + dexMod + 2 for shield + 3 for mage armor "15 (18 with mage armor)" or 17 (shield, 20 with mage armor)
armorClassTotal = kBaseArmorClassUnarmored + dexterityModifier + Int(shieldBonus)
let acWithMageArmor = kBaseArmorClassMageArmor + dexterityModifier + Int(shieldBonus)
return String(format: "%d (%@%d with mage armor)", armorClassTotal, (hasShield ? "shield, " : ""), acWithMageArmor)
} else if (armorType == .padded) {
// 11 + dexMod + 2 for shield "18 (padded armor, shield)"
armorClassTotal = kBaseArmorClassPadded + dexterityModifier + Int(shieldBonus)
return String(format: "%d (padded%@)", armorClassTotal, (hasShield ? "shield, " : ""))
} else if (armorType == .leather) {
// 11 + dexMod + 2 for shield "18 (leather, shield)"
armorClassTotal = kBaseArmorClassLeather + dexterityModifier + Int(shieldBonus)
return String(format:"%d (leather%@)", armorClassTotal, (hasShield ? "shield, " : ""))
} else if (armorType == .studdedLeather) {
// 12 + dexMod +2 for shield "17 (studded leather)"
armorClassTotal = kBaseArmorClassStudded + dexterityModifier + Int(shieldBonus)
return String(format: "%d (studded leather%@)", armorClassTotal, (hasShield ? "shield, " : ""))
} else if (armorType == .hide) {
// 12 + Min(2, dexMod) + 2 for shield "12 (hide armor)"
armorClassTotal = kBaseArmorClassHide + min(2, dexterityModifier) + Int(shieldBonus)
return String(format: "%d (hide%@)", armorClassTotal, (hasShield ? ", shield" : ""))
} else if (armorType == .chainShirt) {
// 13 + Min(2, dexMod) + 2 for shield "12 (chain shirt)"
armorClassTotal = kBaseArmorClassChainShirt + min(2, dexterityModifier) + Int(shieldBonus)
return String(format: "%d (chain shirt%@)", armorClassTotal, (hasShield ? ", shield" : ""))
} else if (armorType == .scaleMail) {
// 14 + Min(2, dexMod) + 2 for shield "14 (scale mail)"
armorClassTotal = kBaseArmorClassScaleMail + min(2, dexterityModifier) + Int(shieldBonus)
return String(format: "%d (scale mail%@)", armorClassTotal, (hasShield ? ", shield" : ""))
} else if (armorType == .breastplate) {
// 14 + Min(2, dexMod) + 2 for shield "16 (breastplate)"
armorClassTotal = kBaseArmorClassBreastplate + min(2, dexterityModifier) + Int(shieldBonus)
return String(format: "%d (breastplate%@)", armorClassTotal, (hasShield ? ", shield" : ""))
} else if (armorType == .halfPlate) {
// 15 + Min(2, dexMod) + 2 for shield "17 (half plate)"
armorClassTotal = kBaseArmorClassHalfPlate + min(2, dexterityModifier) + Int(shieldBonus)
return String(format: "%d (half plate%@)", armorClassTotal, (hasShield ? ", shield" : ""))
} else if (armorType == .ringMail) {
// 14 + 2 for shield "14 (ring mail)
armorClassTotal = kBaseArmorClassRingMail + Int(shieldBonus)
return String(format: "%d (ring mail%@)", armorClassTotal, (hasShield ? ", shield" : ""))
} else if (armorType == .chainMail) {
// 16 + 2 for shield "16 (chain mail)"
armorClassTotal = kBaseArmorClassChainMail + Int(shieldBonus)
return String(format: "%d (chain mail%@)", armorClassTotal, (hasShield ? ", shield" : ""))
} else if (armorType == .splintMail) {
// 17 + 2 for shield "17 (splint)"
armorClassTotal = kBaseArmorClassSplintMail + Int(shieldBonus)
return String(format: "%d (splint%@)", armorClassTotal, (hasShield ? ", shield" : ""))
} else if (armorType == .plateMail) {
// 18 + 2 for shield "18 (plate)"
armorClassTotal = kBaseArmorClassPlate + Int(shieldBonus)
return String(format: "%d (plate%@)", armorClassTotal, (hasShield ? ", shield" : ""))
} else if (armorType == .other) {
// pure string value shield check does nothing just copies the string from otherArmorDesc
return otherArmorDescription;
} else {
return ""
}
}
}
// MARK: Challenge Rating / Proficiency Bonus
var proficiencyBonus: Int {
switch challengeRating {
case .custom:
return Int(customProficiencyBonus)
case .zero:
fallthrough
case .oneEighth:
fallthrough
case .oneQuarter:
fallthrough
case .oneHalf:
fallthrough
case .one:
fallthrough
case .two:
fallthrough
case .three:
fallthrough
case .four:
return 2
case .five:
fallthrough
case .six:
fallthrough
case .seven:
fallthrough
case .eight:
return 3
case .nine:
fallthrough
case .ten:
fallthrough
case .eleven:
fallthrough
case .twelve:
return 4
case .thirteen:
fallthrough
case .fourteen:
fallthrough
case .fifteen:
fallthrough
case .sixteen:
return 5
case .seventeen:
fallthrough
case .eighteen:
fallthrough
case .nineteen:
fallthrough
case .twenty:
return 6
case .twentyOne:
fallthrough
case .twentyTwo:
fallthrough
case .twentyThree:
fallthrough
case .twentyFour:
return 7
case .twentyFive:
fallthrough
case .twentySix:
fallthrough
case .twentySeven:
fallthrough
case .twentyEight:
return 8
case .twentyNine:
fallthrough
case .thirty:
return 9
}
}
func proficiencyBonusForType(_ profType: ProficiencyType) -> Int {
switch profType {
case .none:
return 0
case .proficient:
return proficiencyBonus
case .expertise:
return proficiencyBonus * 2
}
}
// MARK: Saving Throws
var savingThrowsDescription: String {
get {
// TODO: port from objective-c
var parts: [String] = []
var name: String
var advantage: String
var bonus: Int
if (strengthSavingThrowAdvantage != .none || strengthSavingThrowProficiency != .none) {
name = "Strength"
bonus = strengthModifier + proficiencyBonusForType(strengthSavingThrowProficiency)
advantage = MonsterViewModel.advantageLabelStringForType(strengthSavingThrowAdvantage)
if (!advantage.isEmpty) {
advantage = " " + advantage
}
parts.append(String(format: "%@ %+d%@", name, bonus, advantage))
}
if (dexteritySavingThrowAdvantage != .none || dexteritySavingThrowProficiency != .none) {
name = "Dexterity"
bonus = dexterityModifier + proficiencyBonusForType(dexteritySavingThrowProficiency)
advantage = MonsterViewModel.advantageLabelStringForType(dexteritySavingThrowAdvantage)
if (!advantage.isEmpty) {
advantage = " " + advantage
}
parts.append(String(format: "%@ %+d%@", name, bonus, advantage))
}
if (constitutionSavingThrowAdvantage != .none || constitutionSavingThrowProficiency != .none) {
name = "Constitution"
bonus = constitutionModifier + proficiencyBonusForType(constitutionSavingThrowProficiency)
advantage = MonsterViewModel.advantageLabelStringForType(constitutionSavingThrowAdvantage)
if (!advantage.isEmpty) {
advantage = " " + advantage
}
parts.append(String(format: "%@ %+d%@", name, bonus, advantage))
}
if (intelligenceSavingThrowAdvantage != .none || intelligenceSavingThrowProficiency != .none) {
name = "Intelligence"
bonus = intelligenceModifier + proficiencyBonusForType(intelligenceSavingThrowProficiency)
advantage = MonsterViewModel.advantageLabelStringForType(intelligenceSavingThrowAdvantage)
if (!advantage.isEmpty) {
advantage = " " + advantage
}
parts.append(String(format: "%@ %+d%@", name, bonus, advantage))
}
if (wisdomSavingThrowAdvantage != .none || wisdomSavingThrowProficiency != .none) {
name = "Wisdom"
bonus = wisdomModifier + proficiencyBonusForType(wisdomSavingThrowProficiency)
advantage = MonsterViewModel.advantageLabelStringForType(wisdomSavingThrowAdvantage)
if (!advantage.isEmpty) {
advantage = " " + advantage
}
parts.append(String(format: "%@ %+d%@", name, bonus, advantage))
}
if (charismaSavingThrowAdvantage != .none || charismaSavingThrowProficiency != .none) {
name = "Charisma"
bonus = charismaModifier + proficiencyBonusForType(charismaSavingThrowProficiency)
advantage = MonsterViewModel.advantageLabelStringForType(charismaSavingThrowAdvantage)
if (!advantage.isEmpty) {
advantage = " " + advantage
}
parts.append(String(format: "%@ %+d%@", name, bonus, advantage))
}
return parts.joined(separator: ", ")
}
}
// MARK: Misc Helpers
class func advantageLabelStringForType(_ advType: AdvantageType) -> String {
switch advType {
case .none:
return ""
case .advantage:
return "(A)"
case .disadvantage:
return "(D)"
}
}
// MARK: Skills
var skillsDescription: String {
get {
let sortedSkills = self.skills.sorted {$0.name < $1.name}
return sortedSkills.reduce("") {
if $0 == "" {
return $1.skillDescription(forMonster: self)
} else {
return $0 + ", " + $1.skillDescription(forMonster: self)
}
}
}
}
// MARK: Immunities, Resistances, and Vulnerabilities
var damageVulnerabilitiesDescription: String {
get {
// TODO: sort "bludgeoning, piercing, and slashing from nonmagical attacks" to the end and use ; as a separator before it.
let sortedVulnerabilities = self.damageVulnerabilities.sorted().map({$0.name})
return StringHelper.oxfordJoin(sortedVulnerabilities)
}
}
var damageResistancesDescription: String {
get {
// TODO: sort "bludgeoning, piercing, and slashing from nonmagical attacks" to the end and use ; as a separator before it.
let sortedResistances = self.damageResistances.sorted().map({$0.name})
return StringHelper.oxfordJoin(sortedResistances)
}
}
var damageImmunitiesDescription: String {
get {
// TODO: sort "bludgeoning, piercing, and slashing from nonmagical attacks" to the end and use ; as a separator before it.
let sortedImmunities = self.damageImmunities.sorted().map({$0.name})
return StringHelper.oxfordJoin(sortedImmunities)
}
}
var conditionImmunitiesDescription: String {
get {
let sortedImmunities = self.conditionImmunities.sorted().map({$0.name})
return StringHelper.oxfordJoin(sortedImmunities)
}
}
// MARK: OTHER
var passivePerception: Int {
get {
let perceptionSkill = skills.first(where: {
StringHelper.safeEqualsIgnoreCase($0.name, "Perception")
})
if (perceptionSkill == nil) {
return 10 + wisdomModifier
} else if (perceptionSkill!.proficiency == ProficiencyType.expertise) {
return 10 + wisdomModifier + proficiencyBonus + proficiencyBonus
} else if (perceptionSkill!.proficiency == ProficiencyType.proficient) {
return 10 + wisdomModifier + proficiencyBonus
} else {
return 10 + wisdomModifier
}
}
}
var sensesDescription: String {
get {
var modifiedSenses = self.senses.sorted().map({$0.name})
let hasPassivePerceptionSense = modifiedSenses.contains(where: {
$0.starts(with: "passive Perception")
})
if (!hasPassivePerceptionSense) {
let calculatedPassivePerception = String(format: "passive Perception %d", passivePerception)
modifiedSenses.append(calculatedPassivePerception)
}
return modifiedSenses.sorted().joined(separator: ", ")
}
}
var languagesDescription: String {
get {
let spokenLanguages =
languages
.filter({ $0.speaks })
.map({$0.name})
.sorted()
let understoodLanguages =
languages
.filter({ !$0.speaks })
.map({$0.name})
.sorted()
let understandsButText = understandsBut.isEmpty
? ""
: String(format: " but %@", understandsBut)
let telepathyText = telepathy > 0
? String(format: ", telepathy %d ft.", telepathy)
: ""
if (spokenLanguages.count > 0) {
if (understoodLanguages.count > 0) {
return String(
format:"%@ and understands %@%@%@",
StringHelper.oxfordJoin(spokenLanguages),
StringHelper.oxfordJoin(understoodLanguages),
understandsButText,
telepathyText)
} else {
return String(
format: "%@%@%@",
StringHelper.oxfordJoin(spokenLanguages),
understandsButText,
telepathyText)
}
} else {
if (understoodLanguages.count > 0) {
return String(
format: "understands %@%@%@",
StringHelper.oxfordJoin(understoodLanguages),
understandsButText,
telepathyText)
} else if (telepathy > 0){
return String(format: "telepathy %d ft.", telepathy)
} else {
return ""
}
}
}
}
var challengeRatingDescription: String {
get {
if (challengeRating != .custom) {
return challengeRating.displayName
} else {
return customChallengeRating
}
}
}
// MARK: End
}

View File

@@ -0,0 +1,35 @@
//
// SavingThrowDTO.swift
// MonsterCards
//
// Created by Tom Hicks on 3/28/21.
//
import Foundation
struct SavingThrowDTO {
var name: String
var order: Int
}
private enum SavingThrowDTOCodingKeys: String, CodingKey {
case name = "name"
case order = "order"
}
extension SavingThrowDTO: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: SavingThrowDTOCodingKeys.self)
self.name = (try? container.decode(String.self, forKey: .name)) ?? ""
self.order = (try? container.decode(Int.self, forKey: .order)) ?? 0
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: SavingThrowDTOCodingKeys.self)
try container.encode(self.name, forKey: .name)
try container.encode(self.order, forKey: .order)
}
}

View File

@@ -0,0 +1,50 @@
//
// Skill+CoreDataClass.swift
// MonsterCards
//
// Created by Tom Hicks on 1/18/21.
//
//
import Foundation
import CoreData
@objc(Skill)
public class Skill: NSManagedObject {
var wrappedName: String {
get {
return name ?? ""
}
set {
name = newValue
}
}
var wrappedProficiency: ProficiencyType {
get {
return ProficiencyType.init(rawValue: proficiency ?? "") ?? .none
}
set {
proficiency = newValue.rawValue
}
}
var wrappedAbilityScore: AbilityScore {
get {
return AbilityScore.init(rawValue: abilityScoreName ?? "") ?? .strength
}
set {
abilityScoreName = newValue.rawValue
}
}
var wrappedAdvantage: AdvantageType {
get {
return AdvantageType.init(rawValue: advantage ?? "") ?? .none
}
set {
advantage = newValue.rawValue
}
}
}

View File

@@ -0,0 +1,39 @@
//
// SkillDTO.swift
// MonsterCards
//
// Created by Tom Hicks on 3/28/21.
//
import Foundation
struct SkillDTO {
var name: String
var stat: String
var note: String
}
private enum SkillDTOCodingKeys: String, CodingKey {
case name = "name"
case stat = "stat"
case note = "note"
}
extension SkillDTO: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: SkillDTOCodingKeys.self)
self.name = (try? container.decode(String.self, forKey: .name)) ?? ""
self.note = (try? container.decode(String.self, forKey: .note)) ?? ""
self.stat = (try? container.decode(String.self, forKey: .stat)) ?? ""
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: SkillDTOCodingKeys.self)
try container.encode(self.name, forKey: .name)
try container.encode(self.note, forKey: .note)
try container.encode(self.stat, forKey: .stat)
}
}

View File

@@ -0,0 +1,57 @@
//
// SkillViewModel+CoreData.swift
// MonsterCards
//
// Created by Tom Hicks on 4/7/21.
//
import Foundation
import CoreData
extension SkillViewModel {
func isEqualTo(rawSkill: Skill?) -> Bool {
if (rawSkill == nil) {
return false;
} else if (abilityScore != rawSkill!.wrappedAbilityScore) {
return false;
} else if (advantage != rawSkill!.wrappedAdvantage) {
return false;
} else if (name != rawSkill!.name) {
return false;
} else if (proficiency != rawSkill!.wrappedProficiency) {
return false;
} else {
return true
}
}
func copyToSkill(skill: Skill) {
skill.wrappedAbilityScore = abilityScore
skill.wrappedAdvantage = advantage
skill.name = name
skill.wrappedProficiency = proficiency
}
convenience init(_ rawSkill: Skill?) {
if (rawSkill == nil) {
self.init()
} else {
let skill = rawSkill!
self.init(
skill.wrappedName,
skill.wrappedAbilityScore,
skill.wrappedProficiency,
skill.wrappedAdvantage
)
}
}
func buildRawSkill(context: NSManagedObjectContext?) -> Skill {
let newSkill = context == nil ? Skill.init() : Skill.init(context: context!)
newSkill.name = name
newSkill.wrappedAbilityScore = abilityScore
newSkill.wrappedProficiency = proficiency
newSkill.wrappedAdvantage = advantage
return newSkill
}
}

View File

@@ -0,0 +1,67 @@
//
// SkillViewModel.swift
// MonsterCards
//
// Created by Tom Hicks on 1/18/21.
//
import Foundation
import CoreData
class SkillViewModel: ObservableObject, Identifiable {
@Published var name: String
@Published var abilityScore: AbilityScore
@Published var proficiency: ProficiencyType
@Published var advantage: AdvantageType
init(_ name: String = "", _ abilityScore: AbilityScore = .dexterity, _ proficiency: ProficiencyType = .proficient, _ advantage: AdvantageType = .none) {
self.name = name
self.abilityScore = abilityScore
self.proficiency = proficiency
self.advantage = advantage
}
func modifier(forMonster: MonsterViewModel) -> Int {
let proficiencyBonus = Double(forMonster.proficiencyBonus)
let abilityScoreModifier = Double(forMonster.abilityModifierForAbilityScore(abilityScore))
switch proficiency {
case .none:
return Int(abilityScoreModifier)
case .proficient:
return Int(abilityScoreModifier + proficiencyBonus)
case .expertise:
return Int(abilityScoreModifier + 2 * proficiencyBonus)
}
}
func skillDescription(forMonster: MonsterViewModel) -> String {
var advantageLabel = MonsterViewModel.advantageLabelStringForType(advantage)
if (advantageLabel != "") {
advantageLabel = " " + advantageLabel
}
return String(format: "%@ %+d%@", name, modifier(forMonster: forMonster), advantageLabel)
}
}
extension SkillViewModel: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(abilityScore)
hasher.combine(advantage)
hasher.combine(name)
hasher.combine(proficiency)
}
}
extension SkillViewModel: Comparable {
static func < (lhs: SkillViewModel, rhs: SkillViewModel) -> Bool {
return lhs.name < rhs.name
}
static func == (lhs: SkillViewModel, rhs: SkillViewModel) -> Bool {
return lhs.abilityScore == rhs.abilityScore
&& lhs.advantage == rhs.advantage
&& lhs.name == rhs.name
&& lhs.proficiency == rhs.proficiency
}
}

View File

@@ -0,0 +1,24 @@
//
// DamageTypesViewModel.swift
// MonsterCards
//
// Created by Tom Hicks on 3/22/21.
//
import Foundation
class StringViewModel: ObservableObject, Comparable, Identifiable {
static func < (lhs: StringViewModel, rhs: StringViewModel) -> Bool {
lhs.name < rhs.name
}
static func == (lhs: StringViewModel, rhs: StringViewModel) -> Bool {
lhs.name == rhs.name
}
@Published var name: String
init(_ name: String = "") {
self.name = name
}
}

View File

@@ -0,0 +1,39 @@
//
// TraitDTO.swift
// MonsterCards
//
// Created by Tom Hicks on 3/28/21.
//
import Foundation
struct TraitDTO {
var name: String
var note: String
var desc: String
}
private enum TraitDTOCodingKeys: String, CodingKey {
case name = "name"
case note = "note"
case desc = "desc"
}
extension TraitDTO: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: TraitDTOCodingKeys.self)
self.name = (try? container.decode(String.self, forKey: .name)) ?? ""
self.note = (try? container.decode(String.self, forKey: .note)) ?? ""
self.desc = (try? container.decode(String.self, forKey: .desc)) ?? ""
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: TraitDTOCodingKeys.self)
try container.encode(self.name, forKey: .name)
try container.encode(self.note, forKey: .note)
try container.encode(self.desc, forKey: .desc)
}
}

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.icloud-container-identifiers</key>
<array>
<string>iCloud.com.majinnaibu.MonsterCards.MonsterCards</string>
</array>
<key>com.apple.developer.icloud-services</key>
<array>
<string>CloudKit</string>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>MonsterCards.xcdatamodel</string>
</dict>
</plist>

View File

@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17709" systemVersion="20D91" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="YES" userDefinedModelVersionIdentifier="">
<entity name="Monster" representedClassName="Monster" syncable="YES" codeGenerationType="category">
<attribute name="abilities" optional="YES" attributeType="Transformable" valueTransformerName="AbilityViewModelValueTransformer" customClassName="[AbilityViewModel]"/>
<attribute name="actions" optional="YES" attributeType="Transformable" valueTransformerName="AbilityViewModelValueTransformer" customClassName="[AbilityViewModel]"/>
<attribute name="alignment" attributeType="String" defaultValueString=""/>
<attribute name="armorType" attributeType="String" defaultValueString=""/>
<attribute name="blindsightDistance" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="burrowSpeed" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="canHover" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="challengeRating" attributeType="String" defaultValueString=""/>
<attribute name="charismaSavingThrowAdvantage" attributeType="String" defaultValueString=""/>
<attribute name="charismaSavingThrowProficiency" attributeType="String" defaultValueString="none"/>
<attribute name="charismaScore" attributeType="Integer 64" defaultValueString="10" usesScalarValueType="YES"/>
<attribute name="climbSpeed" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="conditionImmunities" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformerName" customClassName="[String]"/>
<attribute name="constitutionSavingThrowAdvantage" attributeType="String" defaultValueString="none"/>
<attribute name="constitutionSavingThrowProficiency" attributeType="String" defaultValueString="none"/>
<attribute name="constitutionScore" attributeType="Integer 64" defaultValueString="10" usesScalarValueType="YES"/>
<attribute name="customArmor" attributeType="String" defaultValueString=""/>
<attribute name="customChallengeRating" attributeType="String" defaultValueString=""/>
<attribute name="customHP" attributeType="String" defaultValueString=""/>
<attribute name="customProficiencyBonus" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="customSpeed" attributeType="String" defaultValueString=""/>
<attribute name="damageImmunities" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformerName" customClassName="[String]"/>
<attribute name="damageResistances" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformerName" customClassName="[String]"/>
<attribute name="damageVulnerabilities" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformerName" customClassName="[String]"/>
<attribute name="darkvisionDistance" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="dexteritySavingThrowAdvantage" attributeType="String" defaultValueString="none"/>
<attribute name="dexteritySavingThrowProficiency" attributeType="String" defaultValueString="none"/>
<attribute name="dexterityScore" attributeType="Integer 64" defaultValueString="10" usesScalarValueType="YES"/>
<attribute name="flySpeed" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="hasCustomHP" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="hasCustomSpeed" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="hasShield" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="hitDice" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="intelligenceSavingThrowAdvantage" attributeType="String" defaultValueString="none"/>
<attribute name="intelligenceSavingThrowProficiency" attributeType="String" defaultValueString="none"/>
<attribute name="intelligenceScore" attributeType="Integer 64" defaultValueString="10" usesScalarValueType="YES"/>
<attribute name="isBlind" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="lairActions" optional="YES" attributeType="Transformable" valueTransformerName="AbilityViewModelValueTransformer" customClassName="[AbilityViewModel]"/>
<attribute name="languages" optional="YES" attributeType="Transformable" valueTransformerName="LanguageViewModelValueTransformer" customClassName="[LanguageViewModel]"/>
<attribute name="legendaryActions" optional="YES" attributeType="Transformable" valueTransformerName="AbilityViewModelValueTransformer" customClassName="[AbilityViewModel]"/>
<attribute name="name" attributeType="String" defaultValueString="Unnamed Monster"/>
<attribute name="naturalArmorBonus" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="otherArmorDescription" attributeType="String" defaultValueString=""/>
<attribute name="reactions" optional="YES" attributeType="Transformable" valueTransformerName="AbilityViewModelValueTransformer" customClassName="[AbilityViewModel]"/>
<attribute name="regionalActions" optional="YES" attributeType="Transformable" valueTransformerName="AbilityViewModelValueTransformer" customClassName="[AbilityViewModel]"/>
<attribute name="senses" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformerName" customClassName="[String]"/>
<attribute name="shieldBonus" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="size" attributeType="String" defaultValueString=""/>
<attribute name="strengthSavingThrowAdvantage" attributeType="String" defaultValueString="none"/>
<attribute name="strengthSavingThrowProficiency" attributeType="String" defaultValueString="none"/>
<attribute name="strengthScore" attributeType="Integer 64" defaultValueString="10" usesScalarValueType="YES"/>
<attribute name="subtype" attributeType="String" defaultValueString=""/>
<attribute name="swimSpeed" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="telepathy" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="tremorsenseDistance" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="truesightDistance" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="type" attributeType="String" defaultValueString=""/>
<attribute name="understandsBut" attributeType="String" defaultValueString=""/>
<attribute name="walkSpeed" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="wisdomSavingThrowAdvantage" attributeType="String" defaultValueString="none"/>
<attribute name="wisdomSavingThrowProficiency" attributeType="String" defaultValueString="none"/>
<attribute name="wisdomScore" attributeType="Integer 64" defaultValueString="10" usesScalarValueType="YES"/>
<relationship name="skills" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="Skill" inverseName="monster" inverseEntity="Skill"/>
</entity>
<entity name="Skill" representedClassName="Skill" syncable="YES" codeGenerationType="category">
<attribute name="abilityScoreName" attributeType="String" defaultValueString="strength"/>
<attribute name="advantage" attributeType="String" defaultValueString="none"/>
<attribute name="name" attributeType="String" defaultValueString=""/>
<attribute name="proficiency" attributeType="String" defaultValueString="none"/>
<relationship name="monster" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Monster" inverseName="skills" inverseEntity="Monster"/>
</entity>
<elements>
<element name="Monster" positionX="-63" positionY="-18" width="128" height="974"/>
<element name="Skill" positionX="-63" positionY="135" width="128" height="14"/>
</elements>
</model>

View File

@@ -0,0 +1,20 @@
//
// MonsterCardsApp.swift
// MonsterCards
//
// Created by Tom Hicks on 1/15/21.
//
import SwiftUI
@main
struct MonsterCardsApp: App {
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
}
}

View File

@@ -0,0 +1,46 @@
//
// Persistence.swift
// MonsterCards
//
// Created by Tom Hicks on 1/15/21.
//
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
let monsters: [Monster] = [
Monster(context:viewContext, name: "Ted", size: "Huge", type: "humanoid", subtype: "any race", alignment: "any alignment"),
Monster(context:viewContext, name: "Steve", size: "Huge", type: "humanoid", subtype: "any race", alignment: "any alignment"),
Monster(context:viewContext, name: "Dave", size: "Huge", type: "humanoid", subtype: "any race", alignment: "any alignment")
]
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
// fatalError("TOMHICKS_Unresolved error \(nsError), \(nsError.userInfo)")
}
return result
}()
let container: NSPersistentCloudKitContainer
init(inMemory: Bool = false) {
container = NSPersistentCloudKitContainer(name: "MonsterCards")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.viewContext.automaticallyMergesChangesFromParent = true
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
}

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,21 @@
//
// Collections.swift
// MonsterCards
//
// Created by Tom Hicks on 1/15/21.
//
import SwiftUI
struct Collections: View {
// @State var allCollections: [MonsterCollection] = []
var body: some View {
Text("Collections")
}
}
struct Collections_Previews: PreviewProvider {
static var previews: some View {
Collections().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}

View File

@@ -0,0 +1,80 @@
//
// ContentView.swift
// MonsterCards
//
// Created by Tom Hicks on 1/15/21.
//
import SwiftUI
import CoreData
struct ImportInfo {
var monster: MonsterViewModel = MonsterViewModel()
}
struct ContentView: View {
@Environment(\.managedObjectContext) private var viewContext
@State private var importInfo = ImportInfo()
@State private var isShowingImportDialog = false
var body: some View {
TabView {
Search()
.tabItem {
Image(systemName: "magnifyingglass")
Text("Search")
}
Dashboard()
.tabItem {
Image(systemName: "rectangle.3.offgrid.fill")
Text("Dashboard")
}
Collections()
.tabItem {
Image(systemName: "tray.full.fill")
Text("Collections")
}
Library()
.tabItem {
Image(systemName: "book.fill")
Text("Library")
}
}
.onOpenURL(perform: beginImportingMonster)
.sheet(isPresented: $isShowingImportDialog) {
ImportMonster(monster: $importInfo.monster, isOpen: $isShowingImportDialog)
}
}
func beginImportingMonster(url: URL) {
// TOOD: only do this if the file name ends in .json or .monster
let decoder = JSONDecoder()
do {
let isAccessing = url.startAccessingSecurityScopedResource()
defer {
if (isAccessing) {
url.stopAccessingSecurityScopedResource()
}
}
let data = try Data(contentsOf: url)
let monsterDTO = try decoder.decode(MonsterDTO.self, from: data)
// TODO: check for some minimal set of properties to ensure this is the expected json schema
self.importInfo.monster = MonsterImportHelper.import5ESBMonster(monsterDTO)
// TODO: throw or set an err here and don't set isShowingImportDialog to true if the file didn't match any of our supported monster schemas.
self.isShowingImportDialog = true
} catch let error as NSError {
// TODO: show an error message to the user that we were unable to open the file and maybe why.
print(error)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}

View File

@@ -0,0 +1,20 @@
//
// Dashboard.swift
// MonsterCards
//
// Created by Tom Hicks on 1/15/21.
//
import SwiftUI
struct Dashboard: View {
var body: some View {
Text("Dashboard")
}
}
struct Dashboard_Previews: PreviewProvider {
static var previews: some View {
Dashboard().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}

View File

@@ -0,0 +1,42 @@
//
// EditAbilityScores.swift
// MonsterCards
//
// Created by Tom Hicks on 3/21/21.
//
import SwiftUI
struct EditAbilityScores: View {
@ObservedObject var monsterViewModel: MonsterViewModel
var body: some View {
List {MCStepperField(
label: "STR",
value: $monsterViewModel.strengthScore)
MCStepperField(
label: "DEX",
value: $monsterViewModel.dexterityScore)
MCStepperField(
label: "CON",
value: $monsterViewModel.constitutionScore)
MCStepperField(
label: "INT",
value: $monsterViewModel.intelligenceScore)
MCStepperField(
label: "WIS",
value: $monsterViewModel.wisdomScore)
MCStepperField(
label: "CHA",
value: $monsterViewModel.charismaScore)
}
.navigationTitle("Ability Scores")
}
}
struct EditAbilityScores_Previews: PreviewProvider {
static var previews: some View {
let viewModel = MonsterViewModel()
EditAbilityScores(monsterViewModel: viewModel)
}
}

View File

@@ -0,0 +1,45 @@
//
// EditArmor.swift
// MonsterCards
//
// Created by Tom Hicks on 3/21/21.
//
import SwiftUI
struct EditArmor: View {
@ObservedObject var monsterViewModel: MonsterViewModel
var body: some View {
List {
// Armor Type select bound to monster.armorTypeEnum
MCArmorTypePicker(
label: "Armor Type",
value: $monsterViewModel.armorType)
// Toggle bound to monster.hasShield?
Toggle(
"Has Shield",
isOn: $monsterViewModel.hasShield)
// Number with -/+ buttons bound to monster.naturalArmorBonus
MCStepperField(
label: "Natural Armor Bonus",
value: $monsterViewModel.naturalArmorBonus)
// Editable Text field bound to monster.customArmorText?
MCTextField(
label: "Custom Armor",
value: $monsterViewModel.customArmor)
.autocapitalization(.none)
}
.navigationTitle("Armor")
}
}
struct EditArmor_Previews: PreviewProvider {
static var previews: some View {
let viewModel = MonsterViewModel()
EditArmor(monsterViewModel: viewModel)
}
}

View File

@@ -0,0 +1,71 @@
//
// EditBasicInfo.swift
// MonsterCards
//
// Created by Tom Hicks on 3/21/21.
//
import SwiftUI
struct EditBasicInfo: View {
@ObservedObject var monsterViewModel: MonsterViewModel
var body: some View {
List {
// Editable Text field bound to monster.name
MCTextField(
label: "Name",
value: $monsterViewModel.name)
.autocapitalization(.words)
// Editable Text field bound to monster.size
MCTextField(
label: "Size",
value: $monsterViewModel.size)
.autocapitalization(.words)
// Editable Text field bound to monster.type
MCTextField(
label: "Type",
value: $monsterViewModel.type)
.autocapitalization(.none)
// Editable Text field bound to monster.subType
MCTextField(
label: "Subtype",
value: $monsterViewModel.subType)
.autocapitalization(.none)
// Editable Text field bound to monster.alignment
MCTextField(
label: "Alignment",
value: $monsterViewModel.alignment)
.autocapitalization(.none)
// Number with -/+ buttons bound to monster.hitDice
MCStepperField(
label: "Hit Dice",
value: $monsterViewModel.hitDice)
// Toggle bound to monster.hasCustomHP?
Toggle(
"Has Custom HP",
isOn:$monsterViewModel.hasCustomHP)
// Editable Text field bound to monster.customHpText?
MCTextField(
label: "Custom HP",
value: $monsterViewModel.customHP)
.autocapitalization(.none)
}
.navigationTitle("Basic Info")
}
}
struct EditBasicInfo_Previews: PreviewProvider {
static var previews: some View {
let viewModel = MonsterViewModel.init(nil)
EditBasicInfo(monsterViewModel: viewModel)
}
}

View File

@@ -0,0 +1,41 @@
//
// EditChallengeRating.swift
// MonsterCards
//
// Created by Tom Hicks on 3/24/21.
//
import SwiftUI
struct EditChallengeRating: View {
@ObservedObject var viewModel: MonsterViewModel
var body: some View {
let isUsingCustomProficiencyBonus = viewModel.challengeRating == ChallengeRating.custom
VStack(alignment: .leading) {
MCChallengeRatingPicker(
label: "Rating",
value: $viewModel.challengeRating)
MCTextField(
label: "Custom Text",
value: $viewModel.customChallengeRating)
.disabled(!isUsingCustomProficiencyBonus)
MCStepperField(
label: "Custom Proficiency Bonus",
value: $viewModel.customProficiencyBonus)
.disabled(!isUsingCustomProficiencyBonus)
Spacer()
}
.padding()
}
}
struct EditChallengeRating_Previews: PreviewProvider {
static var previews: some View {
let viewModel = MonsterViewModel()
EditChallengeRating(viewModel: viewModel)
}
}

View File

@@ -0,0 +1,33 @@
//
// EditLanguage.swift
// MonsterCards
//
// Created by Tom Hicks on 3/24/21.
//
import SwiftUI
struct EditLanguage: View {
@ObservedObject var viewModel: LanguageViewModel
var body: some View {
VStack(alignment: .leading) {
MCTextField(
label: "Name",
value: $viewModel.name)
.autocapitalization(.none)
Toggle("Speaks", isOn: $viewModel.speaks)
Spacer()
}
.padding()
}
}
struct EditLanguage_Previews: PreviewProvider {
static var previews: some View {
let viewModel = LanguageViewModel()
EditLanguage(viewModel: viewModel)
}
}

View File

@@ -0,0 +1,58 @@
//
// EditLanguages.swift
// MonsterCards
//
// Created by Tom Hicks on 3/24/21.
//
import SwiftUI
struct EditLanguages: View {
@ObservedObject var viewModel: MonsterViewModel
var body: some View {
let sortedLanguages = viewModel.languages.sorted()
List {
MCTextField(
label: "Understands But",
value: $viewModel.understandsBut)
MCStepperField(label: "Telepathy", prefix: "", step: 5, suffix: " ft.", value: $viewModel.telepathy)
ForEach(sortedLanguages/*viewModel.languages*/) { language in
NavigationLink(language.name, destination: EditLanguage(viewModel: language))
}
.onDelete(perform: { indexSet in
for index in indexSet {
viewModel.languages.remove(at: index)
}
})
}
.toolbar(content: {
ToolbarItemGroup(placement: .navigationBarTrailing) {
EditButton()
Button(
action: {
let newLanguage = LanguageViewModel("English")
viewModel.languages.append(newLanguage)
viewModel.languages = viewModel.languages.sorted()
},
label: {
Image(systemName: "plus")
}
)
}
})
.onAppear(perform: {
viewModel.languages = viewModel.languages.sorted()
})
}
}
struct EditLanguages_Previews: PreviewProvider {
static var previews: some View {
let viewModel = MonsterViewModel()
EditLanguages(viewModel: viewModel)
}
}

View File

@@ -0,0 +1,220 @@
//
// EditMonster.swift
// MonsterCards
//
// Created by Tom Hicks on 1/16/21.
//
import CoreData
import SwiftUI
struct EditMonster: View {
// TODO: Add challengeRating/challengeRatingEnum and customChallengeRating maybe in basicInfo
// TODO: Add a way to edit the monster being blind. Probably a header section to the senses section.
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
@Environment(\.managedObjectContext) private var viewContext
var monster: Monster
@StateObject private var monsterViewModel: MonsterViewModel = MonsterViewModel()
@State private var hasInitializedViewModel = false
var body: some View {
List {
Group {
NavigationLink(
"Basic Info",
destination: EditBasicInfo(monsterViewModel: monsterViewModel))
NavigationLink(
"Armor",
destination: EditArmor(monsterViewModel: monsterViewModel))
NavigationLink(
"Speed",
destination: EditSpeed(monsterViewModel: monsterViewModel))
NavigationLink(
"Ability Scores",
destination: EditAbilityScores(monsterViewModel: monsterViewModel))
NavigationLink(
"Saving Throws",
destination: EditSavingThrows(monsterViewModel: monsterViewModel))
NavigationLink(
"Skills",
destination: EditSkills(monsterViewModel: monsterViewModel))
NavigationLink(
"Condition Immunities",
destination: EditStrings(
viewModel: monsterViewModel,
path: \.conditionImmunities,
title: "Condition Immunities"))
NavigationLink(
"Damage Immunities",
destination: EditStrings(
viewModel: monsterViewModel,
path: \.damageImmunities,
title: "Damage Immunities"))
NavigationLink(
"Damage Resistances",
destination: EditStrings(
viewModel: monsterViewModel,
path: \.damageResistances,
title: "Damage Resistances"))
NavigationLink(
"Damage Vulnerabilities",
destination: EditStrings(
viewModel: monsterViewModel,
path: \.damageVulnerabilities,
title: "Damage Vulnerabilities"))
}
Group {
NavigationLink(
"Senses",
destination: EditStrings(
viewModel: monsterViewModel,
path: \.senses,
title: "Senses"))
NavigationLink(
"Languages",
destination: EditLanguages(viewModel: monsterViewModel))
NavigationLink(
"Challenge Rating",
destination: EditChallengeRating(viewModel: monsterViewModel))
NavigationLink(
"Abilities",
destination: EditTraits(
viewModel: monsterViewModel,
path: \.abilities,
title: "Abilities"))
NavigationLink(
"Actions",
destination: EditTraits(
viewModel: monsterViewModel,
path: \.actions,
title: "Actions"))
NavigationLink(
"Reactions",
destination: EditTraits(
viewModel: monsterViewModel,
path: \.reactions,
title: "Reactions"))
NavigationLink(
"Legendary Actions",
destination: EditTraits(
viewModel: monsterViewModel,
path: \.legendaryActions,
title: "Legendary Actions"))
NavigationLink(
"Lair Actions",
destination: EditTraits(
viewModel: monsterViewModel,
path: \.lairActions,
title: "Lair Actions"))
NavigationLink(
"Regional Actions",
destination: EditTraits(
viewModel: monsterViewModel,
path: \.regionalActions,
title: "Regional Actions"))
}
}
.onAppear(perform: copyMonsterToLocal)
.toolbar(content: {
ToolbarItem(placement: .primaryAction) {
Button("Save", action: saveMonster)
}
})
.navigationTitle(monsterViewModel.name)
.navigationBarTitleDisplayMode(.inline)
}
private func dismissView() {
self.presentationMode.wrappedValue.dismiss()
}
private func saveMonster() {
copyLocalToMonster()
do {
// Save core data context
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
dismissView()
}
private func copyMonsterToLocal() {
if (!hasInitializedViewModel) {
monsterViewModel.copyFromMonster(monster: monster)
hasInitializedViewModel = true
}
}
private func copyLocalToMonster() {
monsterViewModel.copyToMonster(monster: monster)
}
}
struct EditMonster_Previews: PreviewProvider {
static var previews: some View {
let context = PersistenceController.preview.container.viewContext
let monster = Monster.init(context: context)
monster.name = "Steve"
monster.size = "Medium"
monster.type = "humanoid"
monster.subtype = "human"
monster.alignment = "LG"
monster.hitDice = 6
monster.hasCustomHP = true
monster.customHP = "12 (1d10)+2"
monster.walkSpeed = 5
monster.burrowSpeed = 10
monster.climbSpeed = 15
monster.flySpeed = 20
monster.swimSpeed = 25
monster.canHover = true
monster.hasCustomSpeed = false
monster.customSpeed = "walk: 5 ft."
monster.strengthScore = 8
monster.dexterityScore = 10
monster.constitutionScore = 12
monster.intelligenceScore = 14
monster.wisdomScore = 16
monster.charismaScore = 18
monster.strengthSavingThrowAdvantage = AdvantageType.none.rawValue
monster.strengthSavingThrowProficiency = ProficiencyType.none.rawValue
monster.dexteritySavingThrowAdvantage = AdvantageType.advantage.rawValue
monster.dexteritySavingThrowProficiency = ProficiencyType.proficient.rawValue
monster.constitutionSavingThrowAdvantage = AdvantageType.disadvantage.rawValue
monster.constitutionSavingThrowProficiency = ProficiencyType.expertise.rawValue
monster.intelligenceSavingThrowAdvantage = AdvantageType.none.rawValue
monster.intelligenceSavingThrowProficiency = ProficiencyType.expertise.rawValue
monster.wisdomSavingThrowAdvantage = AdvantageType.advantage.rawValue
monster.wisdomSavingThrowProficiency = ProficiencyType.proficient.rawValue
monster.charismaSavingThrowAdvantage = AdvantageType.disadvantage.rawValue
monster.charismaSavingThrowProficiency = ProficiencyType.none.rawValue
return EditMonster(monster: monster).environment(\.managedObjectContext, context)
}
}

View File

@@ -0,0 +1,80 @@
//
// EditSavingThrows.swift
// MonsterCards
//
// Created by Tom Hicks on 3/21/21.
//
import SwiftUI
struct EditSavingThrows: View {
@ObservedObject var monsterViewModel: MonsterViewModel
var body: some View {
List {
// TODO: Add a version of this layout for wider screens where these VStacks with HStacks
VStack {
MCAdvantagePicker(
label: "Strength Advantage",
value: $monsterViewModel.strengthSavingThrowAdvantage)
MCProficiencyPicker(
label: "Strength Proficiency",
value: $monsterViewModel.strengthSavingThrowProficiency)
}
VStack {
MCAdvantagePicker(
label: "Dexterity Advantage",
value: $monsterViewModel.dexteritySavingThrowAdvantage)
MCProficiencyPicker(
label: "Dexterity Proficiency",
value: $monsterViewModel.dexteritySavingThrowProficiency)
}
VStack {
MCAdvantagePicker(
label: "Constitution Advantage",
value: $monsterViewModel.constitutionSavingThrowAdvantage)
MCProficiencyPicker(
label: "Constitution Proficiency",
value: $monsterViewModel.constitutionSavingThrowProficiency)
}
VStack {
MCAdvantagePicker(
label: "Intelligence Advantage",
value: $monsterViewModel.intelligenceSavingThrowAdvantage)
MCProficiencyPicker(
label: "Intelligence Proficiency",
value: $monsterViewModel.intelligenceSavingThrowProficiency)
}
VStack {
MCAdvantagePicker(
label: "Wisdom Advantage",
value: $monsterViewModel.wisdomSavingThrowAdvantage)
MCProficiencyPicker(
label: "Wisdom Proficiency",
value: $monsterViewModel.wisdomSavingThrowProficiency)
}
VStack {
MCAdvantagePicker(
label: "Charisma Advantage",
value: $monsterViewModel.charismaSavingThrowAdvantage)
MCProficiencyPicker(
label: "Charisma Proficiency",
value: $monsterViewModel.charismaSavingThrowProficiency)
}
}
.navigationTitle("Saving Throws")
}
}
struct EditSavingThrows_Previews: PreviewProvider {
static var previews: some View {
let viewModel = MonsterViewModel()
EditSavingThrows(monsterViewModel: viewModel)
}
}

View File

@@ -0,0 +1,42 @@
//
// EditSkill.swift
// MonsterCards
//
// Created by Tom Hicks on 3/21/21.
//
import SwiftUI
struct EditSkill: View {
@ObservedObject var skillViewModel: SkillViewModel
var body: some View {
List {
MCTextField(
label: "Name",
value: $skillViewModel.name)
.autocapitalization(.words)
MCAbilityScorePicker(
label: "Ability Score",
value: $skillViewModel.abilityScore)
// TODO: Add a version of this layout for wider screens where these two are in an HStack
MCAdvantagePicker(
label: "Advantage",
value: $skillViewModel.advantage)
MCProficiencyPicker(
label: "Proficiency",
value: $skillViewModel.proficiency)
}
.navigationTitle("Skill")
}
}
struct EditSkill_Previews: PreviewProvider {
static var previews: some View {
let viewModel = SkillViewModel()
EditSkill(skillViewModel: viewModel)
}
}

View File

@@ -0,0 +1,49 @@
//
// EditSkills.swift
// MonsterCards
//
// Created by Tom Hicks on 3/21/21.
//
import SwiftUI
struct EditSkills: View {
@ObservedObject var monsterViewModel: MonsterViewModel
var body: some View {
List {
ForEach(monsterViewModel.skills) { skill in
NavigationLink(skill.name, destination: EditSkill(skillViewModel: skill))
}
.onDelete(perform: { indexSet in
for index in indexSet {
monsterViewModel.skills.remove(at: index)
}
})
}
.toolbar(content: {
Button(
action: {
let newSkill = SkillViewModel()
newSkill.name = ""
newSkill.proficiency = .proficient
monsterViewModel.skills.append(newSkill)
},
label: {
Image(systemName: "plus")
}
)
})
.navigationTitle("Skills")
.onAppear(perform: {
monsterViewModel.skills = monsterViewModel.skills.sorted()
})
}
}
struct EditSkills_Previews: PreviewProvider {
static var previews: some View {
let viewModel = MonsterViewModel()
EditSkills(monsterViewModel: viewModel)
}
}

View File

@@ -0,0 +1,75 @@
//
// EditSpeed.swift
// MonsterCards
//
// Created by Tom Hicks on 3/21/21.
//
import SwiftUI
struct EditSpeed: View {
@ObservedObject var monsterViewModel: MonsterViewModel
var body: some View {
List {
// Number bound to monster.walkSpeed
MCStepperField(
label: "Base",
step: 5,
suffix: " ft.",
value: $monsterViewModel.walkSpeed)
// Number bound to monster.burrowSpeed
MCStepperField(
label: "Burrow",
step: 5,
suffix: " ft.",
value: $monsterViewModel.burrowSpeed)
// Number bound to monster.climbSpeed
MCStepperField(
label: "Climb",
step: 5,
suffix: " ft.",
value: $monsterViewModel.climbSpeed)
// Number bound to monster.flySpeed
MCStepperField(
label: "Fly",
step: 5,
suffix: " ft.",
value: $monsterViewModel.flySpeed)
// Toggle bound to monster.canHover
Toggle(
"Can Hover",
isOn: $monsterViewModel.canHover)
// Number bound to monster.swimSpeed
MCStepperField(
label: "Swim",
step: 5,
suffix: " ft.",
value: $monsterViewModel.swimSpeed)
// Toggle bound to monster.hasCustomSpeed
Toggle(
"Has Custom Speed",
isOn: $monsterViewModel.hasCustomSpeed)
// Editable Text field bound to monster.customSpeedText
MCTextField(
label: "Custom Speed",
value: $monsterViewModel.customSpeed)
.autocapitalization(.none)
}
.navigationTitle("Speed")
}
}
struct EditSpeed_Previews: PreviewProvider {
static var previews: some View {
let viewModel = MonsterViewModel()
EditSpeed(monsterViewModel: viewModel)
}
}

View File

@@ -0,0 +1,60 @@
//
// EditStrings.swift
// MonsterCards
//
// Created by Tom Hicks on 3/22/21.
//
import SwiftUI
struct EditStrings: View {
@ObservedObject var viewModel: MonsterViewModel
var path: ReferenceWritableKeyPath<MonsterViewModel, [StringViewModel]>
var title: String
var body: some View {
List {
ForEach(viewModel[keyPath: path]) { damageType in
TextField(
"",
text: Binding<String>(
get: {damageType.name},
set: {damageType.name = $0}
)
)
.autocapitalization(.none)
}
.onDelete(perform: { indexSet in
for index in indexSet {
viewModel[keyPath: path].remove(at: index)
}
})
}
.toolbar(content: {
Button(
action: {
let newString = StringViewModel()
viewModel[keyPath: path].append(newString)
viewModel[keyPath: path] = viewModel[keyPath: path].sorted()
},
label: {
Image(systemName: "plus")
}
)
})
.onAppear(perform: {
viewModel[keyPath: path] = viewModel[keyPath: path].sorted()
})
.navigationTitle(title)
}
}
struct EditStrings_Previews: PreviewProvider {
static var previews: some View {
let viewModel = MonsterViewModel()
EditStrings(
viewModel: viewModel,
path: \.damageImmunities,
title: "Damage Types")
}
}

View File

@@ -0,0 +1,34 @@
//
// EditTrait.swift
// MonsterCards
//
// Created by Tom Hicks on 3/25/21.
//
import SwiftUI
struct EditTrait: View {
@ObservedObject var viewModel: AbilityViewModel
var body: some View {
VStack(alignment: .leading) {
MCTextField(
label: "Name",
value: $viewModel.name)
Text("Description")
.font(.caption2)
TextEditor(text: $viewModel.abilityDescription)
}
.padding()
}
}
struct EditTrait_Previews: PreviewProvider {
static var previews: some View {
let viewModel = AbilityViewModel()
EditTrait(viewModel: viewModel)
}
}

View File

@@ -0,0 +1,64 @@
//
// EditTraits.swift
// MonsterCards
//
// Created by Tom Hicks on 3/25/21.
//
import SwiftUI
struct EditTraits: View {
@ObservedObject var viewModel: MonsterViewModel
var path: ReferenceWritableKeyPath<MonsterViewModel, [AbilityViewModel]>
var title: String
var body: some View {
List {
ForEach(viewModel[keyPath: path]) { ability in
NavigationLink(
ability.name,
destination: EditTrait(viewModel: ability))
}
.onDelete(perform: { indexSet in
for index in indexSet {
viewModel[keyPath: path].remove(at: index)
}
})
.onMove(perform: { indices, newOffset in
viewModel[keyPath: path].move(fromOffsets: indices, toOffset: newOffset)
})
}
.toolbar(content: {
ToolbarItemGroup(placement: .navigationBarTrailing) {
EditButton()
Button(
action: {
let newAbility = AbilityViewModel()
viewModel[keyPath: path].append(newAbility)
viewModel[keyPath: path] = viewModel[keyPath: path].sorted()
},
label: {
Image(systemName: "plus")
}
)
}
})
.onAppear(perform: {
viewModel[keyPath: path] = viewModel[keyPath: path].sorted()
})
.navigationTitle(title)
}
}
struct EditTraits_Previews: PreviewProvider {
static var previews: some View {
let viewModel = MonsterViewModel()
EditTraits(
viewModel: viewModel,
path: \.abilities,
title: "Abilities")
}
}

View File

@@ -0,0 +1,80 @@
//
// ImportMonster.swift
// MonsterCards
//
// Created by Tom Hicks on 4/1/21.
//
import SwiftUI
struct ImportMonster: View {
@Binding var monster: MonsterViewModel
@Environment(\.managedObjectContext) private var viewContext
@Binding var isOpen: Bool
var body: some View {
VStack{
HStack {
Button("Cancel", action: cancelImport)
Spacer()
Button("Add to Library", action: saveNewMonster)
}
MonsterDetailView(viewModel: monster)
}
.padding()
}
func cancelImport() {
isOpen = false
}
func saveNewMonster() {
print("Saving monster: \(monster.name)")
withAnimation {
let newMonster = Monster(context: viewContext)
monster.copyToMonster(monster: newMonster)
do {
try viewContext.save()
isOpen = false
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}
struct ImportMonster_Previews: PreviewProvider {
static var previews: some View {
let monster = MonsterViewModel()
monster.name = "Steve"
monster.size = "Medium"
monster.type = "dwarf"
monster.alignment = "chaotic good"
monster.armorType = .none
monster.hasShield = true
monster.hitDice = 4
monster.strengthScore = 20
monster.dexterityScore = 14
monster.constitutionScore = 18
monster.intelligenceScore = 8
monster.wisdomScore = 8
monster.charismaScore = 15
monster.walkSpeed = 40
monster.challengeRating = .four
monster.languages = [LanguageViewModel("Common", true), LanguageViewModel("Giant", true)]
monster.actions = [AbilityViewModel("Greataxe, +3", "__Badass Attack:___ Hits the other dude on a _3_ or above and does a ton of damage")]
return
VStack{
Text("Hello, World!")
}
.sheet(isPresented: .constant(true)) {
ImportMonster(monster: .constant(monster), isOpen: .constant(true))
}
}
}

View File

@@ -0,0 +1,77 @@
//
// Library.swift
// MonsterCards
//
// Created by Tom Hicks on 1/15/21.
//
import SwiftUI
struct Library: View {
// TODO: add an import button that searches https://api.open5e.com/monsters/ and lets you import a monster from there
// TODO: add an import button that lets you browse for a tetra cube monster file and import it
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(
sortDescriptors: [
NSSortDescriptor(keyPath: \Monster.name, ascending: true),
],
animation: .default)
var allMonsters: FetchedResults<Monster>
var body: some View {
NavigationView{
List {
ForEach(allMonsters) { monster in
NavigationLink(destination: MonsterDetailWrapper(monster: monster)) {
Text(monster.name ?? "")
}
}
.onDelete(perform: { indexSet in
for index in indexSet {
let monster = allMonsters[index]
viewContext.delete(monster)
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
})
}
.navigationTitle("Library")
.navigationBarTitleDisplayMode(.inline)
.toolbar(content: {
ToolbarItem(placement: .primaryAction) {
Button(action: addMonster) {
Image(systemName:"plus")
}
}
})
}
}
private func addMonster() {
withAnimation {
let newItem = Monster(context: viewContext)
newItem.name = "Unnamed Monster"
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}
struct Library_Previews: PreviewProvider {
static var previews: some View {
return Library().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}

View File

@@ -0,0 +1,35 @@
//
// MCAbilityScorePicker.swift
// MonsterCards
//
// Created by Tom Hicks on 2/15/21.
//
import SwiftUI
struct MCAbilityScorePicker: View {
var label: String = ""
var value: Binding<AbilityScore>
var body: some View {
VStack(alignment: .leading) {
Text(label)
.font(.caption2)
Picker(
selection: value,
label: Text(value.wrappedValue.displayName)) {
ForEach(AbilityScore.allCases) {abilityScore in
Text(abilityScore.displayName).tag(abilityScore)
}
}
.pickerStyle(MenuPickerStyle())
}
}
}
struct MCAbilityScorePicker_Previews: PreviewProvider {
static var previews: some View {
MCAbilityScorePicker(
label: "Ability Score",
value: .constant(AbilityScore.strength))
}
}

View File

@@ -0,0 +1,39 @@
//
// MCAdvantagePicker.swift
// MonsterCards
//
// Created by Tom Hicks on 1/17/21.
//
import SwiftUI
struct MCAdvantagePicker: View {
var label: String = ""
var value: Binding<AdvantageType>
var body: some View {
VStack(alignment: .leading) {
Text(label)
.font(.caption2)
Picker(
selection: value,
label: Text(label)) {
ForEach(AdvantageType.allCases) { advType in
Text(advType.displayName).tag(advType)
}
}
.pickerStyle(SegmentedPickerStyle())
// .pickerStyle(SegmentedPickerStyle())
// .pickerStyle(WheelPickerStyle())
// .pickerStyle(DefaultPickerStyle())
// .pickerStyle(InlinePickerStyle())
// .pickerStyle(MenuPickerStyle())
}
}
}
struct MCAdvantagePicker_Previews: PreviewProvider {
static var previews: some View {
MCAdvantagePicker(value: .constant(AdvantageType.none))
}
}

View File

@@ -0,0 +1,36 @@
//
// MCArmorTypePicker.swift
// MonsterCards
//
// Created by Tom Hicks on 2/6/21.
//
import SwiftUI
struct MCArmorTypePicker: View {
var label: String = ""
var value: Binding<ArmorType>
var body: some View {
VStack(alignment: .leading) {
Text(label)
.font(.caption2)
Picker(
selection: value,
label: Text(value.wrappedValue.displayName)) {
ForEach(ArmorType.allCases) {armorType in
Text(armorType.displayName).tag(armorType)
}
}
.pickerStyle(MenuPickerStyle())
}
}
}
struct MCArmorTypePicker_Previews: PreviewProvider {
static var previews: some View {
MCArmorTypePicker(
value: .constant(ArmorType.none)
)
}
}

View File

@@ -0,0 +1,35 @@
//
// MCChallengeRatingPicker.swift
// MonsterCards
//
// Created by Tom Hicks on 3/24/21.
//
import SwiftUI
struct MCChallengeRatingPicker: View {
var label: String = ""
var value: Binding<ChallengeRating>
var body: some View {
VStack(alignment: .leading) {
Text(label)
.font(.caption2)
Picker(
selection: value,
label: Text(value.wrappedValue.displayName)) {
ForEach(ChallengeRating.allCases) {abilityScore in
Text(abilityScore.displayName).tag(abilityScore)
}
}
.pickerStyle(MenuPickerStyle())
}
}
}
struct MCChallengeRatingPicker_Previews: PreviewProvider {
static var previews: some View {
MCChallengeRatingPicker(
label: "Rating",
value: .constant(ChallengeRating.ten))
}
}

View File

@@ -0,0 +1,34 @@
//
// MCProficiencyPicker.swift
// MonsterCards
//
// Created by Tom Hicks on 1/17/21.
//
import SwiftUI
struct MCProficiencyPicker: View {
var label: String = ""
var value: Binding<ProficiencyType>
var body: some View {
VStack(alignment: .leading) {
Text(label)
.font(.caption2)
Picker(
selection: value,
label: Text(label)) {
ForEach(ProficiencyType.allCases) { profType in
Text(profType.displayName).tag(profType)
}
}
.pickerStyle(SegmentedPickerStyle())
}
}
}
struct MCProficiencyPicker_Previews: PreviewProvider {
static var previews: some View {
MCProficiencyPicker(value: .constant(ProficiencyType.none))
}
}

Some files were not shown because too many files have changed in this diff Show More