From f17954283b4e1cdeda85507ced9d848dbada3ab7 Mon Sep 17 00:00:00 2001 From: Rubesh Date: Wed, 17 Apr 2024 15:23:08 +0800 Subject: [PATCH 01/14] Update MoveEvent --- .../Systems/BaseSystems/MovementSystem.swift | 45 +++++++++++-------- .../Inference/InferenceEngineFactory.swift | 4 +- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/TowerForge/TowerForge/GameModule/Systems/BaseSystems/MovementSystem.swift b/TowerForge/TowerForge/GameModule/Systems/BaseSystems/MovementSystem.swift index fd7e3694..9bbf5d0f 100644 --- a/TowerForge/TowerForge/GameModule/Systems/BaseSystems/MovementSystem.swift +++ b/TowerForge/TowerForge/GameModule/Systems/BaseSystems/MovementSystem.swift @@ -24,8 +24,8 @@ class MovementSystem: TFSystem { /// - displacement: Value of the required displacement func handleMovement(for entityId: UUID, with displacement: CGVector) { /*guard isActive else { TODO: Implement boolean check - return - }*/ + return + }*/ guard let currentEntity = entityManager.entity(with: entityId), let movementComponent = currentEntity.component(ofType: MovableComponent.self) else { return @@ -35,25 +35,32 @@ class MovementSystem: TFSystem { } private func processMovableComponent(_ movableComponent: MovableComponent, time: CGFloat) { - guard movableComponent.shouldMove, let entity = movableComponent.entity, - let player = entity.component(ofType: PlayerComponent.self)?.player else { - return - } - - if entity is BaseProjectile { - movableComponent.update(deltaTime: time) - return - } - - guard eventManager.isHost else { - return - } + /* + guard movableComponent.shouldMove, let entity = movableComponent.entity, + let player = entity.component(ofType: PlayerComponent.self)?.player else { + return + } + + if entity is BaseProjectile { + movableComponent.update(deltaTime: time) + return + } + + guard eventManager.isHost else { + return + } + + let displacement = movableComponent.velocity * player.getDirectionVelocity() * time + guard let player = eventManager.currentPlayer else { + eventManager.add(MoveEvent(on: entity.id, at: Date().timeIntervalSince1970, with: displacement)) + return + } + eventManager.add(RemoteMoveEvent(id: entity.id, moveBy: displacement, gamePlayer: player)) + */ - let displacement = movableComponent.velocity * player.getDirectionVelocity() * time - guard let player = eventManager.currentPlayer else { - eventManager.add(MoveEvent(on: entity.id, at: Date().timeIntervalSince1970, with: displacement)) + guard movableComponent.shouldMove else { return } - eventManager.add(RemoteMoveEvent(id: entity.id, moveBy: displacement, gamePlayer: player)) + movableComponent.update(deltaTime: time) } } diff --git a/TowerForge/TowerForge/Metrics/Inference/InferenceEngineFactory.swift b/TowerForge/TowerForge/Metrics/Inference/InferenceEngineFactory.swift index 09f8d16f..d82e6c1d 100644 --- a/TowerForge/TowerForge/Metrics/Inference/InferenceEngineFactory.swift +++ b/TowerForge/TowerForge/Metrics/Inference/InferenceEngineFactory.swift @@ -10,7 +10,5 @@ import Foundation class InferenceEngineFactory { static var availableInferenceEngines: [(StatisticsEngine) -> any InferenceEngine] = - [ { stats in AchievementsEngine(stats) }, - { stats in MissionsEngine(stats) }, - { stats in RankingEngine(stats) } ] + [ { stats in AchievementsEngine(stats) }, { stats in MissionsEngine(stats) }, { stats in RankingEngine(stats) } ] } From 532985b6b2459af323732390dc298e06e76fd3dc Mon Sep 17 00:00:00 2001 From: Rubesh Date: Fri, 19 Apr 2024 01:50:52 +0800 Subject: [PATCH 02/14] Add rudimentary implementation of AchievementsUI --- .../TowerForge.xcodeproj/project.pbxproj | 12 +++ .../Storyboards/Base.lproj/Main.storyboard | 59 ++++++++++++++ .../Utilities/AbstractTypeWrapper.swift | 66 ++++++++++++++++ .../Achievements/AchievementTypeWrapper.swift | 10 ++- .../Achievements/AchievementsEngine.swift | 1 + .../Achievements/AchievementsFactory.swift | 8 ++ .../Metrics/Statistics/Statistic.swift | 4 +- .../Statistics/StatisticTypeWrapper.swift | 3 +- .../StatisticsDatabase+Codable.swift | 19 +---- .../Statistics/StatisticsDatabase.swift | 2 +- .../TowerForge/Storage/StorageDatabase.swift | 13 ++++ .../MainMenuViewController.swift | 12 +++ .../PlayerStatsViewController.swift | 76 +++++++++++++++++++ 13 files changed, 262 insertions(+), 23 deletions(-) create mode 100644 TowerForge/TowerForge/Commons/Utilities/AbstractTypeWrapper.swift create mode 100644 TowerForge/TowerForge/Storage/StorageDatabase.swift create mode 100644 TowerForge/TowerForge/ViewControllers/PlayerStatsViewController.swift diff --git a/TowerForge/TowerForge.xcodeproj/project.pbxproj b/TowerForge/TowerForge.xcodeproj/project.pbxproj index 25494225..11714e87 100644 --- a/TowerForge/TowerForge.xcodeproj/project.pbxproj +++ b/TowerForge/TowerForge.xcodeproj/project.pbxproj @@ -238,6 +238,9 @@ BA82C79D2BCDD9ED000515A0 /* InferenceEngineTypeWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA82C79C2BCDD9ED000515A0 /* InferenceEngineTypeWrapper.swift */; }; BA82C79F2BCE7FBA000515A0 /* AbstractGoal.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA82C79E2BCE7FBA000515A0 /* AbstractGoal.swift */; }; BA82C7A22BCE8138000515A0 /* AbstractGoalTypeWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA82C7A12BCE8138000515A0 /* AbstractGoalTypeWrapper.swift */; }; + BAEC99FA2BD13F2600E0C437 /* AbstractTypeWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAEC99F92BD13F2600E0C437 /* AbstractTypeWrapper.swift */; }; + BAEC99FC2BD15AAB00E0C437 /* StorageDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAEC99FB2BD15AAB00E0C437 /* StorageDatabase.swift */; }; + BAEC99FE2BD15E0200E0C437 /* PlayerStatsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAEC99FD2BD15E0200E0C437 /* PlayerStatsViewController.swift */; }; BAFFB9452BB0A8C800D8301F /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAFFB9442BB0A8C800D8301F /* Constants.swift */; }; BAFFB9492BB0ABC400D8301F /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAFFB9482BB0ABC400D8301F /* Logger.swift */; }; BAFFB94B2BB11F9800D8301F /* GameEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAFFB94A2BB11F9800D8301F /* GameEngine.swift */; }; @@ -502,6 +505,9 @@ BA82C79E2BCE7FBA000515A0 /* AbstractGoal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbstractGoal.swift; sourceTree = ""; }; BA82C7A12BCE8138000515A0 /* AbstractGoalTypeWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbstractGoalTypeWrapper.swift; sourceTree = ""; }; BABB7C052BA9A41000D54DAE /* TowerForceTestPlan.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = TowerForceTestPlan.xctestplan; sourceTree = ""; }; + BAEC99F92BD13F2600E0C437 /* AbstractTypeWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbstractTypeWrapper.swift; sourceTree = ""; }; + BAEC99FB2BD15AAB00E0C437 /* StorageDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageDatabase.swift; sourceTree = ""; }; + BAEC99FD2BD15E0200E0C437 /* PlayerStatsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerStatsViewController.swift; sourceTree = ""; }; BAFFB9442BB0A8C800D8301F /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; BAFFB9482BB0ABC400D8301F /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; BAFFB94A2BB11F9800D8301F /* GameEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameEngine.swift; sourceTree = ""; }; @@ -774,6 +780,7 @@ 52DD8F982BC52F8400D96BAB /* LevelPopupViewController.swift */, 523E5C542BC63A16007444DA /* LeaderboardViewController.swift */, 52F930E62BC63F7F003D11B5 /* LeaderboardSelectionViewController.swift */, + BAEC99FD2BD15E0200E0C437 /* PlayerStatsViewController.swift */, ); path = ViewControllers; sourceTree = ""; @@ -1338,6 +1345,7 @@ children = ( BAFFB9482BB0ABC400D8301F /* Logger.swift */, BAFFB9692BB9A64000D8301F /* ObjectSet.swift */, + BAEC99F92BD13F2600E0C437 /* AbstractTypeWrapper.swift */, ); path = Utilities; sourceTree = ""; @@ -1358,6 +1366,7 @@ BAFFB9512BB342E200D8301F /* Storage */ = { isa = PBXGroup; children = ( + BAEC99FB2BD15AAB00E0C437 /* StorageDatabase.swift */, BA82C76E2BCBDE91000515A0 /* Metadata.swift */, BA82C77A2BCD05DC000515A0 /* MetadataManager.swift */, BA82C76A2BCBD682000515A0 /* StorageManager.swift */, @@ -1555,6 +1564,7 @@ 523C29302BBD0916004C6EAC /* GameWaitingRoomViewController.swift in Sources */, BA2F5AC12BC80BE500CBD8E9 /* Statistic.swift in Sources */, 3C9955A12BA47DA500D33FA5 /* BaseTower.swift in Sources */, + BAEC99FA2BD13F2600E0C437 /* AbstractTypeWrapper.swift in Sources */, 5299D1432BC3AB38003EF746 /* GameRankProvider.swift in Sources */, 5250B42F2BAE0DB000F16CF6 /* LabelComponent.swift in Sources */, 3CCF9CB32BAB1F42004D170E /* SystemManager.swift in Sources */, @@ -1648,6 +1658,7 @@ 52A794112BBC48FE0083C976 /* GameRoom.swift in Sources */, 3CD37AA72BBEC5EF00222D8A /* BaseRemoteEvent.swift in Sources */, 527A077C2BB3F4CC00CD9D08 /* KillEvent.swift in Sources */, + BAEC99FE2BD15E0200E0C437 /* PlayerStatsViewController.swift in Sources */, 5299D1322BC31050003EF746 /* AuthenticationManager.swift in Sources */, 3C3CBE012BB870950001B8A9 /* CGVector+Extensions.swift in Sources */, 5299D13C2BC3670E003EF746 /* LoginViewController.swift in Sources */, @@ -1723,6 +1734,7 @@ 9B0406142BB89BE00026E903 /* PowerUpSelectionNode.swift in Sources */, 5240D0A22BB33183004F1486 /* DeathMatchMode.swift in Sources */, BA2F5AC52BC8143E00CBD8E9 /* TotalKillsStatistic.swift in Sources */, + BAEC99FC2BD15AAB00E0C437 /* StorageDatabase.swift in Sources */, 3CAC4A6F2BB6A4F200A5D22E /* LabelRenderStage.swift in Sources */, BA82C7922BCD6579000515A0 /* StatisticUpdateActor.swift in Sources */, BA82C75F2BCB1528000515A0 /* AchievementsDatabase.swift in Sources */, diff --git a/TowerForge/TowerForge/AppMain/Storyboards/Base.lproj/Main.storyboard b/TowerForge/TowerForge/AppMain/Storyboards/Base.lproj/Main.storyboard index 23021fb6..36396517 100644 --- a/TowerForge/TowerForge/AppMain/Storyboards/Base.lproj/Main.storyboard +++ b/TowerForge/TowerForge/AppMain/Storyboards/Base.lproj/Main.storyboard @@ -87,10 +87,22 @@ + + @@ -99,11 +111,14 @@ + + + @@ -127,6 +142,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1097,6 +1153,9 @@ + + + diff --git a/TowerForge/TowerForge/Commons/Utilities/AbstractTypeWrapper.swift b/TowerForge/TowerForge/Commons/Utilities/AbstractTypeWrapper.swift new file mode 100644 index 00000000..4aaa008a --- /dev/null +++ b/TowerForge/TowerForge/Commons/Utilities/AbstractTypeWrapper.swift @@ -0,0 +1,66 @@ +// +// AbstractTypeWrapper.swift +// TowerForge +// +// Created by Rubesh on 18/4/24. +// + +import Foundation + +/// TODO: Replace type wrappers with this. +protocol AbstractTypeWrapper: Equatable, Hashable { + associatedtype T: Any + var type: T.Type { get } + + var asString: String { get } +} + +struct GenericTypeWrapper: AbstractTypeWrapper { + let type: T.Type + + var asString: String { + String(describing: type) + } + + static func == (lhs: GenericTypeWrapper, rhs: GenericTypeWrapper) -> Bool { + lhs.type == rhs.type + } + + func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(type)) + } +} + +extension GenericTypeWrapper: Codable { + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + let typeName = self.asString + try container.encode(typeName) + } + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let typeName = try container.decode(String.self) + + guard let statType = typeName.asTFClassFromString as? T.Type else { + Logger.log("Error at decoding StatisticType", Self.self) + + let context = DecodingError.Context(codingPath: container.codingPath, + debugDescription: "Cannot decode \(typeName) as Statistic.Type") + + throw DecodingError.typeMismatch(T.Type.self, context) + } + + self.type = statType + } +} + +protocol TypeRepresentable: AnyObject { + static var asType: GenericTypeWrapper { get } +} + +extension TypeRepresentable { + static var asType: GenericTypeWrapper { + GenericTypeWrapper(type: Self.self) + } +} diff --git a/TowerForge/TowerForge/Metrics/Achievements/AchievementTypeWrapper.swift b/TowerForge/TowerForge/Metrics/Achievements/AchievementTypeWrapper.swift index d8fb44f4..08f006b8 100644 --- a/TowerForge/TowerForge/Metrics/Achievements/AchievementTypeWrapper.swift +++ b/TowerForge/TowerForge/Metrics/Achievements/AchievementTypeWrapper.swift @@ -7,7 +7,7 @@ import Foundation -struct AchievementTypeWrapper: Equatable, Hashable { +struct AchievementTypeWrapper: Equatable, Hashable, Comparable { let type: Achievement.Type var asString: String { @@ -21,4 +21,12 @@ struct AchievementTypeWrapper: Equatable, Hashable { func hash(into hasher: inout Hasher) { hasher.combine(ObjectIdentifier(type)) } + + static func < (lhs: AchievementTypeWrapper, rhs: AchievementTypeWrapper) -> Bool { + lhs.asString < rhs.asString + } + + static func > (lhs: AchievementTypeWrapper, rhs: AchievementTypeWrapper) -> Bool { + lhs.asString > rhs.asString + } } diff --git a/TowerForge/TowerForge/Metrics/Achievements/AchievementsEngine.swift b/TowerForge/TowerForge/Metrics/Achievements/AchievementsEngine.swift index 62534cb0..1cd2d421 100644 --- a/TowerForge/TowerForge/Metrics/Achievements/AchievementsEngine.swift +++ b/TowerForge/TowerForge/Metrics/Achievements/AchievementsEngine.swift @@ -15,6 +15,7 @@ import Foundation class AchievementsEngine: InferenceEngine, InferenceDataDelegate { unowned var statisticsEngine: StatisticsEngine var achievementsDatabase: AchievementsDatabase + var statisticsDatabase: StatisticsDatabase { statisticsEngine.statistics } diff --git a/TowerForge/TowerForge/Metrics/Achievements/AchievementsFactory.swift b/TowerForge/TowerForge/Metrics/Achievements/AchievementsFactory.swift index 94d1b534..74a97085 100644 --- a/TowerForge/TowerForge/Metrics/Achievements/AchievementsFactory.swift +++ b/TowerForge/TowerForge/Metrics/Achievements/AchievementsFactory.swift @@ -24,6 +24,14 @@ class AchievementsFactory { let achievementsDatabase = AchievementsDatabase() achievementsDatabase.achievementsDataDelegate = data availableAchievementTypes.values.forEach { achievementsDatabase.addAchievement(for: $0.asType) } + Logger.log("Default achievements retrieved with \(achievementsDatabase.achievements.values.count)", self) + return achievementsDatabase + } + + static func getEmptyAchievementsDatabase() -> AchievementsDatabase { + let achievementsDatabase = AchievementsDatabase() + availableAchievementTypes.values.forEach { achievementsDatabase.addAchievement(for: $0.asType) } + Logger.log("Empty achievements retrieved with \(achievementsDatabase.achievements.values.count)", self) return achievementsDatabase } diff --git a/TowerForge/TowerForge/Metrics/Statistics/Statistic.swift b/TowerForge/TowerForge/Metrics/Statistics/Statistic.swift index 4cf39042..04b4f338 100644 --- a/TowerForge/TowerForge/Metrics/Statistics/Statistic.swift +++ b/TowerForge/TowerForge/Metrics/Statistics/Statistic.swift @@ -70,12 +70,12 @@ extension Statistic { StatisticTypeWrapper(type: Self.self) } - static var expMultiplier: Double { 0.0 } - var statisticName: StatisticTypeWrapper { StatisticTypeWrapper(type: Self.self) } + static var expMultiplier: Double { 0.0 } + var rankValue: Double { permanentValue * Self.expMultiplier } diff --git a/TowerForge/TowerForge/Metrics/Statistics/StatisticTypeWrapper.swift b/TowerForge/TowerForge/Metrics/Statistics/StatisticTypeWrapper.swift index 39516c3c..9e03c106 100644 --- a/TowerForge/TowerForge/Metrics/Statistics/StatisticTypeWrapper.swift +++ b/TowerForge/TowerForge/Metrics/Statistics/StatisticTypeWrapper.swift @@ -29,7 +29,6 @@ struct StatisticTypeWrapper: Equatable, Hashable { /// This extension allows the wrapped type to be written to and read from file extension StatisticTypeWrapper: Codable { - func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() let typeName = self.asString @@ -52,3 +51,5 @@ extension StatisticTypeWrapper: Codable { self.type = statType } } + +// typealias StatisticTypeWrapper = GenericTypeWrapper diff --git a/TowerForge/TowerForge/Metrics/Statistics/StatisticsDatabase+Codable.swift b/TowerForge/TowerForge/Metrics/Statistics/StatisticsDatabase+Codable.swift index 3aae3837..3cc8ae1d 100644 --- a/TowerForge/TowerForge/Metrics/Statistics/StatisticsDatabase+Codable.swift +++ b/TowerForge/TowerForge/Metrics/Statistics/StatisticsDatabase+Codable.swift @@ -9,7 +9,7 @@ import Foundation /// This extension adds encoding and decoding functionality to /// the Statistics Database to allow for storing and loading from file. -extension StatisticsDatabase: Codable { +extension StatisticsDatabase { private static func generateStatisticsCollection(_ statsArray: [Statistic]) -> [StatisticTypeWrapper: Statistic] { var statisticsMap: [StatisticTypeWrapper: Statistic] = [:] @@ -46,23 +46,6 @@ extension StatisticsDatabase: Codable { self.init(statObjectsMap) } - /*convenience init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: StatisticsDatabaseCodingKeys.self) - var statistics = [StatisticTypeWrapper: Statistic]() - let statsContainer = try container.nestedContainer(keyedBy: DynamicCodingKeys.self, forKey: .statistics) - - for key in statsContainer.allKeys { - let statDecoder = try statsContainer.superDecoder(forKey: key) - if let statistic = try StatisticsFactory.statisticDecoder[key.stringValue]?(statDecoder) { - if let statType = key.stringValue.asTFClassFromString as? Statistic.Type { - statistics[statType.asType] = statistic - } - } - } - - self.init(statistics) - }*/ - private static func decodeObject(_ statObjectDict: KeyedDecodingContainer) throws -> (any Statistic)? { diff --git a/TowerForge/TowerForge/Metrics/Statistics/StatisticsDatabase.swift b/TowerForge/TowerForge/Metrics/Statistics/StatisticsDatabase.swift index c6c156cc..88c2f7f1 100644 --- a/TowerForge/TowerForge/Metrics/Statistics/StatisticsDatabase.swift +++ b/TowerForge/TowerForge/Metrics/Statistics/StatisticsDatabase.swift @@ -8,7 +8,7 @@ import Foundation import FirebaseDatabaseInternal -final class StatisticsDatabase { +final class StatisticsDatabase: StorageDatabase { var statistics: [StatisticTypeWrapper: Statistic] = [:] init(_ stats: [StatisticTypeWrapper: Statistic] = [:]) { diff --git a/TowerForge/TowerForge/Storage/StorageDatabase.swift b/TowerForge/TowerForge/Storage/StorageDatabase.swift new file mode 100644 index 00000000..4c1e13e0 --- /dev/null +++ b/TowerForge/TowerForge/Storage/StorageDatabase.swift @@ -0,0 +1,13 @@ +// +// Storage.swift +// TowerForge +// +// Created by Rubesh on 18/4/24. +// + +import Foundation + +protocol StorageDatabase: Codable { + func encode(to encoder: Encoder) throws + init(from decoder: Decoder) throws +} diff --git a/TowerForge/TowerForge/ViewControllers/MainMenuViewController.swift b/TowerForge/TowerForge/ViewControllers/MainMenuViewController.swift index 58acddf0..bc69047b 100644 --- a/TowerForge/TowerForge/ViewControllers/MainMenuViewController.swift +++ b/TowerForge/TowerForge/ViewControllers/MainMenuViewController.swift @@ -14,4 +14,16 @@ class MainMenuViewController: UIViewController { self.navigationController?.isNavigationBarHidden = false AudioManager.shared.playMainMusic() } + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + if segue.identifier == "showPlayerStats" { + // Get the new view controller using segue.destination. + // Pass the selected object to the new view controller. + if let playerStatsVC = segue.destination as? PlayerStatsViewController { + // Set any properties of playerStatsVC here + // For example, pass an AchievementsDatabase instance if needed + } + } + } + } diff --git a/TowerForge/TowerForge/ViewControllers/PlayerStatsViewController.swift b/TowerForge/TowerForge/ViewControllers/PlayerStatsViewController.swift new file mode 100644 index 00000000..dc1da268 --- /dev/null +++ b/TowerForge/TowerForge/ViewControllers/PlayerStatsViewController.swift @@ -0,0 +1,76 @@ +// +// PlayerStatsViewController.swift +// TowerForge +// +// Created by Rubesh on 18/4/24. +// + +import Foundation +import UIKit + +class PlayerStatsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { + + @IBOutlet private var achievementsView: UITableView! + + var achievements: AchievementsDatabase = getAchievements() + + static func getAchievements() -> AchievementsDatabase { + let statsEngine = StatisticsEngine() + let achEngine = statsEngine.inferenceEngines[AchievementsEngine.asType] as? AchievementsEngine + + Logger.log("Count is \(String(describing: achEngine?.achievementsDatabase.achievements.values.count))", self) + + achEngine!.achievementsDatabase.achievements = AchievementsFactory + .getDefaultAchievementsDatabase(achEngine).achievements + + return achEngine!.achievementsDatabase + } + + override func viewDidLoad() { + super.viewDidLoad() + + achievementsView.delegate = self + achievementsView.dataSource = self + reloadAchievements() + } + + // MARK: - Table view data source + + func numberOfSections(in tableView: UITableView) -> Int { + 1 + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + achievements.achievements.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) + + // Sort achievements if necessary and configure the cell + let achievementPair = Array(achievements.achievements).sorted(by: { $0.key < $1.key })[indexPath.row] + let achievement = achievementPair.value + + cell.textLabel?.text = achievement.name + + return cell + } + + func reloadAchievements() { + achievementsView.reloadData() + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + // Deselect the row with animation + tableView.deselectRow(at: indexPath, animated: true) + + // Get the selected achievement + let achievementPair = Array(achievements.achievements).sorted(by: { $0.key < $1.key })[indexPath.row] + let achievement = achievementPair.value + + // Show a detail view controller + // TODO: need to implement the logic for showing the details + // showAchievementDetails(achievement) + } + +} From e77af92abdd8b00436987d7005e663d7fb59c14c Mon Sep 17 00:00:00 2001 From: Rubesh Date: Fri, 19 Apr 2024 04:18:11 +0800 Subject: [PATCH 03/14] Add working AchievementsUI --- .../TowerForge.xcodeproj/project.pbxproj | 4 + .../Storyboards/Base.lproj/Main.storyboard | 77 ++++++++++++++++--- .../Achievements/AchievementsDatabase.swift | 10 ++- .../Achievements/AchievementsFactory.swift | 4 +- .../Metrics/Goals/AbstractGoal.swift | 5 +- .../TowerForge/Metrics/Ranking/Rank.swift | 13 ++++ .../CustomAchievementCell.swift | 26 +++++++ .../PlayerStatsViewController.swift | 50 +++++++++--- 8 files changed, 165 insertions(+), 24 deletions(-) create mode 100644 TowerForge/TowerForge/ViewControllers/CustomAchievementCell.swift diff --git a/TowerForge/TowerForge.xcodeproj/project.pbxproj b/TowerForge/TowerForge.xcodeproj/project.pbxproj index 11714e87..fc5a6e29 100644 --- a/TowerForge/TowerForge.xcodeproj/project.pbxproj +++ b/TowerForge/TowerForge.xcodeproj/project.pbxproj @@ -241,6 +241,7 @@ BAEC99FA2BD13F2600E0C437 /* AbstractTypeWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAEC99F92BD13F2600E0C437 /* AbstractTypeWrapper.swift */; }; BAEC99FC2BD15AAB00E0C437 /* StorageDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAEC99FB2BD15AAB00E0C437 /* StorageDatabase.swift */; }; BAEC99FE2BD15E0200E0C437 /* PlayerStatsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAEC99FD2BD15E0200E0C437 /* PlayerStatsViewController.swift */; }; + BAEC9A002BD1A4B700E0C437 /* CustomAchievementCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAEC99FF2BD1A4B700E0C437 /* CustomAchievementCell.swift */; }; BAFFB9452BB0A8C800D8301F /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAFFB9442BB0A8C800D8301F /* Constants.swift */; }; BAFFB9492BB0ABC400D8301F /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAFFB9482BB0ABC400D8301F /* Logger.swift */; }; BAFFB94B2BB11F9800D8301F /* GameEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAFFB94A2BB11F9800D8301F /* GameEngine.swift */; }; @@ -508,6 +509,7 @@ BAEC99F92BD13F2600E0C437 /* AbstractTypeWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbstractTypeWrapper.swift; sourceTree = ""; }; BAEC99FB2BD15AAB00E0C437 /* StorageDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageDatabase.swift; sourceTree = ""; }; BAEC99FD2BD15E0200E0C437 /* PlayerStatsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerStatsViewController.swift; sourceTree = ""; }; + BAEC99FF2BD1A4B700E0C437 /* CustomAchievementCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomAchievementCell.swift; sourceTree = ""; }; BAFFB9442BB0A8C800D8301F /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; BAFFB9482BB0ABC400D8301F /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; BAFFB94A2BB11F9800D8301F /* GameEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameEngine.swift; sourceTree = ""; }; @@ -781,6 +783,7 @@ 523E5C542BC63A16007444DA /* LeaderboardViewController.swift */, 52F930E62BC63F7F003D11B5 /* LeaderboardSelectionViewController.swift */, BAEC99FD2BD15E0200E0C437 /* PlayerStatsViewController.swift */, + BAEC99FF2BD1A4B700E0C437 /* CustomAchievementCell.swift */, ); path = ViewControllers; sourceTree = ""; @@ -1612,6 +1615,7 @@ BA82C7672BCBCB00000515A0 /* StatisticsDatabase+Codable.swift in Sources */, 3CD37AA52BBEC10700222D8A /* FirebaseRemoteEventSubscriber.swift in Sources */, BA82C7792BCC6943000515A0 /* CenturionAchievement.swift in Sources */, + BAEC9A002BD1A4B700E0C437 /* CustomAchievementCell.swift in Sources */, 52A794172BBC4F690083C976 /* GamePlayer.swift in Sources */, 3CBE72FD2BC8D64F00CC446A /* RemoteMoveEvent.swift in Sources */, 3CE951632BAE037C008B2785 /* AiSystem.swift in Sources */, diff --git a/TowerForge/TowerForge/AppMain/Storyboards/Base.lproj/Main.storyboard b/TowerForge/TowerForge/AppMain/Storyboards/Base.lproj/Main.storyboard index 36396517..70442453 100644 --- a/TowerForge/TowerForge/AppMain/Storyboards/Base.lproj/Main.storyboard +++ b/TowerForge/TowerForge/AppMain/Storyboards/Base.lproj/Main.storyboard @@ -151,27 +151,86 @@ - + - - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + + + @@ -181,7 +240,7 @@ - + diff --git a/TowerForge/TowerForge/Metrics/Achievements/AchievementsDatabase.swift b/TowerForge/TowerForge/Metrics/Achievements/AchievementsDatabase.swift index b5b90946..54669b8e 100644 --- a/TowerForge/TowerForge/Metrics/Achievements/AchievementsDatabase.swift +++ b/TowerForge/TowerForge/Metrics/Achievements/AchievementsDatabase.swift @@ -9,7 +9,15 @@ import Foundation class AchievementsDatabase { weak var achievementsDataDelegate: InferenceDataDelegate? - var achievements: [AchievementTypeWrapper: Achievement] = [:] + private var achievements: [AchievementTypeWrapper: Achievement] = [:] + + var count: Int { + achievements.count + } + + var asSortedArray: [Dictionary.Element] { + Array(achievements).sorted(by: { $0.key < $1.key }) + } init(achievements: [AchievementTypeWrapper: Achievement] = [:]) { self.achievements = achievements diff --git a/TowerForge/TowerForge/Metrics/Achievements/AchievementsFactory.swift b/TowerForge/TowerForge/Metrics/Achievements/AchievementsFactory.swift index 74a97085..1ce2b3cb 100644 --- a/TowerForge/TowerForge/Metrics/Achievements/AchievementsFactory.swift +++ b/TowerForge/TowerForge/Metrics/Achievements/AchievementsFactory.swift @@ -24,14 +24,14 @@ class AchievementsFactory { let achievementsDatabase = AchievementsDatabase() achievementsDatabase.achievementsDataDelegate = data availableAchievementTypes.values.forEach { achievementsDatabase.addAchievement(for: $0.asType) } - Logger.log("Default achievements retrieved with \(achievementsDatabase.achievements.values.count)", self) + Logger.log("Default achievements retrieved with \(achievementsDatabase.count)", self) return achievementsDatabase } static func getEmptyAchievementsDatabase() -> AchievementsDatabase { let achievementsDatabase = AchievementsDatabase() availableAchievementTypes.values.forEach { achievementsDatabase.addAchievement(for: $0.asType) } - Logger.log("Empty achievements retrieved with \(achievementsDatabase.achievements.values.count)", self) + Logger.log("Empty achievements retrieved with \(achievementsDatabase.count)", self) return achievementsDatabase } diff --git a/TowerForge/TowerForge/Metrics/Goals/AbstractGoal.swift b/TowerForge/TowerForge/Metrics/Goals/AbstractGoal.swift index 438407fe..165c2b9d 100644 --- a/TowerForge/TowerForge/Metrics/Goals/AbstractGoal.swift +++ b/TowerForge/TowerForge/Metrics/Goals/AbstractGoal.swift @@ -15,6 +15,7 @@ protocol AbstractGoal: AnyObject { static var goalType: AbstractGoalTypeWrapper { get } var name: String { get } var description: String { get } + var imageIdentifier: String { get } static var definedParameters: [StatisticTypeWrapper: Double] { get } var currentParameters: [StatisticTypeWrapper: Statistic] { get set } @@ -35,6 +36,8 @@ protocol AbstractGoal: AnyObject { extension AbstractGoal { + var imageIdentifier: String { "coin" } + static var goalType: AbstractGoalTypeWrapper { AbstractGoalTypeWrapper(type: Self.self) } @@ -70,7 +73,7 @@ extension AbstractGoal { } var overallProgressRateRounded: Double { - overallProgressRate.rounded() + overallProgressRate.rounded() > 1.0 ? 1.0 : overallProgressRate.rounded() } var isComplete: Bool { diff --git a/TowerForge/TowerForge/Metrics/Ranking/Rank.swift b/TowerForge/TowerForge/Metrics/Ranking/Rank.swift index 9c9e8674..9881c5d8 100644 --- a/TowerForge/TowerForge/Metrics/Ranking/Rank.swift +++ b/TowerForge/TowerForge/Metrics/Ranking/Rank.swift @@ -29,4 +29,17 @@ enum Rank: String, CaseIterable { case .GENERAL: return 8_001..<9_001 } } + + var imageIdentifer: String { + switch self { + case .PRIVATE: return "private" + case .CORPORAL: return "corporal" + case .SERGEANT: return "sergeant" + case .LIEUTENANT: return "lieutenant" + case .CAPTAIN: return "captain" + case .MAJOR: return "major" + case .COLONEL: return "colonel" + case .GENERAL: return "general" + } + } } diff --git a/TowerForge/TowerForge/ViewControllers/CustomAchievementCell.swift b/TowerForge/TowerForge/ViewControllers/CustomAchievementCell.swift new file mode 100644 index 00000000..7cc5fc94 --- /dev/null +++ b/TowerForge/TowerForge/ViewControllers/CustomAchievementCell.swift @@ -0,0 +1,26 @@ +// +// CustomAchievementCell.swift +// TowerForge +// +// Created by Rubesh on 19/4/24. +// + +import Foundation +import UIKit + +class CustomAchievementCell: UITableViewCell { + @IBOutlet var achievementImageView: UIImageView! + @IBOutlet var nameLabel: UILabel! + @IBOutlet var descriptionLabel: UILabel! + @IBOutlet var progressView: UIProgressView! + @IBOutlet var statusImageView: UIImageView! +} + +class CustomMissionCell: UITableViewCell { + @IBOutlet var missionImageView: UIImageView! + @IBOutlet var missionNameLabel: UILabel! + @IBOutlet var descriptionLabel: UILabel! + @IBOutlet var statusImageView: UIImageView! +} + + diff --git a/TowerForge/TowerForge/ViewControllers/PlayerStatsViewController.swift b/TowerForge/TowerForge/ViewControllers/PlayerStatsViewController.swift index dc1da268..7e0f3e10 100644 --- a/TowerForge/TowerForge/ViewControllers/PlayerStatsViewController.swift +++ b/TowerForge/TowerForge/ViewControllers/PlayerStatsViewController.swift @@ -11,20 +11,24 @@ import UIKit class PlayerStatsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { @IBOutlet private var achievementsView: UITableView! + @IBOutlet private var rankView: UILabel! var achievements: AchievementsDatabase = getAchievements() + var rank: Rank = getRank() + static func getAchievements() -> AchievementsDatabase { let statsEngine = StatisticsEngine() let achEngine = statsEngine.inferenceEngines[AchievementsEngine.asType] as? AchievementsEngine - - Logger.log("Count is \(String(describing: achEngine?.achievementsDatabase.achievements.values.count))", self) - - achEngine!.achievementsDatabase.achievements = AchievementsFactory - .getDefaultAchievementsDatabase(achEngine).achievements - + achEngine!.achievementsDatabase.setToDefault() return achEngine!.achievementsDatabase } + + static func getRank() -> Rank { + let statsEngine = StatisticsEngine() + let rankEngine = statsEngine.inferenceEngines[RankingEngine.asType] as? RankingEngine + return rankEngine!.currentRank + } override func viewDidLoad() { super.viewDidLoad() @@ -41,18 +45,42 @@ class PlayerStatsViewController: UIViewController, UITableViewDataSource, UITabl } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - achievements.achievements.count + achievements.count } - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + /*func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) // Sort achievements if necessary and configure the cell - let achievementPair = Array(achievements.achievements).sorted(by: { $0.key < $1.key })[indexPath.row] + let achievementPair = achievements.asSortedArray[indexPath.row] let achievement = achievementPair.value - + cell.textLabel?.text = achievement.name + let permanentValue = achievement.currentProgressRates + cell.detailTextLabel?.text = String(describing: permanentValue) + + return cell + }*/ + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell(withIdentifier: "cell", + for: indexPath) as? CustomAchievementCell else { + fatalError("Could not dequeue CustomAchievementCell") + } + + let achievementPair = achievements.asSortedArray[indexPath.row] + let achievement = achievementPair.value + + // Configure the cell elements + cell.nameLabel.text = achievement.name + cell.descriptionLabel.text = achievement.description + cell.achievementImageView.image = UIImage(named: achievement.imageIdentifier) + cell.progressView.progress = Float(achievement.overallProgressRateRounded) + //cell.progressPercentage.text = String(describing: Float(achievement.overallProgressRateRounded)) + let statusImageName = achievement.isComplete ? "checkmark.circle" : "x.circle" + cell.statusImageView.image = UIImage(systemName: statusImageName) + return cell } @@ -65,7 +93,7 @@ class PlayerStatsViewController: UIViewController, UITableViewDataSource, UITabl tableView.deselectRow(at: indexPath, animated: true) // Get the selected achievement - let achievementPair = Array(achievements.achievements).sorted(by: { $0.key < $1.key })[indexPath.row] + let achievementPair = achievements.asSortedArray[indexPath.row] let achievement = achievementPair.value // Show a detail view controller From 950d28d91c987f18d873a7931b8783000b21e7c6 Mon Sep 17 00:00:00 2001 From: Rubesh Date: Fri, 19 Apr 2024 05:07:06 +0800 Subject: [PATCH 04/14] Add some basic rank functionality --- .../TowerForge.xcodeproj/project.pbxproj | 14 ++++- .../Storyboards/Base.lproj/Main.storyboard | 39 +++++++++++++ .../TowerForge/Metrics/Ranking/Rank.swift | 2 +- .../PlayerStatsViewController.swift | 23 ++++++-- .../CustomAchievementCell.swift | 2 - TowerForge/TowerForge/Views/RankView.swift | 56 +++++++++++++++++++ 6 files changed, 128 insertions(+), 8 deletions(-) rename TowerForge/TowerForge/{ViewControllers => Views}/CustomAchievementCell.swift (99%) create mode 100644 TowerForge/TowerForge/Views/RankView.swift diff --git a/TowerForge/TowerForge.xcodeproj/project.pbxproj b/TowerForge/TowerForge.xcodeproj/project.pbxproj index fc5a6e29..25ebab8a 100644 --- a/TowerForge/TowerForge.xcodeproj/project.pbxproj +++ b/TowerForge/TowerForge.xcodeproj/project.pbxproj @@ -191,6 +191,7 @@ BA2F5AC92BC81BDB00CBD8E9 /* StatisticsEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA2F5AC82BC81BDB00CBD8E9 /* StatisticsEngine.swift */; }; BA2F5ACB2BC82CCC00CBD8E9 /* EventStatisticLinkDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA2F5ACA2BC82CCC00CBD8E9 /* EventStatisticLinkDatabase.swift */; }; BA2F5ACD2BC8313900CBD8E9 /* StatisticUpdateLinkDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA2F5ACC2BC8313900CBD8E9 /* StatisticUpdateLinkDatabase.swift */; }; + BA436ADD2BD1BC3300BE3E4F /* RankView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA436ADC2BD1BC3300BE3E4F /* RankView.swift */; }; BA443D3D2BAD9557009F0FFB /* RemoveSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA443D3C2BAD9557009F0FFB /* RemoveSystem.swift */; }; BA443D3F2BAD9774009F0FFB /* RemoveEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA443D3E2BAD9774009F0FFB /* RemoveEvent.swift */; }; BA443D422BAD9885009F0FFB /* DamageEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA443D412BAD9885009F0FFB /* DamageEventTests.swift */; }; @@ -458,6 +459,7 @@ BA2F5AC82BC81BDB00CBD8E9 /* StatisticsEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticsEngine.swift; sourceTree = ""; }; BA2F5ACA2BC82CCC00CBD8E9 /* EventStatisticLinkDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventStatisticLinkDatabase.swift; sourceTree = ""; }; BA2F5ACC2BC8313900CBD8E9 /* StatisticUpdateLinkDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticUpdateLinkDatabase.swift; sourceTree = ""; }; + BA436ADC2BD1BC3300BE3E4F /* RankView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RankView.swift; sourceTree = ""; }; BA443D3C2BAD9557009F0FFB /* RemoveSystem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveSystem.swift; sourceTree = ""; }; BA443D3E2BAD9774009F0FFB /* RemoveEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveEvent.swift; sourceTree = ""; }; BA443D412BAD9885009F0FFB /* DamageEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamageEventTests.swift; sourceTree = ""; }; @@ -783,7 +785,6 @@ 523E5C542BC63A16007444DA /* LeaderboardViewController.swift */, 52F930E62BC63F7F003D11B5 /* LeaderboardSelectionViewController.swift */, BAEC99FD2BD15E0200E0C437 /* PlayerStatsViewController.swift */, - BAEC99FF2BD1A4B700E0C437 /* CustomAchievementCell.swift */, ); path = ViewControllers; sourceTree = ""; @@ -891,6 +892,7 @@ 5295A2052BAA0208005018A8 /* Nodes */, BAFFB9462BB0AABF00D8301F /* TFCore */, 5295A2002BA9FB97005018A8 /* Scenes */, + BA436ADB2BD1BC1F00BE3E4F /* Views */, 5295A2082BAAE14B005018A8 /* ViewControllers */, 52DF5FDB2BA32CEF00135367 /* LevelModule */, BAFFB9332BB0A24400D8301F /* GameModule */, @@ -1006,6 +1008,15 @@ path = Metrics; sourceTree = ""; }; + BA436ADB2BD1BC1F00BE3E4F /* Views */ = { + isa = PBXGroup; + children = ( + BAEC99FF2BD1A4B700E0C437 /* CustomAchievementCell.swift */, + BA436ADC2BD1BC3300BE3E4F /* RankView.swift */, + ); + path = Views; + sourceTree = ""; + }; BA443D402BAD9872009F0FFB /* EventTests */ = { isa = PBXGroup; children = ( @@ -1756,6 +1767,7 @@ 527A07742BB3D8E800CD9D08 /* GameModeFactory.swift in Sources */, 5240D0AF2BB3B415004F1486 /* LifeEvent.swift in Sources */, 5295A2072BAA02FD005018A8 /* TFButtonNode.swift in Sources */, + BA436ADD2BD1BC3300BE3E4F /* RankView.swift in Sources */, BAFFB96A2BB9A64000D8301F /* ObjectSet.swift in Sources */, BA82C7902BCD2FAF000515A0 /* TotalDamageDealtStatistic.swift in Sources */, 3CD37AA32BBEC0F900222D8A /* FirebaseRemoteEventPublisher.swift in Sources */, diff --git a/TowerForge/TowerForge/AppMain/Storyboards/Base.lproj/Main.storyboard b/TowerForge/TowerForge/AppMain/Storyboards/Base.lproj/Main.storyboard index 70442453..353c1786 100644 --- a/TowerForge/TowerForge/AppMain/Storyboards/Base.lproj/Main.storyboard +++ b/TowerForge/TowerForge/AppMain/Storyboards/Base.lproj/Main.storyboard @@ -221,21 +221,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/TowerForge/TowerForge/Metrics/Ranking/Rank.swift b/TowerForge/TowerForge/Metrics/Ranking/Rank.swift index 9881c5d8..4d801027 100644 --- a/TowerForge/TowerForge/Metrics/Ranking/Rank.swift +++ b/TowerForge/TowerForge/Metrics/Ranking/Rank.swift @@ -29,7 +29,7 @@ enum Rank: String, CaseIterable { case .GENERAL: return 8_001..<9_001 } } - + var imageIdentifer: String { switch self { case .PRIVATE: return "private" diff --git a/TowerForge/TowerForge/ViewControllers/PlayerStatsViewController.swift b/TowerForge/TowerForge/ViewControllers/PlayerStatsViewController.swift index 7e0f3e10..c7e02ce0 100644 --- a/TowerForge/TowerForge/ViewControllers/PlayerStatsViewController.swift +++ b/TowerForge/TowerForge/ViewControllers/PlayerStatsViewController.swift @@ -8,14 +8,26 @@ import Foundation import UIKit +class RankUIView: UIView { + @IBOutlet var rankImageView: UIImageView! + @IBOutlet var rankNameLabel: UILabel! + + // You can add methods here to configure the view + func configure(with rank: Rank) { + rankImageView = UIImageView(image: UIImage(named: rank.imageIdentifer)) + var rankLabel = UILabel() + rankLabel.text = rank.rawValue + rankNameLabel = rankLabel + } +} + class PlayerStatsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { @IBOutlet private var achievementsView: UITableView! - @IBOutlet private var rankView: UILabel! + @IBOutlet var rankingView: RankUIView! var achievements: AchievementsDatabase = getAchievements() var rank: Rank = getRank() - static func getAchievements() -> AchievementsDatabase { let statsEngine = StatisticsEngine() @@ -23,7 +35,7 @@ class PlayerStatsViewController: UIViewController, UITableViewDataSource, UITabl achEngine!.achievementsDatabase.setToDefault() return achEngine!.achievementsDatabase } - + static func getRank() -> Rank { let statsEngine = StatisticsEngine() let rankEngine = statsEngine.inferenceEngines[RankingEngine.asType] as? RankingEngine @@ -35,6 +47,9 @@ class PlayerStatsViewController: UIViewController, UITableViewDataSource, UITabl achievementsView.delegate = self achievementsView.dataSource = self + + let rank = Self.getRank() + rankingView.configure(with: rank) reloadAchievements() } @@ -77,7 +92,7 @@ class PlayerStatsViewController: UIViewController, UITableViewDataSource, UITabl cell.descriptionLabel.text = achievement.description cell.achievementImageView.image = UIImage(named: achievement.imageIdentifier) cell.progressView.progress = Float(achievement.overallProgressRateRounded) - //cell.progressPercentage.text = String(describing: Float(achievement.overallProgressRateRounded)) + // cell.progressPercentage.text = String(describing: Float(achievement.overallProgressRateRounded)) let statusImageName = achievement.isComplete ? "checkmark.circle" : "x.circle" cell.statusImageView.image = UIImage(systemName: statusImageName) diff --git a/TowerForge/TowerForge/ViewControllers/CustomAchievementCell.swift b/TowerForge/TowerForge/Views/CustomAchievementCell.swift similarity index 99% rename from TowerForge/TowerForge/ViewControllers/CustomAchievementCell.swift rename to TowerForge/TowerForge/Views/CustomAchievementCell.swift index 7cc5fc94..bb5ccc52 100644 --- a/TowerForge/TowerForge/ViewControllers/CustomAchievementCell.swift +++ b/TowerForge/TowerForge/Views/CustomAchievementCell.swift @@ -22,5 +22,3 @@ class CustomMissionCell: UITableViewCell { @IBOutlet var descriptionLabel: UILabel! @IBOutlet var statusImageView: UIImageView! } - - diff --git a/TowerForge/TowerForge/Views/RankView.swift b/TowerForge/TowerForge/Views/RankView.swift new file mode 100644 index 00000000..cad59b49 --- /dev/null +++ b/TowerForge/TowerForge/Views/RankView.swift @@ -0,0 +1,56 @@ +// +// RankView.swift +// TowerForge +// +// Created by Rubesh on 19/4/24. +// + +import Foundation +import UIKit + +class RankView: UIView { + let imageView = UIImageView() + let textLabel = UILabel() + + override init(frame: CGRect) { + super.init(frame: frame) + commonInit() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + commonInit() + } + + private func commonInit() { + // Set up the image view + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.contentMode = .scaleAspectFit // Adjust as needed + addSubview(imageView) + + // Set up the text label + textLabel.translatesAutoresizingMaskIntoConstraints = false + textLabel.numberOfLines = 0 // Wrap text as needed + addSubview(textLabel) + + // Add constraints for layout + NSLayoutConstraint.activate([ + imageView.leadingAnchor.constraint(equalTo: leadingAnchor), + imageView.topAnchor.constraint(equalTo: topAnchor), + imageView.bottomAnchor.constraint(equalTo: bottomAnchor), + imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor), // Makes the image view square + + textLabel.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 8), + textLabel.trailingAnchor.constraint(equalTo: trailingAnchor), + textLabel.topAnchor.constraint(equalTo: topAnchor), + textLabel.bottomAnchor.constraint(equalTo: bottomAnchor) + ]) + } + + func configure(with rank: Rank) { + imageView.image = UIImage(named: rank.imageIdentifer) + ?? UIImage(systemName: "checkmark.circle") + + textLabel.text = rank.rawValue + } +} From 2bd4cd901a27349a64cbc74b50e8686779d7320c Mon Sep 17 00:00:00 2001 From: Rubesh Date: Fri, 19 Apr 2024 19:41:48 +0800 Subject: [PATCH 05/14] Update AbstractGaol to properly round stats --- .../Achievements/Implemented/HundredKillsAchievement.swift | 2 +- TowerForge/TowerForge/Metrics/Goals/AbstractGoal.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/TowerForge/TowerForge/Metrics/Achievements/Implemented/HundredKillsAchievement.swift b/TowerForge/TowerForge/Metrics/Achievements/Implemented/HundredKillsAchievement.swift index e83b1587..58e70b32 100644 --- a/TowerForge/TowerForge/Metrics/Achievements/Implemented/HundredKillsAchievement.swift +++ b/TowerForge/TowerForge/Metrics/Achievements/Implemented/HundredKillsAchievement.swift @@ -14,7 +14,7 @@ final class HundredKillsAchievement: Achievement { static var definedParameters: [StatisticTypeWrapper: Double] = [ - TotalKillsStatistic.asType: 100.0 + TotalKillsStatistic.asType: 200.0 ] init(dependentStatistics: [Statistic]) { diff --git a/TowerForge/TowerForge/Metrics/Goals/AbstractGoal.swift b/TowerForge/TowerForge/Metrics/Goals/AbstractGoal.swift index 165c2b9d..85894970 100644 --- a/TowerForge/TowerForge/Metrics/Goals/AbstractGoal.swift +++ b/TowerForge/TowerForge/Metrics/Goals/AbstractGoal.swift @@ -73,7 +73,7 @@ extension AbstractGoal { } var overallProgressRateRounded: Double { - overallProgressRate.rounded() > 1.0 ? 1.0 : overallProgressRate.rounded() + overallProgressRate > 1.0 ? 1.0 : overallProgressRate } var isComplete: Bool { From 18d7157325e36aef8d9b596b5abf214e76f992c7 Mon Sep 17 00:00:00 2001 From: Rubesh Date: Sat, 20 Apr 2024 18:09:23 +0800 Subject: [PATCH 06/14] Add rankview and player stats view --- .../TowerForge.xcodeproj/project.pbxproj | 4 -- .../Storyboards/Base.lproj/Main.storyboard | 54 +++++++----------- .../PlayerStatsViewController.swift | 38 +++---------- TowerForge/TowerForge/Views/RankView.swift | 56 ------------------- 4 files changed, 28 insertions(+), 124 deletions(-) delete mode 100644 TowerForge/TowerForge/Views/RankView.swift diff --git a/TowerForge/TowerForge.xcodeproj/project.pbxproj b/TowerForge/TowerForge.xcodeproj/project.pbxproj index 25ebab8a..e7b4e2c0 100644 --- a/TowerForge/TowerForge.xcodeproj/project.pbxproj +++ b/TowerForge/TowerForge.xcodeproj/project.pbxproj @@ -191,7 +191,6 @@ BA2F5AC92BC81BDB00CBD8E9 /* StatisticsEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA2F5AC82BC81BDB00CBD8E9 /* StatisticsEngine.swift */; }; BA2F5ACB2BC82CCC00CBD8E9 /* EventStatisticLinkDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA2F5ACA2BC82CCC00CBD8E9 /* EventStatisticLinkDatabase.swift */; }; BA2F5ACD2BC8313900CBD8E9 /* StatisticUpdateLinkDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA2F5ACC2BC8313900CBD8E9 /* StatisticUpdateLinkDatabase.swift */; }; - BA436ADD2BD1BC3300BE3E4F /* RankView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA436ADC2BD1BC3300BE3E4F /* RankView.swift */; }; BA443D3D2BAD9557009F0FFB /* RemoveSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA443D3C2BAD9557009F0FFB /* RemoveSystem.swift */; }; BA443D3F2BAD9774009F0FFB /* RemoveEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA443D3E2BAD9774009F0FFB /* RemoveEvent.swift */; }; BA443D422BAD9885009F0FFB /* DamageEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA443D412BAD9885009F0FFB /* DamageEventTests.swift */; }; @@ -459,7 +458,6 @@ BA2F5AC82BC81BDB00CBD8E9 /* StatisticsEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticsEngine.swift; sourceTree = ""; }; BA2F5ACA2BC82CCC00CBD8E9 /* EventStatisticLinkDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventStatisticLinkDatabase.swift; sourceTree = ""; }; BA2F5ACC2BC8313900CBD8E9 /* StatisticUpdateLinkDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticUpdateLinkDatabase.swift; sourceTree = ""; }; - BA436ADC2BD1BC3300BE3E4F /* RankView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RankView.swift; sourceTree = ""; }; BA443D3C2BAD9557009F0FFB /* RemoveSystem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveSystem.swift; sourceTree = ""; }; BA443D3E2BAD9774009F0FFB /* RemoveEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveEvent.swift; sourceTree = ""; }; BA443D412BAD9885009F0FFB /* DamageEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamageEventTests.swift; sourceTree = ""; }; @@ -1012,7 +1010,6 @@ isa = PBXGroup; children = ( BAEC99FF2BD1A4B700E0C437 /* CustomAchievementCell.swift */, - BA436ADC2BD1BC3300BE3E4F /* RankView.swift */, ); path = Views; sourceTree = ""; @@ -1767,7 +1764,6 @@ 527A07742BB3D8E800CD9D08 /* GameModeFactory.swift in Sources */, 5240D0AF2BB3B415004F1486 /* LifeEvent.swift in Sources */, 5295A2072BAA02FD005018A8 /* TFButtonNode.swift in Sources */, - BA436ADD2BD1BC3300BE3E4F /* RankView.swift in Sources */, BAFFB96A2BB9A64000D8301F /* ObjectSet.swift in Sources */, BA82C7902BCD2FAF000515A0 /* TotalDamageDealtStatistic.swift in Sources */, 3CD37AA32BBEC0F900222D8A /* FirebaseRemoteEventPublisher.swift in Sources */, diff --git a/TowerForge/TowerForge/AppMain/Storyboards/Base.lproj/Main.storyboard b/TowerForge/TowerForge/AppMain/Storyboards/Base.lproj/Main.storyboard index 353c1786..d2a8b29f 100644 --- a/TowerForge/TowerForge/AppMain/Storyboards/Base.lproj/Main.storyboard +++ b/TowerForge/TowerForge/AppMain/Storyboards/Base.lproj/Main.storyboard @@ -221,60 +221,48 @@ - - - - - - - - - - - - - - - - - - - - - + + + + - - + + + + + + - + + + - + - - - + + diff --git a/TowerForge/TowerForge/ViewControllers/PlayerStatsViewController.swift b/TowerForge/TowerForge/ViewControllers/PlayerStatsViewController.swift index c7e02ce0..2e2c6a3f 100644 --- a/TowerForge/TowerForge/ViewControllers/PlayerStatsViewController.swift +++ b/TowerForge/TowerForge/ViewControllers/PlayerStatsViewController.swift @@ -8,23 +8,11 @@ import Foundation import UIKit -class RankUIView: UIView { - @IBOutlet var rankImageView: UIImageView! - @IBOutlet var rankNameLabel: UILabel! - - // You can add methods here to configure the view - func configure(with rank: Rank) { - rankImageView = UIImageView(image: UIImage(named: rank.imageIdentifer)) - var rankLabel = UILabel() - rankLabel.text = rank.rawValue - rankNameLabel = rankLabel - } -} - class PlayerStatsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { @IBOutlet private var achievementsView: UITableView! - @IBOutlet var rankingView: RankUIView! + @IBOutlet private var rankNameLabel: UILabel! + @IBOutlet private var characterImage: UIImageView! var achievements: AchievementsDatabase = getAchievements() var rank: Rank = getRank() @@ -47,9 +35,11 @@ class PlayerStatsViewController: UIViewController, UITableViewDataSource, UITabl achievementsView.delegate = self achievementsView.dataSource = self + self.rank = Self.getRank() + // rankImageView.image = UIImage(named: currentRank.imageIdentifer) + rankNameLabel.text = String("--- Rank: \(rank.rawValue) ---") - let rank = Self.getRank() - rankingView.configure(with: rank) + characterImage.image = UIImage(named: "melee-1") reloadAchievements() } @@ -63,21 +53,6 @@ class PlayerStatsViewController: UIViewController, UITableViewDataSource, UITabl achievements.count } - /*func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) - - // Sort achievements if necessary and configure the cell - let achievementPair = achievements.asSortedArray[indexPath.row] - let achievement = achievementPair.value - - cell.textLabel?.text = achievement.name - - let permanentValue = achievement.currentProgressRates - cell.detailTextLabel?.text = String(describing: permanentValue) - - return cell - }*/ - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as? CustomAchievementCell else { @@ -101,6 +76,7 @@ class PlayerStatsViewController: UIViewController, UITableViewDataSource, UITabl func reloadAchievements() { achievementsView.reloadData() + rankNameLabel.reloadInputViews() } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { diff --git a/TowerForge/TowerForge/Views/RankView.swift b/TowerForge/TowerForge/Views/RankView.swift deleted file mode 100644 index cad59b49..00000000 --- a/TowerForge/TowerForge/Views/RankView.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// RankView.swift -// TowerForge -// -// Created by Rubesh on 19/4/24. -// - -import Foundation -import UIKit - -class RankView: UIView { - let imageView = UIImageView() - let textLabel = UILabel() - - override init(frame: CGRect) { - super.init(frame: frame) - commonInit() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - commonInit() - } - - private func commonInit() { - // Set up the image view - imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.contentMode = .scaleAspectFit // Adjust as needed - addSubview(imageView) - - // Set up the text label - textLabel.translatesAutoresizingMaskIntoConstraints = false - textLabel.numberOfLines = 0 // Wrap text as needed - addSubview(textLabel) - - // Add constraints for layout - NSLayoutConstraint.activate([ - imageView.leadingAnchor.constraint(equalTo: leadingAnchor), - imageView.topAnchor.constraint(equalTo: topAnchor), - imageView.bottomAnchor.constraint(equalTo: bottomAnchor), - imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor), // Makes the image view square - - textLabel.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 8), - textLabel.trailingAnchor.constraint(equalTo: trailingAnchor), - textLabel.topAnchor.constraint(equalTo: topAnchor), - textLabel.bottomAnchor.constraint(equalTo: bottomAnchor) - ]) - } - - func configure(with rank: Rank) { - imageView.image = UIImage(named: rank.imageIdentifer) - ?? UIImage(systemName: "checkmark.circle") - - textLabel.text = rank.rawValue - } -} From e99215979dcc01c67fd4b9e9df449b499bf39c2e Mon Sep 17 00:00:00 2001 From: Rubesh Date: Sat, 20 Apr 2024 19:09:37 +0800 Subject: [PATCH 07/14] Add Missions Table and some additional missions --- .../TowerForge.xcodeproj/project.pbxproj | 20 +++- .../Storyboards/Base.lproj/Main.storyboard | 96 ++++++++++++++++--- .../Metrics/Achievements/Achievement.swift | 2 + .../Metrics/Goals/AbstractGoal.swift | 4 +- ...eMission.swift => MassDamageMission.swift} | 4 +- .../Implemented/MassDeathMission.swift | 26 +++++ .../Implemented/MassKillMission.swift | 26 +++++ .../TowerForge/Metrics/Missions/Mission.swift | 2 + .../Metrics/Missions/MissionTypeWrapper.swift | 10 +- .../Metrics/Missions/MissionsDatabase.swift | 8 ++ .../Metrics/Missions/MissionsFactory.swift | 4 +- .../TotalDamageDealtStatistic.swift | 1 + .../Implemented/TotalDeathsStatistic.swift | 1 + .../Implemented/TotalGamesStatistic.swift | 1 + .../Implemented/TotalKillsStatistic.swift | 1 + .../Metrics/Statistics/Statistic.swift | 3 + .../PlayerStatsViewController.swift | 89 +++++++++++------ .../Views/CustomAchievementCell.swift | 7 -- .../TowerForge/Views/CustomMissionCell.swift | 16 ++++ 19 files changed, 259 insertions(+), 62 deletions(-) rename TowerForge/TowerForge/Metrics/Missions/Implemented/{GrandDamageMission.swift => MassDamageMission.swift} (87%) create mode 100644 TowerForge/TowerForge/Metrics/Missions/Implemented/MassDeathMission.swift create mode 100644 TowerForge/TowerForge/Metrics/Missions/Implemented/MassKillMission.swift create mode 100644 TowerForge/TowerForge/Views/CustomMissionCell.swift diff --git a/TowerForge/TowerForge.xcodeproj/project.pbxproj b/TowerForge/TowerForge.xcodeproj/project.pbxproj index d2f61ccf..30da90a2 100644 --- a/TowerForge/TowerForge.xcodeproj/project.pbxproj +++ b/TowerForge/TowerForge.xcodeproj/project.pbxproj @@ -197,6 +197,9 @@ BA2F5AC92BC81BDB00CBD8E9 /* StatisticsEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA2F5AC82BC81BDB00CBD8E9 /* StatisticsEngine.swift */; }; BA2F5ACB2BC82CCC00CBD8E9 /* EventStatisticLinkDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA2F5ACA2BC82CCC00CBD8E9 /* EventStatisticLinkDatabase.swift */; }; BA2F5ACD2BC8313900CBD8E9 /* StatisticUpdateLinkDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA2F5ACC2BC8313900CBD8E9 /* StatisticUpdateLinkDatabase.swift */; }; + BA436ADF2BD3CE9600BE3E4F /* CustomMissionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA436ADE2BD3CE9600BE3E4F /* CustomMissionCell.swift */; }; + BA436AE12BD3D66800BE3E4F /* MassKillMission.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA436AE02BD3D66800BE3E4F /* MassKillMission.swift */; }; + BA436AE32BD3D6FF00BE3E4F /* MassDeathMission.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA436AE22BD3D6FF00BE3E4F /* MassDeathMission.swift */; }; BA443D3D2BAD9557009F0FFB /* RemoveSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA443D3C2BAD9557009F0FFB /* RemoveSystem.swift */; }; BA443D3F2BAD9774009F0FFB /* RemoveEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA443D3E2BAD9774009F0FFB /* RemoveEvent.swift */; }; BA443D422BAD9885009F0FFB /* DamageEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA443D412BAD9885009F0FFB /* DamageEventTests.swift */; }; @@ -235,7 +238,7 @@ BA82C7882BCD2B51000515A0 /* MissionsDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA82C7872BCD2B51000515A0 /* MissionsDatabase.swift */; }; BA82C78A2BCD2B5D000515A0 /* MissionsEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA82C7892BCD2B5D000515A0 /* MissionsEngine.swift */; }; BA82C78C2BCD2B68000515A0 /* MissionsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA82C78B2BCD2B68000515A0 /* MissionsFactory.swift */; }; - BA82C78E2BCD2D2B000515A0 /* GrandDamageMission.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA82C78D2BCD2D2B000515A0 /* GrandDamageMission.swift */; }; + BA82C78E2BCD2D2B000515A0 /* MassDamageMission.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA82C78D2BCD2D2B000515A0 /* MassDamageMission.swift */; }; BA82C7902BCD2FAF000515A0 /* TotalDamageDealtStatistic.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA82C78F2BCD2FAF000515A0 /* TotalDamageDealtStatistic.swift */; }; BA82C7922BCD6579000515A0 /* StatisticUpdateActor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA82C7912BCD6579000515A0 /* StatisticUpdateActor.swift */; }; BA82C7942BCDAA83000515A0 /* InferenceDataDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA82C7932BCDAA83000515A0 /* InferenceDataDelegate.swift */; }; @@ -470,6 +473,9 @@ BA2F5AC82BC81BDB00CBD8E9 /* StatisticsEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticsEngine.swift; sourceTree = ""; }; BA2F5ACA2BC82CCC00CBD8E9 /* EventStatisticLinkDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventStatisticLinkDatabase.swift; sourceTree = ""; }; BA2F5ACC2BC8313900CBD8E9 /* StatisticUpdateLinkDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticUpdateLinkDatabase.swift; sourceTree = ""; }; + BA436ADE2BD3CE9600BE3E4F /* CustomMissionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomMissionCell.swift; sourceTree = ""; }; + BA436AE02BD3D66800BE3E4F /* MassKillMission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MassKillMission.swift; sourceTree = ""; }; + BA436AE22BD3D6FF00BE3E4F /* MassDeathMission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MassDeathMission.swift; sourceTree = ""; }; BA443D3C2BAD9557009F0FFB /* RemoveSystem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveSystem.swift; sourceTree = ""; }; BA443D3E2BAD9774009F0FFB /* RemoveEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveEvent.swift; sourceTree = ""; }; BA443D412BAD9885009F0FFB /* DamageEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamageEventTests.swift; sourceTree = ""; }; @@ -508,7 +514,7 @@ BA82C7872BCD2B51000515A0 /* MissionsDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MissionsDatabase.swift; sourceTree = ""; }; BA82C7892BCD2B5D000515A0 /* MissionsEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MissionsEngine.swift; sourceTree = ""; }; BA82C78B2BCD2B68000515A0 /* MissionsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MissionsFactory.swift; sourceTree = ""; }; - BA82C78D2BCD2D2B000515A0 /* GrandDamageMission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GrandDamageMission.swift; sourceTree = ""; }; + BA82C78D2BCD2D2B000515A0 /* MassDamageMission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MassDamageMission.swift; sourceTree = ""; }; BA82C78F2BCD2FAF000515A0 /* TotalDamageDealtStatistic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TotalDamageDealtStatistic.swift; sourceTree = ""; }; BA82C7912BCD6579000515A0 /* StatisticUpdateActor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticUpdateActor.swift; sourceTree = ""; }; BA82C7932BCDAA83000515A0 /* InferenceDataDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InferenceDataDelegate.swift; sourceTree = ""; }; @@ -1027,6 +1033,7 @@ isa = PBXGroup; children = ( BAEC99FF2BD1A4B700E0C437 /* CustomAchievementCell.swift */, + BA436ADE2BD3CE9600BE3E4F /* CustomMissionCell.swift */, ); path = Views; sourceTree = ""; @@ -1128,7 +1135,9 @@ BA82C7842BCD2B35000515A0 /* Implemented */ = { isa = PBXGroup; children = ( - BA82C78D2BCD2D2B000515A0 /* GrandDamageMission.swift */, + BA82C78D2BCD2D2B000515A0 /* MassDamageMission.swift */, + BA436AE02BD3D66800BE3E4F /* MassKillMission.swift */, + BA436AE22BD3D6FF00BE3E4F /* MassDeathMission.swift */, ); path = Implemented; sourceTree = ""; @@ -1609,7 +1618,7 @@ 5240D0A72BB33356004F1486 /* LifeProp.swift in Sources */, 3CA829C42BB70C5E00D8E72A /* ButtonComponent.swift in Sources */, 3CBECF892BBE9797005EF39B /* TFNetworkCoder.swift in Sources */, - BA82C78E2BCD2D2B000515A0 /* GrandDamageMission.swift in Sources */, + BA82C78E2BCD2D2B000515A0 /* MassDamageMission.swift in Sources */, 523E5C552BC63A16007444DA /* LeaderboardViewController.swift in Sources */, 5299D13E2BC36E61003EF746 /* RegisterViewController.swift in Sources */, 52DF5FFF2BA3656500135367 /* ShootingComponent.swift in Sources */, @@ -1657,6 +1666,7 @@ BA82C7A22BCE8138000515A0 /* AbstractGoalTypeWrapper.swift in Sources */, 3CE9514B2BAC83FA008B2785 /* SpawnableEntities.swift in Sources */, 5299D1302BC31002003EF746 /* AuthenticationProvider.swift in Sources */, + BA436ADF2BD3CE9600BE3E4F /* CustomMissionCell.swift in Sources */, 3C3CBDF92BB821500001B8A9 /* TFScene.swift in Sources */, BA82C79F2BCE7FBA000515A0 /* AbstractGoal.swift in Sources */, BA82C7652BCBC868000515A0 /* LocalStorageManager.swift in Sources */, @@ -1750,6 +1760,7 @@ BA82C7442BC86FFE000515A0 /* GameStartEvent.swift in Sources */, BA82C7692BCBD21F000515A0 /* RemoteStorageManager.swift in Sources */, 9B0406102BB879990026E903 /* InvulnerabilityPowerUp.swift in Sources */, + BA436AE12BD3D66800BE3E4F /* MassKillMission.swift in Sources */, 5299D1412BC3AA3A003EF746 /* GameRankData.swift in Sources */, 52F930E72BC63F7F003D11B5 /* LeaderboardSelectionViewController.swift in Sources */, BA82C76F2BCBDE91000515A0 /* Metadata.swift in Sources */, @@ -1763,6 +1774,7 @@ BAFFB95D2BB978E500D8301F /* StorageEnums.swift in Sources */, 52DF5FED2BA34D0300135367 /* TFComponent.swift in Sources */, 527E3A242BA613F000FE1628 /* PlayerComponent.swift in Sources */, + BA436AE32BD3D6FF00BE3E4F /* MassDeathMission.swift in Sources */, 3C9955C52BA585DD00D33FA5 /* HealthSystem.swift in Sources */, 5240D0A02BB330B5004F1486 /* GameMode.swift in Sources */, 520062562BA8E026000DBA30 /* PlayerSpawnable.swift in Sources */, diff --git a/TowerForge/TowerForge/AppMain/Storyboards/Base.lproj/Main.storyboard b/TowerForge/TowerForge/AppMain/Storyboards/Base.lproj/Main.storyboard index d2a8b29f..ad0d81a9 100644 --- a/TowerForge/TowerForge/AppMain/Storyboards/Base.lproj/Main.storyboard +++ b/TowerForge/TowerForge/AppMain/Storyboards/Base.lproj/Main.storyboard @@ -151,7 +151,7 @@ - + @@ -171,7 +171,7 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + - - - + - + + + + - + - + diff --git a/TowerForge/TowerForge/Metrics/Achievements/Achievement.swift b/TowerForge/TowerForge/Metrics/Achievements/Achievement.swift index 9a6a9873..39752bbf 100644 --- a/TowerForge/TowerForge/Metrics/Achievements/Achievement.swift +++ b/TowerForge/TowerForge/Metrics/Achievements/Achievement.swift @@ -10,6 +10,8 @@ import Foundation protocol Achievement: AbstractGoal { } extension Achievement { + var imageIdentifier: String { "coin" } + static var asType: AchievementTypeWrapper { AchievementTypeWrapper(type: Self.self) } diff --git a/TowerForge/TowerForge/Metrics/Goals/AbstractGoal.swift b/TowerForge/TowerForge/Metrics/Goals/AbstractGoal.swift index 85894970..ace17a96 100644 --- a/TowerForge/TowerForge/Metrics/Goals/AbstractGoal.swift +++ b/TowerForge/TowerForge/Metrics/Goals/AbstractGoal.swift @@ -13,9 +13,9 @@ import Foundation /// Each achievement will correspond to a collection of statistics. protocol AbstractGoal: AnyObject { static var goalType: AbstractGoalTypeWrapper { get } + var imageIdentifier: String { get } var name: String { get } var description: String { get } - var imageIdentifier: String { get } static var definedParameters: [StatisticTypeWrapper: Double] { get } var currentParameters: [StatisticTypeWrapper: Statistic] { get set } @@ -36,8 +36,6 @@ protocol AbstractGoal: AnyObject { extension AbstractGoal { - var imageIdentifier: String { "coin" } - static var goalType: AbstractGoalTypeWrapper { AbstractGoalTypeWrapper(type: Self.self) } diff --git a/TowerForge/TowerForge/Metrics/Missions/Implemented/GrandDamageMission.swift b/TowerForge/TowerForge/Metrics/Missions/Implemented/MassDamageMission.swift similarity index 87% rename from TowerForge/TowerForge/Metrics/Missions/Implemented/GrandDamageMission.swift rename to TowerForge/TowerForge/Metrics/Missions/Implemented/MassDamageMission.swift index db44d08c..86291114 100644 --- a/TowerForge/TowerForge/Metrics/Missions/Implemented/GrandDamageMission.swift +++ b/TowerForge/TowerForge/Metrics/Missions/Implemented/MassDamageMission.swift @@ -7,8 +7,8 @@ import Foundation -final class GrandDamageMission: Mission { - var name: String = "Mission: 1000 Damage" +final class MassDamageMission: Mission { + var name: String = "Mass Damage" var description: String = "Attain 1000 Damage in 1 game" var currentParameters: [StatisticTypeWrapper: any Statistic] diff --git a/TowerForge/TowerForge/Metrics/Missions/Implemented/MassDeathMission.swift b/TowerForge/TowerForge/Metrics/Missions/Implemented/MassDeathMission.swift new file mode 100644 index 00000000..01c0f66e --- /dev/null +++ b/TowerForge/TowerForge/Metrics/Missions/Implemented/MassDeathMission.swift @@ -0,0 +1,26 @@ +// +// MassDeathMission.swift +// TowerForge +// +// Created by Rubesh on 20/4/24. +// + +import Foundation + +final class MassDeathMission: Mission { + var name: String = "Mass Death" + var description: String = "Die 100 times in 1 game" + var currentParameters: [StatisticTypeWrapper: any Statistic] + + static var definedParameters: [StatisticTypeWrapper: Double] { + [ + TotalDeathsStatistic.asType: 100.0 + ] + } + + init(dependentStatistics: [Statistic]) { + var stats: [StatisticTypeWrapper: any Statistic] = [:] + dependentStatistics.forEach { stats[$0.statisticName] = $0 } + self.currentParameters = stats + } +} diff --git a/TowerForge/TowerForge/Metrics/Missions/Implemented/MassKillMission.swift b/TowerForge/TowerForge/Metrics/Missions/Implemented/MassKillMission.swift new file mode 100644 index 00000000..ca389605 --- /dev/null +++ b/TowerForge/TowerForge/Metrics/Missions/Implemented/MassKillMission.swift @@ -0,0 +1,26 @@ +// +// MassKillMission.swift +// TowerForge +// +// Created by Rubesh on 20/4/24. +// + +import Foundation + +final class MassKillMission: Mission { + var name: String = "Mass Kill" + var description: String = "Attain 100 Kills in 1 game" + var currentParameters: [StatisticTypeWrapper: any Statistic] + + static var definedParameters: [StatisticTypeWrapper: Double] { + [ + TotalKillsStatistic.asType: 100.0 + ] + } + + init(dependentStatistics: [Statistic]) { + var stats: [StatisticTypeWrapper: any Statistic] = [:] + dependentStatistics.forEach { stats[$0.statisticName] = $0 } + self.currentParameters = stats + } +} diff --git a/TowerForge/TowerForge/Metrics/Missions/Mission.swift b/TowerForge/TowerForge/Metrics/Missions/Mission.swift index cb6e307c..bdbfa767 100644 --- a/TowerForge/TowerForge/Metrics/Missions/Mission.swift +++ b/TowerForge/TowerForge/Metrics/Missions/Mission.swift @@ -10,6 +10,8 @@ import Foundation protocol Mission: AbstractGoal { } extension Mission { + var imageIdentifier: String { "damage" } + static var asType: MissionTypeWrapper { MissionTypeWrapper(type: Self.self) } diff --git a/TowerForge/TowerForge/Metrics/Missions/MissionTypeWrapper.swift b/TowerForge/TowerForge/Metrics/Missions/MissionTypeWrapper.swift index cd652a66..7aba6d20 100644 --- a/TowerForge/TowerForge/Metrics/Missions/MissionTypeWrapper.swift +++ b/TowerForge/TowerForge/Metrics/Missions/MissionTypeWrapper.swift @@ -7,7 +7,7 @@ import Foundation -struct MissionTypeWrapper: Equatable, Hashable { +struct MissionTypeWrapper: Equatable, Hashable, Comparable { let type: Mission.Type var asString: String { @@ -21,4 +21,12 @@ struct MissionTypeWrapper: Equatable, Hashable { func hash(into hasher: inout Hasher) { hasher.combine(ObjectIdentifier(type)) } + + static func < (lhs: MissionTypeWrapper, rhs: MissionTypeWrapper) -> Bool { + lhs.asString < rhs.asString + } + + static func > (lhs: MissionTypeWrapper, rhs: MissionTypeWrapper) -> Bool { + lhs.asString > rhs.asString + } } diff --git a/TowerForge/TowerForge/Metrics/Missions/MissionsDatabase.swift b/TowerForge/TowerForge/Metrics/Missions/MissionsDatabase.swift index 681448c1..cb4da617 100644 --- a/TowerForge/TowerForge/Metrics/Missions/MissionsDatabase.swift +++ b/TowerForge/TowerForge/Metrics/Missions/MissionsDatabase.swift @@ -11,6 +11,14 @@ class MissionsDatabase { weak var missionsDataDelegate: InferenceDataDelegate? var missions: [MissionTypeWrapper: Mission] = [:] + var count: Int { + missions.count + } + + var asSortedArray: [Dictionary.Element] { + Array(missions).sorted(by: { $0.key < $1.key }) + } + init(missions: [MissionTypeWrapper: Mission] = [:]) { self.missions = missions } diff --git a/TowerForge/TowerForge/Metrics/Missions/MissionsFactory.swift b/TowerForge/TowerForge/Metrics/Missions/MissionsFactory.swift index 7c38a8e2..6c9eeef5 100644 --- a/TowerForge/TowerForge/Metrics/Missions/MissionsFactory.swift +++ b/TowerForge/TowerForge/Metrics/Missions/MissionsFactory.swift @@ -11,7 +11,9 @@ class MissionsFactory { static var availableMissionTypes: [String: Mission.Type] = [ - String(describing: GrandDamageMission.self): GrandDamageMission.self + String(describing: MassDamageMission.self): MassDamageMission.self, + String(describing: MassKillMission.self): MassKillMission.self, + String(describing: MassDeathMission.self): MassDeathMission.self ] static func registerMissionType(_ stat: T) { diff --git a/TowerForge/TowerForge/Metrics/Statistics/Implemented/TotalDamageDealtStatistic.swift b/TowerForge/TowerForge/Metrics/Statistics/Implemented/TotalDamageDealtStatistic.swift index 4630d41f..e1855c74 100644 --- a/TowerForge/TowerForge/Metrics/Statistics/Implemented/TotalDamageDealtStatistic.swift +++ b/TowerForge/TowerForge/Metrics/Statistics/Implemented/TotalDamageDealtStatistic.swift @@ -10,6 +10,7 @@ import Foundation /// Total Damage dealt by the player in the course of the game final class TotalDamageDealtStatistic: Statistic { static let expMultiplier: Double = 10 + var prettyName: String = "Total Damage" var permanentValue: Double = .zero var currentValue: Double = .zero var maximumCurrentValue: Double = .zero diff --git a/TowerForge/TowerForge/Metrics/Statistics/Implemented/TotalDeathsStatistic.swift b/TowerForge/TowerForge/Metrics/Statistics/Implemented/TotalDeathsStatistic.swift index 237b4883..54b2343d 100644 --- a/TowerForge/TowerForge/Metrics/Statistics/Implemented/TotalDeathsStatistic.swift +++ b/TowerForge/TowerForge/Metrics/Statistics/Implemented/TotalDeathsStatistic.swift @@ -8,6 +8,7 @@ import Foundation final class TotalDeathsStatistic: Statistic { + var prettyName: String = "Total Deaths" var permanentValue: Double = .zero var currentValue: Double = .zero var maximumCurrentValue: Double = .zero diff --git a/TowerForge/TowerForge/Metrics/Statistics/Implemented/TotalGamesStatistic.swift b/TowerForge/TowerForge/Metrics/Statistics/Implemented/TotalGamesStatistic.swift index adfbcafb..fecb18b0 100644 --- a/TowerForge/TowerForge/Metrics/Statistics/Implemented/TotalGamesStatistic.swift +++ b/TowerForge/TowerForge/Metrics/Statistics/Implemented/TotalGamesStatistic.swift @@ -9,6 +9,7 @@ import Foundation final class TotalGamesStatistic: Statistic { static let expMultiplier: Double = 100 + var prettyName: String = "Total Games" var permanentValue: Double = .zero var currentValue: Double = .zero var maximumCurrentValue: Double diff --git a/TowerForge/TowerForge/Metrics/Statistics/Implemented/TotalKillsStatistic.swift b/TowerForge/TowerForge/Metrics/Statistics/Implemented/TotalKillsStatistic.swift index df03efa8..f406d296 100644 --- a/TowerForge/TowerForge/Metrics/Statistics/Implemented/TotalKillsStatistic.swift +++ b/TowerForge/TowerForge/Metrics/Statistics/Implemented/TotalKillsStatistic.swift @@ -9,6 +9,7 @@ import Foundation final class TotalKillsStatistic: Statistic { static let expMultiplier: Double = 10 + var prettyName: String = "Total Kills" var permanentValue: Double = .zero var currentValue: Double = .zero var maximumCurrentValue: Double = .zero diff --git a/TowerForge/TowerForge/Metrics/Statistics/Statistic.swift b/TowerForge/TowerForge/Metrics/Statistics/Statistic.swift index 04b4f338..80dc1dd1 100644 --- a/TowerForge/TowerForge/Metrics/Statistics/Statistic.swift +++ b/TowerForge/TowerForge/Metrics/Statistics/Statistic.swift @@ -11,6 +11,9 @@ import Foundation protocol Statistic: AnyObject, Codable { var statisticName: StatisticTypeWrapper { get } + /// A presentable version of the Statistic's identifier + var prettyName: String { get } + /// The original value of the statistic prior to the start of the game seequence var permanentValue: Double { get set } diff --git a/TowerForge/TowerForge/ViewControllers/PlayerStatsViewController.swift b/TowerForge/TowerForge/ViewControllers/PlayerStatsViewController.swift index 2e2c6a3f..1665e913 100644 --- a/TowerForge/TowerForge/ViewControllers/PlayerStatsViewController.swift +++ b/TowerForge/TowerForge/ViewControllers/PlayerStatsViewController.swift @@ -11,10 +11,13 @@ import UIKit class PlayerStatsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { @IBOutlet private var achievementsView: UITableView! + @IBOutlet private var missionsView: UITableView! + @IBOutlet private var rankNameLabel: UILabel! - @IBOutlet private var characterImage: UIImageView! + // @IBOutlet private var characterImage: UIImageView! var achievements: AchievementsDatabase = getAchievements() + var missions: MissionsDatabase = getMissions() var rank: Rank = getRank() static func getAchievements() -> AchievementsDatabase { @@ -24,7 +27,15 @@ class PlayerStatsViewController: UIViewController, UITableViewDataSource, UITabl return achEngine!.achievementsDatabase } + static func getMissions() -> MissionsDatabase { + let statsEngine = StatisticsEngine() + let missionsEngine = statsEngine.inferenceEngines[MissionsEngine.asType] as? MissionsEngine + missionsEngine!.missionsDatabase.setToDefault() + return missionsEngine!.missionsDatabase + } + static func getRank() -> Rank { + let statsEngine = StatisticsEngine() let rankEngine = statsEngine.inferenceEngines[RankingEngine.asType] as? RankingEngine return rankEngine!.currentRank @@ -35,11 +46,17 @@ class PlayerStatsViewController: UIViewController, UITableViewDataSource, UITabl achievementsView.delegate = self achievementsView.dataSource = self + missionsView.delegate = self + missionsView.dataSource = self + + achievementsView.allowsSelection = false + missionsView.allowsSelection = false + self.rank = Self.getRank() // rankImageView.image = UIImage(named: currentRank.imageIdentifer) rankNameLabel.text = String("--- Rank: \(rank.rawValue) ---") - characterImage.image = UIImage(named: "melee-1") + // characterImage.image = UIImage(named: "melee-1") reloadAchievements() } @@ -50,28 +67,53 @@ class PlayerStatsViewController: UIViewController, UITableViewDataSource, UITabl } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - achievements.count + if tableView == achievementsView { + return achievements.count + } else if tableView == missionsView { + return missions.missions.count + } + return 0 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell(withIdentifier: "cell", - for: indexPath) as? CustomAchievementCell else { - fatalError("Could not dequeue CustomAchievementCell") + if tableView == achievementsView { + guard let cell = tableView.dequeueReusableCell(withIdentifier: "cell", + for: indexPath) as? CustomAchievementCell else { + fatalError("Could not dequeue CustomAchievementCell") + } + + let achievementPair = achievements.asSortedArray[indexPath.row] + let achievement = achievementPair.value + + // Configure the cell elements + cell.nameLabel.text = achievement.name + cell.descriptionLabel.text = achievement.description + cell.achievementImageView.image = UIImage(named: achievement.imageIdentifier) + cell.progressView.progress = Float(achievement.overallProgressRateRounded) + // cell.progressPercentage.text = String(describing: Float(achievement.overallProgressRateRounded)) + let statusImageName = achievement.isComplete ? "checkmark.circle" : "x.circle" + cell.statusImageView.image = UIImage(systemName: statusImageName) + + return cell } - let achievementPair = achievements.asSortedArray[indexPath.row] - let achievement = achievementPair.value + guard let missionCell = tableView.dequeueReusableCell(withIdentifier: "cell", + for: indexPath) as? CustomMissionCell else { + fatalError("Could not dequeue CustomMissionCell") + } + + let missionPair = missions.asSortedArray[indexPath.row] + let mission = missionPair.value // Configure the cell elements - cell.nameLabel.text = achievement.name - cell.descriptionLabel.text = achievement.description - cell.achievementImageView.image = UIImage(named: achievement.imageIdentifier) - cell.progressView.progress = Float(achievement.overallProgressRateRounded) - // cell.progressPercentage.text = String(describing: Float(achievement.overallProgressRateRounded)) - let statusImageName = achievement.isComplete ? "checkmark.circle" : "x.circle" - cell.statusImageView.image = UIImage(systemName: statusImageName) - - return cell + missionCell.missionNameLabel.text = mission.name + missionCell.descriptionLabel.text = mission.description + missionCell.missionImageView.image = UIImage(named: mission.imageIdentifier) + let statusImageName = mission.isComplete ? "checkmark.circle" : "x.circle" + missionCell.statusImageView.image = UIImage(systemName: statusImageName) + + return missionCell + } func reloadAchievements() { @@ -79,17 +121,4 @@ class PlayerStatsViewController: UIViewController, UITableViewDataSource, UITabl rankNameLabel.reloadInputViews() } - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - // Deselect the row with animation - tableView.deselectRow(at: indexPath, animated: true) - - // Get the selected achievement - let achievementPair = achievements.asSortedArray[indexPath.row] - let achievement = achievementPair.value - - // Show a detail view controller - // TODO: need to implement the logic for showing the details - // showAchievementDetails(achievement) - } - } diff --git a/TowerForge/TowerForge/Views/CustomAchievementCell.swift b/TowerForge/TowerForge/Views/CustomAchievementCell.swift index bb5ccc52..0d752004 100644 --- a/TowerForge/TowerForge/Views/CustomAchievementCell.swift +++ b/TowerForge/TowerForge/Views/CustomAchievementCell.swift @@ -15,10 +15,3 @@ class CustomAchievementCell: UITableViewCell { @IBOutlet var progressView: UIProgressView! @IBOutlet var statusImageView: UIImageView! } - -class CustomMissionCell: UITableViewCell { - @IBOutlet var missionImageView: UIImageView! - @IBOutlet var missionNameLabel: UILabel! - @IBOutlet var descriptionLabel: UILabel! - @IBOutlet var statusImageView: UIImageView! -} diff --git a/TowerForge/TowerForge/Views/CustomMissionCell.swift b/TowerForge/TowerForge/Views/CustomMissionCell.swift new file mode 100644 index 00000000..7182ef80 --- /dev/null +++ b/TowerForge/TowerForge/Views/CustomMissionCell.swift @@ -0,0 +1,16 @@ +// +// CustomMissionCell.swift +// TowerForge +// +// Created by Rubesh on 20/4/24. +// + +import Foundation +import UIKit + +class CustomMissionCell: UITableViewCell { + @IBOutlet var missionImageView: UIImageView! + @IBOutlet var missionNameLabel: UILabel! + @IBOutlet var descriptionLabel: UILabel! + @IBOutlet var statusImageView: UIImageView! +} From deb18a3eb29fd7b48bfd6245c4d4472e3ed146be Mon Sep 17 00:00:00 2001 From: Rubesh Date: Sat, 20 Apr 2024 19:14:57 +0800 Subject: [PATCH 08/14] Add character screen --- .../AppMain/Storyboards/Base.lproj/Main.storyboard | 12 +++++++++--- .../ViewControllers/PlayerStatsViewController.swift | 6 +++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/TowerForge/TowerForge/AppMain/Storyboards/Base.lproj/Main.storyboard b/TowerForge/TowerForge/AppMain/Storyboards/Base.lproj/Main.storyboard index ad0d81a9..87606a50 100644 --- a/TowerForge/TowerForge/AppMain/Storyboards/Base.lproj/Main.storyboard +++ b/TowerForge/TowerForge/AppMain/Storyboards/Base.lproj/Main.storyboard @@ -296,9 +296,12 @@ - - - + + + + + + @@ -318,17 +321,20 @@ + + + diff --git a/TowerForge/TowerForge/ViewControllers/PlayerStatsViewController.swift b/TowerForge/TowerForge/ViewControllers/PlayerStatsViewController.swift index 1665e913..35c01412 100644 --- a/TowerForge/TowerForge/ViewControllers/PlayerStatsViewController.swift +++ b/TowerForge/TowerForge/ViewControllers/PlayerStatsViewController.swift @@ -14,7 +14,7 @@ class PlayerStatsViewController: UIViewController, UITableViewDataSource, UITabl @IBOutlet private var missionsView: UITableView! @IBOutlet private var rankNameLabel: UILabel! - // @IBOutlet private var characterImage: UIImageView! + @IBOutlet private var characterImage: UIImageView! var achievements: AchievementsDatabase = getAchievements() var missions: MissionsDatabase = getMissions() @@ -48,7 +48,7 @@ class PlayerStatsViewController: UIViewController, UITableViewDataSource, UITabl achievementsView.dataSource = self missionsView.delegate = self missionsView.dataSource = self - + achievementsView.allowsSelection = false missionsView.allowsSelection = false @@ -56,7 +56,7 @@ class PlayerStatsViewController: UIViewController, UITableViewDataSource, UITabl // rankImageView.image = UIImage(named: currentRank.imageIdentifer) rankNameLabel.text = String("--- Rank: \(rank.rawValue) ---") - // characterImage.image = UIImage(named: "melee-1") + characterImage.image = UIImage(named: "melee-1") reloadAchievements() } From 7ceff72b330fae42973f1a730797b10a6e44f8aa Mon Sep 17 00:00:00 2001 From: Rubesh Date: Sat, 20 Apr 2024 19:53:54 +0800 Subject: [PATCH 09/14] Finalize CharacterUI --- .../Storyboards/Base.lproj/Main.storyboard | 54 ++++++++++++++++++- .../TowerForge/Metrics/Ranking/Rank.swift | 26 +++++---- .../Metrics/Ranking/RankingEngine.swift | 22 +++++++- .../PlayerStatsViewController.swift | 34 +++++++++--- 4 files changed, 117 insertions(+), 19 deletions(-) diff --git a/TowerForge/TowerForge/AppMain/Storyboards/Base.lproj/Main.storyboard b/TowerForge/TowerForge/AppMain/Storyboards/Base.lproj/Main.storyboard index 87606a50..90090d0d 100644 --- a/TowerForge/TowerForge/AppMain/Storyboards/Base.lproj/Main.storyboard +++ b/TowerForge/TowerForge/AppMain/Storyboards/Base.lproj/Main.storyboard @@ -297,46 +297,96 @@ - + + + + + + + + + + + + + + + + + + + + + - + + + + + + diff --git a/TowerForge/TowerForge/Metrics/Ranking/Rank.swift b/TowerForge/TowerForge/Metrics/Ranking/Rank.swift index 4d801027..015ee8b0 100644 --- a/TowerForge/TowerForge/Metrics/Ranking/Rank.swift +++ b/TowerForge/TowerForge/Metrics/Ranking/Rank.swift @@ -7,7 +7,7 @@ import Foundation -enum Rank: String, CaseIterable { +enum Rank: String, CaseIterable, Comparable { case PRIVATE case CORPORAL case SERGEANT @@ -19,14 +19,14 @@ enum Rank: String, CaseIterable { var valueRange: Range { switch self { - case .PRIVATE: return 0..<1_001 - case .CORPORAL: return 1_001..<2_001 - case .SERGEANT: return 2_001..<3_001 - case .LIEUTENANT: return 3_001..<4_001 - case .CAPTAIN: return 4_001..<5_001 - case .MAJOR: return 6_001..<7_001 - case .COLONEL: return 7_001..<8_001 - case .GENERAL: return 8_001..<9_001 + case .PRIVATE: return 0..<10_001 + case .CORPORAL: return 10_001..<20_001 + case .SERGEANT: return 20_001..<30_001 + case .LIEUTENANT: return 30_001..<50_001 + case .CAPTAIN: return 50_001..<100_001 + case .MAJOR: return 100_001..<300_001 + case .COLONEL: return 300_001..<800_001 + case .GENERAL: return 800_001.. Bool { + allCases.firstIndex(of: lhs)! < allCases.firstIndex(of: rhs)! + } + + func isOfficer() -> Bool { + self > .SERGEANT + } } diff --git a/TowerForge/TowerForge/Metrics/Ranking/RankingEngine.swift b/TowerForge/TowerForge/Metrics/Ranking/RankingEngine.swift index 80a82daa..c82007fc 100644 --- a/TowerForge/TowerForge/Metrics/Ranking/RankingEngine.swift +++ b/TowerForge/TowerForge/Metrics/Ranking/RankingEngine.swift @@ -17,15 +17,33 @@ class RankingEngine: InferenceEngine, InferenceDataDelegate { unowned var statisticsEngine: StatisticsEngine - var statisticsDatabase: StatisticsDatabase { statisticsEngine.statistics } - var currentExp: Double { Self.defaultExpFormula(statisticsDatabase) } + var statisticsDatabase: StatisticsDatabase { + statisticsEngine.statistics + } + + var currentExp: Double { + Self.defaultExpFormula(statisticsDatabase) + } + var currentRank: Rank { Rank.allCases.first { $0.valueRange.contains(Int(self.currentExp)) } ?? .PRIVATE } + var currentKD: Double { + getPermanentValueFor(TotalKillsStatistic.self) / getPermanentValueFor(TotalDeathsStatistic.self) + } + + var isOfficer: Bool { + currentRank.isOfficer() + } + init(_ statisticsEngine: StatisticsEngine) { self.statisticsEngine = statisticsEngine } func updateOnReceive() { } + + func getPermanentValueFor(_ stat: T.Type) -> Double { + statisticsDatabase.getStatistic(for: stat.asType)?.permanentValue ?? .zero + } } diff --git a/TowerForge/TowerForge/ViewControllers/PlayerStatsViewController.swift b/TowerForge/TowerForge/ViewControllers/PlayerStatsViewController.swift index 35c01412..cbbbdd37 100644 --- a/TowerForge/TowerForge/ViewControllers/PlayerStatsViewController.swift +++ b/TowerForge/TowerForge/ViewControllers/PlayerStatsViewController.swift @@ -16,9 +16,28 @@ class PlayerStatsViewController: UIViewController, UITableViewDataSource, UITabl @IBOutlet private var rankNameLabel: UILabel! @IBOutlet private var characterImage: UIImageView! + @IBOutlet private var currentExp: UILabel! + @IBOutlet private var totalKills: UILabel! + @IBOutlet private var totalDeaths: UILabel! + @IBOutlet private var kdRatio: UILabel! + @IBOutlet private var totalGames: UILabel! + + var statsEngine = StatisticsEngine() + var achievements: AchievementsDatabase = getAchievements() var missions: MissionsDatabase = getMissions() - var rank: Rank = getRank() + + var rankingEngine: RankingEngine { + let rankEngine = statsEngine.inferenceEngines[RankingEngine.asType] as? RankingEngine + return rankEngine! + } + + var rank: Rank { rankingEngine.currentRank } + var exp: Int { Int(rankingEngine.currentExp) } + var kd: Double { rankingEngine.currentKD } + var kills: Int { Int(rankingEngine.getPermanentValueFor(TotalKillsStatistic.self)) } + var deaths: Int { Int(rankingEngine.getPermanentValueFor(TotalDeathsStatistic.self)) } + var games: Int { Int(rankingEngine.getPermanentValueFor(TotalGamesStatistic.self)) } static func getAchievements() -> AchievementsDatabase { let statsEngine = StatisticsEngine() @@ -34,11 +53,10 @@ class PlayerStatsViewController: UIViewController, UITableViewDataSource, UITabl return missionsEngine!.missionsDatabase } - static func getRank() -> Rank { - + static func getRankingEngine() -> RankingEngine { let statsEngine = StatisticsEngine() let rankEngine = statsEngine.inferenceEngines[RankingEngine.asType] as? RankingEngine - return rankEngine!.currentRank + return rankEngine! } override func viewDidLoad() { @@ -52,11 +70,15 @@ class PlayerStatsViewController: UIViewController, UITableViewDataSource, UITabl achievementsView.allowsSelection = false missionsView.allowsSelection = false - self.rank = Self.getRank() // rankImageView.image = UIImage(named: currentRank.imageIdentifer) rankNameLabel.text = String("--- Rank: \(rank.rawValue) ---") + characterImage.image = rank.isOfficer() ? UIImage(named: "Shooter-1") : UIImage(named: "melee-1") + currentExp.text = String("Exp: \(exp)") + totalKills.text = String("Kills: \(kills)") + totalDeaths.text = String("Deaths: \(deaths)") + totalGames.text = String("Games: \(games)") + kdRatio.text = String("K/D Ratio: ") + String(format: "%.2f", kd) - characterImage.image = UIImage(named: "melee-1") reloadAchievements() } From 8e1fe8a2b1376a84d561f1d3777ba69812ee37cc Mon Sep 17 00:00:00 2001 From: Rubesh Date: Sat, 20 Apr 2024 20:01:55 +0800 Subject: [PATCH 10/14] Replace UIKit button with Player button --- .../Storyboards/Base.lproj/Main.storyboard | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/TowerForge/TowerForge/AppMain/Storyboards/Base.lproj/Main.storyboard b/TowerForge/TowerForge/AppMain/Storyboards/Base.lproj/Main.storyboard index 90090d0d..5a6d6864 100644 --- a/TowerForge/TowerForge/AppMain/Storyboards/Base.lproj/Main.storyboard +++ b/TowerForge/TowerForge/AppMain/Storyboards/Base.lproj/Main.storyboard @@ -87,22 +87,23 @@ - - @@ -111,14 +112,16 @@ - + + - - + + + @@ -1363,9 +1366,6 @@ - - - From 2d67f9cdc787250ac37b23ce83709368acfc7429 Mon Sep 17 00:00:00 2001 From: Rubesh Date: Sat, 20 Apr 2024 20:16:06 +0800 Subject: [PATCH 11/14] Add player button --- .../player-button.imageset/Contents.json | 21 ++++++++++++++++++ .../player-button.imageset/player-button.png | Bin 0 -> 33951 bytes .../Storyboards/Base.lproj/Main.storyboard | 3 ++- .../Metrics/Ranking/RankingEngine.swift | 11 ++++++++- .../PlayerStatsViewController.swift | 8 +++---- 5 files changed, 37 insertions(+), 6 deletions(-) create mode 100644 TowerForge/Assets.xcassets/player-button.imageset/Contents.json create mode 100644 TowerForge/Assets.xcassets/player-button.imageset/player-button.png diff --git a/TowerForge/Assets.xcassets/player-button.imageset/Contents.json b/TowerForge/Assets.xcassets/player-button.imageset/Contents.json new file mode 100644 index 00000000..df0ed66d --- /dev/null +++ b/TowerForge/Assets.xcassets/player-button.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "player-button.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/TowerForge/Assets.xcassets/player-button.imageset/player-button.png b/TowerForge/Assets.xcassets/player-button.imageset/player-button.png new file mode 100644 index 0000000000000000000000000000000000000000..bd7ba34fc3807af4102dbf27a5b8fdd55655d9b6 GIT binary patch literal 33951 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 literal 0 HcmV?d00001 diff --git a/TowerForge/TowerForge/AppMain/Storyboards/Base.lproj/Main.storyboard b/TowerForge/TowerForge/AppMain/Storyboards/Base.lproj/Main.storyboard index 5a6d6864..df5beba4 100644 --- a/TowerForge/TowerForge/AppMain/Storyboards/Base.lproj/Main.storyboard +++ b/TowerForge/TowerForge/AppMain/Storyboards/Base.lproj/Main.storyboard @@ -94,7 +94,7 @@ - + @@ -1360,6 +1360,7 @@ + diff --git a/TowerForge/TowerForge/Metrics/Ranking/RankingEngine.swift b/TowerForge/TowerForge/Metrics/Ranking/RankingEngine.swift index c82007fc..f24c8d2c 100644 --- a/TowerForge/TowerForge/Metrics/Ranking/RankingEngine.swift +++ b/TowerForge/TowerForge/Metrics/Ranking/RankingEngine.swift @@ -29,10 +29,19 @@ class RankingEngine: InferenceEngine, InferenceDataDelegate { Rank.allCases.first { $0.valueRange.contains(Int(self.currentExp)) } ?? .PRIVATE } - var currentKD: Double { + var currentKd: Double { getPermanentValueFor(TotalKillsStatistic.self) / getPermanentValueFor(TotalDeathsStatistic.self) } + var currentExpAsString: String { + let numberFormatter = NumberFormatter() + numberFormatter.numberStyle = .decimal + + let number = Int(currentExp) + let formattedString = numberFormatter.string(from: NSNumber(value: number)) ?? "" + return formattedString + } + var isOfficer: Bool { currentRank.isOfficer() } diff --git a/TowerForge/TowerForge/ViewControllers/PlayerStatsViewController.swift b/TowerForge/TowerForge/ViewControllers/PlayerStatsViewController.swift index cbbbdd37..58d9bc55 100644 --- a/TowerForge/TowerForge/ViewControllers/PlayerStatsViewController.swift +++ b/TowerForge/TowerForge/ViewControllers/PlayerStatsViewController.swift @@ -33,8 +33,8 @@ class PlayerStatsViewController: UIViewController, UITableViewDataSource, UITabl } var rank: Rank { rankingEngine.currentRank } - var exp: Int { Int(rankingEngine.currentExp) } - var kd: Double { rankingEngine.currentKD } + var exp: String { rankingEngine.currentExpAsString } + var kd: Double { rankingEngine.currentKd } var kills: Int { Int(rankingEngine.getPermanentValueFor(TotalKillsStatistic.self)) } var deaths: Int { Int(rankingEngine.getPermanentValueFor(TotalDeathsStatistic.self)) } var games: Int { Int(rankingEngine.getPermanentValueFor(TotalGamesStatistic.self)) } @@ -73,7 +73,7 @@ class PlayerStatsViewController: UIViewController, UITableViewDataSource, UITabl // rankImageView.image = UIImage(named: currentRank.imageIdentifer) rankNameLabel.text = String("--- Rank: \(rank.rawValue) ---") characterImage.image = rank.isOfficer() ? UIImage(named: "Shooter-1") : UIImage(named: "melee-1") - currentExp.text = String("Exp: \(exp)") + currentExp.text = String("XP: \(exp)") totalKills.text = String("Kills: \(kills)") totalDeaths.text = String("Deaths: \(deaths)") totalGames.text = String("Games: \(games)") @@ -120,7 +120,7 @@ class PlayerStatsViewController: UIViewController, UITableViewDataSource, UITabl } guard let missionCell = tableView.dequeueReusableCell(withIdentifier: "cell", - for: indexPath) as? CustomMissionCell else { + for: indexPath) as? CustomMissionCell else { fatalError("Could not dequeue CustomMissionCell") } From 369b572eab8f2050ff3e0ba29ce67823b82564ef Mon Sep 17 00:00:00 2001 From: Rubesh Date: Sat, 20 Apr 2024 20:50:12 +0800 Subject: [PATCH 12/14] Add permanent lightmode override --- .../TowerForge/AppMain/Application/AppDelegate.swift | 3 +++ .../ViewControllers/PlayerStatsViewController.swift | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/TowerForge/TowerForge/AppMain/Application/AppDelegate.swift b/TowerForge/TowerForge/AppMain/Application/AppDelegate.swift index 33c48971..0a68e094 100644 --- a/TowerForge/TowerForge/AppMain/Application/AppDelegate.swift +++ b/TowerForge/TowerForge/AppMain/Application/AppDelegate.swift @@ -15,6 +15,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. + if #available(iOS 13.0, *) { + window?.overrideUserInterfaceStyle = .light + } /// Connect to Firebase FirebaseApp.configure() diff --git a/TowerForge/TowerForge/ViewControllers/PlayerStatsViewController.swift b/TowerForge/TowerForge/ViewControllers/PlayerStatsViewController.swift index 58d9bc55..b5967fdf 100644 --- a/TowerForge/TowerForge/ViewControllers/PlayerStatsViewController.swift +++ b/TowerForge/TowerForge/ViewControllers/PlayerStatsViewController.swift @@ -79,6 +79,14 @@ class PlayerStatsViewController: UIViewController, UITableViewDataSource, UITabl totalGames.text = String("Games: \(games)") kdRatio.text = String("K/D Ratio: ") + String(format: "%.2f", kd) + /* TODO: Add background image + let backgroundImage = UIImageView(frame: UIScreen.main.bounds) + backgroundImage.image = UIImage(named: "stone-tile") + backgroundImage.contentMode = .scaleAspectFill + view.addSubview(backgroundImage) // Add the image view to the view hierarchy + view.sendSubviewToBack(backgroundImage) // Send the image to the background + */ + reloadAchievements() } From 3168d51c2001c8dd419dda6a03dcef3ec8dd47e5 Mon Sep 17 00:00:00 2001 From: Rubesh Date: Sat, 20 Apr 2024 20:54:39 +0800 Subject: [PATCH 13/14] Add AppIcon --- .../AppIcon.appiconset/Contents.json | 1 + .../AppIcon.appiconset/tficon.png | Bin 0 -> 52083 bytes 2 files changed, 1 insertion(+) create mode 100644 TowerForge/Assets.xcassets/AppIcon.appiconset/tficon.png diff --git a/TowerForge/Assets.xcassets/AppIcon.appiconset/Contents.json b/TowerForge/Assets.xcassets/AppIcon.appiconset/Contents.json index 13613e3e..41ece9b5 100644 --- a/TowerForge/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/TowerForge/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,6 +1,7 @@ { "images" : [ { + "filename" : "tficon.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" diff --git a/TowerForge/Assets.xcassets/AppIcon.appiconset/tficon.png b/TowerForge/Assets.xcassets/AppIcon.appiconset/tficon.png new file mode 100644 index 0000000000000000000000000000000000000000..d21100aa68f5eec6a2ab82767bcb77f390f84904 GIT binary patch literal 52083 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 literal 0 HcmV?d00001 From 4d56ff72ee94838ec1dffef7deadbc1de91bff30 Mon Sep 17 00:00:00 2001 From: Rubesh Date: Sat, 20 Apr 2024 21:50:50 +0800 Subject: [PATCH 14/14] Remove commented code --- .../ViewControllers/MainMenuViewController.swift | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/TowerForge/TowerForge/ViewControllers/MainMenuViewController.swift b/TowerForge/TowerForge/ViewControllers/MainMenuViewController.swift index bc69047b..58acddf0 100644 --- a/TowerForge/TowerForge/ViewControllers/MainMenuViewController.swift +++ b/TowerForge/TowerForge/ViewControllers/MainMenuViewController.swift @@ -14,16 +14,4 @@ class MainMenuViewController: UIViewController { self.navigationController?.isNavigationBarHidden = false AudioManager.shared.playMainMusic() } - - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - if segue.identifier == "showPlayerStats" { - // Get the new view controller using segue.destination. - // Pass the selected object to the new view controller. - if let playerStatsVC = segue.destination as? PlayerStatsViewController { - // Set any properties of playerStatsVC here - // For example, pass an AchievementsDatabase instance if needed - } - } - } - }