Compare commits

...

2 Commits

Author SHA1 Message Date
Claude 9e501cc4e8 Add RollkofferSimulator iOS SpriteKit arcade game
Complete implementation of a 2D top-down arcade collector game where players
control a rolling suitcase through an airport, collecting good dogs and green
people while avoiding bad dogs and gray people.

Features:
- Touch & drag controls for suitcase movement
- Automatic scrolling airport floor with tile pattern
- 4 entity types: good dogs (small/big), bad dogs, green/gray humans
- Spawn system with configurable distribution rates
- Collision detection with visual feedback effects
- Score tracking with high score persistence
- 90-second time limit with 10 dogs + 5 humans goal
- 3 lives system with invincibility frames
- Menu, Game, GameOver, and Victory scenes
- German UI text (Created by Ingo K.)

Technical:
- iOS 15+ with SpriteKit framework
- Modular architecture with Nodes, Managers, Scenes
- Physics-based collision detection
- UserDefaults for score persistence
2025-12-19 16:47:49 +00:00
admin 8d4afcf0ad Merge pull request #8 from metacube2/claude/ft991a-remote-control-app-T356a
FT-991A Remote Control App for macOS
2025-12-18 12:01:52 +01:00
22 changed files with 3228 additions and 0 deletions
+42
View File
@@ -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")
}
@@ -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
}
}
@@ -0,0 +1,13 @@
{
"images" : [
{
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21678"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="🧳" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Emoji-Label">
<rect key="frame" x="146.66666666666666" y="356" width="100" height="80"/>
<constraints>
<constraint firstAttribute="width" constant="100" id="width-constraint"/>
<constraint firstAttribute="height" constant="80" id="height-constraint"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="60"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Rollkoffer Simulator" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Title-Label">
<rect key="frame" x="46.666666666666657" y="456" width="300" height="30"/>
<constraints>
<constraint firstAttribute="width" constant="300" id="title-width"/>
<constraint firstAttribute="height" constant="30" id="title-height"/>
</constraints>
<fontDescription key="fontDescription" type="boldSystem" pointSize="24"/>
<color key="textColor" red="0.4" green="0.3" blue="0.6" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Created by Ingo K." textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Credits-Label">
<rect key="frame" x="96.666666666666671" y="792" width="200" height="20"/>
<constraints>
<constraint firstAttribute="width" constant="200" id="credits-width"/>
<constraint firstAttribute="height" constant="20" id="credits-height"/>
</constraints>
<fontDescription key="fontDescription" type="italicSystem" pointSize="14"/>
<color key="textColor" systemColor="secondaryLabelColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
<color key="backgroundColor" red="0.9" green="0.9" blue="0.85" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="Emoji-Label" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="emoji-centerx"/>
<constraint firstItem="Emoji-Label" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" constant="-30" id="emoji-centery"/>
<constraint firstItem="Title-Label" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="title-centerx"/>
<constraint firstItem="Title-Label" firstAttribute="top" secondItem="Emoji-Label" secondAttribute="bottom" constant="20" id="title-top"/>
<constraint firstItem="Credits-Label" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="credits-centerx"/>
<constraint firstItem="Credits-Label" firstAttribute="bottom" secondItem="6Tk-OE-BBY" secondAttribute="bottom" constant="-20" id="credits-bottom"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
</document>
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21678"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Game View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="GameViewController" customModule="RollkofferSimulator" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC" customClass="SKView">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-29" y="-21"/>
</scene>
</scenes>
</document>
@@ -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)
}
}
+54
View File
@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Rollkoffer Simulator</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
</dict>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UIRequiresFullScreen</key>
<true/>
<key>UIStatusBarHidden</key>
<true/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
@@ -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
}
}
@@ -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)
"""
}
}
@@ -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)
}
}
}
@@ -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)
}
+103
View File
@@ -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)
}
}
+162
View File
@@ -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
}
}
+176
View File
@@ -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
}
}
+126
View File
@@ -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))
}
}
@@ -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 = "<group>"; };
102 /* GameViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameViewController.swift; sourceTree = "<group>"; };
103 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
104 /* EntityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntityType.swift; sourceTree = "<group>"; };
105 /* GameState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameState.swift; sourceTree = "<group>"; };
106 /* PlayerNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerNode.swift; sourceTree = "<group>"; };
107 /* DogNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DogNode.swift; sourceTree = "<group>"; };
108 /* HumanNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HumanNode.swift; sourceTree = "<group>"; };
109 /* SpawnManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpawnManager.swift; sourceTree = "<group>"; };
110 /* CollisionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollisionManager.swift; sourceTree = "<group>"; };
111 /* ScoreManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScoreManager.swift; sourceTree = "<group>"; };
112 /* MenuScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuScene.swift; sourceTree = "<group>"; };
113 /* GameScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameScene.swift; sourceTree = "<group>"; };
114 /* GameOverScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameOverScene.swift; sourceTree = "<group>"; };
115 /* VictoryScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VictoryScene.swift; sourceTree = "<group>"; };
116 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
117 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
118 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
119 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* 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 = "<group>";
};
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 = "<group>";
};
302 /* Products */ = {
isa = PBXGroup;
children = (
100 /* RollkofferSimulator.app */,
);
name = Products;
sourceTree = "<group>";
};
303 /* Scenes */ = {
isa = PBXGroup;
children = (
112 /* MenuScene.swift */,
113 /* GameScene.swift */,
114 /* GameOverScene.swift */,
115 /* VictoryScene.swift */,
);
path = Scenes;
sourceTree = "<group>";
};
304 /* Nodes */ = {
isa = PBXGroup;
children = (
106 /* PlayerNode.swift */,
107 /* DogNode.swift */,
108 /* HumanNode.swift */,
);
path = Nodes;
sourceTree = "<group>";
};
305 /* Managers */ = {
isa = PBXGroup;
children = (
109 /* SpawnManager.swift */,
110 /* CollisionManager.swift */,
111 /* ScoreManager.swift */,
);
path = Managers;
sourceTree = "<group>";
};
306 /* Models */ = {
isa = PBXGroup;
children = (
104 /* EntityType.swift */,
105 /* GameState.swift */,
);
path = Models;
sourceTree = "<group>";
};
307 /* Utils */ = {
isa = PBXGroup;
children = (
103 /* Constants.swift */,
);
path = Utils;
sourceTree = "<group>";
};
/* 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 = "<group>";
};
118 /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
118 /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* 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 */;
}
@@ -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<UITouch>, 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)
}
}
}
+537
View File
@@ -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..<rows {
for col in 0..<cols {
let tile = SKShapeNode(rect: CGRect(x: 0, y: 0,
width: tileSize - 2, height: tileSize - 2))
let isEven = (row + col) % 2 == 0
tile.fillColor = isEven ?
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.position = CGPoint(x: CGFloat(col) * tileSize,
y: CGFloat(row) * tileSize)
tile.zPosition = Constants.ZPosition.floor
tile.name = "floorTile"
addChild(tile)
floorTiles.append(tile)
}
}
// Add airport markings
addAirportMarkings()
}
private func addAirportMarkings() {
// Center line
let lineWidth: CGFloat = 4
let dashLength: CGFloat = 30
let gapLength: CGFloat = 20
for y in stride(from: CGFloat(0), to: frame.height, by: dashLength + gapLength) {
let dash = SKShapeNode(rect: CGRect(x: frame.midX - lineWidth / 2, y: y,
width: lineWidth, height: dashLength))
dash.fillColor = SKColor.yellow.withAlphaComponent(0.6)
dash.strokeColor = .clear
dash.zPosition = Constants.ZPosition.floor + 0.5
dash.name = "floorMarking"
addChild(dash)
}
}
private func setupPlayer() {
player = PlayerNode()
player.position = CGPoint(x: frame.midX, y: frame.height * 0.15)
addChild(player)
}
private func setupUI() {
let uiY = frame.height - 50
let uiY2 = frame.height - 80
// Lives
livesLabel = createUILabel(text: gameState.formattedLives, fontSize: 24)
livesLabel.position = CGPoint(x: 60, y: uiY)
livesLabel.horizontalAlignmentMode = .left
addChild(livesLabel)
// Score
scoreLabel = createUILabel(text: gameState.formattedScore, fontSize: 20)
scoreLabel.position = CGPoint(x: frame.midX, y: uiY)
addChild(scoreLabel)
// Dogs counter
dogsLabel = createUILabel(text: gameState.formattedDogs, fontSize: 18)
dogsLabel.position = CGPoint(x: frame.width - 120, y: uiY)
addChild(dogsLabel)
// Humans counter
humansLabel = createUILabel(text: gameState.formattedHumans, fontSize: 18)
humansLabel.position = CGPoint(x: frame.width - 50, y: uiY)
addChild(humansLabel)
// Timer
timerLabel = createUILabel(text: "⏱️ \(gameState.formattedTime)", fontSize: 22)
timerLabel.position = CGPoint(x: frame.midX, y: uiY2)
addChild(timerLabel)
// Pause button
setupPauseButton()
// UI background bar
let uiBar = SKShapeNode(rect: CGRect(x: 0, y: frame.height - 100,
width: frame.width, height: 100))
uiBar.fillColor = SKColor.white.withAlphaComponent(0.9)
uiBar.strokeColor = SKColor.gray.withAlphaComponent(0.5)
uiBar.lineWidth = 1
uiBar.zPosition = Constants.ZPosition.ui - 1
addChild(uiBar)
}
private func createUILabel(text: String, fontSize: CGFloat) -> 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<UITouch>, 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<UITouch>, 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<UITouch>, with event: UIEvent?) {
isDragging = false
}
override func touchesCancelled(_ touches: Set<UITouch>, 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<UITouch>) {
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]))
}
}
+265
View File
@@ -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<UITouch>, 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)
}
}
@@ -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<UITouch>, 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)
}
}
}
+79
View File
@@ -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
}