From ab6d39238687d851e2f153fc5d92d85fbb055460 Mon Sep 17 00:00:00 2001 From: Ladislas de Toldi Date: Wed, 4 Dec 2024 20:58:09 +0100 Subject: [PATCH 01/16] =?UTF-8?q?=F0=9F=93=8C=20(pre-commit):=20Upgrade=20?= =?UTF-8?q?swiftformat,=20swiftlint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f6737bfc4f..c96bcebfd3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,14 +36,14 @@ repos: exclude: '(\.vscode/settings\.json|\.jtd\.json$|.*\.xcassets/.*|.*\.colorset/.*|\.animation\.lottie\.json$)' - repo: https://github.com/realm/SwiftLint - rev: 0.57.0 + rev: 0.57.1 hooks: - id: swiftlint entry: swiftlint args: ["--use-alternative-excluding"] - repo: https://github.com/nicklockwood/SwiftFormat - rev: 0.54.5 + rev: 0.55.3 hooks: - id: swiftformat From 1cdff66d4114af84a1869a2f2dd2e63207bd487c Mon Sep 17 00:00:00 2001 From: Hugo Pezziardi Date: Tue, 3 Dec 2024 16:01:25 +0100 Subject: [PATCH 02/16] =?UTF-8?q?=E2=9C=A8=20(New=20GEK):=20Updgrade=20Ans?= =?UTF-8?q?werNode=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DnD/SKComponents/DnDAnswerNode.swift | 66 ++++++++++++------- .../DnD/SKComponents/DnDShadowNode.swift | 39 ++++------- 2 files changed, 54 insertions(+), 51 deletions(-) diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/SKComponents/DnDAnswerNode.swift b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/SKComponents/DnDAnswerNode.swift index 824a34f227..2a4068fe70 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/SKComponents/DnDAnswerNode.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/SKComponents/DnDAnswerNode.swift @@ -19,37 +19,57 @@ public class DnDAnswerNode: SKSpriteNode { fatalError("Image not found") } - super.init(texture: SKTexture(image: image), color: .clear, size: size) + let renderer = UIGraphicsImageRenderer(size: size) + let finalImage = renderer.image { _ in + let rect = CGRect(origin: .zero, size: size) + let path = UIBezierPath(roundedRect: rect, cornerRadius: 10 / 57 * size.width) + path.addClip() + image.draw(in: rect) + } + + let texture = SKTexture(image: finalImage) + super.init(texture: texture, color: .clear, size: texture.size()) case .sfsymbol: - guard let image = UIImage(systemName: value, withConfiguration: UIImage.SymbolConfiguration(pointSize: size.height)) else { + guard let image = UIImage(systemName: value, withConfiguration: UIImage.SymbolConfiguration(pointSize: size.height * 3)) else { fatalError("SFSymbol not found") } - super.init(texture: SKTexture(image: image), color: .clear, size: size) + super.init(texture: SKTexture(image: image), color: .clear, size: CGSize(width: size.width * 0.8, height: size.height * 0.8)) case .text: - super.init(texture: nil, color: .clear, size: size) - - let circle = SKShapeNode(circleOfRadius: size.width / 2) - - circle.fillColor = .white - circle.strokeColor = .black - circle.lineWidth = 0.5 - circle.zPosition = -1 - circle.position = CGPoint(x: 0, y: 0) - - self.addChild(circle) - - let label = SKLabelNode(text: value) - - label.fontSize = 20 - label.fontName = "AvenirNext-Bold" - label.fontColor = .black - label.position = CGPoint(x: 0, y: -10) - label.zPosition = 0 + let rectSize = CGSize(width: size.width, height: size.height) + let renderer = UIGraphicsImageRenderer(size: rectSize) + let finalImage = renderer.image { _ in + let rect = CGRect(origin: .zero, size: rectSize) + + let path = UIBezierPath(roundedRect: rect, cornerRadius: 10 / 57 * size.width) + path.addClip() + + UIColor.white.setFill() + path.fill() + + UIColor.gray.setStroke() + let strokeWidth: CGFloat = 2 + let borderRect = rect.insetBy(dx: strokeWidth / 2, dy: strokeWidth / 2) + let borderPath = UIBezierPath(roundedRect: borderRect, cornerRadius: 10 / 57 * size.width) + borderPath.stroke() + + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = .center + + let attributes: [NSAttributedString.Key: Any] = [ + .font: UIFont(name: "AvenirNext-Bold", size: 20) ?? UIFont.systemFont(ofSize: 20), + .foregroundColor: UIColor.black, + .paragraphStyle: paragraphStyle, + ] + + let textRect = rect.insetBy(dx: 10, dy: (rect.height - 20) / 2) + (value as NSString).draw(in: textRect, withAttributes: attributes) + } - self.addChild(label) + let texture = SKTexture(image: finalImage) + super.init(texture: texture, color: .clear, size: texture.size()) } self.name = value diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/SKComponents/DnDShadowNode.swift b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/SKComponents/DnDShadowNode.swift index dce65fcb44..6aca49638a 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/SKComponents/DnDShadowNode.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/SKComponents/DnDShadowNode.swift @@ -6,37 +6,20 @@ import SpriteKit class DnDShadowNode: SKSpriteNode { init(node: DnDAnswerNode) { - switch node.type { - case .text: - super.init(texture: nil, color: .clear, size: node.size) + super.init(texture: nil, color: .clear, size: node.size) - let shadowNode = SKShapeNode(circleOfRadius: node.size.width / 2) + let shadowTexture = node.texture?.copy() as? SKTexture + let shadowNode = SKSpriteNode(texture: shadowTexture) - shadowNode.fillColor = .black - shadowNode.strokeColor = .clear - shadowNode.alpha = 0.15 - shadowNode.position = node.position - shadowNode.zPosition = -1 + shadowNode.color = .black + shadowNode.colorBlendFactor = 1.0 + shadowNode.alpha = 0.15 + shadowNode.blendMode = .alpha + shadowNode.size = node.size + shadowNode.position = node.position + shadowNode.zPosition = -1 - self.addChild(shadowNode) - - case .sfsymbol, - .image: - super.init(texture: nil, color: .clear, size: node.size) - - let shadowTexture = node.texture?.copy() as? SKTexture - let shadowNode = SKSpriteNode(texture: shadowTexture) - - shadowNode.color = .black - shadowNode.colorBlendFactor = 1.0 - shadowNode.alpha = 0.15 - shadowNode.blendMode = .alpha - shadowNode.size = node.size - shadowNode.position = node.position - shadowNode.zPosition = -1 - - self.addChild(shadowNode) - } + self.addChild(shadowNode) } required init?(coder aDecoder: NSCoder) { From 57c9e0fbc7e483910dc7deae464786d7a7aa7c52 Mon Sep 17 00:00:00 2001 From: Hugo Pezziardi Date: Fri, 29 Nov 2024 11:23:05 +0100 Subject: [PATCH 03/16] =?UTF-8?q?=E2=9C=A8=20(New=20GEK):=20Add=20new=20Dr?= =?UTF-8?q?opZoneNode=20init=20with=20a=20node?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DnD/SKComponents/DnDDropZoneNode.swift | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/SKComponents/DnDDropZoneNode.swift b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/SKComponents/DnDDropZoneNode.swift index e995937b16..ae4a0e4b12 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/SKComponents/DnDDropZoneNode.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/SKComponents/DnDDropZoneNode.swift @@ -60,6 +60,27 @@ public class DnDDropZoneNode: SKSpriteNode { self.zPosition = 10 } + init(node: DnDAnswerNode, position: CGPoint = .zero) { + self.id = node.id + switch node.type { + case .text: + let image = DnDDropZoneNode.createRoundedRectImage(size: node.size) + let texture = SKTexture(image: image) + + super.init(texture: texture, color: .clear, size: node.size) + self.position = position + + case .sfsymbol, + .image: + let image = DnDDropZoneNode.createRoundedRectImage(size: node.size) + let texture = SKTexture(image: image) + + super.init(texture: texture, color: .clear, size: node.size) + self.position = position + } + self.position = position + } + @available(*, unavailable) required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") @@ -70,4 +91,34 @@ public class DnDDropZoneNode: SKSpriteNode { let id: String var initialPosition: CGPoint? var isDraggable = true + + // MARK: Private + + private static let dropzoneBackgroundColor: UIColor = .init( + light: UIColor.white, + dark: UIColor(displayP3Red: 242 / 255, green: 242 / 255, blue: 247 / 255, alpha: 1.0) + ) + + private static func createRoundedRectImage(size: CGSize) -> UIImage { + let rect = CGRect(origin: .zero, size: size) + let cornerRadius: CGFloat = 10 / 57 * size.width + let strokeWidth: CGFloat = 2.0 + + let renderer = UIGraphicsImageRenderer(size: size) + let image = renderer.image { context in + let context = context.cgContext + + context.setFillColor(self.dropzoneBackgroundColor.cgColor) + let path = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius) + path.fill() + + context.setStrokeColor(UIColor.gray.cgColor) + context.setLineWidth(strokeWidth) + let borderRect = rect.insetBy(dx: strokeWidth / 2, dy: strokeWidth / 2) + let borderPath = UIBezierPath(roundedRect: borderRect, cornerRadius: cornerRadius - strokeWidth / 2) + borderPath.stroke() + } + + return image + } } From d229e979cf88bb15f5dfa53d0a1f430b5548609a Mon Sep 17 00:00:00 2001 From: Hugo Pezziardi Date: Fri, 29 Nov 2024 11:24:28 +0100 Subject: [PATCH 04/16] =?UTF-8?q?=F0=9F=90=9B=20(New=20GEK):=20Fix=20Right?= =?UTF-8?q?Order=20GP's=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../_NewSystem/Gameplays/NewGameplayFindTheRightOrder.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Gameplays/NewGameplayFindTheRightOrder.swift b/Modules/GameEngineKit/Sources/_NewSystem/Gameplays/NewGameplayFindTheRightOrder.swift index b0fb0bd53d..d08e1afa6a 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Gameplays/NewGameplayFindTheRightOrder.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Gameplays/NewGameplayFindTheRightOrder.swift @@ -56,7 +56,7 @@ public class NewGameplayFindTheRightOrder: GameplayProtocol { } return self.orderedChoices.enumerated().map { index, choice in - if self.orderedChoices[index] == choice { + if choices[index] == choice { (choice: choice, correctPosition: true) } else { (choice: choice, correctPosition: false) From f9fc8bd564c092d0fa5a85f80df5b4c9962f6ae3 Mon Sep 17 00:00:00 2001 From: Hugo Pezziardi Date: Fri, 29 Nov 2024 11:25:29 +0100 Subject: [PATCH 05/16] =?UTF-8?q?=E2=9C=A8=20(New=20GEK):=20Add=20new=20se?= =?UTF-8?q?t=20of=20FindTheRightOrder=20choices?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Gameplays/NewGameplayFindTheRightOrder.swift | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Gameplays/NewGameplayFindTheRightOrder.swift b/Modules/GameEngineKit/Sources/_NewSystem/Gameplays/NewGameplayFindTheRightOrder.swift index d08e1afa6a..3f34c851e6 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Gameplays/NewGameplayFindTheRightOrder.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Gameplays/NewGameplayFindTheRightOrder.swift @@ -10,10 +10,11 @@ import Foundation public struct NewGameplayFindTheRightOrderChoice: Identifiable, Equatable { // MARK: Lifecycle - public init(id: String = UUID().uuidString, value: String, type: ChoiceType = .text) { + public init(id: String = UUID().uuidString, value: String, type: ChoiceType = .text, alreadyOrdered: Bool = false) { self.id = id self.value = value self.type = type + self.alreadyOrdered = alreadyOrdered } // MARK: Public @@ -26,8 +27,11 @@ public struct NewGameplayFindTheRightOrderChoice: Identifiable, Equatable { // MARK: Internal + static let zero = NewGameplayFindTheRightOrderChoice(value: "") + let value: String let type: ChoiceType + let alreadyOrdered: Bool } // MARK: - NewGameplayFindTheRightOrder @@ -74,4 +78,12 @@ public extension NewGameplayFindTheRightOrder { NewGameplayFindTheRightOrderChoice(value: "5th choice"), NewGameplayFindTheRightOrderChoice(value: "6th choice"), ] + + static let kDefaultImageChoicesWithZones: [NewGameplayFindTheRightOrderChoice] = [ + NewGameplayFindTheRightOrderChoice(value: "sequencing_dressing_up_1", type: .image, alreadyOrdered: true), + NewGameplayFindTheRightOrderChoice(value: "sequencing_dressing_up_2", type: .image, alreadyOrdered: false), + NewGameplayFindTheRightOrderChoice(value: "sequencing_dressing_up_3", type: .image, alreadyOrdered: false), + NewGameplayFindTheRightOrderChoice(value: "sequencing_dressing_up_4", type: .image, alreadyOrdered: true), + NewGameplayFindTheRightOrderChoice(value: "sequencing_dressing_up_5", type: .image, alreadyOrdered: false), + ] } From d1b3042a86f68296edcbfe3d027bb27d20afa8d8 Mon Sep 17 00:00:00 2001 From: Hugo Pezziardi Date: Fri, 29 Nov 2024 11:26:47 +0100 Subject: [PATCH 06/16] =?UTF-8?q?=F0=9F=9A=B8=20(GEK=20):=20Smooth=20snapT?= =?UTF-8?q?oCenter=20movement=20with=20SKAction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SKSpriteNodeExtension.swift | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Modules/GameEngineKit/Sources/OldSystem/Utils/SpriteKitExtensions/SKSpriteNodeExtension.swift b/Modules/GameEngineKit/Sources/OldSystem/Utils/SpriteKitExtensions/SKSpriteNodeExtension.swift index accfd8b08c..ca98eef7de 100644 --- a/Modules/GameEngineKit/Sources/OldSystem/Utils/SpriteKitExtensions/SKSpriteNodeExtension.swift +++ b/Modules/GameEngineKit/Sources/OldSystem/Utils/SpriteKitExtensions/SKSpriteNodeExtension.swift @@ -25,7 +25,20 @@ extension SKSpriteNode { let newY = max(dropZoneFrame.minY + size.height / 2, min(position.y, dropZoneFrame.maxY - size.height / 2)) - position = CGPoint(x: newX, y: newY) + let targetPosition = CGPoint(x: newX, y: newY) + + let moveToCenter = SKAction.move(to: targetPosition, duration: 0.2) + moveToCenter.timingMode = .easeInEaseOut + + run( + moveToCenter, + completion: { + self.position = targetPosition + self.zRotation = 0 + self.zPosition = 10 + self.removeAllActions() + } + ) } func fullyContains(bounds: CGRect) -> Bool { From 8695ed80b774b187add622325bc35d9e36e295ca Mon Sep 17 00:00:00 2001 From: Hugo Pezziardi Date: Fri, 29 Nov 2024 11:46:55 +0100 Subject: [PATCH 07/16] =?UTF-8?q?=E2=9C=A8=20(New=20GEK):=20Add=20DnDGridW?= =?UTF-8?q?ithCorrespondingZonesGameplayCoordinatorProtocol?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...espondingZonesGameplayCoordinatorProtocol.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/GridWithCorrespondingZones/Coordinators/DnDGridWithCorrespondingZonesGameplayCoordinatorProtocol.swift diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/GridWithCorrespondingZones/Coordinators/DnDGridWithCorrespondingZonesGameplayCoordinatorProtocol.swift b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/GridWithCorrespondingZones/Coordinators/DnDGridWithCorrespondingZonesGameplayCoordinatorProtocol.swift new file mode 100644 index 0000000000..15c2d93c41 --- /dev/null +++ b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/GridWithCorrespondingZones/Coordinators/DnDGridWithCorrespondingZonesGameplayCoordinatorProtocol.swift @@ -0,0 +1,14 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import Foundation + +// swiftlint:disable:next type_name +public protocol DnDGridWithCorrespondingZonesGameplayCoordinatorProtocol { + var uiChoices: CurrentValueSubject { get } + var uiDropZones: [DnDDropZoneNode] { get } + func setAlreadyOrderedNodes() + func onTouch(_ event: DnDTouchEvent, choice: DnDAnswerNode, destination: DnDDropZoneNode?) +} From f189a3147f1dd25762a2ebd701169bb88b6d16b1 Mon Sep 17 00:00:00 2001 From: Hugo Pezziardi Date: Fri, 29 Nov 2024 11:47:40 +0100 Subject: [PATCH 08/16] =?UTF-8?q?=E2=9C=A8=20(New=20GEK):=20Add=20DnDGridW?= =?UTF-8?q?ithCorrespondingZonesCoordinatorFindTheRightOrder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ngZonesCoordinator+FindTheRightOrder.swift | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/GridWithCorrespondingZones/Coordinators/DnDGridWithCorrespondingZonesCoordinator+FindTheRightOrder.swift diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/GridWithCorrespondingZones/Coordinators/DnDGridWithCorrespondingZonesCoordinator+FindTheRightOrder.swift b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/GridWithCorrespondingZones/Coordinators/DnDGridWithCorrespondingZonesCoordinator+FindTheRightOrder.swift new file mode 100644 index 0000000000..0c939e7c9c --- /dev/null +++ b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/GridWithCorrespondingZones/Coordinators/DnDGridWithCorrespondingZonesCoordinator+FindTheRightOrder.swift @@ -0,0 +1,187 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import SpriteKit +import SwiftUI +import UtilsKit + +// MARK: - DnDGridWithCorrespondingZonesCoordinatorFindTheRightOrder + +// swiftlint:disable:next type_name +public class DnDGridWithCorrespondingZonesCoordinatorFindTheRightOrder: DnDGridWithCorrespondingZonesGameplayCoordinatorProtocol { + // MARK: Lifecycle + + public init(gameplay: NewGameplayFindTheRightOrder) { + self.gameplay = gameplay + + self.uiChoices.value.choices = gameplay.orderedChoices.map { choice in + DnDAnswerNode(id: choice.id, value: choice.value, type: choice.type, size: self.uiChoices.value.choiceSize) + } + + self.uiDropZones = self.uiChoices.value.choices.map { node in + DnDDropZoneNode(node: node) + } + + self.currentOrderedChoices = Array(repeating: .zero, count: gameplay.orderedChoices.count) + self.alreadyValidatedChoices = Array(repeating: .zero, count: gameplay.orderedChoices.count) + } + + // MARK: Public + + public private(set) var uiDropZones: [DnDDropZoneNode] = [] + public private(set) var uiChoices = CurrentValueSubject(.zero) + + public func setAlreadyOrderedNodes() { + self.gameplay.orderedChoices.enumerated().forEach { index, choice in + if choice.alreadyOrdered { + self.updateChoiceState(for: choice, to: .correct(order: index)) + self.currentOrderedChoices[index] = choice + self.alreadyValidatedChoices[index] = choice + } + } + } + + public func onTouch(_ event: DnDTouchEvent, choice: DnDAnswerNode, destination: DnDDropZoneNode? = nil) { + switch event { + case .began: + self.updateChoiceState(for: self.gameplay.orderedChoices.first(where: { $0.id == choice.id })!, to: .selected) + case .ended: + guard let destination else { + self.updateChoiceState(for: self.gameplay.orderedChoices.first(where: { $0.id == choice.id })!, to: .idle) + return + } + + self.processUserDropOnDestination(choice: choice, destination: destination) + } + } + + // MARK: Private + + private let gameplay: NewGameplayFindTheRightOrder + + private var currentOrderedChoices: [NewGameplayFindTheRightOrderChoice] = [] + private var alreadyValidatedChoices: [NewGameplayFindTheRightOrderChoice] = [] + + private func processUserDropOnDestination(choice: DnDAnswerNode, destination: DnDDropZoneNode) { + guard let sourceChoice = self.gameplay.orderedChoices.first(where: { $0.id == choice.id }), + let destinationIndex = self.uiDropZones.firstIndex(where: { $0.id == destination.id }), + !self.choiceAlreadySelected(choice: sourceChoice) else { return } + + self.select(choice: sourceChoice, dropZoneIndex: destinationIndex) + if self.currentOrderedChoices.doesNotContain(.zero) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { + let results = self.gameplay.process(choices: self.currentOrderedChoices) + + results.enumerated().forEach { index, result in + if result.correctPosition { + self.updateChoiceState(for: result.choice, to: .correct(order: index)) + self.alreadyValidatedChoices[index] = result.choice + } else { + self.updateChoiceState(for: result.choice, to: .idle) + self.currentOrderedChoices[index] = .zero + } + } + } + } + } + + private func updateChoiceState(for choice: NewGameplayFindTheRightOrderChoice, to state: State) { + guard let index = self.gameplay.orderedChoices.firstIndex(where: { $0.id == choice.id }) else { return } + + self.updateUINodeState(node: self.uiChoices.value.choices[index], state: state) + } + + private func choiceAlreadySelected(choice: NewGameplayFindTheRightOrderChoice) -> Bool { + self.alreadyValidatedChoices.contains(where: { $0.id == choice.id }) + } + + private func select(choice: NewGameplayFindTheRightOrderChoice, dropZoneIndex: Int) { + let previousChoice = self.currentOrderedChoices[dropZoneIndex] + if let index = self.currentOrderedChoices.firstIndex(where: { $0 == choice }) { + if previousChoice == .zero { + self.currentOrderedChoices[index] = .zero + self.currentOrderedChoices[dropZoneIndex] = choice + self.updateChoiceState(for: choice, to: .ordered(order: dropZoneIndex)) + } else if self.isMovable(choice: previousChoice) { + self.currentOrderedChoices[index] = previousChoice + self.currentOrderedChoices[dropZoneIndex] = choice + self.updateChoiceState(for: previousChoice, to: .ordered(order: index)) + self.updateChoiceState(for: choice, to: .ordered(order: dropZoneIndex)) + } else { + self.currentOrderedChoices[index] = .zero + self.updateChoiceState(for: choice, to: .idle) + } + } else { + if previousChoice == .zero { + self.currentOrderedChoices[dropZoneIndex] = choice + self.updateChoiceState(for: choice, to: .ordered(order: dropZoneIndex)) + } else if self.isMovable(choice: previousChoice) { + self.currentOrderedChoices[dropZoneIndex] = choice + self.updateChoiceState(for: previousChoice, to: .idle) + self.updateChoiceState(for: choice, to: .ordered(order: dropZoneIndex)) + } else { + self.updateChoiceState(for: choice, to: .idle) + } + } + } + + private func isMovable(choice: NewGameplayFindTheRightOrderChoice) -> Bool { + self.uiChoices.value.choices.first(where: { choice.id == $0.id })!.isDraggable + } +} + +extension DnDGridWithCorrespondingZonesCoordinatorFindTheRightOrder { + enum State: Equatable { + case idle + case selected + case ordered(order: Int) + case correct(order: Int) + } + + private func updateUINodeState(node: DnDAnswerNode, state: State) { + switch state { + case .idle: + self.moveNodeBackToInitialPosition(node) + case .selected: + self.onDragAnimation(node) + case let .ordered(order): + node.snapToCenter(dropZone: self.uiDropZones[order]) + case let .correct(order): + node.snapToCenter(dropZone: self.uiDropZones[order]) + node.isDraggable = false + } + } + + // MARK: - Animations + + private func onDragAnimation(_ node: SKSpriteNode) { + let wiggleAnimation = SKAction.sequence([ + SKAction.rotate(byAngle: CGFloat(degreesToRadian(degrees: -4)), duration: 0.1), + SKAction.rotate(byAngle: 0.0, duration: 0.1), + SKAction.rotate(byAngle: CGFloat(degreesToRadian(degrees: 4)), duration: 0.1), + ]) + node.zPosition += 100 + node.run(SKAction.repeatForever(wiggleAnimation)) + } + + // MARK: Private + + private func onDropAction(_ node: SKSpriteNode) { + node.zRotation = 0 + node.removeAllActions() + } + + private func moveNodeBackToInitialPosition(_ node: DnDAnswerNode) { + let moveAnimation = SKAction.move(to: node.initialPosition ?? .zero, duration: 0.25) + node.run( + moveAnimation, + completion: { + node.position = node.initialPosition ?? .zero + node.zPosition = 10 + self.onDropAction(node) + } + ) + } +} From 65503ca0506ddeb22472cf010b0d68495021199f Mon Sep 17 00:00:00 2001 From: Hugo Pezziardi Date: Fri, 29 Nov 2024 11:48:01 +0100 Subject: [PATCH 09/16] =?UTF-8?q?=E2=9C=A8=20(New=20GEK):=20Add=20DnDGridW?= =?UTF-8?q?ithCorrespondingZones=20VM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...DGridWithCorrespondingZonesViewModel.swift | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/GridWithCorrespondingZones/DnDGridWithCorrespondingZonesViewModel.swift diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/GridWithCorrespondingZones/DnDGridWithCorrespondingZonesViewModel.swift b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/GridWithCorrespondingZones/DnDGridWithCorrespondingZonesViewModel.swift new file mode 100644 index 0000000000..b818afd97f --- /dev/null +++ b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/GridWithCorrespondingZones/DnDGridWithCorrespondingZonesViewModel.swift @@ -0,0 +1,42 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import Combine +import SwiftUI + +// MARK: - DnDGridWithCorrespondingZonesViewModel + +public class DnDGridWithCorrespondingZonesViewModel: ObservableObject { + // MARK: Lifecycle + + public init(coordinator: DnDGridWithCorrespondingZonesGameplayCoordinatorProtocol) { + self.choices = coordinator.uiChoices.value.choices + self.dropzones = coordinator.uiDropZones + self.coordinator = coordinator + self.coordinator.uiChoices + .receive(on: DispatchQueue.main) + .sink { [weak self] choices in + self?.choices = choices.choices + } + .store(in: &self.cancellables) + } + + // MARK: Internal + + @Published var choices: [DnDAnswerNode] = [] + @Published var dropzones: [DnDDropZoneNode] = [] + + func setAlreadyOrderedNodes() { + self.coordinator.setAlreadyOrderedNodes() + } + + func onTouch(_ event: DnDTouchEvent, choice: DnDAnswerNode, destination: DnDDropZoneNode? = nil) { + self.coordinator.onTouch(event, choice: choice, destination: destination) + } + + // MARK: Private + + private let coordinator: DnDGridWithCorrespondingZonesGameplayCoordinatorProtocol + private var cancellables: Set = [] +} From 0760797cf0f2a063f8c266b16c0d55d04195d45f Mon Sep 17 00:00:00 2001 From: Hugo Pezziardi Date: Fri, 29 Nov 2024 11:48:37 +0100 Subject: [PATCH 10/16] =?UTF-8?q?=E2=9C=A8=20(New=20GK):=20Add=20DnDGridWi?= =?UTF-8?q?thCorrespondingZones=20views?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...DGridWithCorrespondingZonesBaseScene.swift | 145 ++++++++++++++++++ .../DnDGridWithCorrespondingZonesView.swift | 41 +++++ 2 files changed, 186 insertions(+) create mode 100644 Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/GridWithCorrespondingZones/BaseScene/DnDGridWithCorrespondingZonesBaseScene.swift create mode 100644 Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/GridWithCorrespondingZones/DnDGridWithCorrespondingZonesView.swift diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/GridWithCorrespondingZones/BaseScene/DnDGridWithCorrespondingZonesBaseScene.swift b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/GridWithCorrespondingZones/BaseScene/DnDGridWithCorrespondingZonesBaseScene.swift new file mode 100644 index 0000000000..0505185f59 --- /dev/null +++ b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/GridWithCorrespondingZones/BaseScene/DnDGridWithCorrespondingZonesBaseScene.swift @@ -0,0 +1,145 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import ContentKit +import SpriteKit +import SwiftUI + +class DnDGridWithCorrespondingZonesBaseScene: SKScene { + // MARK: Lifecycle + + init(viewModel: DnDGridWithCorrespondingZonesViewModel) { + self.viewModel = viewModel + super.init(size: CGSize.zero) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Internal + + override func didMove(to _: SKView) { + self.reset() + } + + // MARK: - Touch Interaction + + override func touchesBegan(_ touches: Set, with _: UIEvent?) { + guard let touch = touches.first else { + return + } + let location = touch.location(in: self) + if let node = atPoint(location) as? DnDAnswerNode { + for choice in self.viewModel.choices where node.id == choice.id && node.isDraggable { + selectedNodes[touch] = node + self.viewModel.onTouch(.began, choice: node) + } + } + } + + override func touchesMoved(_ touches: Set, with _: UIEvent?) { + guard let touch = touches.first else { + return + } + let location = touch.location(in: self) + if let node = selectedNodes[touch] { + node.position = location + } + } + + override func touchesEnded(_ touches: Set, with _: UIEvent?) { + guard let touch = touches.first, + let playedNode = self.selectedNodes[touch] + else { + return + } + + self.viewModel.onTouch(.ended, choice: playedNode, destination: self.dropZonesNodes.first(where: { + $0.frame.contains(touch.location(in: self)) + })) + } + + func reset() { + backgroundColor = .clear + removeAllChildren() + removeAllActions() + + self.setFirstAnswerPosition() + self.layoutChoices() + self.layoutDropzones() + self.viewModel.setAlreadyOrderedNodes() + } + + func exerciseCompletedBehavior() { + for node in self.answerNodes { + node.isDraggable = false + } + } + + func layoutChoices() { + for (index, choice) in self.viewModel.choices.enumerated() { + choice.initialPosition = self.setInitialPosition(index) + choice.position = choice.initialPosition! + + let shadowChoice = DnDShadowNode(node: choice) + + self.bindNodesToSafeArea([choice, shadowChoice]) + self.answerNodes.append(choice) + + addChild(shadowChoice) + addChild(choice) + } + } + + func layoutDropzones() { + for (index, dropzone) in self.viewModel.dropzones.enumerated() { + dropzone.position = self.setInitialDropZonePosition(index) + + self.bindNodesToSafeArea([dropzone]) + self.dropZonesNodes.append(dropzone) + + addChild(dropzone) + } + } + + func bindNodesToSafeArea(_ nodes: [SKSpriteNode], limit: CGFloat = 80) { + let xRange = SKRange(lowerLimit: 0, upperLimit: size.width - limit) + let yRange = SKRange(lowerLimit: 0, upperLimit: size.height - limit) + for node in nodes { + node.constraints = [SKConstraint.positionX(xRange, y: yRange)] + } + } + + func normalizeNodesSize(_ nodes: [SKSpriteNode]) { + for node in nodes { + node.scaleForMax(sizeOf: self.maxWidthAndHeight) + } + } + + func setFirstAnswerPosition() { + self.spacer = size.width / CGFloat(self.viewModel.choices.count + 1) + self.maxWidthAndHeight = 200 - 5 * CGFloat(self.viewModel.choices.count) + } + + func setInitialPosition(_ index: Int) -> CGPoint { + CGPoint(x: self.spacer * CGFloat(index + 1), y: size.height - 120) + } + + func setInitialDropZonePosition(_ index: Int) -> CGPoint { + CGPoint(x: self.spacer * CGFloat(index + 1), y: 120) + } + + // MARK: Private + + private var spacer: CGFloat = .zero + private var maxWidthAndHeight: CGFloat = .zero + private var viewModel: DnDGridWithCorrespondingZonesViewModel + private var expectedItemsNodes: [String: [SKSpriteNode]] = [:] + private var selectedNodes: [UITouch: DnDAnswerNode] = [:] + private var dropZonesNodes: [DnDDropZoneNode] = [] + private var answerNodes: [DnDAnswerNode] = [] + private var playedNode: DnDAnswerNode? +} diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/GridWithCorrespondingZones/DnDGridWithCorrespondingZonesView.swift b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/GridWithCorrespondingZones/DnDGridWithCorrespondingZonesView.swift new file mode 100644 index 0000000000..349e2b9ffb --- /dev/null +++ b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/GridWithCorrespondingZones/DnDGridWithCorrespondingZonesView.swift @@ -0,0 +1,41 @@ +// Leka - iOS Monorepo +// Copyright APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import SpriteKit +import SwiftUI + +// MARK: - DnDGridWithCorrespondingZonesView + +public struct DnDGridWithCorrespondingZonesView: View { + // MARK: Lifecycle + + public init(viewModel: DnDGridWithCorrespondingZonesViewModel) { + self._viewModel = StateObject(wrappedValue: viewModel) + } + + // 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 = DnDGridWithCorrespondingZonesBaseScene(viewModel: self.viewModel) + } + } + } + + // MARK: Private + + @StateObject private var viewModel: DnDGridWithCorrespondingZonesViewModel + @State private var scene: SKScene = .init() + + private func makeScene(size: CGSize) -> SKScene { + guard let finalScene = scene as? DnDGridWithCorrespondingZonesBaseScene else { + return SKScene() + } + finalScene.size = size + return finalScene + } +} From 5f023a5bac2c2c00799bbf2bc44724717ca8d0fe Mon Sep 17 00:00:00 2001 From: Hugo Pezziardi Date: Fri, 29 Nov 2024 11:50:01 +0100 Subject: [PATCH 11/16] =?UTF-8?q?=E2=9C=A8=20(GEKExample):=20Add=20Sequenc?= =?UTF-8?q?ing=20exercise?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/App/ContentView.swift | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Modules/GameEngineKit/Examples/GameEngineKitExample/Sources/App/ContentView.swift b/Modules/GameEngineKit/Examples/GameEngineKitExample/Sources/App/ContentView.swift index a79eb34c55..269063f075 100644 --- a/Modules/GameEngineKit/Examples/GameEngineKitExample/Sources/App/ContentView.swift +++ b/Modules/GameEngineKit/Examples/GameEngineKitExample/Sources/App/ContentView.swift @@ -180,6 +180,19 @@ struct ContentView: View { .buttonStyle(.borderedProminent) .frame(maxWidth: .infinity) + NavigationLink("Drag & Drop With Corresponding Zones In Right Order", destination: { + let gameplay = NewGameplayFindTheRightOrder(choices: NewGameplayFindTheRightOrder.kDefaultChoicesWithZones) + let coordinator = DnDGridWithCorrespondingZonesCoordinatorFindTheRightOrder(gameplay: gameplay) + let viewModel = DnDGridWithCorrespondingZonesViewModel(coordinator: coordinator) + + return DnDGridWithCorrespondingZonesView(viewModel: viewModel) + .navigationTitle("Drag & Drop With Corresponding Zones In Right Order") + .navigationBarTitleDisplayMode(.large) + }) + .tint(.red) + .buttonStyle(.borderedProminent) + .frame(maxWidth: .infinity) + Spacer() } } From 7dcc1366600d36e0cb6bc312ac810b5a0bebd221 Mon Sep 17 00:00:00 2001 From: Hugo Pezziardi Date: Mon, 2 Dec 2024 11:53:33 +0100 Subject: [PATCH 12/16] =?UTF-8?q?=F0=9F=9A=9A=20(New=20GEK):=20Renaming=20?= =?UTF-8?q?into=20DnDOneToOne?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/App/ContentView.swift | 10 +++++----- .../BaseScene/DnDOneToOneBaseScene.swift} | 6 +++--- .../DnDOneToOneCoordinator+FindTheRightOrder.swift} | 10 +++++----- .../DnDOneToOneGameplayCoordinatorProtocol.swift} | 2 +- .../DnDOneToOneView.swift} | 12 ++++++------ .../DnDOneToOneViewModel.swift} | 8 ++++---- 6 files changed, 24 insertions(+), 24 deletions(-) rename Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/{GridWithCorrespondingZones/BaseScene/DnDGridWithCorrespondingZonesBaseScene.swift => OneToOne/BaseScene/DnDOneToOneBaseScene.swift} (95%) rename Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/{GridWithCorrespondingZones/Coordinators/DnDGridWithCorrespondingZonesCoordinator+FindTheRightOrder.swift => OneToOne/Coordinators/DnDOneToOneCoordinator+FindTheRightOrder.swift} (94%) rename Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/{GridWithCorrespondingZones/Coordinators/DnDGridWithCorrespondingZonesGameplayCoordinatorProtocol.swift => OneToOne/Coordinators/DnDOneToOneGameplayCoordinatorProtocol.swift} (84%) rename Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/{GridWithCorrespondingZones/DnDGridWithCorrespondingZonesView.swift => OneToOne/DnDOneToOneView.swift} (64%) rename Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/{GridWithCorrespondingZones/DnDGridWithCorrespondingZonesViewModel.swift => OneToOne/DnDOneToOneViewModel.swift} (77%) diff --git a/Modules/GameEngineKit/Examples/GameEngineKitExample/Sources/App/ContentView.swift b/Modules/GameEngineKit/Examples/GameEngineKitExample/Sources/App/ContentView.swift index 269063f075..d343b0db52 100644 --- a/Modules/GameEngineKit/Examples/GameEngineKitExample/Sources/App/ContentView.swift +++ b/Modules/GameEngineKit/Examples/GameEngineKitExample/Sources/App/ContentView.swift @@ -180,13 +180,13 @@ struct ContentView: View { .buttonStyle(.borderedProminent) .frame(maxWidth: .infinity) - NavigationLink("Drag & Drop With Corresponding Zones In Right Order", destination: { + NavigationLink("Drag & Drop One To One In Right Order", destination: { let gameplay = NewGameplayFindTheRightOrder(choices: NewGameplayFindTheRightOrder.kDefaultChoicesWithZones) - let coordinator = DnDGridWithCorrespondingZonesCoordinatorFindTheRightOrder(gameplay: gameplay) - let viewModel = DnDGridWithCorrespondingZonesViewModel(coordinator: coordinator) + let coordinator = DnDOneToOneCoordinatorFindTheRightOrder(gameplay: gameplay) + let viewModel = DnDOneToOneViewModel(coordinator: coordinator) - return DnDGridWithCorrespondingZonesView(viewModel: viewModel) - .navigationTitle("Drag & Drop With Corresponding Zones In Right Order") + return DnDOneToOneView(viewModel: viewModel) + .navigationTitle("Drag & Drop One To One In Right Order") .navigationBarTitleDisplayMode(.large) }) .tint(.red) diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/GridWithCorrespondingZones/BaseScene/DnDGridWithCorrespondingZonesBaseScene.swift b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/OneToOne/BaseScene/DnDOneToOneBaseScene.swift similarity index 95% rename from Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/GridWithCorrespondingZones/BaseScene/DnDGridWithCorrespondingZonesBaseScene.swift rename to Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/OneToOne/BaseScene/DnDOneToOneBaseScene.swift index 0505185f59..f457584f94 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/GridWithCorrespondingZones/BaseScene/DnDGridWithCorrespondingZonesBaseScene.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/OneToOne/BaseScene/DnDOneToOneBaseScene.swift @@ -6,10 +6,10 @@ import ContentKit import SpriteKit import SwiftUI -class DnDGridWithCorrespondingZonesBaseScene: SKScene { +class DnDOneToOneBaseScene: SKScene { // MARK: Lifecycle - init(viewModel: DnDGridWithCorrespondingZonesViewModel) { + init(viewModel: DnDOneToOneViewModel) { self.viewModel = viewModel super.init(size: CGSize.zero) } @@ -136,7 +136,7 @@ class DnDGridWithCorrespondingZonesBaseScene: SKScene { private var spacer: CGFloat = .zero private var maxWidthAndHeight: CGFloat = .zero - private var viewModel: DnDGridWithCorrespondingZonesViewModel + private var viewModel: DnDOneToOneViewModel private var expectedItemsNodes: [String: [SKSpriteNode]] = [:] private var selectedNodes: [UITouch: DnDAnswerNode] = [:] private var dropZonesNodes: [DnDDropZoneNode] = [] diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/GridWithCorrespondingZones/Coordinators/DnDGridWithCorrespondingZonesCoordinator+FindTheRightOrder.swift b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/OneToOne/Coordinators/DnDOneToOneCoordinator+FindTheRightOrder.swift similarity index 94% rename from Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/GridWithCorrespondingZones/Coordinators/DnDGridWithCorrespondingZonesCoordinator+FindTheRightOrder.swift rename to Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/OneToOne/Coordinators/DnDOneToOneCoordinator+FindTheRightOrder.swift index 0c939e7c9c..9d354d438c 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/GridWithCorrespondingZones/Coordinators/DnDGridWithCorrespondingZonesCoordinator+FindTheRightOrder.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/OneToOne/Coordinators/DnDOneToOneCoordinator+FindTheRightOrder.swift @@ -7,10 +7,10 @@ import SpriteKit import SwiftUI import UtilsKit -// MARK: - DnDGridWithCorrespondingZonesCoordinatorFindTheRightOrder +// MARK: - DnDOneToOneCoordinatorFindTheRightOrder // swiftlint:disable:next type_name -public class DnDGridWithCorrespondingZonesCoordinatorFindTheRightOrder: DnDGridWithCorrespondingZonesGameplayCoordinatorProtocol { +public class DnDOneToOneCoordinatorFindTheRightOrder: DnDOneToOneGameplayCoordinatorProtocol { // MARK: Lifecycle public init(gameplay: NewGameplayFindTheRightOrder) { @@ -69,7 +69,7 @@ public class DnDGridWithCorrespondingZonesCoordinatorFindTheRightOrder: DnDGridW let destinationIndex = self.uiDropZones.firstIndex(where: { $0.id == destination.id }), !self.choiceAlreadySelected(choice: sourceChoice) else { return } - self.select(choice: sourceChoice, dropZoneIndex: destinationIndex) + self.order(choice: sourceChoice, dropZoneIndex: destinationIndex) if self.currentOrderedChoices.doesNotContain(.zero) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { let results = self.gameplay.process(choices: self.currentOrderedChoices) @@ -97,7 +97,7 @@ public class DnDGridWithCorrespondingZonesCoordinatorFindTheRightOrder: DnDGridW self.alreadyValidatedChoices.contains(where: { $0.id == choice.id }) } - private func select(choice: NewGameplayFindTheRightOrderChoice, dropZoneIndex: Int) { + private func order(choice: NewGameplayFindTheRightOrderChoice, dropZoneIndex: Int) { let previousChoice = self.currentOrderedChoices[dropZoneIndex] if let index = self.currentOrderedChoices.firstIndex(where: { $0 == choice }) { if previousChoice == .zero { @@ -132,7 +132,7 @@ public class DnDGridWithCorrespondingZonesCoordinatorFindTheRightOrder: DnDGridW } } -extension DnDGridWithCorrespondingZonesCoordinatorFindTheRightOrder { +extension DnDOneToOneCoordinatorFindTheRightOrder { enum State: Equatable { case idle case selected diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/GridWithCorrespondingZones/Coordinators/DnDGridWithCorrespondingZonesGameplayCoordinatorProtocol.swift b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/OneToOne/Coordinators/DnDOneToOneGameplayCoordinatorProtocol.swift similarity index 84% rename from Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/GridWithCorrespondingZones/Coordinators/DnDGridWithCorrespondingZonesGameplayCoordinatorProtocol.swift rename to Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/OneToOne/Coordinators/DnDOneToOneGameplayCoordinatorProtocol.swift index 15c2d93c41..d0129eae1c 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/GridWithCorrespondingZones/Coordinators/DnDGridWithCorrespondingZonesGameplayCoordinatorProtocol.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/OneToOne/Coordinators/DnDOneToOneGameplayCoordinatorProtocol.swift @@ -6,7 +6,7 @@ import Combine import Foundation // swiftlint:disable:next type_name -public protocol DnDGridWithCorrespondingZonesGameplayCoordinatorProtocol { +public protocol DnDOneToOneGameplayCoordinatorProtocol { var uiChoices: CurrentValueSubject { get } var uiDropZones: [DnDDropZoneNode] { get } func setAlreadyOrderedNodes() diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/GridWithCorrespondingZones/DnDGridWithCorrespondingZonesView.swift b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/OneToOne/DnDOneToOneView.swift similarity index 64% rename from Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/GridWithCorrespondingZones/DnDGridWithCorrespondingZonesView.swift rename to Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/OneToOne/DnDOneToOneView.swift index 349e2b9ffb..2cd45cdcd1 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/GridWithCorrespondingZones/DnDGridWithCorrespondingZonesView.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/OneToOne/DnDOneToOneView.swift @@ -5,12 +5,12 @@ import SpriteKit import SwiftUI -// MARK: - DnDGridWithCorrespondingZonesView +// MARK: - DnDOneToOneView -public struct DnDGridWithCorrespondingZonesView: View { +public struct DnDOneToOneView: View { // MARK: Lifecycle - public init(viewModel: DnDGridWithCorrespondingZonesViewModel) { + public init(viewModel: DnDOneToOneViewModel) { self._viewModel = StateObject(wrappedValue: viewModel) } @@ -21,18 +21,18 @@ public struct DnDGridWithCorrespondingZonesView: View { SpriteView(scene: self.makeScene(size: proxy.size), options: [.allowsTransparency]) .frame(width: proxy.size.width, height: proxy.size.height) .onAppear { - self.scene = DnDGridWithCorrespondingZonesBaseScene(viewModel: self.viewModel) + self.scene = DnDOneToOneBaseScene(viewModel: self.viewModel) } } } // MARK: Private - @StateObject private var viewModel: DnDGridWithCorrespondingZonesViewModel + @StateObject private var viewModel: DnDOneToOneViewModel @State private var scene: SKScene = .init() private func makeScene(size: CGSize) -> SKScene { - guard let finalScene = scene as? DnDGridWithCorrespondingZonesBaseScene else { + guard let finalScene = scene as? DnDOneToOneBaseScene else { return SKScene() } finalScene.size = size diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/GridWithCorrespondingZones/DnDGridWithCorrespondingZonesViewModel.swift b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/OneToOne/DnDOneToOneViewModel.swift similarity index 77% rename from Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/GridWithCorrespondingZones/DnDGridWithCorrespondingZonesViewModel.swift rename to Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/OneToOne/DnDOneToOneViewModel.swift index b818afd97f..52324dffa7 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/GridWithCorrespondingZones/DnDGridWithCorrespondingZonesViewModel.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/OneToOne/DnDOneToOneViewModel.swift @@ -5,12 +5,12 @@ import Combine import SwiftUI -// MARK: - DnDGridWithCorrespondingZonesViewModel +// MARK: - DnDOneToOneViewModel -public class DnDGridWithCorrespondingZonesViewModel: ObservableObject { +public class DnDOneToOneViewModel: ObservableObject { // MARK: Lifecycle - public init(coordinator: DnDGridWithCorrespondingZonesGameplayCoordinatorProtocol) { + public init(coordinator: DnDOneToOneGameplayCoordinatorProtocol) { self.choices = coordinator.uiChoices.value.choices self.dropzones = coordinator.uiDropZones self.coordinator = coordinator @@ -37,6 +37,6 @@ public class DnDGridWithCorrespondingZonesViewModel: ObservableObject { // MARK: Private - private let coordinator: DnDGridWithCorrespondingZonesGameplayCoordinatorProtocol + private let coordinator: DnDOneToOneGameplayCoordinatorProtocol private var cancellables: Set = [] } From 5393c52d9e8097bf00e03a95a622a97b1dbef8b2 Mon Sep 17 00:00:00 2001 From: Hugo Pezziardi Date: Tue, 3 Dec 2024 13:23:33 +0100 Subject: [PATCH 13/16] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(New=20GEK):=20Allow?= =?UTF-8?q?=20shuffling=20in=20sequencing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GameEngineKitExample/Sources/App/ContentView.swift | 2 +- .../DnDOneToOneCoordinator+FindTheRightOrder.swift | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Modules/GameEngineKit/Examples/GameEngineKitExample/Sources/App/ContentView.swift b/Modules/GameEngineKit/Examples/GameEngineKitExample/Sources/App/ContentView.swift index d343b0db52..85e53e5fc5 100644 --- a/Modules/GameEngineKit/Examples/GameEngineKitExample/Sources/App/ContentView.swift +++ b/Modules/GameEngineKit/Examples/GameEngineKitExample/Sources/App/ContentView.swift @@ -181,7 +181,7 @@ struct ContentView: View { .frame(maxWidth: .infinity) NavigationLink("Drag & Drop One To One In Right Order", destination: { - let gameplay = NewGameplayFindTheRightOrder(choices: NewGameplayFindTheRightOrder.kDefaultChoicesWithZones) + let gameplay = NewGameplayFindTheRightOrder(choices: NewGameplayFindTheRightOrder.kDefaultImageChoicesWithZones) let coordinator = DnDOneToOneCoordinatorFindTheRightOrder(gameplay: gameplay) let viewModel = DnDOneToOneViewModel(coordinator: coordinator) diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/OneToOne/Coordinators/DnDOneToOneCoordinator+FindTheRightOrder.swift b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/OneToOne/Coordinators/DnDOneToOneCoordinator+FindTheRightOrder.swift index 9d354d438c..a8fe0a1d21 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/OneToOne/Coordinators/DnDOneToOneCoordinator+FindTheRightOrder.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/OneToOne/Coordinators/DnDOneToOneCoordinator+FindTheRightOrder.swift @@ -9,7 +9,6 @@ import UtilsKit // MARK: - DnDOneToOneCoordinatorFindTheRightOrder -// swiftlint:disable:next type_name public class DnDOneToOneCoordinatorFindTheRightOrder: DnDOneToOneGameplayCoordinatorProtocol { // MARK: Lifecycle @@ -24,6 +23,8 @@ public class DnDOneToOneCoordinatorFindTheRightOrder: DnDOneToOneGameplayCoordin DnDDropZoneNode(node: node) } + self.uiChoices.value.choices.shuffle() + self.currentOrderedChoices = Array(repeating: .zero, count: gameplay.orderedChoices.count) self.alreadyValidatedChoices = Array(repeating: .zero, count: gameplay.orderedChoices.count) } @@ -34,8 +35,9 @@ public class DnDOneToOneCoordinatorFindTheRightOrder: DnDOneToOneGameplayCoordin public private(set) var uiChoices = CurrentValueSubject(.zero) public func setAlreadyOrderedNodes() { - self.gameplay.orderedChoices.enumerated().forEach { index, choice in + self.gameplay.orderedChoices.forEach { choice in if choice.alreadyOrdered { + guard let index = self.uiDropZones.firstIndex(where: { $0.id == choice.id }) else { return } self.updateChoiceState(for: choice, to: .correct(order: index)) self.currentOrderedChoices[index] = choice self.alreadyValidatedChoices[index] = choice @@ -88,7 +90,7 @@ public class DnDOneToOneCoordinatorFindTheRightOrder: DnDOneToOneGameplayCoordin } private func updateChoiceState(for choice: NewGameplayFindTheRightOrderChoice, to state: State) { - guard let index = self.gameplay.orderedChoices.firstIndex(where: { $0.id == choice.id }) else { return } + guard let index = self.uiChoices.value.choices.firstIndex(where: { $0.id == choice.id }) else { return } self.updateUINodeState(node: self.uiChoices.value.choices[index], state: state) } From 1721e1e66ab8d5da644ac25786968f49cdf74f9b Mon Sep 17 00:00:00 2001 From: Hugo Pezziardi Date: Tue, 3 Dec 2024 15:59:59 +0100 Subject: [PATCH 14/16] =?UTF-8?q?=F0=9F=8E=A8=20(New=20GEK):=20Refactor=20?= =?UTF-8?q?DnDOneToOneCoordinatorFindTheRightOrder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BaseScene/DnDOneToOneBaseScene.swift | 35 +++---------------- 1 file changed, 5 insertions(+), 30 deletions(-) diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/OneToOne/BaseScene/DnDOneToOneBaseScene.swift b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/OneToOne/BaseScene/DnDOneToOneBaseScene.swift index f457584f94..29b866735d 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/OneToOne/BaseScene/DnDOneToOneBaseScene.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/OneToOne/BaseScene/DnDOneToOneBaseScene.swift @@ -67,7 +67,8 @@ class DnDOneToOneBaseScene: SKScene { removeAllChildren() removeAllActions() - self.setFirstAnswerPosition() + self.spacer = size.width / CGFloat(self.viewModel.choices.count + 1) + self.layoutChoices() self.layoutDropzones() self.viewModel.setAlreadyOrderedNodes() @@ -83,12 +84,9 @@ class DnDOneToOneBaseScene: SKScene { for (index, choice) in self.viewModel.choices.enumerated() { choice.initialPosition = self.setInitialPosition(index) choice.position = choice.initialPosition! - - let shadowChoice = DnDShadowNode(node: choice) - - self.bindNodesToSafeArea([choice, shadowChoice]) self.answerNodes.append(choice) + let shadowChoice = DnDShadowNode(node: choice) addChild(shadowChoice) addChild(choice) } @@ -97,45 +95,22 @@ class DnDOneToOneBaseScene: SKScene { func layoutDropzones() { for (index, dropzone) in self.viewModel.dropzones.enumerated() { dropzone.position = self.setInitialDropZonePosition(index) - - self.bindNodesToSafeArea([dropzone]) self.dropZonesNodes.append(dropzone) - addChild(dropzone) } } - func bindNodesToSafeArea(_ nodes: [SKSpriteNode], limit: CGFloat = 80) { - let xRange = SKRange(lowerLimit: 0, upperLimit: size.width - limit) - let yRange = SKRange(lowerLimit: 0, upperLimit: size.height - limit) - for node in nodes { - node.constraints = [SKConstraint.positionX(xRange, y: yRange)] - } - } - - func normalizeNodesSize(_ nodes: [SKSpriteNode]) { - for node in nodes { - node.scaleForMax(sizeOf: self.maxWidthAndHeight) - } - } - - func setFirstAnswerPosition() { - self.spacer = size.width / CGFloat(self.viewModel.choices.count + 1) - self.maxWidthAndHeight = 200 - 5 * CGFloat(self.viewModel.choices.count) - } - func setInitialPosition(_ index: Int) -> CGPoint { - CGPoint(x: self.spacer * CGFloat(index + 1), y: size.height - 120) + CGPoint(x: self.spacer * CGFloat(index + 1), y: size.height - self.viewModel.choices[0].size.height * 0.8) } func setInitialDropZonePosition(_ index: Int) -> CGPoint { - CGPoint(x: self.spacer * CGFloat(index + 1), y: 120) + CGPoint(x: self.spacer * CGFloat(index + 1), y: self.viewModel.choices[0].size.height * 0.8) } // MARK: Private private var spacer: CGFloat = .zero - private var maxWidthAndHeight: CGFloat = .zero private var viewModel: DnDOneToOneViewModel private var expectedItemsNodes: [String: [SKSpriteNode]] = [:] private var selectedNodes: [UITouch: DnDAnswerNode] = [:] From 7b0e0b43ac8b15f49e88e9b2be7fc270ecaca83c Mon Sep 17 00:00:00 2001 From: Hugo Pezziardi Date: Tue, 3 Dec 2024 16:03:29 +0100 Subject: [PATCH 15/16] =?UTF-8?q?=F0=9F=90=9B=20(New=20GEK):=20Fix=20choic?= =?UTF-8?q?e=20size=20variable=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...DnDGridCoordinator+AssociateCategories.swift | 2 +- ...thZonesCoordinator+AssociateCategories.swift | 2 +- ...DOneToOneCoordinator+FindTheRightOrder.swift | 2 +- .../Views/DnD/SKComponents/DnDAnswerNode.swift | 17 +++++++++-------- 4 files changed, 12 insertions(+), 11 deletions(-) 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 c899e9a644..fdbbe9cd3f 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 @@ -16,7 +16,7 @@ public class DnDGridCoordinatorAssociateCategories: DnDGridGameplayCoordinatorPr 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) + DnDAnswerNode(id: choice.id, value: choice.value, type: choice.type, size: self.uiChoices.value.choiceSize(for: gameplay.choices.count)) } } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/GridWithZones/Coordinators/DnDGridWithZonesCoordinator+AssociateCategories.swift b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/GridWithZones/Coordinators/DnDGridWithZonesCoordinator+AssociateCategories.swift index 70de2fa2e4..c516ce13b1 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/GridWithZones/Coordinators/DnDGridWithZonesCoordinator+AssociateCategories.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/GridWithZones/Coordinators/DnDGridWithZonesCoordinator+AssociateCategories.swift @@ -23,7 +23,7 @@ public class DnDGridWithZonesCoordinatorAssociateCategories: DnDGridWithZonesGam } self.uiChoices.value.choices = gameplay.choices[2...].map { choice in - DnDAnswerNode(id: choice.id, value: choice.value, type: choice.type, size: self.uiChoices.value.choiceSize) + DnDAnswerNode(id: choice.id, value: choice.value, type: choice.type, size: self.uiChoices.value.choiceSize(for: gameplay.choices.count)) } } diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/OneToOne/Coordinators/DnDOneToOneCoordinator+FindTheRightOrder.swift b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/OneToOne/Coordinators/DnDOneToOneCoordinator+FindTheRightOrder.swift index a8fe0a1d21..812cd0624a 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/OneToOne/Coordinators/DnDOneToOneCoordinator+FindTheRightOrder.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/OneToOne/Coordinators/DnDOneToOneCoordinator+FindTheRightOrder.swift @@ -16,7 +16,7 @@ public class DnDOneToOneCoordinatorFindTheRightOrder: DnDOneToOneGameplayCoordin self.gameplay = gameplay self.uiChoices.value.choices = gameplay.orderedChoices.map { choice in - DnDAnswerNode(id: choice.id, value: choice.value, type: choice.type, size: self.uiChoices.value.choiceSize) + DnDAnswerNode(id: choice.id, value: choice.value, type: choice.type, size: self.uiChoices.value.choiceSize(for: gameplay.orderedChoices.count)) } self.uiDropZones = self.uiChoices.value.choices.map { node in diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/SKComponents/DnDAnswerNode.swift b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/SKComponents/DnDAnswerNode.swift index 2a4068fe70..b43bb85e99 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/SKComponents/DnDAnswerNode.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/SKComponents/DnDAnswerNode.swift @@ -98,8 +98,8 @@ public struct DnDUIChoices { var choices: [DnDAnswerNode] - var choiceSize: CGSize { - DnDGridSize(self.choices.count).choiceSize + func choiceSize(for choiceNumber: Int) -> CGSize { + DnDGridSize(choiceNumber).choiceSize } // MARK: Private @@ -142,14 +142,15 @@ public struct DnDUIChoices { switch self { case .one, .two: + CGSize(width: 220, height: 220) + case .three, + .four: CGSize(width: 200, height: 200) - case .three: - CGSize(width: 180, height: 180) - case .four, - .five, - .six, + case .five: + CGSize(width: 160, height: 160) + case .six, .none: - CGSize(width: 140, height: 140) + CGSize(width: 150, height: 150) } } } From 2a56bfb60fed97b0cda6602ac1fbd818ce4604a7 Mon Sep 17 00:00:00 2001 From: Hugo Pezziardi Date: Wed, 4 Dec 2024 15:06:02 +0100 Subject: [PATCH 16/16] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(New=20GEK):=20Impro?= =?UTF-8?q?ve=20Nodes=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DnD/SKComponents/DnDAnswerNode.swift | 130 ++++++++++-------- .../DnD/SKComponents/DnDDropZoneNode.swift | 129 ++++++++++------- 2 files changed, 151 insertions(+), 108 deletions(-) diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/SKComponents/DnDAnswerNode.swift b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/SKComponents/DnDAnswerNode.swift index b43bb85e99..6f961c292a 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/SKComponents/DnDAnswerNode.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/SKComponents/DnDAnswerNode.swift @@ -13,65 +13,16 @@ public class DnDAnswerNode: SKSpriteNode { init(id: String, value: String, type: ChoiceType, size: CGSize) { self.id = id self.type = type - switch type { + let texture: SKTexture = switch type { + case .text: + Self.createTextTexture(value: value, size: size) case .image: - guard let path = Bundle.path(forImage: value), let image = UIImage(named: path) else { - fatalError("Image not found") - } - - let renderer = UIGraphicsImageRenderer(size: size) - let finalImage = renderer.image { _ in - let rect = CGRect(origin: .zero, size: size) - let path = UIBezierPath(roundedRect: rect, cornerRadius: 10 / 57 * size.width) - path.addClip() - image.draw(in: rect) - } - - let texture = SKTexture(image: finalImage) - super.init(texture: texture, color: .clear, size: texture.size()) - + Self.createImageTexture(value: value, size: size) case .sfsymbol: - guard let image = UIImage(systemName: value, withConfiguration: UIImage.SymbolConfiguration(pointSize: size.height * 3)) else { - fatalError("SFSymbol not found") - } - - super.init(texture: SKTexture(image: image), color: .clear, size: CGSize(width: size.width * 0.8, height: size.height * 0.8)) - - case .text: - let rectSize = CGSize(width: size.width, height: size.height) - let renderer = UIGraphicsImageRenderer(size: rectSize) - let finalImage = renderer.image { _ in - let rect = CGRect(origin: .zero, size: rectSize) - - let path = UIBezierPath(roundedRect: rect, cornerRadius: 10 / 57 * size.width) - path.addClip() - - UIColor.white.setFill() - path.fill() - - UIColor.gray.setStroke() - let strokeWidth: CGFloat = 2 - let borderRect = rect.insetBy(dx: strokeWidth / 2, dy: strokeWidth / 2) - let borderPath = UIBezierPath(roundedRect: borderRect, cornerRadius: 10 / 57 * size.width) - borderPath.stroke() - - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.alignment = .center - - let attributes: [NSAttributedString.Key: Any] = [ - .font: UIFont(name: "AvenirNext-Bold", size: 20) ?? UIFont.systemFont(ofSize: 20), - .foregroundColor: UIColor.black, - .paragraphStyle: paragraphStyle, - ] - - let textRect = rect.insetBy(dx: 10, dy: (rect.height - 20) / 2) - (value as NSString).draw(in: textRect, withAttributes: attributes) - } - - let texture = SKTexture(image: finalImage) - super.init(texture: texture, color: .clear, size: texture.size()) + Self.createSFSymbolTexture(value: value, size: size) } + super.init(texture: texture, color: .clear, size: texture.size()) self.name = value self.zPosition = 10 } @@ -87,6 +38,75 @@ public class DnDAnswerNode: SKSpriteNode { let type: ChoiceType var initialPosition: CGPoint? var isDraggable = true + + // MARK: Private + + private static let cornerRadiusFactor: CGFloat = 10 / 57 + private static let sizeFactorSFSymbol: CGFloat = 0.6 +} + +extension DnDAnswerNode { + private static func createImageTexture(value: String, size: CGSize) -> SKTexture { + guard let path = Bundle.path(forImage: value), + let image = UIImage(named: path) + else { + fatalError("Image not found") + } + + let renderer = UIGraphicsImageRenderer(size: size) + let finalImage = renderer.image { _ in + let rect = CGRect(origin: .zero, size: size) + let path = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadiusFactor * size.width) + path.addClip() + image.draw(in: rect) + } + + return SKTexture(image: finalImage) + } + + private static func createSFSymbolTexture(value: String, size: CGSize) -> SKTexture { + guard let image = UIImage(systemName: value, + withConfiguration: UIImage.SymbolConfiguration(pointSize: size.height * sizeFactorSFSymbol)) + else { + fatalError("SFSymbol not found") + } + + return SKTexture(image: image) + } + + private static func createTextTexture(value: String, size: CGSize) -> SKTexture { + let rectSize = CGSize(width: size.width, height: size.height) + let renderer = UIGraphicsImageRenderer(size: rectSize) + let finalImage = renderer.image { _ in + let rect = CGRect(origin: .zero, size: rectSize) + + let path = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadiusFactor * size.width) + path.addClip() + + UIColor.white.setFill() + path.fill() + + UIColor.gray.setStroke() + let strokeWidth: CGFloat = 2 + let borderRect = rect.insetBy(dx: strokeWidth / 2, dy: strokeWidth / 2) + let borderPath = UIBezierPath(roundedRect: borderRect, cornerRadius: cornerRadiusFactor * size.width) + borderPath.stroke() + + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = .center + + let attributes: [NSAttributedString.Key: Any] = [ + .font: UIFont(name: "AvenirNext-Bold", size: 20) ?? UIFont.systemFont(ofSize: 20), + .foregroundColor: UIColor.black, + .paragraphStyle: paragraphStyle, + ] + + let textRect = rect.insetBy(dx: 10, dy: (rect.height - 20) / 2) + (value as NSString).draw(in: textRect, withAttributes: attributes) + } + + return SKTexture(image: finalImage) + } } // MARK: - DnDUIChoices diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/SKComponents/DnDDropZoneNode.swift b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/SKComponents/DnDDropZoneNode.swift index ae4a0e4b12..b9a75cc5bd 100644 --- a/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/SKComponents/DnDDropZoneNode.swift +++ b/Modules/GameEngineKit/Sources/_NewSystem/Views/DnD/SKComponents/DnDDropZoneNode.swift @@ -13,48 +13,16 @@ public class DnDDropZoneNode: SKSpriteNode { init(id: String, value: String, type: ChoiceType, position: CGPoint) { self.id = id let size = CGSize(width: 420, height: 315) - switch type { + let texture: SKTexture = switch type { case .image: - guard let path = Bundle.path(forImage: value), let image = UIImage(named: path) else { - fatalError("Image not found") - } - - super.init(texture: SKTexture(image: image), color: .clear, size: size) - + Self.createImageTexture(value: value, size: size) case .sfsymbol: - guard let image = UIImage(systemName: value, withConfiguration: UIImage.SymbolConfiguration(pointSize: size.height)) else { - fatalError("SFSymbol not found") - } - - super.init(texture: SKTexture(image: image), color: .clear, size: size) - + Self.createSFSymbolTexture(value: value, size: size) case .text: - super.init(texture: nil, color: .clear, size: size) - - let rectangle = SKShapeNode( - rect: CGRect(x: -size.width / 2, y: -size.height / 2, width: size.width, height: size.height), - cornerRadius: 10 - ) - - rectangle.fillColor = .white - rectangle.strokeColor = .black - rectangle.lineWidth = 0.5 - rectangle.zPosition = -1 - rectangle.position = CGPoint(x: 0, y: 0) - - self.addChild(rectangle) - - let label = SKLabelNode(text: value) - - label.fontSize = 20 - label.fontName = "AvenirNext-Bold" - label.fontColor = .black - label.position = CGPoint(x: 0, y: -10) - label.zPosition = 0 - - self.addChild(label) + Self.createTextTexture(value: value, size: size) } + super.init(texture: texture, color: .clear, size: size) self.name = value self.position = position self.zPosition = 10 @@ -62,22 +30,9 @@ public class DnDDropZoneNode: SKSpriteNode { init(node: DnDAnswerNode, position: CGPoint = .zero) { self.id = node.id - switch node.type { - case .text: - let image = DnDDropZoneNode.createRoundedRectImage(size: node.size) - let texture = SKTexture(image: image) - - super.init(texture: texture, color: .clear, size: node.size) - self.position = position - - case .sfsymbol, - .image: - let image = DnDDropZoneNode.createRoundedRectImage(size: node.size) - let texture = SKTexture(image: image) - - super.init(texture: texture, color: .clear, size: node.size) - self.position = position - } + let image = DnDDropZoneNode.createRoundedRectImage(size: node.size) + let texture = SKTexture(image: image) + super.init(texture: texture, color: .clear, size: node.size) self.position = position } @@ -94,6 +49,74 @@ public class DnDDropZoneNode: SKSpriteNode { // MARK: Private + private static let cornerRadiusFactor: CGFloat = 10 / 57 + private static let defaultSize = CGSize(width: 420, height: 315) + private static let borderWidth: CGFloat = 2.0 + private static let labelFontSize: CGFloat = 20 + private static let labelFontName = "AvenirNext-Bold" + private static let horizontalPadding: CGFloat = 10 +} + +extension DnDDropZoneNode { + private static func createImageTexture(value: String, size: CGSize) -> SKTexture { + guard let path = Bundle.path(forImage: value), let image = UIImage(named: path) else { + fatalError("Image not found") + } + + let renderer = UIGraphicsImageRenderer(size: size) + let finalImage = renderer.image { _ in + let rect = CGRect(origin: .zero, size: size) + let path = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadiusFactor * size.width) + path.addClip() + image.draw(in: rect) + } + + return SKTexture(image: finalImage) + } + + private static func createSFSymbolTexture(value: String, size: CGSize) -> SKTexture { + guard let image = UIImage(systemName: value, withConfiguration: UIImage.SymbolConfiguration(pointSize: size.height * 0.6)) else { + fatalError("SFSymbol not found") + } + + return SKTexture(image: image) + } + + private static func createTextTexture(value: String, size: CGSize) -> SKTexture { + let renderer = UIGraphicsImageRenderer(size: size) + + let finalImage = renderer.image { _ in + let rect = CGRect(origin: .zero, size: size) + + let roundedPath = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadiusFactor * size.width) + roundedPath.addClip() + UIColor.white.setFill() + roundedPath.fill() + + UIColor.gray.setStroke() + let borderRect = rect.insetBy(dx: self.borderWidth / 2, dy: self.borderWidth / 2) + let borderPath = UIBezierPath(roundedRect: borderRect, cornerRadius: cornerRadiusFactor * size.width - self.borderWidth / 2) + borderPath.lineWidth = self.borderWidth + borderPath.stroke() + + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = .center + + let attributes: [NSAttributedString.Key: Any] = [ + .font: UIFont(name: self.labelFontName, size: self.labelFontSize) ?? UIFont.systemFont(ofSize: self.labelFontSize), + .foregroundColor: UIColor.black, + .paragraphStyle: paragraphStyle, + ] + + let textHeight = (attributes[.font] as? UIFont)?.lineHeight ?? self.labelFontSize + let textRect = rect.insetBy(dx: self.horizontalPadding, dy: (rect.height - textHeight) / 2) + + (value as NSString).draw(in: textRect, withAttributes: attributes) + } + + return SKTexture(image: finalImage) + } + private static let dropzoneBackgroundColor: UIColor = .init( light: UIColor.white, dark: UIColor(displayP3Red: 242 / 255, green: 242 / 255, blue: 247 / 255, alpha: 1.0)