diff --git a/.gitignore b/.gitignore index 82e9a12f9..a412663cf 100644 --- a/.gitignore +++ b/.gitignore @@ -48,28 +48,6 @@ Package.resolved *swiftpm/ .build/ -# CocoaPods -# -# We recommend against adding the Pods directory to your .gitignore. However -# you should judge for yourself, the pros and cons are mentioned at: -# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control -# -# Pods/ -# -# Add this line if you want to avoid checking in source code from the Xcode workspace -# *.xcworkspace - -# Carthage -# -# Add this line if you want to avoid checking in source code from Carthage dependencies. -# Carthage/Checkouts - -Carthage/Build/ - -# Accio dependency management -Dependencies/ -.accio/ - # fastlane # # It is recommended to not store the screenshots in the git repo. diff --git a/App/WildWestOnline.xcodeproj/project.pbxproj b/App/WildWestOnline.xcodeproj/project.pbxproj index 215c56917..a0a9d7137 100644 --- a/App/WildWestOnline.xcodeproj/project.pbxproj +++ b/App/WildWestOnline.xcodeproj/project.pbxproj @@ -9,7 +9,7 @@ /* Begin PBXBuildFile section */ 2699EF0129D93AD00030ACCD /* WildWestOnlineApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2699EF0029D93AD00030ACCD /* WildWestOnlineApp.swift */; }; 2699EF0529D93AD10030ACCD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2699EF0429D93AD10030ACCD /* Assets.xcassets */; }; - 26F4048D2EB5CE5700F5FCA9 /* AppBootstrap in Frameworks */ = {isa = PBXBuildFile; productRef = 26F4048C2EB5CE5700F5FCA9 /* AppBootstrap */; }; + 98A1A0BA2EE4A1D7003EB768 /* AppBuilder in Frameworks */ = {isa = PBXBuildFile; productRef = 98A1A0B92EE4A1D7003EB768 /* AppBuilder */; }; 98E868E62D9D19CC00EF4582 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 98E868E52D9D19CC00EF4582 /* LaunchScreen.storyboard */; }; /* End PBXBuildFile section */ @@ -26,7 +26,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 26F4048D2EB5CE5700F5FCA9 /* AppBootstrap in Frameworks */, + 98A1A0BA2EE4A1D7003EB768 /* AppBuilder in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -77,7 +77,7 @@ ); name = WildWestOnline; packageProductDependencies = ( - 26F4048C2EB5CE5700F5FCA9 /* AppBootstrap */, + 98A1A0B92EE4A1D7003EB768 /* AppBuilder */, ); productName = ReduxGameDSL; productReference = 2699EEFD29D93AD00030ACCD /* WildWestOnline.app */; @@ -419,9 +419,10 @@ /* End XCLocalSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 26F4048C2EB5CE5700F5FCA9 /* AppBootstrap */ = { + 98A1A0B92EE4A1D7003EB768 /* AppBuilder */ = { isa = XCSwiftPackageProductDependency; - productName = AppBootstrap; + package = 26F4048B2EB5CE5700F5FCA9 /* XCLocalSwiftPackageReference "../Modules" */; + productName = AppBuilder; }; /* End XCSwiftPackageProductDependency section */ }; diff --git a/App/WildWestOnlineApp.swift b/App/WildWestOnlineApp.swift index 8fafc3384..09caa0515 100644 --- a/App/WildWestOnlineApp.swift +++ b/App/WildWestOnlineApp.swift @@ -5,7 +5,7 @@ // Created by Hugues Telolahy on 02/04/2023. // import SwiftUI -import AppBootstrap +import AppBuilder @main struct WildWestOnlineApp: App { diff --git a/Modules/.swiftlint.yml b/Modules/.swiftlint.yml index 3e0f57f94..d71f69215 100644 --- a/Modules/.swiftlint.yml +++ b/Modules/.swiftlint.yml @@ -102,7 +102,7 @@ opt_in_rules: - prefixed_toplevel_constant - private_action - private_outlet - - private_subject + # - private_subject # - private_swiftui_state # - prohibited_interface_builder - prohibited_super_call diff --git a/Modules/AppBootstrap/Sources/AppBuilder.swift b/Modules/AppBootstrap/Sources/AppBuilder.swift deleted file mode 100644 index 15d33618c..000000000 --- a/Modules/AppBootstrap/Sources/AppBuilder.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// AppBuilder.swift -// WildWestOnline -// -// Created by Hugues Telolahy on 31/10/2025. -// -import SwiftUI -import Redux -import AppFeature -import SettingsFeature -import PreferencesClient -import PreferencesClientLive -import GameFeature -import CardResources -import AudioClient -import AudioClientLive -import AppUI - -@MainActor -public enum AppBuilder { - public static func build() -> some View { - let preferencesClient = PreferencesClient.live() - - let settingsState = SettingsFeature.State.makeBuilder() - .withPlayersCount(preferencesClient.playersCount()) - .withActionDelayMilliSeconds(preferencesClient.actionDelayMilliSeconds()) - .withSimulation(preferencesClient.isSimulationEnabled()) - .withPreferredFigure(preferencesClient.preferredFigure()) - .withMusicVolume(preferencesClient.musicVolume()) - .build() - - let cardLibrary = AppFeature.State.CardLibrary( - cards: Cards.all, - deck: Deck.all, - specialSounds: SFX.specialSounds - ) - - let appState = AppFeature.State( - cardLibrary: cardLibrary, - navigation: .init(), - settings: settingsState - ) - - let audioClient = AudioClient.live() - Task { - await audioClient.setMusicVolume(preferencesClient.musicVolume()) - await audioClient.load(AudioClient.Sound.allSfx) - await audioClient.play(AudioClient.Sound.musicLoneRider) - } - - let queueModifierClient = QueueModifierClient.live(handlers: QueueModifiers.allHandlers) - - var dependencies = Dependencies() - dependencies.preferencesClient = preferencesClient - dependencies.audioClient = audioClient - dependencies.queueModifierClient = queueModifierClient - - let store = Store( - initialState: appState, - reducer: AppFeature.reducer, - dependencies: dependencies - ) - - return AppCoordinator { - store - } - } -} diff --git a/Modules/AppBuilder/Sources/AppBuilder.swift b/Modules/AppBuilder/Sources/AppBuilder.swift new file mode 100644 index 000000000..a4b7690cc --- /dev/null +++ b/Modules/AppBuilder/Sources/AppBuilder.swift @@ -0,0 +1,37 @@ +// +// AppBuilder.swift +// WildWestOnline +// +// Created by Hugues Telolahy on 31/10/2025. +// +import SwiftUI +import Redux +import AppFeature +import PreferencesClient +import PreferencesClientLive +import AudioClient +import AudioClientLive +import CardLibrary +import CardLibraryLive +import GameCore + +@MainActor +public enum AppBuilder { + public static func build() -> some View { + var dependencies = Dependencies() + dependencies.preferencesClient = PreferencesClient.live() + dependencies.audioClient = AudioClient.live() + dependencies.queueModifierClient = QueueModifierClient.live(handlers: QueueModifiers.allHandlers) + dependencies.cardLibrary = CardLibrary.live() + + let store = Store( + initialState: .init(), + reducer: AppFeature.reducer, + dependencies: dependencies + ) + + return AppView { + store + } + } +} diff --git a/Modules/AppFeature/Sources/AppFeature.swift b/Modules/AppFeature/Sources/AppFeature.swift index d7c9c1dcf..6dc6a76e7 100644 --- a/Modules/AppFeature/Sources/AppFeature.swift +++ b/Modules/AppFeature/Sources/AppFeature.swift @@ -6,111 +6,134 @@ // import Redux -import NavigationFeature +import HomeFeature import SettingsFeature -import GameFeature -import AudioClient -import PreferencesClient - -public typealias AppStore = Store +import GameSessionFeature public enum AppFeature { - /// Global app state - /// Organize State Structure Based on Data Types, Not Components - /// https://redux.js.org/style-guide/#organize-state-structure-based-on-data-types-not-components - public struct State: Codable, Equatable, Sendable { - public let cardLibrary: CardLibrary - public var navigation: AppNavigationFeature.State - public var settings: SettingsFeature.State - public var game: GameFeature.State? + public struct State: Equatable { + var path: [Destination] + var home: HomeFeature.State + var gameSession: GameSessionFeature.State + var settings: SettingsFeature.State? + + public enum Destination: Hashable { + case gameSession + } public init( - cardLibrary: CardLibrary, - navigation: AppNavigationFeature.State, - settings: SettingsFeature.State, - game: GameFeature.State? = nil + path: [Destination] = [], + home: HomeFeature.State = .init(), + gameSession: GameSessionFeature.State = .init(), + settings: SettingsFeature.State? = nil ) { - self.cardLibrary = cardLibrary - self.navigation = navigation + self.path = path + self.home = home self.settings = settings - self.game = game - } - - public struct CardLibrary: Codable, Equatable, Sendable { - public let cards: [Card] - public let deck: [String] - public let specialSounds: [Card.ActionName: [String: AudioClient.Sound]] - - public init( - cards: [Card] = [], - deck: [String] = [], - specialSounds: [Card.ActionName: [String: AudioClient.Sound]] = [:] - ) { - self.cards = cards - self.deck = deck - self.specialSounds = specialSounds - } + self.gameSession = gameSession } } public enum Action { - case start - case quit - case setGame(GameFeature.State) - case unsetGame - case navigation(AppNavigationFeature.Action) + // View + case setPath([State.Destination]) + case setSettingsPresented(Bool) + + // Internal + case home(HomeFeature.Action) case settings(SettingsFeature.Action) - case game(GameFeature.Action) + case gameSession(GameSessionFeature.Action) } public static var reducer: Reducer { combine( reducerMain, pullback( - GameFeature.reducer, - state: { globalState in - globalState.game != nil ? \.game! : nil + HomeFeature.reducer, + state: { _ in + \.home }, action: { globalAction in - if case let .game(localAction) = globalAction { + if case let .home(localAction) = globalAction { return localAction } return nil }, - embedAction: Action.game + embedAction: { + .home($0) + } ), pullback( - SettingsFeature.reducer, + GameSessionFeature.reducer, state: { _ in - \.settings + \.gameSession }, action: { globalAction in - if case let .settings(localAction) = globalAction { + if case let .gameSession(localAction) = globalAction { return localAction } return nil }, - embedAction: Action.settings + embedAction: { + .gameSession($0) + } ), pullback( - AppNavigationFeature.reducer, - state: { _ in - \.navigation + SettingsFeature.reducer, + state: { + $0.settings != nil ? \.settings! : nil }, action: { globalAction in - if case let .navigation(localAction) = globalAction { + if case let .settings(localAction) = globalAction { return localAction } return nil }, - embedAction: Action.navigation - ), - pullback( - reducerSound, - state: { _ in \.self }, - action: { $0 }, - embedAction: \.self + embedAction: { + .settings($0) + } ) ) } + + private static func reducerMain( + into state: inout State, + action: Action, + dependencies: Dependencies + ) -> Effect { + switch action { + case .setPath(let path): + state.path = path + return .none + + case .setSettingsPresented(let presented): + if presented { + state.settings = .init() + } else { + state.settings = nil + } + return .none + + case .home(.delegate(.settings)): + return .run { .setSettingsPresented(true) } + + case .home(.delegate(.play)): + return .run { .setPath([.gameSession]) } + + case .home: + return .none + + case .settings: + return .none + + case .gameSession(.delegate(.settings)): + return .run { .setSettingsPresented(true) } + + case .gameSession(.delegate(.quit)): + return .run { .setPath([]) } + + case .gameSession: + return .none + } + } } diff --git a/Modules/AppFeature/Sources/AppReducerMain.swift b/Modules/AppFeature/Sources/AppReducerMain.swift deleted file mode 100644 index 68329bc3c..000000000 --- a/Modules/AppFeature/Sources/AppReducerMain.swift +++ /dev/null @@ -1,70 +0,0 @@ -// -// AppReducerMain.swift -// WildWestOnline -// -// Created by Hugues Telolahy on 31/10/2025. -// -import Redux -import GameFeature -import SettingsFeature - -extension AppFeature { - static func reducerMain( - into state: inout State, - action: Action, - dependencies: Dependencies - ) -> Effect { - switch action { - case .start: - let state = state - return .group([ - .run { - .setGame(.create(settings: state.settings, cardLibrary: state.cardLibrary)) - }, - .run { - .navigation(.push(.game)) - } - ]) - - case .quit: - return .group([ - .run { - .unsetGame - }, - .run { - .navigation(.pop) - } - ]) - - case .setGame(let game): - state.game = game - - case .unsetGame: - state.game = nil - - case .navigation: - break - - case .settings: - break - - case .game: - break - } - - return .none - } -} - -private extension GameFeature.State { - static func create(settings: SettingsFeature.State, cardLibrary: AppFeature.State.CardLibrary) -> Self { - GameSetup.buildGame( - playersCount: settings.playersCount, - cards: cardLibrary.cards, - deck: cardLibrary.deck, - actionDelayMilliSeconds: settings.actionDelayMilliSeconds, - preferredFigure: settings.preferredFigure, - playModeSetup: settings.simulation ? .allAuto : .oneManual - ) - } -} diff --git a/Modules/AppFeature/Sources/AppReducerSound.swift b/Modules/AppFeature/Sources/AppReducerSound.swift deleted file mode 100644 index 5fcad8dcd..000000000 --- a/Modules/AppFeature/Sources/AppReducerSound.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// GameReducerSound.swift -// WildWestOnline -// -// Created by Hugues Telolahy on 31/10/2025. -// -import Redux -import AudioClient - -extension AppFeature { - static func reducerSound( - into state: inout State, - action: Action, - dependencies: Dependencies - ) -> Effect { - switch action { - case .game(let gameAction): - let soundMatcher = SoundMatcher(specialSounds: state.cardLibrary.specialSounds) - if let sfx = soundMatcher.sfx(on: gameAction) { - let playFunc = dependencies.audioClient.play - Task { - await playFunc(sfx) - } - } - - case .navigation(.push(.game)): - let pauseFunc = dependencies.audioClient.pause - Task { - await pauseFunc(.musicLoneRider) - } - - case .navigation(.pop): - let resumeFunc = dependencies.audioClient.resume - Task { - await resumeFunc(.musicLoneRider) - } - - case .settings(.updateMusicVolume(let value)): - let setMusicVolumeFunc = dependencies.audioClient.setMusicVolume - Task { - await setMusicVolumeFunc(value) - } - - default: - break - } - - return .none - } -} diff --git a/Modules/AppFeature/Sources/AppView.swift b/Modules/AppFeature/Sources/AppView.swift new file mode 100644 index 000000000..f8524cfbf --- /dev/null +++ b/Modules/AppFeature/Sources/AppView.swift @@ -0,0 +1,101 @@ +// +// AppView.swift +// WildWestOnline +// +// Created by Stephano Hugues TELOLAHY on 13/09/2024. +// + +import SwiftUI +import Redux +import SettingsFeature +import GameSessionFeature +import HomeFeature + +public struct AppView: View { + public typealias ViewStore = Store + + @StateObject private var store: ViewStore + @State private var path: [AppFeature.State.Destination] = [] + @State private var isSettingsPresented: Bool = false + + @Environment(\.theme) private var theme + + public init(store: @escaping () -> ViewStore) { + // SwiftUI ensures that the following initialization uses the + // closure only once during the lifetime of the view. + _store = StateObject(wrappedValue: store()) + } + + public var body: some View { + NavigationStack(path: $path) { + HomeView { + store.projection( + state: \.home, + action: { .home($0) } + ) + } + .navigationDestination(for: AppFeature.State.Destination.self) { + viewForDestination($0) + } + } + .sheet(isPresented: $isSettingsPresented) { + SettingsView { + store.projection( + state: \.settings, + action: { .settings($0) } + ) + } + } + // Fix Error `Update NavigationAuthority bound path tried to update multiple times per frame` + .onReceive(store.$state) { state in + let newPath = state.path + if newPath != path { + path = newPath + } + + let newIsSettingsPresented = state.settings != nil + if newIsSettingsPresented != isSettingsPresented { + isSettingsPresented = newIsSettingsPresented + } + } + .onChange(of: path) { _, newPath in + if newPath != store.state.path { + Task { + await store.dispatch(.setPath(newPath)) + } + } + } + .onChange(of: isSettingsPresented) { _, newIsSettingsPresented in + let isSettingsPresented = store.state.settings != nil + if newIsSettingsPresented != isSettingsPresented { + Task { + await store.dispatch(.setSettingsPresented(newIsSettingsPresented)) + } + } + } + .onReceive(store.dispatchedAction) { event in + print(event) + } + .accentColor(theme.colorAccent) + } + + @ViewBuilder private func viewForDestination(_ destination: AppFeature.State.Destination) -> some View { + switch destination { + case .gameSession: + GameSessionView { + store.projection( + state: \.gameSession, + action: { .gameSession($0) } + ) + } + } + } +} + +#Preview { + AppView { + .init( + initialState: .init() + ) + } +} diff --git a/Modules/AppFeature/Tests/AppFeatureTest.swift b/Modules/AppFeature/Tests/AppFeatureTest.swift index b8abe72e4..12da00d44 100644 --- a/Modules/AppFeature/Tests/AppFeatureTest.swift +++ b/Modules/AppFeature/Tests/AppFeatureTest.swift @@ -6,66 +6,6 @@ // import Testing -import AppFeature -import GameFeature -import Redux -import SettingsFeature struct AppFeatureTest { - private func createAppStore(initialState: AppFeature.State) async -> AppStore { - await .init( - initialState: initialState, - reducer: AppFeature.reducer - ) - } - - @Test func app_whenStartedGame_shouldShowGameScreen_AndCreateGame() async throws { - // Given - let cards = (1...100).map { - Card( - name: "c\($0)", - type: .figure, - effects: [ - .init( - trigger: .permanent, - action: .setMaxHealth, - amount: 1 - ) - ] - ) - } - let state = AppFeature.State( - cardLibrary: .init(cards: cards), - navigation: .init(), - settings: SettingsFeature.State.makeBuilder().withPlayersCount(5).build() - ) - let sut = await createAppStore(initialState: state) - - // When - let action = AppFeature.Action.start - await sut.dispatch(action) - - // Then - await #expect(sut.state.navigation.path == [.game]) - await #expect(sut.state.game != nil) - } - - @Test func app_whenFinishedGame_shouldBackToHomeScreen_AndDeleteGame() async throws { - // Given - let state = AppFeature.State( - cardLibrary: .init(), - navigation: .init(path: [.game]), - settings: SettingsFeature.State.makeBuilder().build(), - game: GameFeature.State.makeBuilder().build() - ) - let sut = await createAppStore(initialState: state) - - // When - let action = AppFeature.Action.quit - await sut.dispatch(action) - - // Then - await #expect(sut.state.navigation.path.isEmpty) - await #expect(sut.state.game == nil) - } } diff --git a/Modules/AppUI/Sources/AppCoordinator.swift b/Modules/AppUI/Sources/AppCoordinator.swift deleted file mode 100644 index 90d57635b..000000000 --- a/Modules/AppUI/Sources/AppCoordinator.swift +++ /dev/null @@ -1,96 +0,0 @@ -// -// AppCoordinator.swift -// WildWestOnline -// -// Created by Stephano Hugues TELOLAHY on 13/09/2024. -// - -import SwiftUI -import NavigationFeature -import Redux -import AppFeature -import SettingsUI -import HomeUI -import GameUI - -public struct AppCoordinator: View { - @StateObject private var store: AppStore - @State private var path: [AppNavigationFeature.State.Destination] = [] - @State private var settingsSheetPresented: Bool = false - - @Environment(\.theme) private var theme - - public init(store: @escaping () -> AppStore) { - // SwiftUI ensures that the following initialization uses the - // closure only once during the lifetime of the view. - _store = StateObject(wrappedValue: store()) - } - - public var body: some View { - NavigationStack(path: $path) { - HomeView { store.projection(state: HomeView.ViewState.init, action: \.self) } - .navigationDestination(for: AppNavigationFeature.State.Destination.self) { - viewForDestination($0) - } - } - .sheet(isPresented: $settingsSheetPresented) { - SettingsCoordinator(store: store) - } - // Fix Error `Update NavigationAuthority bound path tried to update multiple times per frame` - .onReceive(store.$state) { state in - path = state.navigation.path - settingsSheetPresented = state.navigation.settingsSheet != nil - } - .onChange(of: path) { _, newPath in - guard newPath != store.state.navigation.path else { - return - } - - Task { - await store.dispatch(.navigation(.setPath(newPath))) - } - } - .onChange(of: settingsSheetPresented) { _, isPresented in - guard isPresented != (store.state.navigation.settingsSheet != nil) else { - return - } - - Task { - if isPresented { - await store.dispatch(.navigation(.presentSettingsSheet)) - } else { - await store.dispatch(.navigation(.dismissSettingsSheet)) - } - } - } - .onReceive(store.dispatchedAction) { event in - print(event) - } - .accentColor(theme.colorAccent) - } - - @ViewBuilder private func viewForDestination(_ destination: AppNavigationFeature.State.Destination) -> some View { - switch destination { - case .game: - GameView { store.projection(state: GameView.ViewState.init, action: \.self) } - } - } -} - -#Preview { - AppCoordinator { - .init( - initialState: .previewState - ) - } -} - -private extension AppFeature.State { - static var previewState: Self { - .init( - cardLibrary: .init(), - navigation: .init(), - settings: .makeBuilder().build() - ) - } -} diff --git a/Modules/AppUI/Tests/AppCoordinatorTest.swift b/Modules/AppUI/Tests/AppCoordinatorTest.swift deleted file mode 100644 index 8e400221f..000000000 --- a/Modules/AppUI/Tests/AppCoordinatorTest.swift +++ /dev/null @@ -1,11 +0,0 @@ -// -// AppCoordinatorTest.swift -// WildWestOnline -// -// Created by Hugues Stéphano TELOLAHY on 30/12/2024. -// - -import Testing - -struct AppCoordinatorTest { -} diff --git a/Modules/CardLibrary/Sources/CardsLibrary.swift b/Modules/CardLibrary/Sources/CardsLibrary.swift new file mode 100644 index 000000000..2003f3d81 --- /dev/null +++ b/Modules/CardLibrary/Sources/CardsLibrary.swift @@ -0,0 +1,24 @@ +// +// CardLibrary.swift +// WildWestOnline +// +// Created by Hugues Stéphano TELOLAHY on 03/12/2025. +// +import AudioClient +import GameCore + +public struct CardLibrary { + public var cards: () -> [Card] + public var deck: () -> [String] + public var specialSounds: () -> [Card.ActionName: [String: AudioClient.Sound]] + + public init( + cards: @escaping () -> [Card], + deck: @escaping () -> [String], + specialSounds: @escaping () -> [Card.ActionName: [String: AudioClient.Sound]] + ) { + self.cards = cards + self.deck = deck + self.specialSounds = specialSounds + } +} diff --git a/Modules/CardLibrary/Sources/CardsLibraryKey.swift b/Modules/CardLibrary/Sources/CardsLibraryKey.swift new file mode 100644 index 000000000..964850415 --- /dev/null +++ b/Modules/CardLibrary/Sources/CardsLibraryKey.swift @@ -0,0 +1,29 @@ +// +// CardLibraryKey.swift +// WildWestOnline +// +// Created by Hugues Stéphano TELOLAHY on 03/12/2025. +// + +import Redux + +public extension Dependencies { + var cardLibrary: CardLibrary { + get { self[CardLibraryKey.self] } + set { self[CardLibraryKey.self] = newValue } + } +} + +private enum CardLibraryKey: DependencyKey { + nonisolated(unsafe) static let defaultValue: CardLibrary = .noop +} + +private extension CardLibrary { + static var noop: Self { + .init( + cards: { [] }, + deck: { [] }, + specialSounds: { [:] } + ) + } +} diff --git a/Modules/CardResources/Sources/Cards.swift b/Modules/CardLibraryLive/Sources/Cards.swift similarity index 98% rename from Modules/CardResources/Sources/Cards.swift rename to Modules/CardLibraryLive/Sources/Cards.swift index 8fac11a41..b28c8af77 100644 --- a/Modules/CardResources/Sources/Cards.swift +++ b/Modules/CardLibraryLive/Sources/Cards.swift @@ -4,12 +4,14 @@ // Created by Hugues Telolahy on 28/10/2024. // // swiftlint:disable file_length line_length -import GameFeature + +import GameCore +import CardResources /// BANG! THE BULLET /// https://bang.dvgiochi.com/cardslist.php?id=2#q_result -public enum Cards { - public static let all: [Card] = [ +enum Cards { + static let all: [Card] = [ // MARK: - Aura .endTurn, .playMissedOnShot, @@ -1372,3 +1374,17 @@ private extension Array where Element == Card.Effect { ] } } + +private extension String { + static let playMissedOnShot = "playMissedOnShot" + static let discardExcessHandOnTurnEnded = "discardExcessHandOnTurnEnded" + static let draw2CardsOnTurnStarted = "draw2CardsOnTurnStarted" + static let nextTurnOnTurnEnded = "nextTurnOnTurnEnded" + static let eliminateOnDamagedLethal = "eliminateOnDamagedLethal" + static let endGameOnEliminated = "endGameOnEliminated" + static let discardAllCardsOnEliminated = "discardAllCardsOnEliminated" + static let nextTurnOnEliminated = "nextTurnOnEliminated" + static let discardBeerOnDamagedLethal = "discardBeerOnDamagedLethal" + static let draw3CardsOnEliminating = "draw3CardsOnEliminating" + static let discardEquipedWeaponOnPrePlayed = "discardEquipedWeaponOnPrePlayed" +} diff --git a/Modules/CardLibraryLive/Sources/CardsLibrary+Live.swift b/Modules/CardLibraryLive/Sources/CardsLibrary+Live.swift new file mode 100644 index 000000000..96778a8be --- /dev/null +++ b/Modules/CardLibraryLive/Sources/CardsLibrary+Live.swift @@ -0,0 +1,17 @@ +// +// CardLibrary+Live.swift +// WildWestOnline +// +// Created by Hugues Stéphano TELOLAHY on 03/12/2025. +// +import CardLibrary + +public extension CardLibrary { + static func live() -> Self { + .init( + cards: { Cards.all }, + deck: { Deck.all }, + specialSounds: { SFX.specialSounds } + ) + } +} diff --git a/Modules/CardResources/Sources/Deck.swift b/Modules/CardLibraryLive/Sources/Deck.swift similarity index 96% rename from Modules/CardResources/Sources/Deck.swift rename to Modules/CardLibraryLive/Sources/Deck.swift index 210cd0ae5..494549b27 100644 --- a/Modules/CardResources/Sources/Deck.swift +++ b/Modules/CardLibraryLive/Sources/Deck.swift @@ -5,8 +5,8 @@ // Created by Hugues Stéphano TELOLAHY on 24/11/2024. // -public enum Deck { - public static let all: [String] = Self.flatten(bang) + Self.flatten(dodgeCity) +enum Deck { + static let all: [String] = Self.flatten(bang) + Self.flatten(dodgeCity) static let bang: [String: [String]] = [ .barrel: ["Q♠️", "K♠️"], diff --git a/Modules/CardResources/Sources/Draft.swift b/Modules/CardLibraryLive/Sources/Draft.swift similarity index 100% rename from Modules/CardResources/Sources/Draft.swift rename to Modules/CardLibraryLive/Sources/Draft.swift diff --git a/Modules/CardResources/Sources/Modifiers/IgnoreLimitPerTurn.swift b/Modules/CardLibraryLive/Sources/Modifiers/IgnoreLimitPerTurn.swift similarity index 80% rename from Modules/CardResources/Sources/Modifiers/IgnoreLimitPerTurn.swift rename to Modules/CardLibraryLive/Sources/Modifiers/IgnoreLimitPerTurn.swift index 7210235e4..59be029ce 100644 --- a/Modules/CardResources/Sources/Modifiers/IgnoreLimitPerTurn.swift +++ b/Modules/CardLibraryLive/Sources/Modifiers/IgnoreLimitPerTurn.swift @@ -5,14 +5,14 @@ // Created by Hugues Stéphano TELOLAHY on 23/11/2025. // -@testable import GameFeature +@testable import GameCore -extension GameFeature.Action.QueueModifier { - static let ignoreLimitPerTurn = GameFeature.Action.QueueModifier(rawValue: "ignoreLimitPerTurn") +extension Card.QueueModifier { + static let ignoreLimitPerTurn = Card.QueueModifier(rawValue: "ignoreLimitPerTurn") } struct IgnoreLimitPerTurn: QueueModifierHandler { - static let id = GameFeature.Action.QueueModifier.ignoreLimitPerTurn + static let id = Card.QueueModifier.ignoreLimitPerTurn static func apply(_ action: GameFeature.Action, state: GameFeature.State) throws(GameFeature.Error) -> [GameFeature.Action] { guard let playIndex = state.queue.firstIndex(where: { diff --git a/Modules/CardResources/Sources/Modifiers/IncrementCardsPerTurn.swift b/Modules/CardLibraryLive/Sources/Modifiers/IncrementCardsPerTurn.swift similarity index 80% rename from Modules/CardResources/Sources/Modifiers/IncrementCardsPerTurn.swift rename to Modules/CardLibraryLive/Sources/Modifiers/IncrementCardsPerTurn.swift index a536720e9..ba16aa9fd 100644 --- a/Modules/CardResources/Sources/Modifiers/IncrementCardsPerTurn.swift +++ b/Modules/CardLibraryLive/Sources/Modifiers/IncrementCardsPerTurn.swift @@ -5,14 +5,14 @@ // Created by Hugues Stéphano TELOLAHY on 23/11/2025. // -@testable import GameFeature +@testable import GameCore -extension GameFeature.Action.QueueModifier { - static let incrementCardsPerTurn = GameFeature.Action.QueueModifier(rawValue: "incrementCardsPerTurn") +extension Card.QueueModifier { + static let incrementCardsPerTurn = Card.QueueModifier(rawValue: "incrementCardsPerTurn") } struct IncrementCardsPerTurn: QueueModifierHandler { - static let id = GameFeature.Action.QueueModifier.incrementCardsPerTurn + static let id = Card.QueueModifier.incrementCardsPerTurn static func apply(_ action: GameFeature.Action, state: GameFeature.State) throws(GameFeature.Error) -> [GameFeature.Action] { guard let amount = action.amount else { fatalError("Missing amount") } diff --git a/Modules/CardResources/Sources/Modifiers/IncrementRequiredMisses.swift b/Modules/CardLibraryLive/Sources/Modifiers/IncrementRequiredMisses.swift similarity index 79% rename from Modules/CardResources/Sources/Modifiers/IncrementRequiredMisses.swift rename to Modules/CardLibraryLive/Sources/Modifiers/IncrementRequiredMisses.swift index b09bbb9f5..d8e4395a5 100644 --- a/Modules/CardResources/Sources/Modifiers/IncrementRequiredMisses.swift +++ b/Modules/CardLibraryLive/Sources/Modifiers/IncrementRequiredMisses.swift @@ -5,14 +5,14 @@ // Created by Hugues Stéphano TELOLAHY on 23/11/2025. // -@testable import GameFeature +@testable import GameCore -extension GameFeature.Action.QueueModifier { - static let incrementRequiredMisses = GameFeature.Action.QueueModifier(rawValue: "incrementRequiredMisses") +extension Card.QueueModifier { + static let incrementRequiredMisses = Card.QueueModifier(rawValue: "incrementRequiredMisses") } struct IncrementRequiredMisses: QueueModifierHandler { - static let id = GameFeature.Action.QueueModifier.incrementRequiredMisses + static let id = Card.QueueModifier.incrementRequiredMisses static func apply(_ action: GameFeature.Action, state: GameFeature.State) throws(GameFeature.Error) -> [GameFeature.Action] { guard let amount = action.amount else { fatalError("Missing amount") } diff --git a/Modules/CardLibraryLive/Sources/Modifiers/QueueModifiers.swift b/Modules/CardLibraryLive/Sources/Modifiers/QueueModifiers.swift new file mode 100644 index 000000000..f7fc62c7d --- /dev/null +++ b/Modules/CardLibraryLive/Sources/Modifiers/QueueModifiers.swift @@ -0,0 +1,17 @@ +// +// QueueModifiers.swift +// WildWestOnline +// +// Created by Hugues Stéphano TELOLAHY on 23/11/2025. +// +import GameCore + +public enum QueueModifiers { + public static var allHandlers: [QueueModifierHandler.Type] { + [ + IncrementCardsPerTurn.self, + IncrementRequiredMisses.self, + IgnoreLimitPerTurn.self, + ] + } +} diff --git a/Modules/CardResources/Sources/SFX.swift b/Modules/CardLibraryLive/Sources/SFX.swift similarity index 76% rename from Modules/CardResources/Sources/SFX.swift rename to Modules/CardLibraryLive/Sources/SFX.swift index 3b35407d8..d0ae2435d 100644 --- a/Modules/CardResources/Sources/SFX.swift +++ b/Modules/CardLibraryLive/Sources/SFX.swift @@ -5,11 +5,11 @@ // Created by Hugues Stéphano TELOLAHY on 15/11/2025. // -import GameFeature import AudioClient +import GameCore -public enum SFX { - public static let specialSounds: [Card.ActionName: [String: AudioClient.Sound]] = [ +enum SFX { + static let specialSounds: [Card.ActionName: [String: AudioClient.Sound]] = [ .play: [ .stagecoach: .sfxHorseGalloping, .wellsFargo: .sfxHorseGalloping, diff --git a/Modules/CardResources/Tests/AIStrategyTest.swift b/Modules/CardLibraryLive/Tests/AIStrategyTest.swift similarity index 98% rename from Modules/CardResources/Tests/AIStrategyTest.swift rename to Modules/CardLibraryLive/Tests/AIStrategyTest.swift index 865a8a18a..443260fa9 100644 --- a/Modules/CardResources/Tests/AIStrategyTest.swift +++ b/Modules/CardLibraryLive/Tests/AIStrategyTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore import CardResources struct AIStrategyTest { diff --git a/Modules/CardResources/Tests/ActivatePlayableCardsTest.swift b/Modules/CardLibraryLive/Tests/ActivatePlayableCardsTest.swift similarity index 98% rename from Modules/CardResources/Tests/ActivatePlayableCardsTest.swift rename to Modules/CardLibraryLive/Tests/ActivatePlayableCardsTest.swift index cf5102c37..f52854502 100644 --- a/Modules/CardResources/Tests/ActivatePlayableCardsTest.swift +++ b/Modules/CardLibraryLive/Tests/ActivatePlayableCardsTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore import CardResources struct ActivatePlayableCardsTest { diff --git a/Modules/CardResources/Tests/Bang/Collectible/BangTest.swift b/Modules/CardLibraryLive/Tests/Bang/Collectible/BangTest.swift similarity index 99% rename from Modules/CardResources/Tests/Bang/Collectible/BangTest.swift rename to Modules/CardLibraryLive/Tests/Bang/Collectible/BangTest.swift index 470750c9c..3b2007f93 100644 --- a/Modules/CardResources/Tests/Bang/Collectible/BangTest.swift +++ b/Modules/CardLibraryLive/Tests/Bang/Collectible/BangTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct BangTest { @Test func play_shouldDeal1Damage() async throws { diff --git a/Modules/CardResources/Tests/Bang/Collectible/BarrelTest.swift b/Modules/CardLibraryLive/Tests/Bang/Collectible/BarrelTest.swift similarity index 99% rename from Modules/CardResources/Tests/Bang/Collectible/BarrelTest.swift rename to Modules/CardLibraryLive/Tests/Bang/Collectible/BarrelTest.swift index 37dadf1ef..3d765219f 100644 --- a/Modules/CardResources/Tests/Bang/Collectible/BarrelTest.swift +++ b/Modules/CardLibraryLive/Tests/Bang/Collectible/BarrelTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct BarrelTest { @Test func playingBarrel_shouldEquip() async throws { diff --git a/Modules/CardResources/Tests/Bang/Collectible/BeerTest.swift b/Modules/CardLibraryLive/Tests/Bang/Collectible/BeerTest.swift similarity index 99% rename from Modules/CardResources/Tests/Bang/Collectible/BeerTest.swift rename to Modules/CardLibraryLive/Tests/Bang/Collectible/BeerTest.swift index 754c75c4a..da35618cc 100644 --- a/Modules/CardResources/Tests/Bang/Collectible/BeerTest.swift +++ b/Modules/CardLibraryLive/Tests/Bang/Collectible/BeerTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct BeerTest { @Test func play_beingDamaged_shouldHealOneLifePoint() async throws { diff --git a/Modules/CardResources/Tests/Bang/Collectible/CatBalouTest.swift b/Modules/CardLibraryLive/Tests/Bang/Collectible/CatBalouTest.swift similarity index 99% rename from Modules/CardResources/Tests/Bang/Collectible/CatBalouTest.swift rename to Modules/CardLibraryLive/Tests/Bang/Collectible/CatBalouTest.swift index 24f6efae2..9b4b3b0fc 100644 --- a/Modules/CardResources/Tests/Bang/Collectible/CatBalouTest.swift +++ b/Modules/CardLibraryLive/Tests/Bang/Collectible/CatBalouTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct CatBalouTest { @Test func play_targetHavingHandCards_shouldChooseOneHandCard() async throws { diff --git a/Modules/CardResources/Tests/Bang/Collectible/DuelTest.swift b/Modules/CardLibraryLive/Tests/Bang/Collectible/DuelTest.swift similarity index 99% rename from Modules/CardResources/Tests/Bang/Collectible/DuelTest.swift rename to Modules/CardLibraryLive/Tests/Bang/Collectible/DuelTest.swift index 94f4eb45b..1b8b0d8ec 100644 --- a/Modules/CardResources/Tests/Bang/Collectible/DuelTest.swift +++ b/Modules/CardLibraryLive/Tests/Bang/Collectible/DuelTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct DuelTests { // Given diff --git a/Modules/CardResources/Tests/Bang/Collectible/DynamiteTest.swift b/Modules/CardLibraryLive/Tests/Bang/Collectible/DynamiteTest.swift similarity index 99% rename from Modules/CardResources/Tests/Bang/Collectible/DynamiteTest.swift rename to Modules/CardLibraryLive/Tests/Bang/Collectible/DynamiteTest.swift index cceecb7c6..a80f1bf60 100644 --- a/Modules/CardResources/Tests/Bang/Collectible/DynamiteTest.swift +++ b/Modules/CardLibraryLive/Tests/Bang/Collectible/DynamiteTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct DynamiteTest { @Test func play_shouldEquip() async throws { diff --git a/Modules/CardResources/Tests/Bang/Collectible/GatlingTest.swift b/Modules/CardLibraryLive/Tests/Bang/Collectible/GatlingTest.swift similarity index 97% rename from Modules/CardResources/Tests/Bang/Collectible/GatlingTest.swift rename to Modules/CardLibraryLive/Tests/Bang/Collectible/GatlingTest.swift index ee9e39336..5900be5a9 100644 --- a/Modules/CardResources/Tests/Bang/Collectible/GatlingTest.swift +++ b/Modules/CardLibraryLive/Tests/Bang/Collectible/GatlingTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct GatlingTest { @Test func play_shouldDamageOtherPlayers() async throws { diff --git a/Modules/CardResources/Tests/Bang/Collectible/GeneralStoreTest.swift b/Modules/CardLibraryLive/Tests/Bang/Collectible/GeneralStoreTest.swift similarity index 98% rename from Modules/CardResources/Tests/Bang/Collectible/GeneralStoreTest.swift rename to Modules/CardLibraryLive/Tests/Bang/Collectible/GeneralStoreTest.swift index fb3c6a084..c8edca427 100644 --- a/Modules/CardResources/Tests/Bang/Collectible/GeneralStoreTest.swift +++ b/Modules/CardLibraryLive/Tests/Bang/Collectible/GeneralStoreTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct GeneralStoreTests { @Test func play_shouldAllowEachPlayerToChooseACard() async throws { diff --git a/Modules/CardResources/Tests/Bang/Collectible/IndiansTest.swift b/Modules/CardLibraryLive/Tests/Bang/Collectible/IndiansTest.swift similarity index 98% rename from Modules/CardResources/Tests/Bang/Collectible/IndiansTest.swift rename to Modules/CardLibraryLive/Tests/Bang/Collectible/IndiansTest.swift index e647af39c..4ce7fc983 100644 --- a/Modules/CardResources/Tests/Bang/Collectible/IndiansTest.swift +++ b/Modules/CardLibraryLive/Tests/Bang/Collectible/IndiansTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct IndiansTest { @Test func play_shouldAllowEachPlayerToCounterOrPass() async throws { diff --git a/Modules/CardResources/Tests/Bang/Collectible/JailTest.swift b/Modules/CardLibraryLive/Tests/Bang/Collectible/JailTest.swift similarity index 99% rename from Modules/CardResources/Tests/Bang/Collectible/JailTest.swift rename to Modules/CardLibraryLive/Tests/Bang/Collectible/JailTest.swift index ebf9c35c1..86f5a4539 100644 --- a/Modules/CardResources/Tests/Bang/Collectible/JailTest.swift +++ b/Modules/CardLibraryLive/Tests/Bang/Collectible/JailTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct JailTest { @Test func playAgainstAnyPlayer_shouldHandicap() async throws { diff --git a/Modules/CardResources/Tests/Bang/Collectible/MissedTest.swift b/Modules/CardLibraryLive/Tests/Bang/Collectible/MissedTest.swift similarity index 99% rename from Modules/CardResources/Tests/Bang/Collectible/MissedTest.swift rename to Modules/CardLibraryLive/Tests/Bang/Collectible/MissedTest.swift index 6089b199f..c4cd717d0 100644 --- a/Modules/CardResources/Tests/Bang/Collectible/MissedTest.swift +++ b/Modules/CardLibraryLive/Tests/Bang/Collectible/MissedTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct MissedTest { @Test func beingShot_discardingMissed_shouldCounter() async throws { diff --git a/Modules/CardResources/Tests/Bang/Collectible/MustangTests.swift b/Modules/CardLibraryLive/Tests/Bang/Collectible/MustangTests.swift similarity index 98% rename from Modules/CardResources/Tests/Bang/Collectible/MustangTests.swift rename to Modules/CardLibraryLive/Tests/Bang/Collectible/MustangTests.swift index 1eef79261..7de38eb23 100644 --- a/Modules/CardResources/Tests/Bang/Collectible/MustangTests.swift +++ b/Modules/CardLibraryLive/Tests/Bang/Collectible/MustangTests.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct MustangTests { @Test func play_shouldEquipAndIncreaseRemoteness() async throws { diff --git a/Modules/CardResources/Tests/Bang/Collectible/PanicTest.swift b/Modules/CardLibraryLive/Tests/Bang/Collectible/PanicTest.swift similarity index 99% rename from Modules/CardResources/Tests/Bang/Collectible/PanicTest.swift rename to Modules/CardLibraryLive/Tests/Bang/Collectible/PanicTest.swift index f6a789b8b..8dbcaaffb 100644 --- a/Modules/CardResources/Tests/Bang/Collectible/PanicTest.swift +++ b/Modules/CardLibraryLive/Tests/Bang/Collectible/PanicTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct PanicTest { @Test func play_targetHavingHandCards_shouldChooseOneHandCard() async throws { diff --git a/Modules/CardResources/Tests/Bang/Collectible/RemingtonTest.swift b/Modules/CardLibraryLive/Tests/Bang/Collectible/RemingtonTest.swift similarity index 97% rename from Modules/CardResources/Tests/Bang/Collectible/RemingtonTest.swift rename to Modules/CardLibraryLive/Tests/Bang/Collectible/RemingtonTest.swift index 3bf972fca..0ff661af1 100644 --- a/Modules/CardResources/Tests/Bang/Collectible/RemingtonTest.swift +++ b/Modules/CardLibraryLive/Tests/Bang/Collectible/RemingtonTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct RemingtonTest { @Test func playRemington_shouldEquipAndSetWeapon() async throws { diff --git a/Modules/CardResources/Tests/Bang/Collectible/RevCarabineTest.swift b/Modules/CardLibraryLive/Tests/Bang/Collectible/RevCarabineTest.swift similarity index 97% rename from Modules/CardResources/Tests/Bang/Collectible/RevCarabineTest.swift rename to Modules/CardLibraryLive/Tests/Bang/Collectible/RevCarabineTest.swift index cd2c72683..572a2943d 100644 --- a/Modules/CardResources/Tests/Bang/Collectible/RevCarabineTest.swift +++ b/Modules/CardLibraryLive/Tests/Bang/Collectible/RevCarabineTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct RevCarabineTest { @Test func playRevCarabine_shouldEquipAndSetWeapon() async throws { diff --git a/Modules/CardResources/Tests/Bang/Collectible/SaloonTest.swift b/Modules/CardLibraryLive/Tests/Bang/Collectible/SaloonTest.swift similarity index 99% rename from Modules/CardResources/Tests/Bang/Collectible/SaloonTest.swift rename to Modules/CardLibraryLive/Tests/Bang/Collectible/SaloonTest.swift index 683669540..2027e27ab 100644 --- a/Modules/CardResources/Tests/Bang/Collectible/SaloonTest.swift +++ b/Modules/CardLibraryLive/Tests/Bang/Collectible/SaloonTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct SaloonTest { @Test func play_withSelfDamaged_shouldHealOneLifePoint() async throws { diff --git a/Modules/CardResources/Tests/Bang/Collectible/SchofieldTest.swift b/Modules/CardLibraryLive/Tests/Bang/Collectible/SchofieldTest.swift similarity index 99% rename from Modules/CardResources/Tests/Bang/Collectible/SchofieldTest.swift rename to Modules/CardLibraryLive/Tests/Bang/Collectible/SchofieldTest.swift index f8de9a732..9e832328a 100644 --- a/Modules/CardResources/Tests/Bang/Collectible/SchofieldTest.swift +++ b/Modules/CardLibraryLive/Tests/Bang/Collectible/SchofieldTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct SchofieldTest { @Test func play_shouldSetWeapon() async throws { diff --git a/Modules/CardResources/Tests/Bang/Collectible/ScopeTest.swift b/Modules/CardLibraryLive/Tests/Bang/Collectible/ScopeTest.swift similarity index 98% rename from Modules/CardResources/Tests/Bang/Collectible/ScopeTest.swift rename to Modules/CardLibraryLive/Tests/Bang/Collectible/ScopeTest.swift index f81d2aa5b..0d6e1dad9 100644 --- a/Modules/CardResources/Tests/Bang/Collectible/ScopeTest.swift +++ b/Modules/CardLibraryLive/Tests/Bang/Collectible/ScopeTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct ScopeTest { @Test func play_shouldEquipAndIncreaseMagnifying() async throws { diff --git a/Modules/CardResources/Tests/Bang/Collectible/StagecoachTest.swift b/Modules/CardLibraryLive/Tests/Bang/Collectible/StagecoachTest.swift similarity index 97% rename from Modules/CardResources/Tests/Bang/Collectible/StagecoachTest.swift rename to Modules/CardLibraryLive/Tests/Bang/Collectible/StagecoachTest.swift index b0e84f4c0..112fcd351 100644 --- a/Modules/CardResources/Tests/Bang/Collectible/StagecoachTest.swift +++ b/Modules/CardLibraryLive/Tests/Bang/Collectible/StagecoachTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct StagecoachTest { @Test func play_shouldDraw2Cards() async throws { diff --git a/Modules/CardResources/Tests/Bang/Collectible/VolcanicTest.swift b/Modules/CardLibraryLive/Tests/Bang/Collectible/VolcanicTest.swift similarity index 98% rename from Modules/CardResources/Tests/Bang/Collectible/VolcanicTest.swift rename to Modules/CardLibraryLive/Tests/Bang/Collectible/VolcanicTest.swift index 40d9eee9a..19eb853a0 100644 --- a/Modules/CardResources/Tests/Bang/Collectible/VolcanicTest.swift +++ b/Modules/CardLibraryLive/Tests/Bang/Collectible/VolcanicTest.swift @@ -5,7 +5,7 @@ // Created by Hugues Stephano TELOLAHY on 06/01/2024. // import Testing -import GameFeature +import GameCore struct VolcanicTest { @Test func equiped_shouldPlayBangIgnoringLimitPerTurn() async throws { diff --git a/Modules/CardResources/Tests/Bang/Collectible/WellsFargoTest.swift b/Modules/CardLibraryLive/Tests/Bang/Collectible/WellsFargoTest.swift similarity index 97% rename from Modules/CardResources/Tests/Bang/Collectible/WellsFargoTest.swift rename to Modules/CardLibraryLive/Tests/Bang/Collectible/WellsFargoTest.swift index 8a44de51a..cf742a421 100644 --- a/Modules/CardResources/Tests/Bang/Collectible/WellsFargoTest.swift +++ b/Modules/CardLibraryLive/Tests/Bang/Collectible/WellsFargoTest.swift @@ -5,7 +5,7 @@ // import Testing -import GameFeature +import GameCore struct WellsFargoTest { @Test func play_shouldDraw3Cards() async throws { diff --git a/Modules/CardResources/Tests/Bang/Collectible/WinchesterTest.swift b/Modules/CardLibraryLive/Tests/Bang/Collectible/WinchesterTest.swift similarity index 97% rename from Modules/CardResources/Tests/Bang/Collectible/WinchesterTest.swift rename to Modules/CardLibraryLive/Tests/Bang/Collectible/WinchesterTest.swift index a73ea0f9e..37c39175b 100644 --- a/Modules/CardResources/Tests/Bang/Collectible/WinchesterTest.swift +++ b/Modules/CardLibraryLive/Tests/Bang/Collectible/WinchesterTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct WinchesterTest { @Test func playWinchester_shouldEquipAndSetWeapon() async throws { diff --git a/Modules/CardResources/Tests/Bang/Figures/BartCassidyTests.swift b/Modules/CardLibraryLive/Tests/Bang/Figures/BartCassidyTests.swift similarity index 99% rename from Modules/CardResources/Tests/Bang/Figures/BartCassidyTests.swift rename to Modules/CardLibraryLive/Tests/Bang/Figures/BartCassidyTests.swift index 580221de1..efabb08e1 100644 --- a/Modules/CardResources/Tests/Bang/Figures/BartCassidyTests.swift +++ b/Modules/CardLibraryLive/Tests/Bang/Figures/BartCassidyTests.swift @@ -5,7 +5,7 @@ // Created by Hugues Stephano TELOLAHY on 06/01/2024. // -import GameFeature +import GameCore import Testing struct BartCassidyTests { diff --git a/Modules/CardResources/Tests/Bang/Figures/BlackJackTests.swift b/Modules/CardLibraryLive/Tests/Bang/Figures/BlackJackTests.swift similarity index 97% rename from Modules/CardResources/Tests/Bang/Figures/BlackJackTests.swift rename to Modules/CardLibraryLive/Tests/Bang/Figures/BlackJackTests.swift index 7a3df1f51..094bf4e1c 100644 --- a/Modules/CardResources/Tests/Bang/Figures/BlackJackTests.swift +++ b/Modules/CardLibraryLive/Tests/Bang/Figures/BlackJackTests.swift @@ -5,8 +5,7 @@ // Created by Hugues Stephano TELOLAHY on 10/11/2023. // -import CardResources -import GameFeature +import GameCore import Testing struct BlackJackTests { diff --git a/Modules/CardResources/Tests/Bang/Figures/CalamityJanetTests.swift b/Modules/CardLibraryLive/Tests/Bang/Figures/CalamityJanetTests.swift similarity index 99% rename from Modules/CardResources/Tests/Bang/Figures/CalamityJanetTests.swift rename to Modules/CardLibraryLive/Tests/Bang/Figures/CalamityJanetTests.swift index 1ade52372..47b58db02 100644 --- a/Modules/CardResources/Tests/Bang/Figures/CalamityJanetTests.swift +++ b/Modules/CardLibraryLive/Tests/Bang/Figures/CalamityJanetTests.swift @@ -5,7 +5,7 @@ // Created by Hugues Telolahy on 20/11/2023. // -import GameFeature +import GameCore import Testing struct CalamityJanetTests { diff --git a/Modules/CardResources/Tests/Bang/Figures/ElGringoTests.swift b/Modules/CardLibraryLive/Tests/Bang/Figures/ElGringoTests.swift similarity index 99% rename from Modules/CardResources/Tests/Bang/Figures/ElGringoTests.swift rename to Modules/CardLibraryLive/Tests/Bang/Figures/ElGringoTests.swift index 718f6cbc4..33698d59e 100644 --- a/Modules/CardResources/Tests/Bang/Figures/ElGringoTests.swift +++ b/Modules/CardLibraryLive/Tests/Bang/Figures/ElGringoTests.swift @@ -5,7 +5,7 @@ // Created by Hugues Telolahy on 04/11/2023. // -import GameFeature +import GameCore import Testing struct ElGringoTests { diff --git a/Modules/CardResources/Tests/Bang/Figures/JesseJonesTests.swift b/Modules/CardLibraryLive/Tests/Bang/Figures/JesseJonesTests.swift similarity index 98% rename from Modules/CardResources/Tests/Bang/Figures/JesseJonesTests.swift rename to Modules/CardLibraryLive/Tests/Bang/Figures/JesseJonesTests.swift index 01ee84364..33aefc29c 100644 --- a/Modules/CardResources/Tests/Bang/Figures/JesseJonesTests.swift +++ b/Modules/CardLibraryLive/Tests/Bang/Figures/JesseJonesTests.swift @@ -5,8 +5,7 @@ // Created by Hugues Stephano TELOLAHY on 20/11/2023. // -import CardResources -import GameFeature +import GameCore import Testing struct JesseJonesTests { diff --git a/Modules/CardResources/Tests/Bang/Figures/JourdonnaisTests.swift b/Modules/CardLibraryLive/Tests/Bang/Figures/JourdonnaisTests.swift similarity index 99% rename from Modules/CardResources/Tests/Bang/Figures/JourdonnaisTests.swift rename to Modules/CardLibraryLive/Tests/Bang/Figures/JourdonnaisTests.swift index c3dc0455d..18ff785a5 100644 --- a/Modules/CardResources/Tests/Bang/Figures/JourdonnaisTests.swift +++ b/Modules/CardLibraryLive/Tests/Bang/Figures/JourdonnaisTests.swift @@ -5,7 +5,7 @@ // Created by Hugues Stephano TELOLAHY on 06/01/2024. // -import GameFeature +import GameCore import Testing struct JourdonnaisTests { diff --git a/Modules/CardResources/Tests/Bang/Figures/KitCarlsonTests.swift b/Modules/CardLibraryLive/Tests/Bang/Figures/KitCarlsonTests.swift similarity index 96% rename from Modules/CardResources/Tests/Bang/Figures/KitCarlsonTests.swift rename to Modules/CardLibraryLive/Tests/Bang/Figures/KitCarlsonTests.swift index 3ecd25dd8..835d0309c 100644 --- a/Modules/CardResources/Tests/Bang/Figures/KitCarlsonTests.swift +++ b/Modules/CardLibraryLive/Tests/Bang/Figures/KitCarlsonTests.swift @@ -5,8 +5,7 @@ // Created by Hugues Telolahy on 18/11/2023. // -import CardResources -import GameFeature +import GameCore import Testing struct KitCarlsonTests { diff --git a/Modules/CardResources/Tests/Bang/Figures/LuckyDukeTests.swift b/Modules/CardLibraryLive/Tests/Bang/Figures/LuckyDukeTests.swift similarity index 94% rename from Modules/CardResources/Tests/Bang/Figures/LuckyDukeTests.swift rename to Modules/CardLibraryLive/Tests/Bang/Figures/LuckyDukeTests.swift index 6506a798d..3c50b6aba 100644 --- a/Modules/CardResources/Tests/Bang/Figures/LuckyDukeTests.swift +++ b/Modules/CardLibraryLive/Tests/Bang/Figures/LuckyDukeTests.swift @@ -5,8 +5,7 @@ // Created by Hugues Stephano TELOLAHY on 06/01/2024. // import Testing -import GameFeature -import CardResources +import GameCore struct LuckyDukeTests { @Test func drawing_shouldFlipped2Cards() async throws { diff --git a/Modules/CardResources/Tests/Bang/Figures/PaulRegretTest.swift b/Modules/CardLibraryLive/Tests/Bang/Figures/PaulRegretTest.swift similarity index 91% rename from Modules/CardResources/Tests/Bang/Figures/PaulRegretTest.swift rename to Modules/CardLibraryLive/Tests/Bang/Figures/PaulRegretTest.swift index 104e97981..fcc357a34 100644 --- a/Modules/CardResources/Tests/Bang/Figures/PaulRegretTest.swift +++ b/Modules/CardLibraryLive/Tests/Bang/Figures/PaulRegretTest.swift @@ -6,8 +6,8 @@ // import Testing -import GameFeature -import CardResources +import GameCore +@testable import CardLibraryLive struct PaulRegretTest { @Test func shouldIncrementDistanceFromOthers() async throws { diff --git a/Modules/CardResources/Tests/Bang/Figures/PedroRamirezTests.swift b/Modules/CardLibraryLive/Tests/Bang/Figures/PedroRamirezTests.swift similarity index 98% rename from Modules/CardResources/Tests/Bang/Figures/PedroRamirezTests.swift rename to Modules/CardLibraryLive/Tests/Bang/Figures/PedroRamirezTests.swift index d38e980ff..23e0a7ebf 100644 --- a/Modules/CardResources/Tests/Bang/Figures/PedroRamirezTests.swift +++ b/Modules/CardLibraryLive/Tests/Bang/Figures/PedroRamirezTests.swift @@ -5,8 +5,7 @@ // Created by Hugues Stephano TELOLAHY on 13/11/2023. // -import CardResources -import GameFeature +import GameCore import Testing struct PedroRamirezTests { diff --git a/Modules/CardResources/Tests/Bang/Figures/RoseDoolanTest.swift b/Modules/CardLibraryLive/Tests/Bang/Figures/RoseDoolanTest.swift similarity index 91% rename from Modules/CardResources/Tests/Bang/Figures/RoseDoolanTest.swift rename to Modules/CardLibraryLive/Tests/Bang/Figures/RoseDoolanTest.swift index 84144cd4a..b39c3fc64 100644 --- a/Modules/CardResources/Tests/Bang/Figures/RoseDoolanTest.swift +++ b/Modules/CardLibraryLive/Tests/Bang/Figures/RoseDoolanTest.swift @@ -6,8 +6,8 @@ // import Testing -import GameFeature -import CardResources +import GameCore +@testable import CardLibraryLive struct RoseDoolanTest { @Test func shouldDecrementDistanceToOthers() async throws { diff --git a/Modules/CardResources/Tests/Bang/Figures/SidKetchumTests.swift b/Modules/CardLibraryLive/Tests/Bang/Figures/SidKetchumTests.swift similarity index 99% rename from Modules/CardResources/Tests/Bang/Figures/SidKetchumTests.swift rename to Modules/CardLibraryLive/Tests/Bang/Figures/SidKetchumTests.swift index 0d7a49a27..fa6f4bfe4 100644 --- a/Modules/CardResources/Tests/Bang/Figures/SidKetchumTests.swift +++ b/Modules/CardLibraryLive/Tests/Bang/Figures/SidKetchumTests.swift @@ -5,7 +5,7 @@ // Created by Hugues Stephano TELOLAHY on 06/01/2024. // -import GameFeature +import GameCore import Testing struct SidKetchumTests { diff --git a/Modules/CardResources/Tests/Bang/Figures/SlabTheKillerTests.swift b/Modules/CardLibraryLive/Tests/Bang/Figures/SlabTheKillerTests.swift similarity index 98% rename from Modules/CardResources/Tests/Bang/Figures/SlabTheKillerTests.swift rename to Modules/CardLibraryLive/Tests/Bang/Figures/SlabTheKillerTests.swift index 376e23d88..824c0a608 100644 --- a/Modules/CardResources/Tests/Bang/Figures/SlabTheKillerTests.swift +++ b/Modules/CardLibraryLive/Tests/Bang/Figures/SlabTheKillerTests.swift @@ -5,8 +5,7 @@ // Created by Stephano Hugues TELOLAHY on 29/05/2024. // -import CardResources -import GameFeature +import GameCore import Testing struct SlabTheKillerTests { diff --git a/Modules/CardResources/Tests/Bang/Figures/SuzyLafayetteTests.swift b/Modules/CardLibraryLive/Tests/Bang/Figures/SuzyLafayetteTests.swift similarity index 99% rename from Modules/CardResources/Tests/Bang/Figures/SuzyLafayetteTests.swift rename to Modules/CardLibraryLive/Tests/Bang/Figures/SuzyLafayetteTests.swift index 24c29a17c..fa9242034 100644 --- a/Modules/CardResources/Tests/Bang/Figures/SuzyLafayetteTests.swift +++ b/Modules/CardLibraryLive/Tests/Bang/Figures/SuzyLafayetteTests.swift @@ -5,7 +5,7 @@ // Created by Hugues Stephano TELOLAHY on 06/01/2024. // -import GameFeature +import GameCore import Testing struct SuzyLafayetteTests { diff --git a/Modules/CardResources/Tests/Bang/Figures/VultureSamTests.swift b/Modules/CardLibraryLive/Tests/Bang/Figures/VultureSamTests.swift similarity index 98% rename from Modules/CardResources/Tests/Bang/Figures/VultureSamTests.swift rename to Modules/CardLibraryLive/Tests/Bang/Figures/VultureSamTests.swift index 4ef73e079..22d097b6a 100644 --- a/Modules/CardResources/Tests/Bang/Figures/VultureSamTests.swift +++ b/Modules/CardLibraryLive/Tests/Bang/Figures/VultureSamTests.swift @@ -5,7 +5,7 @@ // Created by Hugues Stephano TELOLAHY on 06/01/2024. // -import GameFeature +import GameCore import Testing struct VultureSamTests { diff --git a/Modules/CardResources/Tests/Bang/Figures/WillyTheKidTest.swift b/Modules/CardLibraryLive/Tests/Bang/Figures/WillyTheKidTest.swift similarity index 98% rename from Modules/CardResources/Tests/Bang/Figures/WillyTheKidTest.swift rename to Modules/CardLibraryLive/Tests/Bang/Figures/WillyTheKidTest.swift index 7358c5695..f249392ee 100644 --- a/Modules/CardResources/Tests/Bang/Figures/WillyTheKidTest.swift +++ b/Modules/CardLibraryLive/Tests/Bang/Figures/WillyTheKidTest.swift @@ -6,8 +6,7 @@ // import Testing -import GameFeature -import CardResources +import GameCore struct WillyTheKidTest { @Test func shouldPlayBangIgnoringLimitPerTurn() async throws { diff --git a/Modules/CardResources/Tests/CardAssetTest.swift b/Modules/CardLibraryLive/Tests/CardAssetTest.swift similarity index 100% rename from Modules/CardResources/Tests/CardAssetTest.swift rename to Modules/CardLibraryLive/Tests/CardAssetTest.swift diff --git a/Modules/CardResources/Tests/Defaults/DiscardAllCardsOnEliminatedTest.swift b/Modules/CardLibraryLive/Tests/Defaults/DiscardAllCardsOnEliminatedTest.swift similarity index 98% rename from Modules/CardResources/Tests/Defaults/DiscardAllCardsOnEliminatedTest.swift rename to Modules/CardLibraryLive/Tests/Defaults/DiscardAllCardsOnEliminatedTest.swift index 6d06f2ccb..fb6e128c3 100644 --- a/Modules/CardResources/Tests/Defaults/DiscardAllCardsOnEliminatedTest.swift +++ b/Modules/CardLibraryLive/Tests/Defaults/DiscardAllCardsOnEliminatedTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct DiscardAllCardsOnEliminatedTest { @Test func beingEliminated_havingCards_shouldDiscardAllCards() async throws { diff --git a/Modules/CardResources/Tests/Defaults/DiscardBeerOnDamagedLethalTest.swift b/Modules/CardLibraryLive/Tests/Defaults/DiscardBeerOnDamagedLethalTest.swift similarity index 99% rename from Modules/CardResources/Tests/Defaults/DiscardBeerOnDamagedLethalTest.swift rename to Modules/CardLibraryLive/Tests/Defaults/DiscardBeerOnDamagedLethalTest.swift index b31fc9479..7aa42df83 100644 --- a/Modules/CardResources/Tests/Defaults/DiscardBeerOnDamagedLethalTest.swift +++ b/Modules/CardLibraryLive/Tests/Defaults/DiscardBeerOnDamagedLethalTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct DiscardBeerOnDamagedLethalTest { @Test func beingDamagedLethal_discardingBeer_shouldRestoreHealth() async throws { diff --git a/Modules/CardResources/Tests/Defaults/DiscardEquipedWeaponOnPrePlayedTest.swift b/Modules/CardLibraryLive/Tests/Defaults/DiscardEquipedWeaponOnPrePlayedTest.swift similarity index 98% rename from Modules/CardResources/Tests/Defaults/DiscardEquipedWeaponOnPrePlayedTest.swift rename to Modules/CardLibraryLive/Tests/Defaults/DiscardEquipedWeaponOnPrePlayedTest.swift index 7d8c32986..351d239d2 100644 --- a/Modules/CardResources/Tests/Defaults/DiscardEquipedWeaponOnPrePlayedTest.swift +++ b/Modules/CardLibraryLive/Tests/Defaults/DiscardEquipedWeaponOnPrePlayedTest.swift @@ -5,7 +5,7 @@ // Created by Hugues Stéphano TELOLAHY on 15/11/2025. // -import GameFeature +import GameCore import Testing struct DiscardEquipedWeaponOnPrePlayedTest { diff --git a/Modules/CardResources/Tests/Defaults/DiscardExcessHandOnTurnEndedTest.swift b/Modules/CardLibraryLive/Tests/Defaults/DiscardExcessHandOnTurnEndedTest.swift similarity index 99% rename from Modules/CardResources/Tests/Defaults/DiscardExcessHandOnTurnEndedTest.swift rename to Modules/CardLibraryLive/Tests/Defaults/DiscardExcessHandOnTurnEndedTest.swift index 958ee79e4..1f46c3449 100644 --- a/Modules/CardResources/Tests/Defaults/DiscardExcessHandOnTurnEndedTest.swift +++ b/Modules/CardLibraryLive/Tests/Defaults/DiscardExcessHandOnTurnEndedTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct DiscardExcessHandOnTurnEndedTest { @Test func endTurn_oneExcessCard_shouldDiscardAHandCard() async throws { diff --git a/Modules/CardResources/Tests/Defaults/Draw2CardsOnTurnStartedTest.swift b/Modules/CardLibraryLive/Tests/Defaults/Draw2CardsOnTurnStartedTest.swift similarity index 97% rename from Modules/CardResources/Tests/Defaults/Draw2CardsOnTurnStartedTest.swift rename to Modules/CardLibraryLive/Tests/Defaults/Draw2CardsOnTurnStartedTest.swift index c8bc37565..62679a2c6 100644 --- a/Modules/CardResources/Tests/Defaults/Draw2CardsOnTurnStartedTest.swift +++ b/Modules/CardLibraryLive/Tests/Defaults/Draw2CardsOnTurnStartedTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct Draw2CardsOnTurnStartedTest { @Test func startTurn_shouldDraw2Cards() async throws { diff --git a/Modules/CardResources/Tests/Defaults/Draw3CardsOnEliminatingTest.swift b/Modules/CardLibraryLive/Tests/Defaults/Draw3CardsOnEliminatingTest.swift similarity index 98% rename from Modules/CardResources/Tests/Defaults/Draw3CardsOnEliminatingTest.swift rename to Modules/CardLibraryLive/Tests/Defaults/Draw3CardsOnEliminatingTest.swift index daa65dc16..eba83a628 100644 --- a/Modules/CardResources/Tests/Defaults/Draw3CardsOnEliminatingTest.swift +++ b/Modules/CardLibraryLive/Tests/Defaults/Draw3CardsOnEliminatingTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct Draw3CardsOnEliminatingTest { @Test func eliminating_shouldDraw2Cards() async throws { diff --git a/Modules/CardResources/Tests/Defaults/EliminateOnDamageLethalTest.swift b/Modules/CardLibraryLive/Tests/Defaults/EliminateOnDamageLethalTest.swift similarity index 98% rename from Modules/CardResources/Tests/Defaults/EliminateOnDamageLethalTest.swift rename to Modules/CardLibraryLive/Tests/Defaults/EliminateOnDamageLethalTest.swift index 6946badd5..271e9e258 100644 --- a/Modules/CardResources/Tests/Defaults/EliminateOnDamageLethalTest.swift +++ b/Modules/CardLibraryLive/Tests/Defaults/EliminateOnDamageLethalTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct EliminateOnDamageLethalTest { @Test func beingDamaged_lethal_shouldBeEliminated() async throws { diff --git a/Modules/CardResources/Tests/Defaults/EndGameOnEliminatedTest.swift b/Modules/CardLibraryLive/Tests/Defaults/EndGameOnEliminatedTest.swift similarity index 98% rename from Modules/CardResources/Tests/Defaults/EndGameOnEliminatedTest.swift rename to Modules/CardLibraryLive/Tests/Defaults/EndGameOnEliminatedTest.swift index fa2b1fb2c..ae9f9b028 100644 --- a/Modules/CardResources/Tests/Defaults/EndGameOnEliminatedTest.swift +++ b/Modules/CardLibraryLive/Tests/Defaults/EndGameOnEliminatedTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct EndGameOnEliminatedTest { @Test func game_withOnePlayerLast_shouldBeOver() async throws { diff --git a/Modules/CardResources/Tests/Defaults/NextTurnOnEliminatedTest.swift b/Modules/CardLibraryLive/Tests/Defaults/NextTurnOnEliminatedTest.swift similarity index 98% rename from Modules/CardResources/Tests/Defaults/NextTurnOnEliminatedTest.swift rename to Modules/CardLibraryLive/Tests/Defaults/NextTurnOnEliminatedTest.swift index aebb32047..825dd4b0f 100644 --- a/Modules/CardResources/Tests/Defaults/NextTurnOnEliminatedTest.swift +++ b/Modules/CardLibraryLive/Tests/Defaults/NextTurnOnEliminatedTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct NextTurnOnEliminatedTest { @Test func beingEliminated_currentTurn_shouldNextTurn() async throws { diff --git a/Modules/CardResources/Tests/Defaults/NextTurnOnTurnEndedTest.swift b/Modules/CardLibraryLive/Tests/Defaults/NextTurnOnTurnEndedTest.swift similarity index 95% rename from Modules/CardResources/Tests/Defaults/NextTurnOnTurnEndedTest.swift rename to Modules/CardLibraryLive/Tests/Defaults/NextTurnOnTurnEndedTest.swift index 77ea6c8b1..98cd913af 100644 --- a/Modules/CardResources/Tests/Defaults/NextTurnOnTurnEndedTest.swift +++ b/Modules/CardLibraryLive/Tests/Defaults/NextTurnOnTurnEndedTest.swift @@ -6,8 +6,7 @@ // import Testing -import GameFeature -import CardResources +import GameCore struct NextTurnOnTurnEndedTest { @Test func endturn_shouldStartNextTurn() async throws { diff --git a/Modules/CardResources/Tests/DodgeCity/Collectible/BinocularTest.swift b/Modules/CardLibraryLive/Tests/DodgeCity/Collectible/BinocularTest.swift similarity index 98% rename from Modules/CardResources/Tests/DodgeCity/Collectible/BinocularTest.swift rename to Modules/CardLibraryLive/Tests/DodgeCity/Collectible/BinocularTest.swift index b39569742..024352d5e 100644 --- a/Modules/CardResources/Tests/DodgeCity/Collectible/BinocularTest.swift +++ b/Modules/CardLibraryLive/Tests/DodgeCity/Collectible/BinocularTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct BinocularTest { @Test func play_shouldEquipAndIncreaseMagnifying() async throws { diff --git a/Modules/CardResources/Tests/DodgeCity/Collectible/BrawlTest.swift b/Modules/CardLibraryLive/Tests/DodgeCity/Collectible/BrawlTest.swift similarity index 99% rename from Modules/CardResources/Tests/DodgeCity/Collectible/BrawlTest.swift rename to Modules/CardLibraryLive/Tests/DodgeCity/Collectible/BrawlTest.swift index a2ec564e1..26431778d 100644 --- a/Modules/CardResources/Tests/DodgeCity/Collectible/BrawlTest.swift +++ b/Modules/CardLibraryLive/Tests/DodgeCity/Collectible/BrawlTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct BrawlTest { @Test func play_withOthersHavingHandCard_shouldForceThemToDiscardACard() async throws { diff --git a/Modules/CardResources/Tests/DodgeCity/Collectible/DodgeTest.swift b/Modules/CardLibraryLive/Tests/DodgeCity/Collectible/DodgeTest.swift similarity index 97% rename from Modules/CardResources/Tests/DodgeCity/Collectible/DodgeTest.swift rename to Modules/CardLibraryLive/Tests/DodgeCity/Collectible/DodgeTest.swift index 6ff0de047..495908fde 100644 --- a/Modules/CardResources/Tests/DodgeCity/Collectible/DodgeTest.swift +++ b/Modules/CardLibraryLive/Tests/DodgeCity/Collectible/DodgeTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct DodgeTest { @Test func beingShot_discardingDodge_shouldCounterAndDrawCard() async throws { diff --git a/Modules/CardResources/Tests/DodgeCity/Collectible/HideoutTests.swift b/Modules/CardLibraryLive/Tests/DodgeCity/Collectible/HideoutTests.swift similarity index 98% rename from Modules/CardResources/Tests/DodgeCity/Collectible/HideoutTests.swift rename to Modules/CardLibraryLive/Tests/DodgeCity/Collectible/HideoutTests.swift index 5e9f4c9aa..7975f3939 100644 --- a/Modules/CardResources/Tests/DodgeCity/Collectible/HideoutTests.swift +++ b/Modules/CardLibraryLive/Tests/DodgeCity/Collectible/HideoutTests.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct HideoutTests { @Test func play_shouldEquipAndIncreaseRemoteness() async throws { diff --git a/Modules/CardResources/Tests/DodgeCity/Collectible/PunchTest.swift b/Modules/CardLibraryLive/Tests/DodgeCity/Collectible/PunchTest.swift similarity index 98% rename from Modules/CardResources/Tests/DodgeCity/Collectible/PunchTest.swift rename to Modules/CardLibraryLive/Tests/DodgeCity/Collectible/PunchTest.swift index 772981a6e..01965055a 100644 --- a/Modules/CardResources/Tests/DodgeCity/Collectible/PunchTest.swift +++ b/Modules/CardLibraryLive/Tests/DodgeCity/Collectible/PunchTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct PunchTest { @Test func play_shouldShootAtRange1() async throws { diff --git a/Modules/CardResources/Tests/DodgeCity/Collectible/RagTimeTest.swift b/Modules/CardLibraryLive/Tests/DodgeCity/Collectible/RagTimeTest.swift similarity index 99% rename from Modules/CardResources/Tests/DodgeCity/Collectible/RagTimeTest.swift rename to Modules/CardLibraryLive/Tests/DodgeCity/Collectible/RagTimeTest.swift index 0b19c9420..d95731bf8 100644 --- a/Modules/CardResources/Tests/DodgeCity/Collectible/RagTimeTest.swift +++ b/Modules/CardLibraryLive/Tests/DodgeCity/Collectible/RagTimeTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct RagTimeTest { @Test func play_shouldStealHandCardFromAnyPlayer() async throws { diff --git a/Modules/CardResources/Tests/DodgeCity/Collectible/SpringfieldTest.swift b/Modules/CardLibraryLive/Tests/DodgeCity/Collectible/SpringfieldTest.swift similarity index 98% rename from Modules/CardResources/Tests/DodgeCity/Collectible/SpringfieldTest.swift rename to Modules/CardLibraryLive/Tests/DodgeCity/Collectible/SpringfieldTest.swift index d4fa58dbc..3db1303cf 100644 --- a/Modules/CardResources/Tests/DodgeCity/Collectible/SpringfieldTest.swift +++ b/Modules/CardLibraryLive/Tests/DodgeCity/Collectible/SpringfieldTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct SpringfieldTest { @Test func play_shouldShootAtUnlimitedRange() async throws { diff --git a/Modules/CardResources/Tests/DodgeCity/Collectible/TequilaTest.swift b/Modules/CardLibraryLive/Tests/DodgeCity/Collectible/TequilaTest.swift similarity index 98% rename from Modules/CardResources/Tests/DodgeCity/Collectible/TequilaTest.swift rename to Modules/CardLibraryLive/Tests/DodgeCity/Collectible/TequilaTest.swift index 05c57f341..ec5139316 100644 --- a/Modules/CardResources/Tests/DodgeCity/Collectible/TequilaTest.swift +++ b/Modules/CardLibraryLive/Tests/DodgeCity/Collectible/TequilaTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct TequilaTest { @Test func play_shouldHeal1AnyWoundedPlayer() async throws { diff --git a/Modules/CardResources/Tests/DodgeCity/Collectible/WhiskyTest.swift b/Modules/CardLibraryLive/Tests/DodgeCity/Collectible/WhiskyTest.swift similarity index 98% rename from Modules/CardResources/Tests/DodgeCity/Collectible/WhiskyTest.swift rename to Modules/CardLibraryLive/Tests/DodgeCity/Collectible/WhiskyTest.swift index 2e1400769..5ea401c06 100644 --- a/Modules/CardResources/Tests/DodgeCity/Collectible/WhiskyTest.swift +++ b/Modules/CardLibraryLive/Tests/DodgeCity/Collectible/WhiskyTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct WhiskyTest { @Test func play_shouldHeal2() async throws { diff --git a/Modules/CardResources/Tests/Misc/DispatchUntilCompleted.swift b/Modules/CardLibraryLive/Tests/Misc/DispatchUntilCompleted.swift similarity index 98% rename from Modules/CardResources/Tests/Misc/DispatchUntilCompleted.swift rename to Modules/CardLibraryLive/Tests/Misc/DispatchUntilCompleted.swift index 93bb2465a..8ab5b1db0 100644 --- a/Modules/CardResources/Tests/Misc/DispatchUntilCompleted.swift +++ b/Modules/CardLibraryLive/Tests/Misc/DispatchUntilCompleted.swift @@ -7,8 +7,8 @@ import Testing import Redux import Combine -import GameFeature -import CardResources +import GameCore +import CardLibraryLive typealias ChoiceHandler = ([String]) -> String diff --git a/Modules/CardResources/Tests/Misc/GameStateBuilder+Cards.swift b/Modules/CardLibraryLive/Tests/Misc/GameStateBuilder+Cards.swift similarity index 92% rename from Modules/CardResources/Tests/Misc/GameStateBuilder+Cards.swift rename to Modules/CardLibraryLive/Tests/Misc/GameStateBuilder+Cards.swift index 610a55ace..9c7454fe2 100644 --- a/Modules/CardResources/Tests/Misc/GameStateBuilder+Cards.swift +++ b/Modules/CardLibraryLive/Tests/Misc/GameStateBuilder+Cards.swift @@ -4,8 +4,8 @@ // // Created by Hugues Telolahy on 28/10/2024. // -import GameFeature -import CardResources +import GameCore +@testable import CardLibraryLive extension GameFeature.State.Builder { func withAllCards() -> Self { diff --git a/Modules/CardResources/Tests/Misc/SampleCards.swift b/Modules/CardLibraryLive/Tests/Misc/SampleCards.swift similarity index 100% rename from Modules/CardResources/Tests/Misc/SampleCards.swift rename to Modules/CardLibraryLive/Tests/Misc/SampleCards.swift diff --git a/Modules/CardResources/Tests/SimulationTest.swift b/Modules/CardLibraryLive/Tests/SimulationTest.swift similarity index 96% rename from Modules/CardResources/Tests/SimulationTest.swift rename to Modules/CardLibraryLive/Tests/SimulationTest.swift index 88bc03def..cac532a2f 100644 --- a/Modules/CardResources/Tests/SimulationTest.swift +++ b/Modules/CardLibraryLive/Tests/SimulationTest.swift @@ -8,8 +8,8 @@ import Testing import Redux import Combine -@testable import GameFeature -import CardResources +@testable import GameCore +@testable import CardLibraryLive struct SimulationTest { @Test func simulateGame_shouldComplete() async throws { diff --git a/Modules/CardResources/Sources/CardResources.swift b/Modules/CardResources/Sources/Bundle+CardResources.swift similarity index 100% rename from Modules/CardResources/Sources/CardResources.swift rename to Modules/CardResources/Sources/Bundle+CardResources.swift diff --git a/Modules/CardResources/Sources/CardNames.swift b/Modules/CardResources/Sources/CardNames.swift index 813eab805..291e5024d 100644 --- a/Modules/CardResources/Sources/CardNames.swift +++ b/Modules/CardResources/Sources/CardNames.swift @@ -1,4 +1,4 @@ -// +// swiftlint:disable:this file_name // CardNames.swift // WildWestOnline // @@ -10,20 +10,6 @@ public extension String { static let endTurn = "endTurn" } -extension String { - static let playMissedOnShot = "playMissedOnShot" - static let discardExcessHandOnTurnEnded = "discardExcessHandOnTurnEnded" - static let draw2CardsOnTurnStarted = "draw2CardsOnTurnStarted" - static let nextTurnOnTurnEnded = "nextTurnOnTurnEnded" - static let eliminateOnDamagedLethal = "eliminateOnDamagedLethal" - static let endGameOnEliminated = "endGameOnEliminated" - static let discardAllCardsOnEliminated = "discardAllCardsOnEliminated" - static let nextTurnOnEliminated = "nextTurnOnEliminated" - static let discardBeerOnDamagedLethal = "discardBeerOnDamagedLethal" - static let draw3CardsOnEliminating = "draw3CardsOnEliminating" - static let discardEquipedWeaponOnPrePlayed = "discardEquipedWeaponOnPrePlayed" -} - // MARK: - Collectible public extension String { static let bang = "bang" diff --git a/Modules/CardResources/Sources/Modifiers/QueueModifiers.swift b/Modules/CardResources/Sources/Modifiers/QueueModifiers.swift deleted file mode 100644 index 7c714d871..000000000 --- a/Modules/CardResources/Sources/Modifiers/QueueModifiers.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// QueueModifiers.swift -// WildWestOnline -// -// Created by Hugues Stéphano TELOLAHY on 23/11/2025. -// -import GameFeature - -public enum QueueModifiers { - public static let allHandlers: [QueueModifierHandler.Type] = [ - IncrementCardsPerTurn.self, - IncrementRequiredMisses.self, - IgnoreLimitPerTurn.self, - ] -} diff --git a/Modules/GameFeature/Sources/Builder/GameAction+Builder.swift b/Modules/GameCore/Sources/Builder/GameFeatureAction+Builder.swift similarity index 99% rename from Modules/GameFeature/Sources/Builder/GameAction+Builder.swift rename to Modules/GameCore/Sources/Builder/GameFeatureAction+Builder.swift index db653e18b..2cbce8c26 100644 --- a/Modules/GameFeature/Sources/Builder/GameAction+Builder.swift +++ b/Modules/GameCore/Sources/Builder/GameFeatureAction+Builder.swift @@ -1,5 +1,5 @@ // -// GameAction+Builder.swift +// GameFeature.Action+Builder.swift // WildWestOnline // // Created by Hugues Stéphano TELOLAHY on 03/01/2025. diff --git a/Modules/GameFeature/Sources/Builder/GameState+Builder.swift b/Modules/GameCore/Sources/Builder/GameFeatureState+Builder.swift similarity index 99% rename from Modules/GameFeature/Sources/Builder/GameState+Builder.swift rename to Modules/GameCore/Sources/Builder/GameFeatureState+Builder.swift index 552bbfc34..c8dd25f7b 100644 --- a/Modules/GameFeature/Sources/Builder/GameState+Builder.swift +++ b/Modules/GameCore/Sources/Builder/GameFeatureState+Builder.swift @@ -1,10 +1,9 @@ // -// GameState+Builder.swift +// GameFeature.State+Builder.swift // WildWestOnline // // Created by Hugues Stéphano TELOLAHY on 03/01/2025. // - public extension GameFeature.State { class Builder { private var players: [String: Player] = [:] diff --git a/Modules/GameFeature/Sources/Card/Card.swift b/Modules/GameCore/Sources/Card.swift similarity index 83% rename from Modules/GameFeature/Sources/Card/Card.swift rename to Modules/GameCore/Sources/Card.swift index 836ed28c5..28ecab8bf 100644 --- a/Modules/GameFeature/Sources/Card/Card.swift +++ b/Modules/GameCore/Sources/Card.swift @@ -4,14 +4,12 @@ // Created by Hugues Telolahy on 28/10/2024. // -import Redux - /// We are working on a Card Definition DSL that will allow people to create new cards, /// not currently in the game and see how they play. /// A `card` is just a collection of effects and attributes /// ℹ️ Inspired by https://github.com/danielyule/hearthbreaker/wiki/Tag-Format /// -public struct Card: Equatable, Codable, Sendable { +public struct Card: Equatable, Sendable { public let name: String public let type: CardType public let description: String? @@ -29,16 +27,16 @@ public struct Card: Equatable, Codable, Sendable { self.effects = effects } - public enum CardType: String, Codable, Sendable { + public enum CardType: String, Sendable { case collectible case figure case ability } - public struct Effect: Equatable, Codable, Sendable { + public struct Effect: Equatable, Sendable { public let trigger: Trigger public let action: ActionName - public let modifier: GameFeature.Action.QueueModifier? + public let modifier: QueueModifier? public let amount: Int? public let amountPerTurn: [String: Int]? public let alias: [String: String]? @@ -47,7 +45,7 @@ public struct Card: Equatable, Codable, Sendable { public init( trigger: Trigger, action: ActionName, - modifier: GameFeature.Action.QueueModifier? = nil, + modifier: QueueModifier? = nil, amount: Int? = nil, amountPerTurn: [String: Int]? = nil, alias: [String: String]? = nil, @@ -63,7 +61,7 @@ public struct Card: Equatable, Codable, Sendable { } } - public enum Trigger: Equatable, Codable, Sendable { + public enum Trigger: Equatable, Sendable { case permanent case cardPrePlayed case cardPlayed @@ -85,7 +83,7 @@ public struct Card: Equatable, Codable, Sendable { case requiredToDraw } - public enum ActionName: String, Codable, Sendable { + public enum ActionName: String, Sendable { case preparePlay case play case equip @@ -122,7 +120,7 @@ public struct Card: Equatable, Codable, Sendable { case dummy } - public enum Selector: Equatable, Codable, Sendable { + public enum Selector: Equatable, Sendable { case `repeat`(RepeatCount) case setTarget(PlayerGroup) case setCard(CardGroup) @@ -131,7 +129,7 @@ public struct Card: Equatable, Codable, Sendable { case applyIf(PlayRequirement) case onComplete([Effect]) - public enum RepeatCount: Equatable, Codable, Sendable { + public enum RepeatCount: Equatable, Sendable { case fixed(Int) case activePlayerCount case playerExcessHandSize @@ -139,7 +137,7 @@ public struct Card: Equatable, Codable, Sendable { case requiredMisses } - public enum PlayerGroup: Equatable, Codable, Sendable { + public enum PlayerGroup: Equatable, Sendable { case activePlayers case woundedPlayers case otherPlayers([PlayerFilter] = []) @@ -149,7 +147,7 @@ public struct Card: Equatable, Codable, Sendable { case eliminatedPlayer } - public enum CardGroup: String, Codable, Sendable { + public enum CardGroup: String, Sendable { case allInHand case allInPlay case played @@ -157,7 +155,7 @@ public struct Card: Equatable, Codable, Sendable { case lastHand } - public indirect enum PlayRequirement: Equatable, Codable, Sendable { + public indirect enum PlayRequirement: Equatable, Sendable { case not(Self) case minimumPlayers(Int) case playLimitThisTurn(Int) @@ -170,7 +168,7 @@ public struct Card: Equatable, Codable, Sendable { case targetedCardFromInPlay } - public enum ChoiceRequirement: Equatable, Codable, Sendable { + public enum ChoiceRequirement: Equatable, Sendable { case targetPlayer([PlayerFilter] = []) case targetCard([CardFilter] = []) case discoverCard @@ -181,7 +179,7 @@ public struct Card: Equatable, Codable, Sendable { case playedCard([CardFilter] = []) } - public enum PlayerFilter: Equatable, Codable, Sendable { + public enum PlayerFilter: Equatable, Sendable { case hasCards case hasHandCards case atDistance(Int) @@ -189,13 +187,13 @@ public struct Card: Equatable, Codable, Sendable { case isWounded } - public enum CardFilter: Equatable, Codable, Sendable { + public enum CardFilter: Equatable, Sendable { case canCounterShot case named(String) case isFromHand } - public struct ChoicePrompt: Equatable, Codable, Sendable { + public struct ChoicePrompt: Equatable, Sendable { public let chooser: String public let options: [Option] @@ -204,7 +202,7 @@ public struct Card: Equatable, Codable, Sendable { self.options = options } - public struct Option: Equatable, Codable, Sendable { + public struct Option: Equatable, Sendable { public let id: String public let label: String @@ -215,6 +213,14 @@ public struct Card: Equatable, Codable, Sendable { } } } + + public struct QueueModifier: RawRepresentable, Hashable, Sendable { + public let rawValue: String + + public init (rawValue: String) { + self.rawValue = rawValue + } + } } public extension String { diff --git a/Modules/GameFeature/Sources/GameFeature.swift b/Modules/GameCore/Sources/GameFeature.swift similarity index 74% rename from Modules/GameFeature/Sources/GameFeature.swift rename to Modules/GameCore/Sources/GameFeature.swift index 2a41cb28a..d81b7f21b 100644 --- a/Modules/GameFeature/Sources/GameFeature.swift +++ b/Modules/GameCore/Sources/GameFeature.swift @@ -5,10 +5,8 @@ // Created by Hugues Stéphano TELOLAHY on 28/03/2025. // import Redux - public enum GameFeature { - /// All aspects of game state - public struct State: Equatable, Codable, Sendable { + public struct State: Equatable, Sendable { public var players: [String: Player] public var playOrder: [String] public let startOrder: [String] @@ -29,7 +27,7 @@ public enum GameFeature { let actionDelayMilliSeconds: Int let showPlayableCards: Bool - public struct Player: Equatable, Codable, Sendable { + public struct Player: Equatable, Sendable { public let figure: [String] public var health: Int public let maxHealth: Int @@ -40,18 +38,18 @@ public enum GameFeature { public var inPlay: [String] } - public enum PlayMode: Equatable, Codable, Sendable { + public enum PlayMode: Equatable, Sendable { case manual case auto } - public struct PlayableCards: Equatable, Codable, Sendable { + public struct PlayableCards: Equatable, Sendable { public var player: String public var cards: [String] } } - public struct Action: Equatable, Codable, Sendable { + public struct Action: Equatable, Sendable { public let name: Card.ActionName public var sourcePlayer: String = "" public var playedCard: String = "" @@ -64,24 +62,16 @@ public enum GameFeature { var selection: String? var alias: String? var playableCards: [String]? - var modifier: QueueModifier? + var modifier: Card.QueueModifier? var children: [Self]? var selectors: [Card.Selector] = [] - public struct QueueModifier: RawRepresentable, Hashable, Codable, Sendable { - public let rawValue: String - - public init (rawValue: String) { - self.rawValue = rawValue - } - } - public static func == (lhs: Self, rhs: Self) -> Bool { NonStandardLogic.areActionsEqual(lhs, rhs) } } - public enum Error: Swift.Error, Equatable, Codable { + public enum Error: Swift.Error, Equatable { case insufficientDeck case insufficientDiscard case playerAlreadyMaxHealth(String) @@ -97,12 +87,7 @@ public enum GameFeature { combine( reducerMechanics, reducerLoop, - pullback( - reducerAI, - state: { _ in \.self }, - action: { $0 }, - embedAction: \.self - ) + reducerAI ) } } diff --git a/Modules/GameFeature/Sources/GameSetup.swift b/Modules/GameCore/Sources/GameSetup.swift similarity index 99% rename from Modules/GameFeature/Sources/GameSetup.swift rename to Modules/GameCore/Sources/GameSetup.swift index 43abfde75..aa51fd975 100644 --- a/Modules/GameFeature/Sources/GameSetup.swift +++ b/Modules/GameCore/Sources/GameSetup.swift @@ -4,7 +4,6 @@ // // Created by Hugues Stéphano TELOLAHY on 24/11/2024. // - public enum PlayModeSetup { case oneManual case allAuto diff --git a/Modules/GameFeature/Sources/Misc/Array+Starting.swift b/Modules/GameCore/Sources/Misc/Array+Starting.swift similarity index 100% rename from Modules/GameFeature/Sources/Misc/Array+Starting.swift rename to Modules/GameCore/Sources/Misc/Array+Starting.swift diff --git a/Modules/GameFeature/Sources/Misc/Cards+Subscript.swift b/Modules/GameCore/Sources/Misc/Array+ToDictionary.swift similarity index 92% rename from Modules/GameFeature/Sources/Misc/Cards+Subscript.swift rename to Modules/GameCore/Sources/Misc/Array+ToDictionary.swift index 554b38c38..94f76836d 100644 --- a/Modules/GameFeature/Sources/Misc/Cards+Subscript.swift +++ b/Modules/GameCore/Sources/Misc/Array+ToDictionary.swift @@ -1,10 +1,9 @@ // -// Cards+Subscript.swift +// Array+ToDictionary.swift // WildWestOnline // // Created by Hugues Stéphano TELOLAHY on 02/11/2025. // - public extension Array where Element == Card { var toDictionary: [String: Card] { reduce(into: [:]) { result, card in diff --git a/Modules/GameFeature/Sources/Misc/Card+ExtractName.swift b/Modules/GameCore/Sources/Misc/Card+ExtractName.swift similarity index 99% rename from Modules/GameFeature/Sources/Misc/Card+ExtractName.swift rename to Modules/GameCore/Sources/Misc/Card+ExtractName.swift index f25d6ddea..8aee88483 100644 --- a/Modules/GameFeature/Sources/Misc/Card+ExtractName.swift +++ b/Modules/GameCore/Sources/Misc/Card+ExtractName.swift @@ -4,7 +4,6 @@ // // Created by Stephano Hugues TELOLAHY on 09/11/2024. // - public extension Card { static func name(of identifier: String) -> String { identifier.split(separator: "-").first.map(String.init) ?? identifier diff --git a/Modules/GameFeature/Sources/Misc/EffectDefinition+Instance.swift b/Modules/GameCore/Sources/Misc/CardEffect+Instance.swift similarity index 96% rename from Modules/GameFeature/Sources/Misc/EffectDefinition+Instance.swift rename to Modules/GameCore/Sources/Misc/CardEffect+Instance.swift index fc2f459d2..b3f6a26a9 100644 --- a/Modules/GameFeature/Sources/Misc/EffectDefinition+Instance.swift +++ b/Modules/GameCore/Sources/Misc/CardEffect+Instance.swift @@ -1,10 +1,9 @@ // -// Effect+Instance.swift +// CardEffect+Instance.swift // WildWestOnline // // Created by Hugues Stéphano TELOLAHY on 27/10/2025. // - extension Card.Effect { func toInstance( withPlayer sourcePlayer: String, diff --git a/Modules/GameFeature/Sources/Misc/Collection+IsNotEmpty.swift b/Modules/GameCore/Sources/Misc/Collection+IsNotEmpty.swift similarity index 100% rename from Modules/GameFeature/Sources/Misc/Collection+IsNotEmpty.swift rename to Modules/GameCore/Sources/Misc/Collection+IsNotEmpty.swift diff --git a/Modules/GameFeature/Sources/Misc/Dictionary+Subscript.swift b/Modules/GameCore/Sources/Misc/Dictionary+Subscript.swift similarity index 100% rename from Modules/GameFeature/Sources/Misc/Dictionary+Subscript.swift rename to Modules/GameCore/Sources/Misc/Dictionary+Subscript.swift diff --git a/Modules/GameFeature/Sources/Misc/GameFeatureAction+Copy.swift b/Modules/GameCore/Sources/Misc/GameFeatureAction+Copy.swift similarity index 99% rename from Modules/GameFeature/Sources/Misc/GameFeatureAction+Copy.swift rename to Modules/GameCore/Sources/Misc/GameFeatureAction+Copy.swift index a26b162c6..cada61532 100644 --- a/Modules/GameFeature/Sources/Misc/GameFeatureAction+Copy.swift +++ b/Modules/GameCore/Sources/Misc/GameFeatureAction+Copy.swift @@ -4,7 +4,6 @@ // // Created by Hugues Stéphano TELOLAHY on 27/10/2025. // - extension GameFeature.Action { func copy( withPlayer sourcePlayer: String? = nil, diff --git a/Modules/GameFeature/Sources/Misc/GameFeatureAction+Description.swift b/Modules/GameCore/Sources/Misc/GameFeatureAction+Description.swift similarity index 99% rename from Modules/GameFeature/Sources/Misc/GameFeatureAction+Description.swift rename to Modules/GameCore/Sources/Misc/GameFeatureAction+Description.swift index dde67534f..b11d7f27f 100644 --- a/Modules/GameFeature/Sources/Misc/GameFeatureAction+Description.swift +++ b/Modules/GameCore/Sources/Misc/GameFeatureAction+Description.swift @@ -4,7 +4,6 @@ // // Created by Hugues Stéphano TELOLAHY on 23/02/2025. // - extension GameFeature.Action: CustomStringConvertible { public var description: String { [ diff --git a/Modules/GameFeature/Sources/Misc/GameFeatureAction+IsResolved.swift b/Modules/GameCore/Sources/Misc/GameFeatureAction+IsResolved.swift similarity index 100% rename from Modules/GameFeature/Sources/Misc/GameFeatureAction+IsResolved.swift rename to Modules/GameCore/Sources/Misc/GameFeatureAction+IsResolved.swift diff --git a/Modules/GameFeature/Sources/Misc/GameFeatureState+Alias.swift b/Modules/GameCore/Sources/Misc/GameFeatureState+Alias.swift similarity index 99% rename from Modules/GameFeature/Sources/Misc/GameFeatureState+Alias.swift rename to Modules/GameCore/Sources/Misc/GameFeatureState+Alias.swift index 1614c8ace..4bb28a896 100644 --- a/Modules/GameFeature/Sources/Misc/GameFeatureState+Alias.swift +++ b/Modules/GameCore/Sources/Misc/GameFeatureState+Alias.swift @@ -4,7 +4,6 @@ // // Created by Hugues Stéphano TELOLAHY on 19/11/2025. // - extension GameFeature.State { func alias( for card: String, diff --git a/Modules/GameFeature/Sources/Misc/GameFeatureState+Display.swift b/Modules/GameCore/Sources/Misc/GameFeatureState+Display.swift similarity index 87% rename from Modules/GameFeature/Sources/Misc/GameFeatureState+Display.swift rename to Modules/GameCore/Sources/Misc/GameFeatureState+Display.swift index 4ca1cfcac..5b964295c 100644 --- a/Modules/GameFeature/Sources/Misc/GameFeatureState+Display.swift +++ b/Modules/GameCore/Sources/Misc/GameFeatureState+Display.swift @@ -4,6 +4,7 @@ // // Created by Hugues Stéphano TELOLAHY on 21/11/2025. // +import Foundation public extension GameFeature.State { func isTargeted(_ player: String) -> Bool { @@ -26,6 +27,10 @@ public extension GameFeature.State { func manuallyControlledPlayer() -> String? { playMode.keys.first { playMode[$0] == .manual } } + + var actionDelaySeconds: TimeInterval { + Double(actionDelayMilliSeconds) / 1000.0 + } } public struct PendingChoice { diff --git a/Modules/GameFeature/Sources/Misc/GameFeatureState+PendingChoice.swift b/Modules/GameCore/Sources/Misc/GameFeatureState+PendingChoice.swift similarity index 99% rename from Modules/GameFeature/Sources/Misc/GameFeatureState+PendingChoice.swift rename to Modules/GameCore/Sources/Misc/GameFeatureState+PendingChoice.swift index 0c33fd857..4a214b9d4 100644 --- a/Modules/GameFeature/Sources/Misc/GameFeatureState+PendingChoice.swift +++ b/Modules/GameCore/Sources/Misc/GameFeatureState+PendingChoice.swift @@ -4,7 +4,6 @@ // // Created by Hugues Stéphano TELOLAHY on 21/11/2025. // - public extension GameFeature.State { var pendingChoice: Card.Selector.ChoicePrompt? { guard let nextAction = queue.first, diff --git a/Modules/GameFeature/Sources/Misc/Player+IsWounded.swift b/Modules/GameCore/Sources/Misc/Player+IsWounded.swift similarity index 100% rename from Modules/GameFeature/Sources/Misc/Player+IsWounded.swift rename to Modules/GameCore/Sources/Misc/Player+IsWounded.swift diff --git a/Modules/GameFeature/Sources/QueueModifierClient/QueueModifierClient+Live.swift b/Modules/GameCore/Sources/QueueModifierClient/QueueModifierClient+Live.swift similarity index 82% rename from Modules/GameFeature/Sources/QueueModifierClient/QueueModifierClient+Live.swift rename to Modules/GameCore/Sources/QueueModifierClient/QueueModifierClient+Live.swift index c7b9a58cc..ba6c092a9 100644 --- a/Modules/GameFeature/Sources/QueueModifierClient/QueueModifierClient+Live.swift +++ b/Modules/GameCore/Sources/QueueModifierClient/QueueModifierClient+Live.swift @@ -4,7 +4,6 @@ // // Created by Hugues Stéphano TELOLAHY on 23/11/2025. // - public extension QueueModifierClient { static func live(handlers: [QueueModifierHandler.Type]) -> Self { let registry = QueueModifierRegistry(handlers: handlers) @@ -16,10 +15,10 @@ public extension QueueModifierClient { } struct QueueModifierRegistry { - let dictionary: [GameFeature.Action.QueueModifier: QueueModifierHandler.Type] + let dictionary: [Card.QueueModifier: QueueModifierHandler.Type] init(handlers: [QueueModifierHandler.Type]) { - var dict: [GameFeature.Action.QueueModifier: QueueModifierHandler.Type] = [:] + var dict: [Card.QueueModifier: QueueModifierHandler.Type] = [:] for type in handlers { dict[type.id] = type } @@ -35,7 +34,7 @@ struct QueueModifierRegistry { } public protocol QueueModifierHandler { - static var id: GameFeature.Action.QueueModifier { get } + static var id: Card.QueueModifier { get } static func apply(_ action: GameFeature.Action, state: GameFeature.State) throws(GameFeature.Error) -> [GameFeature.Action] } diff --git a/Modules/GameFeature/Sources/QueueModifierClient/QueueModifierClient.swift b/Modules/GameCore/Sources/QueueModifierClient/QueueModifierClient.swift similarity index 100% rename from Modules/GameFeature/Sources/QueueModifierClient/QueueModifierClient.swift rename to Modules/GameCore/Sources/QueueModifierClient/QueueModifierClient.swift diff --git a/Modules/GameFeature/Sources/QueueModifierClient/QueueModifierClientKey.swift b/Modules/GameCore/Sources/QueueModifierClient/QueueModifierClientKey.swift similarity index 100% rename from Modules/GameFeature/Sources/QueueModifierClient/QueueModifierClientKey.swift rename to Modules/GameCore/Sources/QueueModifierClient/QueueModifierClientKey.swift diff --git a/Modules/GameFeature/Sources/Rules/AIStrategy.swift b/Modules/GameCore/Sources/Rules/AIStrategy.swift similarity index 99% rename from Modules/GameFeature/Sources/Rules/AIStrategy.swift rename to Modules/GameCore/Sources/Rules/AIStrategy.swift index f5bdaed9a..18eaf15a5 100644 --- a/Modules/GameFeature/Sources/Rules/AIStrategy.swift +++ b/Modules/GameCore/Sources/Rules/AIStrategy.swift @@ -4,7 +4,6 @@ // // Created by Hugues Stéphano TELOLAHY on 03/01/2025. // - public struct AIStrategy { public init() {} diff --git a/Modules/GameFeature/Sources/Rules/CardEffectReducer.swift b/Modules/GameCore/Sources/Rules/CardEffectReducer.swift similarity index 99% rename from Modules/GameFeature/Sources/Rules/CardEffectReducer.swift rename to Modules/GameCore/Sources/Rules/CardEffectReducer.swift index 06121ccaa..8a3476d8c 100644 --- a/Modules/GameFeature/Sources/Rules/CardEffectReducer.swift +++ b/Modules/GameCore/Sources/Rules/CardEffectReducer.swift @@ -4,7 +4,6 @@ // Created by Hugues Telolahy on 30/10/2024. // // swiftlint:disable file_length - extension Card.ActionName { func reduce(_ action: GameFeature.Action, state: GameFeature.State, dependencies: QueueModifierClient) throws(GameFeature.Error) -> GameFeature.State { try reducer.reduce(action, state: state, dependencies: dependencies) diff --git a/Modules/GameFeature/Sources/Rules/CardFilterMatcher.swift b/Modules/GameCore/Sources/Rules/CardFilterMatcher.swift similarity index 99% rename from Modules/GameFeature/Sources/Rules/CardFilterMatcher.swift rename to Modules/GameCore/Sources/Rules/CardFilterMatcher.swift index a0434e666..e11095924 100644 --- a/Modules/GameFeature/Sources/Rules/CardFilterMatcher.swift +++ b/Modules/GameCore/Sources/Rules/CardFilterMatcher.swift @@ -4,7 +4,6 @@ // // Created by Stephano Hugues TELOLAHY on 10/11/2024. // - extension Card.Selector.CardFilter { func match(_ card: String, pendingAction: GameFeature.Action, state: GameFeature.State) -> Bool { matcher.match(card, pendingAction: pendingAction, state: state) diff --git a/Modules/GameFeature/Sources/Rules/CardGroupResolver.swift b/Modules/GameCore/Sources/Rules/CardGroupResolver.swift similarity index 99% rename from Modules/GameFeature/Sources/Rules/CardGroupResolver.swift rename to Modules/GameCore/Sources/Rules/CardGroupResolver.swift index 5ecb96e41..22ee34ebf 100644 --- a/Modules/GameFeature/Sources/Rules/CardGroupResolver.swift +++ b/Modules/GameCore/Sources/Rules/CardGroupResolver.swift @@ -4,7 +4,6 @@ // // Created by Hugues Stephano TELOLAHY on 19/11/2024. // - extension Card.Selector.CardGroup { func resolve(_ pendingAction: GameFeature.Action, state: GameFeature.State) -> [String] { resolver.resolve(pendingAction, state: state) diff --git a/Modules/GameFeature/Sources/Rules/ChoiceRequirementResolver.swift b/Modules/GameCore/Sources/Rules/ChoiceRequirementResolver.swift similarity index 99% rename from Modules/GameFeature/Sources/Rules/ChoiceRequirementResolver.swift rename to Modules/GameCore/Sources/Rules/ChoiceRequirementResolver.swift index eb7a9ca71..2068e08ff 100644 --- a/Modules/GameFeature/Sources/Rules/ChoiceRequirementResolver.swift +++ b/Modules/GameCore/Sources/Rules/ChoiceRequirementResolver.swift @@ -4,7 +4,6 @@ // // Created by Hugues Telolahy on 01/11/2024. // - extension Card.Selector.ChoiceRequirement { func resolveOptions(_ pendingAction: GameFeature.Action, state: GameFeature.State) throws(GameFeature.Error) -> [GameFeature.Action] { try resolver.resolveOptions(self, pendingAction: pendingAction, state: state) diff --git a/Modules/GameFeature/Sources/Rules/GameReducerAI.swift b/Modules/GameCore/Sources/Rules/GameReducerAI.swift similarity index 100% rename from Modules/GameFeature/Sources/Rules/GameReducerAI.swift rename to Modules/GameCore/Sources/Rules/GameReducerAI.swift diff --git a/Modules/GameFeature/Sources/Rules/GameReducerLoop.swift b/Modules/GameCore/Sources/Rules/GameReducerLoop.swift similarity index 99% rename from Modules/GameFeature/Sources/Rules/GameReducerLoop.swift rename to Modules/GameCore/Sources/Rules/GameReducerLoop.swift index 3b14bfb01..644ab0276 100644 --- a/Modules/GameFeature/Sources/Rules/GameReducerLoop.swift +++ b/Modules/GameCore/Sources/Rules/GameReducerLoop.swift @@ -4,7 +4,6 @@ // Created by Hugues Telolahy on 28/10/2024. // import Redux - extension GameFeature { static func reducerLoop( into state: inout State, diff --git a/Modules/GameFeature/Sources/Rules/GameReducerMechanics.swift b/Modules/GameCore/Sources/Rules/GameReducerMechanics.swift similarity index 100% rename from Modules/GameFeature/Sources/Rules/GameReducerMechanics.swift rename to Modules/GameCore/Sources/Rules/GameReducerMechanics.swift diff --git a/Modules/GameFeature/Sources/Rules/NonStandardLogic.swift b/Modules/GameCore/Sources/Rules/NonStandardLogic.swift similarity index 99% rename from Modules/GameFeature/Sources/Rules/NonStandardLogic.swift rename to Modules/GameCore/Sources/Rules/NonStandardLogic.swift index 85df1fe24..96400fddb 100644 --- a/Modules/GameFeature/Sources/Rules/NonStandardLogic.swift +++ b/Modules/GameCore/Sources/Rules/NonStandardLogic.swift @@ -4,7 +4,6 @@ // // Created by Hugues Stéphano TELOLAHY on 23/03/2025. // - enum NonStandardLogic { static func targetedPlayerForTriggeredEffect(_ name: Card.ActionName, parentAction: GameFeature.Action) -> String? { switch name { diff --git a/Modules/GameFeature/Sources/Rules/PlayRequirementMatcher.swift b/Modules/GameCore/Sources/Rules/PlayRequirementMatcher.swift similarity index 99% rename from Modules/GameFeature/Sources/Rules/PlayRequirementMatcher.swift rename to Modules/GameCore/Sources/Rules/PlayRequirementMatcher.swift index f3b2bc49c..cd1eb0c4a 100644 --- a/Modules/GameFeature/Sources/Rules/PlayRequirementMatcher.swift +++ b/Modules/GameCore/Sources/Rules/PlayRequirementMatcher.swift @@ -4,7 +4,6 @@ // // Created by Hugues Telolahy on 30/10/2024. // - extension Card.Selector.PlayRequirement { func match(_ pendingAction: GameFeature.Action, state: GameFeature.State) -> Bool { matcher.match(pendingAction, state: state) diff --git a/Modules/GameFeature/Sources/Rules/PlayerFilterMatcher.swift b/Modules/GameCore/Sources/Rules/PlayerFilterMatcher.swift similarity index 99% rename from Modules/GameFeature/Sources/Rules/PlayerFilterMatcher.swift rename to Modules/GameCore/Sources/Rules/PlayerFilterMatcher.swift index 10860ecb6..649221629 100644 --- a/Modules/GameFeature/Sources/Rules/PlayerFilterMatcher.swift +++ b/Modules/GameCore/Sources/Rules/PlayerFilterMatcher.swift @@ -4,7 +4,6 @@ // // Created by Hugues Telolahy on 31/10/2024. // - extension Card.Selector.PlayerFilter { func match(_ player: String, pendingAction: GameFeature.Action, state: GameFeature.State) -> Bool { matcher.match(player, pendingAction: pendingAction, state: state) diff --git a/Modules/GameFeature/Sources/Rules/PlayerGroupResolver.swift b/Modules/GameCore/Sources/Rules/PlayerGroupResolver.swift similarity index 99% rename from Modules/GameFeature/Sources/Rules/PlayerGroupResolver.swift rename to Modules/GameCore/Sources/Rules/PlayerGroupResolver.swift index 45a9d6f09..30454491c 100644 --- a/Modules/GameFeature/Sources/Rules/PlayerGroupResolver.swift +++ b/Modules/GameCore/Sources/Rules/PlayerGroupResolver.swift @@ -4,7 +4,6 @@ // // Created by Hugues Telolahy on 31/10/2024. // - extension Card.Selector.PlayerGroup { func resolve(_ pendingAction: GameFeature.Action, state: GameFeature.State) -> [String] { resolver.resolve(pendingAction, state: state) diff --git a/Modules/GameFeature/Sources/Rules/RepeatCountResolver.swift b/Modules/GameCore/Sources/Rules/RepeatCountResolver.swift similarity index 99% rename from Modules/GameFeature/Sources/Rules/RepeatCountResolver.swift rename to Modules/GameCore/Sources/Rules/RepeatCountResolver.swift index 8d6ad2d51..d3e5cd79e 100644 --- a/Modules/GameFeature/Sources/Rules/RepeatCountResolver.swift +++ b/Modules/GameCore/Sources/Rules/RepeatCountResolver.swift @@ -4,7 +4,6 @@ // // Created by Hugues Telolahy on 31/10/2024. // - extension Card.Selector.RepeatCount { func resolve(_ pendingAction: GameFeature.Action, state: GameFeature.State) -> Int { resolver.resolve(pendingAction, state: state) diff --git a/Modules/GameFeature/Sources/Rules/SelectorResolver.swift b/Modules/GameCore/Sources/Rules/SelectorResolver.swift similarity index 99% rename from Modules/GameFeature/Sources/Rules/SelectorResolver.swift rename to Modules/GameCore/Sources/Rules/SelectorResolver.swift index 5221a2756..91ac46e8e 100644 --- a/Modules/GameFeature/Sources/Rules/SelectorResolver.swift +++ b/Modules/GameCore/Sources/Rules/SelectorResolver.swift @@ -3,7 +3,6 @@ // // Created by Hugues Telolahy on 30/10/2024. // - extension Card.Selector { func resolve(_ pendingAction: GameFeature.Action, state: GameFeature.State) throws(GameFeature.Error) -> [GameFeature.Action] { try resolver.resolve(pendingAction, state: state) diff --git a/Modules/GameFeature/Sources/Rules/TriggerMatcher.swift b/Modules/GameCore/Sources/Rules/TriggerMatcher.swift similarity index 99% rename from Modules/GameFeature/Sources/Rules/TriggerMatcher.swift rename to Modules/GameCore/Sources/Rules/TriggerMatcher.swift index 6e80c67e3..e024994d5 100644 --- a/Modules/GameFeature/Sources/Rules/TriggerMatcher.swift +++ b/Modules/GameCore/Sources/Rules/TriggerMatcher.swift @@ -4,7 +4,6 @@ // // Created by Hugues Telolahy on 26/07/2025. // - extension Card.Trigger { func match(_ action: GameFeature.Action, card: String, player: String, state: GameFeature.State) -> Bool { matcher.match(action, card: card, player: player, state: state) diff --git a/Modules/GameFeature/Tests/Dispatch.swift b/Modules/GameCore/Tests/Dispatch.swift similarity index 96% rename from Modules/GameFeature/Tests/Dispatch.swift rename to Modules/GameCore/Tests/Dispatch.swift index 2b0d12b51..928f58a0c 100644 --- a/Modules/GameFeature/Tests/Dispatch.swift +++ b/Modules/GameCore/Tests/Dispatch.swift @@ -6,7 +6,7 @@ // import Combine import Redux -@testable import GameFeature +@testable import GameCore @MainActor func dispatch( _ action: GameFeature.Action, diff --git a/Modules/GameFeature/Tests/DistanceTest.swift b/Modules/GameCore/Tests/DistanceTest.swift similarity index 99% rename from Modules/GameFeature/Tests/DistanceTest.swift rename to Modules/GameCore/Tests/DistanceTest.swift index 9af8aa02c..5bcadee88 100644 --- a/Modules/GameFeature/Tests/DistanceTest.swift +++ b/Modules/GameCore/Tests/DistanceTest.swift @@ -5,7 +5,7 @@ // Created by Stephano Hugues TELOLAHY on 05/11/2024. // -@testable import GameFeature +@testable import GameCore import Testing struct DistanceTest { diff --git a/Modules/GameFeature/Tests/GameSetupTest.swift b/Modules/GameCore/Tests/GameSetupTest.swift similarity index 98% rename from Modules/GameFeature/Tests/GameSetupTest.swift rename to Modules/GameCore/Tests/GameSetupTest.swift index 7d13243da..3027bf264 100644 --- a/Modules/GameFeature/Tests/GameSetupTest.swift +++ b/Modules/GameCore/Tests/GameSetupTest.swift @@ -6,7 +6,7 @@ // import Testing -@testable import GameFeature +@testable import GameCore struct GameSetupTest { @Test func setupGame() async throws { diff --git a/Modules/GameFeature/Tests/Reducer/ActivateTest.swift b/Modules/GameCore/Tests/Reducer/ActivateTest.swift similarity index 96% rename from Modules/GameFeature/Tests/Reducer/ActivateTest.swift rename to Modules/GameCore/Tests/Reducer/ActivateTest.swift index 4c66339b5..364c7b96e 100644 --- a/Modules/GameFeature/Tests/Reducer/ActivateTest.swift +++ b/Modules/GameCore/Tests/Reducer/ActivateTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct ActivateTest { @Test func activate() async throws { diff --git a/Modules/GameFeature/Tests/Reducer/DamageTest.swift b/Modules/GameCore/Tests/Reducer/DamageTest.swift similarity index 98% rename from Modules/GameFeature/Tests/Reducer/DamageTest.swift rename to Modules/GameCore/Tests/Reducer/DamageTest.swift index 85cf4fc6c..39c0743c2 100644 --- a/Modules/GameFeature/Tests/Reducer/DamageTest.swift +++ b/Modules/GameCore/Tests/Reducer/DamageTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct DamageTest { @Test func damage_with1LifePoint_shouldReduceHealthBy1() async throws { diff --git a/Modules/GameFeature/Tests/Reducer/DiscardTest.swift b/Modules/GameCore/Tests/Reducer/DiscardTest.swift similarity index 98% rename from Modules/GameFeature/Tests/Reducer/DiscardTest.swift rename to Modules/GameCore/Tests/Reducer/DiscardTest.swift index 6ca8bb00c..25ac5f365 100644 --- a/Modules/GameFeature/Tests/Reducer/DiscardTest.swift +++ b/Modules/GameCore/Tests/Reducer/DiscardTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct DiscardTest { @Test func discard_shouldRemoveCardFromHand() async throws { diff --git a/Modules/GameFeature/Tests/Reducer/DiscoverTest.swift b/Modules/GameCore/Tests/Reducer/DiscoverTest.swift similarity index 99% rename from Modules/GameFeature/Tests/Reducer/DiscoverTest.swift rename to Modules/GameCore/Tests/Reducer/DiscoverTest.swift index 1c3277873..8e3199e5f 100644 --- a/Modules/GameFeature/Tests/Reducer/DiscoverTest.swift +++ b/Modules/GameCore/Tests/Reducer/DiscoverTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct DiscoverTest { @Test func discover_shouldAddCardToDiscovered() async throws { diff --git a/Modules/GameFeature/Tests/Reducer/DrawDeckTest.swift b/Modules/GameCore/Tests/Reducer/DrawDeckTest.swift similarity index 98% rename from Modules/GameFeature/Tests/Reducer/DrawDeckTest.swift rename to Modules/GameCore/Tests/Reducer/DrawDeckTest.swift index dfdb93fc1..c647a0272 100644 --- a/Modules/GameFeature/Tests/Reducer/DrawDeckTest.swift +++ b/Modules/GameCore/Tests/Reducer/DrawDeckTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct DrawDeckTest { @Test func drawDeck_shouldRemoveTopCard() async throws { diff --git a/Modules/GameFeature/Tests/Reducer/DrawDiscardTest.swift b/Modules/GameCore/Tests/Reducer/DrawDiscardTest.swift similarity index 98% rename from Modules/GameFeature/Tests/Reducer/DrawDiscardTest.swift rename to Modules/GameCore/Tests/Reducer/DrawDiscardTest.swift index 6613fbe84..45931e727 100644 --- a/Modules/GameFeature/Tests/Reducer/DrawDiscardTest.swift +++ b/Modules/GameCore/Tests/Reducer/DrawDiscardTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct DrawDiscardTest { @Test func drawDiscard_shouldRemoveTopCard() async throws { diff --git a/Modules/GameFeature/Tests/Reducer/DrawDiscoveredTest.swift b/Modules/GameCore/Tests/Reducer/DrawDiscoveredTest.swift similarity index 97% rename from Modules/GameFeature/Tests/Reducer/DrawDiscoveredTest.swift rename to Modules/GameCore/Tests/Reducer/DrawDiscoveredTest.swift index 8165a955d..12535724b 100644 --- a/Modules/GameFeature/Tests/Reducer/DrawDiscoveredTest.swift +++ b/Modules/GameCore/Tests/Reducer/DrawDiscoveredTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct DrawDiscoveredTest { @Test func drawDiscovered_shouldDrawDeckCard() async throws { diff --git a/Modules/GameFeature/Tests/Reducer/DrawTest.swift b/Modules/GameCore/Tests/Reducer/DrawTest.swift similarity index 98% rename from Modules/GameFeature/Tests/Reducer/DrawTest.swift rename to Modules/GameCore/Tests/Reducer/DrawTest.swift index def597f88..3abb12fc2 100644 --- a/Modules/GameFeature/Tests/Reducer/DrawTest.swift +++ b/Modules/GameCore/Tests/Reducer/DrawTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct DrawTest { @Test func draw_shouldMoveCardFromDeckToDiscard() async throws { diff --git a/Modules/GameFeature/Tests/Reducer/EliminateTest.swift b/Modules/GameCore/Tests/Reducer/EliminateTest.swift similarity index 97% rename from Modules/GameFeature/Tests/Reducer/EliminateTest.swift rename to Modules/GameCore/Tests/Reducer/EliminateTest.swift index 46a01e581..8ea5ccd63 100644 --- a/Modules/GameFeature/Tests/Reducer/EliminateTest.swift +++ b/Modules/GameCore/Tests/Reducer/EliminateTest.swift @@ -6,7 +6,7 @@ // import Testing -@testable import GameFeature +@testable import GameCore struct EliminateTest { @Test func eliminate_shouldRemoveFromPlayOrder() async throws { diff --git a/Modules/GameFeature/Tests/Reducer/EndGameTest.swift b/Modules/GameCore/Tests/Reducer/EndGameTest.swift similarity index 94% rename from Modules/GameFeature/Tests/Reducer/EndGameTest.swift rename to Modules/GameCore/Tests/Reducer/EndGameTest.swift index 799225665..05e305a9b 100644 --- a/Modules/GameFeature/Tests/Reducer/EndGameTest.swift +++ b/Modules/GameCore/Tests/Reducer/EndGameTest.swift @@ -6,7 +6,7 @@ // import Testing -@testable import GameFeature +@testable import GameCore struct EndGameTest { @Test func endGame() async throws { diff --git a/Modules/GameFeature/Tests/Reducer/EndTurnTest.swift b/Modules/GameCore/Tests/Reducer/EndTurnTest.swift similarity index 97% rename from Modules/GameFeature/Tests/Reducer/EndTurnTest.swift rename to Modules/GameCore/Tests/Reducer/EndTurnTest.swift index 3a5df7daa..bb5515e0c 100644 --- a/Modules/GameFeature/Tests/Reducer/EndTurnTest.swift +++ b/Modules/GameCore/Tests/Reducer/EndTurnTest.swift @@ -6,7 +6,7 @@ // import Testing -@testable import GameFeature +@testable import GameCore struct EndTurnTest { @Test func endTurn_shouldUnsetTurn() async throws { diff --git a/Modules/GameFeature/Tests/Reducer/EquipTest.swift b/Modules/GameCore/Tests/Reducer/EquipTest.swift similarity index 98% rename from Modules/GameFeature/Tests/Reducer/EquipTest.swift rename to Modules/GameCore/Tests/Reducer/EquipTest.swift index f46cdb75f..2cc74dc30 100644 --- a/Modules/GameFeature/Tests/Reducer/EquipTest.swift +++ b/Modules/GameCore/Tests/Reducer/EquipTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct EquipTest { @Test func equip_shouldPutCardInPlay() async throws { diff --git a/Modules/GameFeature/Tests/Reducer/HandicapTest.swift b/Modules/GameCore/Tests/Reducer/HandicapTest.swift similarity index 98% rename from Modules/GameFeature/Tests/Reducer/HandicapTest.swift rename to Modules/GameCore/Tests/Reducer/HandicapTest.swift index 9e3deb21c..2d30236cb 100644 --- a/Modules/GameFeature/Tests/Reducer/HandicapTest.swift +++ b/Modules/GameCore/Tests/Reducer/HandicapTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct HandicapTest { @Test func handicap_shouldPutcardInTargetInPlay() async throws { diff --git a/Modules/GameFeature/Tests/Reducer/HealTest.swift b/Modules/GameCore/Tests/Reducer/HealTest.swift similarity index 99% rename from Modules/GameFeature/Tests/Reducer/HealTest.swift rename to Modules/GameCore/Tests/Reducer/HealTest.swift index 13a64415a..9ab45d323 100644 --- a/Modules/GameFeature/Tests/Reducer/HealTest.swift +++ b/Modules/GameCore/Tests/Reducer/HealTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct HealTest { @Test func heal_beingDamaged_amountLessThanDamage_shouldGainLifePoints() async throws { diff --git a/Modules/GameFeature/Tests/Reducer/IncreaseMagnifyingTest.swift b/Modules/GameCore/Tests/Reducer/IncreaseMagnifyingTest.swift similarity index 96% rename from Modules/GameFeature/Tests/Reducer/IncreaseMagnifyingTest.swift rename to Modules/GameCore/Tests/Reducer/IncreaseMagnifyingTest.swift index 7a576896b..863710771 100644 --- a/Modules/GameFeature/Tests/Reducer/IncreaseMagnifyingTest.swift +++ b/Modules/GameCore/Tests/Reducer/IncreaseMagnifyingTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct IncreaseMagnifyingTest { @Test func increaseMagnifying() async throws { diff --git a/Modules/GameFeature/Tests/Reducer/IncreaseRemotenessTest.swift b/Modules/GameCore/Tests/Reducer/IncreaseRemotenessTest.swift similarity index 96% rename from Modules/GameFeature/Tests/Reducer/IncreaseRemotenessTest.swift rename to Modules/GameCore/Tests/Reducer/IncreaseRemotenessTest.swift index b0eaf71e5..4d9f3c641 100644 --- a/Modules/GameFeature/Tests/Reducer/IncreaseRemotenessTest.swift +++ b/Modules/GameCore/Tests/Reducer/IncreaseRemotenessTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct IncreaseRemotenessTest { @Test func increaseRemoteness() async throws { diff --git a/Modules/GameFeature/Tests/Reducer/PassInPlayTest.swift b/Modules/GameCore/Tests/Reducer/PassInPlayTest.swift similarity index 97% rename from Modules/GameFeature/Tests/Reducer/PassInPlayTest.swift rename to Modules/GameCore/Tests/Reducer/PassInPlayTest.swift index 235aa9d3e..7edc994af 100644 --- a/Modules/GameFeature/Tests/Reducer/PassInPlayTest.swift +++ b/Modules/GameCore/Tests/Reducer/PassInPlayTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct PassInPlayTest { @Test func passInPlay_shouldRemoveCardFromInPlay() async throws { diff --git a/Modules/GameFeature/Tests/Reducer/PlayTest.swift b/Modules/GameCore/Tests/Reducer/PlayTest.swift similarity index 97% rename from Modules/GameFeature/Tests/Reducer/PlayTest.swift rename to Modules/GameCore/Tests/Reducer/PlayTest.swift index a3038e8d6..4a0ae3f2c 100644 --- a/Modules/GameFeature/Tests/Reducer/PlayTest.swift +++ b/Modules/GameCore/Tests/Reducer/PlayTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct PlayTest { @Test func play_shouldRemoveCardFromHand() async throws { diff --git a/Modules/GameFeature/Tests/Reducer/PreparePlayTest.swift b/Modules/GameCore/Tests/Reducer/PreparePlayTest.swift similarity index 98% rename from Modules/GameFeature/Tests/Reducer/PreparePlayTest.swift rename to Modules/GameCore/Tests/Reducer/PreparePlayTest.swift index 6e2a6119b..80907d94f 100644 --- a/Modules/GameFeature/Tests/Reducer/PreparePlayTest.swift +++ b/Modules/GameCore/Tests/Reducer/PreparePlayTest.swift @@ -6,7 +6,7 @@ // import Testing -@testable import GameFeature +@testable import GameCore struct PreparePlayTest { @Test func preparePlay_shouldQueueEffects() async throws { diff --git a/Modules/GameFeature/Tests/Reducer/SetWeaponTest.swift b/Modules/GameCore/Tests/Reducer/SetWeaponTest.swift similarity index 96% rename from Modules/GameFeature/Tests/Reducer/SetWeaponTest.swift rename to Modules/GameCore/Tests/Reducer/SetWeaponTest.swift index 041c0b81a..af9e8dccf 100644 --- a/Modules/GameFeature/Tests/Reducer/SetWeaponTest.swift +++ b/Modules/GameCore/Tests/Reducer/SetWeaponTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct SetWeaponTest { @Test func setWeapon_shouldSetValue() async throws { diff --git a/Modules/GameFeature/Tests/Reducer/ShootTest.swift b/Modules/GameCore/Tests/Reducer/ShootTest.swift similarity index 96% rename from Modules/GameFeature/Tests/Reducer/ShootTest.swift rename to Modules/GameCore/Tests/Reducer/ShootTest.swift index 3b8b1ee59..c49e98fd8 100644 --- a/Modules/GameFeature/Tests/Reducer/ShootTest.swift +++ b/Modules/GameCore/Tests/Reducer/ShootTest.swift @@ -6,7 +6,7 @@ // import Testing -@testable import GameFeature +@testable import GameCore struct ShootTest { @Test func shoot() async throws { diff --git a/Modules/GameFeature/Tests/Reducer/StartTurnTest.swift b/Modules/GameCore/Tests/Reducer/StartTurnTest.swift similarity index 96% rename from Modules/GameFeature/Tests/Reducer/StartTurnTest.swift rename to Modules/GameCore/Tests/Reducer/StartTurnTest.swift index af0bf08bb..cec242d6f 100644 --- a/Modules/GameFeature/Tests/Reducer/StartTurnTest.swift +++ b/Modules/GameCore/Tests/Reducer/StartTurnTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct StartTurnTest { @Test func startTurn_shouldSetTurn() async throws { diff --git a/Modules/GameFeature/Tests/Reducer/StealTest.swift b/Modules/GameCore/Tests/Reducer/StealTest.swift similarity index 98% rename from Modules/GameFeature/Tests/Reducer/StealTest.swift rename to Modules/GameCore/Tests/Reducer/StealTest.swift index db1ac0519..1f3ae51ef 100644 --- a/Modules/GameFeature/Tests/Reducer/StealTest.swift +++ b/Modules/GameCore/Tests/Reducer/StealTest.swift @@ -6,7 +6,7 @@ // import Testing -import GameFeature +import GameCore struct StealTest { @Test func steal_shouldRemoveCardFromTargetHand() async throws { diff --git a/Modules/GameFeature/Tests/Reducer/UndiscoverTest.swift b/Modules/GameCore/Tests/Reducer/UndiscoverTest.swift similarity index 96% rename from Modules/GameFeature/Tests/Reducer/UndiscoverTest.swift rename to Modules/GameCore/Tests/Reducer/UndiscoverTest.swift index 3d9fb4152..de958da8f 100644 --- a/Modules/GameFeature/Tests/Reducer/UndiscoverTest.swift +++ b/Modules/GameCore/Tests/Reducer/UndiscoverTest.swift @@ -5,7 +5,7 @@ // Created by Hugues Stéphano TELOLAHY on 12/11/2025. // import Testing -import GameFeature +import GameCore struct UndiscoverTest { @Test func undiscover_shouldResetDiscoveredCards() async throws { diff --git a/Modules/GameUI/Sources/AnimationMatcher.swift b/Modules/GameSessionFeature/Sources/Components/AnimationMatcher.swift similarity index 99% rename from Modules/GameUI/Sources/AnimationMatcher.swift rename to Modules/GameSessionFeature/Sources/Components/AnimationMatcher.swift index 435c92262..6bee2d6cb 100644 --- a/Modules/GameUI/Sources/AnimationMatcher.swift +++ b/Modules/GameSessionFeature/Sources/Components/AnimationMatcher.swift @@ -6,7 +6,7 @@ // // swiftlint:disable force_unwrapping -import GameFeature +import GameCore struct AnimationMatcher { // swiftlint:disable:next cyclomatic_complexity function_body_length diff --git a/Modules/GameSessionFeature/Sources/Components/CardContent.swift b/Modules/GameSessionFeature/Sources/Components/CardContent.swift new file mode 100644 index 000000000..3f537a7b2 --- /dev/null +++ b/Modules/GameSessionFeature/Sources/Components/CardContent.swift @@ -0,0 +1,11 @@ +// +// CardContent.swift +// WildWestOnline +// +// Created by Hugues Stéphano TELOLAHY on 05/12/2025. +// + +enum CardContent: Equatable { + case id(String) + case hidden +} diff --git a/Modules/GameUI/Sources/CardView.swift b/Modules/GameSessionFeature/Sources/Components/CardView.swift similarity index 97% rename from Modules/GameUI/Sources/CardView.swift rename to Modules/GameSessionFeature/Sources/Components/CardView.swift index d9a44b959..128928c27 100644 --- a/Modules/GameUI/Sources/CardView.swift +++ b/Modules/GameSessionFeature/Sources/Components/CardView.swift @@ -6,10 +6,9 @@ // import SwiftUI -import GameFeature +import GameCore import CardResources -/// A view that displays a card struct CardView: View { let content: CardContent var format: Format = .medium diff --git a/Modules/GameSessionFeature/Sources/Components/GameArea.swift b/Modules/GameSessionFeature/Sources/Components/GameArea.swift new file mode 100644 index 000000000..e17879124 --- /dev/null +++ b/Modules/GameSessionFeature/Sources/Components/GameArea.swift @@ -0,0 +1,14 @@ +// +// GameArea.swift +// WildWestOnline +// +// Created by Hugues Stéphano TELOLAHY on 05/12/2025. +// + +enum GameArea: Hashable { + case deck + case discard + case discovered + case playerHand(String) + case playerInPlay(String) +} diff --git a/Modules/GameUI/Sources/PlayerView.swift b/Modules/GameSessionFeature/Sources/Components/PlayerView.swift similarity index 91% rename from Modules/GameUI/Sources/PlayerView.swift rename to Modules/GameSessionFeature/Sources/Components/PlayerView.swift index 021b151fe..4391a7059 100644 --- a/Modules/GameUI/Sources/PlayerView.swift +++ b/Modules/GameSessionFeature/Sources/Components/PlayerView.swift @@ -7,9 +7,8 @@ import SwiftUI import CardResources -// Displays a player's information including figure image, name, role, health, hand count, and in-play cards. struct PlayerView: View { - var player: GameView.ViewState.PlayerItem + var player: GameSessionFeature.State.Player @Environment(\.theme) private var theme @@ -60,7 +59,7 @@ struct PlayerView: View { } } -private extension GameView.ViewState.PlayerItem { +private extension GameSessionFeature.State.Player { var foregroundColor: Color { if isTargeted { Color(.systemRed) diff --git a/Modules/AppFeature/Sources/SoundMatcher.swift b/Modules/GameSessionFeature/Sources/Components/SoundMatcher.swift similarity index 98% rename from Modules/AppFeature/Sources/SoundMatcher.swift rename to Modules/GameSessionFeature/Sources/Components/SoundMatcher.swift index 5e881027b..58574a282 100644 --- a/Modules/AppFeature/Sources/SoundMatcher.swift +++ b/Modules/GameSessionFeature/Sources/Components/SoundMatcher.swift @@ -5,7 +5,7 @@ // Created by Hugues Stéphano TELOLAHY on 28/09/2025. // -import GameFeature +import GameCore import AudioClient struct SoundMatcher { diff --git a/Modules/GameSessionFeature/Sources/GameSessionFeature.swift b/Modules/GameSessionFeature/Sources/GameSessionFeature.swift new file mode 100644 index 000000000..df5d5f377 --- /dev/null +++ b/Modules/GameSessionFeature/Sources/GameSessionFeature.swift @@ -0,0 +1,309 @@ +// +// GameSessionFeature.swift +// WildWestOnline +// +// Created by Hugues Stéphano TELOLAHY on 05/12/2025. +// + +import Foundation +import Redux +import GameCore +import CardLibrary +import PreferencesClient + +public enum GameSessionFeature { + public struct State: Equatable { + var game: GameFeature.State? + + public init(game: GameFeature.State? = nil) { + self.game = game + } + } + + public enum Action { + // View + case didAppear + case didTapQuit + case didTapSettings + case didTapCard(String) + case didChoose(String, player: String) + + // Internal + case setGame(GameFeature.State) + case game(GameFeature.Action) + + // Delegate + case delegate(Delegate) + + public enum Delegate { + case quit + case settings + } + } + + public static var reducer: Reducer { + combine( + reducerMain, + reducerSound, + pullback( + GameFeature.reducer, + state: { + $0.game != nil ? \.game! : nil + }, + action: { globalAction in + if case let .game(localAction) = globalAction { + return localAction + } + return nil + }, + embedAction: { + .game($0) + } + ) + ) + } + + private static func reducerMain( + into state: inout State, + action: Action, + dependencies: Dependencies + ) -> Effect { + switch action { + case .didAppear: + return .run { + .setGame(dependencies.createGame()) + } + + case .didTapQuit: + return .run { + .delegate(.quit) + } + + case .didTapSettings: + return .run { + .delegate(.settings) + } + + case .didTapCard(let card): + guard let controlledPlayer = state.controlledPlayer else { + return .none + } + return .run { + .game(.preparePlay(card, player: controlledPlayer)) + } + + case .didChoose(let option, let chooser): + return .run { + .game(.choose(option, player: chooser)) + } + + case .setGame(let game): + state.game = game + return .run { + .game(.startTurn(player: game.startOrder[0])) + } + + case .game: + return .none + + case .delegate: + return .none + } + } + + private static func reducerSound( + into state: inout State, + action: Action, + dependencies: Dependencies + ) -> Effect { + switch action { + case .game(let gameAction): + let soundMatcher = SoundMatcher(specialSounds: dependencies.cardLibrary.specialSounds()) + if let sfx = soundMatcher.sfx(on: gameAction) { + let playFunc = dependencies.audioClient.play + Task { + await playFunc(sfx) + } + } + return .none + + default: + return .none + } + } +} + +private extension Dependencies { + func createGame() -> GameFeature.State { + GameSetup.buildGame( + playersCount: preferencesClient.playersCount(), + cards: cardLibrary.cards(), + deck: cardLibrary.deck(), + actionDelayMilliSeconds: preferencesClient.actionDelayMilliSeconds(), + preferredFigure: preferencesClient.preferredFigure(), + playModeSetup: preferencesClient.isSimulationEnabled() ? .allAuto : .oneManual + ) + } +} + +extension GameSessionFeature.State { + struct Player: Equatable { + let id: String + let imageName: String + let displayName: String + let health: Int + let handCount: Int + let inPlay: [String] + let isTurn: Bool + let isTargeted: Bool + let isEliminated: Bool + let role: String? + let userPhotoUrl: String? + } + + struct HandCard: Equatable { + let card: String + let active: Bool + } + + struct ChooseOne: Equatable { + let resolvingAction: Card.ActionName + let chooser: String + let options: [String] + } + + var players: [Player] { + guard let game else { + return [] + } + + return game.startOrder.map { playerId in + let playerObj = game.players.get(playerId) + let health = max(0, playerObj.health) + let handCount = playerObj.hand.count + let equipment = playerObj.inPlay + let isTurn = playerId == game.turn + let isEliminated = !game.playOrder.contains(playerId) + let isTargeted = game.isTargeted(playerId) + let name = playerObj.figure.first ?? "" + + return .init( + id: playerId, + imageName: name, + displayName: name.uppercased(), + health: health, + handCount: handCount, + inPlay: equipment, + isTurn: isTurn, + isTargeted: isTargeted, + isEliminated: isEliminated, + role: nil, + userPhotoUrl: nil + ) + } + } + + var message: String { + if let turn = game?.turn { + "\(turn.uppercased())'s turn" + } else { + "-" + } + } + + var chooseOne: ChooseOne? { + guard let game, + let controlledPlayer = game.manuallyControlledPlayer(), + let choice = game.pendingChoice(for: controlledPlayer) else { + return nil + } + + return .init( + resolvingAction: choice.action, + chooser: choice.prompt.chooser, + options: choice.prompt.options.map(\.label) + ) + } + + var handCards: [HandCard] { + guard let game, + let controlledPlayer = game.manuallyControlledPlayer() else { + return [] + } + + var playableCards: [String] = [] + if let playable = game.playable, + playable.player == controlledPlayer { + playableCards = playable.cards + } + let handCards = game.players.get(controlledPlayer).hand + + let hand = handCards.map { card in + HandCard( + card: card, + active: playableCards.contains(card) + ) + } + + let abilities = playableCards.compactMap { card in + if !handCards.contains(card) { + HandCard( + card: card, + active: true + ) + } else { + nil + } + } + + return hand + abilities + } + + var topDiscard: String? { + guard let game else { + return nil + } + + return game.discard.last + } + + var deckCount: Int { + guard let game else { + return 0 + } + + return game.deck.count + } + + var startOrder: [String] { + guard let game else { + return [] + } + + return game.startOrder + } + + var actionDelaySeconds: TimeInterval { + guard let game else { + return 0 + } + + return game.actionDelaySeconds + } + + var lastEvent: GameFeature.Action? { + guard let game else { + return nil + } + + return game.lastEvent + } + + var controlledPlayer: String? { + guard let game else { + return nil + } + + return game.manuallyControlledPlayer() + } +} diff --git a/Modules/GameUI/Sources/GameView.swift b/Modules/GameSessionFeature/Sources/GameSessionView.swift similarity index 65% rename from Modules/GameUI/Sources/GameView.swift rename to Modules/GameSessionFeature/Sources/GameSessionView.swift index 4fe9b02f3..dd5baf4e9 100644 --- a/Modules/GameUI/Sources/GameView.swift +++ b/Modules/GameSessionFeature/Sources/GameSessionView.swift @@ -1,5 +1,5 @@ // -// GameView.swift +// GameSessionView.swift // // // Created by Stephano Hugues TELOLAHY on 23/03/2024. @@ -7,24 +7,15 @@ // swiftlint:disable identifier_name force_unwrapping import SwiftUI +import Redux import Theme -import GameFeature - -enum GameArea: Hashable { - case deck - case discard - case discovered - case playerHand(String) - case playerInPlay(String) -} +import GameCore -enum CardContent: Equatable { - case id(String) - case hidden -} +public struct GameSessionView: View { + public typealias ViewStore = Store -public struct GameView: View { @StateObject private var store: ViewStore + @State private var animationSource: CGPoint = .zero @State private var animationTarget: CGPoint = .zero @State private var animatedCard: CardContent? @@ -65,7 +56,7 @@ public struct GameView: View { #endif .toolbar { toolBarView } .task { - await store.dispatch(.game(.startTurn(player: store.state.startPlayer))) + await store.dispatch(.didAppear) } .onReceive(store.$state) { state in if let action = state.lastEvent { @@ -76,13 +67,13 @@ public struct GameView: View { } } -private extension GameView { +private extension GameSessionView { var toolBarView: some ToolbarContent { ToolbarItem(placement: .automatic) { Menu { Button { Task { - await store.dispatch(.navigation(.presentSettingsSheet)) + await store.dispatch(.didTapSettings) } } label: { Label("Settings", systemImage: "gearshape") @@ -91,7 +82,7 @@ private extension GameView { Divider() Button(role: .destructive) { - Task { await store.dispatch(.quit) } + Task { await store.dispatch(.didTapQuit) } } label: { Label { Text(.gameQuitButton) @@ -108,8 +99,10 @@ private extension GameView { let players = store.state.players let topDiscard: CardContent? = store.state.topDiscard.map { .id($0) } - CardView(content: .hidden) - .position(positions[.deck]!) + if store.state.deckCount > 0 { + CardView(content: .hidden) + .position(positions[.deck]!) + } if let topDiscard { ZStack { @@ -139,51 +132,49 @@ private extension GameView { } @ViewBuilder func controlledHandView() -> some View { - if let player = store.state.controlledPlayer { - VStack(spacing: 0) { - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 12) { - ForEach(store.state.handCards, id: \.card) { item in - Button( - action: { - guard item.active else { - return - } - - Task { - await store.dispatch(.game(.preparePlay(item.card, player: player))) - } - }, - label: { - CardView( - content: .id(item.card), - format: .large, - disabled: !item.active - ) - .scaleEffect(item.active ? 1.04 : 1.0) - .shadow(color: (item.active ? Color.accentColor : .black).opacity(item.active ? 0.45 : 0.25), radius: item.active ? 14 : 8, x: 0, y: item.active ? 10 : 6) - .animation(.spring(response: 0.3, dampingFraction: 0.85), value: item.active) + VStack(spacing: 0) { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 12) { + ForEach(store.state.handCards, id: \.card) { item in + Button( + action: { + guard item.active else { + return } - ) - .buttonStyle(PlainButtonStyle()) - } + + Task { + await store.dispatch(.didTapCard(item.card)) + } + }, + label: { + CardView( + content: .id(item.card), + format: .large, + disabled: !item.active + ) + .scaleEffect(item.active ? 1.04 : 1.0) + .shadow(color: (item.active ? Color.accentColor : .black).opacity(item.active ? 0.45 : 0.25), radius: item.active ? 14 : 8, x: 0, y: item.active ? 10 : 6) + .animation(.spring(response: 0.3, dampingFraction: 0.85), value: item.active) + } + ) + .buttonStyle(PlainButtonStyle()) } - .padding(.horizontal, 16) - .padding(.vertical, 10) } + .padding(.horizontal, 16) + .padding(.vertical, 10) } - .background( - RoundedRectangle(cornerRadius: 20, style: .continuous) - .fill(.ultraThinMaterial) - .overlay( - RoundedRectangle(cornerRadius: 20, style: .continuous) - .stroke(Color.white.opacity(0.15), lineWidth: 1) - ) - .shadow(color: .black.opacity(0.18), radius: 12, x: 0, y: 6) - ) - .padding(.horizontal, 12) - .padding(.bottom, 8) } + .background( + RoundedRectangle(cornerRadius: 20, style: .continuous) + .fill(.ultraThinMaterial) + .overlay( + RoundedRectangle(cornerRadius: 20, style: .continuous) + .stroke(Color.white.opacity(0.15), lineWidth: 1) + ) + .shadow(color: .black.opacity(0.18), radius: 12, x: 0, y: 6) + ) + .padding(.horizontal, 12) + .padding(.bottom, 8) } func chooseOneAnchorView() -> some View { @@ -199,7 +190,9 @@ private extension GameView { actions: { chooseOne in ForEach(chooseOne.options, id: \.self) { option in let button = Button(option) { - Task { await self.store.dispatch(.game(.choose(option, player: chooseOne.chooser))) } + Task { + await self.store.dispatch(.didChoose(option, player: chooseOne.chooser)) + } } if option == .choicePass { @@ -314,77 +307,46 @@ private struct TurnBadge: View { #Preview { NavigationStack { - GameView { + GameSessionView { .init(initialState: .previewState) } } } -private extension GameView.ViewState { +private extension GameSessionFeature.State { static var previewState: Self { - let player1 = GameView.ViewState.PlayerItem( - id: "p1", - imageName: "willyTheKid", - displayName: "willyTheKid", - health: 2, - handCount: 5, - inPlay: ["scope", "jail"], - isTurn: true, - isTargeted: false, - isEliminated: false, - role: nil, - userPhotoUrl: nil - ) - - let player2 = GameView.ViewState.PlayerItem( - id: "p2", - imageName: "calamityJanet", - displayName: "calamityJanet", - health: 1, - handCount: 0, - inPlay: ["scope", "jail"], - isTurn: false, - isTargeted: false, - isEliminated: false, - role: nil, - userPhotoUrl: nil - ) - - let player3 = GameView.ViewState.PlayerItem( - id: "p3", - imageName: "elGringo", - displayName: "elGringo", - health: 0, - handCount: 0, - inPlay: [], - isTurn: false, - isTargeted: false, - isEliminated: true, - role: nil, - userPhotoUrl: nil - ) - - return .init( - players: [player1, player2, player3], - message: "P1's turn", - chooseOne: .init( - resolvingAction: .counterShot, - chooser: "p1", - options: ["o1", "o2", .choicePass] - ), - handCards: [ - .init(card: "mustang-2♥️", active: false), - .init(card: "gatling-4♣️", active: true), - .init(card: "endTurn", active: true) - ], - topDiscard: "bang-A♦️", - topDeck: nil, - startOrder: [], - deckCount: 12, - controlledPlayer: "p1", - startPlayer: "p1", - actionDelaySeconds: 0.5, - lastEvent: nil + .init( + game: GameFeature.State.makeBuilder() + .withPlayer("p1") { + $0.withFigure([.willyTheKid]) + .withHealth(2) + .withHand([.backfire, .bandidos, .barrel]) + .withInPlay([.scope, .jail]) + } + .withPlayer("p2") { + $0.withFigure([.calamityJanet]) + .withHealth(1) + .withHand([.backfire, .bandidos, .barrel]) + .withInPlay([.scope, .jail]) + } + .withPlayer("p3") { + $0.withFigure([.elGringo]) + .withHealth(0) + } + .withTurn("p1") + .withPendingAction( + name: .counterShot, + prompt: .init( + chooser: "p1", + options: [ + .init(id: .missed, label: "o1"), + .init(id: .dodge, label: "o2"), + .init(id: .choicePass, label: .choicePass), + ] + ) + ) + .withDiscard(["bang-A♦️"]) + .build() ) } } diff --git a/Modules/GameUI/Sources/Resources/Assets.xcassets/Contents.json b/Modules/GameSessionFeature/Sources/Resources/Assets.xcassets/Contents.json similarity index 100% rename from Modules/GameUI/Sources/Resources/Assets.xcassets/Contents.json rename to Modules/GameSessionFeature/Sources/Resources/Assets.xcassets/Contents.json diff --git a/Modules/GameUI/Sources/Resources/Assets.xcassets/card_back.imageset/01_back.png b/Modules/GameSessionFeature/Sources/Resources/Assets.xcassets/card_back.imageset/01_back.png similarity index 100% rename from Modules/GameUI/Sources/Resources/Assets.xcassets/card_back.imageset/01_back.png rename to Modules/GameSessionFeature/Sources/Resources/Assets.xcassets/card_back.imageset/01_back.png diff --git a/Modules/GameUI/Sources/Resources/Assets.xcassets/card_back.imageset/Contents.json b/Modules/GameSessionFeature/Sources/Resources/Assets.xcassets/card_back.imageset/Contents.json similarity index 100% rename from Modules/GameUI/Sources/Resources/Assets.xcassets/card_back.imageset/Contents.json rename to Modules/GameSessionFeature/Sources/Resources/Assets.xcassets/card_back.imageset/Contents.json diff --git a/Modules/GameUI/Sources/Resources/Assets.xcassets/roles/Contents.json b/Modules/GameSessionFeature/Sources/Resources/Assets.xcassets/roles/Contents.json similarity index 100% rename from Modules/GameUI/Sources/Resources/Assets.xcassets/roles/Contents.json rename to Modules/GameSessionFeature/Sources/Resources/Assets.xcassets/roles/Contents.json diff --git a/Modules/GameUI/Sources/Resources/Assets.xcassets/roles/deputy.imageset/01_vice-removebg-preview.png b/Modules/GameSessionFeature/Sources/Resources/Assets.xcassets/roles/deputy.imageset/01_vice-removebg-preview.png similarity index 100% rename from Modules/GameUI/Sources/Resources/Assets.xcassets/roles/deputy.imageset/01_vice-removebg-preview.png rename to Modules/GameSessionFeature/Sources/Resources/Assets.xcassets/roles/deputy.imageset/01_vice-removebg-preview.png diff --git a/Modules/GameUI/Sources/Resources/Assets.xcassets/roles/deputy.imageset/Contents.json b/Modules/GameSessionFeature/Sources/Resources/Assets.xcassets/roles/deputy.imageset/Contents.json similarity index 100% rename from Modules/GameUI/Sources/Resources/Assets.xcassets/roles/deputy.imageset/Contents.json rename to Modules/GameSessionFeature/Sources/Resources/Assets.xcassets/roles/deputy.imageset/Contents.json diff --git a/Modules/GameUI/Sources/Resources/Assets.xcassets/roles/outlaw.imageset/01_fuorilegge-removebg-preview.png b/Modules/GameSessionFeature/Sources/Resources/Assets.xcassets/roles/outlaw.imageset/01_fuorilegge-removebg-preview.png similarity index 100% rename from Modules/GameUI/Sources/Resources/Assets.xcassets/roles/outlaw.imageset/01_fuorilegge-removebg-preview.png rename to Modules/GameSessionFeature/Sources/Resources/Assets.xcassets/roles/outlaw.imageset/01_fuorilegge-removebg-preview.png diff --git a/Modules/GameUI/Sources/Resources/Assets.xcassets/roles/outlaw.imageset/Contents.json b/Modules/GameSessionFeature/Sources/Resources/Assets.xcassets/roles/outlaw.imageset/Contents.json similarity index 100% rename from Modules/GameUI/Sources/Resources/Assets.xcassets/roles/outlaw.imageset/Contents.json rename to Modules/GameSessionFeature/Sources/Resources/Assets.xcassets/roles/outlaw.imageset/Contents.json diff --git a/Modules/GameUI/Sources/Resources/Assets.xcassets/roles/renegade.imageset/01_rinnegato-removebg-preview.png b/Modules/GameSessionFeature/Sources/Resources/Assets.xcassets/roles/renegade.imageset/01_rinnegato-removebg-preview.png similarity index 100% rename from Modules/GameUI/Sources/Resources/Assets.xcassets/roles/renegade.imageset/01_rinnegato-removebg-preview.png rename to Modules/GameSessionFeature/Sources/Resources/Assets.xcassets/roles/renegade.imageset/01_rinnegato-removebg-preview.png diff --git a/Modules/GameUI/Sources/Resources/Assets.xcassets/roles/renegade.imageset/Contents.json b/Modules/GameSessionFeature/Sources/Resources/Assets.xcassets/roles/renegade.imageset/Contents.json similarity index 100% rename from Modules/GameUI/Sources/Resources/Assets.xcassets/roles/renegade.imageset/Contents.json rename to Modules/GameSessionFeature/Sources/Resources/Assets.xcassets/roles/renegade.imageset/Contents.json diff --git a/Modules/GameUI/Sources/Resources/Assets.xcassets/roles/sheriff.imageset/01_sceriffo-removebg-preview.png b/Modules/GameSessionFeature/Sources/Resources/Assets.xcassets/roles/sheriff.imageset/01_sceriffo-removebg-preview.png similarity index 100% rename from Modules/GameUI/Sources/Resources/Assets.xcassets/roles/sheriff.imageset/01_sceriffo-removebg-preview.png rename to Modules/GameSessionFeature/Sources/Resources/Assets.xcassets/roles/sheriff.imageset/01_sceriffo-removebg-preview.png diff --git a/Modules/GameUI/Sources/Resources/Assets.xcassets/roles/sheriff.imageset/Contents.json b/Modules/GameSessionFeature/Sources/Resources/Assets.xcassets/roles/sheriff.imageset/Contents.json similarity index 100% rename from Modules/GameUI/Sources/Resources/Assets.xcassets/roles/sheriff.imageset/Contents.json rename to Modules/GameSessionFeature/Sources/Resources/Assets.xcassets/roles/sheriff.imageset/Contents.json diff --git a/Modules/GameUI/Sources/Resources/Localizable.xcstrings b/Modules/GameSessionFeature/Sources/Resources/Localizable.xcstrings similarity index 100% rename from Modules/GameUI/Sources/Resources/Localizable.xcstrings rename to Modules/GameSessionFeature/Sources/Resources/Localizable.xcstrings diff --git a/Modules/GameUI/Tests/AnimationMatcherTest.swift b/Modules/GameSessionFeature/Tests/AnimationMatcherTest.swift similarity index 98% rename from Modules/GameUI/Tests/AnimationMatcherTest.swift rename to Modules/GameSessionFeature/Tests/AnimationMatcherTest.swift index 40b50a16a..b548c7532 100644 --- a/Modules/GameUI/Tests/AnimationMatcherTest.swift +++ b/Modules/GameSessionFeature/Tests/AnimationMatcherTest.swift @@ -6,8 +6,8 @@ // import Testing -@testable import GameUI -import GameFeature +import GameCore +@testable import GameSessionFeature struct AnimationMatcherTest { private let sut = AnimationMatcher() diff --git a/Modules/GameUI/Tests/GameViewTest.swift b/Modules/GameSessionFeature/Tests/GameSessionFeatureTest.swift similarity index 68% rename from Modules/GameUI/Tests/GameViewTest.swift rename to Modules/GameSessionFeature/Tests/GameSessionFeatureTest.swift index 0fe5ca1bd..04af22fce 100644 --- a/Modules/GameUI/Tests/GameViewTest.swift +++ b/Modules/GameSessionFeature/Tests/GameSessionFeatureTest.swift @@ -1,18 +1,15 @@ // -// GameViewTest.swift +// GameSessionFeatureTest.swift // // // Created by Hugues Stephano TELOLAHY on 25/03/2024. // -@testable import GameUI import Testing -import AppFeature -import CardResources -import GameFeature -import SettingsFeature +import GameCore +@testable import GameSessionFeature -struct GameViewTest { +struct GameSessionFeatureTest { @Test func shouldDisplayCurrentTurnPlayer() async throws { // Given let game = GameFeature.State.makeBuilder() @@ -20,18 +17,12 @@ struct GameViewTest { .withPlayMode(["p1": .manual]) .withPlayer("p1") .build() - let appState = AppFeature.State( - cardLibrary: .init(), - navigation: .init(), - settings: SettingsFeature.State.makeBuilder().build(), - game: game - ) + let state = GameSessionFeature.State(game: game) // When - let viewState = try #require(GameView.ViewState(appState: appState)) // Then - #expect(viewState.message == "P1's turn") + #expect(state.message == "P1's turn") } @Test func shouldDisplayStatusForEachPlayers() async throws { @@ -50,20 +41,13 @@ struct GameViewTest { .withPlayMode(["p1": .manual]) .withTurn("p1") .build() - let appState = AppFeature.State( - cardLibrary: .init(), - navigation: .init(), - settings: SettingsFeature.State.makeBuilder().build(), - game: game - ) + let state = GameSessionFeature.State(game: game) // When - let viewState = try #require(GameView.ViewState(appState: appState)) - // Then - #expect(viewState.players.count == 2) + #expect(state.players.count == 2) - let player1 = viewState.players[0] + let player1 = state.players[0] #expect(player1.id == "p1") #expect(player1.imageName == "willyTheKid") #expect(player1.displayName == "WILLYTHEKID") @@ -73,7 +57,7 @@ struct GameViewTest { #expect(player1.isTurn) #expect(!player1.isEliminated) - let player2 = viewState.players[1] + let player2 = state.players[1] #expect(player2.id == "p2") #expect(player2.imageName == "paulRegret") #expect(player2.displayName == "PAULREGRET") @@ -99,18 +83,13 @@ struct GameViewTest { .withPlayable([.bang, .endTurn], player: "p1") .withAuras([.endTurn]) .build() - let appState = AppFeature.State( - cardLibrary: .init(), - navigation: .init(), - settings: SettingsFeature.State.makeBuilder().build(), + let state = GameSessionFeature.State( game: game ) // When - let viewState = try #require(GameView.ViewState(appState: appState)) - // Then - #expect(viewState.handCards == [ + #expect(state.handCards == [ .init(card: .bang, active: true), .init(card: .gatling, active: false), .init(card: .endTurn, active: true) @@ -134,18 +113,13 @@ struct GameViewTest { ) .withPlayMode(["p1": .manual]) .build() - let appState = AppFeature.State( - cardLibrary: .init(), - navigation: .init(), - settings: SettingsFeature.State.makeBuilder().build(), + let state = GameSessionFeature.State( game: game ) // When - let viewState = try #require(GameView.ViewState(appState: appState)) - // Then - let chooseOne = try #require(viewState.chooseOne) + let chooseOne = try #require(state.chooseOne) #expect(chooseOne.options == [.missed, .bang]) #expect(chooseOne.chooser == "p1") #expect(chooseOne.resolvingAction == .counterShot) diff --git a/Modules/AppFeature/Tests/SoundMatcherTest.swift b/Modules/GameSessionFeature/Tests/SoundMatcherTest.swift similarity index 98% rename from Modules/AppFeature/Tests/SoundMatcherTest.swift rename to Modules/GameSessionFeature/Tests/SoundMatcherTest.swift index 075d02119..c1abb0890 100644 --- a/Modules/AppFeature/Tests/SoundMatcherTest.swift +++ b/Modules/GameSessionFeature/Tests/SoundMatcherTest.swift @@ -5,9 +5,9 @@ // Created by Hugues Stéphano TELOLAHY on 17/10/2025. // -@testable import AppFeature import Testing -import GameFeature +import GameCore +@testable import GameSessionFeature struct SoundMatcherTest { private let sut = SoundMatcher(specialSounds: [:]) diff --git a/Modules/GameUI/Sources/GameViewState.swift b/Modules/GameUI/Sources/GameViewState.swift deleted file mode 100644 index 79a2673ca..000000000 --- a/Modules/GameUI/Sources/GameViewState.swift +++ /dev/null @@ -1,173 +0,0 @@ -// -// GameViewState.swift -// WildWestOnline -// -// Created by Hugues Stéphano TELOLAHY on 16/10/2025. -// -// swiftlint:disable force_unwrapping - -import Foundation -import SwiftUI -import Redux -import AppFeature -import GameFeature - -public extension GameView { - struct ViewState: Equatable { - let players: [PlayerItem] - let message: String - let chooseOne: ChooseOne? - let handCards: [HandCard] - let topDiscard: String? - let topDeck: String? - let startOrder: [String] - let deckCount: Int - let controlledPlayer: String? - let startPlayer: String - let actionDelaySeconds: TimeInterval - let lastEvent: GameFeature.Action? - - struct PlayerItem: Equatable { - let id: String - let imageName: String - let displayName: String - let health: Int - let handCount: Int - let inPlay: [String] - let isTurn: Bool - let isTargeted: Bool - let isEliminated: Bool - let role: String? - let userPhotoUrl: String? - } - - struct HandCard: Equatable { - let card: String - let active: Bool - } - - struct ChooseOne: Equatable { - let resolvingAction: Card.ActionName - let chooser: String - let options: [String] - } - } - - typealias ViewStore = Store -} - -public extension GameView.ViewState { - init?(appState: AppFeature.State) { - guard let game = appState.game else { - return nil - } - - players = game.playerItems - message = game.message - chooseOne = game.chooseOne - handCards = game.handCards - topDiscard = game.discard.first - topDeck = game.deck.first - startOrder = game.startOrder - deckCount = game.deck.count - controlledPlayer = game.manuallyControlledPlayer() - startPlayer = game.startPlayerId - actionDelaySeconds = Double(appState.settings.actionDelayMilliSeconds) / 1000.0 - lastEvent = game.lastEvent - } -} - -private extension GameFeature.State { - var playerItems: [GameView.ViewState.PlayerItem] { - self.startOrder.map { playerId in - let playerObj = players.get(playerId) - let health = max(0, playerObj.health) - let handCount = playerObj.hand.count - let equipment = playerObj.inPlay - let isTurn = playerId == turn - let isEliminated = !playOrder.contains(playerId) - let isTargeted = isTargeted(playerId) - let name = playerObj.figure.first ?? "" - - return .init( - id: playerId, - imageName: name, - displayName: name.uppercased(), - health: health, - handCount: handCount, - inPlay: equipment, - isTurn: isTurn, - isTargeted: isTargeted, - isEliminated: isEliminated, - role: nil, - userPhotoUrl: nil - ) - } - } - - var message: String { - if let turn { - "\(turn.uppercased())'s turn" - } else { - "-" - } - } - - var chooseOne: GameView.ViewState.ChooseOne? { - guard let controlledPlayer = manuallyControlledPlayer(), - let choice = pendingChoice(for: controlledPlayer) else { - return nil - } - - return .init( - resolvingAction: choice.action, - chooser: choice.prompt.chooser, - options: choice.prompt.options.map(\.label) - ) - } - - var handCards: [GameView.ViewState.HandCard] { - guard let controlledPlayer = manuallyControlledPlayer() else { - return [] - } - - var playableCards: [String] = [] - if let playable, - playable.player == controlledPlayer { - playableCards = playable.cards - } - let handCards = players.get(controlledPlayer).hand - - let hand = handCards.map { card in - GameView.ViewState.HandCard( - card: card, - active: playableCards.contains(card) - ) - } - - let abilities = playableCards.compactMap { card in - if !handCards.contains(card) { - GameView.ViewState.HandCard( - card: card, - active: true - ) - } else { - nil - } - } - - return hand + abilities - } - - var startingPlayerId: String { - playOrder.first! - } - - var startPlayerId: String { - guard let playerId = startOrder.first else { - fatalError("Missing start player") - } - - return playerId - } -} diff --git a/Modules/HomeFeature/Sources/HomeFeature.swift b/Modules/HomeFeature/Sources/HomeFeature.swift new file mode 100644 index 000000000..44f342e33 --- /dev/null +++ b/Modules/HomeFeature/Sources/HomeFeature.swift @@ -0,0 +1,77 @@ +// +// HomeFeature.swift +// WildWestOnline +// +// Created by Hugues Stéphano TELOLAHY on 04/12/2025. +// + +import Redux +import AudioClient +import PreferencesClient + +public enum HomeFeature { + public struct State: Equatable, Sendable { + var isFirstLoad: Bool = true + + public init() {} + } + + public enum Action { + // View + case didAppear + case didDisappear + case didTapPlay + case didTapSettings + + // Delegate + case delegate(Delegate) + + public enum Delegate { + case play + case settings + } + } + + public static func reducer( + state: inout State, + action: Action, + dependencies: Dependencies + ) -> Effect { + switch action { + case .didAppear: + if state.isFirstLoad { + state.isFirstLoad = false + return .run { + await dependencies.audioClient.setMusicVolume(dependencies.preferencesClient.musicVolume()) + await dependencies.audioClient.load(AudioClient.Sound.allSfx) + await dependencies.audioClient.play(.musicLoneRider) + return .none + } + } else { + return .run { + await dependencies.audioClient.resume(AudioClient.Sound.musicLoneRider) + return .none + } + } + + case .didDisappear: + return .run { + await dependencies.audioClient.pause(AudioClient.Sound.musicLoneRider) + return .none + } + + case .didTapPlay: + return .run { + .delegate(.play) + } + + case .didTapSettings: + return .run { + .delegate(.settings) + } + + case .delegate: + return .none + } + } +} diff --git a/Modules/HomeUI/Sources/HomeView.swift b/Modules/HomeFeature/Sources/HomeView.swift similarity index 85% rename from Modules/HomeUI/Sources/HomeView.swift rename to Modules/HomeFeature/Sources/HomeView.swift index 32e1ed4f6..c5aa14753 100644 --- a/Modules/HomeUI/Sources/HomeView.swift +++ b/Modules/HomeFeature/Sources/HomeView.swift @@ -6,11 +6,13 @@ // import SwiftUI +import Redux import Theme public struct HomeView: View { - @StateObject private var store: ViewStore + public typealias ViewStore = Store + @StateObject private var store: ViewStore @Environment(\.theme) private var theme public init(store: @escaping () -> ViewStore) { @@ -32,6 +34,16 @@ public struct HomeView: View { #if os(iOS) .navigationBarHidden(true) #endif + .onAppear { + Task { + await store.dispatch(.didAppear) + } + } + .onDisappear { + Task { + await store.dispatch(.didDisappear) + } + } } private var contentView: some View { @@ -50,12 +62,12 @@ public struct HomeView: View { VStack(spacing: 8) { mainButton("menu.play.button") { Task { - await store.dispatch(.start) + await store.dispatch(.didTapPlay) } } mainButton("menu.settings.button") { Task { - await store.dispatch(.navigation(.presentSettingsSheet)) + await store.dispatch(.didTapSettings) } } } diff --git a/Modules/HomeUI/Sources/Resources/Assets.xcassets/Contents.json b/Modules/HomeFeature/Sources/Resources/Assets.xcassets/Contents.json similarity index 100% rename from Modules/HomeUI/Sources/Resources/Assets.xcassets/Contents.json rename to Modules/HomeFeature/Sources/Resources/Assets.xcassets/Contents.json diff --git a/Modules/HomeUI/Sources/Resources/Assets.xcassets/logo.imageset/Contents.json b/Modules/HomeFeature/Sources/Resources/Assets.xcassets/logo.imageset/Contents.json similarity index 100% rename from Modules/HomeUI/Sources/Resources/Assets.xcassets/logo.imageset/Contents.json rename to Modules/HomeFeature/Sources/Resources/Assets.xcassets/logo.imageset/Contents.json diff --git a/Modules/HomeUI/Sources/Resources/Assets.xcassets/logo.imageset/logo-4.png b/Modules/HomeFeature/Sources/Resources/Assets.xcassets/logo.imageset/logo-4.png similarity index 100% rename from Modules/HomeUI/Sources/Resources/Assets.xcassets/logo.imageset/logo-4.png rename to Modules/HomeFeature/Sources/Resources/Assets.xcassets/logo.imageset/logo-4.png diff --git a/Modules/HomeUI/Sources/Resources/Localizable.xcstrings b/Modules/HomeFeature/Sources/Resources/Localizable.xcstrings similarity index 100% rename from Modules/HomeUI/Sources/Resources/Localizable.xcstrings rename to Modules/HomeFeature/Sources/Resources/Localizable.xcstrings diff --git a/Modules/HomeFeature/Tests/HomeFeatureTests.swift b/Modules/HomeFeature/Tests/HomeFeatureTests.swift new file mode 100644 index 000000000..53b1d14b9 --- /dev/null +++ b/Modules/HomeFeature/Tests/HomeFeatureTests.swift @@ -0,0 +1,11 @@ +// +// HomeFeatureTests.swift +// WildWestOnline +// +// Created by Hugues Stéphano TELOLAHY on 04/12/2025. +// + +import Testing + +struct HomeFeatureTests { +} diff --git a/Modules/HomeUI/Sources/HomeViewState.swift b/Modules/HomeUI/Sources/HomeViewState.swift deleted file mode 100644 index 128a954a7..000000000 --- a/Modules/HomeUI/Sources/HomeViewState.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// HomeViewState.swift -// WildWestOnline -// -// Created by Hugues Stéphano TELOLAHY on 16/10/2025. -// - -import Foundation -import AppFeature -import Redux - -public extension HomeView { - struct ViewState: Equatable {} - - typealias ViewStore = Store -} - -public extension HomeView.ViewState { - init?(appState: AppFeature.State) { } -} diff --git a/Modules/HomeUI/Tests/HomeViewTest.swift b/Modules/HomeUI/Tests/HomeViewTest.swift deleted file mode 100644 index 2c1860f36..000000000 --- a/Modules/HomeUI/Tests/HomeViewTest.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// HomeViewTest.swift -// -// -// Created by Stephano Hugues TELOLAHY on 24/02/2024. -// - -import HomeUI -import Testing -import AppFeature -import GameFeature -import SettingsFeature - -struct HomeViewTest { - @Test func HomeStateProjection() async throws { - // Given - let appState = AppFeature.State( - cardLibrary: .init(), - navigation: .init(), - settings: SettingsFeature.State.makeBuilder().build() - ) - - // When - let viewState = HomeView.ViewState(appState: appState) - - // Then - #expect(viewState != nil) - } -} diff --git a/Modules/NavigationFeature/Sources/AppNavigationFeature.swift b/Modules/NavigationFeature/Sources/AppNavigationFeature.swift deleted file mode 100644 index 8e983c22b..000000000 --- a/Modules/NavigationFeature/Sources/AppNavigationFeature.swift +++ /dev/null @@ -1,83 +0,0 @@ -// -// AppNavigationFeature.swift -// WildWestOnline -// -// Created by Hugues Stéphano TELOLAHY on 29/03/2025. -// - -import Redux - -public enum AppNavigationFeature { - public struct State: Equatable, Codable, Sendable { - public var path: [Destination] - public var settingsSheet: SettingsNavigationFeature.State? - - public enum Destination: String, Codable, Sendable { - case game - } - - public init( - path: [Destination] = [], - settingsSheet: SettingsNavigationFeature.State? = nil - ) { - self.path = path - self.settingsSheet = settingsSheet - } - } - - public enum Action { - case push(State.Destination) - case pop - case setPath([State.Destination]) - case presentSettingsSheet - case dismissSettingsSheet - case settingsSheet(SettingsNavigationFeature.Action) - } - - public static var reducer: Reducer { - combine( - reducerMain, - pullback( - SettingsNavigationFeature.reducer, - state: { globalState in - globalState.settingsSheet != nil ? \.settingsSheet! : nil - }, - action: { globalAction in - if case let .settingsSheet(localAction) = globalAction { - return localAction - } - return nil - }, - embedAction: Action.settingsSheet - ) - ) - } - - private static func reducerMain( - into state: inout State, - action: Action, - dependencies: Dependencies - ) -> Effect { - switch action { - case .push(let page): - state.path.append(page) - - case .pop: - state.path.removeLast() - - case .setPath(let path): - state.path = path - - case .presentSettingsSheet: - state.settingsSheet = .init(path: []) - - case .dismissSettingsSheet: - state.settingsSheet = nil - - case .settingsSheet: - break - } - - return .none - } -} diff --git a/Modules/NavigationFeature/Sources/SettingsNavigationFeature.swift b/Modules/NavigationFeature/Sources/SettingsNavigationFeature.swift deleted file mode 100644 index 21a74f620..000000000 --- a/Modules/NavigationFeature/Sources/SettingsNavigationFeature.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// SettingsNavigationFeature.swift -// WildWestOnline -// -// Created by Hugues Stéphano TELOLAHY on 29/03/2025. -// - -import Redux - -public enum SettingsNavigationFeature { - public struct State: Equatable, Codable, Sendable { - public var path: [Destination] - - public enum Destination: String, Codable, Sendable { - case figures - case collectibles - } - - public init(path: [Destination] = []) { - self.path = path - } - } - - public enum Action { - case push(State.Destination) - case pop - case setPath([State.Destination]) - } - - public static func reducer( - into state: inout State, - action: Action, - dependencies: Dependencies - ) -> Effect { - switch action { - case .push(let page): - state.path.append(page) - - case .pop: - state.path.removeLast() - - case .setPath(let path): - state.path = path - } - - return .none - } -} diff --git a/Modules/NavigationFeature/Tests/AppNavigationFeatureTests.swift b/Modules/NavigationFeature/Tests/AppNavigationFeatureTests.swift deleted file mode 100644 index cd03cbc35..000000000 --- a/Modules/NavigationFeature/Tests/AppNavigationFeatureTests.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// AppNavigationFeatureTests.swift -// -// -// Created by Stephano Hugues TELOLAHY on 24/07/2024. -// -import Testing -import NavigationFeature -import Redux - -struct AppNavigationFeatureTests { - private typealias NavigationStore = Store - - private func createNavigationStore(initialState: AppNavigationFeature.State) async -> NavigationStore { - await .init( - initialState: initialState, - reducer: AppNavigationFeature.reducer - ) - } - - @Test func showingSettings_shouldDisplaySettings() async throws { - // Given - let state = AppNavigationFeature.State() - let sut = await createNavigationStore(initialState: state) - - // When - let action = AppNavigationFeature.Action.presentSettingsSheet - await sut.dispatch(action) - - // Then - await #expect(sut.state.settingsSheet == .init()) - } - - @Test func showingSettingsFigures_shouldDisplayFigures() async throws { - // Given - let state = AppNavigationFeature.State(settingsSheet: .init()) - let sut = await createNavigationStore(initialState: state) - - // When - let action = AppNavigationFeature.Action.settingsSheet(.push(.figures)) - await sut.dispatch(action) - - // Then - await #expect(sut.state.settingsSheet == .init(path: [.figures])) - } - - @Test func closingSettings_shouldRemoveSettings() async throws { - // Given - let state = AppNavigationFeature.State(settingsSheet: .init(path: [.figures])) - let sut = await createNavigationStore(initialState: state) - - // When - let action = AppNavigationFeature.Action.dismissSettingsSheet - await sut.dispatch(action) - - // Then - await #expect(sut.state.settingsSheet == nil) - } -} diff --git a/Modules/Package.swift b/Modules/Package.swift index aa87dff7f..c0f2cabb9 100644 --- a/Modules/Package.swift +++ b/Modules/Package.swift @@ -3,10 +3,60 @@ import PackageDescription +struct Module { + let name: String + var dependencies: [String] = [] + var resources: Bool = false + var test: Bool = false + var lint: Bool = true +} + +let modules: [Module] = [ + Module(name: "Redux", test: true), + Module(name: "Theme"), + Module(name: "AudioClient", dependencies: ["Redux"]), + Module(name: "AudioClientLive", dependencies: ["AudioClient"], resources: true), + Module(name: "PreferencesClient", dependencies: ["Redux"]), + Module(name: "PreferencesClientLive", dependencies: ["PreferencesClient"]), + Module(name: "GameCore", dependencies: ["Redux"], test: true), + Module(name: "CardResources", resources: true), + Module(name: "CardLibrary", dependencies: ["Redux", "GameCore", "AudioClient"]), + Module(name: "CardLibraryLive", dependencies: ["CardLibrary", "CardResources"], test: true), + Module(name: "HomeFeature", dependencies: ["Redux", "Theme", "AudioClient", "PreferencesClient"], test: true), + Module(name: "SettingsFeature", dependencies: ["Redux", "Theme", "PreferencesClient", "CardLibrary", "CardResources"], test: true), + Module(name: "GameSessionFeature", dependencies: ["Theme", "GameCore", "AudioClient", "CardLibrary", "PreferencesClient", "CardResources"], resources: true, test: true), + Module(name: "AppFeature", dependencies: ["HomeFeature", "GameSessionFeature", "SettingsFeature"], test: true), + Module(name: "AppBuilder", dependencies: ["AppFeature", "PreferencesClientLive", "AudioClientLive", "CardLibraryLive"]) +] + let lintPlugin: [Target.PluginUsage] = [ .plugin(name: "SwiftLintBuildToolPlugin", package: "SwiftLintPlugins") ] +let products: [Product] = modules.map { .library(name: $0.name, targets: [$0.name]) } + +let targets: [Target] = modules.flatMap { module -> [Target] in + var result: [Target] = [ + .target( + name: module.name, + dependencies: module.dependencies.map { .byName(name: $0) }, + path: "\(module.name)/Sources", + resources: module.resources ? [.process("Resources")] : nil, + plugins: module.lint ? lintPlugin : [] + ) + ] + if module.test { + result.append( + .testTarget( + name: "\(module.name)Tests", + dependencies: [.byName(name: module.name)], + path: "\(module.name)/Tests" + ) + ) + } + return result +} + let package = Package( name: "WildWestOnline", defaultLocalization: "fr", @@ -14,275 +64,10 @@ let package = Package( .iOS(.v17), .macOS(.v14) ], - products: [ - // Products define the executables and libraries a package produces, and make them visible to other packages. - // Bootstrap - .library(name: "AppBootstrap", targets: ["AppBootstrap"]), - - // Features - .library(name: "AppFeature", targets: ["AppFeature"]), - .library(name: "GameFeature", targets: ["GameFeature"]), - .library(name: "SettingsFeature", targets: ["SettingsFeature"]), - .library(name: "NavigationFeature", targets: ["NavigationFeature"]), - - // UI - .library(name: "AppUI", targets: ["AppUI"]), - .library(name: "GameUI", targets: ["GameUI"]), - .library(name: "SettingsUI", targets: ["SettingsUI"]), - .library(name: "HomeUI", targets: ["HomeUI"]), - - // Dependencies abstraction - .library(name: "PreferencesClient", targets: ["PreferencesClient"]), - .library(name: "AudioClient", targets: ["AudioClient"]), - - // Dependencies implementation - .library(name: "PreferencesClientLive", targets: ["PreferencesClientLive"]), - .library(name: "AudioClientLive", targets: ["AudioClientLive"]), - .library(name: "CardResources", targets: ["CardResources"]), - - // Utilities - .library(name: "Redux", targets: ["Redux"]), - .library(name: "Theme", targets: ["Theme"]), - .library(name: "Utils", targets: ["Utils"]), - ], + products: products, dependencies: [ // Dependencies declare other packages that this package depends on. .package(url: "https://github.com/SimplyDanny/SwiftLintPlugins", from: "0.62.2") ], - targets: [ - // Targets are the basic building blocks of a package. A target can define a module or a test suite. - // Targets can depend on other targets in this package, and on products in packages this package depends on. - .target( - name: "Redux", - dependencies: [], - path: "Redux/Sources" - ), - .testTarget( - name: "ReduxTests", - dependencies: [ - "Redux" - ], - path: "Redux/Tests" - ), - .target( - name: "Utils", - dependencies: [], - path: "Utils/Sources", - plugins: lintPlugin - ), - .testTarget( - name: "UtilsTests", - dependencies: [ - "Utils" - ], - path: "Utils/Tests" - ), - .target( - name: "GameFeature", - dependencies: [ - "Redux" - ], - path: "GameFeature/Sources", - plugins: lintPlugin - ), - .testTarget( - name: "GameFeatureTests", - dependencies: [ - "GameFeature" - ], - path: "GameFeature/Tests" - ), - .target( - name: "PreferencesClient", - dependencies: [ - "Redux" - ], - path: "PreferencesClient/Sources", - plugins: lintPlugin - ), - .target( - name: "SettingsFeature", - dependencies: [ - "Redux", - "PreferencesClient" - ], - path: "SettingsFeature/Sources", - plugins: lintPlugin - ), - .testTarget( - name: "SettingsFeatureTests", - dependencies: [ - "SettingsFeature" - ], - path: "SettingsFeature/Tests" - ), - .target( - name: "NavigationFeature", - dependencies: [ - "Redux" - ], - path: "NavigationFeature/Sources", - plugins: lintPlugin - ), - .testTarget( - name: "NavigationFeatureTests", - dependencies: [ - "NavigationFeature" - ], - path: "NavigationFeature/Tests" - ), - .target( - name: "AppFeature", - dependencies: [ - "GameFeature", - "SettingsFeature", - "NavigationFeature", - "AudioClient" - ], - path: "AppFeature/Sources", - plugins: lintPlugin - ), - .testTarget( - name: "AppFeatureTests", - dependencies: [ - "AppFeature" - ], - path: "AppFeature/Tests" - ), - .target( - name: "Theme", - dependencies: [], - path: "Theme/Sources", - plugins: lintPlugin - ), - .target( - name: "AudioClient", - dependencies: [ - "Redux" - ], - path: "AudioClient/Sources", - plugins: lintPlugin - ), - .target( - name: "AudioClientLive", - dependencies: [ - "AudioClient" - ], - path: "AudioClientLive/Sources", - resources: [ - .process("Resources") - ], - plugins: lintPlugin - ), - .target( - name: "HomeUI", - dependencies: [ - "AppFeature", - "Theme", - "AudioClient" - ], - path: "HomeUI/Sources", - plugins: lintPlugin - ), - .testTarget( - name: "HomeUITests", - dependencies: [ - "HomeUI" - ], - path: "HomeUI/Tests" - ), - .target( - name: "SettingsUI", - dependencies: [ - "AppFeature", - "CardResources", - "Theme" - ], - path: "SettingsUI/Sources", - plugins: lintPlugin - ), - .testTarget( - name: "SettingsUITests", - dependencies: [ - "SettingsUI" - ], - path: "SettingsUI/Tests" - ), - .target( - name: "GameUI", - dependencies: [ - "AppFeature", - "Theme", - "CardResources" - ], - path: "GameUI/Sources", - resources: [ - .process("Resources") - ], - plugins: lintPlugin - ), - .testTarget( - name: "GameUITests", - dependencies: [ - "GameUI" - ], - path: "GameUI/Tests" - ), - .target( - name: "AppUI", - dependencies: [ - "HomeUI", - "GameUI", - "SettingsUI" - ], - path: "AppUI/Sources", - plugins: lintPlugin - ), - .testTarget( - name: "AppUITests", - dependencies: [ - "AppUI" - ], - path: "AppUI/Tests" - ), - .target( - name: "CardResources", - dependencies: [ - "GameFeature", - "AudioClient" - ], - path: "CardResources/Sources", - resources: [ - .process("Resources") - ], - plugins: lintPlugin - ), - .testTarget( - name: "CardResourcesTests", - dependencies: [ - "CardResources" - ], - path: "CardResources/Tests" - ), - .target( - name: "PreferencesClientLive", - dependencies: [ - "PreferencesClient" - ], - path: "PreferencesClientLive/Sources", - plugins: lintPlugin - ), - .target( - name: "AppBootstrap", - dependencies: [ - "AppUI", - "CardResources", - "PreferencesClientLive", - "AudioClientLive" - ], - path: "AppBootstrap/Sources", - plugins: lintPlugin - ), - ] + targets: targets ) - diff --git a/Modules/PreferencesClient/Sources/PreferencesClient.swift b/Modules/PreferencesClient/Sources/PreferencesClient.swift index c07330b88..5a77de5a6 100644 --- a/Modules/PreferencesClient/Sources/PreferencesClient.swift +++ b/Modules/PreferencesClient/Sources/PreferencesClient.swift @@ -6,11 +6,11 @@ // public struct PreferencesClient { - public var savePlayersCount: (Int) -> Void - public var saveActionDelayMilliSeconds: (Int) -> Void - public var saveSimulationEnabled: (Bool) -> Void - public var savePreferredFigure: (String?) -> Void - public var saveMusicVolume: (Float) -> Void + public var setPlayersCount: (Int) -> Void + public var setActionDelayMilliSeconds: (Int) -> Void + public var setSimulationEnabled: (Bool) -> Void + public var setPreferredFigure: (String?) -> Void + public var setMusicVolume: (Float) -> Void public var playersCount: () -> Int public var actionDelayMilliSeconds: () -> Int public var isSimulationEnabled: () -> Bool @@ -18,22 +18,22 @@ public struct PreferencesClient { public var musicVolume: () -> Float public init( - savePlayersCount: @escaping (Int) -> Void, - saveActionDelayMilliSeconds: @escaping (Int) -> Void, - saveSimulationEnabled: @escaping (Bool) -> Void, - savePreferredFigure: @escaping (String?) -> Void, - saveMusicVolume: @escaping (Float) -> Void, + setPlayersCount: @escaping (Int) -> Void, + setActionDelayMilliSeconds: @escaping (Int) -> Void, + setSimulationEnabled: @escaping (Bool) -> Void, + setPreferredFigure: @escaping (String?) -> Void, + setMusicVolume: @escaping (Float) -> Void, playersCount: @escaping () -> Int, actionDelayMilliSeconds: @escaping () -> Int, isSimulationEnabled: @escaping () -> Bool, preferredFigure: @escaping () -> String?, musicVolume: @escaping () -> Float ) { - self.savePlayersCount = savePlayersCount - self.saveActionDelayMilliSeconds = saveActionDelayMilliSeconds - self.saveSimulationEnabled = saveSimulationEnabled - self.savePreferredFigure = savePreferredFigure - self.saveMusicVolume = saveMusicVolume + self.setPlayersCount = setPlayersCount + self.setActionDelayMilliSeconds = setActionDelayMilliSeconds + self.setSimulationEnabled = setSimulationEnabled + self.setPreferredFigure = setPreferredFigure + self.setMusicVolume = setMusicVolume self.playersCount = playersCount self.actionDelayMilliSeconds = actionDelayMilliSeconds self.isSimulationEnabled = isSimulationEnabled diff --git a/Modules/PreferencesClient/Sources/PreferencesClientKey.swift b/Modules/PreferencesClient/Sources/PreferencesClientKey.swift index c42d81b10..a183fe3e0 100644 --- a/Modules/PreferencesClient/Sources/PreferencesClientKey.swift +++ b/Modules/PreferencesClient/Sources/PreferencesClientKey.swift @@ -21,11 +21,11 @@ private enum PreferencesClientKey: DependencyKey { private extension PreferencesClient { static var noop: Self { .init( - savePlayersCount: { _ in }, - saveActionDelayMilliSeconds: { _ in }, - saveSimulationEnabled: { _ in }, - savePreferredFigure: { _ in }, - saveMusicVolume: { _ in }, + setPlayersCount: { _ in }, + setActionDelayMilliSeconds: { _ in }, + setSimulationEnabled: { _ in }, + setPreferredFigure: { _ in }, + setMusicVolume: { _ in }, playersCount: { 0 }, actionDelayMilliSeconds: { 0 }, isSimulationEnabled: { false }, diff --git a/Modules/PreferencesClientLive/Sources/PreferencesClient+Live.swift b/Modules/PreferencesClientLive/Sources/PreferencesClient+Live.swift index ba1a77fbb..4eb7a19cb 100644 --- a/Modules/PreferencesClientLive/Sources/PreferencesClient+Live.swift +++ b/Modules/PreferencesClientLive/Sources/PreferencesClient+Live.swift @@ -12,11 +12,11 @@ public extension PreferencesClient { static func live() -> Self { let service = StorageService() return .init( - savePlayersCount: { service.playersCount = $0 }, - saveActionDelayMilliSeconds: { service.actionDelayMilliSeconds = $0 }, - saveSimulationEnabled: { service.simulationEnabled = $0 }, - savePreferredFigure: { service.preferredFigure = $0 }, - saveMusicVolume: { service.musicVolume = $0 }, + setPlayersCount: { service.playersCount = $0 }, + setActionDelayMilliSeconds: { service.actionDelayMilliSeconds = $0 }, + setSimulationEnabled: { service.simulationEnabled = $0 }, + setPreferredFigure: { service.preferredFigure = $0 }, + setMusicVolume: { service.musicVolume = $0 }, playersCount: { service.playersCount }, actionDelayMilliSeconds: { service.actionDelayMilliSeconds }, isSimulationEnabled: { service.simulationEnabled }, diff --git a/Modules/Redux/Sources/Reducer+Pullback.swift b/Modules/Redux/Sources/Reducer+Pullback.swift index 79ea4e72a..b0a96c9b8 100644 --- a/Modules/Redux/Sources/Reducer+Pullback.swift +++ b/Modules/Redux/Sources/Reducer+Pullback.swift @@ -17,7 +17,7 @@ public func pullback< action toLocalAction: @escaping (GlobalAction) -> LocalAction?, embedAction: @escaping (LocalAction) -> GlobalAction ) -> Reducer { - return { globalState, globalAction, dependencies in + { globalState, globalAction, dependencies in // Only handle actions that map to the local domain guard let localAction = toLocalAction(globalAction) else { return .none @@ -41,8 +41,10 @@ private extension Effect { switch self { case .none: return .none + case .publisher(let publisher): return .publisher(publisher.map(transform).eraseToAnyPublisher()) + case .run(let asyncWork): return .run { if let result = await asyncWork() { @@ -50,6 +52,7 @@ private extension Effect { } return nil } + case .group(let effects): return .group(effects.map { $0.map(transform) }) } diff --git a/Modules/Redux/Sources/Store.swift b/Modules/Redux/Sources/Store.swift index a4745eda3..5dbaa2749 100644 --- a/Modules/Redux/Sources/Store.swift +++ b/Modules/Redux/Sources/Store.swift @@ -11,6 +11,7 @@ public typealias Reducer = (inout State, Action, Dependencies) -> /// ``Effect`` is an asynchronous `Action` public enum Effect { + // swiftlint:disable:next discouraged_none_name case none case publisher(AnyPublisher) case run(() async -> Action?) @@ -30,6 +31,7 @@ public struct Dependencies { public protocol DependencyKey { associatedtype Value + static var defaultValue: Value { get } } diff --git a/Modules/SettingsFeature/Sources/Collectibles/SettingsCollectiblesFeature.swift b/Modules/SettingsFeature/Sources/Collectibles/SettingsCollectiblesFeature.swift new file mode 100644 index 000000000..690772d33 --- /dev/null +++ b/Modules/SettingsFeature/Sources/Collectibles/SettingsCollectiblesFeature.swift @@ -0,0 +1,53 @@ +// +// SettingsCollectiblesFeature.swift +// WildWestOnline +// +// Created by Hugues Stéphano TELOLAHY on 03/12/2025. +// +import Redux +import CardLibrary + +public enum SettingsCollectiblesFeature { + public struct State: Equatable, Sendable { + public var cards: [Card] + + public struct Card: Equatable, Sendable { + let name: String + let description: String + } + + public init(cards: [Card] = []) { + self.cards = cards + } + } + + public enum Action { + case didAppear + } + + static func reducer( + state: inout State, + action: Action, + dependencies: Dependencies + ) -> Effect { + switch action { + case .didAppear: + state.cards = dependencies.loadCollectibleCards() + } + + return .none + } +} + +private extension Dependencies { + func loadCollectibleCards() -> [SettingsCollectiblesFeature.State.Card] { + cardLibrary.cards() + .filter { $0.type == .collectible } + .map { + .init( + name: $0.name, + description: $0.description ?? "" + ) + } + } +} diff --git a/Modules/SettingsUI/Sources/Collectibles/SettingsCollectiblesView.swift b/Modules/SettingsFeature/Sources/Collectibles/SettingsCollectiblesView.swift similarity index 53% rename from Modules/SettingsUI/Sources/Collectibles/SettingsCollectiblesView.swift rename to Modules/SettingsFeature/Sources/Collectibles/SettingsCollectiblesView.swift index ef58c803b..344c7414f 100644 --- a/Modules/SettingsUI/Sources/Collectibles/SettingsCollectiblesView.swift +++ b/Modules/SettingsFeature/Sources/Collectibles/SettingsCollectiblesView.swift @@ -6,9 +6,12 @@ // import SwiftUI +import Redux import CardResources struct SettingsCollectiblesView: View { + typealias ViewStore = Store + @StateObject private var store: ViewStore init(store: @escaping () -> ViewStore) { @@ -25,9 +28,12 @@ struct SettingsCollectiblesView: View { } .scrollContentBackground(.hidden) .navigationTitle("Collectibles") + .task { + await store.dispatch(.didAppear) + } } - func rowView(card: ViewState.Card) -> some View { + func rowView(card: SettingsCollectiblesFeature.State.Card) -> some View { HStack { Image(card.name, bundle: .cardResources) .resizable() @@ -37,10 +43,7 @@ struct SettingsCollectiblesView: View { VStack(alignment: .leading) { Text(card.name.uppercased()) .bold() - - if let description = card.description { - Text(description) - } + Text(card.description) } } .foregroundStyle(.foreground) @@ -51,11 +54,22 @@ struct SettingsCollectiblesView: View { NavigationStack { SettingsCollectiblesView { .init( - initialState: .init(cards: [ - .init(name: .bang, description: "Lorem Ipsum is simply dummy text of the printing and typesetting industry"), - .init(name: .missed, description: "Lorem Ipsum is simply dummy text of the printing and typesetting industry"), - .init(name: .dodge, description: "Lorem Ipsum is simply dummy text of the printing and typesetting industry") - ]) + initialState: .init( + cards: [ + .init( + name: .bang, + description: "Lorem Ipsum is simply dummy text of the printing and typesetting industry" + ), + .init( + name: .missed, + description: "Lorem Ipsum is simply dummy text of the printing and typesetting industry" + ), + .init( + name: .dodge, + description: "Lorem Ipsum is simply dummy text of the printing and typesetting industry" + ) + ] + ) ) } } diff --git a/Modules/SettingsFeature/Sources/Figures/SettingsFiguresFeature.swift b/Modules/SettingsFeature/Sources/Figures/SettingsFiguresFeature.swift new file mode 100644 index 000000000..b3d4514cb --- /dev/null +++ b/Modules/SettingsFeature/Sources/Figures/SettingsFiguresFeature.swift @@ -0,0 +1,61 @@ +// +// SettingsFiguresFeature.swift +// WildWestOnline +// +// Created by Hugues Stéphano TELOLAHY on 03/12/2025. +// + +import Redux +import CardLibrary + +public enum SettingsFiguresFeature { + public struct State: Equatable, Sendable { + var figures: [Figure] + + public struct Figure: Equatable, Sendable { + let name: String + let description: String + let isFavorite: Bool + } + + public init(figures: [Figure] = []) { + self.figures = figures + } + } + + public enum Action { + case didAppear + case didSelect(String) + } + + static func reducer( + state: inout State, + action: Action, + dependencies: Dependencies + ) -> Effect { + switch action { + case .didAppear: + state.figures = dependencies.loadFigures() + + case .didSelect(let name): + dependencies.preferencesClient.setPreferredFigure(name) + state.figures = dependencies.loadFigures() + } + + return .none + } +} + +private extension Dependencies { + func loadFigures() -> [SettingsFiguresFeature.State.Figure] { + cardLibrary.cards() + .filter { $0.type == .figure } + .map { + .init( + name: $0.name, + description: $0.description ?? "", + isFavorite: $0.name == preferencesClient.preferredFigure() + ) + } + } +} diff --git a/Modules/SettingsUI/Sources/Figures/SettingsFiguresView.swift b/Modules/SettingsFeature/Sources/Figures/SettingsFiguresView.swift similarity index 87% rename from Modules/SettingsUI/Sources/Figures/SettingsFiguresView.swift rename to Modules/SettingsFeature/Sources/Figures/SettingsFiguresView.swift index 867733b79..623a70443 100644 --- a/Modules/SettingsUI/Sources/Figures/SettingsFiguresView.swift +++ b/Modules/SettingsFeature/Sources/Figures/SettingsFiguresView.swift @@ -6,9 +6,12 @@ // import SwiftUI +import Redux import CardResources struct SettingsFiguresView: View { + typealias ViewStore = Store + @StateObject private var store: ViewStore init(store: @escaping () -> ViewStore) { @@ -22,7 +25,7 @@ struct SettingsFiguresView: View { ForEach(store.state.figures, id: \.name) { figure in Button(action: { Task { - await store.dispatch(.settings(.updatePreferredFigure(figure.name))) + await store.dispatch(.didSelect(figure.name)) } }, label: { rowView(figure: figure) @@ -31,9 +34,12 @@ struct SettingsFiguresView: View { } .scrollContentBackground(.hidden) .navigationTitle("Figures") + .task { + await store.dispatch(.didAppear) + } } - func rowView(figure: ViewState.Figure) -> some View { + func rowView(figure: SettingsFiguresFeature.State.Figure) -> some View { HStack { Image(figure.name, bundle: .cardResources) .resizable() @@ -44,10 +50,7 @@ struct SettingsFiguresView: View { VStack(alignment: .leading) { Text(figure.name.uppercased()) .bold() - - if let description = figure.description { - Text(description) - } + Text(figure.description) } Spacer() diff --git a/Modules/SettingsFeature/Sources/Home/SettingsHomeFeature.swift b/Modules/SettingsFeature/Sources/Home/SettingsHomeFeature.swift new file mode 100644 index 000000000..3ad906758 --- /dev/null +++ b/Modules/SettingsFeature/Sources/Home/SettingsHomeFeature.swift @@ -0,0 +1,119 @@ +// +// SettingsHomeFeature.swift +// WildWestOnline +// +// Created by Hugues Stéphano TELOLAHY on 03/01/2025. +// +import Redux +import PreferencesClient +import AudioClient + +public enum SettingsHomeFeature { + public struct State: Equatable, Sendable { + let minPlayersCount = 2 + let maxPlayersCount = 7 + let speedOptions: [SpeedOption] = [ + .init(label: "Normal", value: 500), + .init(label: "Fast", value: 0) + ] + + struct SpeedOption: Equatable, Sendable { + let label: String + let value: Int + } + + public var playersCount: Int + public var actionDelayMilliSeconds: Int + public var simulation: Bool + public var preferredFigure: String? + public var musicVolume: Float + + public init( + playersCount: Int = 0, + actionDelayMilliSeconds: Int = 0, + simulation: Bool = false, + preferredFigure: String? = nil, + musicVolume: Float = 0 + ) { + self.playersCount = playersCount + self.actionDelayMilliSeconds = actionDelayMilliSeconds + self.simulation = simulation + self.preferredFigure = preferredFigure + self.musicVolume = musicVolume + } + } + + public enum Action { + // View + case didAppear + case didUpdatePlayersCount(Int) + case didUpdateActionDelayMilliSeconds(Int) + case didToggleSimulation + case didUpdatePreferredFigure(String?) + case didUpdateMusicVolume(Float) + case didTapFigures + case didTapCollectibles + + // Delegate + case delegate(Delegate) + + public enum Delegate { + case selectedFigures + case selectedCollectibles + } + } + + public static func reducer( + state: inout State, + action: Action, + dependencies: Dependencies + ) -> Effect { + switch action { + case .didAppear: + state.playersCount = dependencies.preferencesClient.playersCount() + state.actionDelayMilliSeconds = dependencies.preferencesClient.actionDelayMilliSeconds() + state.simulation = dependencies.preferencesClient.isSimulationEnabled() + state.preferredFigure = dependencies.preferencesClient.preferredFigure() + state.musicVolume = dependencies.preferencesClient.musicVolume() + + case .didUpdatePlayersCount(let value): + state.playersCount = value + dependencies.preferencesClient.setPlayersCount(value) + + case .didUpdateActionDelayMilliSeconds(let value): + state.actionDelayMilliSeconds = value + dependencies.preferencesClient.setActionDelayMilliSeconds(value) + + case .didToggleSimulation: + state.simulation.toggle() + dependencies.preferencesClient.setSimulationEnabled(state.simulation) + + case .didUpdatePreferredFigure(let value): + state.preferredFigure = value + dependencies.preferencesClient.setPreferredFigure(value) + + case .didUpdateMusicVolume(let value): + state.musicVolume = value + dependencies.preferencesClient.setMusicVolume(value) + return .run { + await dependencies.audioClient.setMusicVolume(value) + return .none + } + + case .didTapFigures: + return .run { + .delegate(.selectedFigures) + } + + case .didTapCollectibles: + return .run { + .delegate(.selectedCollectibles) + } + + case .delegate: + break + } + + return .none + } +} diff --git a/Modules/SettingsUI/Sources/Root/SettingsRootView.swift b/Modules/SettingsFeature/Sources/Home/SettingsHomeView.swift similarity index 77% rename from Modules/SettingsUI/Sources/Root/SettingsRootView.swift rename to Modules/SettingsFeature/Sources/Home/SettingsHomeView.swift index 866ced634..aff183474 100644 --- a/Modules/SettingsUI/Sources/Root/SettingsRootView.swift +++ b/Modules/SettingsFeature/Sources/Home/SettingsHomeView.swift @@ -1,13 +1,16 @@ // -// SettingsRootView.swift +// SettingsHomeView.swift // // // Created by Hugues Telolahy on 08/12/2023. // import SwiftUI +import Redux + +struct SettingsHomeView: View { + typealias ViewStore = Store -struct SettingsRootView: View { @StateObject private var store: ViewStore @Environment(\.dismiss) private var dismiss @@ -36,6 +39,9 @@ struct SettingsRootView: View { } } } + .task { + await store.dispatch(.didAppear) + } } // MARK: - Preferences @@ -56,9 +62,9 @@ struct SettingsRootView: View { "Players count: \(store.state.playersCount)", value: Binding( get: { store.state.playersCount }, - set: { index in + set: { newValue in Task { - await store.dispatch(.settings(.updatePlayersCount(index))) + await store.dispatch(.didUpdatePlayersCount(newValue)) } } ).animation(), @@ -73,12 +79,11 @@ struct SettingsRootView: View { Picker( selection: Binding( get: { - store.state.speedIndex + store.state.speedOptions.firstIndex { $0.value == store.state.actionDelayMilliSeconds } ?? 0 }, set: { index in Task { - let option = store.state.speedOptions[index] - await store.dispatch(.settings(.updateActionDelayMilliSeconds(option.value))) + await store.dispatch(.didUpdateActionDelayMilliSeconds(store.state.speedOptions[index].value)) } } ), @@ -100,7 +105,7 @@ struct SettingsRootView: View { get: { store.state.simulation }, set: { _ in Task { - await store.dispatch(.settings(.toggleSimulation)) + await store.dispatch(.didToggleSimulation) } } ).animation()) { @@ -119,7 +124,7 @@ struct SettingsRootView: View { get: { store.state.musicVolume }, set: { newValue in Task { - await store.dispatch(.settings(.updateMusicVolume(newValue))) + await store.dispatch(.didUpdateMusicVolume(newValue)) } } ), @@ -129,7 +134,7 @@ struct SettingsRootView: View { } } - // MARK: - CardsLibrary + // MARK: - CardLibrary private var cardsLibrarySection: some View { Section(header: Text("LIBRARY")) { @@ -141,7 +146,7 @@ struct SettingsRootView: View { private var figuresView: some View { Button(action: { Task { - await store.dispatch(.navigation(.settingsSheet(.push(.figures)))) + await store.dispatch(.didTapFigures) } }, label: { HStack { @@ -157,7 +162,7 @@ struct SettingsRootView: View { private var collectiblesView: some View { Button(action: { Task { - await store.dispatch(.navigation(.settingsSheet(.push(.collectibles)))) + await store.dispatch(.didTapCollectibles) } }, label: { HStack { @@ -171,20 +176,16 @@ struct SettingsRootView: View { #Preview { NavigationStack { - SettingsRootView { - .init(initialState: .previewState) + SettingsHomeView { + .init( + initialState: .init( + playersCount: 5, + actionDelayMilliSeconds: 0, + simulation: false, + preferredFigure: "Figure1", + musicVolume: 1.0 + ) + ) } } } - -private extension SettingsRootView.ViewState { - static var previewState: Self { - .init( - playersCount: 5, - speedIndex: 0, - simulation: false, - preferredFigure: "Figure1", - musicVolume: 1.0 - ) - } -} diff --git a/Modules/SettingsFeature/Sources/SettingsFeature.swift b/Modules/SettingsFeature/Sources/SettingsFeature.swift index 3aeb8082b..deb105c3b 100644 --- a/Modules/SettingsFeature/Sources/SettingsFeature.swift +++ b/Modules/SettingsFeature/Sources/SettingsFeature.swift @@ -2,55 +2,122 @@ // SettingsFeature.swift // WildWestOnline // -// Created by Hugues Stéphano TELOLAHY on 03/01/2025. +// Created by Hugues Stéphano TELOLAHY on 04/12/2025. // + import Redux -import PreferencesClient public enum SettingsFeature { - public struct State: Equatable, Codable, Sendable { - public var playersCount: Int - public var actionDelayMilliSeconds: Int - public var simulation: Bool - public var preferredFigure: String? - public var musicVolume: Float + public struct State: Equatable { + var path: [Destination] + + var home: SettingsHomeFeature.State + var figures: SettingsFiguresFeature.State + var collectibles: SettingsCollectiblesFeature.State + + public enum Destination: Hashable, Sendable { + case figures + case collectibles + } + + public init( + path: [Destination] = [], + home: SettingsHomeFeature.State = .init(), + figures: SettingsFiguresFeature.State = .init(), + collectibles: SettingsCollectiblesFeature.State = .init() + ) { + self.path = path + self.home = home + self.figures = figures + self.collectibles = collectibles + } } public enum Action { - case updatePlayersCount(Int) - case updateActionDelayMilliSeconds(Int) - case toggleSimulation - case updatePreferredFigure(String?) - case updateMusicVolume(Float) + case setPath([State.Destination]) + + case home(SettingsHomeFeature.Action) + case figures(SettingsFiguresFeature.Action) + case collectibles(SettingsCollectiblesFeature.Action) + } + + public static var reducer: Reducer { + combine( + reducerMain, + pullback( + SettingsHomeFeature.reducer, + state: { _ in + \.home + }, + action: { globalAction in + if case let .home(localAction) = globalAction { + return localAction + } + return nil + }, + embedAction: { + .home($0) + } + ), + pullback( + SettingsFiguresFeature.reducer, + state: { _ in + \.figures + }, + action: { globalAction in + if case let .figures(localAction) = globalAction { + return localAction + } + return nil + }, + embedAction: { + .figures($0) + } + ), + pullback( + SettingsCollectiblesFeature.reducer, + state: { _ in + \.collectibles + }, + action: { globalAction in + if case let .collectibles(localAction) = globalAction { + return localAction + } + return nil + }, + embedAction: { + .collectibles($0) + } + ) + ) } - public static func reducer( - state: inout State, + private static func reducerMain( + into state: inout State, action: Action, dependencies: Dependencies ) -> Effect { switch action { - case .updatePlayersCount(let value): - state.playersCount = value - dependencies.preferencesClient.savePlayersCount(value) + case .setPath(let path): + state.path = path + return .none - case .updateActionDelayMilliSeconds(let value): - state.actionDelayMilliSeconds = value - dependencies.preferencesClient.saveActionDelayMilliSeconds(value) + case .home(.delegate(.selectedCollectibles)): + state.path = [.collectibles] + return .none - case .toggleSimulation: - state.simulation.toggle() - dependencies.preferencesClient.saveSimulationEnabled(state.simulation) + case .home(.delegate(.selectedFigures)): + state.path = [.figures] + return .none - case .updatePreferredFigure(let value): - state.preferredFigure = value - dependencies.preferencesClient.savePreferredFigure(value) + case .home: + return .none - case .updateMusicVolume(let value): - state.musicVolume = value - dependencies.preferencesClient.saveMusicVolume(value) - } + case .figures: + return .none - return .none + case .collectibles: + return .none + } } } diff --git a/Modules/SettingsFeature/Sources/SettingsState+Builder.swift b/Modules/SettingsFeature/Sources/SettingsState+Builder.swift deleted file mode 100644 index cf664f274..000000000 --- a/Modules/SettingsFeature/Sources/SettingsState+Builder.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// SettingsState+Builder.swift -// -// -// Created by Stephano Hugues TELOLAHY on 23/02/2024. -// -public extension SettingsFeature.State { - class Builder { - private var playersCount: Int = 0 - private var actionDelayMilliSeconds: Int = 0 - private var simulation: Bool = false - private var preferredFigure: String? - private var musicVolume: Float = 1.0 - - public func build() -> SettingsFeature.State { - .init( - playersCount: playersCount, - actionDelayMilliSeconds: actionDelayMilliSeconds, - simulation: simulation, - preferredFigure: preferredFigure, - musicVolume: musicVolume - ) - } - - public func withPlayersCount(_ value: Int) -> Self { - playersCount = value - return self - } - - public func withSimulation(_ value: Bool) -> Self { - simulation = value - return self - } - - public func withActionDelayMilliSeconds(_ value: Int) -> Self { - actionDelayMilliSeconds = value - return self - } - - public func withPreferredFigure(_ value: String?) -> Self { - preferredFigure = value - return self - } - - public func withMusicVolume(_ value: Float) -> Self { - musicVolume = value - return self - } - } - - static func makeBuilder() -> Builder { - Builder() - } -} diff --git a/Modules/SettingsFeature/Sources/SettingsView.swift b/Modules/SettingsFeature/Sources/SettingsView.swift new file mode 100644 index 000000000..45f753533 --- /dev/null +++ b/Modules/SettingsFeature/Sources/SettingsView.swift @@ -0,0 +1,82 @@ +// +// SettingsView.swift +// WildWestOnline +// +// Created by Stephano Hugues TELOLAHY on 15/09/2024. +// +import Redux +import SwiftUI + +public struct SettingsView: View { + public typealias ViewStore = Store + + @StateObject private var store: ViewStore + + public init(store: @escaping () -> ViewStore) { + // SwiftUI ensures that the following initialization uses the + // closure only once during the lifetime of the view. + _store = StateObject(wrappedValue: store()) + } + + @State private var path: [SettingsFeature.State.Destination] = [] + + public var body: some View { + NavigationStack(path: $path) { + SettingsHomeView { + store.projection( + state: \.home, + action: { .home($0) } + ) + } +#if os(iOS) || os(tvOS) || os(visionOS) + .navigationBarTitleDisplayMode(.inline) +#endif + .navigationDestination(for: SettingsFeature.State.Destination.self) { + viewForDestination($0) + } + } + // Fix Error `Update NavigationAuthority bound path tried to update multiple times per frame` + .onReceive(store.$state) { state in + let newPath = state.path + if newPath != path { + path = newPath + } + } + .onChange(of: path) { _, newPath in + if newPath != store.state.path { + Task { + await store.dispatch(.setPath(newPath)) + } + } + } + .presentationDetents([.large]) + } + + @ViewBuilder private func viewForDestination(_ destination: SettingsFeature.State.Destination) -> some View { + switch destination { + case .figures: + SettingsFiguresView { + store.projection( + state: \.figures, + action: { .figures($0) } + ) + } + + case .collectibles: + SettingsCollectiblesView { + store.projection( + state: \.collectibles, + action: { .collectibles($0) } + ) + } + } + } +} + +#Preview { + SettingsView { + .init( + initialState: .init() + ) + } +} diff --git a/Modules/SettingsFeature/Tests/SettingsFeatureTests.swift b/Modules/SettingsFeature/Tests/SettingsFeatureTests.swift deleted file mode 100644 index 47030b9a1..000000000 --- a/Modules/SettingsFeature/Tests/SettingsFeatureTests.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// SettingsFeatureTest.swift -// -// -// Created by Stephano Hugues TELOLAHY on 23/02/2024. -// - -import Testing -import Redux -import SettingsFeature -import PreferencesClient - -struct SettingsFeatureTests { - private typealias SettingsStore = Store - - private func createSettingsStore(initialState: SettingsFeature.State) async -> SettingsStore { - await .init( - initialState: initialState, - reducer: SettingsFeature.reducer - ) - } - - @Test func updatePlayersCount() async throws { - // Given - let state = SettingsFeature.State.makeBuilder().withPlayersCount(2).build() - let sut = await createSettingsStore(initialState: state) - - // When - let action = SettingsFeature.Action.updatePlayersCount(5) - await sut.dispatch(action) - - // Then - await #expect(sut.state.playersCount == 5) - } - - @Test func toggleSimulation() async throws { - // Given - let state = SettingsFeature.State.makeBuilder().withSimulation(true).build() - let sut = await createSettingsStore(initialState: state) - - // When - let action = SettingsFeature.Action.toggleSimulation - await sut.dispatch(action) - - // Then - await #expect(sut.state.simulation == false) - } - - @Test func updateWaitDelay() async throws { - // Given - let state = SettingsFeature.State.makeBuilder().withActionDelayMilliSeconds(0).build() - let sut = await createSettingsStore(initialState: state) - - // When - let action = SettingsFeature.Action.updateActionDelayMilliSeconds(500) - await sut.dispatch(action) - - // Then - await #expect(sut.state.actionDelayMilliSeconds == 500) - } -} diff --git a/Modules/SettingsFeature/Tests/SettingsHomeFeatureTests.swift b/Modules/SettingsFeature/Tests/SettingsHomeFeatureTests.swift new file mode 100644 index 000000000..f6d6c572c --- /dev/null +++ b/Modules/SettingsFeature/Tests/SettingsHomeFeatureTests.swift @@ -0,0 +1,79 @@ +// +// SettingsHomeFeatureTests.swift +// +// +// Created by Stephano Hugues TELOLAHY on 23/02/2024. +// + +import Testing +import Redux +@testable import SettingsFeature +import PreferencesClient + +struct SettingsHomeFeatureTests { + private typealias SettingsStore = Store + + private func createSettingsStore(initialState: SettingsHomeFeature.State) async -> SettingsStore { + await .init( + initialState: initialState, + reducer: SettingsHomeFeature.reducer + ) + } + + @Test func updatePlayersCount() async throws { + // Given + let state = SettingsHomeFeature.State.create(playersCount: 2) + let sut = await createSettingsStore(initialState: state) + + // When + let action = SettingsHomeFeature.Action.didUpdatePlayersCount(5) + await sut.dispatch(action) + + // Then + await #expect(sut.state.playersCount == 5) + } + + @Test func toggleSimulation() async throws { + // Given + let state = SettingsHomeFeature.State.create(simulation: true) + let sut = await createSettingsStore(initialState: state) + + // When + let action = SettingsHomeFeature.Action.didToggleSimulation + await sut.dispatch(action) + + // Then + await #expect(sut.state.simulation == false) + } + + @Test func updateWaitDelay() async throws { + // Given + let state = SettingsHomeFeature.State.create(actionDelayMilliSeconds: 0) + let sut = await createSettingsStore(initialState: state) + + // When + let action = SettingsHomeFeature.Action.didUpdateActionDelayMilliSeconds(500) + await sut.dispatch(action) + + // Then + await #expect(sut.state.actionDelayMilliSeconds == 500) + } +} + +extension SettingsHomeFeature.State { + static func create( + playersCount: Int = 0, + actionDelayMilliSeconds: Int = 0, + simulation: Bool = false, + preferredFigure: String? = nil, + musicVolume: Float = 0 + ) -> Self { + .init( + playersCount: playersCount, + actionDelayMilliSeconds: actionDelayMilliSeconds, + simulation: simulation, + preferredFigure: preferredFigure, + musicVolume: musicVolume + ) + } +} diff --git a/Modules/SettingsUI/Sources/Collectibles/SettingsCollectiblesViewState.swift b/Modules/SettingsUI/Sources/Collectibles/SettingsCollectiblesViewState.swift deleted file mode 100644 index 8c56230dd..000000000 --- a/Modules/SettingsUI/Sources/Collectibles/SettingsCollectiblesViewState.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// SettingsFiguresViewState.swift -// WildWestOnline -// -// Created by Hugues Stéphano TELOLAHY on 16/10/2025. -// -import Redux -import AppFeature - -extension SettingsCollectiblesView { - struct ViewState: Equatable { - let cards: [Card] - - struct Card: Equatable { - let name: String - let description: String? - } - } - - typealias ViewStore = Store -} - -extension SettingsCollectiblesView.ViewState { - init?(appState: AppFeature.State) { - cards = appState.cardLibrary.cards.filter { $0.type == .collectible } - .map { - .init( - name: $0.name, - description: $0.description - ) - } - } -} diff --git a/Modules/SettingsUI/Sources/Figures/SettingsFiguresViewState.swift b/Modules/SettingsUI/Sources/Figures/SettingsFiguresViewState.swift deleted file mode 100644 index cb4e83856..000000000 --- a/Modules/SettingsUI/Sources/Figures/SettingsFiguresViewState.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// SettingsFiguresViewState.swift -// WildWestOnline -// -// Created by Hugues Stéphano TELOLAHY on 16/10/2025. -// -import Redux -import AppFeature - -extension SettingsFiguresView { - struct ViewState: Equatable { - let figures: [Figure] - - struct Figure: Equatable { - let name: String - let description: String? - let isFavorite: Bool - } - } - - typealias ViewStore = Store -} - -extension SettingsFiguresView.ViewState { - init?(appState: AppFeature.State) { - figures = appState.cardLibrary.cards.filter { $0.type == .figure } - .map { - .init( - name: $0.name, - description: $0.description, - isFavorite: $0.name == appState.settings.preferredFigure - ) - } - } -} diff --git a/Modules/SettingsUI/Sources/Root/SettingsRootViewState.swift b/Modules/SettingsUI/Sources/Root/SettingsRootViewState.swift deleted file mode 100644 index c25e1f216..000000000 --- a/Modules/SettingsUI/Sources/Root/SettingsRootViewState.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// SettingsRootViewState.swift -// WildWestOnline -// -// Created by Hugues Stéphano TELOLAHY on 16/10/2025. -// -import Redux -import AppFeature - -extension SettingsRootView { - struct ViewState: Equatable { - let minPlayersCount = 2 - let maxPlayersCount = 7 - let speedOptions: [SpeedOption] = SpeedOption.all - let playersCount: Int - let speedIndex: Int - let simulation: Bool - let preferredFigure: String? - let musicVolume: Float - - struct SpeedOption: Equatable { - let label: String - let value: Int - - static let all: [Self] = [ - .init(label: "Normal", value: 500), - .init(label: "Fast", value: 0) - ] - } - } - - typealias ViewStore = Store -} - -extension SettingsRootView.ViewState { - init?(appState: AppFeature.State) { - playersCount = appState.settings.playersCount - speedIndex = SpeedOption.all.firstIndex { $0.value == appState.settings.actionDelayMilliSeconds } ?? 0 - simulation = appState.settings.simulation - preferredFigure = appState.settings.preferredFigure - musicVolume = appState.settings.musicVolume - } -} diff --git a/Modules/SettingsUI/Sources/SettingsCoordinator.swift b/Modules/SettingsUI/Sources/SettingsCoordinator.swift deleted file mode 100644 index fdc7acba7..000000000 --- a/Modules/SettingsUI/Sources/SettingsCoordinator.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// SettingsCoordinator.swift -// WildWestOnline -// -// Created by Stephano Hugues TELOLAHY on 15/09/2024. -// -import Redux -import SwiftUI -import AppFeature -import NavigationFeature - -public struct SettingsCoordinator: View { - @ObservedObject private var store: AppStore - @State private var path: [SettingsNavigationFeature.State.Destination] = [] - - public init(store: AppStore) { - self.store = store - } - - public var body: some View { - NavigationStack(path: $path) { - SettingsRootView { store.projection(state: SettingsRootView.ViewState.init, action: \.self) } - .navigationDestination(for: SettingsNavigationFeature.State.Destination.self) { - viewForDestination($0) - } - } - // Fix Error `Update NavigationAuthority bound path tried to update multiple times per frame` - .onReceive(store.$state) { state in - guard let newPath = state.navigation.settingsSheet?.path else { - return - } - - path = newPath - } - .onChange(of: path) { _, newPath in - guard newPath != store.state.navigation.settingsSheet?.path else { - return - } - - Task { - await store.dispatch(.navigation(.settingsSheet(.setPath(newPath)))) - } - } - .presentationDetents([.large]) - } - - @ViewBuilder private func viewForDestination(_ destination: SettingsNavigationFeature.State.Destination) -> some View { - switch destination { - case .figures: SettingsFiguresView { store.projection(state: SettingsFiguresView.ViewState.init, action: \.self) } - case .collectibles: SettingsCollectiblesView { store.projection(state: SettingsCollectiblesView.ViewState.init, action: \.self) } - } - } -} - -#Preview { - SettingsCoordinator( - store: Store( - initialState: .previewState - ) - ) -} - -private extension AppFeature.State { - static var previewState: Self { - .init( - cardLibrary: .init(), - navigation: .init(), - settings: .makeBuilder().build() - ) - } -} diff --git a/Modules/SettingsUI/Tests/SetttingsCoordinatorTest.swift b/Modules/SettingsUI/Tests/SetttingsCoordinatorTest.swift deleted file mode 100644 index a5ebcade8..000000000 --- a/Modules/SettingsUI/Tests/SetttingsCoordinatorTest.swift +++ /dev/null @@ -1,11 +0,0 @@ -// -// SetttingsCoordinatorTest.swift -// -// -// Created by Stephano Hugues TELOLAHY on 24/02/2024. -// - -import Testing - -struct SetttingsCoordinatorTest { -} diff --git a/Modules/Theme/Sources/AppTheme.swift b/Modules/Theme/Sources/Theme.swift similarity index 80% rename from Modules/Theme/Sources/AppTheme.swift rename to Modules/Theme/Sources/Theme.swift index 8f8f57bcb..3a17a5d37 100644 --- a/Modules/Theme/Sources/AppTheme.swift +++ b/Modules/Theme/Sources/Theme.swift @@ -1,14 +1,12 @@ // -// AppTheme.swift +// Theme.swift // // // Created by Stephano Hugues TELOLAHY on 24/02/2024. // import SwiftUI -/// App theme -/// -public protocol AppTheme { +public protocol Theme { var colorBackground: Color { get } var colorAccent: Color { get } var fontHeadline: Font { get } @@ -16,10 +14,10 @@ public protocol AppTheme { } public extension EnvironmentValues { - @Entry var theme: AppTheme = DefaultTheme() + @Entry var theme: Theme = DefaultTheme() } -struct DefaultTheme: AppTheme { +struct DefaultTheme: Theme { private static let fontName = "AmericanTypewriter-Bold" var colorBackground = Color("BackgroundColor", bundle: .module) diff --git a/Modules/Utils/Sources/DocumentConvertible.swift b/Modules/Utils/Sources/DocumentConvertible.swift deleted file mode 100644 index e2b4dd139..000000000 --- a/Modules/Utils/Sources/DocumentConvertible.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// DocumentConvertible.swift -// -// -// Created by Hugues Telolahy on 13/12/2023. -// - -import Foundation - -public protocol DocumentConvertible { - var dictionary: [String: Any] { get } - - init(dictionary: [String: Any]) throws -} - -/// Extension allowing serialization to a Document data -public extension DocumentConvertible where Self: Codable { - var dictionary: [String: Any] { - guard let dict = try? JSONEncoder().encodeToDictionary(self) else { - fatalError("unexpected") - } - - return dict - } - - init(dictionary: [String: Any]) throws { - guard let model = try? JSONDecoder().decode(Self.self, from: dictionary) else { - fatalError("unexpected") - } - - self = model - } -} - -private extension JSONEncoder { - func encodeToDictionary(_ value: T) throws -> [String: Any]? where T: Encodable { - let data = try encode(value) - return try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] - } -} - -private extension JSONDecoder { - func decode(_ type: T.Type, from dictionary: [String: Any]) throws -> T where T: Decodable { - let data = try JSONSerialization.data(withJSONObject: dictionary, options: []) - return try decode(type, from: data) - } -} diff --git a/Modules/Utils/Tests/DocumentConvertibleTest.swift b/Modules/Utils/Tests/DocumentConvertibleTest.swift deleted file mode 100644 index b1d0d1c08..000000000 --- a/Modules/Utils/Tests/DocumentConvertibleTest.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// DocumentConvertibleTest.swift -// -// -// Created by Hugues Telolahy on 13/12/2023. -// - -import Utils -import Testing -import Foundation - -struct DocumentConvertibleTest { - @Test func EncodingToDocument() async throws { - let document: MyDocument = .init(name: "beer") - let dictionary: [String: Any] = [ - "name": "beer" - ] - #expect(document.dictionary.isEqual(to: dictionary)) - } - - @Test func DecodingFromDocument() async throws { - let document: MyDocument = .init(name: "beer") - let dictionary: [String: Any] = [ - "name": "beer" - ] - #expect(try MyDocument(dictionary: dictionary) == document) - } -} - -private struct MyDocument: Equatable, Codable, DocumentConvertible { - let name: String -} - -private extension Dictionary where Key == String, Value == Any { - func isEqual(to other: [String: Any]) -> Bool { - let lhs = self as NSDictionary - let rhs = other as NSDictionary - return lhs == rhs - } -}