Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"currencies": ["Currencies"],
"skills": ["Skills"],
"backgrounds": ["Backgrounds"],
"creature types": ["CreatureTypes"],
"classes": ["Classes"],
"species": ["Species"],
"species names": "SpeciesNames"
Expand Down
Original file line number Diff line number Diff line change
@@ -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" }
]
}
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
24 changes: 12 additions & 12 deletions Sources/RolePlayingCore/Configuration/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]?
Expand All @@ -23,6 +24,7 @@ public struct ConfigurationFiles: Decodable {
case currencies
case skills
case backgrounds
case creatureTypes = "creature types"
case species
case classes
case players
Expand All @@ -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()
Expand Down Expand Up @@ -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 {
Expand Down
3 changes: 2 additions & 1 deletion Sources/RolePlayingCore/Player/Backgrounds.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 4 additions & 4 deletions Sources/RolePlayingCore/Player/ClassTraits.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
55 changes: 42 additions & 13 deletions Sources/RolePlayingCore/Player/Classes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
38 changes: 38 additions & 0 deletions Sources/RolePlayingCore/Player/CreatureType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
4 changes: 2 additions & 2 deletions Sources/RolePlayingCore/Player/Player.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
3 changes: 2 additions & 1 deletion Sources/RolePlayingCore/Player/Skills.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
46 changes: 25 additions & 21 deletions Sources/RolePlayingCore/Player/Species.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<G: RandomIndexGenerator>(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)

Expand All @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/RolePlayingCore/Player/SpeciesTraits.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
Loading