diff --git a/Moco.xcodeproj/project.pbxproj b/Moco.xcodeproj/project.pbxproj index acb0fd8..58dbdbf 100644 --- a/Moco.xcodeproj/project.pbxproj +++ b/Moco.xcodeproj/project.pbxproj @@ -8,8 +8,6 @@ /* Begin PBXBuildFile section */ 050077182B08033200376135 /* MakeSentence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 050077172B08033200376135 /* MakeSentence.swift */; }; - 050077202B08B5B200376135 /* Apa yang akan dilakukan bebe selanjutnya.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 0500771E2B081F3C00376135 /* Apa yang akan dilakukan bebe selanjutnya.m4a */; }; - 050077212B08B5B200376135 /* Susunlah sebuah kalimat.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 0500771F2B08244200376135 /* Susunlah sebuah kalimat.m4a */; }; 050077222B08B5B200376135 /* Cari kartu benda.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 0500771C2B081ED200376135 /* Cari kartu benda.m4a */; }; 050077232B08B5B200376135 /* Cari kartu karakter.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 0500771D2B081ED200376135 /* Cari kartu karakter.m4a */; }; 050077242B08B5B200376135 /* Cari kartu kata kerja.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 0500771B2B081ED200376135 /* Cari kartu kata kerja.m4a */; }; @@ -69,6 +67,8 @@ 05AE93682ADE83430082B21D /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 05AE93672ADE83430082B21D /* Lottie */; }; 05B8EDA02AEE65FE009ED2C5 /* MotionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05B8ED9F2AEE65FE009ED2C5 /* MotionViewModel.swift */; }; 05BA97AA2AE11EE400F64F52 /* MPVolumeViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05BA97A92AE11EE400F64F52 /* MPVolumeViewExtension.swift */; }; + 05C13CFC2B13B74500F8F00E /* Susunlah sebuah kalimat.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 05C13CFA2B13B6F600F8F00E /* Susunlah sebuah kalimat.m4a */; }; + 05C13CFD2B13B74A00F8F00E /* Apa yang akan dilakukan bebe selanjutnya.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 05C13CFB2B13B6F600F8F00E /* Apa yang akan dilakukan bebe selanjutnya.m4a */; }; 05D966E12ADEE6CF00997E3A /* success.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 05D966E02ADEE6CF00997E3A /* success.mp3 */; }; 05E17AD52ADE86DE00A34547 /* FindTheObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05E17AD42ADE86DE00A34547 /* FindTheObject.swift */; }; 05E17AD72ADEBFC500A34547 /* BalloonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05E17AD62ADEBFC500A34547 /* BalloonView.swift */; }; @@ -106,6 +106,8 @@ 0B6E2F742ADC9DC90001D320 /* HintViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B6E2F732ADC9DC90001D320 /* HintViewModel.swift */; }; 0B6E2F762ADC9F020001D320 /* PromptViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B6E2F752ADC9F020001D320 /* PromptViewModel.swift */; }; 0B6E2F782ADCA8990001D320 /* StoryContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B6E2F772ADCA8990001D320 /* StoryContentViewModel.swift */; }; + 0B771A622B10B1DB00F1D8CA /* UserViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B771A612B10B1DB00F1D8CA /* UserViewModel.swift */; }; + 0B771A642B10B1F500F1D8CA /* UserModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B771A632B10B1F500F1D8CA /* UserModel.swift */; }; 0BA3E5092ADFC61500BCFE9E /* StoryThemeAdminView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BA3E5082ADFC61500BCFE9E /* StoryThemeAdminView.swift */; }; 0BA3E50B2ADFC62000BCFE9E /* StoryAdminView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BA3E50A2ADFC62000BCFE9E /* StoryAdminView.swift */; }; 0BA3E5102ADFD04700BCFE9E /* StoryThemeModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BA3E50F2ADFD04700BCFE9E /* StoryThemeModalView.swift */; }; @@ -134,6 +136,7 @@ 931722232AE5508300346382 /* Page10-monolog1.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 056949822AE0ED3300830D57 /* Page10-monolog1.m4a */; }; 931722242AE5508300346382 /* Page10-monolog2.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 056949812AE0ED3300830D57 /* Page10-monolog2.m4a */; }; 931722252AE5508300346382 /* congratulations.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 053393122AE13A310085BB30 /* congratulations.m4a */; }; + 931B8DA12B13344E0025F604 /* MocoIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 931B8DA02B13344E0025F604 /* MocoIcon.swift */; }; 932357D62ADE6EF3009B3D86 /* bg-story.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 932357D52ADE6EF3009B3D86 /* bg-story.mp3 */; }; 932357D82ADE700D009B3D86 /* AudioViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 932357D72ADE700D009B3D86 /* AudioViewModel.swift */; }; 932357DA2ADE7190009B3D86 /* AudioModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 932357D92ADE7190009B3D86 /* AudioModel.swift */; }; @@ -284,8 +287,6 @@ 0500771B2B081ED200376135 /* Cari kartu kata kerja.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Cari kartu kata kerja.m4a"; sourceTree = ""; }; 0500771C2B081ED200376135 /* Cari kartu benda.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Cari kartu benda.m4a"; sourceTree = ""; }; 0500771D2B081ED200376135 /* Cari kartu karakter.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Cari kartu karakter.m4a"; sourceTree = ""; }; - 0500771E2B081F3C00376135 /* Apa yang akan dilakukan bebe selanjutnya.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Apa yang akan dilakukan bebe selanjutnya.m4a"; sourceTree = ""; }; - 0500771F2B08244200376135 /* Susunlah sebuah kalimat.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Susunlah sebuah kalimat.m4a"; sourceTree = ""; }; 050353E02ADFE2970066591A /* FindHoney.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindHoney.swift; sourceTree = ""; }; 050E42062AFDC540009666DB /* ARTutorialView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ARTutorialView.swift; sourceTree = ""; }; 050E420E2AFDCC7D009666DB /* arrange_camera_to_barcode.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = arrange_camera_to_barcode.json; sourceTree = ""; }; @@ -361,6 +362,8 @@ 05AE93622ADE82920082B21D /* LottieView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LottieView.swift; sourceTree = ""; }; 05B8ED9F2AEE65FE009ED2C5 /* MotionViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MotionViewModel.swift; sourceTree = ""; }; 05BA97A92AE11EE400F64F52 /* MPVolumeViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPVolumeViewExtension.swift; sourceTree = ""; }; + 05C13CFA2B13B6F600F8F00E /* Susunlah sebuah kalimat.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Susunlah sebuah kalimat.m4a"; sourceTree = ""; }; + 05C13CFB2B13B6F600F8F00E /* Apa yang akan dilakukan bebe selanjutnya.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Apa yang akan dilakukan bebe selanjutnya.m4a"; sourceTree = ""; }; 05D966E02ADEE6CF00997E3A /* success.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = success.mp3; sourceTree = ""; }; 05E17AD42ADE86DE00A34547 /* FindTheObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindTheObject.swift; sourceTree = ""; }; 05E17AD62ADEBFC500A34547 /* BalloonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalloonView.swift; sourceTree = ""; }; @@ -399,6 +402,8 @@ 0B6E2F732ADC9DC90001D320 /* HintViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HintViewModel.swift; sourceTree = ""; }; 0B6E2F752ADC9F020001D320 /* PromptViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromptViewModel.swift; sourceTree = ""; }; 0B6E2F772ADCA8990001D320 /* StoryContentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryContentViewModel.swift; sourceTree = ""; }; + 0B771A612B10B1DB00F1D8CA /* UserViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserViewModel.swift; sourceTree = ""; }; + 0B771A632B10B1F500F1D8CA /* UserModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserModel.swift; sourceTree = ""; }; 0BA3E5082ADFC61500BCFE9E /* StoryThemeAdminView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryThemeAdminView.swift; sourceTree = ""; }; 0BA3E50A2ADFC62000BCFE9E /* StoryAdminView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryAdminView.swift; sourceTree = ""; }; 0BA3E50F2ADFD04700BCFE9E /* StoryThemeModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryThemeModalView.swift; sourceTree = ""; }; @@ -412,6 +417,7 @@ 9310EC7C2B07E1280045A908 /* maaf_kartu_tidak_tepat.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = maaf_kartu_tidak_tepat.m4a; sourceTree = ""; }; 9310EC802B0A353A0045A908 /* StoryViewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryViewViewModel.swift; sourceTree = ""; }; 9310EC852B0A40FA0045A908 /* Story1.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Story1.swift; sourceTree = ""; }; + 931B8DA02B13344E0025F604 /* MocoIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MocoIcon.swift; sourceTree = ""; }; 932357D52ADE6EF3009B3D86 /* bg-story.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = "bg-story.mp3"; sourceTree = ""; }; 932357D72ADE700D009B3D86 /* AudioViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioViewModel.swift; sourceTree = ""; }; 932357D92ADE7190009B3D86 /* AudioModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioModel.swift; sourceTree = ""; }; @@ -578,8 +584,8 @@ 0500771A2B081EC100376135 /* MakeSentence */ = { isa = PBXGroup; children = ( - 0500771E2B081F3C00376135 /* Apa yang akan dilakukan bebe selanjutnya.m4a */, - 0500771F2B08244200376135 /* Susunlah sebuah kalimat.m4a */, + 05C13CFB2B13B6F600F8F00E /* Apa yang akan dilakukan bebe selanjutnya.m4a */, + 05C13CFA2B13B6F600F8F00E /* Susunlah sebuah kalimat.m4a */, 0500771C2B081ED200376135 /* Cari kartu benda.m4a */, 0500771D2B081ED200376135 /* Cari kartu karakter.m4a */, 0500771B2B081ED200376135 /* Cari kartu kata kerja.m4a */, @@ -1014,6 +1020,7 @@ 93C30CB92AF926150076808F /* TimerView.swift */, 0592100F2AFB601E0008CD2C /* QRScannerSheet.swift */, 93692EA92AFD43FD00CD6561 /* PauseMenu.swift */, + 931B8DA02B13344E0025F604 /* MocoIcon.swift */, ); path = Components; sourceTree = ""; @@ -1308,6 +1315,7 @@ 938E3D302AD8FD2E003D2AE1 /* ItemViewModel.swift */, 0B2E31E12AD9064900DFC16F /* CollectionViewModel.swift */, 938E3D422ADABD9F003D2AE1 /* BaseViewModel.swift */, + 0B771A612B10B1DB00F1D8CA /* UserViewModel.swift */, 0B6E2F6F2ADC96200001D320 /* StoryThemeViewModel.swift */, 0B46D7202AF12077002DE821 /* EpisodeViewModel.swift */, 0B6E2F712ADC97BE0001D320 /* StoryViewModel.swift */, @@ -1343,6 +1351,7 @@ 938E3D382AD939DC003D2AE1 /* Item.swift */, 0B2E31D92AD7EBE500DFC16F /* CollectionModel.swift */, 938E3D1E2AD7AD78003D2AE1 /* Collection.swift */, + 0B771A632B10B1F500F1D8CA /* UserModel.swift */, 0B6E2F652ADC8ECE0001D320 /* StoryThemeModel.swift */, 0B46D7222AF120BC002DE821 /* EpisodeModel.swift */, 0B6E2F672ADC908B0001D320 /* StoryModel.swift */, @@ -1577,8 +1586,8 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 050077202B08B5B200376135 /* Apa yang akan dilakukan bebe selanjutnya.m4a in Resources */, - 050077212B08B5B200376135 /* Susunlah sebuah kalimat.m4a in Resources */, + 05C13CFD2B13B74A00F8F00E /* Apa yang akan dilakukan bebe selanjutnya.m4a in Resources */, + 05C13CFC2B13B74500F8F00E /* Susunlah sebuah kalimat.m4a in Resources */, 050077222B08B5B200376135 /* Cari kartu benda.m4a in Resources */, 050077232B08B5B200376135 /* Cari kartu karakter.m4a in Resources */, 050077242B08B5B200376135 /* Cari kartu kata kerja.m4a in Resources */, @@ -1702,6 +1711,7 @@ 9310EC782B07C3040045A908 /* ScaleEffect.swift in Sources */, 938E3D4D2ADBA247003D2AE1 /* ContentViewContainer.swift in Sources */, 9310EC812B0A353A0045A908 /* StoryViewViewModel.swift in Sources */, + 931B8DA12B13344E0025F604 /* MocoIcon.swift in Sources */, 0B1197622AD7D4F3004FAD14 /* IconButton.swift in Sources */, 938E3D3F2AD990C3003D2AE1 /* Buttons.swift in Sources */, 937AF2952AE8C06C008C69BB /* ARView.swift in Sources */, @@ -1773,6 +1783,7 @@ 9310EC862B0A40FA0045A908 /* Story1.swift in Sources */, 935D78552AEC09A000DF64A5 /* GlobalStorage.swift in Sources */, 05BA97AA2AE11EE400F64F52 /* MPVolumeViewExtension.swift in Sources */, + 0B771A622B10B1DB00F1D8CA /* UserViewModel.swift in Sources */, 93676D242AF139A800E756A5 /* ProgressBar.swift in Sources */, 05B8EDA02AEE65FE009ED2C5 /* MotionViewModel.swift in Sources */, 938E3D452ADAD5F3003D2AE1 /* StoryBook.swift in Sources */, @@ -1810,6 +1821,7 @@ 93C2F8BE2AD684CE007D1ABC /* Device.swift in Sources */, 93676D2D2AF1475200E756A5 /* PointerViewModifier.swift in Sources */, 93692EAA2AFD43FD00CD6561 /* PauseMenu.swift in Sources */, + 0B771A642B10B1F500F1D8CA /* UserModel.swift in Sources */, 937AF2A22AE930AF008C69BB /* MazeScene.swift in Sources */, 935D78622AEE1AC400DF64A5 /* MazePrompt.swift in Sources */, 938C553E2AE1B76600BC233D /* ObjectDetectionModel.swift in Sources */, @@ -2022,14 +2034,14 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.0.3; PRODUCT_BUNDLE_IDENTIFIER = com.mocoteam.Moco; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 2; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; @@ -2057,14 +2069,14 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.0.3; PRODUCT_BUNDLE_IDENTIFIER = com.mocoteam.Moco; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 2; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; diff --git a/Moco/Assets.xcassets/Buttons/button-switch-camera.imageset/Button Switch Camera.png b/Moco/Assets.xcassets/Buttons/button-switch-camera.imageset/Button Switch Camera.png new file mode 100644 index 0000000..be0c413 Binary files /dev/null and b/Moco/Assets.xcassets/Buttons/button-switch-camera.imageset/Button Switch Camera.png differ diff --git a/Moco/Assets.xcassets/Buttons/button-switch-camera.imageset/Contents.json b/Moco/Assets.xcassets/Buttons/button-switch-camera.imageset/Contents.json new file mode 100644 index 0000000..92c51fd --- /dev/null +++ b/Moco/Assets.xcassets/Buttons/button-switch-camera.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Button Switch Camera.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Moco/Assets.xcassets/Colors/blue-txt.colorset/Contents.json b/Moco/Assets.xcassets/Colors/blue-txt.colorset/Contents.json index 27219ff..a3fb18f 100644 --- a/Moco/Assets.xcassets/Colors/blue-txt.colorset/Contents.json +++ b/Moco/Assets.xcassets/Colors/blue-txt.colorset/Contents.json @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xFF", - "red" : "0xFE" + "blue" : "0xB8", + "green" : "0x92", + "red" : "0x68" } }, "idiom" : "universal" diff --git a/Moco/Assets.xcassets/Colors/blue2-txt.colorset/Contents.json b/Moco/Assets.xcassets/Colors/blue2-txt.colorset/Contents.json index fc66ae3..a2b4816 100644 --- a/Moco/Assets.xcassets/Colors/blue2-txt.colorset/Contents.json +++ b/Moco/Assets.xcassets/Colors/blue2-txt.colorset/Contents.json @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xFF", - "red" : "0xFE" + "blue" : "0x92", + "green" : "0x6F", + "red" : "0x4A" } }, "idiom" : "universal" diff --git a/Moco/Assets.xcassets/Colors/brown-txt.colorset/Contents.json b/Moco/Assets.xcassets/Colors/brown-txt.colorset/Contents.json index ba2289a..2635e1e 100644 --- a/Moco/Assets.xcassets/Colors/brown-txt.colorset/Contents.json +++ b/Moco/Assets.xcassets/Colors/brown-txt.colorset/Contents.json @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xFF", - "red" : "0xFE" + "blue" : "0x4E", + "green" : "0x74", + "red" : "0x94" } }, "idiom" : "universal" diff --git a/Moco/Assets.xcassets/Colors/green-btn.colorset/Contents.json b/Moco/Assets.xcassets/Colors/green-btn.colorset/Contents.json index f079895..e663c65 100644 --- a/Moco/Assets.xcassets/Colors/green-btn.colorset/Contents.json +++ b/Moco/Assets.xcassets/Colors/green-btn.colorset/Contents.json @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xFF", - "red" : "0xFE" + "blue" : "0x90", + "green" : "0xBB", + "red" : "0x9E" } }, "idiom" : "universal" diff --git a/Moco/Assets.xcassets/Colors/red-btn.colorset/Contents.json b/Moco/Assets.xcassets/Colors/red-btn.colorset/Contents.json index 62f1597..dc3daf3 100644 --- a/Moco/Assets.xcassets/Colors/red-btn.colorset/Contents.json +++ b/Moco/Assets.xcassets/Colors/red-btn.colorset/Contents.json @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xFF", - "red" : "0xFE" + "blue" : "0x71", + "green" : "0x71", + "red" : "0xDF" } }, "idiom" : "universal" diff --git a/Moco/Assets.xcassets/Story/Content/Story1/Ep3/Page1/Contents.json b/Moco/Assets.xcassets/Story/Content/Story1/Ep3/Page1/Contents.json new file mode 100644 index 0000000..6e96565 --- /dev/null +++ b/Moco/Assets.xcassets/Story/Content/Story1/Ep3/Page1/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/Moco/Assets.xcassets/Story/Content/Story1/Ep3/Page1/background.imageset/Contents.json b/Moco/Assets.xcassets/Story/Content/Story1/Ep3/Page1/background.imageset/Contents.json new file mode 100644 index 0000000..85ea695 --- /dev/null +++ b/Moco/Assets.xcassets/Story/Content/Story1/Ep3/Page1/background.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Page 8 1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Moco/Assets.xcassets/Story/Content/Story1/Ep3/Page1/background.imageset/Page 8 1.png b/Moco/Assets.xcassets/Story/Content/Story1/Ep3/Page1/background.imageset/Page 8 1.png new file mode 100644 index 0000000..c6aae69 Binary files /dev/null and b/Moco/Assets.xcassets/Story/Content/Story1/Ep3/Page1/background.imageset/Page 8 1.png differ diff --git a/Moco/Assets.xcassets/Story/Content/Story1/Ep3/Page6/Contents.json b/Moco/Assets.xcassets/Story/Content/Story1/Ep3/Page6/Contents.json new file mode 100644 index 0000000..6e96565 --- /dev/null +++ b/Moco/Assets.xcassets/Story/Content/Story1/Ep3/Page6/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/Moco/Assets.xcassets/Story/Content/Story1/Ep3/Page6/background.imageset/Contents.json b/Moco/Assets.xcassets/Story/Content/Story1/Ep3/Page6/background.imageset/Contents.json new file mode 100644 index 0000000..e05c069 --- /dev/null +++ b/Moco/Assets.xcassets/Story/Content/Story1/Ep3/Page6/background.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Page 62.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Page 63.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Page 64.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Moco/Assets.xcassets/Story/Content/Story1/Ep3/Page6/background.imageset/Page 62.png b/Moco/Assets.xcassets/Story/Content/Story1/Ep3/Page6/background.imageset/Page 62.png new file mode 100644 index 0000000..29917b4 Binary files /dev/null and b/Moco/Assets.xcassets/Story/Content/Story1/Ep3/Page6/background.imageset/Page 62.png differ diff --git a/Moco/Assets.xcassets/Story/Content/Story1/Ep3/Page6/background.imageset/Page 63.png b/Moco/Assets.xcassets/Story/Content/Story1/Ep3/Page6/background.imageset/Page 63.png new file mode 100644 index 0000000..29917b4 Binary files /dev/null and b/Moco/Assets.xcassets/Story/Content/Story1/Ep3/Page6/background.imageset/Page 63.png differ diff --git a/Moco/Assets.xcassets/Story/Content/Story1/Ep3/Page6/background.imageset/Page 64.png b/Moco/Assets.xcassets/Story/Content/Story1/Ep3/Page6/background.imageset/Page 64.png new file mode 100644 index 0000000..29917b4 Binary files /dev/null and b/Moco/Assets.xcassets/Story/Content/Story1/Ep3/Page6/background.imageset/Page 64.png differ diff --git a/Moco/Assets.xcassets/Story/Content/Story1/Ep3/Page7/Contents.json b/Moco/Assets.xcassets/Story/Content/Story1/Ep3/Page7/Contents.json new file mode 100644 index 0000000..6e96565 --- /dev/null +++ b/Moco/Assets.xcassets/Story/Content/Story1/Ep3/Page7/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/Moco/Assets.xcassets/Story/Content/Story1/Ep3/Page7/background.imageset/Contents.json b/Moco/Assets.xcassets/Story/Content/Story1/Ep3/Page7/background.imageset/Contents.json new file mode 100644 index 0000000..6379d35 --- /dev/null +++ b/Moco/Assets.xcassets/Story/Content/Story1/Ep3/Page7/background.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Page 66.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Moco/Assets.xcassets/Story/Content/Story1/Ep3/Page7/background.imageset/Page 66.png b/Moco/Assets.xcassets/Story/Content/Story1/Ep3/Page7/background.imageset/Page 66.png new file mode 100644 index 0000000..39e4955 Binary files /dev/null and b/Moco/Assets.xcassets/Story/Content/Story1/Ep3/Page7/background.imageset/Page 66.png differ diff --git a/Moco/Assets.xcassets/Story/done-icon.imageset/Contents.json b/Moco/Assets.xcassets/Story/done-icon.imageset/Contents.json new file mode 100644 index 0000000..4274619 --- /dev/null +++ b/Moco/Assets.xcassets/Story/done-icon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "done-icon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "done-icon 1.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "done-icon 2.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Moco/Assets.xcassets/Story/done-icon.imageset/done-icon 1.png b/Moco/Assets.xcassets/Story/done-icon.imageset/done-icon 1.png new file mode 100644 index 0000000..963d882 Binary files /dev/null and b/Moco/Assets.xcassets/Story/done-icon.imageset/done-icon 1.png differ diff --git a/Moco/Assets.xcassets/Story/done-icon.imageset/done-icon 2.png b/Moco/Assets.xcassets/Story/done-icon.imageset/done-icon 2.png new file mode 100644 index 0000000..963d882 Binary files /dev/null and b/Moco/Assets.xcassets/Story/done-icon.imageset/done-icon 2.png differ diff --git a/Moco/Assets.xcassets/Story/done-icon.imageset/done-icon.png b/Moco/Assets.xcassets/Story/done-icon.imageset/done-icon.png new file mode 100644 index 0000000..963d882 Binary files /dev/null and b/Moco/Assets.xcassets/Story/done-icon.imageset/done-icon.png differ diff --git a/Moco/Assets.xcassets/Story/episode-list-locked.imageset/Contents.json b/Moco/Assets.xcassets/Story/episode-list-locked.imageset/Contents.json new file mode 100644 index 0000000..df9f26d --- /dev/null +++ b/Moco/Assets.xcassets/Story/episode-list-locked.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "MOCO Front (Page 1).png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Moco/Assets.xcassets/Story/episode-list-locked.imageset/MOCO Front (Page 1).png b/Moco/Assets.xcassets/Story/episode-list-locked.imageset/MOCO Front (Page 1).png new file mode 100644 index 0000000..f0bb722 Binary files /dev/null and b/Moco/Assets.xcassets/Story/episode-list-locked.imageset/MOCO Front (Page 1).png differ diff --git a/Moco/Assets/Audio/Story/Story1/Ep4/MakeSentence/Apa yang akan dilakukan bebe selanjutnya.m4a b/Moco/Assets/Audio/Story/Story1/Ep4/MakeSentence/Apa yang akan dilakukan bebe selanjutnya.m4a index 94a87a8..3e15867 100644 Binary files a/Moco/Assets/Audio/Story/Story1/Ep4/MakeSentence/Apa yang akan dilakukan bebe selanjutnya.m4a and b/Moco/Assets/Audio/Story/Story1/Ep4/MakeSentence/Apa yang akan dilakukan bebe selanjutnya.m4a differ diff --git a/Moco/Assets/Audio/Story/Story1/Ep4/MakeSentence/Susunlah sebuah kalimat.m4a b/Moco/Assets/Audio/Story/Story1/Ep4/MakeSentence/Susunlah sebuah kalimat.m4a index f49c19c..40c8437 100644 Binary files a/Moco/Assets/Audio/Story/Story1/Ep4/MakeSentence/Susunlah sebuah kalimat.m4a and b/Moco/Assets/Audio/Story/Story1/Ep4/MakeSentence/Susunlah sebuah kalimat.m4a differ diff --git a/Moco/Extension/EnvironmentValue.swift b/Moco/Extension/EnvironmentValue.swift index d93afe4..0cff720 100644 --- a/Moco/Extension/EnvironmentValue.swift +++ b/Moco/Extension/EnvironmentValue.swift @@ -21,6 +21,11 @@ extension EnvironmentValues { set { self[TimerViewModelKey.self] = newValue } } + var userViewModel: UserViewModel { + get { self[UserViewModelKey.self] } + set { self[UserViewModelKey.self] = newValue } + } + var storyThemeViewModel: StoryThemeViewModel { get { self[StoryThemeViewModelKey.self] } set { self[StoryThemeViewModelKey.self] = newValue } @@ -60,6 +65,11 @@ extension EnvironmentValues { get { self[MazePromptViewModelKey.self] } set { self[MazePromptViewModelKey.self] = newValue } } + + var gameKitViewModel: GameKitViewModel { + get { self[GameKitViewModelKey.self] } + set { self[GameKitViewModelKey.self] = newValue } + } } // MARK: - View Model Keys @@ -72,6 +82,10 @@ private struct TimerViewModelKey: EnvironmentKey { static var defaultValue: TimerViewModel = .init() } +private struct UserViewModelKey: EnvironmentKey { + static var defaultValue: UserViewModel = .init() +} + private struct StoryThemeViewModelKey: EnvironmentKey { static var defaultValue: StoryThemeViewModel = .init() } @@ -103,3 +117,7 @@ private struct SettingsViewModelKey: EnvironmentKey { private struct MazePromptViewModelKey: EnvironmentKey { static var defaultValue: MazePromptViewModel = .init() } + +private struct GameKitViewModelKey: EnvironmentKey { + static var defaultValue: GameKitViewModel = .init() +} diff --git a/Moco/Extension/PopUpExtension.swift b/Moco/Extension/PopUpExtension.swift index 80b37e9..e87f858 100644 --- a/Moco/Extension/PopUpExtension.swift +++ b/Moco/Extension/PopUpExtension.swift @@ -43,7 +43,7 @@ public extension View { textColor: Color.blue2Txt, overlayOpacity: overlayOpacity, isLarge: isLarge, - width: width ?? Screen.width * 0.32, + width: width ?? Screen.width * (UIDevice.isIPad ? 0.32 : 0.4), height: height ?? Screen.height * 0.3, type: type, disableCancel: disableCancel, diff --git a/Moco/Extension/View.swift b/Moco/Extension/View.swift index ee43e27..e26553e 100644 --- a/Moco/Extension/View.swift +++ b/Moco/Extension/View.swift @@ -68,6 +68,7 @@ extension View { struct ForceRotationViewModifier: ViewModifier { var orientation: UIInterfaceOrientationMask? + var resetOrientation = true func body(content: Content) -> some View { content @@ -75,13 +76,15 @@ struct ForceRotationViewModifier: ViewModifier { AppDelegate.orientationLock = orientation ?? (Screen.orientation == .landscapeLeft ? .landscapeLeft : .landscapeRight) } .onDisappear { - AppDelegate.orientationLock = nil + if resetOrientation { + AppDelegate.orientationLock = nil + } } } } extension View { - func forceRotation(_ orientation: UIInterfaceOrientationMask? = nil) -> some View { - modifier(ForceRotationViewModifier(orientation: orientation)) + func forceRotation(_ orientation: UIInterfaceOrientationMask? = nil, resetOrientation: Bool? = true) -> some View { + modifier(ForceRotationViewModifier(orientation: orientation, resetOrientation: resetOrientation ?? true)) } } diff --git a/Moco/MocoApp.swift b/Moco/MocoApp.swift index 19855bf..6ac7dff 100644 --- a/Moco/MocoApp.swift +++ b/Moco/MocoApp.swift @@ -26,14 +26,16 @@ struct MocoApp: App { @State private var storyContentViewModel = StoryContentViewModel.shared @State private var promptViewModel = PromptViewModel.shared @State private var hintViewModel = HintViewModel.shared + @State private var userViewModel = UserViewModel.shared @State private var settingsViewModel = SettingsViewModel.shared @State private var mazePromptViewModel = MazePromptViewModel.shared + @State private var gameKitViewModel = GameKitViewModel.shared // MARK: - State Objects @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) @@ -45,6 +47,7 @@ struct MocoApp: App { ContentViewContainer() }.environment(\.navigate, routeViewModel) .environment(\.font, Font.custom("CherryBomb-Regular", size: 24, relativeTo: .body)) + .environment(\.userViewModel, userViewModel) .environment(\.storyThemeViewModel, storyThemeViewModel) .environment(\.episodeViewModel, episodeViewModel) .environment(\.storyViewModel, storyViewModel) diff --git a/Moco/Model/MazeModel.swift b/Moco/Model/MazeModel.swift index 29db9ff..77eecf9 100644 --- a/Moco/Model/MazeModel.swift +++ b/Moco/Model/MazeModel.swift @@ -5,7 +5,7 @@ // Created by Aaron Christopher Tanhar on 26/10/23. // -import Foundation +import SwiftUI enum MoveDirection: Equatable { case left @@ -30,7 +30,7 @@ struct MazeModel { private(set) var startPoint = LocationPoint() private(set) var exitPoints = [LocationPoint()] - static var mapSize = MapSize(width: 25, height: 13) + static var mapSize = UIDevice.isIPad ? MapSize(width: 25, height: 13) : MapSize(width: 25, height: 9) var arrayPoint: [[Int]] = [[]] 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/Model/ModelData/Stories/Story1.swift b/Moco/Model/ModelData/Stories/Story1.swift index 1011bb8..9dc1b4c 100644 --- a/Moco/Model/ModelData/Stories/Story1.swift +++ b/Moco/Model/ModelData/Stories/Story1.swift @@ -8,7 +8,7 @@ import Foundation private let episode1 = EpisodeModel( - pictureName: "", + pictureName: "Story/Content/Story1/Pages/Page1/background", stories: [ // Story page-1 StoryModel( @@ -409,7 +409,7 @@ private let episode2 = EpisodeModel( ] ) ], - isAvailable: true + isAvailable: false ) private let episode3 = EpisodeModel( @@ -417,7 +417,7 @@ private let episode3 = EpisodeModel( stories: [ // Story page-1 StoryModel( - background: "", + background: "Story/Content/Story1/Ep3/Page1/background", pageNumber: 1, isHavePrompt: true, prompts: [ @@ -452,7 +452,7 @@ private let episode3 = EpisodeModel( fontSize: 0 ) ], - earlyPrompt: true + earlyPrompt: false ), // Story page-2 StoryModel( @@ -610,10 +610,42 @@ private let episode3 = EpisodeModel( ), // Story page-4 StoryModel( - background: "Story/Content/Story1/Pages/Page6/background", + background: "Story/Content/Story1/Ep3/Page6/background", pageNumber: 6, + isHavePrompt: true, + prompts: [ + PromptModel( + correctAnswer: "luna", + startTime: 0, + promptType: PromptType.card, + hints: nil, + question: """ + Siapakah seekor ular dewasa yang cerdik? + """, + imageCard: "", + cardLocationX: 0.4, + cardLocationY: 0.7, + cardType: CardType.character + ) + ], + storyContents: [ + StoryContentModel( + duration: 0, + contentName: "bg-story", + contentType: StoryContentType.audio, + positionX: 0, + positionY: 0, + maxWidth: 0, + color: "", + fontSize: 0 + ) + ], + earlyPrompt: true + ), + StoryModel( + background: "Story/Content/Story1/Ep3/Page7/background", + pageNumber: 7, isHavePrompt: false, - prompts: nil, storyContents: [ StoryContentModel( duration: 0, @@ -628,7 +660,7 @@ private let episode3 = EpisodeModel( ] ) ], - isAvailable: true + isAvailable: false ) private let episode4 = EpisodeModel( @@ -730,7 +762,7 @@ private let episode4 = EpisodeModel( earlyPrompt: true ) ], - isAvailable: true + isAvailable: false ) struct Story1: StoryProtocol { diff --git a/Moco/Model/StoryThemeModel.swift b/Moco/Model/StoryThemeModel.swift index f678a0d..613fb33 100644 --- a/Moco/Model/StoryThemeModel.swift +++ b/Moco/Model/StoryThemeModel.swift @@ -17,6 +17,7 @@ final class StoryThemeModel: Identifiable, CustomPersistentModel { @Attribute var updatedAt = Date() var episodes: [EpisodeModel]? + var users: [UserModel]? init(pictureName: String, episodes: [EpisodeModel]?, slug: String = "") { uid = UUID().uuidString diff --git a/Moco/Model/UserModel.swift b/Moco/Model/UserModel.swift new file mode 100644 index 0000000..34a16b4 --- /dev/null +++ b/Moco/Model/UserModel.swift @@ -0,0 +1,31 @@ +// +// UserModel.swift +// Moco +// +// Created by Nur Azizah on 24/11/23. +// + +import Foundation +import SwiftData + +@Model +final class UserModel: Identifiable, CustomPersistentModel { + @Attribute var uid: String = "" + @Attribute var slug: String = "" + @Attribute var availableStoryThemeSum: Int = 1 + @Attribute var availableEpisodeSum: Int = 0 + @Attribute var clearedEpisodeSum: Int = 0 + @Attribute var createdAt = Date() + @Attribute var updatedAt = Date() + + var storyThemes: [StoryThemeModel]? + + init(availableStoryThemeSum: Int, availableEpisodeSum: Int, slug: String = "") { + uid = UUID().uuidString + self.slug = slug + self.availableStoryThemeSum = availableStoryThemeSum + self.availableEpisodeSum = availableEpisodeSum + createdAt = Date() + updatedAt = Date() + } +} diff --git a/Moco/View/Components/3D/3DRenderer.swift b/Moco/View/Components/3D/3DRenderer.swift index 72165af..eddbc63 100644 --- a/Moco/View/Components/3D/3DRenderer.swift +++ b/Moco/View/Components/3D/3DRenderer.swift @@ -6,51 +6,38 @@ // import SceneKit -import SceneKit.ModelIO import SwiftUI struct SceneKitView: UIViewRepresentable { func makeUIView(context _: Context) -> SCNView { - guard let urlPath = Bundle.main.url(forResource: "Floating_Lighthouse", withExtension: "usdz") else { - fatalError("usdz not found") - } - let mdlAsset = MDLAsset(url: urlPath) - // you can load the textures on an MDAsset so it's not white - mdlAsset.loadTextures() - -// let asset = mdlAsset.object(at: 0) // extract first object - // let assetNode = SCNNode(mdlObject: asset) + guard let sceneUrl = Bundle.main.url(forResource: "Floating_Lighthouse", withExtension: "usdz") else { fatalError() } let scnView = SCNView() - scnView.backgroundColor = UIColor.clear - scnView.scene = SCNScene(mdlAsset: mdlAsset) - - let cameraNode = SCNNode() - cameraNode.camera = SCNCamera() - cameraNode.position = SCNVector3(x: 0, y: 3.5, z: 15) - - scnView.scene?.rootNode.addChildNode(cameraNode) - - scnView.debugOptions = .showWorldOrigin + do { + scnView.scene = try SCNScene(url: sceneUrl, options: [.checkConsistency: true]) + } catch { + fatalError("Failed to load SCNScene") + } + scnView.backgroundColor = .clear scnView.allowsCameraControl = true scnView.autoenablesDefaultLighting = true scnView.isTemporalAntialiasingEnabled = true - -// scnView.cameraControlConfiguration.autoSwitchToFreeCamera = false + scnView.antialiasingMode = .multisampling4X let camera = scnView.defaultCameraController let cameraConfig = scnView.cameraControlConfiguration - camera.pointOfView?.look(at: SCNVector3(x: 0, y: 0, z: 0)) camera.maximumVerticalAngle = 50 - camera.minimumVerticalAngle = 20 + camera.minimumVerticalAngle = 10 camera.interactionMode = .orbitTurntable cameraConfig.rotationSensitivity = 0.3 cameraConfig.panSensitivity = 0.3 + camera.pointOfView?.worldPosition = SCNVector3(x: 0, y: 0, z: 30) + if let recognizers = scnView.gestureRecognizers { for gestureRecognizer in recognizers { if let gesture = gestureRecognizer as? UIPanGestureRecognizer { @@ -121,14 +108,14 @@ struct ThreeDRenderer: View { HStack { Spacer() Text("To be continued...") - .customFont(.cherryBomb, size: 30) + .customFont(.cherryBomb, size: UIDevice.isIPad ? 30 : 20) .foregroundColor(.blue2Txt) .glowBorder(color: .white, lineWidth: 5) Button("Keluar") { navigate.popToRoot() action() } - .buttonStyle(MainButton(width: 180, type: .danger)) + .buttonStyle(MainButton(width: UIDevice.isIPad ? 180 : 100, type: .danger)) .padding(.bottom, 20) .modifier(ShakeEffect(animatableData: shakeAnimation)) } @@ -184,7 +171,7 @@ struct ThreeDRendererOld: View { } } .task { - scene = SCNScene(mdlAsset: mdlAsset) +// scene = SCNScene(mdlAsset: mdlAsset) } } } diff --git a/Moco/View/Components/BurgerMenu.swift b/Moco/View/Components/BurgerMenu.swift index eaff847..382c386 100644 --- a/Moco/View/Components/BurgerMenu.swift +++ b/Moco/View/Components/BurgerMenu.swift @@ -13,6 +13,14 @@ struct BurgerMenu: View { @State private var expand = false @State var isGameCenterAchievementsPresented = false + var size: CGFloat { + UIDevice.isIPad ? 90 : 60 + } + + var expandedSize: CGFloat { + UIDevice.isIPad ? 50 : 35 + } + var body: some View { HStack(spacing: 0) { Spacer() @@ -24,7 +32,7 @@ struct BurgerMenu: View { } label: { Image("Story/Icons/achievements") .resizable() - .frame(width: 90, height: 90) + .frame(width: size, height: size) .shadow(radius: 4, x: -2, y: 2) .onTapGesture { isGameCenterAchievementsPresented = true @@ -38,12 +46,12 @@ struct BurgerMenu: View { } label: { Image("Story/Icons/settings") .resizable() - .frame(width: 90, height: 90) + .frame(width: size, height: size) .shadow(radius: 4, x: -2, y: 2) } - }.padding(.vertical, 18) - .padding(.leading, 20) - .padding(.trailing, 10) + }.padding(.vertical, UIDevice.isIPad ? 18 : 12) + .padding(.leading, UIDevice.isIPad ? 20 : 15) + .padding(.trailing, UIDevice.isIPad ? 10 : 7) } SfxButton { withAnimation(.spring()) { @@ -52,13 +60,16 @@ struct BurgerMenu: View { } label: { Image(expand ? "Story/Icons/burger-menu-opened" : "Story/Icons/burger-menu") .resizable() - .frame(width: expand ? 50 : 90, height: expand ? 50 : 90) + .frame( + width: expand ? expandedSize : size, + height: expand ? expandedSize : size + ) .shadow(radius: 4, x: -2, y: 2) } - .padding(.trailing, 20) + .padding(.trailing, UIDevice.isIPad ? 20 : 10) } .background(expand ? .white : .clear) - .cornerRadius(38) + .cornerRadius(UIDevice.isIPad ? 38 : 24) } } } diff --git a/Moco/View/Components/Buttons.swift b/Moco/View/Components/Buttons.swift index 4edee8b..fa84782 100644 --- a/Moco/View/Components/Buttons.swift +++ b/Moco/View/Components/Buttons.swift @@ -56,7 +56,7 @@ struct MainButton: ButtonStyle { var buttonColor = Color.redBtn var cornerRadius: CGFloat = 8 var type = MainButtonType.success - var fontSize = CGFloat(20) + var fontSize = UIDevice.isIPad ? CGFloat(20) : CGFloat(16) func makeBody(configuration: Configuration) -> some View { configuration.label .padding() diff --git a/Moco/View/Components/Maze/MazeProgress.swift b/Moco/View/Components/Maze/MazeProgress.swift index 4ed3d41..937f033 100644 --- a/Moco/View/Components/Maze/MazeProgress.swift +++ b/Moco/View/Components/Maze/MazeProgress.swift @@ -24,7 +24,7 @@ struct MazeProgress: View { }.frame(width: Screen.width * 0.08, height: Screen.height * 0.1) }.frame(width: Screen.width * 0.08, height: Screen.height * 0.1) Text("\(Int(mazePromptViewModel.progress * 100))%") - .customFont(.cherryBomb, size: 40) + .customFont(.cherryBomb, size: UIDevice.isIPad ? 40 : 25) .foregroundColor(.text.darkBlue) }.padding() } diff --git a/Moco/View/Components/MocoIcon.swift b/Moco/View/Components/MocoIcon.swift new file mode 100644 index 0000000..00f3ffc --- /dev/null +++ b/Moco/View/Components/MocoIcon.swift @@ -0,0 +1,26 @@ +// +// MocoIcon.swift +// Moco +// +// Created by Aaron Christopher Tanhar on 26/11/23. +// + +import SwiftUI + +struct MocoIcon: View { + var width: CGFloat { + UIDevice.isIPad ? 0.4 * Screen.width : 0.3 * Screen.width + } + + var body: some View { + Image("Story/nav-icon") + .resizable() + .scaledToFit() + .frame(width: width) + .padding(.top, Screen.height * 0.02) + } +} + +#Preview { + MocoIcon() +} diff --git a/Moco/View/Components/PauseMenu.swift b/Moco/View/Components/PauseMenu.swift index bb8e050..5ee5756 100644 --- a/Moco/View/Components/PauseMenu.swift +++ b/Moco/View/Components/PauseMenu.swift @@ -35,6 +35,18 @@ struct PauseMenu: View { var repeatHandler: (() -> Void)? var cancelHandler: (() -> Void)? + var buttonWidth: CGFloat { + UIDevice.isIPad ? 250 : 125 + } + + var buttonHeight: CGFloat { + UIDevice.isIPad ? 120 : 60 + } + + var buttonFontSize: CGFloat { + UIDevice.isIPad ? 30 : 15 + } + var body: some View { ZStack { Color(.black) @@ -80,12 +92,12 @@ struct PauseMenu: View { repeatHandler?() close() } - .buttonStyle(MainButton(width: 250, height: 120, type: .danger, fontSize: CGFloat(30))) + .buttonStyle(MainButton(width: buttonWidth, height: buttonHeight, type: .danger, fontSize: buttonFontSize)) SfxButton(confirmText) { function() close() } - .buttonStyle(MainButton(width: 250, height: 120, type: .success, fontSize: CGFloat(30))) + .buttonStyle(MainButton(width: buttonWidth, height: buttonHeight, type: .success, fontSize: buttonFontSize)) } if bottomImage != nil { @@ -102,7 +114,10 @@ struct PauseMenu: View { SfxButton { close() } label: { - Image("Buttons/button-x").resizable().frame(width: 80, height: 80).shadow(radius: 20, x: -20, y: 20) + Image("Buttons/button-x") + .resizable() + .frame(width: UIDevice.isIPad ? 80 : 50, height: UIDevice.isIPad ? 80 : 50) + .shadow(radius: 20, x: -20, y: 20) } } .offset(x: 0, y: offset) diff --git a/Moco/View/Components/PopUpComponent.swift b/Moco/View/Components/PopUpComponent.swift index e499e46..8bb3e26 100644 --- a/Moco/View/Components/PopUpComponent.swift +++ b/Moco/View/Components/PopUpComponent.swift @@ -128,13 +128,13 @@ struct PopUpComponentView: View { Spacer() Text(title) - .customFont(.cherryBomb, size: 32) + .customFont(.cherryBomb, size: UIDevice.isIPad ? 32 : 24) .fontWeight(.heavy) .foregroundColor(textColor) .glowBorder(color: .white, lineWidth: 5) .padding(.top, 10) .padding(.bottom, 20) - .padding(.horizontal, 70) + .padding(.horizontal, 10) .multilineTextAlignment(.center) .fixedSize(horizontal: false, vertical: true) @@ -184,7 +184,10 @@ struct PopUpComponentView: View { } label: { Image("Buttons/button-x") .resizable() - .frame(width: 50, height: 50) + .frame( + width: UIDevice.isIPad ? 50 : 40, + height: UIDevice.isIPad ? 50 : 40 + ) .shadow(radius: 20, x: -20, y: 20) } } @@ -288,7 +291,8 @@ struct PopUpComponentViewOld: View { .cornerRadius(10) VStack { - Text(title ?? "Congratulations").customFont(.cherryBomb, size: 22) + Text(title ?? "Congratulations") + .customFont(.cherryBomb, size: 22) .foregroundColor(textColor) .padding(.top, 10) .padding(.bottom, 20) diff --git a/Moco/View/Components/Prompts/ARStory/ARStory.swift b/Moco/View/Components/Prompts/ARStory/ARStory.swift index 5d41b8b..908cefb 100644 --- a/Moco/View/Components/Prompts/ARStory/ARStory.swift +++ b/Moco/View/Components/Prompts/ARStory/ARStory.swift @@ -24,6 +24,7 @@ struct ARStory: View { } @State private var isStoryDone: Bool = false + @State private var isMakeSentence: Bool = false var prompt: PromptModel = .init( correctAnswer: "honey_jar", // object to be found @@ -67,7 +68,7 @@ struct ARStory: View { var body: some View { ZStack { if isTutorialFinished { - if !isStoryDone { + if !isStoryDone && !showLastIsland && !isMakeSentence { ARCameraView( clue: prompt, lastPrompt: lastPrompt, @@ -87,6 +88,8 @@ struct ARStory: View { isStoryDone = true if !lastPrompt { doneHandler?() + } else { + isMakeSentence = true } } ) @@ -114,10 +117,14 @@ struct ARStory: View { } } } - } else if lastPrompt && !showLastIsland { + } else if lastPrompt && !showLastIsland && isMakeSentence { MakeSentence { showLastIsland = true } + .onAppear { + arViewModel.pause() + arViewModel.resetSession() + } } else if showLastIsland { ThreeDRenderer { doneHandler?() @@ -151,6 +158,13 @@ struct ARStory: View { } .ignoresSafeArea() .frame(width: Screen.width, height: Screen.height) + .onDisappear { + arViewModel.pause() + arViewModel.resetSession() + } + .onAppear { + isStoryDone = false + } .task { isTutorialFinished = arViewModel.isTutorialDone } diff --git a/Moco/View/Components/Prompts/ARStory/ARView.swift b/Moco/View/Components/Prompts/ARStory/ARView.swift index 676f399..ab36d4c 100644 --- a/Moco/View/Components/Prompts/ARStory/ARView.swift +++ b/Moco/View/Components/Prompts/ARStory/ARView.swift @@ -25,27 +25,32 @@ struct ARCameraView: View { @State var isEndTheStoryPopupActive = false @State var isLastNarrativePopupActive = false + var hintButtonSize: CGFloat { + UIDevice.isIPad ? 160 : 90 + } + var body: some View { ZStack { - ARViewContainer(isShowHint: $isShowHint, meshes: clue.answerAssets ?? nil).edgesIgnoringSafeArea(.all) + ARViewContainer(isShowHint: $isShowHint, meshes: clue.answerAssets ?? nil) + .edgesIgnoringSafeArea(.all) // Overlay above the camera VStack { ZStack { Image("Components/modal-base").resizable().scaledToFill() - .padding(80) - .position(x: Screen.width / 2, y: 70.0) + .padding(UIDevice.isIPad ? 80 : 40) + .position(x: Screen.width / 2, y: UIDevice.isIPad ? 70.0 : 30) Text(clue.question!) - .customFont(.didactGothic, size: 30) + .customFont(.didactGothic, size: UIDevice.isIPad ? 30 : 15) .foregroundColor(.blue2Txt) .glowBorder(color: .white, lineWidth: 5) .padding(.horizontal, 120) - .padding(.top, 120) + .padding(.top, UIDevice.isIPad ? 120 : 60) .multilineTextAlignment(.center) .fixedSize(horizontal: false, vertical: true) } - .frame(height: 150) + .frame(height: UIDevice.isIPad ? 150 : 50) Spacer() HStack { @@ -62,13 +67,13 @@ struct ARCameraView: View { } .buttonStyle( CircleButton( - width: 160, - height: 160, + width: hintButtonSize, + height: hintButtonSize, backgroundColor: .clear, foregroundColor: .clear ) ) - .padding(50) + .padding(UIDevice.isIPad ? 50 : 20) .onAppear { withAnimation(Animation.easeIn(duration: 1.5)) { fadeInHintButton.toggle() @@ -119,7 +124,8 @@ struct ARCameraView: View { disableCancel: true ) { isLastNarrativePopupActive = false - isEndTheStoryPopupActive = true +// isEndTheStoryPopupActive = true + onFoundObject() onEnd() } .popUp( diff --git a/Moco/View/Components/Prompts/ARStory/MakeSentence.swift b/Moco/View/Components/Prompts/ARStory/MakeSentence.swift index 775ff49..d8b3087 100644 --- a/Moco/View/Components/Prompts/ARStory/MakeSentence.swift +++ b/Moco/View/Components/Prompts/ARStory/MakeSentence.swift @@ -92,6 +92,9 @@ struct MakeSentence: View { $0.fromBase64() ?? "" } + print(scanResult.joined(separator: " ") + .trimmingCharacters(in: .whitespacesAndNewlines)) + if scanResult.joined(separator: " ") .trimmingCharacters(in: .whitespacesAndNewlines) != prompts[currentPromptIndex].correctAnswer { showWrongAnswerPopup = true @@ -113,6 +116,7 @@ struct MakeSentence: View { } } .onAppear { + print("cukurukuk") currentPromptIndex = 0 DispatchQueue.main.asyncAfter(deadline: .now() + 1) { diff --git a/Moco/View/Components/Prompts/Card/CardPrompt.swift b/Moco/View/Components/Prompts/Card/CardPrompt.swift index 008249b..57853fc 100644 --- a/Moco/View/Components/Prompts/Card/CardPrompt.swift +++ b/Moco/View/Components/Prompts/Card/CardPrompt.swift @@ -59,7 +59,7 @@ struct CardPrompt: View { let promptContent = cardQuestions[currentCard] VStack { Text(promptContent.text) - .customFont(.didactGothic, size: 40) + .customFont(.didactGothic, size: UIDevice.isIPad ? 40 : 25) } .position( CGPoint( @@ -88,14 +88,14 @@ struct CardPrompt: View { showWrongAnswerPopup = true return } - + currentCard += 1 if let prompts = promptViewModel.prompts, currentCard >= prompts.count { showNext = true onComplete?() } - + DispatchQueue.global().async { audioViewModel.playSound( soundFileName: "bagus_berhasil_scan", @@ -103,7 +103,6 @@ struct CardPrompt: View { category: .narration ) } - } } } diff --git a/Moco/View/Components/Prompts/Card/CardScan.swift b/Moco/View/Components/Prompts/Card/CardScan.swift index a63a1fd..b7ab97a 100644 --- a/Moco/View/Components/Prompts/Card/CardScan.swift +++ b/Moco/View/Components/Prompts/Card/CardScan.swift @@ -5,16 +5,75 @@ // Created by Aaron Christopher Tanhar on 14/11/23. // +import AVFoundation import CodeScanner import SwiftUI +enum CameraMode { + case front + case back + + mutating func toggle() { + switch self { + case .back: + self = .front + case .front: + self = .back + } + } +} + struct CardScan: View { @Environment(\.audioViewModel) private var audioViewModel + @State private var cameraMode = CameraMode.back + @State private var internalResultCount = 0 + @State private var stopInstruction = false @Binding var scanResult: [String] + var resultCount = 1 + var onComplete: (() -> Void)? + var captureDevice: AVCaptureDevice? { + do { + let captureDevice = AVCaptureDevice.default(cameraMode == .back ? + .builtInTripleCamera : + .builtInWideAngleCamera, + for: .video, + position: (cameraMode == .back) ? .back : .front) + try captureDevice?.lockForConfiguration() + if let autoFocusRangeSupported = captureDevice?.isAutoFocusRangeRestrictionSupported, autoFocusRangeSupported { + captureDevice?.autoFocusRangeRestriction = .near + } + if let isFocusModeSupported = captureDevice?.isFocusModeSupported(.continuousAutoFocus), isFocusModeSupported { + captureDevice?.focusMode = .continuousAutoFocus + } + + return captureDevice + } catch { + return nil + } + } + + var capturedAnswer: String { + scanResult.map { $0.fromBase64() ?? "" }.joined(separator: " ").capitalized + } + + func playInstruction() { + // because the arahkan_kamera audio is 5 seconds, we need to do 7 + 5 + guard !stopInstruction else { return } + DispatchQueue.main.asyncAfter(deadline: .now() + 12) { + guard !stopInstruction else { return } + audioViewModel.playSound( + soundFileName: "arahkan_kamera", + type: .m4a, + category: .narration + ) + playInstruction() + } + } + var body: some View { ZStack { Color.bg.blue @@ -31,27 +90,77 @@ struct CardScan: View { ) .frame(height: Screen.height * 0.2) } + if resultCount > 1 && scanResult.count > 0 { + Text("Jawaban Kamu: \(capturedAnswer)") + HStack(spacing: 40) { + Button("Ulangi") { + scanResult = [] + internalResultCount = 0 + } + .buttonStyle(MainButton(width: 180, type: .danger)) + .font(.footnote) + if internalResultCount >= resultCount { + Button("Lanjut") { + onComplete?() + } + .buttonStyle(MainButton(width: 180, type: .success)) + .font(.footnote) + } + } + } } VStack { - CodeScannerView( - codeTypes: [.qr], - scanMode: .oncePerCode, - completion: { result in - if case let .success(code) = result { - scanResult.append(code.string) - onComplete?() + ZStack { + CodeScannerView( + codeTypes: [.qr], + scanMode: .oncePerCode, + videoCaptureDevice: captureDevice, + completion: { result in + if case let .success(code) = result { + internalResultCount += 1 + print(code.string) + print(scanResult) + scanResult.append(code.string) + if resultCount == 1 { + onComplete?() + } + } + } + ).id(cameraMode) + VStack { + Spacer() + HStack { + Spacer() + SfxButton { + cameraMode.toggle() + } label: { + Image("Buttons/button-switch-camera").resizable().scaledToFit() + }.buttonStyle( + CircleButton( + width: 80, + height: 80, + backgroundColor: .clear, + foregroundColor: .clear + ) + ).padding() } } - ) + } } } }.task { + stopInstruction = false scanResult = [] + internalResultCount = 0 audioViewModel.playSound( soundFileName: "arahkan_kamera", type: .m4a, category: .narration ) + playInstruction() + } + .onDisappear { + stopInstruction = true } } } diff --git a/Moco/View/Components/Prompts/Card/CardView.swift b/Moco/View/Components/Prompts/Card/CardView.swift index 4e6b886..21553b8 100644 --- a/Moco/View/Components/Prompts/Card/CardView.swift +++ b/Moco/View/Components/Prompts/Card/CardView.swift @@ -53,22 +53,25 @@ struct CardView: View { VStack { switch state { case .active: - VStack { - GeometryReader { proxy in - let frame = proxy.frame(in: .local) - Image(getActiveCard()) - .resizable() - .scaledToFit() - .onTapGesture { - onTap?() - } - .scaleEffect(minScale: 1.0, maxScale: 1.05) - .pointer( - position: CGPoint(x: frame.midX, y: frame.midY), - isShowing: showPointer - ) + Image(getActiveCard()) + .resizable() + .scaledToFit() + .overlay { + GeometryReader { proxy in + let frame = proxy.frame(in: .local) + Image(getActiveCard()) + .resizable() + .scaledToFit() + .onTapGesture { + onTap?() + } + .scaleEffect(minScale: 1.0, maxScale: 1.05) + .pointer( + position: CGPoint(x: frame.midX, y: frame.midY), + isShowing: showPointer + ) + } } - } case .inactive: Image("Story/Prompts/card-inactive") .resizable() diff --git a/Moco/View/Components/Prompts/Maze/MazePrompt.swift b/Moco/View/Components/Prompts/Maze/MazePrompt.swift index 6eb399f..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,9 +32,15 @@ struct MazePrompt: View { var action: () -> Void = {} + var onRestart: (() -> Void)? + func playInitialNarration() { if mazePromptViewModel.isTutorialDone { - audioViewModel.playSound(soundFileName: "013 (maze) - bantu arahkan Moco ke jawaban yang benar ya", type: .m4a, category: .narration) + audioViewModel.playSound( + soundFileName: "013 (maze) - bantu arahkan Moco ke jawaban yang benar ya", + type: .m4a, + category: .narration + ) } } @@ -45,11 +53,18 @@ 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) - .customFont(.didactGothic, size: 40) + .customFont(.didactGothic, size: UIDevice.isIPad ? 35 : 20) .foregroundColor(.text.brown) Spacer() MazeView( @@ -58,7 +73,7 @@ struct MazePrompt: View { correctAnswerAsset: correctAnswerAsset ) { action() - }.padding(.bottom, 20) + }.padding(.bottom, 10) .id(promptText) } .ignoresSafeArea() @@ -108,12 +123,24 @@ 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 } } - .forceRotation() + .forceRotation(resetOrientation: false) } } diff --git a/Moco/View/Components/StoryBookNew.swift b/Moco/View/Components/StoryBookNew.swift index c54a311..daae1c7 100644 --- a/Moco/View/Components/StoryBookNew.swift +++ b/Moco/View/Components/StoryBookNew.swift @@ -27,7 +27,7 @@ struct StoryBookNew: View { } var height: CGFloat { - Screen.height * 0.3 + UIDevice.isIPad ? Screen.height * 0.3 : Screen.height * 0.4 } var body: some View { @@ -71,8 +71,8 @@ struct StoryBookNew: View { Rectangle() .fill(.white) .frame( - width: width + 20, - height: height + 20 + width: width + (UIDevice.isIPad ? 20 : 15), + height: height + (UIDevice.isIPad ? 20 : 15) ) // Adjust the frame size as needed .clipShape( .rect( @@ -96,7 +96,7 @@ struct StoryBookNew: View { Image("Story/Cover/locked") .resizable() .scaledToFit() - .frame(width: 100) + .frame(width: UIDevice.isIPad ? 100 : 60) } } else { EmptyView() diff --git a/Moco/View/Components/StoryNavigationButton.swift b/Moco/View/Components/StoryNavigationButton.swift index 9647760..7865d64 100644 --- a/Moco/View/Components/StoryNavigationButton.swift +++ b/Moco/View/Components/StoryNavigationButton.swift @@ -13,8 +13,8 @@ enum Direction { } struct StoryNavigationButton: View { - var width = 140.0 - var height = 100.0 + var width = UIDevice.isIPad ? 140.0 : 70.0 + var height = UIDevice.isIPad ? 100.0 : 50 var direction: Direction var pressHandler: (() -> Void)? diff --git a/Moco/View/Components/TimerView.swift b/Moco/View/Components/TimerView.swift index d7a3356..3273399 100644 --- a/Moco/View/Components/TimerView.swift +++ b/Moco/View/Components/TimerView.swift @@ -77,7 +77,7 @@ struct TimerView: View { var body: some View { VStack { Text(formattedTimer) - .customFont(.cherryBomb, size: 35) + .customFont(.cherryBomb, size: UIDevice.isIPad ? 35 : 15) .foregroundColor(.text.darkBlue) .onReceive(timer) { _ in if self.isTimerRunning && durationInSeconds > 0 { diff --git a/Moco/View/User/EpisodeItem.swift b/Moco/View/User/EpisodeItem.swift index f2e20a9..1e9f5f3 100644 --- a/Moco/View/User/EpisodeItem.swift +++ b/Moco/View/User/EpisodeItem.swift @@ -8,38 +8,66 @@ import SwiftUI struct EpisodeItem: View { + @Environment(\.userViewModel) private var userViewModel + @Environment(\.episodeViewModel) private var episodeViewModel + var number = 1 - var fontSize = CGFloat(55) + var fontSize: CGFloat = UIDevice.isIPad ? 55 : 30 var width = CGFloat(Screen.width * 0.3) var height = CGFloat(Screen.height * 0.5) var onTap: (() -> Void)? + var isAvailable: Bool { + guard let episodes = episodeViewModel.episodes, let userLogin = userViewModel.userLogin else { + return false + } + + return episodes[number - 1].isAvailable || number - 1 < userLogin.availableEpisodeSum + } + var body: some View { GeometryReader { proxy in ZStack(alignment: .center) { - Image("Story/episode-list") + Image(isAvailable ? "Story/episode-list" : "Story/episode-list-locked") .resizable() .scaledToFit() - HStack { - VStack { - Text("Bagian") - .customFont(.cherryBomb, size: fontSize - 18) - .foregroundColor(.text.brown) - Text("\(number)") - .customFont(.cherryBomb, size: fontSize) - .foregroundColor(.text.brown) + if isAvailable { + HStack { + ZStack { + VStack { + Text("Bagian") + .customFont(.cherryBomb, size: fontSize - 18) + .foregroundColor(.text.brown) + Text("\(number)") + .customFont(.cherryBomb, size: fontSize) + .foregroundColor(.text.brown) + } + if number < userViewModel.userLogin!.availableEpisodeSum { + HStack { + Spacer() + VStack { + Spacer() + Image("Story/done-icon") + .resizable() + .scaledToFit() + .frame(width: UIDevice.isIPad ? 40 : 20) + .padding() + } + } + } + } } + .frame( + width: proxy.size.width * (UIDevice.isIPad ? 0.5 : 0.4), + height: proxy.size.height * 0.3 + ) + .offset( + x: proxy.size.width * 0.02, + y: proxy.size.height * 0.078 + ) } - .frame( - width: proxy.size.width * 0.5, - height: proxy.size.height * 0.3 - ) - .offset( - x: proxy.size.width * 0.02, - y: proxy.size.height * 0.078 - ) } .frame( width: proxy.size.width, @@ -55,4 +83,6 @@ struct EpisodeItem: View { #Preview { EpisodeItem() + .environment(\.userViewModel, UserViewModel.shared) + .environment(\.episodeViewModel, EpisodeViewModel.shared) } diff --git a/Moco/View/User/EpisodeView.swift b/Moco/View/User/EpisodeView.swift index 9c33893..fecf682 100644 --- a/Moco/View/User/EpisodeView.swift +++ b/Moco/View/User/EpisodeView.swift @@ -11,6 +11,7 @@ import SwiftUI struct EpisodeView: View { @Environment(\.audioViewModel) private var audioViewModel @Environment(\.storyThemeViewModel) private var storyThemeViewModel + @Environment(\.userViewModel) private var userViewModel @Environment(\.episodeViewModel) private var episodeViewModel @Environment(\.storyViewModel) private var storyViewModel @Environment(\.storyContentViewModel) private var storyContentViewModel @@ -18,6 +19,10 @@ struct EpisodeView: View { @Environment(\.hintViewModel) private var hintViewModel @Environment(\.navigate) private var navigate + var homeButtonSize: CGFloat { + UIDevice.isIPad ? 70 : 50 + } + var body: some View { ZStack { VStack { @@ -29,11 +34,7 @@ struct EpisodeView: View { VStack { HStack(alignment: .center) { - Image("Story/nav-icon") - .resizable() - .scaledToFit() - .frame(width: 0.4 * Screen.width) - .padding(.top, Screen.height * 0.02) + MocoIcon() Spacer() @@ -45,7 +46,7 @@ struct EpisodeView: View { HStack(spacing: 40) { Image("Buttons/button-home") .resizable() - .frame(width: 70, height: 70) + .frame(width: homeButtonSize, height: homeButtonSize) .shadow(radius: 4, x: -2, y: 2) .foregroundColor(.white) .onTapGesture { @@ -53,7 +54,7 @@ struct EpisodeView: View { } Text("Episode") - .customFont(.cherryBomb, size: 50) + .customFont(.cherryBomb, size: UIDevice.isIPad ? 50 : 30) .foregroundColor(Color.blueTxt) .fontWeight(.bold) } @@ -62,38 +63,52 @@ struct EpisodeView: View { }.padding(.leading, 60) .padding(.vertical, Screen.height * 0.1) - ScrollView(.horizontal, showsIndicators: false) { - LazyHGrid(rows: [GridItem(.flexible())]) { - if let availableEpisodes = episodeViewModel.availableEpisodes { - ForEach( - Array(availableEpisodes.enumerated()), id: \.element - ) { index, episode in - EpisodeItem( - number: index + 1 - ) { - Task { - episodeViewModel.setSelectedEpisode(episode) - - // open new story page - storyViewModel.fetchStory(0, episodeViewModel.selectedEpisode!) - storyContentViewModel.fetchStoryContents(storyViewModel.storyPage!) - - if storyViewModel.storyPage!.isHavePrompt { - promptViewModel.fetchPrompts(storyViewModel.storyPage!) - - if promptViewModel.prompts![0].hints != nil { - hintViewModel.fetchHints(promptViewModel.prompts![0]) + ScrollViewReader { proxy in + ScrollView(.horizontal, showsIndicators: false) { + LazyHGrid(rows: [GridItem(.flexible())]) { + if let episodes = episodeViewModel.episodes { + ForEach( + Array(episodes.enumerated()), id: \.element + ) { index, episode in + EpisodeItem( + number: index + 1 + ) { + if episode.isAvailable || index < userViewModel.userLogin!.availableEpisodeSum { + Task { + episodeViewModel.setSelectedEpisode(episode, index) + + // open new story page + storyViewModel.fetchStory(0, episodeViewModel.selectedEpisode!) + storyContentViewModel.fetchStoryContents(storyViewModel.storyPage!) + + if storyViewModel.storyPage!.isHavePrompt { + promptViewModel.fetchPrompts(storyViewModel.storyPage!) + + if promptViewModel.prompts![0].hints != nil { + hintViewModel.fetchHints(promptViewModel.prompts![0]) + } + } + + navigate.append(.story) } } - - navigate.append(.story) } + .id(episode.uid) } } } + .padding(.horizontal, UIDevice.isIPad ? 30 : 10) } - .padding(.horizontal, 30) - }.scrollClipDisabled() + .defaultScrollAnchor(.trailing) + .scrollClipDisabled() + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { + withAnimation { + proxy.scrollTo(episodeViewModel.episodes?.first?.uid) + } + } + } + } Spacer() } @@ -104,6 +119,7 @@ struct EpisodeView: View { audioViewModel.clearAll() audioViewModel.playSound(soundFileName: "bg-shop", numberOfLoops: -1, category: .backsound) } + AppDelegate.orientationLock = nil } } } diff --git a/Moco/View/User/HomeView.swift b/Moco/View/User/HomeView.swift index 49421b3..6718a00 100644 --- a/Moco/View/User/HomeView.swift +++ b/Moco/View/User/HomeView.swift @@ -14,13 +14,11 @@ struct HomeView: View { @Environment(\.timerViewModel) private var timerViewModel @Environment(\.storyThemeViewModel) private var storyThemeViewModel + @Environment(\.userViewModel) private var userViewModel @Environment(\.navigate) private var navigate @State private var homeViewModel = HomeViewModel() - @State private var isShowing3d = false - @State private var isMakeSentenceTest = true - var body: some View { ZStack { VStack { @@ -32,11 +30,7 @@ struct HomeView: View { VStack { HStack(alignment: .center) { - Image("Story/nav-icon") - .resizable() - .scaledToFit() - .frame(width: 0.4 * Screen.width) - .padding(.top, Screen.height * 0.02) + MocoIcon() Spacer() @@ -45,8 +39,8 @@ struct HomeView: View { .padding(.horizontal, 0.05 * Screen.width) HStack { - Text("Koleksi Cerita Dunia Ajaib") - .customFont(.cherryBomb, size: 50) + Text("Koleksi Cerita Ajaib") + .customFont(.cherryBomb, size: UIDevice.isIPad ? 50 : 30) .foregroundColor(Color.blueTxt) .fontWeight(.bold) Spacer() @@ -96,6 +90,22 @@ struct HomeView: View { } .onAppear { storyThemeViewModel.fetchStoryThemes() + + // Setiap user punya progress masing-masing dan kalau mau terulang dari episode awal, + // maka availableEpisodeSum milik user harus direset jumlahnya menjadi 1 + // (bisa juga dengan cara membuka comment line di bawah ini untuk proses testing saja!) + +// userViewModel.deleteAllUsers() + + userViewModel.fetchUsers() + + if let users = userViewModel.users, users.count > 0 { + userViewModel.userLogin = users[0] + } else { + userViewModel.addUser(userData: UserModel(availableStoryThemeSum: 1, availableEpisodeSum: 1)) + userViewModel.userLogin = userViewModel.users![0] + } + if navigate.previousRoute == nil { audioViewModel.clearAll() audioViewModel.playSound(soundFileName: "bg-shop", numberOfLoops: -1, category: .backsound) @@ -103,11 +113,6 @@ struct HomeView: View { homeViewModel.soundLevel = 0.3 homeViewModel.setVolume() } - if isShowing3d { - ThreeDRenderer { - print("test 3d render selesai!") - } - } } } } 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 20c1c17..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 { @@ -42,7 +41,7 @@ struct MazeView: View { let screenWidth = Screen.width let screenHeight = Screen.height let scene = MazeScene( - size: CGSize(width: screenWidth, height: screenHeight * 0.7) + size: CGSize(width: screenWidth, height: screenHeight * (UIDevice.isIPad ? 0.7 : 0.6)) ) scene.scaleMode = .fill @@ -51,24 +50,28 @@ struct MazeView: View { var onComplete: () -> Void = {} + var fontSize: CGFloat { + UIDevice.isIPad ? 25 : 15 + } + var body: some View { VStack(alignment: .leading) { ZStack { SpriteView(scene: scene, options: [.allowsTransparency]) - .padding(.vertical, 12) + .padding(.vertical, UIDevice.isIPad ? 5 : 0) .ignoresSafeArea() - .frame(width: Screen.width, height: Screen.height * 0.7) + .frame(width: Screen.width, height: Screen.height * (UIDevice.isIPad ? 0.7 : 0.6)) } ZStack { if let obj1 = scene.obj01, scene.obj03 != nil { Text(answers[0]).offset(x: obj1.position.x - answersWidth[0] / 2) - .customFont(.didactGothic, size: 30) + .customFont(.didactGothic, size: fontSize) .foregroundColor(.text.brown) .background { GeometryReader { proxy in HStack {} // just an empty container to triggers the onAppear - .onAppear { + .task { answersWidth[0] = proxy.size.width } } @@ -76,13 +79,13 @@ struct MazeView: View { } if let obj2 = scene.obj02, scene.obj03 != nil { Text(answers[1]).offset(x: obj2.position.x - answersWidth[1] / 2) - .customFont(.didactGothic, size: 30) + .customFont(.didactGothic, size: fontSize) .foregroundColor(.text.brown) .background { GeometryReader { proxy in HStack {} // just an empty container to triggers the onAppear - .onAppear { + .task { answersWidth[1] = proxy.size.width } } @@ -90,13 +93,13 @@ struct MazeView: View { } if let obj3 = scene.obj03, scene.obj03 != nil { Text(answers[2]).offset(x: obj3.position.x - answersWidth[2] / 2) - .customFont(.didactGothic, size: 30) + .customFont(.didactGothic, size: fontSize) .foregroundColor(.text.brown) .background { GeometryReader { proxy in HStack {} // just an empty container to triggers the onAppear - .onAppear { + .task { answersWidth[2] = proxy.size.width } } @@ -109,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 { @@ -187,10 +124,89 @@ 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() + } + } } -#Preview { - MazeView { - print("Done") +struct MazeViewPreview: View { + var body: some View { + MazeView { + print("Done") + } + .environment(\.mazePromptViewModel, MazePromptViewModel.shared) + .environmentObject(MotionViewModel()) + .environmentObject(OrientationInfo.shared) } } + +#Preview { + MazeViewPreview() +} diff --git a/Moco/View/User/SettingsView.swift b/Moco/View/User/SettingsView.swift index 9ec2ff8..3e3a6b0 100644 --- a/Moco/View/User/SettingsView.swift +++ b/Moco/View/User/SettingsView.swift @@ -14,72 +14,81 @@ struct SettingsView: View { var body: some View { VStack { - Image("Buttons/button-home") - .resizable() - .frame(width: 70, height: 70) - .shadow(radius: 4, x: -2, y: 2) - .foregroundColor(.white) - .onTapGesture { - navigate.pop() - } + VStack { + Image("Buttons/button-home") + .resizable() + .frame(width: 70, height: 70) + .shadow(radius: 4, x: -2, y: 2) + .onTapGesture { + navigate.pop() + } - Text("Backsound") - HStack { - Image(systemName: "speaker.fill") - .foregroundColor(Color.black) + Text("Backsound") + HStack { + Image(systemName: "speaker.fill") + .foregroundColor(Color.black) - Slider(value: $settingsViewModel.backsoundVolume, - in: 0 ... 1, - step: 0.01) { _ in - audioViewModel - .setVolumeByCategory( - Float( - settingsViewModel.backsoundVolume), - category: .backsound - ) - }.foregroundColor(.black).accentColor(.black) + Slider(value: $settingsViewModel.backsoundVolume, + in: 0 ... 1, + step: 0.01) { _ in + audioViewModel + .setVolumeByCategory( + Float( + settingsViewModel.backsoundVolume), + category: .backsound + ) + }.foregroundColor(.black).accentColor(.black) - Image(systemName: "speaker.wave.2.fill") - .foregroundColor(Color.black) - } - Text("Narration") - HStack { - Image(systemName: "speaker.fill") - .foregroundColor(Color.black) + Image(systemName: "speaker.wave.2.fill") + .foregroundColor(Color.black) + } + Text("Narration") + HStack { + Image(systemName: "speaker.fill") + .foregroundColor(Color.black) - Slider(value: $settingsViewModel.narrationVolume, - in: 0 ... 1, - step: 0.01) { _ in - audioViewModel - .setVolumeByCategory( - Float( - settingsViewModel.narrationVolume), - category: .narration - ) - }.foregroundColor(.black).accentColor(.black) + Slider(value: $settingsViewModel.narrationVolume, + in: 0 ... 1, + step: 0.01) { _ in + audioViewModel + .setVolumeByCategory( + Float( + settingsViewModel.narrationVolume), + category: .narration + ) + }.foregroundColor(.black).accentColor(.black) - Image(systemName: "speaker.wave.2.fill") - .foregroundColor(Color.black) - } - Text("Sound effect") - HStack { - Image(systemName: "speaker.fill") - .foregroundColor(Color.black) + Image(systemName: "speaker.wave.2.fill") + .foregroundColor(Color.black) + } + Text("Sound effect") + HStack { + Image(systemName: "speaker.fill") + .foregroundColor(Color.black) - Slider(value: $settingsViewModel.soundEffectVolume, - in: 0 ... 1, - step: 0.01) { _ in - audioViewModel - .setVolumeByCategory( - Float( - settingsViewModel.soundEffectVolume), - category: .soundEffect - ) - }.foregroundColor(.black).accentColor(.black) + Slider(value: $settingsViewModel.soundEffectVolume, + in: 0 ... 1, + step: 0.01) { _ in + audioViewModel + .setVolumeByCategory( + Float( + settingsViewModel.soundEffectVolume), + category: .soundEffect + ) + }.foregroundColor(.black).accentColor(.black) - Image(systemName: "speaker.wave.2.fill") - .foregroundColor(Color.black) - } + Image(systemName: "speaker.wave.2.fill") + .foregroundColor(Color.black) + } + }.frame(maxWidth: 0.75 * Screen.width) + } + .frame(width: Screen.width, height: Screen.height) + .foregroundColor(.text.darkBlue) + .background { + Image("Story/main-background") + .resizable() + .aspectRatio(contentMode: .fill) + .ignoresSafeArea() } } } diff --git a/Moco/View/User/StoryView.swift b/Moco/View/User/StoryView.swift index a2155b1..b8ff401 100644 --- a/Moco/View/User/StoryView.swift +++ b/Moco/View/User/StoryView.swift @@ -9,6 +9,7 @@ import SwiftUI struct StoryView: View { // MARK: - Environments + @Environment(\.storyThemeViewModel) private var storyThemeViewModel @Environment(\.storyViewModel) private var storyViewModel @Environment(\.episodeViewModel) private var episodeViewModel @@ -26,21 +27,37 @@ struct StoryView: View { @StateObject private var svvm = StoryViewViewModel() + // MARK: - Variables + + var buttonSize: CGFloat { + UIDevice.isIPad ? 80 : 50 + } + // MARK: - View var body: some View { ZStack { + Color.text.primary ScrollView(.horizontal) { LazyHStack(spacing: 0) { if let stories = episodeViewModel.selectedEpisode!.stories { ForEach(Array(stories.enumerated()), id: \.offset) { index, _ in ZStack { PeelEffectTappable(state: $svvm.peelEffectState, isReverse: svvm.isReversePeel) { - Image(storyViewModel.storyPage!.background) - .resizable() - .scaledToFill() - .frame(width: Screen.width, height: Screen.height, alignment: .center) - .clipped() + if UIDevice.isIPad { + Image(storyViewModel.storyPage!.background) + .resizable() + .scaledToFill() + .frame(width: Screen.width, height: Screen.height, alignment: .center) + .clipped() + } else { + Image(storyViewModel.storyPage!.background) + .resizable() + .scaledToFit() + .scaleEffect(1.4) + .frame(width: Screen.width, height: Screen.height, alignment: .center) + .clipped() + } } background: { svvm.peelBackground } onComplete: { @@ -98,7 +115,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] { @@ -129,8 +149,8 @@ struct StoryView: View { Image("Buttons/button-home").resizable().scaledToFit() }.buttonStyle( CircleButton( - width: 80, - height: 80, + width: buttonSize, + height: buttonSize, backgroundColor: .clear, foregroundColor: .clear ) @@ -144,8 +164,8 @@ struct StoryView: View { .scaledToFit() }.buttonStyle( CircleButton( - width: 80, - height: 80, + width: buttonSize, + height: buttonSize, backgroundColor: .clear, foregroundColor: .clear ) @@ -159,8 +179,8 @@ struct StoryView: View { .scaledToFit() }.buttonStyle( CircleButton( - width: 80, - height: 80, + width: buttonSize, + height: buttonSize, backgroundColor: .clear, foregroundColor: .clear ) diff --git a/Moco/ViewModel/CloudKitManager.swift b/Moco/ViewModel/CloudKitManager.swift new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/Moco/ViewModel/CloudKitManager.swift @@ -0,0 +1 @@ + diff --git a/Moco/ViewModel/EpisodeViewModel.swift b/Moco/ViewModel/EpisodeViewModel.swift index 64a1226..81683bd 100644 --- a/Moco/ViewModel/EpisodeViewModel.swift +++ b/Moco/ViewModel/EpisodeViewModel.swift @@ -12,7 +12,7 @@ import SwiftData static let shared = EpisodeViewModel() var selectedEpisode: EpisodeModel? - var availableEpisodes: [EpisodeModel]? + var indexEpisodePlay: Int? var episodes: [EpisodeModel]? init(modelContext: ModelContext? = nil) { @@ -22,8 +22,9 @@ import SwiftData } } - func setSelectedEpisode(_ episode: EpisodeModel) { + func setSelectedEpisode(_ episode: EpisodeModel, _ indexEpisode: Int) { selectedEpisode = episode + indexEpisodePlay = indexEpisode } func fetchEpisodes(storyThemeId: String) { @@ -35,32 +36,26 @@ import SwiftData ) episodes = (try? modelContext?.fetch(fetchDescriptor) ?? []) ?? [] + } - availableEpisodes = [] + func fetchAvailableEpisodes(storyThemeId: String) -> [EpisodeModel]? { + let fetchDescriptor = FetchDescriptor( + predicate: #Predicate { + $0.storyTheme?.uid == storyThemeId && $0.isAvailable == true + }, + sortBy: [SortDescriptor(\.createdAt)] + ) - for episode in episodes ?? [] { - if episode.isAvailable { - availableEpisodes?.append(episode) - } - } + return (try? modelContext?.fetch(fetchDescriptor) ?? []) ?? [] } func setToAvailable(selectedStoryTheme: StoryThemeModel) { - if let episodes = episodes, let availableEpisodes = availableEpisodes { - if availableEpisodes.count < episodes.count && - selectedEpisode!.uid == availableEpisodes[availableEpisodes.count - 1].uid { - let storyThemeId = selectedStoryTheme.uid - let fetchDescriptor = FetchDescriptor( - predicate: #Predicate { - $0.storyTheme?.uid == storyThemeId - }, - sortBy: [SortDescriptor(\.createdAt)] - ) + fetchEpisodes(storyThemeId: selectedStoryTheme.uid) - if let getEpisodes = (try? modelContext?.fetch(fetchDescriptor)) { - getEpisodes[availableEpisodes.count].isAvailable = true - try? modelContext?.save() - } + if let availableEpisode = fetchAvailableEpisodes(storyThemeId: selectedStoryTheme.uid) { + if EpisodeViewModel.shared.indexEpisodePlay == availableEpisode.count - 1 { + episodes![availableEpisode.count].isAvailable = true + try? modelContext?.save() } } } diff --git a/Moco/ViewModel/GameKitViewModel.swift b/Moco/ViewModel/GameKitViewModel.swift index 44d36fc..b3c0718 100644 --- a/Moco/ViewModel/GameKitViewModel.swift +++ b/Moco/ViewModel/GameKitViewModel.swift @@ -12,6 +12,8 @@ import SwiftUI @Observable class GameKitViewModel: NSObject, GKLocalPlayerListener { var playerModel = PlayerViewModel.shared + static var shared = GameKitViewModel() + override init() { super.init() 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 be5496f..3df0774 100644 --- a/Moco/ViewModel/StoryViewViewModel.swift +++ b/Moco/ViewModel/StoryViewViewModel.swift @@ -10,6 +10,7 @@ import SwiftUI class StoryViewViewModel: ObservableObject { // MARK: - Environments stored property + private(set) var userViewModel = UserViewModel.shared private(set) var storyThemeViewModel = StoryThemeViewModel.shared private(set) var storyViewModel = StoryViewModel.shared private(set) var episodeViewModel = EpisodeViewModel.shared @@ -21,6 +22,7 @@ class StoryViewViewModel: ObservableObject { private(set) var audioViewModel = AudioViewModel.shared private(set) var settingsViewModel = SettingsViewModel.shared private(set) var navigate = RouteViewModel.shared + private(set) var gameKitViewModel = GameKitViewModel.shared // MARK: - Static Variables @@ -48,7 +50,9 @@ class StoryViewViewModel: ObservableObject { // MARK: - Variables @Published var enableUI = true +} +extension StoryViewViewModel { // MARK: - Functions private func updateText() { @@ -68,7 +72,7 @@ class StoryViewViewModel: ObservableObject { private func startNarrative() { guard storyContentViewModel.narratives != nil else { return } narrativeIndex = -1 -// updateText() + // updateText() } private func startPrompt() { @@ -85,7 +89,7 @@ class StoryViewViewModel: ObservableObject { } } - func onPageChange() { + func onPageChange(_ earlyPrompt: Bool? = false) { stop() setNewStoryPage(scrollPosition ?? -1) @@ -102,7 +106,7 @@ class StoryViewViewModel: ObservableObject { 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 @@ -123,10 +127,14 @@ class StoryViewViewModel: ObservableObject { let nextPageBg = storyViewModel.getPageBackground(scrollPosition! + 1, episode: episodeViewModel.selectedEpisode!) - peelBackground = AnyView(Image(nextPageBg ?? storyViewModel.storyPage!.background) + peelBackground = UIDevice.isIPad ? AnyView(Image(nextPageBg ?? storyViewModel.storyPage!.background) .resizable() .scaledToFill() .frame(width: Screen.width, height: Screen.height, alignment: .center) + .clipped()) : AnyView(Image(nextPageBg ?? storyViewModel.storyPage!.background) + .resizable() + .scaledToFit() + .frame(width: Screen.width, height: Screen.height, alignment: .center) .clipped()) peelEffectState = .start toBeExecutedByPeelEffect = { @@ -198,6 +206,8 @@ class StoryViewViewModel: ObservableObject { func continueStory() { episodeViewModel.setToAvailable(selectedStoryTheme: storyThemeViewModel.selectedStoryTheme!) + userViewModel.addingAvailableEpisode() + registerAchievement() storyThemeViewModel.fetchStoryThemes() storyThemeViewModel.setSelectedStoryTheme(storyThemeViewModel.findWithID(storyThemeViewModel.selectedStoryTheme!.uid)!) navigate.pop { @@ -205,8 +215,50 @@ class StoryViewViewModel: ObservableObject { } } + func registerAchievement() { + var achievementId = AchievementID.firstEpisode + switch userViewModel.userLogin?.availableEpisodeSum { + case 1: + achievementId = AchievementID.firstEpisode + case 2: + achievementId = AchievementID.secondEpisode + case 3: + achievementId = AchievementID.thirdEpisode + case 4: + achievementId = AchievementID.fourthEpisode + default: + break + } + gameKitViewModel.reportAchievement(achievementID: achievementId, percentComplete: 100) + } + func onAppear() { 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) + } } diff --git a/Moco/ViewModel/UserViewModel.swift b/Moco/ViewModel/UserViewModel.swift new file mode 100644 index 0000000..00d0c17 --- /dev/null +++ b/Moco/ViewModel/UserViewModel.swift @@ -0,0 +1,69 @@ +// +// UserViewModel.swift +// Moco +// +// Created by Nur Azizah on 24/11/23. +// + +import Foundation +import SwiftData + +@Observable class UserViewModel: BaseViewModel { + static var shared = UserViewModel() + + var users: [UserModel]? + var userLogin: UserModel? + + init(modelContext: ModelContext? = nil) { + super.init() + if modelContext != nil { + self.modelContext = modelContext + } + } + + func fetchUsers() { + let fetchDescriptor = FetchDescriptor( + sortBy: [SortDescriptor(\.createdAt)] + ) + + users = (try? modelContext?.fetch(fetchDescriptor) ?? []) ?? [] + } + + func addUser(userData: UserModel) { + modelContext?.insert(userData) + try? modelContext?.save() + + fetchUsers() + } + + func setUserLogin(user: UserModel) { + userLogin = user + } + + func addingAvailableStoryTheme() { + if let userLogin = userLogin { + userLogin.availableStoryThemeSum += 1 + try? modelContext?.save() + } + } + + func addingAvailableEpisode() { + if let userLogin = userLogin { + if EpisodeViewModel.shared.indexEpisodePlay == userLogin.availableEpisodeSum - 1 { + userLogin.availableEpisodeSum += 1 + try? modelContext?.save() + } + } + } + + func deleteAllUsers() { + fetchUsers() + + if let users = users { + for user in users { + modelContext?.delete(user) + try? modelContext?.save() + } + } + } +} diff --git a/README.md b/README.md index 083c5be..8dc510a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Moco +# Moco Kids ## Platform