9e501cc4e8
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
163 lines
5.9 KiB
Swift
163 lines
5.9 KiB
Swift
//
|
|
// 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
|
|
}
|
|
}
|