privyet/Privyet/GameScene.swift

259 lines
11 KiB
Swift

//
// GameScene.swift
// Privyet
//
// Created by Amy Bowersox on 5/23/20.
// Copyright © 2020 Erbosoft Metaverse Design Solutions. All rights reserved.
//
import SpriteKit
import GameplayKit
let BlockSize: CGFloat = 20.0
let TickLengthLevelOne = TimeInterval(600)
class GameScene: SKScene {
let gameLayer = SKNode()
let shapeLayer = SKNode()
let LayerPosition = CGPoint(x: 6, y: -6)
var tick: (() -> ())?
var tickLengthMillis = TickLengthLevelOne
var lastTick: NSDate?
var scaleFactor: CGFloat!
var holdControlRect: CGRect!
var textureCache = Dictionary<String, SKTexture>()
var textureAtlas: SKTextureAtlas?
required init(coder aDecoder: NSCoder) {
fatalError("NSCoder not supported")
}
override init(size: CGSize) {
super.init(size: size)
//print("Init scene with size \(size)")
textureAtlas = SKTextureAtlas(named: "Sprites")
anchorPoint = CGPoint(x: 0, y: 1.0)
let background = SKSpriteNode(imageNamed: "background")
//print("Background is sized \(background.size)")
// Scale computations
scaleFactor = CGFloat(min(size.width / background.size.width, size.height / background.size.height))
background.position = CGPoint(x: 0, y: 0)
background.anchorPoint = CGPoint(x: 0, y: 1.0)
background.setScale(scaleFactor)
addChild(background)
gameLayer.setScale(scaleFactor)
addChild(gameLayer)
// Load and add the game board and shape layer
let gameBoardTexture = SKTexture(imageNamed: "gameboard")
let gameBoard = SKSpriteNode(texture: gameBoardTexture, size: CGSize(width: BlockSize * CGFloat(NumColumns), height: BlockSize * CGFloat(NumRows)))
gameBoard.anchorPoint = CGPoint(x: 0, y: 1.0)
gameBoard.position = LayerPosition
shapeLayer.position = LayerPosition
shapeLayer.addChild(gameBoard)
gameLayer.addChild(shapeLayer)
// Calculate area of "hold" piece for hit-testing
let holdBBox = Shape.boundingBox(baseColumn: HoldColumn, baseRow: HoldRow)
let holdWidth = CGFloat(holdBBox.width) * BlockSize
let holdHeight = CGFloat(holdBBox.height) * BlockSize
let holdX = LayerPosition.x + (CGFloat(HoldColumn) * BlockSize)
let holdY = LayerPosition.y + (CGFloat(HoldRow) * BlockSize)
holdControlRect = CGRect(x: holdX * scaleFactor, y: holdY * scaleFactor, width: holdWidth * scaleFactor, height: holdHeight * scaleFactor)
// Set the theme music to play infinitely.
run(SKAction.repeatForever(SKAction.playSoundFileNamed("Sounds/theme.mp3", waitForCompletion: true)))
}
// Convenience function to play a sound file.
func playSound(sound: String) {
run(SKAction.playSoundFileNamed(sound, waitForCompletion: false))
}
// Called once per frame by the framework, before it's rendered.
override func update(_ currentTime: TimeInterval) {
guard let lastTick = lastTick else {
return
}
let timePassed = lastTick.timeIntervalSinceNow * -1000.0
if timePassed > tickLengthMillis {
self.lastTick = NSDate()
tick?()
}
}
// Starts the game tick going.
func startTicking() {
lastTick = NSDate()
}
// Stops the game tick.
func stopTicking() {
lastTick = nil
}
// Convert bucket row/column coordinates to display coordinates.
func pointForColumn(column: Int, row: Int) -> CGPoint {
let x = LayerPosition.x + (CGFloat(column) * BlockSize) + (BlockSize / 2)
let y = LayerPosition.y - ((CGFloat(row) * BlockSize) + (BlockSize / 2))
return CGPoint(x: x, y: y)
}
func rectForBucket() -> CGRect {
let width = CGFloat(NumColumns) * BlockSize
let height = CGFloat(NumRows) * BlockSize
return CGRect(x: (LayerPosition.x + (BlockSize / 2)) * scaleFactor, y: (LayerPosition.y + (BlockSize / 2)) * scaleFactor, width: width * scaleFactor, height: height * scaleFactor)
}
// Add the "preview" shape to the current scene. This also "rezzes in" a shape's block sprites.
func addPreviewShapeToScene(shape: Shape, completion: @escaping () -> ()) {
for block in shape.blocks {
var texture = textureCache[block.spriteName]
if texture == nil {
texture = textureAtlas?.textureNamed(block.spriteName)
textureCache[block.spriteName] = texture
}
let sprite = SKSpriteNode(texture: texture)
sprite.position = pointForColumn(column: block.column, row: block.row - 2)
shapeLayer.addChild(sprite)
block.sprite = sprite
// Animation
sprite.alpha = 0
let moveAction = SKAction.move(to: pointForColumn(column: block.column, row: block.row), duration: TimeInterval(0.2))
moveAction.timingMode = .easeOut
let fadeInAction = SKAction.fadeAlpha(to: 0.7, duration: 0.4)
fadeInAction.timingMode = .easeOut
sprite.run(SKAction.group([moveAction, fadeInAction]))
}
let waitAction = SKAction.wait(forDuration: 0.4)
let completeAction = SKAction.run(completion)
run(SKAction.sequence([waitAction, completeAction]))
}
// Move the "preview" shape into position so it becomes the new "falling" shape.
func movePreviewShape(shape: Shape, completion: @escaping () -> ()) {
for block in shape.blocks {
let sprite = block.sprite!
let moveTo = pointForColumn(column: block.column, row: block.row)
let moveToAction: SKAction = SKAction.move(to: moveTo, duration: 0.4)
moveToAction.timingMode = .easeOut
sprite.run(SKAction.group([moveToAction, SKAction.fadeAlpha(to: 1.0, duration: 0.2)]))
}
let waitAction = SKAction.wait(forDuration: 0.2)
let completeAction = SKAction.run(completion)
run(SKAction.sequence([waitAction, completeAction]))
}
// Redraw the falling shape by shifting the position of its component blocks.
func redrawShape(shape: Shape, completion: @escaping () -> ()) {
for block in shape.blocks {
let sprite = block.sprite!
let moveTo = pointForColumn(column: block.column, row: block.row)
let moveToAction: SKAction = SKAction.move(to: moveTo, duration: 0.05)
moveToAction.timingMode = .easeOut
sprite.run(moveToAction)
if block == shape.blocks.last {
sprite.run(SKAction.run(completion))
}
}
}
// Animate the removed lines "exploding" and the remaining blocks dropping into place.
func animateCollapsingLines(linesToRemove: Array<Array<Block>>, fallenBlocks: Array<Array<Block>>, fadingShapes: Array<Shape>, completion: @escaping () -> ()) {
var longestDuration: TimeInterval = 0
// Animate the shapes fading out
for fadeShape in fadingShapes {
for block in fadeShape.blocks {
let sprite = block.sprite!
let fadeOutAction: SKAction = SKAction.fadeOut(withDuration: TimeInterval(0.1))
fadeOutAction.timingMode = .easeIn
sprite.run(SKAction.sequence([fadeOutAction, SKAction.removeFromParent()]))
}
}
// Animate the falling blocks dropping into place
for (columnIdx, column) in fallenBlocks.enumerated() {
for (blockIdx, block) in column.enumerated() {
let newPosition = pointForColumn(column: block.column, row: block.row)
let sprite = block.sprite!
let delay = (TimeInterval(columnIdx) * 0.05) + (TimeInterval(blockIdx) * 0.05)
let duration = TimeInterval(((sprite.position.y - newPosition.y) / BlockSize) * 0.1)
let moveAction = SKAction.move(to: newPosition, duration: duration)
moveAction.timingMode = .easeOut
sprite.run(SKAction.sequence([SKAction.wait(forDuration: delay), moveAction]))
longestDuration = max(longestDuration, duration + delay)
}
}
// Animate the "removed" blocks "exploding"
for rowToRemove in linesToRemove {
for block in rowToRemove {
let randomRadius = CGFloat(Float.random(in: 100.0 ... 500.0))
let goLeft = Bool.random()
var point = pointForColumn(column: block.column, row: block.row)
point = CGPoint(x: point.x + (goLeft ? -randomRadius : randomRadius), y: point.y)
let randomDuration = TimeInterval(Float.random(in: 0.5 ... 2.5))
var startAngle = CGFloat(Double.pi)
var endAngle = startAngle * 2
if (goLeft) {
endAngle = startAngle
startAngle = 0
}
let archPath = UIBezierPath(arcCenter: point, radius: randomRadius, startAngle: startAngle, endAngle: endAngle, clockwise: goLeft)
let archAction = SKAction.follow(archPath.cgPath, asOffset: false, orientToPath: true, duration: randomDuration)
archAction.timingMode = .easeIn
let sprite = block.sprite!
sprite.zPosition = 100
sprite.run(SKAction.sequence([
SKAction.group([archAction, SKAction.fadeOut(withDuration: TimeInterval(randomDuration))]),
SKAction.removeFromParent()]))
}
}
run(SKAction.sequence([SKAction.wait(forDuration: longestDuration), SKAction.run(completion)]))
}
// Animate the multiple-line clearance banner.
func animateClearanceBanner(bannerName: String?) {
guard let name = bannerName else {
return
}
var texture = textureCache[name]
if texture == nil {
texture = textureAtlas?.textureNamed(name)
textureCache[name] = texture
}
let sprite = SKSpriteNode(texture: texture)
sprite.position = CGPoint(x: LayerPosition.x + (CGFloat(NumColumns / 2) * BlockSize), y: LayerPosition.y - (CGFloat(NumRows / 2) * BlockSize))
sprite.zPosition = 20
sprite.alpha = 0
shapeLayer.addChild(sprite)
// Animate a quick fade-in followed by a slow fade out
let fadeInAction: SKAction = SKAction.fadeAlpha(to: 0.9, duration: 0.05)
let fadeOutAction: SKAction = SKAction.fadeOut(withDuration: 1.5)
fadeOutAction.timingMode = .easeIn
sprite.run(SKAction.sequence([fadeInAction, fadeOutAction, SKAction.removeFromParent()]))
}
}