192 Commits

Author SHA1 Message Date
Tom Hicks
019c2fea60 Fixes opening files from outside the app's sandbox. 2021-06-27 13:51:03 -07:00
Maj
2da820b980 Adds .zshrc files to gitignore. 2021-05-22 00:39:56 -07:00
Tom Hicks
1ebf6e5348 Fixes numbers and skills when importing. 2021-05-11 23:48:42 -07:00
Tom Hicks
dc543660be Adds footer links to privacy page and terms & conditions page. Adds header build status badges. 2021-04-15 22:29:03 -07:00
Tom Hicks
f665bf86f1 Merge branch 'develop' of github.com:headhunter45/MonsterCards-iOS into develop 2021-04-15 21:46:17 -07:00
Tom Hicks
a57c43a7e4 Updates docs and readme to include a description of the app features. 2021-04-15 21:45:52 -07:00
e8358bfb59 Maybe fixed signing for the preview extension. 2021-04-10 23:09:24 -07:00
750bb2543d Sets version label to 0.6. 2021-04-10 21:34:54 -07:00
8558cacc83 Adds CloudKit support to the monster library between devices. 2021-04-08 21:58:54 -07:00
563df6ca28 Adds QuickLook preview for monster files. It shows how the monster will appear after being imported. 2021-04-08 19:18:27 -07:00
5ba59bbdf3 Moves MonsterDetailWrapper into its own file to separate the core data dependencies. 2021-04-08 19:18:27 -07:00
5304c66b0b Model cleanup.
Separates core data transport stuff to extensions so we can use the view models without a core data dependency.
2021-04-08 19:18:27 -07:00
e741d4fb33 Makes DTOs implement Codable instead of just Encodable and Decodable.
Adds MonsterDocument to load/save .monster files.
2021-04-08 19:18:27 -07:00
cc963f547f Enables document browser support and loading files in place so we can open monster files for import. 2021-04-08 19:18:27 -07:00
f04932899a Adds some import tasks. 2021-04-05 01:01:41 -07:00
52f37b494e Adds the ability to delete a monster from your library. 2021-04-05 00:46:44 -07:00
c7fdb7ecc5 Adds importing a character from a share. You can now save the monster as previewed. 2021-04-05 00:33:11 -07:00
e7ccc0e1ab Adds reactions, lair actions, and regional actions to the editor and monster display. 2021-04-04 17:48:06 -07:00
0ac780c188 Merge pull request #1 from headhunter45/add-file-import
Add Opening monster files from tetra cube's generator
2021-04-04 03:31:19 -07:00
ab218fbe34 Reorganizes the project tree and adds a task. 2021-04-04 03:28:20 -07:00
a43d2f11ea Adds monster import helper. 2021-04-04 03:27:50 -07:00
b519b80209 Adds lair actions, regional actions, and reactions to the data model. Renames baseSpeed to walkSpeed. 2021-04-04 03:27:00 -07:00
f9647eaf97 Fixes toggling a shield in the editor not affecting the displayed AC. 2021-04-04 03:13:19 -07:00
2e6b8bb377 Loads the imported monster from file and sets the new monster's name from the loaded data. 2021-04-04 03:11:34 -07:00
c09c332758 Adds DTO classes to help load a monster file from https://tetra-cube.com/dnd/dnd-statblock.html 2021-04-04 01:41:41 -07:00
cd2be8490f Adds a view to show the monster being imported and confirm the user wants to import that monster. 2021-04-04 01:38:53 -07:00
6eca7efb0c Adds an import view that shows when the app is opening a .monster file. 2021-04-04 01:18:00 -07:00
9076f5896d Creates new document type for .monster files.
Sets the app as the default editor for .monster files.
2021-04-04 01:12:10 -07:00
94a3ceb9e0 Removes a bunch of commented out code. 2021-04-04 01:04:40 -07:00
46682e1de7 Adds the monster name to MonsterDetail. 2021-04-04 00:31:18 -07:00
68a6051dca Makes MonsterDetail use a MonsterViewModel instead of the core data type Monster. 2021-04-04 00:30:55 -07:00
568485a62e Adds calculated fields from Monster to MonsterViewModel. 2021-04-04 00:19:49 -07:00
7d6bf6ec34 Adds titles to terms and privacy pages. 2021-03-25 23:51:03 -07:00
4506e76a1a Attempt 2 to fix the broken theme. 2021-03-25 23:47:18 -07:00
ef75c7d0ce Set theme jekyll-theme-tactile 2021-03-25 23:42:23 -07:00
5b6b185da4 Attempt to fix the broken theme. 2021-03-25 23:41:20 -07:00
66a4eab5a1 Adds the default jekyll site to docs.
This was from the guide to setup github-pages.
2021-03-25 23:38:06 -07:00
25377a495e Adds license link to terms. 2021-03-25 21:35:05 -07:00
7bcfc7898c Upadtes terms, privacy, and license. 2021-03-25 21:32:56 -07:00
5e48cb976b Adds generated privacy policy and terms and conditions. 2021-03-25 21:23:13 -07:00
8ffa6231c2 Set theme jekyll-theme-tactile 2021-03-25 20:02:43 -07:00
59bbec7952 Adds app icon. 2021-03-25 19:49:04 -07:00
3d68d93789 Adds proficiency bonus to monster detail. 2021-03-25 17:32:40 -07:00
6625039561 Fixes the display of the actions label. 2021-03-25 16:56:31 -07:00
0fdb054234 Removes the + from the passive perception display. 2021-03-25 16:56:04 -07:00
0a335b372a Adds legendary actions. 2021-03-25 16:55:03 -07:00
b5dc107766 Fixes the layout of the trait editor. 2021-03-25 16:17:38 -07:00
ebd60fbb2e Fixing whitespace and adding tasks. 2021-03-25 16:17:13 -07:00
45b9959ef4 Renames the ability editor to trait editor since it's not just for abilities any more. 2021-03-25 15:53:38 -07:00
55e0ef65fd Changes languages so you can remove them. 2021-03-25 15:46:38 -07:00
8d9908369e Adds actions. 2021-03-25 15:45:33 -07:00
a9ad7a7fa8 Adds MarkdownUI dependency and abilities. 2021-03-25 14:31:35 -07:00
9fd4c1f71d Makes the SizeType initializer prefer proper case but fall back to case insensitive. 2021-03-25 01:21:45 -07:00
cd559abbb0 Fixes how passive perception is calculated.
The base value of 10 was left out.
2021-03-25 01:20:35 -07:00
d6286006b8 Removes unneeded parameters passed to oxfordJoin 2021-03-25 01:19:44 -07:00
c970dfc4ed Adds tasks to show the proficiency bonus and to sort damage types better. 2021-03-25 01:18:35 -07:00
d408edfdeb Switches HP calculations to be NPC style instead of PC style.
A PC would get full HP at level 1, but a 1 hit die creature would only get the average roll. This will be a configurable option later.
2021-03-25 01:16:54 -07:00
9fd5f55e9d Adds challenge rating and proficiency bonus to the monster editor. 2021-03-25 00:30:58 -07:00
33e2b52dc3 Language editor layout fixes. 2021-03-25 00:19:44 -07:00
627f02409c Adds languages to the editor. 2021-03-24 22:29:54 -07:00
07f59788a3 Makes oxfordJoin use standard english separators as defaults. 2021-03-24 22:27:13 -07:00
3ec62789c6 Adds senses and passive perception.
Also makes modifier calculations return Int instead of Int64.
2021-03-24 17:46:04 -07:00
596186deaa Refactors DamageTypes to String since we using it as a generic list of strings editor. 2021-03-24 17:44:53 -07:00
055a8b28cc Expands fields searched in the monster search. 2021-03-24 16:00:26 -07:00
3fa4ae0bdb Removes unused handler function. 2021-03-24 15:46:00 -07:00
9e3b36da69 Adds titles to the sub views that were missing them. 2021-03-24 15:44:30 -07:00
ed44cd9947 Sets default capitalization of text fields to make sense. 2021-03-24 15:40:44 -07:00
0c3821dbe6 Sets default names for skills and damage types to an empty string. 2021-03-24 15:37:10 -07:00
06069e89ba Sets default names for skills and damage types to an empty string. 2021-03-24 15:36:52 -07:00
08fb3745c8 Reorders condition immunities in the editor. 2021-03-23 23:17:38 -07:00
af7e3666de Adding missed file for previous commit
> Stops saving the raw core data objects in view models and makes them optional in the constructor.
2021-03-23 23:16:15 -07:00
d27a7ca5c5 Stops saving the raw core data objects in view models and makes them optional in the constructor. 2021-03-23 23:15:02 -07:00
749da2151e Adds missing file to last commit.
> Adds damage types and condition immunities to core data so they are saved now.
2021-03-23 23:12:52 -07:00
85e2529289 Adds damage types and condition immunities to core data so they are saved now. 2021-03-23 23:12:01 -07:00
d4e93a92a7 Adds shared scheme. 2021-03-23 21:34:45 -07:00
2b6741b5dc Adds damage vulnerabilities to the monster editor. 2021-03-22 18:06:50 -07:00
f90227bc29 Adds damage resistances to the monster editor. 2021-03-22 18:06:30 -07:00
09a16c85b7 Adds damage immunities to the monster editor. 2021-03-22 18:05:05 -07:00
c8f18a00dd Adds condition immunities to the monster editor. 2021-03-22 18:00:05 -07:00
e23b35f75e Reorganized the MonsterDetail view to get around the 10 items per group limit.
Adds layout for resistances, immunities, and languages.
2021-03-22 01:02:21 -07:00
2cd9e6d92d Adjusts the monster detail view so it makes better use of small screens. 2021-03-21 20:28:05 -07:00
f5c3ce57de Adds skills display to the monster detail view. 2021-03-21 19:49:03 -07:00
0299213dfa Sorts skills specifically for the EditSkills view, but also more generally when creating a MonsterViewModel. 2021-03-21 18:28:04 -07:00
7073e3d952 Fixes to skill saving 2021-03-21 18:27:17 -07:00
44b585aab8 Adds EditSkill view to allow editing a specific skill. 2021-03-21 17:25:28 -07:00
106b41d2ee Adds MCAbilityScorePicker. 2021-03-21 17:25:00 -07:00
861bae24d6 Adds EditSkills view bound to the monster view model's skills. 2021-03-21 16:18:04 -07:00
e25e37c871 Moves saving throws from EditMonster to a sub view. 2021-03-21 14:47:15 -07:00
21eae233f3 Moves ability scores from EditMonster to a sub view. 2021-03-21 14:42:32 -07:00
7bef443ead Moves speed info from EditMonster to a sub view. 2021-03-21 14:38:13 -07:00
ecfdf7ae58 Fixes the fetched monster on MonsterDetail not updating when the EditMonster view saves. 2021-03-21 14:37:11 -07:00
d90c32691a Changes the NavigationView to stack navigation style so the save button on EditMonster takes you back to MonsterDetail instead of the root of the NavigationView. 2021-03-21 14:23:38 -07:00
b83a88c1f2 Moves editing armor to a sub view of EditMonster. 2021-03-21 14:22:30 -07:00
9f0896943f Moves Basic Info section of the monster editor to a sub view. 2021-03-21 14:13:10 -07:00
a6ad738d48 Fixes some bugs with how the editing monster is passed around.
Removes the custom cancel since we don't need it now.
2021-03-21 14:07:11 -07:00
0e551ce01b Fixes HP display. 2021-03-21 00:46:23 -07:00
46372268d4 Makes EditMonster use MonsterViewModel instead of binding directly to the Core Data types. 2021-03-21 00:43:24 -07:00
cee4f24e93 Started adding skills. 2021-02-13 20:53:38 -08:00
da74b68a9c Hides elements on monster detail if they don't have values to show.
Adds TODO to hide dividers when applicable.
2021-02-07 13:00:48 -08:00
fe431475a2 Adds comments with other picker types to the advantage picker for refreence. 2021-02-07 12:45:27 -08:00
9d185d27a5 Makes saving throw proficiencies and advantages use enums instead of raw strings. 2021-02-07 12:45:03 -08:00
f960df1424 Makes armor type a picker instead of a string. 2021-02-07 12:43:23 -08:00
f6ef6a7f3d Convertes to Swift and SwiftUI 2021-01-18 00:30:45 -08:00
3d54342687 Fixes a bug editing a new monster. 2020-10-09 22:04:28 -07:00
5da0ee2549 Updates storyboard to fix display on iPad.
Disables dark mode until we can come up with a dark mode color scheme that looks good
2020-10-04 01:12:25 -07:00
d0bd26c7e0 Fixes how we check which saving throws to show. 2020-10-04 00:46:12 -07:00
692bfdd943 Adds advantage/disadvantage to saving throw display. 2020-10-04 00:37:11 -07:00
0e800dfd1c Adds saving throws to monster card display.
Adds proficiencyBonus implementation to Monster. The proficiency bonus relies on CR and defaults to 0 until the CR fields are implemented
2020-10-04 00:19:04 -07:00
3c3ed3c94b Adds Saving Throws to the data model and monster editor. 2020-10-03 22:44:15 -07:00
23b840f3ff Disables row selection in the edit form table. 2020-10-03 22:22:12 -07:00
f4c981ab36 Fixes JSON initializer and tests. 2020-09-26 23:10:24 -07:00
2a9b936d0d Adds ability scores to monster cards. 2020-09-26 23:06:03 -07:00
0912ac0fd8 Adds select field with picker as TextField inputView. 2020-09-26 22:18:04 -07:00
57bf1f2e3a Renames armorName to armorType.
Sets default values for core data fields.
Moves hit dice and hp related fields into the basic info section of the editor.
2020-09-26 17:15:43 -07:00
ec7f827123 Fixes initial state of integer fields. 2020-09-26 16:37:01 -07:00
caa1be50cf Cleans up code that generates HTML labels.
Adds Label for speed.
Makes the Monster Card refresh the monster from CoreData when the view is shown.
2020-09-26 15:09:46 -07:00
d041105e1e Adds speed properties to Core Data and monster editor. 2020-09-26 01:22:11 -07:00
868bc86143 Adds Hit Dice and Custom HP to monster edit form. 2020-09-25 04:47:23 -07:00
5e00722c3b Adds boolean field to MCFormFields. 2020-09-25 04:46:50 -07:00
b2eed1ffc7 Adds HP to monster card. 2020-09-25 03:52:44 -07:00
82e5545904 Adds HP related fields to core data.
Implements hitDieForSize and hitPointsDescription in Monster.
Adds tests.
2020-09-20 03:21:34 -07:00
29f5ef991e Exposes constants used by Monster internally for values. 2020-09-20 03:19:24 -07:00
edb9449fbc Fixes EditMonsterViewController tests to use the new cell reuse identifier. 2020-09-20 03:17:28 -07:00
28c1e271ab Adds ability scores (strength, dexterity, constitution, intelligence, wisdom, and charisma) to the edit monster form. 2020-09-18 01:00:30 -07:00
a6c33fb803 Adds a label to the integer form field.
Makes the string value and both string and integer label update the underlying controls when set.
2020-09-18 00:47:48 -07:00
71cd2572a2 Partial fixes to tests to run with Xcode 12. 2020-09-18 00:27:31 -07:00
f973a618c6 Refactors form field cell creation into separate reusable methods. 2020-09-18 00:16:38 -07:00
893559baa6 Renames old form field class and delegate.
Adds new form field for integers.
2020-09-17 23:45:05 -07:00
3dc1707f3c Updates to Xcode 12.
Drops the iOS version in both projects to 13 from 13.0 and 13.7.
2020-09-17 20:24:12 -07:00
544c19c959 Updates comment explaining the format of the monster meta string. 2020-09-17 13:04:28 -07:00
d1a3a1d247 Makes Monster initializer use new JSONHelper methods to make parsing more expressive. 2020-09-17 13:03:48 -07:00
7514237a84 Adds JSONHelper methods to make parsing json from strings and NSData objects easier. 2020-09-17 12:58:59 -07:00
74745f6d54 Adds JSONHelper methods to read arrays. 2020-09-17 12:37:03 -07:00
81726e9554 Adds JSONHelper methods to read dictionaries. 2020-09-17 01:32:52 -07:00
a4774c2401 Adds methods to JSONHelper to read boolean values. 2020-09-17 00:50:27 -07:00
c9b15a21a5 Adds JSONHelper methods to read numbers as ints. 2020-09-17 00:30:22 -07:00
2ef6c06e32 Adds JSONHelper methods to read numbers as NSNumber objects. 2020-09-17 00:27:40 -07:00
e821656871 Adds JSONHelper methods to read strings. (+1 squashed commit)
Squashed commits:
[30b0a71] Adds JSONHelper methods to read strings.
2020-09-17 00:23:28 -07:00
0fe24d767c Adds armor class to monster cards. 2020-09-15 20:26:39 -07:00
6586b429b7 Adds HTMLHelper to convert from HTML in an NSString to a properly attributed NSAttributedString. 2020-09-15 20:18:08 -07:00
a78b6e03c8 Adds armorClassDescription to Monster. 2020-09-15 20:00:15 -07:00
88927c9ddc Adds shieldBonus to Monster entity.
Adds tests for shieldBonus.
2020-09-15 20:00:15 -07:00
700724ce5b Adds otherArmorDescription to Monster entity.
Adds tests for otherArmorDescription.
2020-09-15 20:00:15 -07:00
e309e15af4 Adds armorName to Monster entity.
Adds tests for armorName.
2020-09-15 20:00:15 -07:00
f8d3a893ca Moves ability scores to Core Data entity. 2020-09-15 20:00:15 -07:00
d66e5140e7 Adds charismaScore and charismaModifier to Monster.
Adds tests for charismaScore and charismaModifier.
2020-09-15 20:00:15 -07:00
efaf492e41 Adds wisdomScore and wisdomModifier to Monster.
Adds tests for wisdomScore and wisdomModifier.
2020-09-15 20:00:15 -07:00
dc8299ade6 Adds intelligenceScore and intelligenceModifier to Monster.
Adds tests for intelligenceScore and intelligenceModifier.
2020-09-15 20:00:15 -07:00
aca029955e Adds constitutionScore and constitutionModifier to Monster.
Adds tests for constitutionScore and constitutionModifier.
2020-09-15 20:00:15 -07:00
58ce77e4df Adds dexterityScore and dexterityModifier to Monster.
Adds tests for dexterityScore and dexterityModifier.
2020-09-15 20:00:14 -07:00
c14b10a032 Adds strengthScore and strengthModifier to Monster.
Adds tests for strengthScore and strengthModifier.
2020-09-15 20:00:14 -07:00
52a7ba871a Adds abilityModifierForScore and tests. 2020-09-15 19:58:58 -07:00
2aaca29741 Adds alignment to monsters.
Adds tests for editing alignment on monsters.
Adds tests for monster meta text when alignment is set.
2020-09-12 17:39:07 -07:00
7643b98c01 Adds subtype to monster.
Adds EditMonsterViewController tests for editing subtype.
Adds tests for meta property of Monster.
2020-09-12 17:16:36 -07:00
91df63802a Adds monster type to editor.
Sets all entity attributes for monster to default to empty string instead of null.
Adds test for copyFromMonster.
Makes initWithMonster:andContext call copyFromMonster to ensure they use the same logic to clone the other monster.
2020-09-12 12:52:45 -07:00
9396502b3d Adds meta string to monster cards.
Implements meta method in Monster to return a formatted meta string.
Adds size to edit monster.
Moves monster copy logic to copyFromMonster in the Monster class.
Fixes JSON parsing to set strings to an empty string if they're missing from the json blob.
Makes Monster.size default to an empty string instead of null.
Cleans up some raw strings to use NSLocalizedString instead.
2020-09-12 03:08:10 -07:00
edb1c672eb Makes search and library use the same monster detail view/controller. 2020-09-12 02:16:14 -07:00
fa553a447a Fixes tests.
Adds CoreData codegen categories to tests.
Updates initializers to pass coredata contexts.
2020-09-12 02:06:25 -07:00
c7d821e72f Makes the search bar redo the search when the view is reloaded. 2020-09-12 01:26:59 -07:00
e0fce50e1a Adds swipe action to delete a card from the library. 2020-09-12 01:25:56 -07:00
1e79bc5500 Makes views use monsters from CoreData instead of hard coded ones. 2020-09-12 01:16:56 -07:00
f61fdc0aba Makes Monster a CoreData entity 2020-09-12 00:55:49 -07:00
dd7f46f580 Adds monster name as editable in the edit monster view controller. 2020-09-12 00:21:49 -07:00
a8c88feb1f Adds string form field cell for use in table views to edit a string property. 2020-09-12 00:17:41 -07:00
e3384538a5 Makes nav back from edit monster to monster view update the monster. 2020-09-12 00:14:01 -07:00
5c18e815dd Adds a monster edit view. 2020-09-12 00:11:20 -07:00
b1fbc169dc Makes the library view display a list of monsters.
Makes the library view ad search view share a view controller for their destination.
2020-09-11 23:42:23 -07:00
314906f74d Adds copy constructor to Monster initWithMonster. 2020-09-11 23:30:22 -07:00
2a20e7262d Removes some debug logging statements. 2020-09-07 22:37:57 -07:00
0cff85092b Droppes deployment target to iOS 13.0 2020-09-07 18:13:59 -07:00
49cb07704b Makes the search screen actually do searches.
Makes the monster detail view set the title if there is no name label bound.
2020-09-07 16:26:51 -07:00
6e76767c26 Removes default text in search bar. 2020-09-06 18:47:14 -07:00
1342c55a1f Adds name to Monster detail view.
Adds json initializers for Monster.
2020-09-06 17:33:58 -07:00
32025eb4e7 Adds name to monster detail view.
Passes the selected monster from the search view to the monster detail view.
2020-09-06 16:42:40 -07:00
2f57c10a5a Adds placeholder monsters to search view.
Adds navigation from search view to monster detail view.
2020-09-06 13:05:14 -07:00
84ce8ba80c Adds UITableView for search results.
Adds constraints to search view.
2020-09-06 12:48:03 -07:00
555ba4c007 Fixes default initializer of Skill model.
Adds tests for Skill model.
2020-09-05 23:25:40 -07:00
c3031fbc39 Adds cocoapods for libraries.
Adds OCMockito and OCHamcrest libs.
2020-09-05 22:06:51 -07:00
af47156557 Fixes default initializer of SavingThrow model.
Adds tests for SavingThrow model.
2020-09-05 21:15:07 -07:00
5c5a0bb4f1 Fixes typing in Language initializer.
Fixes default initializer of Language model.
Adds tests for Language model.
2020-09-05 21:01:23 -07:00
70ddeeb5f1 Fixes default initializer of DamageType model.
Adds tests for DamageType model.
2020-09-05 20:01:53 -07:00
e4f33e553a Fixes default initializer of Action model.
Adds tests for Action model.
2020-09-05 19:37:28 -07:00
d8ddde6e7a Fixes default initializer of Ability model.
Adds tests for Ability model.
2020-09-05 19:33:05 -07:00
473fa15f7c Adds StringHelper with isStringNilOrEmpty method. 2020-09-05 19:06:39 -07:00
1e1abba82f Adds implementation for Skill model.
Adds stubbed out implementation of Monster model. all methods throw exceptions.
2020-09-05 19:04:56 -07:00
8871a57a2c Adds implementation of SavingThrow model. 2020-09-05 18:33:57 -07:00
1e5e7b4cca Adds implementation of Language model. 2020-09-05 18:33:25 -07:00
e98443a5dd Adds implementation of DamageType model. 2020-09-05 18:19:32 -07:00
fbbecce6e6 Adds implementation of Action model. 2020-09-05 18:19:18 -07:00
bc947f5c10 Adds implementation of Ability model. 2020-09-05 18:18:53 -07:00
ea2539e21c Adds stubbed out model classes Ability, Action, DamageType, Language, Monster, SavingThrow, and Skill. 2020-09-05 18:16:24 -07:00
c7202763ec Adds top level nav elements to the tab bar. 2020-09-04 21:54:00 -07:00
147 changed files with 6998 additions and 837 deletions

