diff --git a/QuackFight/AppDelegate.swift b/QuackFight/AppDelegate.swift index 7f2335e..4a936ec 100644 --- a/QuackFight/AppDelegate.swift +++ b/QuackFight/AppDelegate.swift @@ -16,6 +16,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { configureAudioSession() requestMicrophonePermission() + AudioManager.shared.startBGM() return true } diff --git a/QuackFight/Resources/.gitkeep b/QuackFight/Resources/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/QuackFight/Resources/bgm.mp3 b/QuackFight/Resources/bgm.mp3 new file mode 100644 index 0000000..88d17a5 Binary files /dev/null and b/QuackFight/Resources/bgm.mp3 differ diff --git a/QuackFight/Sources/Managers/AudioManager.swift b/QuackFight/Sources/Managers/AudioManager.swift new file mode 100644 index 0000000..3d54ed7 --- /dev/null +++ b/QuackFight/Sources/Managers/AudioManager.swift @@ -0,0 +1,101 @@ +// +// AudioManager.swift +// QuackFight +// +// Created by Justin Chow on 20/05/26. +// + +import AVFoundation +import UIKit + +final class AudioManager: NSObject { + + static let shared = AudioManager() + + private var bgmPlayer: AVAudioPlayer? + + private override init() { + super.init() + prepareBGM() + observeInterruptions() + observeAppLifecycle() + } + + // MARK: - BGM + + private func prepareBGM() { + guard let url = Bundle.main.url(forResource: "bgm", withExtension: "mp3") else { + print("AudioManager: bgm.mp3 not found in bundle — add it to the Xcode target's Copy Bundle Resources phase") + return + } + do { + bgmPlayer = try AVAudioPlayer(contentsOf: url) + bgmPlayer?.numberOfLoops = -1 + bgmPlayer?.volume = 0.6 + bgmPlayer?.prepareToPlay() + } catch { + print("AudioManager: failed to prepare BGM — \(error)") + } + } + + func startBGM() { + guard let player = bgmPlayer, !player.isPlaying else { return } + player.play() + } + + func pauseBGM() { + bgmPlayer?.pause() + } + + func resumeBGM() { + guard let player = bgmPlayer, !player.isPlaying else { return } + player.play() + } + + // MARK: - Interruption Handling (phone calls, Siri, etc.) + + private func observeInterruptions() { + NotificationCenter.default.addObserver( + self, + selector: #selector(handleInterruption(_:)), + name: AVAudioSession.interruptionNotification, + object: AVAudioSession.sharedInstance() + ) + } + + @objc private func handleInterruption(_ notification: Notification) { + guard + let info = notification.userInfo, + let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt, + let type = AVAudioSession.InterruptionType(rawValue: typeValue) + else { return } + + switch type { + case .began: + pauseBGM() + case .ended: + let options = AVAudioSession.InterruptionOptions( + rawValue: info[AVAudioSessionInterruptionOptionKey] as? UInt ?? 0 + ) + if options.contains(.shouldResume) { resumeBGM() } + @unknown default: + break + } + } + + // MARK: - App Lifecycle + + private func observeAppLifecycle() { + NotificationCenter.default.addObserver( + self, selector: #selector(appDidBackground), + name: UIApplication.didEnterBackgroundNotification, object: nil + ) + NotificationCenter.default.addObserver( + self, selector: #selector(appWillForeground), + name: UIApplication.willEnterForegroundNotification, object: nil + ) + } + + @objc private func appDidBackground() { pauseBGM() } + @objc private func appWillForeground() { resumeBGM() } +} diff --git a/Quack_Fight_GDD.md b/Quack_Fight_GDD.md index 7d7c196..445ece6 100644 --- a/Quack_Fight_GDD.md +++ b/Quack_Fight_GDD.md @@ -5,6 +5,7 @@ This document serves as a comprehensive blueprint for the "Quack Fight" game. It --- ## 1. Game Overview + **Title:** Quack Fight **Genre:** 1v1 Turn-Based Artillery Game (e.g., Worms, Bowmasters) **Platform:** iOS (iPhone) @@ -13,17 +14,20 @@ This document serves as a comprehensive blueprint for the "Quack Fight" game. It --- ## 2. Technical Stack & Architecture + - **Frameworks:** SpriteKit (2D Canvas, Physics, Camera, UI Nodes), GameplayKit (ECS, State Machine), CoreMotion (Gyroscope), AVFoundation (Microphone). - **UI Paradigm:** 100% SpriteKit. SwiftUI is **NOT** used for game overlays. UI is handled via `SKNode` subclasses (e.g., `HUDNode`, `TurnHandoffOverlay`). - **Architectural Pattern:** Strict Entity-Component-System (ECS). - - **Entities:** Pure identifiers/containers (e.g., `PlayerEntity`, `CameraEntity`, `BreadEntity`). - - **Components:** Pure data objects (`HealthComponent`, `TransformComponent`, `VelocityComponent`). - - **Systems:** Logic processors that iterate over components (`PhysicsSystem`, `DamageSystem`, `InputSystem`). - - **State Machine:** A global `GameStateMachine` rigidly controls the flow of a single turn. - - **Event Bus:** Centralized `EventBus` for decoupling systems (e.g., `onPlayerHit`, `onSkillUsed`). + - **Entities:** Pure identifiers/containers (e.g., `PlayerEntity`, `CameraEntity`, `BreadEntity`). + - **Components:** Pure data objects (`HealthComponent`, `TransformComponent`, `VelocityComponent`). + - **Systems:** Logic processors that iterate over components (`PhysicsSystem`, `DamageSystem`, `InputSystem`). + - **State Machine:** A global `GameStateMachine` rigidly controls the flow of a single turn. + - **Event Bus:** Centralized `EventBus` for decoupling systems (e.g., `onPlayerHit`, `onSkillUsed`). ### Directory Structure Reference + The architecture must strictly follow this folder tree: + ```text QuackFight/ ├── Sources/ @@ -40,25 +44,27 @@ QuackFight/ │ ├── UI/ (HUDNode, PowerBarNode, TrajectoryOverlayNode, SkillSelectionViewController, TurnHandoffOverlay) │ ├── Types/ (GameTypes, GameConstants, GameErrors) │ └── Extensions/ (CGPoint+Helpers, GKEntity+Components, SKNode+ECS) +├── Resources/ ``` --- ## 3. Game State Machine (Turn Flow) + The `GameStateMachine` cycles through these states every turn: 1. **`TurnHandoffState`**: A full-screen dark overlay dims the screen showing "Player 1 Turn" or "Player 2 Turn". This auto-dismisses after a short delay (no tap required). 2. **`PreviewPanState`**: The camera pans smoothly from the opponent to the active player. -3. **`AimState`**: - - **Duration:** 5.0 seconds. - - **Input:** CoreMotion Gyroscope. Tilting the phone adjusts the aiming angle. - - **Visual:** A dashed line with an arrowhead indicates trajectory (`TrajectoryRenderSystem`). A screen flash with "Tilt to Aim" auto-dismisses upon entry. - - **Skip:** Tapping the screen locks the angle immediately. -4. **`PowerState`**: - - **Duration:** 5.0 seconds. - - **Input:** Microphone (`AVAudioEngine`). Shouting charges power. - - **Visual:** A voice meter appears beside the player and scales vertically based on microphone decibels. A screen flash with "Shout!" auto-dismisses upon entry. - - **Skip:** Tapping the screen locks the power immediately. +3. **`AimState`**: + - **Duration:** 5.0 seconds. + - **Input:** CoreMotion Gyroscope. Tilting the phone adjusts the aiming angle. + - **Visual:** A dashed line with an arrowhead indicates trajectory (`TrajectoryRenderSystem`). A screen flash with "Tilt to Aim" auto-dismisses upon entry. + - **Skip:** Tapping the screen locks the angle immediately. +4. **`PowerState`**: + - **Duration:** 5.0 seconds. + - **Input:** Microphone (`AVAudioEngine`). Shouting charges power. + - **Visual:** A voice meter appears beside the player and scales vertically based on microphone decibels. A screen flash with "Shout!" auto-dismisses upon entry. + - **Skip:** Tapping the screen locks the power immediately. 5. **`ThrowResolveState`**: Projectile is spawned, camera follows it, hits are detected by `HitDetectionSystem`, and damage is dealt by `DamageSystem`. 6. **`HealResolveState`**: Specific state triggered if the Heal skill is used. 7. **`FixedHitResolveState`**: Specific state triggered if the Fixed Hit skill is used. @@ -67,59 +73,63 @@ The `GameStateMachine` cycles through these states every turn: --- ## 4. Characters & Physics + - **Characters:** Player 1 ("Goose", left), Player 2 ("Duck", right, `xScale = -1`). Fixed height exactly `100px`. Static physics bodies. - **Camera System:** Constrained strictly within the `Background` image boundaries. Hits a "hard wall" at the boundaries when tracking flying projectiles. --- ## 5. Combat, Weapons & Cycles + Damage scales based on the current round/cycle managed by `DamageCycleManager`. After the third cycle, it goes back to cycle 1. - **Cycle 1 & 2:** - - **Asset:** `BaseBread` - - **Base Damage:** 10 + - **Asset:** `BaseBread` + - **Base Damage:** 10 - **Cycle 3:** - - **Asset:** `BaseToaster` - - **Base Damage:** 15 - + - **Asset:** `BaseToaster` + - **Base Damage:** 15 + --- ## 6. Skills + Each player has 3 one-time-use skills. They can be activated during `AimState` or `PowerState`. 1. **2x Damage (Asset: `2xSkill`)** - - Modifies the next attack. Turn continues normally. - - **Projectile Asset:** Changes to `Skill1Bread` (if cycle 1-2) or `Skill1Toaster` (if cycle 3). - - **Animation:** Plays a "fire bread" or "fire toaster" animation from the projectile folder. - - **Damage:** 20 (2x10) or 30 (2x15) depending on the cycle. + - Modifies the next attack. Turn continues normally. + - **Projectile Asset:** Changes to `Skill1Bread` (if cycle 1-2) or `Skill1Toaster` (if cycle 3). + - **Animation:** Plays a "fire bread" or "fire toaster" animation from the projectile folder. + - **Damage:** 20 (2x10) or 30 (2x15) depending on the cycle. 2. **Self Heal (Asset: `HealSkill`)** - - Heals user by current weapon damage (10 or 15). - - **Execution:** Immediately interrupts the turn and enters `HealResolveState`. - - **Animation:** Plays an "eating the bread" animation on the player character, then skips to the next player's turn. + - Heals user by current weapon damage (10 or 15). + - **Execution:** Immediately interrupts the turn and enters `HealResolveState`. + - **Animation:** Plays an "eating the bread" animation on the player character, then skips to the next player's turn. 3. **Fixed Hit (Asset: `FixedHitSkill`)** - - Unmissable attack that bypasses aiming/power. - - **Execution:** Immediately interrupts the turn and enters `FixedHitResolveState`. - - **Animation Sequence:** - 1. Camera pans to the enemy location. - 2. A "missile" spawns from outside the screen (Asset path: `projectile/FixedHitMissile` - a burning missile animation) and flies into the enemy. - 3. On impact, plays an explosion animation from `Player/Duck/Explode` or `Player/Goose/Explode`. - - **Damage:** Applies current round damage (10 or 15). + - Unmissable attack that bypasses aiming/power. + - **Execution:** Immediately interrupts the turn and enters `FixedHitResolveState`. + - **Animation Sequence:** + 1. Camera pans to the enemy location. + 2. A "missile" spawns from outside the screen (Asset path: `projectile/FixedHitMissile` - a burning missile animation) and flies into the enemy. + 3. On impact, plays an explosion animation from `Player/Duck/Explode` or `Player/Goose/Explode`. + - **Damage:** Applies current round damage (10 or 15). --- ## 7. User Interface (SpriteKit `UISystem`) + UI is built entirely using `SKNode` subclasses layered over the game scene. - **Top HUD (HP Bars):** - - **Asset:** `HUD/HPBar` provides a template border. - - **Fill:** The actual health progress bar is drawn programmatically using SpriteKit shapes/code behind the border. - - **Text:** Displays remaining HP (e.g., `90`) and floating damage text when hit (e.g., `-15`). + - **Asset:** `HUD/HPBar` provides a template border. + - **Fill:** The actual health progress bar is drawn programmatically using SpriteKit shapes/code behind the border. + - **Text:** Displays remaining HP (e.g., `90`) and floating damage text when hit (e.g., `-15`). - **Bottom HUD (`HUDNode`):** - - Full width background using the `BottomHUDBackground` asset. - - **Timer Bar:** The `BottomHUDTimerBar` asset sits at the top of the HUD and visually shrinks from right-to-left based on remaining state time. - - **Skill Buttons:** 3 buttons centered in the HUD. Use `2xSkill`, `HealSkill`, `FixedHitSkill`. When consumed, they switch to their `_Disabled` counterparts. - - **No UI Text:** There are no timer numbers, angle numbers, or hint text on the bottom HUD. + - Full width background using the `BottomHUDBackground` asset. + - **Timer Bar:** The `BottomHUDTimerBar` asset sits at the top of the HUD and visually shrinks from right-to-left based on remaining state time. + - **Skill Buttons:** 3 buttons centered in the HUD. Use `2xSkill`, `HealSkill`, `FixedHitSkill`. When consumed, they switch to their `_Disabled` counterparts. + - **No UI Text:** There are no timer numbers, angle numbers, or hint text on the bottom HUD. - **Instruction Overlays:** - - Auto-dismissing dark overlays handled by the `UISystem` for "Player 1/2 Turn", "Tilt to Aim", and "Shout!". + - Auto-dismissing dark overlays handled by the `UISystem` for "Player 1/2 Turn", "Tilt to Aim", and "Shout!".