diff --git a/TowerForge/TowerForge.xcodeproj/project.pbxproj b/TowerForge/TowerForge.xcodeproj/project.pbxproj index 7dc09ffb..36f87297 100644 --- a/TowerForge/TowerForge.xcodeproj/project.pbxproj +++ b/TowerForge/TowerForge.xcodeproj/project.pbxproj @@ -87,6 +87,7 @@ 523E5C4C2BC53F70007444DA /* WaveSpawnEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523E5C4B2BC53F70007444DA /* WaveSpawnEvent.swift */; }; 523E5C512BC60563007444DA /* SurvivalModeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523E5C502BC60563007444DA /* SurvivalModeTests.swift */; }; 523E5C532BC60A15007444DA /* DeathMatchModeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523E5C522BC60A15007444DA /* DeathMatchModeTests.swift */; }; + 523E5C552BC63A16007444DA /* LeaderboardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 523E5C542BC63A16007444DA /* LeaderboardViewController.swift */; }; 5240D08F2BAE6D0A004F1486 /* Point.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5240D08E2BAE6D0A004F1486 /* Point.swift */; }; 5240D0912BAF3453004F1486 /* Life.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5240D0902BAF3453004F1486 /* Life.swift */; }; 5240D0A02BB330B5004F1486 /* GameMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5240D09F2BB330B4004F1486 /* GameMode.swift */; }; @@ -165,6 +166,7 @@ 52DF5FFB2BA3601400135367 /* HealthComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52DF5FFA2BA3601400135367 /* HealthComponent.swift */; }; 52DF5FFF2BA3656500135367 /* ShootingComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52DF5FFE2BA3656500135367 /* ShootingComponent.swift */; }; 52F268702BB4B319009599AD /* GameModeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52F2686F2BB4B319009599AD /* GameModeViewController.swift */; }; + 52F930E72BC63F7F003D11B5 /* LeaderboardSelectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52F930E62BC63F7F003D11B5 /* LeaderboardSelectionViewController.swift */; }; 9B04060D2BB875740026E903 /* EventTransformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B04060C2BB875740026E903 /* EventTransformation.swift */; }; 9B0406102BB879990026E903 /* InvulnerabilityPowerUp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B04060F2BB879990026E903 /* InvulnerabilityPowerUp.swift */; }; 9B0406122BB889940026E903 /* PowerUpNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B0406112BB889940026E903 /* PowerUpNode.swift */; }; @@ -301,6 +303,7 @@ 523E5C4B2BC53F70007444DA /* WaveSpawnEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaveSpawnEvent.swift; sourceTree = ""; }; 523E5C502BC60563007444DA /* SurvivalModeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurvivalModeTests.swift; sourceTree = ""; }; 523E5C522BC60A15007444DA /* DeathMatchModeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeathMatchModeTests.swift; sourceTree = ""; }; + 523E5C542BC63A16007444DA /* LeaderboardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeaderboardViewController.swift; sourceTree = ""; }; 5240D08E2BAE6D0A004F1486 /* Point.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Point.swift; sourceTree = ""; }; 5240D0902BAF3453004F1486 /* Life.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Life.swift; sourceTree = ""; }; 5240D0952BB04E57004F1486 /* Nosifer-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Nosifer-Regular.ttf"; sourceTree = ""; }; @@ -381,6 +384,7 @@ 52DF5FFA2BA3601400135367 /* HealthComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthComponent.swift; sourceTree = ""; }; 52DF5FFE2BA3656500135367 /* ShootingComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShootingComponent.swift; sourceTree = ""; }; 52F2686F2BB4B319009599AD /* GameModeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameModeViewController.swift; sourceTree = ""; }; + 52F930E62BC63F7F003D11B5 /* LeaderboardSelectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeaderboardSelectionViewController.swift; sourceTree = ""; }; 9B04060C2BB875740026E903 /* EventTransformation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventTransformation.swift; sourceTree = ""; }; 9B04060F2BB879990026E903 /* InvulnerabilityPowerUp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvulnerabilityPowerUp.swift; sourceTree = ""; }; 9B0406112BB889940026E903 /* PowerUpNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PowerUpNode.swift; sourceTree = ""; }; @@ -670,6 +674,8 @@ 5299D13B2BC3670E003EF746 /* LoginViewController.swift */, 5299D13D2BC36E61003EF746 /* RegisterViewController.swift */, 52DD8F982BC52F8400D96BAB /* LevelPopupViewController.swift */, + 523E5C542BC63A16007444DA /* LeaderboardViewController.swift */, + 52F930E62BC63F7F003D11B5 /* LeaderboardSelectionViewController.swift */, ); path = ViewControllers; sourceTree = ""; @@ -715,8 +721,8 @@ 52A7940E2BBC476B0083C976 /* Networking */ = { isa = PBXGroup; children = ( - 52A794072BBC35E30083C976 /* Constants */, 5299D13F2BC3AA27003EF746 /* RankingNetwork */, + 52A794072BBC35E30083C976 /* Constants */, 3CFA72E52BC039740081337F /* Utils */, 3CBECF822BBDC36A005EF39B /* GameNetwork */, 52A7940F2BBC47770083C976 /* RoomNetwork */, @@ -1360,6 +1366,7 @@ 5240D0A72BB33356004F1486 /* LifeProp.swift in Sources */, 3CA829C42BB70C5E00D8E72A /* ButtonComponent.swift in Sources */, 3CBECF892BBE9797005EF39B /* TFNetworkCoder.swift in Sources */, + 523E5C552BC63A16007444DA /* LeaderboardViewController.swift in Sources */, 5299D13E2BC36E61003EF746 /* RegisterViewController.swift in Sources */, 52DF5FFF2BA3656500135367 /* ShootingComponent.swift in Sources */, BA443D3D2BAD9557009F0FFB /* RemoveSystem.swift in Sources */, @@ -1459,6 +1466,7 @@ 52A794062BBC32A10083C976 /* FirebaseRepositoryProtocol.swift in Sources */, 9B0406102BB879990026E903 /* InvulnerabilityPowerUp.swift in Sources */, 5299D1412BC3AA3A003EF746 /* GameRankData.swift in Sources */, + 52F930E72BC63F7F003D11B5 /* LeaderboardSelectionViewController.swift in Sources */, 9BD669682BAFDE5E00DC8C4C /* GridDelegate.swift in Sources */, 52DF5FEB2BA3400C00135367 /* TFAnimatableNode.swift in Sources */, 3C3CBDFF2BB8708A0001B8A9 /* CGPoint+Extensions.swift in Sources */, diff --git a/TowerForge/TowerForge.xcodeproj/project.xcworkspace/xcuserdata/macbookpro.xcuserdatad/UserInterfaceState.xcuserstate b/TowerForge/TowerForge.xcodeproj/project.xcworkspace/xcuserdata/macbookpro.xcuserdatad/UserInterfaceState.xcuserstate index e3d3c3fe..7df9bf31 100644 Binary files a/TowerForge/TowerForge.xcodeproj/project.xcworkspace/xcuserdata/macbookpro.xcuserdatad/UserInterfaceState.xcuserstate and b/TowerForge/TowerForge.xcodeproj/project.xcworkspace/xcuserdata/macbookpro.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/TowerForge/TowerForge/AppMain/Storyboards/Base.lproj/Main.storyboard b/TowerForge/TowerForge/AppMain/Storyboards/Base.lproj/Main.storyboard index 05f5b2e4..cee41713 100644 --- a/TowerForge/TowerForge/AppMain/Storyboards/Base.lproj/Main.storyboard +++ b/TowerForge/TowerForge/AppMain/Storyboards/Base.lproj/Main.storyboard @@ -1,9 +1,9 @@ - + - + @@ -112,6 +112,21 @@ + + + + + + + + + + + + + + + @@ -568,6 +583,7 @@ + - + diff --git a/TowerForge/TowerForge/GameModule/GameModes/CaptureTheFlagMode.swift b/TowerForge/TowerForge/GameModule/GameModes/CaptureTheFlagMode.swift index 33d60fc1..d03f6e86 100644 --- a/TowerForge/TowerForge/GameModule/GameModes/CaptureTheFlagMode.swift +++ b/TowerForge/TowerForge/GameModule/GameModes/CaptureTheFlagMode.swift @@ -50,6 +50,6 @@ class CaptureTheFlagMode: GameMode { } } func getGameResults() -> [GameResult] { - [GameResult(variable: "Life left", value: String(self.currentOwnLife))] + [LocalResult(variable: "Life left", value: String(self.currentOwnLife))] } } diff --git a/TowerForge/TowerForge/GameModule/GameModes/DeathMatchMode.swift b/TowerForge/TowerForge/GameModule/GameModes/DeathMatchMode.swift index 96ce9ab8..24cd4815 100644 --- a/TowerForge/TowerForge/GameModule/GameModes/DeathMatchMode.swift +++ b/TowerForge/TowerForge/GameModule/GameModes/DeathMatchMode.swift @@ -56,8 +56,13 @@ class DeathMatchMode: GameMode { } } func getGameResults() -> [GameResult] { - let result = [GameResult(variable: "Total Kill", value: String(self.currentOwnKillCounter)), - GameResult(variable: "Opponent Kill", value: String(self.currentOpponentKillCounter))] + let result: [GameResult] = [ + LeaderboardResult(variable: RankType.TotalKill.rawValue, + result: Double(self.currentOwnKillCounter), + value: String(self.currentOwnKillCounter)), + LocalResult(variable: "Opponent Kill", + value: String(self.currentOpponentKillCounter)) + ] return result } diff --git a/TowerForge/TowerForge/GameModule/GameModes/GameMode.swift b/TowerForge/TowerForge/GameModule/GameModes/GameMode.swift index 23e3fdcb..027ed544 100644 --- a/TowerForge/TowerForge/GameModule/GameModes/GameMode.swift +++ b/TowerForge/TowerForge/GameModule/GameModes/GameMode.swift @@ -7,7 +7,18 @@ import Foundation -struct GameResult { +protocol GameResult { + var variable: String { get } + var value: String { get } +} + +struct LeaderboardResult: GameResult { + var variable: RankType.RawValue + var result: Double + var value: String +} + +struct LocalResult: GameResult { var variable: String var value: String } diff --git a/TowerForge/TowerForge/GameModule/GameModes/SurvivalGameMode.swift b/TowerForge/TowerForge/GameModule/GameModes/SurvivalGameMode.swift index ec843c63..afa47dc1 100644 --- a/TowerForge/TowerForge/GameModule/GameModes/SurvivalGameMode.swift +++ b/TowerForge/TowerForge/GameModule/GameModes/SurvivalGameMode.swift @@ -68,7 +68,7 @@ class SurvivalGameMode: GameMode { } func getGameResults() -> [GameResult] { - [GameResult(variable: "Finished Waves", value: String(currentLevel - 1))] + [LocalResult(variable: "Finished Waves", value: String(currentLevel - 1))] } private func generateWaveSpawns(enemyCount: Int) { diff --git a/TowerForge/TowerForge/GameViewController.swift b/TowerForge/TowerForge/GameViewController.swift index 5dc8ac40..334df1db 100644 --- a/TowerForge/TowerForge/GameViewController.swift +++ b/TowerForge/TowerForge/GameViewController.swift @@ -9,6 +9,8 @@ import SpriteKit class GameViewController: UIViewController { private var gameWorld: GameWorld? + private var playerData: AuthenticationData? + private var gameRankProvider: GameRankProvider? var gameMode: Mode? var isPaused = false var gameRoom: GameRoom? @@ -24,7 +26,14 @@ class GameViewController: UIViewController { super.viewDidLoad() AchievementManager.incrementTotalGamesStarted() AudioManager.shared.playBackground() - showGameLevelScene(level: 1) // TODO : Change hardcoded level value + showGameLevelScene() + + let auth = AuthenticationProvider() + if auth.isUserLoggedIn() { + auth.getUserDetails { data, _ in + self.playerData = data + } + } } override func viewDidDisappear(_ animated: Bool) { @@ -79,11 +88,22 @@ extension GameViewController: SceneManagerDelegate { } func showGameOverScene(isWin: Bool, results: [GameResult]) { let gameOverScene = GameOverScene(win: isWin, results: results) + if let data = self.playerData { + for result in results { + if let leaderboardResult = result as? LeaderboardResult { + let rank = GameRankProvider(type: leaderboardResult.variable) + let data = GameRankData(userId: data.userId, + username: data.username ?? "", + score: leaderboardResult.result) + rank.setNewRank(rank: data) + } + } + } gameOverScene.sceneManagerDelegate = self gamePopupButton.isHidden = true showScene(scene: gameOverScene) } - func showGameLevelScene(level: Int) { + func showGameLevelScene() { guard let gameScene = GameScene(fileNamed: "GameScene") else { return } diff --git a/TowerForge/TowerForge/Networking/RankingNetwork/GameRankProvider.swift b/TowerForge/TowerForge/Networking/RankingNetwork/GameRankProvider.swift index d4cf567e..9785dace 100644 --- a/TowerForge/TowerForge/Networking/RankingNetwork/GameRankProvider.swift +++ b/TowerForge/TowerForge/Networking/RankingNetwork/GameRankProvider.swift @@ -8,11 +8,41 @@ import Foundation import FirebaseDatabaseInternal +enum RankType: String, CaseIterable { + case TotalKill + static var allCasesAsString: [String] { + allCases.map { $0.rawValue } + } +} + class GameRankProvider { - private let ranksRef = FirebaseDatabaseReference(.Ranks) + private let ranksRef: DatabaseReference + init(type: String) { + self.ranksRef = FirebaseDatabaseReference(.Ranks).child(type) + } func setNewRank(rank: GameRankData) { - let userRankData = ["username": rank.username, "score": rank.score] as [String: Any] - ranksRef.child(rank.userId).setValue(userRankData) + self.getHighScore(forPlayer: rank.userId) { result, _ in + guard let oldResult = result else { + let userRankData = ["username": rank.username, "score": rank.score] as [String: Any] + self.ranksRef.child(rank.userId).setValue(userRankData) + return + } + if rank.score > oldResult { + let userRankData = ["username": rank.username, "score": rank.score] as [String: Any] + self.ranksRef.child(rank.userId).setValue(userRankData) + } + } + + } + private func getHighScore(forPlayer playerId: String, completion: @escaping (Double?, Error?) -> Void) { + ranksRef.child(playerId).observeSingleEvent(of: .value) { snapshot in + if let userData = snapshot.value as? [String: Any], + let score = userData["score"] as? Double { + completion(score, nil) + } else { + completion(nil, nil) + } + } } func getTopRanks(completion: @escaping ([GameRankData]?, Error?) -> Void) { ranksRef.queryOrdered(byChild: "score").queryLimited(toLast: 10).observeSingleEvent(of: .value) { snapshot in diff --git a/TowerForge/TowerForge/Scenes/SceneDelegates/SceneManagerDelegate.swift b/TowerForge/TowerForge/Scenes/SceneDelegates/SceneManagerDelegate.swift index 04e5f2c4..e39f1ca3 100644 --- a/TowerForge/TowerForge/Scenes/SceneDelegates/SceneManagerDelegate.swift +++ b/TowerForge/TowerForge/Scenes/SceneDelegates/SceneManagerDelegate.swift @@ -11,5 +11,5 @@ protocol SceneManagerDelegate: AnyObject { func showMenuScene() func showGameOverScene(isWin: Bool, results: [GameResult]) func showLevelScene() - func showGameLevelScene(level: Int) + func showGameLevelScene() } diff --git a/TowerForge/TowerForge/ViewControllers/LeaderboardSelectionViewController.swift b/TowerForge/TowerForge/ViewControllers/LeaderboardSelectionViewController.swift new file mode 100644 index 00000000..f97d7ff9 --- /dev/null +++ b/TowerForge/TowerForge/ViewControllers/LeaderboardSelectionViewController.swift @@ -0,0 +1,24 @@ +// +// LeaderboardSelectionViewController.swift +// TowerForge +// +// Created by Vanessa Mae on 10/04/24. +// + +import Foundation +import UIKit + +class LeaderboardSelectionViewController: UITabBarController { + + override func viewDidLoad() { + super.viewDidLoad() + let viewControllers = RankType.allCases.map { type -> UINavigationController in + let leaderboardVC = LeaderboardViewController(type: type) + let navigationController = UINavigationController(rootViewController: leaderboardVC) + navigationController.tabBarItem.title = type.rawValue + return navigationController + } + + self.setViewControllers(viewControllers, animated: false) + } +} diff --git a/TowerForge/TowerForge/ViewControllers/LeaderboardViewController.swift b/TowerForge/TowerForge/ViewControllers/LeaderboardViewController.swift new file mode 100644 index 00000000..c45b34b6 --- /dev/null +++ b/TowerForge/TowerForge/ViewControllers/LeaderboardViewController.swift @@ -0,0 +1,102 @@ +// +// LeaderboardViewController.swift +// TowerForge +// +// Created by Vanessa Mae on 10/04/24. +// + +import Foundation +import UIKit + +class LeaderboardViewController: UIViewController { + private let type: RankType + + init(type: RankType) { + self.type = type + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + private let stackView: UIStackView = { + let stackView = UIStackView() + stackView.axis = .vertical + stackView.spacing = 10 + stackView.backgroundColor = .yellow + stackView.translatesAutoresizingMaskIntoConstraints = false + return stackView + }() + + override func viewDidLoad() { + super.viewDidLoad() + setupScrollView() + setupViews() + + fetchTopRanks(type: self.type.rawValue) + } + + private func setupViews() { + view.addSubview(stackView) + NSLayoutConstraint.activate([ + stackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 20), + stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), + stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20) + ]) + } + + private func fetchTopRanks(type: String) { + let rank = GameRankProvider(type: type) + rank.getTopRanks { data, _ in + guard let result = data else { + return + } + self.displayLeaderboard(ranks: result) + } + } + private let scrollView: UIScrollView = { + let scrollView = UIScrollView() + scrollView.translatesAutoresizingMaskIntoConstraints = false + return scrollView + }() + + private func setupScrollView() { + view.addSubview(scrollView) + scrollView.addSubview(stackView) + + NSLayoutConstraint.activate([ + scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 20), + scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), + scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), + scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -20), + + stackView.topAnchor.constraint(equalTo: scrollView.topAnchor), + stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), + stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), + stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), + stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor) + ]) + } + + private func displayLeaderboard(ranks: [GameRankData]) { + for rankData in ranks { + let rankLabel = UILabel() + + if let customFont = UIFont(name: "Nosifer-Regular", size: 24) { + rankLabel.font = customFont + } + + rankLabel.text = "\(rankData.username): \(rankData.score)" + rankLabel.textColor = .white + + rankLabel.backgroundColor = .gray + rankLabel.layer.cornerRadius = 8 + rankLabel.layer.masksToBounds = true + rankLabel.textAlignment = .center + rankLabel.layoutMargins = UIEdgeInsets(top: 10.0, left: 10.0, bottom: 10.0, right: 10.0) + + rankLabel.translatesAutoresizingMaskIntoConstraints = false + stackView.addArrangedSubview(rankLabel) + } + } +} diff --git a/TowerForge/TowerForge/ViewControllers/RegisterViewController.swift b/TowerForge/TowerForge/ViewControllers/RegisterViewController.swift index 02b6f455..56471b8d 100644 --- a/TowerForge/TowerForge/ViewControllers/RegisterViewController.swift +++ b/TowerForge/TowerForge/ViewControllers/RegisterViewController.swift @@ -33,7 +33,7 @@ class RegisterViewController: UIViewController { if let navigationController = self.navigationController, let gameModeVC = navigationController.viewControllers.first(where: { $0 is GameModeViewController }) { self.navigationController?.popToViewController(gameModeVC, animated: true) - } + } } } private func showAlert(message: String, title: String) {