diff --git a/iOS/.gitignore b/iOS/.gitignore new file mode 100644 index 0000000..898055c --- /dev/null +++ b/iOS/.gitignore @@ -0,0 +1,2 @@ +xcuserdata +.zshrc diff --git a/iOS/LICENSE b/iOS/LICENSE new file mode 100644 index 0000000..d6a1af8 --- /dev/null +++ b/iOS/LICENSE @@ -0,0 +1,24 @@ +MIT License +----------- + +Copyright (c) 2020-2021 Tom Hicks +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/iOS/MonsterCards.xcodeproj/project.pbxproj b/iOS/MonsterCards.xcodeproj/project.pbxproj new file mode 100644 index 0000000..78eea58 --- /dev/null +++ b/iOS/MonsterCards.xcodeproj/project.pbxproj @@ -0,0 +1,1122 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 52; + objects = { + +/* Begin PBXBuildFile section */ + E20209D325D8DD9600EFE733 /* Skill+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20209D225D8DD9600EFE733 /* Skill+CoreDataClass.swift */; }; + E20209E825D8DEC100EFE733 /* AbilityScore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20209E725D8DEC100EFE733 /* AbilityScore.swift */; }; + E20209F425D8E04300EFE733 /* ProficiencyType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20209F225D8E04300EFE733 /* ProficiencyType.swift */; }; + E20209F525D8E04300EFE733 /* AdvantageType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20209F325D8E04300EFE733 /* AdvantageType.swift */; }; + E20209FB25D8E19100EFE733 /* SkillViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20209F925D8E19100EFE733 /* SkillViewModel.swift */; }; + E20209FC25D8E19100EFE733 /* MonsterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20209FA25D8E19100EFE733 /* MonsterViewModel.swift */; }; + E20CEFEC261FEA2100B55D72 /* MonsterDetailWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20CEFEB261FEA2100B55D72 /* MonsterDetailWrapper.swift */; }; + E20CEFFA261FEBBA00B55D72 /* MonsterDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E257100825B1B2470055B23B /* MonsterDetailView.swift */; }; + E20CF000261FEBD300B55D72 /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = E20CEFFF261FEBD300B55D72 /* MarkdownUI */; }; + E20CF00E261FF38600B55D72 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E20CF00D261FF38600B55D72 /* CloudKit.framework */; }; + E210B83A25B42D980083EAC5 /* MCProficiencyPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = E210B83925B42D980083EAC5 /* MCProficiencyPicker.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 */; }; + E216B799260C2DF200FB205F /* EditLanguages.swift in Sources */ = {isa = PBXBuildFile; fileRef = E216B798260C2DF200FB205F /* EditLanguages.swift */; }; + E216B79E260C396F00FB205F /* EditLanguage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E216B79D260C396F00FB205F /* EditLanguage.swift */; }; + E216B7B7260C5A9800FB205F /* ChallengeRatingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E216B7B6260C5A9800FB205F /* ChallengeRatingViewModel.swift */; }; + E216B7BC260C691400FB205F /* EditChallengeRating.swift in Sources */ = {isa = PBXBuildFile; fileRef = E216B7BB260C691400FB205F /* EditChallengeRating.swift */; }; + E216B7C1260C6B6000FB205F /* MCChallengeRatingPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = E216B7C0260C6B6000FB205F /* MCChallengeRatingPicker.swift */; }; + E216E465261FDA2E00FD9262 /* MonsterDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = E216E464261FDA2E00FD9262 /* MonsterDocument.swift */; }; + E216E46D261FDE5600FD9262 /* MonsterViewModel+CoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = E216E46C261FDE5600FD9262 /* MonsterViewModel+CoreData.swift */; }; + E216E472261FDF3200FD9262 /* SkillViewModel+CoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = E216E471261FDF3200FD9262 /* SkillViewModel+CoreData.swift */; }; + E216E47E261FE76F00FD9262 /* QuickLook.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E216E47D261FE76F00FD9262 /* QuickLook.framework */; }; + E216E481261FE76F00FD9262 /* PreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E216E480261FE76F00FD9262 /* PreviewViewController.swift */; }; + E216E484261FE76F00FD9262 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E216E482261FE76F00FD9262 /* MainInterface.storyboard */; }; + E216E488261FE76F00FD9262 /* MonsterPreview.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = E216E47B261FE76F00FD9262 /* MonsterPreview.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + E216E491261FE7B200FD9262 /* SavingThrowDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = E219247F261989F700C84E12 /* SavingThrowDTO.swift */; }; + E216E492261FE7B200FD9262 /* AbilityViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E254F905260D0818009295A5 /* AbilityViewModel.swift */; }; + E216E493261FE7B200FD9262 /* DamageTypeDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = E219248E26198A6A00C84E12 /* DamageTypeDTO.swift */; }; + E216E494261FE7B200FD9262 /* SkillDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = E219248426198A1200C84E12 /* SkillDTO.swift */; }; + E216E495261FE7B200FD9262 /* SkillViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20209F925D8E19100EFE733 /* SkillViewModel.swift */; }; + E216E496261FE7B200FD9262 /* ArmorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2CB0DC926086E8300142591 /* ArmorType.swift */; }; + E216E497261FE7B200FD9262 /* SizeType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2CB0DC426086E5F00142591 /* SizeType.swift */; }; + E216E498261FE7B200FD9262 /* LanguageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E216B790260C1FE800FB205F /* LanguageViewModel.swift */; }; + E216E499261FE7B200FD9262 /* StringViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2CB0DE0260887ED00142591 /* StringViewModel.swift */; }; + E216E49A261FE7B200FD9262 /* ProficiencyType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20209F225D8E04300EFE733 /* ProficiencyType.swift */; }; + E216E49B261FE7B200FD9262 /* ChallengeRating.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2CB0DBF26086E3C00142591 /* ChallengeRating.swift */; }; + E216E49C261FE7B200FD9262 /* MonsterDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = E216E464261FDA2E00FD9262 /* MonsterDocument.swift */; }; + E216E49D261FE7B200FD9262 /* ChallengeRatingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E216B7B6260C5A9800FB205F /* ChallengeRatingViewModel.swift */; }; + E216E49E261FE7B200FD9262 /* MonsterDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = E219247A261989B400C84E12 /* MonsterDTO.swift */; }; + E216E49F261FE7B200FD9262 /* AdvantageType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20209F325D8E04300EFE733 /* AdvantageType.swift */; }; + E216E4A0261FE7B200FD9262 /* TraitDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = E219248926198A5400C84E12 /* TraitDTO.swift */; }; + E216E4A1261FE7B200FD9262 /* AbilityScore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20209E725D8DEC100EFE733 /* AbilityScore.swift */; }; + E216E4A2261FE7B200FD9262 /* LanguageDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = E219249326198A8200C84E12 /* LanguageDTO.swift */; }; + E216E4A3261FE7B200FD9262 /* MonsterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20209FA25D8E19100EFE733 /* MonsterViewModel.swift */; }; + E216E4A8261FE7D100FD9262 /* StringHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2CB0DD62608720000142591 /* StringHelper.swift */; }; + E216E4A9261FE7D100FD9262 /* MonsterImportHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E219249826198E0D00C84E12 /* MonsterImportHelper.swift */; }; + E216E4AA261FE7D100FD9262 /* Color+Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D473FC25B532C900CB36D7 /* Color+Hex.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 */; }; + E24ACE562607EE94009BF703 /* EditArmor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24ACE552607EE94009BF703 /* EditArmor.swift */; }; + E24ACE5B2607F0F2009BF703 /* EditSpeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24ACE5A2607F0F2009BF703 /* EditSpeed.swift */; }; + E24ACE602607F45E009BF703 /* EditAbilityScores.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24ACE5F2607F45E009BF703 /* EditAbilityScores.swift */; }; + E24ACE652607F55D009BF703 /* EditSavingThrows.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24ACE642607F55D009BF703 /* EditSavingThrows.swift */; }; + E24ACE6A2607F715009BF703 /* EditSkills.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24ACE692607F715009BF703 /* EditSkills.swift */; }; + E254F901260D07C1009295A5 /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = E254F900260D07C1009295A5 /* MarkdownUI */; }; + E254F906260D0818009295A5 /* AbilityViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E254F905260D0818009295A5 /* AbilityViewModel.swift */; }; + E254F90E260D19A0009295A5 /* EditTraits.swift in Sources */ = {isa = PBXBuildFile; fileRef = E254F90D260D19A0009295A5 /* EditTraits.swift */; }; + E254F913260D1F6D009295A5 /* EditTrait.swift in Sources */ = {isa = PBXBuildFile; fileRef = E254F912260D1F6D009295A5 /* EditTrait.swift */; }; + E2570FB925B1AC520055B23B /* MonsterCardsApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2570FB825B1AC520055B23B /* MonsterCardsApp.swift */; }; + E2570FBB25B1AC520055B23B /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2570FBA25B1AC520055B23B /* ContentView.swift */; }; + E2570FBD25B1AC550055B23B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E2570FBC25B1AC550055B23B /* Assets.xcassets */; }; + E2570FC025B1AC550055B23B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E2570FBF25B1AC550055B23B /* Preview Assets.xcassets */; }; + E2570FC225B1AC550055B23B /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2570FC125B1AC550055B23B /* Persistence.swift */; }; + E2570FC525B1AC550055B23B /* MonsterCards.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = E2570FC325B1AC550055B23B /* MonsterCards.xcdatamodeld */; }; + E2570FD025B1AC550055B23B /* MonsterCardsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2570FCF25B1AC550055B23B /* MonsterCardsTests.swift */; }; + E2570FDB25B1AC550055B23B /* MonsterCardsUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2570FDA25B1AC550055B23B /* MonsterCardsUITests.swift */; }; + E2570FF025B1ADC10055B23B /* Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2570FEF25B1ADC10055B23B /* Search.swift */; }; + E2570FF525B1ADEB0055B23B /* Dashboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2570FF425B1ADEB0055B23B /* Dashboard.swift */; }; + E2570FFA25B1AE020055B23B /* Collections.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2570FF925B1AE020055B23B /* Collections.swift */; }; + E2570FFF25B1AE180055B23B /* Library.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2570FFE25B1AE180055B23B /* Library.swift */; }; + E257100425B1AF4A0055B23B /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E257100325B1AF4A0055B23B /* SearchBar.swift */; }; + E257100925B1B2480055B23B /* MonsterDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E257100825B1B2470055B23B /* MonsterDetailView.swift */; }; + E26CDA2B25CFB38E00E3F50D /* MCArmorTypePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = E26CDA2A25CFB38E00E3F50D /* MCArmorTypePicker.swift */; }; + E2B5285925B3028700AAA69E /* EditMonster.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B5285825B3028700AAA69E /* EditMonster.swift */; }; + E2BD702C25B3A8D70058ED69 /* MCTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2BD702B25B3A8D70058ED69 /* MCTextField.swift */; }; + E2BD703125B3BBB90058ED69 /* MCStepperField.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2BD703025B3BBB90058ED69 /* MCStepperField.swift */; }; + E2CB0DB326080C0500142591 /* EditSkill.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2CB0DB226080C0500142591 /* EditSkill.swift */; }; + E2CB0DB826081A2F00142591 /* MCAbilityScorePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2CB0DB726081A2F00142591 /* MCAbilityScorePicker.swift */; }; + E2CB0DC026086E3C00142591 /* ChallengeRating.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2CB0DBF26086E3C00142591 /* ChallengeRating.swift */; }; + E2CB0DC526086E5F00142591 /* SizeType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2CB0DC426086E5F00142591 /* SizeType.swift */; }; + E2CB0DCA26086E8300142591 /* ArmorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2CB0DC926086E8300142591 /* ArmorType.swift */; }; + E2CB0DD72608720000142591 /* StringHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2CB0DD62608720000142591 /* StringHelper.swift */; }; + E2CB0DE1260887ED00142591 /* StringViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2CB0DE0260887ED00142591 /* StringViewModel.swift */; }; + E2CB0DE626088CE400142591 /* EditStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2CB0DE526088CE400142591 /* EditStrings.swift */; }; + E2D473FD25B532C900CB36D7 /* Color+Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D473FC25B532C900CB36D7 /* Color+Hex.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + E216E486261FE76F00FD9262 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = E2570FAD25B1AC520055B23B /* Project object */; + proxyType = 1; + remoteGlobalIDString = E216E47A261FE76F00FD9262; + remoteInfo = MonsterPreview; + }; + E2570FCC25B1AC550055B23B /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = E2570FAD25B1AC520055B23B /* Project object */; + proxyType = 1; + remoteGlobalIDString = E2570FB425B1AC520055B23B; + remoteInfo = MonsterCards; + }; + E2570FD725B1AC550055B23B /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = E2570FAD25B1AC520055B23B /* Project object */; + proxyType = 1; + remoteGlobalIDString = E2570FB425B1AC520055B23B; + remoteInfo = MonsterCards; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + E216E48C261FE76F00FD9262 /* Embed App Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + E216E488261FE76F00FD9262 /* MonsterPreview.appex in Embed App Extensions */, + ); + name = "Embed App Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + E20209D225D8DD9600EFE733 /* Skill+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Skill+CoreDataClass.swift"; sourceTree = ""; }; + E20209E725D8DEC100EFE733 /* AbilityScore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbilityScore.swift; sourceTree = ""; }; + E20209F225D8E04300EFE733 /* ProficiencyType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProficiencyType.swift; sourceTree = ""; }; + E20209F325D8E04300EFE733 /* AdvantageType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdvantageType.swift; sourceTree = ""; }; + E20209F925D8E19100EFE733 /* SkillViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SkillViewModel.swift; sourceTree = ""; }; + E20209FA25D8E19100EFE733 /* MonsterViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MonsterViewModel.swift; sourceTree = ""; }; + E20CEFEB261FEA2100B55D72 /* MonsterDetailWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonsterDetailWrapper.swift; sourceTree = ""; }; + E20CF008261FF37700B55D72 /* MonsterCards.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MonsterCards.entitlements; sourceTree = ""; }; + E20CF00D261FF38600B55D72 /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; }; + E210B83925B42D980083EAC5 /* MCProficiencyPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MCProficiencyPicker.swift; sourceTree = ""; }; + E210B83E25B42DAB0083EAC5 /* MCAdvantagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MCAdvantagePicker.swift; sourceTree = ""; }; + E21661D02616E9A800117782 /* ImportMonster.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImportMonster.swift; sourceTree = ""; }; + E216B790260C1FE800FB205F /* LanguageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageViewModel.swift; sourceTree = ""; }; + E216B798260C2DF200FB205F /* EditLanguages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditLanguages.swift; sourceTree = ""; }; + E216B79D260C396F00FB205F /* EditLanguage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditLanguage.swift; sourceTree = ""; }; + E216B7B6260C5A9800FB205F /* ChallengeRatingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChallengeRatingViewModel.swift; sourceTree = ""; }; + E216B7BB260C691400FB205F /* EditChallengeRating.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditChallengeRating.swift; sourceTree = ""; }; + E216B7C0260C6B6000FB205F /* MCChallengeRatingPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MCChallengeRatingPicker.swift; sourceTree = ""; }; + E216E464261FDA2E00FD9262 /* MonsterDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonsterDocument.swift; sourceTree = ""; }; + E216E46C261FDE5600FD9262 /* MonsterViewModel+CoreData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MonsterViewModel+CoreData.swift"; sourceTree = ""; }; + E216E471261FDF3200FD9262 /* SkillViewModel+CoreData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SkillViewModel+CoreData.swift"; sourceTree = ""; }; + E216E47B261FE76F00FD9262 /* MonsterPreview.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = MonsterPreview.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + E216E47D261FE76F00FD9262 /* QuickLook.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuickLook.framework; path = System/Library/Frameworks/QuickLook.framework; sourceTree = SDKROOT; }; + E216E480261FE76F00FD9262 /* PreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewViewController.swift; sourceTree = ""; }; + E216E483261FE76F00FD9262 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; + E216E485261FE76F00FD9262 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + E2182E6225B22F8A00DFAEF8 /* Monster+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Monster+CoreDataClass.swift"; sourceTree = ""; }; + E219247A261989B400C84E12 /* MonsterDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonsterDTO.swift; sourceTree = ""; }; + E219247F261989F700C84E12 /* SavingThrowDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavingThrowDTO.swift; sourceTree = ""; }; + E219248426198A1200C84E12 /* SkillDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SkillDTO.swift; sourceTree = ""; }; + E219248926198A5400C84E12 /* TraitDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraitDTO.swift; sourceTree = ""; }; + E219248E26198A6A00C84E12 /* DamageTypeDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamageTypeDTO.swift; sourceTree = ""; }; + E219249326198A8200C84E12 /* LanguageDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageDTO.swift; sourceTree = ""; }; + E219249826198E0D00C84E12 /* MonsterImportHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonsterImportHelper.swift; sourceTree = ""; }; + E24ACE4F2607326E009BF703 /* EditBasicInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditBasicInfo.swift; sourceTree = ""; }; + E24ACE552607EE94009BF703 /* EditArmor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditArmor.swift; sourceTree = ""; }; + E24ACE5A2607F0F2009BF703 /* EditSpeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditSpeed.swift; sourceTree = ""; }; + E24ACE5F2607F45E009BF703 /* EditAbilityScores.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditAbilityScores.swift; sourceTree = ""; }; + E24ACE642607F55D009BF703 /* EditSavingThrows.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditSavingThrows.swift; sourceTree = ""; }; + E24ACE692607F715009BF703 /* EditSkills.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditSkills.swift; sourceTree = ""; }; + E254F905260D0818009295A5 /* AbilityViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbilityViewModel.swift; sourceTree = ""; }; + E254F90D260D19A0009295A5 /* EditTraits.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditTraits.swift; sourceTree = ""; }; + E254F912260D1F6D009295A5 /* EditTrait.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditTrait.swift; sourceTree = ""; }; + E2570FB525B1AC520055B23B /* MonsterCards.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MonsterCards.app; sourceTree = BUILT_PRODUCTS_DIR; }; + E2570FB825B1AC520055B23B /* MonsterCardsApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonsterCardsApp.swift; sourceTree = ""; }; + E2570FBA25B1AC520055B23B /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + E2570FBC25B1AC550055B23B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + E2570FBF25B1AC550055B23B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + E2570FC125B1AC550055B23B /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; + E2570FC425B1AC550055B23B /* MonsterCards.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MonsterCards.xcdatamodel; sourceTree = ""; }; + E2570FC625B1AC550055B23B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + E2570FCB25B1AC550055B23B /* MonsterCardsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MonsterCardsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + E2570FCF25B1AC550055B23B /* MonsterCardsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonsterCardsTests.swift; sourceTree = ""; }; + E2570FD125B1AC550055B23B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + E2570FD625B1AC550055B23B /* MonsterCardsUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MonsterCardsUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + E2570FDA25B1AC550055B23B /* MonsterCardsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonsterCardsUITests.swift; sourceTree = ""; }; + E2570FDC25B1AC550055B23B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + E2570FEF25B1ADC10055B23B /* Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Search.swift; sourceTree = ""; }; + E2570FF425B1ADEB0055B23B /* Dashboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dashboard.swift; sourceTree = ""; }; + E2570FF925B1AE020055B23B /* Collections.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Collections.swift; sourceTree = ""; }; + E2570FFE25B1AE180055B23B /* Library.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Library.swift; sourceTree = ""; }; + E257100325B1AF4A0055B23B /* SearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBar.swift; sourceTree = ""; }; + E257100825B1B2470055B23B /* MonsterDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonsterDetailView.swift; sourceTree = ""; }; + E26CDA2A25CFB38E00E3F50D /* MCArmorTypePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MCArmorTypePicker.swift; sourceTree = ""; }; + E2B5285825B3028700AAA69E /* EditMonster.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditMonster.swift; sourceTree = ""; }; + E2BD702B25B3A8D70058ED69 /* MCTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MCTextField.swift; sourceTree = ""; }; + E2BD703025B3BBB90058ED69 /* MCStepperField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MCStepperField.swift; sourceTree = ""; }; + E2CB0DB226080C0500142591 /* EditSkill.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditSkill.swift; sourceTree = ""; }; + E2CB0DB726081A2F00142591 /* MCAbilityScorePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MCAbilityScorePicker.swift; sourceTree = ""; }; + E2CB0DBF26086E3C00142591 /* ChallengeRating.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChallengeRating.swift; sourceTree = ""; }; + E2CB0DC426086E5F00142591 /* SizeType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SizeType.swift; sourceTree = ""; }; + E2CB0DC926086E8300142591 /* ArmorType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArmorType.swift; sourceTree = ""; }; + E2CB0DD62608720000142591 /* StringHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringHelper.swift; sourceTree = ""; }; + E2CB0DE0260887ED00142591 /* StringViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringViewModel.swift; sourceTree = ""; }; + E2CB0DE526088CE400142591 /* EditStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditStrings.swift; sourceTree = ""; }; + E2D473FC25B532C900CB36D7 /* Color+Hex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Hex.swift"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + E216E478261FE76F00FD9262 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E216E47E261FE76F00FD9262 /* QuickLook.framework in Frameworks */, + E20CF000261FEBD300B55D72 /* MarkdownUI in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E2570FB225B1AC520055B23B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E254F901260D07C1009295A5 /* MarkdownUI in Frameworks */, + E20CF00E261FF38600B55D72 /* CloudKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E2570FC825B1AC550055B23B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E2570FD325B1AC550055B23B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + E20209E625D8DEB600EFE733 /* Enums */ = { + isa = PBXGroup; + children = ( + E20209E725D8DEC100EFE733 /* AbilityScore.swift */, + E20209F325D8E04300EFE733 /* AdvantageType.swift */, + E2CB0DC926086E8300142591 /* ArmorType.swift */, + E2CB0DBF26086E3C00142591 /* ChallengeRating.swift */, + E20209F225D8E04300EFE733 /* ProficiencyType.swift */, + E2CB0DC426086E5F00142591 /* SizeType.swift */, + ); + path = Enums; + sourceTree = ""; + }; + E216E47C261FE76F00FD9262 /* Frameworks */ = { + isa = PBXGroup; + children = ( + E20CF00D261FF38600B55D72 /* CloudKit.framework */, + E216E47D261FE76F00FD9262 /* QuickLook.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + E216E47F261FE76F00FD9262 /* MonsterPreview */ = { + isa = PBXGroup; + children = ( + E216E480261FE76F00FD9262 /* PreviewViewController.swift */, + E216E482261FE76F00FD9262 /* MainInterface.storyboard */, + E216E485261FE76F00FD9262 /* Info.plist */, + ); + path = MonsterPreview; + sourceTree = ""; + }; + E2570FAC25B1AC520055B23B = { + isa = PBXGroup; + children = ( + E2570FB725B1AC520055B23B /* MonsterCards */, + E2570FCE25B1AC550055B23B /* MonsterCardsTests */, + E2570FD925B1AC550055B23B /* MonsterCardsUITests */, + E216E47F261FE76F00FD9262 /* MonsterPreview */, + E216E47C261FE76F00FD9262 /* Frameworks */, + E2570FB625B1AC520055B23B /* Products */, + ); + sourceTree = ""; + }; + E2570FB625B1AC520055B23B /* Products */ = { + isa = PBXGroup; + children = ( + E2570FB525B1AC520055B23B /* MonsterCards.app */, + E2570FCB25B1AC550055B23B /* MonsterCardsTests.xctest */, + E2570FD625B1AC550055B23B /* MonsterCardsUITests.xctest */, + E216E47B261FE76F00FD9262 /* MonsterPreview.appex */, + ); + name = Products; + sourceTree = ""; + }; + E2570FB725B1AC520055B23B /* MonsterCards */ = { + isa = PBXGroup; + children = ( + E20CF008261FF37700B55D72 /* MonsterCards.entitlements */, + E2570FBC25B1AC550055B23B /* Assets.xcassets */, + E2D473FB25B5328800CB36D7 /* Helpers */, + E2570FC625B1AC550055B23B /* Info.plist */, + E257101225B1B2790055B23B /* Models */, + E2570FC325B1AC550055B23B /* MonsterCards.xcdatamodeld */, + E2570FB825B1AC520055B23B /* MonsterCardsApp.swift */, + E2570FC125B1AC550055B23B /* Persistence.swift */, + E2570FBE25B1AC550055B23B /* Preview Content */, + E2570FEB25B1ADA90055B23B /* Views */, + ); + path = MonsterCards; + sourceTree = ""; + }; + E2570FBE25B1AC550055B23B /* Preview Content */ = { + isa = PBXGroup; + children = ( + E2570FBF25B1AC550055B23B /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + E2570FCE25B1AC550055B23B /* MonsterCardsTests */ = { + isa = PBXGroup; + children = ( + E2570FCF25B1AC550055B23B /* MonsterCardsTests.swift */, + E2570FD125B1AC550055B23B /* Info.plist */, + ); + path = MonsterCardsTests; + sourceTree = ""; + }; + E2570FD925B1AC550055B23B /* MonsterCardsUITests */ = { + isa = PBXGroup; + children = ( + E2570FDA25B1AC550055B23B /* MonsterCardsUITests.swift */, + E2570FDC25B1AC550055B23B /* Info.plist */, + ); + path = MonsterCardsUITests; + sourceTree = ""; + }; + E2570FEB25B1ADA90055B23B /* Views */ = { + isa = PBXGroup; + children = ( + E2570FF925B1AE020055B23B /* Collections.swift */, + E2570FBA25B1AC520055B23B /* ContentView.swift */, + E2570FF425B1ADEB0055B23B /* Dashboard.swift */, + E24ACE5F2607F45E009BF703 /* EditAbilityScores.swift */, + E24ACE552607EE94009BF703 /* EditArmor.swift */, + E24ACE4F2607326E009BF703 /* EditBasicInfo.swift */, + E216B7BB260C691400FB205F /* EditChallengeRating.swift */, + E216B79D260C396F00FB205F /* EditLanguage.swift */, + E216B798260C2DF200FB205F /* EditLanguages.swift */, + E2B5285825B3028700AAA69E /* EditMonster.swift */, + E24ACE642607F55D009BF703 /* EditSavingThrows.swift */, + E2CB0DB226080C0500142591 /* EditSkill.swift */, + E24ACE692607F715009BF703 /* EditSkills.swift */, + E24ACE5A2607F0F2009BF703 /* EditSpeed.swift */, + E2CB0DE526088CE400142591 /* EditStrings.swift */, + E254F912260D1F6D009295A5 /* EditTrait.swift */, + E254F90D260D19A0009295A5 /* EditTraits.swift */, + E21661D02616E9A800117782 /* ImportMonster.swift */, + E2570FFE25B1AE180055B23B /* Library.swift */, + E2CB0DB726081A2F00142591 /* MCAbilityScorePicker.swift */, + E210B83E25B42DAB0083EAC5 /* MCAdvantagePicker.swift */, + E26CDA2A25CFB38E00E3F50D /* MCArmorTypePicker.swift */, + E216B7C0260C6B6000FB205F /* MCChallengeRatingPicker.swift */, + E210B83925B42D980083EAC5 /* MCProficiencyPicker.swift */, + E2BD703025B3BBB90058ED69 /* MCStepperField.swift */, + E2BD702B25B3A8D70058ED69 /* MCTextField.swift */, + E257100825B1B2470055B23B /* MonsterDetailView.swift */, + E20CEFEB261FEA2100B55D72 /* MonsterDetailWrapper.swift */, + E2570FEF25B1ADC10055B23B /* Search.swift */, + E257100325B1AF4A0055B23B /* SearchBar.swift */, + ); + path = Views; + sourceTree = ""; + }; + E257101225B1B2790055B23B /* Models */ = { + isa = PBXGroup; + children = ( + E254F905260D0818009295A5 /* AbilityViewModel.swift */, + E216B7B6260C5A9800FB205F /* ChallengeRatingViewModel.swift */, + E219248E26198A6A00C84E12 /* DamageTypeDTO.swift */, + E20209E625D8DEB600EFE733 /* Enums */, + E219249326198A8200C84E12 /* LanguageDTO.swift */, + E216B790260C1FE800FB205F /* LanguageViewModel.swift */, + E2182E6225B22F8A00DFAEF8 /* Monster+CoreDataClass.swift */, + E216E464261FDA2E00FD9262 /* MonsterDocument.swift */, + E219247A261989B400C84E12 /* MonsterDTO.swift */, + E20209FA25D8E19100EFE733 /* MonsterViewModel.swift */, + E216E46C261FDE5600FD9262 /* MonsterViewModel+CoreData.swift */, + E219247F261989F700C84E12 /* SavingThrowDTO.swift */, + E20209D225D8DD9600EFE733 /* Skill+CoreDataClass.swift */, + E219248426198A1200C84E12 /* SkillDTO.swift */, + E20209F925D8E19100EFE733 /* SkillViewModel.swift */, + E216E471261FDF3200FD9262 /* SkillViewModel+CoreData.swift */, + E2CB0DE0260887ED00142591 /* StringViewModel.swift */, + E219248926198A5400C84E12 /* TraitDTO.swift */, + ); + path = Models; + sourceTree = ""; + }; + E2D473FB25B5328800CB36D7 /* Helpers */ = { + isa = PBXGroup; + children = ( + E2D473FC25B532C900CB36D7 /* Color+Hex.swift */, + E219249826198E0D00C84E12 /* MonsterImportHelper.swift */, + E2CB0DD62608720000142591 /* StringHelper.swift */, + ); + path = Helpers; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + E216E47A261FE76F00FD9262 /* MonsterPreview */ = { + isa = PBXNativeTarget; + buildConfigurationList = E216E489261FE76F00FD9262 /* Build configuration list for PBXNativeTarget "MonsterPreview" */; + buildPhases = ( + E216E477261FE76F00FD9262 /* Sources */, + E216E478261FE76F00FD9262 /* Frameworks */, + E216E479261FE76F00FD9262 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = MonsterPreview; + packageProductDependencies = ( + E20CEFFF261FEBD300B55D72 /* MarkdownUI */, + ); + productName = MonsterPreview; + productReference = E216E47B261FE76F00FD9262 /* MonsterPreview.appex */; + productType = "com.apple.product-type.app-extension"; + }; + E2570FB425B1AC520055B23B /* MonsterCards */ = { + isa = PBXNativeTarget; + buildConfigurationList = E2570FDF25B1AC550055B23B /* Build configuration list for PBXNativeTarget "MonsterCards" */; + buildPhases = ( + E2570FB125B1AC520055B23B /* Sources */, + E2570FB225B1AC520055B23B /* Frameworks */, + E2570FB325B1AC520055B23B /* Resources */, + E216E48C261FE76F00FD9262 /* Embed App Extensions */, + ); + buildRules = ( + ); + dependencies = ( + E216E487261FE76F00FD9262 /* PBXTargetDependency */, + ); + name = MonsterCards; + packageProductDependencies = ( + E254F900260D07C1009295A5 /* MarkdownUI */, + ); + productName = MonsterCards; + productReference = E2570FB525B1AC520055B23B /* MonsterCards.app */; + productType = "com.apple.product-type.application"; + }; + E2570FCA25B1AC550055B23B /* MonsterCardsTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = E2570FE225B1AC550055B23B /* Build configuration list for PBXNativeTarget "MonsterCardsTests" */; + buildPhases = ( + E2570FC725B1AC550055B23B /* Sources */, + E2570FC825B1AC550055B23B /* Frameworks */, + E2570FC925B1AC550055B23B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + E2570FCD25B1AC550055B23B /* PBXTargetDependency */, + ); + name = MonsterCardsTests; + productName = MonsterCardsTests; + productReference = E2570FCB25B1AC550055B23B /* MonsterCardsTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + E2570FD525B1AC550055B23B /* MonsterCardsUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = E2570FE525B1AC550055B23B /* Build configuration list for PBXNativeTarget "MonsterCardsUITests" */; + buildPhases = ( + E2570FD225B1AC550055B23B /* Sources */, + E2570FD325B1AC550055B23B /* Frameworks */, + E2570FD425B1AC550055B23B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + E2570FD825B1AC550055B23B /* PBXTargetDependency */, + ); + name = MonsterCardsUITests; + productName = MonsterCardsUITests; + productReference = E2570FD625B1AC550055B23B /* MonsterCardsUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + E2570FAD25B1AC520055B23B /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1240; + LastUpgradeCheck = 1200; + TargetAttributes = { + E216E47A261FE76F00FD9262 = { + CreatedOnToolsVersion = 12.4; + }; + E2570FB425B1AC520055B23B = { + CreatedOnToolsVersion = 12.0.1; + }; + E2570FCA25B1AC550055B23B = { + CreatedOnToolsVersion = 12.0.1; + TestTargetID = E2570FB425B1AC520055B23B; + }; + E2570FD525B1AC550055B23B = { + CreatedOnToolsVersion = 12.0.1; + TestTargetID = E2570FB425B1AC520055B23B; + }; + }; + }; + buildConfigurationList = E2570FB025B1AC520055B23B /* Build configuration list for PBXProject "MonsterCards" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = E2570FAC25B1AC520055B23B; + packageReferences = ( + E254F8FF260D07C1009295A5 /* XCRemoteSwiftPackageReference "MarkdownUI" */, + ); + productRefGroup = E2570FB625B1AC520055B23B /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + E2570FB425B1AC520055B23B /* MonsterCards */, + E2570FCA25B1AC550055B23B /* MonsterCardsTests */, + E2570FD525B1AC550055B23B /* MonsterCardsUITests */, + E216E47A261FE76F00FD9262 /* MonsterPreview */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + E216E479261FE76F00FD9262 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E216E484261FE76F00FD9262 /* MainInterface.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E2570FB325B1AC520055B23B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E2570FC025B1AC550055B23B /* Preview Assets.xcassets in Resources */, + E2570FBD25B1AC550055B23B /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E2570FC925B1AC550055B23B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E2570FD425B1AC550055B23B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + E216E477261FE76F00FD9262 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E20CEFFA261FEBBA00B55D72 /* MonsterDetailView.swift in Sources */, + E216E4AA261FE7D100FD9262 /* Color+Hex.swift in Sources */, + E216E491261FE7B200FD9262 /* SavingThrowDTO.swift in Sources */, + E216E499261FE7B200FD9262 /* StringViewModel.swift in Sources */, + E216E496261FE7B200FD9262 /* ArmorType.swift in Sources */, + E216E4A1261FE7B200FD9262 /* AbilityScore.swift in Sources */, + E216E481261FE76F00FD9262 /* PreviewViewController.swift in Sources */, + E216E493261FE7B200FD9262 /* DamageTypeDTO.swift in Sources */, + E216E4A2261FE7B200FD9262 /* LanguageDTO.swift in Sources */, + E216E498261FE7B200FD9262 /* LanguageViewModel.swift in Sources */, + E216E492261FE7B200FD9262 /* AbilityViewModel.swift in Sources */, + E216E49D261FE7B200FD9262 /* ChallengeRatingViewModel.swift in Sources */, + E216E49E261FE7B200FD9262 /* MonsterDTO.swift in Sources */, + E216E495261FE7B200FD9262 /* SkillViewModel.swift in Sources */, + E216E49B261FE7B200FD9262 /* ChallengeRating.swift in Sources */, + E216E49F261FE7B200FD9262 /* AdvantageType.swift in Sources */, + E216E4A0261FE7B200FD9262 /* TraitDTO.swift in Sources */, + E216E49C261FE7B200FD9262 /* MonsterDocument.swift in Sources */, + E216E497261FE7B200FD9262 /* SizeType.swift in Sources */, + E216E4A3261FE7B200FD9262 /* MonsterViewModel.swift in Sources */, + E216E4A8261FE7D100FD9262 /* StringHelper.swift in Sources */, + E216E494261FE7B200FD9262 /* SkillDTO.swift in Sources */, + E216E49A261FE7B200FD9262 /* ProficiencyType.swift in Sources */, + E216E4A9261FE7D100FD9262 /* MonsterImportHelper.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E2570FB125B1AC520055B23B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E20209FB25D8E19100EFE733 /* SkillViewModel.swift in Sources */, + E21661D12616E9A800117782 /* ImportMonster.swift in Sources */, + E24ACE602607F45E009BF703 /* EditAbilityScores.swift in Sources */, + E2570FC225B1AC550055B23B /* Persistence.swift in Sources */, + E216B799260C2DF200FB205F /* EditLanguages.swift in Sources */, + E2570FBB25B1AC520055B23B /* ContentView.swift in Sources */, + E24ACE502607326E009BF703 /* EditBasicInfo.swift in Sources */, + E219249426198A8200C84E12 /* LanguageDTO.swift in Sources */, + E254F90E260D19A0009295A5 /* EditTraits.swift in Sources */, + E2570FC525B1AC550055B23B /* MonsterCards.xcdatamodeld in Sources */, + E2182E6425B22F8A00DFAEF8 /* Monster+CoreDataClass.swift in Sources */, + E216B791260C1FE800FB205F /* LanguageViewModel.swift in Sources */, + E210B83A25B42D980083EAC5 /* MCProficiencyPicker.swift in Sources */, + E2570FF025B1ADC10055B23B /* Search.swift in Sources */, + E257100925B1B2480055B23B /* MonsterDetailView.swift in Sources */, + E219248526198A1200C84E12 /* SkillDTO.swift in Sources */, + E2D473FD25B532C900CB36D7 /* Color+Hex.swift in Sources */, + E2B5285925B3028700AAA69E /* EditMonster.swift in Sources */, + E219247B261989B400C84E12 /* MonsterDTO.swift in Sources */, + E2CB0DD72608720000142591 /* StringHelper.swift in Sources */, + E216E46D261FDE5600FD9262 /* MonsterViewModel+CoreData.swift in Sources */, + E216E472261FDF3200FD9262 /* SkillViewModel+CoreData.swift in Sources */, + E2570FF525B1ADEB0055B23B /* Dashboard.swift in Sources */, + E2CB0DB826081A2F00142591 /* MCAbilityScorePicker.swift in Sources */, + E219249926198E0D00C84E12 /* MonsterImportHelper.swift in Sources */, + E2CB0DC026086E3C00142591 /* ChallengeRating.swift in Sources */, + E257100425B1AF4A0055B23B /* SearchBar.swift in Sources */, + E20209F525D8E04300EFE733 /* AdvantageType.swift in Sources */, + E24ACE6A2607F715009BF703 /* EditSkills.swift in Sources */, + E20209FC25D8E19100EFE733 /* MonsterViewModel.swift in Sources */, + E2570FFF25B1AE180055B23B /* Library.swift in Sources */, + E216B79E260C396F00FB205F /* EditLanguage.swift in Sources */, + E2BD703125B3BBB90058ED69 /* MCStepperField.swift in Sources */, + E2CB0DE626088CE400142591 /* EditStrings.swift in Sources */, + E2CB0DB326080C0500142591 /* EditSkill.swift in Sources */, + E2CB0DCA26086E8300142591 /* ArmorType.swift in Sources */, + E24ACE562607EE94009BF703 /* EditArmor.swift in Sources */, + E2CB0DE1260887ED00142591 /* StringViewModel.swift in Sources */, + E219248F26198A6A00C84E12 /* DamageTypeDTO.swift in Sources */, + E20209F425D8E04300EFE733 /* ProficiencyType.swift in Sources */, + E2CB0DC526086E5F00142591 /* SizeType.swift in Sources */, + E216E465261FDA2E00FD9262 /* MonsterDocument.swift in Sources */, + E254F906260D0818009295A5 /* AbilityViewModel.swift in Sources */, + E2570FFA25B1AE020055B23B /* Collections.swift in Sources */, + E24ACE5B2607F0F2009BF703 /* EditSpeed.swift in Sources */, + E2570FB925B1AC520055B23B /* MonsterCardsApp.swift in Sources */, + E254F913260D1F6D009295A5 /* EditTrait.swift in Sources */, + E2192480261989F700C84E12 /* SavingThrowDTO.swift in Sources */, + E216B7B7260C5A9800FB205F /* ChallengeRatingViewModel.swift in Sources */, + E20209D325D8DD9600EFE733 /* Skill+CoreDataClass.swift in Sources */, + E24ACE652607F55D009BF703 /* EditSavingThrows.swift in Sources */, + E2BD702C25B3A8D70058ED69 /* MCTextField.swift in Sources */, + E216B7BC260C691400FB205F /* EditChallengeRating.swift in Sources */, + E20209E825D8DEC100EFE733 /* AbilityScore.swift in Sources */, + E219248A26198A5400C84E12 /* TraitDTO.swift in Sources */, + E210B83F25B42DAB0083EAC5 /* MCAdvantagePicker.swift in Sources */, + E20CEFEC261FEA2100B55D72 /* MonsterDetailWrapper.swift in Sources */, + E216B7C1260C6B6000FB205F /* MCChallengeRatingPicker.swift in Sources */, + E26CDA2B25CFB38E00E3F50D /* MCArmorTypePicker.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E2570FC725B1AC550055B23B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E2570FD025B1AC550055B23B /* MonsterCardsTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E2570FD225B1AC550055B23B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E2570FDB25B1AC550055B23B /* MonsterCardsUITests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + E216E487261FE76F00FD9262 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = E216E47A261FE76F00FD9262 /* MonsterPreview */; + targetProxy = E216E486261FE76F00FD9262 /* PBXContainerItemProxy */; + }; + E2570FCD25B1AC550055B23B /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = E2570FB425B1AC520055B23B /* MonsterCards */; + targetProxy = E2570FCC25B1AC550055B23B /* PBXContainerItemProxy */; + }; + E2570FD825B1AC550055B23B /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = E2570FB425B1AC520055B23B /* MonsterCards */; + targetProxy = E2570FD725B1AC550055B23B /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + E216E482261FE76F00FD9262 /* MainInterface.storyboard */ = { + isa = PBXVariantGroup; + children = ( + E216E483261FE76F00FD9262 /* Base */, + ); + name = MainInterface.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + E216E48A261FE76F00FD9262 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = J793L9LQJ2; + INFOPLIST_FILE = MonsterPreview/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 0.6; + PRODUCT_BUNDLE_IDENTIFIER = com.majinnaibu.monstercards.MonsterCards.MonsterPreview; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + E216E48B261FE76F00FD9262 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = J793L9LQJ2; + INFOPLIST_FILE = MonsterPreview/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.4; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 0.6; + PRODUCT_BUNDLE_IDENTIFIER = com.majinnaibu.monstercards.MonsterCards.MonsterPreview; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + E2570FDD25B1AC550055B23B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + E2570FDE25B1AC550055B23B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + E2570FE025B1AC550055B23B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = MonsterCards/MonsterCards.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"MonsterCards/Preview Content\""; + DEVELOPMENT_TEAM = J793L9LQJ2; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = MonsterCards/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 0.6; + PRODUCT_BUNDLE_IDENTIFIER = com.majinnaibu.monstercards.MonsterCards; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + E2570FE125B1AC550055B23B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = MonsterCards/MonsterCards.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"MonsterCards/Preview Content\""; + DEVELOPMENT_TEAM = J793L9LQJ2; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = MonsterCards/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 0.6; + PRODUCT_BUNDLE_IDENTIFIER = com.majinnaibu.monstercards.MonsterCards; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + E2570FE325B1AC550055B23B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = J793L9LQJ2; + INFOPLIST_FILE = MonsterCardsTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.majinnaibu.monstercards.MonsterCardsTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MonsterCards.app/MonsterCards"; + }; + name = Debug; + }; + E2570FE425B1AC550055B23B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = J793L9LQJ2; + INFOPLIST_FILE = MonsterCardsTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.majinnaibu.monstercards.MonsterCardsTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MonsterCards.app/MonsterCards"; + }; + name = Release; + }; + E2570FE625B1AC550055B23B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = J793L9LQJ2; + INFOPLIST_FILE = MonsterCardsUITests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.majinnaibu.monstercards.MonsterCardsUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = MonsterCards; + }; + name = Debug; + }; + E2570FE725B1AC550055B23B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = J793L9LQJ2; + INFOPLIST_FILE = MonsterCardsUITests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.majinnaibu.monstercards.MonsterCardsUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = MonsterCards; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + E216E489261FE76F00FD9262 /* Build configuration list for PBXNativeTarget "MonsterPreview" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E216E48A261FE76F00FD9262 /* Debug */, + E216E48B261FE76F00FD9262 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + E2570FB025B1AC520055B23B /* Build configuration list for PBXProject "MonsterCards" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E2570FDD25B1AC550055B23B /* Debug */, + E2570FDE25B1AC550055B23B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + E2570FDF25B1AC550055B23B /* Build configuration list for PBXNativeTarget "MonsterCards" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E2570FE025B1AC550055B23B /* Debug */, + E2570FE125B1AC550055B23B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + E2570FE225B1AC550055B23B /* Build configuration list for PBXNativeTarget "MonsterCardsTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E2570FE325B1AC550055B23B /* Debug */, + E2570FE425B1AC550055B23B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + E2570FE525B1AC550055B23B /* Build configuration list for PBXNativeTarget "MonsterCardsUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E2570FE625B1AC550055B23B /* Debug */, + E2570FE725B1AC550055B23B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + E254F8FF260D07C1009295A5 /* XCRemoteSwiftPackageReference "MarkdownUI" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/gonzalezreal/MarkdownUI"; + requirement = { + kind = upToNextMinorVersion; + minimumVersion = 0.5.1; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + E20CEFFF261FEBD300B55D72 /* MarkdownUI */ = { + isa = XCSwiftPackageProductDependency; + package = E254F8FF260D07C1009295A5 /* XCRemoteSwiftPackageReference "MarkdownUI" */; + productName = MarkdownUI; + }; + E254F900260D07C1009295A5 /* MarkdownUI */ = { + isa = XCSwiftPackageProductDependency; + package = E254F8FF260D07C1009295A5 /* XCRemoteSwiftPackageReference "MarkdownUI" */; + productName = MarkdownUI; + }; +/* End XCSwiftPackageProductDependency section */ + +/* Begin XCVersionGroup section */ + E2570FC325B1AC550055B23B /* MonsterCards.xcdatamodeld */ = { + isa = XCVersionGroup; + children = ( + E2570FC425B1AC550055B23B /* MonsterCards.xcdatamodel */, + ); + currentVersion = E2570FC425B1AC550055B23B /* MonsterCards.xcdatamodel */; + path = MonsterCards.xcdatamodeld; + sourceTree = ""; + versionGroupType = wrapper.xcdatamodel; + }; +/* End XCVersionGroup section */ + }; + rootObject = E2570FAD25B1AC520055B23B /* Project object */; +} diff --git a/iOS/MonsterCards.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/iOS/MonsterCards.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/iOS/MonsterCards.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/iOS/MonsterCards.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/iOS/MonsterCards.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/iOS/MonsterCards.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/iOS/MonsterCards.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/iOS/MonsterCards.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..4fd9a9d --- /dev/null +++ b/iOS/MonsterCards.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,70 @@ +{ + "object": { + "pins": [ + { + "package": "AttributedText", + "repositoryURL": "https://github.com/gonzalezreal/AttributedText", + "state": { + "branch": null, + "revision": "bf076de48dbb2172525486936d512e1bba062642", + "version": "0.3.0" + } + }, + { + "package": "combine-schedulers", + "repositoryURL": "https://github.com/pointfreeco/combine-schedulers", + "state": { + "branch": null, + "revision": "f1250faa1c1436ca83950ce676a4fe97a309a457", + "version": "0.4.1" + } + }, + { + "package": "MarkdownUI", + "repositoryURL": "https://github.com/gonzalezreal/MarkdownUI", + "state": { + "branch": null, + "revision": "e8931e37dcf777b4c03ca76aa09c10cf246a2ced", + "version": "0.5.1" + } + }, + { + "package": "NetworkImage", + "repositoryURL": "https://github.com/gonzalezreal/NetworkImage", + "state": { + "branch": null, + "revision": "15582b821cb097012b41b83d6219717926ec4ed6", + "version": "2.1.0" + } + }, + { + "package": "cmark", + "repositoryURL": "https://github.com/SwiftDocOrg/swift-cmark.git", + "state": { + "branch": null, + "revision": "9c8096a23f44794bde297452d87c455fc4f76d42", + "version": "0.29.0+20210102.9c8096a" + } + }, + { + "package": "SwiftCommonMark", + "repositoryURL": "https://github.com/gonzalezreal/SwiftCommonMark", + "state": { + "branch": null, + "revision": "f1575c37110a386e50da3208a04266b398bcefaa", + "version": "0.1.1" + } + }, + { + "package": "xctest-dynamic-overlay", + "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state": { + "branch": null, + "revision": "603974e3909ad4b48ba04aad7e0ceee4f077a518", + "version": "0.1.0" + } + } + ] + }, + "version": 1 +} diff --git a/iOS/MonsterCards.xcodeproj/xcshareddata/xcschemes/MonsterCards.xcscheme b/iOS/MonsterCards.xcodeproj/xcshareddata/xcschemes/MonsterCards.xcscheme new file mode 100644 index 0000000..37f0408 --- /dev/null +++ b/iOS/MonsterCards.xcodeproj/xcshareddata/xcschemes/MonsterCards.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS/MonsterCards.xcodeproj/xcshareddata/xcschemes/MonsterPreview.xcscheme b/iOS/MonsterCards.xcodeproj/xcshareddata/xcschemes/MonsterPreview.xcscheme new file mode 100644 index 0000000..d24779d --- /dev/null +++ b/iOS/MonsterCards.xcodeproj/xcshareddata/xcschemes/MonsterPreview.xcscheme @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS/MonsterCards.xcodeproj/xcuserdata/tom.xcuserdatad/xcschemes/xcschememanagement.plist b/iOS/MonsterCards.xcodeproj/xcuserdata/tom.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..186f2ae --- /dev/null +++ b/iOS/MonsterCards.xcodeproj/xcuserdata/tom.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,105 @@ + + + + + SchemeUserState + + AttributedText_iOS (Playground) 1.xcscheme + + isShown + + orderHint + 14 + + AttributedText_iOS (Playground) 2.xcscheme + + isShown + + orderHint + 15 + + AttributedText_iOS (Playground).xcscheme + + isShown + + orderHint + 13 + + AttributedText_macOS (Playground) 1.xcscheme + + isShown + + orderHint + 11 + + AttributedText_macOS (Playground) 2.xcscheme + + isShown + + orderHint + 12 + + AttributedText_macOS (Playground).xcscheme + + isShown + + orderHint + 10 + + AttributedText_tvOS (Playground) 1.xcscheme + + isShown + + orderHint + 8 + + AttributedText_tvOS (Playground) 2.xcscheme + + isShown + + orderHint + 9 + + AttributedText_tvOS (Playground).xcscheme + + isShown + + orderHint + 7 + + MonsterCards.xcscheme_^#shared#^_ + + orderHint + 0 + + MonsterPreview.xcscheme_^#shared#^_ + + orderHint + 16 + + + SuppressBuildableAutocreation + + E216E47A261FE76F00FD9262 + + primary + + + E2570FB425B1AC520055B23B + + primary + + + E2570FCA25B1AC550055B23B + + primary + + + E2570FD525B1AC550055B23B + + primary + + + + + diff --git a/iOS/MonsterCards/Assets.xcassets/AccentColor.colorset/Contents.json b/iOS/MonsterCards/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/iOS/MonsterCards/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/100.png b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/100.png new file mode 100644 index 0000000..6fc3f1d Binary files /dev/null and b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/100.png differ diff --git a/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/1024.png b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/1024.png new file mode 100644 index 0000000..d9ae53c Binary files /dev/null and b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/1024.png differ diff --git a/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/114.png b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/114.png new file mode 100644 index 0000000..9be0b02 Binary files /dev/null and b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/114.png differ diff --git a/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/120.png b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/120.png new file mode 100644 index 0000000..9015ac6 Binary files /dev/null and b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/120.png differ diff --git a/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/128.png b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/128.png new file mode 100644 index 0000000..fd29ddc Binary files /dev/null and b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/128.png differ diff --git a/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/144.png b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/144.png new file mode 100644 index 0000000..bbe1d41 Binary files /dev/null and b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/144.png differ diff --git a/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/152.png b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/152.png new file mode 100644 index 0000000..8ccc187 Binary files /dev/null and b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/152.png differ diff --git a/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/16.png b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/16.png new file mode 100644 index 0000000..87801bc Binary files /dev/null and b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/16.png differ diff --git a/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/167.png b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/167.png new file mode 100644 index 0000000..10fecda Binary files /dev/null and b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/167.png differ diff --git a/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/172.png b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/172.png new file mode 100644 index 0000000..a827132 Binary files /dev/null and b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/172.png differ diff --git a/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/180.png b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/180.png new file mode 100644 index 0000000..4f6b1af Binary files /dev/null and b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/180.png differ diff --git a/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/196.png b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/196.png new file mode 100644 index 0000000..50f25a5 Binary files /dev/null and b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/196.png differ diff --git a/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/20.png b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/20.png new file mode 100644 index 0000000..f0fcf07 Binary files /dev/null and b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/20.png differ diff --git a/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/216.png b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/216.png new file mode 100644 index 0000000..32a8205 Binary files /dev/null and b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/216.png differ diff --git a/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/256.png b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/256.png new file mode 100644 index 0000000..e51b913 Binary files /dev/null and b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/256.png differ diff --git a/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/29.png b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/29.png new file mode 100644 index 0000000..571ac5f Binary files /dev/null and b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/29.png differ diff --git a/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/32.png b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/32.png new file mode 100644 index 0000000..6fb71f6 Binary files /dev/null and b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/32.png differ diff --git a/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/40.png b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/40.png new file mode 100644 index 0000000..de23913 Binary files /dev/null and b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/40.png differ diff --git a/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/48.png b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/48.png new file mode 100644 index 0000000..9bb8ad3 Binary files /dev/null and b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/48.png differ diff --git a/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/50.png b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/50.png new file mode 100644 index 0000000..8883373 Binary files /dev/null and b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/50.png differ diff --git a/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/512.png b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/512.png new file mode 100644 index 0000000..a1cd097 Binary files /dev/null and b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/512.png differ diff --git a/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/55.png b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/55.png new file mode 100644 index 0000000..0b44ddb Binary files /dev/null and b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/55.png differ diff --git a/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/57.png b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/57.png new file mode 100644 index 0000000..616190f Binary files /dev/null and b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/57.png differ diff --git a/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/58.png b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/58.png new file mode 100644 index 0000000..45734b0 Binary files /dev/null and b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/58.png differ diff --git a/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/60.png b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/60.png new file mode 100644 index 0000000..34064eb Binary files /dev/null and b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/60.png differ diff --git a/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/64.png b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/64.png new file mode 100644 index 0000000..d6a2356 Binary files /dev/null and b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/64.png differ diff --git a/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/72.png b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/72.png new file mode 100644 index 0000000..9c9fd20 Binary files /dev/null and b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/72.png differ diff --git a/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/76.png b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/76.png new file mode 100644 index 0000000..00eb479 Binary files /dev/null and b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/76.png differ diff --git a/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/80.png b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/80.png new file mode 100644 index 0000000..34b710f Binary files /dev/null and b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/80.png differ diff --git a/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/87.png b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/87.png new file mode 100644 index 0000000..a093d90 Binary files /dev/null and b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/87.png differ diff --git a/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/88.png b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/88.png new file mode 100644 index 0000000..9312e5e Binary files /dev/null and b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/88.png differ diff --git a/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/Contents.json b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..e138c0b --- /dev/null +++ b/iOS/MonsterCards/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1 @@ +{"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"72x72","expected-size":"72","filename":"72.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"76x76","expected-size":"152","filename":"152.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"50x50","expected-size":"100","filename":"100.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"76x76","expected-size":"76","filename":"76.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"50x50","expected-size":"50","filename":"50.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"72x72","expected-size":"144","filename":"144.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"40x40","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"83.5x83.5","expected-size":"167","filename":"167.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"20x20","expected-size":"20","filename":"20.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"idiom":"watch","filename":"172.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"38mm","scale":"2x","size":"86x86","expected-size":"172","role":"quickLook"},{"idiom":"watch","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"38mm","scale":"2x","size":"40x40","expected-size":"80","role":"appLauncher"},{"idiom":"watch","filename":"88.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"40mm","scale":"2x","size":"44x44","expected-size":"88","role":"appLauncher"},{"idiom":"watch","filename":"100.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"44mm","scale":"2x","size":"50x50","expected-size":"100","role":"appLauncher"},{"idiom":"watch","filename":"196.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"42mm","scale":"2x","size":"98x98","expected-size":"196","role":"quickLook"},{"idiom":"watch","filename":"216.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"44mm","scale":"2x","size":"108x108","expected-size":"216","role":"quickLook"},{"idiom":"watch","filename":"48.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"38mm","scale":"2x","size":"24x24","expected-size":"48","role":"notificationCenter"},{"idiom":"watch","filename":"55.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"42mm","scale":"2x","size":"27.5x27.5","expected-size":"55","role":"notificationCenter"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"watch","role":"companionSettings","scale":"3x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"watch","role":"companionSettings","scale":"2x"},{"size":"1024x1024","expected-size":"1024","filename":"1024.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"watch-marketing","scale":"1x"},{"size":"128x128","expected-size":"128","filename":"128.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"256x256","expected-size":"256","filename":"256.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"128x128","expected-size":"256","filename":"256.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"256x256","expected-size":"512","filename":"512.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"32","filename":"32.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"512x512","expected-size":"512","filename":"512.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"16","filename":"16.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"32","filename":"32.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"64","filename":"64.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"512x512","expected-size":"1024","filename":"1024.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"}]} \ No newline at end of file diff --git a/iOS/MonsterCards/Assets.xcassets/Contents.json b/iOS/MonsterCards/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/iOS/MonsterCards/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iOS/MonsterCards/Assets.xcassets/section-divider.imageset/Contents.json b/iOS/MonsterCards/Assets.xcassets/section-divider.imageset/Contents.json new file mode 100644 index 0000000..0db353e --- /dev/null +++ b/iOS/MonsterCards/Assets.xcassets/section-divider.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "section-divider.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "section-divider@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "section-divider@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iOS/MonsterCards/Assets.xcassets/section-divider.imageset/section-divider.png b/iOS/MonsterCards/Assets.xcassets/section-divider.imageset/section-divider.png new file mode 100644 index 0000000..6340565 Binary files /dev/null and b/iOS/MonsterCards/Assets.xcassets/section-divider.imageset/section-divider.png differ diff --git a/iOS/MonsterCards/Assets.xcassets/section-divider.imageset/section-divider@2x.png b/iOS/MonsterCards/Assets.xcassets/section-divider.imageset/section-divider@2x.png new file mode 100644 index 0000000..5a17101 Binary files /dev/null and b/iOS/MonsterCards/Assets.xcassets/section-divider.imageset/section-divider@2x.png differ diff --git a/iOS/MonsterCards/Assets.xcassets/section-divider.imageset/section-divider@3x.png b/iOS/MonsterCards/Assets.xcassets/section-divider.imageset/section-divider@3x.png new file mode 100644 index 0000000..831a843 Binary files /dev/null and b/iOS/MonsterCards/Assets.xcassets/section-divider.imageset/section-divider@3x.png differ diff --git a/iOS/MonsterCards/Helpers/Color+Hex.swift b/iOS/MonsterCards/Helpers/Color+Hex.swift new file mode 100644 index 0000000..1b84d5e --- /dev/null +++ b/iOS/MonsterCards/Helpers/Color+Hex.swift @@ -0,0 +1,20 @@ +// +// Color+Hex.swift +// MonsterCards +// +// Created by Tom Hicks on 1/17/21. +// + +import Foundation +import SwiftUI + +extension Color { + init(hex: UInt, alpha: Double = 1) { + self.init( + .sRGB, + red: Double((hex >> 16) & 0xff) / 255, + green: Double((hex >> 8) & 0xff) / 255, + blue: Double((hex >> 0) & 0xff) / 255, + opacity: alpha) + } +} diff --git a/iOS/MonsterCards/Helpers/MonsterImportHelper.swift b/iOS/MonsterCards/Helpers/MonsterImportHelper.swift new file mode 100644 index 0000000..42be564 --- /dev/null +++ b/iOS/MonsterCards/Helpers/MonsterImportHelper.swift @@ -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 + } +} diff --git a/iOS/MonsterCards/Helpers/StringHelper.swift b/iOS/MonsterCards/Helpers/StringHelper.swift new file mode 100644 index 0000000..45b6801 --- /dev/null +++ b/iOS/MonsterCards/Helpers/StringHelper.swift @@ -0,0 +1,50 @@ +// +// StringHelper.swift +// MonsterCards +// +// Created by Tom Hicks on 3/21/21. +// + +import Foundation + +class StringHelper { + static func oxfordJoin( + _ strings: [String], + _ separator: String = ", ", + _ lastSeparator: String = ", and ", + _ onlySeparator: String = " and " + ) -> String { + let numStrings = strings.count + if (numStrings < 1) { + return ""; + } else if (numStrings == 2) { + return strings[0] + onlySeparator + strings[1] + } else { + var joined = "" + var index = 0 + let lastIndex = numStrings - 1 + + strings.forEach { + if index > 0 && index < lastIndex { + joined.append(separator) + } else if (index > 0 && index >= lastIndex) { + joined.append(lastSeparator) + } + joined.append($0) + index = index + 1 + } + + return joined + } + } + + static func safeContainsCaseInsensitive(_ str: String?, _ match: String) -> Bool { + guard let str = str else { return false } + return str.localizedCaseInsensitiveContains(match) + } + + static func safeEqualsIgnoreCase(_ str: String?, _ match: String) -> Bool { + guard let str = str else { return false } + return str.lowercased() == match.lowercased() + } +} diff --git a/iOS/MonsterCards/Images.xcassets/Contents.json b/iOS/MonsterCards/Images.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/iOS/MonsterCards/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iOS/MonsterCards/Images.xcassets/section-divider.imageset/Contents.json b/iOS/MonsterCards/Images.xcassets/section-divider.imageset/Contents.json new file mode 100644 index 0000000..0db353e --- /dev/null +++ b/iOS/MonsterCards/Images.xcassets/section-divider.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "section-divider.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "section-divider@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "section-divider@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iOS/MonsterCards/Images.xcassets/section-divider.imageset/section-divider.png b/iOS/MonsterCards/Images.xcassets/section-divider.imageset/section-divider.png new file mode 100644 index 0000000..6340565 Binary files /dev/null and b/iOS/MonsterCards/Images.xcassets/section-divider.imageset/section-divider.png differ diff --git a/iOS/MonsterCards/Images.xcassets/section-divider.imageset/section-divider@2x.png b/iOS/MonsterCards/Images.xcassets/section-divider.imageset/section-divider@2x.png new file mode 100644 index 0000000..5a17101 Binary files /dev/null and b/iOS/MonsterCards/Images.xcassets/section-divider.imageset/section-divider@2x.png differ diff --git a/iOS/MonsterCards/Images.xcassets/section-divider.imageset/section-divider@3x.png b/iOS/MonsterCards/Images.xcassets/section-divider.imageset/section-divider@3x.png new file mode 100644 index 0000000..831a843 Binary files /dev/null and b/iOS/MonsterCards/Images.xcassets/section-divider.imageset/section-divider@3x.png differ diff --git a/iOS/MonsterCards/Info.plist b/iOS/MonsterCards/Info.plist new file mode 100644 index 0000000..954b4e7 --- /dev/null +++ b/iOS/MonsterCards/Info.plist @@ -0,0 +1,131 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDocumentTypes + + + CFBundleTypeIconFiles + + CFBundleTypeName + Monster Data + LSHandlerRank + Owner + LSItemContentTypes + + com.majinnaibu.MonsterCards.Monster + + + + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconName + AppIcon + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + LSRequiresIPhoneOS + + LSSupportsOpeningDocumentsInPlace + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + + UIApplicationSupportsIndirectInputEvents + + UIBackgroundModes + + remote-notification + + UILaunchScreen + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportsDocumentBrowser + + UIUserInterfaceStyle + Light + UTExportedTypeDeclarations + + + UTTypeConformsTo + + public.data + public.content + + UTTypeDescription + Monster data file + UTTypeIconFiles + + UTTypeIdentifier + com.majinnaibu.MonsterCards.Monster + UTTypeTagSpecification + + public.filename-extension + + monster + + public.mime-type + + text/vnd.monstercards.monster + + + + + UTImportedTypeDeclarations + + + UTTypeConformsTo + + public.data + public.content + + UTTypeDescription + Monster data file + UTTypeIconFiles + + UTTypeIdentifier + com.majinnaibu.MonsterCards.Monster + UTTypeTagSpecification + + public.filename-extension + + monster + + public.mime-type + + text/vnd.monstercards.monster + + + + + + diff --git a/iOS/MonsterCards/Models/AbilityViewModel.swift b/iOS/MonsterCards/Models/AbilityViewModel.swift new file mode 100644 index 0000000..474ed13 --- /dev/null +++ b/iOS/MonsterCards/Models/AbilityViewModel.swift @@ -0,0 +1,116 @@ +// +// AbilityViewModel.swift +// MonsterCards +// +// Created by Tom Hicks on 3/25/21. +// + +import Foundation + +public class AbilityViewModel: NSObject, ObservableObject, Identifiable, NSSecureCoding { + public static var supportsSecureCoding = true + + public func encode(with coder: NSCoder) { + coder.encode(self.name, forKey: "name") + coder.encode(self.abilityDescription, forKey: "abilityDescription") + } + + public required init?(coder: NSCoder) { + self.name = coder.decodeObject(of: NSString.self, forKey: "name")! as String + self.abilityDescription = coder.decodeObject(of: NSString.self, forKey: "abilityDescription")! as String + } + + @Published public var name: String + @Published public var abilityDescription: String + + public init(_ name: String = "", _ abilityDescription: String = "") { + self.name = name + self.abilityDescription = abilityDescription + } + + public var fullText: String { + get { + return String(format: "___%@:___ %@", name, abilityDescription) + } + } + + func renderedText(_ monster: MonsterViewModel) -> String { + let strSave = monster.strengthModifier + monster.proficiencyBonus + 8 + let dexSave = monster.dexterityModifier + monster.proficiencyBonus + 8 + let conSave = monster.constitutionModifier + monster.proficiencyBonus + 8 + let intSave = monster.intelligenceModifier + monster.proficiencyBonus + 8 + let wisSave = monster.wisdomModifier + monster.proficiencyBonus + 8 + let chaSave = monster.charismaModifier + monster.proficiencyBonus + 8 + let strAttack = monster.strengthModifier + monster.proficiencyBonus + let dexAttack = monster.dexterityModifier + monster.proficiencyBonus + let conAttack = monster.constitutionModifier + monster.proficiencyBonus + let intAttack = monster.intelligenceModifier + monster.proficiencyBonus + let wisAttack = monster.wisdomModifier + monster.proficiencyBonus + let chaAttack = monster.charismaModifier + monster.proficiencyBonus + + // TODO: find the other options and implement them [WIS], [WIS STAT], [WIS DMG], [WIS STAT 1d12] + + let finalText = fullText + .replacingOccurrences(of: "[STR SAVE]", with: String(strSave)) + .replacingOccurrences(of: "[DEX SAVE]", with: String(dexSave)) + .replacingOccurrences(of: "[CON SAVE]", with: String(conSave)) + .replacingOccurrences(of: "[INT SAVE]", with: String(intSave)) + .replacingOccurrences(of: "[WIS SAVE]", with: String(wisSave)) + .replacingOccurrences(of: "[CHA SAVE]", with: String(chaSave)) + .replacingOccurrences(of: "[STR ATK]", with: String(strAttack)) + .replacingOccurrences(of: "[DEX ATK]", with: String(dexAttack)) + .replacingOccurrences(of: "[CON ATK]", with: String(conAttack)) + .replacingOccurrences(of: "[INT ATK]", with: String(intAttack)) + .replacingOccurrences(of: "[WIS ATK]", with: String(wisAttack)) + .replacingOccurrences(of: "[CHA ATK]", with: String(chaAttack)) + + return finalText + } +} + +extension AbilityViewModel: Comparable { + public static func < (lhs: AbilityViewModel, rhs: AbilityViewModel) -> Bool { + lhs.name < rhs.name + } + + public static func == (lhs: AbilityViewModel, rhs: AbilityViewModel) -> Bool { + lhs.name == rhs.name && + lhs.abilityDescription == rhs.abilityDescription + } +} + +// TODO: figure out how to add this to the set of known transformers so it will work with transformer set to NSSecureUnarchiveFromDataTransformerName +@objc(AbilityViewModelValueTransformer) +public final class AbilityViewModelValueTransformer: ValueTransformer { + override public class func transformedValueClass() -> AnyClass { + return NSArray.self + } + + override public class func allowsReverseTransformation() -> Bool { + return true + } + + override public func transformedValue(_ value: Any?) -> Any? { + guard let language = value as? NSArray else { return nil } + + do { + let data = try NSKeyedArchiver.archivedData(withRootObject: language, requiringSecureCoding: true) + return data + } catch { + assertionFailure("Failed to transform `AbilityViewModel` to `Data`") + return nil + } + } + + override public func reverseTransformedValue(_ value: Any?) -> Any? { + guard let data = value as? NSData else { return nil } + + do { + let language = try NSKeyedUnarchiver.unarchivedArrayOfObjects(ofClass: AbilityViewModel.self, from: data as Data) + return language + } catch { + assertionFailure("Failed to transform `Data` to `AbilityViewModel`") + return nil + } + } +} diff --git a/iOS/MonsterCards/Models/ChallengeRatingViewModel.swift b/iOS/MonsterCards/Models/ChallengeRatingViewModel.swift new file mode 100644 index 0000000..33209fc --- /dev/null +++ b/iOS/MonsterCards/Models/ChallengeRatingViewModel.swift @@ -0,0 +1,46 @@ +// +// ChallengeRatingViewModel.swift +// MonsterCards +// +// Created by Tom Hicks on 3/24/21. +// + +import Foundation + +class ChallengeRatingViewModel: ObservableObject/*, Comparable*/, Identifiable { + + func encode(with coder: NSCoder) { + coder.encode(self.rating.rawValue, forKey: "rating") + + } + + static func == (lhs: ChallengeRatingViewModel, rhs: ChallengeRatingViewModel) -> Bool { + lhs.rating == rhs.rating && + lhs.customText == rhs.customText && + lhs.customProficiencyBonus == rhs.customProficiencyBonus + } + + @Published var rating: ChallengeRating + @Published var customText: String + @Published var customProficiencyBonus: Int64 + + init( + _ rating: ChallengeRating = .one, + _ customText: String = "", + _ customProficiencyBonus: Int64 = 0 + ) { + self.rating = rating + self.customText = customText + self.customProficiencyBonus = customProficiencyBonus + } + + init( + _ rating: String = ChallengeRating.one.rawValue, + _ customText: String = "", + _ customProficiencyBonus: Int64 = 0 + ) { + self.rating = ChallengeRating(rawValue: rating) ?? .one + self.customText = customText + self.customProficiencyBonus = customProficiencyBonus + } +} diff --git a/iOS/MonsterCards/Models/DamageTypeDTO.swift b/iOS/MonsterCards/Models/DamageTypeDTO.swift new file mode 100644 index 0000000..62f7e92 --- /dev/null +++ b/iOS/MonsterCards/Models/DamageTypeDTO.swift @@ -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) + } +} diff --git a/iOS/MonsterCards/Models/Enums/AbilityScore.swift b/iOS/MonsterCards/Models/Enums/AbilityScore.swift new file mode 100644 index 0000000..b440413 --- /dev/null +++ b/iOS/MonsterCards/Models/Enums/AbilityScore.swift @@ -0,0 +1,73 @@ +// +// AbilityScore.swift +// MonsterCards +// +// Created by Tom Hicks on 1/18/21. +// + +import Foundation + +enum AbilityScore: String, CaseIterable, Identifiable { + case strength = "strength" + case dexterity = "dexterity" + case constitution = "constitution" + case intelligence = "intelligence" + case wisdom = "wisdom" + case charisma = "charisma" + + var id: AbilityScore { self } + + var displayName: String { + switch self { + case .strength: + return "Strength" + case .dexterity: + return "Dexterity" + case .constitution: + return "Constitution" + case .intelligence: + return "Intelligence" + case .wisdom: + return "Wisdom" + case .charisma: + return "Charisma" + } + } + + var shortDisplayName: String { + switch self { + case .strength: + return "STR" + case .dexterity: + return "DEX" + case .constitution: + return "CON" + case .intelligence: + return "INT" + case .wisdom: + return "WIS" + case .charisma: + return "CHA" + } + } + + init?(rawValue: String) { + var match: AbilityScore? = nil + let raw = rawValue.lowercased() + + for abilityScore in AbilityScore.allCases { + if (abilityScore.rawValue.lowercased() == raw) { + match = abilityScore + } + if (abilityScore.shortDisplayName.lowercased() == raw) { + match = abilityScore + } + } + + if (match == nil) { + return nil + } else { + self = match! + } + } +} diff --git a/iOS/MonsterCards/Models/Enums/AdvantageType.swift b/iOS/MonsterCards/Models/Enums/AdvantageType.swift new file mode 100644 index 0000000..35e6497 --- /dev/null +++ b/iOS/MonsterCards/Models/Enums/AdvantageType.swift @@ -0,0 +1,27 @@ +// +// AdvantageType.swift +// MonsterCards +// +// Created by Tom Hicks on 1/17/21. +// + +import Foundation + +enum AdvantageType: String, CaseIterable, Identifiable { + case none = "none" + case advantage = "advantage" + case disadvantage = "disadvantage" + + var id: AdvantageType { self } + + var displayName: String { + switch self { + case .none: + return "None" + case .advantage: + return "Advantage" + case .disadvantage: + return "Disadvantage" + } + } +} diff --git a/iOS/MonsterCards/Models/Enums/ArmorType.swift b/iOS/MonsterCards/Models/Enums/ArmorType.swift new file mode 100644 index 0000000..5c9a58d --- /dev/null +++ b/iOS/MonsterCards/Models/Enums/ArmorType.swift @@ -0,0 +1,50 @@ +// +// ArmorType.swift +// MonsterCards +// +// Created by Tom Hicks on 3/21/21. +// + +import Foundation + +enum ArmorType: String, CaseIterable, Identifiable { + case none = "none" + case naturalArmor = "natural armor" + case mageArmor = "mage armor" + case padded = "padded" + case leather = "leather" + case studdedLeather = "studded" + case hide = "hide" + case chainShirt = "chain shirt" + case scaleMail = "scale mail" + case breastplate = "breastplate" + case halfPlate = "half plate" + case ringMail = "ring mail" + case chainMail = "chain mail" + case splintMail = "splint" + case plateMail = "plate" + case other = "other" + + var id: ArmorType { self } + + var displayName: String { + switch self { + case .none: return "None" + case .naturalArmor: return "Natural Armor" + case .mageArmor: return "Mage Armor" + case .padded: return "Padded" + case .leather: return "Leather" + case .studdedLeather: return "Studded Leather" + case .hide: return "Hide" + case .chainShirt: return "Chain Shirt" + case .scaleMail: return "Scale Mail" + case .breastplate: return "Breastplate" + case .halfPlate: return "Half Plate" + case .ringMail: return "Ring Mail" + case .chainMail: return "Chain Mail" + case .splintMail: return "Splint Mail" + case .plateMail: return "Plate Mail" + case .other: return "Other" + } + } +} diff --git a/iOS/MonsterCards/Models/Enums/ChallengeRating.swift b/iOS/MonsterCards/Models/Enums/ChallengeRating.swift new file mode 100644 index 0000000..396ac7d --- /dev/null +++ b/iOS/MonsterCards/Models/Enums/ChallengeRating.swift @@ -0,0 +1,123 @@ +// +// ChallengeRating.swift +// MonsterCards +// +// Created by Tom Hicks on 3/21/21. +// + +import Foundation + +enum ChallengeRating: String, CaseIterable, Identifiable { + case custom = "Custom" + case zero = "0" + case oneEighth = "1/8" + case oneQuarter = "1/4" + case oneHalf = "1/2" + case one = "1" + case two = "2" + case three = "3" + case four = "4" + case five = "5" + case six = "6" + case seven = "7" + case eight = "8" + case nine = "9" + case ten = "10" + case eleven = "11" + case twelve = "12" + case thirteen = "13" + case fourteen = "14" + case fifteen = "15" + case sixteen = "16" + case seventeen = "17" + case eighteen = "18" + case nineteen = "19" + case twenty = "20" + case twentyOne = "21" + case twentyTwo = "22" + case twentyThree = "23" + case twentyFour = "24" + case twentyFive = "25" + case twentySix = "26" + case twentySeven = "27" + case twentyEight = "28" + case twentyNine = "29" + case thirty = "30" + + var id: ChallengeRating { self } + + var displayName: String { + switch(self) { + case .custom: + return "Custom" + case .zero: + return "0 (10 XP)" + case .oneEighth: + return "1/8 (25 XP)" + case .oneQuarter: + return "1/4 (50 XP)" + case .oneHalf: + return "1/2 (100 XP)" + case .one: + return "1 (200 XP)" + case .two: + return "2 (450 XP)" + case .three: + return "3 (700 XP)" + case .four: + return "4 (1,100 XP)" + case .five: + return "5 (1,800 XP)" + case .six: + return "6 (2,300 XP)" + case .seven: + return "7 (2,900 XP)" + case .eight: + return "8 (3,900 XP)" + case .nine: + return "9 (5,000 XP)" + case .ten: + return "10 (5,900 XP)" + case .eleven: + return "11 (7,200 XP)" + case .twelve: + return "12 (8,400 XP)" + case .thirteen: + return "13 (10,000 XP)" + case .fourteen: + return "14 (11,500 XP)" + case .fifteen: + return "15 (13,000 XP)" + case .sixteen: + return "16 (15,000 XP)" + case .seventeen: + return "17 (18,000 XP)" + case .eighteen: + return "18 (20,000 XP)" + case .nineteen: + return "19 (22,000 XP)" + case .twenty: + return "20 (25,000 XP)" + case .twentyOne: + return "21 (33,000 XP)" + case .twentyTwo: + return "22 (41,000 XP)" + case .twentyThree: + return "23 (50,000 XP)" + case .twentyFour: + return "24 (62,000 XP)" + case .twentyFive: + return "25 (75,000 XP)" + case .twentySix: + return "26 (90,000 XP)" + case .twentySeven: + return "27 (105,000 XP)" + case .twentyEight: + return "28 (120,000 XP)" + case .twentyNine: + return "29 (135,000 XP)" + case .thirty: + return "30 (155,000 XP)" + } + } +} diff --git a/iOS/MonsterCards/Models/Enums/ProficiencyType.swift b/iOS/MonsterCards/Models/Enums/ProficiencyType.swift new file mode 100644 index 0000000..8bc4cc7 --- /dev/null +++ b/iOS/MonsterCards/Models/Enums/ProficiencyType.swift @@ -0,0 +1,27 @@ +// +// ProficiencyType.swift +// MonsterCards +// +// Created by Tom Hicks on 1/17/21. +// + +import Foundation + +enum ProficiencyType: String, CaseIterable, Identifiable { + case none = "none" + case proficient = "proficient" + case expertise = "expertise" + + var id: ProficiencyType { self } + + var displayName: String { + switch self { + case .none: + return "None" + case .proficient: + return "Proficient" + case .expertise: + return "Expertise" + } + } +} diff --git a/iOS/MonsterCards/Models/Enums/SizeType.swift b/iOS/MonsterCards/Models/Enums/SizeType.swift new file mode 100644 index 0000000..3d482c2 --- /dev/null +++ b/iOS/MonsterCards/Models/Enums/SizeType.swift @@ -0,0 +1,52 @@ +// +// SizeType.swift +// MonsterCards +// +// Created by Tom Hicks on 3/21/21. +// + +import Foundation + +enum SizeType: String, CaseIterable, Identifiable { + case tiny = "tiny" + case small = "small" + case medium = "medium" + case large = "large" + case huge = "huge" + case gargantuan = "gargantuan" + + var id: SizeType { self } + + var displayName: String { + switch self { + case .tiny: return "Tiny" + case .small: return "Small" + case .medium: return "Medium" + case .large: return "Large" + case .huge: return "Huge" + case .gargantuan: return "gargantuan" + } + } + + init?(rawValue: String) { + var match: SizeType? = nil + + for size in SizeType.allCases { + if (size.rawValue == rawValue) { + match = size + } + } + + for size in SizeType.allCases { + if (size.rawValue.lowercased() == rawValue.lowercased()) { + match = size + } + } + + if (match == nil) { + return nil + } else { + self = match! + } + } +} diff --git a/iOS/MonsterCards/Models/LanguageDTO.swift b/iOS/MonsterCards/Models/LanguageDTO.swift new file mode 100644 index 0000000..46f3d38 --- /dev/null +++ b/iOS/MonsterCards/Models/LanguageDTO.swift @@ -0,0 +1,35 @@ +// +// LanguageDTO.swift +// MonsterCards +// +// Created by Tom Hicks on 3/28/21. +// + +import Foundation + +struct LanguageDTO { + var name: String + var speaks: Bool +} + +private enum LanguageDTOCodingKeys: String, CodingKey { + case name = "name" + case speaks = "speaks" +} + +extension LanguageDTO: Codable { + + init(from decoder: Decoder) throws { + + let container = try decoder.container(keyedBy: LanguageDTOCodingKeys.self) + self.name = (try? container.decode(String.self, forKey: .name)) ?? "" + self.speaks = (try? container.decode(Bool.self, forKey: .speaks)) ?? false + } + + func encode(to encoder: Encoder) throws { + + var container = encoder.container(keyedBy: LanguageDTOCodingKeys.self) + try container.encode(self.name, forKey: .name) + try container.encode(self.speaks, forKey: .speaks) + } +} diff --git a/iOS/MonsterCards/Models/LanguageViewModel.swift b/iOS/MonsterCards/Models/LanguageViewModel.swift new file mode 100644 index 0000000..65bbccd --- /dev/null +++ b/iOS/MonsterCards/Models/LanguageViewModel.swift @@ -0,0 +1,79 @@ +// +// LanguageViewModel.swift +// MonsterCards +// +// Created by Tom Hicks on 3/24/21. +// + +import Foundation + +// TODO: split this into separate Model and ViewModel classes later. +public class LanguageViewModel : NSObject, ObservableObject, Comparable, Identifiable, NSSecureCoding { + public static var supportsSecureCoding = true + + public func encode(with coder: NSCoder) { + coder.encode(self.name, forKey: "name") + coder.encode(self.speaks, forKey: "speaks") + } + + public required init?(coder: NSCoder) { + self.name = coder.decodeObject(of: NSString.self, forKey: "name")! as String + self.speaks = coder.decodeBool(forKey: "speaks") + } + + public static func < (lhs: LanguageViewModel, rhs: LanguageViewModel) -> Bool { + lhs.name < rhs.name + } + + public static func == (lhs: LanguageViewModel, rhs: LanguageViewModel) -> Bool { + lhs.name == rhs.name && + lhs.speaks == rhs.speaks + } + + @Published var name: String + @Published var speaks: Bool + + public init( + _ name: String = "", + _ speaks: Bool = true + ) { + self.name = name + self.speaks = speaks + } +} + +// TODO: figure out how to add this to the set of known transformers so it will work with transformer set to NSSecureUnarchiveFromDataTransformerName +@objc(LanguageViewModelValueTransformer) +public final class LanguageViewModelValueTransformer: ValueTransformer { + override public class func transformedValueClass() -> AnyClass { + return NSArray.self + } + + override public class func allowsReverseTransformation() -> Bool { + return true + } + + override public func transformedValue(_ value: Any?) -> Any? { + guard let language = value as? NSArray else { return nil } + + do { + let data = try NSKeyedArchiver.archivedData(withRootObject: language, requiringSecureCoding: true) + return data + } catch { + assertionFailure("Failed to transform `LanguageViewModel` to `Data`") + return nil + } + } + + override public func reverseTransformedValue(_ value: Any?) -> Any? { + guard let data = value as? NSData else { return nil } + + do { + let language = try NSKeyedUnarchiver.unarchivedArrayOfObjects(ofClass: LanguageViewModel.self, from: data as Data) + return language + } catch { + assertionFailure("Failed to transform `Data` to `LanguageViewModel`") + return nil + } + } +} diff --git a/iOS/MonsterCards/Models/Monster+CoreDataClass.swift b/iOS/MonsterCards/Models/Monster+CoreDataClass.swift new file mode 100644 index 0000000..328e927 --- /dev/null +++ b/iOS/MonsterCards/Models/Monster+CoreDataClass.swift @@ -0,0 +1,161 @@ +// +// Monster+CoreDataClass.swift +// MonsterCards +// +// Created by Tom Hicks on 1/15/21. +// +// + +import Foundation +import CoreData + +@objc(Monster) +public class Monster: NSManagedObject { + convenience init(context: NSManagedObjectContext, name: String, size: String, type: String, subtype: String, alignment: String) { + self.init(context:context) + self.name = name; + self.size = size; + self.type = type; + self.subtype = subtype; + self.alignment = alignment; + } + // MARK: Armor + + var armorTypeEnum: ArmorType { + get { + return ArmorType.init(rawValue: armorType ?? "none") ?? .none + } + set { + armorType = newValue.rawValue + } + } + + // MARK: Challenge Rating / Proficiency Bonus + + var challengeRatingEnum: ChallengeRating { + get { + return ChallengeRating.init(rawValue: challengeRating ?? "1") ?? .one + } + set { + challengeRating = newValue.rawValue + } + } + + + // MARK: Saving Throws + + var strengthSavingThrowProficiencyEnum: ProficiencyType { + get { + return ProficiencyType.init(rawValue: strengthSavingThrowProficiency ?? "") ?? .none + } + set { + strengthSavingThrowProficiency = newValue.rawValue + } + } + + var strengthSavingThrowAdvantageEnum: AdvantageType { + get { + return AdvantageType.init(rawValue: strengthSavingThrowAdvantage ?? "") ?? .none + } + set { + strengthSavingThrowAdvantage = newValue.rawValue + } + } + + var dexteritySavingThrowProficiencyEnum: ProficiencyType { + get { + return ProficiencyType.init(rawValue: dexteritySavingThrowProficiency ?? "") ?? .none + } + set { + dexteritySavingThrowProficiency = newValue.rawValue + } + } + + var dexteritySavingThrowAdvantageEnum: AdvantageType { + get { + return AdvantageType.init(rawValue: dexteritySavingThrowAdvantage ?? "") ?? .none + } + set { + dexteritySavingThrowAdvantage = newValue.rawValue + } + } + + var constitutionSavingThrowProficiencyEnum: ProficiencyType { + get { + return ProficiencyType.init(rawValue: constitutionSavingThrowProficiency ?? "") ?? .none + } + set { + constitutionSavingThrowProficiency = newValue.rawValue + } + } + + var constitutionSavingThrowAdvantageEnum: AdvantageType { + get { + return AdvantageType.init(rawValue: constitutionSavingThrowAdvantage ?? "") ?? .none + } + set { + constitutionSavingThrowAdvantage = newValue.rawValue + } + } + + var intelligenceSavingThrowProficiencyEnum: ProficiencyType { + get { + return ProficiencyType.init(rawValue: intelligenceSavingThrowProficiency ?? "") ?? .none + } + set { + intelligenceSavingThrowProficiency = newValue.rawValue + } + } + + var intelligenceSavingThrowAdvantageEnum: AdvantageType { + get { + return AdvantageType.init(rawValue: intelligenceSavingThrowAdvantage ?? "") ?? .none + } + set { + intelligenceSavingThrowAdvantage = newValue.rawValue + } + } + + var wisdomSavingThrowProficiencyEnum: ProficiencyType { + get { + return ProficiencyType.init(rawValue: wisdomSavingThrowProficiency ?? "") ?? .none + } + set { + wisdomSavingThrowProficiency = newValue.rawValue + } + } + + var wisdomSavingThrowAdvantageEnum: AdvantageType { + get { + return AdvantageType.init(rawValue: wisdomSavingThrowAdvantage ?? "") ?? .none + } + set { + wisdomSavingThrowAdvantage = newValue.rawValue + } + } + + var charismaSavingThrowProficiencyEnum: ProficiencyType { + get { + return ProficiencyType.init(rawValue: charismaSavingThrowProficiency ?? "") ?? .none + } + set { + charismaSavingThrowProficiency = newValue.rawValue + } + } + + var charismaSavingThrowAdvantageEnum: AdvantageType { + get { + return AdvantageType.init(rawValue: charismaSavingThrowAdvantage ?? "") ?? .none + } + set { + charismaSavingThrowAdvantage = newValue.rawValue + } + } + + // MARK: End + +} + + + + diff --git a/iOS/MonsterCards/Models/MonsterDTO.swift b/iOS/MonsterCards/Models/MonsterDTO.swift new file mode 100644 index 0000000..5db901a --- /dev/null +++ b/iOS/MonsterCards/Models/MonsterDTO.swift @@ -0,0 +1,353 @@ +// +// MonsterDTO.swift +// MonsterCards +// +// Created by Tom Hicks on 3/28/21. +// + +import Foundation + +struct MonsterDTO { + var name: String + var type: String + var alignment: String + var size: String + var hitDice: Int + var armorName: String + var otherArmorDesc: String + var shieldBonus: Int + var natArmorBonus: Int + var speed: Int + var burrowSpeed: Int + var climbSpeed: Int + var flySpeed: Int + var hover: Bool + var swimSpeed: Int + var speedDesc: String + var customSpeed: Bool + var strPoints: Int + var dexPoints: Int + var conPoints: Int + var intPoints: Int + var wisPoints: Int + var chaPoints: Int + var cr: String + var customCr: String + var customProf: Int + var hpText: String + var sthrows: [SavingThrowDTO] + var skills: [SkillDTO] + var actions: [TraitDTO] + var legendaryDescription: String + var legendaries: [TraitDTO] + var reactions: [TraitDTO]// TODO: verify this + var abilities: [TraitDTO] + var damageTypes: [DamageTypeDTO] + var conditions: [DamageTypeDTO] // TODO: figure this out + var languages: [LanguageDTO] + var telepathy: Int + var understandsBut: String + var blindsight: Int + var blind: Bool + var darkvision: Int + var tremorsense: Int + var truesight: Int + var tag: String + var customHP: Bool + var isLegendary: Bool + var isLair: Bool + var lairDescription: String + var lairDescriptionEnd: String + var isRegional: Bool + var regionalDescription: String + var regionalDescriptionEnd: String +// var properties: [???] // TODO: figure this out + var lairs: [TraitDTO] + var regionals: [TraitDTO] + var specialDamage: [DamageTypeDTO] + var shortName: String + var doubleColumns: Bool + var separationPoint: Int + var damage: [DamageTypeDTO] // TODO: figure this out + + init() { + self.abilities = [] + self.actions = [] + self.alignment = "" + self.armorName = "" + self.blind = false + self.blindsight = 0 + self.burrowSpeed = 0 + self.chaPoints = 0 + self.climbSpeed = 0 + self.conPoints = 0 + self.conditions = [] + self.cr = "" + self.customCr = "" + self.customHP = false + self.customProf = 0 + self.customSpeed = false + self.damage = [] + self.damageTypes = [] + self.darkvision = 0 + self.dexPoints = 0 + self.doubleColumns = false + self.flySpeed = 0 + self.hitDice = 0 + self.hover = false + self.hpText = "" + self.intPoints = 0 + self.isLair = false + self.isLegendary = false + self.isRegional = false + self.lairs = [] + self.languages = [] + self.legendaries = [] + self.lairDescription = "" + self.legendaryDescription = "" + self.lairDescriptionEnd = "" + self.name = "" + self.natArmorBonus = 0 + self.otherArmorDesc = "" + self.reactions = [] + self.regionals = [] + self.regionalDescription = "" + self.regionalDescriptionEnd = "" + self.size = "" + self.speed = 0 + self.skills = [] + self.sthrows = [] + self.strPoints = 0 + self.swimSpeed = 0 + self.speedDesc = "" + self.shortName = "" + self.specialDamage = [] + self.shieldBonus = 0 + self.separationPoint = 0 + self.type = "" + self.tag = "" + self.telepathy = 0 + self.truesight = 0 + self.tremorsense = 0 + self.understandsBut = "" + self.wisPoints = 0 + } + +} + +enum MonsterDTOCodingKeys: String, CodingKey { + case name = "name" + case type = "type" + case alignment = "alignment" + case size = "size" + case hitDice = "hitDice" + case armorName = "armorName" + case otherArmorDesc = "otherArmorDesc" + case shieldBonus = "shieldBonus" + case natArmorBonus = "natArmorBonus" + case speed = "speed" + case burrowSpeed = "burrowSpeed" + case climbSpeed = "climbSpeed" + case flySpeed = "flySpeed" + case hover = "hover" + case swimSpeed = "swimSpeed" + case speedDesc = "speedDesc" + case customSpeed = "customSpeed" + case strPoints = "strPoints" + case dexPoints = "dexPoints" + case conPoints = "conPoints" + case intPoints = "intPoints" + case wisPoints = "wisPoints" + case chaPoints = "chaPoints" + case cr = "cr" + case customCr = "customCr" + case customProf = "customProf" + case hpText = "hpText" + case sthrows = "sthrows" + case skills = "skills" + case actions = "actions" + case legendaryDescription = "legendaryDescription" + case legendaries = "legendaries" + case reactions = "reactions" + case abilities = "abilities" + case damageTypes = "damageTypes" + case conditions = "conditions" + case languages = "languages" + case telepathy = "telepathy" + case understandsBut = "understandsBut" + case blindsight = "blindsight" + case blind = "blind" + case darkvision = "darkvision" + case tremorsense = "tremorsense" + case truesight = "truesight" + case tag = "tag" + case customHP = "customHP" + case isLegendary = "isLegendary" + case isLair = "isLair" + case lairDescription = "lairDescription" + case lairDescriptionEnd = "lairDescriptionEnd" + case isRegional = "isRegional" + case regionalDescription = "regionalDescription" + case regionalDescriptionEnd = "regionalDescriptionEnd" + case properties = "properties" + case lairs = "lairs" + case regionals = "regionals" + case specialDamage = "specialDamage" + case shortName = "shortName" + case doubleColumns = "doubleColumns" + case separationPoint = "separationPoint" + case damage = "damage" +} + +func readInt(_ container: KeyedDecodingContainer, _ key: MonsterDTOCodingKeys, _ defaultValue: Int = 0) -> Int { + let readInt = try? container.decode(Int.self, forKey: key) + let readString = try? container.decode(String.self, forKey: key) + if (readInt != nil) { + return readInt! + } + if (readString != nil) { + return Int(readString!) ?? defaultValue + } + + return defaultValue; +} + +func readString(_ container: KeyedDecodingContainer, _ key: MonsterDTOCodingKeys, _ defaultValue: String = "") -> String { + return (try? container.decode(String.self, forKey: key)) ?? defaultValue +} + +extension MonsterDTO: Codable { + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: MonsterDTOCodingKeys.self) + self.name = readString(container, .name, "Imported Monster") + self.type = readString(container, .type, "") + self.alignment = readString(container, .alignment, "") + self.size = readString(container, .size, "") + self.hitDice = readInt(container, .hitDice, 0) + self.armorName = readString(container, .armorName, "") + self.otherArmorDesc = readString(container, .otherArmorDesc, "") + self.shieldBonus = readInt(container, .shieldBonus, 0) + self.natArmorBonus = readInt(container, .natArmorBonus, 0) + self.speed = readInt(container, .speed, 0) + self.burrowSpeed = readInt(container, .burrowSpeed, 0) + self.climbSpeed = readInt(container, .climbSpeed, 0) + self.flySpeed = readInt(container, .flySpeed, 0) + self.hover = (try? container.decode(Bool.self, forKey: .hover)) ?? false + self.swimSpeed = readInt(container, .swimSpeed, 0) + self.speedDesc = readString(container, .speedDesc, "") + self.customSpeed = (try? container.decode(Bool.self, forKey: .customSpeed)) ?? false + self.strPoints = readInt(container, .strPoints, 0) + self.dexPoints = readInt(container, .dexPoints, 0) + self.conPoints = readInt(container, .conPoints, 0) + self.intPoints = readInt(container, .intPoints, 0) + self.wisPoints = readInt(container, .wisPoints, 0) + self.chaPoints = readInt(container, .chaPoints, 0) + self.cr = readString(container, .cr, "") + self.customCr = readString(container, .customCr, "") + self.customProf = readInt(container, .customProf, 0) + self.hpText = readString(container, .hpText, "") + self.legendaryDescription = readString(container, .legendaryDescription, "") + self.understandsBut = readString(container, .understandsBut, "") + self.tag = readString(container, .tag, "") + self.lairDescription = readString(container, .lairDescription, "") + self.lairDescriptionEnd = readString(container, .lairDescriptionEnd, "") + self.regionalDescription = readString(container, .regionalDescription, "") + self.regionalDescriptionEnd = readString(container, .regionalDescriptionEnd, "") + self.shortName = readString(container, .shortName, "") + + self.telepathy = readInt(container, .telepathy, 0) + self.blindsight = readInt(container, .blindsight, 0) + self.darkvision = readInt(container, .darkvision, 0) + self.tremorsense = readInt(container, .tremorsense, 0) + self.truesight = readInt(container, .truesight, 0) + self.separationPoint = readInt(container, .separationPoint, 0) + + self.blind = (try? container.decode(Bool.self, forKey: .blind)) ?? false + self.customHP = (try? container.decode(Bool.self, forKey: .customHP)) ?? false + self.isLegendary = (try? container.decode(Bool.self, forKey: .isLegendary)) ?? false + self.isLair = (try? container.decode(Bool.self, forKey: .isLair)) ?? false + self.isRegional = (try? container.decode(Bool.self, forKey: .isRegional)) ?? false + self.doubleColumns = (try? container.decode(Bool.self, forKey: .doubleColumns)) ?? false + + self.lairs = (try? container.decode([TraitDTO].self, forKey: .lairs)) ?? [] + self.regionals = (try? container.decode([TraitDTO].self, forKey: .regionals)) ?? [] + self.specialDamage = (try? container.decode([DamageTypeDTO].self, forKey: .specialDamage)) ?? [] + self.damage = (try? container.decode([DamageTypeDTO].self, forKey: .damage)) ?? [] + self.conditions = (try? container.decode([DamageTypeDTO].self, forKey: .conditions)) ?? [] + + + self.sthrows = (try? container.decode([SavingThrowDTO].self, forKey: .sthrows)) ?? [] + self.skills = (try? container.decode([SkillDTO].self, forKey: .skills)) ?? [] + self.actions = (try? container.decode([TraitDTO].self, forKey: .actions)) ?? [] + self.legendaries = (try? container.decode([TraitDTO].self, forKey: .legendaries)) ?? [] + self.reactions = (try? container.decode([TraitDTO].self, forKey: .reactions)) ?? [] + self.abilities = (try? container.decode([TraitDTO].self, forKey: .abilities)) ?? [] + self.damageTypes = (try? container.decode([DamageTypeDTO].self, forKey: .damageTypes)) ?? [] + self.languages = (try? container.decode([LanguageDTO].self, forKey: .languages)) ?? [] + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: MonsterDTOCodingKeys.self) + try container.encode(self.name, forKey: .name) + try container.encode(self.type, forKey: .type) + try container.encode(self.alignment, forKey: .alignment) + try container.encode(self.size, forKey: .size) + try container.encode(self.hitDice, forKey: .hitDice) + try container.encode(self.armorName, forKey: .armorName) + try container.encode(self.otherArmorDesc, forKey: .otherArmorDesc) + try container.encode(self.shieldBonus, forKey: .shieldBonus) + try container.encode(self.natArmorBonus, forKey: .natArmorBonus) + try container.encode(self.speed, forKey: .speed) + try container.encode(self.burrowSpeed, forKey: .burrowSpeed) + try container.encode(self.climbSpeed, forKey: .climbSpeed) + try container.encode(self.flySpeed, forKey: .flySpeed) + try container.encode(self.hover, forKey: .hover) + try container.encode(self.swimSpeed, forKey: .swimSpeed) + try container.encode(self.speedDesc, forKey: .speedDesc) + try container.encode(self.customSpeed, forKey: .customSpeed) + try container.encode(self.strPoints, forKey: .strPoints) + try container.encode(self.dexPoints, forKey: .dexPoints) + try container.encode(self.conPoints, forKey: .conPoints) + try container.encode(self.intPoints, forKey: .intPoints) + try container.encode(self.wisPoints, forKey: .wisPoints) + try container.encode(self.chaPoints, forKey: .chaPoints) + try container.encode(self.cr, forKey: .cr) + try container.encode(self.customCr, forKey: .customCr) + try container.encode(self.customProf, forKey: .customProf) + try container.encode(self.hpText, forKey: .hpText) + try container.encode(self.sthrows, forKey: .sthrows) + try container.encode(self.skills, forKey: .skills) + try container.encode(self.actions, forKey: .actions) + try container.encode(self.legendaryDescription, forKey: .legendaryDescription) + try container.encode(self.legendaries, forKey: .legendaries) + try container.encode(self.reactions, forKey: .reactions) + try container.encode(self.abilities, forKey: .abilities) + try container.encode(self.damageTypes, forKey: .damageTypes) + try container.encode(self.conditions, forKey: .conditions) + try container.encode(self.languages, forKey: .languages) + try container.encode(self.telepathy, forKey: .telepathy) + try container.encode(self.understandsBut, forKey: .understandsBut) + try container.encode(self.blindsight, forKey: .blindsight) + try container.encode(self.blind, forKey: .blind) + try container.encode(self.darkvision, forKey: .darkvision) + try container.encode(self.tremorsense, forKey: .tremorsense) + try container.encode(self.truesight, forKey: .truesight) + try container.encode(self.tag, forKey: .tag) + try container.encode(self.customHP, forKey: .customHP) + try container.encode(self.isLegendary, forKey: .isLegendary) + try container.encode(self.isLair, forKey: .isLair) + try container.encode(self.lairDescription, forKey: .lairDescription) + try container.encode(self.lairDescriptionEnd, forKey: .lairDescriptionEnd) + try container.encode(self.isRegional, forKey: .isRegional) + try container.encode(self.regionalDescription, forKey: .regionalDescription) + try container.encode(self.regionalDescriptionEnd, forKey: .regionalDescriptionEnd) + try container.encode([] as [TraitDTO], forKey: .properties) + try container.encode(self.lairs, forKey: .lairs) + try container.encode(self.regionals, forKey: .regionals) + try container.encode(self.specialDamage, forKey: .specialDamage) + try container.encode(self.shortName, forKey: .shortName) + try container.encode(self.doubleColumns, forKey: .doubleColumns) + try container.encode(self.separationPoint, forKey: .separationPoint) + try container.encode(self.damage, forKey: .damage) + } +} diff --git a/iOS/MonsterCards/Models/MonsterDocument.swift b/iOS/MonsterCards/Models/MonsterDocument.swift new file mode 100644 index 0000000..c073217 --- /dev/null +++ b/iOS/MonsterCards/Models/MonsterDocument.swift @@ -0,0 +1,37 @@ +// +// Document.swift +// MonsterCards +// +// Created by Tom Hicks on 4/7/21. +// + +import UIKit +import SceneKit + +class MonsterDocument: UIDocument { + + var monsterDTO: MonsterDTO? + var error: Error? + + override func contents(forType typeName: String) throws -> Any { + let encoder = JSONEncoder() + do { + let data = try encoder.encode(monsterDTO) + return data + } catch { + return Data() + } + } + + override func load(fromContents contents: Any, ofType typeName: String?) throws { + let decoder = JSONDecoder() + do { + let data = contents as! Data + monsterDTO = try decoder.decode(MonsterDTO.self, from: data) + } catch { + monsterDTO = MonsterDTO() + } + } + +} + diff --git a/iOS/MonsterCards/Models/MonsterViewModel+CoreData.swift b/iOS/MonsterCards/Models/MonsterViewModel+CoreData.swift new file mode 100644 index 0000000..b3835c9 --- /dev/null +++ b/iOS/MonsterCards/Models/MonsterViewModel+CoreData.swift @@ -0,0 +1,223 @@ +// +// MonsterViewModel+CoreData.swift +// MonsterCards +// +// Created by Tom Hicks on 4/7/21. +// + +import Foundation +import CoreData + +extension MonsterViewModel { + convenience init(_ rawMonster: Monster?) { + self.init() + + // Call the copy constructor + if (rawMonster != nil) { + self.copyFromMonster(monster: rawMonster!) + } + } + + func copyFromMonster(monster: Monster) { + self.name = monster.name ?? "" + self.size = monster.size ?? "" + self.type = monster.type ?? "" + self.subType = monster.subtype ?? "" + self.alignment = monster.alignment ?? "" + self.hitDice = monster.hitDice + self.hasCustomHP = monster.hasCustomHP + self.customHP = monster.customHP ?? "" + self.armorType = monster.armorTypeEnum + self.hasShield = monster.hasShield + self.naturalArmorBonus = monster.naturalArmorBonus + self.customArmor = monster.customArmor ?? "" + self.walkSpeed = monster.walkSpeed + self.burrowSpeed = monster.burrowSpeed + self.climbSpeed = monster.climbSpeed + self.flySpeed = monster.flySpeed + self.canHover = monster.canHover + self.swimSpeed = monster.swimSpeed + self.hasCustomSpeed = monster.hasCustomSpeed + self.customSpeed = monster.customSpeed ?? "" + self.strengthScore = monster.strengthScore + self.strengthSavingThrowAdvantage = monster.strengthSavingThrowAdvantageEnum + self.strengthSavingThrowProficiency = monster.strengthSavingThrowProficiencyEnum + self.dexterityScore = monster.dexterityScore + self.dexteritySavingThrowAdvantage = monster.dexteritySavingThrowAdvantageEnum + self.dexteritySavingThrowProficiency = monster.dexteritySavingThrowProficiencyEnum + self.constitutionScore = monster.constitutionScore + self.constitutionSavingThrowAdvantage = monster.constitutionSavingThrowAdvantageEnum + self.constitutionSavingThrowProficiency = monster.constitutionSavingThrowProficiencyEnum + self.intelligenceScore = monster.intelligenceScore + self.intelligenceSavingThrowAdvantage = monster.intelligenceSavingThrowAdvantageEnum + self.intelligenceSavingThrowProficiency = monster.intelligenceSavingThrowProficiencyEnum + self.wisdomScore = monster.wisdomScore + self.wisdomSavingThrowAdvantage = monster.wisdomSavingThrowAdvantageEnum + self.wisdomSavingThrowProficiency = monster.wisdomSavingThrowProficiencyEnum + self.charismaScore = monster.charismaScore + self.charismaSavingThrowAdvantage = monster.charismaSavingThrowAdvantageEnum + self.charismaSavingThrowProficiency = monster.charismaSavingThrowProficiencyEnum + self.telepathy = monster.telepathy + self.understandsBut = monster.understandsBut ?? "" + self.challengeRating = monster.challengeRatingEnum + self.customChallengeRating = monster.customChallengeRating ?? "" + self.customProficiencyBonus = monster.customProficiencyBonus + self.isBlind = monster.isBlind + + self.skills = (monster.skills?.allObjects.map { + let skill = $0 as! Skill + return SkillViewModel( + skill.name ?? "", + AbilityScore(rawValue: skill.abilityScoreName ?? "") ?? .dexterity, + ProficiencyType(rawValue: skill.proficiency ?? "") ?? .none, + AdvantageType(rawValue: skill.advantage ?? "") ?? .none + ) + })!.sorted() + + // self.name = rawSkill!.name ?? "" + // self.abilityScore = AbilityScore(rawValue: rawSkill!.abilityScoreName ?? "") ?? .strength + // self.proficiency = ProficiencyType(rawValue: rawSkill!.proficiency ?? "") ?? .none + // self.advantage = AdvantageType(rawValue: rawSkill!.advantage ?? "") ?? .none + + + self.damageImmunities = (monster.damageImmunities ?? []) + .map {StringViewModel($0)} + .sorted() + + self.damageResistances = (monster.damageResistances ?? []) + .map {StringViewModel($0)} + .sorted() + + self.damageVulnerabilities = (monster.damageVulnerabilities ?? []) + .map {StringViewModel($0)} + .sorted() + + self.conditionImmunities = (monster.conditionImmunities ?? []) + .map {StringViewModel($0)} + .sorted() + + self.senses = (monster.senses ?? []) + .map {StringViewModel($0)} + .sorted() + + self.languages = (monster.languages ?? []) + .map {LanguageViewModel($0.name, $0.speaks)} + .sorted() + + // These are manually sorted in the UI + self.abilities = (monster.abilities ?? []) + .map {AbilityViewModel($0.name, $0.abilityDescription)} + + self.actions = (monster.actions ?? []) + .map {AbilityViewModel($0.name, $0.abilityDescription)} + + self.legendaryActions = (monster.legendaryActions ?? []) + .map {AbilityViewModel($0.name, $0.abilityDescription)} + + self.lairActions = (monster.lairActions ?? []) + .map {AbilityViewModel($0.name, $0.abilityDescription)} + + self.regionalActions = (monster.regionalActions ?? []) + .map {AbilityViewModel($0.name, $0.abilityDescription)} + + self.reactions = (monster.reactions ?? []) + .map {AbilityViewModel($0.name, $0.abilityDescription)} + + // Private fields + + self.shieldBonus = Int(monster.shieldBonus) + self.otherArmorDescription = monster.otherArmorDescription ?? "" + } + + func copyToMonster(monster: Monster) { + monster.name = name + monster.size = size + monster.type = type + monster.subtype = subType + monster.alignment = alignment + monster.hitDice = hitDice + monster.hasCustomHP = hasCustomHP + monster.customHP = customHP + monster.armorTypeEnum = armorType + monster.hasShield = hasShield + monster.naturalArmorBonus = naturalArmorBonus + monster.customArmor = customArmor + monster.walkSpeed = walkSpeed + monster.burrowSpeed = burrowSpeed + monster.climbSpeed = climbSpeed + monster.flySpeed = flySpeed + monster.canHover = canHover + monster.swimSpeed = swimSpeed + monster.hasCustomSpeed = hasCustomSpeed + monster.customSpeed = customSpeed + monster.strengthScore = strengthScore + monster.strengthSavingThrowAdvantageEnum = strengthSavingThrowAdvantage + monster.strengthSavingThrowProficiencyEnum = strengthSavingThrowProficiency + monster.dexterityScore = dexterityScore + monster.dexteritySavingThrowAdvantageEnum = dexteritySavingThrowAdvantage + monster.dexteritySavingThrowProficiencyEnum = dexteritySavingThrowProficiency + monster.constitutionScore = constitutionScore + monster.constitutionSavingThrowAdvantageEnum = constitutionSavingThrowAdvantage + monster.constitutionSavingThrowProficiencyEnum = constitutionSavingThrowProficiency + monster.intelligenceScore = intelligenceScore + monster.intelligenceSavingThrowAdvantageEnum = intelligenceSavingThrowAdvantage + monster.intelligenceSavingThrowProficiencyEnum = intelligenceSavingThrowProficiency + monster.wisdomScore = wisdomScore + monster.wisdomSavingThrowAdvantageEnum = wisdomSavingThrowAdvantage + monster.wisdomSavingThrowProficiencyEnum = wisdomSavingThrowProficiency + monster.charismaScore = charismaScore + monster.charismaSavingThrowAdvantageEnum = charismaSavingThrowAdvantage + monster.charismaSavingThrowProficiencyEnum = charismaSavingThrowProficiency + monster.telepathy = telepathy + monster.understandsBut = understandsBut + monster.challengeRatingEnum = challengeRating + monster.customChallengeRating = customChallengeRating + monster.customProficiencyBonus = customProficiencyBonus + monster.isBlind = isBlind + + // Remove missing skills from raw monster + monster.skills?.forEach {s in + let skill = s as! Skill + let skillVM = skills.first { $0.isEqualTo(rawSkill: skill) } + if (skillVM != nil) { + skillVM!.copyToSkill(skill: skill) + } else { + monster.removeFromSkills(skill) + } + } + // Add new skills to raw monster + skills.forEach {skillVM in + if (!(monster.skills?.contains( + where: { + skillVM.isEqualTo(rawSkill: $0 as? Skill) + }) ?? true)){ + monster.addToSkills(skillVM.buildRawSkill(context: monster.managedObjectContext)) + } + } + + monster.conditionImmunities = conditionImmunities.map {$0.name} + monster.damageImmunities = damageImmunities.map {$0.name} + monster.damageResistances = damageResistances.map {$0.name} + monster.damageVulnerabilities = damageVulnerabilities.map {$0.name} + monster.senses = senses.map {$0.name} + + // This is necessary so core data sees the language objects as changed. Without it they won't be persisted. + monster.languages = languages.map {LanguageViewModel($0.name, $0.speaks)} + + monster.abilities = abilities.map {AbilityViewModel($0.name, $0.abilityDescription)} + + monster.actions = actions.map {AbilityViewModel($0.name, $0.abilityDescription)} + + monster.legendaryActions = legendaryActions.map {AbilityViewModel($0.name, $0.abilityDescription)} + + monster.lairActions = lairActions.map {AbilityViewModel($0.name, $0.abilityDescription)} + + monster.regionalActions = regionalActions.map {AbilityViewModel($0.name, $0.abilityDescription)} + + monster.reactions = reactions.map {AbilityViewModel($0.name, $0.abilityDescription)} + + monster.shieldBonus = Int64(shieldBonus) + monster.otherArmorDescription = otherArmorDescription + } + +} diff --git a/iOS/MonsterCards/Models/MonsterViewModel.swift b/iOS/MonsterCards/Models/MonsterViewModel.swift new file mode 100644 index 0000000..cf0c52e --- /dev/null +++ b/iOS/MonsterCards/Models/MonsterViewModel.swift @@ -0,0 +1,706 @@ +// +// MonsterViewModel.swift +// MonsterCards +// +// Created by Tom Hicks on 1/18/21. +// + +import Foundation +import CoreData + +class MonsterViewModel: ObservableObject { + + // TODO: Determine whether to prefer Int or Int64 for these fields and switch as many as possible to the winner. + + @Published var name: String + @Published var size: String + @Published var type: String + @Published var subType: String + @Published var alignment: String + @Published var hitDice: Int64 + @Published var hasCustomHP: Bool + @Published var customHP: String + @Published var armorType: ArmorType + @Published var hasShield: Bool { + didSet { shieldBonus = hasShield ? 2 : 0 } + } + @Published var naturalArmorBonus: Int64 + @Published var customArmor: String + @Published var walkSpeed: Int64 + @Published var burrowSpeed: Int64 + @Published var climbSpeed: Int64 + @Published var flySpeed: Int64 + @Published var canHover: Bool + @Published var swimSpeed: Int64 + @Published var hasCustomSpeed: Bool + @Published var customSpeed: String + @Published var strengthScore: Int64 + @Published var dexterityScore: Int64 + @Published var constitutionScore: Int64 + @Published var intelligenceScore: Int64 + @Published var wisdomScore: Int64 + @Published var charismaScore: Int64 + @Published var strengthSavingThrowProficiency: ProficiencyType + @Published var strengthSavingThrowAdvantage: AdvantageType + @Published var dexteritySavingThrowProficiency: ProficiencyType + @Published var dexteritySavingThrowAdvantage: AdvantageType + @Published var constitutionSavingThrowProficiency: ProficiencyType + @Published var constitutionSavingThrowAdvantage: AdvantageType + @Published var intelligenceSavingThrowProficiency: ProficiencyType + @Published var intelligenceSavingThrowAdvantage: AdvantageType + @Published var wisdomSavingThrowProficiency: ProficiencyType + @Published var wisdomSavingThrowAdvantage: AdvantageType + @Published var charismaSavingThrowProficiency: ProficiencyType + @Published var charismaSavingThrowAdvantage: AdvantageType + @Published var skills: [SkillViewModel] + @Published var damageImmunities: [StringViewModel] + @Published var damageResistances: [StringViewModel] + @Published var damageVulnerabilities: [StringViewModel] + @Published var conditionImmunities: [StringViewModel] + @Published var senses: [StringViewModel] + @Published var languages: [LanguageViewModel] + @Published var telepathy: Int64 + @Published var understandsBut: String + @Published var challengeRating: ChallengeRating + @Published var customChallengeRating: String + @Published var customProficiencyBonus: Int64 + @Published var abilities: [AbilityViewModel] + @Published var actions: [AbilityViewModel] + @Published var legendaryActions: [AbilityViewModel] + @Published var lairActions: [AbilityViewModel] + @Published var regionalActions: [AbilityViewModel] + @Published var reactions: [AbilityViewModel] + @Published var isBlind: Bool + @Published var shieldBonus: Int + @Published var otherArmorDescription: String + + let kBaseArmorClassUnarmored = 10; + let kBaseArmorClassMageArmor = 13; + let kBaseArmorClassPadded = 11; + let kBaseArmorClassLeather = 11; + let kBaseArmorClassStudded = 12; + let kBaseArmorClassHide = 12; + let kBaseArmorClassChainShirt = 13; + let kBaseArmorClassScaleMail = 14; + let kBaseArmorClassBreastplate = 14; + let kBaseArmorClassHalfPlate = 15; + let kBaseArmorClassRingMail = 14; + let kBaseArmorClassChainMail = 16; + let kBaseArmorClassSplintMail = 17; + let kBaseArmorClassPlate = 18; + + init() { + self.name = "" + self.size = "" + self.type = "" + self.subType = "" + self.alignment = "" + self.hitDice = 0 + self.hasCustomHP = false + self.customHP = "" + self.armorType = .none + self.hasShield = false + self.naturalArmorBonus = 0 + self.customArmor = "" + self.walkSpeed = 0 + self.burrowSpeed = 0 + self.climbSpeed = 0 + self.flySpeed = 0 + self.canHover = false + self.swimSpeed = 0 + self.hasCustomSpeed = false + self.customSpeed = "" + self.strengthScore = 10 + self.strengthSavingThrowAdvantage = .none + self.strengthSavingThrowProficiency = .none + self.dexterityScore = 10 + self.dexteritySavingThrowAdvantage = .none + self.dexteritySavingThrowProficiency = .none + self.constitutionScore = 10 + self.constitutionSavingThrowAdvantage = .none + self.constitutionSavingThrowProficiency = .none + self.intelligenceScore = 10 + self.intelligenceSavingThrowAdvantage = .none + self.intelligenceSavingThrowProficiency = .none + self.wisdomScore = 10 + self.wisdomSavingThrowAdvantage = .none + self.wisdomSavingThrowProficiency = .none + self.charismaScore = 10 + self.charismaSavingThrowAdvantage = .none + self.charismaSavingThrowProficiency = .none + self.skills = [] + self.damageImmunities = [] + self.damageResistances = [] + self.damageVulnerabilities = [] + self.conditionImmunities = [] + self.senses = [] + self.languages = [] + self.telepathy = 0 + self.understandsBut = "" + self.challengeRating = .one + self.customChallengeRating = "" + self.customProficiencyBonus = 0 + self.abilities = [] + self.actions = [] + self.legendaryActions = [] + self.lairActions = [] + self.regionalActions = [] + self.reactions = [] + self.isBlind = false + + // Private properties + self.shieldBonus = 0 + self.otherArmorDescription = "" + } + + // MARK: Basic Info + + var meta: String { + get { + // size type (subtype) alignment + var parts: [String] = [] + + if (!(self.size.isEmpty)) { + parts.append(self.size) + } + + if (!(self.type.isEmpty)) { + parts.append(self.type) + } + + if (!(self.subType.isEmpty)) { + parts.append(String.init(format: "(%@)", arguments: [self.subType])) + } + + if (!(self.alignment.isEmpty)) { + parts.append(self.alignment) + } + + return parts.joined(separator: " ") + } + } + + var sizeEnum: SizeType { + get { + return SizeType.init(rawValue: size) ?? .medium + } + set { + size = newValue.rawValue + } + } + + var hitPoints: String { + get { + if (hasCustomHP) { + return customHP; + } else { + let dieSize = Double(MonsterViewModel.hitDieForSize(sizeEnum)) + let conMod = Double(constitutionModifier) +// let level1HP = Double(dieSize + conMod) + let level1HP = Double(dieSize/2.0 + conMod) + let extraLevels = Double(hitDice - 1) + let levelNHP = (dieSize + 1.0) / 2.0 + conMod + let extraLevelsHP = extraLevels * levelNHP + let hpTotal = Int(ceil(level1HP + extraLevelsHP)) + let conBonus = Int(conMod) * Int(hitDice) + return String(format: "%d (%dd%d%+d)", hpTotal, hitDice, Int(dieSize), conBonus) + } + } + } + + var speed: String { + get { + if (hasCustomSpeed) { + return customSpeed + } else { + var parts: [String] = [] + + if (walkSpeed > 0) { + parts.append("\(walkSpeed) ft.") + } + if (burrowSpeed > 0) { + parts.append("burrow \(burrowSpeed) ft.") + } + if (climbSpeed > 0) { + parts.append("climb \(climbSpeed) ft.") + } + if (flySpeed > 0) { + parts.append("fly \(flySpeed) ft.\(canHover ? " (hover)": "")") + } + if (swimSpeed > 0) { + parts.append("swim \(swimSpeed) ft.") + } + + return parts.joined(separator: ", ") + } + } + } + + class func hitDieForSize(_ size: SizeType) -> Int { + switch size { + case .tiny: return 4 + case .small: return 6 + case .medium: return 8 + case .large: return 10 + case .huge: return 12 + case .gargantuan: return 20 + } + } + + // MARK: Ability Scores + class func abilityModifierForScore(_ score: Int) -> Int { + return Int(floor(Double((score - 10)) / 2.0)) + } + + func abilityModifierForAbilityScore(_ abilityScore: AbilityScore) -> Int { + switch abilityScore { + case .strength: + return strengthModifier; + case .dexterity: + return dexterityModifier + case .constitution: + return constitutionModifier + case .intelligence: + return intelligenceModifier + case .wisdom: + return wisdomModifier + case .charisma: + return charismaModifier + } + } + + var strengthModifier: Int { + get { + return MonsterViewModel.abilityModifierForScore(Int(strengthScore)) + } + } + + var dexterityModifier: Int { + get { + return MonsterViewModel.abilityModifierForScore(Int(dexterityScore)) + } + } + + var constitutionModifier: Int { + get { + return MonsterViewModel.abilityModifierForScore(Int(constitutionScore)) + } + } + + var intelligenceModifier: Int { + get { + return MonsterViewModel.abilityModifierForScore(Int(intelligenceScore)) + } + } + + var wisdomModifier: Int { + get { + return MonsterViewModel.abilityModifierForScore(Int(wisdomScore)) + } + } + + var charismaModifier: Int { + get { + return MonsterViewModel.abilityModifierForScore(Int(charismaScore)) + } + } + + // MARK: Armor + + var armorClassDescription: String { + get { + var armorClassTotal = 0 + if (armorType == ArmorType.none) { + // 10 + dexMod + 2 for shieldBonus "15" or "17 (shield)" + armorClassTotal = kBaseArmorClassUnarmored + dexterityModifier + Int(shieldBonus) + return "\(armorClassTotal)\(hasShield ? " (shield)" : "")" + } else if (armorType == .naturalArmor) { + // 10 + dexMod + naturalArmorBonus + 2 for shieldBonus "16 (natural armor)" or "18 (natural armor, shield)" + armorClassTotal = kBaseArmorClassUnarmored + dexterityModifier + Int(naturalArmorBonus) + Int(shieldBonus) + return "\(armorClassTotal) (natural armor\(hasShield ? " (shield)" : ""))" + } else if (armorType == .mageArmor) { + // 10 + dexMod + 2 for shield + 3 for mage armor "15 (18 with mage armor)" or 17 (shield, 20 with mage armor) + armorClassTotal = kBaseArmorClassUnarmored + dexterityModifier + Int(shieldBonus) + let acWithMageArmor = kBaseArmorClassMageArmor + dexterityModifier + Int(shieldBonus) + return String(format: "%d (%@%d with mage armor)", armorClassTotal, (hasShield ? "shield, " : ""), acWithMageArmor) + } else if (armorType == .padded) { + // 11 + dexMod + 2 for shield "18 (padded armor, shield)" + armorClassTotal = kBaseArmorClassPadded + dexterityModifier + Int(shieldBonus) + return String(format: "%d (padded%@)", armorClassTotal, (hasShield ? "shield, " : "")) + } else if (armorType == .leather) { + // 11 + dexMod + 2 for shield "18 (leather, shield)" + armorClassTotal = kBaseArmorClassLeather + dexterityModifier + Int(shieldBonus) + return String(format:"%d (leather%@)", armorClassTotal, (hasShield ? "shield, " : "")) + } else if (armorType == .studdedLeather) { + // 12 + dexMod +2 for shield "17 (studded leather)" + armorClassTotal = kBaseArmorClassStudded + dexterityModifier + Int(shieldBonus) + return String(format: "%d (studded leather%@)", armorClassTotal, (hasShield ? "shield, " : "")) + } else if (armorType == .hide) { + // 12 + Min(2, dexMod) + 2 for shield "12 (hide armor)" + armorClassTotal = kBaseArmorClassHide + min(2, dexterityModifier) + Int(shieldBonus) + return String(format: "%d (hide%@)", armorClassTotal, (hasShield ? ", shield" : "")) + } else if (armorType == .chainShirt) { + // 13 + Min(2, dexMod) + 2 for shield "12 (chain shirt)" + armorClassTotal = kBaseArmorClassChainShirt + min(2, dexterityModifier) + Int(shieldBonus) + return String(format: "%d (chain shirt%@)", armorClassTotal, (hasShield ? ", shield" : "")) + } else if (armorType == .scaleMail) { + // 14 + Min(2, dexMod) + 2 for shield "14 (scale mail)" + armorClassTotal = kBaseArmorClassScaleMail + min(2, dexterityModifier) + Int(shieldBonus) + return String(format: "%d (scale mail%@)", armorClassTotal, (hasShield ? ", shield" : "")) + } else if (armorType == .breastplate) { + // 14 + Min(2, dexMod) + 2 for shield "16 (breastplate)" + armorClassTotal = kBaseArmorClassBreastplate + min(2, dexterityModifier) + Int(shieldBonus) + return String(format: "%d (breastplate%@)", armorClassTotal, (hasShield ? ", shield" : "")) + } else if (armorType == .halfPlate) { + // 15 + Min(2, dexMod) + 2 for shield "17 (half plate)" + armorClassTotal = kBaseArmorClassHalfPlate + min(2, dexterityModifier) + Int(shieldBonus) + return String(format: "%d (half plate%@)", armorClassTotal, (hasShield ? ", shield" : "")) + } else if (armorType == .ringMail) { + // 14 + 2 for shield "14 (ring mail) + armorClassTotal = kBaseArmorClassRingMail + Int(shieldBonus) + return String(format: "%d (ring mail%@)", armorClassTotal, (hasShield ? ", shield" : "")) + } else if (armorType == .chainMail) { + // 16 + 2 for shield "16 (chain mail)" + armorClassTotal = kBaseArmorClassChainMail + Int(shieldBonus) + return String(format: "%d (chain mail%@)", armorClassTotal, (hasShield ? ", shield" : "")) + } else if (armorType == .splintMail) { + // 17 + 2 for shield "17 (splint)" + armorClassTotal = kBaseArmorClassSplintMail + Int(shieldBonus) + return String(format: "%d (splint%@)", armorClassTotal, (hasShield ? ", shield" : "")) + } else if (armorType == .plateMail) { + // 18 + 2 for shield "18 (plate)" + armorClassTotal = kBaseArmorClassPlate + Int(shieldBonus) + return String(format: "%d (plate%@)", armorClassTotal, (hasShield ? ", shield" : "")) + } else if (armorType == .other) { + // pure string value shield check does nothing just copies the string from otherArmorDesc + return otherArmorDescription; + } else { + return "" + } + } + } + + // MARK: Challenge Rating / Proficiency Bonus + + var proficiencyBonus: Int { + switch challengeRating { + case .custom: + return Int(customProficiencyBonus) + case .zero: + fallthrough + case .oneEighth: + fallthrough + case .oneQuarter: + fallthrough + case .oneHalf: + fallthrough + case .one: + fallthrough + case .two: + fallthrough + case .three: + fallthrough + case .four: + return 2 + case .five: + fallthrough + case .six: + fallthrough + case .seven: + fallthrough + case .eight: + return 3 + case .nine: + fallthrough + case .ten: + fallthrough + case .eleven: + fallthrough + case .twelve: + return 4 + case .thirteen: + fallthrough + case .fourteen: + fallthrough + case .fifteen: + fallthrough + case .sixteen: + return 5 + case .seventeen: + fallthrough + case .eighteen: + fallthrough + case .nineteen: + fallthrough + case .twenty: + return 6 + case .twentyOne: + fallthrough + case .twentyTwo: + fallthrough + case .twentyThree: + fallthrough + case .twentyFour: + return 7 + case .twentyFive: + fallthrough + case .twentySix: + fallthrough + case .twentySeven: + fallthrough + case .twentyEight: + return 8 + case .twentyNine: + fallthrough + case .thirty: + return 9 + } + } + + func proficiencyBonusForType(_ profType: ProficiencyType) -> Int { + switch profType { + case .none: + return 0 + case .proficient: + return proficiencyBonus + case .expertise: + return proficiencyBonus * 2 + } + } + + // MARK: Saving Throws + + var savingThrowsDescription: String { + get { + // TODO: port from objective-c + var parts: [String] = [] + var name: String + var advantage: String + var bonus: Int + + if (strengthSavingThrowAdvantage != .none || strengthSavingThrowProficiency != .none) { + name = "Strength" + bonus = strengthModifier + proficiencyBonusForType(strengthSavingThrowProficiency) + advantage = MonsterViewModel.advantageLabelStringForType(strengthSavingThrowAdvantage) + if (!advantage.isEmpty) { + advantage = " " + advantage + } + parts.append(String(format: "%@ %+d%@", name, bonus, advantage)) + } + + if (dexteritySavingThrowAdvantage != .none || dexteritySavingThrowProficiency != .none) { + name = "Dexterity" + bonus = dexterityModifier + proficiencyBonusForType(dexteritySavingThrowProficiency) + advantage = MonsterViewModel.advantageLabelStringForType(dexteritySavingThrowAdvantage) + if (!advantage.isEmpty) { + advantage = " " + advantage + } + parts.append(String(format: "%@ %+d%@", name, bonus, advantage)) + } + + if (constitutionSavingThrowAdvantage != .none || constitutionSavingThrowProficiency != .none) { + name = "Constitution" + bonus = constitutionModifier + proficiencyBonusForType(constitutionSavingThrowProficiency) + advantage = MonsterViewModel.advantageLabelStringForType(constitutionSavingThrowAdvantage) + if (!advantage.isEmpty) { + advantage = " " + advantage + } + parts.append(String(format: "%@ %+d%@", name, bonus, advantage)) + } + + if (intelligenceSavingThrowAdvantage != .none || intelligenceSavingThrowProficiency != .none) { + name = "Intelligence" + bonus = intelligenceModifier + proficiencyBonusForType(intelligenceSavingThrowProficiency) + advantage = MonsterViewModel.advantageLabelStringForType(intelligenceSavingThrowAdvantage) + if (!advantage.isEmpty) { + advantage = " " + advantage + } + parts.append(String(format: "%@ %+d%@", name, bonus, advantage)) + } + + if (wisdomSavingThrowAdvantage != .none || wisdomSavingThrowProficiency != .none) { + name = "Wisdom" + bonus = wisdomModifier + proficiencyBonusForType(wisdomSavingThrowProficiency) + advantage = MonsterViewModel.advantageLabelStringForType(wisdomSavingThrowAdvantage) + if (!advantage.isEmpty) { + advantage = " " + advantage + } + parts.append(String(format: "%@ %+d%@", name, bonus, advantage)) + } + + if (charismaSavingThrowAdvantage != .none || charismaSavingThrowProficiency != .none) { + name = "Charisma" + bonus = charismaModifier + proficiencyBonusForType(charismaSavingThrowProficiency) + advantage = MonsterViewModel.advantageLabelStringForType(charismaSavingThrowAdvantage) + if (!advantage.isEmpty) { + advantage = " " + advantage + } + parts.append(String(format: "%@ %+d%@", name, bonus, advantage)) + } + + return parts.joined(separator: ", ") + } + } + + + // MARK: Misc Helpers + + class func advantageLabelStringForType(_ advType: AdvantageType) -> String { + switch advType { + case .none: + return "" + case .advantage: + return "(A)" + case .disadvantage: + return "(D)" + } + } + + // MARK: Skills + + var skillsDescription: String { + get { + let sortedSkills = self.skills.sorted {$0.name < $1.name} + return sortedSkills.reduce("") { + if $0 == "" { + return $1.skillDescription(forMonster: self) + } else { + return $0 + ", " + $1.skillDescription(forMonster: self) + } + } + } + } + + + // MARK: Immunities, Resistances, and Vulnerabilities + + var damageVulnerabilitiesDescription: String { + get { + // TODO: sort "bludgeoning, piercing, and slashing from nonmagical attacks" to the end and use ; as a separator before it. + let sortedVulnerabilities = self.damageVulnerabilities.sorted().map({$0.name}) + return StringHelper.oxfordJoin(sortedVulnerabilities) + } + } + + var damageResistancesDescription: String { + get { + // TODO: sort "bludgeoning, piercing, and slashing from nonmagical attacks" to the end and use ; as a separator before it. + let sortedResistances = self.damageResistances.sorted().map({$0.name}) + return StringHelper.oxfordJoin(sortedResistances) + } + } + + var damageImmunitiesDescription: String { + get { + // TODO: sort "bludgeoning, piercing, and slashing from nonmagical attacks" to the end and use ; as a separator before it. + let sortedImmunities = self.damageImmunities.sorted().map({$0.name}) + return StringHelper.oxfordJoin(sortedImmunities) + } + } + + var conditionImmunitiesDescription: String { + get { + let sortedImmunities = self.conditionImmunities.sorted().map({$0.name}) + return StringHelper.oxfordJoin(sortedImmunities) + } + } + + // MARK: OTHER + + var passivePerception: Int { + get { + let perceptionSkill = skills.first(where: { + StringHelper.safeEqualsIgnoreCase($0.name, "Perception") + }) + if (perceptionSkill == nil) { + return 10 + wisdomModifier + } else if (perceptionSkill!.proficiency == ProficiencyType.expertise) { + return 10 + wisdomModifier + proficiencyBonus + proficiencyBonus + } else if (perceptionSkill!.proficiency == ProficiencyType.proficient) { + return 10 + wisdomModifier + proficiencyBonus + } else { + return 10 + wisdomModifier + } + } + } + + var sensesDescription: String { + get { + var modifiedSenses = self.senses.sorted().map({$0.name}) + let hasPassivePerceptionSense = modifiedSenses.contains(where: { + $0.starts(with: "passive Perception") + }) + if (!hasPassivePerceptionSense) { + let calculatedPassivePerception = String(format: "passive Perception %d", passivePerception) + modifiedSenses.append(calculatedPassivePerception) + } + + return modifiedSenses.sorted().joined(separator: ", ") + } + } + + + var languagesDescription: String { + get { + let spokenLanguages = + languages + .filter({ $0.speaks }) + .map({$0.name}) + .sorted() + let understoodLanguages = + languages + .filter({ !$0.speaks }) + .map({$0.name}) + .sorted() + + let understandsButText = understandsBut.isEmpty + ? "" + : String(format: " but %@", understandsBut) + + let telepathyText = telepathy > 0 + ? String(format: ", telepathy %d ft.", telepathy) + : "" + + if (spokenLanguages.count > 0) { + if (understoodLanguages.count > 0) { + return String( + format:"%@ and understands %@%@%@", + StringHelper.oxfordJoin(spokenLanguages), + StringHelper.oxfordJoin(understoodLanguages), + understandsButText, + telepathyText) + } else { + return String( + format: "%@%@%@", + StringHelper.oxfordJoin(spokenLanguages), + understandsButText, + telepathyText) + } + } else { + if (understoodLanguages.count > 0) { + return String( + format: "understands %@%@%@", + StringHelper.oxfordJoin(understoodLanguages), + understandsButText, + telepathyText) + } else if (telepathy > 0){ + return String(format: "telepathy %d ft.", telepathy) + } else { + return "" + } + } + } + } + + var challengeRatingDescription: String { + get { + if (challengeRating != .custom) { + return challengeRating.displayName + } else { + return customChallengeRating + } + } + } + + // MARK: End +} diff --git a/iOS/MonsterCards/Models/SavingThrowDTO.swift b/iOS/MonsterCards/Models/SavingThrowDTO.swift new file mode 100644 index 0000000..553e82f --- /dev/null +++ b/iOS/MonsterCards/Models/SavingThrowDTO.swift @@ -0,0 +1,35 @@ +// +// SavingThrowDTO.swift +// MonsterCards +// +// Created by Tom Hicks on 3/28/21. +// + +import Foundation + +struct SavingThrowDTO { + var name: String + var order: Int +} + +private enum SavingThrowDTOCodingKeys: String, CodingKey { + case name = "name" + case order = "order" +} + +extension SavingThrowDTO: Codable { + + init(from decoder: Decoder) throws { + + let container = try decoder.container(keyedBy: SavingThrowDTOCodingKeys.self) + self.name = (try? container.decode(String.self, forKey: .name)) ?? "" + self.order = (try? container.decode(Int.self, forKey: .order)) ?? 0 + } + + func encode(to encoder: Encoder) throws { + + var container = encoder.container(keyedBy: SavingThrowDTOCodingKeys.self) + try container.encode(self.name, forKey: .name) + try container.encode(self.order, forKey: .order) + } +} diff --git a/iOS/MonsterCards/Models/Skill+CoreDataClass.swift b/iOS/MonsterCards/Models/Skill+CoreDataClass.swift new file mode 100644 index 0000000..a29477a --- /dev/null +++ b/iOS/MonsterCards/Models/Skill+CoreDataClass.swift @@ -0,0 +1,50 @@ +// +// Skill+CoreDataClass.swift +// MonsterCards +// +// Created by Tom Hicks on 1/18/21. +// +// + +import Foundation +import CoreData + +@objc(Skill) +public class Skill: NSManagedObject { + + var wrappedName: String { + get { + return name ?? "" + } + set { + name = newValue + } + } + + var wrappedProficiency: ProficiencyType { + get { + return ProficiencyType.init(rawValue: proficiency ?? "") ?? .none + } + set { + proficiency = newValue.rawValue + } + } + + var wrappedAbilityScore: AbilityScore { + get { + return AbilityScore.init(rawValue: abilityScoreName ?? "") ?? .strength + } + set { + abilityScoreName = newValue.rawValue + } + } + + var wrappedAdvantage: AdvantageType { + get { + return AdvantageType.init(rawValue: advantage ?? "") ?? .none + } + set { + advantage = newValue.rawValue + } + } +} diff --git a/iOS/MonsterCards/Models/SkillDTO.swift b/iOS/MonsterCards/Models/SkillDTO.swift new file mode 100644 index 0000000..f9f5b6a --- /dev/null +++ b/iOS/MonsterCards/Models/SkillDTO.swift @@ -0,0 +1,39 @@ +// +// SkillDTO.swift +// MonsterCards +// +// Created by Tom Hicks on 3/28/21. +// + +import Foundation + +struct SkillDTO { + var name: String + var stat: String + var note: String +} + +private enum SkillDTOCodingKeys: String, CodingKey { + case name = "name" + case stat = "stat" + case note = "note" +} + +extension SkillDTO: Codable { + + init(from decoder: Decoder) throws { + + let container = try decoder.container(keyedBy: SkillDTOCodingKeys.self) + self.name = (try? container.decode(String.self, forKey: .name)) ?? "" + self.note = (try? container.decode(String.self, forKey: .note)) ?? "" + self.stat = (try? container.decode(String.self, forKey: .stat)) ?? "" + } + + func encode(to encoder: Encoder) throws { + + var container = encoder.container(keyedBy: SkillDTOCodingKeys.self) + try container.encode(self.name, forKey: .name) + try container.encode(self.note, forKey: .note) + try container.encode(self.stat, forKey: .stat) + } +} diff --git a/iOS/MonsterCards/Models/SkillViewModel+CoreData.swift b/iOS/MonsterCards/Models/SkillViewModel+CoreData.swift new file mode 100644 index 0000000..5b40151 --- /dev/null +++ b/iOS/MonsterCards/Models/SkillViewModel+CoreData.swift @@ -0,0 +1,57 @@ +// +// SkillViewModel+CoreData.swift +// MonsterCards +// +// Created by Tom Hicks on 4/7/21. +// + +import Foundation +import CoreData + +extension SkillViewModel { + func isEqualTo(rawSkill: Skill?) -> Bool { + if (rawSkill == nil) { + return false; + } else if (abilityScore != rawSkill!.wrappedAbilityScore) { + return false; + } else if (advantage != rawSkill!.wrappedAdvantage) { + return false; + } else if (name != rawSkill!.name) { + return false; + } else if (proficiency != rawSkill!.wrappedProficiency) { + return false; + } else { + return true + } + } + + func copyToSkill(skill: Skill) { + skill.wrappedAbilityScore = abilityScore + skill.wrappedAdvantage = advantage + skill.name = name + skill.wrappedProficiency = proficiency + } + + convenience init(_ rawSkill: Skill?) { + if (rawSkill == nil) { + self.init() + } else { + let skill = rawSkill! + self.init( + skill.wrappedName, + skill.wrappedAbilityScore, + skill.wrappedProficiency, + skill.wrappedAdvantage + ) + } + } + + func buildRawSkill(context: NSManagedObjectContext?) -> Skill { + let newSkill = context == nil ? Skill.init() : Skill.init(context: context!) + newSkill.name = name + newSkill.wrappedAbilityScore = abilityScore + newSkill.wrappedProficiency = proficiency + newSkill.wrappedAdvantage = advantage + return newSkill + } +} diff --git a/iOS/MonsterCards/Models/SkillViewModel.swift b/iOS/MonsterCards/Models/SkillViewModel.swift new file mode 100644 index 0000000..9153ce4 --- /dev/null +++ b/iOS/MonsterCards/Models/SkillViewModel.swift @@ -0,0 +1,67 @@ +// +// SkillViewModel.swift +// MonsterCards +// +// Created by Tom Hicks on 1/18/21. +// + +import Foundation +import CoreData + +class SkillViewModel: ObservableObject, Identifiable { + + @Published var name: String + @Published var abilityScore: AbilityScore + @Published var proficiency: ProficiencyType + @Published var advantage: AdvantageType + + init(_ name: String = "", _ abilityScore: AbilityScore = .dexterity, _ proficiency: ProficiencyType = .proficient, _ advantage: AdvantageType = .none) { + self.name = name + self.abilityScore = abilityScore + self.proficiency = proficiency + self.advantage = advantage + } + + func modifier(forMonster: MonsterViewModel) -> Int { + let proficiencyBonus = Double(forMonster.proficiencyBonus) + let abilityScoreModifier = Double(forMonster.abilityModifierForAbilityScore(abilityScore)) + switch proficiency { + case .none: + return Int(abilityScoreModifier) + case .proficient: + return Int(abilityScoreModifier + proficiencyBonus) + case .expertise: + return Int(abilityScoreModifier + 2 * proficiencyBonus) + } + } + + func skillDescription(forMonster: MonsterViewModel) -> String { + var advantageLabel = MonsterViewModel.advantageLabelStringForType(advantage) + if (advantageLabel != "") { + advantageLabel = " " + advantageLabel + } + return String(format: "%@ %+d%@", name, modifier(forMonster: forMonster), advantageLabel) + } +} + +extension SkillViewModel: Hashable { + func hash(into hasher: inout Hasher) { + hasher.combine(abilityScore) + hasher.combine(advantage) + hasher.combine(name) + hasher.combine(proficiency) + } +} + +extension SkillViewModel: Comparable { + static func < (lhs: SkillViewModel, rhs: SkillViewModel) -> Bool { + return lhs.name < rhs.name + } + + static func == (lhs: SkillViewModel, rhs: SkillViewModel) -> Bool { + return lhs.abilityScore == rhs.abilityScore + && lhs.advantage == rhs.advantage + && lhs.name == rhs.name + && lhs.proficiency == rhs.proficiency + } +} diff --git a/iOS/MonsterCards/Models/StringViewModel.swift b/iOS/MonsterCards/Models/StringViewModel.swift new file mode 100644 index 0000000..d95e417 --- /dev/null +++ b/iOS/MonsterCards/Models/StringViewModel.swift @@ -0,0 +1,24 @@ +// +// DamageTypesViewModel.swift +// MonsterCards +// +// Created by Tom Hicks on 3/22/21. +// + +import Foundation + +class StringViewModel: ObservableObject, Comparable, Identifiable { + static func < (lhs: StringViewModel, rhs: StringViewModel) -> Bool { + lhs.name < rhs.name + } + + static func == (lhs: StringViewModel, rhs: StringViewModel) -> Bool { + lhs.name == rhs.name + } + + @Published var name: String + + init(_ name: String = "") { + self.name = name + } +} diff --git a/iOS/MonsterCards/Models/TraitDTO.swift b/iOS/MonsterCards/Models/TraitDTO.swift new file mode 100644 index 0000000..e722719 --- /dev/null +++ b/iOS/MonsterCards/Models/TraitDTO.swift @@ -0,0 +1,39 @@ +// +// TraitDTO.swift +// MonsterCards +// +// Created by Tom Hicks on 3/28/21. +// + +import Foundation + +struct TraitDTO { + var name: String + var note: String + var desc: String +} + +private enum TraitDTOCodingKeys: String, CodingKey { + case name = "name" + case note = "note" + case desc = "desc" +} + +extension TraitDTO: Codable { + + init(from decoder: Decoder) throws { + + let container = try decoder.container(keyedBy: TraitDTOCodingKeys.self) + self.name = (try? container.decode(String.self, forKey: .name)) ?? "" + self.note = (try? container.decode(String.self, forKey: .note)) ?? "" + self.desc = (try? container.decode(String.self, forKey: .desc)) ?? "" + } + + func encode(to encoder: Encoder) throws { + + var container = encoder.container(keyedBy: TraitDTOCodingKeys.self) + try container.encode(self.name, forKey: .name) + try container.encode(self.note, forKey: .note) + try container.encode(self.desc, forKey: .desc) + } +} diff --git a/iOS/MonsterCards/MonsterCards.entitlements b/iOS/MonsterCards/MonsterCards.entitlements new file mode 100644 index 0000000..90aa826 --- /dev/null +++ b/iOS/MonsterCards/MonsterCards.entitlements @@ -0,0 +1,16 @@ + + + + + aps-environment + development + com.apple.developer.icloud-container-identifiers + + iCloud.com.majinnaibu.MonsterCards.MonsterCards + + com.apple.developer.icloud-services + + CloudKit + + + diff --git a/iOS/MonsterCards/MonsterCards.xcdatamodeld/.xccurrentversion b/iOS/MonsterCards/MonsterCards.xcdatamodeld/.xccurrentversion new file mode 100644 index 0000000..93079bf --- /dev/null +++ b/iOS/MonsterCards/MonsterCards.xcdatamodeld/.xccurrentversion @@ -0,0 +1,8 @@ + + + + + _XCCurrentVersionName + MonsterCards.xcdatamodel + + diff --git a/iOS/MonsterCards/MonsterCards.xcdatamodeld/MonsterCards.xcdatamodel/contents b/iOS/MonsterCards/MonsterCards.xcdatamodeld/MonsterCards.xcdatamodel/contents new file mode 100644 index 0000000..d51cf96 --- /dev/null +++ b/iOS/MonsterCards/MonsterCards.xcdatamodeld/MonsterCards.xcdatamodel/contents @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/iOS/MonsterCards/MonsterCardsApp.swift b/iOS/MonsterCards/MonsterCardsApp.swift new file mode 100644 index 0000000..d0b292f --- /dev/null +++ b/iOS/MonsterCards/MonsterCardsApp.swift @@ -0,0 +1,20 @@ +// +// MonsterCardsApp.swift +// MonsterCards +// +// Created by Tom Hicks on 1/15/21. +// + +import SwiftUI + +@main +struct MonsterCardsApp: App { + let persistenceController = PersistenceController.shared + + var body: some Scene { + WindowGroup { + ContentView() + .environment(\.managedObjectContext, persistenceController.container.viewContext) + } + } +} diff --git a/iOS/MonsterCards/Persistence.swift b/iOS/MonsterCards/Persistence.swift new file mode 100644 index 0000000..3130ab0 --- /dev/null +++ b/iOS/MonsterCards/Persistence.swift @@ -0,0 +1,46 @@ +// +// Persistence.swift +// MonsterCards +// +// Created by Tom Hicks on 1/15/21. +// + +import CoreData + +struct PersistenceController { + static let shared = PersistenceController() + + static var preview: PersistenceController = { + let result = PersistenceController(inMemory: true) + let viewContext = result.container.viewContext + let monsters: [Monster] = [ + Monster(context:viewContext, name: "Ted", size: "Huge", type: "humanoid", subtype: "any race", alignment: "any alignment"), + Monster(context:viewContext, name: "Steve", size: "Huge", type: "humanoid", subtype: "any race", alignment: "any alignment"), + Monster(context:viewContext, name: "Dave", size: "Huge", type: "humanoid", subtype: "any race", alignment: "any alignment") + ] + do { + try viewContext.save() + } catch { + // Replace this implementation with code to handle the error appropriately. + // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. + let nsError = error as NSError +// fatalError("TOMHICKS_Unresolved error \(nsError), \(nsError.userInfo)") + } + return result + }() + + let container: NSPersistentCloudKitContainer + + init(inMemory: Bool = false) { + container = NSPersistentCloudKitContainer(name: "MonsterCards") + if inMemory { + container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") + } + container.viewContext.automaticallyMergesChangesFromParent = true + container.loadPersistentStores(completionHandler: { (storeDescription, error) in + if let error = error as NSError? { + fatalError("Unresolved error \(error), \(error.userInfo)") + } + }) + } +} diff --git a/iOS/MonsterCards/Preview Content/Preview Assets.xcassets/Contents.json b/iOS/MonsterCards/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/iOS/MonsterCards/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iOS/MonsterCards/Views/Collections.swift b/iOS/MonsterCards/Views/Collections.swift new file mode 100644 index 0000000..d8c9094 --- /dev/null +++ b/iOS/MonsterCards/Views/Collections.swift @@ -0,0 +1,21 @@ +// +// Collections.swift +// MonsterCards +// +// Created by Tom Hicks on 1/15/21. +// + +import SwiftUI + +struct Collections: View { +// @State var allCollections: [MonsterCollection] = [] + var body: some View { + Text("Collections") + } +} + +struct Collections_Previews: PreviewProvider { + static var previews: some View { + Collections().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) + } +} diff --git a/iOS/MonsterCards/Views/ContentView.swift b/iOS/MonsterCards/Views/ContentView.swift new file mode 100644 index 0000000..2acb3d5 --- /dev/null +++ b/iOS/MonsterCards/Views/ContentView.swift @@ -0,0 +1,80 @@ +// +// ContentView.swift +// MonsterCards +// +// Created by Tom Hicks on 1/15/21. +// + +import SwiftUI +import CoreData + +struct ImportInfo { + var monster: MonsterViewModel = MonsterViewModel() +} + +struct ContentView: View { + @Environment(\.managedObjectContext) private var viewContext + @State private var importInfo = ImportInfo() + @State private var isShowingImportDialog = false + + var body: some View { + TabView { + Search() + .tabItem { + Image(systemName: "magnifyingglass") + Text("Search") + } + Dashboard() + .tabItem { + Image(systemName: "rectangle.3.offgrid.fill") + Text("Dashboard") + + } + Collections() + .tabItem { + Image(systemName: "tray.full.fill") + Text("Collections") + } + Library() + .tabItem { + Image(systemName: "book.fill") + Text("Library") + } + } + .onOpenURL(perform: beginImportingMonster) + .sheet(isPresented: $isShowingImportDialog) { + ImportMonster(monster: $importInfo.monster, isOpen: $isShowingImportDialog) + } + } + + func beginImportingMonster(url: URL) { + + // TOOD: only do this if the file name ends in .json or .monster + + let decoder = JSONDecoder() + do { + let isAccessing = url.startAccessingSecurityScopedResource() + defer { + if (isAccessing) { + url.stopAccessingSecurityScopedResource() + } + } + let data = try Data(contentsOf: url) + let monsterDTO = try decoder.decode(MonsterDTO.self, from: data) + // TODO: check for some minimal set of properties to ensure this is the expected json schema + self.importInfo.monster = MonsterImportHelper.import5ESBMonster(monsterDTO) + // TODO: throw or set an err here and don't set isShowingImportDialog to true if the file didn't match any of our supported monster schemas. + self.isShowingImportDialog = true + } catch let error as NSError { + // TODO: show an error message to the user that we were unable to open the file and maybe why. + print(error) + } + } + +} + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) + } +} diff --git a/iOS/MonsterCards/Views/Dashboard.swift b/iOS/MonsterCards/Views/Dashboard.swift new file mode 100644 index 0000000..b567cc4 --- /dev/null +++ b/iOS/MonsterCards/Views/Dashboard.swift @@ -0,0 +1,20 @@ +// +// Dashboard.swift +// MonsterCards +// +// Created by Tom Hicks on 1/15/21. +// + +import SwiftUI + +struct Dashboard: View { + var body: some View { + Text("Dashboard") + } +} + +struct Dashboard_Previews: PreviewProvider { + static var previews: some View { + Dashboard().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) + } +} diff --git a/iOS/MonsterCards/Views/EditAbilityScores.swift b/iOS/MonsterCards/Views/EditAbilityScores.swift new file mode 100644 index 0000000..08c7f1c --- /dev/null +++ b/iOS/MonsterCards/Views/EditAbilityScores.swift @@ -0,0 +1,42 @@ +// +// EditAbilityScores.swift +// MonsterCards +// +// Created by Tom Hicks on 3/21/21. +// + +import SwiftUI + +struct EditAbilityScores: View { + @ObservedObject var monsterViewModel: MonsterViewModel + + var body: some View { + List {MCStepperField( + label: "STR", + value: $monsterViewModel.strengthScore) + MCStepperField( + label: "DEX", + value: $monsterViewModel.dexterityScore) + MCStepperField( + label: "CON", + value: $monsterViewModel.constitutionScore) + MCStepperField( + label: "INT", + value: $monsterViewModel.intelligenceScore) + MCStepperField( + label: "WIS", + value: $monsterViewModel.wisdomScore) + MCStepperField( + label: "CHA", + value: $monsterViewModel.charismaScore) + } + .navigationTitle("Ability Scores") + } +} + +struct EditAbilityScores_Previews: PreviewProvider { + static var previews: some View { + let viewModel = MonsterViewModel() + EditAbilityScores(monsterViewModel: viewModel) + } +} diff --git a/iOS/MonsterCards/Views/EditArmor.swift b/iOS/MonsterCards/Views/EditArmor.swift new file mode 100644 index 0000000..fa19b2b --- /dev/null +++ b/iOS/MonsterCards/Views/EditArmor.swift @@ -0,0 +1,45 @@ +// +// EditArmor.swift +// MonsterCards +// +// Created by Tom Hicks on 3/21/21. +// + +import SwiftUI + +struct EditArmor: View { + @ObservedObject var monsterViewModel: MonsterViewModel + + var body: some View { + List { + // Armor Type select bound to monster.armorTypeEnum + MCArmorTypePicker( + label: "Armor Type", + value: $monsterViewModel.armorType) + + // Toggle bound to monster.hasShield? + Toggle( + "Has Shield", + isOn: $monsterViewModel.hasShield) + + // Number with -/+ buttons bound to monster.naturalArmorBonus + MCStepperField( + label: "Natural Armor Bonus", + value: $monsterViewModel.naturalArmorBonus) + + // Editable Text field bound to monster.customArmorText? + MCTextField( + label: "Custom Armor", + value: $monsterViewModel.customArmor) + .autocapitalization(.none) + } + .navigationTitle("Armor") + } +} + +struct EditArmor_Previews: PreviewProvider { + static var previews: some View { + let viewModel = MonsterViewModel() + EditArmor(monsterViewModel: viewModel) + } +} diff --git a/iOS/MonsterCards/Views/EditBasicInfo.swift b/iOS/MonsterCards/Views/EditBasicInfo.swift new file mode 100644 index 0000000..4ef5807 --- /dev/null +++ b/iOS/MonsterCards/Views/EditBasicInfo.swift @@ -0,0 +1,71 @@ +// +// EditBasicInfo.swift +// MonsterCards +// +// Created by Tom Hicks on 3/21/21. +// + +import SwiftUI + +struct EditBasicInfo: View { + + @ObservedObject var monsterViewModel: MonsterViewModel + + var body: some View { + List { + // Editable Text field bound to monster.name + MCTextField( + label: "Name", + value: $monsterViewModel.name) + .autocapitalization(.words) + + // Editable Text field bound to monster.size + MCTextField( + label: "Size", + value: $monsterViewModel.size) + .autocapitalization(.words) + + // Editable Text field bound to monster.type + MCTextField( + label: "Type", + value: $monsterViewModel.type) + .autocapitalization(.none) + + // Editable Text field bound to monster.subType + MCTextField( + label: "Subtype", + value: $monsterViewModel.subType) + .autocapitalization(.none) + + // Editable Text field bound to monster.alignment + MCTextField( + label: "Alignment", + value: $monsterViewModel.alignment) + .autocapitalization(.none) + + // Number with -/+ buttons bound to monster.hitDice + MCStepperField( + label: "Hit Dice", + value: $monsterViewModel.hitDice) + + // Toggle bound to monster.hasCustomHP? + Toggle( + "Has Custom HP", + isOn:$monsterViewModel.hasCustomHP) + + // Editable Text field bound to monster.customHpText? + MCTextField( + label: "Custom HP", + value: $monsterViewModel.customHP) + .autocapitalization(.none) + } + .navigationTitle("Basic Info") + } +} + +struct EditBasicInfo_Previews: PreviewProvider { + static var previews: some View { + let viewModel = MonsterViewModel.init(nil) + EditBasicInfo(monsterViewModel: viewModel) + } +} diff --git a/iOS/MonsterCards/Views/EditChallengeRating.swift b/iOS/MonsterCards/Views/EditChallengeRating.swift new file mode 100644 index 0000000..4042e7a --- /dev/null +++ b/iOS/MonsterCards/Views/EditChallengeRating.swift @@ -0,0 +1,41 @@ +// +// EditChallengeRating.swift +// MonsterCards +// +// Created by Tom Hicks on 3/24/21. +// + +import SwiftUI + +struct EditChallengeRating: View { + @ObservedObject var viewModel: MonsterViewModel + + var body: some View { + let isUsingCustomProficiencyBonus = viewModel.challengeRating == ChallengeRating.custom + + VStack(alignment: .leading) { + MCChallengeRatingPicker( + label: "Rating", + value: $viewModel.challengeRating) + + MCTextField( + label: "Custom Text", + value: $viewModel.customChallengeRating) + .disabled(!isUsingCustomProficiencyBonus) + + MCStepperField( + label: "Custom Proficiency Bonus", + value: $viewModel.customProficiencyBonus) + .disabled(!isUsingCustomProficiencyBonus) + Spacer() + } + .padding() + } +} + +struct EditChallengeRating_Previews: PreviewProvider { + static var previews: some View { + let viewModel = MonsterViewModel() + EditChallengeRating(viewModel: viewModel) + } +} diff --git a/iOS/MonsterCards/Views/EditLanguage.swift b/iOS/MonsterCards/Views/EditLanguage.swift new file mode 100644 index 0000000..c6b07f0 --- /dev/null +++ b/iOS/MonsterCards/Views/EditLanguage.swift @@ -0,0 +1,33 @@ +// +// EditLanguage.swift +// MonsterCards +// +// Created by Tom Hicks on 3/24/21. +// + +import SwiftUI + +struct EditLanguage: View { + @ObservedObject var viewModel: LanguageViewModel + + var body: some View { + VStack(alignment: .leading) { + MCTextField( + label: "Name", + value: $viewModel.name) + .autocapitalization(.none) + + Toggle("Speaks", isOn: $viewModel.speaks) + + Spacer() + } + .padding() + } +} + +struct EditLanguage_Previews: PreviewProvider { + static var previews: some View { + let viewModel = LanguageViewModel() + EditLanguage(viewModel: viewModel) + } +} diff --git a/iOS/MonsterCards/Views/EditLanguages.swift b/iOS/MonsterCards/Views/EditLanguages.swift new file mode 100644 index 0000000..7ba30e2 --- /dev/null +++ b/iOS/MonsterCards/Views/EditLanguages.swift @@ -0,0 +1,58 @@ +// +// EditLanguages.swift +// MonsterCards +// +// Created by Tom Hicks on 3/24/21. +// + +import SwiftUI + +struct EditLanguages: View { + @ObservedObject var viewModel: MonsterViewModel + + var body: some View { + let sortedLanguages = viewModel.languages.sorted() + List { + MCTextField( + label: "Understands But", + value: $viewModel.understandsBut) + + MCStepperField(label: "Telepathy", prefix: "", step: 5, suffix: " ft.", value: $viewModel.telepathy) + + ForEach(sortedLanguages/*viewModel.languages*/) { language in + NavigationLink(language.name, destination: EditLanguage(viewModel: language)) + } + .onDelete(perform: { indexSet in + for index in indexSet { + viewModel.languages.remove(at: index) + } + }) + } + .toolbar(content: { + ToolbarItemGroup(placement: .navigationBarTrailing) { + EditButton() + + Button( + action: { + let newLanguage = LanguageViewModel("English") + viewModel.languages.append(newLanguage) + viewModel.languages = viewModel.languages.sorted() + }, + label: { + Image(systemName: "plus") + } + ) + } + }) + .onAppear(perform: { + viewModel.languages = viewModel.languages.sorted() + }) + } +} + +struct EditLanguages_Previews: PreviewProvider { + static var previews: some View { + let viewModel = MonsterViewModel() + EditLanguages(viewModel: viewModel) + } +} diff --git a/iOS/MonsterCards/Views/EditMonster.swift b/iOS/MonsterCards/Views/EditMonster.swift new file mode 100644 index 0000000..9a962b2 --- /dev/null +++ b/iOS/MonsterCards/Views/EditMonster.swift @@ -0,0 +1,220 @@ +// +// EditMonster.swift +// MonsterCards +// +// Created by Tom Hicks on 1/16/21. +// + +import CoreData +import SwiftUI + +struct EditMonster: View { + // TODO: Add challengeRating/challengeRatingEnum and customChallengeRating maybe in basicInfo + // TODO: Add a way to edit the monster being blind. Probably a header section to the senses section. + @Environment(\.presentationMode) var presentationMode: Binding + @Environment(\.managedObjectContext) private var viewContext + + var monster: Monster + + @StateObject private var monsterViewModel: MonsterViewModel = MonsterViewModel() + @State private var hasInitializedViewModel = false + + var body: some View { + List { + Group { + NavigationLink( + "Basic Info", + destination: EditBasicInfo(monsterViewModel: monsterViewModel)) + + NavigationLink( + "Armor", + destination: EditArmor(monsterViewModel: monsterViewModel)) + + NavigationLink( + "Speed", + destination: EditSpeed(monsterViewModel: monsterViewModel)) + + NavigationLink( + "Ability Scores", + destination: EditAbilityScores(monsterViewModel: monsterViewModel)) + + NavigationLink( + "Saving Throws", + destination: EditSavingThrows(monsterViewModel: monsterViewModel)) + + NavigationLink( + "Skills", + destination: EditSkills(monsterViewModel: monsterViewModel)) + + NavigationLink( + "Condition Immunities", + destination: EditStrings( + viewModel: monsterViewModel, + path: \.conditionImmunities, + title: "Condition Immunities")) + + NavigationLink( + "Damage Immunities", + destination: EditStrings( + viewModel: monsterViewModel, + path: \.damageImmunities, + title: "Damage Immunities")) + + NavigationLink( + "Damage Resistances", + destination: EditStrings( + viewModel: monsterViewModel, + path: \.damageResistances, + title: "Damage Resistances")) + + NavigationLink( + "Damage Vulnerabilities", + destination: EditStrings( + viewModel: monsterViewModel, + path: \.damageVulnerabilities, + title: "Damage Vulnerabilities")) + } + Group { + NavigationLink( + "Senses", + destination: EditStrings( + viewModel: monsterViewModel, + path: \.senses, + title: "Senses")) + + NavigationLink( + "Languages", + destination: EditLanguages(viewModel: monsterViewModel)) + + NavigationLink( + "Challenge Rating", + destination: EditChallengeRating(viewModel: monsterViewModel)) + + NavigationLink( + "Abilities", + destination: EditTraits( + viewModel: monsterViewModel, + path: \.abilities, + title: "Abilities")) + + NavigationLink( + "Actions", + destination: EditTraits( + viewModel: monsterViewModel, + path: \.actions, + title: "Actions")) + + NavigationLink( + "Reactions", + destination: EditTraits( + viewModel: monsterViewModel, + path: \.reactions, + title: "Reactions")) + + NavigationLink( + "Legendary Actions", + destination: EditTraits( + viewModel: monsterViewModel, + path: \.legendaryActions, + title: "Legendary Actions")) + + NavigationLink( + "Lair Actions", + destination: EditTraits( + viewModel: monsterViewModel, + path: \.lairActions, + title: "Lair Actions")) + + NavigationLink( + "Regional Actions", + destination: EditTraits( + viewModel: monsterViewModel, + path: \.regionalActions, + title: "Regional Actions")) + } + + } + .onAppear(perform: copyMonsterToLocal) + .toolbar(content: { + ToolbarItem(placement: .primaryAction) { + Button("Save", action: saveMonster) + } + }) + .navigationTitle(monsterViewModel.name) + .navigationBarTitleDisplayMode(.inline) + } + + private func dismissView() { + self.presentationMode.wrappedValue.dismiss() + } + + private func saveMonster() { + copyLocalToMonster() + + do { + // Save core data context + try viewContext.save() + } catch { + // Replace this implementation with code to handle the error appropriately. + // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. + let nsError = error as NSError + fatalError("Unresolved error \(nsError), \(nsError.userInfo)") + } + dismissView() + } + + private func copyMonsterToLocal() { + if (!hasInitializedViewModel) { + monsterViewModel.copyFromMonster(monster: monster) + hasInitializedViewModel = true + } + } + + private func copyLocalToMonster() { + monsterViewModel.copyToMonster(monster: monster) + } +} + +struct EditMonster_Previews: PreviewProvider { + static var previews: some View { + let context = PersistenceController.preview.container.viewContext + let monster = Monster.init(context: context) + + monster.name = "Steve" + monster.size = "Medium" + monster.type = "humanoid" + monster.subtype = "human" + monster.alignment = "LG" + monster.hitDice = 6 + monster.hasCustomHP = true + monster.customHP = "12 (1d10)+2" + monster.walkSpeed = 5 + monster.burrowSpeed = 10 + monster.climbSpeed = 15 + monster.flySpeed = 20 + monster.swimSpeed = 25 + monster.canHover = true + monster.hasCustomSpeed = false + monster.customSpeed = "walk: 5 ft." + monster.strengthScore = 8 + monster.dexterityScore = 10 + monster.constitutionScore = 12 + monster.intelligenceScore = 14 + monster.wisdomScore = 16 + monster.charismaScore = 18 + monster.strengthSavingThrowAdvantage = AdvantageType.none.rawValue + monster.strengthSavingThrowProficiency = ProficiencyType.none.rawValue + monster.dexteritySavingThrowAdvantage = AdvantageType.advantage.rawValue + monster.dexteritySavingThrowProficiency = ProficiencyType.proficient.rawValue + monster.constitutionSavingThrowAdvantage = AdvantageType.disadvantage.rawValue + monster.constitutionSavingThrowProficiency = ProficiencyType.expertise.rawValue + monster.intelligenceSavingThrowAdvantage = AdvantageType.none.rawValue + monster.intelligenceSavingThrowProficiency = ProficiencyType.expertise.rawValue + monster.wisdomSavingThrowAdvantage = AdvantageType.advantage.rawValue + monster.wisdomSavingThrowProficiency = ProficiencyType.proficient.rawValue + monster.charismaSavingThrowAdvantage = AdvantageType.disadvantage.rawValue + monster.charismaSavingThrowProficiency = ProficiencyType.none.rawValue + + return EditMonster(monster: monster).environment(\.managedObjectContext, context) + } +} diff --git a/iOS/MonsterCards/Views/EditSavingThrows.swift b/iOS/MonsterCards/Views/EditSavingThrows.swift new file mode 100644 index 0000000..71c6736 --- /dev/null +++ b/iOS/MonsterCards/Views/EditSavingThrows.swift @@ -0,0 +1,80 @@ +// +// EditSavingThrows.swift +// MonsterCards +// +// Created by Tom Hicks on 3/21/21. +// + +import SwiftUI + +struct EditSavingThrows: View { + @ObservedObject var monsterViewModel: MonsterViewModel + + var body: some View { + List { + // TODO: Add a version of this layout for wider screens where these VStacks with HStacks + VStack { + MCAdvantagePicker( + label: "Strength Advantage", + value: $monsterViewModel.strengthSavingThrowAdvantage) + + MCProficiencyPicker( + label: "Strength Proficiency", + value: $monsterViewModel.strengthSavingThrowProficiency) + } + VStack { + MCAdvantagePicker( + label: "Dexterity Advantage", + value: $monsterViewModel.dexteritySavingThrowAdvantage) + + MCProficiencyPicker( + label: "Dexterity Proficiency", + value: $monsterViewModel.dexteritySavingThrowProficiency) + } + VStack { + MCAdvantagePicker( + label: "Constitution Advantage", + value: $monsterViewModel.constitutionSavingThrowAdvantage) + + MCProficiencyPicker( + label: "Constitution Proficiency", + value: $monsterViewModel.constitutionSavingThrowProficiency) + } + VStack { + MCAdvantagePicker( + label: "Intelligence Advantage", + value: $monsterViewModel.intelligenceSavingThrowAdvantage) + + MCProficiencyPicker( + label: "Intelligence Proficiency", + value: $monsterViewModel.intelligenceSavingThrowProficiency) + } + VStack { + MCAdvantagePicker( + label: "Wisdom Advantage", + value: $monsterViewModel.wisdomSavingThrowAdvantage) + + MCProficiencyPicker( + label: "Wisdom Proficiency", + value: $monsterViewModel.wisdomSavingThrowProficiency) + } + VStack { + MCAdvantagePicker( + label: "Charisma Advantage", + value: $monsterViewModel.charismaSavingThrowAdvantage) + + MCProficiencyPicker( + label: "Charisma Proficiency", + value: $monsterViewModel.charismaSavingThrowProficiency) + } + } + .navigationTitle("Saving Throws") + } +} + +struct EditSavingThrows_Previews: PreviewProvider { + static var previews: some View { + let viewModel = MonsterViewModel() + EditSavingThrows(monsterViewModel: viewModel) + } +} diff --git a/iOS/MonsterCards/Views/EditSkill.swift b/iOS/MonsterCards/Views/EditSkill.swift new file mode 100644 index 0000000..b98065c --- /dev/null +++ b/iOS/MonsterCards/Views/EditSkill.swift @@ -0,0 +1,42 @@ +// +// EditSkill.swift +// MonsterCards +// +// Created by Tom Hicks on 3/21/21. +// + +import SwiftUI + +struct EditSkill: View { + @ObservedObject var skillViewModel: SkillViewModel + + var body: some View { + List { + MCTextField( + label: "Name", + value: $skillViewModel.name) + .autocapitalization(.words) + + MCAbilityScorePicker( + label: "Ability Score", + value: $skillViewModel.abilityScore) + + // TODO: Add a version of this layout for wider screens where these two are in an HStack + MCAdvantagePicker( + label: "Advantage", + value: $skillViewModel.advantage) + + MCProficiencyPicker( + label: "Proficiency", + value: $skillViewModel.proficiency) + } + .navigationTitle("Skill") + } +} + +struct EditSkill_Previews: PreviewProvider { + static var previews: some View { + let viewModel = SkillViewModel() + EditSkill(skillViewModel: viewModel) + } +} diff --git a/iOS/MonsterCards/Views/EditSkills.swift b/iOS/MonsterCards/Views/EditSkills.swift new file mode 100644 index 0000000..0cfcc11 --- /dev/null +++ b/iOS/MonsterCards/Views/EditSkills.swift @@ -0,0 +1,49 @@ +// +// EditSkills.swift +// MonsterCards +// +// Created by Tom Hicks on 3/21/21. +// + +import SwiftUI + +struct EditSkills: View { + @ObservedObject var monsterViewModel: MonsterViewModel + + var body: some View { + List { + ForEach(monsterViewModel.skills) { skill in + NavigationLink(skill.name, destination: EditSkill(skillViewModel: skill)) + } + .onDelete(perform: { indexSet in + for index in indexSet { + monsterViewModel.skills.remove(at: index) + } + }) + } + .toolbar(content: { + Button( + action: { + let newSkill = SkillViewModel() + newSkill.name = "" + newSkill.proficiency = .proficient + monsterViewModel.skills.append(newSkill) + }, + label: { + Image(systemName: "plus") + } + ) + }) + .navigationTitle("Skills") + .onAppear(perform: { + monsterViewModel.skills = monsterViewModel.skills.sorted() + }) + } +} + +struct EditSkills_Previews: PreviewProvider { + static var previews: some View { + let viewModel = MonsterViewModel() + EditSkills(monsterViewModel: viewModel) + } +} diff --git a/iOS/MonsterCards/Views/EditSpeed.swift b/iOS/MonsterCards/Views/EditSpeed.swift new file mode 100644 index 0000000..e935bd0 --- /dev/null +++ b/iOS/MonsterCards/Views/EditSpeed.swift @@ -0,0 +1,75 @@ +// +// EditSpeed.swift +// MonsterCards +// +// Created by Tom Hicks on 3/21/21. +// + +import SwiftUI + +struct EditSpeed: View { + @ObservedObject var monsterViewModel: MonsterViewModel + + var body: some View { + List { + // Number bound to monster.walkSpeed + MCStepperField( + label: "Base", + step: 5, + suffix: " ft.", + value: $monsterViewModel.walkSpeed) + + // Number bound to monster.burrowSpeed + MCStepperField( + label: "Burrow", + step: 5, + suffix: " ft.", + value: $monsterViewModel.burrowSpeed) + + // Number bound to monster.climbSpeed + MCStepperField( + label: "Climb", + step: 5, + suffix: " ft.", + value: $monsterViewModel.climbSpeed) + + // Number bound to monster.flySpeed + MCStepperField( + label: "Fly", + step: 5, + suffix: " ft.", + value: $monsterViewModel.flySpeed) + + // Toggle bound to monster.canHover + Toggle( + "Can Hover", + isOn: $monsterViewModel.canHover) + + // Number bound to monster.swimSpeed + MCStepperField( + label: "Swim", + step: 5, + suffix: " ft.", + value: $monsterViewModel.swimSpeed) + + // Toggle bound to monster.hasCustomSpeed + Toggle( + "Has Custom Speed", + isOn: $monsterViewModel.hasCustomSpeed) + + // Editable Text field bound to monster.customSpeedText + MCTextField( + label: "Custom Speed", + value: $monsterViewModel.customSpeed) + .autocapitalization(.none) + } + .navigationTitle("Speed") + } +} + +struct EditSpeed_Previews: PreviewProvider { + static var previews: some View { + let viewModel = MonsterViewModel() + EditSpeed(monsterViewModel: viewModel) + } +} diff --git a/iOS/MonsterCards/Views/EditStrings.swift b/iOS/MonsterCards/Views/EditStrings.swift new file mode 100644 index 0000000..2e35e6a --- /dev/null +++ b/iOS/MonsterCards/Views/EditStrings.swift @@ -0,0 +1,60 @@ +// +// EditStrings.swift +// MonsterCards +// +// Created by Tom Hicks on 3/22/21. +// + +import SwiftUI + +struct EditStrings: View { + @ObservedObject var viewModel: MonsterViewModel + var path: ReferenceWritableKeyPath + var title: String + + var body: some View { + List { + ForEach(viewModel[keyPath: path]) { damageType in + TextField( + "", + text: Binding( + get: {damageType.name}, + set: {damageType.name = $0} + ) + ) + .autocapitalization(.none) + } + .onDelete(perform: { indexSet in + for index in indexSet { + viewModel[keyPath: path].remove(at: index) + } + }) + } + .toolbar(content: { + Button( + action: { + let newString = StringViewModel() + viewModel[keyPath: path].append(newString) + viewModel[keyPath: path] = viewModel[keyPath: path].sorted() + }, + label: { + Image(systemName: "plus") + } + ) + }) + .onAppear(perform: { + viewModel[keyPath: path] = viewModel[keyPath: path].sorted() + }) + .navigationTitle(title) + } +} + +struct EditStrings_Previews: PreviewProvider { + static var previews: some View { + let viewModel = MonsterViewModel() + EditStrings( + viewModel: viewModel, + path: \.damageImmunities, + title: "Damage Types") + } +} diff --git a/iOS/MonsterCards/Views/EditTrait.swift b/iOS/MonsterCards/Views/EditTrait.swift new file mode 100644 index 0000000..a0ef193 --- /dev/null +++ b/iOS/MonsterCards/Views/EditTrait.swift @@ -0,0 +1,34 @@ +// +// EditTrait.swift +// MonsterCards +// +// Created by Tom Hicks on 3/25/21. +// + +import SwiftUI + +struct EditTrait: View { + @ObservedObject var viewModel: AbilityViewModel + + var body: some View { + VStack(alignment: .leading) { + MCTextField( + label: "Name", + value: $viewModel.name) + + Text("Description") + .font(.caption2) + + TextEditor(text: $viewModel.abilityDescription) + + } + .padding() + } +} + +struct EditTrait_Previews: PreviewProvider { + static var previews: some View { + let viewModel = AbilityViewModel() + EditTrait(viewModel: viewModel) + } +} diff --git a/iOS/MonsterCards/Views/EditTraits.swift b/iOS/MonsterCards/Views/EditTraits.swift new file mode 100644 index 0000000..7b534ad --- /dev/null +++ b/iOS/MonsterCards/Views/EditTraits.swift @@ -0,0 +1,64 @@ +// +// EditTraits.swift +// MonsterCards +// +// Created by Tom Hicks on 3/25/21. +// + +import SwiftUI + +struct EditTraits: View { + @ObservedObject var viewModel: MonsterViewModel + var path: ReferenceWritableKeyPath + var title: String + + var body: some View { + List { + ForEach(viewModel[keyPath: path]) { ability in + NavigationLink( + ability.name, + destination: EditTrait(viewModel: ability)) + } + .onDelete(perform: { indexSet in + for index in indexSet { + viewModel[keyPath: path].remove(at: index) + } + }) + .onMove(perform: { indices, newOffset in + viewModel[keyPath: path].move(fromOffsets: indices, toOffset: newOffset) + + }) + } + .toolbar(content: { + ToolbarItemGroup(placement: .navigationBarTrailing) { + EditButton() + + Button( + action: { + let newAbility = AbilityViewModel() + viewModel[keyPath: path].append(newAbility) + viewModel[keyPath: path] = viewModel[keyPath: path].sorted() + }, + label: { + Image(systemName: "plus") + } + ) + } + }) + .onAppear(perform: { + viewModel[keyPath: path] = viewModel[keyPath: path].sorted() + }) + .navigationTitle(title) + + } +} + +struct EditTraits_Previews: PreviewProvider { + static var previews: some View { + let viewModel = MonsterViewModel() + EditTraits( + viewModel: viewModel, + path: \.abilities, + title: "Abilities") + } +} diff --git a/iOS/MonsterCards/Views/ImportMonster.swift b/iOS/MonsterCards/Views/ImportMonster.swift new file mode 100644 index 0000000..ae83665 --- /dev/null +++ b/iOS/MonsterCards/Views/ImportMonster.swift @@ -0,0 +1,80 @@ +// +// ImportMonster.swift +// MonsterCards +// +// Created by Tom Hicks on 4/1/21. +// + +import SwiftUI + +struct ImportMonster: View { + @Binding var monster: MonsterViewModel + @Environment(\.managedObjectContext) private var viewContext + @Binding var isOpen: Bool + + var body: some View { + VStack{ + HStack { + Button("Cancel", action: cancelImport) + Spacer() + Button("Add to Library", action: saveNewMonster) + } + MonsterDetailView(viewModel: monster) + } + .padding() + } + + func cancelImport() { + isOpen = false + } + + func saveNewMonster() { + print("Saving monster: \(monster.name)") + withAnimation { + let newMonster = Monster(context: viewContext) + monster.copyToMonster(monster: newMonster) + + do { + try viewContext.save() + isOpen = false + } 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)") + } + } + } +} + +struct ImportMonster_Previews: PreviewProvider { + static var previews: some View { + let monster = MonsterViewModel() + monster.name = "Steve" + monster.size = "Medium" + monster.type = "dwarf" + monster.alignment = "chaotic good" + monster.armorType = .none + monster.hasShield = true + monster.hitDice = 4 + monster.strengthScore = 20 + monster.dexterityScore = 14 + monster.constitutionScore = 18 + monster.intelligenceScore = 8 + monster.wisdomScore = 8 + monster.charismaScore = 15 + monster.walkSpeed = 40 + monster.challengeRating = .four + monster.languages = [LanguageViewModel("Common", true), LanguageViewModel("Giant", true)] + monster.actions = [AbilityViewModel("Greataxe, +3", "__Badass Attack:___ Hits the other dude on a _3_ or above and does a ton of damage")] + + return + VStack{ + Text("Hello, World!") + } + .sheet(isPresented: .constant(true)) { + ImportMonster(monster: .constant(monster), isOpen: .constant(true)) + } + + } +} diff --git a/iOS/MonsterCards/Views/Library.swift b/iOS/MonsterCards/Views/Library.swift new file mode 100644 index 0000000..8566476 --- /dev/null +++ b/iOS/MonsterCards/Views/Library.swift @@ -0,0 +1,77 @@ +// +// Library.swift +// MonsterCards +// +// Created by Tom Hicks on 1/15/21. +// + +import SwiftUI + +struct Library: View { + // TODO: add an import button that searches https://api.open5e.com/monsters/ and lets you import a monster from there + // TODO: add an import button that lets you browse for a tetra cube monster file and import it + @Environment(\.managedObjectContext) private var viewContext + @FetchRequest( + sortDescriptors: [ + NSSortDescriptor(keyPath: \Monster.name, ascending: true), + ], + animation: .default) + var allMonsters: FetchedResults + + var body: some View { + NavigationView{ + List { + ForEach(allMonsters) { monster in + NavigationLink(destination: MonsterDetailWrapper(monster: monster)) { + Text(monster.name ?? "") + } + } + .onDelete(perform: { indexSet in + for index in indexSet { + let monster = allMonsters[index] + viewContext.delete(monster) + 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)") + } + } + }) + } + .navigationTitle("Library") + .navigationBarTitleDisplayMode(.inline) + .toolbar(content: { + ToolbarItem(placement: .primaryAction) { + Button(action: addMonster) { + Image(systemName:"plus") + } + } + }) + } + } + + private func addMonster() { + withAnimation { + let newItem = Monster(context: viewContext) + newItem.name = "Unnamed Monster" + + 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)") + } + } + } +} + +struct Library_Previews: PreviewProvider { + static var previews: some View { + return Library().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) + } +} diff --git a/iOS/MonsterCards/Views/MCAbilityScorePicker.swift b/iOS/MonsterCards/Views/MCAbilityScorePicker.swift new file mode 100644 index 0000000..cc4b94d --- /dev/null +++ b/iOS/MonsterCards/Views/MCAbilityScorePicker.swift @@ -0,0 +1,35 @@ +// +// MCAbilityScorePicker.swift +// MonsterCards +// +// Created by Tom Hicks on 2/15/21. +// + +import SwiftUI + +struct MCAbilityScorePicker: View { + var label: String = "" + var value: Binding + var body: some View { + VStack(alignment: .leading) { + Text(label) + .font(.caption2) + Picker( + selection: value, + label: Text(value.wrappedValue.displayName)) { + ForEach(AbilityScore.allCases) {abilityScore in + Text(abilityScore.displayName).tag(abilityScore) + } + } + .pickerStyle(MenuPickerStyle()) + } + } +} + +struct MCAbilityScorePicker_Previews: PreviewProvider { + static var previews: some View { + MCAbilityScorePicker( + label: "Ability Score", + value: .constant(AbilityScore.strength)) + } +} diff --git a/iOS/MonsterCards/Views/MCAdvantagePicker.swift b/iOS/MonsterCards/Views/MCAdvantagePicker.swift new file mode 100644 index 0000000..e895867 --- /dev/null +++ b/iOS/MonsterCards/Views/MCAdvantagePicker.swift @@ -0,0 +1,39 @@ +// +// MCAdvantagePicker.swift +// MonsterCards +// +// Created by Tom Hicks on 1/17/21. +// + +import SwiftUI + +struct MCAdvantagePicker: View { + var label: String = "" + var value: Binding + + var body: some View { + VStack(alignment: .leading) { + Text(label) + .font(.caption2) + Picker( + selection: value, + label: Text(label)) { + ForEach(AdvantageType.allCases) { advType in + Text(advType.displayName).tag(advType) + } + } + .pickerStyle(SegmentedPickerStyle()) +// .pickerStyle(SegmentedPickerStyle()) +// .pickerStyle(WheelPickerStyle()) +// .pickerStyle(DefaultPickerStyle()) +// .pickerStyle(InlinePickerStyle()) +// .pickerStyle(MenuPickerStyle()) + } + } +} + +struct MCAdvantagePicker_Previews: PreviewProvider { + static var previews: some View { + MCAdvantagePicker(value: .constant(AdvantageType.none)) + } +} diff --git a/iOS/MonsterCards/Views/MCArmorTypePicker.swift b/iOS/MonsterCards/Views/MCArmorTypePicker.swift new file mode 100644 index 0000000..a59b6df --- /dev/null +++ b/iOS/MonsterCards/Views/MCArmorTypePicker.swift @@ -0,0 +1,36 @@ +// +// MCArmorTypePicker.swift +// MonsterCards +// +// Created by Tom Hicks on 2/6/21. +// + +import SwiftUI + +struct MCArmorTypePicker: View { + var label: String = "" + var value: Binding + + var body: some View { + VStack(alignment: .leading) { + Text(label) + .font(.caption2) + Picker( + selection: value, + label: Text(value.wrappedValue.displayName)) { + ForEach(ArmorType.allCases) {armorType in + Text(armorType.displayName).tag(armorType) + } + } + .pickerStyle(MenuPickerStyle()) + } + } +} + +struct MCArmorTypePicker_Previews: PreviewProvider { + static var previews: some View { + MCArmorTypePicker( + value: .constant(ArmorType.none) + ) + } +} diff --git a/iOS/MonsterCards/Views/MCChallengeRatingPicker.swift b/iOS/MonsterCards/Views/MCChallengeRatingPicker.swift new file mode 100644 index 0000000..2a88c30 --- /dev/null +++ b/iOS/MonsterCards/Views/MCChallengeRatingPicker.swift @@ -0,0 +1,35 @@ +// +// MCChallengeRatingPicker.swift +// MonsterCards +// +// Created by Tom Hicks on 3/24/21. +// + +import SwiftUI + +struct MCChallengeRatingPicker: View { + var label: String = "" + var value: Binding + var body: some View { + VStack(alignment: .leading) { + Text(label) + .font(.caption2) + Picker( + selection: value, + label: Text(value.wrappedValue.displayName)) { + ForEach(ChallengeRating.allCases) {abilityScore in + Text(abilityScore.displayName).tag(abilityScore) + } + } + .pickerStyle(MenuPickerStyle()) + } + } +} + +struct MCChallengeRatingPicker_Previews: PreviewProvider { + static var previews: some View { + MCChallengeRatingPicker( + label: "Rating", + value: .constant(ChallengeRating.ten)) + } +} diff --git a/iOS/MonsterCards/Views/MCProficiencyPicker.swift b/iOS/MonsterCards/Views/MCProficiencyPicker.swift new file mode 100644 index 0000000..6ccd39d --- /dev/null +++ b/iOS/MonsterCards/Views/MCProficiencyPicker.swift @@ -0,0 +1,34 @@ +// +// MCProficiencyPicker.swift +// MonsterCards +// +// Created by Tom Hicks on 1/17/21. +// + +import SwiftUI + +struct MCProficiencyPicker: View { + var label: String = "" + var value: Binding + + var body: some View { + VStack(alignment: .leading) { + Text(label) + .font(.caption2) + Picker( + selection: value, + label: Text(label)) { + ForEach(ProficiencyType.allCases) { profType in + Text(profType.displayName).tag(profType) + } + } + .pickerStyle(SegmentedPickerStyle()) + } + } +} + +struct MCProficiencyPicker_Previews: PreviewProvider { + static var previews: some View { + MCProficiencyPicker(value: .constant(ProficiencyType.none)) + } +} diff --git a/iOS/MonsterCards/Views/MCStepperField.swift b/iOS/MonsterCards/Views/MCStepperField.swift new file mode 100644 index 0000000..68e41f9 --- /dev/null +++ b/iOS/MonsterCards/Views/MCStepperField.swift @@ -0,0 +1,37 @@ +// +// MCStepperField.swift +// MonsterCards +// +// Created by Tom Hicks on 1/16/21. +// + +import SwiftUI + +struct MCStepperField: View { + var label: String = "" + var prefix: String = "" + var step: Int = 1 + var suffix: String = "" + var value: Binding + + var body: some View { + VStack(alignment: .leading) { + Text(label) + .font(.caption2) + Stepper( + value: value, + step: step + ) { + Text("\(prefix)\(value.wrappedValue)\(suffix)" as String) + } + } + } +} + +struct MCStepperField_Previews: PreviewProvider { + static var previews: some View { + MCStepperField( + label: "Hit Dice", + value: .constant(4)) + } +} diff --git a/iOS/MonsterCards/Views/MCTextField.swift b/iOS/MonsterCards/Views/MCTextField.swift new file mode 100644 index 0000000..74da62c --- /dev/null +++ b/iOS/MonsterCards/Views/MCTextField.swift @@ -0,0 +1,26 @@ +// +// MCTextField.swift +// MonsterCards +// +// Created by Tom Hicks on 1/16/21. +// + +import SwiftUI + +struct MCTextField: View { + var label: String + var value: Binding + var body: some View { + VStack(alignment: .leading) { + Text(label) + .font(.caption2) + TextField(label, text: value) + } + } +} + +struct MCTextField_Previews: PreviewProvider { + static var previews: some View { + MCTextField(label: "Name", value: .constant("Ted")) + } +} diff --git a/iOS/MonsterCards/Views/MonsterDetailView.swift b/iOS/MonsterCards/Views/MonsterDetailView.swift new file mode 100644 index 0000000..b9ae5d6 --- /dev/null +++ b/iOS/MonsterCards/Views/MonsterDetailView.swift @@ -0,0 +1,374 @@ +// +// MonsterDetail.swift +// MonsterCards +// +// Created by Tom Hicks on 1/15/21. +// + +import SwiftUI +import MarkdownUI + +struct LabeledField: View { + @Environment(\.horizontalSizeClass) var sizeClass + + let content: Content + let label: String + + @inlinable public init( + _ label: String, + @ViewBuilder content: () -> Content) { + self.content = content() + self.label = label + } + + var body: some View { + if (sizeClass == .compact) { + VStack(alignment: .leading) { + Text(label) + .fontWeight(.bold) + content + } + } else { + HStack(alignment: .top) { + Text(label) + .fontWeight(.bold) + content + } + } + } +} + +struct SectionDivider: View { + var body: some View { + Image("section-divider") // divider + .resizable() + .scaledToFit() + .padding(.vertical, 4) + } +} + +struct SmallAbilityScore: View { + var label: String + var score: Int64 + var modifier: Int + + public init( + _ label: String, + _ score: Int64, + _ modifier: Int) { + self.label = label + self.score = score + self.modifier = modifier + } + + var body: some View { + VStack { + Text(label) + .fontWeight(.bold) + Text(String(format: "%d (%+d)", score, modifier)) + } + .frame(maxWidth: .infinity) + } +} + +struct BasicInfoView: View { + @ObservedObject var monster: MonsterViewModel + + var body: some View { + let monsterMeta = monster.meta + let monsterArmorClassDescription = monster.armorClassDescription + let monsterHitPoints = monster.hitPoints + let monsterSpeed = monster.speed + + if (!monster.name.isEmpty) { + Text(monster.name) + .font(.largeTitle) + } + + // meta: "(large humanoid (elf) lawful evil" + if (!monsterMeta.isEmpty) { + Text(monsterMeta) + .font(.subheadline) + .foregroundColor(.secondary) + } + + SectionDivider() + + // AC + if (!monsterArmorClassDescription.isEmpty) { + LabeledField("Armor Class") { + Text(monsterArmorClassDescription)// armor class + } + } + + // HP + if (!monsterHitPoints.isEmpty) { + LabeledField("Hit Points") { + Text(monsterHitPoints) // hit points + } + } + + // Speed + if (!monsterSpeed.isEmpty) { + LabeledField("Speed") { + Text(monsterSpeed) // speed + } + } + } +} + +struct AbilityScoresView: View { + @ObservedObject var monster: MonsterViewModel + + var body: some View { + SectionDivider() + + // Ability Scores + HStack { + SmallAbilityScore("STR", monster.strengthScore, monster.strengthModifier) + SmallAbilityScore("DEX", monster.dexterityScore, monster.dexterityModifier) + SmallAbilityScore("CON", monster.constitutionScore, monster.constitutionModifier) + SmallAbilityScore("INT", monster.intelligenceScore, monster.intelligenceModifier) + SmallAbilityScore("WIS", monster.wisdomScore, monster.wisdomModifier) + SmallAbilityScore("CHA", monster.charismaScore, monster.charismaModifier) + } + } +} + +struct ResistancesAndImmunitiesView: View { + @ObservedObject var monster: MonsterViewModel + + var body: some View { + let monsterDamageVulnerabilitiesDescription = monster.damageVulnerabilitiesDescription + let monsterDamageResistancesDescription = monster.damageResistancesDescription + let monsterDamageImmunitiesDescription = monster.damageImmunitiesDescription + let monsterConditionImmunitiesDescription = monster.conditionImmunitiesDescription + let monsterSensesDescription = monster.sensesDescription + + // Damage Vulnerabilities + if (!monsterDamageVulnerabilitiesDescription.isEmpty) { + LabeledField("Damage Vulnerabilities") { + Text(monsterDamageVulnerabilitiesDescription) + } + } + + // Damage Resistances + if (!monsterDamageResistancesDescription.isEmpty) { + LabeledField("Damage Resistances") { + Text(monsterDamageResistancesDescription) + } + } + + // Damage Immunities + if (!monsterDamageImmunitiesDescription.isEmpty) { + LabeledField("Damage Immunities") { + Text(monsterDamageImmunitiesDescription) + } + } + + // Condition Immunities + if (!monsterConditionImmunitiesDescription.isEmpty) { + LabeledField("Condition Immunities") { + Text(monsterConditionImmunitiesDescription) + } + } + + // Senses + if (!monsterSensesDescription.isEmpty) { + LabeledField("Senses") { + Text(monsterSensesDescription) + } + } + } +} + +struct SavingThrowsAndSkillsView: View { + @ObservedObject var monster: MonsterViewModel + + var body: some View { + let savingThrowsDescription = monster.savingThrowsDescription + let skillsDescription = monster.skillsDescription + + // Saving Throws + if (!savingThrowsDescription.isEmpty) { + LabeledField("Saving Throws") { + Text(savingThrowsDescription) + } + } + + // Skills + if (!skillsDescription.isEmpty) { + LabeledField("Skills") { + Text(skillsDescription) + } + } + } +} + +struct TraitList: View { + var title: String + var traits: [AbilityViewModel] + var viewModel: MonsterViewModel + + var body: some View { + VStack(alignment: .leading) { + Text(title) + .font(.system(size: 20, weight: .bold)) + ForEach(traits) { action in + VStack { + Markdown(Document(action.renderedText(viewModel))) + Divider() + } + } + } + + } +} + +struct MonsterDetailView: View { + let kTextColor = Color(hex: 0x982818) + + @ObservedObject var viewModel: MonsterViewModel + + var body: some View { + let monsterLanguagesDescription = viewModel.languagesDescription + let monsterChallengeRatingDescription = viewModel.challengeRatingDescription + + ScrollView { + // TODO: Consider adding an inmage here at the top + VStack (alignment: .leading) { + // 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 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 + + BasicInfoView(monster: viewModel) + AbilityScoresView(monster: viewModel) + SectionDivider() + SavingThrowsAndSkillsView(monster: viewModel) + ResistancesAndImmunitiesView(monster: viewModel) + Group { + // Languages + if (!monsterLanguagesDescription.isEmpty) { + LabeledField("Languages") { + Text(monsterLanguagesDescription) + } + } + + // Challenge Rating + if (!monsterChallengeRatingDescription.isEmpty) { + LabeledField("Challenge") { + Text(monsterChallengeRatingDescription) + } + } + + // Proficiency Bonus + LabeledField("Proficiency Bonus") { + Text(String(viewModel.proficiencyBonus)) + } + + // Abilities + if (viewModel.abilities.count > 0) { + ForEach(viewModel.abilities) { ability in + VStack { + Markdown(Document(ability.renderedText(viewModel))) + Divider() + } + } + } + + // Actions + if (viewModel.actions.count > 0) { + TraitList( + title:"Actions", + traits: viewModel.actions, + viewModel: viewModel) + } + + // Reactions + if (viewModel.reactions.count > 0) { + TraitList( + title:"Reactions", + traits: viewModel.reactions, + viewModel: viewModel) + } + + // Legendary Actions + if (viewModel.legendaryActions.count > 0) { + TraitList( + title:"Legendary Actions", + traits: viewModel.legendaryActions, + viewModel: viewModel) + } + + // Lair Actions + if (viewModel.lairActions.count > 0) { + TraitList( + title: "Lair Actions", + traits: viewModel.lairActions, + viewModel: viewModel) + } + + // Regional Actions + if (viewModel.regionalActions.count > 0) { + TraitList( + title: "Regional Actions", + traits: viewModel.regionalActions, + viewModel: viewModel) + } + } + } + .padding(.horizontal) + .foregroundColor(kTextColor) + } + } +} + +struct MonsterDetailView_Previews: PreviewProvider { + static var previews: some View { + let monster = MonsterViewModel.init() + monster.name = "Steve" + monster.size = "Medium" + monster.type = "humanoid" + monster.subType = "human" + monster.alignment = "LG" + monster.hitDice = 6 + monster.hasCustomHP = true + monster.customHP = "12 (1d10)+2" + monster.walkSpeed = 5 + monster.burrowSpeed = 10 + monster.climbSpeed = 15 + monster.flySpeed = 20 + monster.swimSpeed = 25 + monster.canHover = true + monster.hasCustomSpeed = false + monster.customSpeed = "walk: 5 ft." + monster.strengthScore = 8 + monster.dexterityScore = 10 + monster.constitutionScore = 12 + monster.intelligenceScore = 14 + monster.wisdomScore = 16 + monster.charismaScore = 18 + monster.strengthSavingThrowAdvantage = AdvantageType.none + monster.strengthSavingThrowProficiency = ProficiencyType.none + monster.dexteritySavingThrowAdvantage = AdvantageType.advantage + monster.dexteritySavingThrowProficiency = ProficiencyType.proficient + monster.constitutionSavingThrowAdvantage = AdvantageType.disadvantage + monster.constitutionSavingThrowProficiency = ProficiencyType.expertise + monster.intelligenceSavingThrowAdvantage = AdvantageType.none + monster.intelligenceSavingThrowProficiency = ProficiencyType.expertise + monster.wisdomSavingThrowAdvantage = AdvantageType.advantage + monster.wisdomSavingThrowProficiency = ProficiencyType.proficient + monster.charismaSavingThrowAdvantage = AdvantageType.disadvantage + monster.charismaSavingThrowProficiency = ProficiencyType.none + monster.telepathy = 1 + monster.languages = [ + LanguageViewModel("English", true), + LanguageViewModel("French", false) + ] + + return Group { + MonsterDetailView(viewModel: monster) + } + } +} diff --git a/iOS/MonsterCards/Views/MonsterDetailWrapper.swift b/iOS/MonsterCards/Views/MonsterDetailWrapper.swift new file mode 100644 index 0000000..e24123b --- /dev/null +++ b/iOS/MonsterCards/Views/MonsterDetailWrapper.swift @@ -0,0 +1,89 @@ +// +// MonsterDetailWrapper.swift +// MonsterCards +// +// Created by Tom Hicks on 4/7/21. +// + +import SwiftUI + +struct MonsterDetailWrapper: View { + let kTextColor = 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: { + ToolbarItem(placement: .primaryAction) { + NavigationLink("Edit", destination: EditMonster(monster: monster)) + } + }) + .navigationTitle(monster.name ?? "") + .navigationBarTitleDisplayMode(.inline) + } + + private func editMonster() { + print("Edit Monster pressed") + } +} + +struct MonsterDetailWrapper_Previews: PreviewProvider { + static var previews: some View { + let context = PersistenceController.preview.container.viewContext + let monster = Monster.init(context: context) + monster.name = "Steve" + monster.size = "Medium" + monster.type = "humanoid" + monster.subtype = "human" + monster.alignment = "LG" + monster.hitDice = 6 + monster.hasCustomHP = true + monster.customHP = "12 (1d10)+2" + monster.walkSpeed = 5 + monster.burrowSpeed = 10 + monster.climbSpeed = 15 + monster.flySpeed = 20 + monster.swimSpeed = 25 + monster.canHover = true + monster.hasCustomSpeed = false + monster.customSpeed = "walk: 5 ft." + monster.strengthScore = 8 + monster.dexterityScore = 10 + monster.constitutionScore = 12 + monster.intelligenceScore = 14 + monster.wisdomScore = 16 + monster.charismaScore = 18 + monster.strengthSavingThrowAdvantageEnum = AdvantageType.none + monster.strengthSavingThrowProficiencyEnum = ProficiencyType.none + monster.dexteritySavingThrowAdvantageEnum = AdvantageType.advantage + monster.dexteritySavingThrowProficiencyEnum = ProficiencyType.proficient + monster.constitutionSavingThrowAdvantageEnum = AdvantageType.disadvantage + monster.constitutionSavingThrowProficiencyEnum = ProficiencyType.expertise + monster.intelligenceSavingThrowAdvantageEnum = AdvantageType.none + monster.intelligenceSavingThrowProficiencyEnum = ProficiencyType.expertise + monster.wisdomSavingThrowAdvantageEnum = AdvantageType.advantage + monster.wisdomSavingThrowProficiencyEnum = ProficiencyType.proficient + monster.charismaSavingThrowAdvantageEnum = AdvantageType.disadvantage + monster.charismaSavingThrowProficiencyEnum = ProficiencyType.none + monster.telepathy = 1 + monster.languages = [ + LanguageViewModel("English", true), + LanguageViewModel("French", false) + ] + + return Group { + MonsterDetailWrapper(monster: monster) + .environment(\.managedObjectContext, context) + .previewDevice("iPod touch (7th generation)") + MonsterDetailWrapper(monster: monster) + .environment(\.managedObjectContext, context) + .previewDevice("iPad Pro (11-inch) (2nd generation)") + } + } +} diff --git a/iOS/MonsterCards/Views/Search.swift b/iOS/MonsterCards/Views/Search.swift new file mode 100644 index 0000000..0b2c295 --- /dev/null +++ b/iOS/MonsterCards/Views/Search.swift @@ -0,0 +1,75 @@ +// +// Search.swift +// MonsterCards +// +// Created by Tom Hicks on 1/15/21. +// + +import SwiftUI + +struct Search: View { + @State private var searchText = "" + + @Environment(\.managedObjectContext) var managedObjectContext + @FetchRequest( + sortDescriptors: [ + NSSortDescriptor(keyPath: \Monster.name, ascending: true), + ], + animation: .default) + var allMonsters: FetchedResults + + var body: some View { + NavigationView { + List { + SearchBar(text: $searchText) + .padding(.top, -30) + + ForEach( + allMonsters.filter( + { + // TODO: consider splitting search text into words and if each word appears in any of these fields return true e.g, "large demon" would match large in size and demon in type. + // TODO: add tags and search by tags + // TODO: add a display of what fields matched on each item in the results + // TODO: make the criteria configurable from this screen + // TODO: find a way to add challenge rating as a search criteria + + if (searchText.isEmpty) { + return true + } + + if (StringHelper.safeContainsCaseInsensitive($0.name, searchText)) { + return true + } + + if (StringHelper.safeContainsCaseInsensitive($0.size, searchText)) { + return true + } + + if (StringHelper.safeContainsCaseInsensitive($0.type, searchText)) { + return true + } + + if (StringHelper.safeContainsCaseInsensitive($0.subtype, searchText)) { + return true + } + + if (StringHelper.safeContainsCaseInsensitive($0.alignment, searchText)) { + return true + } + + return false + })) { monster in + NavigationLink(destination: MonsterDetailWrapper(monster: monster)) { + Text(monster.name ?? "") + } + } + } + }.navigationViewStyle(StackNavigationViewStyle()) + } +} + +struct Search_Previews: PreviewProvider { + static var previews: some View { + Search().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) + } +} diff --git a/iOS/MonsterCards/Views/SearchBar.swift b/iOS/MonsterCards/Views/SearchBar.swift new file mode 100644 index 0000000..115c0a2 --- /dev/null +++ b/iOS/MonsterCards/Views/SearchBar.swift @@ -0,0 +1,63 @@ +// +// SearchBar.swift +// MonsterCards +// +// Created by Tom Hicks on 1/15/21. +// + +import Foundation +import UIKit +import SwiftUI + +struct SearchBar: UIViewRepresentable { + + @Binding var text: String + + class Coordinator: NSObject, UISearchBarDelegate { + @Binding var text: String + + init(text: Binding) { + _text = text + } + + func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { + text = searchText + } + + func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { + UIApplication.shared.endEditing() + } + } + + func makeCoordinator() -> SearchBar.Coordinator { + return Coordinator(text: $text) + } + + func makeUIView(context: UIViewRepresentableContext) -> UISearchBar { + let searchBar = UISearchBar(frame: .zero) + searchBar.delegate = context.coordinator + searchBar.autocapitalizationType = .none + return searchBar + } + + func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext) { + uiView.text = text + } +} + +extension String { + func containsCaseInsensitive(_ string: String) -> Bool { + return self.localizedCaseInsensitiveContains(string) + } +} + +extension UIApplication { + func endEditing() { + sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) + } +} +struct SearchBar_Previews: PreviewProvider { + static var previews: some View { + SearchBar(text: .constant("")) + } +} diff --git a/iOS/MonsterCardsTests/Info.plist b/iOS/MonsterCardsTests/Info.plist new file mode 100644 index 0000000..64d65ca --- /dev/null +++ b/iOS/MonsterCardsTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/iOS/MonsterCardsTests/MonsterCardsTests.swift b/iOS/MonsterCardsTests/MonsterCardsTests.swift new file mode 100644 index 0000000..9b1f4ea --- /dev/null +++ b/iOS/MonsterCardsTests/MonsterCardsTests.swift @@ -0,0 +1,33 @@ +// +// MonsterCardsTests.swift +// MonsterCardsTests +// +// Created by Tom Hicks on 1/15/21. +// + +import XCTest +@testable import MonsterCards + +class MonsterCardsTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/iOS/MonsterCardsUITests/Info.plist b/iOS/MonsterCardsUITests/Info.plist new file mode 100644 index 0000000..64d65ca --- /dev/null +++ b/iOS/MonsterCardsUITests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/iOS/MonsterCardsUITests/MonsterCardsUITests.swift b/iOS/MonsterCardsUITests/MonsterCardsUITests.swift new file mode 100644 index 0000000..92fbd4d --- /dev/null +++ b/iOS/MonsterCardsUITests/MonsterCardsUITests.swift @@ -0,0 +1,42 @@ +// +// MonsterCardsUITests.swift +// MonsterCardsUITests +// +// Created by Tom Hicks on 1/15/21. +// + +import XCTest + +class MonsterCardsUITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use recording to get started writing UI tests. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } + } +} diff --git a/iOS/MonsterPreview/Base.lproj/MainInterface.storyboard b/iOS/MonsterPreview/Base.lproj/MainInterface.storyboard new file mode 100644 index 0000000..0486cd4 --- /dev/null +++ b/iOS/MonsterPreview/Base.lproj/MainInterface.storyboard @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS/MonsterPreview/Info.plist b/iOS/MonsterPreview/Info.plist new file mode 100644 index 0000000..ecf8cab --- /dev/null +++ b/iOS/MonsterPreview/Info.plist @@ -0,0 +1,40 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + MonsterPreview + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSExtension + + NSExtensionAttributes + + QLSupportedContentTypes + + com.majinnaibu.MonsterCards.Monster + + QLSupportsSearchableItems + + + NSExtensionMainStoryboard + MainInterface + NSExtensionPointIdentifier + com.apple.quicklook.preview + + + diff --git a/iOS/MonsterPreview/PreviewViewController.swift b/iOS/MonsterPreview/PreviewViewController.swift new file mode 100644 index 0000000..618c794 --- /dev/null +++ b/iOS/MonsterPreview/PreviewViewController.swift @@ -0,0 +1,43 @@ +// +// PreviewViewController.swift +// MonsterPreview +// +// Created by Tom Hicks on 4/7/21. +// + +import UIKit +import QuickLook +import SwiftUI + +class PreviewViewController: UIViewController, QLPreviewingController { + + func preparePreviewOfFile(at url: URL, completionHandler handler: @escaping (Error?) -> Void) { + + let document = MonsterDocument(fileURL: url) + document.open(completionHandler: {[weak self](_) in + self?.presentMonsterViewController(for: document) + handler(nil) + }) + } + + func presentMonsterViewController(for document: MonsterDocument) { + let monsterViewModel = MonsterImportHelper.import5ESBMonster(document.monsterDTO ?? MonsterDTO()) + let monsterViewController = UIHostingController(rootView: MonsterDetailView(viewModel: monsterViewModel)) + monsterViewController.loadViewIfNeeded() + monsterViewController.view.layoutIfNeeded() + addChild(monsterViewController) + view.addSubview(monsterViewController.view) + monsterViewController.didMove(toParent: self) + + if let monsterView = monsterViewController.view { + monsterView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + monsterView.leftAnchor.constraint(equalTo: view.leftAnchor), + monsterView.rightAnchor.constraint(equalTo: view.rightAnchor), + monsterView.topAnchor.constraint(equalTo: view.topAnchor), + monsterView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + } + } + +} diff --git a/iOS/README.md b/iOS/README.md new file mode 100644 index 0000000..c67c4dd --- /dev/null +++ b/iOS/README.md @@ -0,0 +1,24 @@ +# MonsterCards + +master: [![Build status](https://build.appcenter.ms/v0.1/apps/bbde5430-0b21-4b78-aa2f-32ce210fc578/branches/master/badge)](https://appcenter.ms) +beta: [![Build status](https://build.appcenter.ms/v0.1/apps/bbde5430-0b21-4b78-aa2f-32ce210fc578/branches/beta/badge)](https://appcenter.ms) +develop: [![Build status](https://build.appcenter.ms/v0.1/apps/bbde5430-0b21-4b78-aa2f-32ce210fc578/branches/develop/badge)](https://appcenter.ms) + +MonsterCards let you create and store monster statblocks for 5e compatible sytems. You can also import the files created by the popular 5e stat block generator at [https://tetra-cube.com/dnd/dnd-statblock.html] You monster library is synced between your devices using iCloud. You'll need to create your own app ids, provisioning profiles, and iCloud storage if building from source. Due to the way iCloud works. Building from source will mean you sync data to a different iCloud container than using the official app. The app is currently in limited beta but will be available in the app store as soon as the collection and dashboard features are finished. + +## Coming Soon + +These are things we intend to complete before launching v1.0 + +* Collections - You will be able to group your saved monsters in collections. This is useful for grouping monsters by session/location/campaign. +* Dashboard - A set of monster thumbnails with vital stats that you can open to view full monster cards. This is useful for running encounters or having all the monsters you need for a session handy +* Native export/import format - We will be adding a file format for sharing monsters with other users so you can easily share creations to other users. + +## Wishlist + +These may never happen. + +* [Android version](https://github.com/headhunter45/MonsterCards-Android) +* Other import formats - We're taking suggestions +* Initiative Tracker - A basic initiative/hit point tracker. +* NFC - Put an NFC sticker on a mini and scan the mini to show it's card. diff --git a/iOS/docs/.gitignore b/iOS/docs/.gitignore new file mode 100644 index 0000000..f40fbd8 --- /dev/null +++ b/iOS/docs/.gitignore @@ -0,0 +1,5 @@ +_site +.sass-cache +.jekyll-cache +.jekyll-metadata +vendor diff --git a/iOS/docs/404.html b/iOS/docs/404.html new file mode 100644 index 0000000..086a5c9 --- /dev/null +++ b/iOS/docs/404.html @@ -0,0 +1,25 @@ +--- +permalink: /404.html +layout: default +--- + + + +
+

