Skip to content

Commit

Permalink
Part 14
Browse files Browse the repository at this point in the history
  • Loading branch information
nicklockwood committed Feb 10, 2020
1 parent 1d96a5d commit 5e6d652
Show file tree
Hide file tree
Showing 27 changed files with 383 additions and 31 deletions.
46 changes: 46 additions & 0 deletions Source/Engine/Pickup.swift
Original file line number Diff line number Diff line change
@@ -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
)
}
}
76 changes: 50 additions & 26 deletions Source/Engine/Player.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ public struct Player: Actor {
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 private(set) var weapon: Weapon = .pistol
public private(set) var ammo: Double
public var animation: Animation
public let soundChannel: Int

public init(position: Vector, soundChannel: Int) {
Expand All @@ -30,6 +31,8 @@ public struct Player: Actor {
self.direction = Vector(x: 1, y: 0)
self.health = 100
self.soundChannel = soundChannel
self.animation = weapon.attributes.idleAnimation
self.ammo = weapon.attributes.defaultAmmo
}
}

Expand All @@ -43,29 +46,60 @@ public extension Player {
}

var canFire: Bool {
guard ammo > 0 else {
return false
}
switch state {
case .idle:
return true
case .firing:
return animation.time >= attackCooldown
return animation.time >= weapon.attributes.cooldown
}
}

mutating func setWeapon(_ weapon: Weapon) {
self.weapon = weapon
animation = weapon.attributes.idleAnimation
ammo = weapon.attributes.defaultAmmo
}

mutating func inherit(from player: Player) {
health = player.health
setWeapon(player.weapon)
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
world.playSound(.pistolFire, at: position)
let ray = Ray(origin: position, direction: direction)
if let index = world.pickMonster(ray) {
world.hurtMonster(at: index, damage: 10)
world.playSound(.monsterHit, at: world.monsters[index].position)
} else {
let position = world.hitTest(ray)
world.playSound(.ricochet, at: position)
ammo -= 1
animation = weapon.attributes.fireAnimation
world.playSound(weapon.attributes.fireSound, at: position)
let projectiles = weapon.attributes.projectiles
var hitPosition, missPosition: Vector?
for _ in 0 ..< projectiles {
let spread = weapon.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 = weapon.attributes.damage / Double(projectiles)
world.hurtMonster(at: index, damage: damage)
hitPosition = world.monsters[index].position
} else {
missPosition = world.hitTest(ray)
}
}
if let hitPosition = hitPosition {
world.playSound(.monsterHit, at: hitPosition)
}
if let missPosition = missPosition {
world.playSound(.ricochet, at: missPosition)
}
}
switch state {
Expand All @@ -74,7 +108,10 @@ public extension Player {
case .firing:
if animation.isCompleted {
state = .idle
animation = .pistolIdle
animation = weapon.attributes.idleAnimation
if ammo == 0 {
setWeapon(.pistol)
}
}
}
if isMoving, !wasMoving {
Expand All @@ -84,16 +121,3 @@ public extension Player {
}
}
}

public extension Animation {
static let pistolIdle = Animation(frames: [
.pistol
], duration: 0)
static let pistolFire = Animation(frames: [
.pistolFire1,
.pistolFire2,
.pistolFire3,
.pistolFire4,
.pistol
], duration: 0.5)
}
9 changes: 6 additions & 3 deletions Source/Engine/Renderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,14 @@ public extension Renderer {
}

// Player weapon
let weaponTexture = textures[world.player.animation.texture]
let aspectRatio = Double(weaponTexture.width) / Double(weaponTexture.height)
let screenHeight = Double(bitmap.height)
let weaponWidth = screenHeight * aspectRatio
bitmap.drawImage(
textures[world.player.animation.texture],
at: Vector(x: Double(bitmap.width) / 2 - screenHeight / 2, y: 0),
size: Vector(x: screenHeight, y: screenHeight)
weaponTexture,
at: Vector(x: Double(bitmap.width) / 2 - weaponWidth / 2, y: 0),
size: Vector(x: weaponWidth, y: screenHeight)
)

// Effects
Expand Down
3 changes: 3 additions & 0 deletions Source/Engine/Sounds.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

public enum SoundName: String, CaseIterable {
case pistolFire
case shotgunFire
case shotgunPickup
case ricochet
case monsterHit
case monsterGroan
Expand All @@ -20,6 +22,7 @@ public enum SoundName: String, CaseIterable {
case playerDeath
case playerWalk
case squelch
case medkit
}

public struct Sound {
Expand Down
4 changes: 4 additions & 0 deletions Source/Engine/Textures.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@ public enum Texture: String, CaseIterable {
case monsterHurt, monsterDeath1, monsterDeath2, monsterDead
case pistol
case pistolFire1, pistolFire2, pistolFire3, pistolFire4
case shotgun
case shotgunFire1, shotgunFire2, shotgunFire3, shotgunFire4
case shotgunPickup
case switch1, switch2, switch3, switch4
case elevatorFloor, elevatorCeiling, elevatorSideWall, elevatorBackWall
case medkit
}

public struct Textures {
Expand Down
2 changes: 2 additions & 0 deletions Source/Engine/Thing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ public enum Thing: Int, Decodable {
case door
case pushwall
case `switch`
case medkit
case shotgun
}
75 changes: 75 additions & 0 deletions Source/Engine/Weapon.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
//
// Weapon.swift
// Engine
//
// Created by Nick Lockwood on 07/02/2020.
// Copyright © 2020 Nick Lockwood. All rights reserved.
//

public enum Weapon: Int {
case pistol
case shotgun
}

public extension Weapon {
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
}

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
)
case .shotgun:
return Attributes(
idleAnimation: .shotgunIdle,
fireAnimation: .shotgunFire,
fireSound: .shotgunFire,
damage: 50,
cooldown: 0.5,
projectiles: 5,
spread: 0.4,
defaultAmmo: 5
)
}
}
}

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)
}
28 changes: 28 additions & 0 deletions Source/Engine/World.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public struct World {
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]
Expand All @@ -27,6 +28,7 @@ public struct World {
self.doors = []
self.pushwalls = []
self.switches = []
self.pickups = []
self.monsters = []
self.effects = []
self.isLevelEnded = false
Expand Down Expand Up @@ -124,6 +126,24 @@ public extension World {
}
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)
Expand All @@ -141,6 +161,7 @@ public extension World {
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) {
Expand Down Expand Up @@ -205,14 +226,17 @@ public extension World {

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
Expand Down Expand Up @@ -259,6 +283,10 @@ public extension World {
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))
}
}
}
Expand Down
Loading

0 comments on commit 5e6d652

Please sign in to comment.