Skip to content

Commit 85a7cb2

Browse files
authored
add Player and Party value types to DHModels (#18)
Closes #14, closes #15
1 parent 4b671f7 commit 85a7cb2

3 files changed

Lines changed: 191 additions & 0 deletions

File tree

Sources/DHModels/Party.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//
2+
// Party.swift
3+
// DHModels
4+
//
5+
// A named group of player characters.
6+
//
7+
8+
import Foundation
9+
10+
#if canImport(FoundationEssentials)
11+
import FoundationEssentials
12+
#endif
13+
14+
// MARK: - Party
15+
16+
/// A named group of player characters.
17+
///
18+
/// `Party` stores ordered player IDs only; resolving full ``Player`` objects
19+
/// from those IDs is the store's responsibility. A player may belong to
20+
/// multiple parties.
21+
nonisolated public struct Party: Codable, Sendable, Equatable, Hashable, Identifiable {
22+
public let id: UUID
23+
public var name: String
24+
/// Ordered list of player IDs; order determines display order.
25+
public var playerIDs: [UUID]
26+
27+
public init(
28+
id: UUID = UUID(),
29+
name: String,
30+
playerIDs: [UUID] = []
31+
) {
32+
self.id = id
33+
self.name = name
34+
self.playerIDs = playerIDs
35+
}
36+
}

Sources/DHModels/Player.swift

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
//
2+
// Player.swift
3+
// DHModels
4+
//
5+
// A persistent record of a player character tracked by the GM across encounters.
6+
//
7+
8+
import Foundation
9+
10+
#if canImport(FoundationEssentials)
11+
import FoundationEssentials
12+
#endif
13+
14+
// MARK: - Player
15+
16+
/// A persistent record of a player character tracked by the GM across encounters.
17+
///
18+
/// `Player` is the canonical global identity for a PC. It carries the same
19+
/// stat fields as ``PlayerConfig`` — the encounter-snapshot counterpart —
20+
/// but exists independently of any encounter or party.
21+
///
22+
/// Use ``asConfig()`` to snapshot a `Player` into a ``PlayerConfig`` for
23+
/// use in an ``EncounterDefinition``.
24+
nonisolated public struct Player: Codable, Sendable, Equatable, Hashable, Identifiable {
25+
public let id: UUID
26+
public var name: String
27+
public var maxHP: Int
28+
public var maxStress: Int
29+
public var evasion: Int
30+
public var thresholdMajor: Int
31+
public var thresholdSevere: Int
32+
public var armorSlots: Int
33+
34+
public init(
35+
id: UUID = UUID(),
36+
name: String,
37+
maxHP: Int,
38+
maxStress: Int,
39+
evasion: Int,
40+
thresholdMajor: Int,
41+
thresholdSevere: Int,
42+
armorSlots: Int
43+
) {
44+
self.id = id
45+
self.name = name
46+
self.maxHP = maxHP
47+
self.maxStress = maxStress
48+
self.evasion = evasion
49+
self.thresholdMajor = thresholdMajor
50+
self.thresholdSevere = thresholdSevere
51+
self.armorSlots = armorSlots
52+
}
53+
54+
/// Snapshots this player's current stats into a ``PlayerConfig`` for use
55+
/// in an ``EncounterDefinition`` or session creation.
56+
public func asConfig() -> PlayerConfig {
57+
PlayerConfig(
58+
id: id,
59+
name: name,
60+
maxHP: maxHP,
61+
maxStress: maxStress,
62+
evasion: evasion,
63+
thresholdMajor: thresholdMajor,
64+
thresholdSevere: thresholdSevere,
65+
armorSlots: armorSlots
66+
)
67+
}
68+
}

Tests/DHModelsTests/ModelTests.swift

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,93 @@ struct DifficultyBudgetTests {
442442
}
443443
}
444444

445+
// MARK: - Player
446+
447+
struct PlayerTests {
448+
449+
@Test func playerIsValueType() {
450+
var player1 = Player(
451+
name: "Aldric", maxHP: 6, maxStress: 6,
452+
evasion: 12, thresholdMajor: 8, thresholdSevere: 15, armorSlots: 3
453+
)
454+
let player2 = player1
455+
player1.name = "Modified"
456+
#expect(player2.name == "Aldric")
457+
}
458+
459+
@Test func playerCodableRoundTrip() throws {
460+
let player = Player(
461+
name: "Sera", maxHP: 8, maxStress: 6,
462+
evasion: 14, thresholdMajor: 10, thresholdSevere: 18, armorSlots: 4
463+
)
464+
let data = try JSONEncoder().encode(player)
465+
let decoded = try JSONDecoder().decode(Player.self, from: data)
466+
467+
#expect(decoded.id == player.id)
468+
#expect(decoded.name == "Sera")
469+
#expect(decoded.maxHP == 8)
470+
#expect(decoded.maxStress == 6)
471+
#expect(decoded.evasion == 14)
472+
#expect(decoded.thresholdMajor == 10)
473+
#expect(decoded.thresholdSevere == 18)
474+
#expect(decoded.armorSlots == 4)
475+
}
476+
477+
@Test func asConfigPreservesAllFields() {
478+
let player = Player(
479+
name: "Torven", maxHP: 10, maxStress: 5,
480+
evasion: 11, thresholdMajor: 7, thresholdSevere: 14, armorSlots: 2
481+
)
482+
let config = player.asConfig()
483+
484+
#expect(config.id == player.id)
485+
#expect(config.name == player.name)
486+
#expect(config.maxHP == player.maxHP)
487+
#expect(config.maxStress == player.maxStress)
488+
#expect(config.evasion == player.evasion)
489+
#expect(config.thresholdMajor == player.thresholdMajor)
490+
#expect(config.thresholdSevere == player.thresholdSevere)
491+
#expect(config.armorSlots == player.armorSlots)
492+
}
493+
}
494+
495+
// MARK: - Party
496+
497+
struct PartyTests {
498+
499+
@Test func partyIsValueType() {
500+
var party1 = Party(name: "The Wanderers")
501+
let party2 = party1
502+
party1.name = "Modified"
503+
#expect(party2.name == "The Wanderers")
504+
}
505+
506+
@Test func partyCodableRoundTrip() throws {
507+
let ids = [UUID(), UUID(), UUID()]
508+
let party = Party(name: "Ironclad Company", playerIDs: ids)
509+
let data = try JSONEncoder().encode(party)
510+
let decoded = try JSONDecoder().decode(Party.self, from: data)
511+
512+
#expect(decoded.id == party.id)
513+
#expect(decoded.name == "Ironclad Company")
514+
#expect(decoded.playerIDs == ids)
515+
}
516+
517+
@Test func playerIDsOrderIsPreserved() throws {
518+
let ids = (0..<5).map { _ in UUID() }
519+
let party = Party(name: "Order Test", playerIDs: ids)
520+
let data = try JSONEncoder().encode(party)
521+
let decoded = try JSONDecoder().decode(Party.self, from: data)
522+
523+
#expect(decoded.playerIDs == ids)
524+
}
525+
526+
@Test func defaultPartyHasNoPlayers() {
527+
let party = Party(name: "Empty")
528+
#expect(party.playerIDs.isEmpty)
529+
}
530+
}
531+
445532
// MARK: - DaggerheartEnvironment
446533

447534
struct EnvironmentModelTests {

0 commit comments

Comments
 (0)