2
iOS/.gitignore vendored
View File

@@ -1,2 +1,2 @@
xcuserdata xcuserdata
.zshrc

24
iOS/LICENSE Normal file
View File

@@ -0,0 +1,24 @@
MIT License
-----------
Copyright (c) 2020-2021 Tom Hicks
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,6 @@
<Workspace <Workspace
version = "1.0"> version = "1.0">
<FileRef <FileRef
location = "self:MonsterCards.xcodeproj"> location = "self:">
</FileRef> </FileRef>
</Workspace> </Workspace>

View File

@@ -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
}

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1170" LastUpgradeVersion = "1240"
version = "1.3"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
@@ -14,8 +14,8 @@
buildForAnalyzing = "YES"> buildForAnalyzing = "YES">
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "E2F7246F25005E89007D87ED" BlueprintIdentifier = "E2570FB425B1AC520055B23B"
BuildableName = "Monster Cards.app" BuildableName = "MonsterCards.app"
BlueprintName = "MonsterCards" BlueprintName = "MonsterCards"
ReferencedContainer = "container:MonsterCards.xcodeproj"> ReferencedContainer = "container:MonsterCards.xcodeproj">
</BuildableReference> </BuildableReference>
@@ -32,7 +32,7 @@
skipped = "NO"> skipped = "NO">
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "E2F7249025005E8A007D87ED" BlueprintIdentifier = "E2570FCA25B1AC550055B23B"
BuildableName = "MonsterCardsTests.xctest" BuildableName = "MonsterCardsTests.xctest"
BlueprintName = "MonsterCardsTests" BlueprintName = "MonsterCardsTests"
ReferencedContainer = "container:MonsterCards.xcodeproj"> ReferencedContainer = "container:MonsterCards.xcodeproj">
@@ -42,7 +42,7 @@
skipped = "NO"> skipped = "NO">
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "E2F7249B25005E8A007D87ED" BlueprintIdentifier = "E2570FD525B1AC550055B23B"
BuildableName = "MonsterCardsUITests.xctest" BuildableName = "MonsterCardsUITests.xctest"
BlueprintName = "MonsterCardsUITests" BlueprintName = "MonsterCardsUITests"
ReferencedContainer = "container:MonsterCards.xcodeproj"> ReferencedContainer = "container:MonsterCards.xcodeproj">
@@ -64,8 +64,8 @@
runnableDebuggingMode = "0"> runnableDebuggingMode = "0">
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "E2F7246F25005E89007D87ED" BlueprintIdentifier = "E2570FB425B1AC520055B23B"
BuildableName = "Monster Cards.app" BuildableName = "MonsterCards.app"
BlueprintName = "MonsterCards" BlueprintName = "MonsterCards"
ReferencedContainer = "container:MonsterCards.xcodeproj"> ReferencedContainer = "container:MonsterCards.xcodeproj">
</BuildableReference> </BuildableReference>
@@ -81,8 +81,8 @@
runnableDebuggingMode = "0"> runnableDebuggingMode = "0">
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "E2F7246F25005E89007D87ED" BlueprintIdentifier = "E2570FB425B1AC520055B23B"
BuildableName = "Monster Cards.app" BuildableName = "MonsterCards.app"
BlueprintName = "MonsterCards" BlueprintName = "MonsterCards"
ReferencedContainer = "container:MonsterCards.xcodeproj"> ReferencedContainer = "container:MonsterCards.xcodeproj">
</BuildableReference> </BuildableReference>

View File

@@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1240"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "E216E47A261FE76F00FD9262"
BuildableName = "MonsterPreview.appex"
BlueprintName = "MonsterPreview"
ReferencedContainer = "container:MonsterCards.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "E2570FB425B1AC520055B23B"
BuildableName = "MonsterCards.app"
BlueprintName = "MonsterCards"
ReferencedContainer = "container:MonsterCards.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = ""
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
launchStyle = "0"
askForAppToLaunch = "Yes"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "E2570FB425B1AC520055B23B"
BuildableName = "MonsterCards.app"
BlueprintName = "MonsterCards"
ReferencedContainer = "container:MonsterCards.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"
askForAppToLaunch = "Yes"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "E2570FB425B1AC520055B23B"
BuildableName = "MonsterCards.app"
BlueprintName = "MonsterCards"
ReferencedContainer = "container:MonsterCards.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -4,11 +4,102 @@
<dict> <dict>
<key>SchemeUserState</key> <key>SchemeUserState</key>
<dict> <dict>
<key>AttributedText_iOS (Playground) 1.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>14</integer>
</dict>
<key>AttributedText_iOS (Playground) 2.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>15</integer>
</dict>
<key>AttributedText_iOS (Playground).xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>13</integer>
</dict>
<key>AttributedText_macOS (Playground) 1.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>11</integer>
</dict>
<key>AttributedText_macOS (Playground) 2.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>12</integer>
</dict>
<key>AttributedText_macOS (Playground).xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>10</integer>
</dict>
<key>AttributedText_tvOS (Playground) 1.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>8</integer>
</dict>
<key>AttributedText_tvOS (Playground) 2.xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>9</integer>
</dict>
<key>AttributedText_tvOS (Playground).xcscheme</key>
<dict>
<key>isShown</key>
<false/>
<key>orderHint</key>
<integer>7</integer>
</dict>
<key>MonsterCards.xcscheme_^#shared#^_</key> <key>MonsterCards.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>0</integer> <integer>0</integer>
</dict> </dict>
<key>MonsterPreview.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>16</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>
<dict>
<key>E216E47A261FE76F00FD9262</key>
<dict>
<key>primary</key>
<true/>
</dict>
<key>E2570FB425B1AC520055B23B</key>
<dict>
<key>primary</key>
<true/>
</dict>
<key>E2570FCA25B1AC550055B23B</key>
<dict>
<key>primary</key>
<true/>
</dict>
<key>E2570FD525B1AC550055B23B</key>
<dict>
<key>primary</key>
<true/>
</dict>
</dict> </dict>
</dict> </dict>
</plist> </plist>

View File

@@ -1,20 +0,0 @@
//
// AppDelegate.h
// MonsterCards
//
// Created by Tom Hicks on 9/2/20.
// Copyright © 2020 Tom Hicks. All rights reserved.
//
#import <UIKit/UIKit.h>
#import <CoreData/CoreData.h>
@interface AppDelegate : UIResponder <UIApplicationDelegate>
@property (readonly, strong) NSPersistentCloudKitContainer *persistentContainer;
- (void)saveContext;
@end

View File

