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
36 changes: 36 additions & 0 deletions Sources/DHModels/Party.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// Party.swift
// DHModels
//
// A named group of player characters.
//

import Foundation

#if canImport(FoundationEssentials)
import FoundationEssentials
#endif

// MARK: - Party

/// A named group of player characters.
///
/// `Party` stores ordered player IDs only; resolving full ``Player`` objects
/// from those IDs is the store's responsibility. A player may belong to
/// multiple parties.
nonisolated public struct Party: Codable, Sendable, Equatable, Hashable, Identifiable {
public let id: UUID
public var name: String
/// Ordered list of player IDs; order determines display order.
public var playerIDs: [UUID]

public init(
id: UUID = UUID(),
name: String,
playerIDs: [UUID] = []
) {
self.id = id
self.name = name
self.playerIDs = playerIDs
}
}
68 changes: 68 additions & 0 deletions Sources/DHModels/Player.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//
// Player.swift
// DHModels
//
// A persistent record of a player character tracked by the GM across encounters.
//

import Foundation

#if canImport(FoundationEssentials)
import FoundationEssentials
#endif

// MARK: - Player

/// A persistent record of a player character tracked by the GM across encounters.
///
/// `Player` is the canonical global identity for a PC. It carries the same
/// stat fields as ``PlayerConfig`` — the encounter-snapshot counterpart —
/// but exists independently of any encounter or party.
///
/// Use ``asConfig()`` to snapshot a `Player` into a ``PlayerConfig`` for
/// use in an ``EncounterDefinition``.
nonisolated public struct Player: Codable, Sendable, Equatable, Hashable, Identifiable {
public let id: UUID
public var name: String
public var maxHP: Int
public var maxStress: Int
public var evasion: Int
public var thresholdMajor: Int
public var thresholdSevere: Int
public var armorSlots: Int

public init(
id: UUID = UUID(),
name: String,
maxHP: Int,
maxStress: Int,
evasion: Int,
thresholdMajor: Int,
thresholdSevere: Int,
armorSlots: Int
) {
self.id = id
self.name = name
self.maxHP = maxHP
self.maxStress = maxStress
self.evasion = evasion
self.thresholdMajor = thresholdMajor
self.thresholdSevere = thresholdSevere
self.armorSlots = armorSlots
}

/// Snapshots this player's current stats into a ``PlayerConfig`` for use
/// in an ``EncounterDefinition`` or session creation.
public func asConfig() -> PlayerConfig {
PlayerConfig(
id: id,
name: name,
maxHP: maxHP,
maxStress: maxStress,
evasion: evasion,
thresholdMajor: thresholdMajor,
thresholdSevere: thresholdSevere,
armorSlots: armorSlots
)
}
}
87 changes: 87 additions & 0 deletions Tests/DHModelsTests/ModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,93 @@ struct DifficultyBudgetTests {
}
}

// MARK: - Player

struct PlayerTests {

@Test func playerIsValueType() {
var player1 = Player(
name: "Aldric", maxHP: 6, maxStress: 6,
evasion: 12, thresholdMajor: 8, thresholdSevere: 15, armorSlots: 3
)
let player2 = player1
player1.name = "Modified"
#expect(player2.name == "Aldric")
}

@Test func playerCodableRoundTrip() throws {
let player = Player(
name: "Sera", maxHP: 8, maxStress: 6,
evasion: 14, thresholdMajor: 10, thresholdSevere: 18, armorSlots: 4
)
let data = try JSONEncoder().encode(player)
let decoded = try JSONDecoder().decode(Player.self, from: data)

#expect(decoded.id == player.id)
#expect(decoded.name == "Sera")
#expect(decoded.maxHP == 8)
#expect(decoded.maxStress == 6)
#expect(decoded.evasion == 14)
#expect(decoded.thresholdMajor == 10)
#expect(decoded.thresholdSevere == 18)
#expect(decoded.armorSlots == 4)
}

@Test func asConfigPreservesAllFields() {
let player = Player(
name: "Torven", maxHP: 10, maxStress: 5,
evasion: 11, thresholdMajor: 7, thresholdSevere: 14, armorSlots: 2
)
let config = player.asConfig()

#expect(config.id == player.id)
#expect(config.name == player.name)
#expect(config.maxHP == player.maxHP)
#expect(config.maxStress == player.maxStress)
#expect(config.evasion == player.evasion)
#expect(config.thresholdMajor == player.thresholdMajor)
#expect(config.thresholdSevere == player.thresholdSevere)
#expect(config.armorSlots == player.armorSlots)
}
}

// MARK: - Party

struct PartyTests {

@Test func partyIsValueType() {
var party1 = Party(name: "The Wanderers")
let party2 = party1
party1.name = "Modified"
#expect(party2.name == "The Wanderers")
}

@Test func partyCodableRoundTrip() throws {
let ids = [UUID(), UUID(), UUID()]
let party = Party(name: "Ironclad Company", playerIDs: ids)
let data = try JSONEncoder().encode(party)
let decoded = try JSONDecoder().decode(Party.self, from: data)

#expect(decoded.id == party.id)
#expect(decoded.name == "Ironclad Company")
#expect(decoded.playerIDs == ids)
}

@Test func playerIDsOrderIsPreserved() throws {
let ids = (0..<5).map { _ in UUID() }
let party = Party(name: "Order Test", playerIDs: ids)
let data = try JSONEncoder().encode(party)
let decoded = try JSONDecoder().decode(Party.self, from: data)

#expect(decoded.playerIDs == ids)
}

@Test func defaultPartyHasNoPlayers() {
let party = Party(name: "Empty")
#expect(party.playerIDs.isEmpty)
}
}

// MARK: - DaggerheartEnvironment

struct EnvironmentModelTests {
Expand Down
Loading