Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
c499e6e
wip
stephtelolahy Dec 3, 2025
de7eedc
wip
stephtelolahy Dec 3, 2025
f60fc1b
wip
stephtelolahy Dec 3, 2025
72073dd
wip
stephtelolahy Dec 3, 2025
ee1b4af
wip
stephtelolahy Dec 3, 2025
d5c1c43
wip
stephtelolahy Dec 3, 2025
5bda86c
wip
stephtelolahy Dec 3, 2025
0eed264
wip
stephtelolahy Dec 3, 2025
3674156
wip
stephtelolahy Dec 3, 2025
f722128
wip
stephtelolahy Dec 3, 2025
efe7e9a
wip
stephtelolahy Dec 3, 2025
539dae3
wip
stephtelolahy Dec 3, 2025
3faaa65
wip
stephtelolahy Dec 3, 2025
e2e5921
wip
stephtelolahy Dec 4, 2025
625292f
wip
stephtelolahy Dec 4, 2025
d4a23c8
wip
stephtelolahy Dec 4, 2025
0b92f66
wip
stephtelolahy Dec 4, 2025
aead320
wip
stephtelolahy Dec 4, 2025
3307364
wip
stephtelolahy Dec 4, 2025
a6220c4
wip
stephtelolahy Dec 5, 2025
a37d6a5
wip
stephtelolahy Dec 5, 2025
7f6e84f
wip
stephtelolahy Dec 5, 2025
5e4188e
wip
stephtelolahy Dec 5, 2025
79365d6
wip
stephtelolahy Dec 6, 2025
27196d3
wip
stephtelolahy Dec 6, 2025
e7028d2
wip
stephtelolahy Dec 6, 2025
d51c010
wip
stephtelolahy Dec 6, 2025
8f9eb5e
wip
stephtelolahy Dec 6, 2025
b655da3
wip
stephtelolahy Dec 7, 2025
5361904
wip
stephtelolahy Dec 7, 2025
265e49b
wip
stephtelolahy Dec 7, 2025
30a2559
wip
stephtelolahy Dec 7, 2025
7e3c291
wip
stephtelolahy Dec 7, 2025
c4186f4
wip
stephtelolahy Dec 7, 2025
169b0c5
wip
stephtelolahy Dec 7, 2025
4c16d99
gamesessionn feature
stephtelolahy Dec 8, 2025
81038a5
wip
stephtelolahy Dec 8, 2025
62ed1a0
wip
stephtelolahy Dec 8, 2025
323bed4
wip
stephtelolahy Dec 9, 2025
6b5ce7e
wip
stephtelolahy Dec 9, 2025
59533ed
wip
stephtelolahy Dec 9, 2025
67e1efa
wip
stephtelolahy Dec 10, 2025
cd76fcb
wip
stephtelolahy Dec 10, 2025
73e5c08
wip
stephtelolahy Dec 10, 2025
93b1734
wip
stephtelolahy Dec 10, 2025
9b7c099
wip
stephtelolahy Dec 10, 2025
2834fe9
wip
stephtelolahy Dec 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 0 additions & 22 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 6 additions & 5 deletions App/WildWestOnline.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */

Expand All @@ -26,7 +26,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
26F4048D2EB5CE5700F5FCA9 /* AppBootstrap in Frameworks */,
98A1A0BA2EE4A1D7003EB768 /* AppBuilder in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -77,7 +77,7 @@
);
name = WildWestOnline;
packageProductDependencies = (
26F4048C2EB5CE5700F5FCA9 /* AppBootstrap */,
98A1A0B92EE4A1D7003EB768 /* AppBuilder */,
);
productName = ReduxGameDSL;
productReference = 2699EEFD29D93AD00030ACCD /* WildWestOnline.app */;
Expand Down Expand Up @@ -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 */
};
Expand Down
2 changes: 1 addition & 1 deletion App/WildWestOnlineApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// Created by Hugues Telolahy on 02/04/2023.
//
import SwiftUI
import AppBootstrap
import AppBuilder

@main
struct WildWestOnlineApp: App {
Expand Down
2 changes: 1 addition & 1 deletion Modules/.swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
68 changes: 0 additions & 68 deletions Modules/AppBootstrap/Sources/AppBuilder.swift

This file was deleted.

37 changes: 37 additions & 0 deletions Modules/AppBuilder/Sources/AppBuilder.swift
Original file line number Diff line number Diff line change
@@ -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<AppFeature.State, AppFeature.Action>(
initialState: .init(),
reducer: AppFeature.reducer,
dependencies: dependencies
)

return AppView {
store
}
}
}
149 changes: 86 additions & 63 deletions Modules/AppFeature/Sources/AppFeature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,111 +6,134 @@
//

import Redux
import NavigationFeature
import HomeFeature
import SettingsFeature
import GameFeature
import AudioClient
import PreferencesClient

public typealias AppStore = Store<AppFeature.State, AppFeature.Action>
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<State, Action> {
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<Action> {
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
}
}
}
Loading