diff --git a/Source/Engine/Actor.swift b/Source/Engine/Actor.swift index f8e581f..217932c 100644 --- a/Source/Engine/Actor.swift +++ b/Source/Engine/Actor.swift @@ -41,6 +41,10 @@ public extension Actor { return rect.intersection(with: door.rect) } + func intersection(with pushwall: Pushwall) -> Vector? { + return rect.intersection(with: pushwall.rect) + } + func intersection(with world: World) -> Vector? { if let intersection = intersection(with: world.map) { return intersection @@ -50,6 +54,11 @@ public extension Actor { return intersection } } + for pushwall in world.pushwalls where pushwall.position != position { + if let intersection = intersection(with: pushwall) { + return intersection + } + } return nil } @@ -67,4 +76,20 @@ public extension Actor { attempts -= 1 } } + + func isStuck(in world: World) -> Bool { + // If outside map + if position.x < 1 || position.x > world.map.size.x - 1 || + position.y < 1 || position.y > world.map.size.y - 1 { + return true + } + // If stuck in a wall + if world.map[Int(position.x), Int(position.y)].isWall { + return true + } + // If stuck in pushwall + return world.pushwalls.contains(where: { + abs(position.x - $0.position.x) < 0.6 && abs(position.y - $0.position.y) < 0.6 + }) + } } diff --git a/Source/Engine/Color.swift b/Source/Engine/Color.swift index b0b590f..6081eee 100644 --- a/Source/Engine/Color.swift +++ b/Source/Engine/Color.swift @@ -25,8 +25,7 @@ public extension Color { static let clear = Color(r: 0, g: 0, b: 0, a: 0) static let black = Color(r: 0, g: 0, b: 0) static let white = Color(r: 255, g: 255, b: 255) - static let gray = Color(r: 192, g: 192, b: 192) - static let red = Color(r: 255, g: 0, b: 0) - static let green = Color(r: 0, g: 255, b: 0) - static let blue = Color(r: 0, g: 0, b: 255) + static let red = Color(r: 217, g: 87, b: 99) + static let green = Color(r: 153, g: 229, b: 80) + static let yellow = Color(r: 251, g: 242, b: 54) } diff --git a/Source/Engine/Door.swift b/Source/Engine/Door.swift index 2372e7f..de2848f 100644 --- a/Source/Engine/Door.swift +++ b/Source/Engine/Door.swift @@ -37,7 +37,8 @@ public struct Door { public extension Door { var rect: Rect { let position = self.position + direction * (offset - 0.5) - return Rect(min: position, max: position + direction) + let depth = direction.orthogonal * 0.1 + return Rect(min: position + depth, max: position + direction - depth) } var offset: Double { @@ -70,8 +71,13 @@ public extension Door { mutating func update(in world: inout World) { switch state { case .closed: - if world.player.intersection(with: self) != nil { + if world.player.intersection(with: self) != nil || + world.monsters.contains(where: { monster in + monster.isDead == false && + monster.intersection(with: self) != nil + }) { state = .opening + world.playSound(.doorSlide, at: position) time = 0 } case .opening: @@ -82,6 +88,7 @@ public extension Door { case .open: if time >= closeDelay { state = .closing + world.playSound(.doorSlide, at: position) time = 0 } case .closing: diff --git a/Source/Engine/Font.swift b/Source/Engine/Font.swift new file mode 100644 index 0000000..3055737 --- /dev/null +++ b/Source/Engine/Font.swift @@ -0,0 +1,12 @@ +// +// Font.swift +// Engine +// +// Created by Nick Lockwood on 21/04/2020. +// Copyright © 2020 Nick Lockwood. All rights reserved. +// + +public struct Font: Decodable { + public let texture: Texture + public let characters: [String] +} diff --git a/Source/Engine/Game.swift b/Source/Engine/Game.swift new file mode 100644 index 0000000..3a12b7b --- /dev/null +++ b/Source/Engine/Game.swift @@ -0,0 +1,78 @@ +// +// Game.swift +// Engine +// +// Created by Nick Lockwood on 07/10/2019. +// Copyright © 2019 Nick Lockwood. All rights reserved. +// + +public protocol GameDelegate: AnyObject { + func playSound(_ sound: Sound) + func clearSounds() +} + +public enum GameState { + case title + case starting + case playing +} + +public struct Game { + public weak var delegate: GameDelegate? + public let levels: [Tilemap] + public private(set) var world: World + public private(set) var state: GameState + public private(set) var transition: Effect? + public let font: Font + public var titleText = "TAP TO START" + + public init(levels: [Tilemap], font: Font) { + self.state = .title + self.levels = levels + self.world = World(map: levels[0]) + self.font = font + } +} + +public extension Game { + var hud: HUD { + return HUD(player: world.player, font: font) + } + + mutating func update(timeStep: Double, input: Input) { + guard let delegate = delegate else { + return + } + + // Update transition + if var effect = transition { + effect.time += timeStep + transition = effect + } + + // Update state + switch state { + case .title: + if input.isFiring { + transition = Effect(type: .fadeOut, color: .black, duration: 0.5) + state = .starting + } + case .starting: + if transition?.isCompleted == true { + transition = Effect(type: .fadeIn, color: .black, duration: 0.5) + state = .playing + } + case .playing: + if let action = world.update(timeStep: timeStep, input: input) { + switch action { + case .loadLevel(let index): + let index = index % levels.count + world.setLevel(levels[index]) + delegate.clearSounds() + case .playSounds(let sounds): + sounds.forEach(delegate.playSound) + } + } + } + } +} diff --git a/Source/Engine/HUD.swift b/Source/Engine/HUD.swift new file mode 100644 index 0000000..d9c3797 --- /dev/null +++ b/Source/Engine/HUD.swift @@ -0,0 +1,35 @@ +// +// HUD.swift +// Engine +// +// Created by Nick Lockwood on 19/04/2020. +// Copyright © 2020 Nick Lockwood. All rights reserved. +// + +public struct HUD { + public let healthString: String + public let healthTint: Color + public let ammoString: String + public let rightWeapon: Texture + public let leftWeapon: Texture? + public let weaponIcon: Texture + public let font: Font + + public init(player: Player, font: Font) { + let health = Int(max(0, player.health)) + switch health { + case ...10: + self.healthTint = .red + case 10 ... 30: + self.healthTint = .yellow + default: + self.healthTint = .green + } + self.healthString = String(health) + self.ammoString = String(Int(max(0, min(99, player.ammo)))) + self.rightWeapon = player.rightWeapon.animation.texture + self.leftWeapon = player.leftWeapon.map { $0.animation.texture } + self.weaponIcon = player.rightWeapon.attributes.hudIcon + self.font = font + } +} diff --git a/Source/Engine/Monster.swift b/Source/Engine/Monster.swift index 1de258d..f585a17 100644 --- a/Source/Engine/Monster.swift +++ b/Source/Engine/Monster.swift @@ -9,6 +9,7 @@ public enum MonsterState { case idle case chasing + case blocked case scratching case hurt case dead @@ -24,6 +25,7 @@ public struct Monster: Actor { public var animation: Animation = .monsterIdle public let attackCooldown: Double = 0.4 public private(set) var lastAttackTime: Double = 0 + public private(set) var path: [Vector] = [] public init(position: Vector) { self.position = position @@ -38,26 +40,42 @@ public extension Monster { mutating func update(in world: inout World) { switch state { case .idle: - if canSeePlayer(in: world) { + if canSeePlayer(in: world) || canHearPlayer(in: world) { state = .chasing animation = .monsterWalk + world.playSound(.monsterGroan, at: position) } case .chasing: - guard canSeePlayer(in: world) else { - state = .idle - animation = .monsterIdle - velocity = Vector(x: 0, y: 0) + if canSeePlayer(in: world) || canHearPlayer(in: world) { + path = world.findPath(from: position, to: world.player.position) + if canReachPlayer(in: world) { + state = .scratching + animation = .monsterScratch + lastAttackTime = -attackCooldown + velocity = Vector(x: 0, y: 0) + break + } + } + guard let destination = path.first else { break } - if canReachPlayer(in: world) { - state = .scratching - animation = .monsterScratch - lastAttackTime = -attackCooldown - velocity = Vector(x: 0, y: 0) + let direction = destination - position + let distance = direction.length + if distance < 0.1 { + path.removeFirst() break } - let direction = world.player.position - position - velocity = direction * (speed / direction.length) + velocity = direction * (speed / distance) + if world.monsters.contains(where: isBlocked(by:)) { + state = .blocked + animation = .monsterBlocked + velocity = Vector(x: 0, y: 0) + } + case .blocked: + if animation.isCompleted { + state = .chasing + animation = .monsterWalk + } case .scratching: guard canReachPlayer(in: world) else { state = .chasing @@ -67,11 +85,12 @@ public extension Monster { if animation.time - lastAttackTime >= attackCooldown { lastAttackTime = animation.time world.hurtPlayer(10) + world.playSound(.monsterSwipe, at: position) } case .hurt: if animation.isCompleted { - state = .idle - animation = .monsterIdle + state = .chasing + animation = .monsterWalk } case .dead: if animation.isCompleted { @@ -80,13 +99,47 @@ public extension Monster { } } + func isBlocked(by other: Monster) -> Bool { + // Ignore dead or inactive monsters + if other.isDead || other.state != .chasing { + return false + } + // Ignore if too far away + let direction = other.position - position + let distance = direction.length + if distance > radius + other.radius + 0.5 { + return false + } + // Is standing in the direction we're moving + return (direction / distance).dot(velocity / velocity.length) > 0.5 + } + func canSeePlayer(in world: World) -> Bool { - let direction = world.player.position - position + var direction = world.player.position - position let playerDistance = direction.length - let ray = Ray(origin: position, direction: direction / playerDistance) - let wallHit = world.hitTest(ray) - let wallDistance = (wallHit - position).length - return wallDistance > playerDistance + direction /= playerDistance + let orthogonal = direction.orthogonal + for offset in [-0.2, 0.2] { + let origin = position + orthogonal * offset + let ray = Ray(origin: origin, direction: direction) + let wallHit = world.hitTest(ray) + let wallDistance = (wallHit - position).length + if wallDistance > playerDistance { + return true + } + } + return false + } + + func canHearPlayer(in world: World) -> Bool { + guard world.player.isFiring else { + return false + } + return world.findPath( + from: position, + to: world.player.position, + maxDistance: 12 + ).isEmpty == false } func canReachPlayer(in world: World) -> Bool { @@ -120,6 +173,9 @@ public extension Animation { static let monsterIdle = Animation(frames: [ .monster ], duration: 0) + static let monsterBlocked = Animation(frames: [ + .monster + ], duration: 1) static let monsterWalk = Animation(frames: [ .monsterWalk1, .monster, diff --git a/Source/Engine/Pathfinder.swift b/Source/Engine/Pathfinder.swift new file mode 100644 index 0000000..b7a1195 --- /dev/null +++ b/Source/Engine/Pathfinder.swift @@ -0,0 +1,87 @@ +// +// Pathfinder.swift +// Engine +// +// Created by Nick Lockwood on 10/02/2020. +// Copyright © 2020 Nick Lockwood. All rights reserved. +// + +import Foundation + +public protocol Graph { + associatedtype Node: Hashable + + func nodesConnectedTo(_ node: Node) -> [Node] + func estimatedDistance(from a: Node, to b: Node) -> Double + func stepDistance(from a: Node, to b: Node) -> Double +} + +private class Path { + let head: Node + let tail: Path? + let distanceTravelled: Double + let totalDistance: Double + + init(head: Node, tail: Path?, stepDistance: Double, remaining: Double) { + self.head = head + self.tail = tail + self.distanceTravelled = (tail?.distanceTravelled ?? 0) + stepDistance + self.totalDistance = distanceTravelled + remaining + } + + var nodes: [Node] { + var nodes = [head] + var tail = self.tail + while let path = tail { + nodes.insert(path.head, at: 0) + tail = path.tail + } + nodes.removeFirst() + return nodes + } +} + +public extension Graph { + func findPath(from start: Node, to end: Node, maxDistance: Double) -> [Node] { + var visited = Set([start]) + var paths = [Path( + head: start, + tail: nil, + stepDistance: 0, + remaining: estimatedDistance(from: start, to: end) + )] + + while let path = paths.popLast() { + // Finish if goal reached + if path.head == end { + return path.nodes + } + + // Get connected nodes + for node in nodesConnectedTo(path.head) where !visited.contains(node) { + visited.insert(node) + let next = Path( + head: node, + tail: path, + stepDistance: stepDistance(from: path.head, to: node), + remaining: estimatedDistance(from: node, to: end) + ) + // Skip this node if max distance exceeded + if next.totalDistance > maxDistance { + break + } + // Insert shortest path last + if let index = paths.firstIndex(where: { + $0.totalDistance <= next.totalDistance + }) { + paths.insert(next, at: index) + } else { + paths.append(next) + } + } + } + + // Unreachable + return [] + } +} diff --git a/Source/Engine/Pickup.swift b/Source/Engine/Pickup.swift new file mode 100644 index 0000000..abcc39f --- /dev/null +++ b/Source/Engine/Pickup.swift @@ -0,0 +1,46 @@ +// +// Pickup.swift +// Engine +// +// Created by Nick Lockwood on 27/01/2020. +// Copyright © 2020 Nick Lockwood. All rights reserved. +// + +public enum PickupType { + case medkit + case shotgun +} + +public struct Pickup: Actor { + public let type: PickupType + public var radius: Double = 0.5 + public var position: Vector + + public init(type: PickupType, position: Vector) { + self.type = type + self.position = position + } +} + +public extension Pickup { + var isDead: Bool { return false } + + var texture: Texture { + switch type { + case .medkit: + return .medkit + case .shotgun: + return .shotgunPickup + } + } + + func billboard(for ray: Ray) -> Billboard { + let plane = ray.direction.orthogonal + return Billboard( + start: position - plane / 2, + direction: plane, + length: 1, + texture: texture + ) + } +} diff --git a/Source/Engine/Player.swift b/Source/Engine/Player.swift index e220676..c2fdd8c 100644 --- a/Source/Engine/Player.swift +++ b/Source/Engine/Player.swift @@ -6,11 +6,6 @@ // Copyright © 2019 Nick Lockwood. All rights reserved. // -public enum PlayerState { - case idle - case firing -} - public struct Player: Actor { public let speed: Double = 2 public let turningSpeed: Double = .pi @@ -19,15 +14,18 @@ public struct Player: Actor { public var velocity: Vector public var direction: Vector public var health: Double - public var state: PlayerState = .idle - public var animation: Animation = .pistolIdle - public let attackCooldown: Double = 0.25 + public var leftWeapon: Weapon? + public var rightWeapon: Weapon = Weapon(type: .pistol) + public private(set) var ammo: Double + public let soundChannel: Int - public init(position: Vector) { + public init(position: Vector, soundChannel: Int) { self.position = position self.velocity = Vector(x: 0, y: 0) self.direction = Vector(x: 1, y: 0) self.health = 100 + self.soundChannel = soundChannel + self.ammo = rightWeapon.attributes.defaultAmmo } } @@ -36,47 +34,77 @@ public extension Player { return health <= 0 } - var canFire: Bool { - switch state { - case .idle: - return true - case .firing: - return animation.time >= attackCooldown + var isMoving: Bool { + return velocity.x != 0 || velocity.y != 0 + } + + var isFiring: Bool { + return rightWeapon.state == .firing || leftWeapon?.state == .firing + } + + mutating func setWeapon(_ weapon: WeaponType) { + if rightWeapon.type == weapon { + leftWeapon = Weapon(type: weapon) + ammo += weapon.attributes.defaultAmmo + } else { + rightWeapon = Weapon(type: weapon) + leftWeapon = nil + ammo = weapon.attributes.defaultAmmo } } + mutating func inherit(from player: Player) { + health = player.health + rightWeapon = Weapon(type: player.rightWeapon.type) + leftWeapon = player.leftWeapon.map { Weapon(type: $0.type) } + ammo = player.ammo + } + mutating func update(with input: Input, in world: inout World) { + let wasMoving = isMoving direction = direction.rotated(by: input.rotation) velocity = direction * input.speed * speed - if input.isFiring, canFire { - state = .firing - animation = .pistolFire - let ray = Ray(origin: position, direction: direction) - if let index = world.pickMonster(ray) { - world.hurtMonster(at: index, damage: 10) + if input.isFiring, ammo > 0, rightWeapon.fire() || leftWeapon?.fire() == true { + ammo -= 1 + world.playSound(rightWeapon.attributes.fireSound, at: position) + let projectiles = rightWeapon.attributes.projectiles + var hitPosition, missPosition: Vector? + for _ in 0 ..< projectiles { + let spread = rightWeapon.attributes.spread + let sine = Double.random(in: -spread ... spread) + let cosine = (1 - sine * sine).squareRoot() + let rotation = Rotation(sine: sine, cosine: cosine) + let direction = self.direction.rotated(by: rotation) + let ray = Ray(origin: position, direction: direction) + if let index = world.pickMonster(ray) { + let damage = rightWeapon.attributes.damage / Double(projectiles) + world.hurtMonster(at: index, damage: damage) + hitPosition = world.monsters[index].position + } else { + missPosition = world.hitTest(ray) + } } - } - switch state { - case .idle: - break - case .firing: - if animation.isCompleted { - state = .idle - animation = .pistolIdle + if let hitPosition = hitPosition { + world.playSound(.monsterHit, at: hitPosition) + } + if let missPosition = missPosition { + world.playSound(.ricochet, at: missPosition) } } + leftWeapon?.update(in: &world) + if ammo < 2, leftWeapon?.state != .firing { + leftWeapon = nil + } + rightWeapon.update(in: &world) + if ammo == 0, rightWeapon.state == .idle, leftWeapon == nil { + rightWeapon = Weapon(type: .pistol) + leftWeapon = nil + ammo = rightWeapon.attributes.defaultAmmo + } + if isMoving, !wasMoving { + world.playSound(.playerWalk, at: position, in: soundChannel) + } else if !isMoving { + world.playSound(nil, at: position, in: soundChannel) + } } } - -public extension Animation { - static let pistolIdle = Animation(frames: [ - .pistol - ], duration: 0) - static let pistolFire = Animation(frames: [ - .pistolFire1, - .pistolFire2, - .pistolFire3, - .pistolFire4, - .pistol - ], duration: 0.5) -} diff --git a/Source/Engine/Pushwall.swift b/Source/Engine/Pushwall.swift new file mode 100644 index 0000000..0326f8c --- /dev/null +++ b/Source/Engine/Pushwall.swift @@ -0,0 +1,73 @@ +// +// Pushwall.swift +// Engine +// +// Created by Nick Lockwood on 30/09/2019. +// Copyright © 2019 Nick Lockwood. All rights reserved. +// + +public struct Pushwall: Actor { + public let radius: Double = 0.5 + public let speed: Double = 0.25 + public var position: Vector + public var velocity: Vector + public let tile: Tile + public let soundChannel: Int + + public init(position: Vector, tile: Tile, soundChannel: Int) { + self.position = position + self.velocity = Vector(x: 0, y: 0) + self.tile = tile + self.soundChannel = soundChannel + } +} + +public extension Pushwall { + var isDead: Bool { return false } + + func billboards(facing viewpoint: Vector) -> [Billboard] { + let topLeft = rect.min, bottomRight = rect.max + let topRight = Vector(x: bottomRight.x, y: topLeft.y) + let bottomLeft = Vector(x: topLeft.x, y: bottomRight.y) + let textures = tile.textures + return [ + Billboard(start: topLeft, direction: Vector(x: 0, y: 1), length: 1, texture: textures[0]), + Billboard(start: topRight, direction: Vector(x: -1, y: 0), length: 1, texture: textures[1]), + Billboard(start: bottomRight, direction: Vector(x: 0, y: -1), length: 1, texture: textures[0]), + Billboard(start: bottomLeft, direction: Vector(x: 1, y: 0), length: 1, texture: textures[1]) + ].filter { billboard in + let ray = billboard.start - viewpoint + let faceNormal = billboard.direction.orthogonal + return ray.dot(faceNormal) < 0 + } + } + + var isMoving: Bool { + return velocity.x != 0 || velocity.y != 0 + } + + mutating func update(in world: inout World) { + let wasMoving = isMoving + if isMoving == false, let intersection = world.player.intersection(with: self) { + if abs(intersection.x) > abs(intersection.y) { + velocity = Vector(x: intersection.x > 0 ? speed : -speed, y: 0) + } else { + velocity = Vector(x: 0, y: intersection.y > 0 ? speed : -speed) + } + } + if let intersection = self.intersection(with: world), + abs(intersection.x) > 0.001 || abs(intersection.y) > 0.001 { + velocity = Vector(x: 0, y: 0) + position.x = position.x.rounded(.down) + 0.5 + position.y = position.y.rounded(.down) + 0.5 + } + if isMoving { + world.playSound(.wallSlide, at: position, in: soundChannel) + } else if !isMoving { + world.playSound(nil, at: position, in: soundChannel) + if wasMoving { + world.playSound(.wallThud, at: position) + } + } + } +} diff --git a/Source/Engine/Rect.swift b/Source/Engine/Rect.swift index 753754c..62494d7 100644 --- a/Source/Engine/Rect.swift +++ b/Source/Engine/Rect.swift @@ -7,7 +7,7 @@ // public struct Rect { - var min, max: Vector + public var min, max: Vector public init(min: Vector, max: Vector) { self.min = min diff --git a/Source/Engine/Renderer.swift b/Source/Engine/Renderer.swift deleted file mode 100644 index 2a96bab..0000000 --- a/Source/Engine/Renderer.swift +++ /dev/null @@ -1,149 +0,0 @@ -// -// Renderer.swift -// Engine -// -// Created by Nick Lockwood on 02/06/2019. -// Copyright © 2019 Nick Lockwood. All rights reserved. -// - -private let fizzle = (0 ..< 10000).shuffled() - -public struct Renderer { - public private(set) var bitmap: Bitmap - private let textures: Textures - - public init(width: Int, height: Int, textures: Textures) { - self.bitmap = Bitmap(width: width, height: height, color: .black) - self.textures = textures - } -} - -public extension Renderer { - mutating func draw(_ world: World) { - let focalLength = 1.0 - let viewWidth = Double(bitmap.width) / Double(bitmap.height) - let viewPlane = world.player.direction.orthogonal * viewWidth - let viewCenter = world.player.position + world.player.direction * focalLength - let viewStart = viewCenter - viewPlane / 2 - - // Sort sprites by distance - var spritesByDistance: [(distance: Double, sprite: Billboard)] = [] - for sprite in world.sprites { - let spriteDistance = (sprite.start - world.player.position).length - spritesByDistance.append( - (distance: spriteDistance, sprite: sprite) - ) - } - spritesByDistance.sort(by: { $0.distance > $1.distance }) - - // Cast rays - let columns = bitmap.width - let step = viewPlane / Double(columns) - var columnPosition = viewStart - for x in 0 ..< columns { - let rayDirection = columnPosition - world.player.position - let viewPlaneDistance = rayDirection.length - let ray = Ray( - origin: world.player.position, - direction: rayDirection / viewPlaneDistance - ) - let end = world.map.hitTest(ray) - let wallDistance = (end - ray.origin).length - - // Draw wall - let wallHeight = 1.0 - let distanceRatio = viewPlaneDistance / focalLength - let perpendicular = wallDistance / distanceRatio - let height = wallHeight * focalLength / perpendicular * Double(bitmap.height) - let wallTexture: Bitmap - let wallX: Double - let (tileX, tileY) = world.map.tileCoords(at: end, from: ray.direction) - let tile = world.map[tileX, tileY] - if end.x.rounded(.down) == end.x { - let neighborX = tileX + (ray.direction.x > 0 ? -1 : 1) - let isDoor = world.isDoor(at: neighborX, tileY) - wallTexture = textures[isDoor ? .doorjamb : tile.textures[0]] - wallX = end.y - end.y.rounded(.down) - } else { - let neighborY = tileY + (ray.direction.y > 0 ? -1 : 1) - let isDoor = world.isDoor(at: tileX, neighborY) - wallTexture = textures[isDoor ? .doorjamb2 : tile.textures[1]] - wallX = end.x - end.x.rounded(.down) - } - let textureX = Int(wallX * Double(wallTexture.width)) - let wallStart = Vector(x: Double(x), y: (Double(bitmap.height) - height) / 2 - 0.001) - bitmap.drawColumn(textureX, of: wallTexture, at: wallStart, height: height) - - // Draw floor and ceiling - var floorTile: Tile! - var floorTexture, ceilingTexture: Bitmap! - let floorStart = Int(wallStart.y + height) + 1 - for y in min(floorStart, bitmap.height) ..< bitmap.height { - let normalizedY = (Double(y) / Double(bitmap.height)) * 2 - 1 - let perpendicular = wallHeight * focalLength / normalizedY - let distance = perpendicular * distanceRatio - let mapPosition = ray.origin + ray.direction * distance - let tileX = mapPosition.x.rounded(.down), tileY = mapPosition.y.rounded(.down) - let tile = world.map[Int(tileX), Int(tileY)] - if tile != floorTile { - floorTexture = textures[tile.textures[0]] - ceilingTexture = textures[tile.textures[1]] - floorTile = tile - } - let textureX = mapPosition.x - tileX, textureY = mapPosition.y - tileY - bitmap[x, y] = floorTexture[normalized: textureX, textureY] - bitmap[x, bitmap.height - 1 - y] = ceilingTexture[normalized: textureX, textureY] - } - - // Draw sprites - for (_, sprite) in spritesByDistance { - guard let hit = sprite.hitTest(ray) else { - continue - } - let spriteDistance = (hit - ray.origin).length - if spriteDistance > wallDistance { - continue - } - let perpendicular = spriteDistance / distanceRatio - let height = wallHeight / perpendicular * Double(bitmap.height) - let spriteX = (hit - sprite.start).length / sprite.length - let spriteTexture = textures[sprite.texture] - let textureX = min(Int(spriteX * Double(spriteTexture.width)), spriteTexture.width - 1) - let start = Vector(x: Double(x), y: (Double(bitmap.height) - height) / 2 + 0.001) - bitmap.drawColumn(textureX, of: spriteTexture, at: start, height: height) - } - - columnPosition += step - } - - // Player weapon - let screenHeight = Double(bitmap.height) - bitmap.drawImage( - textures[world.player.animation.texture], - at: Vector(x: Double(bitmap.width) / 2 - screenHeight / 2, y: 0), - size: Vector(x: screenHeight, y: screenHeight) - ) - - // Effects - for effect in world.effects { - switch effect.type { - case .fadeIn: - bitmap.tint(with: effect.color, opacity: 1 - effect.progress) - case .fadeOut: - bitmap.tint(with: effect.color, opacity: effect.progress) - case .fizzleOut: - let threshold = Int(effect.progress * Double(fizzle.count)) - for x in 0 ..< bitmap.width { - for y in 0 ..< bitmap.height { - let granularity = 4 - let index = y / granularity * bitmap.width + x / granularity - let fizzledIndex = fizzle[index % fizzle.count] - if fizzledIndex <= threshold { - bitmap[x, y] = effect.color - } - } - } - } - } - } -} diff --git a/Source/Engine/Sounds.swift b/Source/Engine/Sounds.swift new file mode 100644 index 0000000..35ed47f --- /dev/null +++ b/Source/Engine/Sounds.swift @@ -0,0 +1,34 @@ +// +// Sounds.swift +// Engine +// +// Created by Nick Lockwood on 11/11/2019. +// Copyright © 2019 Nick Lockwood. All rights reserved. +// + +public enum SoundName: String, CaseIterable { + case pistolFire + case shotgunFire + case shotgunPickup + case ricochet + case monsterHit + case monsterGroan + case monsterDeath + case monsterSwipe + case doorSlide + case wallSlide + case wallThud + case switchFlip + case playerDeath + case playerWalk + case squelch + case medkit +} + +public struct Sound { + public let name: SoundName? + public let channel: Int? + public let volume: Double + public let pan: Double + public let delay: Double +} diff --git a/Source/Engine/Switch.swift b/Source/Engine/Switch.swift new file mode 100644 index 0000000..fd98774 --- /dev/null +++ b/Source/Engine/Switch.swift @@ -0,0 +1,62 @@ +// +// Switch.swift +// Engine +// +// Created by Nick Lockwood on 15/08/2019. +// Copyright © 2019 Nick Lockwood. All rights reserved. +// + +public enum SwitchState { + case off + case on +} + +public struct Switch { + public let position: Vector + public var state: SwitchState = .off + public var animation: Animation = .switchOff + + public init(position: Vector) { + self.position = position + } +} + +public extension Switch { + var rect: Rect { + return Rect( + min: position - Vector(x: 0.5, y: 0.5), + max: position + Vector(x: 0.5, y: 0.5) + ) + } + + mutating func update(in world: inout World) { + switch state { + case .off: + if world.player.rect.intersection(with: self.rect) != nil { + state = .on + animation = .switchFlip + world.playSound(.switchFlip, at: position) + } + case .on: + if animation.isCompleted { + animation = .switchOn + world.endLevel() + } + } + } +} + +public extension Animation { + static let switchOff = Animation(frames: [ + .switch1 + ], duration: 0) + static let switchFlip = Animation(frames: [ + .switch1, + .switch2, + .switch3, + .switch4 + ], duration: 0.4) + static let switchOn = Animation(frames: [ + .switch4 + ], duration: 0) +} diff --git a/Source/Engine/Textures.swift b/Source/Engine/Texture.swift similarity index 53% rename from Source/Engine/Textures.swift rename to Source/Engine/Texture.swift index b74ffa6..a04114f 100644 --- a/Source/Engine/Textures.swift +++ b/Source/Engine/Texture.swift @@ -1,12 +1,12 @@ // -// Textures.swift +// Texture.swift // Engine // -// Created by Nick Lockwood on 05/06/2019. +// Created by Nick Lockwood on 13/02/2020. // Copyright © 2019 Nick Lockwood. All rights reserved. // -public enum Texture: String, CaseIterable { +public enum Texture: String, CaseIterable, Decodable { case wall, wall2 case crackWall, crackWall2 case slimeWall, slimeWall2 @@ -22,22 +22,15 @@ public enum Texture: String, CaseIterable { case monsterHurt, monsterDeath1, monsterDeath2, monsterDead case pistol case pistolFire1, pistolFire2, pistolFire3, pistolFire4 -} - -public struct Textures { - private let textures: [Texture: Bitmap] -} - -public extension Textures { - init(loader: (String) -> Bitmap) { - var textures = [Texture: Bitmap]() - for texture in Texture.allCases { - textures[texture] = loader(texture.rawValue) - } - self.init(textures: textures) - } - - subscript(_ texture: Texture) -> Bitmap { - return textures[texture]! - } + case shotgun + case shotgunFire1, shotgunFire2, shotgunFire3, shotgunFire4 + case shotgunPickup + case switch1, switch2, switch3, switch4 + case elevatorFloor, elevatorCeiling, elevatorSideWall, elevatorBackWall + case medkit + case crosshair + case healthIcon + case pistolIcon, shotgunIcon + case font + case titleBackground, titleLogo } diff --git a/Source/Engine/Thing.swift b/Source/Engine/Thing.swift index f202675..fb3908c 100644 --- a/Source/Engine/Thing.swift +++ b/Source/Engine/Thing.swift @@ -11,4 +11,8 @@ public enum Thing: Int, Decodable { case player case monster case door + case pushwall + case `switch` + case medkit + case shotgun } diff --git a/Source/Engine/Tile.swift b/Source/Engine/Tile.swift index 1e9b15b..23a04f2 100644 --- a/Source/Engine/Tile.swift +++ b/Source/Engine/Tile.swift @@ -12,14 +12,17 @@ public enum Tile: Int, Decodable { case crackWall case slimeWall case crackFloor + case elevatorFloor + case elevatorSideWall + case elevatorBackWall } public extension Tile { var isWall: Bool { switch self { - case .wall, .crackWall, .slimeWall: + case .wall, .crackWall, .slimeWall, .elevatorSideWall, .elevatorBackWall: return true - case .floor, .crackFloor: + case .floor, .crackFloor, .elevatorFloor: return false } } @@ -36,6 +39,12 @@ public extension Tile { return [.crackWall, .crackWall2] case .slimeWall: return [.slimeWall, .slimeWall2] + case .elevatorSideWall: + return [.elevatorSideWall, .elevatorSideWall] + case .elevatorBackWall: + return [.elevatorBackWall, .elevatorBackWall] + case .elevatorFloor: + return [.elevatorFloor, .elevatorCeiling] } } } diff --git a/Source/Engine/Tilemap.swift b/Source/Engine/Tilemap.swift index 8b8b409..c50cf46 100644 --- a/Source/Engine/Tilemap.swift +++ b/Source/Engine/Tilemap.swift @@ -6,10 +6,24 @@ // Copyright © 2019 Nick Lockwood. All rights reserved. // -public struct Tilemap: Decodable { - private let tiles: [Tile] +public struct MapData: Decodable { + fileprivate let tiles: [Tile] + fileprivate let things: [Thing] + fileprivate let width: Int +} + +public struct Tilemap { + private(set) var tiles: [Tile] public let things: [Thing] public let width: Int + public let index: Int + + public init(_ map: MapData, index: Int) { + self.tiles = map.tiles + self.things = map.things + self.width = map.width + self.index = index + } } public extension Tilemap { @@ -22,7 +36,8 @@ public extension Tilemap { } subscript(x: Int, y: Int) -> Tile { - return tiles[y * width + x] + get { return tiles[y * width + x] } + set { tiles[y * width + x] = newValue } } func tileCoords(at position: Vector, from direction: Vector) -> (x: Int, y: Int) { @@ -41,6 +56,18 @@ public extension Tilemap { return self[x, y] } + func closestFloorTile(to x: Int, _ y: Int) -> Tile? { + for y in max(0, y - 1) ... min(height - 1, y + 1) { + for x in max(0, x - 1) ... min(width - 1, x + 1) { + let tile = self[x, y] + if tile.isWall == false { + return tile + } + } + } + return nil + } + func hitTest(_ ray: Ray) -> Vector { var position = ray.origin let slope = ray.direction.x / ray.direction.y diff --git a/Source/Engine/Vector.swift b/Source/Engine/Vector.swift index 40ac090..f9494b7 100644 --- a/Source/Engine/Vector.swift +++ b/Source/Engine/Vector.swift @@ -6,7 +6,7 @@ // Copyright © 2019 Nick Lockwood. All rights reserved. // -public struct Vector { +public struct Vector: Hashable { public var x, y: Double public init(x: Double, y: Double) { @@ -24,6 +24,10 @@ public extension Vector { return (x * x + y * y).squareRoot() } + func dot(_ rhs: Vector) -> Double { + return x * rhs.x + y * rhs.y + } + static func + (lhs: Vector, rhs: Vector) -> Vector { return Vector(x: lhs.x + rhs.x, y: lhs.y + rhs.y) } diff --git a/Source/Engine/Weapon.swift b/Source/Engine/Weapon.swift new file mode 100644 index 0000000..4d7e404 --- /dev/null +++ b/Source/Engine/Weapon.swift @@ -0,0 +1,130 @@ +// +// Weapon.swift +// Engine +// +// Created by Nick Lockwood on 07/02/2020. +// Copyright © 2020 Nick Lockwood. All rights reserved. +// + +public enum WeaponType: Int { + case pistol + case shotgun +} + +public extension WeaponType { + struct Attributes { + let idleAnimation: Animation + let fireAnimation: Animation + let fireSound: SoundName + let damage: Double + let cooldown: Double + let projectiles: Int + let spread: Double + let defaultAmmo: Double + public let hudIcon: Texture + } + + var attributes: Attributes { + switch self { + case .pistol: + return Attributes( + idleAnimation: .pistolIdle, + fireAnimation: .pistolFire, + fireSound: .pistolFire, + damage: 10, + cooldown: 0.25, + projectiles: 1, + spread: 0, + defaultAmmo: .infinity, + hudIcon: .pistolIcon + ) + case .shotgun: + return Attributes( + idleAnimation: .shotgunIdle, + fireAnimation: .shotgunFire, + fireSound: .shotgunFire, + damage: 50, + cooldown: 0.5, + projectiles: 5, + spread: 0.4, + defaultAmmo: 10, + hudIcon: .shotgunIcon + ) + } + } +} + +public extension Animation { + static let pistolIdle = Animation(frames: [ + .pistol + ], duration: 0) + static let pistolFire = Animation(frames: [ + .pistolFire1, + .pistolFire2, + .pistolFire3, + .pistolFire4, + .pistol + ], duration: 0.5) + static let shotgunIdle = Animation(frames: [ + .shotgun + ], duration: 0) + static let shotgunFire = Animation(frames: [ + .shotgunFire1, + .shotgunFire2, + .shotgunFire3, + .shotgunFire4, + .shotgun + ], duration: 0.5) +} + +public enum WeaponState { + case idle + case firing +} + +public struct Weapon { + public var state: WeaponState = .idle + public let type: WeaponType + public var animation: Animation + + public init(type: WeaponType) { + self.type = type + self.animation = type.attributes.idleAnimation + } +} + +public extension Weapon { + var attributes: WeaponType.Attributes { + return type.attributes + } + + var canFire: Bool { + switch state { + case .idle: + return true + case .firing: + return animation.time >= type.attributes.cooldown + } + } + + mutating func fire() -> Bool { + guard canFire else { + return false + } + state = .firing + animation = type.attributes.fireAnimation + return true + } + + mutating func update(in world: inout World) { + switch state { + case .idle: + break + case .firing: + if animation.isCompleted { + state = .idle + animation = type.attributes.idleAnimation + } + } + } +} diff --git a/Source/Engine/World.swift b/Source/Engine/World.swift index acb6d39..64c40bf 100644 --- a/Source/Engine/World.swift +++ b/Source/Engine/World.swift @@ -6,18 +6,32 @@ // Copyright © 2019 Nick Lockwood. All rights reserved. // +public enum WorldAction { + case loadLevel(Int) + case playSounds([Sound]) +} + public struct World { - public let map: Tilemap + public private(set) var map: Tilemap public private(set) var doors: [Door] + public private(set) var pushwalls: [Pushwall] + public private(set) var switches: [Switch] + public private(set) var pickups: [Pickup] public private(set) var monsters: [Monster] public private(set) var player: Player! public private(set) var effects: [Effect] + public private(set) var isLevelEnded: Bool + private var sounds: [Sound] = [] public init(map: Tilemap) { self.map = map self.doors = [] + self.pushwalls = [] + self.switches = [] + self.pickups = [] self.monsters = [] self.effects = [] + self.isLevelEnded = false reset() } } @@ -27,7 +41,7 @@ public extension World { return map.size } - mutating func update(timeStep: Double, input: Input) { + mutating func update(timeStep: Double, input: Input) -> WorldAction? { // Update effects effects = effects.compactMap { effect in guard effect.time < effect.duration else { @@ -38,17 +52,28 @@ public extension World { return effect } + // Check for level end + if isLevelEnded { + if effects.isEmpty { + effects.append(Effect(type: .fadeIn, color: .black, duration: 0.5)) + return .loadLevel(map.index + 1) + } + return nil + } + // Update player if player.isDead == false { var player = self.player! - player.animation.time += timeStep + player.rightWeapon.animation.time += timeStep + player.leftWeapon?.animation.time += timeStep player.update(with: input, in: &self) player.position += player.velocity * timeStep self.player = player } else if effects.isEmpty { + player = nil reset() effects.append(Effect(type: .fadeIn, color: .red, duration: 0.5)) - return + return nil } // Update monsters @@ -68,6 +93,22 @@ public extension World { doors[i] = door } + // Update pushwalls + for i in 0 ..< pushwalls.count { + var pushwall = pushwalls[i] + pushwall.update(in: &self) + pushwall.position += pushwall.velocity * timeStep + pushwalls[i] = pushwall + } + + // Update switches + for i in 0 ..< switches.count { + var s = switches[i] + s.animation.time += timeStep + s.update(in: &self) + switches[i] = s + } + // Handle collisions for i in 0 ..< monsters.count { var monster = monsters[i] @@ -85,11 +126,43 @@ public extension World { monsters[i] = monster } player.avoidWalls(in: self) + + // Handle pickups + for i in (0 ..< pickups.count).reversed() { + let pickup = pickups[i] + if player.intersection(with: pickup) != nil { + pickups.remove(at: i) + switch pickup.type { + case .medkit: + player.health += 25 + playSound(.medkit, at: pickup.position) + effects.append(Effect(type: .fadeIn, color: .green, duration: 0.5)) + case .shotgun: + player.setWeapon(.shotgun) + playSound(.shotgunPickup, at: pickup.position) + effects.append(Effect(type: .fadeIn, color: .white, duration: 0.5)) + } + } + } + + // Check for stuck actors + if player.isStuck(in: self) { + hurtPlayer(1) + } + for i in 0 ..< monsters.count where monsters[i].isStuck(in: self) { + hurtMonster(at: i, damage: 1) + } + + // Play sounds + defer { sounds.removeAll() } + return .playSounds(sounds) } var sprites: [Billboard] { let ray = Ray(origin: player.position, direction: player.direction) return monsters.map { $0.billboard(for: ray) } + doors.map { $0.billboard } + + pushwalls.flatMap { $0.billboards(facing: player.position) } + + pickups.map { $0.billboard(for: ray) } } mutating func hurtPlayer(_ damage: Double) { @@ -102,6 +175,10 @@ public extension World { effects.append(Effect(type: .fadeIn, color: color, duration: 0.2)) if player.isDead { effects.append(Effect(type: .fizzleOut, color: .red, duration: 2)) + playSound(.playerDeath, at: player.position) + if player.isStuck(in: self) { + playSound(.squelch, at: player.position) + } } } @@ -115,6 +192,10 @@ public extension World { if monster.isDead { monster.state = .dead monster.animation = .monsterDeath + playSound(.monsterDeath, at: monster.position) + if monster.isStuck(in: self) { + playSound(.squelch, at: monster.position) + } } else { monster.state = .hurt monster.animation = .monsterHurt @@ -122,9 +203,44 @@ public extension World { monsters[index] = monster } + mutating func playSound(_ name: SoundName?, at position: Vector, in channel: Int? = nil) { + let delta = position - player.position + let distance = delta.length + let dropOff = 0.5 + let volume = 1 / (distance * distance * dropOff + 1) + let delay = distance * 2 / 343 + let direction = distance > 0 ? delta / distance : player.direction + let pan = player.direction.orthogonal.dot(direction) + sounds.append(Sound( + name: name, + channel: channel, + volume: volume, + pan: pan, + delay: delay + )) + } + + mutating func endLevel() { + isLevelEnded = true + effects.append(Effect(type: .fadeOut, color: .black, duration: 2)) + } + + mutating func setLevel(_ map: Tilemap) { + let effects = self.effects + let player = self.player! + self = World(map: map) + self.effects = effects + self.player.inherit(from: player) + } + mutating func reset() { self.monsters = [] self.doors = [] + self.switches = [] + self.pickups = [] + self.isLevelEnded = false + var pushwallCount = 0 + var soundChannel = 0 for y in 0 ..< map.height { for x in 0 ..< map.width { let position = Vector(x: Double(x) + 0.5, y: Double(y) + 0.5) @@ -133,13 +249,45 @@ public extension World { case .nothing: break case .player: - self.player = Player(position: position) + self.player = Player(position: position, soundChannel: soundChannel) + soundChannel += 1 case .monster: monsters.append(Monster(position: position)) case .door: precondition(y > 0 && y < map.height, "Door cannot be placed on map edge") let isVertical = map[x, y - 1].isWall && map[x, y + 1].isWall doors.append(Door(position: position, isVertical: isVertical)) + case .pushwall: + pushwallCount += 1 + if pushwalls.count >= pushwallCount { + let tile = pushwalls[pushwallCount - 1].tile + pushwalls[pushwallCount - 1] = Pushwall( + position: position, + tile: tile, + soundChannel: soundChannel + ) + soundChannel += 1 + break + } + var tile = map[x, y] + if tile.isWall { + map[x, y] = .floor + } else { + tile = map.closestFloorTile(to: x, y) ?? .wall + } + pushwalls.append(Pushwall( + position: position, + tile: tile, + soundChannel: soundChannel + )) + soundChannel += 1 + case .switch: + precondition(map[x, y].isWall, "Switch must be placed on a wall tile") + switches.append(Switch(position: position)) + case .medkit: + pickups.append(Pickup(type: .medkit, position: position)) + case .shotgun: + pickups.append(Pickup(type: .shotgun, position: position)) } } } @@ -148,8 +296,10 @@ public extension World { func hitTest(_ ray: Ray) -> Vector { var wallHit = map.hitTest(ray) var distance = (wallHit - ray.origin).length - for door in doors { - guard let hit = door.billboard.hitTest(ray) else { + let billboards = doors.map { $0.billboard } + + pushwalls.flatMap { $0.billboards(facing: ray.origin) } + for billboard in billboards { + guard let hit = billboard.hitTest(ray) else { continue } let hitDistance = (hit - ray.origin).length @@ -183,4 +333,77 @@ public extension World { func isDoor(at x: Int, _ y: Int) -> Bool { return map.things[y * map.width + x] == .door } + + func door(at x: Int, _ y: Int) -> Door? { + guard isDoor(at: x, y) else { + return nil + } + return doors.first(where: { + Int($0.position.x) == x && Int($0.position.y) == y + }) + } + + func pushwall(at x: Int, _ y: Int) -> Pushwall? { + return pushwalls.first(where: { + Int($0.position.x) == x && Int($0.position.y) == y + }) + } + + func `switch`(at x: Int, _ y: Int) -> Switch? { + guard map.things[y * map.width + x] == .switch else { + return nil + } + return switches.first(where: { + Int($0.position.x) == x && Int($0.position.y) == y + }) + } +} + +extension World: Graph { + public struct Node: Hashable { + public let x, y: Double + + public init(x: Double, y: Double) { + self.x = x.rounded(.down) + 0.5 + self.y = y.rounded(.down) + 0.5 + } + } + + public func findPath( + from start: Vector, + to end: Vector, + maxDistance: Double = 50 + ) -> [Vector] { + return findPath( + from: Node(x: start.x, y: start.y), + to: Node(x: end.x, y: end.y), + maxDistance: maxDistance + ).map { node in + Vector(x: node.x, y: node.y) + } + } + + public func nodesConnectedTo(_ node: Node) -> [Node] { + return [ + Node(x: node.x - 1, y: node.y), + Node(x: node.x + 1, y: node.y), + Node(x: node.x, y: node.y - 1), + Node(x: node.x, y: node.y + 1), + ].filter { node in + let x = Int(node.x), y = Int(node.y) + return map[x, y].isWall == false && pushwall(at: x, y) == nil + } + } + + public func estimatedDistance(from a: Node, to b: Node) -> Double { + return abs(b.x - a.x) + abs(b.y - a.y) + } + + public func stepDistance(from a: Node, to b: Node) -> Double { + let x = Int(b.x), y = Int(b.y) + if door(at: x, y)?.state == .closed { + return 5 + } + return 1 + } } diff --git a/Source/Rampage.xcodeproj/project.pbxproj b/Source/Rampage.xcodeproj/project.pbxproj index 8dfcb18..d7af904 100644 --- a/Source/Rampage.xcodeproj/project.pbxproj +++ b/Source/Rampage.xcodeproj/project.pbxproj @@ -7,6 +7,15 @@ objects = { /* Begin PBXBuildFile section */ + 0108A65E23F4D84C0075E1AF /* Renderer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0108A65723F4D84C0075E1AF /* Renderer.framework */; }; + 0108A65F23F4D84C0075E1AF /* Renderer.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 0108A65723F4D84C0075E1AF /* Renderer.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 0108A66323F4D8E80075E1AF /* Bitmap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D09AF222A482030052745A /* Bitmap.swift */; }; + 0108A66523F4D8F00075E1AF /* Renderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01ADC63B22B957FD00DC8AAD /* Renderer.swift */; }; + 0108A66723F4D9370075E1AF /* Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D09AF022A481AB0052745A /* Color.swift */; }; + 0108A66923F4D9B70075E1AF /* Textures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0108A66823F4D9B70075E1AF /* Textures.swift */; }; + 0108A66F23F543750075E1AF /* Pathfinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0108A66E23F543740075E1AF /* Pathfinder.swift */; }; + 0128F26223EEE7AE00439050 /* shotgunFire.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 0128F26123EEE7AE00439050 /* shotgunFire.mp3 */; }; + 0128F26423EEEB0A00439050 /* shotgunPickup.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 0128F26323EEEB0A00439050 /* shotgunPickup.mp3 */; }; 012A0C4D22C96E150068E8EF /* Tile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012A0C4C22C96E150068E8EF /* Tile.swift */; }; 012A0C4F22C96E1F0068E8EF /* Thing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012A0C4E22C96E1F0068E8EF /* Thing.swift */; }; 012A0C6222CC200E0068E8EF /* Billboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012A0C6022CC200D0068E8EF /* Billboard.swift */; }; @@ -14,7 +23,12 @@ 012A0C9E22D47C220068E8EF /* Actor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012A0C9D22D47C220068E8EF /* Actor.swift */; }; 012A0CA222D7AD0A0068E8EF /* Animation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012A0CA122D7AD0A0068E8EF /* Animation.swift */; }; 012DF10822E251CF00D52706 /* Effect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 012DF10722E251CF00D52706 /* Effect.swift */; }; + 013D492523ED607D00763FCA /* medkit.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 013D492423ED607D00763FCA /* medkit.mp3 */; }; + 013D492723EE17C000763FCA /* Weapon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 013D492623EE17C000763FCA /* Weapon.swift */; }; 01467C3E22E6F54600B5607D /* Easing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01467C3D22E6F54600B5607D /* Easing.swift */; }; + 01557AD0245109E600FF8FF0 /* HUD.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01557ACF245109E600FF8FF0 /* HUD.swift */; }; + 0159A3F523DEF636001EEB81 /* Pickup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0159A3F423DEF636001EEB81 /* Pickup.swift */; }; + 015A23C9230586E3004CBB78 /* Switch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 015A23C8230586E3004CBB78 /* Switch.swift */; }; 016E41B3228E9A5B00ACF137 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 016E41B2228E9A5B00ACF137 /* AppDelegate.swift */; }; 016E41B5228E9A5B00ACF137 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 016E41B4228E9A5B00ACF137 /* ViewController.swift */; }; 016E41B8228E9A5B00ACF137 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 016E41B6228E9A5B00ACF137 /* Main.storyboard */; }; @@ -22,25 +36,55 @@ 016E41BD228E9A5E00ACF137 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 016E41BB228E9A5E00ACF137 /* LaunchScreen.storyboard */; }; 016E41D0228E9A8600ACF137 /* Engine.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 016E41C9228E9A8600ACF137 /* Engine.framework */; }; 016E41D1228E9A8600ACF137 /* Engine.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 016E41C9228E9A8600ACF137 /* Engine.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 01ADC63C22B957FD00DC8AAD /* Renderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01ADC63B22B957FD00DC8AAD /* Renderer.swift */; }; + 0199F56223E1A517003E3F08 /* monsterSwipe.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 0199F55523E1A517003E3F08 /* monsterSwipe.mp3 */; }; + 0199F56323E1A517003E3F08 /* pistolFire.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 0199F55623E1A517003E3F08 /* pistolFire.mp3 */; }; + 0199F56423E1A517003E3F08 /* wallSlide.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 0199F55723E1A517003E3F08 /* wallSlide.mp3 */; }; + 0199F56523E1A517003E3F08 /* doorSlide.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 0199F55823E1A517003E3F08 /* doorSlide.mp3 */; }; + 0199F56623E1A517003E3F08 /* playerWalk.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 0199F55923E1A517003E3F08 /* playerWalk.mp3 */; }; + 0199F56723E1A517003E3F08 /* monsterHit.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 0199F55A23E1A517003E3F08 /* monsterHit.mp3 */; }; + 0199F56823E1A517003E3F08 /* squelch.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 0199F55B23E1A517003E3F08 /* squelch.mp3 */; }; + 0199F56923E1A517003E3F08 /* ricochet.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 0199F55C23E1A517003E3F08 /* ricochet.mp3 */; }; + 0199F56A23E1A517003E3F08 /* monsterDeath.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 0199F55D23E1A517003E3F08 /* monsterDeath.mp3 */; }; + 0199F56B23E1A517003E3F08 /* playerDeath.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 0199F55E23E1A517003E3F08 /* playerDeath.mp3 */; }; + 0199F56C23E1A517003E3F08 /* monsterGroan.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 0199F55F23E1A517003E3F08 /* monsterGroan.mp3 */; }; + 0199F56D23E1A517003E3F08 /* switchFlip.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 0199F56023E1A517003E3F08 /* switchFlip.mp3 */; }; + 0199F56E23E1A517003E3F08 /* wallThud.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 0199F56123E1A517003E3F08 /* wallThud.mp3 */; }; + 0199F57023E1AFEA003E3F08 /* Sounds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0199F56F23E1AFEA003E3F08 /* Sounds.swift */; }; + 0199F57423E242D4003E3F08 /* SoundManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0199F57323E242D4003E3F08 /* SoundManager.swift */; }; 01ADC64022B9846B00DC8AAD /* World.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01ADC63F22B9846B00DC8AAD /* World.swift */; }; - 01D09AF122A481AB0052745A /* Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D09AF022A481AB0052745A /* Color.swift */; }; - 01D09AF322A482030052745A /* Bitmap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D09AF222A482030052745A /* Bitmap.swift */; }; 01D09AF522A482450052745A /* UIImage+Bitmap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D09AF422A482450052745A /* UIImage+Bitmap.swift */; }; 01D09AF922A484B10052745A /* Vector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D09AF822A484B10052745A /* Vector.swift */; }; 01D09AFB22A485040052745A /* Player.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D09AFA22A485040052745A /* Player.swift */; }; 01D09AFD22A4873B0052745A /* Rect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D09AFC22A4873B0052745A /* Rect.swift */; }; 01D09AFF22A48E990052745A /* Tilemap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D09AFE22A48E990052745A /* Tilemap.swift */; }; - 01D09B0122A493A70052745A /* Map.json in Resources */ = {isa = PBXBuildFile; fileRef = 01D09B0022A493A70052745A /* Map.json */; }; + 01D09B0122A493A70052745A /* Levels.json in Resources */ = {isa = PBXBuildFile; fileRef = 01D09B0022A493A70052745A /* Levels.json */; }; 01D09B0322A4958E0052745A /* Input.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D09B0222A4958E0052745A /* Input.swift */; }; 01D09B0522A5C9DB0052745A /* Ray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D09B0422A5C9DB0052745A /* Ray.swift */; }; 01D09B0722A6E09B0052745A /* Rotation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D09B0622A6E09A0052745A /* Rotation.swift */; }; - 01D09B0B22A7F7570052745A /* Textures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D09B0A22A7F7570052745A /* Textures.swift */; }; + 01D09B0B22A7F7570052745A /* Texture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D09B0A22A7F7570052745A /* Texture.swift */; }; 01D0F5D922F80E1600682CA1 /* RampageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D0F5D822F80E1600682CA1 /* RampageTests.swift */; }; 01D0F5F122FF095E00682CA1 /* Door.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D0F5F022FF095E00682CA1 /* Door.swift */; }; + 01DD25AA244FA74900D00FE5 /* Font.json in Resources */ = {isa = PBXBuildFile; fileRef = 01DD25A9244FA74900D00FE5 /* Font.json */; }; + 01DD25AC244FA85E00D00FE5 /* Font.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01DD25AB244FA85E00D00FE5 /* Font.swift */; }; + 01E3963A2342758D00D02236 /* Pushwall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01E396392342758D00D02236 /* Pushwall.swift */; }; + 01EDA5DB2444DC2C00FC1795 /* Game.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01EDA5DA2444DC2B00FC1795 /* Game.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 0108A65C23F4D84C0075E1AF /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 016E41A7228E9A5B00ACF137 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 0108A65623F4D84C0075E1AF; + remoteInfo = Renderer; + }; + 0108A66C23F4DA5D0075E1AF /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 016E41A7228E9A5B00ACF137 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 016E41C8228E9A8600ACF137; + remoteInfo = Engine; + }; 016E41CE228E9A8600ACF137 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 016E41A7228E9A5B00ACF137 /* Project object */; @@ -65,6 +109,7 @@ dstSubfolderSpec = 10; files = ( 016E41D1228E9A8600ACF137 /* Engine.framework in Embed Frameworks */, + 0108A65F23F4D84C0075E1AF /* Renderer.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -72,6 +117,12 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 0108A65723F4D84C0075E1AF /* Renderer.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Renderer.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 0108A65A23F4D84C0075E1AF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 0108A66823F4D9B70075E1AF /* Textures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Textures.swift; sourceTree = ""; }; + 0108A66E23F543740075E1AF /* Pathfinder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Pathfinder.swift; sourceTree = ""; }; + 0128F26123EEE7AE00439050 /* shotgunFire.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = shotgunFire.mp3; sourceTree = ""; }; + 0128F26323EEEB0A00439050 /* shotgunPickup.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = shotgunPickup.mp3; sourceTree = ""; }; 012A0C4C22C96E150068E8EF /* Tile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tile.swift; sourceTree = ""; }; 012A0C4E22C96E1F0068E8EF /* Thing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Thing.swift; sourceTree = ""; }; 012A0C6022CC200D0068E8EF /* Billboard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Billboard.swift; sourceTree = ""; }; @@ -79,7 +130,12 @@ 012A0C9D22D47C220068E8EF /* Actor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Actor.swift; sourceTree = ""; }; 012A0CA122D7AD0A0068E8EF /* Animation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Animation.swift; sourceTree = ""; }; 012DF10722E251CF00D52706 /* Effect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Effect.swift; sourceTree = ""; }; + 013D492423ED607D00763FCA /* medkit.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = medkit.mp3; sourceTree = ""; }; + 013D492623EE17C000763FCA /* Weapon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weapon.swift; sourceTree = ""; }; 01467C3D22E6F54600B5607D /* Easing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Easing.swift; sourceTree = ""; }; + 01557ACF245109E600FF8FF0 /* HUD.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HUD.swift; sourceTree = ""; }; + 0159A3F423DEF636001EEB81 /* Pickup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pickup.swift; sourceTree = ""; }; + 015A23C8230586E3004CBB78 /* Switch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Switch.swift; sourceTree = ""; }; 016E41AF228E9A5B00ACF137 /* Rampage.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Rampage.app; sourceTree = BUILT_PRODUCTS_DIR; }; 016E41B2228E9A5B00ACF137 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 016E41B4228E9A5B00ACF137 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; @@ -89,6 +145,21 @@ 016E41BE228E9A5E00ACF137 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 016E41C9228E9A8600ACF137 /* Engine.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Engine.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 016E41CC228E9A8600ACF137 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 0199F55523E1A517003E3F08 /* monsterSwipe.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = monsterSwipe.mp3; sourceTree = ""; }; + 0199F55623E1A517003E3F08 /* pistolFire.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = pistolFire.mp3; sourceTree = ""; }; + 0199F55723E1A517003E3F08 /* wallSlide.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = wallSlide.mp3; sourceTree = ""; }; + 0199F55823E1A517003E3F08 /* doorSlide.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = doorSlide.mp3; sourceTree = ""; }; + 0199F55923E1A517003E3F08 /* playerWalk.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = playerWalk.mp3; sourceTree = ""; }; + 0199F55A23E1A517003E3F08 /* monsterHit.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = monsterHit.mp3; sourceTree = ""; }; + 0199F55B23E1A517003E3F08 /* squelch.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = squelch.mp3; sourceTree = ""; }; + 0199F55C23E1A517003E3F08 /* ricochet.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = ricochet.mp3; sourceTree = ""; }; + 0199F55D23E1A517003E3F08 /* monsterDeath.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = monsterDeath.mp3; sourceTree = ""; }; + 0199F55E23E1A517003E3F08 /* playerDeath.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = playerDeath.mp3; sourceTree = ""; }; + 0199F55F23E1A517003E3F08 /* monsterGroan.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = monsterGroan.mp3; sourceTree = ""; }; + 0199F56023E1A517003E3F08 /* switchFlip.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = switchFlip.mp3; sourceTree = ""; }; + 0199F56123E1A517003E3F08 /* wallThud.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = wallThud.mp3; sourceTree = ""; }; + 0199F56F23E1AFEA003E3F08 /* Sounds.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Sounds.swift; sourceTree = ""; }; + 0199F57323E242D4003E3F08 /* SoundManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SoundManager.swift; sourceTree = ""; }; 01ADC63B22B957FD00DC8AAD /* Renderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Renderer.swift; sourceTree = ""; }; 01ADC63F22B9846B00DC8AAD /* World.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = World.swift; sourceTree = ""; }; 01D09AF022A481AB0052745A /* Color.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Color.swift; sourceTree = ""; }; @@ -98,23 +169,35 @@ 01D09AFA22A485040052745A /* Player.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Player.swift; sourceTree = ""; }; 01D09AFC22A4873B0052745A /* Rect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Rect.swift; sourceTree = ""; }; 01D09AFE22A48E990052745A /* Tilemap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tilemap.swift; sourceTree = ""; }; - 01D09B0022A493A70052745A /* Map.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = Map.json; sourceTree = ""; }; + 01D09B0022A493A70052745A /* Levels.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = Levels.json; sourceTree = ""; }; 01D09B0222A4958E0052745A /* Input.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Input.swift; sourceTree = ""; }; 01D09B0422A5C9DB0052745A /* Ray.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ray.swift; sourceTree = ""; }; 01D09B0622A6E09A0052745A /* Rotation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Rotation.swift; sourceTree = ""; }; - 01D09B0A22A7F7570052745A /* Textures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Textures.swift; sourceTree = ""; }; + 01D09B0A22A7F7570052745A /* Texture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Texture.swift; sourceTree = ""; }; 01D0F5D622F80E1600682CA1 /* RampageTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RampageTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 01D0F5D822F80E1600682CA1 /* RampageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RampageTests.swift; sourceTree = ""; }; 01D0F5DA22F80E1600682CA1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 01D0F5F022FF095E00682CA1 /* Door.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Door.swift; sourceTree = ""; }; + 01DD25A9244FA74900D00FE5 /* Font.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = Font.json; sourceTree = ""; }; + 01DD25AB244FA85E00D00FE5 /* Font.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Font.swift; sourceTree = ""; }; + 01E396392342758D00D02236 /* Pushwall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pushwall.swift; sourceTree = ""; }; + 01EDA5DA2444DC2B00FC1795 /* Game.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Game.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 0108A65423F4D84C0075E1AF /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 016E41AC228E9A5B00ACF137 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 016E41D0228E9A8600ACF137 /* Engine.framework in Frameworks */, + 0108A65E23F4D84C0075E1AF /* Renderer.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -135,13 +218,33 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 0108A65823F4D84C0075E1AF /* Renderer */ = { + isa = PBXGroup; + children = ( + 01D09AF222A482030052745A /* Bitmap.swift */, + 01ADC63B22B957FD00DC8AAD /* Renderer.swift */, + 0108A66823F4D9B70075E1AF /* Textures.swift */, + 0108A65A23F4D84C0075E1AF /* Info.plist */, + ); + path = Renderer; + sourceTree = ""; + }; + 0108A66A23F4DA580075E1AF /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; 016E41A6228E9A5B00ACF137 = { isa = PBXGroup; children = ( 016E41B1228E9A5B00ACF137 /* Rampage */, 016E41CA228E9A8600ACF137 /* Engine */, + 0108A65823F4D84C0075E1AF /* Renderer */, 01D0F5D722F80E1600682CA1 /* RampageTests */, 016E41B0228E9A5B00ACF137 /* Products */, + 0108A66A23F4DA580075E1AF /* Frameworks */, ); sourceTree = ""; }; @@ -151,6 +254,7 @@ 016E41AF228E9A5B00ACF137 /* Rampage.app */, 016E41C9228E9A8600ACF137 /* Engine.framework */, 01D0F5D622F80E1600682CA1 /* RampageTests.xctest */, + 0108A65723F4D84C0075E1AF /* Renderer.framework */, ); name = Products; sourceTree = ""; @@ -161,9 +265,12 @@ 016E41B2228E9A5B00ACF137 /* AppDelegate.swift */, 016E41B4228E9A5B00ACF137 /* ViewController.swift */, 01D09AF422A482450052745A /* UIImage+Bitmap.swift */, + 0199F57323E242D4003E3F08 /* SoundManager.swift */, 016E41B6228E9A5B00ACF137 /* Main.storyboard */, - 01D09B0022A493A70052745A /* Map.json */, + 01D09B0022A493A70052745A /* Levels.json */, + 01DD25A9244FA74900D00FE5 /* Font.json */, 016E41B9228E9A5E00ACF137 /* Assets.xcassets */, + 0199F55423E1A517003E3F08 /* Sounds */, 016E41BB228E9A5E00ACF137 /* LaunchScreen.storyboard */, 016E41BE228E9A5E00ACF137 /* Info.plist */, ); @@ -176,29 +283,59 @@ 012A0C9D22D47C220068E8EF /* Actor.swift */, 012A0CA122D7AD0A0068E8EF /* Animation.swift */, 012A0C6022CC200D0068E8EF /* Billboard.swift */, - 01D09AF222A482030052745A /* Bitmap.swift */, 01D09AF022A481AB0052745A /* Color.swift */, 01D0F5F022FF095E00682CA1 /* Door.swift */, 01467C3D22E6F54600B5607D /* Easing.swift */, 012DF10722E251CF00D52706 /* Effect.swift */, + 01DD25AB244FA85E00D00FE5 /* Font.swift */, + 01EDA5DA2444DC2B00FC1795 /* Game.swift */, + 01557ACF245109E600FF8FF0 /* HUD.swift */, 01D09B0222A4958E0052745A /* Input.swift */, 012A0C6122CC200D0068E8EF /* Monster.swift */, + 0108A66E23F543740075E1AF /* Pathfinder.swift */, + 0159A3F423DEF636001EEB81 /* Pickup.swift */, 01D09AFA22A485040052745A /* Player.swift */, + 01E396392342758D00D02236 /* Pushwall.swift */, 01D09B0422A5C9DB0052745A /* Ray.swift */, 01D09AFC22A4873B0052745A /* Rect.swift */, - 01ADC63B22B957FD00DC8AAD /* Renderer.swift */, 01D09B0622A6E09A0052745A /* Rotation.swift */, - 01D09B0A22A7F7570052745A /* Textures.swift */, + 015A23C8230586E3004CBB78 /* Switch.swift */, + 0199F56F23E1AFEA003E3F08 /* Sounds.swift */, + 01D09B0A22A7F7570052745A /* Texture.swift */, 012A0C4C22C96E150068E8EF /* Tile.swift */, 01D09AFE22A48E990052745A /* Tilemap.swift */, 012A0C4E22C96E1F0068E8EF /* Thing.swift */, 01D09AF822A484B10052745A /* Vector.swift */, + 013D492623EE17C000763FCA /* Weapon.swift */, 01ADC63F22B9846B00DC8AAD /* World.swift */, 016E41CC228E9A8600ACF137 /* Info.plist */, ); path = Engine; sourceTree = ""; }; + 0199F55423E1A517003E3F08 /* Sounds */ = { + isa = PBXGroup; + children = ( + 0199F55623E1A517003E3F08 /* pistolFire.mp3 */, + 0128F26123EEE7AE00439050 /* shotgunFire.mp3 */, + 0128F26323EEEB0A00439050 /* shotgunPickup.mp3 */, + 0199F55F23E1A517003E3F08 /* monsterGroan.mp3 */, + 0199F55523E1A517003E3F08 /* monsterSwipe.mp3 */, + 0199F55A23E1A517003E3F08 /* monsterHit.mp3 */, + 0199F55D23E1A517003E3F08 /* monsterDeath.mp3 */, + 0199F55C23E1A517003E3F08 /* ricochet.mp3 */, + 0199F55923E1A517003E3F08 /* playerWalk.mp3 */, + 0199F55E23E1A517003E3F08 /* playerDeath.mp3 */, + 0199F55B23E1A517003E3F08 /* squelch.mp3 */, + 0199F55823E1A517003E3F08 /* doorSlide.mp3 */, + 0199F55723E1A517003E3F08 /* wallSlide.mp3 */, + 0199F56123E1A517003E3F08 /* wallThud.mp3 */, + 0199F56023E1A517003E3F08 /* switchFlip.mp3 */, + 013D492423ED607D00763FCA /* medkit.mp3 */, + ); + path = Sounds; + sourceTree = ""; + }; 01D0F5D722F80E1600682CA1 /* RampageTests */ = { isa = PBXGroup; children = ( @@ -211,6 +348,13 @@ /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ + 0108A65223F4D84C0075E1AF /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 016E41C4228E9A8600ACF137 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; @@ -221,6 +365,25 @@ /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ + 0108A65623F4D84C0075E1AF /* Renderer */ = { + isa = PBXNativeTarget; + buildConfigurationList = 0108A66223F4D84C0075E1AF /* Build configuration list for PBXNativeTarget "Renderer" */; + buildPhases = ( + 0108A65223F4D84C0075E1AF /* Headers */, + 0108A65323F4D84C0075E1AF /* Sources */, + 0108A65423F4D84C0075E1AF /* Frameworks */, + 0108A65523F4D84C0075E1AF /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 0108A66D23F4DA5D0075E1AF /* PBXTargetDependency */, + ); + name = Renderer; + productName = Renderer; + productReference = 0108A65723F4D84C0075E1AF /* Renderer.framework */; + productType = "com.apple.product-type.framework"; + }; 016E41AE228E9A5B00ACF137 /* Rampage */ = { isa = PBXNativeTarget; buildConfigurationList = 016E41C1228E9A5E00ACF137 /* Build configuration list for PBXNativeTarget "Rampage" */; @@ -234,6 +397,7 @@ ); dependencies = ( 016E41CF228E9A8600ACF137 /* PBXTargetDependency */, + 0108A65D23F4D84C0075E1AF /* PBXTargetDependency */, ); name = Rampage; productName = Rampage; @@ -282,19 +446,26 @@ 016E41A7228E9A5B00ACF137 /* Project object */ = { isa = PBXProject; attributes = { + DefaultBuildSystemTypeForWorkspace = Original; LastSwiftUpdateCheck = 1010; - LastUpgradeCheck = 1010; + LastUpgradeCheck = 1130; ORGANIZATIONNAME = "Nick Lockwood"; TargetAttributes = { + 0108A65623F4D84C0075E1AF = { + CreatedOnToolsVersion = 11.3.1; + LastSwiftMigration = 1130; + }; 016E41AE228E9A5B00ACF137 = { CreatedOnToolsVersion = 10.1; + LastSwiftMigration = 1130; }; 016E41C8228E9A8600ACF137 = { CreatedOnToolsVersion = 10.1; - LastSwiftMigration = 1010; + LastSwiftMigration = 1130; }; 01D0F5D522F80E1600682CA1 = { CreatedOnToolsVersion = 10.1; + LastSwiftMigration = 1130; TestTargetID = 016E41AE228E9A5B00ACF137; }; }; @@ -314,20 +485,45 @@ targets = ( 016E41AE228E9A5B00ACF137 /* Rampage */, 016E41C8228E9A8600ACF137 /* Engine */, + 0108A65623F4D84C0075E1AF /* Renderer */, 01D0F5D522F80E1600682CA1 /* RampageTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 0108A65523F4D84C0075E1AF /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 016E41AD228E9A5B00ACF137 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 0128F26223EEE7AE00439050 /* shotgunFire.mp3 in Resources */, + 01DD25AA244FA74900D00FE5 /* Font.json in Resources */, + 0199F56E23E1A517003E3F08 /* wallThud.mp3 in Resources */, + 0199F56623E1A517003E3F08 /* playerWalk.mp3 in Resources */, + 0199F56C23E1A517003E3F08 /* monsterGroan.mp3 in Resources */, + 0199F56523E1A517003E3F08 /* doorSlide.mp3 in Resources */, 016E41BD228E9A5E00ACF137 /* LaunchScreen.storyboard in Resources */, + 0199F56423E1A517003E3F08 /* wallSlide.mp3 in Resources */, + 0199F56323E1A517003E3F08 /* pistolFire.mp3 in Resources */, 016E41BA228E9A5E00ACF137 /* Assets.xcassets in Resources */, - 01D09B0122A493A70052745A /* Map.json in Resources */, + 013D492523ED607D00763FCA /* medkit.mp3 in Resources */, + 0199F56223E1A517003E3F08 /* monsterSwipe.mp3 in Resources */, + 01D09B0122A493A70052745A /* Levels.json in Resources */, + 0199F56D23E1A517003E3F08 /* switchFlip.mp3 in Resources */, 016E41B8228E9A5B00ACF137 /* Main.storyboard in Resources */, + 0199F56A23E1A517003E3F08 /* monsterDeath.mp3 in Resources */, + 0199F56723E1A517003E3F08 /* monsterHit.mp3 in Resources */, + 0199F56823E1A517003E3F08 /* squelch.mp3 in Resources */, + 0199F56B23E1A517003E3F08 /* playerDeath.mp3 in Resources */, + 0199F56923E1A517003E3F08 /* ricochet.mp3 in Resources */, + 0128F26423EEEB0A00439050 /* shotgunPickup.mp3 in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -348,11 +544,22 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 0108A65323F4D84C0075E1AF /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0108A66323F4D8E80075E1AF /* Bitmap.swift in Sources */, + 0108A66923F4D9B70075E1AF /* Textures.swift in Sources */, + 0108A66523F4D8F00075E1AF /* Renderer.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 016E41AB228E9A5B00ACF137 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 01D09AF522A482450052745A /* UIImage+Bitmap.swift in Sources */, + 0199F57423E242D4003E3F08 /* SoundManager.swift in Sources */, 016E41B5228E9A5B00ACF137 /* ViewController.swift in Sources */, 016E41B3228E9A5B00ACF137 /* AppDelegate.swift in Sources */, ); @@ -364,25 +571,32 @@ files = ( 012DF10822E251CF00D52706 /* Effect.swift in Sources */, 012A0C6322CC200E0068E8EF /* Monster.swift in Sources */, - 01D09B0B22A7F7570052745A /* Textures.swift in Sources */, + 01D09B0B22A7F7570052745A /* Texture.swift in Sources */, + 01EDA5DB2444DC2C00FC1795 /* Game.swift in Sources */, + 0159A3F523DEF636001EEB81 /* Pickup.swift in Sources */, + 015A23C9230586E3004CBB78 /* Switch.swift in Sources */, 01D09AFF22A48E990052745A /* Tilemap.swift in Sources */, 01D09AFD22A4873B0052745A /* Rect.swift in Sources */, - 01ADC63C22B957FD00DC8AAD /* Renderer.swift in Sources */, + 0199F57023E1AFEA003E3F08 /* Sounds.swift in Sources */, 012A0C9E22D47C220068E8EF /* Actor.swift in Sources */, 012A0C4F22C96E1F0068E8EF /* Thing.swift in Sources */, 01D09AF922A484B10052745A /* Vector.swift in Sources */, - 01D09AF322A482030052745A /* Bitmap.swift in Sources */, 01467C3E22E6F54600B5607D /* Easing.swift in Sources */, 01D09B0722A6E09B0052745A /* Rotation.swift in Sources */, 01D09AFB22A485040052745A /* Player.swift in Sources */, + 01557AD0245109E600FF8FF0 /* HUD.swift in Sources */, 012A0C6222CC200E0068E8EF /* Billboard.swift in Sources */, + 013D492723EE17C000763FCA /* Weapon.swift in Sources */, 01ADC64022B9846B00DC8AAD /* World.swift in Sources */, + 0108A66723F4D9370075E1AF /* Color.swift in Sources */, 012A0C4D22C96E150068E8EF /* Tile.swift in Sources */, 01D0F5F122FF095E00682CA1 /* Door.swift in Sources */, + 0108A66F23F543750075E1AF /* Pathfinder.swift in Sources */, 01D09B0322A4958E0052745A /* Input.swift in Sources */, - 01D09AF122A481AB0052745A /* Color.swift in Sources */, + 01DD25AC244FA85E00D00FE5 /* Font.swift in Sources */, 012A0CA222D7AD0A0068E8EF /* Animation.swift in Sources */, 01D09B0522A5C9DB0052745A /* Ray.swift in Sources */, + 01E3963A2342758D00D02236 /* Pushwall.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -397,6 +611,16 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 0108A65D23F4D84C0075E1AF /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 0108A65623F4D84C0075E1AF /* Renderer */; + targetProxy = 0108A65C23F4D84C0075E1AF /* PBXContainerItemProxy */; + }; + 0108A66D23F4DA5D0075E1AF /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 016E41C8228E9A8600ACF137 /* Engine */; + targetProxy = 0108A66C23F4DA5D0075E1AF /* PBXContainerItemProxy */; + }; 016E41CF228E9A8600ACF137 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 016E41C8228E9A8600ACF137 /* Engine */; @@ -429,6 +653,63 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 0108A66023F4D84C0075E1AF /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 8VQKF583ED; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Renderer/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.charcoaldesign.Renderer; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_DISABLE_SAFETY_CHECKS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 0108A66123F4D84C0075E1AF /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = 8VQKF583ED; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = Renderer/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.charcoaldesign.Renderer; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_DISABLE_SAFETY_CHECKS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; 016E41BF228E9A5E00ACF137 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -480,7 +761,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.1; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -535,7 +816,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.1; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -559,7 +840,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.charcoaldesign.Rampage; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -578,7 +859,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.charcoaldesign.Rampage; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; @@ -607,7 +888,7 @@ SKIP_INSTALL = YES; SWIFT_DISABLE_SAFETY_CHECKS = NO; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -638,7 +919,7 @@ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_DISABLE_SAFETY_CHECKS = YES; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; @@ -659,7 +940,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.charcoaldesign.RampageTests; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Rampage.app/Rampage"; }; @@ -679,7 +960,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.charcoaldesign.RampageTests; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 4.2; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Rampage.app/Rampage"; }; @@ -688,6 +969,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 0108A66223F4D84C0075E1AF /* Build configuration list for PBXNativeTarget "Renderer" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0108A66023F4D84C0075E1AF /* Debug */, + 0108A66123F4D84C0075E1AF /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 016E41AA228E9A5B00ACF137 /* Build configuration list for PBXProject "Rampage" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Source/Rampage.xcodeproj/xcshareddata/xcschemes/Engine.xcscheme b/Source/Rampage.xcodeproj/xcshareddata/xcschemes/Engine.xcscheme index b8e8ec2..8fdc9d0 100644 --- a/Source/Rampage.xcodeproj/xcshareddata/xcschemes/Engine.xcscheme +++ b/Source/Rampage.xcodeproj/xcshareddata/xcschemes/Engine.xcscheme @@ -1,6 +1,6 @@ - - - - + + + + @@ -39,20 +48,9 @@ - - - - - - - - armv7 + UIRequiresFullScreen + + UIStatusBarHidden + UISupportedInterfaceOrientations - UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight diff --git a/Source/Rampage/Levels.json b/Source/Rampage/Levels.json new file mode 100644 index 0000000..67f37f5 --- /dev/null +++ b/Source/Rampage/Levels.json @@ -0,0 +1,44 @@ +[ + { + "width": 8, + "tiles": [ + 1, 3, 1, 1, 3, 1, 1, 1, + 1, 0, 0, 2, 0, 0, 0, 1, + 1, 4, 0, 3, 4, 0, 0, 3, + 2, 0, 0, 0, 0, 0, 4, 3, + 1, 4, 0, 1, 3, 1, 0, 1, + 1, 0, 1, 2, 0, 0, 0, 1, + 6, 5, 6, 1, 0, 4, 4, 1, + 1, 7, 3, 1, 1, 3, 1, 1 + ], + "things": [ + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 2, 0, 0, 0, 0, 7, 0, + 0, 0, 0, 0, 2, 0, 0, 0, + 0, 6, 0, 3, 0, 0, 0, 0, + 0, 0, 2, 0, 4, 0, 3, 0, + 0, 3, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 2, 0, + 0, 5, 0, 0, 0, 0, 0, 0 + ] + }, + { + "width": 5, + "tiles": [ + 2, 1, 1, 6, 1, + 1, 0, 4, 5, 7, + 1, 1, 1, 6, 1, + 2, 0, 0, 1, 3, + 1, 0, 3, 1, 3, + 1, 1, 1, 1, 1 + ], + "things": [ + 0, 0, 0, 0, 0, + 0, 1, 3, 0, 5, + 0, 4, 0, 0, 0, + 0, 0, 2, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0 + ] + } +] diff --git a/Source/Rampage/Map.json b/Source/Rampage/Map.json deleted file mode 100644 index 55476d0..0000000 --- a/Source/Rampage/Map.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "width": 8, - "tiles": [ - 1, 3, 1, 1, 3, 1, 1, 1, - 1, 0, 0, 2, 0, 0, 0, 1, - 1, 4, 0, 3, 4, 0, 0, 3, - 2, 0, 0, 0, 0, 0, 4, 3, - 1, 4, 0, 1, 1, 1, 0, 1, - 1, 0, 4, 2, 0, 0, 0, 1, - 1, 0, 0, 1, 0, 4, 4, 1, - 1, 3, 3, 1, 1, 3, 1, 1 - ], - "things": [ - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 2, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 2, 0, - 0, 0, 0, 3, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 3, 0, - 0, 0, 2, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 1, 0, 2, 0, - 0, 0, 0, 0, 0, 0, 0, 0 - ] -} diff --git a/Source/Rampage/SoundManager.swift b/Source/Rampage/SoundManager.swift new file mode 100644 index 0000000..d2b7175 --- /dev/null +++ b/Source/Rampage/SoundManager.swift @@ -0,0 +1,60 @@ +// +// SoundManager.swift +// Rampage +// +// Created by Nick Lockwood on 11/11/2019. +// Copyright © 2019 Nick Lockwood. All rights reserved. +// + +import AVFoundation + +public class SoundManager: NSObject, AVAudioPlayerDelegate { + private var channels = [Int: (url: URL, player: AVAudioPlayer)]() + private var playing = Set() + + public static let shared = SoundManager() + + private override init() {} + + public func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { + playing.remove(player) + } +} + +public extension SoundManager { + func activate() throws { + try AVAudioSession.sharedInstance().setActive(true) + } + + func preload(_ url: URL, channel: Int? = nil) throws -> AVAudioPlayer { + if let channel = channel, let (oldURL, oldSound) = channels[channel] { + if oldURL == url { + return oldSound + } + oldSound.stop() + } + return try AVAudioPlayer(contentsOf: url) + } + + func play(_ url: URL, channel: Int?, volume: Double, pan: Double) throws { + let player = try preload(url, channel: channel) + if let channel = channel { + channels[channel] = (url, player) + player.numberOfLoops = -1 + } + playing.insert(player) + player.delegate = self + player.volume = Float(volume) + player.pan = Float(pan) + player.play() + } + + func clearChannel(_ channel: Int) { + channels[channel]?.player.stop() + channels[channel] = nil + } + + func clearAll() { + channels.keys.forEach(clearChannel) + } +} diff --git a/Source/Rampage/Sounds/doorSlide.mp3 b/Source/Rampage/Sounds/doorSlide.mp3 new file mode 100644 index 0000000..901f50e Binary files /dev/null and b/Source/Rampage/Sounds/doorSlide.mp3 differ diff --git a/Source/Rampage/Sounds/medkit.mp3 b/Source/Rampage/Sounds/medkit.mp3 new file mode 100644 index 0000000..b2ba6cd Binary files /dev/null and b/Source/Rampage/Sounds/medkit.mp3 differ diff --git a/Source/Rampage/Sounds/monsterDeath.mp3 b/Source/Rampage/Sounds/monsterDeath.mp3 new file mode 100644 index 0000000..57c95d1 Binary files /dev/null and b/Source/Rampage/Sounds/monsterDeath.mp3 differ diff --git a/Source/Rampage/Sounds/monsterGroan.mp3 b/Source/Rampage/Sounds/monsterGroan.mp3 new file mode 100644 index 0000000..c0f1685 Binary files /dev/null and b/Source/Rampage/Sounds/monsterGroan.mp3 differ diff --git a/Source/Rampage/Sounds/monsterHit.mp3 b/Source/Rampage/Sounds/monsterHit.mp3 new file mode 100644 index 0000000..2e53bb3 Binary files /dev/null and b/Source/Rampage/Sounds/monsterHit.mp3 differ diff --git a/Source/Rampage/Sounds/monsterSwipe.mp3 b/Source/Rampage/Sounds/monsterSwipe.mp3 new file mode 100644 index 0000000..3404c68 Binary files /dev/null and b/Source/Rampage/Sounds/monsterSwipe.mp3 differ diff --git a/Source/Rampage/Sounds/pistolFire.mp3 b/Source/Rampage/Sounds/pistolFire.mp3 new file mode 100644 index 0000000..9ae81df Binary files /dev/null and b/Source/Rampage/Sounds/pistolFire.mp3 differ diff --git a/Source/Rampage/Sounds/playerDeath.mp3 b/Source/Rampage/Sounds/playerDeath.mp3 new file mode 100644 index 0000000..12efd32 Binary files /dev/null and b/Source/Rampage/Sounds/playerDeath.mp3 differ diff --git a/Source/Rampage/Sounds/playerWalk.mp3 b/Source/Rampage/Sounds/playerWalk.mp3 new file mode 100644 index 0000000..1b4a6da Binary files /dev/null and b/Source/Rampage/Sounds/playerWalk.mp3 differ diff --git a/Source/Rampage/Sounds/ricochet.mp3 b/Source/Rampage/Sounds/ricochet.mp3 new file mode 100644 index 0000000..1c30b3f Binary files /dev/null and b/Source/Rampage/Sounds/ricochet.mp3 differ diff --git a/Source/Rampage/Sounds/shotgunFire.mp3 b/Source/Rampage/Sounds/shotgunFire.mp3 new file mode 100644 index 0000000..729206a Binary files /dev/null and b/Source/Rampage/Sounds/shotgunFire.mp3 differ diff --git a/Source/Rampage/Sounds/shotgunPickup.mp3 b/Source/Rampage/Sounds/shotgunPickup.mp3 new file mode 100644 index 0000000..bff323e Binary files /dev/null and b/Source/Rampage/Sounds/shotgunPickup.mp3 differ diff --git a/Source/Rampage/Sounds/squelch.mp3 b/Source/Rampage/Sounds/squelch.mp3 new file mode 100644 index 0000000..11d0c2a Binary files /dev/null and b/Source/Rampage/Sounds/squelch.mp3 differ diff --git a/Source/Rampage/Sounds/switchFlip.mp3 b/Source/Rampage/Sounds/switchFlip.mp3 new file mode 100644 index 0000000..4a88013 Binary files /dev/null and b/Source/Rampage/Sounds/switchFlip.mp3 differ diff --git a/Source/Rampage/Sounds/wallSlide.mp3 b/Source/Rampage/Sounds/wallSlide.mp3 new file mode 100644 index 0000000..6c5b15f Binary files /dev/null and b/Source/Rampage/Sounds/wallSlide.mp3 differ diff --git a/Source/Rampage/Sounds/wallThud.mp3 b/Source/Rampage/Sounds/wallThud.mp3 new file mode 100644 index 0000000..6105416 Binary files /dev/null and b/Source/Rampage/Sounds/wallThud.mp3 differ diff --git a/Source/Rampage/UIImage+Bitmap.swift b/Source/Rampage/UIImage+Bitmap.swift index 666deb4..f86f9e3 100644 --- a/Source/Rampage/UIImage+Bitmap.swift +++ b/Source/Rampage/UIImage+Bitmap.swift @@ -8,6 +8,7 @@ import UIKit import Engine +import Renderer extension UIImage { convenience init?(bitmap: Bitmap) { diff --git a/Source/Rampage/ViewController.swift b/Source/Rampage/ViewController.swift index 91ccd43..86bf586 100644 --- a/Source/Rampage/ViewController.swift +++ b/Source/Rampage/ViewController.swift @@ -8,15 +8,23 @@ import UIKit import Engine +import Renderer private let joystickRadius: Double = 40 private let maximumTimeStep: Double = 1 / 20 private let worldTimeStep: Double = 1 / 120 -public func loadMap() -> Tilemap { - let jsonURL = Bundle.main.url(forResource: "Map", withExtension: "json")! +public func loadLevels() -> [Tilemap] { + let jsonURL = Bundle.main.url(forResource: "Levels", withExtension: "json")! let jsonData = try! Data(contentsOf: jsonURL) - return try! JSONDecoder().decode(Tilemap.self, from: jsonData) + let levels = try! JSONDecoder().decode([MapData].self, from: jsonData) + return levels.enumerated().map { Tilemap($0.element, index: $0.offset) } +} + +public func loadFont() -> Font { + let jsonURL = Bundle.main.url(forResource: "Font", withExtension: "json")! + let jsonData = try! Data(contentsOf: jsonURL) + return try! JSONDecoder().decode(Font.self, from: jsonData) } public func loadTextures() -> Textures { @@ -25,12 +33,26 @@ public func loadTextures() -> Textures { }) } +public extension SoundName { + var url: URL? { + return Bundle.main.url(forResource: rawValue, withExtension: "mp3") + } +} + +func setUpAudio() { + for name in SoundName.allCases { + precondition(name.url != nil, "Missing mp3 file for \(name.rawValue)") + } + try? SoundManager.shared.activate() + _ = try? SoundManager.shared.preload(SoundName.allCases[0].url!) +} + class ViewController: UIViewController { private let imageView = UIImageView() private let panGesture = UIPanGestureRecognizer() private let tapGesture = UITapGestureRecognizer() private let textures = loadTextures() - private var world = World(map: loadMap()) + private var game = Game(levels: loadLevels(), font: loadFont()) private var lastFrameTime = CACurrentMediaTime() private var lastFiredTime = 0.0 @@ -41,6 +63,7 @@ class ViewController: UIViewController { return } + setUpAudio() setUpImageView() let displayLink = CADisplayLink(target: self, selector: #selector(update)) @@ -52,6 +75,20 @@ class ViewController: UIViewController { view.addGestureRecognizer(tapGesture) tapGesture.addTarget(self, action: #selector(fire)) tapGesture.delegate = self + + game.delegate = self + } + + override var supportedInterfaceOrientations: UIInterfaceOrientationMask { + return .landscape + } + + override var prefersStatusBarHidden: Bool { + return true + } + + override var prefersHomeIndicatorAutoHidden: Bool { + return true } private var inputVector: Vector { @@ -73,21 +110,29 @@ class ViewController: UIViewController { @objc func update(_ displayLink: CADisplayLink) { let timeStep = min(maximumTimeStep, displayLink.timestamp - lastFrameTime) let inputVector = self.inputVector - let rotation = inputVector.x * world.player.turningSpeed * worldTimeStep - let input = Input( + let rotation = inputVector.x * game.world.player.turningSpeed * worldTimeStep + var input = Input( speed: -inputVector.y, rotation: Rotation(sine: sin(rotation), cosine: cos(rotation)), isFiring: lastFiredTime > lastFrameTime ) + lastFrameTime = displayLink.timestamp + lastFiredTime = min(lastFiredTime, lastFrameTime) + let worldSteps = (timeStep / worldTimeStep).rounded(.up) for _ in 0 ..< Int(worldSteps) { - world.update(timeStep: timeStep / worldSteps, input: input) + game.update(timeStep: timeStep / worldSteps, input: input) + input.isFiring = false } - lastFrameTime = displayLink.timestamp let width = Int(imageView.bounds.width), height = Int(imageView.bounds.height) var renderer = Renderer(width: width, height: height, textures: textures) - renderer.draw(world) + let insets = self.view.safeAreaInsets + renderer.safeArea = Rect( + min: Vector(x: Double(insets.left), y: Double(insets.top)), + max: renderer.bitmap.size - Vector(x: Double(insets.left), y: Double(insets.bottom)) + ) + renderer.draw(game) imageView.image = UIImage(bitmap: renderer.bitmap) } @@ -117,3 +162,26 @@ extension ViewController: UIGestureRecognizerDelegate { return true } } + +extension ViewController: GameDelegate { + func playSound(_ sound: Sound) { + DispatchQueue.main.asyncAfter(deadline: .now() + sound.delay) { + guard let url = sound.name?.url else { + if let channel = sound.channel { + SoundManager.shared.clearChannel(channel) + } + return + } + try? SoundManager.shared.play( + url, + channel: sound.channel, + volume: sound.volume, + pan: sound.pan + ) + } + } + + func clearSounds() { + SoundManager.shared.clearAll() + } +} diff --git a/Source/RampageTests/RampageTests.swift b/Source/RampageTests/RampageTests.swift index 7504500..e001075 100644 --- a/Source/RampageTests/RampageTests.swift +++ b/Source/RampageTests/RampageTests.swift @@ -9,9 +9,10 @@ import XCTest import Engine import Rampage +import Renderer class RampageTests: XCTestCase { - let world = World(map: loadMap()) + let world = World(map: loadLevels()[0]) let textures = loadTextures() func testRenderFrame() { diff --git a/Source/Engine/Bitmap.swift b/Source/Renderer/Bitmap.swift similarity index 63% rename from Source/Engine/Bitmap.swift rename to Source/Renderer/Bitmap.swift index 3afce9e..55e1aac 100644 --- a/Source/Engine/Bitmap.swift +++ b/Source/Renderer/Bitmap.swift @@ -6,6 +6,8 @@ // Copyright © 2019 Nick Lockwood. All rights reserved. // +import Engine + public struct Bitmap { public private(set) var pixels: [Color] public let width, height: Int @@ -20,6 +22,10 @@ public struct Bitmap { } public extension Bitmap { + var size: Vector { + return Vector(x: Double(width), y: Double(height)) + } + subscript(x: Int, y: Int) -> Color { get { return pixels[x * height + y] } set { @@ -67,32 +73,67 @@ public extension Bitmap { } } - mutating func drawColumn(_ sourceX: Int, of source: Bitmap, at point: Vector, height: Double) { + mutating func drawColumn( + _ sourceX: Int, + of source: Bitmap, + at point: Vector, + height: Double, + tint: Color? = nil + ) { let start = Int(point.y), end = Int((point.y + height).rounded(.up)) let stepY = Double(source.height) / height let offset = Int(point.x) * self.height if source.isOpaque { for y in max(0, start) ..< min(self.height, end) { let sourceY = max(0, Double(y) - point.y) * stepY - let sourceColor = source[sourceX, Int(sourceY)] + var sourceColor = source[sourceX, Int(sourceY)] + if let tint = tint { + sourceColor.r = UInt8(UInt16(sourceColor.r) * UInt16(tint.r) / 255) + sourceColor.g = UInt8(UInt16(sourceColor.g) * UInt16(tint.g) / 255) + sourceColor.b = UInt8(UInt16(sourceColor.b) * UInt16(tint.b) / 255) + sourceColor.a = UInt8(UInt16(sourceColor.a) * UInt16(tint.a) / 255) + } pixels[offset + y] = sourceColor } } else { for y in max(0, start) ..< min(self.height, end) { let sourceY = max(0, Double(y) - point.y) * stepY - let sourceColor = source[sourceX, Int(sourceY)] + var sourceColor = source[sourceX, Int(sourceY)] + if let tint = tint { + sourceColor.r = UInt8(UInt16(sourceColor.r) * UInt16(tint.r) / 255) + sourceColor.g = UInt8(UInt16(sourceColor.g) * UInt16(tint.g) / 255) + sourceColor.b = UInt8(UInt16(sourceColor.b) * UInt16(tint.b) / 255) + sourceColor.a = UInt8(UInt16(sourceColor.a) * UInt16(tint.a) / 255) + } blendPixel(at: offset + y, with: sourceColor) } } } - mutating func drawImage(_ source: Bitmap, at point: Vector, size: Vector) { + mutating func drawImage( + _ source: Bitmap, + xRange: Range? = nil, + at point: Vector, + size: Vector, + flipped: Bool = false, + tint: Color? = nil + ) { + let xRange = xRange ?? 0 ..< source.width let start = Int(point.x), end = Int(point.x + size.x) - let stepX = Double(source.width) / size.x - for x in max(0, start) ..< min(width, end) { - let sourceX = (Double(x) - point.x) * stepX - let outputPosition = Vector(x: Double(x), y: point.y) - drawColumn(Int(sourceX), of: source, at: outputPosition, height: size.y) + let stepX = Double(xRange.count) / size.x + let range = max(0, start) ..< max(0, start, min(width, end)) + if flipped { + for x in range { + let sourceX = xRange.upperBound - Int(max(0, Double(x) - point.x) * stepX) + let outputPosition = Vector(x: Double(x), y: point.y) + drawColumn(sourceX, of: source, at: outputPosition, height: size.y, tint: tint) + } + } else { + for x in range { + let sourceX = Int(max(0, Double(x) - point.x) * stepX) + xRange.lowerBound + let outputPosition = Vector(x: Double(x), y: point.y) + drawColumn(sourceX, of: source, at: outputPosition, height: size.y, tint: tint) + } } } diff --git a/Source/Renderer/Info.plist b/Source/Renderer/Info.plist new file mode 100644 index 0000000..9bcb244 --- /dev/null +++ b/Source/Renderer/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/Source/Renderer/Renderer.swift b/Source/Renderer/Renderer.swift new file mode 100644 index 0000000..a43279f --- /dev/null +++ b/Source/Renderer/Renderer.swift @@ -0,0 +1,263 @@ +// +// Renderer.swift +// Engine +// +// Created by Nick Lockwood on 02/06/2019. +// Copyright © 2019 Nick Lockwood. All rights reserved. +// + +import Engine + +private let fizzle = (0 ..< 10000).shuffled() + +public struct Renderer { + public private(set) var bitmap: Bitmap + private let textures: Textures + public var safeArea: Rect + + public init(width: Int, height: Int, textures: Textures) { + self.bitmap = Bitmap(width: width, height: height, color: .black) + self.textures = textures + self.safeArea = Rect(min: Vector(x: 0, y: 0), max: bitmap.size) + } +} + +public extension Renderer { + mutating func draw(_ game: Game) { + switch game.state { + case .title, .starting: + // Background + let background = textures[.titleBackground] + let backgroundScale = bitmap.size.y / background.size.y + let backgroundSize = background.size * backgroundScale + let backgroundPosition = (bitmap.size - backgroundSize) / 2 + bitmap.drawImage(background, at: backgroundPosition, size: backgroundSize) + + // Logo + let logo = textures[.titleLogo] + let logoScale = bitmap.size.y / logo.size.y / 2 + let logoSize = logo.size * logoScale + let logoPosition = Vector(x: (bitmap.size.x - logoSize.x) / 2, y: bitmap.size.y * 0.15) + bitmap.drawImage(logo, at: logoPosition, size: logoSize) + + // Text + let textScale = bitmap.size.y / 64 + let font = textures[game.font.texture] + let charSize = Vector(x: Double(font.width / game.font.characters.count), y: font.size.y) + let textWidth = charSize.x * Double(game.titleText.count) * textScale + var offset = Vector(x: (bitmap.size.x - textWidth) / 2, y: bitmap.size.y * 0.75) + for char in game.titleText { + let index = game.font.characters.firstIndex(of: String(char)) ?? 0 + let step = Int(charSize.x) + let xRange = index * step ..< (index + 1) * step + bitmap.drawImage( + font, + xRange: xRange, + at: offset, + size: charSize * textScale, + tint: .yellow + ) + offset.x += charSize.x * textScale + } + case .playing: + draw(game.world) + draw(game.hud) + + // Effects + for effect in game.world.effects { + draw(effect) + } + } + + // Transition + if let effect = game.transition { + draw(effect) + } + } + + mutating func draw(_ world: World) { + let focalLength = 1.0 + let viewWidth = Double(bitmap.width) / Double(bitmap.height) + let viewPlane = world.player.direction.orthogonal * viewWidth + let viewCenter = world.player.position + world.player.direction * focalLength + let viewStart = viewCenter - viewPlane / 2 + + // Cast rays + let columns = bitmap.width + let step = viewPlane / Double(columns) + var columnPosition = viewStart + for x in 0 ..< columns { + let rayDirection = columnPosition - world.player.position + let viewPlaneDistance = rayDirection.length + let ray = Ray( + origin: world.player.position, + direction: rayDirection / viewPlaneDistance + ) + let end = world.map.hitTest(ray) + let wallDistance = (end - ray.origin).length + + // Draw wall + let wallHeight = 1.0 + let distanceRatio = viewPlaneDistance / focalLength + let perpendicular = wallDistance / distanceRatio + let height = wallHeight * focalLength / perpendicular * Double(bitmap.height) + let wallTexture: Bitmap + let wallX: Double + let (tileX, tileY) = world.map.tileCoords(at: end, from: ray.direction) + let tile = world.map[tileX, tileY] + if end.x.rounded(.down) == end.x { + let neighborX = tileX + (ray.direction.x > 0 ? -1 : 1) + let isDoor = world.isDoor(at: neighborX, tileY) + wallTexture = textures[isDoor ? .doorjamb : tile.textures[0]] + wallX = end.y - end.y.rounded(.down) + } else { + let neighborY = tileY + (ray.direction.y > 0 ? -1 : 1) + let isDoor = world.isDoor(at: tileX, neighborY) + wallTexture = textures[isDoor ? .doorjamb2 : tile.textures[1]] + wallX = end.x - end.x.rounded(.down) + } + let textureX = Int(wallX * Double(wallTexture.width)) + let wallStart = Vector(x: Double(x), y: (Double(bitmap.height) - height) / 2 - 0.001) + bitmap.drawColumn(textureX, of: wallTexture, at: wallStart, height: height) + + // Draw switch + if let s = world.switch(at: tileX, tileY) { + let switchTexture = textures[s.animation.texture] + bitmap.drawColumn(textureX, of: switchTexture, at: wallStart, height: height) + } + + // Draw floor and ceiling + var floorTile: Tile! + var floorTexture, ceilingTexture: Bitmap! + let floorStart = Int(wallStart.y + height) + 1 + for y in min(floorStart, bitmap.height) ..< bitmap.height { + let normalizedY = (Double(y) / Double(bitmap.height)) * 2 - 1 + let perpendicular = wallHeight * focalLength / normalizedY + let distance = perpendicular * distanceRatio + let mapPosition = ray.origin + ray.direction * distance + let tileX = mapPosition.x.rounded(.down), tileY = mapPosition.y.rounded(.down) + let tile = world.map[Int(tileX), Int(tileY)] + if tile != floorTile { + floorTexture = textures[tile.textures[0]] + ceilingTexture = textures[tile.textures[1]] + floorTile = tile + } + let textureX = mapPosition.x - tileX, textureY = mapPosition.y - tileY + bitmap[x, y] = floorTexture[normalized: textureX, textureY] + bitmap[x, bitmap.height - 1 - y] = ceilingTexture[normalized: textureX, textureY] + } + + // Sort sprites by distance + var spritesByDistance: [(hit: Vector, distance: Double, sprite: Billboard)] = [] + for sprite in world.sprites { + guard let hit = sprite.hitTest(ray) else { + continue + } + let spriteDistance = (hit - ray.origin).length + if spriteDistance > wallDistance { + continue + } + spritesByDistance.append( + (hit: hit, distance: spriteDistance, sprite: sprite) + ) + } + spritesByDistance.sort(by: { $0.distance > $1.distance }) + + // Draw sprites + for (hit, spriteDistance, sprite) in spritesByDistance { + let perpendicular = spriteDistance / distanceRatio + let height = wallHeight / perpendicular * Double(bitmap.height) + let spriteX = (hit - sprite.start).length / sprite.length + let spriteTexture = textures[sprite.texture] + let textureX = min(Int(spriteX * Double(spriteTexture.width)), spriteTexture.width - 1) + let start = Vector(x: Double(x), y: (Double(bitmap.height) - height) / 2 + 0.001) + bitmap.drawColumn(textureX, of: spriteTexture, at: start, height: height) + } + + columnPosition += step + } + } + + mutating func draw(_ hud: HUD) { + // Right weapon + let weaponTexture = textures[hud.rightWeapon] + let weaponScale = bitmap.size.y / weaponTexture.size.y + let weaponSize = weaponTexture.size * weaponScale + bitmap.drawImage(weaponTexture, at: (bitmap.size - weaponSize) / 2, size: weaponSize) + + // Left weapon + if let leftWeapon = hud.leftWeapon { + let weaponTexture = textures[leftWeapon] + let weaponScale = bitmap.size.y / weaponTexture.size.y + let weaponSize = weaponTexture.size * weaponScale + bitmap.drawImage(weaponTexture, at: (bitmap.size - weaponSize) / 2, size: weaponSize, + flipped: true) + } + + // Crosshair + let crosshair = textures[.crosshair] + let hudScale = bitmap.size.y / 64 + let crosshairSize = crosshair.size * hudScale + bitmap.drawImage(crosshair, at: (bitmap.size - crosshairSize) / 2, size: crosshairSize) + + // Health icon + let healthIcon = textures[.healthIcon] + var offset = safeArea.min + Vector(x: 1, y: 1) * hudScale + bitmap.drawImage(healthIcon, at: offset, size: healthIcon.size * hudScale) + offset.x += healthIcon.size.x * hudScale + + // Health + let font = textures[hud.font.texture] + let charSize = Vector(x: Double(font.width / hud.font.characters.count), y: font.size.y) + let healthTint = hud.healthTint + for char in hud.healthString { + let index = hud.font.characters.firstIndex(of: String(char)) ?? 0 + let step = Int(charSize.x) + let xRange = index * step ..< (index + 1) * step + bitmap.drawImage( + font, + xRange: xRange, + at: offset, + size: charSize * hudScale, + tint: healthTint + ) + offset.x += charSize.x * hudScale + } + + // Ammunition + offset.x = safeArea.max.x + for char in hud.ammoString.reversed() { + let index = hud.font.characters.firstIndex(of: String(char)) ?? 0 + let step = Int(charSize.x) + let xRange = index * step ..< (index + 1) * step + offset.x -= charSize.x * hudScale + bitmap.drawImage(font, xRange: xRange, at: offset, size: charSize * hudScale) + } + + // Weapon icon + let weaponIcon = textures[hud.weaponIcon] + offset.x -= weaponIcon.size.x * hudScale + bitmap.drawImage(weaponIcon, at: offset, size: weaponIcon.size * hudScale) + } + + mutating func draw(_ effect: Effect) { + switch effect.type { + case .fadeIn: + bitmap.tint(with: effect.color, opacity: 1 - effect.progress) + case .fadeOut: + bitmap.tint(with: effect.color, opacity: effect.progress) + case .fizzleOut: + let threshold = Int(effect.progress * Double(fizzle.count)) + for x in 0 ..< bitmap.width { + for y in 0 ..< bitmap.height { + let granularity = 4 + let index = y / granularity * bitmap.width + x / granularity + let fizzledIndex = fizzle[index % fizzle.count] + if fizzledIndex <= threshold { + bitmap[x, y] = effect.color + } + } + } + } + } +} diff --git a/Source/Renderer/Textures.swift b/Source/Renderer/Textures.swift new file mode 100644 index 0000000..bd952b7 --- /dev/null +++ b/Source/Renderer/Textures.swift @@ -0,0 +1,27 @@ +// +// Textures.swift +// Renderer +// +// Created by Nick Lockwood on 05/06/2019. +// Copyright © 2020 Nick Lockwood. All rights reserved. +// + +import Engine + +public struct Textures { + private let textures: [Texture: Bitmap] +} + +public extension Textures { + init(loader: (String) -> Bitmap) { + var textures = [Texture: Bitmap]() + for texture in Texture.allCases { + textures[texture] = loader(texture.rawValue) + } + self.init(textures: textures) + } + + subscript(_ texture: Texture) -> Bitmap { + return textures[texture]! + } +}