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 all commits
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
25 changes: 25 additions & 0 deletions Source/Engine/Actor.swift
Original file line number Diff line number Diff line change
@@ -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
})
}
}
7 changes: 3 additions & 4 deletions Source/Engine/Color.swift
Original file line number Diff line number Diff line change
@@ -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)
}
11 changes: 9 additions & 2 deletions Source/Engine/Door.swift
Original file line number Diff line number Diff line change
@@ -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:
12 changes: 12 additions & 0 deletions Source/Engine/Font.swift
Original file line number Diff line number Diff line change
@@ -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]
}
78 changes: 78 additions & 0 deletions Source/Engine/Game.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
}
}
35 changes: 35 additions & 0 deletions Source/Engine/HUD.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
94 changes: 75 additions & 19 deletions Source/Engine/Monster.swift
Original file line number Diff line number Diff line change
@@ -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,
Loading