Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dual-wield weapons #15

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
)
}
}
77 changes: 50 additions & 27 deletions Source/Engine/Player.swift
Original file line number Diff line number Diff line change
@@ -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) {
@@ -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
}
}

@@ -43,38 +46,71 @@ 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 {
case .idle:
break
if ammo == 0 {
setWeapon(.pistol)
}
case .firing:
if animation.isCompleted {
state = .idle
animation = .pistolIdle
animation = weapon.attributes.idleAnimation
}
}
if isMoving, !wasMoving {
@@ -84,16 +120,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
@@ -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
3 changes: 3 additions & 0 deletions Source/Engine/Sounds.swift
Original file line number Diff line number Diff line change
@@ -8,6 +8,8 @@

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

public struct Sound {
4 changes: 4 additions & 0 deletions Source/Engine/Textures.swift
Original file line number Diff line number Diff line change
@@ -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 {
2 changes: 2 additions & 0 deletions Source/Engine/Thing.swift
Original file line number Diff line number Diff line change
@@ -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
@@ -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]
@@ -27,6 +28,7 @@ public struct World {
self.doors = []
self.pushwalls = []
self.switches = []
self.pickups = []
self.monsters = []
self.effects = []
self.isLevelEnded = false
@@ -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)
@@ -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) {
@@ -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
@@ -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))
}
}
}
Loading