@@ -1,86 +0,0 @@
//
// AppDelegate.m
// MonsterCards
//
// Created by Tom Hicks on 9/2/20.
// Copyright © 2020 Tom Hicks. All rights reserved.
//
#import "AppDelegate.h"
@interface AppDelegate ()
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
return YES;
}
#pragma mark - UISceneSession lifecycle
- (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options {
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
return [[UISceneConfiguration alloc] initWithName:@"Default Configuration" sessionRole:connectingSceneSession.role];
}
- (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet<UISceneSession *> *)sceneSessions {
// Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
#pragma mark - Core Data stack
@synthesize persistentContainer = _persistentContainer;
- (NSPersistentCloudKitContainer *)persistentContainer {
// The persistent container for the application. This implementation creates and returns a container, having loaded the store for the application to it.
@synchronized (self) {
if (_persistentContainer == nil) {
_persistentContainer = [[NSPersistentCloudKitContainer alloc] initWithName:@"MonsterCards"];
[_persistentContainer loadPersistentStoresWithCompletionHandler:^(NSPersistentStoreDescription *storeDescription, NSError *error) {
if (error != nil) {
// Replace this implementation with code to handle the error appropriately.
// abort() 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.
/*
Typical reasons for an error here include:
* The parent directory does not exist, cannot be created, or disallows writing.
* The persistent store is not accessible, due to permissions or data protection when the device is locked.
* The device is out of space.
* The store could not be migrated to the current model version.
Check the error message to determine what the actual problem was.
*/
NSLog(@"Unresolved error %@, %@", error, error.userInfo);
abort();
}
}];
}
}
return _persistentContainer;
}
#pragma mark - Core Data Saving support
- (void)saveContext {
NSManagedObjectContext *context = self.persistentContainer.viewContext;
NSError *error = nil;
if ([context hasChanges] && ![context save:&error]) {
// Replace this implementation with code to handle the error appropriately.
// abort() 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.
NSLog(@"Unresolved error %@, %@", error, error.userInfo);
abort();
}
}
@end

View File

@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 521 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 667 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1014 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

File diff suppressed because one or more lines are too long

View File

@@ -1,12 +0,0 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "first.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@@ -1,12 +0,0 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "second.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "section-divider.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "section-divider@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "section-divider@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1001 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -1,25 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
</document>

View File

@@ -1,102 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="49e-Tb-3d3">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--First-->
<scene sceneID="hNz-n2-bh7">
<objects>
<viewController id="9pv-A4-QxB" customClass="FirstViewController" customModuleProvider="" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="tsR-hK-woN">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" text="First View" textAlignment="center" lineBreakMode="tailTruncation" minimumFontSize="10" translatesAutoresizingMaskIntoConstraints="NO" id="KQZ-1w-vlD">
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<fontDescription key="fontDescription" type="system" pointSize="36"/>
<color key="textColor" xcode11CocoaTouchSystemColor="labelColor" cocoaTouchSystemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Loaded by FirstViewController" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="A5M-7J-77L">
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" xcode11CocoaTouchSystemColor="labelColor" cocoaTouchSystemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstAttribute="centerX" secondItem="KQZ-1w-vlD" secondAttribute="centerX" id="6BV-lF-sBN"/>
<constraint firstItem="A5M-7J-77L" firstAttribute="top" secondItem="KQZ-1w-vlD" secondAttribute="bottom" constant="8" symbolic="YES" id="cfb-er-3JN"/>
<constraint firstItem="A5M-7J-77L" firstAttribute="centerX" secondItem="KQZ-1w-vlD" secondAttribute="centerX" id="e1l-AV-tCB"/>
<constraint firstAttribute="centerY" secondItem="KQZ-1w-vlD" secondAttribute="centerY" id="exm-UA-ej4"/>
</constraints>
<viewLayoutGuide key="safeArea" id="PQr-Ze-W5v"/>
</view>
<tabBarItem key="tabBarItem" title="First" image="first" id="acW-dT-cKf"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="W5J-7L-Pyd" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="750" y="-320"/>
</scene>
<!--Second-->
<scene sceneID="wg7-f3-ORb">
<objects>
<viewController id="8rJ-Kc-sve" customClass="SecondViewController" customModuleProvider="" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="QS5-Rx-YEW">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" text="Second View" textAlignment="center" lineBreakMode="tailTruncation" minimumFontSize="10" translatesAutoresizingMaskIntoConstraints="NO" id="zEq-FU-wV5">
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<fontDescription key="fontDescription" type="system" pointSize="36"/>
<color key="textColor" xcode11CocoaTouchSystemColor="labelColor" cocoaTouchSystemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Loaded by SecondViewController" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="NDk-cv-Gan">
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" xcode11CocoaTouchSystemColor="labelColor" cocoaTouchSystemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="NDk-cv-Gan" firstAttribute="top" secondItem="zEq-FU-wV5" secondAttribute="bottom" constant="8" symbolic="YES" id="Day-4N-Vmt"/>
<constraint firstItem="NDk-cv-Gan" firstAttribute="centerX" secondItem="zEq-FU-wV5" secondAttribute="centerX" id="JgO-Fn-dHn"/>
<constraint firstAttribute="centerX" secondItem="zEq-FU-wV5" secondAttribute="centerX" id="qqM-NS-xev"/>
<constraint firstAttribute="centerY" secondItem="zEq-FU-wV5" secondAttribute="centerY" id="qzY-Ky-pLD"/>
</constraints>
<viewLayoutGuide key="safeArea" id="O1u-W8-tvY"/>
</view>
<tabBarItem key="tabBarItem" title="Second" image="second" id="cPa-gy-q4n"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="4Nw-L8-lE0" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="750" y="360"/>
</scene>
<!--Tab Bar Controller-->
<scene sceneID="yl2-sM-qoP">
<objects>
<tabBarController id="49e-Tb-3d3" sceneMemberID="viewController">
<tabBar key="tabBar" contentMode="scaleToFill" id="W28-zg-YXA">
<rect key="frame" x="0.0" y="975" width="768" height="49"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
</tabBar>
<connections>
<segue destination="9pv-A4-QxB" kind="relationship" relationship="viewControllers" id="u7Y-xg-7CH"/>
<segue destination="8rJ-Kc-sve" kind="relationship" relationship="viewControllers" id="lzU-1b-eKA"/>
</connections>
</tabBarController>
<placeholder placeholderIdentifier="IBFirstResponder" id="HuB-VB-40B" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="0.0" y="0.0"/>
</scene>
</scenes>
<resources>
<image name="first" width="30" height="30"/>
<image name="second" width="30" height="30"/>
</resources>
</document>

View File

@@ -1,15 +0,0 @@
//
// FirstViewController.h
// MonsterCards
//
// Created by Tom Hicks on 9/2/20.
// Copyright © 2020 Tom Hicks. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface FirstViewController : UIViewController
@end

View File

@@ -1,23 +0,0 @@
//
// FirstViewController.m
// MonsterCards
//
// Created by Tom Hicks on 9/2/20.
// Copyright © 2020 Tom Hicks. All rights reserved.
//
#import "FirstViewController.h"
@interface FirstViewController ()
@end
@implementation FirstViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
}
@end

View File

@@ -0,0 +1,20 @@
//
// Color+Hex.swift
// MonsterCards
//
// Created by Tom Hicks on 1/17/21.
//
import Foundation
import SwiftUI
extension Color {
init(hex: UInt, alpha: Double = 1) {
self.init(
.sRGB,
red: Double((hex >> 16) & 0xff) / 255,
green: Double((hex >> 8) & 0xff) / 255,
blue: Double((hex >> 0) & 0xff) / 255,
opacity: alpha)
}
}

View File

@@ -0,0 +1,111 @@
//
// MonsterImportHelper.swift
// MonsterCards
//
// Created by Tom Hicks on 4/3/21.
//
import Foundation
extension MonsterViewModel {
func maybeAddSense(_ name: String, _ distance: Int) {
if (distance > 0) {
senses.append(StringViewModel("\(name): \(distance) ft."))
}
}
}
class MonsterImportHelper {
static func import5ESBMonster(_ monsterDTO: MonsterDTO) -> MonsterViewModel {
let monster = MonsterViewModel()
monster.name = monsterDTO.name
monster.size = monsterDTO.size
monster.type = monsterDTO.type
monster.subType = monsterDTO.tag
monster.alignment = monsterDTO.alignment
monster.hitDice = Int64(monsterDTO.hitDice)
monster.armorType = ArmorType(rawValue: monsterDTO.armorName) ?? .none
monster.hasShield = monsterDTO.shieldBonus != 0
monster.naturalArmorBonus = Int64(monsterDTO.natArmorBonus)
monster.customArmor = monsterDTO.otherArmorDesc
monster.walkSpeed = Int64(monsterDTO.speed)
monster.burrowSpeed = Int64(monsterDTO.burrowSpeed)
monster.climbSpeed = Int64(monsterDTO.climbSpeed)
monster.flySpeed = Int64(monsterDTO.flySpeed)
monster.swimSpeed = Int64(monsterDTO.swimSpeed)
monster.canHover = monsterDTO.hover
monster.hasCustomHP = monsterDTO.customHP
monster.customHP = monsterDTO.hpText
monster.hasCustomSpeed = monsterDTO.customSpeed
monster.customSpeed = monsterDTO.speedDesc
monster.strengthScore = Int64(monsterDTO.strPoints)
monster.dexterityScore = Int64(monsterDTO.dexPoints)
monster.constitutionScore = Int64(monsterDTO.conPoints)
monster.intelligenceScore = Int64(monsterDTO.intPoints)
monster.wisdomScore = Int64(monsterDTO.wisPoints)
monster.charismaScore = Int64(monsterDTO.chaPoints)
monster.isBlind = monsterDTO.blind
monster.maybeAddSense("blindsight", monsterDTO.blindsight)
monster.maybeAddSense("darkvision", monsterDTO.darkvision)
monster.maybeAddSense("tremorsense", monsterDTO.tremorsense)
monster.maybeAddSense("turesight", monsterDTO.truesight)
monster.telepathy = Int64(monsterDTO.telepathy)
monster.challengeRating = ChallengeRating(rawValue: monsterDTO.cr) ?? ChallengeRating.one
monster.customChallengeRating = monsterDTO.customCr
monster.customProficiencyBonus = Int64(monsterDTO.customProf)
// TODO: Think about adding legendary properties isLegendary, legendariesDescription, isLair, lairDescription, lairDescriptionEnd, isRegional, regionalDescription, regionalDescriptionEnd
monster.abilities = monsterDTO.abilities.map({AbilityViewModel($0.name, $0.desc)})
monster.actions = monsterDTO.actions.map({AbilityViewModel($0.name, $0.desc)})
monster.reactions = monsterDTO.reactions.map({AbilityViewModel($0.name, $0.desc)})
monster.legendaryActions = monsterDTO.legendaries.map({AbilityViewModel($0.name, $0.desc)})
monster.lairActions = monsterDTO.lairs.map({AbilityViewModel($0.name, $0.desc)})
monster.regionalActions = monsterDTO.regionals.map({AbilityViewModel($0.name, $0.desc)})
monsterDTO.sthrows.forEach({
switch $0.name {
case "str":
monster.strengthSavingThrowProficiency = .proficient
case "dex":
monster.dexteritySavingThrowProficiency = .proficient
case "con":
monster.constitutionSavingThrowProficiency = .proficient
case "int":
monster.intelligenceSavingThrowProficiency = .proficient
case "wis":
monster.wisdomSavingThrowProficiency = .proficient
case "cha":
monster.charismaSavingThrowProficiency = .proficient
default:
break
}
})
monster.skills = monsterDTO.skills.map({
// TODO: consider using a lookup table to make fixing missing stats easier
SkillViewModel(
$0.name,
AbilityScore(rawValue: $0.stat) ?? .dexterity,
$0.note == " (ex)" ? .expertise : .proficient)
})
monster.damageImmunities = monsterDTO.damageTypes
.filter({$0.type == "i" || $0.type == " (Immune)"})
.map({StringViewModel($0.name)})
monster.damageResistances = monsterDTO.damageTypes
.filter({$0.type == "r" || $0.type == " (Resistant)"})
.map({StringViewModel($0.name)})
monster.damageVulnerabilities = monsterDTO.damageTypes
.filter({$0.type == "v" || $0.type == " (Vulnerable)"})
.map({StringViewModel($0.name)})
monster.conditionImmunities = monsterDTO.conditions.map({StringViewModel($0.name)})
monster.languages = monsterDTO.languages
.map({
LanguageViewModel($0.name, $0.speaks)
})
monster.understandsBut = monsterDTO.understandsBut
// TODO: add shortName or nickname
// monster.shortName = monsterDTO.shortName
// TODO: look into what goes in specialdamage and damage
return monster
}
}

View File

@@ -0,0 +1,50 @@
//
// StringHelper.swift
// MonsterCards
//
// Created by Tom Hicks on 3/21/21.
//
import Foundation
class StringHelper {
static func oxfordJoin(
_ strings: [String],
_ separator: String = ", ",
_ lastSeparator: String = ", and ",
_ onlySeparator: String = " and "
) -> String {
let numStrings = strings.count
if (numStrings < 1) {
return "";
} else if (numStrings == 2) {
return strings[0] + onlySeparator + strings[1]
} else {
var joined = ""
var index = 0
let lastIndex = numStrings - 1
strings.forEach {
if index > 0 && index < lastIndex {
joined.append(separator)
} else if (index > 0 && index >= lastIndex) {
joined.append(lastSeparator)
}
joined.append($0)
index = index + 1
}
return joined
}
}
static func safeContainsCaseInsensitive(_ str: String?, _ match: String) -> Bool {
guard let str = str else { return false }
return str.localizedCaseInsensitiveContains(match)
}
static func safeEqualsIgnoreCase(_ str: String?, _ match: String) -> Bool {
guard let str = str else { return false }
return str.lowercased() == match.lowercased()
}
}

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "section-divider.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "section-divider@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "section-divider@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1001 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -4,8 +4,25 @@
<dict> <dict>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeIconFiles</key>
<array/>
<key>CFBundleTypeName</key>
<string>Monster Data</string>
<key>LSHandlerRank</key>
<string>Owner</string>
<key>LSItemContentTypes</key>
<array>
<string>com.majinnaibu.MonsterCards.Monster</string>
</array>
</dict>
</array>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIconName</key>
<string>AppIcon</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
@@ -15,48 +32,30 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string> <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.0</string> <string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1</string> <string>$(CURRENT_PROJECT_VERSION)</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>UIApplicationSceneManifest</key> <key>UIApplicationSceneManifest</key>
<dict> <dict>
<key>UIApplicationSupportsMultipleScenes</key> <key>UIApplicationSupportsMultipleScenes</key>
<false/> <true/>
<key>UISceneConfigurations</key> </dict>
<dict> <key>UIApplicationSupportsIndirectInputEvents</key>
<key>UIWindowSceneSessionRoleApplication</key> <true/>
<key>UIBackgroundModes</key>
<array> <array>
<dict> <string>remote-notification</string>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array> </array>
</dict> <key>UILaunchScreen</key>
</dict> <dict/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIRequiredDeviceCapabilities</key> <key>UIRequiredDeviceCapabilities</key>
<array> <array>
<string>armv7</string> <string>armv7</string>
</array> </array>
<key>UIStatusBarTintParameters</key>
<dict>
<key>UINavigationBar</key>
<dict>
<key>Style</key>
<string>UIBarStyleDefault</string>
<key>Translucent</key>
<false/>
</dict>
</dict>
<key>UISupportedInterfaceOrientations</key> <key>UISupportedInterfaceOrientations</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
@@ -70,5 +69,63 @@
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>UISupportsDocumentBrowser</key>
<true/>
<key>UIUserInterfaceStyle</key>
<string>Light</string>
<key>UTExportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
<string>public.content</string>
</array>
<key>UTTypeDescription</key>
<string>Monster data file</string>
<key>UTTypeIconFiles</key>
<array/>
<key>UTTypeIdentifier</key>
<string>com.majinnaibu.MonsterCards.Monster</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>monster</string>
</array>
<key>public.mime-type</key>
<array>
<string>text/vnd.monstercards.monster</string>
</array>
</dict>
</dict>
</array>
<key>UTImportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
<string>public.content</string>
</array>
<key>UTTypeDescription</key>
<string>Monster data file</string>
<key>UTTypeIconFiles</key>
<array/>
<key>UTTypeIdentifier</key>
<string>com.majinnaibu.MonsterCards.Monster</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>monster</string>
</array>
<key>public.mime-type</key>
<array>
<string>text/vnd.monstercards.monster</string>
</array>
</dict>
</dict>
</array>
</dict> </dict>
</plist> </plist>

View File

@@ -0,0 +1,116 @@
//
// AbilityViewModel.swift
// MonsterCards
//
// Created by Tom Hicks on 3/25/21.
//
import Foundation
public class AbilityViewModel: NSObject, ObservableObject, Identifiable, NSSecureCoding {
public static var supportsSecureCoding = true
public func encode(with coder: NSCoder) {
coder.encode(self.name, forKey: "name")
coder.encode(self.abilityDescription, forKey: "abilityDescription")
}
public required init?(coder: NSCoder) {
self.name = coder.decodeObject(of: NSString.self, forKey: "name")! as String
self.abilityDescription = coder.decodeObject(of: NSString.self, forKey: "abilityDescription")! as String
}
@Published public var name: String
@Published public var abilityDescription: String
public init(_ name: String = "", _ abilityDescription: String = "") {
self.name = name
self.abilityDescription = abilityDescription
}
public var fullText: String {
get {
return String(format: "___%@:___ %@", name, abilityDescription)
}
}
func renderedText(_ monster: MonsterViewModel) -> String {
let strSave = monster.strengthModifier + monster.proficiencyBonus + 8
let dexSave = monster.dexterityModifier + monster.proficiencyBonus + 8
let conSave = monster.constitutionModifier + monster.proficiencyBonus + 8
let intSave = monster.intelligenceModifier + monster.proficiencyBonus + 8
let wisSave = monster.wisdomModifier + monster.proficiencyBonus + 8
let chaSave = monster.charismaModifier + monster.proficiencyBonus + 8
let strAttack = monster.strengthModifier + monster.proficiencyBonus
let dexAttack = monster.dexterityModifier + monster.proficiencyBonus
let conAttack = monster.constitutionModifier + monster.proficiencyBonus
let intAttack = monster.intelligenceModifier + monster.proficiencyBonus
let wisAttack = monster.wisdomModifier + monster.proficiencyBonus
let chaAttack = monster.charismaModifier + monster.proficiencyBonus
// TODO: find the other options and implement them [WIS], [WIS STAT], [WIS DMG], [WIS STAT 1d12]
let finalText = fullText
.replacingOccurrences(of: "[STR SAVE]", with: String(strSave))
.replacingOccurrences(of: "[DEX SAVE]", with: String(dexSave))
.replacingOccurrences(of: "[CON SAVE]", with: String(conSave))
.replacingOccurrences(of: "[INT SAVE]", with: String(intSave))
.replacingOccurrences(of: "[WIS SAVE]", with: String(wisSave))
.replacingOccurrences(of: "[CHA SAVE]", with: String(chaSave))
.replacingOccurrences(of: "[STR ATK]", with: String(strAttack))
.replacingOccurrences(of: "[DEX ATK]", with: String(dexAttack))
.replacingOccurrences(of: "[CON ATK]", with: String(conAttack))
.replacingOccurrences(of: "[INT ATK]", with: String(intAttack))
.replacingOccurrences(of: "[WIS ATK]", with: String(wisAttack))
.replacingOccurrences(of: "[CHA ATK]", with: String(chaAttack))
return finalText
}
}
extension AbilityViewModel: Comparable {
public static func < (lhs: AbilityViewModel, rhs: AbilityViewModel) -> Bool {
lhs.name < rhs.name
}
public static func == (lhs: AbilityViewModel, rhs: AbilityViewModel) -> Bool {
lhs.name == rhs.name &&
lhs.abilityDescription == rhs.abilityDescription
}
}
// TODO: figure out how to add this to the set of known transformers so it will work with transformer set to NSSecureUnarchiveFromDataTransformerName
@objc(AbilityViewModelValueTransformer)
public final class AbilityViewModelValueTransformer: ValueTransformer {
override public class func transformedValueClass() -> AnyClass {
return NSArray.self
}
override public class func allowsReverseTransformation() -> Bool {
return true
}
override public func transformedValue(_ value: Any?) -> Any? {
guard let language = value as? NSArray else { return nil }
do {
let data = try NSKeyedArchiver.archivedData(withRootObject: language, requiringSecureCoding: true)
return data
} catch {
assertionFailure("Failed to transform `AbilityViewModel` to `Data`")
return nil
}
}
override public func reverseTransformedValue(_ value: Any?) -> Any? {
guard let data = value as? NSData else { return nil }
do {
let language = try NSKeyedUnarchiver.unarchivedArrayOfObjects(ofClass: AbilityViewModel.self, from: data as Data)
return language
} catch {
assertionFailure("Failed to transform `Data` to `AbilityViewModel`")
return nil
}
}
}

View File

@@ -0,0 +1,46 @@
//
// ChallengeRatingViewModel.swift
// MonsterCards
//
// Created by Tom Hicks on 3/24/21.
//
import Foundation
class ChallengeRatingViewModel: ObservableObject/*, Comparable*/, Identifiable {
func encode(with coder: NSCoder) {
coder.encode(self.rating.rawValue, forKey: "rating")
}
static func == (lhs: ChallengeRatingViewModel, rhs: ChallengeRatingViewModel) -> Bool {
lhs.rating == rhs.rating &&
lhs.customText == rhs.customText &&
lhs.customProficiencyBonus == rhs.customProficiencyBonus
}
@Published var rating: ChallengeRating
@Published var customText: String
@Published var customProficiencyBonus: Int64
init(
_ rating: ChallengeRating = .one,
_ customText: String = "",
_ customProficiencyBonus: Int64 = 0
) {
self.rating = rating
self.customText = customText
self.customProficiencyBonus = customProficiencyBonus
}
init(
_ rating: String = ChallengeRating.one.rawValue,
_ customText: String = "",
_ customProficiencyBonus: Int64 = 0
) {
self.rating = ChallengeRating(rawValue: rating) ?? .one
self.customText = customText
self.customProficiencyBonus = customProficiencyBonus
}
}

View File

@@ -0,0 +1,42 @@
//
// DamageTypeDTO.swift
// MonsterCards
//
// Created by Tom Hicks on 3/28/21.
//
import Foundation
struct DamageTypeDTO {
var name: String
var note: String
var type: String
}
private enum DamageTypeDTOCodingKeys: String, CodingKey {
case name = "name"
case note = "note"
case type = "type"
}
extension DamageTypeDTO: Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: DamageTypeDTOCodingKeys.self)
self.name = (try? container.decode(String.self, forKey: .name)) ?? ""
self.note = (try? container.decode(String.self, forKey: .note)) ?? ""
self.type = (try? container.decode(String.self, forKey: .type)) ?? ""
}
}
extension DamageTypeDTO: Encodable {
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: DamageTypeDTOCodingKeys.self)
try container.encode(self.name, forKey: .name)
try container.encode(self.note, forKey: .note)
try container.encode(self.type, forKey: .type)
}
}

