Merge pull request #1 from headhunter45/add-file-import
Add Opening monster files from tetra cube's generator
This commit is contained in:
		| @@ -15,6 +15,7 @@ | |||||||
| 		E20209FC25D8E19100EFE733 /* MonsterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20209FA25D8E19100EFE733 /* MonsterViewModel.swift */; }; | 		E20209FC25D8E19100EFE733 /* MonsterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20209FA25D8E19100EFE733 /* MonsterViewModel.swift */; }; | ||||||
| 		E210B83A25B42D980083EAC5 /* MCProficiencyPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = E210B83925B42D980083EAC5 /* MCProficiencyPicker.swift */; }; | 		E210B83A25B42D980083EAC5 /* MCProficiencyPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = E210B83925B42D980083EAC5 /* MCProficiencyPicker.swift */; }; | ||||||
| 		E210B83F25B42DAB0083EAC5 /* MCAdvantagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = E210B83E25B42DAB0083EAC5 /* MCAdvantagePicker.swift */; }; | 		E210B83F25B42DAB0083EAC5 /* MCAdvantagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = E210B83E25B42DAB0083EAC5 /* MCAdvantagePicker.swift */; }; | ||||||
|  | 		E21661D12616E9A800117782 /* ImportMonster.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21661D02616E9A800117782 /* ImportMonster.swift */; }; | ||||||
| 		E216B791260C1FE800FB205F /* LanguageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E216B790260C1FE800FB205F /* LanguageViewModel.swift */; }; | 		E216B791260C1FE800FB205F /* LanguageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E216B790260C1FE800FB205F /* LanguageViewModel.swift */; }; | ||||||
| 		E216B799260C2DF200FB205F /* EditLanguages.swift in Sources */ = {isa = PBXBuildFile; fileRef = E216B798260C2DF200FB205F /* EditLanguages.swift */; }; | 		E216B799260C2DF200FB205F /* EditLanguages.swift in Sources */ = {isa = PBXBuildFile; fileRef = E216B798260C2DF200FB205F /* EditLanguages.swift */; }; | ||||||
| 		E216B79E260C396F00FB205F /* EditLanguage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E216B79D260C396F00FB205F /* EditLanguage.swift */; }; | 		E216B79E260C396F00FB205F /* EditLanguage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E216B79D260C396F00FB205F /* EditLanguage.swift */; }; | ||||||
| @@ -22,6 +23,13 @@ | |||||||
| 		E216B7BC260C691400FB205F /* EditChallengeRating.swift in Sources */ = {isa = PBXBuildFile; fileRef = E216B7BB260C691400FB205F /* EditChallengeRating.swift */; }; | 		E216B7BC260C691400FB205F /* EditChallengeRating.swift in Sources */ = {isa = PBXBuildFile; fileRef = E216B7BB260C691400FB205F /* EditChallengeRating.swift */; }; | ||||||
| 		E216B7C1260C6B6000FB205F /* MCChallengeRatingPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = E216B7C0260C6B6000FB205F /* MCChallengeRatingPicker.swift */; }; | 		E216B7C1260C6B6000FB205F /* MCChallengeRatingPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = E216B7C0260C6B6000FB205F /* MCChallengeRatingPicker.swift */; }; | ||||||
| 		E2182E6425B22F8A00DFAEF8 /* Monster+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2182E6225B22F8A00DFAEF8 /* Monster+CoreDataClass.swift */; }; | 		E2182E6425B22F8A00DFAEF8 /* Monster+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2182E6225B22F8A00DFAEF8 /* Monster+CoreDataClass.swift */; }; | ||||||
|  | 		E219247B261989B400C84E12 /* MonsterDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = E219247A261989B400C84E12 /* MonsterDTO.swift */; }; | ||||||
|  | 		E2192480261989F700C84E12 /* SavingThrowDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = E219247F261989F700C84E12 /* SavingThrowDTO.swift */; }; | ||||||
|  | 		E219248526198A1200C84E12 /* SkillDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = E219248426198A1200C84E12 /* SkillDTO.swift */; }; | ||||||
|  | 		E219248A26198A5400C84E12 /* TraitDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = E219248926198A5400C84E12 /* TraitDTO.swift */; }; | ||||||
|  | 		E219248F26198A6A00C84E12 /* DamageTypeDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = E219248E26198A6A00C84E12 /* DamageTypeDTO.swift */; }; | ||||||
|  | 		E219249426198A8200C84E12 /* LanguageDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = E219249326198A8200C84E12 /* LanguageDTO.swift */; }; | ||||||
|  | 		E219249926198E0D00C84E12 /* MonsterImportHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E219249826198E0D00C84E12 /* MonsterImportHelper.swift */; }; | ||||||
| 		E24ACE502607326E009BF703 /* EditBasicInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24ACE4F2607326E009BF703 /* EditBasicInfo.swift */; }; | 		E24ACE502607326E009BF703 /* EditBasicInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24ACE4F2607326E009BF703 /* EditBasicInfo.swift */; }; | ||||||
| 		E24ACE562607EE94009BF703 /* EditArmor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24ACE552607EE94009BF703 /* EditArmor.swift */; }; | 		E24ACE562607EE94009BF703 /* EditArmor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24ACE552607EE94009BF703 /* EditArmor.swift */; }; | ||||||
| 		E24ACE5B2607F0F2009BF703 /* EditSpeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24ACE5A2607F0F2009BF703 /* EditSpeed.swift */; }; | 		E24ACE5B2607F0F2009BF703 /* EditSpeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24ACE5A2607F0F2009BF703 /* EditSpeed.swift */; }; | ||||||
| @@ -87,6 +95,7 @@ | |||||||
| 		E20209FA25D8E19100EFE733 /* MonsterViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MonsterViewModel.swift; sourceTree = "<group>"; }; | 		E20209FA25D8E19100EFE733 /* MonsterViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MonsterViewModel.swift; sourceTree = "<group>"; }; | ||||||
| 		E210B83925B42D980083EAC5 /* MCProficiencyPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MCProficiencyPicker.swift; sourceTree = "<group>"; }; | 		E210B83925B42D980083EAC5 /* MCProficiencyPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MCProficiencyPicker.swift; sourceTree = "<group>"; }; | ||||||
| 		E210B83E25B42DAB0083EAC5 /* MCAdvantagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MCAdvantagePicker.swift; sourceTree = "<group>"; }; | 		E210B83E25B42DAB0083EAC5 /* MCAdvantagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MCAdvantagePicker.swift; sourceTree = "<group>"; }; | ||||||
|  | 		E21661D02616E9A800117782 /* ImportMonster.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportMonster.swift; sourceTree = "<group>"; }; | ||||||
| 		E216B790260C1FE800FB205F /* LanguageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageViewModel.swift; sourceTree = "<group>"; }; | 		E216B790260C1FE800FB205F /* LanguageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageViewModel.swift; sourceTree = "<group>"; }; | ||||||
| 		E216B798260C2DF200FB205F /* EditLanguages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditLanguages.swift; sourceTree = "<group>"; }; | 		E216B798260C2DF200FB205F /* EditLanguages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditLanguages.swift; sourceTree = "<group>"; }; | ||||||
| 		E216B79D260C396F00FB205F /* EditLanguage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditLanguage.swift; sourceTree = "<group>"; }; | 		E216B79D260C396F00FB205F /* EditLanguage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditLanguage.swift; sourceTree = "<group>"; }; | ||||||
| @@ -94,6 +103,13 @@ | |||||||
| 		E216B7BB260C691400FB205F /* EditChallengeRating.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditChallengeRating.swift; sourceTree = "<group>"; }; | 		E216B7BB260C691400FB205F /* EditChallengeRating.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditChallengeRating.swift; sourceTree = "<group>"; }; | ||||||
| 		E216B7C0260C6B6000FB205F /* MCChallengeRatingPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MCChallengeRatingPicker.swift; sourceTree = "<group>"; }; | 		E216B7C0260C6B6000FB205F /* MCChallengeRatingPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MCChallengeRatingPicker.swift; sourceTree = "<group>"; }; | ||||||
| 		E2182E6225B22F8A00DFAEF8 /* Monster+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Monster+CoreDataClass.swift"; sourceTree = "<group>"; }; | 		E2182E6225B22F8A00DFAEF8 /* Monster+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Monster+CoreDataClass.swift"; sourceTree = "<group>"; }; | ||||||
|  | 		E219247A261989B400C84E12 /* MonsterDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonsterDTO.swift; sourceTree = "<group>"; }; | ||||||
|  | 		E219247F261989F700C84E12 /* SavingThrowDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavingThrowDTO.swift; sourceTree = "<group>"; }; | ||||||
|  | 		E219248426198A1200C84E12 /* SkillDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SkillDTO.swift; sourceTree = "<group>"; }; | ||||||
|  | 		E219248926198A5400C84E12 /* TraitDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraitDTO.swift; sourceTree = "<group>"; }; | ||||||
|  | 		E219248E26198A6A00C84E12 /* DamageTypeDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamageTypeDTO.swift; sourceTree = "<group>"; }; | ||||||
|  | 		E219249326198A8200C84E12 /* LanguageDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageDTO.swift; sourceTree = "<group>"; }; | ||||||
|  | 		E219249826198E0D00C84E12 /* MonsterImportHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonsterImportHelper.swift; sourceTree = "<group>"; }; | ||||||
| 		E24ACE4F2607326E009BF703 /* EditBasicInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditBasicInfo.swift; sourceTree = "<group>"; }; | 		E24ACE4F2607326E009BF703 /* EditBasicInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditBasicInfo.swift; sourceTree = "<group>"; }; | ||||||
| 		E24ACE552607EE94009BF703 /* EditArmor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditArmor.swift; sourceTree = "<group>"; }; | 		E24ACE552607EE94009BF703 /* EditArmor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditArmor.swift; sourceTree = "<group>"; }; | ||||||
| 		E24ACE5A2607F0F2009BF703 /* EditSpeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditSpeed.swift; sourceTree = "<group>"; }; | 		E24ACE5A2607F0F2009BF703 /* EditSpeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditSpeed.swift; sourceTree = "<group>"; }; | ||||||
| @@ -259,6 +275,7 @@ | |||||||
| 				E2CB0DE526088CE400142591 /* EditStrings.swift */, | 				E2CB0DE526088CE400142591 /* EditStrings.swift */, | ||||||
| 				E254F912260D1F6D009295A5 /* EditTrait.swift */, | 				E254F912260D1F6D009295A5 /* EditTrait.swift */, | ||||||
| 				E254F90D260D19A0009295A5 /* EditTraits.swift */, | 				E254F90D260D19A0009295A5 /* EditTraits.swift */, | ||||||
|  | 				E21661D02616E9A800117782 /* ImportMonster.swift */, | ||||||
| 				E2570FFE25B1AE180055B23B /* Library.swift */, | 				E2570FFE25B1AE180055B23B /* Library.swift */, | ||||||
| 				E2CB0DB726081A2F00142591 /* MCAbilityScorePicker.swift */, | 				E2CB0DB726081A2F00142591 /* MCAbilityScorePicker.swift */, | ||||||
| 				E210B83E25B42DAB0083EAC5 /* MCAdvantagePicker.swift */, | 				E210B83E25B42DAB0083EAC5 /* MCAdvantagePicker.swift */, | ||||||
| @@ -279,13 +296,19 @@ | |||||||
| 			children = ( | 			children = ( | ||||||
| 				E254F905260D0818009295A5 /* AbilityViewModel.swift */, | 				E254F905260D0818009295A5 /* AbilityViewModel.swift */, | ||||||
| 				E216B7B6260C5A9800FB205F /* ChallengeRatingViewModel.swift */, | 				E216B7B6260C5A9800FB205F /* ChallengeRatingViewModel.swift */, | ||||||
|  | 				E219248E26198A6A00C84E12 /* DamageTypeDTO.swift */, | ||||||
|  | 				E20209E625D8DEB600EFE733 /* Enums */, | ||||||
|  | 				E219249326198A8200C84E12 /* LanguageDTO.swift */, | ||||||
| 				E216B790260C1FE800FB205F /* LanguageViewModel.swift */, | 				E216B790260C1FE800FB205F /* LanguageViewModel.swift */, | ||||||
| 				E2182E6225B22F8A00DFAEF8 /* Monster+CoreDataClass.swift */, | 				E2182E6225B22F8A00DFAEF8 /* Monster+CoreDataClass.swift */, | ||||||
|  | 				E219247A261989B400C84E12 /* MonsterDTO.swift */, | ||||||
| 				E20209FA25D8E19100EFE733 /* MonsterViewModel.swift */, | 				E20209FA25D8E19100EFE733 /* MonsterViewModel.swift */, | ||||||
|  | 				E219247F261989F700C84E12 /* SavingThrowDTO.swift */, | ||||||
| 				E20209D225D8DD9600EFE733 /* Skill+CoreDataClass.swift */, | 				E20209D225D8DD9600EFE733 /* Skill+CoreDataClass.swift */, | ||||||
|  | 				E219248426198A1200C84E12 /* SkillDTO.swift */, | ||||||
| 				E20209F925D8E19100EFE733 /* SkillViewModel.swift */, | 				E20209F925D8E19100EFE733 /* SkillViewModel.swift */, | ||||||
| 				E2CB0DE0260887ED00142591 /* StringViewModel.swift */, | 				E2CB0DE0260887ED00142591 /* StringViewModel.swift */, | ||||||
| 				E20209E625D8DEB600EFE733 /* Enums */, | 				E219248926198A5400C84E12 /* TraitDTO.swift */, | ||||||
| 			); | 			); | ||||||
| 			path = Models; | 			path = Models; | ||||||
| 			sourceTree = "<group>"; | 			sourceTree = "<group>"; | ||||||
| @@ -294,6 +317,7 @@ | |||||||
| 			isa = PBXGroup; | 			isa = PBXGroup; | ||||||
| 			children = ( | 			children = ( | ||||||
| 				E2D473FC25B532C900CB36D7 /* Color+Hex.swift */, | 				E2D473FC25B532C900CB36D7 /* Color+Hex.swift */, | ||||||
|  | 				E219249826198E0D00C84E12 /* MonsterImportHelper.swift */, | ||||||
| 				E2CB0DD62608720000142591 /* StringHelper.swift */, | 				E2CB0DD62608720000142591 /* StringHelper.swift */, | ||||||
| 			); | 			); | ||||||
| 			path = Helpers; | 			path = Helpers; | ||||||
| @@ -435,11 +459,13 @@ | |||||||
| 			buildActionMask = 2147483647; | 			buildActionMask = 2147483647; | ||||||
| 			files = ( | 			files = ( | ||||||
| 				E20209FB25D8E19100EFE733 /* SkillViewModel.swift in Sources */, | 				E20209FB25D8E19100EFE733 /* SkillViewModel.swift in Sources */, | ||||||
|  | 				E21661D12616E9A800117782 /* ImportMonster.swift in Sources */, | ||||||
| 				E24ACE602607F45E009BF703 /* EditAbilityScores.swift in Sources */, | 				E24ACE602607F45E009BF703 /* EditAbilityScores.swift in Sources */, | ||||||
| 				E2570FC225B1AC550055B23B /* Persistence.swift in Sources */, | 				E2570FC225B1AC550055B23B /* Persistence.swift in Sources */, | ||||||
| 				E216B799260C2DF200FB205F /* EditLanguages.swift in Sources */, | 				E216B799260C2DF200FB205F /* EditLanguages.swift in Sources */, | ||||||
| 				E2570FBB25B1AC520055B23B /* ContentView.swift in Sources */, | 				E2570FBB25B1AC520055B23B /* ContentView.swift in Sources */, | ||||||
| 				E24ACE502607326E009BF703 /* EditBasicInfo.swift in Sources */, | 				E24ACE502607326E009BF703 /* EditBasicInfo.swift in Sources */, | ||||||
|  | 				E219249426198A8200C84E12 /* LanguageDTO.swift in Sources */, | ||||||
| 				E254F90E260D19A0009295A5 /* EditTraits.swift in Sources */, | 				E254F90E260D19A0009295A5 /* EditTraits.swift in Sources */, | ||||||
| 				E2570FC525B1AC550055B23B /* MonsterCards.xcdatamodeld in Sources */, | 				E2570FC525B1AC550055B23B /* MonsterCards.xcdatamodeld in Sources */, | ||||||
| 				E2182E6425B22F8A00DFAEF8 /* Monster+CoreDataClass.swift in Sources */, | 				E2182E6425B22F8A00DFAEF8 /* Monster+CoreDataClass.swift in Sources */, | ||||||
| @@ -447,11 +473,14 @@ | |||||||
| 				E210B83A25B42D980083EAC5 /* MCProficiencyPicker.swift in Sources */, | 				E210B83A25B42D980083EAC5 /* MCProficiencyPicker.swift in Sources */, | ||||||
| 				E2570FF025B1ADC10055B23B /* Search.swift in Sources */, | 				E2570FF025B1ADC10055B23B /* Search.swift in Sources */, | ||||||
| 				E257100925B1B2480055B23B /* MonsterDetail.swift in Sources */, | 				E257100925B1B2480055B23B /* MonsterDetail.swift in Sources */, | ||||||
|  | 				E219248526198A1200C84E12 /* SkillDTO.swift in Sources */, | ||||||
| 				E2D473FD25B532C900CB36D7 /* Color+Hex.swift in Sources */, | 				E2D473FD25B532C900CB36D7 /* Color+Hex.swift in Sources */, | ||||||
| 				E2B5285925B3028700AAA69E /* EditMonster.swift in Sources */, | 				E2B5285925B3028700AAA69E /* EditMonster.swift in Sources */, | ||||||
|  | 				E219247B261989B400C84E12 /* MonsterDTO.swift in Sources */, | ||||||
| 				E2CB0DD72608720000142591 /* StringHelper.swift in Sources */, | 				E2CB0DD72608720000142591 /* StringHelper.swift in Sources */, | ||||||
| 				E2570FF525B1ADEB0055B23B /* Dashboard.swift in Sources */, | 				E2570FF525B1ADEB0055B23B /* Dashboard.swift in Sources */, | ||||||
| 				E2CB0DB826081A2F00142591 /* MCAbilityScorePicker.swift in Sources */, | 				E2CB0DB826081A2F00142591 /* MCAbilityScorePicker.swift in Sources */, | ||||||
|  | 				E219249926198E0D00C84E12 /* MonsterImportHelper.swift in Sources */, | ||||||
| 				E2CB0DC026086E3C00142591 /* ChallengeRating.swift in Sources */, | 				E2CB0DC026086E3C00142591 /* ChallengeRating.swift in Sources */, | ||||||
| 				E257100425B1AF4A0055B23B /* SearchBar.swift in Sources */, | 				E257100425B1AF4A0055B23B /* SearchBar.swift in Sources */, | ||||||
| 				E20209F525D8E04300EFE733 /* AdvantageType.swift in Sources */, | 				E20209F525D8E04300EFE733 /* AdvantageType.swift in Sources */, | ||||||
| @@ -465,6 +494,7 @@ | |||||||
| 				E2CB0DCA26086E8300142591 /* ArmorType.swift in Sources */, | 				E2CB0DCA26086E8300142591 /* ArmorType.swift in Sources */, | ||||||
| 				E24ACE562607EE94009BF703 /* EditArmor.swift in Sources */, | 				E24ACE562607EE94009BF703 /* EditArmor.swift in Sources */, | ||||||
| 				E2CB0DE1260887ED00142591 /* StringViewModel.swift in Sources */, | 				E2CB0DE1260887ED00142591 /* StringViewModel.swift in Sources */, | ||||||
|  | 				E219248F26198A6A00C84E12 /* DamageTypeDTO.swift in Sources */, | ||||||
| 				E20209F425D8E04300EFE733 /* ProficiencyType.swift in Sources */, | 				E20209F425D8E04300EFE733 /* ProficiencyType.swift in Sources */, | ||||||
| 				E2CB0DC526086E5F00142591 /* SizeType.swift in Sources */, | 				E2CB0DC526086E5F00142591 /* SizeType.swift in Sources */, | ||||||
| 				E254F906260D0818009295A5 /* AbilityViewModel.swift in Sources */, | 				E254F906260D0818009295A5 /* AbilityViewModel.swift in Sources */, | ||||||
| @@ -472,12 +502,14 @@ | |||||||
| 				E24ACE5B2607F0F2009BF703 /* EditSpeed.swift in Sources */, | 				E24ACE5B2607F0F2009BF703 /* EditSpeed.swift in Sources */, | ||||||
| 				E2570FB925B1AC520055B23B /* MonsterCardsApp.swift in Sources */, | 				E2570FB925B1AC520055B23B /* MonsterCardsApp.swift in Sources */, | ||||||
| 				E254F913260D1F6D009295A5 /* EditTrait.swift in Sources */, | 				E254F913260D1F6D009295A5 /* EditTrait.swift in Sources */, | ||||||
|  | 				E2192480261989F700C84E12 /* SavingThrowDTO.swift in Sources */, | ||||||
| 				E216B7B7260C5A9800FB205F /* ChallengeRatingViewModel.swift in Sources */, | 				E216B7B7260C5A9800FB205F /* ChallengeRatingViewModel.swift in Sources */, | ||||||
| 				E20209D325D8DD9600EFE733 /* Skill+CoreDataClass.swift in Sources */, | 				E20209D325D8DD9600EFE733 /* Skill+CoreDataClass.swift in Sources */, | ||||||
| 				E24ACE652607F55D009BF703 /* EditSavingThrows.swift in Sources */, | 				E24ACE652607F55D009BF703 /* EditSavingThrows.swift in Sources */, | ||||||
| 				E2BD702C25B3A8D70058ED69 /* MCTextField.swift in Sources */, | 				E2BD702C25B3A8D70058ED69 /* MCTextField.swift in Sources */, | ||||||
| 				E216B7BC260C691400FB205F /* EditChallengeRating.swift in Sources */, | 				E216B7BC260C691400FB205F /* EditChallengeRating.swift in Sources */, | ||||||
| 				E20209E825D8DEC100EFE733 /* AbilityScore.swift in Sources */, | 				E20209E825D8DEC100EFE733 /* AbilityScore.swift in Sources */, | ||||||
|  | 				E219248A26198A5400C84E12 /* TraitDTO.swift in Sources */, | ||||||
| 				E210B83F25B42DAB0083EAC5 /* MCAdvantagePicker.swift in Sources */, | 				E210B83F25B42DAB0083EAC5 /* MCAdvantagePicker.swift in Sources */, | ||||||
| 				E216B7C1260C6B6000FB205F /* MCChallengeRatingPicker.swift in Sources */, | 				E216B7C1260C6B6000FB205F /* MCChallengeRatingPicker.swift in Sources */, | ||||||
| 				E26CDA2B25CFB38E00E3F50D /* MCArmorTypePicker.swift in Sources */, | 				E26CDA2B25CFB38E00E3F50D /* MCArmorTypePicker.swift in Sources */, | ||||||
|   | |||||||
							
								
								
									
										111
									
								
								MonsterCards/Helpers/MonsterImportHelper.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								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,5 +50,76 @@ | |||||||
| 		<string>UIInterfaceOrientationLandscapeLeft</string> | 		<string>UIInterfaceOrientationLandscapeLeft</string> | ||||||
| 		<string>UIInterfaceOrientationLandscapeRight</string> | 		<string>UIInterfaceOrientationLandscapeRight</string> | ||||||
| 	</array> | 	</array> | ||||||
|  |     <key>UISupportsDocumentBrowser</key> | ||||||
|  |     <false/> | ||||||
|  |     <key>LSSupportsOpeningDocumentsInPlace</key> | ||||||
|  |     <false/> | ||||||
|  |     <key>CFBundleDocumentTypes</key> | ||||||
|  |     <array> | ||||||
|  |         <dict> | ||||||
|  |             <key>CFBundleTypeName</key> | ||||||
|  |             <string>Monster Data</string> | ||||||
|  |             <key>CFBundleTypeRole</key> | ||||||
|  |             <string>Editor</string> | ||||||
|  |             <key>LSHandlerRank</key> | ||||||
|  |             <string>Owner</string> | ||||||
|  |             <key>LSItemContentTypes</key> | ||||||
|  |             <array> | ||||||
|  |                 <string>com.majinnaibu.Monster</string> | ||||||
|  |             </array> | ||||||
|  |         </dict> | ||||||
|  |     </array> | ||||||
|  |     <key>UTExportedTypeDeclarations</key> | ||||||
|  |     <array> | ||||||
|  |         <dict> | ||||||
|  |             <key>UTTypeConformsTo</key> | ||||||
|  |             <array> | ||||||
|  |                 <string>public.json</string> | ||||||
|  |             </array> | ||||||
|  |             <key>UTTypeDescription</key> | ||||||
|  |             <string>Monster data file</string> | ||||||
|  |             <key>UTTypeIconFiles</key> | ||||||
|  |             <array/> | ||||||
|  |             <key>UTTypeIdentifier</key> | ||||||
|  |             <string>com.majinnaibu.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.json</string> | ||||||
|  |             </array> | ||||||
|  |             <key>UTTypeDescription</key> | ||||||
|  |             <string>Monster data file</string> | ||||||
|  |             <key>UTTypeIconFiles</key> | ||||||
|  |             <array/> | ||||||
|  |             <key>UTTypeIdentifier</key> | ||||||
|  |             <string>com.majinnaibu.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> | </dict> | ||||||
| </plist> | </plist> | ||||||
|   | |||||||
| @@ -34,7 +34,7 @@ public class AbilityViewModel: NSObject, ObservableObject, Identifiable, NSSecur | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     public func renderedText(_ monster: Monster) -> String { |     func renderedText(_ monster: MonsterViewModel) -> String { | ||||||
|         let strSave = monster.strengthModifier + monster.proficiencyBonus + 8 |         let strSave = monster.strengthModifier + monster.proficiencyBonus + 8 | ||||||
|         let dexSave = monster.dexterityModifier + monster.proficiencyBonus + 8 |         let dexSave = monster.dexterityModifier + monster.proficiencyBonus + 8 | ||||||
|         let conSave = monster.constitutionModifier + monster.proficiencyBonus + 8 |         let conSave = monster.constitutionModifier + monster.proficiencyBonus + 8 | ||||||
|   | |||||||
							
								
								
									
										42
									
								
								MonsterCards/Models/DamageTypeDTO.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								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) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										38
									
								
								MonsterCards/Models/LanguageDTO.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								MonsterCards/Models/LanguageDTO.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | |||||||
|  | // | ||||||
|  | //  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: Decodable { | ||||||
|  |          | ||||||
|  |     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 | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | extension LanguageDTO: Encodable { | ||||||
|  |      | ||||||
|  |     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) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -100,8 +100,8 @@ public class Monster: NSManagedObject { | |||||||
|             } else { |             } else { | ||||||
|                 var parts: [String] = [] |                 var parts: [String] = [] | ||||||
|                  |                  | ||||||
|                 if (baseSpeed > 0) { |                 if (walkSpeed > 0) { | ||||||
|                     parts.append("\(baseSpeed) ft.") |                     parts.append("\(walkSpeed) ft.") | ||||||
|                 } |                 } | ||||||
|                 if (burrowSpeed > 0) { |                 if (burrowSpeed > 0) { | ||||||
|                     parts.append("burrow \(burrowSpeed) ft.") |                     parts.append("burrow \(burrowSpeed) ft.") | ||||||
|   | |||||||
							
								
								
									
										278
									
								
								MonsterCards/Models/MonsterDTO.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										278
									
								
								MonsterCards/Models/MonsterDTO.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,278 @@ | |||||||
|  | // | ||||||
|  | //  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 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | 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" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | extension MonsterDTO: Decodable { | ||||||
|  |     init(from decoder: Decoder) throws { | ||||||
|  |         let container = try decoder.container(keyedBy: MonsterDTOCodingKeys.self) | ||||||
|  |         self.name = (try? container.decode(String.self, forKey: .name)) ?? "Imported Monster" | ||||||
|  |         self.type = (try? container.decode(String.self, forKey: .type)) ?? "" | ||||||
|  |         self.alignment = (try? container.decode(String.self, forKey: .alignment)) ?? "" | ||||||
|  |         self.size = (try? container.decode(String.self, forKey: .size)) ?? "" | ||||||
|  |         self.hitDice = (try? container.decode(Int.self, forKey: .hitDice)) ?? 0 | ||||||
|  |         self.armorName = (try? container.decode(String.self, forKey: .armorName)) ?? "" | ||||||
|  |         self.otherArmorDesc = (try? container.decode(String.self, forKey: .otherArmorDesc)) ?? "" | ||||||
|  |         self.shieldBonus = (try? container.decode(Int.self, forKey: .shieldBonus)) ?? 0 | ||||||
|  |         self.natArmorBonus = (try? container.decode(Int.self, forKey: .natArmorBonus)) ?? 0 | ||||||
|  |         self.speed = (try? container.decode(Int.self, forKey: .speed)) ?? 0 | ||||||
|  |         self.burrowSpeed = (try? container.decode(Int.self, forKey: .burrowSpeed)) ?? 0 | ||||||
|  |         self.climbSpeed = (try? container.decode(Int.self, forKey: .climbSpeed)) ?? 0 | ||||||
|  |         self.flySpeed = (try? container.decode(Int.self, forKey: .flySpeed)) ?? 0 | ||||||
|  |         self.hover = (try? container.decode(Bool.self, forKey: .hover)) ?? false | ||||||
|  |         self.swimSpeed = (try? container.decode(Int.self, forKey: .swimSpeed)) ?? 0 | ||||||
|  |         self.speedDesc = (try? container.decode(String.self, forKey: .speedDesc)) ?? "" | ||||||
|  |         self.customSpeed = (try? container.decode(Bool.self, forKey: .customSpeed)) ?? false | ||||||
|  |         self.strPoints = (try? container.decode(Int.self, forKey: .strPoints)) ?? 0 | ||||||
|  |         self.dexPoints = (try? container.decode(Int.self, forKey: .dexPoints)) ?? 0 | ||||||
|  |         self.conPoints = (try? container.decode(Int.self, forKey: .conPoints)) ?? 0 | ||||||
|  |         self.intPoints = (try? container.decode(Int.self, forKey: .intPoints)) ?? 0 | ||||||
|  |         self.wisPoints = (try? container.decode(Int.self, forKey: .wisPoints)) ?? 0 | ||||||
|  |         self.chaPoints = (try? container.decode(Int.self, forKey: .chaPoints)) ?? 0 | ||||||
|  |         self.cr = (try? container.decode(String.self, forKey: .cr)) ?? "" | ||||||
|  |         self.customCr = (try? container.decode(String.self, forKey: .customCr)) ?? "" | ||||||
|  |         self.customProf = (try? container.decode(Int.self, forKey: .customProf)) ?? 0 | ||||||
|  |         self.hpText = (try? container.decode(String.self, forKey: .hpText)) ?? "" | ||||||
|  |         self.legendaryDescription = (try? container.decode(String.self, forKey: .legendaryDescription)) ?? "" | ||||||
|  |         self.understandsBut = (try? container.decode(String.self, forKey: .understandsBut)) ?? "" | ||||||
|  |         self.tag = (try? container.decode(String.self, forKey: .tag)) ?? "" | ||||||
|  |         self.lairDescription = (try? container.decode(String.self, forKey: .lairDescription)) ?? "" | ||||||
|  |         self.lairDescriptionEnd = (try? container.decode(String.self, forKey: .lairDescriptionEnd)) ?? "" | ||||||
|  |         self.regionalDescription = (try? container.decode(String.self, forKey: .regionalDescription)) ?? "" | ||||||
|  |         self.regionalDescriptionEnd = (try? container.decode(String.self, forKey: .regionalDescriptionEnd)) ?? "" | ||||||
|  |         self.shortName = (try? container.decode(String.self, forKey: .shortName)) ?? "" | ||||||
|  |          | ||||||
|  |         self.telepathy = (try? container.decode(Int.self, forKey: .telepathy)) ?? 0 | ||||||
|  |         self.blindsight = (try? container.decode(Int.self, forKey: .blindsight)) ?? 0 | ||||||
|  |         self.darkvision = (try? container.decode(Int.self, forKey: .darkvision)) ?? 0 | ||||||
|  |         self.tremorsense = (try? container.decode(Int.self, forKey: .tremorsense)) ?? 0 | ||||||
|  |         self.truesight = (try? container.decode(Int.self, forKey: .truesight)) ?? 0 | ||||||
|  |         self.separationPoint = (try? container.decode(Int.self, forKey: .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 | ||||||
|  |  | ||||||
|  |         // properties is always an empty array | ||||||
|  |          | ||||||
|  | //        self.properties = (try? container.decode([String].self, forKey: .properties)) ?? [] | ||||||
|  |         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)) ?? [] | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | extension MonsterDTO: Encodable { | ||||||
|  |      | ||||||
|  |     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(self.properties, 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) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -10,6 +10,8 @@ import CoreData | |||||||
|  |  | ||||||
| class MonsterViewModel: ObservableObject { | 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 name: String | ||||||
|     @Published var size: String |     @Published var size: String | ||||||
|     @Published var type: String |     @Published var type: String | ||||||
| @@ -19,10 +21,12 @@ class MonsterViewModel: ObservableObject { | |||||||
|     @Published var hasCustomHP: Bool |     @Published var hasCustomHP: Bool | ||||||
|     @Published var customHP: String |     @Published var customHP: String | ||||||
|     @Published var armorType: ArmorType |     @Published var armorType: ArmorType | ||||||
|     @Published var hasShield: Bool |     @Published var hasShield: Bool { | ||||||
|  |         didSet { shieldBonus = hasShield ? 2 : 0 } | ||||||
|  |     } | ||||||
|     @Published var naturalArmorBonus: Int64 |     @Published var naturalArmorBonus: Int64 | ||||||
|     @Published var customArmor: String |     @Published var customArmor: String | ||||||
|     @Published var baseSpeed: Int64 |     @Published var walkSpeed: Int64 | ||||||
|     @Published var burrowSpeed: Int64 |     @Published var burrowSpeed: Int64 | ||||||
|     @Published var climbSpeed: Int64 |     @Published var climbSpeed: Int64 | ||||||
|     @Published var flySpeed: Int64 |     @Published var flySpeed: Int64 | ||||||
| @@ -63,6 +67,28 @@ class MonsterViewModel: ObservableObject { | |||||||
|     @Published var abilities: [AbilityViewModel] |     @Published var abilities: [AbilityViewModel] | ||||||
|     @Published var actions: [AbilityViewModel] |     @Published var actions: [AbilityViewModel] | ||||||
|     @Published var legendaryActions: [AbilityViewModel] |     @Published var legendaryActions: [AbilityViewModel] | ||||||
|  |     @Published var lairActions: [AbilityViewModel] | ||||||
|  |     @Published var regionalActions: [AbilityViewModel] | ||||||
|  |     @Published var reactions: [AbilityViewModel] | ||||||
|  |     @Published var isBlind: Bool | ||||||
|  |      | ||||||
|  |     private var shieldBonus: Int | ||||||
|  |     private 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(_ rawMonster: Monster? = nil) { |     init(_ rawMonster: Monster? = nil) { | ||||||
|         self.name = "" |         self.name = "" | ||||||
| @@ -77,7 +103,7 @@ class MonsterViewModel: ObservableObject { | |||||||
|         self.hasShield = false |         self.hasShield = false | ||||||
|         self.naturalArmorBonus = 0 |         self.naturalArmorBonus = 0 | ||||||
|         self.customArmor = "" |         self.customArmor = "" | ||||||
|         self.baseSpeed = 0 |         self.walkSpeed = 0 | ||||||
|         self.burrowSpeed = 0 |         self.burrowSpeed = 0 | ||||||
|         self.climbSpeed = 0 |         self.climbSpeed = 0 | ||||||
|         self.flySpeed = 0 |         self.flySpeed = 0 | ||||||
| @@ -118,7 +144,16 @@ class MonsterViewModel: ObservableObject { | |||||||
|         self.abilities = [] |         self.abilities = [] | ||||||
|         self.actions = [] |         self.actions = [] | ||||||
|         self.legendaryActions = [] |         self.legendaryActions = [] | ||||||
|  |         self.lairActions = [] | ||||||
|  |         self.regionalActions = [] | ||||||
|  |         self.reactions = [] | ||||||
|  |         self.isBlind = false | ||||||
|          |          | ||||||
|  |         // Private properties | ||||||
|  |         self.shieldBonus = 0 | ||||||
|  |         self.otherArmorDescription = "" | ||||||
|  |  | ||||||
|  |         // Call the copy constructor | ||||||
|         if (rawMonster != nil) { |         if (rawMonster != nil) { | ||||||
|             self.copyFromMonster(monster: rawMonster!) |             self.copyFromMonster(monster: rawMonster!) | ||||||
|         } |         } | ||||||
| @@ -137,7 +172,7 @@ class MonsterViewModel: ObservableObject { | |||||||
|         self.hasShield = monster.hasShield |         self.hasShield = monster.hasShield | ||||||
|         self.naturalArmorBonus = monster.naturalArmorBonus |         self.naturalArmorBonus = monster.naturalArmorBonus | ||||||
|         self.customArmor = monster.customArmor ?? "" |         self.customArmor = monster.customArmor ?? "" | ||||||
|         self.baseSpeed = monster.baseSpeed |         self.walkSpeed = monster.walkSpeed | ||||||
|         self.burrowSpeed = monster.burrowSpeed |         self.burrowSpeed = monster.burrowSpeed | ||||||
|         self.climbSpeed = monster.climbSpeed |         self.climbSpeed = monster.climbSpeed | ||||||
|         self.flySpeed = monster.flySpeed |         self.flySpeed = monster.flySpeed | ||||||
| @@ -168,6 +203,7 @@ class MonsterViewModel: ObservableObject { | |||||||
|         self.challengeRating = monster.challengeRatingEnum |         self.challengeRating = monster.challengeRatingEnum | ||||||
|         self.customChallengeRating = monster.customChallengeRating ?? "" |         self.customChallengeRating = monster.customChallengeRating ?? "" | ||||||
|         self.customProficiencyBonus = monster.customProficiencyBonus |         self.customProficiencyBonus = monster.customProficiencyBonus | ||||||
|  |         self.isBlind = monster.isBlind | ||||||
|          |          | ||||||
|         self.skills = (monster.skills?.allObjects.map {SkillViewModel(($0 as! Skill))})!.sorted() |         self.skills = (monster.skills?.allObjects.map {SkillViewModel(($0 as! Skill))})!.sorted() | ||||||
|      |      | ||||||
| @@ -204,6 +240,20 @@ class MonsterViewModel: ObservableObject { | |||||||
|          |          | ||||||
|         self.legendaryActions = (monster.legendaryActions ?? []) |         self.legendaryActions = (monster.legendaryActions ?? []) | ||||||
|             .map {AbilityViewModel($0.name, $0.abilityDescription)} |             .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) { |     func copyToMonster(monster: Monster) { | ||||||
| @@ -219,7 +269,7 @@ class MonsterViewModel: ObservableObject { | |||||||
|         monster.hasShield = hasShield |         monster.hasShield = hasShield | ||||||
|         monster.naturalArmorBonus = naturalArmorBonus |         monster.naturalArmorBonus = naturalArmorBonus | ||||||
|         monster.customArmor = customArmor |         monster.customArmor = customArmor | ||||||
|         monster.baseSpeed = baseSpeed |         monster.walkSpeed = walkSpeed | ||||||
|         monster.burrowSpeed = burrowSpeed |         monster.burrowSpeed = burrowSpeed | ||||||
|         monster.climbSpeed = climbSpeed |         monster.climbSpeed = climbSpeed | ||||||
|         monster.flySpeed = flySpeed |         monster.flySpeed = flySpeed | ||||||
| @@ -250,6 +300,7 @@ class MonsterViewModel: ObservableObject { | |||||||
|         monster.challengeRatingEnum = challengeRating |         monster.challengeRatingEnum = challengeRating | ||||||
|         monster.customChallengeRating = customChallengeRating |         monster.customChallengeRating = customChallengeRating | ||||||
|         monster.customProficiencyBonus = customProficiencyBonus |         monster.customProficiencyBonus = customProficiencyBonus | ||||||
|  |         monster.isBlind = isBlind | ||||||
|  |  | ||||||
|         // Remove missing skills from raw monster |         // Remove missing skills from raw monster | ||||||
|         monster.skills?.forEach {s in |         monster.skills?.forEach {s in | ||||||
| @@ -285,5 +336,547 @@ class MonsterViewModel: ObservableObject { | |||||||
|         monster.actions = actions.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.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 | ||||||
|     } |     } | ||||||
|  |      | ||||||
|  |     // 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(Monster.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: ", ") | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |  | ||||||
|  |     // 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 = Monster.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 = Monster.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 = Monster.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 = Monster.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 = Monster.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 = Monster.advantageLabelStringForType(charismaSavingThrowAdvantage) | ||||||
|  |                 if (!advantage.isEmpty) { | ||||||
|  |                     advantage = " " + advantage | ||||||
|  |                 } | ||||||
|  |                 parts.append(String(format: "%@ %+d%@", name, bonus, advantage)) | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             return parts.joined(separator: ", ") | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     // MARK: Misc Helpers | ||||||
|  |  | ||||||
|  |      | ||||||
|  |     // 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 | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										38
									
								
								MonsterCards/Models/SavingThrowDTO.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								MonsterCards/Models/SavingThrowDTO.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | |||||||
|  | // | ||||||
|  | //  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: Decodable { | ||||||
|  |      | ||||||
|  |     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 | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | extension SavingThrowDTO: Encodable { | ||||||
|  |      | ||||||
|  |     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) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										42
									
								
								MonsterCards/Models/SkillDTO.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								MonsterCards/Models/SkillDTO.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | |||||||
|  | // | ||||||
|  | //  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: Decodable { | ||||||
|  |      | ||||||
|  |     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)) ?? "" | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | extension SkillDTO: Encodable { | ||||||
|  |      | ||||||
|  |     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) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -65,6 +65,13 @@ class SkillViewModel: ObservableObject, Comparable, Hashable, Identifiable { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|      |      | ||||||
|  |     init(_ name: String, _ abilityScore: AbilityScore, _ proficiency: ProficiencyType = .proficient, _ advantage: AdvantageType = .none) { | ||||||
|  |         _name = name | ||||||
|  |         _abilityScore = abilityScore | ||||||
|  |         _proficiency = proficiency | ||||||
|  |         _advantage = advantage | ||||||
|  |     } | ||||||
|  |      | ||||||
|     private var _name: String = "" |     private var _name: String = "" | ||||||
|     var name: String { |     var name: String { | ||||||
|         get { |         get { | ||||||
| @@ -125,4 +132,25 @@ class SkillViewModel: ObservableObject, Comparable, Hashable, Identifiable { | |||||||
|         newSkill.wrappedAdvantage = advantage |         newSkill.wrappedAdvantage = advantage | ||||||
|         return newSkill |         return newSkill | ||||||
|     } |     } | ||||||
|  |      | ||||||
|  |     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 = Monster.advantageLabelStringForType(advantage) | ||||||
|  |         if (advantageLabel != "") { | ||||||
|  |             advantageLabel = " " + advantageLabel | ||||||
|  |         } | ||||||
|  |         return String(format: "%@ %+d%@", name, modifier(forMonster: forMonster), advantageLabel) | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										42
									
								
								MonsterCards/Models/TraitDTO.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								MonsterCards/Models/TraitDTO.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | |||||||
|  | // | ||||||
|  | //  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: Decodable { | ||||||
|  |      | ||||||
|  |     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)) ?? "" | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | extension TraitDTO: Encodable { | ||||||
|  |          | ||||||
|  |     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) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -5,7 +5,6 @@ | |||||||
|         <attribute name="actions" 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="alignment" attributeType="String" defaultValueString=""/> | ||||||
|         <attribute name="armorType" attributeType="String" defaultValueString=""/> |         <attribute name="armorType" attributeType="String" defaultValueString=""/> | ||||||
|         <attribute name="baseSpeed" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/> |  | ||||||
|         <attribute name="blindsightDistance" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/> |         <attribute name="blindsightDistance" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/> | ||||||
|         <attribute name="burrowSpeed" 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="canHover" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/> | ||||||
| @@ -39,11 +38,14 @@ | |||||||
|         <attribute name="intelligenceSavingThrowProficiency" attributeType="String" defaultValueString="none"/> |         <attribute name="intelligenceSavingThrowProficiency" attributeType="String" defaultValueString="none"/> | ||||||
|         <attribute name="intelligenceScore" attributeType="Integer 64" defaultValueString="10" usesScalarValueType="YES"/> |         <attribute name="intelligenceScore" attributeType="Integer 64" defaultValueString="10" usesScalarValueType="YES"/> | ||||||
|         <attribute name="isBlind" attributeType="Boolean" defaultValueString="NO" 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="languages" optional="YES" attributeType="Transformable" valueTransformerName="LanguageViewModelValueTransformer" customClassName="[LanguageViewModel]"/> | ||||||
|         <attribute name="legendaryActions" optional="YES" attributeType="Transformable" valueTransformerName="AbilityViewModelValueTransformer" customClassName="[AbilityViewModel]"/> |         <attribute name="legendaryActions" optional="YES" attributeType="Transformable" valueTransformerName="AbilityViewModelValueTransformer" customClassName="[AbilityViewModel]"/> | ||||||
|         <attribute name="name" attributeType="String" defaultValueString="Unnamed Monster"/> |         <attribute name="name" attributeType="String" defaultValueString="Unnamed Monster"/> | ||||||
|         <attribute name="naturalArmorBonus" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/> |         <attribute name="naturalArmorBonus" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/> | ||||||
|         <attribute name="otherArmorDescription" attributeType="String" defaultValueString=""/> |         <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="senses" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformerName" customClassName="[String]"/> | ||||||
|         <attribute name="shieldBonus" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/> |         <attribute name="shieldBonus" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/> | ||||||
|         <attribute name="size" attributeType="String" defaultValueString=""/> |         <attribute name="size" attributeType="String" defaultValueString=""/> | ||||||
| @@ -57,6 +59,7 @@ | |||||||
|         <attribute name="truesightDistance" 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="type" attributeType="String" defaultValueString=""/> | ||||||
|         <attribute name="understandsBut" 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="wisdomSavingThrowAdvantage" attributeType="String" defaultValueString="none"/> | ||||||
|         <attribute name="wisdomSavingThrowProficiency" attributeType="String" defaultValueString="none"/> |         <attribute name="wisdomSavingThrowProficiency" attributeType="String" defaultValueString="none"/> | ||||||
|         <attribute name="wisdomScore" attributeType="Integer 64" defaultValueString="10" usesScalarValueType="YES"/> |         <attribute name="wisdomScore" attributeType="Integer 64" defaultValueString="10" usesScalarValueType="YES"/> | ||||||
| @@ -70,7 +73,7 @@ | |||||||
|         <relationship name="monster" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Monster" inverseName="skills" inverseEntity="Monster"/> |         <relationship name="monster" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Monster" inverseName="skills" inverseEntity="Monster"/> | ||||||
|     </entity> |     </entity> | ||||||
|     <elements> |     <elements> | ||||||
|         <element name="Monster" positionX="-63" positionY="-18" width="128" height="929"/> |         <element name="Monster" positionX="-63" positionY="-18" width="128" height="974"/> | ||||||
|         <element name="Skill" positionX="-63" positionY="135" width="128" height="14"/> |         <element name="Skill" positionX="-63" positionY="135" width="128" height="14"/> | ||||||
|     </elements> |     </elements> | ||||||
| </model> | </model> | ||||||
| @@ -8,8 +8,14 @@ | |||||||
| import SwiftUI | import SwiftUI | ||||||
| import CoreData | import CoreData | ||||||
|  |  | ||||||
|  | struct ImportInfo { | ||||||
|  |     var monster: MonsterViewModel = MonsterViewModel() | ||||||
|  | } | ||||||
|  |  | ||||||
| struct ContentView: View { | struct ContentView: View { | ||||||
|     @Environment(\.managedObjectContext) private var viewContext |     @Environment(\.managedObjectContext) private var viewContext | ||||||
|  |     @State private var importInfo = ImportInfo() | ||||||
|  |     @State private var isShowingImportDialog = false | ||||||
|          |          | ||||||
|     var body: some View { |     var body: some View { | ||||||
|         TabView { |         TabView { | ||||||
| @@ -35,69 +41,32 @@ struct ContentView: View { | |||||||
|                     Text("Library") |                     Text("Library") | ||||||
|                 } |                 } | ||||||
|         } |         } | ||||||
|  |         .onOpenURL(perform: beginImportingMonster) | ||||||
|  |         .sheet(isPresented: self.$isShowingImportDialog) { | ||||||
|  |             ImportMonster(monster: $importInfo.monster) | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|      |      | ||||||
| //    @FetchRequest( |     func beginImportingMonster(url: URL) { | ||||||
| //        sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)], |  | ||||||
| //        animation: .default) |  | ||||||
| //    private var items: FetchedResults<Item> |  | ||||||
| // |  | ||||||
| //    var body: some View { |  | ||||||
| //        List { |  | ||||||
| //            ForEach(items) { item in |  | ||||||
| //                Text("Item at \(item.timestamp!, formatter: itemFormatter)") |  | ||||||
| //            } |  | ||||||
| //            .onDelete(perform: deleteItems) |  | ||||||
| //        } |  | ||||||
| //        .toolbar { |  | ||||||
| //            #if os(iOS) |  | ||||||
| //            EditButton() |  | ||||||
| //            #endif |  | ||||||
| // |  | ||||||
| //            Button(action: addItem) { |  | ||||||
| //                Label("Add Item", systemImage: "plus") |  | ||||||
| //            } |  | ||||||
| //        } |  | ||||||
| //    } |  | ||||||
| // |  | ||||||
| //    private func addItem() { |  | ||||||
| //        withAnimation { |  | ||||||
| //            let newItem = Item(context: viewContext) |  | ||||||
| //            newItem.timestamp = Date() |  | ||||||
| // |  | ||||||
| //            do { |  | ||||||
| //                try viewContext.save() |  | ||||||
| //            } catch { |  | ||||||
| //                // Replace this implementation with code to handle the error appropriately. |  | ||||||
| //                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. |  | ||||||
| //                let nsError = error as NSError |  | ||||||
| //                fatalError("Unresolved error \(nsError), \(nsError.userInfo)") |  | ||||||
| //            } |  | ||||||
| //        } |  | ||||||
| //    } |  | ||||||
| // |  | ||||||
| //    private func deleteItems(offsets: IndexSet) { |  | ||||||
| //        withAnimation { |  | ||||||
| //            offsets.map { items[$0] }.forEach(viewContext.delete) |  | ||||||
| // |  | ||||||
| //            do { |  | ||||||
| //                try viewContext.save() |  | ||||||
| //            } catch { |  | ||||||
| //                // Replace this implementation with code to handle the error appropriately. |  | ||||||
| //                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. |  | ||||||
| //                let nsError = error as NSError |  | ||||||
| //                fatalError("Unresolved error \(nsError), \(nsError.userInfo)") |  | ||||||
| //            } |  | ||||||
| //        } |  | ||||||
| //    } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| //private let itemFormatter: DateFormatter = { |         // TOOD: only do this if the file name ends in .json or .monster | ||||||
| //    let formatter = DateFormatter() |          | ||||||
| //    formatter.dateStyle = .short |         let decoder = JSONDecoder() | ||||||
| //    formatter.timeStyle = .medium |         do { | ||||||
| //    return formatter |             let data = try Data(contentsOf: url) | ||||||
| //}() |             let monsterDTO = try decoder.decode(MonsterDTO.self, from: data) | ||||||
|  |             print(String(format: "Loaded monster: %@", monsterDTO.name)) | ||||||
|  |             // 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: handle this better | ||||||
|  |             print(error) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  | } | ||||||
|  |  | ||||||
| struct ContentView_Previews: PreviewProvider { | struct ContentView_Previews: PreviewProvider { | ||||||
|     static var previews: some View { |     static var previews: some View { | ||||||
|   | |||||||
| @@ -9,7 +9,11 @@ import CoreData | |||||||
| import SwiftUI | import SwiftUI | ||||||
|  |  | ||||||
| struct EditMonster: View { | struct EditMonster: View { | ||||||
|     // TODO: add challengeRating/challengeRatingEnum and customChallengeRating maybe in basicInfo |     // 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. | ||||||
|  |     // TODO: Add a way to edit lair actions | ||||||
|  |     // TODO: Add a way to edit regional actions | ||||||
|  |     // TODO: Add a way to edit reactions | ||||||
|     @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode> |     @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode> | ||||||
|     @Environment(\.managedObjectContext) private var viewContext |     @Environment(\.managedObjectContext) private var viewContext | ||||||
|      |      | ||||||
| @@ -141,7 +145,7 @@ struct EditMonster_Previews: PreviewProvider { | |||||||
|         monster.hitDice = 6 |         monster.hitDice = 6 | ||||||
|         monster.hasCustomHP = true |         monster.hasCustomHP = true | ||||||
|         monster.customHP = "12 (1d10)+2" |         monster.customHP = "12 (1d10)+2" | ||||||
|         monster.baseSpeed = 5 |         monster.walkSpeed = 5 | ||||||
|         monster.burrowSpeed = 10 |         monster.burrowSpeed = 10 | ||||||
|         monster.climbSpeed = 15 |         monster.climbSpeed = 15 | ||||||
|         monster.flySpeed = 20 |         monster.flySpeed = 20 | ||||||
|   | |||||||
| @@ -12,12 +12,12 @@ struct EditSpeed: View { | |||||||
|      |      | ||||||
|     var body: some View { |     var body: some View { | ||||||
|         List { |         List { | ||||||
|             // Number bound to monster.baseSpeed |             // Number bound to monster.walkSpeed | ||||||
|             MCStepperField( |             MCStepperField( | ||||||
|                 label: "Base", |                 label: "Base", | ||||||
|                 step: 5, |                 step: 5, | ||||||
|                 suffix: " ft.", |                 suffix: " ft.", | ||||||
|                 value: $monsterViewModel.baseSpeed) |                 value: $monsterViewModel.walkSpeed) | ||||||
|              |              | ||||||
|             // Number bound to monster.burrowSpeed |             // Number bound to monster.burrowSpeed | ||||||
|             MCStepperField( |             MCStepperField( | ||||||
|   | |||||||
							
								
								
									
										24
									
								
								MonsterCards/Views/ImportMonster.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								MonsterCards/Views/ImportMonster.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | |||||||
|  | // | ||||||
|  | //  ImportMonster.swift | ||||||
|  | //  MonsterCards | ||||||
|  | // | ||||||
|  | //  Created by Tom Hicks on 4/1/21. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import SwiftUI | ||||||
|  |  | ||||||
|  | struct ImportMonster: View { | ||||||
|  |     @Binding var monster: MonsterViewModel | ||||||
|  |      | ||||||
|  |     var body: some View { | ||||||
|  |         MonsterDetailView(viewModel: monster) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct ImportMonster_Previews: PreviewProvider { | ||||||
|  |     static var previews: some View { | ||||||
|  |         ImportMonster( | ||||||
|  |             monster: .constant(MonsterViewModel()) | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -20,7 +20,7 @@ struct Library: View { | |||||||
|     var body: some View { |     var body: some View { | ||||||
|         NavigationView{ |         NavigationView{ | ||||||
|             List(allMonsters) { monster in |             List(allMonsters) { monster in | ||||||
|                 NavigationLink(destination: MonsterDetail(monster: monster)) { |                 NavigationLink(destination: MonsterDetailWrapper(monster: monster)) { | ||||||
|                     Text(monster.name ?? "") |                     Text(monster.name ?? "") | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|   | |||||||
| @@ -72,7 +72,7 @@ struct SmallAbilityScore: View { | |||||||
| } | } | ||||||
|  |  | ||||||
| struct BasicInfoView: View { | struct BasicInfoView: View { | ||||||
|     @ObservedObject var monster: Monster |     @ObservedObject var monster: MonsterViewModel | ||||||
|      |      | ||||||
|     var body: some View { |     var body: some View { | ||||||
|         let monsterMeta = monster.meta |         let monsterMeta = monster.meta | ||||||
| @@ -80,6 +80,11 @@ struct BasicInfoView: View { | |||||||
|         let monsterHitPoints = monster.hitPoints |         let monsterHitPoints = monster.hitPoints | ||||||
|         let monsterSpeed = monster.speed |         let monsterSpeed = monster.speed | ||||||
|          |          | ||||||
|  |         if (!monster.name.isEmpty) { | ||||||
|  |             Text(monster.name) | ||||||
|  |                 .font(.largeTitle) | ||||||
|  |         } | ||||||
|  |          | ||||||
|         // meta: "(large humanoid (elf) lawful evil" |         // meta: "(large humanoid (elf) lawful evil" | ||||||
|         if (!monsterMeta.isEmpty) { |         if (!monsterMeta.isEmpty) { | ||||||
|             Text(monsterMeta) |             Text(monsterMeta) | ||||||
| @@ -113,7 +118,7 @@ struct BasicInfoView: View { | |||||||
| } | } | ||||||
|  |  | ||||||
| struct AbilityScoresView: View { | struct AbilityScoresView: View { | ||||||
|     @ObservedObject var monster: Monster |     @ObservedObject var monster: MonsterViewModel | ||||||
|      |      | ||||||
|     var body: some View { |     var body: some View { | ||||||
|         SectionDivider() |         SectionDivider() | ||||||
| @@ -131,7 +136,7 @@ struct AbilityScoresView: View { | |||||||
| } | } | ||||||
|  |  | ||||||
| struct ResistancesAndImmunitiesView: View { | struct ResistancesAndImmunitiesView: View { | ||||||
|     @ObservedObject var monster: Monster |     @ObservedObject var monster: MonsterViewModel | ||||||
|      |      | ||||||
|     var body: some View { |     var body: some View { | ||||||
|         let monsterDamageVulnerabilitiesDescription = monster.damageVulnerabilitiesDescription |         let monsterDamageVulnerabilitiesDescription = monster.damageVulnerabilitiesDescription | ||||||
| @@ -178,7 +183,7 @@ struct ResistancesAndImmunitiesView: View { | |||||||
| } | } | ||||||
|  |  | ||||||
| struct SavingThrowsAndSkillsView: View { | struct SavingThrowsAndSkillsView: View { | ||||||
|     @ObservedObject var monster: Monster |     @ObservedObject var monster: MonsterViewModel | ||||||
|      |      | ||||||
|     var body: some View { |     var body: some View { | ||||||
|         let savingThrowsDescription = monster.savingThrowsDescription |         let savingThrowsDescription = monster.savingThrowsDescription | ||||||
| @@ -200,37 +205,28 @@ struct SavingThrowsAndSkillsView: View { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| struct MonsterDetail: View { | struct MonsterDetailView: View { | ||||||
|     let kTextColor: Color = Color(hex: 0x982818) |     let kTextColor = Color(hex: 0x982818) | ||||||
|      |      | ||||||
|     @ObservedObject var monster: Monster |     var viewModel: MonsterViewModel | ||||||
|      |      | ||||||
|     var body: some View { |     var body: some View { | ||||||
|  |         let monsterLanguagesDescription = viewModel.languagesDescription | ||||||
|  |         let monsterChallengeRatingDescription = viewModel.challengeRatingDescription | ||||||
|  |          | ||||||
|         ScrollView { |         ScrollView { | ||||||
|             // TODO: Consider adding an inmage here at the top |             // TODO: Consider adding an inmage here at the top | ||||||
|             VStack (alignment: .leading) { |             VStack (alignment: .leading) { | ||||||
|                 let monsterLanguagesDescription = monster.languagesDescription |  | ||||||
|                 let monsterChallengeRatingDescription = monster.challengeRatingDescription |  | ||||||
|                 let monsterAbilities: [AbilityViewModel] = monster.abilities ?? [] |  | ||||||
|                 let monsterActions: [AbilityViewModel] = monster.actions ?? [] |  | ||||||
|                 let monsterLegendaryActions: [AbilityViewModel] = monster.legendaryActions ?? [] |  | ||||||
|                  |  | ||||||
|                 BasicInfoView(monster: monster) |  | ||||||
|                  |  | ||||||
|                 // TODO: Find a way to hide unnecessarry dividiers. |                 // TODO: Find a way to hide unnecessarry dividiers. | ||||||
|                 // if sections 0, 1, 2, and 3 are present there should be a divider between each of them |                 // if sections 0, 1, 2, and 3 are present there should be a divider between each of them | ||||||
|                 // if section 1 is not present there should be one and only one divider between sections 0 and 2 as well as the one between 2 and 3 |                 // if section 1 is not present there should be one and only one divider between sections 0 and 2 as well as the one between 2 and 3 | ||||||
|                 // if sections 1 and 2 are not present there should be a single divider between sections 0 and 3 |                 // if sections 1 and 2 are not present there should be a single divider between sections 0 and 3 | ||||||
|  |  | ||||||
|  |                 BasicInfoView(monster: viewModel) | ||||||
|                 AbilityScoresView(monster: monster) |                 AbilityScoresView(monster: viewModel) | ||||||
|  |  | ||||||
|                 SectionDivider() |                 SectionDivider() | ||||||
|                  |                 SavingThrowsAndSkillsView(monster: viewModel) | ||||||
|                 SavingThrowsAndSkillsView(monster: monster) |                 ResistancesAndImmunitiesView(monster: viewModel) | ||||||
|                  |  | ||||||
|                 ResistancesAndImmunitiesView(monster: monster) |  | ||||||
|                  |  | ||||||
|                 Group { |                 Group { | ||||||
|                     // Languages |                     // Languages | ||||||
|                     if (!monsterLanguagesDescription.isEmpty) { |                     if (!monsterLanguagesDescription.isEmpty) { | ||||||
| @@ -248,27 +244,27 @@ struct MonsterDetail: View { | |||||||
|                      |                      | ||||||
|                     // Proficiency Bonus |                     // Proficiency Bonus | ||||||
|                     LabeledField("Proficiency Bonus") { |                     LabeledField("Proficiency Bonus") { | ||||||
|                         Text(String(monster.proficiencyBonus)) |                         Text(String(viewModel.proficiencyBonus)) | ||||||
|                     } |                     } | ||||||
|                  |                  | ||||||
|                     // Abilities |                     // Abilities | ||||||
|                     if (monsterAbilities.count > 0) { |                     if (viewModel.abilities.count > 0) { | ||||||
|                         ForEach(monsterAbilities) { ability in |                         ForEach(viewModel.abilities) { ability in | ||||||
|                             VStack { |                             VStack { | ||||||
|                                 Markdown(Document(ability.renderedText(monster))) |                                 Markdown(Document(ability.renderedText(viewModel))) | ||||||
|                                 Divider() |                                 Divider() | ||||||
|                             } |                             } | ||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
|                      |                      | ||||||
|                     // Actions |                     // Actions | ||||||
|                     if (monsterActions.count > 0) { |                     if (viewModel.actions.count > 0) { | ||||||
|                         VStack(alignment: .leading) { |                         VStack(alignment: .leading) { | ||||||
|                             Text("Actions") |                             Text("Actions") | ||||||
|                                 .font(.system(size: 24, weight: .bold)) |                                 .font(.system(size: 24, weight: .bold)) | ||||||
|                             ForEach(monsterActions) { action in |                             ForEach(viewModel.actions) { action in | ||||||
|                                 VStack { |                                 VStack { | ||||||
|                                     Markdown(Document(action.renderedText(monster))) |                                     Markdown(Document(action.renderedText(viewModel))) | ||||||
|                                     Divider() |                                     Divider() | ||||||
|                                 } |                                 } | ||||||
|                             } |                             } | ||||||
| @@ -276,13 +272,13 @@ struct MonsterDetail: View { | |||||||
|                     } |                     } | ||||||
|  |  | ||||||
|                     // Legendary Actions |                     // Legendary Actions | ||||||
|                     if (monsterLegendaryActions.count > 0) { |                     if (viewModel.legendaryActions.count > 0) { | ||||||
|                         VStack(alignment: .leading) { |                         VStack(alignment: .leading) { | ||||||
|                             Text("Legendary Actions") |                             Text("Legendary Actions") | ||||||
|                                 .font(.system(size: 20, weight: .bold)) |                                 .font(.system(size: 20, weight: .bold)) | ||||||
|                             ForEach(monsterLegendaryActions) { action in |                             ForEach(viewModel.legendaryActions) { action in | ||||||
|                                 VStack { |                                 VStack { | ||||||
|                                     Markdown(Document(action.renderedText(monster))) |                                     Markdown(Document(action.renderedText(viewModel))) | ||||||
|                                     Divider() |                                     Divider() | ||||||
|                                 } |                                 } | ||||||
|                             } |                             } | ||||||
| @@ -293,6 +289,25 @@ struct MonsterDetail: View { | |||||||
|             .padding(.horizontal) |             .padding(.horizontal) | ||||||
|             .foregroundColor(kTextColor) |             .foregroundColor(kTextColor) | ||||||
|         } |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct MonsterDetailWrapper: View { | ||||||
|  |     // TODO: Add display for when the monster is blind | ||||||
|  |     // TODO: Add display for lair actions | ||||||
|  |     // TODO: Add display for regional actions | ||||||
|  |     // TODO: Add display for reactions | ||||||
|  |     let kTextColor: Color = Color(hex: 0x982818) | ||||||
|  |      | ||||||
|  |     @ObservedObject var monster: Monster | ||||||
|  |     @StateObject private var viewModel = MonsterViewModel() | ||||||
|  |      | ||||||
|  |     var body: some View { | ||||||
|  |          | ||||||
|  |         MonsterDetailView(viewModel: viewModel) | ||||||
|  |             .onAppear(perform: { | ||||||
|  |                 viewModel.copyFromMonster(monster: monster) | ||||||
|  |             }) | ||||||
|             .toolbar(content: { |             .toolbar(content: { | ||||||
|                 ToolbarItem(placement: .primaryAction) { |                 ToolbarItem(placement: .primaryAction) { | ||||||
|                     NavigationLink("Edit", destination: EditMonster(monster: monster)) |                     NavigationLink("Edit", destination: EditMonster(monster: monster)) | ||||||
| @@ -307,7 +322,7 @@ struct MonsterDetail: View { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| struct MonsterDetail_Previews: PreviewProvider { | struct MonsterDetailWrapper_Previews: PreviewProvider { | ||||||
|     static var previews: some View { |     static var previews: some View { | ||||||
|         let context = PersistenceController.preview.container.viewContext |         let context = PersistenceController.preview.container.viewContext | ||||||
|         let monster = Monster.init(context: context) |         let monster = Monster.init(context: context) | ||||||
| @@ -319,7 +334,7 @@ struct MonsterDetail_Previews: PreviewProvider { | |||||||
|         monster.hitDice = 6 |         monster.hitDice = 6 | ||||||
|         monster.hasCustomHP = true |         monster.hasCustomHP = true | ||||||
|         monster.customHP = "12 (1d10)+2" |         monster.customHP = "12 (1d10)+2" | ||||||
|         monster.baseSpeed = 5 |         monster.walkSpeed = 5 | ||||||
|         monster.burrowSpeed = 10 |         monster.burrowSpeed = 10 | ||||||
|         monster.climbSpeed = 15 |         monster.climbSpeed = 15 | ||||||
|         monster.flySpeed = 20 |         monster.flySpeed = 20 | ||||||
| @@ -352,10 +367,10 @@ struct MonsterDetail_Previews: PreviewProvider { | |||||||
|         ] |         ] | ||||||
|          |          | ||||||
|         return Group { |         return Group { | ||||||
|             MonsterDetail(monster: monster) |             MonsterDetailWrapper(monster: monster) | ||||||
|                 .environment(\.managedObjectContext, context) |                 .environment(\.managedObjectContext, context) | ||||||
|                 .previewDevice("iPod touch (7th generation)") |                 .previewDevice("iPod touch (7th generation)") | ||||||
|             MonsterDetail(monster: monster) |             MonsterDetailWrapper(monster: monster) | ||||||
|                 .environment(\.managedObjectContext, context) |                 .environment(\.managedObjectContext, context) | ||||||
|                 .previewDevice("iPad Pro (11-inch) (2nd generation)") |                 .previewDevice("iPad Pro (11-inch) (2nd generation)") | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -59,7 +59,7 @@ struct Search: View { | |||||||
|                              |                              | ||||||
|                             return false |                             return false | ||||||
|                         })) { monster in |                         })) { monster in | ||||||
|                     NavigationLink(destination: MonsterDetail(monster: monster)) { |                     NavigationLink(destination: MonsterDetailWrapper(monster: monster)) { | ||||||
|                         Text(monster.name ?? "") |                         Text(monster.name ?? "") | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 GitHub
						GitHub