// // 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() 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>, fallenBlocks: Array>, fadingShapes: Array, 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()])) } }