View File

@@ -0,0 +1,73 @@
//
// AbilityScore.swift
// MonsterCards
//
// Created by Tom Hicks on 1/18/21.
//
import Foundation
enum AbilityScore: String, CaseIterable, Identifiable {
case strength = "strength"
case dexterity = "dexterity"
case constitution = "constitution"
case intelligence = "intelligence"
case wisdom = "wisdom"
case charisma = "charisma"
var id: AbilityScore { self }
var displayName: String {
switch self {
case .strength:
return "Strength"
case .dexterity:
return "Dexterity"
case .constitution:
return "Constitution"
case .intelligence:
return "Intelligence"
case .wisdom:
return "Wisdom"
case .charisma:
return "Charisma"
}
}
var shortDisplayName: String {
switch self {
case .strength:
return "STR"
case .dexterity:
return "DEX"
case .constitution:
return "CON"
case .intelligence:
return "INT"
case .wisdom:
return "WIS"
case .charisma:
return "CHA"
}
}
init?(rawValue: String) {
var match: AbilityScore? = nil
let raw = rawValue.lowercased()
for abilityScore in AbilityScore.allCases {
if (abilityScore.rawValue.lowercased() == raw) {
match = abilityScore
}
if (abilityScore.shortDisplayName.lowercased() == raw) {
match = abilityScore
}
}
if (match == nil) {
return nil
} else {
self = match!
}
}
}

View File

@@ -0,0 +1,27 @@
//
// AdvantageType.swift
// MonsterCards
//
// Created by Tom Hicks on 1/17/21.
//
import Foundation
enum AdvantageType: String, CaseIterable, Identifiable {
case none = "none"
case advantage = "advantage"
case disadvantage = "disadvantage"
var id: AdvantageType { self }
var displayName: String {
switch self {
case .none:
return "None"
case .advantage:
return "Advantage"
case .disadvantage:
return "Disadvantage"
}
}
}

View File

@@ -0,0 +1,50 @@
//
// ArmorType.swift
// MonsterCards
//
// Created by Tom Hicks on 3/21/21.
//
import Foundation
enum ArmorType: String, CaseIterable, Identifiable {
case none = "none"
case naturalArmor = "natural armor"
case mageArmor = "mage armor"
case padded = "padded"
case leather = "leather"
case studdedLeather = "studded"
case hide = "hide"
case chainShirt = "chain shirt"
case scaleMail = "scale mail"
case breastplate = "breastplate"
case halfPlate = "half plate"
case ringMail = "ring mail"
case chainMail = "chain mail"
case splintMail = "splint"
case plateMail = "plate"
case other = "other"
var id: ArmorType { self }
var displayName: String {
switch self {
case .none: return "None"
case .naturalArmor: return "Natural Armor"
case .mageArmor: return "Mage Armor"
case .padded: return "Padded"
case .leather: return "Leather"
case .studdedLeather: return "Studded Leather"
case .hide: return "Hide"
case .chainShirt: return "Chain Shirt"
case .scaleMail: return "Scale Mail"
case .breastplate: return "Breastplate"
case .halfPlate: return "Half Plate"
case .ringMail: return "Ring Mail"
case .chainMail: return "Chain Mail"
case .splintMail: return "Splint Mail"
case .plateMail: return "Plate Mail"
case .other: return "Other"
}
}
}

View File

@@ -0,0 +1,123 @@
//
// ChallengeRating.swift
// MonsterCards
//
// Created by Tom Hicks on 3/21/21.
//
import Foundation
enum ChallengeRating: String, CaseIterable, Identifiable {
case custom = "Custom"
case zero = "0"
case oneEighth = "1/8"
case oneQuarter = "1/4"
case oneHalf = "1/2"
case one = "1"
case two = "2"
case three = "3"
case four = "4"
case five = "5"
case six = "6"
case seven = "7"
case eight = "8"
case nine = "9"
case ten = "10"
case eleven = "11"
case twelve = "12"
case thirteen = "13"
case fourteen = "14"
case fifteen = "15"
case sixteen = "16"
case seventeen = "17"
case eighteen = "18"
case nineteen = "19"
case twenty = "20"
case twentyOne = "21"
case twentyTwo = "22"
case twentyThree = "23"
case twentyFour = "24"
case twentyFive = "25"
case twentySix = "26"
case twentySeven = "27"
case twentyEight = "28"
case twentyNine = "29"
case thirty = "30"
var id: ChallengeRating { self }
var displayName: String {
switch(self) {
case .custom:
return "Custom"
case .zero:
return "0 (10 XP)"
case .oneEighth:
return "1/8 (25 XP)"
case .oneQuarter:
return "1/4 (50 XP)"
case .oneHalf:
return "1/2 (100 XP)"
case .one:
return "1 (200 XP)"
case .two:
return "2 (450 XP)"
case .three:
return "3 (700 XP)"
case .four:
return "4 (1,100 XP)"
case .five:
return "5 (1,800 XP)"
case .six:
return "6 (2,300 XP)"
case .seven:
return "7 (2,900 XP)"
case .eight:
return "8 (3,900 XP)"
case .nine:
return "9 (5,000 XP)"
case .ten:
return "10 (5,900 XP)"
case .eleven:
return "11 (7,200 XP)"
case .twelve:
return "12 (8,400 XP)"
case .thirteen:
return "13 (10,000 XP)"
case .fourteen:
return "14 (11,500 XP)"
case .fifteen:
return "15 (13,000 XP)"
case .sixteen:
return "16 (15,000 XP)"
case .seventeen:
return "17 (18,000 XP)"
case .eighteen:
return "18 (20,000 XP)"
case .nineteen:
return "19 (22,000 XP)"
case .twenty:
return "20 (25,000 XP)"
case .twentyOne:
return "21 (33,000 XP)"
case .twentyTwo:
return "22 (41,000 XP)"
case .twentyThree:
return "23 (50,000 XP)"
case .twentyFour:
return "24 (62,000 XP)"
case .twentyFive:
return "25 (75,000 XP)"
case .twentySix:
return "26 (90,000 XP)"
case .twentySeven:
return "27 (105,000 XP)"
case .twentyEight:
return "28 (120,000 XP)"
case .twentyNine:
return "29 (135,000 XP)"
case .thirty:
return "30 (155,000 XP)"
}
}
}

View File

@@ -0,0 +1,27 @@
//
// ProficiencyType.swift
// MonsterCards
//
// Created by Tom Hicks on 1/17/21.
//
import Foundation
enum ProficiencyType: String, CaseIterable, Identifiable {
case none = "none"
case proficient = "proficient"
case expertise = "expertise"
var id: ProficiencyType { self }
var displayName: String {
switch self {
case .none:
return "None"
case .proficient:
return "Proficient"
case .expertise:
return "Expertise"
}
}
}

View File

@@ -0,0 +1,52 @@
//
// SizeType.swift
// MonsterCards
//
// Created by Tom Hicks on 3/21/21.
//
import Foundation
enum SizeType: String, CaseIterable, Identifiable {
case tiny = "tiny"
case small = "small"
case medium = "medium"
case large = "large"
case huge = "huge"
case gargantuan = "gargantuan"
var id: SizeType { self }
var displayName: String {
switch self {
case .tiny: return "Tiny"
case .small: return "Small"
case .medium: return "Medium"
case .large: return "Large"
case .huge: return "Huge"
case .gargantuan: return "gargantuan"
}
}
init?(rawValue: String) {
var match: SizeType? = nil
for size in SizeType.allCases {
if (size.rawValue == rawValue) {
match = size
}
}
for size in SizeType.allCases {
if (size.rawValue.lowercased() == rawValue.lowercased()) {
match = size
}
}
if (match == nil) {
return nil
} else {
self = match!
}
}
}

View File

@@ -0,0 +1,35 @@
//
// LanguageDTO.swift
// MonsterCards
//
// Created by Tom Hicks on 3/28/21.
//
import Foundation
struct LanguageDTO {
var name: String
var speaks: Bool
}
private enum LanguageDTOCodingKeys: String, CodingKey {
case name = "name"
case speaks = "speaks"
}
extension LanguageDTO: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: LanguageDTOCodingKeys.self)
self.name = (try? container.decode(String.self, forKey: .name)) ?? ""
self.speaks = (try? container.decode(Bool.self, forKey: .speaks)) ?? false
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: LanguageDTOCodingKeys.self)
try container.encode(self.name, forKey: .name)
try container.encode(self.speaks, forKey: .speaks)
}
}

View File

@@ -0,0 +1,79 @@
//
// LanguageViewModel.swift
// MonsterCards
//
// Created by Tom Hicks on 3/24/21.
//
import Foundation
// TODO: split this into separate Model and ViewModel classes later.
public class LanguageViewModel : NSObject, ObservableObject, Comparable, Identifiable, NSSecureCoding {
public static var supportsSecureCoding = true
public func encode(with coder: NSCoder) {
coder.encode(self.name, forKey: "name")
coder.encode(self.speaks, forKey: "speaks")
}
public required init?(coder: NSCoder) {
self.name = coder.decodeObject(of: NSString.self, forKey: "name")! as String
self.speaks = coder.decodeBool(forKey: "speaks")
}
public static func < (lhs: LanguageViewModel, rhs: LanguageViewModel) -> Bool {
lhs.name < rhs.name
}
public static func == (lhs: LanguageViewModel, rhs: LanguageViewModel) -> Bool {
lhs.name == rhs.name &&
lhs.speaks == rhs.speaks
}
@Published var name: String
@Published var speaks: Bool
public init(
_ name: String = "",
_ speaks: Bool = true
) {
self.name = name
self.speaks = speaks
}
}
// TODO: figure out how to add this to the set of known transformers so it will work with transformer set to NSSecureUnarchiveFromDataTransformerName
@objc(LanguageViewModelValueTransformer)
public final class LanguageViewModelValueTransformer: ValueTransformer {
override public class func transformedValueClass() -> AnyClass {
return NSArray.self
}
override public class func allowsReverseTransformation() -> Bool {
return true
}
override public func transformedValue(_ value: Any?) -> Any? {
guard let language = value as? NSArray else { return nil }
do {
let data = try NSKeyedArchiver.archivedData(withRootObject: language, requiringSecureCoding: true)
return data
} catch {
assertionFailure("Failed to transform `LanguageViewModel` to `Data`")
return nil
}
}
override public func reverseTransformedValue(_ value: Any?) -> Any? {
guard let data = value as? NSData else { return nil }
do {
let language = try NSKeyedUnarchiver.unarchivedArrayOfObjects(ofClass: LanguageViewModel.self, from: data as Data)
return language
} catch {
assertionFailure("Failed to transform `Data` to `LanguageViewModel`")
return nil
}
}
}

View File

@@ -0,0 +1,161 @@
//
// Monster+CoreDataClass.swift
// MonsterCards
//
// Created by Tom Hicks on 1/15/21.
//
//
import Foundation
import CoreData
@objc(Monster)
public class Monster: NSManagedObject {
convenience init(context: NSManagedObjectContext, name: String, size: String, type: String, subtype: String, alignment: String) {
self.init(context:context)
self.name = name;
self.size = size;
self.type = type;
self.subtype = subtype;
self.alignment = alignment;
}
// MARK: Armor
var armorTypeEnum: ArmorType {
get {
return ArmorType.init(rawValue: armorType ?? "none") ?? .none
}
set {
armorType = newValue.rawValue
}
}
// MARK: Challenge Rating / Proficiency Bonus
var challengeRatingEnum: ChallengeRating {
get {
return ChallengeRating.init(rawValue: challengeRating ?? "1") ?? .one
}
set {
challengeRating = newValue.rawValue
}
}
// MARK: Saving Throws
var strengthSavingThrowProficiencyEnum: ProficiencyType {
get {
return ProficiencyType.init(rawValue: strengthSavingThrowProficiency ?? "") ?? .none
}
set {
strengthSavingThrowProficiency = newValue.rawValue
}
}
var strengthSavingThrowAdvantageEnum: AdvantageType {
get {
return AdvantageType.init(rawValue: strengthSavingThrowAdvantage ?? "") ?? .none
}
set {
strengthSavingThrowAdvantage = newValue.rawValue
}
}
var dexteritySavingThrowProficiencyEnum: ProficiencyType {
get {
return ProficiencyType.init(rawValue: dexteritySavingThrowProficiency ?? "") ?? .none
}
set {
dexteritySavingThrowProficiency = newValue.rawValue
}
}
var dexteritySavingThrowAdvantageEnum: AdvantageType {
get {
return AdvantageType.init(rawValue: dexteritySavingThrowAdvantage ?? "") ?? .none
}
set {
dexteritySavingThrowAdvantage = newValue.rawValue
}
}
var constitutionSavingThrowProficiencyEnum: ProficiencyType {
get {
return ProficiencyType.init(rawValue: constitutionSavingThrowProficiency ?? "") ?? .none
}
set {
constitutionSavingThrowProficiency = newValue.rawValue
}
}
var constitutionSavingThrowAdvantageEnum: AdvantageType {
get {
return AdvantageType.init(rawValue: constitutionSavingThrowAdvantage ?? "") ?? .none
}
set {
constitutionSavingThrowAdvantage = newValue.rawValue
}
}
var intelligenceSavingThrowProficiencyEnum: ProficiencyType {
get {
return ProficiencyType.init(rawValue: intelligenceSavingThrowProficiency ?? "") ?? .none
}
set {
intelligenceSavingThrowProficiency = newValue.rawValue
}
}
var intelligenceSavingThrowAdvantageEnum: AdvantageType {
get {
return AdvantageType.init(rawValue: intelligenceSavingThrowAdvantage ?? "") ?? .none
}
set {
intelligenceSavingThrowAdvantage = newValue.rawValue
}
}
var wisdomSavingThrowProficiencyEnum: ProficiencyType {
get {
return ProficiencyType.init(rawValue: wisdomSavingThrowProficiency ?? "") ?? .none
}
set {
wisdomSavingThrowProficiency = newValue.rawValue
}
}
var wisdomSavingThrowAdvantageEnum: AdvantageType {
get {
return AdvantageType.init(rawValue: wisdomSavingThrowAdvantage ?? "") ?? .none
}
set {
wisdomSavingThrowAdvantage = newValue.rawValue
}
}
var charismaSavingThrowProficiencyEnum: ProficiencyType {
get {
return ProficiencyType.init(rawValue: charismaSavingThrowProficiency ?? "") ?? .none
}
set {
charismaSavingThrowProficiency = newValue.rawValue
}
}
var charismaSavingThrowAdvantageEnum: AdvantageType {
get {
return AdvantageType.init(rawValue: charismaSavingThrowAdvantage ?? "") ?? .none
}
set {
charismaSavingThrowAdvantage = newValue.rawValue
}
}
// MARK: End
}

