diff --git a/AGENTS.md b/AGENTS.md index e3cf243..0297608 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,9 +14,44 @@ SOPA iOS soll in der **Spiellogik** Android-Verhalten erreichen (UI darf abweich ## Build und Test - Build: - - `xcodebuild -project SOPA.xcodeproj -scheme SOPA -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.0' build` + - `xcodebuild -project SOPA.xcodeproj -scheme SOPA -destination 'platform=iOS Simulator,name=iPhone 17 Pro' build` - Voller Testlauf: - - `xcodebuild -project SOPA.xcodeproj -scheme SOPA -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.0' test` + - `xcodebuild -project SOPA.xcodeproj -scheme SOPA -destination 'platform=iOS Simulator,name=iPhone 17 Pro' test` +- Hinweis: Simulator-Namen mit `xcrun simctl list devices available` prüfen (der Name variiert je nach installierter Xcode-Version; aktuell iOS 26.5, iPhone 17 Pro). + +## App im Simulator ansehen (visuelle Verifikation mit idb) +Für UI-/Design-Arbeit lässt sich die App im Simulator fernsteuern (Taps/Swipes), um Screens +gegen das Android-Design abzugleichen. Setup ist installiert (siehe unten bei Problemen). + +Ablauf (Beispiel iPhone 17 Pro): +```bash +export PATH="$HOME/.local/bin:$PATH" # idb-CLI liegt in ~/.local/bin (pipx) +UDID=$(xcrun simctl list devices booted | grep -oE '[0-9A-F-]{36}' | head -1) +BUNDLE=de.davidschilling.SOPA + +xcrun simctl boot "iPhone 17 Pro" 2>/dev/null # falls nicht gebootet +xcrun simctl install "$UDID" "$(find ~/Library/Developer/Xcode/DerivedData/SOPA-*/Build/Products/Debug-iphonesimulator -maxdepth 1 -name SOPA.app | head -1)" +xcrun simctl launch "$UDID" "$BUNDLE" + +idb connect "$UDID" +idb ui tap --udid "$UDID" # Koordinaten in POINTS (iPhone 17 Pro = 402x874) +idb ui swipe --udid "$UDID" # z.B. Reihe/Spalte im Spielfeld schieben +xcrun simctl io "$UDID" screenshot out.png # Screenshot zum Vergleich +``` +Menü-Tap-Punkte (Points, iPhone 17 Pro): LEVEL MODE ≈ (201,345), JUST PLAY ≈ (201,439), +TUTORIAL ≈ (201,535), CREDITS ≈ (201,629). + +Grenzen: Screens hinter Gameplay (Level Complete, JustPlay-Score) erscheinen erst nach +gelöstem Puzzle und lassen sich per Blind-Swipe kaum reproduzierbar erreichen – dafür +Quellcode heranziehen. JustPlay-Game-Over erreicht man einfach, indem man den Timer +ablaufen lässt (~12 s ohne Lösung). + +### idb-Setup (falls nicht vorhanden) +- `brew install facebook/fb/idb-companion` +- `pipx install --python /opt/homebrew/bin/python3.12 fb-idb` + (**muss Python 3.12 sein**, nicht 3.14 – fb-idb 1.1.7 crasht auf 3.14 wegen entferntem + `asyncio.get_event_loop()`). +- Die Meldung `objc[...] Class FBProcess is implemented in both ...` bei jedem Aufruf ist harmlos. ## Wichtige Dateien - Produktplan: `IOS_PRODUCT_COMPLETION_PLAN.md` diff --git a/ANDROID_DESIGN_PARITY_PLAN.md b/ANDROID_DESIGN_PARITY_PLAN.md new file mode 100644 index 0000000..0c37022 --- /dev/null +++ b/ANDROID_DESIGN_PARITY_PLAN.md @@ -0,0 +1,160 @@ +# iOS ↔ Android Design-Parität – Aufgabenplan + +Ziel: Das aktuelle **Android-Design** (siehe `sopa-android/pr/dev-screenshots/`) auf iOS +übernehmen. Grundlage dieses Plans ist ein Vergleich der 7 Android-Referenz-Screens mit +dem aktuellen iOS-Zustand (Branch `backup/ios-ui-level-overview-2026-02-25`). + +Die iOS-Screens wurden **live im Simulator** (iPhone 17 Pro, iOS 26.5) per idb erfasst und +mit den Android-Vorlagen verglichen. Ausnahmen: „Level Complete" und der JustPlay-Zwischen- +Score erscheinen nur nach gelöstem Puzzle und wurden über den Quellcode abgeglichen. + +## Statusüberblick pro Screen + +| Screen | iOS-Datei | Status | Kernabweichung zu Android | +|---|---|---|---| +| Hauptmenü | `StartMenuScene.swift` | 🟢 fast fertig | Menüpunkt-Label + Social-Icons; kein Settings-Screen | +| Level-Auswahl | `LevelChoiceScene` + `LevelButtonArea` + `LevelSelectButton` | 🟢 (live bestätigt) | fehlender Tutorial-Einstieg | +| Level-Mode Spiel | `LevelModeGameScene.swift` | 🟢 (live bestätigt) | Back-Button (Android hat keinen) | +| Level Complete | `LevelModeScoreScene.swift` | 🟢 (P2 erledigt) | Wording + Play-Icon angepasst | +| Just-Play Spiel | `JustPlayGameScene.swift` (+ Basis `GameScene`) | 🟢 (P1 erledigt) | – (schwarz + Android-HUD) | +| Just-Play Highscore | `JustPlayLostScene` (in `JustPlayGameScene.swift`) | 🟢 (P2 erledigt) | Farbbalken + „Share Score" ergänzt | +| Tutorial | `TutorialScene.swift` / `TutorialGameScene.swift` | 🟢 (P2 erledigt) | visuelle Android-Screens; Einstiegspunkt offen (P3) | +| Credits | `CreditsScene.swift` | 🟢 (kein Referenz-Screenshot) | – | + +Grundursache vieler roter Punkte: Die Basis-Klasse `GameScene` (`SOPA/view/GameScene.swift:33`) +setzt weiterhin den **warmen braunen** Hintergrund und ein altes Inline-HUD +(„Optimum: N", „Moves:", „N. Level"). `LevelModeGameScene` überschreibt beides bereits +Android-konform – `JustPlayGameScene` und die Score-Szenen tun das noch nicht. + +--- + +## P1 – Hintergrund & Theme vereinheitlichen (schnelle, hochwirksame Fixes) + +Android nutzt durchgängig **Schwarz** als Hintergrund. Auf iOS sind noch braun: +`LevelModeScoreScene`, `JustPlayScoreScene`, `JustPlayLostScene`, `TutorialScene` sowie die +Basis `GameScene` (→ trifft `JustPlayGameScene` und `TutorialGameScene`). + +- [x] `GameScene` Default-Hintergrund auf schwarz umgestellt (`GameScene.swift`) +- [x] `LevelModeScoreScene` Hintergrund schwarz +- [x] `JustPlayScoreScene` + `JustPlayLostScene` Hintergrund schwarz +- [x] `TutorialScene` Hintergrund schwarz +- [ ] Toten braunen `warmBackground`-Pfad in `SopaTheme` später entfernen + +## P1 – Just-Play Spiel-HUD an Android angleichen + +Android zeigt oben „Min. Moves"/„Current Moves" (Label + große Zahl darunter) und unten +„Left Time" (große Zahl). iOS `JustPlayGameScene` erbt aktuell das alte Basis-HUD und +addiert nur „Left Time" → doppelte/inkonsistente Beschriftung. + +- [x] In `JustPlayGameScene` `addStaticLabels()`/`addDynamicLabels()` überschrieben, analog + zu `LevelModeGameScene` (Impact-Font, Label + große Zahl) +- [x] „Left Time" ins gleiche Layout gebracht (Label oben, große Zahl darunter, unten links) +- [ ] Prüfen, ob das rote Warn-Overlay bei ≤5 s dem Android-Verhalten entspricht + +Live bestätigt (idb): JustPlay zeigt jetzt schwarz „Min. Moves / Current Moves" oben und +„Left Time" unten links, Restart unten rechts – wie Android. + +## P1 – Safe-Area / Notch (weitgehend erledigt) + +Live-Befund war: im JustPlay ragte der „1. Level"-Titel (altes Basis-HUD) in die +**Dynamic Island**, und die Spiel-Buttons überlappten die Labels. + +- [x] Ursache behoben durch den JustPlay-HUD-Umbau (kein Level-Titel oben mehr) und die + manuelle Button-Positionierung (Restart unten rechts, minimaler Back oben links) +- [ ] Optional: Top-Labels zusätzlich an Safe-Area-Insets ausrichten (statt `size.height`), + auf mehreren iPhone-Größen gegenprüfen + +Hinweis: Der große Leerraum **unter** dem Spielfeld ist **kein** Parität-Fehler – +Android platziert das Feld ebenfalls im oberen Bereich (`FIELD_SART_FROM_TOP = 0.2`, +`GameFieldNode.swift:18`), der untere Bereich trägt das HUD. `GameFieldNode` bleibt +daher unverändert; eine Zentrierung würde von Android *wegführen*. + +Zusatzbefund: `IPadProportionSet` ist nur eine leere Unterklasse von `IPhone6Proportionset` +(`IPadProportionSet.swift`), also faktisch identisch – die frühere „iPad-hardcoded"-Sorge +ist gegenstandslos. Positionswerte für das Spiel-HUD kommen aus `ProportionSet`, werden aber +in Level-Mode/JustPlay inzwischen durch absolute Layouts überschrieben. + +## P2 – Level Complete an Android angleichen + +Android: **schwarzer** Hintergrund, Titel „N. Level Complete", „Your moves: X", +„Moves for 3 Stars: Y", 3 goldene Sterne, unten 3 weiße Kreis-Buttons (Grid / Restart / Play). + +- [x] Hintergrund schwarz (P1) +- [x] Titel-Wording auf „N. Level Complete" +- [x] Untertitel „Moves for 3 Stars: N" +- [x] „Next"-Button: gefülltes Play-Dreieck (`play.fill`) +- [ ] Optional: Titel-Font/Größe/links-Ausrichtung noch näher an Android (aktuell zentriert, Optima) +- [ ] Sterne-Optik gegen Android prüfen (`star_score`/`starSW_score`) + +## P2 – Just-Play Highscore an Android angleichen + +Android: schwarzer Hintergrund, Titel „New Highscore", **blauer Balken** „Score: N", +**gelber Balken** „Level: N", unten 3 Kreis-Buttons: Zurück-Pfeil, Restart, **„SHARE SCORE"**. + +- [x] Hintergrund schwarz (P1) +- [x] Score/Level als farbige Balken (blau/gelb) statt reinem Text +- [x] **„SHARE SCORE"-Button ergänzt** (neuer `makeCircleTextButtonTexture`) +- [x] Button-Set an Android angeglichen (Zurück-Pfeil / Restart / Share) +- [x] „Reached Level"/„Highscore"-Klartextzeilen für Parität entfernt (Konfetti bleibt als iOS-Extra bei neuem Highscore) + +## P2 – Tutorial neu aufbauen (anderes Konzept) + +Android-Tutorial = **visueller** 2-Screen-Ablauf: Titel „Tutorial" + „Click to see next info", +ein Spielfeld-Grid mit grünem/rotem Gate und blauen Röhren, unten farbig hervorgehobener +Erklärtext („Connect the **GREEN** and the **RED** gate, using the **BLUE** tubes. Use all tubes."). +Wird in Android **aus der Level-Auswahl** gestartet (`loadTutorialSceneFromLevelChoiceScene`) +bzw. beim Erststart, nicht aus dem Menü. + +- [x] Tutorial als visuelle Screens umgebaut – Android-Artwork nach + `Assets.xcassets/tutorial` importiert (first/second Screen A+B, Let's Go) +- [x] Farbige Keywords (GREEN/RED/BLUE) – über die importierten Screens abgedeckt +- [x] 2-Screen-Ablauf + „Lets GO" → interaktives Tutorial-Level (analog Android) +- [ ] Einstiegspunkt: Android startet aus der Level-Auswahl / beim Erststart; iOS startet + aktuell aus dem Menü (hängt an der SETTINGS/TUTORIAL-Entscheidung, siehe P3) + +## P3 – Menü: Settings vs. Tutorial klären + Social-Icons + +Android-Menü: **LEVEL MODE / JUST PLAY / SETTINGS / CREDITS** (kein Tutorial-Eintrag). +Der Settings-Screen in Android ist ein **Musik-Mute-Toggle** (`SettingsService`: `MUTE`, +`MY_FIRST_TIME`). Tutorial läuft über Level-Auswahl/Erststart. + +iOS aktuell: der 3. Menüpunkt wurde von „SETTINGS" auf **„TUTORIAL"** umbenannt und öffnet +das (Text-)Tutorial. Es gibt auf iOS **keinerlei Audio/Musik** und keinen Settings-Screen. + +- [x] Social-Icons an Android angeglichen: Android-`ShareThis` + Twitter-Vogel importiert und + im `StartMenuScene` statt der SF-Symbol-Texturen verwendet +- [~] **Entscheidung getroffen (Nutzer):** iOS weicht bewusst ab – Menüpunkt bleibt „TUTORIAL", + **kein** SETTINGS-Eintrag. Settings/Audio werden vorerst **nicht** gebaut. + +## P3 – Audio/Musik + Settings (zurückgestellt) + +Android hat Menü-Musik und einen Mute-Toggle; iOS hat aktuell **kein Audio**. +**Auf Nutzerwunsch vorerst zurückgestellt** – wird derzeit nicht umgesetzt. + +- [ ] (später) Musik-Playback (z. B. `AVAudioPlayer`) + Mute-Persistenz +- [ ] (später) Settings-Szene mit Mute/Unmute-Button +- [ ] (später) Erststart-Logik (`isFirstTime`) für automatischen Tutorial-Aufruf + +## Tutorial-Einstiegspunkt (zurückgestellt) + +Android startet das Tutorial aus der Level-Auswahl / beim Erststart. iOS behält auf +Nutzerwunsch den **Menü-Eintrag „TUTORIAL"**; eine Verlegung des Einstiegs wird nicht gemacht. + +--- + +## Offene Punkte / zu verifizieren + +- Android-Credits-Screen liegt nicht als Screenshot vor → Parität nicht abgleichbar. +- Ob Android im Just-Play einen Zwischen-Score-Screen pro Level hat (iOS `JustPlayScoreScene`), + ist aus den Screenshots nicht ersichtlich – gegen die Android-App verifizieren. + +## Empfohlene Reihenfolge + +1. ~~**P1** – Hintergründe schwarz + Just-Play-HUD~~ ✅ erledigt +2. ~~**P2** – Level Complete, Just-Play Highscore, Tutorial-Neuaufbau~~ ✅ erledigt +3. ~~**P3** – Social-Icons~~ ✅ erledigt; Settings/Audio + Tutorial-Einstieg auf + Nutzerwunsch zurückgestellt + +Damit ist die sichtbare Design-Parität für alle vorhandenen Screens erreicht. +Offen bleiben nur die bewusst zurückgestellten Punkte (Audio/Settings) sowie kleinere +Feinschliff-Optionen (Level-Complete-Titelstil, Sterne-Optik). diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ea9c29b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +See [AGENTS.md](./AGENTS.md) for project instructions and conventions. diff --git a/SOPA.xcodeproj/project.pbxproj b/SOPA.xcodeproj/project.pbxproj index 575c50f..90a4f0f 100644 --- a/SOPA.xcodeproj/project.pbxproj +++ b/SOPA.xcodeproj/project.pbxproj @@ -112,6 +112,20 @@ 64EFCB3E240E732F00714E7C /* LevelDestroyer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EFCB3C240E732F00714E7C /* LevelDestroyer.swift */; }; 64EFCB40240E763A00714E7C /* LevelSolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EFCB3F240E763A00714E7C /* LevelSolver.swift */; }; 64EFCB41240E763A00714E7C /* LevelSolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EFCB3F240E763A00714E7C /* LevelSolver.swift */; }; + A11E00012602000100AAB001 /* StartMenuScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11E00012602000100AAA001 /* StartMenuScene.swift */; }; + A11E00012602000100AAB002 /* StartMenuScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11E00012602000100AAA001 /* StartMenuScene.swift */; }; + A11E00012602000100AAB003 /* CreditsScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11E00012602000100AAA002 /* CreditsScene.swift */; }; + A11E00012602000100AAB004 /* CreditsScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11E00012602000100AAA002 /* CreditsScene.swift */; }; + A11E00012602000100AAB005 /* TutorialScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11E00012602000100AAA003 /* TutorialScene.swift */; }; + A11E00012602000100AAB006 /* TutorialScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11E00012602000100AAA003 /* TutorialScene.swift */; }; + A11E00012602000100AAB007 /* TutorialGameScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11E00012602000100AAA004 /* TutorialGameScene.swift */; }; + A11E00012602000100AAB008 /* TutorialGameScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11E00012602000100AAA004 /* TutorialGameScene.swift */; }; + A11E00012602000100AAB009 /* SopaTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11E00012602000100AAA005 /* SopaTheme.swift */; }; + A11E00012602000100AAB00A /* SopaTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11E00012602000100AAA005 /* SopaTheme.swift */; }; + A11E00012602000100AAB00B /* LabelSizing.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11E00012602000100AAA006 /* LabelSizing.swift */; }; + A11E00012602000100AAB00C /* LabelSizing.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11E00012602000100AAA006 /* LabelSizing.swift */; }; + A11E00012602000100AAB00D /* ButtonTextureFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11E00012602000100AAA007 /* ButtonTextureFactory.swift */; }; + A11E00012602000100AAB00E /* ButtonTextureFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = A11E00012602000100AAA007 /* ButtonTextureFactory.swift */; }; A1B2C3D424FA000100000001 /* JustPlayServiceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D424FA000100000011 /* JustPlayServiceTest.swift */; }; A1B2C3D424FA000100000002 /* helper/LevelServiceImplUnlockingTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D424FA000100000012 /* helper/LevelServiceImplUnlockingTest.swift */; }; A1B2C3D424FA000100000003 /* database/JustPlayHighscorePersistenceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D424FA000100000013 /* database/JustPlayHighscorePersistenceTest.swift */; }; @@ -191,6 +205,13 @@ 64D66B1A21426ADC00595BD3 /* progbot.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = progbot.ttf; sourceTree = ""; }; 64EFCB3C240E732F00714E7C /* LevelDestroyer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LevelDestroyer.swift; sourceTree = ""; }; 64EFCB3F240E763A00714E7C /* LevelSolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LevelSolver.swift; sourceTree = ""; }; + A11E00012602000100AAA001 /* StartMenuScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartMenuScene.swift; sourceTree = ""; }; + A11E00012602000100AAA002 /* CreditsScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreditsScene.swift; sourceTree = ""; }; + A11E00012602000100AAA003 /* TutorialScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TutorialScene.swift; sourceTree = ""; }; + A11E00012602000100AAA004 /* TutorialGameScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TutorialGameScene.swift; sourceTree = ""; }; + A11E00012602000100AAA005 /* SopaTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SopaTheme.swift; sourceTree = ""; }; + A11E00012602000100AAA006 /* LabelSizing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelSizing.swift; sourceTree = ""; }; + A11E00012602000100AAA007 /* ButtonTextureFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonTextureFactory.swift; sourceTree = ""; }; A1B2C3D424FA000100000011 /* JustPlayServiceTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JustPlayServiceTest.swift; sourceTree = ""; }; A1B2C3D424FA000100000012 /* helper/LevelServiceImplUnlockingTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = helper/LevelServiceImplUnlockingTest.swift; sourceTree = ""; }; A1B2C3D424FA000100000013 /* database/JustPlayHighscorePersistenceTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = database/JustPlayHighscorePersistenceTest.swift; sourceTree = ""; }; @@ -377,10 +398,17 @@ 64182B18208DCE0A00CF639A /* LevelModeGameScene.swift */, 64D5B8CE20ADE2680007D982 /* LevelModeScoreScene.swift */, 64D5B8D020B46BD70007D982 /* LevelChoiceScene.swift */, + A11E00012602000100AAA001 /* StartMenuScene.swift */, + A11E00012602000100AAA002 /* CreditsScene.swift */, + A11E00012602000100AAA003 /* TutorialScene.swift */, + A11E00012602000100AAA004 /* TutorialGameScene.swift */, 642E2B3E213B0B1E002669E5 /* LevelSelectButton.swift */, 642E2B41213D257C002669E5 /* LevelButtonPositioner.swift */, 646E6AE8213E80F7001D195B /* LevelButtonArea.swift */, 648558C221412BBD00B61C31 /* EffectSpriteButton.swift */, + A11E00012602000100AAA005 /* SopaTheme.swift */, + A11E00012602000100AAA006 /* LabelSizing.swift */, + A11E00012602000100AAA007 /* ButtonTextureFactory.swift */, ); path = LevelMode; sourceTree = ""; @@ -563,6 +591,10 @@ 64EFCB40240E763A00714E7C /* LevelSolver.swift in Sources */, 6493FB652073D6510044B4E0 /* SOPA.xcdatamodeld in Sources */, 64D5B8CF20ADE2680007D982 /* LevelModeScoreScene.swift in Sources */, + A11E00012602000100AAB003 /* CreditsScene.swift in Sources */, + A11E00012602000100AAB001 /* StartMenuScene.swift in Sources */, + A11E00012602000100AAB005 /* TutorialScene.swift in Sources */, + A11E00012602000100AAB007 /* TutorialGameScene.swift in Sources */, 63F661231F9B346E0043ABCF /* AppDelegate.swift in Sources */, E1AA11122602000100ABCDEF /* SceneDelegate.swift in Sources */, 6446C3031FAC80DC00806A10 /* LevelTranslator.swift in Sources */, @@ -574,6 +606,9 @@ 6446C2EF1FAC802900806A10 /* GameServiceImpl.swift in Sources */, 642E2B3F213B0B1E002669E5 /* LevelSelectButton.swift in Sources */, 64206E6C240EBB71001151B4 /* JustPlayGameScene.swift in Sources */, + A11E00012602000100AAB009 /* SopaTheme.swift in Sources */, + A11E00012602000100AAB00B /* LabelSizing.swift in Sources */, + A11E00012602000100AAB00D /* ButtonTextureFactory.swift in Sources */, 6446C3011FAC80DC00806A10 /* LevelFileService.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -632,9 +667,16 @@ 646902C6207F6B7600283F71 /* GameServiceImpl.swift in Sources */, 64847A0B215A382B0012B732 /* LevelModeScoreScene.swift in Sources */, 64206E6D240EBB71001151B4 /* JustPlayGameScene.swift in Sources */, + A11E00012602000100AAB004 /* CreditsScene.swift in Sources */, + A11E00012602000100AAB002 /* StartMenuScene.swift in Sources */, + A11E00012602000100AAB006 /* TutorialScene.swift in Sources */, + A11E00012602000100AAB008 /* TutorialGameScene.swift in Sources */, 646902CE207F6B7D00283F71 /* LevelFileService.swift in Sources */, 646902DA207F6BD500283F71 /* GameScene.swift in Sources */, 642E2B40213B0B1E002669E5 /* LevelSelectButton.swift in Sources */, + A11E00012602000100AAB00A /* SopaTheme.swift in Sources */, + A11E00012602000100AAB00C /* LabelSizing.swift in Sources */, + A11E00012602000100AAB00E /* ButtonTextureFactory.swift in Sources */, 646902C7207F6B7600283F71 /* Level.swift in Sources */, 642EE5BD22423AAF00680612 /* ProportionSet.swift in Sources */, ); diff --git a/SOPA/Assets.xcassets/border/borders.imageset/Contents.json b/SOPA/Assets.xcassets/border/borders.imageset/Contents.json index 82d3b31..3cb9267 100644 --- a/SOPA/Assets.xcassets/border/borders.imageset/Contents.json +++ b/SOPA/Assets.xcassets/border/borders.imageset/Contents.json @@ -2,11 +2,11 @@ "images" : [ { "idiom" : "universal", - "filename" : "empty.png" + "filename" : "borders.png" } ], "info" : { "version" : 1, "author" : "xcode" } -} \ No newline at end of file +} diff --git a/SOPA/Assets.xcassets/border/borders.imageset/borders.png b/SOPA/Assets.xcassets/border/borders.imageset/borders.png new file mode 100644 index 0000000..2327281 Binary files /dev/null and b/SOPA/Assets.xcassets/border/borders.imageset/borders.png differ diff --git a/SOPA/Assets.xcassets/border/bordersFinish.imageset/Contents.json b/SOPA/Assets.xcassets/border/bordersFinish.imageset/Contents.json index 3e8a31a..642f573 100644 --- a/SOPA/Assets.xcassets/border/bordersFinish.imageset/Contents.json +++ b/SOPA/Assets.xcassets/border/bordersFinish.imageset/Contents.json @@ -2,11 +2,11 @@ "images" : [ { "idiom" : "universal", - "filename" : "border.png" + "filename" : "bordersFinish.png" } ], "info" : { "version" : 1, "author" : "xcode" } -} \ No newline at end of file +} diff --git a/SOPA/Assets.xcassets/border/bordersFinish.imageset/bordersFinish.png b/SOPA/Assets.xcassets/border/bordersFinish.imageset/bordersFinish.png new file mode 100644 index 0000000..9291faf Binary files /dev/null and b/SOPA/Assets.xcassets/border/bordersFinish.imageset/bordersFinish.png differ diff --git a/SOPA/Assets.xcassets/border/bordersFinish_filled.imageset/Contents.json b/SOPA/Assets.xcassets/border/bordersFinish_filled.imageset/Contents.json index 3e8a31a..f25dac8 100644 --- a/SOPA/Assets.xcassets/border/bordersFinish_filled.imageset/Contents.json +++ b/SOPA/Assets.xcassets/border/bordersFinish_filled.imageset/Contents.json @@ -2,11 +2,11 @@ "images" : [ { "idiom" : "universal", - "filename" : "border.png" + "filename" : "bordersFinish_filled.png" } ], "info" : { "version" : 1, "author" : "xcode" } -} \ No newline at end of file +} diff --git a/SOPA/Assets.xcassets/border/bordersFinish_filled.imageset/bordersFinish_filled.png b/SOPA/Assets.xcassets/border/bordersFinish_filled.imageset/bordersFinish_filled.png new file mode 100644 index 0000000..155afa9 Binary files /dev/null and b/SOPA/Assets.xcassets/border/bordersFinish_filled.imageset/bordersFinish_filled.png differ diff --git a/SOPA/Assets.xcassets/border/bordersStart.imageset/Contents.json b/SOPA/Assets.xcassets/border/bordersStart.imageset/Contents.json index 3e8a31a..00d1855 100644 --- a/SOPA/Assets.xcassets/border/bordersStart.imageset/Contents.json +++ b/SOPA/Assets.xcassets/border/bordersStart.imageset/Contents.json @@ -2,11 +2,11 @@ "images" : [ { "idiom" : "universal", - "filename" : "border.png" + "filename" : "bordersStart.png" } ], "info" : { "version" : 1, "author" : "xcode" } -} \ No newline at end of file +} diff --git a/SOPA/Assets.xcassets/border/bordersStart.imageset/bordersStart.png b/SOPA/Assets.xcassets/border/bordersStart.imageset/bordersStart.png new file mode 100644 index 0000000..7e8beeb Binary files /dev/null and b/SOPA/Assets.xcassets/border/bordersStart.imageset/bordersStart.png differ diff --git a/SOPA/Assets.xcassets/border/bordersStart_filled.imageset/Contents.json b/SOPA/Assets.xcassets/border/bordersStart_filled.imageset/Contents.json index 3e8a31a..7d11783 100644 --- a/SOPA/Assets.xcassets/border/bordersStart_filled.imageset/Contents.json +++ b/SOPA/Assets.xcassets/border/bordersStart_filled.imageset/Contents.json @@ -2,11 +2,11 @@ "images" : [ { "idiom" : "universal", - "filename" : "border.png" + "filename" : "bordersStart_filled.png" } ], "info" : { "version" : 1, "author" : "xcode" } -} \ No newline at end of file +} diff --git a/SOPA/Assets.xcassets/border/bordersStart_filled.imageset/bordersStart_filled.png b/SOPA/Assets.xcassets/border/bordersStart_filled.imageset/bordersStart_filled.png new file mode 100644 index 0000000..0e0b7fa Binary files /dev/null and b/SOPA/Assets.xcassets/border/bordersStart_filled.imageset/bordersStart_filled.png differ diff --git a/SOPA/Assets.xcassets/menu/Contents.json b/SOPA/Assets.xcassets/menu/Contents.json new file mode 100644 index 0000000..0ce4e31 --- /dev/null +++ b/SOPA/Assets.xcassets/menu/Contents.json @@ -0,0 +1,3 @@ +{ + "info" : { "version" : 1, "author" : "xcode" } +} diff --git a/SOPA/Assets.xcassets/menu/ShareThis.imageset/Contents.json b/SOPA/Assets.xcassets/menu/ShareThis.imageset/Contents.json new file mode 100644 index 0000000..36afda9 --- /dev/null +++ b/SOPA/Assets.xcassets/menu/ShareThis.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "filename" : "ShareThis.png", "scale" : "1x" }, + { "idiom" : "universal", "scale" : "2x" }, + { "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "version" : 1, "author" : "xcode" } +} diff --git a/SOPA/Assets.xcassets/menu/ShareThis.imageset/ShareThis.png b/SOPA/Assets.xcassets/menu/ShareThis.imageset/ShareThis.png new file mode 100644 index 0000000..f5e9f82 Binary files /dev/null and b/SOPA/Assets.xcassets/menu/ShareThis.imageset/ShareThis.png differ diff --git a/SOPA/Assets.xcassets/menu/TwitterLogo.imageset/Contents.json b/SOPA/Assets.xcassets/menu/TwitterLogo.imageset/Contents.json new file mode 100644 index 0000000..ac755ac --- /dev/null +++ b/SOPA/Assets.xcassets/menu/TwitterLogo.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "filename" : "Twitter_logo_blue.png", "scale" : "1x" }, + { "idiom" : "universal", "scale" : "2x" }, + { "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "version" : 1, "author" : "xcode" } +} diff --git a/SOPA/Assets.xcassets/menu/TwitterLogo.imageset/Twitter_logo_blue.png b/SOPA/Assets.xcassets/menu/TwitterLogo.imageset/Twitter_logo_blue.png new file mode 100644 index 0000000..b87ae9d Binary files /dev/null and b/SOPA/Assets.xcassets/menu/TwitterLogo.imageset/Twitter_logo_blue.png differ diff --git a/SOPA/Assets.xcassets/tiles/a.imageset/Contents.json b/SOPA/Assets.xcassets/tiles/a.imageset/Contents.json index a39878b..adf2f50 100644 --- a/SOPA/Assets.xcassets/tiles/a.imageset/Contents.json +++ b/SOPA/Assets.xcassets/tiles/a.imageset/Contents.json @@ -9,4 +9,4 @@ "version" : 1, "author" : "xcode" } -} \ No newline at end of file +} diff --git a/SOPA/Assets.xcassets/tiles/a.imageset/a.png b/SOPA/Assets.xcassets/tiles/a.imageset/a.png index 6dfbddb..d1e26b2 100644 Binary files a/SOPA/Assets.xcassets/tiles/a.imageset/a.png and b/SOPA/Assets.xcassets/tiles/a.imageset/a.png differ diff --git a/SOPA/Assets.xcassets/tiles/a_filled.imageset/Contents.json b/SOPA/Assets.xcassets/tiles/a_filled.imageset/Contents.json index a39878b..aa211cc 100644 --- a/SOPA/Assets.xcassets/tiles/a_filled.imageset/Contents.json +++ b/SOPA/Assets.xcassets/tiles/a_filled.imageset/Contents.json @@ -2,11 +2,11 @@ "images" : [ { "idiom" : "universal", - "filename" : "a.png" + "filename" : "a_filled.png" } ], "info" : { "version" : 1, "author" : "xcode" } -} \ No newline at end of file +} diff --git a/SOPA/Assets.xcassets/tiles/a_filled.imageset/a_filled.png b/SOPA/Assets.xcassets/tiles/a_filled.imageset/a_filled.png new file mode 100644 index 0000000..cf0fa7f Binary files /dev/null and b/SOPA/Assets.xcassets/tiles/a_filled.imageset/a_filled.png differ diff --git a/SOPA/Assets.xcassets/tiles/c.imageset/Contents.json b/SOPA/Assets.xcassets/tiles/c.imageset/Contents.json index f71c70d..9f72339 100644 --- a/SOPA/Assets.xcassets/tiles/c.imageset/Contents.json +++ b/SOPA/Assets.xcassets/tiles/c.imageset/Contents.json @@ -2,11 +2,11 @@ "images" : [ { "idiom" : "universal", - "filename" : "g.png" + "filename" : "c.png" } ], "info" : { "version" : 1, "author" : "xcode" } -} \ No newline at end of file +} diff --git a/SOPA/Assets.xcassets/tiles/c.imageset/c.png b/SOPA/Assets.xcassets/tiles/c.imageset/c.png new file mode 100644 index 0000000..5ea3189 Binary files /dev/null and b/SOPA/Assets.xcassets/tiles/c.imageset/c.png differ diff --git a/SOPA/Assets.xcassets/tiles/c_filled.imageset/Contents.json b/SOPA/Assets.xcassets/tiles/c_filled.imageset/Contents.json index f71c70d..67739fc 100644 --- a/SOPA/Assets.xcassets/tiles/c_filled.imageset/Contents.json +++ b/SOPA/Assets.xcassets/tiles/c_filled.imageset/Contents.json @@ -2,11 +2,11 @@ "images" : [ { "idiom" : "universal", - "filename" : "g.png" + "filename" : "c_filled.png" } ], "info" : { "version" : 1, "author" : "xcode" } -} \ No newline at end of file +} diff --git a/SOPA/Assets.xcassets/tiles/c_filled.imageset/c_filled.png b/SOPA/Assets.xcassets/tiles/c_filled.imageset/c_filled.png new file mode 100644 index 0000000..a4d24d4 Binary files /dev/null and b/SOPA/Assets.xcassets/tiles/c_filled.imageset/c_filled.png differ diff --git a/SOPA/Assets.xcassets/tiles/e.imageset/Contents.json b/SOPA/Assets.xcassets/tiles/e.imageset/Contents.json index f71c70d..3004157 100644 --- a/SOPA/Assets.xcassets/tiles/e.imageset/Contents.json +++ b/SOPA/Assets.xcassets/tiles/e.imageset/Contents.json @@ -2,11 +2,11 @@ "images" : [ { "idiom" : "universal", - "filename" : "g.png" + "filename" : "e.png" } ], "info" : { "version" : 1, "author" : "xcode" } -} \ No newline at end of file +} diff --git a/SOPA/Assets.xcassets/tiles/e.imageset/e.png b/SOPA/Assets.xcassets/tiles/e.imageset/e.png new file mode 100644 index 0000000..608b4b7 Binary files /dev/null and b/SOPA/Assets.xcassets/tiles/e.imageset/e.png differ diff --git a/SOPA/Assets.xcassets/tiles/e_filled.imageset/Contents.json b/SOPA/Assets.xcassets/tiles/e_filled.imageset/Contents.json index f71c70d..0d614b3 100644 --- a/SOPA/Assets.xcassets/tiles/e_filled.imageset/Contents.json +++ b/SOPA/Assets.xcassets/tiles/e_filled.imageset/Contents.json @@ -2,11 +2,11 @@ "images" : [ { "idiom" : "universal", - "filename" : "g.png" + "filename" : "e_filled.png" } ], "info" : { "version" : 1, "author" : "xcode" } -} \ No newline at end of file +} diff --git a/SOPA/Assets.xcassets/tiles/e_filled.imageset/e_filled.png b/SOPA/Assets.xcassets/tiles/e_filled.imageset/e_filled.png new file mode 100644 index 0000000..23b5e47 Binary files /dev/null and b/SOPA/Assets.xcassets/tiles/e_filled.imageset/e_filled.png differ diff --git a/SOPA/Assets.xcassets/tiles/g.imageset/Contents.json b/SOPA/Assets.xcassets/tiles/g.imageset/Contents.json index f71c70d..9831764 100644 --- a/SOPA/Assets.xcassets/tiles/g.imageset/Contents.json +++ b/SOPA/Assets.xcassets/tiles/g.imageset/Contents.json @@ -9,4 +9,4 @@ "version" : 1, "author" : "xcode" } -} \ No newline at end of file +} diff --git a/SOPA/Assets.xcassets/tiles/g.imageset/g.png b/SOPA/Assets.xcassets/tiles/g.imageset/g.png index 78f7814..804a12b 100644 Binary files a/SOPA/Assets.xcassets/tiles/g.imageset/g.png and b/SOPA/Assets.xcassets/tiles/g.imageset/g.png differ diff --git a/SOPA/Assets.xcassets/tiles/g_filled.imageset/Contents.json b/SOPA/Assets.xcassets/tiles/g_filled.imageset/Contents.json index f71c70d..4dfac92 100644 --- a/SOPA/Assets.xcassets/tiles/g_filled.imageset/Contents.json +++ b/SOPA/Assets.xcassets/tiles/g_filled.imageset/Contents.json @@ -2,11 +2,11 @@ "images" : [ { "idiom" : "universal", - "filename" : "g.png" + "filename" : "g_filled.png" } ], "info" : { "version" : 1, "author" : "xcode" } -} \ No newline at end of file +} diff --git a/SOPA/Assets.xcassets/tiles/g_filled.imageset/g_filled.png b/SOPA/Assets.xcassets/tiles/g_filled.imageset/g_filled.png new file mode 100644 index 0000000..e3d58d8 Binary files /dev/null and b/SOPA/Assets.xcassets/tiles/g_filled.imageset/g_filled.png differ diff --git a/SOPA/Assets.xcassets/tiles/i.imageset/Contents.json b/SOPA/Assets.xcassets/tiles/i.imageset/Contents.json index 98c4b4e..11078e4 100644 --- a/SOPA/Assets.xcassets/tiles/i.imageset/Contents.json +++ b/SOPA/Assets.xcassets/tiles/i.imageset/Contents.json @@ -9,4 +9,4 @@ "version" : 1, "author" : "xcode" } -} \ No newline at end of file +} diff --git a/SOPA/Assets.xcassets/tiles/i.imageset/i.png b/SOPA/Assets.xcassets/tiles/i.imageset/i.png index 23b4513..2dab43e 100644 Binary files a/SOPA/Assets.xcassets/tiles/i.imageset/i.png and b/SOPA/Assets.xcassets/tiles/i.imageset/i.png differ diff --git a/SOPA/Assets.xcassets/tiles/i_filled.imageset/Contents.json b/SOPA/Assets.xcassets/tiles/i_filled.imageset/Contents.json index 98c4b4e..f8e2ec5 100644 --- a/SOPA/Assets.xcassets/tiles/i_filled.imageset/Contents.json +++ b/SOPA/Assets.xcassets/tiles/i_filled.imageset/Contents.json @@ -2,11 +2,11 @@ "images" : [ { "idiom" : "universal", - "filename" : "i.png" + "filename" : "i_filled.png" } ], "info" : { "version" : 1, "author" : "xcode" } -} \ No newline at end of file +} diff --git a/SOPA/Assets.xcassets/tiles/i_filled.imageset/i_filled.png b/SOPA/Assets.xcassets/tiles/i_filled.imageset/i_filled.png new file mode 100644 index 0000000..3754665 Binary files /dev/null and b/SOPA/Assets.xcassets/tiles/i_filled.imageset/i_filled.png differ diff --git a/SOPA/Assets.xcassets/tiles/o.imageset/Contents.json b/SOPA/Assets.xcassets/tiles/o.imageset/Contents.json index e4460a0..15d73a9 100644 --- a/SOPA/Assets.xcassets/tiles/o.imageset/Contents.json +++ b/SOPA/Assets.xcassets/tiles/o.imageset/Contents.json @@ -9,4 +9,4 @@ "version" : 1, "author" : "xcode" } -} \ No newline at end of file +} diff --git a/SOPA/Assets.xcassets/tiles/o.imageset/o.png b/SOPA/Assets.xcassets/tiles/o.imageset/o.png index af9eb82..6f082fb 100644 Binary files a/SOPA/Assets.xcassets/tiles/o.imageset/o.png and b/SOPA/Assets.xcassets/tiles/o.imageset/o.png differ diff --git a/SOPA/Assets.xcassets/tiles/u.imageset/Contents.json b/SOPA/Assets.xcassets/tiles/u.imageset/Contents.json index f71c70d..7754ae7 100644 --- a/SOPA/Assets.xcassets/tiles/u.imageset/Contents.json +++ b/SOPA/Assets.xcassets/tiles/u.imageset/Contents.json @@ -2,11 +2,11 @@ "images" : [ { "idiom" : "universal", - "filename" : "g.png" + "filename" : "u.png" } ], "info" : { "version" : 1, "author" : "xcode" } -} \ No newline at end of file +} diff --git a/SOPA/Assets.xcassets/tiles/u.imageset/u.png b/SOPA/Assets.xcassets/tiles/u.imageset/u.png new file mode 100644 index 0000000..6e7529f Binary files /dev/null and b/SOPA/Assets.xcassets/tiles/u.imageset/u.png differ diff --git a/SOPA/Assets.xcassets/tiles/u_filled.imageset/Contents.json b/SOPA/Assets.xcassets/tiles/u_filled.imageset/Contents.json index f71c70d..64e76c4 100644 --- a/SOPA/Assets.xcassets/tiles/u_filled.imageset/Contents.json +++ b/SOPA/Assets.xcassets/tiles/u_filled.imageset/Contents.json @@ -2,11 +2,11 @@ "images" : [ { "idiom" : "universal", - "filename" : "g.png" + "filename" : "u_filled.png" } ], "info" : { "version" : 1, "author" : "xcode" } -} \ No newline at end of file +} diff --git a/SOPA/Assets.xcassets/tiles/u_filled.imageset/u_filled.png b/SOPA/Assets.xcassets/tiles/u_filled.imageset/u_filled.png new file mode 100644 index 0000000..7280c46 Binary files /dev/null and b/SOPA/Assets.xcassets/tiles/u_filled.imageset/u_filled.png differ diff --git a/SOPA/Assets.xcassets/tutorial/Contents.json b/SOPA/Assets.xcassets/tutorial/Contents.json new file mode 100644 index 0000000..2d92bd5 --- /dev/null +++ b/SOPA/Assets.xcassets/tutorial/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/SOPA/Assets.xcassets/tutorial/firstTutorialScreenA.imageset/Contents.json b/SOPA/Assets.xcassets/tutorial/firstTutorialScreenA.imageset/Contents.json new file mode 100644 index 0000000..b82038b --- /dev/null +++ b/SOPA/Assets.xcassets/tutorial/firstTutorialScreenA.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "filename" : "firstTutorialScreenA.png", "scale" : "1x" }, + { "idiom" : "universal", "scale" : "2x" }, + { "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "version" : 1, "author" : "xcode" } +} diff --git a/SOPA/Assets.xcassets/tutorial/firstTutorialScreenA.imageset/firstTutorialScreenA.png b/SOPA/Assets.xcassets/tutorial/firstTutorialScreenA.imageset/firstTutorialScreenA.png new file mode 100644 index 0000000..03a7604 Binary files /dev/null and b/SOPA/Assets.xcassets/tutorial/firstTutorialScreenA.imageset/firstTutorialScreenA.png differ diff --git a/SOPA/Assets.xcassets/tutorial/firstTutorialScreenB.imageset/Contents.json b/SOPA/Assets.xcassets/tutorial/firstTutorialScreenB.imageset/Contents.json new file mode 100644 index 0000000..a3a5ffd --- /dev/null +++ b/SOPA/Assets.xcassets/tutorial/firstTutorialScreenB.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "filename" : "firstTutorialScreenB.png", "scale" : "1x" }, + { "idiom" : "universal", "scale" : "2x" }, + { "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "version" : 1, "author" : "xcode" } +} diff --git a/SOPA/Assets.xcassets/tutorial/firstTutorialScreenB.imageset/firstTutorialScreenB.png b/SOPA/Assets.xcassets/tutorial/firstTutorialScreenB.imageset/firstTutorialScreenB.png new file mode 100644 index 0000000..f3e8832 Binary files /dev/null and b/SOPA/Assets.xcassets/tutorial/firstTutorialScreenB.imageset/firstTutorialScreenB.png differ diff --git a/SOPA/Assets.xcassets/tutorial/secondTutorialScreenA.imageset/Contents.json b/SOPA/Assets.xcassets/tutorial/secondTutorialScreenA.imageset/Contents.json new file mode 100644 index 0000000..e49f7fa --- /dev/null +++ b/SOPA/Assets.xcassets/tutorial/secondTutorialScreenA.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "filename" : "secondTutorialScreenA.png", "scale" : "1x" }, + { "idiom" : "universal", "scale" : "2x" }, + { "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "version" : 1, "author" : "xcode" } +} diff --git a/SOPA/Assets.xcassets/tutorial/secondTutorialScreenA.imageset/secondTutorialScreenA.png b/SOPA/Assets.xcassets/tutorial/secondTutorialScreenA.imageset/secondTutorialScreenA.png new file mode 100644 index 0000000..5276d39 Binary files /dev/null and b/SOPA/Assets.xcassets/tutorial/secondTutorialScreenA.imageset/secondTutorialScreenA.png differ diff --git a/SOPA/Assets.xcassets/tutorial/secondTutorialScreenB.imageset/Contents.json b/SOPA/Assets.xcassets/tutorial/secondTutorialScreenB.imageset/Contents.json new file mode 100644 index 0000000..cb72b8e --- /dev/null +++ b/SOPA/Assets.xcassets/tutorial/secondTutorialScreenB.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "filename" : "secondTutorialScreenB.png", "scale" : "1x" }, + { "idiom" : "universal", "scale" : "2x" }, + { "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "version" : 1, "author" : "xcode" } +} diff --git a/SOPA/Assets.xcassets/tutorial/secondTutorialScreenB.imageset/secondTutorialScreenB.png b/SOPA/Assets.xcassets/tutorial/secondTutorialScreenB.imageset/secondTutorialScreenB.png new file mode 100644 index 0000000..d80857f Binary files /dev/null and b/SOPA/Assets.xcassets/tutorial/secondTutorialScreenB.imageset/secondTutorialScreenB.png differ diff --git a/SOPA/Assets.xcassets/tutorial/tutorialLetsGo.imageset/Contents.json b/SOPA/Assets.xcassets/tutorial/tutorialLetsGo.imageset/Contents.json new file mode 100644 index 0000000..9b6acb5 --- /dev/null +++ b/SOPA/Assets.xcassets/tutorial/tutorialLetsGo.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "idiom" : "universal", "filename" : "letsGo.png", "scale" : "1x" }, + { "idiom" : "universal", "scale" : "2x" }, + { "idiom" : "universal", "scale" : "3x" } + ], + "info" : { "version" : 1, "author" : "xcode" } +} diff --git a/SOPA/Assets.xcassets/tutorial/tutorialLetsGo.imageset/letsGo.png b/SOPA/Assets.xcassets/tutorial/tutorialLetsGo.imageset/letsGo.png new file mode 100644 index 0000000..fcd0c9a Binary files /dev/null and b/SOPA/Assets.xcassets/tutorial/tutorialLetsGo.imageset/letsGo.png differ diff --git a/SOPA/JustPlay/JustPlayGameScene.swift b/SOPA/JustPlay/JustPlayGameScene.swift index cfdc485..dc7aefd 100644 --- a/SOPA/JustPlay/JustPlayGameScene.swift +++ b/SOPA/JustPlay/JustPlayGameScene.swift @@ -216,7 +216,7 @@ class JustPlayGameScene: GameScene, JustPlaySceneObserver { private var leaveScene = false private var restartButton: SpriteButton? private var levelChoiceButton: SpriteButton? - private var leftTimeNode = SKLabelNode(fontNamed: "Optima-Bold") + private var leftTimeNode = SKLabelNode(fontNamed: "Impact") private var warningOverlay: SKSpriteNode? init(size: CGSize, proportionSet: ProportionSet, justPlayLevel: JustPlayLevel, justPlayService: JustPlayService, timeBasedGameService: TimeBasedGameService) { @@ -253,32 +253,77 @@ class JustPlayGameScene: GameScene, JustPlaySceneObserver { } override func addButtons() { - let restartSide = proportionSet.buttonSize() + let restartSide = min(size.width, size.height) * 0.18 restartButton = SpriteButton(texture: makeCircleButtonTexture(symbolName: "arrow.counterclockwise", side: restartSide), onClick: restartLevel) - restartButton!.position = proportionSet.restartButtonPos() + restartButton!.position = CGPoint(x: size.width * 0.86, y: size.height * 0.12) addChild(restartButton!) - let side = proportionSet.levelChoiceSize() - levelChoiceButton = SpriteButton(texture: makeCircleButtonTexture(symbolName: "chevron.left", side: side), onClick: loadStartMenuScene) - levelChoiceButton!.position = proportionSet.levelChoicePos() + let backSide = min(size.width, size.height) * 0.09 + levelChoiceButton = SpriteButton(texture: ButtonTextureFactory.makeMinimalBackTexture(side: backSide), onClick: loadStartMenuScene) + levelChoiceButton!.position = CGPoint(x: size.width * 0.08, y: size.height * 0.95) addChild(levelChoiceButton!) } + private let headingColor = UIColor(white: 0.90, alpha: 1.0) + private let valueColor = UIColor(white: 0.96, alpha: 1.0) + private let hudFont = "Impact" + + override func addStaticLabels() { + addChild(movesLabels) + + let minTitle = SKLabelNode(fontNamed: hudFont) + minTitle.text = "Min. Moves" + minTitle.horizontalAlignmentMode = .center + minTitle.verticalAlignmentMode = .center + minTitle.fontSize = min(size.width, size.height) * 0.055 + minTitle.fontColor = headingColor + minTitle.position = CGPoint(x: size.width * 0.14, y: size.height * 0.90) + movesLabels.addChild(minTitle) + + let minValue = SKLabelNode(fontNamed: hudFont) + minValue.text = String(gameService.getLevel().minimumMovesToSolve ?? 0) + minValue.horizontalAlignmentMode = .center + minValue.verticalAlignmentMode = .center + minValue.fontSize = min(size.width, size.height) * 0.15 + minValue.fontColor = valueColor + minValue.position = CGPoint(x: size.width * 0.04, y: size.height * 0.84) + movesLabels.addChild(minValue) + + let currentTitle = SKLabelNode(fontNamed: hudFont) + currentTitle.text = "Current Moves" + currentTitle.horizontalAlignmentMode = .center + currentTitle.verticalAlignmentMode = .center + currentTitle.fontSize = min(size.width, size.height) * 0.055 + currentTitle.fontColor = headingColor + currentTitle.position = CGPoint(x: size.width * 0.84, y: size.height * 0.90) + movesLabels.addChild(currentTitle) + } + + override func addDynamicLabels() { + currentMovesNode.text = String(gameService.getLevel().movesCounter) + currentMovesNode.horizontalAlignmentMode = .center + currentMovesNode.verticalAlignmentMode = .center + currentMovesNode.fontSize = min(size.width, size.height) * 0.15 + currentMovesNode.position = CGPoint(x: size.width * 0.80, y: size.height * 0.84) + currentMovesNode.fontColor = valueColor + movesLabels.addChild(currentMovesNode) + } + private func addTimerLabels() { - let leftTimeLabel = SKLabelNode(fontNamed: fontName) + let leftTimeLabel = SKLabelNode(fontNamed: hudFont) leftTimeLabel.text = "Left Time" leftTimeLabel.horizontalAlignmentMode = .left - leftTimeLabel.verticalAlignmentMode = .top - leftTimeLabel.position = CGPoint(x: proportionSet.movesFontSize() * -2.6, y: proportionSet.movesFontSize() * 1.4) - leftTimeLabel.fontSize = proportionSet.movesFontSize() - leftTimeLabel.fontColor = fontColor + leftTimeLabel.verticalAlignmentMode = .center + leftTimeLabel.fontSize = min(size.width, size.height) * 0.06 + leftTimeLabel.fontColor = headingColor + leftTimeLabel.position = CGPoint(x: size.width * 0.04, y: size.height * 0.11) movesLabels.addChild(leftTimeLabel) - leftTimeNode.horizontalAlignmentMode = .right - leftTimeNode.verticalAlignmentMode = .top - leftTimeNode.position = CGPoint(x: proportionSet.movesFontSize() * 2.7, y: proportionSet.movesFontSize() * 1.4) - leftTimeNode.fontSize = proportionSet.movesFontSize() - leftTimeNode.fontColor = fontColor + leftTimeNode.horizontalAlignmentMode = .left + leftTimeNode.verticalAlignmentMode = .center + leftTimeNode.fontSize = min(size.width, size.height) * 0.15 + leftTimeNode.fontColor = valueColor + leftTimeNode.position = CGPoint(x: size.width * 0.04, y: size.height * 0.045) movesLabels.addChild(leftTimeNode) } @@ -361,7 +406,7 @@ class JustPlayGameScene: GameScene, JustPlaySceneObserver { class JustPlayScoreScene: SKScene { private let justPlayResult: JustPlayResult private let textColor = UIColor(red: 240.0 / 255.0, green: 239.0 / 255.0, blue: 238.0 / 255.0, alpha: 1.0) - private let background = UIColor(red: 90.6 / 255.0, green: 86.7 / 255.0, blue: 70.6 / 255.0, alpha: 1.0) + private let background = UIColor.black init(size: CGSize, proportionSet: ProportionSet, justPlayResult: JustPlayResult) { self.justPlayResult = justPlayResult @@ -495,7 +540,7 @@ class JustPlayLostScene: SKScene { self.previousHighscore = ResourcesManager.getInstance().levelService?.getBestJustPlayScore() self.isNewHighscore = previousHighscore == nil || justPlayResult.score > previousHighscore!.points super.init(size: size) - self.backgroundColor = UIColor(red: 90.6 / 255.0, green: 86.7 / 255.0, blue: 70.6 / 255, alpha: 1.0) + self.backgroundColor = .black ResourcesManager.getInstance().levelService?.submitJustPlayScore(score: JustPlayScore(points: justPlayResult.score, solvedLevels: justPlayResult.levelCount)) if isNewHighscore { addConfetti() @@ -543,31 +588,40 @@ class JustPlayLostScene: SKScene { addChild(title) } - let score = SKLabelNode(fontNamed: "Optima-Bold") - score.text = "Score: \(justPlayResult.score)" - score.fontSize = size.height * 0.05 - score.fontColor = textColor - score.position = CGPoint(x: size.width / 2.0, y: size.height * 0.62) - score.zPosition = 2 - addChild(score) - - let level = SKLabelNode(fontNamed: "Optima-Bold") - level.text = "Reached Level: \(justPlayResult.levelCount)" - level.fontSize = size.height * 0.04 - level.fontColor = textColor - level.position = CGPoint(x: size.width / 2.0, y: size.height * 0.54) - level.zPosition = 2 - addChild(level) - - if let highscore = ResourcesManager.getInstance().levelService?.getBestJustPlayScore() { - let highscoreLabel = SKLabelNode(fontNamed: "Optima-Bold") - highscoreLabel.text = "Highscore: \(max(highscore.points, justPlayResult.score))" - highscoreLabel.fontSize = size.height * 0.04 - highscoreLabel.fontColor = textColor - highscoreLabel.position = CGPoint(x: size.width / 2.0, y: size.height * 0.46) - highscoreLabel.zPosition = 2 - addChild(highscoreLabel) - } + addStatBar(color: UIColor(red: 0.13, green: 0.35, blue: 1.0, alpha: 1.0), + label: "Score:", value: "\(justPlayResult.score)", + centerY: size.height * 0.60) + addStatBar(color: UIColor(red: 1.0, green: 0.93, blue: 0.0, alpha: 1.0), + label: "Level:", value: "\(justPlayResult.levelCount)", + centerY: size.height * 0.525) + } + + private func addStatBar(color: UIColor, label: String, value: String, centerY: CGFloat) { + let barHeight = size.height * 0.075 + let bar = SKSpriteNode(color: color, size: CGSize(width: size.width, height: barHeight)) + bar.position = CGPoint(x: size.width / 2.0, y: centerY) + bar.zPosition = 1 + addChild(bar) + + let labelNode = SKLabelNode(fontNamed: "Impact") + labelNode.text = label + labelNode.horizontalAlignmentMode = .left + labelNode.verticalAlignmentMode = .center + labelNode.fontSize = barHeight * 0.55 + labelNode.fontColor = .black + labelNode.position = CGPoint(x: size.width * 0.06, y: centerY) + labelNode.zPosition = 2 + addChild(labelNode) + + let valueNode = SKLabelNode(fontNamed: "Impact") + valueNode.text = value + valueNode.horizontalAlignmentMode = .right + valueNode.verticalAlignmentMode = .center + valueNode.fontSize = barHeight * 0.55 + valueNode.fontColor = .black + valueNode.position = CGPoint(x: size.width * 0.94, y: centerY) + valueNode.zPosition = 2 + addChild(valueNode) } private func addConfetti() { @@ -616,20 +670,50 @@ class JustPlayLostScene: SKScene { } private func addButtons() { - let side = proportionSet.buttonSize() + let side = min(size.width, size.height) * 0.20 + let buttonSize = CGSize(width: side, height: side) + let buttonY = size.height * 0.17 + + let back = SpriteButton(texture: makeCircleButtonTexture(symbolName: "arrow.left", side: side)) { + ResourcesManager.getInstance().storyService?.loadStartMenuScene() + } + back.size = buttonSize + back.position = CGPoint(x: size.width * 0.20, y: buttonY) + addChild(back) let restart = SpriteButton(texture: makeCircleButtonTexture(symbolName: "arrow.counterclockwise", side: side)) { ResourcesManager.getInstance().storyService?.loadJustPlaySceneFromMenuScene() } - restart.size = CGSize(width: side, height: side) - restart.position = CGPoint(x: size.width * 0.72, y: size.height * 0.18) + restart.size = buttonSize + restart.position = CGPoint(x: size.width * 0.50, y: buttonY) addChild(restart) - let levelChoice = SpriteButton(texture: makeCircleButtonTexture(symbolName: "chevron.left", side: proportionSet.buttonSize())) { - ResourcesManager.getInstance().storyService?.loadStartMenuScene() + let share = SpriteButton(texture: ButtonTextureFactory.makeCircleTextButtonTexture(title: "SHARE\nSCORE", side: side)) { [weak self] in + self?.shareScore() } - levelChoice.size = CGSize(width: proportionSet.buttonSize(), height: proportionSet.buttonSize()) - levelChoice.position = CGPoint(x: size.width * 0.28, y: size.height * 0.18) - addChild(levelChoice) + share.size = buttonSize + share.position = CGPoint(x: size.width * 0.80, y: buttonY) + addChild(share) + } + + private func shareScore() { + guard let controller = topPresentedViewController() else { + return + } + let text = "I reached level \(justPlayResult.levelCount) with \(justPlayResult.score) points in SOPA!" + let shareController = UIActivityViewController(activityItems: [text], applicationActivities: nil) + shareController.popoverPresentationController?.sourceView = controller.view + controller.present(shareController, animated: true) + } + + private func topPresentedViewController() -> UIViewController? { + var controller = UIApplication.shared.connectedScenes + .compactMap { $0 as? UIWindowScene } + .flatMap { $0.windows } + .first { $0.isKeyWindow }?.rootViewController + while let presented = controller?.presentedViewController { + controller = presented + } + return controller } } diff --git a/SOPA/view/GameScene.swift b/SOPA/view/GameScene.swift index f177f61..768e4db 100644 --- a/SOPA/view/GameScene.swift +++ b/SOPA/view/GameScene.swift @@ -29,8 +29,7 @@ class GameScene: SKScene { addButtons() addStaticLabels() addDynamicLabels() - //self.backgroundColor = UIColor(red: 169.0 / 255.0, green: 162.0 / 255.0, blue: 121.0 / 255, alpha: 1.0)greenbrown - self.backgroundColor = UIColor(red: 90.6 / 255.0, green: 86.7 / 255.0, blue: 70.6 / 255, alpha: 1.0) + self.backgroundColor = .black } diff --git a/SOPA/view/LevelMode/ButtonTextureFactory.swift b/SOPA/view/LevelMode/ButtonTextureFactory.swift new file mode 100644 index 0000000..e73b1d3 --- /dev/null +++ b/SOPA/view/LevelMode/ButtonTextureFactory.swift @@ -0,0 +1,173 @@ +import SpriteKit +import UIKit + +enum ButtonTextureFactory { + static func makeCircleButtonTexture( + symbolName: String, + side: CGFloat, + circleColor: UIColor = SopaTheme.circleButtonFill, + iconColor: UIColor = SopaTheme.circleButtonIcon, + symbolScale: CGFloat = 0.42, + symbolWeight: UIImage.SymbolWeight = .semibold + ) -> SKTexture { + let textureSize = CGSize(width: side, height: side) + let renderer = UIGraphicsImageRenderer(size: textureSize) + let image = renderer.image { _ in + circleColor.setFill() + UIBezierPath(ovalIn: CGRect(origin: .zero, size: textureSize)).fill() + + let config = UIImage.SymbolConfiguration(pointSize: side * symbolScale, weight: symbolWeight) + if let symbol = UIImage(systemName: symbolName, withConfiguration: config)? + .withTintColor(iconColor, renderingMode: .alwaysOriginal) { + let symbolRect = CGRect( + x: (side - symbol.size.width) / 2.0, + y: (side - symbol.size.height) / 2.0, + width: symbol.size.width, + height: symbol.size.height + ) + symbol.draw(in: symbolRect) + } + } + return SKTexture(image: image) + } + + static func makeMinimalBackTexture(side: CGFloat) -> SKTexture { + let textureSize = CGSize(width: side, height: side) + let renderer = UIGraphicsImageRenderer(size: textureSize) + let image = renderer.image { _ in + let config = UIImage.SymbolConfiguration(pointSize: side * 0.72, weight: .semibold) + if let symbol = UIImage(systemName: "chevron.left", withConfiguration: config)? + .withTintColor(UIColor(red: 160.0 / 255.0, green: 164.0 / 255.0, blue: 170.0 / 255.0, alpha: 0.95), renderingMode: .alwaysOriginal) { + let symbolRect = CGRect( + x: (side - symbol.size.width) / 2.0, + y: (side - symbol.size.height) / 2.0, + width: symbol.size.width, + height: symbol.size.height + ) + symbol.draw(in: symbolRect) + } + } + return SKTexture(image: image) + } + + static func makePageArrowTexture(imageNamed: String, side: CGFloat) -> SKTexture { + let texture = SKTexture(imageNamed: imageNamed) + let imageSize = texture.size() + let ratio = imageSize.width / max(1, imageSize.height) + let targetSize = CGSize(width: side * ratio, height: side) + let renderer = UIGraphicsImageRenderer(size: targetSize) + let image = renderer.image { _ in + UIImage(named: imageNamed)?.draw(in: CGRect(origin: .zero, size: targetSize)) + } + return SKTexture(image: image) + } + + static func makeNeonTextTexture(title: String, size: CGSize) -> SKTexture { + let renderer = UIGraphicsImageRenderer(size: size) + let image = renderer.image { _ in + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = .center + + let glowShadow = NSShadow() + glowShadow.shadowColor = SopaTheme.neonGlowBlue + glowShadow.shadowBlurRadius = size.height * 0.22 + glowShadow.shadowOffset = .zero + + let font = UIFont(name: "HelveticaNeue-Light", size: size.height * 0.60) + ?? UIFont.systemFont(ofSize: size.height * 0.60, weight: .light) + let textRect = CGRect(x: 0, y: size.height * 0.22, width: size.width, height: size.height * 0.60) + + let glowAttributes: [NSAttributedString.Key: Any] = [ + .font: font, + .foregroundColor: SopaTheme.neonBlue, + .strokeColor: SopaTheme.neonBlue, + .strokeWidth: -1.2, + .paragraphStyle: paragraphStyle, + .shadow: glowShadow + ] + title.draw(in: textRect, withAttributes: glowAttributes) + + let coreAttributes: [NSAttributedString.Key: Any] = [ + .font: font, + .foregroundColor: SopaTheme.neonCoreBlue, + .paragraphStyle: paragraphStyle + ] + title.draw(in: textRect, withAttributes: coreAttributes) + } + return SKTexture(image: image) + } + + static func makeCircleTextButtonTexture( + title: String, + side: CGFloat, + circleColor: UIColor = SopaTheme.circleButtonFill, + textColor: UIColor = SopaTheme.circleButtonIcon + ) -> SKTexture { + let textureSize = CGSize(width: side, height: side) + let renderer = UIGraphicsImageRenderer(size: textureSize) + let image = renderer.image { _ in + circleColor.setFill() + UIBezierPath(ovalIn: CGRect(origin: .zero, size: textureSize)).fill() + + let lines = title.split(separator: "\n").map(String.init) + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = .center + let fontSize = side * 0.20 + let font = UIFont(name: "Impact", size: fontSize) ?? UIFont.systemFont(ofSize: fontSize, weight: .bold) + let attributes: [NSAttributedString.Key: Any] = [ + .font: font, + .foregroundColor: textColor, + .paragraphStyle: paragraphStyle + ] + let lineHeight = fontSize * 1.05 + let totalHeight = lineHeight * CGFloat(lines.count) + var y = (side - totalHeight) / 2.0 + for line in lines { + line.draw(in: CGRect(x: 0, y: y, width: side, height: lineHeight), withAttributes: attributes) + y += lineHeight + } + } + return SKTexture(image: image) + } + + static func makeTextButtonTexture( + title: String, + size: CGSize, + fontName: String, + textColor: UIColor, + weight: UIFont.Weight = .semibold + ) -> SKTexture { + let renderer = UIGraphicsImageRenderer(size: size) + let image = renderer.image { _ in + let frame = CGRect(origin: .zero, size: size).insetBy(dx: size.width * 0.01, dy: size.height * 0.06) + let backgroundPath = UIBezierPath(roundedRect: frame, cornerRadius: size.height * 0.23) + UIColor(white: 1.0, alpha: 0.10).setFill() + backgroundPath.fill() + UIColor(white: 1.0, alpha: 0.22).setStroke() + backgroundPath.lineWidth = size.height * 0.08 + backgroundPath.stroke() + + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = .center + let buttonFontSize = LabelSizing.fittedTextFontSize( + text: title, + fontName: fontName, + preferredSize: size.height * 0.38, + weight: weight, + maxWidth: size.width * 0.90 + ) + let attributes: [NSAttributedString.Key: Any] = [ + .font: UIFont(name: fontName, size: buttonFontSize) ?? UIFont.systemFont(ofSize: buttonFontSize, weight: weight), + .foregroundColor: textColor, + .paragraphStyle: paragraphStyle + ] + let textRect = CGRect(x: 0.0, y: size.height * 0.30, width: size.width, height: size.height * 0.5) + title.draw(in: textRect, withAttributes: attributes) + } + return SKTexture(image: image) + } +} + +func makeCircleButtonTexture(symbolName: String, side: CGFloat) -> SKTexture { + ButtonTextureFactory.makeCircleButtonTexture(symbolName: symbolName, side: side) +} diff --git a/SOPA/view/LevelMode/CreditsScene.swift b/SOPA/view/LevelMode/CreditsScene.swift new file mode 100644 index 0000000..63a9359 --- /dev/null +++ b/SOPA/view/LevelMode/CreditsScene.swift @@ -0,0 +1,89 @@ +import Foundation +import SpriteKit +import UIKit + +class CreditsScene: SKScene { + private let sectionColor = SopaTheme.neonBlue + + override init(size: CGSize) { + super.init(size: size) + backgroundColor = SopaTheme.blackBackground + addBackButton() + addTitle() + addCreditsContent() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func addBackButton() { + let side = size.height * 0.08 + let backButton = SpriteButton(texture: ButtonTextureFactory.makeCircleButtonTexture(symbolName: "chevron.left", side: side)) { + ResourcesManager.getInstance().storyService?.loadStartMenuScene() + } + backButton.position = CGPoint(x: size.height * 0.057, y: size.height * 0.91) + addChild(backButton) + } + + private func addTitle() { + let title = SKLabelNode(fontNamed: "Impact") + title.text = "CREDITS" + title.fontSize = size.height * 0.09 + title.fontColor = SopaTheme.titleGray + title.position = CGPoint(x: size.width * 0.5, y: size.height * 0.78) + addChild(title) + } + + private func addCreditsContent() { + let content: [(section: String, entries: [String])] = [ + ("DEVELOPMENT", ["David Schilling - @schillda710", "Raphael Schilling - @ubuntius"]), + ("DESIGN", ["Raphael Schilling - @ubuntius"]), + ("MUSIC", ["Menu - axtoncrolley on opengameart.org"]) + ] + + var currentY = size.height * 0.70 + for block in content { + addSectionLine(text: block.section, y: currentY) + currentY -= size.height * 0.045 + + for entry in block.entries { + addBodyLine(text: entry, y: currentY) + currentY -= size.height * 0.038 + } + currentY -= size.height * 0.028 + } + } + + private func addSectionLine(text: String, y: CGFloat) { + let line = SKLabelNode(fontNamed: "HelveticaNeue-Bold") + line.horizontalAlignmentMode = .left + line.verticalAlignmentMode = .center + line.text = text + line.fontSize = LabelSizing.fittedLabelFontSize( + text: text, + fontName: "HelveticaNeue-Bold", + preferredSize: size.height * 0.032, + maxWidth: size.width * 0.88 + ) + line.fontColor = sectionColor + line.position = CGPoint(x: size.width * 0.06, y: y) + addChild(line) + } + + private func addBodyLine(text: String, y: CGFloat) { + let line = SKLabelNode(fontNamed: "HelveticaNeue") + line.horizontalAlignmentMode = .left + line.verticalAlignmentMode = .center + line.text = text + line.fontSize = LabelSizing.fittedLabelFontSize( + text: text, + fontName: "HelveticaNeue", + preferredSize: size.height * 0.026, + maxWidth: size.width * 0.90 + ) + line.fontColor = SopaTheme.bodyText + line.position = CGPoint(x: size.width * 0.09, y: y) + addChild(line) + } +} diff --git a/SOPA/view/LevelMode/LabelSizing.swift b/SOPA/view/LevelMode/LabelSizing.swift new file mode 100644 index 0000000..fd4e7e6 --- /dev/null +++ b/SOPA/view/LevelMode/LabelSizing.swift @@ -0,0 +1,47 @@ +import SpriteKit +import UIKit + +enum LabelSizing { + static func fittedLabelFontSize( + text: String, + fontName: String, + preferredSize: CGFloat, + maxWidth: CGFloat, + minSize: CGFloat = 10.0 + ) -> CGFloat { + guard !text.isEmpty else { + return preferredSize + } + + let label = SKLabelNode(fontNamed: fontName) + label.text = text + var currentSize = preferredSize + label.fontSize = currentSize + + while label.frame.width > maxWidth && currentSize > minSize { + currentSize -= 1.0 + label.fontSize = currentSize + } + return currentSize + } + + static func fittedTextFontSize( + text: String, + fontName: String, + preferredSize: CGFloat, + weight: UIFont.Weight, + maxWidth: CGFloat, + minSize: CGFloat = 10.0 + ) -> CGFloat { + var currentSize = preferredSize + while currentSize > minSize { + let font = UIFont(name: fontName, size: currentSize) ?? UIFont.systemFont(ofSize: currentSize, weight: weight) + let measuredWidth = (text as NSString).size(withAttributes: [.font: font]).width + if measuredWidth <= maxWidth { + return currentSize + } + currentSize -= 1.0 + } + return minSize + } +} diff --git a/SOPA/view/LevelMode/LevelButtonArea.swift b/SOPA/view/LevelMode/LevelButtonArea.swift index 64e82c8..b2a1747 100644 --- a/SOPA/view/LevelMode/LevelButtonArea.swift +++ b/SOPA/view/LevelMode/LevelButtonArea.swift @@ -68,18 +68,16 @@ class LevelButtonArea: SKSpriteNode { } func tabOn(location: CGPoint) { + // Resolve the tapped node to its enclosing level button. The button's visuals are a mix + // of shape (neon frame), label (number) and sprite (stars) children, so walk up the + // parent chain instead of special-casing individual node types. var node: SKNode? = atPoint(location) - - if !(node is LevelSelectButton) { - if node is SKLabelNode || node is SKSpriteNode { - node = node?.parent - } + while node != nil && !(node is LevelSelectButton) { + node = node?.parent } - - if node is LevelSelectButton { - if !(node as! LevelSelectButton).levelInfo.locked { - ResourcesManager.getInstance().storyService?.loadLevelModeGameSceneFromChoiceScene(levelId: (node as! LevelSelectButton).levelInfo.levelId) - } + + if let levelButton = node as? LevelSelectButton, !levelButton.levelInfo.locked { + ResourcesManager.getInstance().storyService?.loadLevelModeGameSceneFromChoiceScene(levelId: levelButton.levelInfo.levelId) } } diff --git a/SOPA/view/LevelMode/LevelButtonPositioner.swift b/SOPA/view/LevelMode/LevelButtonPositioner.swift index 35d34c0..167fee2 100644 --- a/SOPA/view/LevelMode/LevelButtonPositioner.swift +++ b/SOPA/view/LevelMode/LevelButtonPositioner.swift @@ -16,7 +16,7 @@ class LevelButtonPositioner { init(size: CGSize) { self.size = size - self.buttonSize = CGSize(width: size.width * 0.3, height: size.width * 0.3) + self.buttonSize = CGSize(width: size.width * 0.24, height: size.width * 0.24) self.drawingHeight = size.height } @@ -26,7 +26,7 @@ class LevelButtonPositioner { let column = idOnPage % 3 let row = idOnPage / 3 let xPos: CGFloat = size.width / 4.0 * CGFloat(1 + column) + (size.width * CGFloat(page)) - let yPos: CGFloat = size.height - (drawingHeight / 5.0 * CGFloat(row + 1)) + let yPos: CGFloat = size.height - (drawingHeight / 5.0 * CGFloat(row + 1)) + size.height * 0.03 let position = CGPoint(x: xPos, y: yPos) return position diff --git a/SOPA/view/LevelMode/LevelChoiceScene.swift b/SOPA/view/LevelMode/LevelChoiceScene.swift index f0f8af7..f422720 100644 --- a/SOPA/view/LevelMode/LevelChoiceScene.swift +++ b/SOPA/view/LevelMode/LevelChoiceScene.swift @@ -10,600 +10,71 @@ import Foundation import SpriteKit import UIKit -func makeCircleButtonTexture(symbolName: String, side: CGFloat) -> SKTexture { - let textureSize = CGSize(width: side, height: side) - let circleColor = UIColor(white: 0.94, alpha: 1.0) - let iconColor = UIColor(red: 90.6 / 255.0, green: 86.7 / 255.0, blue: 70.6 / 255.0, alpha: 1.0) - let renderer = UIGraphicsImageRenderer(size: textureSize) - let image = renderer.image { _ in - circleColor.setFill() - UIBezierPath(ovalIn: CGRect(origin: .zero, size: textureSize)).fill() - - let config = UIImage.SymbolConfiguration(pointSize: side * 0.42, weight: .semibold) - if let symbol = UIImage(systemName: symbolName, withConfiguration: config)? - .withTintColor(iconColor, renderingMode: .alwaysOriginal) { - let symbolRect = CGRect( - x: (side - symbol.size.width) / 2.0, - y: (side - symbol.size.height) / 2.0, - width: symbol.size.width, - height: symbol.size.height - ) - symbol.draw(in: symbolRect) - } - } - return SKTexture(image: image) -} - class LevelChoiceScene: SKScene { private let levelInfos: [LevelInfo] private var levelButtonArea: LevelButtonArea? private var menuButton: SpriteButton? + private var leftArrowButton: SpriteButton? + private var rightArrowButton: SpriteButton? init(size: CGSize, levelService: LevelService) { levelInfos = levelService.getLevelInfos() super.init(size: size) levelButtonArea = LevelButtonArea(size: size, levelInfos: levelInfos, update: update) addChild(levelButtonArea!) + backgroundColor = .black addButtons() - self.backgroundColor = UIColor(red: 90.6 / 255.0, green: 86.7 / 255.0, blue: 70.6 / 255, alpha: 1.0) - - - } - - private func addButtons() { - let side = size.height * 0.08 - menuButton = SpriteButton(texture: makeCircleButtonTexture(symbolName: "chevron.left", side: side), onClick: backToStartMenu) - menuButton?.position = CGPoint(x: size.height * 0.057, y: size.height * 0.91) - addChild(menuButton!) - } - - private func backToStartMenu() { - ResourcesManager.getInstance().storyService?.loadStartMenuScene() - } - - func update() { - // Intentionally empty: level paging is swipe-only. - } - - - - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") + addPageArrows() + update() } -} -class StartMenuScene: SKScene { - private let background = UIColor(red: 90.6 / 255.0, green: 86.7 / 255.0, blue: 70.6 / 255, alpha: 1.0) - private let textColor = UIColor(red: 240.0 / 255.0, green: 239.0 / 255.0, blue: 238.0 / 255.0, alpha: 1.0) - - override init(size: CGSize) { - super.init(size: size) - self.backgroundColor = background - addTitle() - addButtons() - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func addTitle() { - let title = SKLabelNode(fontNamed: "Optima-Bold") - title.text = "SOPA" - title.fontSize = size.height * 0.12 - title.fontColor = textColor - title.position = CGPoint(x: size.width / 2, y: size.height * 0.78) - addChild(title) - - let subtitle = SKLabelNode(fontNamed: "Optima-Bold") - subtitle.text = "Choose a mode" - subtitle.fontSize = size.height * 0.04 - subtitle.fontColor = textColor - subtitle.position = CGPoint(x: size.width / 2, y: size.height * 0.70) - addChild(subtitle) - } - private func addButtons() { - let horizontalPadding = size.width * 0.10 - let interButtonGap = size.width * 0.06 - let maxButtonWidthFromScreen = (size.width - (horizontalPadding * 2.0) - interButtonGap) / 2.0 - let buttonSize = min(size.height * 0.20, maxButtonWidthFromScreen) - let iconTextureSize = CGSize(width: buttonSize, height: buttonSize) - let totalGroupWidth = buttonSize * 2.0 + interButtonGap - let leftCenterX = (size.width - totalGroupWidth) / 2.0 + buttonSize / 2.0 - let rightCenterX = leftCenterX + buttonSize + interButtonGap - let buttonCenterY = size.height * 0.42 - - let levelModeButton = SpriteButton(texture: makeModeTexture(symbolName: "square.grid.2x2", textureSize: iconTextureSize)) { - ResourcesManager.getInstance().storyService?.loadLevelCoiceScene() - } - levelModeButton.position = CGPoint(x: leftCenterX, y: buttonCenterY) - addChild(levelModeButton) - - let levelModeLabel = SKLabelNode(fontNamed: "Optima-Bold") - levelModeLabel.text = "Level Mode" - levelModeLabel.fontSize = size.height * 0.035 - levelModeLabel.fontColor = textColor - levelModeLabel.position = CGPoint(x: 0, y: -buttonSize * 0.70) - levelModeButton.addChild(levelModeLabel) - - let justPlayButton = SpriteButton(texture: makeModeTexture(symbolName: "bolt.fill", textureSize: iconTextureSize)) { - ResourcesManager.getInstance().storyService?.loadJustPlaySceneFromMenuScene() - } - justPlayButton.position = CGPoint(x: rightCenterX, y: buttonCenterY) - addChild(justPlayButton) - - let justPlayLabel = SKLabelNode(fontNamed: "Optima-Bold") - justPlayLabel.text = "Just Play" - justPlayLabel.fontSize = size.height * 0.035 - justPlayLabel.fontColor = textColor - justPlayLabel.position = CGPoint(x: 0, y: -buttonSize * 0.70) - justPlayButton.addChild(justPlayLabel) - - let utilityButtonY = size.height * 0.20 - let utilityButtonSize = CGSize(width: size.width * 0.30, height: size.height * 0.075) - let utilityGap = size.width * 0.04 - let totalUtilityWidth = utilityButtonSize.width * 2.0 + utilityGap - let firstUtilityX = (size.width - totalUtilityWidth) / 2.0 + utilityButtonSize.width / 2.0 - - let tutorialButton = SpriteButton(texture: makeTextButtonTexture(title: "Tutorial", size: utilityButtonSize)) { - ResourcesManager.getInstance().storyService?.loadTutorialSceneFromMenuScene() - } - tutorialButton.position = CGPoint(x: firstUtilityX, y: utilityButtonY) - addChild(tutorialButton) - - let creditsButton = SpriteButton(texture: makeTextButtonTexture(title: "Credits", size: utilityButtonSize)) { - ResourcesManager.getInstance().storyService?.loadCreditsSceneFromMenuScene() - } - creditsButton.position = CGPoint(x: firstUtilityX + utilityButtonSize.width + utilityGap, y: utilityButtonY) - addChild(creditsButton) - } - - private func makeModeTexture(symbolName: String, textureSize: CGSize) -> SKTexture { - let renderer = UIGraphicsImageRenderer(size: textureSize) - let image = renderer.image { _ in - let frame = CGRect(origin: .zero, size: textureSize).insetBy(dx: textureSize.width * 0.05, dy: textureSize.height * 0.05) - let backgroundPath = UIBezierPath(roundedRect: frame, cornerRadius: textureSize.width * 0.18) - UIColor(white: 1.0, alpha: 0.10).setFill() - backgroundPath.fill() - UIColor(white: 1.0, alpha: 0.22).setStroke() - backgroundPath.lineWidth = textureSize.width * 0.03 - backgroundPath.stroke() - - let config = UIImage.SymbolConfiguration(pointSize: textureSize.width * 0.44, weight: .semibold) - if let symbol = UIImage(systemName: symbolName, withConfiguration: config)? - .withTintColor(textColor, renderingMode: .alwaysOriginal) { - let symbolRect = CGRect( - x: (textureSize.width - symbol.size.width) / 2.0, - y: (textureSize.height - symbol.size.height) / 2.0, - width: symbol.size.width, - height: symbol.size.height - ) - symbol.draw(in: symbolRect) - } - } - return SKTexture(image: image) - } - - private func makeTextButtonTexture(title: String, size: CGSize) -> SKTexture { - let renderer = UIGraphicsImageRenderer(size: size) - let image = renderer.image { _ in - let frame = CGRect(origin: .zero, size: size).insetBy(dx: size.width * 0.01, dy: size.height * 0.04) - let backgroundPath = UIBezierPath(roundedRect: frame, cornerRadius: size.height * 0.25) - UIColor(white: 1.0, alpha: 0.10).setFill() - backgroundPath.fill() - UIColor(white: 1.0, alpha: 0.22).setStroke() - backgroundPath.lineWidth = size.height * 0.07 - backgroundPath.stroke() - - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.alignment = .center - let attributes: [NSAttributedString.Key: Any] = [ - .font: UIFont(name: "Optima-Bold", size: size.height * 0.40) ?? UIFont.systemFont(ofSize: size.height * 0.40, weight: .semibold), - .foregroundColor: textColor, - .paragraphStyle: paragraphStyle - ] - let textRect = CGRect(x: 0.0, y: size.height * 0.28, width: size.width, height: size.height * 0.5) - title.draw(in: textRect, withAttributes: attributes) - } - return SKTexture(image: image) - } -} - -class CreditsScene: SKScene { - private let background = UIColor(red: 90.6 / 255.0, green: 86.7 / 255.0, blue: 70.6 / 255, alpha: 1.0) - private let textColor = UIColor(red: 240.0 / 255.0, green: 239.0 / 255.0, blue: 238.0 / 255.0, alpha: 1.0) - - override init(size: CGSize) { - super.init(size: size) - self.backgroundColor = background - addBackButton() - addTitle() - addLines() - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func addBackButton() { - let side = size.height * 0.08 - let backButton = SpriteButton(texture: makeCircleButtonTexture(symbolName: "chevron.left", side: side)) { - ResourcesManager.getInstance().storyService?.loadStartMenuScene() - } - backButton.position = CGPoint(x: size.height * 0.057, y: size.height * 0.91) - addChild(backButton) - } - - private func addTitle() { - let title = SKLabelNode(fontNamed: "Optima-Bold") - title.text = "Credits" - title.fontSize = size.height * 0.10 - title.fontColor = textColor - title.position = CGPoint(x: size.width * 0.5, y: size.height * 0.78) - addChild(title) - } - - private func addLines() { - addLine(text: "Design and Development", y: size.height * 0.62, sizeFactor: 0.038) - addLine(text: "David Schilling", y: size.height * 0.55, sizeFactor: 0.048) - addLine(text: "Raphael Schilling", y: size.height * 0.49, sizeFactor: 0.048) - addLine(text: "Gameplay and Product Iteration", y: size.height * 0.39, sizeFactor: 0.038) - addLine(text: "SOPA Team", y: size.height * 0.33, sizeFactor: 0.048) - } - - private func addLine(text: String, y: CGFloat, sizeFactor: CGFloat) { - let line = SKLabelNode(fontNamed: "Optima-Bold") - line.text = text - line.fontSize = fittedFontSize(for: text, preferredSize: size.height * sizeFactor, maxWidth: size.width * 0.86) - line.fontColor = textColor - line.position = CGPoint(x: size.width * 0.5, y: y) - addChild(line) - } - - private func fittedFontSize(for text: String, preferredSize: CGFloat, maxWidth: CGFloat) -> CGFloat { - let label = SKLabelNode(fontNamed: "Optima-Bold") - label.text = text - var currentSize = preferredSize - label.fontSize = currentSize - - while label.frame.width > maxWidth && currentSize > 10.0 { - currentSize -= 1.0 - label.fontSize = currentSize - } - return currentSize - } -} - -class TutorialScene: SKScene { - private let background = UIColor(red: 90.6 / 255.0, green: 86.7 / 255.0, blue: 70.6 / 255, alpha: 1.0) - private let textColor = UIColor(red: 240.0 / 255.0, green: 239.0 / 255.0, blue: 238.0 / 255.0, alpha: 1.0) - private var currentPage = 0 - private let headline = SKLabelNode(fontNamed: "Optima-Bold") - private let textLine1 = SKLabelNode(fontNamed: "Optima-Bold") - private let textLine2 = SKLabelNode(fontNamed: "Optima-Bold") - private let textLine3 = SKLabelNode(fontNamed: "Optima-Bold") - private let tapHint = SKLabelNode(fontNamed: "Optima-Bold") - private var letsGoButton: SpriteButton? - - override init(size: CGSize) { - super.init(size: size) - self.backgroundColor = background - addBackButton() - addTitle() - addContentNodes() - updatePage() - } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func addBackButton() { - let side = size.height * 0.08 - let backButton = SpriteButton(texture: makeCircleButtonTexture(symbolName: "chevron.left", side: side)) { - ResourcesManager.getInstance().storyService?.loadStartMenuScene() - } - backButton.position = CGPoint(x: size.height * 0.057, y: size.height * 0.91) - addChild(backButton) - } - - private func addTitle() { - let title = SKLabelNode(fontNamed: "Optima-Bold") - title.text = "Tutorial" - title.fontSize = size.height * 0.10 - title.fontColor = textColor - title.position = CGPoint(x: size.width * 0.5, y: size.height * 0.78) - addChild(title) - } - - private func addContentNodes() { - headline.fontColor = textColor - headline.fontSize = size.height * 0.048 - headline.position = CGPoint(x: size.width * 0.5, y: size.height * 0.62) - addChild(headline) - - textLine1.fontColor = textColor - textLine1.fontSize = size.height * 0.034 - textLine1.position = CGPoint(x: size.width * 0.5, y: size.height * 0.54) - addChild(textLine1) - - textLine2.fontColor = textColor - textLine2.fontSize = size.height * 0.034 - textLine2.position = CGPoint(x: size.width * 0.5, y: size.height * 0.48) - addChild(textLine2) - - textLine3.fontColor = textColor - textLine3.fontSize = size.height * 0.034 - textLine3.position = CGPoint(x: size.width * 0.5, y: size.height * 0.42) - addChild(textLine3) - - tapHint.fontColor = textColor - tapHint.fontSize = size.height * 0.030 - tapHint.position = CGPoint(x: size.width * 0.5, y: size.height * 0.26) - addChild(tapHint) - } - - override func touchesBegan(_ touches: Set, with event: UIEvent?) { - if currentPage < 2 { - currentPage += 1 - updatePage() - } - } - - private func updatePage() { - switch currentPage { - case 0: - setPage( - headlineText: "Goal", - line1: "Connect the start and finish pipes.", - line2: "Swipe rows or columns to move tiles.", - line3: "Only matching pipe segments will connect.", - hint: "Tap anywhere to continue" - ) - letsGoButton?.isHidden = true - case 1: - setPage( - headlineText: "How It Works", - line1: "Horizontal swipe moves a full row.", - line2: "Vertical swipe moves a full column.", - line3: "Try to solve it in as few moves as possible.", - hint: "Tap anywhere to continue" - ) - letsGoButton?.isHidden = true - default: - setPage( - headlineText: "Try It Now", - line1: "Next you play a simple interactive level.", - line2: "Use restart if you want to try again.", - line3: "", - hint: "" - ) - ensureLetsGoButton() - letsGoButton?.isHidden = false - } - } - - private func setPage(headlineText: String, line1: String, line2: String, line3: String, hint: String) { - headline.text = headlineText - headline.fontSize = fittedFontSize(for: headlineText, preferredSize: size.height * 0.048, maxWidth: size.width * 0.90) - - textLine1.text = line1 - textLine1.fontSize = fittedFontSize(for: line1, preferredSize: size.height * 0.034, maxWidth: size.width * 0.92) - textLine2.text = line2 - textLine2.fontSize = fittedFontSize(for: line2, preferredSize: size.height * 0.034, maxWidth: size.width * 0.92) - textLine3.text = line3 - textLine3.fontSize = fittedFontSize(for: line3, preferredSize: size.height * 0.034, maxWidth: size.width * 0.92) - tapHint.text = hint - tapHint.fontSize = fittedFontSize(for: hint, preferredSize: size.height * 0.030, maxWidth: size.width * 0.92) - } - - private func ensureLetsGoButton() { - if letsGoButton != nil { - return - } - - let buttonSize = CGSize(width: size.width * 0.34, height: size.height * 0.10) - let button = SpriteButton(texture: makeTextButtonTexture(title: "Let's Go", size: buttonSize)) { - ResourcesManager.getInstance().storyService?.loadTutorialGameSceneFromTutorialScene() - } - button.position = CGPoint(x: size.width * 0.5, y: size.height * 0.25) - addChild(button) - letsGoButton = button - } - - private func makeTextButtonTexture(title: String, size: CGSize) -> SKTexture { - let renderer = UIGraphicsImageRenderer(size: size) - let image = renderer.image { _ in - let frame = CGRect(origin: .zero, size: size).insetBy(dx: size.width * 0.01, dy: size.height * 0.06) - let backgroundPath = UIBezierPath(roundedRect: frame, cornerRadius: size.height * 0.23) - UIColor(white: 1.0, alpha: 0.10).setFill() - backgroundPath.fill() - UIColor(white: 1.0, alpha: 0.22).setStroke() - backgroundPath.lineWidth = size.height * 0.08 - backgroundPath.stroke() - - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.alignment = .center - let attributes: [NSAttributedString.Key: Any] = [ - .font: UIFont(name: "Optima-Bold", size: size.height * 0.42) ?? UIFont.systemFont(ofSize: size.height * 0.42, weight: .semibold), - .foregroundColor: textColor, - .paragraphStyle: paragraphStyle - ] - let textRect = CGRect(x: 0.0, y: size.height * 0.28, width: size.width, height: size.height * 0.5) - title.draw(in: textRect, withAttributes: attributes) - } - return SKTexture(image: image) - } - - private func fittedFontSize(for text: String, preferredSize: CGFloat, maxWidth: CGFloat) -> CGFloat { - if text.isEmpty { - return preferredSize - } - let label = SKLabelNode(fontNamed: "Optima-Bold") - label.text = text - var currentSize = preferredSize - label.fontSize = currentSize - - while label.frame.width > maxWidth && currentSize > 10.0 { - currentSize -= 1.0 - label.fontSize = currentSize - } - return currentSize - } -} - -class TutorialGameScene: GameScene { - private var restartButton: SpriteButton? - private var menuButton: SpriteButton? - private var finishButton: SpriteButton? - private let tutorialHint = SKLabelNode(fontNamed: "Optima-Bold") - private let tutorialSubHint = SKLabelNode(fontNamed: "Optima-Bold") - - override init(size: CGSize, proportionSet: ProportionSet, level: Level) { - super.init(size: size, proportionSet: proportionSet, level: level) - addTutorialHints() - updateHintText() - } - - required init(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func addStaticLabels() { - // Tutorial scene keeps UI minimal and avoids overlap with end-of-tutorial CTA. - } - - override func addDynamicLabels() { - // No move counters in tutorial. - } - - override func addButtons() { - let side = proportionSet.levelChoiceSize() - restartButton = SpriteButton(texture: makeCircleButtonTexture(symbolName: "arrow.counterclockwise", side: side), onClick: restartTutorial) - restartButton!.position = CGPoint(x: size.width - size.height * 0.057, y: size.height * 0.91) - addChild(restartButton!) - - menuButton = SpriteButton(texture: makeCircleButtonTexture(symbolName: "chevron.left", side: side), onClick: backToStartMenu) - menuButton!.position = proportionSet.levelChoicePos() + let side = min(size.width * 0.095, size.height * 0.07) + menuButton = SpriteButton(texture: ButtonTextureFactory.makeMinimalBackTexture(side: side), onClick: backToStartMenu) + menuButton?.position = CGPoint(x: size.width * 0.085, y: size.height * 0.90) + menuButton?.zPosition = 20 addChild(menuButton!) } - override func moveLine(horizontal: Bool, rowOrColumn: Int, steps: Int) { - super.moveLine(horizontal: horizontal, rowOrColumn: rowOrColumn, steps: steps) - if !levelSolved { - updateHintText() - } - } + private func addPageArrows() { + let arrowSide = min(size.width * 0.16, size.height * 0.13) + leftArrowButton = SpriteButton(texture: ButtonTextureFactory.makePageArrowTexture(imageNamed: "ArrowLeft", side: arrowSide), onClick: goToPreviousPage) + leftArrowButton?.position = CGPoint(x: size.width * 0.08, y: size.height * 0.21) + leftArrowButton?.zPosition = 20 + addChild(leftArrowButton!) - override func onSolvedGame() { - tutorialHint.text = "Solved" - tutorialHint.fontSize = size.height * 0.06 - tutorialHint.position = CGPoint(x: size.width * 0.5, y: size.height * 0.30) - tutorialSubHint.text = "" - showFinishButton() + rightArrowButton = SpriteButton(texture: ButtonTextureFactory.makePageArrowTexture(imageNamed: "ArrowRight", side: arrowSide), onClick: goToNextPage) + rightArrowButton?.position = CGPoint(x: size.width * 0.92, y: size.height * 0.21) + rightArrowButton?.zPosition = 20 + addChild(rightArrowButton!) } - private func addTutorialHints() { - tutorialHint.fontColor = fontColor - tutorialHint.position = CGPoint(x: size.width * 0.5, y: size.height * 0.24) - tutorialHint.zPosition = 10 - addChild(tutorialHint) - - tutorialSubHint.fontColor = fontColor - tutorialSubHint.position = CGPoint(x: size.width * 0.5, y: size.height * 0.19) - tutorialSubHint.zPosition = 10 - addChild(tutorialSubHint) + private func goToPreviousPage() { + levelButtonArea?.swipeLeft() } - private func updateHintText() { - if gameService.getLevel().movesCounter == 0 { - tutorialHint.text = "Swipe any row or column" - tutorialHint.fontSize = fittedFontSize(for: tutorialHint.text ?? "", preferredSize: size.height * 0.040, maxWidth: size.width * 0.92) - tutorialSubHint.text = "Connect start and finish in this level." - tutorialSubHint.fontSize = fittedFontSize(for: tutorialSubHint.text ?? "", preferredSize: size.height * 0.032, maxWidth: size.width * 0.92) - } else { - tutorialHint.text = "Good" - tutorialHint.fontSize = size.height * 0.044 - tutorialSubHint.text = "Keep going until the path is connected." - tutorialSubHint.fontSize = fittedFontSize(for: tutorialSubHint.text ?? "", preferredSize: size.height * 0.032, maxWidth: size.width * 0.92) - } - } - - private func showFinishButton() { - if finishButton != nil { - finishButton?.isHidden = false - return - } - let buttonSize = CGSize(width: size.width * 0.62, height: size.height * 0.095) - finishButton = SpriteButton(texture: makeTextButtonTexture(title: "Back to Menu", size: buttonSize)) { - ResourcesManager.getInstance().storyService?.loadStartMenuScene() - } - finishButton?.position = CGPoint(x: size.width * 0.5, y: size.height * 0.15) - finishButton?.zPosition = 10 - addChild(finishButton!) - } - - private func restartTutorial() { - ResourcesManager.getInstance().storyService?.loadTutorialGameSceneFromTutorialScene() + private func goToNextPage() { + levelButtonArea?.swipeRight() } private func backToStartMenu() { ResourcesManager.getInstance().storyService?.loadStartMenuScene() } - private func makeTextButtonTexture(title: String, size: CGSize) -> SKTexture { - let renderer = UIGraphicsImageRenderer(size: size) - let image = renderer.image { _ in - let frame = CGRect(origin: .zero, size: size).insetBy(dx: size.width * 0.01, dy: size.height * 0.06) - let backgroundPath = UIBezierPath(roundedRect: frame, cornerRadius: size.height * 0.23) - UIColor(white: 1.0, alpha: 0.10).setFill() - backgroundPath.fill() - UIColor(white: 1.0, alpha: 0.22).setStroke() - backgroundPath.lineWidth = size.height * 0.08 - backgroundPath.stroke() - - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.alignment = .center - let buttonFontSize = fittedButtonFontSize(for: title, preferredSize: size.height * 0.38, maxWidth: size.width * 0.90) - let attributes: [NSAttributedString.Key: Any] = [ - .font: UIFont(name: "Optima-Bold", size: buttonFontSize) ?? UIFont.systemFont(ofSize: buttonFontSize, weight: .semibold), - .foregroundColor: fontColor, - .paragraphStyle: paragraphStyle - ] - let textRect = CGRect(x: 0.0, y: size.height * 0.30, width: size.width, height: size.height * 0.5) - title.draw(in: textRect, withAttributes: attributes) + func update() { + guard let area = levelButtonArea else { + leftArrowButton?.isHidden = true + rightArrowButton?.isHidden = true + return } - return SKTexture(image: image) - } - private func fittedButtonFontSize(for text: String, preferredSize: CGFloat, maxWidth: CGFloat) -> CGFloat { - var currentSize = preferredSize - while currentSize > 10.0 { - let font = UIFont(name: "Optima-Bold", size: currentSize) ?? UIFont.systemFont(ofSize: currentSize, weight: .semibold) - let measuredWidth = (text as NSString).size(withAttributes: [.font: font]).width - if measuredWidth <= maxWidth { - return currentSize - } - currentSize -= 1.0 - } - return 10.0 + let isFirstPage = area.currentLevelPage == 0 + let isLastPage = area.currentLevelPage >= area.pageCount - 1 + leftArrowButton?.isHidden = isFirstPage + rightArrowButton?.isHidden = isLastPage || area.pageCount <= 1 } - private func fittedFontSize(for text: String, preferredSize: CGFloat, maxWidth: CGFloat) -> CGFloat { - if text.isEmpty { - return preferredSize - } - let label = SKLabelNode(fontNamed: "Optima-Bold") - label.text = text - var currentSize = preferredSize - label.fontSize = currentSize - - while label.frame.width > maxWidth && currentSize > 10.0 { - currentSize -= 1.0 - label.fontSize = currentSize - } - return currentSize + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") } } diff --git a/SOPA/view/LevelMode/LevelModeGameScene.swift b/SOPA/view/LevelMode/LevelModeGameScene.swift index 059ed47..2f44dd1 100644 --- a/SOPA/view/LevelMode/LevelModeGameScene.swift +++ b/SOPA/view/LevelMode/LevelModeGameScene.swift @@ -12,8 +12,10 @@ class LevelModeGameScene: GameScene { var restartButton: SpriteButton? var levelChoiceButton: SpriteButton? var start: NSDate? + override init(size: CGSize, proportionSet: ProportionSet, level: Level) { super.init(size: size, proportionSet: proportionSet, level: level) + backgroundColor = .black startCounter() } @@ -24,34 +26,106 @@ class LevelModeGameScene: GameScene { private func stopCounter() -> Double { let end = NSDate() let difference: Double = end.timeIntervalSince(start! as Date) - print(difference) return difference } required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } + override func addButtons() { - let restartSide = proportionSet.buttonSize() - restartButton = SpriteButton(texture: makeCircleButtonTexture(symbolName: "arrow.counterclockwise", side: restartSide), onClick: restartLevel) - restartButton!.position = proportionSet.restartButtonPos() - addChild(restartButton!) - - let side = proportionSet.levelChoiceSize() - levelChoiceButton = SpriteButton(texture: makeCircleButtonTexture(symbolName: "chevron.left", side: side), onClick: loadLevelChoiceScene) - levelChoiceButton!.position = proportionSet.levelChoicePos() + let backSide = min(size.width, size.height) * 0.09 + levelChoiceButton = SpriteButton(texture: ButtonTextureFactory.makeMinimalBackTexture(side: backSide), onClick: loadLevelChoiceScene) + levelChoiceButton!.position = CGPoint(x: size.width * 0.08, y: size.height * 0.95) addChild(levelChoiceButton!) + let restartSide = min(size.width, size.height) * 0.18 + restartButton = SpriteButton( + texture: ButtonTextureFactory.makeCircleButtonTexture( + symbolName: "arrow.counterclockwise", + side: restartSide, + circleColor: UIColor(white: 0.96, alpha: 1.0), + iconColor: UIColor(white: 0.05, alpha: 1.0), + symbolScale: 0.52, + symbolWeight: .bold + ), + onClick: restartLevel + ) + restartButton!.position = CGPoint(x: size.width * 0.86, y: size.height * 0.12) + addChild(restartButton!) } - + + override func addStaticLabels() { + addChild(movesLabels) + + let headingColor = UIColor(white: 0.90, alpha: 1.0) + let valueColor = UIColor(white: 0.96, alpha: 1.0) + let titleFont = "Impact" + + let minTitle = SKLabelNode(fontNamed: titleFont) + minTitle.text = "Min. Moves" + minTitle.horizontalAlignmentMode = .center + minTitle.verticalAlignmentMode = .center + minTitle.fontSize = min(size.width, size.height) * 0.055 + minTitle.fontColor = headingColor + minTitle.position = CGPoint(x: size.width * 0.14, y: size.height * 0.90) + movesLabels.addChild(minTitle) + + let minValue = SKLabelNode(fontNamed: titleFont) + minValue.text = String(gameService.getLevel().minimumMovesToSolve ?? 0) + minValue.horizontalAlignmentMode = .center + minValue.verticalAlignmentMode = .center + minValue.fontSize = min(size.width, size.height) * 0.15 + minValue.fontColor = valueColor + minValue.position = CGPoint(x: size.width * 0.04, y: size.height * 0.84) + movesLabels.addChild(minValue) + + let currentTitle = SKLabelNode(fontNamed: titleFont) + currentTitle.text = "Current Moves" + currentTitle.horizontalAlignmentMode = .center + currentTitle.verticalAlignmentMode = .center + currentTitle.fontSize = min(size.width, size.height) * 0.055 + currentTitle.fontColor = headingColor + currentTitle.position = CGPoint(x: size.width * 0.84, y: size.height * 0.90) + movesLabels.addChild(currentTitle) + + let levelNumber = SKLabelNode(fontNamed: titleFont) + levelNumber.horizontalAlignmentMode = .left + levelNumber.verticalAlignmentMode = .center + levelNumber.position = CGPoint(x: size.width * 0.04, y: size.height * 0.11) + levelNumber.fontSize = min(size.width, size.height) * 0.15 + levelNumber.text = String(gameService.getLevel().id!) + levelNumber.fontColor = valueColor + addChild(levelNumber) + + let levelLabel = SKLabelNode(fontNamed: titleFont) + levelLabel.text = "Level" + levelLabel.horizontalAlignmentMode = .left + levelLabel.verticalAlignmentMode = .center + levelLabel.fontSize = min(size.width, size.height) * 0.06 + levelLabel.fontColor = headingColor + levelLabel.position = CGPoint(x: size.width * 0.04, y: size.height * 0.045) + addChild(levelLabel) + } + + override func addDynamicLabels() { + currentMovesNode.text = String(gameService.getLevel().movesCounter) + currentMovesNode.horizontalAlignmentMode = .center + currentMovesNode.verticalAlignmentMode = .center + currentMovesNode.fontSize = min(size.width, size.height) * 0.15 + currentMovesNode.position = CGPoint(x: size.width * 0.80, y: size.height * 0.84) + currentMovesNode.fontColor = UIColor(white: 0.96, alpha: 1.0) + movesLabels.addChild(currentMovesNode) + } + func restartLevel() { ResourcesManager.getInstance().storyService?.reloadLevelModeGameScene(levelId: gameService.getLevel().id!) } - - func loadLevelChoiceScene() { + + private func loadLevelChoiceScene() { ResourcesManager.getInstance().storyService?.loadLevelCoiceSceneFromLevelModeScene() } - + override func onSolvedGame() { let time = stopCounter() let level = gameService.getLevel() diff --git a/SOPA/view/LevelMode/LevelModeScoreScene.swift b/SOPA/view/LevelMode/LevelModeScoreScene.swift index 107ec04..c55495e 100644 --- a/SOPA/view/LevelMode/LevelModeScoreScene.swift +++ b/SOPA/view/LevelMode/LevelModeScoreScene.swift @@ -11,7 +11,7 @@ import SpriteKit class LevelModeScoreScene: SKScene { private let levelResult: LevelResult - private let background = UIColor(red: 90.6 / 255.0, green: 86.7 / 255.0, blue: 70.6 / 255.0, alpha: 1.0) + private let background = UIColor.black private let textColor = UIColor(red: 240.0 / 255.0, green: 239.0 / 255.0, blue: 238.0 / 255.0, alpha: 1.0) init(size: CGSize, levelResult: LevelResult) { @@ -27,7 +27,7 @@ class LevelModeScoreScene: SKScene { let side = min(size.width, size.height) * 0.20 let restartButton = SpriteButton(texture: makeCircleButtonTexture(symbolName: "arrow.counterclockwise", side: side), onClick: restartLevel) let levelChoiceButton = SpriteButton(texture: makeCircleButtonTexture(symbolName: "square.grid.2x2", side: side), onClick: levelChoiceMenu) - let nextLevelButton = SpriteButton(texture: makeCircleButtonTexture(symbolName: "chevron.right", side: side), onClick: startNextLevel) + let nextLevelButton = SpriteButton(texture: makeCircleButtonTexture(symbolName: "play.fill", side: side), onClick: startNextLevel) let buttonSize = CGSize(width: side, height: side) restartButton.size = buttonSize @@ -46,7 +46,7 @@ class LevelModeScoreScene: SKScene { private func addStaticObjects() { let title = SKLabelNode(fontNamed: "Optima-Bold") - title.text = "Level \(levelResult.levelId) Complete" + title.text = "\(levelResult.levelId). Level Complete" title.horizontalAlignmentMode = .center title.verticalAlignmentMode = .center title.position = CGPoint(x: size.width * 0.5, y: size.height * 0.86) @@ -65,7 +65,7 @@ class LevelModeScoreScene: SKScene { let level = ResourcesManager.getInstance().levelService!.getLevelById(id: levelResult.levelId)! let optimumLabel = SKLabelNode(fontNamed: "Optima-Bold") - optimumLabel.text = "3-star target: \(level.minimumMovesToSolve!)" + optimumLabel.text = "Moves for 3 Stars: \(level.minimumMovesToSolve!)" optimumLabel.horizontalAlignmentMode = .center optimumLabel.verticalAlignmentMode = .center optimumLabel.position = CGPoint(x: size.width * 0.5, y: size.height * 0.46) diff --git a/SOPA/view/LevelMode/LevelSelectButton.swift b/SOPA/view/LevelMode/LevelSelectButton.swift index ee5716f..6af97be 100644 --- a/SOPA/view/LevelMode/LevelSelectButton.swift +++ b/SOPA/view/LevelMode/LevelSelectButton.swift @@ -10,24 +10,21 @@ import Foundation import SpriteKit class LevelSelectButton: SKSpriteNode { - let green = UIColor(red: 169.0 / 255.0, green: 162.0 / 255.0, blue: 121.0 / 255.0, alpha: 1.0) - let grey = UIColor(red: 0.5 , green: 0.5, blue: 0.5, alpha: 1.0) + let numberActive = UIColor(red: 223.0 / 255.0, green: 106.0 / 255.0, blue: 37.0 / 255.0, alpha: 1.0) + let numberUnlocked = UIColor(red: 98.0 / 255.0, green: 100.0 / 255.0, blue: 106.0 / 255.0, alpha: 1.0) + let numberLocked = UIColor(red: 110.0 / 255.0, green: 112.0 / 255.0, blue: 116.0 / 255.0, alpha: 1.0) let levelInfo: LevelInfo + init(levelInfo: LevelInfo, levelButtonPositioner: LevelButtonPositioner) { self.levelInfo = levelInfo + super.init(texture: nil, color: .clear, size: levelButtonPositioner.getLevelSize()) + position = levelButtonPositioner.getLevelPosition(id: levelInfo.levelId) + + let isCurrentTarget = !levelInfo.locked && levelInfo.fewestMoves < 0 + addFrame(locked: levelInfo.locked, highlighted: isCurrentTarget) + addLable(id: levelInfo.levelId, color: labelColor(locked: levelInfo.locked, highlighted: isCurrentTarget)) if !levelInfo.locked { - let texture = SKTexture(imageNamed: "Level") - super.init(texture: texture, color: UIColor.clear, size: levelButtonPositioner.getLevelSize()) - position = levelButtonPositioner.getLevelPosition(id: levelInfo.levelId) addStars(stars: levelInfo.stars) - addLable(id: levelInfo.levelId, color: green) - - } else { - let texture = SKTexture(imageNamed: "LevelSW") - super.init(texture: texture, color: UIColor.clear, size: levelButtonPositioner.getLevelSize()) - position = levelButtonPositioner.getLevelPosition(id: levelInfo.levelId) - addLable(id: levelInfo.levelId, color: grey) - } } @@ -36,14 +33,56 @@ class LevelSelectButton: SKSpriteNode { fatalError("init(coder:) has not been implemented") } + private func addFrame(locked: Bool, highlighted: Bool) { + let inset = size.width * 0.04 + let frameRect = CGRect( + x: -size.width / 2.0 + inset, + y: -size.height / 2.0 + inset, + width: size.width - 2.0 * inset, + height: size.height - 2.0 * inset + ) + let cornerRadius = size.width * 0.20 + + let glowNode = SKShapeNode(rect: frameRect, cornerRadius: cornerRadius) + glowNode.fillColor = .clear + glowNode.lineWidth = size.width * 0.040 + glowNode.strokeColor = highlighted + ? UIColor(red: 245.0 / 255.0, green: 102.0 / 255.0, blue: 34.0 / 255.0, alpha: 0.70) + : UIColor(white: 1.0, alpha: locked ? 0.12 : 0.30) + glowNode.glowWidth = size.width * 0.10 + glowNode.blendMode = .add + glowNode.zPosition = zPosition + addChild(glowNode) + + let frameNode = SKShapeNode(rect: frameRect, cornerRadius: cornerRadius) + frameNode.fillColor = .clear + frameNode.lineWidth = size.width * 0.030 + frameNode.strokeColor = highlighted + ? UIColor(red: 250.0 / 255.0, green: 192.0 / 255.0, blue: 107.0 / 255.0, alpha: 1.0) + : UIColor(white: locked ? 0.55 : 0.90, alpha: 0.95) + frameNode.lineJoin = .round + frameNode.zPosition = zPosition + 1 + addChild(frameNode) + } + + private func labelColor(locked: Bool, highlighted: Bool) -> UIColor { + if highlighted { + return numberActive + } + if locked { + return numberLocked + } + return numberUnlocked + } + func addStars(stars: Int) { let star1: SKSpriteNode = generateStar(achieved: stars >= 1) let star2: SKSpriteNode = generateStar(achieved: stars >= 2) let star3: SKSpriteNode = generateStar(achieved: stars >= 3) - star1.position = CGPoint(x: -size.width * 0.25, y: -size.height * 0.2) - star2.position = CGPoint(x: 0, y: -size.height * 0.27 ) - star3.position = CGPoint(x: size.width * 0.25, y: -size.height * 0.2) + star1.position = CGPoint(x: -size.width * 0.25, y: -size.height * 0.50) + star2.position = CGPoint(x: 0, y: -size.height * 0.56) + star3.position = CGPoint(x: size.width * 0.25, y: -size.height * 0.50) addChild(star1) addChild(star2) @@ -52,11 +91,11 @@ class LevelSelectButton: SKSpriteNode { } func addLable(id: Int, color: UIColor) { - let idLable = SKLabelNode(fontNamed: "Optima-Bold") + let idLable = SKLabelNode(fontNamed: "Impact") idLable.text = String(id) idLable.horizontalAlignmentMode = SKLabelHorizontalAlignmentMode.center idLable.verticalAlignmentMode = SKLabelVerticalAlignmentMode.center - idLable.fontSize = size.height * 0.34 + idLable.fontSize = size.height * 0.32 idLable.fontColor = color idLable.zPosition = zPosition + 1 addChild(idLable) @@ -68,7 +107,7 @@ class LevelSelectButton: SKSpriteNode { if achieved { star = SKSpriteNode(imageNamed: "star") } - star.size = CGSize(width: size.width/3, height: size.height/3) + star.size = CGSize(width: size.width/3.4, height: size.height/3.4) star.zPosition = zPosition + 1 return star } diff --git a/SOPA/view/LevelMode/SopaTheme.swift b/SOPA/view/LevelMode/SopaTheme.swift new file mode 100644 index 0000000..ee57c31 --- /dev/null +++ b/SOPA/view/LevelMode/SopaTheme.swift @@ -0,0 +1,17 @@ +import UIKit + +enum SopaTheme { + static let blackBackground = UIColor.black + static let warmBackground = UIColor(red: 90.6 / 255.0, green: 86.7 / 255.0, blue: 70.6 / 255.0, alpha: 1.0) + + static let titleGray = UIColor(red: 214.0 / 255.0, green: 214.0 / 255.0, blue: 214.0 / 255.0, alpha: 1.0) + static let neonBlue = UIColor(red: 111.0 / 255.0, green: 227.0 / 255.0, blue: 1.0, alpha: 1.0) + static let neonGlowBlue = UIColor(red: 23.0 / 255.0, green: 183.0 / 255.0, blue: 1.0, alpha: 1.0) + static let neonCoreBlue = UIColor(red: 183.0 / 255.0, green: 245.0 / 255.0, blue: 1.0, alpha: 1.0) + + static let bodyText = UIColor(red: 230.0 / 255.0, green: 236.0 / 255.0, blue: 242.0 / 255.0, alpha: 1.0) + static let gameText = UIColor(red: 240.0 / 255.0, green: 239.0 / 255.0, blue: 238.0 / 255.0, alpha: 1.0) + + static let circleButtonFill = UIColor(white: 0.94, alpha: 1.0) + static let circleButtonIcon = UIColor(red: 90.6 / 255.0, green: 86.7 / 255.0, blue: 70.6 / 255.0, alpha: 1.0) +} diff --git a/SOPA/view/LevelMode/StartMenuScene.swift b/SOPA/view/LevelMode/StartMenuScene.swift new file mode 100644 index 0000000..2f08eab --- /dev/null +++ b/SOPA/view/LevelMode/StartMenuScene.swift @@ -0,0 +1,102 @@ +import Foundation +import SpriteKit +import UIKit + +class StartMenuScene: SKScene { + private let twitterUrl = "https://twitter.com/sopagame" + private let shareText = "I played SOPA: https://play.google.com/store/apps/details?id=com.sopaapp" + + override init(size: CGSize) { + super.init(size: size) + backgroundColor = SopaTheme.blackBackground + addTitle() + addButtons() + addSocialButtons() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func addTitle() { + let title = SKLabelNode(fontNamed: "Impact") + title.text = "SOPA" + title.fontSize = size.height * 0.14 + title.fontColor = SopaTheme.titleGray + title.position = CGPoint(x: size.width / 2, y: size.height * 0.77) + addChild(title) + } + + private func addButtons() { + let menuItems: [(title: String, action: () -> Void)] = [ + ("LEVEL MODE", { ResourcesManager.getInstance().storyService?.loadLevelCoiceScene() }), + ("JUST PLAY", { ResourcesManager.getInstance().storyService?.loadJustPlaySceneFromMenuScene() }), + ("TUTORIAL", { ResourcesManager.getInstance().storyService?.loadTutorialSceneFromMenuScene() }), + ("CREDITS", { ResourcesManager.getInstance().storyService?.loadCreditsSceneFromMenuScene() }) + ] + + let buttonSize = CGSize(width: size.width * 0.76, height: size.height * 0.095) + let firstY = size.height * 0.62 + let verticalGap = size.height * 0.11 + + for (index, item) in menuItems.enumerated() { + let button = SpriteButton(texture: ButtonTextureFactory.makeNeonTextTexture(title: item.title, size: buttonSize), onClick: item.action) + button.position = CGPoint(x: size.width / 2.0, y: firstY - CGFloat(index) * verticalGap) + addChild(button) + } + } + + private func addSocialButtons() { + let buttonSide = min(size.width * 0.18, size.height * 0.10) + + let shareButton = SpriteButton(texture: SKTexture(imageNamed: "ShareThis"), onClick: shareApp) + shareButton.size = CGSize(width: buttonSide, height: buttonSide) + shareButton.position = CGPoint(x: size.width * 0.20, y: size.height * 0.14) + addChild(shareButton) + + let twitterWidth = buttonSide * 1.2 + let twitterButton = SpriteButton(texture: SKTexture(imageNamed: "TwitterLogo"), onClick: openTwitter) + twitterButton.size = CGSize(width: twitterWidth, height: twitterWidth * 244.0 / 300.0) + twitterButton.position = CGPoint(x: size.width * 0.74, y: size.height * 0.14) + addChild(twitterButton) + } + + private func openTwitter() { + guard let url = URL(string: twitterUrl) else { + return + } + UIApplication.shared.open(url) + } + + private func shareApp() { + guard let controller = topViewController() else { + return + } + let shareController = UIActivityViewController(activityItems: [shareText], applicationActivities: nil) + shareController.popoverPresentationController?.sourceView = controller.view + controller.present(shareController, animated: true) + } + + private func topViewController(base: UIViewController? = nil) -> UIViewController? { + let initialController: UIViewController? + if let base = base { + initialController = base + } else { + initialController = UIApplication.shared.connectedScenes + .compactMap { $0 as? UIWindowScene } + .flatMap { $0.windows } + .first { $0.isKeyWindow }?.rootViewController + } + + if let navController = initialController as? UINavigationController { + return topViewController(base: navController.visibleViewController) + } + if let tabController = initialController as? UITabBarController { + return topViewController(base: tabController.selectedViewController) + } + if let presented = initialController?.presentedViewController { + return topViewController(base: presented) + } + return initialController + } +} diff --git a/SOPA/view/LevelMode/TutorialGameScene.swift b/SOPA/view/LevelMode/TutorialGameScene.swift new file mode 100644 index 0000000..889b057 --- /dev/null +++ b/SOPA/view/LevelMode/TutorialGameScene.swift @@ -0,0 +1,103 @@ +import Foundation +import SpriteKit +import UIKit + +class TutorialGameScene: GameScene { + private var restartButton: SpriteButton? + private var menuButton: SpriteButton? + private var finishButton: SpriteButton? + private let tutorialHint = SKLabelNode(fontNamed: "Optima-Bold") + private let tutorialSubHint = SKLabelNode(fontNamed: "Optima-Bold") + + override init(size: CGSize, proportionSet: ProportionSet, level: Level) { + super.init(size: size, proportionSet: proportionSet, level: level) + addTutorialHints() + updateHintText() + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func addStaticLabels() { + // Tutorial scene keeps UI minimal and avoids overlap with end-of-tutorial CTA. + } + + override func addDynamicLabels() { + // No move counters in tutorial. + } + + override func addButtons() { + let side = proportionSet.levelChoiceSize() + restartButton = SpriteButton(texture: makeCircleButtonTexture(symbolName: "arrow.counterclockwise", side: side), onClick: restartTutorial) + restartButton!.position = CGPoint(x: size.width - size.height * 0.057, y: size.height * 0.91) + addChild(restartButton!) + + menuButton = SpriteButton(texture: makeCircleButtonTexture(symbolName: "chevron.left", side: side), onClick: backToStartMenu) + menuButton!.position = proportionSet.levelChoicePos() + addChild(menuButton!) + } + + override func moveLine(horizontal: Bool, rowOrColumn: Int, steps: Int) { + super.moveLine(horizontal: horizontal, rowOrColumn: rowOrColumn, steps: steps) + if !levelSolved { + updateHintText() + } + } + + override func onSolvedGame() { + tutorialHint.text = "Solved" + tutorialHint.fontSize = size.height * 0.06 + tutorialHint.position = CGPoint(x: size.width * 0.5, y: size.height * 0.30) + tutorialSubHint.text = "" + showFinishButton() + } + + private func addTutorialHints() { + tutorialHint.fontColor = fontColor + tutorialHint.position = CGPoint(x: size.width * 0.5, y: size.height * 0.24) + tutorialHint.zPosition = 10 + addChild(tutorialHint) + + tutorialSubHint.fontColor = fontColor + tutorialSubHint.position = CGPoint(x: size.width * 0.5, y: size.height * 0.19) + tutorialSubHint.zPosition = 10 + addChild(tutorialSubHint) + } + + private func updateHintText() { + if gameService.getLevel().movesCounter == 0 { + tutorialHint.text = "Swipe any row or column" + tutorialHint.fontSize = LabelSizing.fittedLabelFontSize(text: tutorialHint.text ?? "", fontName: "Optima-Bold", preferredSize: size.height * 0.040, maxWidth: size.width * 0.92) + tutorialSubHint.text = "Connect start and finish in this level." + tutorialSubHint.fontSize = LabelSizing.fittedLabelFontSize(text: tutorialSubHint.text ?? "", fontName: "Optima-Bold", preferredSize: size.height * 0.032, maxWidth: size.width * 0.92) + } else { + tutorialHint.text = "Good" + tutorialHint.fontSize = size.height * 0.044 + tutorialSubHint.text = "Keep going until the path is connected." + tutorialSubHint.fontSize = LabelSizing.fittedLabelFontSize(text: tutorialSubHint.text ?? "", fontName: "Optima-Bold", preferredSize: size.height * 0.032, maxWidth: size.width * 0.92) + } + } + + private func showFinishButton() { + if finishButton != nil { + finishButton?.isHidden = false + return + } + let buttonSize = CGSize(width: size.width * 0.62, height: size.height * 0.095) + finishButton = SpriteButton(texture: ButtonTextureFactory.makeTextButtonTexture(title: "Back to Menu", size: buttonSize, fontName: "Optima-Bold", textColor: fontColor)) { + ResourcesManager.getInstance().storyService?.loadStartMenuScene() + } + finishButton?.position = CGPoint(x: size.width * 0.5, y: size.height * 0.15) + finishButton?.zPosition = 10 + addChild(finishButton!) + } + + private func restartTutorial() { + ResourcesManager.getInstance().storyService?.loadTutorialGameSceneFromTutorialScene() + } + + private func backToStartMenu() { + ResourcesManager.getInstance().storyService?.loadStartMenuScene() + } +} diff --git a/SOPA/view/LevelMode/TutorialScene.swift b/SOPA/view/LevelMode/TutorialScene.swift new file mode 100644 index 0000000..562ea7f --- /dev/null +++ b/SOPA/view/LevelMode/TutorialScene.swift @@ -0,0 +1,95 @@ +import Foundation +import SpriteKit +import UIKit + +class TutorialScene: SKScene { + private var step = 0 + private var leaveScene = false + + private var firstA: SKSpriteNode! + private var firstB: SKSpriteNode! + private var secondA: SKSpriteNode! + private var secondB: SKSpriteNode! + private var letsGo: SKSpriteNode! + + override init(size: CGSize) { + super.init(size: size) + backgroundColor = SopaTheme.blackBackground + addScreens() + addBackButton() + showStep() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func addScreens() { + let width = size.width + // The screens are split into a top (A) and bottom (B) half that tile vertically, + // mirroring the Android tutorial artwork. Keep each half's aspect ratio. + let aHeight = width * 910.0 / 1024.0 + let bHeight = width * 960.0 / 1080.0 + let topInset = (size.height - (aHeight + bHeight)) / 2.0 + let aCenterY = size.height - topInset - aHeight / 2.0 + let bCenterY = size.height - topInset - aHeight - bHeight / 2.0 + + firstA = makeScreen(imageNamed: "firstTutorialScreenA", width: width, height: aHeight, centerY: aCenterY) + firstB = makeScreen(imageNamed: "firstTutorialScreenB", width: width, height: bHeight, centerY: bCenterY) + secondA = makeScreen(imageNamed: "secondTutorialScreenA", width: width, height: aHeight, centerY: aCenterY) + secondB = makeScreen(imageNamed: "secondTutorialScreenB", width: width, height: bHeight, centerY: bCenterY) + + let letsGoWidth = width * 0.5 + letsGo = SKSpriteNode(imageNamed: "tutorialLetsGo") + letsGo.size = CGSize(width: letsGoWidth, height: letsGoWidth * 234.0 / 788.0) + letsGo.position = CGPoint(x: size.width / 2.0, y: size.height * 0.5) + letsGo.zPosition = 5 + addChild(letsGo) + } + + private func makeScreen(imageNamed: String, width: CGFloat, height: CGFloat, centerY: CGFloat) -> SKSpriteNode { + let node = SKSpriteNode(imageNamed: imageNamed) + node.size = CGSize(width: width, height: height) + node.position = CGPoint(x: size.width / 2.0, y: centerY) + addChild(node) + return node + } + + private func addBackButton() { + let side = size.height * 0.08 + let backButton = SpriteButton(texture: ButtonTextureFactory.makeMinimalBackTexture(side: side)) { [weak self] in + self?.leaveScene = true + ResourcesManager.getInstance().storyService?.loadStartMenuScene() + } + backButton.position = CGPoint(x: size.height * 0.057, y: size.height * 0.95) + backButton.zPosition = 10 + addChild(backButton) + } + + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + guard step < 2 else { + return + } + step += 1 + showStep() + } + + private func showStep() { + let onFirst = step == 0 + let onSecond = step == 1 + firstA.isHidden = !onFirst + firstB.isHidden = !onFirst + secondA.isHidden = !onSecond + secondB.isHidden = !onSecond + letsGo.isHidden = step < 2 + + if step >= 2 { + DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in + guard let self = self, !self.leaveScene else { + return + } + ResourcesManager.getInstance().storyService?.loadTutorialGameSceneFromTutorialScene() + } + } + } +}