404

+ +

Page not found :(

+

The requested page could not be found.

+
diff --git a/iOS/docs/Gemfile b/iOS/docs/Gemfile new file mode 100644 index 0000000..a1bbc53 --- /dev/null +++ b/iOS/docs/Gemfile @@ -0,0 +1,33 @@ +source "https://rubygems.org" +# Hello! This is where you manage which Jekyll version is used to run. +# When you want to use a different version, change it below, save the +# file and run `bundle install`. Run Jekyll with `bundle exec`, like so: +# +# bundle exec jekyll serve +# +# This will help ensure the proper Jekyll version is running. +# Happy Jekylling! +# gem "jekyll", "~> 4.2.0" +gem "github-pages", "~> 213", group: :jekyll_plugins +# This is the default theme for new Jekyll sites. You may change this to anything you like. +gem "minima", "~> 2.5" +# If you want to use GitHub Pages, remove the "gem "jekyll"" above and +# uncomment the line below. To upgrade, run `bundle update github-pages`. +# gem "github-pages", group: :jekyll_plugins +# If you have any plugins, put them here! +group :jekyll_plugins do + gem "jekyll-feed", "~> 0.12" +end + +# Windows and JRuby does not include zoneinfo files, so bundle the tzinfo-data gem +# and associated library. +platforms :mingw, :x64_mingw, :mswin, :jruby do + gem "tzinfo", "~> 1.2" + gem "tzinfo-data" +end + +# Performance-booster for watching directories on Windows +gem "wdm", "~> 0.1.1", :platforms => [:mingw, :x64_mingw, :mswin] + + +gem "webrick", "~> 1.7" diff --git a/iOS/docs/Gemfile.lock b/iOS/docs/Gemfile.lock new file mode 100644 index 0000000..dd81607 --- /dev/null +++ b/iOS/docs/Gemfile.lock @@ -0,0 +1,272 @@ +GEM + remote: https://rubygems.org/ + specs: + activesupport (6.0.3.5) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 0.7, < 2) + minitest (~> 5.1) + tzinfo (~> 1.1) + zeitwerk (~> 2.2, >= 2.2.2) + addressable (2.7.0) + public_suffix (>= 2.0.2, < 5.0) + coffee-script (2.4.1) + coffee-script-source + execjs + coffee-script-source (1.11.1) + colorator (1.1.0) + commonmarker (0.17.13) + ruby-enum (~> 0.5) + concurrent-ruby (1.1.8) + dnsruby (1.61.5) + simpleidn (~> 0.1) + em-websocket (0.5.2) + eventmachine (>= 0.12.9) + http_parser.rb (~> 0.6.0) + ethon (0.12.0) + ffi (>= 1.3.0) + eventmachine (1.2.7) + execjs (2.7.0) + faraday (1.3.0) + faraday-net_http (~> 1.0) + multipart-post (>= 1.2, < 3) + ruby2_keywords + faraday-net_http (1.0.1) + ffi (1.15.0) + forwardable-extended (2.6.0) + gemoji (3.0.1) + github-pages (213) + github-pages-health-check (= 1.17.0) + jekyll (= 3.9.0) + jekyll-avatar (= 0.7.0) + jekyll-coffeescript (= 1.1.1) + jekyll-commonmark-ghpages (= 0.1.6) + jekyll-default-layout (= 0.1.4) + jekyll-feed (= 0.15.1) + jekyll-gist (= 1.5.0) + jekyll-github-metadata (= 2.13.0) + jekyll-mentions (= 1.6.0) + jekyll-optional-front-matter (= 0.3.2) + jekyll-paginate (= 1.1.0) + jekyll-readme-index (= 0.3.0) + jekyll-redirect-from (= 0.16.0) + jekyll-relative-links (= 0.6.1) + jekyll-remote-theme (= 0.4.3) + jekyll-sass-converter (= 1.5.2) + jekyll-seo-tag (= 2.7.1) + jekyll-sitemap (= 1.4.0) + jekyll-swiss (= 1.0.0) + jekyll-theme-architect (= 0.1.1) + jekyll-theme-cayman (= 0.1.1) + jekyll-theme-dinky (= 0.1.1) + jekyll-theme-hacker (= 0.1.2) + jekyll-theme-leap-day (= 0.1.1) + jekyll-theme-merlot (= 0.1.1) + jekyll-theme-midnight (= 0.1.1) + jekyll-theme-minimal (= 0.1.1) + jekyll-theme-modernist (= 0.1.1) + jekyll-theme-primer (= 0.5.4) + jekyll-theme-slate (= 0.1.1) + jekyll-theme-tactile (= 0.1.1) + jekyll-theme-time-machine (= 0.1.1) + jekyll-titles-from-headings (= 0.5.3) + jemoji (= 0.12.0) + kramdown (= 2.3.0) + kramdown-parser-gfm (= 1.1.0) + liquid (= 4.0.3) + mercenary (~> 0.3) + minima (= 2.5.1) + nokogiri (>= 1.10.4, < 2.0) + rouge (= 3.26.0) + terminal-table (~> 1.4) + github-pages-health-check (1.17.0) + addressable (~> 2.3) + dnsruby (~> 1.60) + octokit (~> 4.0) + public_suffix (>= 2.0.2, < 5.0) + typhoeus (~> 1.3) + html-pipeline (2.14.0) + activesupport (>= 2) + nokogiri (>= 1.4) + http_parser.rb (0.6.0) + i18n (0.9.5) + concurrent-ruby (~> 1.0) + jekyll (3.9.0) + addressable (~> 2.4) + colorator (~> 1.0) + em-websocket (~> 0.5) + i18n (~> 0.7) + jekyll-sass-converter (~> 1.0) + jekyll-watch (~> 2.0) + kramdown (>= 1.17, < 3) + liquid (~> 4.0) + mercenary (~> 0.3.3) + pathutil (~> 0.9) + rouge (>= 1.7, < 4) + safe_yaml (~> 1.0) + jekyll-avatar (0.7.0) + jekyll (>= 3.0, < 5.0) + jekyll-coffeescript (1.1.1) + coffee-script (~> 2.2) + coffee-script-source (~> 1.11.1) + jekyll-commonmark (1.3.1) + commonmarker (~> 0.14) + jekyll (>= 3.7, < 5.0) + jekyll-commonmark-ghpages (0.1.6) + commonmarker (~> 0.17.6) + jekyll-commonmark (~> 1.2) + rouge (>= 2.0, < 4.0) + jekyll-default-layout (0.1.4) + jekyll (~> 3.0) + jekyll-feed (0.15.1) + jekyll (>= 3.7, < 5.0) + jekyll-gist (1.5.0) + octokit (~> 4.2) + jekyll-github-metadata (2.13.0) + jekyll (>= 3.4, < 5.0) + octokit (~> 4.0, != 4.4.0) + jekyll-mentions (1.6.0) + html-pipeline (~> 2.3) + jekyll (>= 3.7, < 5.0) + jekyll-optional-front-matter (0.3.2) + jekyll (>= 3.0, < 5.0) + jekyll-paginate (1.1.0) + jekyll-readme-index (0.3.0) + jekyll (>= 3.0, < 5.0) + jekyll-redirect-from (0.16.0) + jekyll (>= 3.3, < 5.0) + jekyll-relative-links (0.6.1) + jekyll (>= 3.3, < 5.0) + jekyll-remote-theme (0.4.3) + addressable (~> 2.0) + jekyll (>= 3.5, < 5.0) + jekyll-sass-converter (>= 1.0, <= 3.0.0, != 2.0.0) + rubyzip (>= 1.3.0, < 3.0) + jekyll-sass-converter (1.5.2) + sass (~> 3.4) + jekyll-seo-tag (2.7.1) + jekyll (>= 3.8, < 5.0) + jekyll-sitemap (1.4.0) + jekyll (>= 3.7, < 5.0) + jekyll-swiss (1.0.0) + jekyll-theme-architect (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-theme-cayman (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-theme-dinky (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-theme-hacker (0.1.2) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-leap-day (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-theme-merlot (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-theme-midnight (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-theme-minimal (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-theme-modernist (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-theme-primer (0.5.4) + jekyll (> 3.5, < 5.0) + jekyll-github-metadata (~> 2.9) + jekyll-seo-tag (~> 2.0) + jekyll-theme-slate (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-theme-tactile (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-theme-time-machine (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-titles-from-headings (0.5.3) + jekyll (>= 3.3, < 5.0) + jekyll-watch (2.2.1) + listen (~> 3.0) + jemoji (0.12.0) + gemoji (~> 3.0) + html-pipeline (~> 2.2) + jekyll (>= 3.0, < 5.0) + kramdown (2.3.0) + rexml + kramdown-parser-gfm (1.1.0) + kramdown (~> 2.0) + liquid (4.0.3) + listen (3.5.0) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) + mercenary (0.3.6) + minima (2.5.1) + jekyll (>= 3.5, < 5.0) + jekyll-feed (~> 0.9) + jekyll-seo-tag (~> 2.1) + minitest (5.14.4) + multipart-post (2.1.1) + nokogiri (1.11.2-arm64-darwin) + racc (~> 1.4) + nokogiri (1.11.2-x86_64-darwin) + racc (~> 1.4) + octokit (4.20.0) + faraday (>= 0.9) + sawyer (~> 0.8.0, >= 0.5.3) + pathutil (0.16.2) + forwardable-extended (~> 2.6) + public_suffix (4.0.6) + racc (1.5.2) + rb-fsevent (0.10.4) + rb-inotify (0.10.1) + ffi (~> 1.0) + rexml (3.2.4) + rouge (3.26.0) + ruby-enum (0.9.0) + i18n + ruby2_keywords (0.0.4) + rubyzip (2.3.0) + safe_yaml (1.0.5) + sass (3.7.4) + sass-listen (~> 4.0.0) + sass-listen (4.0.0) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) + sawyer (0.8.2) + addressable (>= 2.3.5) + faraday (> 0.8, < 2.0) + simpleidn (0.2.1) + unf (~> 0.1.4) + terminal-table (1.8.0) + unicode-display_width (~> 1.1, >= 1.1.1) + thread_safe (0.3.6) + typhoeus (1.4.0) + ethon (>= 0.9.0) + tzinfo (1.2.9) + thread_safe (~> 0.1) + unf (0.1.4) + unf_ext + unf_ext (0.0.7.7) + unicode-display_width (1.7.0) + webrick (1.7.0) + zeitwerk (2.4.2) + +PLATFORMS + universal-darwin-20 + +DEPENDENCIES + github-pages (~> 213) + jekyll-feed (~> 0.12) + minima (~> 2.5) + tzinfo (~> 1.2) + tzinfo-data + wdm (~> 0.1.1) + webrick (~> 1.7) + +BUNDLED WITH + 2.2.16 diff --git a/iOS/docs/_config.yml b/iOS/docs/_config.yml new file mode 100644 index 0000000..443c01d --- /dev/null +++ b/iOS/docs/_config.yml @@ -0,0 +1,60 @@ +# Welcome to Jekyll! +# +# This config file is meant for settings that affect your whole blog, values +# which you are expected to set up once and rarely edit after that. If you find +# yourself editing this file very often, consider using Jekyll's data files +# feature for the data you need to update frequently. +# +# For technical reasons, this file is *NOT* reloaded automatically when you use +# 'bundle exec jekyll serve'. If you change this file, please restart the server process. +# +# If you need help with YAML syntax, here are some quick references for you: +# https://learn-the-web.algonquindesign.ca/topics/markdown-yaml-cheat-sheet/#yaml +# https://learnxinyminutes.com/docs/yaml/ +# +# Site settings +# These are used to personalize your new site. If you look in the HTML files, +# you will see them accessed via {{ site.title }}, {{ site.email }}, and so on. +# You can create any custom variable you would like, and they will be accessible +# in the templates via {{ site.myvariable }}. + +title: Monster Cards +email: monstercardsapp@gmail.com +# description: >- # this means to ignore newlines until "baseurl:" +# Write an awesome description for your new site here. You can edit this +# line in _config.yml. It will appear in your document head meta (for +# Google search results) and in your feed.xml site description. +baseurl: "/MonsterCards-iOS" # the subpath of your site, e.g. /blog +url: "https://headhunter45.github.io/" # the base hostname & protocol for your site, e.g. http://example.com +# twitter_username: jekyllrb +github_username: headhunter45 + +# Build settings +theme: jekyll-theme-tactile +plugins: + # - jekyll-feed + +github: + public: true + is_project_page: true + repository_url: https://github.com/headhunter45/MonsterCards-iOS + +# Exclude from processing. +# The following items will not be processed, by default. +# Any item listed under the `exclude:` key here will be automatically added to +# the internal "default list". +# +# Excluded items can be processed by explicitly listing the directories or +# their entries' file path in the `include:` list. +# +# exclude: +# - .sass-cache/ +# - .jekyll-cache/ +# - gemfiles/ +# - Gemfile +# - Gemfile.lock +# - node_modules/ +# - vendor/bundle/ +# - vendor/cache/ +# - vendor/gems/ +# - vendor/ruby/ diff --git a/iOS/docs/_layouts/default.html b/iOS/docs/_layouts/default.html new file mode 100644 index 0000000..0f7837d --- /dev/null +++ b/iOS/docs/_layouts/default.html @@ -0,0 +1,69 @@ + + + + + + + + + + + +{% seo %} + + + +
+
+ +
+

{{ page.title | default: site.title | default: site.github.repository_name }}

+

{{ page.description | default: site.description | default: site.github.project_tagline }}

+ +
+
+ {% if site.show_downloads %} + Download .zip + Download .tar.gz + {% endif %} + {% if site.github.public %} + {% if site.github.is_project_page %} + View on GitHub + {% else %} + View on GitHub + {% endif %} + {% endif %} +
+
+
+ {{ content }} +
+ + + +
+
+ + {% if site.google_analytics %} + + {% endif %} + + \ No newline at end of file diff --git a/iOS/docs/about.markdown b/iOS/docs/about.markdown new file mode 100644 index 0000000..d256e7f --- /dev/null +++ b/iOS/docs/about.markdown @@ -0,0 +1,18 @@ +--- +# layout: page +title: About +permalink: /about/ +--- + +This is the base Jekyll theme. You can find out more info about customizing your Jekyll theme, as well as basic Jekyll usage documentation at [jekyllrb.com](https://jekyllrb.com/) + +You can find the source code for Minima at GitHub: +[jekyll][jekyll-organization] / +[minima](https://github.com/jekyll/minima) + +You can find the source code for Jekyll at GitHub: +[jekyll][jekyll-organization] / +[jekyll](https://github.com/jekyll/jekyll) + + +[jekyll-organization]: https://github.com/jekyll diff --git a/iOS/docs/assets/css/customstyles.css b/iOS/docs/assets/css/customstyles.css new file mode 100644 index 0000000..82f46c8 --- /dev/null +++ b/iOS/docs/assets/css/customstyles.css @@ -0,0 +1,12 @@ +.linklist { + list-style-type: none; +} + +.linklist li { + display: inline-block; +} + +.inner { + width: auto; + max-width: 800px; +} \ No newline at end of file diff --git a/iOS/docs/assets/css/style.css b/iOS/docs/assets/css/style.css new file mode 100644 index 0000000..56b9e3e --- /dev/null +++ b/iOS/docs/assets/css/style.css @@ -0,0 +1,183 @@ +/* generated by rouge http://rouge.jneen.net/ original base16 by Chris Kempson (https://github.com/chriskempson/base16) +*/ +@import url("https://fonts.googleapis.com/css?family=Chivo:900"); +.highlight table td { padding: 5px; } + +.highlight table pre { margin: 0; } + +.highlight, .highlight .w { color: #d0d0d0; } + +.highlight .err { color: #151515; background-color: #ac4142; } + +.highlight .c, .highlight .cd, .highlight .cm, .highlight .c1, .highlight .cs { color: #888; } + +.highlight .cp { color: #f4bf75; } + +.highlight .nt { color: #f4bf75; } + +.highlight .o, .highlight .ow { color: #d0d0d0; } + +.highlight .p, .highlight .pi { color: #d0d0d0; } + +.highlight .gi { color: #90a959; } + +.highlight .gd { color: #ac4142; } + +.highlight .gh { color: #6a9fb5; font-weight: bold; } + +.highlight .k, .highlight .kn, .highlight .kp, .highlight .kr, .highlight .kv { color: #aa759f; } + +.highlight .kc { color: #d28445; } + +.highlight .kt { color: #d28445; } + +.highlight .kd { color: #d28445; } + +.highlight .s, .highlight .sb, .highlight .sc, .highlight .sd, .highlight .s2, .highlight .sh, .highlight .sx, .highlight .s1 { color: #90a959; } + +.highlight .sr { color: #75b5aa; } + +.highlight .si { color: #8f5536; } + +.highlight .se { color: #8f5536; } + +.highlight .nn { color: #f4bf75; } + +.highlight .nc { color: #f4bf75; } + +.highlight .no { color: #f4bf75; } + +.highlight .na { color: #6a9fb5; } + +.highlight .m, .highlight .mf, .highlight .mh, .highlight .mi, .highlight .il, .highlight .mo, .highlight .mb, .highlight .mx { color: #90a959; } + +.highlight .ss { color: #90a959; } + +/* http://meyerweb.com/eric/tools/css/reset/ v2.0 | 20110126 License: none (public domain) +*/ +html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { padding: 0; margin: 0; font: inherit; font-size: 100%; vertical-align: baseline; border: 0; } + +/* HTML5 display-role reset for older browsers */ +article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section { display: block; } + +body { line-height: 1; } + +ol, ul { list-style: none; } + +blockquote, q { quotes: none; } + +blockquote:before, blockquote:after, q:before, q:after { content: ''; content: none; } + +table { border-spacing: 0; border-collapse: collapse; } + +/* LAYOUT STYLES */ +body { font-family: 'Helvetica Neue', Helvetica, Arial, serif; font-size: 1em; line-height: 1.5; color: #6d6d6d; text-shadow: 0 1px 0 rgba(255, 255, 255, 0.8); background: #e7e7e7 url(../images/body-bg.png) 0 0 repeat; } + +a { color: #d5000d; } + +a:hover { color: #c5000c; } + +header { padding-top: 35px; padding-bottom: 25px; } + +header h1 { font-family: 'Chivo', 'Helvetica Neue', Helvetica, Arial, serif; font-size: 48px; font-weight: 900; line-height: 1.2; color: #303030; letter-spacing: -1px; } + +header h2 { font-size: 24px; font-weight: normal; line-height: 1.3; color: #aaa; letter-spacing: -1px; } + +#container { min-height: 595px; background: transparent url(../images/highlight-bg.jpg) 50% 0 no-repeat; } + +.inner { max-width: 800px; margin: 0 auto; } + +#container .inner img { max-width: 100%; } + +a.button { display: block; float: left; width: 179px; padding: 12px 8px 12px 8px; margin-right: 14px; font-size: 15px; font-weight: bold; line-height: 25px; color: #303030; background: #fdfdfd; /* Old browsers */ background: -moz-linear-gradient(top, #fdfdfd 0%, #f2f2f2 100%); /* FF3.6+ */ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #fdfdfd), color-stop(100%, #f2f2f2)); /* Chrome,Safari4+ */ background: -webkit-linear-gradient(top, #fdfdfd 0%, #f2f2f2 100%); /* Chrome10+,Safari5.1+ */ background: -o-linear-gradient(top, #fdfdfd 0%, #f2f2f2 100%); /* Opera 11.10+ */ background: -ms-linear-gradient(top, #fdfdfd 0%, #f2f2f2 100%); /* IE10+ */ background: linear-gradient(to top, #fdfdfd 0%, #f2f2f2 100%); /* W3C */ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#fdfdfd', endColorstr='#f2f2f2',GradientType=0 ); /* IE6-9 */ border-top: solid 1px #cbcbcb; border-right: solid 1px #b7b7b7; border-bottom: solid 1px #b3b3b3; border-left: solid 1px #b7b7b7; border-radius: 30px; -webkit-box-shadow: 10px 10px 5px #888; -moz-box-shadow: 10px 10px 5px #888; box-shadow: 0px 1px 5px #e8e8e8; -moz-border-radius: 30px; -webkit-border-radius: 30px; } + +a.button:hover { background: #fafafa; /* Old browsers */ background: -moz-linear-gradient(top, #fdfdfd 0%, #f6f6f6 100%); /* FF3.6+ */ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #fdfdfd), color-stop(100%, #f6f6f6)); /* Chrome,Safari4+ */ background: -webkit-linear-gradient(top, #fdfdfd 0%, #f6f6f6 100%); /* Chrome10+,Safari5.1+ */ background: -o-linear-gradient(top, #fdfdfd 0%, #f6f6f6 100%); /* Opera 11.10+ */ background: -ms-linear-gradient(top, #fdfdfd 0%, #f6f6f6 100%); /* IE10+ */ background: linear-gradient(to top, #fdfdfd 0%, #f6f6f6, 100%); /* W3C */ filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#fdfdfd', endColorstr='#f6f6f6',GradientType=0 ); /* IE6-9 */ border-top: solid 1px #b7b7b7; border-right: solid 1px #b3b3b3; border-bottom: solid 1px #b3b3b3; border-left: solid 1px #b3b3b3; } + +a.button span { display: block; height: 23px; padding-left: 50px; } + +#download-zip span { background: transparent url(../images/zip-icon.png) 12px 50% no-repeat; } + +#download-tar-gz span { background: transparent url(../images/tar-gz-icon.png) 12px 50% no-repeat; } + +#view-on-github span { background: transparent url(../images/octocat-icon.png) 12px 50% no-repeat; } + +#view-on-github { margin-right: 0; } + +code, pre { margin-bottom: 30px; font-family: Monaco, "Bitstream Vera Sans Mono", "Lucida Console", Terminal; font-size: 14px; color: #222; } + +code { padding: 0 3px; background-color: #f2f2f2; border: solid 1px #ddd; } + +pre { padding: 20px; overflow: auto; color: #f2f2f2; text-shadow: none; background: #303030; } + +pre code { padding: 0; color: #f2f2f2; background-color: #303030; border: none; } + +ul, ol, dl { margin-bottom: 20px; } + +/* COMMON STYLES */ +hr { height: 1px; padding-bottom: 1em; margin-top: 1em; line-height: 1px; background: transparent url("../images/hr.png") 50% 0 no-repeat; border: none; } + +strong { font-weight: bold; } + +em { font-style: italic; } + +table { width: 100%; border: 1px solid #ebebeb; } + +th { font-weight: 500; } + +td { font-weight: 300; text-align: center; border: 1px solid #ebebeb; } + +form { padding: 20px; background: #f2f2f2; } + +/* GENERAL ELEMENT TYPE STYLES */ +h1 { font-size: 32px; } + +h2 { margin-bottom: 8px; font-size: 22px; font-weight: bold; color: #303030; } + +h3 { margin-bottom: 8px; font-size: 18px; font-weight: bold; color: #d5000d; } + +h4 { font-size: 16px; font-weight: bold; color: #303030; } + +h5 { font-size: 1em; color: #303030; } + +h6 { font-size: .8em; color: #303030; } + +p { margin-bottom: 20px; font-weight: 300; } + +a { text-decoration: none; } + +p a { font-weight: 400; } + +blockquote { padding: 0 0 0 30px; margin-bottom: 20px; font-size: 1.6em; border-left: 10px solid #e9e9e9; } + +ul li { list-style-position: inside; list-style: disc; padding-left: 20px; } + +ol li { list-style-position: inside; list-style: decimal; padding-left: 3px; } + +dl dt { color: #303030; } + +footer { padding-top: 20px; padding-bottom: 30px; margin-top: 40px; font-size: 13px; color: #aaa; background: transparent url("../images/hr.png") 0 0 no-repeat; } + +footer a { color: #666; } + +footer a:hover { color: #444; } + +/* MISC */ +.clearfix:after { display: block; height: 0; clear: both; visibility: hidden; content: '.'; } + +.clearfix { display: inline-block; } + +* html .clearfix { height: 1%; } + +.clearfix { display: block; } + +/* #Media Queries +================================================== */ +/* Smaller than standard 960 (devices and browsers) */ +/* Tablet Portrait size to standard 960 (devices and browsers) */ +/* All Mobile Sizes (devices and browser) */ +@media only screen and (max-width: 767px) { header { padding-top: 10px; padding-bottom: 10px; } + #downloads { margin-bottom: 25px; } + #download-zip, #download-tar-gz { display: none; } + .inner { width: 94%; margin: 0 auto; } } +/* Mobile Landscape Size to Tablet Portrait (devices and browsers) */ +/* Mobile Portrait Size to Mobile Landscape Size (devices and browsers) */ diff --git a/iOS/docs/index.markdown b/iOS/docs/index.markdown new file mode 100644 index 0000000..f16ba62 --- /dev/null +++ b/iOS/docs/index.markdown @@ -0,0 +1,25 @@ +--- +# Feel free to add content and custom Front Matter to this file. +# To modify the layout, see https://jekyllrb.com/docs/themes/#overriding-theme-defaults + +# layout: home +--- + +MonsterCards let you create and store monster statblocks for 5e compatible sytems. You can also import the files created by the popular 5e stat block generator at [https://tetra-cube.com/dnd/dnd-statblock.html](https://tetra-cube.com/dnd/dnd-statblock.html) You monster library is synced between your devices using iCloud. You'll need to create your own app ids, provisioning profiles, and iCloud storage if building from source. Due to the way iCloud works. Building from source will mean you sync data to a different iCloud container than using the official app. The app is currently in limited beta but will be available in the app store as soon as the collection and dashboard features are finished. + +## Coming Soon + +These are things we intend to complete before launching v1.0 + +* __Collections__ - You will be able to group your saved monsters in collections. This is useful for grouping monsters by session/location/campaign. +* __Dashboard__ - A set of monster thumbnails with vital stats that you can open to view full monster cards. This is useful for running encounters or having all the monsters you need for a session handy +* __Native export/import format__ - We will be adding a file format for sharing monsters with other users so you can easily share creations to other users. + +## Wishlist + +These may never happen. + +* __Android version__ - An android version without iCloud support is being worked on [here](https://github.com/headhunter45/MonsterCards-Android) +* __Other import formats__ - We're taking suggestions +* __Initiative Tracker__ - A basic initiative/hit point tracker. +* __NFC__ - Put an NFC sticker on a mini and scan the mini to show it's card. diff --git a/iOS/docs/privacy.md b/iOS/docs/privacy.md new file mode 100644 index 0000000..e54b697 --- /dev/null +++ b/iOS/docs/privacy.md @@ -0,0 +1,64 @@ +--- +# layout: page +title: Privacy Policy +permalink: /privacy +--- + +**Privacy Policy** + +Tom Hicks built the Monster Cards app as an Open Source app. This SERVICE is provided by Tom Hicks at no cost and is intended for use as is. + +This page is used to inform visitors regarding my policies with the collection, use, and disclosure of Personal Information if anyone decided to use my Service. + +If you choose to use my Service, then you agree to the collection and use of information in relation to this policy. The Personal Information that I collect is used for providing and improving the Service. I will not use or share your information with anyone except as described in this Privacy Policy. + +The terms used in this Privacy Policy have the same meanings as in our Terms and Conditions, which is accessible at Monster Cards unless otherwise defined in this Privacy Policy. + +**Information Collection and Use** + +For a better experience, while using our Service, I may require you to provide us with certain personally identifiable information. I don't currently collect any, but this may change in the future. The information that I request will be retained on your device and is not collected by me in any way. + +**Log Data** + +I want to inform you that whenever you use my Service, in a case of an error in the app I collect data and information (through third party products) on your phone called Log Data. This Log Data may include information such as your device Internet Protocol (“IP”) address, device name, operating system version, the configuration of the app when utilizing my Service, the time and date of your use of the Service, and other statistics. + +**Cookies** + +Cookies are files with a small amount of data that are commonly used as anonymous unique identifiers. These are sent to your browser from the websites that you visit and are stored on your device's internal memory. + +This Service does not use these “cookies” explicitly. However, the app may use third party code and libraries that use “cookies” to collect information and improve their services. You have the option to either accept or refuse these cookies and know when a cookie is being sent to your device. If you choose to refuse our cookies, you may not be able to use some portions of this Service. + +**Service Providers** + +I may employ third-party companies and individuals due to the following reasons: + +* To facilitate our Service; +* To provide the Service on our behalf; +* To perform Service-related services; or +* To assist us in analyzing how our Service is used. + +I want to inform users of this Service that these third parties have access to your Personal Information. The reason is to perform the tasks assigned to them on our behalf. However, they are obligated not to disclose or use the information for any other purpose. + +**Security** + +I value your trust in providing us your Personal Information, thus we are striving to use commercially acceptable means of protecting it. But remember that no method of transmission over the internet, or method of electronic storage is 100% secure and reliable, and I cannot guarantee its absolute security. + +**Links to Other Sites** + +This Service may contain links to other sites. If you click on a third-party link, you will be directed to that site. Note that these external sites are not operated by me. Therefore, I strongly advise you to review the Privacy Policy of these websites. I have no control over and assume no responsibility for the content, privacy policies, or practices of any third-party sites or services. + +**Children’s Privacy** + +These Services do not address anyone under the age of 13. I do not knowingly collect personally identifiable information from children under 13 years of age. In the case I discover that a child under 13 has provided me with personal information, I immediately delete this from our servers. If you are a parent or guardian and you are aware that your child has provided us with personal information, please contact me so that I will be able to do necessary actions. + +**Changes to This Privacy Policy** + +I may update our Privacy Policy from time to time. Thus, you are advised to review this page periodically for any changes. I will notify you of any changes by posting the new Privacy Policy on this page. + +This policy is effective as of 2021-03-26 + +**Contact Us** + +If you have any questions or suggestions about my Privacy Policy, do not hesitate to contact me at monstercardsapp@gmail.com. + +This privacy policy page was created at [privacypolicytemplate.net](https://privacypolicytemplate.net) and modified/generated by [App Privacy Policy Generator](https://app-privacy-policy-generator.nisrulz.com/) \ No newline at end of file diff --git a/iOS/docs/terms.md b/iOS/docs/terms.md new file mode 100644 index 0000000..04e9d9c --- /dev/null +++ b/iOS/docs/terms.md @@ -0,0 +1,34 @@ +--- +title: Terms & Conditions +permalink: /terms +--- + +**Terms & Conditions** + +By downloading or using the app, these terms will automatically apply to you – you should make sure therefore that you read them carefully before using the app. The app is governed by the MIT license located [here](https://github.com/headhunter45/MonsterCards-iOS/blob/develop/LICENSE). + +Tom Hicks is committed to ensuring that the app is as useful and efficient as possible. For that reason, we reserve the right to make changes to the app or to charge for its services, at any time and for any reason. We will never charge you for the app or its services without making it very clear to you exactly what you’re paying for. + +The Monster Cards app stores and processes personal data that you have provided to us, in order to provide my Service. It’s your responsibility to keep your phone and access to the app secure. We therefore recommend that you do not jailbreak or root your phone, which is the process of removing software restrictions and limitations imposed by the official operating system of your device. It could make your phone vulnerable to malware/viruses/malicious programs, compromise your phone’s security features and it could mean that the Monster Cards app won’t work properly or at all. + +You should be aware that there are certain things that Tom Hicks will not take responsibility for. Certain functions of the app will require the app to have an active internet connection. The connection can be Wi-Fi, or provided by your mobile network provider, but Tom Hicks cannot take responsibility for the app not working at full functionality if you don’t have access to Wi-Fi, and you don’t have any of your data allowance left. + +If you’re using the app outside of an area with Wi-Fi, you should remember that your terms of the agreement with your mobile network provider will still apply. As a result, you may be charged by your mobile provider for the cost of data for the duration of the connection while accessing the app, or other third party charges. In using the app, you’re accepting responsibility for any such charges, including roaming data charges if you use the app outside of your home territory (i.e. region or country) without turning off data roaming. If you are not the bill payer for the device on which you’re using the app, please be aware that we assume that you have received permission from the bill payer for using the app. + +Along the same lines, Tom Hicks cannot always take responsibility for the way you use the app i.e. You need to make sure that your device stays charged – if it runs out of battery and you can’t turn it on to avail the Service, Tom Hicks cannot accept responsibility. + +With respect to Tom Hicks’s responsibility for your use of the app, when you’re using the app, it’s important to bear in mind that although we endeavour to ensure that it is updated and correct at all times, we do rely on third parties to provide information to us so that we can make it available to you. Tom Hicks accepts no liability for any loss, direct or indirect, you experience as a result of relying wholly on this functionality of the app. + +At some point, we may wish to update the app. The app is currently available on Android & iOS – the requirements for both systems(and for any additional systems we decide to extend the availability of the app to) may change, and you’ll need to download the updates if you want to keep using the app. Tom Hicks does not promise that it will always update the app so that it is relevant to you and/or works with the Android & iOS version that you have installed on your device. However, you promise to always accept updates to the application when offered to you, We may also wish to stop providing the app, and may terminate use of it at any time without giving notice of termination to you. Unless we tell you otherwise, upon any termination, (a) the rights and licenses granted to you in these terms will end; (b) you must stop using the app, and (if needed) delete it from your device. + +**Changes to This Terms and Conditions** + +I may update our Terms and Conditions from time to time. Thus, you are advised to review this page periodically for any changes. I will notify you of any changes by posting the new Terms and Conditions on this page. + +These terms and conditions are effective as of 2021-03-26 + +**Contact Us** + +If you have any questions or suggestions about my Terms and Conditions, do not hesitate to contact me at monstercardsapp@gmail.com. + +This Terms and Conditions page was generated by [App Privacy Policy Generator](https://app-privacy-policy-generator.nisrulz.com/)