View File

@@ -0,0 +1,353 @@
//
// MonsterDTO.swift
// MonsterCards
//
// Created by Tom Hicks on 3/28/21.
//
import Foundation
struct MonsterDTO {
var name: String
var type: String
var alignment: String
var size: String
var hitDice: Int
var armorName: String
var otherArmorDesc: String
var shieldBonus: Int
var natArmorBonus: Int
var speed: Int
var burrowSpeed: Int
var climbSpeed: Int
var flySpeed: Int
var hover: Bool
var swimSpeed: Int
var speedDesc: String
var customSpeed: Bool
var strPoints: Int
var dexPoints: Int
var conPoints: Int
var intPoints: Int
var wisPoints: Int
var chaPoints: Int
var cr: String
var customCr: String
var customProf: Int
var hpText: String
var sthrows: [SavingThrowDTO]
var skills: [SkillDTO]
var actions: [TraitDTO]
var legendaryDescription: String
var legendaries: [TraitDTO]
var reactions: [TraitDTO]// TODO: verify this
var abilities: [TraitDTO]
var damageTypes: [DamageTypeDTO]
var conditions: [DamageTypeDTO] // TODO: figure this out
var languages: [LanguageDTO]
var telepathy: Int
var understandsBut: String
var blindsight: Int
var blind: Bool
var darkvision: Int
var tremorsense: Int
var truesight: Int
var tag: String
var customHP: Bool
var isLegendary: Bool
var isLair: Bool
var lairDescription: String
var lairDescriptionEnd: String
var isRegional: Bool
var regionalDescription: String
var regionalDescriptionEnd: String
// var properties: [???] // TODO: figure this out
var lairs: [TraitDTO]
var regionals: [TraitDTO]
var specialDamage: [DamageTypeDTO]
var shortName: String
var doubleColumns: Bool
var separationPoint: Int
var damage: [DamageTypeDTO] // TODO: figure this out
init() {
self.abilities = []
self.actions = []
self.alignment = ""
self.armorName = ""
self.blind = false
self.blindsight = 0
self.burrowSpeed = 0
self.chaPoints = 0
self.climbSpeed = 0
self.conPoints = 0
self.conditions = []
self.cr = ""
self.customCr = ""
self.customHP = false
self.customProf = 0
self.customSpeed = false
self.damage = []
self.damageTypes = []
self.darkvision = 0
self.dexPoints = 0
self.doubleColumns = false
self.flySpeed = 0
self.hitDice = 0
self.hover = false
self.hpText = ""
self.intPoints = 0
self.isLair = false
self.isLegendary = false
self.isRegional = false
self.lairs = []
self.languages = []
self.legendaries = []
self.lairDescription = ""
self.legendaryDescription = ""
self.lairDescriptionEnd = ""
self.name = ""
self.natArmorBonus = 0
self.otherArmorDesc = ""
self.reactions = []
self.regionals = []
self.regionalDescription = ""
self.regionalDescriptionEnd = ""
self.size = ""
self.speed = 0
self.skills = []
self.sthrows = []
self.strPoints = 0
self.swimSpeed = 0
self.speedDesc = ""
self.shortName = ""
self.specialDamage = []
self.shieldBonus = 0
self.separationPoint = 0
self.type = ""
self.tag = ""
self.telepathy = 0
self.truesight = 0
self.tremorsense = 0
self.understandsBut = ""
self.wisPoints = 0
}
}
enum MonsterDTOCodingKeys: String, CodingKey {
case name = "name"
case type = "type"
case alignment = "alignment"
case size = "size"
case hitDice = "hitDice"
case armorName = "armorName"
case otherArmorDesc = "otherArmorDesc"
case shieldBonus = "shieldBonus"
case natArmorBonus = "natArmorBonus"
case speed = "speed"
case burrowSpeed = "burrowSpeed"
case climbSpeed = "climbSpeed"
case flySpeed = "flySpeed"
case hover = "hover"
case swimSpeed = "swimSpeed"
case speedDesc = "speedDesc"
case customSpeed = "customSpeed"
case strPoints = "strPoints"
case dexPoints = "dexPoints"
case conPoints = "conPoints"
case intPoints = "intPoints"
case wisPoints = "wisPoints"
case chaPoints = "chaPoints"
case cr = "cr"
case customCr = "customCr"
case customProf = "customProf"
case hpText = "hpText"
case sthrows = "sthrows"
case skills = "skills"
case actions = "actions"
case legendaryDescription = "legendaryDescription"
case legendaries = "legendaries"
case reactions = "reactions"
case abilities = "abilities"
case damageTypes = "damageTypes"
case conditions = "conditions"
case languages = "languages"
case telepathy = "telepathy"
case understandsBut = "understandsBut"
case blindsight = "blindsight"
case blind = "blind"
case darkvision = "darkvision"
case tremorsense = "tremorsense"
case truesight = "truesight"
case tag = "tag"
case customHP = "customHP"
case isLegendary = "isLegendary"
case isLair = "isLair"
case lairDescription = "lairDescription"
case lairDescriptionEnd = "lairDescriptionEnd"
case isRegional = "isRegional"
case regionalDescription = "regionalDescription"
case regionalDescriptionEnd = "regionalDescriptionEnd"
case properties = "properties"
case lairs = "lairs"
case regionals = "regionals"
case specialDamage = "specialDamage"
case shortName = "shortName"
case doubleColumns = "doubleColumns"
case separationPoint = "separationPoint"
case damage = "damage"
}
func readInt(_ container: KeyedDecodingContainer<MonsterDTOCodingKeys>, _ key: MonsterDTOCodingKeys, _ defaultValue: Int = 0) -> Int {
let readInt = try? container.decode(Int.self, forKey: key)
let readString = try? container.decode(String.self, forKey: key)
if (readInt != nil) {
return readInt!
}
if (readString != nil) {
return Int(readString!) ?? defaultValue
}
return defaultValue;
}
func readString(_ container: KeyedDecodingContainer<MonsterDTOCodingKeys>, _ key: MonsterDTOCodingKeys, _ defaultValue: String = "") -> String {
return (try? container.decode(String.self, forKey: key)) ?? defaultValue
}
extension MonsterDTO: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: MonsterDTOCodingKeys.self)
self.name = readString(container, .name, "Imported Monster")
self.type = readString(container, .type, "")
self.alignment = readString(container, .alignment, "")
self.size = readString(container, .size, "")
self.hitDice = readInt(container, .hitDice, 0)
self.armorName = readString(container, .armorName, "")
self.otherArmorDesc = readString(container, .otherArmorDesc, "")
self.shieldBonus = readInt(container, .shieldBonus, 0)
self.natArmorBonus = readInt(container, .natArmorBonus, 0)
self.speed = readInt(container, .speed, 0)
self.burrowSpeed = readInt(container, .burrowSpeed, 0)
self.climbSpeed = readInt(container, .climbSpeed, 0)
self.flySpeed = readInt(container, .flySpeed, 0)
self.hover = (try? container.decode(Bool.self, forKey: .hover)) ?? false
self.swimSpeed = readInt(container, .swimSpeed, 0)
self.speedDesc = readString(container, .speedDesc, "")
self.customSpeed = (try? container.decode(Bool.self, forKey: .customSpeed)) ?? false
self.strPoints = readInt(container, .strPoints, 0)
self.dexPoints = readInt(container, .dexPoints, 0)
self.conPoints = readInt(container, .conPoints, 0)
self.intPoints = readInt(container, .intPoints, 0)
self.wisPoints = readInt(container, .wisPoints, 0)
self.chaPoints = readInt(container, .chaPoints, 0)
self.cr = readString(container, .cr, "")
self.customCr = readString(container, .customCr, "")
self.customProf = readInt(container, .customProf, 0)
self.hpText = readString(container, .hpText, "")
self.legendaryDescription = readString(container, .legendaryDescription, "")
self.understandsBut = readString(container, .understandsBut, "")
self.tag = readString(container, .tag, "")
self.lairDescription = readString(container, .lairDescription, "")
self.lairDescriptionEnd = readString(container, .lairDescriptionEnd, "")
self.regionalDescription = readString(container, .regionalDescription, "")
self.regionalDescriptionEnd = readString(container, .regionalDescriptionEnd, "")
self.shortName = readString(container, .shortName, "")
self.telepathy = readInt(container, .telepathy, 0)
self.blindsight = readInt(container, .blindsight, 0)
self.darkvision = readInt(container, .darkvision, 0)
self.tremorsense = readInt(container, .tremorsense, 0)
self.truesight = readInt(container, .truesight, 0)
self.separationPoint = readInt(container, .separationPoint, 0)
self.blind = (try? container.decode(Bool.self, forKey: .blind)) ?? false
self.customHP = (try? container.decode(Bool.self, forKey: .customHP)) ?? false
self.isLegendary = (try? container.decode(Bool.self, forKey: .isLegendary)) ?? false
self.isLair = (try? container.decode(Bool.self, forKey: .isLair)) ?? false
self.isRegional = (try? container.decode(Bool.self, forKey: .isRegional)) ?? false
self.doubleColumns = (try? container.decode(Bool.self, forKey: .doubleColumns)) ?? false
self.lairs = (try? container.decode([TraitDTO].self, forKey: .lairs)) ?? []
self.regionals = (try? container.decode([TraitDTO].self, forKey: .regionals)) ?? []
self.specialDamage = (try? container.decode([DamageTypeDTO].self, forKey: .specialDamage)) ?? []
self.damage = (try? container.decode([DamageTypeDTO].self, forKey: .damage)) ?? []
self.conditions = (try? container.decode([DamageTypeDTO].self, forKey: .conditions)) ?? []
self.sthrows = (try? container.decode([SavingThrowDTO].self, forKey: .sthrows)) ?? []
self.skills = (try? container.decode([SkillDTO].self, forKey: .skills)) ?? []
self.actions = (try? container.decode([TraitDTO].self, forKey: .actions)) ?? []
self.legendaries = (try? container.decode([TraitDTO].self, forKey: .legendaries)) ?? []
self.reactions = (try? container.decode([TraitDTO].self, forKey: .reactions)) ?? []
self.abilities = (try? container.decode([TraitDTO].self, forKey: .abilities)) ?? []
self.damageTypes = (try? container.decode([DamageTypeDTO].self, forKey: .damageTypes)) ?? []
self.languages = (try? container.decode([LanguageDTO].self, forKey: .languages)) ?? []
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: MonsterDTOCodingKeys.self)
try container.encode(self.name, forKey: .name)
try container.encode(self.type, forKey: .type)
try container.encode(self.alignment, forKey: .alignment)
try container.encode(self.size, forKey: .size)
try container.encode(self.hitDice, forKey: .hitDice)
try container.encode(self.armorName, forKey: .armorName)
try container.encode(self.otherArmorDesc, forKey: .otherArmorDesc)
try container.encode(self.shieldBonus, forKey: .shieldBonus)
try container.encode(self.natArmorBonus, forKey: .natArmorBonus)
try container.encode(self.speed, forKey: .speed)
try container.encode(self.burrowSpeed, forKey: .burrowSpeed)
try container.encode(self.climbSpeed, forKey: .climbSpeed)
try container.encode(self.flySpeed, forKey: .flySpeed)
try container.encode(self.hover, forKey: .hover)
try container.encode(self.swimSpeed, forKey: .swimSpeed)
try container.encode(self.speedDesc, forKey: .speedDesc)
try container.encode(self.customSpeed, forKey: .customSpeed)
try container.encode(self.strPoints, forKey: .strPoints)
try container.encode(self.dexPoints, forKey: .dexPoints)
try container.encode(self.conPoints, forKey: .conPoints)
try container.encode(self.intPoints, forKey: .intPoints)
try container.encode(self.wisPoints, forKey: .wisPoints)
try container.encode(self.chaPoints, forKey: .chaPoints)
try container.encode(self.cr, forKey: .cr)
try container.encode(self.customCr, forKey: .customCr)
try container.encode(self.customProf, forKey: .customProf)
try container.encode(self.hpText, forKey: .hpText)
try container.encode(self.sthrows, forKey: .sthrows)
try container.encode(self.skills, forKey: .skills)
try container.encode(self.actions, forKey: .actions)
try container.encode(self.legendaryDescription, forKey: .legendaryDescription)
try container.encode(self.legendaries, forKey: .legendaries)
try container.encode(self.reactions, forKey: .reactions)
try container.encode(self.abilities, forKey: .abilities)
try container.encode(self.damageTypes, forKey: .damageTypes)
try container.encode(self.conditions, forKey: .conditions)
try container.encode(self.languages, forKey: .languages)
try container.encode(self.telepathy, forKey: .telepathy)
try container.encode(self.understandsBut, forKey: .understandsBut)
try container.encode(self.blindsight, forKey: .blindsight)
try container.encode(self.blind, forKey: .blind)
try container.encode(self.darkvision, forKey: .darkvision)
try container.encode(self.tremorsense, forKey: .tremorsense)
try container.encode(self.truesight, forKey: .truesight)
try container.encode(self.tag, forKey: .tag)
try container.encode(self.customHP, forKey: .customHP)
try container.encode(self.isLegendary, forKey: .isLegendary)
try container.encode(self.isLair, forKey: .isLair)
try container.encode(self.lairDescription, forKey: .lairDescription)
try container.encode(self.lairDescriptionEnd, forKey: .lairDescriptionEnd)
try container.encode(self.isRegional, forKey: .isRegional)
try container.encode(self.regionalDescription, forKey: .regionalDescription)
try container.encode(self.regionalDescriptionEnd, forKey: .regionalDescriptionEnd)
try container.encode([] as [TraitDTO], forKey: .properties)
try container.encode(self.lairs, forKey: .lairs)
try container.encode(self.regionals, forKey: .regionals)
try container.encode(self.specialDamage, forKey: .specialDamage)
try container.encode(self.shortName, forKey: .shortName)
try container.encode(self.doubleColumns, forKey: .doubleColumns)
try container.encode(self.separationPoint, forKey: .separationPoint)
try container.encode(self.damage, forKey: .damage)
}
}

View File

@@ -0,0 +1,37 @@
//
// Document.swift
// MonsterCards
//
// Created by Tom Hicks on 4/7/21.
//
import UIKit
import SceneKit
class MonsterDocument: UIDocument {
var monsterDTO: MonsterDTO?
var error: Error?
override func contents(forType typeName: String) throws -> Any {
let encoder = JSONEncoder()
do {
let data = try encoder.encode(monsterDTO)
return data
} catch {
return Data()
}
}
override func load(fromContents contents: Any, ofType typeName: String?) throws {
let decoder = JSONDecoder()
do {
let data = contents as! Data
monsterDTO = try decoder.decode(MonsterDTO.self, from: data)
} catch {
monsterDTO = MonsterDTO()
}
}
}

View File

