diff --git a/RollkofferSimulator/AppDelegate.swift b/RollkofferSimulator/AppDelegate.swift
new file mode 100644
index 0000000..5a9cbe4
--- /dev/null
+++ b/RollkofferSimulator/AppDelegate.swift
@@ -0,0 +1,42 @@
+//
+// AppDelegate.swift
+// RollkofferSimulator
+//
+// Created by Ingo K.
+//
+
+import UIKit
+
+@main
+class AppDelegate: UIResponder, UIApplicationDelegate {
+
+ var window: UIWindow?
+
+ func application(_ application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
+ return true
+ }
+
+ func applicationWillResignActive(_ application: UIApplication) {
+ // Pause the game when app goes to background
+ NotificationCenter.default.post(name: .pauseGame, object: nil)
+ }
+
+ func applicationDidEnterBackground(_ application: UIApplication) {
+ // Save game state if needed
+ }
+
+ func applicationWillEnterForeground(_ application: UIApplication) {
+ // Restore game state if needed
+ }
+
+ func applicationDidBecomeActive(_ application: UIApplication) {
+ // Resume game if needed
+ }
+}
+
+// MARK: - Notification Names
+extension Notification.Name {
+ static let pauseGame = Notification.Name("pauseGame")
+ static let resumeGame = Notification.Name("resumeGame")
+}
diff --git a/RollkofferSimulator/Assets.xcassets/AccentColor.colorset/Contents.json b/RollkofferSimulator/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 0000000..27ea04d
--- /dev/null
+++ b/RollkofferSimulator/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.600",
+ "green" : "0.300",
+ "red" : "0.400"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/RollkofferSimulator/Assets.xcassets/AppIcon.appiconset/Contents.json b/RollkofferSimulator/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..13613e3
--- /dev/null
+++ b/RollkofferSimulator/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,13 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/RollkofferSimulator/Assets.xcassets/Contents.json b/RollkofferSimulator/Assets.xcassets/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/RollkofferSimulator/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/RollkofferSimulator/Base.lproj/LaunchScreen.storyboard b/RollkofferSimulator/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..d140726
--- /dev/null
+++ b/RollkofferSimulator/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/RollkofferSimulator/Base.lproj/Main.storyboard b/RollkofferSimulator/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..29f730e
--- /dev/null
+++ b/RollkofferSimulator/Base.lproj/Main.storyboard
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/RollkofferSimulator/GameViewController.swift b/RollkofferSimulator/GameViewController.swift
new file mode 100644
index 0000000..9d0e5a3
--- /dev/null
+++ b/RollkofferSimulator/GameViewController.swift
@@ -0,0 +1,79 @@
+//
+// GameViewController.swift
+// RollkofferSimulator
+//
+// Created by Ingo K.
+//
+
+import UIKit
+import SpriteKit
+import GameplayKit
+
+class GameViewController: UIViewController {
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ // Configure the view
+ guard let skView = self.view as? SKView else {
+ fatalError("View is not an SKView")
+ }
+
+ // Create and configure the initial scene
+ let scene = MenuScene(size: skView.bounds.size)
+ scene.scaleMode = .aspectFill
+
+ // Configure view options
+ skView.ignoresSiblingOrder = true
+
+ #if DEBUG
+ skView.showsFPS = true
+ skView.showsNodeCount = true
+ #endif
+
+ // Present the scene
+ skView.presentScene(scene)
+
+ // Setup notification observers
+ setupNotificationObservers()
+ }
+
+ private func setupNotificationObservers() {
+ NotificationCenter.default.addObserver(
+ self,
+ selector: #selector(handlePauseNotification),
+ name: .pauseGame,
+ object: nil
+ )
+ }
+
+ @objc private func handlePauseNotification() {
+ guard let skView = self.view as? SKView,
+ let gameScene = skView.scene as? GameScene else {
+ return
+ }
+
+ // The GameScene should handle pausing internally
+ // This is just a notification that the app is going to background
+ }
+
+ override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
+ if UIDevice.current.userInterfaceIdiom == .phone {
+ return .portrait
+ } else {
+ return .all
+ }
+ }
+
+ override var prefersStatusBarHidden: Bool {
+ return true
+ }
+
+ override var preferredScreenEdgesDeferringSystemGestures: UIRectEdge {
+ return .all
+ }
+
+ deinit {
+ NotificationCenter.default.removeObserver(self)
+ }
+}
diff --git a/RollkofferSimulator/Info.plist b/RollkofferSimulator/Info.plist
new file mode 100644
index 0000000..c05f1e7
--- /dev/null
+++ b/RollkofferSimulator/Info.plist
@@ -0,0 +1,54 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ Rollkoffer Simulator
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ $(PRODUCT_BUNDLE_PACKAGE_TYPE)
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1
+ LSRequiresIPhoneOS
+
+ UIApplicationSceneManifest
+
+ UIApplicationSupportsMultipleScenes
+
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIMainStoryboardFile
+ Main
+ UIRequiredDeviceCapabilities
+
+ armv7
+
+ UIRequiresFullScreen
+
+ UIStatusBarHidden
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+
+
diff --git a/RollkofferSimulator/Managers/CollisionManager.swift b/RollkofferSimulator/Managers/CollisionManager.swift
new file mode 100644
index 0000000..d227bae
--- /dev/null
+++ b/RollkofferSimulator/Managers/CollisionManager.swift
@@ -0,0 +1,197 @@
+//
+// CollisionManager.swift
+// RollkofferSimulator
+//
+// Created by Ingo K.
+//
+
+import SpriteKit
+
+/// Protocol for collision event handling
+protocol CollisionManagerDelegate: AnyObject {
+ func didCollectGoodDog(points: Int)
+ func didCollectGreenHuman(points: Int)
+ func didHitHarmfulEntity()
+}
+
+/// Manages collision detection and response
+class CollisionManager: NSObject, SKPhysicsContactDelegate {
+
+ // MARK: - Properties
+ weak var delegate: CollisionManagerDelegate?
+
+ // MARK: - SKPhysicsContactDelegate
+ func didBegin(_ contact: SKPhysicsContact) {
+ let collision = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask
+
+ // Check if suitcase is involved
+ guard collision & Constants.PhysicsCategory.suitcase != 0 else { return }
+
+ let otherBody: SKPhysicsBody
+ if contact.bodyA.categoryBitMask == Constants.PhysicsCategory.suitcase {
+ otherBody = contact.bodyB
+ } else {
+ otherBody = contact.bodyA
+ }
+
+ handleCollision(with: otherBody)
+ }
+
+ // MARK: - Private Methods
+ private func handleCollision(with body: SKPhysicsBody) {
+ guard let node = body.node else { return }
+
+ switch body.categoryBitMask {
+ case Constants.PhysicsCategory.goodDog:
+ handleGoodDogCollision(node: node)
+
+ case Constants.PhysicsCategory.badDog:
+ handleBadDogCollision(node: node)
+
+ case Constants.PhysicsCategory.greenHuman:
+ handleGreenHumanCollision(node: node)
+
+ case Constants.PhysicsCategory.grayHuman:
+ handleGrayHumanCollision(node: node)
+
+ default:
+ break
+ }
+ }
+
+ private func handleGoodDogCollision(node: SKNode) {
+ guard let dogNode = node as? DogNode else { return }
+
+ let points = dogNode.dogType.points
+ showCollectEffect(at: node.position, color: .green, text: "+\(points)")
+ node.removeFromParent()
+ delegate?.didCollectGoodDog(points: points)
+ }
+
+ private func handleBadDogCollision(node: SKNode) {
+ showDamageEffect(at: node.position)
+ node.removeFromParent()
+ delegate?.didHitHarmfulEntity()
+ }
+
+ private func handleGreenHumanCollision(node: SKNode) {
+ guard let humanNode = node as? HumanNode else { return }
+
+ let points = humanNode.humanType.points
+ showCollectEffect(at: node.position, color: .green, text: "+\(points)")
+ node.removeFromParent()
+ delegate?.didCollectGreenHuman(points: points)
+ }
+
+ private func handleGrayHumanCollision(node: SKNode) {
+ showDamageEffect(at: node.position)
+ node.removeFromParent()
+ delegate?.didHitHarmfulEntity()
+ }
+
+ // MARK: - Visual Effects
+ private func showCollectEffect(at position: CGPoint, color: SKColor, text: String) {
+ guard let scene = getScene() else { return }
+
+ // Particle burst
+ let emitter = SKEmitterNode()
+ emitter.particleTexture = nil
+ emitter.particleBirthRate = 50
+ emitter.numParticlesToEmit = 20
+ emitter.particleLifetime = 0.5
+ emitter.particleSpeed = 100
+ emitter.particleSpeedRange = 50
+ emitter.emissionAngleRange = .pi * 2
+ emitter.particleScale = 0.3
+ emitter.particleScaleRange = 0.2
+ emitter.particleColor = color
+ emitter.particleColorBlendFactor = 1.0
+ emitter.position = position
+ emitter.zPosition = Constants.ZPosition.ui - 1
+
+ // Create a simple circle shape for particles
+ let shape = SKShapeNode(circleOfRadius: 5)
+ shape.fillColor = color
+ shape.strokeColor = .clear
+ if let texture = scene.view?.texture(from: shape) {
+ emitter.particleTexture = texture
+ }
+
+ scene.addChild(emitter)
+
+ let waitAction = SKAction.wait(forDuration: 1.0)
+ let removeAction = SKAction.removeFromParent()
+ emitter.run(SKAction.sequence([waitAction, removeAction]))
+
+ // Floating text
+ let label = SKLabelNode(text: text)
+ label.fontName = "AvenirNext-Bold"
+ label.fontSize = 24
+ label.fontColor = color
+ label.position = position
+ label.zPosition = Constants.ZPosition.ui
+
+ scene.addChild(label)
+
+ let moveUp = SKAction.moveBy(x: 0, y: 50, duration: 0.5)
+ let fadeOut = SKAction.fadeOut(withDuration: 0.5)
+ let group = SKAction.group([moveUp, fadeOut])
+ let remove = SKAction.removeFromParent()
+ label.run(SKAction.sequence([group, remove]))
+ }
+
+ private func showDamageEffect(at position: CGPoint) {
+ guard let scene = getScene() else { return }
+
+ // Red flash
+ let flash = SKShapeNode(circleOfRadius: 30)
+ flash.fillColor = .red
+ flash.strokeColor = .clear
+ flash.alpha = 0.7
+ flash.position = position
+ flash.zPosition = Constants.ZPosition.ui - 1
+
+ scene.addChild(flash)
+
+ let scaleUp = SKAction.scale(to: 2.0, duration: 0.2)
+ let fadeOut = SKAction.fadeOut(withDuration: 0.2)
+ let group = SKAction.group([scaleUp, fadeOut])
+ let remove = SKAction.removeFromParent()
+ flash.run(SKAction.sequence([group, remove]))
+
+ // Floating text
+ let label = SKLabelNode(text: "-1 โค๏ธ")
+ label.fontName = "AvenirNext-Bold"
+ label.fontSize = 24
+ label.fontColor = .red
+ label.position = position
+ label.zPosition = Constants.ZPosition.ui
+
+ scene.addChild(label)
+
+ let moveUp = SKAction.moveBy(x: 0, y: 50, duration: 0.5)
+ let labelFadeOut = SKAction.fadeOut(withDuration: 0.5)
+ let labelGroup = SKAction.group([moveUp, labelFadeOut])
+ let labelRemove = SKAction.removeFromParent()
+ label.run(SKAction.sequence([labelGroup, labelRemove]))
+ }
+
+ private func getScene() -> SKScene? {
+ // This would typically be set via dependency injection
+ // For simplicity, we'll use the notification pattern
+ return nil
+ }
+}
+
+// MARK: - Scene Reference Extension
+extension CollisionManager {
+ private static var sceneReference: SKScene?
+
+ func setScene(_ scene: SKScene) {
+ CollisionManager.sceneReference = scene
+ }
+
+ private func getSceneFromReference() -> SKScene? {
+ return CollisionManager.sceneReference
+ }
+}
diff --git a/RollkofferSimulator/Managers/ScoreManager.swift b/RollkofferSimulator/Managers/ScoreManager.swift
new file mode 100644
index 0000000..4445ae6
--- /dev/null
+++ b/RollkofferSimulator/Managers/ScoreManager.swift
@@ -0,0 +1,91 @@
+//
+// ScoreManager.swift
+// RollkofferSimulator
+//
+// Created by Ingo K.
+//
+
+import Foundation
+
+/// Manages high scores and game statistics
+class ScoreManager {
+
+ // MARK: - Singleton
+ static let shared = ScoreManager()
+
+ // MARK: - UserDefaults Keys
+ private let highScoreKey = "RollkofferSimulator.HighScore"
+ private let gamesPlayedKey = "RollkofferSimulator.GamesPlayed"
+ private let totalDogsCollectedKey = "RollkofferSimulator.TotalDogsCollected"
+ private let totalHumansCollectedKey = "RollkofferSimulator.TotalHumansCollected"
+ private let victoriesKey = "RollkofferSimulator.Victories"
+
+ // MARK: - Properties
+ private let defaults = UserDefaults.standard
+
+ var highScore: Int {
+ get { defaults.integer(forKey: highScoreKey) }
+ set { defaults.set(newValue, forKey: highScoreKey) }
+ }
+
+ var gamesPlayed: Int {
+ get { defaults.integer(forKey: gamesPlayedKey) }
+ set { defaults.set(newValue, forKey: gamesPlayedKey) }
+ }
+
+ var totalDogsCollected: Int {
+ get { defaults.integer(forKey: totalDogsCollectedKey) }
+ set { defaults.set(newValue, forKey: totalDogsCollectedKey) }
+ }
+
+ var totalHumansCollected: Int {
+ get { defaults.integer(forKey: totalHumansCollectedKey) }
+ set { defaults.set(newValue, forKey: totalHumansCollectedKey) }
+ }
+
+ var victories: Int {
+ get { defaults.integer(forKey: victoriesKey) }
+ set { defaults.set(newValue, forKey: victoriesKey) }
+ }
+
+ // MARK: - Initialization
+ private init() {}
+
+ // MARK: - Public Methods
+ func recordGameEnd(score: Int, dogsCollected: Int, humansCollected: Int, didWin: Bool) {
+ gamesPlayed += 1
+ totalDogsCollected += dogsCollected
+ totalHumansCollected += humansCollected
+
+ if score > highScore {
+ highScore = score
+ }
+
+ if didWin {
+ victories += 1
+ }
+ }
+
+ func isNewHighScore(_ score: Int) -> Bool {
+ return score > highScore
+ }
+
+ func resetStatistics() {
+ highScore = 0
+ gamesPlayed = 0
+ totalDogsCollected = 0
+ totalHumansCollected = 0
+ victories = 0
+ }
+
+ // MARK: - Formatted Statistics
+ func getStatisticsText() -> String {
+ return """
+ High Score: \(highScore)
+ Games Played: \(gamesPlayed)
+ Victories: \(victories)
+ Total Dogs: \(totalDogsCollected)
+ Total Humans: \(totalHumansCollected)
+ """
+ }
+}
diff --git a/RollkofferSimulator/Managers/SpawnManager.swift b/RollkofferSimulator/Managers/SpawnManager.swift
new file mode 100644
index 0000000..ba7515f
--- /dev/null
+++ b/RollkofferSimulator/Managers/SpawnManager.swift
@@ -0,0 +1,104 @@
+//
+// SpawnManager.swift
+// RollkofferSimulator
+//
+// Created by Ingo K.
+//
+
+import SpriteKit
+
+/// Manages spawning of entities at the top of the screen
+class SpawnManager {
+
+ // MARK: - Properties
+ private weak var scene: SKScene?
+ private var spawnTimer: TimeInterval = 0
+ private var nextSpawnInterval: TimeInterval = 0
+ private var isSpawning: Bool = false
+
+ // MARK: - Initialization
+ init(scene: SKScene) {
+ self.scene = scene
+ resetSpawnInterval()
+ }
+
+ // MARK: - Public Methods
+ func startSpawning() {
+ isSpawning = true
+ resetSpawnInterval()
+ }
+
+ func stopSpawning() {
+ isSpawning = false
+ }
+
+ func update(deltaTime: TimeInterval) {
+ guard isSpawning else { return }
+
+ spawnTimer += deltaTime
+
+ if spawnTimer >= nextSpawnInterval {
+ spawnEntity()
+ spawnTimer = 0
+ resetSpawnInterval()
+ }
+ }
+
+ // MARK: - Private Methods
+ private func resetSpawnInterval() {
+ nextSpawnInterval = TimeInterval.random(in: Constants.spawnIntervalMin...Constants.spawnIntervalMax)
+ }
+
+ private func spawnEntity() {
+ guard let scene = scene else { return }
+
+ let entityType = determineEntityType()
+ let entity = createEntity(type: entityType)
+
+ // Random x position
+ let margin: CGFloat = 60
+ let minX = margin
+ let maxX = scene.frame.width - margin
+ let randomX = CGFloat.random(in: minX...maxX)
+
+ // Spawn above screen
+ entity.position = CGPoint(x: randomX, y: scene.frame.height + 50)
+
+ scene.addChild(entity)
+
+ // Move entity down
+ let moveDistance = scene.frame.height + 200
+ let moveDuration = moveDistance / Constants.scrollSpeed
+ let moveAction = SKAction.moveBy(x: 0, y: -moveDistance, duration: moveDuration)
+ let removeAction = SKAction.removeFromParent()
+ entity.run(SKAction.sequence([moveAction, removeAction]))
+ }
+
+ private func determineEntityType() -> EntityType {
+ let roll = Int.random(in: 0..<100)
+
+ if roll < Constants.spawnChanceGoodDog {
+ // 40% good dogs (split between small and big)
+ let isSmall = Bool.random()
+ return .dog(isSmall ? .smallGood : .bigGood)
+ } else if roll < Constants.spawnChanceBadDog {
+ // 20% bad dogs
+ return .dog(.bad)
+ } else if roll < Constants.spawnChanceGreenHuman {
+ // 25% green humans
+ return .human(.green)
+ } else {
+ // 15% gray humans
+ return .human(.gray)
+ }
+ }
+
+ private func createEntity(type: EntityType) -> SKNode {
+ switch type {
+ case .dog(let dogType):
+ return DogNode(type: dogType)
+ case .human(let humanType):
+ return HumanNode(type: humanType)
+ }
+ }
+}
diff --git a/RollkofferSimulator/Models/EntityType.swift b/RollkofferSimulator/Models/EntityType.swift
new file mode 100644
index 0000000..cf477ae
--- /dev/null
+++ b/RollkofferSimulator/Models/EntityType.swift
@@ -0,0 +1,58 @@
+//
+// EntityType.swift
+// RollkofferSimulator
+//
+// Created by Ingo K.
+//
+
+import Foundation
+
+/// Types of dogs in the game
+enum DogType {
+ case smallGood // 40x40, gold brown, +10 points
+ case bigGood // 70x70, gold brown, +25 points
+ case bad // 55x55, red outlined, -1 life
+
+ var points: Int {
+ switch self {
+ case .smallGood: return Constants.pointsSmallGoodDog
+ case .bigGood: return Constants.pointsBigGoodDog
+ case .bad: return 0
+ }
+ }
+
+ var isHarmful: Bool {
+ return self == .bad
+ }
+
+ var countsTowardGoal: Bool {
+ return self == .smallGood || self == .bigGood
+ }
+}
+
+/// Types of humans in the game
+enum HumanType {
+ case green // 50x80, green, +15 points
+ case gray // 50x80, gray, -1 life
+
+ var points: Int {
+ switch self {
+ case .green: return Constants.pointsGreenHuman
+ case .gray: return 0
+ }
+ }
+
+ var isHarmful: Bool {
+ return self == .gray
+ }
+
+ var countsTowardGoal: Bool {
+ return self == .green
+ }
+}
+
+/// All entity types for spawn system
+enum EntityType {
+ case dog(DogType)
+ case human(HumanType)
+}
diff --git a/RollkofferSimulator/Models/GameState.swift b/RollkofferSimulator/Models/GameState.swift
new file mode 100644
index 0000000..9822b18
--- /dev/null
+++ b/RollkofferSimulator/Models/GameState.swift
@@ -0,0 +1,103 @@
+//
+// GameState.swift
+// RollkofferSimulator
+//
+// Created by Ingo K.
+//
+
+import Foundation
+
+/// Possible game states
+enum GameStateType {
+ case startScreen
+ case playing
+ case paused
+ case gameOver
+ case victory
+}
+
+/// Manages the current game state and statistics
+class GameState {
+
+ // MARK: - Properties
+ private(set) var currentState: GameStateType = .startScreen
+ private(set) var score: Int = 0
+ private(set) var lives: Int = Constants.startLives
+ private(set) var dogsCollected: Int = 0
+ private(set) var humansCollected: Int = 0
+ private(set) var timeRemaining: TimeInterval = Constants.gameTime
+
+ var isInvincible: Bool = false
+
+ // MARK: - Computed Properties
+ var hasWon: Bool {
+ return dogsCollected >= Constants.targetDogs &&
+ humansCollected >= Constants.targetHumans
+ }
+
+ var hasLost: Bool {
+ return lives <= 0 || (timeRemaining <= 0 && !hasWon)
+ }
+
+ // MARK: - State Management
+ func setState(_ state: GameStateType) {
+ currentState = state
+ }
+
+ func reset() {
+ score = 0
+ lives = Constants.startLives
+ dogsCollected = 0
+ humansCollected = 0
+ timeRemaining = Constants.gameTime
+ isInvincible = false
+ currentState = .playing
+ }
+
+ // MARK: - Score Management
+ func addPoints(_ points: Int) {
+ score += points
+ }
+
+ func collectDog() {
+ dogsCollected += 1
+ }
+
+ func collectHuman() {
+ humansCollected += 1
+ }
+
+ // MARK: - Lives Management
+ func loseLife() -> Bool {
+ guard !isInvincible else { return false }
+ lives -= 1
+ return true
+ }
+
+ // MARK: - Time Management
+ func updateTime(delta: TimeInterval) {
+ timeRemaining = max(0, timeRemaining - delta)
+ }
+
+ // MARK: - Formatted Strings
+ var formattedTime: String {
+ let seconds = Int(timeRemaining)
+ return "\(seconds)s"
+ }
+
+ var formattedScore: String {
+ return "SCORE: \(score)"
+ }
+
+ var formattedDogs: String {
+ return "๐ \(dogsCollected)/\(Constants.targetDogs)"
+ }
+
+ var formattedHumans: String {
+ return "๐ค \(humansCollected)/\(Constants.targetHumans)"
+ }
+
+ var formattedLives: String {
+ return String(repeating: "โค๏ธ", count: lives)
+ }
+}
diff --git a/RollkofferSimulator/Nodes/DogNode.swift b/RollkofferSimulator/Nodes/DogNode.swift
new file mode 100644
index 0000000..be59365
--- /dev/null
+++ b/RollkofferSimulator/Nodes/DogNode.swift
@@ -0,0 +1,162 @@
+//
+// DogNode.swift
+// RollkofferSimulator
+//
+// Created by Ingo K.
+//
+
+import SpriteKit
+
+/// A dog entity node
+class DogNode: SKNode {
+
+ // MARK: - Properties
+ let dogType: DogType
+ private let bodyNode: SKShapeNode
+
+ // MARK: - Initialization
+ init(type: DogType) {
+ self.dogType = type
+
+ let size: CGSize
+ let color: SKColor
+ let hasRedOutline: Bool
+
+ switch type {
+ case .smallGood:
+ size = Constants.smallDogSize
+ color = Constants.Colors.goodDogColor
+ hasRedOutline = false
+ case .bigGood:
+ size = Constants.bigDogSize
+ color = Constants.Colors.goodDogColor
+ hasRedOutline = false
+ case .bad:
+ size = Constants.badDogSize
+ color = Constants.Colors.goodDogColor
+ hasRedOutline = true
+ }
+
+ // Create dog body (simple oval shape)
+ let bodyPath = CGMutablePath()
+ bodyPath.addEllipse(in: CGRect(x: -size.width / 2, y: -size.height / 2,
+ width: size.width, height: size.height * 0.7))
+ bodyNode = SKShapeNode(path: bodyPath)
+ bodyNode.fillColor = color
+ bodyNode.strokeColor = hasRedOutline ? Constants.Colors.badDogColor : SKColor(white: 0.3, alpha: 1.0)
+ bodyNode.lineWidth = hasRedOutline ? 4 : 2
+
+ super.init()
+
+ addChild(bodyNode)
+
+ // Add head
+ let headSize = size.width * 0.5
+ let head = SKShapeNode(circleOfRadius: headSize / 2)
+ head.position = CGPoint(x: 0, y: size.height * 0.25)
+ head.fillColor = color
+ head.strokeColor = hasRedOutline ? Constants.Colors.badDogColor : SKColor(white: 0.3, alpha: 1.0)
+ head.lineWidth = hasRedOutline ? 3 : 1.5
+ addChild(head)
+
+ // Add ears
+ let earSize = headSize * 0.4
+ for xOffset in [-headSize * 0.4, headSize * 0.4] {
+ let ear = SKShapeNode(ellipseOf: CGSize(width: earSize, height: earSize * 1.5))
+ ear.position = CGPoint(x: xOffset, y: size.height * 0.25 + headSize * 0.35)
+ ear.fillColor = color.withAlphaComponent(0.8)
+ ear.strokeColor = hasRedOutline ? Constants.Colors.badDogColor : SKColor(white: 0.3, alpha: 1.0)
+ ear.lineWidth = hasRedOutline ? 2 : 1
+ addChild(ear)
+ }
+
+ // Add eyes
+ let eyeSize: CGFloat = headSize * 0.15
+ for xOffset in [-headSize * 0.15, headSize * 0.15] {
+ let eye = SKShapeNode(circleOfRadius: eyeSize)
+ eye.position = CGPoint(x: xOffset, y: size.height * 0.28)
+ eye.fillColor = hasRedOutline ? .red : .black
+ eye.strokeColor = .clear
+ addChild(eye)
+ }
+
+ // Add nose
+ let nose = SKShapeNode(circleOfRadius: headSize * 0.1)
+ nose.position = CGPoint(x: 0, y: size.height * 0.18)
+ nose.fillColor = .black
+ nose.strokeColor = .clear
+ addChild(nose)
+
+ // Add tail
+ let tailPath = CGMutablePath()
+ tailPath.move(to: CGPoint(x: 0, y: -size.height * 0.25))
+ tailPath.addQuadCurve(to: CGPoint(x: size.width * 0.3, y: -size.height * 0.1),
+ control: CGPoint(x: size.width * 0.4, y: -size.height * 0.3))
+ let tail = SKShapeNode(path: tailPath)
+ tail.strokeColor = color
+ tail.lineWidth = size.width * 0.1
+ tail.lineCap = .round
+ addChild(tail)
+
+ // Add legs
+ let legWidth = size.width * 0.12
+ let legHeight = size.height * 0.25
+ let legPositions: [CGFloat] = [-size.width * 0.25, -size.width * 0.1,
+ size.width * 0.1, size.width * 0.25]
+ for xPos in legPositions {
+ let leg = SKShapeNode(rect: CGRect(x: xPos - legWidth / 2,
+ y: -size.height * 0.35 - legHeight,
+ width: legWidth, height: legHeight),
+ cornerRadius: legWidth / 2)
+ leg.fillColor = color
+ leg.strokeColor = hasRedOutline ? Constants.Colors.badDogColor : SKColor(white: 0.3, alpha: 1.0)
+ leg.lineWidth = hasRedOutline ? 2 : 1
+ addChild(leg)
+ }
+
+ // Add angry expression for bad dogs
+ if hasRedOutline {
+ let browPath = CGMutablePath()
+ browPath.move(to: CGPoint(x: -headSize * 0.3, y: size.height * 0.35))
+ browPath.addLine(to: CGPoint(x: -headSize * 0.05, y: size.height * 0.32))
+ let leftBrow = SKShapeNode(path: browPath)
+ leftBrow.strokeColor = .black
+ leftBrow.lineWidth = 2
+ addChild(leftBrow)
+
+ let browPath2 = CGMutablePath()
+ browPath2.move(to: CGPoint(x: headSize * 0.3, y: size.height * 0.35))
+ browPath2.addLine(to: CGPoint(x: headSize * 0.05, y: size.height * 0.32))
+ let rightBrow = SKShapeNode(path: browPath2)
+ rightBrow.strokeColor = .black
+ rightBrow.lineWidth = 2
+ addChild(rightBrow)
+ }
+
+ setupPhysics(size: size)
+ self.zPosition = Constants.ZPosition.entities
+ self.name = type.isHarmful ? "badDog" : "goodDog"
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ // MARK: - Physics Setup
+ private func setupPhysics(size: CGSize) {
+ let radius = min(size.width, size.height) / 2
+ physicsBody = SKPhysicsBody(circleOfRadius: radius)
+ physicsBody?.isDynamic = true
+ physicsBody?.affectedByGravity = false
+ physicsBody?.allowsRotation = false
+
+ if dogType.isHarmful {
+ physicsBody?.categoryBitMask = Constants.PhysicsCategory.badDog
+ } else {
+ physicsBody?.categoryBitMask = Constants.PhysicsCategory.goodDog
+ }
+
+ physicsBody?.contactTestBitMask = Constants.PhysicsCategory.suitcase
+ physicsBody?.collisionBitMask = Constants.PhysicsCategory.none
+ }
+}
diff --git a/RollkofferSimulator/Nodes/HumanNode.swift b/RollkofferSimulator/Nodes/HumanNode.swift
new file mode 100644
index 0000000..f00aaff
--- /dev/null
+++ b/RollkofferSimulator/Nodes/HumanNode.swift
@@ -0,0 +1,176 @@
+//
+// HumanNode.swift
+// RollkofferSimulator
+//
+// Created by Ingo K.
+//
+
+import SpriteKit
+
+/// A human entity node
+class HumanNode: SKNode {
+
+ // MARK: - Properties
+ let humanType: HumanType
+ private let bodyNode: SKShapeNode
+
+ // MARK: - Initialization
+ init(type: HumanType) {
+ self.humanType = type
+
+ let size = Constants.humanSize
+ let color: SKColor
+
+ switch type {
+ case .green:
+ color = Constants.Colors.greenHumanColor
+ case .gray:
+ color = Constants.Colors.grayHumanColor
+ }
+
+ // Create body (torso)
+ let torsoHeight = size.height * 0.4
+ let torsoWidth = size.width * 0.6
+ let torsoRect = CGRect(x: -torsoWidth / 2, y: -size.height * 0.1,
+ width: torsoWidth, height: torsoHeight)
+ bodyNode = SKShapeNode(rect: torsoRect, cornerRadius: 5)
+ bodyNode.fillColor = color
+ bodyNode.strokeColor = color.withAlphaComponent(0.7)
+ bodyNode.lineWidth = 2
+
+ super.init()
+
+ addChild(bodyNode)
+
+ // Add head
+ let headRadius = size.width * 0.25
+ let head = SKShapeNode(circleOfRadius: headRadius)
+ head.position = CGPoint(x: 0, y: size.height * 0.35)
+ head.fillColor = SKColor(red: 1.0, green: 0.87, blue: 0.77, alpha: 1.0) // Skin tone
+ head.strokeColor = SKColor(red: 0.9, green: 0.75, blue: 0.65, alpha: 1.0)
+ head.lineWidth = 1
+ addChild(head)
+
+ // Add eyes
+ let eyeSize: CGFloat = 3
+ for xOffset in [-headRadius * 0.35, headRadius * 0.35] {
+ let eye = SKShapeNode(circleOfRadius: eyeSize)
+ eye.position = CGPoint(x: xOffset, y: size.height * 0.37)
+ eye.fillColor = .black
+ eye.strokeColor = .clear
+ addChild(eye)
+ }
+
+ // Add mouth/expression
+ if type == .green {
+ // Happy smile
+ let smilePath = CGMutablePath()
+ smilePath.addArc(center: CGPoint(x: 0, y: size.height * 0.32),
+ radius: headRadius * 0.3,
+ startAngle: .pi * 0.2,
+ endAngle: .pi * 0.8,
+ clockwise: true)
+ let smile = SKShapeNode(path: smilePath)
+ smile.strokeColor = .black
+ smile.lineWidth = 2
+ smile.lineCap = .round
+ addChild(smile)
+ } else {
+ // Neutral/frowning expression
+ let frownPath = CGMutablePath()
+ frownPath.move(to: CGPoint(x: -headRadius * 0.25, y: size.height * 0.3))
+ frownPath.addLine(to: CGPoint(x: headRadius * 0.25, y: size.height * 0.3))
+ let frown = SKShapeNode(path: frownPath)
+ frown.strokeColor = .black
+ frown.lineWidth = 2
+ addChild(frown)
+ }
+
+ // Add arms
+ let armWidth: CGFloat = 6
+ let armLength = size.height * 0.3
+ for xOffset in [-torsoWidth / 2 - armWidth / 2, torsoWidth / 2 + armWidth / 2] {
+ let arm = SKShapeNode(rect: CGRect(x: xOffset - armWidth / 2,
+ y: size.height * 0.05,
+ width: armWidth, height: armLength),
+ cornerRadius: armWidth / 2)
+ arm.fillColor = color
+ arm.strokeColor = color.withAlphaComponent(0.7)
+ arm.lineWidth = 1
+ addChild(arm)
+
+ // Add hand
+ let hand = SKShapeNode(circleOfRadius: armWidth * 0.8)
+ hand.position = CGPoint(x: xOffset, y: size.height * 0.05)
+ hand.fillColor = SKColor(red: 1.0, green: 0.87, blue: 0.77, alpha: 1.0)
+ hand.strokeColor = .clear
+ addChild(hand)
+ }
+
+ // Add legs
+ let legWidth: CGFloat = 10
+ let legHeight = size.height * 0.35
+ for xOffset in [-torsoWidth * 0.25, torsoWidth * 0.25] {
+ let leg = SKShapeNode(rect: CGRect(x: xOffset - legWidth / 2,
+ y: -size.height * 0.45,
+ width: legWidth, height: legHeight),
+ cornerRadius: legWidth / 2)
+ leg.fillColor = SKColor(red: 0.2, green: 0.2, blue: 0.4, alpha: 1.0) // Pants color
+ leg.strokeColor = SKColor(red: 0.15, green: 0.15, blue: 0.3, alpha: 1.0)
+ leg.lineWidth = 1
+ addChild(leg)
+
+ // Add shoe
+ let shoe = SKShapeNode(rect: CGRect(x: xOffset - legWidth * 0.6,
+ y: -size.height * 0.48,
+ width: legWidth * 1.2, height: 6),
+ cornerRadius: 2)
+ shoe.fillColor = .black
+ shoe.strokeColor = .clear
+ addChild(shoe)
+ }
+
+ // Add indicator icon for type
+ if type == .green {
+ let checkmark = SKLabelNode(text: "โ")
+ checkmark.fontSize = 16
+ checkmark.fontColor = .white
+ checkmark.position = CGPoint(x: 0, y: size.height * 0.1)
+ checkmark.verticalAlignmentMode = .center
+ addChild(checkmark)
+ } else {
+ let xMark = SKLabelNode(text: "โ")
+ xMark.fontSize = 16
+ xMark.fontColor = .white
+ xMark.position = CGPoint(x: 0, y: size.height * 0.1)
+ xMark.verticalAlignmentMode = .center
+ addChild(xMark)
+ }
+
+ setupPhysics(size: size)
+ self.zPosition = Constants.ZPosition.entities
+ self.name = type.isHarmful ? "grayHuman" : "greenHuman"
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ // MARK: - Physics Setup
+ private func setupPhysics(size: CGSize) {
+ physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: size.width * 0.6,
+ height: size.height * 0.8))
+ physicsBody?.isDynamic = true
+ physicsBody?.affectedByGravity = false
+ physicsBody?.allowsRotation = false
+
+ if humanType.isHarmful {
+ physicsBody?.categoryBitMask = Constants.PhysicsCategory.grayHuman
+ } else {
+ physicsBody?.categoryBitMask = Constants.PhysicsCategory.greenHuman
+ }
+
+ physicsBody?.contactTestBitMask = Constants.PhysicsCategory.suitcase
+ physicsBody?.collisionBitMask = Constants.PhysicsCategory.none
+ }
+}
diff --git a/RollkofferSimulator/Nodes/PlayerNode.swift b/RollkofferSimulator/Nodes/PlayerNode.swift
new file mode 100644
index 0000000..7064b49
--- /dev/null
+++ b/RollkofferSimulator/Nodes/PlayerNode.swift
@@ -0,0 +1,126 @@
+//
+// PlayerNode.swift
+// RollkofferSimulator
+//
+// Created by Ingo K.
+//
+
+import SpriteKit
+
+/// The player-controlled suitcase node
+class PlayerNode: SKNode {
+
+ // MARK: - Properties
+ private let suitcaseBody: SKShapeNode
+ private let handle: SKShapeNode
+ private let wheels: [SKShapeNode]
+
+ // MARK: - Initialization
+ override init() {
+ let size = Constants.suitcaseSize
+
+ // Create suitcase body (rounded rectangle)
+ let bodyRect = CGRect(x: -size.width / 2, y: -size.height / 2 + 10,
+ width: size.width, height: size.height - 15)
+ suitcaseBody = SKShapeNode(rect: bodyRect, cornerRadius: 8)
+ suitcaseBody.fillColor = Constants.Colors.suitcaseColor
+ suitcaseBody.strokeColor = SKColor(white: 0.2, alpha: 1.0)
+ suitcaseBody.lineWidth = 2
+
+ // Create handle
+ let handlePath = CGMutablePath()
+ handlePath.move(to: CGPoint(x: -10, y: size.height / 2 - 5))
+ handlePath.addLine(to: CGPoint(x: -10, y: size.height / 2 + 15))
+ handlePath.addLine(to: CGPoint(x: 10, y: size.height / 2 + 15))
+ handlePath.addLine(to: CGPoint(x: 10, y: size.height / 2 - 5))
+ handle = SKShapeNode(path: handlePath)
+ handle.strokeColor = SKColor(white: 0.3, alpha: 1.0)
+ handle.lineWidth = 4
+ handle.lineCap = .round
+
+ // Create wheels
+ var tempWheels: [SKShapeNode] = []
+ let wheelPositions = [
+ CGPoint(x: -size.width / 2 + 8, y: -size.height / 2 + 5),
+ CGPoint(x: size.width / 2 - 8, y: -size.height / 2 + 5)
+ ]
+ for pos in wheelPositions {
+ let wheel = SKShapeNode(circleOfRadius: 6)
+ wheel.position = pos
+ wheel.fillColor = SKColor.darkGray
+ wheel.strokeColor = SKColor.black
+ wheel.lineWidth = 1
+ tempWheels.append(wheel)
+ }
+ wheels = tempWheels
+
+ super.init()
+
+ // Add decorative stripes
+ let stripe1 = SKShapeNode(rect: CGRect(x: -size.width / 2 + 5, y: 0,
+ width: size.width - 10, height: 3))
+ stripe1.fillColor = SKColor(white: 0.3, alpha: 0.5)
+ stripe1.strokeColor = .clear
+
+ let stripe2 = SKShapeNode(rect: CGRect(x: -size.width / 2 + 5, y: -15,
+ width: size.width - 10, height: 3))
+ stripe2.fillColor = SKColor(white: 0.3, alpha: 0.5)
+ stripe2.strokeColor = .clear
+
+ addChild(suitcaseBody)
+ addChild(handle)
+ addChild(stripe1)
+ addChild(stripe2)
+ for wheel in wheels {
+ addChild(wheel)
+ }
+
+ setupPhysics()
+ self.zPosition = Constants.ZPosition.player
+ self.name = "player"
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ // MARK: - Physics Setup
+ private func setupPhysics() {
+ let size = Constants.suitcaseSize
+ physicsBody = SKPhysicsBody(rectangleOf: size)
+ physicsBody?.isDynamic = true
+ physicsBody?.affectedByGravity = false
+ physicsBody?.allowsRotation = false
+ physicsBody?.categoryBitMask = Constants.PhysicsCategory.suitcase
+ physicsBody?.contactTestBitMask = Constants.PhysicsCategory.all
+ physicsBody?.collisionBitMask = Constants.PhysicsCategory.none
+ }
+
+ // MARK: - Visual Effects
+ func startBlinking() {
+ let fadeOut = SKAction.fadeAlpha(to: 0.3, duration: Constants.blinkDuration)
+ let fadeIn = SKAction.fadeAlpha(to: 1.0, duration: Constants.blinkDuration)
+ let blink = SKAction.sequence([fadeOut, fadeIn])
+ let blinkRepeat = SKAction.repeat(blink, count: Constants.blinkCount)
+ run(blinkRepeat, withKey: "blink")
+ }
+
+ func stopBlinking() {
+ removeAction(forKey: "blink")
+ alpha = 1.0
+ }
+
+ // MARK: - Movement
+ func constrainToScreen(in frame: CGRect) {
+ let halfWidth = Constants.suitcaseSize.width / 2
+ let halfHeight = Constants.suitcaseSize.height / 2
+
+ let minX = frame.minX + halfWidth + 10
+ let maxX = frame.maxX - halfWidth - 10
+ let minY = frame.minY + halfHeight + 10
+ let maxY = frame.maxY - halfHeight - 100 // Leave space for UI
+
+ position.x = max(minX, min(maxX, position.x))
+ position.y = max(minY, min(maxY, position.y))
+ }
+}
diff --git a/RollkofferSimulator/RollkofferSimulator.xcodeproj/project.pbxproj b/RollkofferSimulator/RollkofferSimulator.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..0531fdd
--- /dev/null
+++ b/RollkofferSimulator/RollkofferSimulator.xcodeproj/project.pbxproj
@@ -0,0 +1,457 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 56;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 001 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 101 /* AppDelegate.swift */; };
+ 002 /* GameViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 102 /* GameViewController.swift */; };
+ 003 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 103 /* Constants.swift */; };
+ 004 /* EntityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 104 /* EntityType.swift */; };
+ 005 /* GameState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 105 /* GameState.swift */; };
+ 006 /* PlayerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 106 /* PlayerNode.swift */; };
+ 007 /* DogNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 107 /* DogNode.swift */; };
+ 008 /* HumanNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 108 /* HumanNode.swift */; };
+ 009 /* SpawnManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 109 /* SpawnManager.swift */; };
+ 010 /* CollisionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 110 /* CollisionManager.swift */; };
+ 011 /* ScoreManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 111 /* ScoreManager.swift */; };
+ 012 /* MenuScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 112 /* MenuScene.swift */; };
+ 013 /* GameScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 113 /* GameScene.swift */; };
+ 014 /* GameOverScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 114 /* GameOverScene.swift */; };
+ 015 /* VictoryScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 115 /* VictoryScene.swift */; };
+ 016 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 116 /* Assets.xcassets */; };
+ 017 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 117 /* Main.storyboard */; };
+ 018 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 118 /* LaunchScreen.storyboard */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXFileReference section */
+ 100 /* RollkofferSimulator.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RollkofferSimulator.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 101 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ 102 /* GameViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameViewController.swift; sourceTree = ""; };
+ 103 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; };
+ 104 /* EntityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntityType.swift; sourceTree = ""; };
+ 105 /* GameState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameState.swift; sourceTree = ""; };
+ 106 /* PlayerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerNode.swift; sourceTree = ""; };
+ 107 /* DogNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DogNode.swift; sourceTree = ""; };
+ 108 /* HumanNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HumanNode.swift; sourceTree = ""; };
+ 109 /* SpawnManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpawnManager.swift; sourceTree = ""; };
+ 110 /* CollisionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollisionManager.swift; sourceTree = ""; };
+ 111 /* ScoreManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScoreManager.swift; sourceTree = ""; };
+ 112 /* MenuScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuScene.swift; sourceTree = ""; };
+ 113 /* GameScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameScene.swift; sourceTree = ""; };
+ 114 /* GameOverScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameOverScene.swift; sourceTree = ""; };
+ 115 /* VictoryScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VictoryScene.swift; sourceTree = ""; };
+ 116 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ 117 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
+ 118 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
+ 119 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 200 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 300 = {
+ isa = PBXGroup;
+ children = (
+ 301 /* RollkofferSimulator */,
+ 302 /* Products */,
+ );
+ sourceTree = "";
+ };
+ 301 /* RollkofferSimulator */ = {
+ isa = PBXGroup;
+ children = (
+ 101 /* AppDelegate.swift */,
+ 102 /* GameViewController.swift */,
+ 303 /* Scenes */,
+ 304 /* Nodes */,
+ 305 /* Managers */,
+ 306 /* Models */,
+ 307 /* Utils */,
+ 116 /* Assets.xcassets */,
+ 117 /* Main.storyboard */,
+ 118 /* LaunchScreen.storyboard */,
+ 119 /* Info.plist */,
+ );
+ path = .;
+ sourceTree = "";
+ };
+ 302 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 100 /* RollkofferSimulator.app */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 303 /* Scenes */ = {
+ isa = PBXGroup;
+ children = (
+ 112 /* MenuScene.swift */,
+ 113 /* GameScene.swift */,
+ 114 /* GameOverScene.swift */,
+ 115 /* VictoryScene.swift */,
+ );
+ path = Scenes;
+ sourceTree = "";
+ };
+ 304 /* Nodes */ = {
+ isa = PBXGroup;
+ children = (
+ 106 /* PlayerNode.swift */,
+ 107 /* DogNode.swift */,
+ 108 /* HumanNode.swift */,
+ );
+ path = Nodes;
+ sourceTree = "";
+ };
+ 305 /* Managers */ = {
+ isa = PBXGroup;
+ children = (
+ 109 /* SpawnManager.swift */,
+ 110 /* CollisionManager.swift */,
+ 111 /* ScoreManager.swift */,
+ );
+ path = Managers;
+ sourceTree = "";
+ };
+ 306 /* Models */ = {
+ isa = PBXGroup;
+ children = (
+ 104 /* EntityType.swift */,
+ 105 /* GameState.swift */,
+ );
+ path = Models;
+ sourceTree = "";
+ };
+ 307 /* Utils */ = {
+ isa = PBXGroup;
+ children = (
+ 103 /* Constants.swift */,
+ );
+ path = Utils;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 400 /* RollkofferSimulator */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 501 /* Build configuration list for PBXNativeTarget "RollkofferSimulator" */;
+ buildPhases = (
+ 401 /* Sources */,
+ 200 /* Frameworks */,
+ 402 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = RollkofferSimulator;
+ productName = RollkofferSimulator;
+ productReference = 100 /* RollkofferSimulator.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 600 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ BuildIndependentTargetsInParallel = 1;
+ LastSwiftUpdateCheck = 1500;
+ LastUpgradeCheck = 1500;
+ TargetAttributes = {
+ 400 = {
+ CreatedOnToolsVersion = 15.0;
+ };
+ };
+ };
+ buildConfigurationList = 500 /* Build configuration list for PBXProject "RollkofferSimulator" */;
+ compatibilityVersion = "Xcode 14.0";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ de,
+ );
+ mainGroup = 300;
+ productRefGroup = 302 /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 400 /* RollkofferSimulator */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 402 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 016 /* Assets.xcassets in Resources */,
+ 017 /* Main.storyboard in Resources */,
+ 018 /* LaunchScreen.storyboard in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 401 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 001 /* AppDelegate.swift in Sources */,
+ 002 /* GameViewController.swift in Sources */,
+ 003 /* Constants.swift in Sources */,
+ 004 /* EntityType.swift in Sources */,
+ 005 /* GameState.swift in Sources */,
+ 006 /* PlayerNode.swift in Sources */,
+ 007 /* DogNode.swift in Sources */,
+ 008 /* HumanNode.swift in Sources */,
+ 009 /* SpawnManager.swift in Sources */,
+ 010 /* CollisionManager.swift in Sources */,
+ 011 /* ScoreManager.swift in Sources */,
+ 012 /* MenuScene.swift in Sources */,
+ 013 /* GameScene.swift in Sources */,
+ 014 /* GameOverScene.swift in Sources */,
+ 015 /* VictoryScene.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXVariantGroup section */
+ 117 /* Main.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 117 /* Base */,
+ );
+ name = Main.storyboard;
+ sourceTree = "";
+ };
+ 118 /* LaunchScreen.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 118 /* Base */,
+ );
+ name = LaunchScreen.storyboard;
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 700 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 15.0;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ };
+ name = Debug;
+ };
+ 701 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 15.0;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ 702 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = Info.plist;
+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
+ INFOPLIST_KEY_UIMainStoryboardFile = Main;
+ INFOPLIST_KEY_UIStatusBarHidden = YES;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.ingok.RollkofferSimulator;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 703 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = Info.plist;
+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+ INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
+ INFOPLIST_KEY_UIMainStoryboardFile = Main;
+ INFOPLIST_KEY_UIStatusBarHidden = YES;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.ingok.RollkofferSimulator;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 500 /* Build configuration list for PBXProject "RollkofferSimulator" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 700 /* Debug */,
+ 701 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 501 /* Build configuration list for PBXNativeTarget "RollkofferSimulator" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 702 /* Debug */,
+ 703 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 600 /* Project object */;
+}
diff --git a/RollkofferSimulator/Scenes/GameOverScene.swift b/RollkofferSimulator/Scenes/GameOverScene.swift
new file mode 100644
index 0000000..6d66b8f
--- /dev/null
+++ b/RollkofferSimulator/Scenes/GameOverScene.swift
@@ -0,0 +1,255 @@
+//
+// GameOverScene.swift
+// RollkofferSimulator
+//
+// Created by Ingo K.
+//
+
+import SpriteKit
+
+class GameOverScene: SKScene {
+
+ // MARK: - Properties
+ var finalScore: Int = 0
+ var dogsCollected: Int = 0
+ var humansCollected: Int = 0
+ var isNewHighScore: Bool = false
+
+ private var retryButton: SKShapeNode!
+ private var menuButton: SKShapeNode!
+
+ // MARK: - Scene Lifecycle
+ override func didMove(to view: SKView) {
+ setupBackground()
+ setupContent()
+ setupButtons()
+ startAnimations()
+ }
+
+ // MARK: - Setup
+ private func setupBackground() {
+ backgroundColor = SKColor(red: 0.15, green: 0.1, blue: 0.1, alpha: 1.0)
+
+ // Add dim pattern
+ for i in 0..<20 {
+ let x = CGFloat.random(in: 0...frame.width)
+ let y = CGFloat.random(in: 0...frame.height)
+ let size = CGFloat.random(in: 20...60)
+
+ let shape = SKShapeNode(circleOfRadius: size)
+ shape.position = CGPoint(x: x, y: y)
+ shape.fillColor = SKColor.red.withAlphaComponent(0.05)
+ shape.strokeColor = .clear
+ shape.zPosition = 0.1
+ addChild(shape)
+ }
+ }
+
+ private func setupContent() {
+ // Game Over title
+ let titleLabel = SKLabelNode(text: "๐ GAME OVER ๐")
+ titleLabel.fontName = "AvenirNext-Heavy"
+ titleLabel.fontSize = 42
+ titleLabel.fontColor = .red
+ titleLabel.position = CGPoint(x: frame.midX, y: frame.height * 0.8)
+ titleLabel.zPosition = Constants.ZPosition.ui
+ addChild(titleLabel)
+
+ // Subtitle based on reason
+ let subtitleText: String
+ if dogsCollected < Constants.targetDogs || humansCollected < Constants.targetHumans {
+ subtitleText = "Zeit abgelaufen!"
+ } else {
+ subtitleText = "Keine Leben mehr!"
+ }
+
+ let subtitleLabel = SKLabelNode(text: subtitleText)
+ subtitleLabel.fontName = "AvenirNext-Medium"
+ subtitleLabel.fontSize = 22
+ subtitleLabel.fontColor = SKColor(white: 0.8, alpha: 1.0)
+ subtitleLabel.position = CGPoint(x: frame.midX, y: frame.height * 0.72)
+ subtitleLabel.zPosition = Constants.ZPosition.ui
+ addChild(subtitleLabel)
+
+ // Score display
+ let scoreLabel = SKLabelNode(text: "Punkte: \(finalScore)")
+ scoreLabel.fontName = "AvenirNext-Bold"
+ scoreLabel.fontSize = 32
+ scoreLabel.fontColor = .white
+ scoreLabel.position = CGPoint(x: frame.midX, y: frame.height * 0.58)
+ scoreLabel.zPosition = Constants.ZPosition.ui
+ addChild(scoreLabel)
+
+ // New high score indicator
+ if isNewHighScore {
+ let highScoreLabel = SKLabelNode(text: "๐ NEUER HIGHSCORE! ๐")
+ highScoreLabel.fontName = "AvenirNext-Heavy"
+ highScoreLabel.fontSize = 24
+ highScoreLabel.fontColor = SKColor.yellow
+ highScoreLabel.position = CGPoint(x: frame.midX, y: frame.height * 0.65)
+ highScoreLabel.zPosition = Constants.ZPosition.ui
+ addChild(highScoreLabel)
+
+ // Animate high score
+ let scale = SKAction.sequence([
+ SKAction.scale(to: 1.1, duration: 0.5),
+ SKAction.scale(to: 1.0, duration: 0.5)
+ ])
+ highScoreLabel.run(SKAction.repeatForever(scale))
+ }
+
+ // Stats display
+ let dogsText = "๐ \(dogsCollected)/\(Constants.targetDogs)"
+ let humansText = "๐ค \(humansCollected)/\(Constants.targetHumans)"
+
+ let statsLabel = SKLabelNode(text: "\(dogsText) | \(humansText)")
+ statsLabel.fontName = "AvenirNext-Medium"
+ statsLabel.fontSize = 24
+ statsLabel.fontColor = SKColor(white: 0.7, alpha: 1.0)
+ statsLabel.position = CGPoint(x: frame.midX, y: frame.height * 0.48)
+ statsLabel.zPosition = Constants.ZPosition.ui
+ addChild(statsLabel)
+
+ // Progress indicators
+ let dogsProgress = min(1.0, CGFloat(dogsCollected) / CGFloat(Constants.targetDogs))
+ let humansProgress = min(1.0, CGFloat(humansCollected) / CGFloat(Constants.targetHumans))
+
+ addProgressBar(at: CGPoint(x: frame.midX - 60, y: frame.height * 0.42),
+ progress: dogsProgress, color: .orange, label: "Hunde")
+ addProgressBar(at: CGPoint(x: frame.midX + 60, y: frame.height * 0.42),
+ progress: humansProgress, color: .green, label: "Menschen")
+
+ // Sad suitcase
+ let sadSuitcase = PlayerNode()
+ sadSuitcase.position = CGPoint(x: frame.midX, y: frame.height * 0.25)
+ sadSuitcase.alpha = 0.6
+ sadSuitcase.setScale(0.8)
+ addChild(sadSuitcase)
+
+ // Sad face on suitcase area
+ let sadFace = SKLabelNode(text: "๐ข")
+ sadFace.fontSize = 30
+ sadFace.position = CGPoint(x: frame.midX, y: frame.height * 0.25 + 20)
+ sadFace.zPosition = Constants.ZPosition.ui
+ addChild(sadFace)
+ }
+
+ private func addProgressBar(at position: CGPoint, progress: CGFloat, color: SKColor, label: String) {
+ let barWidth: CGFloat = 80
+ let barHeight: CGFloat = 12
+
+ // Background
+ let bg = SKShapeNode(rect: CGRect(x: -barWidth / 2, y: 0, width: barWidth, height: barHeight),
+ cornerRadius: 6)
+ bg.position = position
+ bg.fillColor = SKColor(white: 0.3, alpha: 1.0)
+ bg.strokeColor = .clear
+ bg.zPosition = Constants.ZPosition.ui
+ addChild(bg)
+
+ // Progress fill
+ let fillWidth = barWidth * progress
+ if fillWidth > 0 {
+ let fill = SKShapeNode(rect: CGRect(x: -barWidth / 2, y: 0, width: fillWidth, height: barHeight),
+ cornerRadius: 6)
+ fill.position = position
+ fill.fillColor = progress >= 1.0 ? .green : color
+ fill.strokeColor = .clear
+ fill.zPosition = Constants.ZPosition.ui + 0.1
+ addChild(fill)
+ }
+ }
+
+ private func setupButtons() {
+ // Retry button
+ retryButton = createButton(text: "๐ Nochmal", color: SKColor(red: 0.2, green: 0.6, blue: 0.2, alpha: 1.0))
+ retryButton.position = CGPoint(x: frame.midX, y: frame.height * 0.15)
+ retryButton.name = "retryButton"
+ addChild(retryButton)
+
+ // Menu button
+ menuButton = createButton(text: "๐ Menรผ", color: SKColor(red: 0.3, green: 0.3, blue: 0.5, alpha: 1.0))
+ menuButton.position = CGPoint(x: frame.midX, y: frame.height * 0.08)
+ menuButton.name = "menuButton"
+ addChild(menuButton)
+ }
+
+ private func createButton(text: String, color: SKColor) -> SKShapeNode {
+ let buttonWidth: CGFloat = 180
+ let buttonHeight: CGFloat = 50
+
+ let button = SKShapeNode(rect: CGRect(x: -buttonWidth / 2, y: -buttonHeight / 2,
+ width: buttonWidth, height: buttonHeight),
+ cornerRadius: 12)
+ button.fillColor = color
+ button.strokeColor = color.withAlphaComponent(0.5)
+ button.lineWidth = 2
+ button.zPosition = Constants.ZPosition.ui
+
+ let label = SKLabelNode(text: text)
+ label.fontName = "AvenirNext-Bold"
+ label.fontSize = 22
+ label.fontColor = .white
+ label.verticalAlignmentMode = .center
+ label.zPosition = 1
+ button.addChild(label)
+
+ return button
+ }
+
+ private func startAnimations() {
+ // Fade in effect
+ alpha = 0
+ let fadeIn = SKAction.fadeIn(withDuration: 0.5)
+ run(fadeIn)
+
+ // Button pulse
+ let pulse = SKAction.sequence([
+ SKAction.scale(to: 1.05, duration: 0.8),
+ SKAction.scale(to: 1.0, duration: 0.8)
+ ])
+ retryButton.run(SKAction.repeatForever(pulse))
+ }
+
+ // MARK: - Touch Handling
+ override func touchesBegan(_ touches: Set, with event: UIEvent?) {
+ guard let touch = touches.first else { return }
+ let location = touch.location(in: self)
+
+ if retryButton.contains(location) {
+ retryGame()
+ } else if menuButton.contains(location) {
+ returnToMenu()
+ }
+ }
+
+ private func retryGame() {
+ let pressDown = SKAction.scale(to: 0.9, duration: 0.1)
+ let pressUp = SKAction.scale(to: 1.0, duration: 0.1)
+
+ retryButton.run(SKAction.sequence([pressDown, pressUp])) { [weak self] in
+ guard let self = self else { return }
+
+ let gameScene = GameScene(size: self.size)
+ gameScene.scaleMode = self.scaleMode
+
+ let transition = SKTransition.fade(withDuration: 0.5)
+ self.view?.presentScene(gameScene, transition: transition)
+ }
+ }
+
+ private func returnToMenu() {
+ let pressDown = SKAction.scale(to: 0.9, duration: 0.1)
+ let pressUp = SKAction.scale(to: 1.0, duration: 0.1)
+
+ menuButton.run(SKAction.sequence([pressDown, pressUp])) { [weak self] in
+ guard let self = self else { return }
+
+ let menuScene = MenuScene(size: self.size)
+ menuScene.scaleMode = self.scaleMode
+
+ let transition = SKTransition.fade(withDuration: 0.5)
+ self.view?.presentScene(menuScene, transition: transition)
+ }
+ }
+}
diff --git a/RollkofferSimulator/Scenes/GameScene.swift b/RollkofferSimulator/Scenes/GameScene.swift
new file mode 100644
index 0000000..1df64d7
--- /dev/null
+++ b/RollkofferSimulator/Scenes/GameScene.swift
@@ -0,0 +1,537 @@
+//
+// GameScene.swift
+// RollkofferSimulator
+//
+// Created by Ingo K.
+//
+
+import SpriteKit
+
+class GameScene: SKScene {
+
+ // MARK: - Game Objects
+ private var player: PlayerNode!
+ private var gameState: GameState!
+
+ // MARK: - Managers
+ private var spawnManager: SpawnManager!
+ private var collisionManager: CollisionManager!
+
+ // MARK: - UI Elements
+ private var livesLabel: SKLabelNode!
+ private var scoreLabel: SKLabelNode!
+ private var dogsLabel: SKLabelNode!
+ private var humansLabel: SKLabelNode!
+ private var timerLabel: SKLabelNode!
+ private var pauseButton: SKShapeNode!
+ private var pauseOverlay: SKNode?
+
+ // MARK: - Touch Handling
+ private var touchOffset: CGPoint = .zero
+ private var isDragging: Bool = false
+
+ // MARK: - Timing
+ private var lastUpdateTime: TimeInterval = 0
+
+ // MARK: - Background Scrolling
+ private var floorTiles: [SKShapeNode] = []
+ private let tileSize: CGFloat = 80
+
+ // MARK: - Scene Lifecycle
+ override func didMove(to view: SKView) {
+ setupGame()
+ }
+
+ // MARK: - Setup
+ private func setupGame() {
+ // Initialize game state
+ gameState = GameState()
+ gameState.reset()
+
+ // Setup physics
+ physicsWorld.gravity = .zero
+ collisionManager = CollisionManager()
+ collisionManager.delegate = self
+ collisionManager.setScene(self)
+ physicsWorld.contactDelegate = collisionManager
+
+ setupBackground()
+ setupPlayer()
+ setupUI()
+ setupSpawnManager()
+
+ spawnManager.startSpawning()
+ }
+
+ private func setupBackground() {
+ backgroundColor = Constants.Colors.backgroundColor
+
+ // Create scrolling floor tiles
+ let rows = Int(frame.height / tileSize) + 3
+ let cols = Int(frame.width / tileSize) + 1
+
+ for row in 0.. SKLabelNode {
+ let label = SKLabelNode(text: text)
+ label.fontName = "AvenirNext-Bold"
+ label.fontSize = fontSize
+ label.fontColor = .darkGray
+ label.zPosition = Constants.ZPosition.ui
+ label.verticalAlignmentMode = .center
+ return label
+ }
+
+ private func setupPauseButton() {
+ let buttonSize: CGFloat = 36
+ pauseButton = SKShapeNode(rect: CGRect(x: -buttonSize / 2, y: -buttonSize / 2,
+ width: buttonSize, height: buttonSize),
+ cornerRadius: 8)
+ pauseButton.fillColor = SKColor(white: 0.9, alpha: 1.0)
+ pauseButton.strokeColor = SKColor.gray
+ pauseButton.lineWidth = 2
+ pauseButton.position = CGPoint(x: 40, y: frame.height - 75)
+ pauseButton.zPosition = Constants.ZPosition.ui
+ pauseButton.name = "pauseButton"
+ addChild(pauseButton)
+
+ // Pause icon (two vertical bars)
+ let barWidth: CGFloat = 4
+ let barHeight: CGFloat = 16
+ let barGap: CGFloat = 5
+
+ let leftBar = SKShapeNode(rect: CGRect(x: -barGap - barWidth / 2, y: -barHeight / 2,
+ width: barWidth, height: barHeight))
+ leftBar.fillColor = .darkGray
+ leftBar.strokeColor = .clear
+ pauseButton.addChild(leftBar)
+
+ let rightBar = SKShapeNode(rect: CGRect(x: barGap - barWidth / 2, y: -barHeight / 2,
+ width: barWidth, height: barHeight))
+ rightBar.fillColor = .darkGray
+ rightBar.strokeColor = .clear
+ pauseButton.addChild(rightBar)
+ }
+
+ private func setupSpawnManager() {
+ spawnManager = SpawnManager(scene: self)
+ }
+
+ // MARK: - Update Loop
+ override func update(_ currentTime: TimeInterval) {
+ guard gameState.currentState == .playing else { return }
+
+ // Calculate delta time
+ let deltaTime = lastUpdateTime > 0 ? currentTime - lastUpdateTime : 0
+ lastUpdateTime = currentTime
+
+ // Update game time
+ gameState.updateTime(delta: deltaTime)
+ updateUI()
+
+ // Update spawn manager
+ spawnManager.update(deltaTime: deltaTime)
+
+ // Update floor scrolling
+ updateFloorScrolling(deltaTime: deltaTime)
+
+ // Check game end conditions
+ checkGameEndConditions()
+ }
+
+ private func updateFloorScrolling(deltaTime: TimeInterval) {
+ let scrollAmount = Constants.scrollSpeed * CGFloat(deltaTime)
+
+ for tile in floorTiles {
+ tile.position.y -= scrollAmount
+
+ // Reset tile position when it scrolls off screen
+ if tile.position.y < -tileSize {
+ tile.position.y += CGFloat(floorTiles.count / (Int(frame.width / tileSize) + 1)) * tileSize
+ }
+ }
+
+ // Also scroll floor markings
+ enumerateChildNodes(withName: "floorMarking") { node, _ in
+ node.position.y -= scrollAmount
+ if node.position.y < -30 {
+ node.position.y += self.frame.height + 50
+ }
+ }
+ }
+
+ private func updateUI() {
+ livesLabel.text = gameState.formattedLives
+ scoreLabel.text = gameState.formattedScore
+ dogsLabel.text = gameState.formattedDogs
+ humansLabel.text = gameState.formattedHumans
+ timerLabel.text = "โฑ๏ธ \(gameState.formattedTime)"
+
+ // Flash timer when low
+ if gameState.timeRemaining <= 10 {
+ timerLabel.fontColor = .red
+
+ if timerLabel.action(forKey: "flash") == nil {
+ let flash = SKAction.sequence([
+ SKAction.scale(to: 1.2, duration: 0.25),
+ SKAction.scale(to: 1.0, duration: 0.25)
+ ])
+ timerLabel.run(SKAction.repeatForever(flash), withKey: "flash")
+ }
+ }
+ }
+
+ private func checkGameEndConditions() {
+ if gameState.hasWon {
+ handleVictory()
+ } else if gameState.hasLost {
+ handleGameOver()
+ }
+ }
+
+ // MARK: - Touch Handling
+ override func touchesBegan(_ touches: Set, with event: UIEvent?) {
+ guard let touch = touches.first else { return }
+ let location = touch.location(in: self)
+
+ // Check pause button
+ if pauseButton.contains(location) {
+ togglePause()
+ return
+ }
+
+ // Check if touching player for dragging
+ if player.contains(location) {
+ isDragging = true
+ touchOffset = CGPoint(x: player.position.x - location.x,
+ y: player.position.y - location.y)
+ } else {
+ // Move player to touch location
+ isDragging = true
+ touchOffset = .zero
+ player.position = location
+ player.constrainToScreen(in: frame)
+ }
+ }
+
+ override func touchesMoved(_ touches: Set, with event: UIEvent?) {
+ guard isDragging, gameState.currentState == .playing,
+ let touch = touches.first else { return }
+
+ let location = touch.location(in: self)
+ player.position = CGPoint(x: location.x + touchOffset.x,
+ y: location.y + touchOffset.y)
+ player.constrainToScreen(in: frame)
+ }
+
+ override func touchesEnded(_ touches: Set, with event: UIEvent?) {
+ isDragging = false
+ }
+
+ override func touchesCancelled(_ touches: Set, with event: UIEvent?) {
+ isDragging = false
+ }
+
+ // MARK: - Pause Handling
+ private func togglePause() {
+ if gameState.currentState == .playing {
+ pauseGame()
+ } else if gameState.currentState == .paused {
+ resumeGame()
+ }
+ }
+
+ private func pauseGame() {
+ gameState.setState(.paused)
+ spawnManager.stopSpawning()
+ isPaused = true
+
+ showPauseOverlay()
+ }
+
+ private func resumeGame() {
+ hidePauseOverlay()
+
+ gameState.setState(.playing)
+ spawnManager.startSpawning()
+ isPaused = false
+ lastUpdateTime = 0
+ }
+
+ private func showPauseOverlay() {
+ pauseOverlay = SKNode()
+ pauseOverlay?.zPosition = Constants.ZPosition.ui + 10
+
+ // Dimmed background
+ let dim = SKShapeNode(rect: frame)
+ dim.fillColor = SKColor.black.withAlphaComponent(0.6)
+ dim.strokeColor = .clear
+ pauseOverlay?.addChild(dim)
+
+ // Pause text
+ let pauseText = SKLabelNode(text: "โธ๏ธ PAUSIERT")
+ pauseText.fontName = "AvenirNext-Heavy"
+ pauseText.fontSize = 40
+ pauseText.fontColor = .white
+ pauseText.position = CGPoint(x: frame.midX, y: frame.midY + 40)
+ pauseOverlay?.addChild(pauseText)
+
+ // Resume button
+ let resumeButton = createButton(text: "โถ๏ธ Weiter", at: CGPoint(x: frame.midX, y: frame.midY - 20))
+ resumeButton.name = "resumeButton"
+ pauseOverlay?.addChild(resumeButton)
+
+ // Menu button
+ let menuButton = createButton(text: "๐ Menรผ", at: CGPoint(x: frame.midX, y: frame.midY - 80))
+ menuButton.name = "menuButton"
+ pauseOverlay?.addChild(menuButton)
+
+ addChild(pauseOverlay!)
+ }
+
+ private func createButton(text: String, at position: CGPoint) -> SKNode {
+ let container = SKNode()
+ container.position = position
+
+ let bg = SKShapeNode(rect: CGRect(x: -80, y: -25, width: 160, height: 50),
+ cornerRadius: 10)
+ bg.fillColor = SKColor(white: 0.2, alpha: 0.9)
+ bg.strokeColor = .white
+ bg.lineWidth = 2
+ container.addChild(bg)
+
+ let label = SKLabelNode(text: text)
+ label.fontName = "AvenirNext-Bold"
+ label.fontSize = 22
+ label.fontColor = .white
+ label.verticalAlignmentMode = .center
+ container.addChild(label)
+
+ return container
+ }
+
+ private func hidePauseOverlay() {
+ pauseOverlay?.removeFromParent()
+ pauseOverlay = nil
+ }
+
+ // MARK: - Handle pause overlay touches (override to handle when paused)
+ func handlePauseOverlayTouch(at location: CGPoint) {
+ guard let overlay = pauseOverlay else { return }
+
+ if let resumeButton = overlay.childNode(withName: "resumeButton"),
+ resumeButton.contains(location) {
+ resumeGame()
+ } else if let menuButton = overlay.childNode(withName: "menuButton"),
+ menuButton.contains(location) {
+ returnToMenu()
+ }
+ }
+
+ // Override touchesBegan to handle pause overlay
+ func handleTouchInPausedState(_ touches: Set) {
+ guard let touch = touches.first else { return }
+ let location = touch.location(in: self)
+ handlePauseOverlayTouch(at: location)
+ }
+
+ // MARK: - Game End Handling
+ private func handleDamage() {
+ guard !gameState.isInvincible else { return }
+
+ if gameState.loseLife() {
+ // Start invincibility
+ gameState.isInvincible = true
+ player.startBlinking()
+
+ // End invincibility after duration
+ let wait = SKAction.wait(forDuration: Constants.invincibilityDuration)
+ run(wait) { [weak self] in
+ self?.gameState.isInvincible = false
+ self?.player.stopBlinking()
+ }
+ }
+ }
+
+ private func handleVictory() {
+ gameState.setState(.victory)
+ spawnManager.stopSpawning()
+
+ // Record score
+ ScoreManager.shared.recordGameEnd(
+ score: gameState.score,
+ dogsCollected: gameState.dogsCollected,
+ humansCollected: gameState.humansCollected,
+ didWin: true
+ )
+
+ // Transition to victory scene
+ let victoryScene = VictoryScene(size: size)
+ victoryScene.scaleMode = scaleMode
+ victoryScene.finalScore = gameState.score
+ victoryScene.dogsCollected = gameState.dogsCollected
+ victoryScene.humansCollected = gameState.humansCollected
+ victoryScene.timeRemaining = gameState.timeRemaining
+
+ let transition = SKTransition.fade(withDuration: 0.5)
+ view?.presentScene(victoryScene, transition: transition)
+ }
+
+ private func handleGameOver() {
+ gameState.setState(.gameOver)
+ spawnManager.stopSpawning()
+
+ // Record score
+ ScoreManager.shared.recordGameEnd(
+ score: gameState.score,
+ dogsCollected: gameState.dogsCollected,
+ humansCollected: gameState.humansCollected,
+ didWin: false
+ )
+
+ // Transition to game over scene
+ let gameOverScene = GameOverScene(size: size)
+ gameOverScene.scaleMode = scaleMode
+ gameOverScene.finalScore = gameState.score
+ gameOverScene.dogsCollected = gameState.dogsCollected
+ gameOverScene.humansCollected = gameState.humansCollected
+ gameOverScene.isNewHighScore = ScoreManager.shared.isNewHighScore(gameState.score)
+
+ let transition = SKTransition.fade(withDuration: 0.5)
+ view?.presentScene(gameOverScene, transition: transition)
+ }
+
+ private func returnToMenu() {
+ let menuScene = MenuScene(size: size)
+ menuScene.scaleMode = scaleMode
+
+ let transition = SKTransition.fade(withDuration: 0.5)
+ view?.presentScene(menuScene, transition: transition)
+ }
+}
+
+// MARK: - CollisionManagerDelegate
+extension GameScene: CollisionManagerDelegate {
+ func didCollectGoodDog(points: Int) {
+ gameState.addPoints(points)
+ gameState.collectDog()
+
+ // Show collection effect
+ showPointsEffect(points: points, at: player.position)
+ }
+
+ func didCollectGreenHuman(points: Int) {
+ gameState.addPoints(points)
+ gameState.collectHuman()
+
+ // Show collection effect
+ showPointsEffect(points: points, at: player.position)
+ }
+
+ func didHitHarmfulEntity() {
+ handleDamage()
+ }
+
+ private func showPointsEffect(points: Int, at position: CGPoint) {
+ let label = SKLabelNode(text: "+\(points)")
+ label.fontName = "AvenirNext-Bold"
+ label.fontSize = 24
+ label.fontColor = .green
+ label.position = CGPoint(x: position.x, y: position.y + 50)
+ label.zPosition = Constants.ZPosition.ui
+
+ addChild(label)
+
+ let moveUp = SKAction.moveBy(x: 0, y: 40, duration: 0.5)
+ let fadeOut = SKAction.fadeOut(withDuration: 0.5)
+ let group = SKAction.group([moveUp, fadeOut])
+ let remove = SKAction.removeFromParent()
+ label.run(SKAction.sequence([group, remove]))
+ }
+}
diff --git a/RollkofferSimulator/Scenes/MenuScene.swift b/RollkofferSimulator/Scenes/MenuScene.swift
new file mode 100644
index 0000000..7a650ce
--- /dev/null
+++ b/RollkofferSimulator/Scenes/MenuScene.swift
@@ -0,0 +1,265 @@
+//
+// MenuScene.swift
+// RollkofferSimulator
+//
+// Created by Ingo K.
+//
+
+import SpriteKit
+
+class MenuScene: SKScene {
+
+ // MARK: - Properties
+ private var startButton: SKShapeNode!
+ private var titleLabel: SKLabelNode!
+ private var subtitleLabel: SKLabelNode!
+ private var highScoreLabel: SKLabelNode!
+ private var creditsLabel: SKLabelNode!
+
+ // MARK: - Decorative elements
+ private var decorativeSuitcase: PlayerNode!
+ private var decorativeDogs: [DogNode] = []
+ private var decorativeHumans: [HumanNode] = []
+
+ // MARK: - Scene Lifecycle
+ override func didMove(to view: SKView) {
+ setupBackground()
+ setupTitle()
+ setupStartButton()
+ setupHighScore()
+ setupDecorations()
+ setupCredits()
+ startAnimations()
+ }
+
+ // MARK: - Setup
+ private func setupBackground() {
+ backgroundColor = Constants.Colors.backgroundColor
+
+ // Create floor pattern
+ let floorHeight = frame.height * 0.6
+ let floor = SKShapeNode(rect: CGRect(x: 0, y: 0,
+ width: frame.width, height: floorHeight))
+ floor.fillColor = Constants.Colors.floorColor
+ floor.strokeColor = .clear
+ floor.zPosition = Constants.ZPosition.floor
+ addChild(floor)
+
+ // Add floor tiles pattern
+ let tileSize: CGFloat = 80
+ for x in stride(from: CGFloat(0), to: frame.width, by: tileSize) {
+ for y in stride(from: CGFloat(0), to: floorHeight, by: tileSize) {
+ let tile = SKShapeNode(rect: CGRect(x: x, y: y,
+ width: tileSize - 2, height: tileSize - 2))
+ tile.fillColor = (Int((x + y) / tileSize) % 2 == 0) ?
+ SKColor(white: 0.78, alpha: 1.0) : SKColor(white: 0.72, alpha: 1.0)
+ tile.strokeColor = SKColor(white: 0.65, alpha: 0.5)
+ tile.lineWidth = 1
+ tile.zPosition = Constants.ZPosition.floor + 0.1
+ addChild(tile)
+ }
+ }
+ }
+
+ private func setupTitle() {
+ // Main title
+ titleLabel = SKLabelNode(text: "๐งณ Rollkoffer Simulator ๐งณ")
+ titleLabel.fontName = "AvenirNext-Heavy"
+ titleLabel.fontSize = 36
+ titleLabel.fontColor = SKColor(red: 0.3, green: 0.2, blue: 0.5, alpha: 1.0)
+ titleLabel.position = CGPoint(x: frame.midX, y: frame.height * 0.85)
+ titleLabel.zPosition = Constants.ZPosition.ui
+ addChild(titleLabel)
+
+ // Subtitle
+ subtitleLabel = SKLabelNode(text: "Sammle Hunde & Menschen am Flughafen!")
+ subtitleLabel.fontName = "AvenirNext-Medium"
+ subtitleLabel.fontSize = 18
+ subtitleLabel.fontColor = SKColor.darkGray
+ subtitleLabel.position = CGPoint(x: frame.midX, y: frame.height * 0.78)
+ subtitleLabel.zPosition = Constants.ZPosition.ui
+ addChild(subtitleLabel)
+
+ // Goal info
+ let goalLabel = SKLabelNode(text: "Ziel: ๐ 10 Hunde + ๐ค 5 Grรผne Menschen")
+ goalLabel.fontName = "AvenirNext-Medium"
+ goalLabel.fontSize = 16
+ goalLabel.fontColor = SKColor.gray
+ goalLabel.position = CGPoint(x: frame.midX, y: frame.height * 0.72)
+ goalLabel.zPosition = Constants.ZPosition.ui
+ addChild(goalLabel)
+ }
+
+ private func setupStartButton() {
+ // Button background
+ let buttonWidth: CGFloat = 200
+ let buttonHeight: CGFloat = 60
+
+ startButton = SKShapeNode(rect: CGRect(x: -buttonWidth / 2, y: -buttonHeight / 2,
+ width: buttonWidth, height: buttonHeight),
+ cornerRadius: 15)
+ startButton.fillColor = SKColor(red: 0.2, green: 0.7, blue: 0.3, alpha: 1.0)
+ startButton.strokeColor = SKColor(red: 0.15, green: 0.5, blue: 0.2, alpha: 1.0)
+ startButton.lineWidth = 3
+ startButton.position = CGPoint(x: frame.midX, y: frame.height * 0.5)
+ startButton.zPosition = Constants.ZPosition.ui
+ startButton.name = "startButton"
+ addChild(startButton)
+
+ // Button label
+ let buttonLabel = SKLabelNode(text: "โถ START")
+ buttonLabel.fontName = "AvenirNext-Bold"
+ buttonLabel.fontSize = 28
+ buttonLabel.fontColor = .white
+ buttonLabel.verticalAlignmentMode = .center
+ buttonLabel.position = .zero
+ buttonLabel.zPosition = 1
+ startButton.addChild(buttonLabel)
+
+ // Button pulse animation
+ let scaleUp = SKAction.scale(to: 1.05, duration: 0.8)
+ let scaleDown = SKAction.scale(to: 1.0, duration: 0.8)
+ let pulse = SKAction.sequence([scaleUp, scaleDown])
+ startButton.run(SKAction.repeatForever(pulse))
+ }
+
+ private func setupHighScore() {
+ let highScore = ScoreManager.shared.highScore
+
+ highScoreLabel = SKLabelNode(text: "๐ High Score: \(highScore)")
+ highScoreLabel.fontName = "AvenirNext-DemiBold"
+ highScoreLabel.fontSize = 20
+ highScoreLabel.fontColor = SKColor(red: 0.8, green: 0.6, blue: 0.1, alpha: 1.0)
+ highScoreLabel.position = CGPoint(x: frame.midX, y: frame.height * 0.38)
+ highScoreLabel.zPosition = Constants.ZPosition.ui
+ addChild(highScoreLabel)
+
+ // Statistics label
+ let victories = ScoreManager.shared.victories
+ let gamesPlayed = ScoreManager.shared.gamesPlayed
+ let statsText = "Siege: \(victories) | Spiele: \(gamesPlayed)"
+
+ let statsLabel = SKLabelNode(text: statsText)
+ statsLabel.fontName = "AvenirNext-Regular"
+ statsLabel.fontSize = 14
+ statsLabel.fontColor = SKColor.gray
+ statsLabel.position = CGPoint(x: frame.midX, y: frame.height * 0.33)
+ statsLabel.zPosition = Constants.ZPosition.ui
+ addChild(statsLabel)
+ }
+
+ private func setupDecorations() {
+ // Decorative suitcase
+ decorativeSuitcase = PlayerNode()
+ decorativeSuitcase.position = CGPoint(x: frame.midX, y: frame.height * 0.2)
+ decorativeSuitcase.zPosition = Constants.ZPosition.entities
+ addChild(decorativeSuitcase)
+
+ // Add some decorative dogs
+ let dogTypes: [DogType] = [.smallGood, .bigGood, .bad]
+ let dogPositions: [CGPoint] = [
+ CGPoint(x: frame.width * 0.15, y: frame.height * 0.25),
+ CGPoint(x: frame.width * 0.85, y: frame.height * 0.22),
+ CGPoint(x: frame.width * 0.25, y: frame.height * 0.12)
+ ]
+
+ for (index, type) in dogTypes.enumerated() {
+ let dog = DogNode(type: type)
+ dog.position = dogPositions[index]
+ dog.zPosition = Constants.ZPosition.entities
+ addChild(dog)
+ decorativeDogs.append(dog)
+ }
+
+ // Add decorative humans
+ let humanTypes: [HumanType] = [.green, .gray]
+ let humanPositions: [CGPoint] = [
+ CGPoint(x: frame.width * 0.75, y: frame.height * 0.15),
+ CGPoint(x: frame.width * 0.6, y: frame.height * 0.1)
+ ]
+
+ for (index, type) in humanTypes.enumerated() {
+ let human = HumanNode(type: type)
+ human.position = humanPositions[index]
+ human.zPosition = Constants.ZPosition.entities
+ addChild(human)
+ decorativeHumans.append(human)
+ }
+ }
+
+ private func setupCredits() {
+ creditsLabel = SKLabelNode(text: "Created by Ingo K.")
+ creditsLabel.fontName = "AvenirNext-Italic"
+ creditsLabel.fontSize = 14
+ creditsLabel.fontColor = SKColor.gray
+ creditsLabel.position = CGPoint(x: frame.midX, y: 30)
+ creditsLabel.zPosition = Constants.ZPosition.ui
+ addChild(creditsLabel)
+ }
+
+ private func startAnimations() {
+ // Animate decorative suitcase
+ let moveLeft = SKAction.moveBy(x: -20, y: 0, duration: 1.5)
+ let moveRight = SKAction.moveBy(x: 40, y: 0, duration: 3.0)
+ let moveBack = SKAction.moveBy(x: -20, y: 0, duration: 1.5)
+ let suitcaseSequence = SKAction.sequence([moveLeft, moveRight, moveBack])
+ decorativeSuitcase.run(SKAction.repeatForever(suitcaseSequence))
+
+ // Animate decorative entities
+ for (index, dog) in decorativeDogs.enumerated() {
+ let delay = Double(index) * 0.3
+ let bounce = SKAction.sequence([
+ SKAction.wait(forDuration: delay),
+ SKAction.moveBy(x: 0, y: 10, duration: 0.4),
+ SKAction.moveBy(x: 0, y: -10, duration: 0.4)
+ ])
+ dog.run(SKAction.repeatForever(bounce))
+ }
+
+ for (index, human) in decorativeHumans.enumerated() {
+ let delay = Double(index) * 0.4 + 0.2
+ let sway = SKAction.sequence([
+ SKAction.wait(forDuration: delay),
+ SKAction.rotate(byAngle: 0.05, duration: 0.5),
+ SKAction.rotate(byAngle: -0.1, duration: 1.0),
+ SKAction.rotate(byAngle: 0.05, duration: 0.5)
+ ])
+ human.run(SKAction.repeatForever(sway))
+ }
+
+ // Title animation
+ let titleScale = SKAction.sequence([
+ SKAction.scale(to: 1.02, duration: 2.0),
+ SKAction.scale(to: 1.0, duration: 2.0)
+ ])
+ titleLabel.run(SKAction.repeatForever(titleScale))
+ }
+
+ // MARK: - Touch Handling
+ override func touchesBegan(_ touches: Set, with event: UIEvent?) {
+ guard let touch = touches.first else { return }
+ let location = touch.location(in: self)
+
+ if startButton.contains(location) {
+ startGame()
+ }
+ }
+
+ private func startGame() {
+ // Button press effect
+ let pressDown = SKAction.scale(to: 0.9, duration: 0.1)
+ let pressUp = SKAction.scale(to: 1.0, duration: 0.1)
+
+ startButton.run(SKAction.sequence([pressDown, pressUp])) { [weak self] in
+ self?.transitionToGame()
+ }
+ }
+
+ private func transitionToGame() {
+ let gameScene = GameScene(size: size)
+ gameScene.scaleMode = scaleMode
+
+ let transition = SKTransition.fade(withDuration: 0.5)
+ view?.presentScene(gameScene, transition: transition)
+ }
+}
diff --git a/RollkofferSimulator/Scenes/VictoryScene.swift b/RollkofferSimulator/Scenes/VictoryScene.swift
new file mode 100644
index 0000000..c6f4fa4
--- /dev/null
+++ b/RollkofferSimulator/Scenes/VictoryScene.swift
@@ -0,0 +1,312 @@
+//
+// VictoryScene.swift
+// RollkofferSimulator
+//
+// Created by Ingo K.
+//
+
+import SpriteKit
+
+class VictoryScene: SKScene {
+
+ // MARK: - Properties
+ var finalScore: Int = 0
+ var dogsCollected: Int = 0
+ var humansCollected: Int = 0
+ var timeRemaining: TimeInterval = 0
+
+ private var playAgainButton: SKShapeNode!
+ private var menuButton: SKShapeNode!
+
+ // MARK: - Scene Lifecycle
+ override func didMove(to view: SKView) {
+ setupBackground()
+ setupContent()
+ setupButtons()
+ startCelebration()
+ }
+
+ // MARK: - Setup
+ private func setupBackground() {
+ backgroundColor = SKColor(red: 0.1, green: 0.2, blue: 0.1, alpha: 1.0)
+
+ // Add celebration particles
+ for _ in 0..<30 {
+ let confetti = createConfetti()
+ addChild(confetti)
+ }
+ }
+
+ private func createConfetti() -> SKShapeNode {
+ let size = CGFloat.random(in: 8...15)
+ let confetti = SKShapeNode(rectOf: CGSize(width: size, height: size * 1.5))
+ confetti.fillColor = [SKColor.red, SKColor.yellow, SKColor.green,
+ SKColor.blue, SKColor.orange, SKColor.purple].randomElement()!
+ confetti.strokeColor = .clear
+ confetti.position = CGPoint(x: CGFloat.random(in: 0...frame.width),
+ y: frame.height + CGFloat.random(in: 50...200))
+ confetti.zPosition = Constants.ZPosition.ui - 5
+ confetti.zRotation = CGFloat.random(in: 0...(.pi * 2))
+
+ // Falling animation
+ let fallDuration = Double.random(in: 3...6)
+ let fall = SKAction.moveTo(y: -50, duration: fallDuration)
+ let rotate = SKAction.rotate(byAngle: .pi * 4, duration: fallDuration)
+ let sway = SKAction.sequence([
+ SKAction.moveBy(x: 30, y: 0, duration: 0.5),
+ SKAction.moveBy(x: -30, y: 0, duration: 0.5)
+ ])
+ let swayRepeat = SKAction.repeat(sway, count: Int(fallDuration))
+
+ let group = SKAction.group([fall, rotate, swayRepeat])
+ let reset = SKAction.run { [weak confetti, weak self] in
+ confetti?.position = CGPoint(x: CGFloat.random(in: 0...(self?.frame.width ?? 400)),
+ y: (self?.frame.height ?? 800) + 50)
+ }
+ let sequence = SKAction.sequence([group, reset])
+ confetti.run(SKAction.repeatForever(sequence))
+
+ return confetti
+ }
+
+ private func setupContent() {
+ // Victory title
+ let titleLabel = SKLabelNode(text: "๐ GEWONNEN! ๐")
+ titleLabel.fontName = "AvenirNext-Heavy"
+ titleLabel.fontSize = 46
+ titleLabel.fontColor = .yellow
+ titleLabel.position = CGPoint(x: frame.midX, y: frame.height * 0.82)
+ titleLabel.zPosition = Constants.ZPosition.ui
+ addChild(titleLabel)
+
+ // Animate title
+ let titlePulse = SKAction.sequence([
+ SKAction.scale(to: 1.1, duration: 0.3),
+ SKAction.scale(to: 1.0, duration: 0.3)
+ ])
+ titleLabel.run(SKAction.repeatForever(titlePulse))
+
+ // Subtitle
+ let subtitleLabel = SKLabelNode(text: "Du hast alle Ziele erreicht!")
+ subtitleLabel.fontName = "AvenirNext-Medium"
+ subtitleLabel.fontSize = 20
+ subtitleLabel.fontColor = .white
+ subtitleLabel.position = CGPoint(x: frame.midX, y: frame.height * 0.74)
+ subtitleLabel.zPosition = Constants.ZPosition.ui
+ addChild(subtitleLabel)
+
+ // Score display
+ let scoreLabel = SKLabelNode(text: "Endpunktzahl: \(finalScore)")
+ scoreLabel.fontName = "AvenirNext-Bold"
+ scoreLabel.fontSize = 36
+ scoreLabel.fontColor = SKColor(red: 1.0, green: 0.85, blue: 0.0, alpha: 1.0)
+ scoreLabel.position = CGPoint(x: frame.midX, y: frame.height * 0.62)
+ scoreLabel.zPosition = Constants.ZPosition.ui
+ addChild(scoreLabel)
+
+ // Time bonus display
+ let timeBonus = Int(timeRemaining) * 5
+ if timeBonus > 0 {
+ let timeBonusLabel = SKLabelNode(text: "โฑ๏ธ Zeitbonus: +\(timeBonus) (\(Int(timeRemaining))s รผbrig)")
+ timeBonusLabel.fontName = "AvenirNext-Medium"
+ timeBonusLabel.fontSize = 18
+ timeBonusLabel.fontColor = SKColor.cyan
+ timeBonusLabel.position = CGPoint(x: frame.midX, y: frame.height * 0.55)
+ timeBonusLabel.zPosition = Constants.ZPosition.ui
+ addChild(timeBonusLabel)
+ }
+
+ // Stats display
+ let statsY = frame.height * 0.45
+
+ let dogsLabel = SKLabelNode(text: "๐ Hunde: \(dogsCollected)")
+ dogsLabel.fontName = "AvenirNext-DemiBold"
+ dogsLabel.fontSize = 22
+ dogsLabel.fontColor = .white
+ dogsLabel.position = CGPoint(x: frame.midX - 80, y: statsY)
+ dogsLabel.zPosition = Constants.ZPosition.ui
+ addChild(dogsLabel)
+
+ let humansLabel = SKLabelNode(text: "๐ค Menschen: \(humansCollected)")
+ humansLabel.fontName = "AvenirNext-DemiBold"
+ humansLabel.fontSize = 22
+ humansLabel.fontColor = .white
+ humansLabel.position = CGPoint(x: frame.midX + 80, y: statsY)
+ humansLabel.zPosition = Constants.ZPosition.ui
+ addChild(humansLabel)
+
+ // Happy suitcase with collected items
+ let happySuitcase = PlayerNode()
+ happySuitcase.position = CGPoint(x: frame.midX, y: frame.height * 0.28)
+ addChild(happySuitcase)
+
+ // Happy face
+ let happyFace = SKLabelNode(text: "๐")
+ happyFace.fontSize = 30
+ happyFace.position = CGPoint(x: frame.midX, y: frame.height * 0.28 + 20)
+ happyFace.zPosition = Constants.ZPosition.ui
+ addChild(happyFace)
+
+ // Add small dogs and humans around suitcase
+ let collectibles = [
+ ("๐", CGPoint(x: -50, y: 0)),
+ ("๐", CGPoint(x: 50, y: 0)),
+ ("๐ค", CGPoint(x: -35, y: 30)),
+ ("๐ค", CGPoint(x: 35, y: 30))
+ ]
+
+ for (emoji, offset) in collectibles {
+ let label = SKLabelNode(text: emoji)
+ label.fontSize = 24
+ label.position = CGPoint(x: frame.midX + offset.x,
+ y: frame.height * 0.28 + offset.y)
+ label.zPosition = Constants.ZPosition.ui
+ addChild(label)
+
+ // Bounce animation
+ let bounce = SKAction.sequence([
+ SKAction.moveBy(x: 0, y: 5, duration: 0.3),
+ SKAction.moveBy(x: 0, y: -5, duration: 0.3)
+ ])
+ label.run(SKAction.repeatForever(bounce))
+ }
+
+ // High score check
+ if ScoreManager.shared.isNewHighScore(finalScore) {
+ let highScoreLabel = SKLabelNode(text: "๐ NEUER HIGHSCORE! ๐")
+ highScoreLabel.fontName = "AvenirNext-Heavy"
+ highScoreLabel.fontSize = 26
+ highScoreLabel.fontColor = SKColor.yellow
+ highScoreLabel.position = CGPoint(x: frame.midX, y: frame.height * 0.68)
+ highScoreLabel.zPosition = Constants.ZPosition.ui
+ addChild(highScoreLabel)
+
+ let glow = SKAction.sequence([
+ SKAction.fadeAlpha(to: 0.6, duration: 0.4),
+ SKAction.fadeAlpha(to: 1.0, duration: 0.4)
+ ])
+ highScoreLabel.run(SKAction.repeatForever(glow))
+ }
+ }
+
+ private func setupButtons() {
+ // Play Again button
+ playAgainButton = createButton(text: "๐ฎ Nochmal spielen",
+ color: SKColor(red: 0.2, green: 0.7, blue: 0.3, alpha: 1.0))
+ playAgainButton.position = CGPoint(x: frame.midX, y: frame.height * 0.13)
+ playAgainButton.name = "playAgainButton"
+ addChild(playAgainButton)
+
+ // Menu button
+ menuButton = createButton(text: "๐ Hauptmenรผ",
+ color: SKColor(red: 0.3, green: 0.3, blue: 0.6, alpha: 1.0))
+ menuButton.position = CGPoint(x: frame.midX, y: frame.height * 0.06)
+ menuButton.name = "menuButton"
+ addChild(menuButton)
+ }
+
+ private func createButton(text: String, color: SKColor) -> SKShapeNode {
+ let buttonWidth: CGFloat = 220
+ let buttonHeight: CGFloat = 50
+
+ let button = SKShapeNode(rect: CGRect(x: -buttonWidth / 2, y: -buttonHeight / 2,
+ width: buttonWidth, height: buttonHeight),
+ cornerRadius: 12)
+ button.fillColor = color
+ button.strokeColor = .white.withAlphaComponent(0.5)
+ button.lineWidth = 2
+ button.zPosition = Constants.ZPosition.ui
+
+ let label = SKLabelNode(text: text)
+ label.fontName = "AvenirNext-Bold"
+ label.fontSize = 20
+ label.fontColor = .white
+ label.verticalAlignmentMode = .center
+ label.zPosition = 1
+ button.addChild(label)
+
+ return button
+ }
+
+ private func startCelebration() {
+ // Screen flash
+ let flash = SKShapeNode(rect: frame)
+ flash.fillColor = .white
+ flash.strokeColor = .clear
+ flash.zPosition = Constants.ZPosition.ui + 100
+ flash.alpha = 0.8
+ addChild(flash)
+
+ let fadeOut = SKAction.fadeOut(withDuration: 0.5)
+ let remove = SKAction.removeFromParent()
+ flash.run(SKAction.sequence([fadeOut, remove]))
+
+ // Star burst effect
+ for _ in 0..<12 {
+ let star = SKLabelNode(text: "โญ")
+ star.fontSize = CGFloat.random(in: 20...40)
+ star.position = CGPoint(x: frame.midX, y: frame.height * 0.82)
+ star.zPosition = Constants.ZPosition.ui - 1
+ star.alpha = 0
+ addChild(star)
+
+ let angle = CGFloat.random(in: 0...(.pi * 2))
+ let distance = CGFloat.random(in: 100...200)
+ let endPoint = CGPoint(x: frame.midX + cos(angle) * distance,
+ y: frame.height * 0.82 + sin(angle) * distance)
+
+ let fadeIn = SKAction.fadeIn(withDuration: 0.2)
+ let move = SKAction.move(to: endPoint, duration: 0.5)
+ let fadeOutStar = SKAction.fadeOut(withDuration: 0.3)
+ let removeStar = SKAction.removeFromParent()
+
+ let group = SKAction.group([move, SKAction.sequence([fadeIn,
+ SKAction.wait(forDuration: 0.2),
+ fadeOutStar])])
+ star.run(SKAction.sequence([SKAction.wait(forDuration: Double.random(in: 0...0.3)),
+ group, removeStar]))
+ }
+ }
+
+ // MARK: - Touch Handling
+ override func touchesBegan(_ touches: Set, with event: UIEvent?) {
+ guard let touch = touches.first else { return }
+ let location = touch.location(in: self)
+
+ if playAgainButton.contains(location) {
+ playAgain()
+ } else if menuButton.contains(location) {
+ returnToMenu()
+ }
+ }
+
+ private func playAgain() {
+ let pressDown = SKAction.scale(to: 0.9, duration: 0.1)
+ let pressUp = SKAction.scale(to: 1.0, duration: 0.1)
+
+ playAgainButton.run(SKAction.sequence([pressDown, pressUp])) { [weak self] in
+ guard let self = self else { return }
+
+ let gameScene = GameScene(size: self.size)
+ gameScene.scaleMode = self.scaleMode
+
+ let transition = SKTransition.fade(withDuration: 0.5)
+ self.view?.presentScene(gameScene, transition: transition)
+ }
+ }
+
+ private func returnToMenu() {
+ let pressDown = SKAction.scale(to: 0.9, duration: 0.1)
+ let pressUp = SKAction.scale(to: 1.0, duration: 0.1)
+
+ menuButton.run(SKAction.sequence([pressDown, pressUp])) { [weak self] in
+ guard let self = self else { return }
+
+ let menuScene = MenuScene(size: self.size)
+ menuScene.scaleMode = self.scaleMode
+
+ let transition = SKTransition.fade(withDuration: 0.5)
+ self.view?.presentScene(menuScene, transition: transition)
+ }
+ }
+}
diff --git a/RollkofferSimulator/Utils/Constants.swift b/RollkofferSimulator/Utils/Constants.swift
new file mode 100644
index 0000000..4ff63df
--- /dev/null
+++ b/RollkofferSimulator/Utils/Constants.swift
@@ -0,0 +1,79 @@
+//
+// Constants.swift
+// RollkofferSimulator
+//
+// Created by Ingo K.
+//
+
+import SpriteKit
+
+struct Constants {
+
+ // MARK: - Game Settings
+ static let gameTime: TimeInterval = 90.0
+ static let startLives: Int = 3
+ static let targetDogs: Int = 10
+ static let targetHumans: Int = 5
+
+ // MARK: - Points
+ static let pointsSmallGoodDog: Int = 10
+ static let pointsBigGoodDog: Int = 25
+ static let pointsGreenHuman: Int = 15
+
+ // MARK: - Spawn Settings
+ static let spawnIntervalMin: TimeInterval = 0.8
+ static let spawnIntervalMax: TimeInterval = 1.5
+ static let scrollSpeed: CGFloat = 200.0
+
+ // MARK: - Spawn Distribution (cumulative percentages)
+ static let spawnChanceGoodDog: Int = 40 // 0-39: good dogs
+ static let spawnChanceBadDog: Int = 60 // 40-59: bad dogs (20%)
+ static let spawnChanceGreenHuman: Int = 85 // 60-84: green humans (25%)
+ // 85-99: gray humans (15%)
+
+ // MARK: - Sprite Sizes
+ static let suitcaseSize = CGSize(width: 60, height: 80)
+ static let smallDogSize = CGSize(width: 40, height: 40)
+ static let bigDogSize = CGSize(width: 70, height: 70)
+ static let badDogSize = CGSize(width: 55, height: 55)
+ static let humanSize = CGSize(width: 50, height: 80)
+
+ // MARK: - Physics Categories (Bitmasks)
+ struct PhysicsCategory {
+ static let none: UInt32 = 0
+ static let suitcase: UInt32 = 0x1 << 0
+ static let goodDog: UInt32 = 0x1 << 1
+ static let badDog: UInt32 = 0x1 << 2
+ static let greenHuman: UInt32 = 0x1 << 3
+ static let grayHuman: UInt32 = 0x1 << 4
+
+ static let collectible: UInt32 = goodDog | greenHuman
+ static let harmful: UInt32 = badDog | grayHuman
+ static let all: UInt32 = goodDog | badDog | greenHuman | grayHuman
+ }
+
+ // MARK: - Colors
+ struct Colors {
+ static let goodDogColor = SKColor(red: 0.85, green: 0.65, blue: 0.13, alpha: 1.0) // Gold brown
+ static let badDogColor = SKColor.red
+ static let greenHumanColor = SKColor(red: 0.2, green: 0.8, blue: 0.2, alpha: 1.0)
+ static let grayHumanColor = SKColor.gray
+ static let suitcaseColor = SKColor(red: 0.4, green: 0.3, blue: 0.6, alpha: 1.0)
+ static let backgroundColor = SKColor(red: 0.9, green: 0.9, blue: 0.85, alpha: 1.0)
+ static let floorColor = SKColor(red: 0.75, green: 0.75, blue: 0.7, alpha: 1.0)
+ }
+
+ // MARK: - Z-Positions
+ struct ZPosition {
+ static let background: CGFloat = 0
+ static let floor: CGFloat = 1
+ static let entities: CGFloat = 10
+ static let player: CGFloat = 20
+ static let ui: CGFloat = 100
+ }
+
+ // MARK: - Animation
+ static let blinkDuration: TimeInterval = 0.1
+ static let blinkCount: Int = 6
+ static let invincibilityDuration: TimeInterval = 1.5
+}