diff --git a/Documentation/TODO.md b/Documentation/TODO.md deleted file mode 100644 index 7f7beac..0000000 --- a/Documentation/TODO.md +++ /dev/null @@ -1,10 +0,0 @@ -# TODO -* Move these items into github issues -* Handle multiple row removals -* break pieces into more than 2 pieces when row is removed -* Create enums for string constants (eg. node names) -* Attempt more refined joystick controls (small movements should cause slower piece movement) -* Increase speed at higher levels -* Pause on mobile (not just enter) (restart in menu?) -* Unit tests -* consider visual changes diff --git a/Wonky Blocks.xcodeproj/project.pbxproj b/Wonky Blocks.xcodeproj/project.pbxproj index c3ce4b4..b90fcaf 100644 --- a/Wonky Blocks.xcodeproj/project.pbxproj +++ b/Wonky Blocks.xcodeproj/project.pbxproj @@ -37,7 +37,6 @@ /* Begin PBXFileReference section */ 45332F4C24ECA5D900F7A72C /* preview.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = preview.gif; sourceTree = ""; }; 453EDD6124998982006470E7 /* Joystick.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Joystick.swift; sourceTree = ""; }; - 453FBC7A249ADEB20056E4D7 /* TODO.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = TODO.md; sourceTree = ""; }; 45525A65244C7FD4004974AB /* Wonky Blocks.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Wonky Blocks.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 45525A68244C7FD4004974AB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 45525A6A244C7FD4004974AB /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -89,7 +88,6 @@ 45332F4924ECA14500F7A72C /* Documentation */ = { isa = PBXGroup; children = ( - 453FBC7A249ADEB20056E4D7 /* TODO.md */, 45332F4C24ECA5D900F7A72C /* preview.gif */, ); path = Documentation; diff --git a/Wonky Blocks/Support/extensions.swift b/Wonky Blocks/Support/extensions.swift index 7395e0e..6f07c6c 100644 --- a/Wonky Blocks/Support/extensions.swift +++ b/Wonky Blocks/Support/extensions.swift @@ -19,13 +19,14 @@ extension CGPath { extension SKNode { // Gets the paths of all children, positioned based on the nodes position - var childrenPositionPaths: [CGPath] { + var childrenPositionPaths: [(path: CGPath, center: CGPoint)] { return self.children.compactMap{ child in if let childPath = (child as? SKShapeNode)?.path, + let center = child.userData?.value(forKey: "center") as? CGPoint, let transformedPath = childPath.rotateAroundCenter(path: childPath, float: self.zRotation) { var translateTransform = CGAffineTransform(translationX: self.position.x, y: self.position.y) - return transformedPath.copy(using: &translateTransform) + return ((path: transformedPath.copy(using: &translateTransform), center: center) as! (path: CGPath, center: CGPoint)) } else { return nil } diff --git a/Wonky Blocks/UI/GameViewController.swift b/Wonky Blocks/UI/GameViewController.swift index 791ea75..806f088 100644 --- a/Wonky Blocks/UI/GameViewController.swift +++ b/Wonky Blocks/UI/GameViewController.swift @@ -9,6 +9,7 @@ import UIKit import SpriteKit import Combine +import SwiftClipper class WonkyGameViewController: UIViewController { var spriteKitView: SKView { @@ -159,8 +160,8 @@ class WonkyGameViewController: UIViewController { contactingBodies.forEach({ (intersectingNode) in // This will go through each of the bodies that intersects the row being removed. // collect the paths that will make up the piece above and below the removed row. - var belowPaths: [CGPath] = [] - var abovePaths: [CGPath] = [] + var belowPaths: [(path: Path, center: CGPoint)] = [] + var abovePaths: [(path: Path, center: CGPoint)] = [] // One square of the tetronimo intersectingNode.childrenPositionPaths.forEach { intChildPath in @@ -169,31 +170,76 @@ class WonkyGameViewController: UIViewController { var rowTranslateTransform = CGAffineTransform(translationX: fromRow.position.x, y: fromRow.position.y) let transformedRowPath = fromRow.path?.copy(using: &rowTranslateTransform) - let difference = intChildPath.getPathElementsPoints().difference((transformedRowPath!.getPathElementsPoints())) + let difference = intChildPath.path.getPathElementsPoints().difference((transformedRowPath!.getPathElementsPoints())) difference.forEach({ (differencePiece) in // Area only seems to be accurate when the points of the path are clockwise. // If the remaining are of the piece is very small (less than 50 area), we will remove it completely. if differencePiece.asClockwise().area > 50 || differencePiece.asClockwise().area < -50 { if differencePiece.first!.y > fromRow.position.y { - abovePaths.append(differencePiece.asCgPath()) + abovePaths.append((differencePiece, intChildPath.center)) } else { - belowPaths.append(differencePiece.asCgPath()) + belowPaths.append((differencePiece, intChildPath.center)) } } }) } if !abovePaths.isEmpty { - let newNode = WonkyTetronimo(with: abovePaths) - resultNodes.append(newNode) - newNode.physicsBody?.affectedByGravity = true + let fragments = groupFragments(from: abovePaths) + fragments.forEach { (fragment) in + let newNode = WonkyTetronimo(with: fragment) + resultNodes.append(newNode) + newNode.physicsBody?.affectedByGravity = true + } } if !belowPaths.isEmpty { - let newNodeBelow = WonkyTetronimo(with: belowPaths) - resultNodes.append(newNodeBelow) - newNodeBelow.physicsBody?.affectedByGravity = true + let fragments = groupFragments(from: belowPaths) + fragments.forEach { (fragment) in + let newNode = WonkyTetronimo(with: fragment) + resultNodes.append(newNode) + newNode.physicsBody?.affectedByGravity = true + } } breakingNodes.append(intersectingNode) }) return (resultNodes, breakingNodes) } + + /// given some shape paths, return the same paths with touching shapes grouped together. + func groupFragments(from paths: [(path: SwiftClipper.Path, center: CGPoint)]) -> [[(path: SwiftClipper.Path, center: CGPoint)]] { + /// each element is a group of paths, and the node's original center (used to determine connection) + var allPathGroups: [[(path: SwiftClipper.Path, center: CGPoint)]] = [] + paths.forEach { path in + let touchingPieces = allPathGroups.enumerated().filter { existingPath in + let matchingPaths = existingPath.element.filter { existingPathPiece in + + let distance = path.center.distance(to: existingPathPiece.center) + return distance < 60 + } + // print(matchingPaths.count, path.center) + return existingPath.element.first { existingPathPiece in + + let distance = path.center.distance(to: existingPathPiece.center) + print(distance) + return distance < 60 + } != nil + } + print("touching pieces? \(touchingPieces.count)") + if touchingPieces.count > 1 { + // Some pieces were previously put in separate groups, but this new path connects them. + if touchingPieces.count > 2 { + print("warning, more than 2 touching pieces, but only 2 will be merged") + } + let pieceToMerge = allPathGroups.remove(at: touchingPieces[1].offset) + allPathGroups[touchingPieces[0].offset].append(contentsOf: pieceToMerge) + allPathGroups[touchingPieces[0].offset].append(path) + } else if let touchingPiece = touchingPieces.first?.offset { + // path is touching a path within touchingPiece, so add it to the same piece. + allPathGroups[touchingPiece].append(path) + } else { + // path isn't touching any of the existing pieces, so make a new one with this new path. + allPathGroups.append([path]) + } + } + return allPathGroups + } } diff --git a/Wonky Blocks/UI/SKNodes/WonkyRow.swift b/Wonky Blocks/UI/SKNodes/WonkyRow.swift index 41bae74..5ebc839 100644 --- a/Wonky Blocks/UI/SKNodes/WonkyRow.swift +++ b/Wonky Blocks/UI/SKNodes/WonkyRow.swift @@ -39,7 +39,7 @@ class WonkyRow: SKShapeNode { // take the sum of the area of the intersection between every piece and the row. var totalArea = soFar current.node?.childrenPositionPaths.forEach { childNodePath in - let intersect = self.path?.getPathElementsPoints().intersection(childNodePath.getPathElementsPoints()) + let intersect = self.path?.getPathElementsPoints().intersection(childNodePath.path.getPathElementsPoints()) intersect?.forEach({ (path) in totalArea += -path.asClockwise().area }) diff --git a/Wonky Blocks/UI/SKNodes/WonkyTetronimo.swift b/Wonky Blocks/UI/SKNodes/WonkyTetronimo.swift index 365da41..94e3ad2 100644 --- a/Wonky Blocks/UI/SKNodes/WonkyTetronimo.swift +++ b/Wonky Blocks/UI/SKNodes/WonkyTetronimo.swift @@ -7,6 +7,7 @@ // import SpriteKit +import SwiftClipper class WonkyTetronimo: SKShapeNode { var center: CGPoint { @@ -33,6 +34,8 @@ class WonkyTetronimo: SKShapeNode { // unfortunately, this also causes things to "catch" on each other occasionally. let path = CGPath(rect: CGRect(x: row.offset * 50, y: col.offset * 50, width: 48, height: 48), transform: nil) let square = SKShapeNode(path: path) + square.userData = NSMutableDictionary() + square.userData?.setValue(path.getPathElementsPoints().asClockwise().centroid, forKey: "center") square.lineWidth = 1 square.fillColor = .blue square.strokeColor = .clear @@ -48,14 +51,17 @@ class WonkyTetronimo: SKShapeNode { } /// creates a tetronimo from a collection of paths. Used to create a tetronimo that replaces a piece that's been chopped up by a removed line. - convenience init(with childrenPaths: [CGPath]) { + /// the orignal center point of each path is also saved into the userData of the nodes + convenience init(with childrenPaths: [(path: Path, center: CGPoint)]) { self.init() let children = childrenPaths.compactMap{(path) -> (SKPhysicsBody, SKShapeNode)? in - guard let physicsBody = SKPhysicsBody(polygonFrom: path) as SKPhysicsBody? else { + guard let physicsBody = SKPhysicsBody(polygonFrom: path.path.asCgPath()) as SKPhysicsBody? else { // sometimes physics bodies fail to create from paths - we don't seem to miss any important nodes because of this. return nil } - let diffNode = SKShapeNode(path: path) + let diffNode = SKShapeNode(path: path.path.asCgPath()) + diffNode.userData = NSMutableDictionary() + diffNode.userData?.setValue(path.path.asClockwise().centroid, forKey: "center") diffNode.fillColor = .blue diffNode.lineWidth = 1 diffNode.strokeColor = .clear