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