fix: resolve specificStadium achievement ID mismatch
The Green Monster (Fenway) and Ivy League (Wrigley) achievements weren't working because: 1. Symbolic IDs use lowercase sport (stadium_mlb_bos) 2. Sport enum uses uppercase raw values (MLB) 3. Visits store stadium UUIDs, not symbolic IDs Added resolveSymbolicStadiumId() helper that: - Uppercases the sport string before Sport(rawValue:) - Looks up team by abbreviation and sport - Returns the team's stadiumId as UUID string Also fixed: - getStadiumIdsForLeague returns UUID strings (not symbolic IDs) - AchievementProgress.isEarned computed from progress OR stored record - getStadiumIdsForDivision queries CanonicalTeam properly Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -165,14 +165,14 @@ final class AchievementEngine {
|
||||
visitedStadiumIds: visitedStadiumIds
|
||||
)
|
||||
|
||||
let isEarned = earnedIds.contains(definition.id)
|
||||
let hasStoredAchievement = earnedIds.contains(definition.id)
|
||||
let earnedAt = earnedAchievements.first(where: { $0.achievementTypeId == definition.id })?.earnedAt
|
||||
|
||||
progress.append(AchievementProgress(
|
||||
definition: definition,
|
||||
currentProgress: current,
|
||||
totalRequired: total,
|
||||
isEarned: isEarned,
|
||||
hasStoredAchievement: hasStoredAchievement,
|
||||
earnedAt: earnedAt
|
||||
))
|
||||
}
|
||||
@@ -214,11 +214,41 @@ final class AchievementEngine {
|
||||
case .multipleLeagues(let leagueCount):
|
||||
return checkMultipleLeagues(visits: visits, requiredLeagues: leagueCount)
|
||||
|
||||
case .specificStadium(let stadiumId):
|
||||
return visitedStadiumIds.contains(stadiumId)
|
||||
case .specificStadium(let symbolicId):
|
||||
// Resolve symbolic ID (e.g., "stadium_mlb_bos") to actual UUID string
|
||||
guard let resolvedId = resolveSymbolicStadiumId(symbolicId) else { return false }
|
||||
return visitedStadiumIds.contains(resolvedId)
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves symbolic stadium IDs (e.g., "stadium_mlb_bos") to actual stadium UUID strings
|
||||
private func resolveSymbolicStadiumId(_ symbolicId: String) -> String? {
|
||||
// Parse symbolic ID format: "stadium_{sport}_{teamAbbrev}"
|
||||
let parts = symbolicId.split(separator: "_")
|
||||
guard parts.count == 3,
|
||||
parts[0] == "stadium" else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sport raw values are uppercase (e.g., "MLB"), but symbolic IDs use lowercase
|
||||
let sportString = String(parts[1]).uppercased()
|
||||
guard let sport = Sport(rawValue: sportString) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let teamAbbrev = String(parts[2]).uppercased()
|
||||
|
||||
// Find team by abbreviation and sport
|
||||
guard let team = dataProvider.teams.first(where: {
|
||||
$0.abbreviation.uppercased() == teamAbbrev && $0.sport == sport
|
||||
}) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Return the stadium UUID as string (matches visit's canonicalStadiumId format)
|
||||
return team.stadiumId.uuidString
|
||||
}
|
||||
|
||||
private func checkDivisionComplete(_ divisionId: String, visitedStadiumIds: Set<String>) -> Bool {
|
||||
guard let division = LeagueStructure.division(byId: divisionId) else { return false }
|
||||
|
||||
@@ -318,8 +348,10 @@ final class AchievementEngine {
|
||||
let leagues = Set(visits.compactMap { Sport(rawValue: $0.sport) })
|
||||
return (leagues.count, leagueCount)
|
||||
|
||||
case .specificStadium(let stadiumId):
|
||||
return (visitedStadiumIds.contains(stadiumId) ? 1 : 0, 1)
|
||||
case .specificStadium(let symbolicId):
|
||||
// Resolve symbolic ID to actual UUID string
|
||||
guard let resolvedId = resolveSymbolicStadiumId(symbolicId) else { return (0, 1) }
|
||||
return (visitedStadiumIds.contains(resolvedId) ? 1 : 0, 1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,25 +391,36 @@ final class AchievementEngine {
|
||||
}
|
||||
return []
|
||||
|
||||
case .specificStadium(let stadiumId):
|
||||
return visits.filter { $0.canonicalStadiumId == stadiumId }.map { $0.id }
|
||||
case .specificStadium(let symbolicId):
|
||||
// Resolve symbolic ID to actual UUID string
|
||||
guard let resolvedId = resolveSymbolicStadiumId(symbolicId) else { return [] }
|
||||
return visits.filter { $0.canonicalStadiumId == resolvedId }.map { $0.id }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Stadium Lookups
|
||||
|
||||
private func getStadiumIdsForDivision(_ divisionId: String) -> [String] {
|
||||
// Get teams in division, then their stadiums
|
||||
let teams = dataProvider.teams.filter { team in
|
||||
// Match division by checking team's division assignment
|
||||
// This would normally come from CanonicalTeam.divisionId
|
||||
// For now, return based on division structure
|
||||
return false // Will be populated when division data is linked
|
||||
// Query CanonicalTeam to find teams in this division
|
||||
let descriptor = FetchDescriptor<CanonicalTeam>(
|
||||
predicate: #Predicate { $0.divisionId == divisionId && $0.deprecatedAt == nil }
|
||||
)
|
||||
|
||||
guard let canonicalTeams = try? modelContext.fetch(descriptor) else {
|
||||
return []
|
||||
}
|
||||
|
||||
// For now, return hardcoded counts based on typical division sizes
|
||||
// This should be replaced with actual team-to-stadium mapping
|
||||
return []
|
||||
// Get stadium UUIDs for these teams
|
||||
// CanonicalTeam has stadiumCanonicalId, we need to find the corresponding Stadium UUID
|
||||
var stadiumIds: [String] = []
|
||||
for canonicalTeam in canonicalTeams {
|
||||
// Find the domain team by matching name/abbreviation to get stadium UUID
|
||||
if let team = dataProvider.teams.first(where: { $0.abbreviation == canonicalTeam.abbreviation && $0.sport.rawValue == canonicalTeam.sport }) {
|
||||
stadiumIds.append(team.stadiumId.uuidString)
|
||||
}
|
||||
}
|
||||
|
||||
return stadiumIds
|
||||
}
|
||||
|
||||
private func getStadiumIdsForConference(_ conferenceId: String) -> [String] {
|
||||
@@ -391,7 +434,7 @@ final class AchievementEngine {
|
||||
}
|
||||
|
||||
private func getStadiumIdsForLeague(_ sport: Sport) -> [String] {
|
||||
// Get all stadiums for this sport
|
||||
// Get all stadiums for this sport - return UUID strings to match visit format
|
||||
return dataProvider.stadiums
|
||||
.filter { stadium in
|
||||
// Check if stadium hosts teams of this sport
|
||||
@@ -399,7 +442,7 @@ final class AchievementEngine {
|
||||
team.stadiumId == stadium.id && team.sport == sport
|
||||
}
|
||||
}
|
||||
.map { "stadium_\(sport.rawValue.lowercased())_\($0.id.uuidString)" }
|
||||
.map { $0.id.uuidString }
|
||||
}
|
||||
|
||||
// MARK: - Data Fetching
|
||||
@@ -425,11 +468,17 @@ struct AchievementProgress: Identifiable {
|
||||
let definition: AchievementDefinition
|
||||
let currentProgress: Int
|
||||
let totalRequired: Int
|
||||
let isEarned: Bool
|
||||
let hasStoredAchievement: Bool // Whether an Achievement record exists in SwiftData
|
||||
let earnedAt: Date?
|
||||
|
||||
var id: String { definition.id }
|
||||
|
||||
/// Whether the achievement is earned (either stored or computed from progress)
|
||||
var isEarned: Bool {
|
||||
// Earned if we have a stored record OR if progress is complete
|
||||
hasStoredAchievement || (totalRequired > 0 && currentProgress >= totalRequired)
|
||||
}
|
||||
|
||||
var progressPercentage: Double {
|
||||
guard totalRequired > 0 else { return 0 }
|
||||
return Double(currentProgress) / Double(totalRequired)
|
||||
|
||||
Reference in New Issue
Block a user