diff --git a/.spi.yml b/.spi.yml index b2bce0a..d2b5894 100644 --- a/.spi.yml +++ b/.spi.yml @@ -1,4 +1,4 @@ version: 1 builder: configs: - - documentation_targets: [DaggerheartModels, DaggerheartKit] + - documentation_targets: [DHModels, DHKit] diff --git a/CLAUDE.md b/CLAUDE.md index b37dc13..ee32075 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -112,13 +112,33 @@ git ls-files -z '*.swift' | xargs -0 swift-format lint --strict --parallel ## Adding new model types -1. Add the `.swift` file to `Sources/DHModels/` if the type is - Foundation-only, or to `Sources/DHKit/` if it needs `@Observable` - or Apple-only frameworks. +Use this decision tree to choose the right target: + +- **`DHModels`** — static catalog and prep-time types: `Adversary`, + `DaggerheartEnvironment`, `EncounterDefinition`, `PlayerConfig`, `Condition`, + etc. Must be Foundation-only and Linux-safe. Will appear in `.dhpack`/JSON files + or be referenced by `validate-dhpack`. Test in `DHModelsTests`. +- **`DHKit`** — everything else on Apple platforms: `@Observable` stores + (`Compendium`, `EncounterStore`, `SessionRegistry`), live session types + (`EncounterSession`, `AdversarySlot`, `PlayerSlot`, `EnvironmentSlot`, + `EncounterParticipant`/`CombatParticipant`), and any type that depends on + `Observation` or other Apple-only frameworks. Test in `DHKitTests`. + +The key distinction is **catalog vs. runtime**: a type that models static +game-data definitions belongs in `DHModels`; a type that represents live, +in-play encounter state belongs in `DHKit` even if it is a plain value type +with no Apple-only imports. + +**`nonisolated` on DHKit value types:** `DHKit` uses +`.defaultIsolation(MainActor.self)`. Any `struct` or `enum` that must be +usable across isolation contexts (e.g. slot types) needs `nonisolated` on its +declaration to opt out of the default `@MainActor` isolation. + +1. Add the `.swift` file to the correct target (see above). 2. Make it `public`. 3. Add `Codable` conformance if it will appear in `.dhpack` or JSON files. -4. Write tests in `DHModelsTests` (for model types) or `DHKitTests` - (for observable stores). Follow red-green TDD. +4. Write tests in `DHModelsTests` (for `DHModels` types) or `DHKitTests` + (for `DHKit` types). Follow red-green TDD. 5. Run `./Scripts/format.sh` before committing. --- diff --git a/DHKit-diagram.md b/DHKit-diagram.md new file mode 100644 index 0000000..3049135 --- /dev/null +++ b/DHKit-diagram.md @@ -0,0 +1,228 @@ +# DHKit — Type Relationship Diagram + +`DHKit` is the observable store layer. All top-level store classes +are `@Observable @MainActor`. Slot types are `nonisolated` value types so they +can be passed freely across isolation boundaries. + +```mermaid +classDiagram + direction TB + + %% ── Protocols ───────────────────────────────────────────────────────── + class EncounterParticipant { + <> + +UUID id + } + + class CombatParticipant { + <> + +Int currentHP + +Int maxHP + +Int currentStress + +Int maxStress + +Set~Condition~ conditions + } + + EncounterParticipant <|-- CombatParticipant + + %% ── Slot value types ───────────────────────────────────────────────── + class AdversarySlot { + +UUID id + +String adversaryID + +String? customName + +Int maxHP + +Int maxStress + +Int currentHP + +Int currentStress + +Bool isDefeated + +Set~Condition~ conditions + +init(from: Adversary, customName:) + +applying(currentHP:currentStress:isDefeated:conditions:) AdversarySlot + } + + class PlayerSlot { + +UUID id + +String name + +Int maxHP + +Int currentHP + +Int maxStress + +Int currentStress + +Int evasion + +Int thresholdMajor + +Int thresholdSevere + +Int armorSlots + +Int currentArmorSlots + +Set~Condition~ conditions + +applying(currentHP:currentStress:currentArmorSlots:conditions:) PlayerSlot + } + + class EnvironmentSlot { + +UUID id + +String environmentID + +Bool isActive + +applying(isActive:) EnvironmentSlot + } + + CombatParticipant <|.. AdversarySlot + CombatParticipant <|.. PlayerSlot + EncounterParticipant <|.. EnvironmentSlot + + %% ── EncounterSession ───────────────────────────────────────────────── + class EncounterSession { + <> + +UUID id + +String name + +[AdversarySlot] adversarySlots + +[PlayerSlot] playerSlots + +[EnvironmentSlot] environmentSlots + +Int fearPool + +Int hopePool + +UUID? spotlightedSlotID + +Int spotlightCount + +String gmNotes + +[AdversarySlot] activeAdversaries + +Bool isOver + +add(adversary: Adversary, customName:) + +add(environment: DaggerheartEnvironment) + +add(player: PlayerSlot) + +removeAdversary(withID:) + +removePlayer(withID:) + +spotlight(id:) + +yieldSpotlight() + +applyDamage(_:to:) + +applyHealing(_:to:) + +applyStress(_:to:) + +reduceStress(_:from:) + +applyCondition(_:to:) + +removeCondition(_:from:) + +markArmorSlot(for:) + +restoreArmorSlot(for:) + +incrementFear(by:) + +spendFear(by:) + +incrementHope(by:) + +spendHope(by:) + +make(from: EncounterDefinition, using: Compendium) EncounterSession$ + } + + EncounterSession "1" *-- "0..*" AdversarySlot : adversarySlots + EncounterSession "1" *-- "0..*" PlayerSlot : playerSlots + EncounterSession "1" *-- "0..*" EnvironmentSlot : environmentSlots + + %% ── Compendium ─────────────────────────────────────────────────────── + class Compendium { + <> + +[String: Adversary] adversariesByID + +[String: DaggerheartEnvironment] environmentsByID + +[Adversary] adversaries + +[DaggerheartEnvironment] environments + +[Adversary] homebrewAdversaries + +[DaggerheartEnvironment] homebrewEnvironments + +Bool isLoading + +CompendiumError? loadError + +init(bundle: Bundle?) + +load() async throws + +adversary(id:) Adversary? + +environment(id:) DaggerheartEnvironment? + +adversaries(ofTier:) [Adversary] + +adversaries(ofRole:) [Adversary] + +adversaries(matching:) [Adversary] + +addAdversary(_:) + +removeHomebrewAdversary(id:) + +addEnvironment(_:) + +removeHomebrewEnvironment(id:) + +replaceSRDContent(adversaries:environments:) + +replaceSourceContent(sourceID:adversaries:environments:) + +removeSourceContent(sourceID:) + } + + class CompendiumError { + <> + fileNotFound(resourceName:) + decodingFailed(resourceName:underlying:) + } + + Compendium ..> CompendiumError : throws + + %% ── EncounterStore ─────────────────────────────────────────────────── + class EncounterStore { + <> + +[EncounterDefinition] definitions + +URL directory + +Bool isLoading + +Error? loadError + +init(directory: URL) + +defaultDirectory() URL$ async + +localDirectory URL$ + +relocate(to:) + +load() async + +create(name:) async throws + +save(_:) async throws + +delete(id:) async throws + +duplicate(id:) async throws + } + + class EncounterStoreError { + <> + notFound(UUID) + saveFailed(UUID, String) + deleteFailed(UUID, String) + } + + EncounterStore ..> EncounterStoreError : throws + + %% ── SessionRegistry ────────────────────────────────────────────────── + class SessionRegistry { + <> + +[UUID: EncounterSession] sessions + +init() + +session(for:definition:compendium:) EncounterSession + +clearSession(for:) + +resetSession(for:definition:compendium:) EncounterSession + } + + SessionRegistry "1" o-- "0..*" EncounterSession : sessions + + %% ── Cross-type dependencies ────────────────────────────────────────── + EncounterSession ..> Compendium : make(from:using:) + SessionRegistry ..> Compendium : session(for:definition:compendium:) + SessionRegistry ..> EncounterSession : creates / owns +``` + +## Typical usage flow + +```mermaid +sequenceDiagram + participant App + participant Compendium + participant EncounterStore + participant SessionRegistry + participant EncounterSession + + App->>Compendium: init() + load() + Note over Compendium: Decodes SRD JSON from bundle + + App->>EncounterStore: init(directory:) + load() + Note over EncounterStore: Reads .encounter.json files from disk + + App->>EncounterStore: create(name:) + EncounterStore-->>App: definitions updated + + App->>SessionRegistry: session(for:definition:compendium:) + SessionRegistry->>EncounterSession: make(from:using:) + Note over EncounterSession: Resolves adversary/environment IDs
via Compendium; builds slots + + App->>EncounterSession: add(player:) / spotlight(id:) + App->>EncounterSession: applyDamage(_:to:) / applyCondition(_:to:) + App->>EncounterStore: save(definition) when prep changes +``` + +## Key design points + +| Concern | Approach | +|---|---| +| Default isolation | `@MainActor` on all `@Observable` classes; slots are `nonisolated` structs | +| Mutation pattern | Slots are immutable; `EncounterSession` replaces them wholesale via `applying(...)` | +| Catalog vs. runtime | `Compendium` holds static catalog data; `EncounterSession` holds live session state | +| Persistence | `EncounterStore` persists `EncounterDefinition` (prep); sessions are in-memory only | +| Session lifecycle | `SessionRegistry` holds sessions keyed by definition ID; `clearSession` / `resetSession` to restart | +| Homebrew priority | Compendium merges: homebrew → source packs → SRD (last writer wins on ID conflict) | diff --git a/DHModels-diagram.md b/DHModels-diagram.md new file mode 100644 index 0000000..a818669 --- /dev/null +++ b/DHModels-diagram.md @@ -0,0 +1,194 @@ +# DHModels — Type Relationship Diagram + +`DHModels` is the catalog layer. All types are value types +(`struct` or `enum`), `Codable`, `Sendable`, and Linux-safe. + +```mermaid +classDiagram + direction TB + + %% ── Adversary cluster ───────────────────────────────────────────────── + class Adversary { + +String id + +String name + +String source + +Int tier + +AdversaryType role + +String flavorText + +String? motivesAndTactics + +Int difficulty + +Int thresholdMajor + +Int thresholdSevere + +Int hp + +Int stress + +String attackModifier + +String attackName + +AttackRange attackRange + +String damage + +String? experience + +[EncounterFeature] features + +Bool isHomebrew + } + + class AdversaryType { + <> + bruiser + horde + leader + minion + ranged + skulk + social + solo + standard + support + } + + class AttackRange { + <> + melee + veryClose + close + far + veryFar + } + + class FeatureType { + <> + action + reaction + passive + +inferred(from:) FeatureType$ + } + + class EncounterFeature { + +String id + +String name + +String text + +FeatureType kind + } + + Adversary --> AdversaryType : role + Adversary --> AttackRange : attackRange + Adversary "1" *-- "0..*" EncounterFeature : features + EncounterFeature --> FeatureType : kind + + %% ── Environment cluster ─────────────────────────────────────────────── + class DaggerheartEnvironment { + +String id + +String name + +String source + +String flavorText + +[EncounterFeature] features + +Bool isHomebrew + } + + DaggerheartEnvironment "1" *-- "0..*" EncounterFeature : features + + %% ── Conditions ──────────────────────────────────────────────────────── + class Condition { + <> + hidden + restrained + vulnerable + custom(String) + +String displayName + } + + %% ── Encounter definition (prep / save layer) ────────────────────────── + class EncounterDefinition { + +UUID id + +String name + +[String] adversaryIDs + +[String] environmentIDs + +[PlayerConfig] playerConfigs + +String gmNotes + +Date createdAt + +Date modifiedAt + } + + class PlayerConfig { + +UUID id + +String name + +Int maxHP + +Int maxStress + +Int evasion + +Int thresholdMajor + +Int thresholdSevere + +Int armorSlots + } + + EncounterDefinition "1" *-- "0..*" PlayerConfig : playerConfigs + + %% ── Difficulty budget (static utility) ─────────────────────────────── + class DifficultyBudget { + <> + +cost(for: AdversaryType) Int$ + +baseBudget(playerCount:) Int$ + +totalCost(for: [AdversaryType]) Int$ + +rating(adversaryTypes:playerCount:budgetAdjustment:) Rating$ + +suggestedAdjustments(adversaryTypes:) [Adjustment]$ + } + + class DifficultyBudget_Rating { + +Int cost + +Int budget + +Int remaining + } + + class DifficultyBudget_Adjustment { + <> + easierFight + multipleSolos + boostedDamage + lowerTierAdversary + noBigThreats + harderFight + +Int pointValue + } + + DifficultyBudget ..> AdversaryType : uses + DifficultyBudget ..> DifficultyBudget_Rating : returns + DifficultyBudget ..> DifficultyBudget_Adjustment : returns + + %% ── Content pack types ──────────────────────────────────────────────── + class DHPackContent { + +[Adversary] adversaries + +[DaggerheartEnvironment] environments + } + + DHPackContent "1" *-- "0..*" Adversary : adversaries + DHPackContent "1" *-- "0..*" DaggerheartEnvironment : environments + + class ContentSource { + +UUID id + +String? displayName + +URL? url + +Date addedAt + +Date? lastFetchedAt + +String? etag + +Bool isThrottled(at:) + +TimeInterval nextAllowedFetch(after:) + } + + class ContentFingerprint { + +String sha256 + } + + class ContentStoreError { + <> + fileNotFound + decodingFailed + downloadFailed + invalidURL + } +``` + +## Key relationships + +| Relationship | Description | +|---|---| +| `EncounterDefinition` stores IDs | Holds `adversaryIDs` and `environmentIDs` as `[String]` slugs — resolved into live slots at session creation via `Compendium` (DHKit) | +| `PlayerConfig` → `PlayerSlot` | `PlayerConfig` is the serialisable prep-time form; `PlayerSlot` (DHKit) is the live runtime form created from it | +| `DifficultyBudget` | Stateless utility — call its static methods with a list of `AdversaryType` to estimate encounter difficulty | +| `DHPackContent` | Decoded form of a `.dhpack` file; fed into `Compendium.replaceSourceContent(sourceID:adversaries:environments:)` | diff --git a/README.md b/README.md index 561036f..17189dc 100644 --- a/README.md +++ b/README.md @@ -32,40 +32,12 @@ is hosted on the **Swift Package Index**. ### `DHModels` Pure value types (structs and enums) that model Daggerheart catalog and encounter -data. No UIKit, AppKit, Observation, or Apple-only frameworks — safe to use on -Linux, server-side Swift, and hopefully Wasm as well. - -| Type | Purpose | -|---|---| -| `Adversary` | Catalog entry for a Daggerheart adversary (stats, features, thresholds) | -| `AdversaryType` | Adversary role enum: Bruiser, Horde, Leader, Minion, Ranged, Skulk, Social, Solo, Standard, Support | -| `EncounterFeature` | Named action, reaction, or passive on an adversary or environment | -| `FeatureType` | Feature category enum: action, reaction, passive | -| `AttackRange` | Attack range enum: Melee, Very Close, Close, Far, Very Far | -| `DaggerheartEnvironment` | Catalog entry for a scene environment | -| `EncounterDefinition` | Saved encounter definition (name, adversary roster, GM notes) | -| `PlayerSlot` | Player configuration within an encounter definition | -| `DHPackContent` | Top-level type for `.dhpack` content pack files | -| `ContentSource` | Remote content source (URL, display name, cache metadata) | -| `ContentFingerprint` | Snapshot hash + etag for change detection | -| `ContentStoreError` | Errors from content source management | -| `DifficultyBudget` | Difficulty assessment helpers | -| `Condition` | Status condition (name, description) | - -### `DHKit` — Apple-platform `@Observable` stores - -`@MainActor` observable classes for SwiftUI integration. Requires Apple platforms -(iOS 17+, macOS 14+, tvOS 17+, watchOS 10+). Depends on `DHModels` and -`swift-log`. - -| Type | Purpose | -|---|---| -| `Compendium` | Loads SRD adversary and environment JSON from the bundle; supports homebrew and community source packs; full-text search | -| `EncounterStore` | Persists `EncounterDefinition` files to disk; create, save, delete, duplicate | -| `EncounterSession` | Runtime mutable state for a live encounter: HP/stress tracking, adversary slots, player slots | -| `SessionRegistry` | Cache of active `EncounterSession` instances keyed by encounter ID | -| `CompendiumError` | Errors from Compendium loading | -| `EncounterStoreError` | Errors from EncounterStore persistence | +data. + +### `DHKit` — `@Observable` stores + +`@MainActor` observable classes for UI integration. +Depends on `DHModels` and `swift-log`. ### `validate-dhpack` — CLI tool @@ -127,7 +99,7 @@ swift build # Run all tests swift test -# Linux-safe model tests only +# Model tests only swift test --filter DHModelsTests ``` diff --git a/Sources/DHModels/AdversarySlot.swift b/Sources/DHKit/AdversarySlot.swift similarity index 96% rename from Sources/DHModels/AdversarySlot.swift rename to Sources/DHKit/AdversarySlot.swift index 517189d..d25025d 100644 --- a/Sources/DHModels/AdversarySlot.swift +++ b/Sources/DHKit/AdversarySlot.swift @@ -1,10 +1,11 @@ // // AdversarySlot.swift -// DHModels +// DHKit // // A single adversary participant in a live encounter. // +import DHModels import Foundation /// A single adversary participant in a live encounter. @@ -19,7 +20,7 @@ import Foundation /// /// All properties are immutable. Mutations are performed by ``EncounterSession``, /// which replaces slots wholesale (copy-with-update pattern). -public struct AdversarySlot: CombatParticipant, Sendable, Equatable, Hashable { +nonisolated public struct AdversarySlot: CombatParticipant, Sendable, Equatable, Hashable { public let id: UUID /// The slug that identifies this adversary in the ``Compendium``. public let adversaryID: String diff --git a/Sources/DHModels/EncounterParticipant.swift b/Sources/DHKit/EncounterParticipant.swift similarity index 82% rename from Sources/DHModels/EncounterParticipant.swift rename to Sources/DHKit/EncounterParticipant.swift index 0444d33..d5fdb45 100644 --- a/Sources/DHModels/EncounterParticipant.swift +++ b/Sources/DHKit/EncounterParticipant.swift @@ -1,24 +1,25 @@ // // EncounterParticipant.swift -// DaggerheartModels +// DHKit // // Protocols for encounter participants, enabling the spotlight and // combat mutation APIs to work uniformly across adversary and player slots. // +import DHModels import Foundation /// A participant in a Daggerheart encounter that can hold the spotlight. /// /// All adversary, environment, and player slots conform to this protocol, /// allowing the spotlight API to accept any participant type uniformly. -public protocol EncounterParticipant: Identifiable where ID == UUID {} +nonisolated public protocol EncounterParticipant: Identifiable where ID == UUID {} /// An encounter participant that tracks HP, Stress, and Conditions. /// /// Conformed to by ``AdversarySlot`` and ``PlayerSlot``. Used as a read/display /// contract; all mutations are performed by ``EncounterSession`` via UUID. -public protocol CombatParticipant: EncounterParticipant { +nonisolated public protocol CombatParticipant: EncounterParticipant { var currentHP: Int { get } var maxHP: Int { get } var currentStress: Int { get } diff --git a/Sources/DHModels/EnvironmentSlot.swift b/Sources/DHKit/EnvironmentSlot.swift similarity index 91% rename from Sources/DHModels/EnvironmentSlot.swift rename to Sources/DHKit/EnvironmentSlot.swift index 94580bc..27ed776 100644 --- a/Sources/DHModels/EnvironmentSlot.swift +++ b/Sources/DHKit/EnvironmentSlot.swift @@ -1,6 +1,6 @@ // // EnvironmentSlot.swift -// DHModels +// DHKit // // An environment element active in the current encounter scene. // @@ -14,7 +14,7 @@ import Foundation /// /// All properties are immutable. Mutations are performed by ``EncounterSession``, /// which replaces slots wholesale (copy-with-update pattern). -public struct EnvironmentSlot: EncounterParticipant, Sendable, Equatable, Hashable { +nonisolated public struct EnvironmentSlot: EncounterParticipant, Sendable, Equatable, Hashable { public let id: UUID /// The slug identifying this environment in the ``Compendium``. public let environmentID: String diff --git a/Sources/DHModels/PlayerSlot.swift b/Sources/DHKit/PlayerSlot.swift similarity index 96% rename from Sources/DHModels/PlayerSlot.swift rename to Sources/DHKit/PlayerSlot.swift index 7c92c8d..27769ec 100644 --- a/Sources/DHModels/PlayerSlot.swift +++ b/Sources/DHKit/PlayerSlot.swift @@ -1,6 +1,6 @@ // // PlayerSlot.swift -// Encounter +// DHKit // // A player character participant in a live encounter. // Tracks the combat-relevant subset of a PC's stats that the GM needs @@ -13,13 +13,14 @@ // - Armor Slots: Marks available to reduce damage severity (equals Armor Score). // +import DHModels import Foundation /// A player character participant in a live encounter. /// /// Tracks combat-relevant PC stats the GM needs to resolve hits and /// track health during play. The full character sheet remains with the player. -public struct PlayerSlot: CombatParticipant, Sendable, Equatable, Hashable { +nonisolated public struct PlayerSlot: CombatParticipant, Sendable, Equatable, Hashable { public let id: UUID public let name: String diff --git a/Tests/DHKitTests/SlotTests.swift b/Tests/DHKitTests/SlotTests.swift new file mode 100644 index 0000000..362ef0e --- /dev/null +++ b/Tests/DHKitTests/SlotTests.swift @@ -0,0 +1,104 @@ +// +// SlotTests.swift +// DHKitTests +// +// Unit tests for the slot value types: PlayerSlot, AdversarySlot, EnvironmentSlot. +// + +import DHModels +import Foundation +import Testing + +@testable import DHKit + +// MARK: - PlayerSlot + +struct PlayerSlotTests { + + @Test func playerSlotInitializesWithCorrectDefaults() { + let slot = PlayerSlot( + name: "Aldric", + maxHP: 6, + maxStress: 6, + evasion: 12, + thresholdMajor: 8, + thresholdSevere: 15, + armorSlots: 3 + ) + #expect(slot.name == "Aldric") + #expect(slot.currentHP == 6) + #expect(slot.currentStress == 0) + #expect(slot.currentArmorSlots == 3) + #expect(slot.conditions.isEmpty) + } + + @Test func playerSlotEquality() { + let id = UUID() + let slot1 = PlayerSlot( + id: id, name: "A", maxHP: 6, maxStress: 6, + evasion: 10, thresholdMajor: 5, thresholdSevere: 10, armorSlots: 2 + ) + let slot2 = PlayerSlot( + id: id, name: "A", maxHP: 6, maxStress: 6, + evasion: 10, thresholdMajor: 5, thresholdSevere: 10, armorSlots: 2 + ) + #expect(slot1 == slot2) + } +} + +// MARK: - AdversarySlot + +struct AdversarySlotTests { + + @Test func adversarySlotInitializesWithCorrectDefaults() { + let slot = AdversarySlot(adversaryID: "ironguard-soldier", maxHP: 6, maxStress: 3) + #expect(slot.currentHP == 6) + #expect(slot.currentStress == 0) + #expect(slot.isDefeated == false) + #expect(slot.conditions.isEmpty) + #expect(slot.customName == nil) + } + + @Test func adversarySlotConvenienceInitFromAdversary() { + let adversary = Adversary( + id: "bandit", name: "Bandit", + tier: 1, role: .minion, flavorText: "A common thug.", + difficulty: 8, thresholdMajor: 3, thresholdSevere: 6, + hp: 4, stress: 2, attackModifier: "+1", attackName: "Dagger", + attackRange: .veryClose, damage: "1d6 phy" + ) + let slot = AdversarySlot(from: adversary, customName: "Grim") + #expect(slot.adversaryID == "bandit") + #expect(slot.maxHP == 4) + #expect(slot.maxStress == 2) + #expect(slot.customName == "Grim") + } + + @Test func adversarySlotApplyingPreservesUnchangedFields() { + let slot = AdversarySlot(adversaryID: "orc", maxHP: 8, maxStress: 4) + let updated = slot.applying(currentHP: 5) + #expect(updated.currentHP == 5) + #expect(updated.maxHP == 8) + #expect(updated.adversaryID == "orc") + #expect(updated.id == slot.id) + } +} + +// MARK: - EnvironmentSlot + +struct EnvironmentSlotTests { + + @Test func environmentSlotDefaultsToActive() { + let slot = EnvironmentSlot(environmentID: "arcane-storm") + #expect(slot.isActive == true) + #expect(slot.environmentID == "arcane-storm") + } + + @Test func environmentSlotApplyingTogglesActive() { + let slot = EnvironmentSlot(environmentID: "collapsing-bridge", isActive: true) + let deactivated = slot.applying(isActive: false) + #expect(deactivated.isActive == false) + #expect(deactivated.id == slot.id) + #expect(deactivated.environmentID == slot.environmentID) + } +} diff --git a/Tests/DHModelsTests/ModelTests.swift b/Tests/DHModelsTests/ModelTests.swift index cfac10b..13b791b 100644 --- a/Tests/DHModelsTests/ModelTests.swift +++ b/Tests/DHModelsTests/ModelTests.swift @@ -3,8 +3,8 @@ // DaggerheartModelsTests // // Unit tests for pure Codable model types: -// Adversary, Condition, PlayerSlot, EncounterDefinition, -// DifficultyBudget, EncounterStoreError, DaggerheartEnvironment. +// Adversary, Condition, EncounterDefinition, +// DifficultyBudget, DaggerheartEnvironment. // import Foundation @@ -225,41 +225,6 @@ struct ConditionTests { } } -// MARK: - PlayerSlot - -struct PlayerSlotTests { - - @Test func playerSlotInitializesWithCorrectDefaults() { - let slot = PlayerSlot( - name: "Aldric", - maxHP: 6, - maxStress: 6, - evasion: 12, - thresholdMajor: 8, - thresholdSevere: 15, - armorSlots: 3 - ) - #expect(slot.name == "Aldric") - #expect(slot.currentHP == 6) - #expect(slot.currentStress == 0) - #expect(slot.currentArmorSlots == 3) - #expect(slot.conditions.isEmpty) - } - - @Test func playerSlotEquality() { - let id = UUID() - let slot1 = PlayerSlot( - id: id, name: "A", maxHP: 6, maxStress: 6, - evasion: 10, thresholdMajor: 5, thresholdSevere: 10, armorSlots: 2 - ) - let slot2 = PlayerSlot( - id: id, name: "A", maxHP: 6, maxStress: 6, - evasion: 10, thresholdMajor: 5, thresholdSevere: 10, armorSlots: 2 - ) - #expect(slot1 == slot2) - } -} - // MARK: - EncounterDefinition struct EncounterDefinitionTests {