diff --git a/Modules/GameEngineKit/Examples/GameEngineKitExample/Sources/App/ContentView.swift b/Modules/GameEngineKit/Examples/GameEngineKitExample/Sources/App/ContentView.swift index 85e53e5fc..a5a1ce5b2 100644 --- a/Modules/GameEngineKit/Examples/GameEngineKitExample/Sources/App/ContentView.swift +++ b/Modules/GameEngineKit/Examples/GameEngineKitExample/Sources/App/ContentView.swift @@ -195,6 +195,80 @@ struct ContentView: View { Spacer() } + + HStack(spacing: 20) { + Text("Action Then DnDGrid") + .font(.title) + .padding() + + NavigationLink("Observe Image Then Drag & Drop Categories", destination: { + let gameplay = NewGameplayAssociateCategories(choices: NewGameplayAssociateCategories.kDefaultChoices) + let coordinator = DnDGridCoordinatorAssociateCategories(gameplay: gameplay, + action: .ipad(type: .image("sport_dance_player_man"))) + let viewModel = DnDGridViewModel(coordinator: coordinator) + + return DnDGridView(viewModel: viewModel) + .navigationTitle("Observe Image Then Drag & Drop Categories") + .navigationBarTitleDisplayMode(.large) + }) + .tint(.pink) + .buttonStyle(.borderedProminent) + .frame(maxWidth: .infinity) + + NavigationLink("Observe SFSymbol Then Drag & Drop Categories", destination: { + let gameplay = NewGameplayAssociateCategories(choices: NewGameplayAssociateCategories.kDefaultChoices) + let coordinator = DnDGridCoordinatorAssociateCategories(gameplay: gameplay, action: .ipad(type: .sfsymbol("star"))) + let viewModel = DnDGridViewModel(coordinator: coordinator) + + return DnDGridView(viewModel: viewModel) + .navigationTitle("Observe SFSymbol Then Drag & Drop Categories") + .navigationBarTitleDisplayMode(.large) + }) + .tint(.pink) + .buttonStyle(.borderedProminent) + .frame(maxWidth: .infinity) + + NavigationLink("Listen Then Drag & Drop Categories", destination: { + let gameplay = NewGameplayAssociateCategories(choices: NewGameplayAssociateCategories.kDefaultChoices) + let coordinator = DnDGridCoordinatorAssociateCategories(gameplay: gameplay, action: .ipad(type: .audio("sound_animal_duck"))) + let viewModel = DnDGridViewModel(coordinator: coordinator) + + return DnDGridView(viewModel: viewModel) + .navigationTitle("Listen Then Drag & Drop Categories") + .navigationBarTitleDisplayMode(.large) + }) + .tint(.pink) + .buttonStyle(.borderedProminent) + .frame(maxWidth: .infinity) + + NavigationLink("Listen Speech Then Drag & Drop Categories", destination: { + let gameplay = NewGameplayAssociateCategories(choices: NewGameplayAssociateCategories.kDefaultImageChoices) + let coordinator = DnDGridCoordinatorAssociateCategories(gameplay: gameplay, action: .ipad(type: .speech("Correct answer"))) + let viewModel = DnDGridViewModel(coordinator: coordinator) + + return DnDGridView(viewModel: viewModel) + .navigationTitle("Listen Speech Then Drag & Drop Categories") + .navigationBarTitleDisplayMode(.large) + }) + .tint(.pink) + .buttonStyle(.borderedProminent) + .frame(maxWidth: .infinity) + + NavigationLink("Robot Then Drag & Drop Categories", destination: { + let gameplay = NewGameplayAssociateCategories(choices: NewGameplayAssociateCategories.kDefaultChoices) + let coordinator = DnDGridCoordinatorAssociateCategories(gameplay: gameplay, action: .robot(type: .color("red"))) + let viewModel = DnDGridViewModel(coordinator: coordinator) + + return DnDGridView(viewModel: viewModel) + .navigationTitle("Listen Speech Then Drag & Drop Categories") + .navigationBarTitleDisplayMode(.large) + }) + .tint(.pink) + .buttonStyle(.borderedProminent) + .frame(maxWidth: .infinity) + + Spacer() + } } Text("Or choose a template") diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Gameplays/NewGameplayAssociateCategories.swift b/Modules/GameEngineKit/Sources/_NewSystem/Gameplays/NewGameplayAssociateCategories.swift index bfb94e5b6..72a50a232 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Gameplays/NewGameplayAssociateCategories.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Gameplays/NewGameplayAssociateCategories.swift @@ -104,6 +104,15 @@ public extension NewGameplayAssociateCategories { NewGameplayAssociateCategoriesChoice(value: "Maison", category: nil, type: .text), ] + static let kDefaultImageChoices: [NewGameplayAssociateCategoriesChoice] = [ + NewGameplayAssociateCategoriesChoice(value: "pictograms-weather-sun_yellow-0106", category: .categoryA, type: .image), + NewGameplayAssociateCategoriesChoice(value: "pictograms-animals-arctic-penguin_yellow-0088", category: .categoryB, type: .image), + NewGameplayAssociateCategoriesChoice(value: "pictograms-weather-sun_yellow-0106", category: .categoryA, type: .image), + NewGameplayAssociateCategoriesChoice(value: "pictograms-animals-arctic-penguin_yellow-0088", category: .categoryB, type: .image), + NewGameplayAssociateCategoriesChoice(value: "pictograms-weather-sun_yellow-0106", category: .categoryA, type: .image), + NewGameplayAssociateCategoriesChoice(value: "Maison", category: nil, type: .text), + ] + static let kDefaultChoicesWithZones: [NewGameplayAssociateCategoriesChoice] = [ NewGameplayAssociateCategoriesChoice(value: "sun", category: .categoryA, type: .text), NewGameplayAssociateCategoriesChoice(value: "car", category: .categoryB, type: .text), diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/Grid/Coordinators/DnDGridCoordinator+AssociateCategories.swift b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/Grid/Coordinators/DnDGridCoordinator+AssociateCategories.swift index fdbbe9cd3..a32c2c0e6 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/Grid/Coordinators/DnDGridCoordinator+AssociateCategories.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/Grid/Coordinators/DnDGridCoordinator+AssociateCategories.swift @@ -3,6 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 import Combine +import ContentKit import SpriteKit import SwiftUI import UtilsKit @@ -12,17 +13,18 @@ import UtilsKit public class DnDGridCoordinatorAssociateCategories: DnDGridGameplayCoordinatorProtocol { // MARK: Lifecycle - public init(gameplay: NewGameplayAssociateCategories) { + public init(gameplay: NewGameplayAssociateCategories, action: Exercise.Action? = nil) { self.gameplay = gameplay - self.uiChoices.value.choices = gameplay.choices.map { choice in - DnDAnswerNode(id: choice.id, value: choice.value, type: choice.type, size: self.uiChoices.value.choiceSize(for: gameplay.choices.count)) + self.uiModel.value.action = action + self.uiModel.value.choices = gameplay.choices.map { choice in + DnDAnswerNode(id: choice.id, value: choice.value, type: choice.type, size: self.uiModel.value.choiceSize(for: gameplay.choices.count)) } } // MARK: Public - public private(set) var uiChoices = CurrentValueSubject(.zero) + public private(set) var uiModel = CurrentValueSubject(.zero) public func onTouch(_ event: DnDTouchEvent, choice: DnDAnswerNode, destination: DnDAnswerNode? = nil) { switch event { @@ -76,7 +78,7 @@ public class DnDGridCoordinatorAssociateCategories: DnDGridGameplayCoordinatorPr private func updateChoiceState(for choice: NewGameplayAssociateCategoriesChoice, to state: State) { guard let index = self.gameplay.choices.firstIndex(where: { $0.id == choice.id }) else { return } - self.updateUINodeState(node: self.uiChoices.value.choices[index], state: state) + self.updateUINodeState(node: self.uiModel.value.choices[index], state: state) } private func choiceAlreadySelected(choice: NewGameplayAssociateCategoriesChoice) -> Bool { diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/Grid/Coordinators/DnDGridGameplayCoordinatorProtocol.swift b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/Grid/Coordinators/DnDGridGameplayCoordinatorProtocol.swift index b9b9290d5..68bc81ca3 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/Grid/Coordinators/DnDGridGameplayCoordinatorProtocol.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/Grid/Coordinators/DnDGridGameplayCoordinatorProtocol.swift @@ -6,6 +6,6 @@ import Combine import Foundation public protocol DnDGridGameplayCoordinatorProtocol { - var uiChoices: CurrentValueSubject { get } + var uiModel: CurrentValueSubject { get } func onTouch(_ event: DnDTouchEvent, choice: DnDAnswerNode, destination: DnDAnswerNode?) } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/Grid/DnDGridUIModel.swift b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/Grid/DnDGridUIModel.swift new file mode 100644 index 000000000..7f2a385ba --- /dev/null +++ b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/Grid/DnDGridUIModel.swift @@ -0,0 +1,59 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import SpriteKit +import SwiftUI + +// MARK: - DnDGridViewUIChoicesWrapper + +public struct DnDGridUIModel { + static let zero = DnDGridUIModel(action: nil, choices: []) + + var action: Exercise.Action? + var choices: [DnDAnswerNode] + + // swiftlint:disable cyclomatic_complexity + + func choiceSize(for numberOfChoices: Int) -> CGSize { + switch self.action { + case .ipad(type: .image), + .ipad(type: .sfsymbol): + switch numberOfChoices { + case 1...4: + CGSize(width: 180, height: 180) + case 5...6: + CGSize(width: 150, height: 150) + default: + CGSize(width: 150, height: 150) + } + case .none: + switch numberOfChoices { + case 1...2: + CGSize(width: 300, height: 300) + case 3...4: + CGSize(width: 240, height: 240) + case 5...6: + CGSize(width: 200, height: 200) + default: + CGSize(width: 200, height: 200) + } + default: + switch numberOfChoices { + case 1...2: + CGSize(width: 220, height: 220) + case 3...4: + CGSize(width: 200, height: 200) + case 5: + CGSize(width: 160, height: 160) + case 6: + CGSize(width: 150, height: 150) + default: + CGSize(width: 150, height: 150) + } + } + } + + // swiftlint:enable cyclomatic_complexity +} diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/Grid/DnDGridView.swift b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/Grid/DnDGridView.swift index ba2e032f0..ca21614a4 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/Grid/DnDGridView.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/Grid/DnDGridView.swift @@ -17,12 +17,44 @@ public struct DnDGridView: View { // MARK: Public public var body: some View { - GeometryReader { proxy in - SpriteView(scene: self.makeScene(size: proxy.size), options: [.allowsTransparency]) - .frame(width: proxy.size.width, height: proxy.size.height) - .onAppear { - self.scene = self.getScene(for: self.viewModel.choices.count, size: proxy.size) + HStack(spacing: 0) { + if let action = self.viewModel.action { + Button { + // nothing to do } + label: { + ActionButtonView(action: action) + .padding(20) + } + .simultaneousGesture( + TapGesture() + .onEnded { _ in + withAnimation { + self.viewModel.isActionTriggered = true + } + } + ) + + Divider() + .opacity(0.4) + .frame(maxHeight: 500) + .padding(.vertical, 20) + } + + Spacer() + + GeometryReader { proxy in + SpriteView(scene: self.makeScene(size: proxy.size), options: [.allowsTransparency]) + .frame(width: proxy.size.width, height: proxy.size.height) + .onAppear { + self.scene = self.getScene(for: self.viewModel.choices.count, size: proxy.size) + } + } + .colorMultiply(self.viewModel.isActionTriggered ? .white : .gray.opacity(0.4)) + .animation(.easeOut(duration: 0.3), value: self.viewModel.isActionTriggered) + .allowsHitTesting(self.viewModel.isActionTriggered) + + Spacer() } } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/Grid/DnDGridViewModel.swift b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/Grid/DnDGridViewModel.swift index f83120008..79d8c8fbf 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/Grid/DnDGridViewModel.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/Grid/DnDGridViewModel.swift @@ -3,6 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 import Combine +import ContentKit import SwiftUI // MARK: - DnDGridViewModel @@ -11,12 +12,14 @@ public class DnDGridViewModel: ObservableObject { // MARK: Lifecycle public init(coordinator: DnDGridGameplayCoordinatorProtocol) { - self.choices = coordinator.uiChoices.value.choices + self.choices = coordinator.uiModel.value.choices + self.action = coordinator.uiModel.value.action + self.isActionTriggered = (self.action == nil) ? true : false self.coordinator = coordinator - self.coordinator.uiChoices + self.coordinator.uiModel .receive(on: DispatchQueue.main) - .sink { [weak self] choices in - self?.choices = choices.choices + .sink { [weak self] model in + self?.choices = model.choices } .store(in: &self.cancellables) } @@ -29,8 +32,11 @@ public class DnDGridViewModel: ObservableObject { // MARK: Internal + @Published var isActionTriggered = false @Published var choices: [DnDAnswerNode] = [] + let action: Exercise.Action? + // MARK: Private private let coordinator: DnDGridGameplayCoordinatorProtocol