Adds MarkdownUI dependency and abilities.
This commit is contained in:
116
iOS/MonsterCards/Models/AbilityViewModel.swift
Normal file
116
iOS/MonsterCards/Models/AbilityViewModel.swift
Normal file
@@ -0,0 +1,116 @@
|
||||
//
|
||||
// AbilityViewModel.swift
|
||||
// MonsterCards
|
||||
//
|
||||
// Created by Tom Hicks on 3/25/21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public class AbilityViewModel: NSObject, ObservableObject, Identifiable, NSSecureCoding {
|
||||
public static var supportsSecureCoding = true
|
||||
|
||||
public func encode(with coder: NSCoder) {
|
||||
coder.encode(self.name, forKey: "name")
|
||||
coder.encode(self.abilityDescription, forKey: "abilityDescription")
|
||||
}
|
||||
|
||||
public required init?(coder: NSCoder) {
|
||||
self.name = coder.decodeObject(of: NSString.self, forKey: "name")! as String
|
||||
self.abilityDescription = coder.decodeObject(of: NSString.self, forKey: "abilityDescription")! as String
|
||||
}
|
||||
|
||||
@Published public var name: String
|
||||
@Published public var abilityDescription: String
|
||||
|
||||
public init(_ name: String = "", _ abilityDescription: String = "") {
|
||||
self.name = name
|
||||
self.abilityDescription = abilityDescription
|
||||
}
|
||||
|
||||
public var fullText: String {
|
||||
get {
|
||||
return String(format: "___%@:___ %@", name, abilityDescription)
|
||||
}
|
||||
}
|
||||
|
||||
public func renderedText(_ monster: Monster) -> 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,7 @@ class MonsterViewModel: ObservableObject {
|
||||
@Published var challengeRating: ChallengeRating
|
||||
@Published var customChallengeRating: String
|
||||
@Published var customProficiencyBonus: Int64
|
||||
@Published var abilities: [AbilityViewModel]
|
||||
|
||||
init(_ rawMonster: Monster? = nil) {
|
||||
self.name = ""
|
||||
@@ -112,6 +113,7 @@ class MonsterViewModel: ObservableObject {
|
||||
self.challengeRating = .one
|
||||
self.customChallengeRating = ""
|
||||
self.customProficiencyBonus = 0
|
||||
self.abilities = []
|
||||
|
||||
if (rawMonster != nil) {
|
||||
self.copyFromMonster(monster: rawMonster!)
|
||||
@@ -186,7 +188,12 @@ class MonsterViewModel: ObservableObject {
|
||||
.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)}
|
||||
}
|
||||
|
||||
func copyToMonster(monster: Monster) {
|
||||
@@ -262,5 +269,7 @@ class MonsterViewModel: ObservableObject {
|
||||
|
||||
// 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)}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17709" systemVersion="20D91" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="YES" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Monster" representedClassName="Monster" syncable="YES" codeGenerationType="category">
|
||||
<attribute name="abilities" optional="YES" attributeType="Transformable" valueTransformerName="AbilityViewModelValueTransformer" customClassName="[AbilityViewModel]"/>
|
||||
<attribute name="alignment" attributeType="String" defaultValueString=""/>
|
||||
<attribute name="armorType" attributeType="String" defaultValueString=""/>
|
||||
<attribute name="baseSpeed" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
@@ -67,7 +68,7 @@
|
||||
<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="884"/>
|
||||
<element name="Monster" positionX="-63" positionY="-18" width="128" height="899"/>
|
||||
<element name="Skill" positionX="-63" positionY="135" width="128" height="14"/>
|
||||
</elements>
|
||||
</model>
|
||||
64
iOS/MonsterCards/Views/EditAbilities.swift
Normal file
64
iOS/MonsterCards/Views/EditAbilities.swift
Normal file
@@ -0,0 +1,64 @@
|
||||
//
|
||||
// EditAbilities.swift
|
||||
// MonsterCards
|
||||
//
|
||||
// Created by Tom Hicks on 3/25/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct EditAbilities: View {
|
||||
@ObservedObject var viewModel: MonsterViewModel
|
||||
var path: ReferenceWritableKeyPath<MonsterViewModel, [AbilityViewModel]>
|
||||
var title: String
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
ForEach(viewModel[keyPath: path]) { ability in
|
||||
NavigationLink(
|
||||
ability.name,
|
||||
destination: EditAbility(viewModel: ability))
|
||||
}
|
||||
.onDelete(perform: { indexSet in
|
||||
for index in indexSet {
|
||||
viewModel[keyPath: path].remove(at: index)
|
||||
}
|
||||
})
|
||||
.onMove(perform: { indices, newOffset in
|
||||
viewModel[keyPath: path].move(fromOffsets: indices, toOffset: newOffset)
|
||||
|
||||
})
|
||||
}
|
||||
.toolbar(content: {
|
||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||
EditButton()
|
||||
|
||||
Button(
|
||||
action: {
|
||||
let newAbility = AbilityViewModel()
|
||||
viewModel[keyPath: path].append(newAbility)
|
||||
viewModel[keyPath: path] = viewModel[keyPath: path].sorted()
|
||||
},
|
||||
label: {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
.onAppear(perform: {
|
||||
viewModel[keyPath: path] = viewModel[keyPath: path].sorted()
|
||||
})
|
||||
.navigationTitle(title)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
struct EditAbilities_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let viewModel = MonsterViewModel()
|
||||
EditAbilities(
|
||||
viewModel: viewModel,
|
||||
path: \.abilities,
|
||||
title: "Abilities")
|
||||
}
|
||||
}
|
||||
29
iOS/MonsterCards/Views/EditAbility.swift
Normal file
29
iOS/MonsterCards/Views/EditAbility.swift
Normal file
@@ -0,0 +1,29 @@
|
||||
//
|
||||
// EditAbility.swift
|
||||
// MonsterCards
|
||||
//
|
||||
// Created by Tom Hicks on 3/25/21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct EditAbility: View {
|
||||
@ObservedObject var viewModel: AbilityViewModel
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
MCTextField(
|
||||
label: "Name",
|
||||
value: $viewModel.name)
|
||||
|
||||
TextEditor(text: $viewModel.abilityDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct EditAbility_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let viewModel = AbilityViewModel()
|
||||
EditAbility(viewModel: viewModel)
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,9 @@ struct EditMonster: View {
|
||||
NavigationLink(
|
||||
"Challenge Rating",
|
||||
destination: EditChallengeRating(viewModel: monsterViewModel))
|
||||
|
||||
NavigationLink(
|
||||
"Abilities", destination: EditAbilities(viewModel: monsterViewModel, path: \.abilities, title: "Abilities"))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -33,8 +33,8 @@ struct EditStrings: View {
|
||||
.toolbar(content: {
|
||||
Button(
|
||||
action: {
|
||||
let newDamageType = StringViewModel()
|
||||
viewModel[keyPath: path].append(newDamageType)
|
||||
let newString = StringViewModel()
|
||||
viewModel[keyPath: path].append(newString)
|
||||
viewModel[keyPath: path] = viewModel[keyPath: path].sorted()
|
||||
},
|
||||
label: {
|
||||
@@ -44,13 +44,17 @@ struct EditStrings: View {
|
||||
})
|
||||
.onAppear(perform: {
|
||||
viewModel[keyPath: path] = viewModel[keyPath: path].sorted()
|
||||
}).navigationTitle(title)
|
||||
})
|
||||
.navigationTitle(title)
|
||||
}
|
||||
}
|
||||
|
||||
struct EditStrings_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let viewModel = MonsterViewModel()
|
||||
EditStrings(viewModel: viewModel, path: \.damageImmunities, title: "Damage Types")
|
||||
EditStrings(
|
||||
viewModel: viewModel,
|
||||
path: \.damageImmunities,
|
||||
title: "Damage Types")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import MarkdownUI
|
||||
|
||||
struct LabeledField<Content: View>: View {
|
||||
@Environment(\.horizontalSizeClass) var sizeClass
|
||||
@@ -209,6 +210,7 @@ struct MonsterDetail: View {
|
||||
VStack (alignment: .leading) {
|
||||
let monsterLanguagesDescription = monster.languagesDescription
|
||||
let monsterChallengeRatingDescription = monster.challengeRatingDescription
|
||||
let monsterAbilities: [AbilityViewModel] = monster.abilities ?? []
|
||||
|
||||
BasicInfoView(monster: monster)
|
||||
|
||||
@@ -243,7 +245,15 @@ struct MonsterDetail: View {
|
||||
}
|
||||
|
||||
// Abilities
|
||||
|
||||
if (monsterAbilities.count > 0) {
|
||||
ForEach(monsterAbilities) { ability in
|
||||
VStack {
|
||||
Markdown(Document(ability.renderedText(monster)/*.fullText*/))
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Actions
|
||||
|
||||
// Legendary Actions
|
||||
|
||||
Reference in New Issue
Block a user