Skip to content

Commit

Permalink
enemies now follow player and use a* for path to player
Browse files Browse the repository at this point in the history
  • Loading branch information
Acepie committed Jun 7, 2024
1 parent 6a4fb2a commit d48e260
Show file tree
Hide file tree
Showing 5 changed files with 354 additions and 3 deletions.
1 change: 1 addition & 0 deletions gleam.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ gleam_stdlib = ">= 0.36.0 and < 2.0.0"
p5js_gleam = ">= 2.1.2 and < 3.0.0"
prng = ">= 3.0.2 and < 4.0.0"
gleam_community_maths = ">= 1.1.0 and < 2.0.0"
priorityq = ">= 0.1.0 and < 1.0.0"

[dev-dependencies]
esgleam = ">= 0.6.0 and < 1.0.0"
Expand Down
2 changes: 2 additions & 0 deletions manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ packages = [
{ name = "gleam_stdlib", version = "0.38.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "663CF11861179AF415A625307447775C09404E752FF99A24E2057C835319F1BE" },
{ name = "glint", version = "1.0.0-rc2", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "FD5C47CE237CA67121F3946ADE7C630750BB67F5E8A4717D2DF5B5EE758CCFDB" },
{ name = "p5js_gleam", version = "2.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "p5js_gleam", source = "hex", outer_checksum = "6280E50C5CF893BA84B6A2D82A3EA83BB764EA7F3509CA22BB12326D01283F85" },
{ name = "priorityq", version = "0.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "priorityq", source = "hex", outer_checksum = "979D1654394E48CCB1F2758F4783ED1E36274F007CCC6A8C1BB5D2492889102D" },
{ name = "prng", version = "3.0.3", build_tools = ["gleam"], requirements = ["gleam_bitwise", "gleam_stdlib"], otp_app = "prng", source = "hex", outer_checksum = "53006736FE23A0F61828C95B505193E10905D8DB76E128F1642D3E571E08F589" },
{ name = "ranger", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "ranger", source = "hex", outer_checksum = "1566C272B1D141B3BBA38B25CB761EF56E312E79EC0E2DFD4D3C19FB0CC1F98C" },
{ name = "simplifile", version = "1.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "1D5DFA3A2F9319EC85825F6ED88B8E449F381B0D55A62F5E61424E748E7DDEB0" },
Expand All @@ -33,5 +34,6 @@ esgleam = { version = ">= 0.6.0 and < 1.0.0" }
gleam_community_maths = { version = ">= 1.1.0 and < 2.0.0" }
gleam_stdlib = { version = ">= 0.36.0 and < 2.0.0" }
p5js_gleam = { version = ">= 2.1.2 and < 3.0.0" }
priorityq = { version = ">= 0.1.0 and < 1.0.0"}
prng = { version = ">= 3.0.2 and < 4.0.0" }
startest = { version = ">= 0.2.1 and < 1.0.0" }
99 changes: 99 additions & 0 deletions src/dungeon.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import gleam/float
import gleam/int
import gleam/iterator
import gleam/list
import gleam/option
import gleam/result
import obstacle.{type Obstacle}
import p5js_gleam.{type P5}
import pit.{type Pit}
import priorityq
import prng/random
import room
import vector
Expand Down Expand Up @@ -496,3 +498,100 @@ pub fn get_reflecting_point(
_, _ -> Error(Nil)
}
}

type CoordinateEntry =
#(Coordinate, Float, option.Option(Coordinate))

/// Gets a path from 1 point to another in a dungeon
pub fn path_to(
dungeon: Dungeon,
from: vector.Vector,
to: vector.Vector,
) -> List(vector.Vector) {
let start = point_to_coordinate(from)
let initial_entry = #(start, 0.0, option.None)
path_to_helper(
dungeon,
point_to_coordinate(to),
priorityq.new(fn(a: CoordinateEntry, b: CoordinateEntry) {
float.compare(b.1, a.1)
})
|> priorityq.push(initial_entry),
dict.from_list([#(start, initial_entry)]),
)
}

fn path_to_helper(
dungeon: Dungeon,
to: Coordinate,
worklist: priorityq.PriorityQueue(CoordinateEntry),
seen_so_far: dict.Dict(Coordinate, CoordinateEntry),
) -> List(vector.Vector) {
use <- bool.guard(priorityq.is_empty(worklist), [])

let assert option.Some(#(t, cost, _)) = priorityq.peek(worklist)
let worklist = priorityq.pop(worklist)

case t == to {
True -> list.reverse(path_from_costs(seen_so_far, to))
False -> {
let #(worklist, seen_so_far) =
[
room.Left,
room.Right,
room.Top,
room.Bottom,
room.TopLeft,
room.TopRight,
room.BottomLeft,
room.BottomRight,
]
|> list.fold(#(worklist, seen_so_far), fn(acc, dir) {
let #(worklist, seen_so_far) = acc
let assert Ok(to_room) = dict.get(dungeon.rooms, t)
case room.is_navigable(to_room, dir) {
False -> #(worklist, seen_so_far)
True -> {
let next = next_room_indices(t.0, t.1, dir)
let next_cost =
cost
+. vector.distance(
vector.Vector(int.to_float(next.0), int.to_float(next.1), 0.0),
vector.Vector(int.to_float(to.0), int.to_float(to.1), 0.0),
)

case dict.get(seen_so_far, next) {
// we have a better path do not traverse
Ok(#(_, old_best, _)) if old_best <. next_cost -> #(
worklist,
seen_so_far,
)
_ -> #(
worklist
|> priorityq.push(#(next, next_cost, option.Some(t))),
seen_so_far
|> dict.insert(next, #(next, next_cost, option.Some(t))),
)
}
}
}
})

path_to_helper(dungeon, to, worklist, seen_so_far)
}
}
}

fn path_from_costs(
seen_so_far: dict.Dict(Coordinate, CoordinateEntry),
end: Coordinate,
) -> List(vector.Vector) {
let assert Ok(#(_, _, prev)) = dict.get(seen_so_far, end)
case prev {
option.None -> []
option.Some(prev) -> [
coordinate_to_point(end),
..path_from_costs(seen_so_far, prev)
]
}
}
111 changes: 109 additions & 2 deletions src/enemy.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import behavior_tree/behavior_tree
import bullet.{type Bullet}
import dungeon.{type Dungeon}
import gleam/bool
import gleam/dict
import gleam/float
import gleam/int
import gleam/list
Expand Down Expand Up @@ -184,6 +183,105 @@ pub fn follow_path_behavior(inputs: BehaviorInput) -> BehaviorResult {
}
}

// Behavior that just succeeds if the enemy has line of sight and is in range of the player.
pub fn in_range_of_player_behavior(inputs: BehaviorInput) -> BehaviorResult {
let behavior_tree.BehaviorInput(
entity: enemy,
additional_inputs: Inputs(_, dungeon, player),
) = inputs

use <- bool.guard(
vector.distance(enemy.position, player.position)
>. int.to_float(dungeon.room_size / 2),
behavior_tree.BehaviorResult(False, enemy, AdditionalOutputs([])),
)

behavior_tree.BehaviorResult(
dungeon.can_move(dungeon, enemy.position, player.position),
enemy,
AdditionalOutputs([]),
)
}

// Simple behavior that marks the player as spotted.
pub fn spot_player_behavior(inputs: BehaviorInput) -> BehaviorResult {
let behavior_tree.BehaviorInput(entity: enemy, additional_inputs: _) = inputs

behavior_tree.BehaviorResult(
True,
Enemy(..enemy, spotted_player: True),
AdditionalOutputs([]),
)
}

// Simple behavior that checks if the enemy spotted the player.
pub fn spotted_player_behavior(inputs: BehaviorInput) -> BehaviorResult {
let behavior_tree.BehaviorInput(entity: enemy, additional_inputs: _) = inputs

behavior_tree.BehaviorResult(
enemy.spotted_player,
enemy,
AdditionalOutputs([]),
)
}

// Behavior that set's the enemy's path to the player.
// Always succeeds.
pub fn path_to_player_behavior(inputs: BehaviorInput) -> BehaviorResult {
let behavior_tree.BehaviorInput(
entity: enemy,
additional_inputs: Inputs(_, dungeon, player),
) = inputs

let path = dungeon.path_to(dungeon, enemy.position, player.position)
// Drop last path entry because player is in that room
let path = path |> list.take(list.length(path) - 1)

let enemy =
Enemy(..enemy, path: path, last_path_updated: utils.now_in_milliseconds())

behavior_tree.BehaviorResult(True, enemy, AdditionalOutputs([]))
}

// Simple behavior that moves the enemy toward the spotted player.
pub fn follow_player_behavior(inputs: BehaviorInput) -> BehaviorResult {
let behavior_tree.BehaviorInput(
entity: enemy,
additional_inputs: Inputs(enemies, dungeon, player),
) = inputs

let target = vector.vector_2d(player.position)
let whiskers =
create_whisker_points(enemy)
|> list.filter_map(dungeon.get_reflecting_point(dungeon, enemy.position, _))

let #(enemy, new_position) =
move_enemy(
enemy,
target,
dungeon.obstacles,
dungeon.pits,
enemies,
whiskers,
)
let new_position = case
dungeon.can_move(dungeon, enemy.position, new_position)
{
True -> new_position
False ->
// Apply downward velocity but don't move forward
vector.Vector(
enemy.position.x,
enemy.position.y,
enemy.position.z +. enemy.velocity.z,
)
}

let enemy = Enemy(..enemy, position: new_position) |> steer_enemy(target)

behavior_tree.BehaviorResult(True, enemy, AdditionalOutputs([]))
}

/// Represents an enemy to defeat.
pub type Enemy {
/// Represents an enemy to defeat.
Expand All @@ -204,6 +302,8 @@ pub type Enemy {
path: List(Vector),
/// The last time the path was updated.
last_path_updated: Int,
/// Can the enemy currently see the player.
spotted_player: Bool,
/// The behavior that an enemy will follow on each tick.
btree: BehaviorTree,
)
Expand Down Expand Up @@ -239,11 +339,15 @@ pub fn new_enemy(initial_position: Vector) -> Enemy {
max_health: max_enemy_health,
path: [],
last_path_updated: 0,
spotted_player: False,
// TODO: actually make a behavior
btree: behavior_tree.all(
[
// Look for player
sequence([in_range_of_player_behavior, spot_player_behavior]),
// Update the enemy's path
selector([
sequence([spotted_player_behavior, path_to_player_behavior]),
sequence([
selector([
behavior_tree.not(has_path_behavior),
Expand All @@ -253,7 +357,10 @@ pub fn new_enemy(initial_position: Vector) -> Enemy {
]),
]),
// Move the enemy
selector([sequence([has_path_behavior, follow_path_behavior])]),
selector([
sequence([has_path_behavior, follow_path_behavior]),
sequence([spotted_player_behavior, follow_player_behavior]),
]),
],
default_output,
output_to_input,
Expand Down
Loading

0 comments on commit d48e260

Please sign in to comment.