@@ -0,0 +1,223 @@
//
// MonsterViewModel+CoreData.swift
// MonsterCards
//
// Created by Tom Hicks on 4/7/21.
//
import Foundation
import CoreData
extension MonsterViewModel {
convenience init(_ rawMonster: Monster?) {
self.init()
// Call the copy constructor
if (rawMonster != nil) {
self.copyFromMonster(monster: rawMonster!)
}
}
func copyFromMonster(monster: Monster) {
self.name = monster.name ?? ""
self.size = monster.size ?? ""
self.type = monster.type ?? ""
self.subType = monster.subtype ?? ""
self.alignment = monster.alignment ?? ""
self.hitDice = monster.hitDice
self.hasCustomHP = monster.hasCustomHP
self.customHP = monster.customHP ?? ""
self.armorType = monster.armorTypeEnum
self.hasShield = monster.hasShield
self.naturalArmorBonus = monster.naturalArmorBonus
self.customArmor = monster.customArmor ?? ""
self.walkSpeed = monster.walkSpeed
self.burrowSpeed = monster.burrowSpeed
self.climbSpeed = monster.climbSpeed
self.flySpeed = monster.flySpeed
self.canHover = monster.canHover
self.swimSpeed = monster.swimSpeed
self.hasCustomSpeed = monster.hasCustomSpeed
self.customSpeed = monster.customSpeed ?? ""
self.strengthScore = monster.strengthScore
self.strengthSavingThrowAdvantage = monster.strengthSavingThrowAdvantageEnum
self.strengthSavingThrowProficiency = monster.strengthSavingThrowProficiencyEnum
self.dexterityScore = monster.dexterityScore
self.dexteritySavingThrowAdvantage = monster.dexteritySavingThrowAdvantageEnum
self.dexteritySavingThrowProficiency = monster.dexteritySavingThrowProficiencyEnum
self.constitutionScore = monster.constitutionScore
self.constitutionSavingThrowAdvantage = monster.constitutionSavingThrowAdvantageEnum
self.constitutionSavingThrowProficiency = monster.constitutionSavingThrowProficiencyEnum
self.intelligenceScore = monster.intelligenceScore
self.intelligenceSavingThrowAdvantage = monster.intelligenceSavingThrowAdvantageEnum
self.intelligenceSavingThrowProficiency = monster.intelligenceSavingThrowProficiencyEnum
self.wisdomScore = monster.wisdomScore
self.wisdomSavingThrowAdvantage = monster.wisdomSavingThrowAdvantageEnum
self.wisdomSavingThrowProficiency = monster.wisdomSavingThrowProficiencyEnum
self.charismaScore = monster.charismaScore
self.charismaSavingThrowAdvantage = monster.charismaSavingThrowAdvantageEnum
self.charismaSavingThrowProficiency = monster.charismaSavingThrowProficiencyEnum
self.telepathy = monster.telepathy
self.understandsBut = monster.understandsBut ?? ""
self.challengeRating = monster.challengeRatingEnum
self.customChallengeRating = monster.customChallengeRating ?? ""
self.customProficiencyBonus = monster.customProficiencyBonus
self.isBlind = monster.isBlind
self.skills = (monster.skills?.allObjects.map {
let skill = $0 as! Skill
return SkillViewModel(
skill.name ?? "",
AbilityScore(rawValue: skill.abilityScoreName ?? "") ?? .dexterity,
ProficiencyType(rawValue: skill.proficiency ?? "") ?? .none,
AdvantageType(rawValue: skill.advantage ?? "") ?? .none
)
})!.sorted()
// self.name = rawSkill!.name ?? ""
// self.abilityScore = AbilityScore(rawValue: rawSkill!.abilityScoreName ?? "") ?? .strength
// self.proficiency = ProficiencyType(rawValue: rawSkill!.proficiency ?? "") ?? .none
// self.advantage = AdvantageType(rawValue: rawSkill!.advantage ?? "") ?? .none
self.damageImmunities = (monster.damageImmunities ?? [])
.map {StringViewModel($0)}
.sorted()
self.damageResistances = (monster.damageResistances ?? [])
.map {StringViewModel($0)}
.sorted()
self.damageVulnerabilities = (monster.damageVulnerabilities ?? [])
.map {StringViewModel($0)}
.sorted()
self.conditionImmunities = (monster.conditionImmunities ?? [])
.map {StringViewModel($0)}
.sorted()
self.senses = (monster.senses ?? [])
.map {StringViewModel($0)}
.sorted()
self.languages = (monster.languages ?? [])
.map {LanguageViewModel($0.name, $0.speaks)}
.sorted()
// These are manually sorted in the UI
self.abilities = (monster.abilities ?? [])
.map {AbilityViewModel($0.name, $0.abilityDescription)}
self.actions = (monster.actions ?? [])
.map {AbilityViewModel($0.name, $0.abilityDescription)}
self.legendaryActions = (monster.legendaryActions ?? [])
.map {AbilityViewModel($0.name, $0.abilityDescription)}
self.lairActions = (monster.lairActions ?? [])
.map {AbilityViewModel($0.name, $0.abilityDescription)}
self.regionalActions = (monster.regionalActions ?? [])
.map {AbilityViewModel($0.name, $0.abilityDescription)}
self.reactions = (monster.reactions ?? [])
.map {AbilityViewModel($0.name, $0.abilityDescription)}
// Private fields
self.shieldBonus = Int(monster.shieldBonus)
self.otherArmorDescription = monster.otherArmorDescription ?? ""
}
func copyToMonster(monster: Monster) {
monster.name = name
monster.size = size
monster.type = type
monster.subtype = subType
monster.alignment = alignment
monster.hitDice = hitDice
monster.hasCustomHP = hasCustomHP
monster.customHP = customHP
monster.armorTypeEnum = armorType
monster.hasShield = hasShield
monster.naturalArmorBonus = naturalArmorBonus
monster.customArmor = customArmor
monster.walkSpeed = walkSpeed
monster.burrowSpeed = burrowSpeed
monster.climbSpeed = climbSpeed
monster.flySpeed = flySpeed
monster.canHover = canHover
monster.swimSpeed = swimSpeed
monster.hasCustomSpeed = hasCustomSpeed
monster.customSpeed = customSpeed
monster.strengthScore = strengthScore
monster.strengthSavingThrowAdvantageEnum = strengthSavingThrowAdvantage
monster.strengthSavingThrowProficiencyEnum = strengthSavingThrowProficiency
monster.dexterityScore = dexterityScore
monster.dexteritySavingThrowAdvantageEnum = dexteritySavingThrowAdvantage
monster.dexteritySavingThrowProficiencyEnum = dexteritySavingThrowProficiency
monster.constitutionScore = constitutionScore
monster.constitutionSavingThrowAdvantageEnum = constitutionSavingThrowAdvantage
monster.constitutionSavingThrowProficiencyEnum = constitutionSavingThrowProficiency
monster.intelligenceScore = intelligenceScore
monster.intelligenceSavingThrowAdvantageEnum = intelligenceSavingThrowAdvantage
monster.intelligenceSavingThrowProficiencyEnum = intelligenceSavingThrowProficiency
monster.wisdomScore = wisdomScore
monster.wisdomSavingThrowAdvantageEnum = wisdomSavingThrowAdvantage
monster.wisdomSavingThrowProficiencyEnum = wisdomSavingThrowProficiency
monster.charismaScore = charismaScore
monster.charismaSavingThrowAdvantageEnum = charismaSavingThrowAdvantage
monster.charismaSavingThrowProficiencyEnum = charismaSavingThrowProficiency
monster.telepathy = telepathy
monster.understandsBut = understandsBut
monster.challengeRatingEnum = challengeRating
monster.customChallengeRating = customChallengeRating
monster.customProficiencyBonus = customProficiencyBonus
monster.isBlind = isBlind
// Remove missing skills from raw monster
monster.skills?.forEach {s in
let skill = s as! Skill
let skillVM = skills.first { $0.isEqualTo(rawSkill: skill) }
if (skillVM != nil) {
skillVM!.copyToSkill(skill: skill)
} else {
monster.removeFromSkills(skill)
}
}
// Add new skills to raw monster
skills.forEach {skillVM in
if (!(monster.skills?.contains(
where: {
skillVM.isEqualTo(rawSkill: $0 as? Skill)
}) ?? true)){
monster.addToSkills(skillVM.buildRawSkill(context: monster.managedObjectContext))
}
}
monster.conditionImmunities = conditionImmunities.map {$0.name}
monster.damageImmunities = damageImmunities.map {$0.name}
monster.damageResistances = damageResistances.map {$0.name}
monster.damageVulnerabilities = damageVulnerabilities.map {$0.name}
monster.senses = senses.map {$0.name}
// This is necessary so core data sees the language objects as changed. Without it they won't be persisted.
monster.languages = languages.map {LanguageViewModel($0.name, $0.speaks)}
monster.abilities = abilities.map {AbilityViewModel($0.name, $0.abilityDescription)}
monster.actions = actions.map {AbilityViewModel($0.name, $0.abilityDescription)}
monster.legendaryActions = legendaryActions.map {AbilityViewModel($0.name, $0.abilityDescription)}
monster.lairActions = lairActions.map {AbilityViewModel($0.name, $0.abilityDescription)}
monster.regionalActions = regionalActions.map {AbilityViewModel($0.name, $0.abilityDescription)}
monster.reactions = reactions.map {AbilityViewModel($0.name, $0.abilityDescription)}
monster.shieldBonus = Int64(shieldBonus)
monster.otherArmorDescription = otherArmorDescription
}
}

View File

