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