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

2
iOS/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
xcuserdata
.zshrc

24
iOS/LICENSE Normal file
View File

@@ -0,0 +1,24 @@
MIT License
-----------
Copyright (c) 2020-2021 Tom Hicks
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

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>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,70 @@
{
"object": {
"pins": [
{
"package": "AttributedText",
"repositoryURL": "https://github.com/gonzalezreal/AttributedText",
"state": {
"branch": null,
"revision": "bf076de48dbb2172525486936d512e1bba062642",
"version": "0.3.0"
}
},
{
"package": "combine-schedulers",
"repositoryURL": "https://github.com/pointfreeco/combine-schedulers",
"state": {
"branch": null,
"revision": "f1250faa1c1436ca83950ce676a4fe97a309a457",
"version": "0.4.1"
}
},
{
"package": "MarkdownUI",
"repositoryURL": "https://github.com/gonzalezreal/MarkdownUI",
"state": {
"branch": null,
"revision": "e8931e37dcf777b4c03ca76aa09c10cf246a2ced",
"version": "0.5.1"
}
},
{
"package": "NetworkImage",
"repositoryURL": "https://github.com/gonzalezreal/NetworkImage",
"state": {
"branch": null,
"revision": "15582b821cb097012b41b83d6219717926ec4ed6",
"version": "2.1.0"
}
},
{
"package": "cmark",
"repositoryURL": "https://github.com/SwiftDocOrg/swift-cmark.git",
"state": {
"branch": null,
"revision": "9c8096a23f44794bde297452d87c455fc4f76d42",
"version": "0.29.0+20210102.9c8096a"
}
},
{
"package": "SwiftCommonMark",
"repositoryURL": "https://github.com/gonzalezreal/SwiftCommonMark",
"state": {
"branch": null,
"revision": "f1575c37110a386e50da3208a04266b398bcefaa",
"version": "0.1.1"
}
},
{
"package": "xctest-dynamic-overlay",
"repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay",
"state": {
"branch": null,
"revision": "603974e3909ad4b48ba04aad7e0ceee4f077a518",
"version": "0.1.0"
}
}
]
},
"version": 1
}

View File

@@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1240"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "E2570FB425B1AC520055B23B"
BuildableName = "MonsterCards.app"
BlueprintName = "MonsterCards"
ReferencedContainer = "container:MonsterCards.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "E2570FCA25B1AC550055B23B"
BuildableName = "MonsterCardsTests.xctest"
BlueprintName = "MonsterCardsTests"
ReferencedContainer = "container:MonsterCards.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "E2570FD525B1AC550055B23B"
BuildableName = "MonsterCardsUITests.xctest"
BlueprintName = "MonsterCardsUITests"
ReferencedContainer = "container:MonsterCards.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "E2570FB425B1AC520055B23B"
BuildableName = "MonsterCards.app"
BlueprintName = "MonsterCards"
ReferencedContainer = "container:MonsterCards.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "E2570FB425B1AC520055B23B"
BuildableName = "MonsterCards.app"
BlueprintName = "MonsterCards"
ReferencedContainer = "container:MonsterCards.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1240"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "E216E47A261FE76F00FD9262"
BuildableName = "MonsterPreview.appex"
BlueprintName = "MonsterPreview"
ReferencedContainer = "container:MonsterCards.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "E2570FB425B1AC520055B23B"
BuildableName = "MonsterCards.app"
BlueprintName = "MonsterCards"
ReferencedContainer = "container:MonsterCards.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = ""
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
launchStyle = "0"
askForAppToLaunch = "Yes"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "E2570FB425B1AC520055B23B"
BuildableName = "MonsterCards.app"
BlueprintName = "MonsterCards"
ReferencedContainer = "container:MonsterCards.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"
askForAppToLaunch = "Yes"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "E2570FB425B1AC520055B23B"
BuildableName = "MonsterCards.app"
BlueprintName = "MonsterCards"
ReferencedContainer = "container:MonsterCards.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,105 @@
<?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>SchemeUserState</key>
<dict>
<key>AttributedText_iOS (Playground) 1.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>14</integer>
</dict>
<key>AttributedText_iOS (Playground) 2.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>15</integer>
</dict>
<key>AttributedText_iOS (Playground).xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>13</integer>
</dict>
<key>AttributedText_macOS (Playground) 1.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>11</integer>
</dict>
<key>AttributedText_macOS (Playground) 2.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>12</integer>
</dict>
<key>AttributedText_macOS (Playground).xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>10</integer>
</dict>
<key>AttributedText_tvOS (Playground) 1.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>8</integer>
</dict>
<key>AttributedText_tvOS (Playground) 2.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>9</integer>
</dict>
<key>AttributedText_tvOS (Playground).xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>7</integer>
</dict>
<key>MonsterCards.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
</dict>
<key>MonsterPreview.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>16</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>
<dict>
<key>E216E47A261FE76F00FD9262</key>
<dict>
<key>primary</key>
<true/>
</dict>
<key>E2570FB425B1AC520055B23B</key>
<dict>
<key>primary</key>
<true/>
</dict>
<key>E2570FCA25B1AC550055B23B</key>
<dict>
<key>primary</key>
<true/>
</dict>
<key>E2570FD525B1AC550055B23B</key>
<dict>
<key>primary</key>
<true/>
</dict>
</dict>
</dict>
</plist>

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")
}
}

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