@@ -0,0 +1,706 @@
//
// MonsterViewModel.swift
// MonsterCards
//
// Created by Tom Hicks on 1/18/21.
//
import Foundation
import CoreData
class MonsterViewModel: ObservableObject {
// TODO: Determine whether to prefer Int or Int64 for these fields and switch as many as possible to the winner.
@Published var name: String
@Published var size: String
@Published var type: String
@Published var subType: String
@Published var alignment: String
@Published var hitDice: Int64
@Published var hasCustomHP: Bool
@Published var customHP: String
@Published var armorType: ArmorType
@Published var hasShield: Bool {
didSet { shieldBonus = hasShield ? 2 : 0 }
}
@Published var naturalArmorBonus: Int64
@Published var customArmor: String
@Published var walkSpeed: Int64
@Published var burrowSpeed: Int64
@Published var climbSpeed: Int64
@Published var flySpeed: Int64
@Published var canHover: Bool
@Published var swimSpeed: Int64
@Published var hasCustomSpeed: Bool
@Published var customSpeed: String
@Published var strengthScore: Int64
@Published var dexterityScore: Int64
@Published var constitutionScore: Int64
@Published var intelligenceScore: Int64
@Published var wisdomScore: Int64
@Published var charismaScore: Int64
@Published var strengthSavingThrowProficiency: ProficiencyType
@Published var strengthSavingThrowAdvantage: AdvantageType
@Published var dexteritySavingThrowProficiency: ProficiencyType
@Published var dexteritySavingThrowAdvantage: AdvantageType
@Published var constitutionSavingThrowProficiency: ProficiencyType
@Published var constitutionSavingThrowAdvantage: AdvantageType
@Published var intelligenceSavingThrowProficiency: ProficiencyType
@Published var intelligenceSavingThrowAdvantage: AdvantageType
@Published var wisdomSavingThrowProficiency: ProficiencyType
@Published var wisdomSavingThrowAdvantage: AdvantageType
@Published var charismaSavingThrowProficiency: ProficiencyType
@Published var charismaSavingThrowAdvantage: AdvantageType
@Published var skills: [SkillViewModel]
@Published var damageImmunities: [StringViewModel]
@Published var damageResistances: [StringViewModel]
@Published var damageVulnerabilities: [StringViewModel]
@Published var conditionImmunities: [StringViewModel]
@Published var senses: [StringViewModel]
@Published var languages: [LanguageViewModel]
@Published var telepathy: Int64
@Published var understandsBut: String
@Published var challengeRating: ChallengeRating
@Published var customChallengeRating: String
@Published var customProficiencyBonus: Int64
@Published var abilities: [AbilityViewModel]
@Published var actions: [AbilityViewModel]
@Published var legendaryActions: [AbilityViewModel]
@Published var lairActions: [AbilityViewModel]
@Published var regionalActions: [AbilityViewModel]
@Published var reactions: [AbilityViewModel]
@Published var isBlind: Bool
@Published var shieldBonus: Int
@Published var otherArmorDescription: String
let kBaseArmorClassUnarmored = 10;
let kBaseArmorClassMageArmor = 13;
let kBaseArmorClassPadded = 11;
let kBaseArmorClassLeather = 11;
let kBaseArmorClassStudded = 12;
let kBaseArmorClassHide = 12;
let kBaseArmorClassChainShirt = 13;
let kBaseArmorClassScaleMail = 14;
let kBaseArmorClassBreastplate = 14;
let kBaseArmorClassHalfPlate = 15;
let kBaseArmorClassRingMail = 14;
let kBaseArmorClassChainMail = 16;
let kBaseArmorClassSplintMail = 17;
let kBaseArmorClassPlate = 18;
init() {
self.name = ""
self.size = ""
self.type = ""
self.subType = ""
self.alignment = ""
self.hitDice = 0
self.hasCustomHP = false
self.customHP = ""
self.armorType = .none
self.hasShield = false
self.naturalArmorBonus = 0
self.customArmor = ""
self.walkSpeed = 0
self.burrowSpeed = 0
self.climbSpeed = 0
self.flySpeed = 0
self.canHover = false
self.swimSpeed = 0
self.hasCustomSpeed = false
self.customSpeed = ""
self.strengthScore = 10
self.strengthSavingThrowAdvantage = .none
self.strengthSavingThrowProficiency = .none
self.dexterityScore = 10
self.dexteritySavingThrowAdvantage = .none
self.dexteritySavingThrowProficiency = .none
self.constitutionScore = 10
self.constitutionSavingThrowAdvantage = .none
self.constitutionSavingThrowProficiency = .none
self.intelligenceScore = 10
self.intelligenceSavingThrowAdvantage = .none
self.intelligenceSavingThrowProficiency = .none
self.wisdomScore = 10
self.wisdomSavingThrowAdvantage = .none
self.wisdomSavingThrowProficiency = .none
self.charismaScore = 10
self.charismaSavingThrowAdvantage = .none
self.charismaSavingThrowProficiency = .none
self.skills = []
self.damageImmunities = []
self.damageResistances = []
self.damageVulnerabilities = []
self.conditionImmunities = []
self.senses = []
self.languages = []
self.telepathy = 0
self.understandsBut = ""
self.challengeRating = .one
self.customChallengeRating = ""
self.customProficiencyBonus = 0
self.abilities = []
self.actions = []
self.legendaryActions = []
self.lairActions = []
self.regionalActions = []
self.reactions = []
self.isBlind = false
// Private properties
self.shieldBonus = 0
self.otherArmorDescription = ""
}
// MARK: Basic Info
var meta: String {
get {
// size type (subtype) alignment
var parts: [String] = []
if (!(self.size.isEmpty)) {
parts.append(self.size)
}
if (!(self.type.isEmpty)) {
parts.append(self.type)
}
if (!(self.subType.isEmpty)) {
parts.append(String.init(format: "(%@)", arguments: [self.subType]))
}
if (!(self.alignment.isEmpty)) {
parts.append(self.alignment)
}
return parts.joined(separator: " ")
}
}
var sizeEnum: SizeType {
get {
return SizeType.init(rawValue: size) ?? .medium
}
set {
size = newValue.rawValue
}
}
var hitPoints: String {
get {
if (hasCustomHP) {
return customHP;
} else {
let dieSize = Double(MonsterViewModel.hitDieForSize(sizeEnum))
let conMod = Double(constitutionModifier)
// let level1HP = Double(dieSize + conMod)
let level1HP = Double(dieSize/2.0 + conMod)
let extraLevels = Double(hitDice - 1)
let levelNHP = (dieSize + 1.0) / 2.0 + conMod
let extraLevelsHP = extraLevels * levelNHP
let hpTotal = Int(ceil(level1HP + extraLevelsHP))
let conBonus = Int(conMod) * Int(hitDice)
return String(format: "%d (%dd%d%+d)", hpTotal, hitDice, Int(dieSize), conBonus)
}
}
}
var speed: String {
get {
if (hasCustomSpeed) {
return customSpeed
} else {
var parts: [String] = []
if (walkSpeed > 0) {
parts.append("\(walkSpeed) ft.")
}
if (burrowSpeed > 0) {
parts.append("burrow \(burrowSpeed) ft.")
}
if (climbSpeed > 0) {
parts.append("climb \(climbSpeed) ft.")
}
if (flySpeed > 0) {
parts.append("fly \(flySpeed) ft.\(canHover ? " (hover)": "")")
}
if (swimSpeed > 0) {
parts.append("swim \(swimSpeed) ft.")
}
return parts.joined(separator: ", ")
}
}
}
class func hitDieForSize(_ size: SizeType) -> Int {
switch size {
case .tiny: return 4
case .small: return 6
case .medium: return 8
case .large: return 10
case .huge: return 12
case .gargantuan: return 20
}
}
// MARK: Ability Scores
class func abilityModifierForScore(_ score: Int) -> Int {
return Int(floor(Double((score - 10)) / 2.0))
}
func abilityModifierForAbilityScore(_ abilityScore: AbilityScore) -> Int {
switch abilityScore {
case .strength:
return strengthModifier;
case .dexterity:
return dexterityModifier
case .constitution:
return constitutionModifier
case .intelligence:
return intelligenceModifier
case .wisdom:
return wisdomModifier
case .charisma:
return charismaModifier
}
}
var strengthModifier: Int {
get {
return MonsterViewModel.abilityModifierForScore(Int(strengthScore))
}
}
var dexterityModifier: Int {
get {
return MonsterViewModel.abilityModifierForScore(Int(dexterityScore))
}
}
var constitutionModifier: Int {
get {
return MonsterViewModel.abilityModifierForScore(Int(constitutionScore))
}
}
var intelligenceModifier: Int {
get {
return MonsterViewModel.abilityModifierForScore(Int(intelligenceScore))
}
}
var wisdomModifier: Int {
get {
return MonsterViewModel.abilityModifierForScore(Int(wisdomScore))
}
}
var charismaModifier: Int {
get {
return MonsterViewModel.abilityModifierForScore(Int(charismaScore))
}
}
// MARK: Armor
var armorClassDescription: String {
get {
var armorClassTotal = 0
if (armorType == ArmorType.none) {
// 10 + dexMod + 2 for shieldBonus "15" or "17 (shield)"
armorClassTotal = kBaseArmorClassUnarmored + dexterityModifier + Int(shieldBonus)
return "\(armorClassTotal)\(hasShield ? " (shield)" : "")"
} else if (armorType == .naturalArmor) {
// 10 + dexMod + naturalArmorBonus + 2 for shieldBonus "16 (natural armor)" or "18 (natural armor, shield)"
armorClassTotal = kBaseArmorClassUnarmored + dexterityModifier + Int(naturalArmorBonus) + Int(shieldBonus)
return "\(armorClassTotal) (natural armor\(hasShield ? " (shield)" : ""))"
} else if (armorType == .mageArmor) {
// 10 + dexMod + 2 for shield + 3 for mage armor "15 (18 with mage armor)" or 17 (shield, 20 with mage armor)
armorClassTotal = kBaseArmorClassUnarmored + dexterityModifier + Int(shieldBonus)
let acWithMageArmor = kBaseArmorClassMageArmor + dexterityModifier + Int(shieldBonus)
return String(format: "%d (%@%d with mage armor)", armorClassTotal, (hasShield ? "shield, " : ""), acWithMageArmor)
} else if (armorType == .padded) {
// 11 + dexMod + 2 for shield "18 (padded armor, shield)"
armorClassTotal = kBaseArmorClassPadded + dexterityModifier + Int(shieldBonus)
return String(format: "%d (padded%@)", armorClassTotal, (hasShield ? "shield, " : ""))
} else if (armorType == .leather) {
// 11 + dexMod + 2 for shield "18 (leather, shield)"
armorClassTotal = kBaseArmorClassLeather + dexterityModifier + Int(shieldBonus)
return String(format:"%d (leather%@)", armorClassTotal, (hasShield ? "shield, " : ""))
} else if (armorType == .studdedLeather) {
// 12 + dexMod +2 for shield "17 (studded leather)"
armorClassTotal = kBaseArmorClassStudded + dexterityModifier + Int(shieldBonus)
return String(format: "%d (studded leather%@)", armorClassTotal, (hasShield ? "shield, " : ""))
} else if (armorType == .hide) {
// 12 + Min(2, dexMod) + 2 for shield "12 (hide armor)"
armorClassTotal = kBaseArmorClassHide + min(2, dexterityModifier) + Int(shieldBonus)
return String(format: "%d (hide%@)", armorClassTotal, (hasShield ? ", shield" : ""))
} else if (armorType == .chainShirt) {
// 13 + Min(2, dexMod) + 2 for shield "12 (chain shirt)"
armorClassTotal = kBaseArmorClassChainShirt + min(2, dexterityModifier) + Int(shieldBonus)
return String(format: "%d (chain shirt%@)", armorClassTotal, (hasShield ? ", shield" : ""))
} else if (armorType == .scaleMail) {
// 14 + Min(2, dexMod) + 2 for shield "14 (scale mail)"
armorClassTotal = kBaseArmorClassScaleMail + min(2, dexterityModifier) + Int(shieldBonus)
return String(format: "%d (scale mail%@)", armorClassTotal, (hasShield ? ", shield" : ""))
} else if (armorType == .breastplate) {
// 14 + Min(2, dexMod) + 2 for shield "16 (breastplate)"
armorClassTotal = kBaseArmorClassBreastplate + min(2, dexterityModifier) + Int(shieldBonus)
return String(format: "%d (breastplate%@)", armorClassTotal, (hasShield ? ", shield" : ""))
} else if (armorType == .halfPlate) {
// 15 + Min(2, dexMod) + 2 for shield "17 (half plate)"
armorClassTotal = kBaseArmorClassHalfPlate + min(2, dexterityModifier) + Int(shieldBonus)
return String(format: "%d (half plate%@)", armorClassTotal, (hasShield ? ", shield" : ""))
} else if (armorType == .ringMail) {
// 14 + 2 for shield "14 (ring mail)
armorClassTotal = kBaseArmorClassRingMail + Int(shieldBonus)
return String(format: "%d (ring mail%@)", armorClassTotal, (hasShield ? ", shield" : ""))
} else if (armorType == .chainMail) {
// 16 + 2 for shield "16 (chain mail)"
armorClassTotal = kBaseArmorClassChainMail + Int(shieldBonus)
return String(format: "%d (chain mail%@)", armorClassTotal, (hasShield ? ", shield" : ""))
} else if (armorType == .splintMail) {
// 17 + 2 for shield "17 (splint)"
armorClassTotal = kBaseArmorClassSplintMail + Int(shieldBonus)
return String(format: "%d (splint%@)", armorClassTotal, (hasShield ? ", shield" : ""))
} else if (armorType == .plateMail) {
// 18 + 2 for shield "18 (plate)"
armorClassTotal = kBaseArmorClassPlate + Int(shieldBonus)
return String(format: "%d (plate%@)", armorClassTotal, (hasShield ? ", shield" : ""))
} else if (armorType == .other) {
// pure string value shield check does nothing just copies the string from otherArmorDesc
return otherArmorDescription;
} else {
return ""
}
}
}
// MARK: Challenge Rating / Proficiency Bonus
var proficiencyBonus: Int {
switch challengeRating {
case .custom:
return Int(customProficiencyBonus)
case .zero:
fallthrough
case .oneEighth:
fallthrough
case .oneQuarter:
fallthrough
case .oneHalf:
fallthrough
case .one:
fallthrough
case .two:
fallthrough
case .three:
fallthrough
case .four:
return 2
case .five:
fallthrough
case .six:
fallthrough
case .seven:
fallthrough
case .eight:
return 3
case .nine:
fallthrough
case .ten:
fallthrough
case .eleven:
fallthrough
case .twelve:
return 4
case .thirteen:
fallthrough
case .fourteen:
fallthrough
case .fifteen:
fallthrough
case .sixteen:
return 5
case .seventeen:
fallthrough
case .eighteen:
fallthrough
case .nineteen:
fallthrough
case .twenty:
return 6
case .twentyOne:
fallthrough
case .twentyTwo:
fallthrough
case .twentyThree:
fallthrough
case .twentyFour:
return 7
case .twentyFive:
fallthrough
case .twentySix:
fallthrough
case .twentySeven:
fallthrough
case .twentyEight:
return 8
case .twentyNine:
fallthrough
case .thirty:
return 9
}
}
func proficiencyBonusForType(_ profType: ProficiencyType) -> Int {
switch profType {
case .none:
return 0
case .proficient:
return proficiencyBonus
case .expertise:
return proficiencyBonus * 2
}
}
// MARK: Saving Throws
var savingThrowsDescription: String {
get {
// TODO: port from objective-c
var parts: [String] = []
var name: String
var advantage: String
var bonus: Int
if (strengthSavingThrowAdvantage != .none || strengthSavingThrowProficiency != .none) {
name = "Strength"
bonus = strengthModifier + proficiencyBonusForType(strengthSavingThrowProficiency)
advantage = MonsterViewModel.advantageLabelStringForType(strengthSavingThrowAdvantage)
if (!advantage.isEmpty) {
advantage = " " + advantage
}
parts.append(String(format: "%@ %+d%@", name, bonus, advantage))
}
if (dexteritySavingThrowAdvantage != .none || dexteritySavingThrowProficiency != .none) {
name = "Dexterity"
bonus = dexterityModifier + proficiencyBonusForType(dexteritySavingThrowProficiency)
advantage = MonsterViewModel.advantageLabelStringForType(dexteritySavingThrowAdvantage)
if (!advantage.isEmpty) {
advantage = " " + advantage
}
parts.append(String(format: "%@ %+d%@", name, bonus, advantage))
}
if (constitutionSavingThrowAdvantage != .none || constitutionSavingThrowProficiency != .none) {
name = "Constitution"
bonus = constitutionModifier + proficiencyBonusForType(constitutionSavingThrowProficiency)
advantage = MonsterViewModel.advantageLabelStringForType(constitutionSavingThrowAdvantage)
if (!advantage.isEmpty) {
advantage = " " + advantage
}
parts.append(String(format: "%@ %+d%@", name, bonus, advantage))
}
if (intelligenceSavingThrowAdvantage != .none || intelligenceSavingThrowProficiency != .none) {
name = "Intelligence"
bonus = intelligenceModifier + proficiencyBonusForType(intelligenceSavingThrowProficiency)
advantage = MonsterViewModel.advantageLabelStringForType(intelligenceSavingThrowAdvantage)
if (!advantage.isEmpty) {
advantage = " " + advantage
}
parts.append(String(format: "%@ %+d%@", name, bonus, advantage))
}
if (wisdomSavingThrowAdvantage != .none || wisdomSavingThrowProficiency != .none) {
name = "Wisdom"
bonus = wisdomModifier + proficiencyBonusForType(wisdomSavingThrowProficiency)
advantage = MonsterViewModel.advantageLabelStringForType(wisdomSavingThrowAdvantage)
if (!advantage.isEmpty) {
advantage = " " + advantage
}
parts.append(String(format: "%@ %+d%@", name, bonus, advantage))
}
if (charismaSavingThrowAdvantage != .none || charismaSavingThrowProficiency != .none) {
name = "Charisma"
bonus = charismaModifier + proficiencyBonusForType(charismaSavingThrowProficiency)
advantage = MonsterViewModel.advantageLabelStringForType(charismaSavingThrowAdvantage)
if (!advantage.isEmpty) {
advantage = " " + advantage
}
parts.append(String(format: "%@ %+d%@", name, bonus, advantage))
}
return parts.joined(separator: ", ")
}
}
// MARK: Misc Helpers
class func advantageLabelStringForType(_ advType: AdvantageType) -> String {
switch advType {
case .none:
return ""
case .advantage:
return "(A)"
case .disadvantage:
return "(D)"
}
}
// MARK: Skills
var skillsDescription: String {
get {
let sortedSkills = self.skills.sorted {$0.name < $1.name}
return sortedSkills.reduce("") {
if $0 == "" {
return $1.skillDescription(forMonster: self)
} else {
return $0 + ", " + $1.skillDescription(forMonster: self)
}
}
}
}
// MARK: Immunities, Resistances, and Vulnerabilities
var damageVulnerabilitiesDescription: String {
get {
// TODO: sort "bludgeoning, piercing, and slashing from nonmagical attacks" to the end and use ; as a separator before it.
let sortedVulnerabilities = self.damageVulnerabilities.sorted().map({$0.name})
return StringHelper.oxfordJoin(sortedVulnerabilities)
}
}
var damageResistancesDescription: String {
get {
// TODO: sort "bludgeoning, piercing, and slashing from nonmagical attacks" to the end and use ; as a separator before it.
let sortedResistances = self.damageResistances.sorted().map({$0.name})
return StringHelper.oxfordJoin(sortedResistances)
}
}
var damageImmunitiesDescription: String {
get {
// TODO: sort "bludgeoning, piercing, and slashing from nonmagical attacks" to the end and use ; as a separator before it.
let sortedImmunities = self.damageImmunities.sorted().map({$0.name})
return StringHelper.oxfordJoin(sortedImmunities)
}
}
var conditionImmunitiesDescription: String {
get {
let sortedImmunities = self.conditionImmunities.sorted().map({$0.name})
return StringHelper.oxfordJoin(sortedImmunities)
}
}
// MARK: OTHER
var passivePerception: Int {
get {
let perceptionSkill = skills.first(where: {
StringHelper.safeEqualsIgnoreCase($0.name, "Perception")
})
if (perceptionSkill == nil) {
return 10 + wisdomModifier
} else if (perceptionSkill!.proficiency == ProficiencyType.expertise) {
return 10 + wisdomModifier + proficiencyBonus + proficiencyBonus
} else if (perceptionSkill!.proficiency == ProficiencyType.proficient) {
return 10 + wisdomModifier + proficiencyBonus
} else {
return 10 + wisdomModifier
}
}
}
var sensesDescription: String {
get {
var modifiedSenses = self.senses.sorted().map({$0.name})
let hasPassivePerceptionSense = modifiedSenses.contains(where: {
$0.starts(with: "passive Perception")
})
if (!hasPassivePerceptionSense) {
let calculatedPassivePerception = String(format: "passive Perception %d", passivePerception)
modifiedSenses.append(calculatedPassivePerception)
}
return modifiedSenses.sorted().joined(separator: ", ")
}
}
var languagesDescription: String {
get {
let spokenLanguages =
languages
.filter({ $0.speaks })
.map({$0.name})
.sorted()
let understoodLanguages =
languages
.filter({ !$0.speaks })
.map({$0.name})
.sorted()
let understandsButText = understandsBut.isEmpty
? ""
: String(format: " but %@", understandsBut)
let telepathyText = telepathy > 0
? String(format: ", telepathy %d ft.", telepathy)
: ""
if (spokenLanguages.count > 0) {
if (understoodLanguages.count > 0) {
return String(
format:"%@ and understands %@%@%@",
StringHelper.oxfordJoin(spokenLanguages),
StringHelper.oxfordJoin(understoodLanguages),
understandsButText,
telepathyText)
} else {
return String(
format: "%@%@%@",
StringHelper.oxfordJoin(spokenLanguages),
understandsButText,
telepathyText)
}
} else {
if (understoodLanguages.count > 0) {
return String(
format: "understands %@%@%@",
StringHelper.oxfordJoin(understoodLanguages),
understandsButText,
telepathyText)
} else if (telepathy > 0){
return String(format: "telepathy %d ft.", telepathy)
} else {
return ""
}
}
}
}
var challengeRatingDescription: String {
get {
if (challengeRating != .custom) {
return challengeRating.displayName
} else {
return customChallengeRating
}
}
}
// MARK: End
}

View File

@@ -0,0 +1,35 @@
//
// SavingThrowDTO.swift
// MonsterCards
//
// Created by Tom Hicks on 3/28/21.
//
import Foundation
struct SavingThrowDTO {
var name: String
var order: Int
}
private enum SavingThrowDTOCodingKeys: String, CodingKey {
case name = "name"
case order = "order"
}
extension SavingThrowDTO: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: SavingThrowDTOCodingKeys.self)
self.name = (try? container.decode(String.self, forKey: .name)) ?? ""
self.order = (try? container.decode(Int.self, forKey: .order)) ?? 0
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: SavingThrowDTOCodingKeys.self)
try container.encode(self.name, forKey: .name)
try container.encode(self.order, forKey: .order)
}
}

View File

@@ -0,0 +1,50 @@
//
// Skill+CoreDataClass.swift
// MonsterCards
//
// Created by Tom Hicks on 1/18/21.
//
//
import Foundation
import CoreData
@objc(Skill)
public class Skill: NSManagedObject {
var wrappedName: String {
get {
return name ?? ""
}
set {
name = newValue
}
}
var wrappedProficiency: ProficiencyType {
get {
return ProficiencyType.init(rawValue: proficiency ?? "") ?? .none
}
set {
proficiency = newValue.rawValue
}
}
var wrappedAbilityScore: AbilityScore {
get {
return AbilityScore.init(rawValue: abilityScoreName ?? "") ?? .strength
}
set {
abilityScoreName = newValue.rawValue
}
}
var wrappedAdvantage: AdvantageType {
get {
return AdvantageType.init(rawValue: advantage ?? "") ?? .none
}
set {
advantage = newValue.rawValue
}
}
}

View File

@@ -0,0 +1,39 @@
//
// SkillDTO.swift
// MonsterCards
//
// Created by Tom Hicks on 3/28/21.
//
import Foundation
struct SkillDTO {
var name: String
var stat: String
var note: String
}
private enum SkillDTOCodingKeys: String, CodingKey {
case name = "name"
case stat = "stat"
case note = "note"
}
extension SkillDTO: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: SkillDTOCodingKeys.self)
self.name = (try? container.decode(String.self, forKey: .name)) ?? ""
self.note = (try? container.decode(String.self, forKey: .note)) ?? ""
self.stat = (try? container.decode(String.self, forKey: .stat)) ?? ""
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: SkillDTOCodingKeys.self)
try container.encode(self.name, forKey: .name)
try container.encode(self.note, forKey: .note)
try container.encode(self.stat, forKey: .stat)
}
}

View File

@@ -0,0 +1,57 @@
//
// SkillViewModel+CoreData.swift
// MonsterCards
//
// Created by Tom Hicks on 4/7/21.
//
import Foundation
import CoreData
extension SkillViewModel {
func isEqualTo(rawSkill: Skill?) -> Bool {
if (rawSkill == nil) {
return false;
} else if (abilityScore != rawSkill!.wrappedAbilityScore) {
return false;
} else if (advantage != rawSkill!.wrappedAdvantage) {
return false;
} else if (name != rawSkill!.name) {
return false;
} else if (proficiency != rawSkill!.wrappedProficiency) {
return false;
} else {
return true
}
}
func copyToSkill(skill: Skill) {
skill.wrappedAbilityScore = abilityScore
skill.wrappedAdvantage = advantage
skill.name = name
skill.wrappedProficiency = proficiency
}
convenience init(_ rawSkill: Skill?) {
if (rawSkill == nil) {
self.init()
} else {
let skill = rawSkill!
self.init(
skill.wrappedName,
skill.wrappedAbilityScore,
skill.wrappedProficiency,
skill.wrappedAdvantage
)
}
}
func buildRawSkill(context: NSManagedObjectContext?) -> Skill {
let newSkill = context == nil ? Skill.init() : Skill.init(context: context!)
newSkill.name = name
newSkill.wrappedAbilityScore = abilityScore
newSkill.wrappedProficiency = proficiency
newSkill.wrappedAdvantage = advantage
return newSkill
}
}

View File

