diff --git a/Moco/MocoApp.swift b/Moco/MocoApp.swift index 0106e81..cdeb2d9 100644 --- a/Moco/MocoApp.swift +++ b/Moco/MocoApp.swift @@ -35,7 +35,7 @@ struct MocoApp: App { @StateObject private var objectDetectionViewModel = ObjectDetectionViewModel.shared @StateObject private var arViewModel = ARViewModel() - @StateObject private var motionViewModel = MotionViewModel() + @StateObject private var motionViewModel = MotionViewModel.shared @StateObject private var orientationInfo = OrientationInfo.shared private static let sharedModelContainer: ModelContainer = ModelGenerator.generator(false) diff --git a/Moco/Model/MazePromptModel.swift b/Moco/Model/MazePromptModel.swift index a158ff4..0a096db 100644 --- a/Moco/Model/MazePromptModel.swift +++ b/Moco/Model/MazePromptModel.swift @@ -15,6 +15,7 @@ struct MazePromptModel { var isCorrectAnswer: Bool = false var isWrongAnswer: Bool = false var isTutorialDone = GlobalStorage.mazeTutorialFinished + var isGameOver = false var mazeCount = -1 var currentMazeIndex = 0 @@ -28,6 +29,7 @@ struct MazePromptModel { progress = 0.0 isCorrectAnswer = false isWrongAnswer = false + isGameOver = false isTutorialDone = GlobalStorage.mazeTutorialFinished mazeCount = -1 currentMazeIndex = 0 diff --git a/Moco/View/Components/Prompts/Maze/MazePrompt.swift b/Moco/View/Components/Prompts/Maze/MazePrompt.swift index 45c1da7..d38a865 100644 --- a/Moco/View/Components/Prompts/Maze/MazePrompt.swift +++ b/Moco/View/Components/Prompts/Maze/MazePrompt.swift @@ -8,6 +8,7 @@ import SwiftUI struct MazePrompt: View { + @Environment(\.navigate) private var navigate @Environment(\.settingsViewModel) private var settingsViewModel @Environment(\.audioViewModel) private var audioViewModel @Environment(\.episodeViewModel) private var episodeViewModel @@ -15,6 +16,7 @@ struct MazePrompt: View { @State private var isCorrectAnswerPopup = false @State private var isWrongAnswerPopup = false + @State private var gameOverPopup = false @State private var updateTimer = true @State private var elapsedSecond = 0 @@ -30,6 +32,8 @@ struct MazePrompt: View { var action: () -> Void = {} + var onRestart: (() -> Void)? + func playInitialNarration() { if mazePromptViewModel.isTutorialDone { audioViewModel.playSound( @@ -49,7 +53,14 @@ struct MazePrompt: View { Spacer() TimerView( durationParamInSeconds: mazePromptViewModel.durationInSeconds - ) + ) { + // MARK: - Game Over + + gameOverPopup = true + mazePromptViewModel.isGameOver = true + + // MARK: - + } .padding(.trailing, Screen.width * 0.3) } Text(promptText) @@ -112,8 +123,20 @@ struct MazePrompt: View { .popUp(isActive: $isWrongAnswerPopup, title: "Oh tidak! Kamu pergi ke jalan yang salah", disableCancel: true) { action() } + .popUp( + isActive: $gameOverPopup, + title: "Waktu telah habis!", + cancelText: "Keluar", + confirmText: "Ulangi", + disableCancel: true, + type: .danger + ) { + onRestart?() + } cancelHandler: { + navigate.popToRoot() + } .onReceive(timer) { _ in - if updateTimer { + if updateTimer && mazePromptViewModel.isTutorialDone { elapsedSecond += 1 } } diff --git a/Moco/View/User/Maze/MazeScene.swift b/Moco/View/User/Maze/MazeScene.swift index eb7cd5b..14017c0 100644 --- a/Moco/View/User/Maze/MazeScene.swift +++ b/Moco/View/User/Maze/MazeScene.swift @@ -8,6 +8,17 @@ import SpriteKit import SwiftUI +enum CollisionTypes: UInt32 { + case player = 1 + case wall = 2 + case finish = 4 +} + +enum FinishType: String { + case wrong + case correct +} + @propertyWrapper struct MazeAnswerAssets { private var answerAssets: [String] = [] @@ -22,12 +33,17 @@ struct MazeAnswerAssets { } } -class MazeScene: SKScene, ObservableObject { +class MazeScene: SKScene, SKPhysicsContactDelegate, ObservableObject { + let motionViewModel = MotionViewModel.shared + let mazePromptViewModel = MazePromptViewModel.shared + var moco: SKSpriteNode! var obj01: SKSpriteNode! var obj02: SKSpriteNode! var obj03: SKSpriteNode! + var lastTouchPosition: CGPoint? + var touched: Bool = false var score: Int = 0 @@ -49,46 +65,12 @@ class MazeScene: SKScene, ObservableObject { var mazeModel = MazeModel() override func didMove(to _: SKView) { + physicsWorld.gravity = CGVector(dx: 0, dy: 0) + physicsWorld.contactDelegate = self createMap() createPlayer() createObjective() - } - - func createMap() { - let screenWidth = size.width - let screenHeight = size.height - let tileSize = min(screenWidth, screenHeight) / CGFloat(mazeModel.arrayPoint.count) - - var xRenderPos: CGFloat - var yRenderPos: CGFloat = screenHeight - for index in 0 ..< mazeModel.arrayPoint.count { - xRenderPos = tileSize / 2 + screenWidth / 2 - xRenderPos -= (tileSize * CGFloat(mazeModel.arrayPoint.first!.count)) / 2 - - if index == 0 { - yRenderPos = screenHeight - tileSize / 2 - } else { - yRenderPos -= tileSize - } - - for jIndex in 0 ..< mazeModel.arrayPoint[index].count { - let ground = SKSpriteNode() - ground.size = CGSize(width: tileSize, height: tileSize) - - if mazeModel.arrayPoint[index][jIndex] == 0 { - ground.name = "0" - ground.texture = SKTexture(imageNamed: "Maze/floor") - } else if mazeModel.arrayPoint[index][jIndex] == 1 { - ground.name = "1" - ground.texture = SKTexture(imageNamed: "Maze/wall") - } - - ground.position = CGPoint(x: xRenderPos, y: yRenderPos) - xRenderPos += tileSize - addChild(ground) - mazeModel.points[index][jIndex] = ground.position - } - } + motionViewModel.startUpdates() } func move(_ direction: MoveDirection) { @@ -104,18 +86,9 @@ class MazeScene: SKScene, ObservableObject { if mazeModel.characterLocationPoint.yPos == mazeModel.correctPoint.yPos && mazeModel.characterLocationPoint.xPos != mazeModel.correctPoint.xPos { -// let move = SKAction.move(to: position, duration: 0.3) -// let scale = SKAction.scale(to: 0.0001, duration: 0.3) -// let remove = SKAction.removeFromParent() -// let sequence = SKAction.sequence([move, scale, remove]) -// moco.run(sequence) { [unowned self] in -// createPlayer() -// } - print("char", mazeModel.characterLocationPoint, "goal", mazeModel.correctPoint) correctAnswer = false wrongAnswer = true } else if mazeModel.characterLocationPoint == mazeModel.correctPoint { - print("char", mazeModel.characterLocationPoint, "goal", mazeModel.correctPoint) correctAnswer = true wrongAnswer = false } else { @@ -133,6 +106,14 @@ class MazeScene: SKScene, ObservableObject { ) ) + moco.physicsBody = SKPhysicsBody(circleOfRadius: (moco.size.width * 0.9) / 2) + moco.physicsBody?.allowsRotation = false + moco.physicsBody?.linearDamping = 0.5 + + moco.physicsBody?.categoryBitMask = CollisionTypes.player.rawValue + moco.physicsBody?.contactTestBitMask = CollisionTypes.finish.rawValue + moco.physicsBody?.collisionBitMask = CollisionTypes.wall.rawValue + correctAnswer = nil wrongAnswer = nil @@ -188,62 +169,144 @@ class MazeScene: SKScene, ObservableObject { obj03?.texture = SKTexture(imageNamed: wrongAnswerAsset[1]) } - // MARK: - Not used - - /* - func actionMovePlayer(to: SKNode, xPos: CGFloat, yPos: CGFloat) { - let move = SKAction.move(to: to.position, duration: 0.15) - let void = SKAction.run { [self] in - movePacman(xPos: xPos, yPos: yPos) - } - let sequence = SKAction.sequence([move, void]) - moco.run(sequence) - } - - func movePacman(xPos: CGFloat, yPos: CGFloat) { - let next = nodes(at: CGPoint(x: moco.position.x + xPos, y: moco.position.y + yPos)).last - if next?.name == "0" { - if let nextChildNode = next?.childNode(withName: "0") { - nextChildNode.removeFromParent() - } - actionMovePlayer(to: next!, xPos: xPos, yPos: yPos) - } - } - - override func touchesBegan(_: Set, with _: UIEvent?) { - // let touch = touches.first! - // let location = touch.location(in: self) - // if atPoint(location).name == "left" { - // touched = true - // childNode(withName: "left")?.alpha = 1 - // movePacman(x: -size.width / CGFloat(arrayPoint.count), y: 0) - // } - // if atPoint(location).name == "right" { - // touched = true - // childNode(withName: "right")?.alpha = 1 - // movePacman(x: size.width / CGFloat(arrayPoint.count), y: 0) - // } - // if atPoint(location).name == "up" { - // touched = true - // childNode(withName: "up")?.alpha = 1 - // movePacman(x: 0, y: size.width / CGFloat(arrayPoint.count)) - // } - // if atPoint(location).name == "down" { - // touched = true - // childNode(withName: "down")?.alpha = 1 - // movePacman(x: 0, y: -size.width / CGFloat(arrayPoint.count)) - // } - } - - override func touchesEnded(_: Set, with _: UIEvent?) { - // for child in children { - // if child.name == "left" || child.name == "right" || child.name == "up" || child.name == "down" { - // child.alpha = 0.5 - // } - // } - // touched = false - } - */ + override func touchesBegan(_ touches: Set, with _: UIEvent?) { + if let touch = touches.first { + let location = touch.location(in: self) + lastTouchPosition = location + } + } + + override func touchesMoved(_ touches: Set, with _: UIEvent?) { + if let touch = touches.first { + let location = touch.location(in: self) + lastTouchPosition = location + } + } + + override func touchesEnded(_: Set, with _: UIEvent?) { + lastTouchPosition = nil + } + + override func touchesCancelled(_: Set, with _: UIEvent?) { + lastTouchPosition = nil + } + + func didBegin(_ contact: SKPhysicsContact) { + if contact.bodyA.node == moco { + playerCollided(with: contact.bodyB.node!) + } else if contact.bodyB.node == moco { + playerCollided(with: contact.bodyA.node!) + } + } + + func playerCollided(with node: SKNode) { + if node.name == FinishType.correct.rawValue { + correctAnswer = true + wrongAnswer = false + } else if node.name == FinishType.wrong.rawValue { + correctAnswer = false + wrongAnswer = true + } + } + + override func update(_: TimeInterval) { + if !mazePromptViewModel.canMove { + physicsWorld.gravity = CGVector( + dx: 0, + dy: 0 + ) + return + } + #if targetEnvironment(simulator) + if let currentTouch = lastTouchPosition { + let diff = CGPoint(x: currentTouch.x - moco.position.x, y: currentTouch.y - moco.position.y) + physicsWorld.gravity = CGVector(dx: diff.x / 100, dy: diff.y / 100) + } + #else + if let accelerometerData = motionViewModel.accelerometerData { + physicsWorld.gravity = CGVector( + dx: accelerometerData.acceleration.y * -2, + dy: accelerometerData.acceleration.x * 2 + ) + } + #endif + } +} + +extension MazeScene { + func createMap() { + let screenWidth = size.width + let screenHeight = size.height + let tileSize = min(screenWidth, screenHeight) / CGFloat(mazeModel.arrayPoint.count) + + var xRenderPos: CGFloat + var yRenderPos: CGFloat = screenHeight + for index in mazeModel.arrayPoint.indices { + xRenderPos = tileSize / 2 + screenWidth / 2 + xRenderPos -= (tileSize * CGFloat(mazeModel.arrayPoint.first!.count)) / 2 + + if index == 0 { + yRenderPos = screenHeight - tileSize / 2 + } else { + yRenderPos -= tileSize + } + + for jIndex in mazeModel.arrayPoint[index].indices { + let ground = SKSpriteNode() + ground.size = CGSize(width: tileSize, height: tileSize) + + if mazeModel.arrayPoint[index][jIndex] == 0 { + ground.name = "0" + ground.texture = SKTexture(imageNamed: "Maze/floor") + if index == mazeModel.arrayPoint.indices.last { // last tile + ground.name = FinishType.wrong.rawValue + if jIndex == mazeModel.correctPoint.xPos { + ground.name = FinishType.correct.rawValue + } + ground.physicsBody = SKPhysicsBody(rectangleOf: CGSize( + width: ground.size.width * 0.5, + height: ground.size.height * 0.5 + ) + ) + ground.physicsBody?.categoryBitMask = CollisionTypes.finish.rawValue + ground.physicsBody?.contactTestBitMask = CollisionTypes.player.rawValue + ground.physicsBody?.isDynamic = false + ground.physicsBody?.collisionBitMask = 0 + } + } else if mazeModel.arrayPoint[index][jIndex] == 1 { + ground.name = "1" + ground.texture = SKTexture(imageNamed: "Maze/wall") + ground.physicsBody = SKPhysicsBody(rectangleOf: ground.size) + ground.physicsBody?.categoryBitMask = CollisionTypes.wall.rawValue + ground.physicsBody?.isDynamic = false + } + + // MARK: - Outer wall + + if mazeModel.arrayPoint[index][jIndex] == 0, + [0, mazeModel.arrayPoint.indices.last].contains(index) { + let outerWall = SKSpriteNode() + outerWall.size = CGSize(width: tileSize, height: tileSize) + outerWall.name = "outer_wall" + outerWall.texture = SKTexture(imageNamed: "Maze/wall") + outerWall.alpha = 0 + outerWall.physicsBody = SKPhysicsBody(rectangleOf: outerWall.size) + outerWall.physicsBody?.categoryBitMask = CollisionTypes.wall.rawValue + outerWall.physicsBody?.isDynamic = false + outerWall.position = CGPoint( + x: xRenderPos, + y: yRenderPos + (index == 0 ? tileSize : -tileSize) + ) + addChild(outerWall) + } + + ground.position = CGPoint(x: xRenderPos, y: yRenderPos) + xRenderPos += tileSize + addChild(ground) + mazeModel.points[index][jIndex] = ground.position + } + } + } } #Preview { diff --git a/Moco/View/User/Maze/MazeView.swift b/Moco/View/User/Maze/MazeView.swift index a315022..7416fc4 100644 --- a/Moco/View/User/Maze/MazeView.swift +++ b/Moco/View/User/Maze/MazeView.swift @@ -20,7 +20,6 @@ struct MazeView: View { @EnvironmentObject var motionViewModel: MotionViewModel @EnvironmentObject var orientationInfo: OrientationInfo @Environment(\.mazePromptViewModel) private var mazePromptViewModel - @State private var timerViewModel = TimerViewModel() var answersAsset = ["Maze/answer_one", "Maze/answer_two"] { didSet { @@ -113,72 +112,6 @@ struct MazeView: View { motionViewModel.startUpdates() scene.correctAnswerAsset = correctAnswerAsset scene.wrongAnswerAsset = answersAsset - timerViewModel.stopTimer("mazeTimer\(correctAnswerAsset)") - timerViewModel.setTimer(key: "mazeTimer\(correctAnswerAsset)", withInterval: 0.02) { - guard mazePromptViewModel.isTutorialDone else { return } - motionViewModel.updateMotion() - if orientationInfo.orientation == .landscapeLeft { - if abs(motionViewModel.rollNum) > abs(motionViewModel.pitchNum) { - if motionViewModel.rollNum > 0 { - switch motionViewModel.gravityDegree { - case -75 ... -10, 10 ... 80: - scene.move(.right) - case 100 ... 170, 190 ... 255: - scene.move(.left) - default: - scene.move(.up) - } - } else if motionViewModel.rollNum < 0 { - switch motionViewModel.gravityDegree { - case -75 ... -10, 105 ... 170: - scene.move(.right) - case 10 ... 75, 190 ... 255: - scene.move(.left) - default: - scene.move(.down) - } - } - } else { - if motionViewModel.pitchNum > 0 { - scene.move(.right) - } else if motionViewModel.pitchNum < 0 { - scene.move(.left) - } - } - } else if orientationInfo.orientation == .landscapeRight { - if abs(motionViewModel.rollNum) > abs(motionViewModel.pitchNum) { - if motionViewModel.rollNum > 0 { - switch motionViewModel.gravityDegree { - case -75 ... -10, 105 ... 170: - scene.move(.right) - case 10 ... 75, 190 ... 255: - scene.move(.left) - default: - scene.move(.down) - } - } else if motionViewModel.rollNum < 0 { - switch motionViewModel.gravityDegree { - case -75 ... -10, 10 ... 80: - scene.move(.left) - case 100 ... 170, 190 ... 255: - scene.move(.right) - default: - scene.move(.up) - } - } - } else { - if motionViewModel.pitchNum > 0 { - scene.move(.left) - } else if motionViewModel.pitchNum < 0 { - scene.move(.right) - } - } - } - } - } - .onDisappear { -// motionViewModel.stopUpdates() - timerViewModel.stopTimer("mazeTimer\(correctAnswerAsset)") } .onChange(of: scene.correctAnswer) { if let sceneCorrectAnswer = scene.correctAnswer { @@ -191,6 +124,76 @@ struct MazeView: View { } } } + + private func landscapeLeftControl() { + if abs(motionViewModel.rollNum) > abs(motionViewModel.pitchNum) { + if motionViewModel.rollNum > 0 { + switch motionViewModel.gravityDegree { + case -75 ... -10, 10 ... 80: + scene.move(.right) + case 100 ... 170, 190 ... 255: + scene.move(.left) + default: + scene.move(.up) + } + } else if motionViewModel.rollNum < 0 { + switch motionViewModel.gravityDegree { + case -75 ... -10, 105 ... 170: + scene.move(.right) + case 10 ... 75, 190 ... 255: + scene.move(.left) + default: + scene.move(.down) + } + } + } else { + if motionViewModel.pitchNum > 0 { + scene.move(.right) + } else if motionViewModel.pitchNum < 0 { + scene.move(.left) + } + } + } + + private func landscapeRightControl() { + if abs(motionViewModel.rollNum) > abs(motionViewModel.pitchNum) { + if motionViewModel.rollNum > 0 { + switch motionViewModel.gravityDegree { + case -75 ... -10, 105 ... 170: + scene.move(.right) + case 10 ... 75, 190 ... 255: + scene.move(.left) + default: + scene.move(.down) + } + } else if motionViewModel.rollNum < 0 { + switch motionViewModel.gravityDegree { + case -75 ... -10, 10 ... 80: + scene.move(.left) + case 100 ... 170, 190 ... 255: + scene.move(.right) + default: + scene.move(.up) + } + } + } else { + if motionViewModel.pitchNum > 0 { + scene.move(.left) + } else if motionViewModel.pitchNum < 0 { + scene.move(.right) + } + } + } + + private func updateMazeControl() { + guard mazePromptViewModel.isTutorialDone else { return } + motionViewModel.updateMotion() + if orientationInfo.orientation == .landscapeLeft { + landscapeLeftControl() + } else if orientationInfo.orientation == .landscapeRight { + landscapeRightControl() + } + } } struct MazeViewPreview: View { diff --git a/Moco/View/User/StoryView.swift b/Moco/View/User/StoryView.swift index 5e3f671..34205be 100644 --- a/Moco/View/User/StoryView.swift +++ b/Moco/View/User/StoryView.swift @@ -105,7 +105,10 @@ struct StoryView: View { promptId: mazePrompt.uid ) { svvm.nextPage() - }.id(mazePrompt.id) + } onRestart: { + svvm.restart(true) + } + .id(mazePrompt.id) } case .ar: if let ARPrompt = promptViewModel.prompts?[0] { diff --git a/Moco/ViewModel/MazePromptViewModel.swift b/Moco/ViewModel/MazePromptViewModel.swift index 86eeeb9..6a00a96 100644 --- a/Moco/ViewModel/MazePromptViewModel.swift +++ b/Moco/ViewModel/MazePromptViewModel.swift @@ -105,6 +105,19 @@ import SwiftUI } } + var canMove: Bool { + isTutorialDone && !mazePromptModel.isGameOver + } + + var isGameOver: Bool { + get { + mazePromptModel.isGameOver + } + set { + mazePromptModel.isGameOver = newValue + } + } + func playPrompt() { mazePromptModel.isStarted = false withAnimation(.easeInOut(duration: 3)) { diff --git a/Moco/ViewModel/MotionViewModel.swift b/Moco/ViewModel/MotionViewModel.swift index ef46bdb..546fd57 100644 --- a/Moco/ViewModel/MotionViewModel.swift +++ b/Moco/ViewModel/MotionViewModel.swift @@ -9,6 +9,8 @@ import CoreMotion import Foundation class MotionViewModel: ObservableObject { + static var shared = MotionViewModel() + @Published var accelerationValue: String = "" @Published var gravityValue: String = "" @Published var rotationValue: String = "" @@ -31,6 +33,10 @@ class MotionViewModel: ObservableObject { private var rotationRate: CMRotationRate = .init() private var attitude: CMAttitude? + var accelerometerData: CMAccelerometerData? { + motionManager.accelerometerData + } + init() { // Set the update interval to any time that you want motionManager.deviceMotionUpdateInterval = 1.0 / 60.0 // 60 Hz @@ -63,6 +69,8 @@ class MotionViewModel: ObservableObject { guard let self = self else { return } getRotation(gyroData: gyroData) } + + motionManager.startAccelerometerUpdates() } } diff --git a/Moco/ViewModel/StoryViewViewModel.swift b/Moco/ViewModel/StoryViewViewModel.swift index 1c14bd1..2e0d45f 100644 --- a/Moco/ViewModel/StoryViewViewModel.swift +++ b/Moco/ViewModel/StoryViewViewModel.swift @@ -89,7 +89,7 @@ extension StoryViewViewModel { } } - func onPageChange() { + func onPageChange(_ earlyPrompt: Bool? = false) { stop() setNewStoryPage(scrollPosition ?? -1) @@ -106,7 +106,7 @@ extension StoryViewViewModel { promptViewModel.fetchPrompts(storyPage) } startPrompt() - if let storyPage = storyViewModel.storyPage, storyPage.earlyPrompt { + if let storyPage = storyViewModel.storyPage, storyPage.earlyPrompt || earlyPrompt! { promptViewModel.fetchPrompts(storyPage) if let prompt = promptViewModel.prompts?.first { activePrompt = prompt @@ -232,4 +232,29 @@ extension StoryViewViewModel { onPageChange() mazePromptViewModel.reset(true) } + + func reset() { + scrollPosition = 0 + isExitPopUpActive = false + isEpisodeFinished = false + isMuted = false + text = "" + narrativeIndex = -1 + showPromptButton = false + activePrompt = nil + peelEffectState = PeelEffectState.stop + toBeExecutedByPeelEffect = {} + peelBackground = AnyView(EmptyView()) + isReversePeel = false + showWrongAnsPopup = false + mazeQuestionIndex = 0 + forceShowNext = false + showPauseMenu = false + } + + func restart(_ earlyPrompt: Bool? = false) { + reset() + onPageChange(earlyPrompt!) + mazePromptViewModel.reset(true) + } }