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
20 changes: 20 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,25 @@ CLI-driven iteration.

---

## Code Changes

When making bulk renames or refactors, do NOT make unrequested additional renames. Only change what was explicitly asked for. If you think something else should be renamed, ask first.

---

## Interaction Style

When working through a list of items (issues, review findings, renames), present them one at a time and wait for user input before proceeding to the next.


---

## Problem Solving

When fixing build errors or investigating issues, always research the root cause before attempting a fix. Do not apply quick fixes without understanding the underlying problem (e.g., platform availability, import visibility rules).

---

## Testing

Tests use **Swift Testing** (`import Testing`), not XCTest.
Expand All @@ -188,6 +207,7 @@ Test coverage priorities:
3. `Compendium` loading and lookup correctness
4. UI flows for encounter setup and live tracking (UITests)

Always run a clean build and full test suite after making changes. Never reuse stale test output or cached build results. Use `swift build` and `swift test` fresh each time.
---

## Architecture Decision Records
Expand Down
73 changes: 66 additions & 7 deletions Encounter/Views/AdversaryRunnerCard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ struct AdversaryRunnerCard: View {
let compendium: Compendium
let onCollapse: () -> Void

@State private var isEditingName = false
@State private var pendingName = ""
@State private var nameError: String?
@FocusState private var isNameFieldFocused: Bool

private var displayName: String {
let adversary = compendium.adversary(id: slot.adversaryID)
return slot.customName ?? adversary?.name ?? "Unknown (\(slot.adversaryID))"
Expand All @@ -36,13 +41,39 @@ struct AdversaryRunnerCard: View {

// Header
HStack {
Text(displayName)
.font(.headline)
Spacer()
Button("Collapse", systemImage: "chevron.up", action: onCollapse)
.labelStyle(.iconOnly)
.buttonStyle(.borderless)
.accessibilityIdentifier("runner.adversary-card.collapse-button")
if isEditingName {
TextField("Name", text: $pendingName)
.font(.headline)
.focused($isNameFieldFocused)
.onSubmit { commitRename() }
.accessibilityIdentifier("runner.adversary-card.name-field")
Button("Done") { commitRename() }
.buttonStyle(.borderless)
.accessibilityIdentifier("runner.adversary-card.rename-done-button")
Button("Cancel") { cancelRename() }
.buttonStyle(.borderless)
.accessibilityIdentifier("runner.adversary-card.rename-cancel-button")
} else {
Button(action: beginRename) {
Text(displayName)
.font(.headline)
.foregroundStyle(.primary)
}
.buttonStyle(.plain)
.accessibilityIdentifier("runner.adversary-card.name")
.accessibilityHint("Tap to rename")
Spacer()
Button("Collapse", systemImage: "chevron.up", action: onCollapse)
.labelStyle(.iconOnly)
.buttonStyle(.borderless)
.accessibilityIdentifier("runner.adversary-card.collapse-button")
}
}
if isEditingName, let error = nameError {
Text(error)
.font(.caption)
.foregroundStyle(.red)
.accessibilityIdentifier("runner.adversary-card.name-error")
}

// HP pip track
Expand Down Expand Up @@ -93,6 +124,34 @@ struct AdversaryRunnerCard: View {
}
.padding(.vertical, 8)
}

private func beginRename() {
pendingName = displayName
nameError = nil
isEditingName = true
isNameFieldFocused = true
}

private func commitRename() {
let trimmed = pendingName.trimmingCharacters(in: .whitespaces)
guard !trimmed.isEmpty else {
cancelRename()
return
}
if session.renameAdversary(id: slot.id, name: trimmed) {
isNameFieldFocused = false
isEditingName = false
nameError = nil
} else {
nameError = "Name already in use"
}
}

private func cancelRename() {
isNameFieldFocused = false
isEditingName = false
nameError = nil
}
}

#Preview {
Expand Down
16 changes: 13 additions & 3 deletions Encounter/Views/AdversaryRunnerSection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,19 @@ struct AdversaryRunnerSection: View {
.disabled(slot.isDefeated)
.accessibilityIdentifier("runner.adversary-row")
.accessibilityLabel(
slot.isDefeated
? "\(slot.customName ?? slot.adversaryID), defeated"
: (slot.customName ?? slot.adversaryID)
{
let name = slot.customName ?? slot.adversaryID
var parts: [String] = [name]
if slot.isDefeated {
parts.append("defeated")
}
if !slot.conditions.isEmpty {
let conditionNames = slot.conditions.map(\.displayName).sorted().joined(
separator: ", ")
parts.append("Conditions: \(conditionNames)")
}
return parts.joined(separator: ", ")
}()
)
.accessibilityHint(slot.isDefeated ? "" : "Tap to expand")
}
Expand Down
52 changes: 51 additions & 1 deletion EncounterTests/EncounterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,57 @@ struct ConditionTests {
#expect(session.adversarySlots[0].isDefeated == false)
}

// MARK: - Rename

@Test func renameAdversary_success() {
let session = makeSession()
let soldier = makeSoldier()
session.add(adversary: soldier, customName: "Alpha")
session.add(adversary: soldier, customName: "Bravo")
let id = session.adversarySlots[0].id

let result = session.renameAdversary(id: id, name: "Charlie")

#expect(result == true)
#expect(session.adversarySlots[0].customName == "Charlie")
}

@Test func renameAdversary_duplicate() {
let session = makeSession()
let soldier = makeSoldier()
session.add(adversary: soldier, customName: "Alpha")
session.add(adversary: soldier, customName: "Bravo")
let id = session.adversarySlots[0].id

let result = session.renameAdversary(id: id, name: "Bravo")

#expect(result == false)
#expect(session.adversarySlots[0].customName == "Alpha")
}

@Test func renameAdversary_emptyName() {
let session = makeSession()
let soldier = makeSoldier()
session.add(adversary: soldier, customName: "Alpha")
let id = session.adversarySlots[0].id

#expect(session.renameAdversary(id: id, name: "") == false)
#expect(session.renameAdversary(id: id, name: " ") == false)
#expect(session.adversarySlots[0].customName == "Alpha")
}

@Test func renameAdversary_selfNameAllowed() {
let session = makeSession()
let soldier = makeSoldier()
session.add(adversary: soldier, customName: "Alpha")
let id = session.adversarySlots[0].id

let result = session.renameAdversary(id: id, name: "Alpha")

#expect(result == true)
#expect(session.adversarySlots[0].customName == "Alpha")
}

@Test func applyDamageReducesHP() {
let session = makeSession()
let soldier = makeSoldier()
Expand Down Expand Up @@ -367,7 +418,6 @@ struct ConditionTests {
#expect(session.adversarySlots[0].conditions.contains(.custom("Enraged")))
}


}

// MARK: - PlayerState
Expand Down
3 changes: 2 additions & 1 deletion EncounterUITests/AccessibilityTreeDump.swift
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ final class AccessibilityTreeDump: XCTestCase {
let createButton = app.buttons.matching(identifier: "library.create-button").firstMatch
XCTAssertTrue(createButton.waitForExistence(timeout: 3), "library.create-button not found")
XCTAssertTrue(createButton.isEnabled, "Create button should be enabled")
let result = "library.create-button found: true, enabled: \(createButton.isEnabled), frame: \(createButton.frame)\n"
let result =
"library.create-button found: true, enabled: \(createButton.isEnabled), frame: \(createButton.frame)\n"
try? result.write(toFile: "/tmp/library_create_button.txt", atomically: true, encoding: .utf8)
}

Expand Down
Loading