@@ -0,0 +1,67 @@
//
// SkillViewModel.swift
// MonsterCards
//
// Created by Tom Hicks on 1/18/21.
//
import Foundation
import CoreData
class SkillViewModel: ObservableObject, Identifiable {
@Published var name: String
@Published var abilityScore: AbilityScore
@Published var proficiency: ProficiencyType
@Published var advantage: AdvantageType
init(_ name: String = "", _ abilityScore: AbilityScore = .dexterity, _ proficiency: ProficiencyType = .proficient, _ advantage: AdvantageType = .none) {
self.name = name
self.abilityScore = abilityScore
self.proficiency = proficiency
self.advantage = advantage
}
func modifier(forMonster: MonsterViewModel) -> Int {
let proficiencyBonus = Double(forMonster.proficiencyBonus)
let abilityScoreModifier = Double(forMonster.abilityModifierForAbilityScore(abilityScore))
switch proficiency {
case .none:
return Int(abilityScoreModifier)
case .proficient:
return Int(abilityScoreModifier + proficiencyBonus)
case .expertise:
return Int(abilityScoreModifier + 2 * proficiencyBonus)
}
}
func skillDescription(forMonster: MonsterViewModel) -> String {
var advantageLabel = MonsterViewModel.advantageLabelStringForType(advantage)
if (advantageLabel != "") {
advantageLabel = " " + advantageLabel
}
return String(format: "%@ %+d%@", name, modifier(forMonster: forMonster), advantageLabel)
}
}
extension SkillViewModel: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(abilityScore)
hasher.combine(advantage)
hasher.combine(name)
hasher.combine(proficiency)
}
}
extension SkillViewModel: Comparable {
static func < (lhs: SkillViewModel, rhs: SkillViewModel) -> Bool {
return lhs.name < rhs.name
}
static func == (lhs: SkillViewModel, rhs: SkillViewModel) -> Bool {
return lhs.abilityScore == rhs.abilityScore
&& lhs.advantage == rhs.advantage
&& lhs.name == rhs.name
&& lhs.proficiency == rhs.proficiency
}
}

View File

@@ -0,0 +1,24 @@
//
// DamageTypesViewModel.swift
// MonsterCards
//
// Created by Tom Hicks on 3/22/21.
//
import Foundation
class StringViewModel: ObservableObject, Comparable, Identifiable {
static func < (lhs: StringViewModel, rhs: StringViewModel) -> Bool {
lhs.name < rhs.name
}
static func == (lhs: StringViewModel, rhs: StringViewModel) -> Bool {
lhs.name == rhs.name
}
@Published var name: String
init(_ name: String = "") {
self.name = name
}
}

View File

@@ -0,0 +1,39 @@
//
// TraitDTO.swift
// MonsterCards
//
// Created by Tom Hicks on 3/28/21.
//
import Foundation
struct TraitDTO {
var name: String
var note: String
var desc: String
}
private enum TraitDTOCodingKeys: String, CodingKey {
case name = "name"
case note = "note"
case desc = "desc"
}
extension TraitDTO: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: TraitDTOCodingKeys.self)
self.name = (try? container.decode(String.self, forKey: .name)) ?? ""
self.note = (try? container.decode(String.self, forKey: .note)) ?? ""
self.desc = (try? container.decode(String.self, forKey: .desc)) ?? ""
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: TraitDTOCodingKeys.self)
try container.encode(self.name, forKey: .name)
try container.encode(self.note, forKey: .note)
try container.encode(self.desc, forKey: .desc)
}
}

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.icloud-container-identifiers</key>
<array>
<string>iCloud.com.majinnaibu.MonsterCards.MonsterCards</string>
</array>
<key>com.apple.developer.icloud-services</key>
<array>
<string>CloudKit</string>
</array>
</dict>
</plist>

View File

@@ -1,4 +1,79 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="1" systemVersion="11A491" minimumToolsVersion="Automatic" sourceLanguage="Objective-C" usedWithCloudKit="true" userDefinedModelVersionIdentifier=""> <model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17709" systemVersion="20D91" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="YES" userDefinedModelVersionIdentifier="">
<elements/> <entity name="Monster" representedClassName="Monster" syncable="YES" codeGenerationType="category">
<attribute name="abilities" optional="YES" attributeType="Transformable" valueTransformerName="AbilityViewModelValueTransformer" customClassName="[AbilityViewModel]"/>
<attribute name="actions" optional="YES" attributeType="Transformable" valueTransformerName="AbilityViewModelValueTransformer" customClassName="[AbilityViewModel]"/>
<attribute name="alignment" attributeType="String" defaultValueString=""/>
<attribute name="armorType" attributeType="String" defaultValueString=""/>
<attribute name="blindsightDistance" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="burrowSpeed" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="canHover" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="challengeRating" attributeType="String" defaultValueString=""/>
<attribute name="charismaSavingThrowAdvantage" attributeType="String" defaultValueString=""/>
<attribute name="charismaSavingThrowProficiency" attributeType="String" defaultValueString="none"/>
<attribute name="charismaScore" attributeType="Integer 64" defaultValueString="10" usesScalarValueType="YES"/>
<attribute name="climbSpeed" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="conditionImmunities" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformerName" customClassName="[String]"/>
<attribute name="constitutionSavingThrowAdvantage" attributeType="String" defaultValueString="none"/>
<attribute name="constitutionSavingThrowProficiency" attributeType="String" defaultValueString="none"/>
<attribute name="constitutionScore" attributeType="Integer 64" defaultValueString="10" usesScalarValueType="YES"/>
<attribute name="customArmor" attributeType="String" defaultValueString=""/>
<attribute name="customChallengeRating" attributeType="String" defaultValueString=""/>
<attribute name="customHP" attributeType="String" defaultValueString=""/>
<attribute name="customProficiencyBonus" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="customSpeed" attributeType="String" defaultValueString=""/>
<attribute name="damageImmunities" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformerName" customClassName="[String]"/>
<attribute name="damageResistances" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformerName" customClassName="[String]"/>
<attribute name="damageVulnerabilities" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformerName" customClassName="[String]"/>
<attribute name="darkvisionDistance" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="dexteritySavingThrowAdvantage" attributeType="String" defaultValueString="none"/>
<attribute name="dexteritySavingThrowProficiency" attributeType="String" defaultValueString="none"/>
<attribute name="dexterityScore" attributeType="Integer 64" defaultValueString="10" usesScalarValueType="YES"/>
<attribute name="flySpeed" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="hasCustomHP" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="hasCustomSpeed" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="hasShield" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="hitDice" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="intelligenceSavingThrowAdvantage" attributeType="String" defaultValueString="none"/>
<attribute name="intelligenceSavingThrowProficiency" attributeType="String" defaultValueString="none"/>
<attribute name="intelligenceScore" attributeType="Integer 64" defaultValueString="10" usesScalarValueType="YES"/>
<attribute name="isBlind" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="lairActions" optional="YES" attributeType="Transformable" valueTransformerName="AbilityViewModelValueTransformer" customClassName="[AbilityViewModel]"/>
<attribute name="languages" optional="YES" attributeType="Transformable" valueTransformerName="LanguageViewModelValueTransformer" customClassName="[LanguageViewModel]"/>
<attribute name="legendaryActions" optional="YES" attributeType="Transformable" valueTransformerName="AbilityViewModelValueTransformer" customClassName="[AbilityViewModel]"/>
<attribute name="name" attributeType="String" defaultValueString="Unnamed Monster"/>
<attribute name="naturalArmorBonus" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="otherArmorDescription" attributeType="String" defaultValueString=""/>
<attribute name="reactions" optional="YES" attributeType="Transformable" valueTransformerName="AbilityViewModelValueTransformer" customClassName="[AbilityViewModel]"/>
<attribute name="regionalActions" optional="YES" attributeType="Transformable" valueTransformerName="AbilityViewModelValueTransformer" customClassName="[AbilityViewModel]"/>
<attribute name="senses" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformerName" customClassName="[String]"/>
<attribute name="shieldBonus" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="size" attributeType="String" defaultValueString=""/>
<attribute name="strengthSavingThrowAdvantage" attributeType="String" defaultValueString="none"/>
<attribute name="strengthSavingThrowProficiency" attributeType="String" defaultValueString="none"/>
<attribute name="strengthScore" attributeType="Integer 64" defaultValueString="10" usesScalarValueType="YES"/>
<attribute name="subtype" attributeType="String" defaultValueString=""/>
<attribute name="swimSpeed" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="telepathy" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="tremorsenseDistance" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="truesightDistance" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="type" attributeType="String" defaultValueString=""/>
<attribute name="understandsBut" attributeType="String" defaultValueString=""/>
<attribute name="walkSpeed" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="wisdomSavingThrowAdvantage" attributeType="String" defaultValueString="none"/>
<attribute name="wisdomSavingThrowProficiency" attributeType="String" defaultValueString="none"/>
<attribute name="wisdomScore" attributeType="Integer 64" defaultValueString="10" usesScalarValueType="YES"/>
<relationship name="skills" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="Skill" inverseName="monster" inverseEntity="Skill"/>
</entity>
<entity name="Skill" representedClassName="Skill" syncable="YES" codeGenerationType="category">
<attribute name="abilityScoreName" attributeType="String" defaultValueString="strength"/>
<attribute name="advantage" attributeType="String" defaultValueString="none"/>
<attribute name="name" attributeType="String" defaultValueString=""/>
<attribute name="proficiency" attributeType="String" defaultValueString="none"/>
<relationship name="monster" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Monster" inverseName="skills" inverseEntity="Monster"/>
</entity>
<elements>
<element name="Monster" positionX="-63" positionY="-18" width="128" height="974"/>
<element name="Skill" positionX="-63" positionY="135" width="128" height="14"/>
</elements>
</model> </model>

View File

@@ -0,0 +1,20 @@
//
// MonsterCardsApp.swift
// MonsterCards
//
// Created by Tom Hicks on 1/15/21.
//
import SwiftUI
@main
struct MonsterCardsApp: App {
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistenceController.container.viewContext)
}
}
}

View File

@@ -0,0 +1,46 @@
//
// Persistence.swift
// MonsterCards
//
// Created by Tom Hicks on 1/15/21.
//
import CoreData
struct PersistenceController {
static let shared = PersistenceController()
static var preview: PersistenceController = {
let result = PersistenceController(inMemory: true)
let viewContext = result.container.viewContext
let monsters: [Monster] = [
Monster(context:viewContext, name: "Ted", size: "Huge", type: "humanoid", subtype: "any race", alignment: "any alignment"),
Monster(context:viewContext, name: "Steve", size: "Huge", type: "humanoid", subtype: "any race", alignment: "any alignment"),
Monster(context:viewContext, name: "Dave", size: "Huge", type: "humanoid", subtype: "any race", alignment: "any alignment")
]
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
// fatalError("TOMHICKS_Unresolved error \(nsError), \(nsError.userInfo)")
}
return result
}()
let container: NSPersistentCloudKitContainer
init(inMemory: Bool = false) {
container = NSPersistentCloudKitContainer(name: "MonsterCards")
if inMemory {
container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
}
container.viewContext.automaticallyMergesChangesFromParent = true
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
}
}

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -1,16 +0,0 @@
//
// SceneDelegate.h
// MonsterCards
//
// Created by Tom Hicks on 9/2/20.
// Copyright © 2020 Tom Hicks. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface SceneDelegate : UIResponder <UIWindowSceneDelegate>
@property (strong, nonatomic) UIWindow * window;
@end

View File

@@ -1,62 +0,0 @@
//
// SceneDelegate.m
// MonsterCards
//
// Created by Tom Hicks on 9/2/20.
// Copyright © 2020 Tom Hicks. All rights reserved.
//
#import "SceneDelegate.h"
#import "AppDelegate.h"
@interface SceneDelegate ()
@end
@implementation SceneDelegate
- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
}
- (void)sceneDidDisconnect:(UIScene *)scene {
// Called as the scene is being released by the system.
// This occurs shortly after the scene enters the background, or when its session is discarded.
// Release any resources associated with this scene that can be re-created the next time the scene connects.
// The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
}
- (void)sceneDidBecomeActive:(UIScene *)scene {
// Called when the scene has moved from an inactive state to an active state.
// Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
}
- (void)sceneWillResignActive:(UIScene *)scene {
// Called when the scene will move from an active state to an inactive state.
// This may occur due to temporary interruptions (ex. an incoming phone call).
}
- (void)sceneWillEnterForeground:(UIScene *)scene {
// Called as the scene transitions from the background to the foreground.
// Use this method to undo the changes made on entering the background.
}
- (void)sceneDidEnterBackground:(UIScene *)scene {
// Called as the scene transitions from the foreground to the background.
// Use this method to save data, release shared resources, and store enough scene-specific state information
// to restore the scene back to its current state.
// Save changes in the application's managed object context when the application transitions to the background.
[(AppDelegate *)UIApplication.sharedApplication.delegate saveContext];
}
@end

View File

@@ -1,15 +0,0 @@
//
// SecondViewController.h
// MonsterCards
//
// Created by Tom Hicks on 9/2/20.
// Copyright © 2020 Tom Hicks. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface SecondViewController : UIViewController
@end

View File

@@ -1,23 +0,0 @@
//
// SecondViewController.m
// MonsterCards
//
// Created by Tom Hicks on 9/2/20.
// Copyright © 2020 Tom Hicks. All rights reserved.
//
#import "SecondViewController.h"
@interface SecondViewController ()
@end
@implementation SecondViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
}
@end

View File

@@ -0,0 +1,21 @@
//
// Collections.swift
// MonsterCards
//
// Created by Tom Hicks on 1/15/21.
//
import SwiftUI
struct Collections: View {
// @State var allCollections: [MonsterCollection] = []
var body: some View {
Text("Collections")
}
}
struct Collections_Previews: PreviewProvider {
static var previews: some View {
Collections().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}

View File

@@ -0,0 +1,80 @@
//
// ContentView.swift
// MonsterCards
//
// Created by Tom Hicks on 1/15/21.
//
import SwiftUI
import CoreData
struct ImportInfo {
var monster: MonsterViewModel = MonsterViewModel()
}
struct ContentView: View {
@Environment(\.managedObjectContext) private var viewContext
@State private var importInfo = ImportInfo()
@State private var isShowingImportDialog = false
var body: some View {
TabView {
Search()
.tabItem {
Image(systemName: "magnifyingglass")
Text("Search")
}
Dashboard()
.tabItem {
Image(systemName: "rectangle.3.offgrid.fill")
Text("Dashboard")
}
Collections()
.tabItem {
Image(systemName: "tray.full.fill")
Text("Collections")
}
Library()
.tabItem {
Image(systemName: "book.fill")
Text("Library")
}
}
.onOpenURL(perform: beginImportingMonster)
.sheet(isPresented: $isShowingImportDialog) {
ImportMonster(monster: $importInfo.monster, isOpen: $isShowingImportDialog)
}
}
func beginImportingMonster(url: URL) {
// TOOD: only do this if the file name ends in .json or .monster
let decoder = JSONDecoder()
do {
let isAccessing = url.startAccessingSecurityScopedResource()
defer {
if (isAccessing) {
url.stopAccessingSecurityScopedResource()
}
}
let data = try Data(contentsOf: url)
let monsterDTO = try decoder.decode(MonsterDTO.self, from: data)
// TODO: check for some minimal set of properties to ensure this is the expected json schema
self.importInfo.monster = MonsterImportHelper.import5ESBMonster(monsterDTO)
// TODO: throw or set an err here and don't set isShowingImportDialog to true if the file didn't match any of our supported monster schemas.
self.isShowingImportDialog = true
} catch let error as NSError {
// TODO: show an error message to the user that we were unable to open the file and maybe why.
print(error)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}

View File

@@ -0,0 +1,20 @@
//
// Dashboard.swift
// MonsterCards
//
// Created by Tom Hicks on 1/15/21.
//
import SwiftUI
struct Dashboard: View {
var body: some View {
Text("Dashboard")
}
}
struct Dashboard_Previews: PreviewProvider {
static var previews: some View {
Dashboard().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}

View File

@@ -0,0 +1,42 @@
//
// EditAbilityScores.swift
// MonsterCards
//
// Created by Tom Hicks on 3/21/21.
//
import SwiftUI
struct EditAbilityScores: View {
@ObservedObject var monsterViewModel: MonsterViewModel
var body: some View {
List {MCStepperField(
label: "STR",
value: $monsterViewModel.strengthScore)
MCStepperField(
label: "DEX",
value: $monsterViewModel.dexterityScore)
MCStepperField(
label: "CON",
value: $monsterViewModel.constitutionScore)
MCStepperField(
label: "INT",
value: $monsterViewModel.intelligenceScore)
MCStepperField(
label: "WIS",
value: $monsterViewModel.wisdomScore)
MCStepperField(
label: "CHA",
value: $monsterViewModel.charismaScore)
}
.navigationTitle("Ability Scores")
}
}
struct EditAbilityScores_Previews: PreviewProvider {
static var previews: some View {
let viewModel = MonsterViewModel()
EditAbilityScores(monsterViewModel: viewModel)
}
}

Some files were not shown because too many files have changed in this diff Show More