Add 'iOS/' from commit '938f0fb75860d3637b998bdd0c27dcffd9fc9451'
git-subtree-dir: iOS git-subtree-mainline:c4bb775af4git-subtree-split:938f0fb758
2
iOS/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
xcuserdata
|
||||
.zshrc
|
||||
24
iOS/LICENSE
Normal 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.
|
||||
1122
iOS/MonsterCards.xcodeproj/project.pbxproj
Normal file
7
iOS/MonsterCards.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/100.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/1024.png
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/114.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/120.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/128.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/144.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/152.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/16.png
Normal file
|
After Width: | Height: | Size: 521 B |
BIN
iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/167.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/172.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/180.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/196.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/20.png
Normal file
|
After Width: | Height: | Size: 667 B |
BIN
iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/216.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/256.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/29.png
Normal file
|
After Width: | Height: | Size: 1014 B |
BIN
iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/32.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/40.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/48.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/50.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/512.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/55.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/57.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/58.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/60.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/64.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/72.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/76.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/80.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/87.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/88.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
6
iOS/MonsterCards/Assets.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
23
iOS/MonsterCards/Assets.xcassets/section-divider.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
BIN
iOS/MonsterCards/Assets.xcassets/section-divider.imageset/section-divider.png
vendored
Normal file
|
After Width: | Height: | Size: 338 B |
BIN
iOS/MonsterCards/Assets.xcassets/section-divider.imageset/section-divider@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1001 B |
BIN
iOS/MonsterCards/Assets.xcassets/section-divider.imageset/section-divider@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
20
iOS/MonsterCards/Helpers/Color+Hex.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
111
iOS/MonsterCards/Helpers/MonsterImportHelper.swift
Normal 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
|
||||
}
|
||||
}
|
||||
50
iOS/MonsterCards/Helpers/StringHelper.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
6
iOS/MonsterCards/Images.xcassets/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
23
iOS/MonsterCards/Images.xcassets/section-divider.imageset/Contents.json
vendored
Normal 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
|
||||
}
|
||||
}
|
||||
BIN
iOS/MonsterCards/Images.xcassets/section-divider.imageset/section-divider.png
vendored
Normal file
|
After Width: | Height: | Size: 338 B |
BIN
iOS/MonsterCards/Images.xcassets/section-divider.imageset/section-divider@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 1001 B |
BIN
iOS/MonsterCards/Images.xcassets/section-divider.imageset/section-divider@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
131
iOS/MonsterCards/Info.plist
Normal 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>
|
||||
116
iOS/MonsterCards/Models/AbilityViewModel.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
46
iOS/MonsterCards/Models/ChallengeRatingViewModel.swift
Normal 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
|
||||
}
|
||||
}
|
||||
42
iOS/MonsterCards/Models/DamageTypeDTO.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
73
iOS/MonsterCards/Models/Enums/AbilityScore.swift
Normal 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!
|
||||
}
|
||||
}
|
||||
}
|
||||
27
iOS/MonsterCards/Models/Enums/AdvantageType.swift
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
50
iOS/MonsterCards/Models/Enums/ArmorType.swift
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
123
iOS/MonsterCards/Models/Enums/ChallengeRating.swift
Normal 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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
27
iOS/MonsterCards/Models/Enums/ProficiencyType.swift
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
52
iOS/MonsterCards/Models/Enums/SizeType.swift
Normal 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!
|
||||
}
|
||||
}
|
||||
}
|
||||
35
iOS/MonsterCards/Models/LanguageDTO.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
79
iOS/MonsterCards/Models/LanguageViewModel.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
161
iOS/MonsterCards/Models/Monster+CoreDataClass.swift
Normal 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
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
353
iOS/MonsterCards/Models/MonsterDTO.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
37
iOS/MonsterCards/Models/MonsterDocument.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
223
iOS/MonsterCards/Models/MonsterViewModel+CoreData.swift
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
706
iOS/MonsterCards/Models/MonsterViewModel.swift
Normal 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
|
||||
}
|
||||
35
iOS/MonsterCards/Models/SavingThrowDTO.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
50
iOS/MonsterCards/Models/Skill+CoreDataClass.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
39
iOS/MonsterCards/Models/SkillDTO.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
57
iOS/MonsterCards/Models/SkillViewModel+CoreData.swift
Normal 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
|
||||
}
|
||||
}
|
||||
67
iOS/MonsterCards/Models/SkillViewModel.swift
Normal 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
|
||||
}
|
||||
}
|
||||
24
iOS/MonsterCards/Models/StringViewModel.swift
Normal 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
|
||||
}
|
||||
}
|
||||
39
iOS/MonsterCards/Models/TraitDTO.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
16
iOS/MonsterCards/MonsterCards.entitlements
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
20
iOS/MonsterCards/MonsterCardsApp.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
46
iOS/MonsterCards/Persistence.swift
Normal 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)")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
21
iOS/MonsterCards/Views/Collections.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
80
iOS/MonsterCards/Views/ContentView.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
20
iOS/MonsterCards/Views/Dashboard.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
42
iOS/MonsterCards/Views/EditAbilityScores.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
45
iOS/MonsterCards/Views/EditArmor.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
71
iOS/MonsterCards/Views/EditBasicInfo.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
41
iOS/MonsterCards/Views/EditChallengeRating.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
33
iOS/MonsterCards/Views/EditLanguage.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
58
iOS/MonsterCards/Views/EditLanguages.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
220
iOS/MonsterCards/Views/EditMonster.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
80
iOS/MonsterCards/Views/EditSavingThrows.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
42
iOS/MonsterCards/Views/EditSkill.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
49
iOS/MonsterCards/Views/EditSkills.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
75
iOS/MonsterCards/Views/EditSpeed.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
60
iOS/MonsterCards/Views/EditStrings.swift
Normal 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")
|
||||
}
|
||||
}
|
||||