From 84729a1570bf824b5893d218450e31cb7549866c Mon Sep 17 00:00:00 2001 From: Brian Arnold Date: Sun, 9 Nov 2025 18:57:41 -0500 Subject: [PATCH] Updated to break out creature types so they are fully formed before species is loaded and reconciled. Changed all types to support dictionary name lookup so that they are never non-performant when looking up types during loading, or other activities. --- .../Configuration/Configuration.json | 1 + .../Configuration/CreatureTypes.json | 18 ++++++ .../Configuration/Species.json | 16 ------ .../Configuration/Configuration.swift | 24 ++++---- .../RolePlayingCore/Player/Backgrounds.swift | 3 +- .../RolePlayingCore/Player/ClassTraits.swift | 8 +-- Sources/RolePlayingCore/Player/Classes.swift | 55 ++++++++++++++----- .../RolePlayingCore/Player/CreatureType.swift | 38 +++++++++++++ Sources/RolePlayingCore/Player/Player.swift | 4 +- Sources/RolePlayingCore/Player/Skills.swift | 3 +- Sources/RolePlayingCore/Player/Species.swift | 46 +++++++++------- .../Player/SpeciesTraits.swift | 2 +- Tests/RolePlayingCoreTests/ClassesTests.swift | 9 ++- .../ConfigurationTests.swift | 2 +- .../SpeciesNamesTests.swift | 14 ++--- Tests/RolePlayingCoreTests/SpeciesTests.swift | 8 +-- .../SpeciesTraitsTests.swift | 6 +- .../TestResources/TestCharacterGenerator.json | 1 + .../TestClassesConfiguration.json | 1 + .../TestResources/TestConfiguration.json | 1 + .../TestResources/TestCreatureTypes.json | 18 ++++++ .../TestResources/TestSpecies.json | 16 ------ 22 files changed, 187 insertions(+), 107 deletions(-) create mode 100644 Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/CreatureTypes.json create mode 100644 Tests/RolePlayingCoreTests/TestResources/TestCreatureTypes.json diff --git a/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Configuration.json b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Configuration.json index 1252ee6..6207c32 100644 --- a/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Configuration.json +++ b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Configuration.json @@ -2,6 +2,7 @@ "currencies": ["Currencies"], "skills": ["Skills"], "backgrounds": ["Backgrounds"], + "creature types": ["CreatureTypes"], "classes": ["Classes"], "species": ["Species"], "species names": "SpeciesNames" diff --git a/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/CreatureTypes.json b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/CreatureTypes.json new file mode 100644 index 0000000..42d5a5d --- /dev/null +++ b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/CreatureTypes.json @@ -0,0 +1,18 @@ +{ + "creature types": [ + { "name": "Aberration" }, + { "name": "Beast" }, + { "name": "Celestial" }, + { "name": "Construct" }, + { "name": "Dragon" }, + { "name": "Elemental" }, + { "name": "Fey" }, + { "name": "Fiend" }, + { "name": "Giant" }, + { "name": "Humanoid", "is default": true }, + { "name": "Monstrosity" }, + { "name": "Ooze" }, + { "name": "Plant" }, + { "name": "Undead" } + ] +} diff --git a/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Species.json b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Species.json index e717b81..68e61f8 100644 --- a/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Species.json +++ b/Examples/CharacterGenerator/CharacterGenerator/CharacterGenerator/Configuration/Species.json @@ -1,20 +1,4 @@ { - "creature types": [ - { "name": "Aberration" }, - { "name": "Beast" }, - { "name": "Celestial" }, - { "name": "Construct" }, - { "name": "Dragon" }, - { "name": "Elemental" }, - { "name": "Fey" }, - { "name": "Fiend" }, - { "name": "Giant" }, - { "name": "Humanoid", "is default": true }, - { "name": "Monstrosity" }, - { "name": "Ooze" }, - { "name": "Plant" }, - { "name": "Undead" } - ], "species": [ { "name": "Aasimar", diff --git a/Sources/RolePlayingCore/Configuration/Configuration.swift b/Sources/RolePlayingCore/Configuration/Configuration.swift index cec418e..4b015d9 100644 --- a/Sources/RolePlayingCore/Configuration/Configuration.swift +++ b/Sources/RolePlayingCore/Configuration/Configuration.swift @@ -14,6 +14,7 @@ public struct ConfigurationFiles: Decodable { let currencies: [String] let skills: [String] let backgrounds: [String] + let creatureTypes: [String] let species: [String] let classes: [String] let players: [String]? @@ -23,6 +24,7 @@ public struct ConfigurationFiles: Decodable { case currencies case skills case backgrounds + case creatureTypes = "creature types" case species case classes case players @@ -37,8 +39,9 @@ public struct Configuration { public var configurationFiles: ConfigurationFiles public var currencies = Currencies() - public var backgrounds = Backgrounds() public var skills = Skills() + public var backgrounds = Backgrounds() + public var creatureTypes = CreatureTypes() public var species = Species() public var classes = Classes() public var players = Players() @@ -72,25 +75,22 @@ public struct Configuration { self.backgrounds.add(backgrounds.all) } + for creatureTypesFile in configurationFiles.creatureTypes { + let jsonData = try bundle.loadJSON(creatureTypesFile) + let creatureTypes = try jsonDecoder.decode(CreatureTypes.self, from: jsonData) + self.creatureTypes.add(creatureTypes.all) + } + for speciesFile in configurationFiles.species { let jsonData = try bundle.loadJSON(speciesFile) let species = try jsonDecoder.decode(Species.self, from: jsonData, configuration: self) - self.species.species += species.species + self.species.add(species) } for classFile in configurationFiles.classes { let jsonData = try bundle.loadJSON(classFile) let classes = try jsonDecoder.decode(Classes.self, from: jsonData, configuration: self) - self.classes.classes += classes.classes - - // Update the shared classes experience points table, then - // update all of the classes to point to it. TODO: improve this. - if let experiencePoints = classes.experiencePoints { - self.classes.experiencePoints = experiencePoints - for (index, _) in self.classes.classes.enumerated() { - self.classes.classes[index].experiencePoints = experiencePoints - } - } + self.classes.add(classes) } if let playersFiles = configurationFiles.players { diff --git a/Sources/RolePlayingCore/Player/Backgrounds.swift b/Sources/RolePlayingCore/Player/Backgrounds.swift index bb3729f..f224d04 100644 --- a/Sources/RolePlayingCore/Player/Backgrounds.swift +++ b/Sources/RolePlayingCore/Player/Backgrounds.swift @@ -24,7 +24,8 @@ public struct Backgrounds: CodableWithConfiguration { /// Adds the array of background traits to the collection. mutating func add(_ backgrounds: [BackgroundTraits]) { - allBackgrounds = Dictionary(backgrounds.map { ($0.name, $0) }, uniquingKeysWith: { _, last in last }) + let mappedBackgrounds = Dictionary(backgrounds.map { ($0.name, $0) }, uniquingKeysWith: { _, last in last }) + allBackgrounds.merge(mappedBackgrounds, uniquingKeysWith: { _, last in last }) } /// Accesses a background traits instance by name. diff --git a/Sources/RolePlayingCore/Player/ClassTraits.swift b/Sources/RolePlayingCore/Player/ClassTraits.swift index 2155215..798d356 100644 --- a/Sources/RolePlayingCore/Player/ClassTraits.swift +++ b/Sources/RolePlayingCore/Player/ClassTraits.swift @@ -119,16 +119,16 @@ extension ClassTraits: CodableWithConfiguration { let primaryAbility = try values.decodeIfPresent([Ability].self, forKey: .primaryAbility) let alternatePrimaryAbility = try values.decodeIfPresent([Ability].self, forKey: .alternatePrimaryAbility) let savingThrows = try values.decodeIfPresent([Ability].self, forKey: .savingThrows) - let startingSkillCount: Int? = try values.decodeIfPresent(Int.self, forKey: .startingSkillCount) + let startingSkillCount = try values.decodeIfPresent(Int.self, forKey: .startingSkillCount) // Decode skill proficiency names and resolve them using configuration let skillNames = try values.decodeIfPresent([String].self, forKey: .skillProficiencies) ?? [] - let resolvedSkills: [Skill] = try skillNames.skills(from: configuration.skills) + let resolvedSkills = try skillNames.skills(from: configuration.skills) let weaponProficiencies = try values.decodeIfPresent([String].self, forKey: .weaponProficiencies) let toolProficiencies = try values.decodeIfPresent([String].self, forKey: .toolProficiencies) - let armorTraining: [String]? = try values.decodeIfPresent([String].self, forKey: .armorTraining) - let startingEquipment: [[String]]? = try values.decodeIfPresent([[String]].self, forKey: .startingEquipment) + let armorTraining = try values.decodeIfPresent([String].self, forKey: .armorTraining) + let startingEquipment = try values.decodeIfPresent([[String]].self, forKey: .startingEquipment) let experiencePoints = try values.decodeIfPresent([Int].self, forKey: .experiencePoints) diff --git a/Sources/RolePlayingCore/Player/Classes.swift b/Sources/RolePlayingCore/Player/Classes.swift index 7e826fa..79e4131 100644 --- a/Sources/RolePlayingCore/Player/Classes.swift +++ b/Sources/RolePlayingCore/Player/Classes.swift @@ -11,40 +11,69 @@ import Foundation /// A collection of class traits. public struct Classes: CodableWithConfiguration { - public var classes: [ClassTraits] + /// A dictionary of class traits indexed by name. + private var allClasses: [String: ClassTraits] = [:] + + /// An array of class traits. + public var all: [ClassTraits] { Array(allClasses.values) } + + /// An optional table of the minimum experience points required to reach the next level. public var experiencePoints: [Int]? - public init(_ classes: [ClassTraits] = []) { - self.classes = classes + /// Returns an instance of a collection of background traits., optionally with a shared experience points table. + public init(_ classes: [ClassTraits] = [], experiencePoints: [Int]? = nil) { + add(classes, experiencePoints) } - private enum CodingKeys: String, CodingKey { - case classes - case experiencePoints = "experience points" + /// Adds the array of class traits to the collection, and optionally updates all experience points with a shared experience points table. + mutating func add(_ classes: [ClassTraits], _ experiencePoints: [Int]? = nil) { + let mappedClasses = Dictionary(classes.map { ($0.name, $0) }, uniquingKeysWith: { _, last in last }) + allClasses.merge(mappedClasses, uniquingKeysWith: { _, last in last }) + + if let experiencePoints { + self.experiencePoints = experiencePoints + for name in allClasses.keys { + self.allClasses[name]?.experiencePoints = experiencePoints + } + } + } + + /// Adds the collection of class traits to the collection, and if present, optionally updates all experience points with a shared experience points table. + mutating func add(_ classes: Classes) { + add(classes.all, classes.experiencePoints) } - public func find(_ className: String?) -> ClassTraits? { - return classes.first(where: { $0.name == className }) + /// Accesses a class traits instance by name. + public subscript(className: String) -> ClassTraits? { + return allClasses[className] } - public var count: Int { classes.count } + /// Returns the number of class traits in the collection. + public var count: Int { allClasses.count } + /// Accesses a class traits instance by index. public subscript(index: Int) -> ClassTraits? { - guard index >= 0 && index < classes.count else { return nil } - return classes[index] + guard index >= 0 && index < count else { return nil } + return all[index] } // MARK: CodableWithConfiguration conformance + private enum CodingKeys: String, CodingKey { + case classes + case experiencePoints = "experience points" + } + public init(from decoder: Decoder, configuration: Configuration) throws { let values = try decoder.container(keyedBy: CodingKeys.self) - self.classes = try values.decode([ClassTraits].self, forKey: .classes, configuration: configuration) + let classes = try values.decode([ClassTraits].self, forKey: .classes, configuration: configuration) + add(classes) self.experiencePoints = try values.decodeIfPresent([Int].self, forKey: .experiencePoints) } public func encode(to encoder: Encoder, configuration: Configuration) throws { var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(classes, forKey: .classes, configuration: configuration) + try container.encode(all, forKey: .classes, configuration: configuration) try container.encodeIfPresent(experiencePoints, forKey: .experiencePoints) } } diff --git a/Sources/RolePlayingCore/Player/CreatureType.swift b/Sources/RolePlayingCore/Player/CreatureType.swift index 468c6bf..82e815d 100644 --- a/Sources/RolePlayingCore/Player/CreatureType.swift +++ b/Sources/RolePlayingCore/Player/CreatureType.swift @@ -22,3 +22,41 @@ public struct CreatureType: Sendable { extension CreatureType: Codable { } +extension CreatureType: Hashable { } + +public struct CreatureTypes: Codable, Sendable { + + private var allCreatureTypes: [String: CreatureType] = [:] + + public var all: [CreatureType] { Array(allCreatureTypes.values) } + + public var defaultCreatureType: CreatureType { + all.first(where: { $0.isDefault != nil && $0.isDefault! })! //?? CreatureType("Humanoid") + } + + public init (_ creatureTypes: [CreatureType] = []) { + add(creatureTypes) + } + + mutating func add(_ creatureTypes: [CreatureType]) { + let mappedCreatureTypes = Dictionary(creatureTypes.map { ($0.name, $0) }, uniquingKeysWith: { _, last in last }) + allCreatureTypes.merge(mappedCreatureTypes, uniquingKeysWith: { _, last in last }) + } + + // MARK: Codable conformance + + private enum CodingKeys: String, CodingKey { + case creatureTypes = "creature types" + } + + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + let creatureTypes = try values.decode([CreatureType].self, forKey: .creatureTypes) + add(creatureTypes) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(all, forKey: .creatureTypes) + } +} diff --git a/Sources/RolePlayingCore/Player/Player.swift b/Sources/RolePlayingCore/Player/Player.swift index 7c4ffd8..63731cf 100644 --- a/Sources/RolePlayingCore/Player/Player.swift +++ b/Sources/RolePlayingCore/Player/Player.swift @@ -161,12 +161,12 @@ public class Player: CodableWithConfiguration { } // Resolve speciesTraits from configuration - guard let speciesTraits = configuration.species.find(speciesName) else { + guard let speciesTraits = configuration.species[speciesName] else { throw missingTypeError("species", speciesName) } // Resolve classTraits from configuration - guard let classTraits = configuration.classes.find(className) else { + guard let classTraits = configuration.classes[className] else { throw missingTypeError("class", className) } diff --git a/Sources/RolePlayingCore/Player/Skills.swift b/Sources/RolePlayingCore/Player/Skills.swift index 3c834ef..b352841 100644 --- a/Sources/RolePlayingCore/Player/Skills.swift +++ b/Sources/RolePlayingCore/Player/Skills.swift @@ -32,7 +32,8 @@ public struct Skills: Codable { /// Adds the array of skills to the collection. mutating func add(_ skills: [Skill]) { - allSkills = Dictionary(skills.map { ($0.name, $0) }, uniquingKeysWith: { _, last in last }) + let mappedSkills = Dictionary(skills.map { ($0.name, $0) }, uniquingKeysWith: { _, last in last }) + allSkills.merge(mappedSkills, uniquingKeysWith: { _, last in last }) } /// Accesses a skill by name. diff --git a/Sources/RolePlayingCore/Player/Species.swift b/Sources/RolePlayingCore/Player/Species.swift index 5dfc715..87cb480 100644 --- a/Sources/RolePlayingCore/Player/Species.swift +++ b/Sources/RolePlayingCore/Player/Species.swift @@ -11,49 +11,56 @@ import Foundation /// A collection of species traits, including subspecies. public class Species: CodableWithConfiguration { + /// All of the species and subspecies traits as a flattened dictionary, indexed by species name. + private var allSpecies: [String: SpeciesTraits] = [:] + /// Accesses all of the species and subspecies that have been loaded. - public var species = [SpeciesTraits]() + public var all: [SpeciesTraits] { Array(allSpecies.values) } - public var creatureTypes = [CreatureType]() + /// Creates a Species instance. + public init(_ species: [SpeciesTraits] = []) { + add(species) + } - public var defaultCreatureType: CreatureType { - creatureTypes.first(where: { $0.isDefault != nil && $0.isDefault! }) ?? CreatureType("Humanoid") + public func add(_ species: [SpeciesTraits]) { + let mappedSpecies = Dictionary(species.map { ($0.name, $0) }, uniquingKeysWith: { _, last in last }) + allSpecies.merge(mappedSpecies, uniquingKeysWith: { _, last in last }) } - /// Creates a Species instance. - public init() { } + public func add(_ species: Species) { + add(species.all) + } /// Returns all of the leaf species (species that contain no subspecies). public var leafSpecies: [SpeciesTraits] { - return species.filter { $0.subspecies.isEmpty } + return all.filter { $0.subspecies.isEmpty } } /// Returns the species matching the specified name, or nil if not present. - public func find(_ speciesName: String) -> SpeciesTraits? { - return species.first(where: { $0.name == speciesName }) + public subscript(speciesName: String) -> SpeciesTraits? { + return allSpecies[speciesName] } - public var count: Int { species.count } + public var count: Int { allSpecies.count } public subscript(index: Int) -> SpeciesTraits? { - guard index >= 0 && index < species.count else { return nil } - return species[index] + guard index >= 0 && index < count else { return nil } + return all[index] } public func randomElementByIndex(using generator: inout G) -> SpeciesTraits { - return species.randomElementByIndex(using: &generator)! + return all.randomElementByIndex(using: &generator)! } + // MARK: CodableWithConfiguration support + enum CodingKeys: String, CodingKey { case species - case creatureTypes = "creature types" } /// Overridden to stitch together subspecies embedded in species. public required init(from decoder: Decoder, configuration: Configuration) throws { let root = try decoder.container(keyedBy: CodingKeys.self) - let creatureTypes = try root.decodeIfPresent([CreatureType].self, forKey: .creatureTypes) - self.creatureTypes = creatureTypes ?? [] var leaf = try root.nestedUnkeyedContainer(forKey: .species) @@ -68,17 +75,14 @@ public class Species: CodableWithConfiguration { } } - self.species = species + add(species) } public func encode(to encoder: any Encoder, configuration: Configuration) throws { var root = encoder.container(keyedBy: CodingKeys.self) - if !creatureTypes.isEmpty { - try root.encode(creatureTypes, forKey: .creatureTypes) - } var leaf = root.nestedUnkeyedContainer(forKey: .species) - let rootSpecies = self.species.filter { $0.parentName == nil } + let rootSpecies = all.filter { $0.parentName == nil } for speciesTraits in rootSpecies { try leaf.encode(speciesTraits, configuration: configuration) } diff --git a/Sources/RolePlayingCore/Player/SpeciesTraits.swift b/Sources/RolePlayingCore/Player/SpeciesTraits.swift index 79ac703..1c1835e 100644 --- a/Sources/RolePlayingCore/Player/SpeciesTraits.swift +++ b/Sources/RolePlayingCore/Player/SpeciesTraits.swift @@ -80,7 +80,7 @@ extension SpeciesTraits: CodableWithConfiguration { self.name = name self.plural = plural self.aliases = aliases ?? [] - self.creatureType = creatureType != nil ? CreatureType(creatureType!) : configuration.species.defaultCreatureType + self.creatureType = creatureType != nil ? CreatureType(creatureType!) : configuration.creatureTypes.defaultCreatureType self.descriptiveTraits = descriptiveTraits ?? [:] self.lifespan = lifespan self.baseSizes = baseSizes ?? ["4-7"] diff --git a/Tests/RolePlayingCoreTests/ClassesTests.swift b/Tests/RolePlayingCoreTests/ClassesTests.swift index 12a27e6..79f7dc4 100644 --- a/Tests/RolePlayingCoreTests/ClassesTests.swift +++ b/Tests/RolePlayingCoreTests/ClassesTests.swift @@ -25,16 +25,15 @@ struct ClassesTests { func defaultClasses() throws { let jsonData = try bundle.loadJSON("TestClasses") let classes = try decoder.decode(Classes.self, from: jsonData, configuration: configuration) - #expect(classes.classes.count == 4, "classes count failed") + #expect(classes.all.count == 4, "classes count failed") #expect(classes.count == 4, "classes count failed") #expect(classes[0] != nil, "class by index failed") #expect(classes.experiencePoints?.count == 20, "array of experience points failed") // Test finding a class by name - #expect(classes.find("Fighter") != nil, "Fighter should be non-nil") - #expect(classes.find("Foo") == nil, "Foo should be nil") - #expect(classes.find(nil) == nil, "nil class name should find nil") + #expect(classes["Fighter"] != nil, "Fighter should be non-nil") + #expect(classes["Foo"] == nil, "Foo should be nil") } @Test("Uncommon classes") @@ -42,7 +41,7 @@ struct ClassesTests { let jsonData = try bundle.loadJSON("TestMoreClasses") let classes = try decoder.decode(Classes.self, from: jsonData, configuration: configuration) - #expect(classes.classes.count == 8, "classes count failed") + #expect(classes.count == 8, "classes count failed") } } diff --git a/Tests/RolePlayingCoreTests/ConfigurationTests.swift b/Tests/RolePlayingCoreTests/ConfigurationTests.swift index ae153d9..8442096 100644 --- a/Tests/RolePlayingCoreTests/ConfigurationTests.swift +++ b/Tests/RolePlayingCoreTests/ConfigurationTests.swift @@ -25,7 +25,7 @@ struct ConfigurationTests { for ability in abilities { var importantFor = [String]() - for classTraits in configuration.classes.classes { + for classTraits in configuration.classes.all { if classTraits.primaryAbility.contains(ability) { importantFor.append(classTraits.name) } diff --git a/Tests/RolePlayingCoreTests/SpeciesNamesTests.swift b/Tests/RolePlayingCoreTests/SpeciesNamesTests.swift index 481d3a2..3ecbdd2 100644 --- a/Tests/RolePlayingCoreTests/SpeciesNamesTests.swift +++ b/Tests/RolePlayingCoreTests/SpeciesNamesTests.swift @@ -36,25 +36,25 @@ struct SpeciesNamesTests { let moreSpecies = try decoder.decode(Species.self, from: moreJsonData, configuration: configuration) let allSpecies = Species() - allSpecies.species = species.species + moreSpecies.species + allSpecies.add(species.all + moreSpecies.all) // TODO: random names are hard; for now, get code coverage. - let human = try #require(allSpecies.find("Human")) + let human = try #require(allSpecies["Human"]) _ = speciesNames.randomName(speciesTraits: human, gender: .female) - let elf = try #require(allSpecies.find("Elf")) + let elf = try #require(allSpecies["Elf"]) _ = speciesNames.randomName(speciesTraits: elf, gender: .male) - let mountainDwarf = try #require(allSpecies.find("Mountain Dwarf")) + let mountainDwarf = try #require(allSpecies["Mountain Dwarf"]) _ = speciesNames.randomName(speciesTraits: mountainDwarf, gender: nil) - let stout = try #require(allSpecies.find("Stout")) + let stout = try #require(allSpecies["Stout"]) _ = speciesNames.randomName(speciesTraits: stout, gender: nil) - let dragonborn = try #require(allSpecies.find("Dragonborn")) + let dragonborn = try #require(allSpecies["Dragonborn"]) _ = speciesNames.randomName(speciesTraits: dragonborn, gender: nil) - let tiefling = try #require(allSpecies.find("Tiefling")) + let tiefling = try #require(allSpecies["Tiefling"]) _ = speciesNames.randomName(speciesTraits: tiefling, gender: nil) let encoder = JSONEncoder() diff --git a/Tests/RolePlayingCoreTests/SpeciesTests.swift b/Tests/RolePlayingCoreTests/SpeciesTests.swift index 04b77e0..0da6457 100644 --- a/Tests/RolePlayingCoreTests/SpeciesTests.swift +++ b/Tests/RolePlayingCoreTests/SpeciesTests.swift @@ -24,7 +24,7 @@ struct SpeciesTests { @Test("Default initialization creates empty species") func defaultInit() async throws { let species = Species() - #expect(species.species.count == 0, "default init") + #expect(species.count == 0, "default init") } @Test("Load and parse species from JSON file") @@ -37,8 +37,8 @@ struct SpeciesTests { #expect(species[0] != nil, "species by index") // Test finding a species by name - #expect(species.find("Human") != nil, "Fighter should be non-nil") - #expect(species.find("Foo") == nil, "Foo should be nil") + #expect(species["Human"] != nil, "Fighter should be non-nil") + #expect(species["Foo"] == nil, "Foo should be nil") } @Test("Load uncommon species from JSON file") @@ -47,6 +47,6 @@ struct SpeciesTests { let species = try decoder.decode(Species.self, from: jsonData, configuration: configuration) // There should be 5 species plus 2 subspecies - #expect(species.species.count == 5, "all species") + #expect(species.count == 5, "all species") } } diff --git a/Tests/RolePlayingCoreTests/SpeciesTraitsTests.swift b/Tests/RolePlayingCoreTests/SpeciesTraitsTests.swift index f7bcc3e..ecd56f3 100644 --- a/Tests/RolePlayingCoreTests/SpeciesTraitsTests.swift +++ b/Tests/RolePlayingCoreTests/SpeciesTraitsTests.swift @@ -159,14 +159,14 @@ struct SpeciesTraitsTests { @Test("Encode subspecies traits with blending") func encodingSubspeciesTraits() async throws { - let speciesTraits = SpeciesTraits(name: "Human", plural: "Humans", aliases: [], creatureType: configuration.species.defaultCreatureType, descriptiveTraits: [:], lifespan: 90, darkVision: 0, speed: 45) + let speciesTraits = SpeciesTraits(name: "Human", plural: "Humans", aliases: [], creatureType: configuration.creatureTypes.defaultCreatureType, descriptiveTraits: [:], lifespan: 90, darkVision: 0, speed: 45) let encoder = JSONEncoder() // Test 1: Subspecies with blended traits do { var copyOfSpeciesTraits = speciesTraits - var subspeciesTraits = SpeciesTraits(name: "Subhuman", plural: "Subhumans", creatureType: configuration.species.defaultCreatureType, lifespan: 45, darkVision: 0, speed: 30) + var subspeciesTraits = SpeciesTraits(name: "Subhuman", plural: "Subhumans", creatureType: configuration.creatureTypes.defaultCreatureType, lifespan: 45, darkVision: 0, speed: 30) subspeciesTraits.blendTraits(from: copyOfSpeciesTraits) copyOfSpeciesTraits.subspecies.append(subspeciesTraits) @@ -193,7 +193,7 @@ struct SpeciesTraitsTests { // Test 2: Subspecies with different overrides do { var copyOfSpeciesTraits = speciesTraits - let subspeciesTraits = SpeciesTraits(name: "Subhuman", plural: "Subhumans", aliases: ["Minions"], creatureType: configuration.species.defaultCreatureType, descriptiveTraits: ["background": "Something"], lifespan: 45, darkVision: 10, speed: 45) + let subspeciesTraits = SpeciesTraits(name: "Subhuman", plural: "Subhumans", aliases: ["Minions"], creatureType: configuration.creatureTypes.defaultCreatureType, descriptiveTraits: ["background": "Something"], lifespan: 45, darkVision: 10, speed: 45) copyOfSpeciesTraits.subspecies.append(subspeciesTraits) let encoded = try encoder.encode(copyOfSpeciesTraits, configuration: configuration) diff --git a/Tests/RolePlayingCoreTests/TestResources/TestCharacterGenerator.json b/Tests/RolePlayingCoreTests/TestResources/TestCharacterGenerator.json index 9d84902..1260253 100644 --- a/Tests/RolePlayingCoreTests/TestResources/TestCharacterGenerator.json +++ b/Tests/RolePlayingCoreTests/TestResources/TestCharacterGenerator.json @@ -2,6 +2,7 @@ "currencies": ["TestCurrencies"], "skills": ["TestSkills"], "backgrounds": ["TestBackgrounds"], + "creature types": ["TestCreatureTypes"], "classes": ["TestClasses", "TestMoreClasses"], "species": ["TestSpecies", "TestMoreSpecies"], "players": ["TestPlayers"], diff --git a/Tests/RolePlayingCoreTests/TestResources/TestClassesConfiguration.json b/Tests/RolePlayingCoreTests/TestResources/TestClassesConfiguration.json index 09157d3..0f6c136 100644 --- a/Tests/RolePlayingCoreTests/TestResources/TestClassesConfiguration.json +++ b/Tests/RolePlayingCoreTests/TestResources/TestClassesConfiguration.json @@ -2,6 +2,7 @@ "currencies": ["TestCurrencies"], "skills": ["TestSkills"], "backgrounds": [], + "creature types": [], "classes": [], "species": [], "players": [] diff --git a/Tests/RolePlayingCoreTests/TestResources/TestConfiguration.json b/Tests/RolePlayingCoreTests/TestResources/TestConfiguration.json index ffddca6..f594493 100644 --- a/Tests/RolePlayingCoreTests/TestResources/TestConfiguration.json +++ b/Tests/RolePlayingCoreTests/TestResources/TestConfiguration.json @@ -2,6 +2,7 @@ "currencies": ["TestCurrencies"], "skills": ["TestSkills"], "backgrounds": ["TestBackgrounds"], + "creature types": ["TestCreatureTypes"], "classes": ["TestClasses", "TestMoreClasses"], "species": ["TestSpecies", "TestMoreSpecies"], "players": ["TestPlayers"] diff --git a/Tests/RolePlayingCoreTests/TestResources/TestCreatureTypes.json b/Tests/RolePlayingCoreTests/TestResources/TestCreatureTypes.json new file mode 100644 index 0000000..42d5a5d --- /dev/null +++ b/Tests/RolePlayingCoreTests/TestResources/TestCreatureTypes.json @@ -0,0 +1,18 @@ +{ + "creature types": [ + { "name": "Aberration" }, + { "name": "Beast" }, + { "name": "Celestial" }, + { "name": "Construct" }, + { "name": "Dragon" }, + { "name": "Elemental" }, + { "name": "Fey" }, + { "name": "Fiend" }, + { "name": "Giant" }, + { "name": "Humanoid", "is default": true }, + { "name": "Monstrosity" }, + { "name": "Ooze" }, + { "name": "Plant" }, + { "name": "Undead" } + ] +} diff --git a/Tests/RolePlayingCoreTests/TestResources/TestSpecies.json b/Tests/RolePlayingCoreTests/TestResources/TestSpecies.json index 8aff22b..abd5b6c 100644 --- a/Tests/RolePlayingCoreTests/TestResources/TestSpecies.json +++ b/Tests/RolePlayingCoreTests/TestResources/TestSpecies.json @@ -1,20 +1,4 @@ { - "creature types": [ - { "name": "Aberration" }, - { "name": "Beast" }, - { "name": "Celestial" }, - { "name": "Construct" }, - { "name": "Dragon" }, - { "name": "Elemental" }, - { "name": "Fey" }, - { "name": "Fiend" }, - { "name": "Giant" }, - { "name": "Humanoid", "is default": true }, - { "name": "Monstrosity" }, - { "name": "Ooze" }, - { "name": "Plant" }, - { "name": "Undead" } - ], "species": [ { "name": "Dwarf",