raycaster
A first-person walk through a hangar full of zombies. The engine draws every frame in Vader, pixel by pixel; raylib opens the window. Its output is that of the frames entry, which draws the game's views in characters.
examples/raycaster in the repository.
// examples/raycaster: a first-person walk through a hangar full of zombies, then
// a depot, in a window.
//
// The engine (`engine/`) draws each frame in Vader, pixel by pixel, into a
// buffer; raylib only opens the window, reads the keyboard and the mouse, plays
// the sounds, and shows the buffer, scaled up, as one texture. The level, the
// textures, the sprites and the sounds are files under `assets/`, read when the
// program is compiled. A level ends at its exit switch: thrown, the next level
// begins, the player keeping their health and bullets.
//
// vader build --run --release examples/raycaster
//
// Moving goes by the keys' places: W A S D on a QWERTY keyboard, Z Q S D on an
// AZERTY one, or Up and Down; the mouse, Left and Right turn; the left button or
// Ctrl fires. A command letter goes by what its key types, whatever the layout: E
// (or Space) for the door or the switch ahead, M for the map, F (or F11) to fill
// the screen or leave it, P to save the window as `raycaster.png` in the working
// directory. Enter starts again once fallen, and goes on to the next level once
// one is done; Escape pauses. The game opens on its title: Enter plays, Escape
// leaves; from the pause, Escape plays on and Q leaves.
// `frames/` is the same engine without a window.
module "main"
import "engine"
import "raylib"
import "std/abort"
import "std/io"
import "std/math"
/// The size of the frame the engine draws; the window shows it as large as fits.
VIEW_WIDTH: i32: 960
VIEW_HEIGHT: i32: 540
/// The window's size when it opens, until the user resizes it or fills the screen.
/// The HUD's sizes and places are given in pixels of a window this size, and scale
/// with the view the window shows.
WINDOW_WIDTH: i32: 1280
WINDOW_HEIGHT: i32: 720
/// How loud the sounds play, as a share of how they were recorded.
VOLUME: f32: 0.5
/// Walking speed, in cells per second.
WALK_SPEED :: 3.2
/// Turning speed with the arrow keys, in radians per second.
TURN_SPEED :: 2.4
/// Radians turned per pixel the mouse moves.
MOUSE_TURN :: 0.0025
/// Each axis of a diagonal step is this share of a straight one, so that walking
/// diagonally is no faster: the square root of one half.
DIAGONAL :: 0.70710678
HELP :: "WASD / ZQSD: move mouse: turn click: fire E: use M: map F: full screen P: screenshot Esc: pause"
/// The side of the HUD's icons, in HUD pixels.
ICON_SIZE: i32: 40
/// How far down the HUD the line of counts sits, in HUD pixels.
STATUS_TOP: i32: 646
/// What darkens the view under the title and the pause, so that their text reads.
SHADE :: Color {
.r = 0,
.g = 0,
.b = 0,
.a = 160,
}
/// Which screen the loop shows: what it draws over the view, and which keys it reads.
Phase :: enum {
/// The title, over the level as it starts.
Title,
/// The game running.
Playing,
/// The game stopped, drawn as it stood.
Paused,
/// The player asked to leave.
Leaving,
}
/// One game: the level as it stands, the player in it, and which of `LEVELS` it is.
Game :: struct {
level: Level
/// How `level` is lit: its `light_map`.
lights: LightMap
player: Player
index: i64
}
/// How a game stands.
Progress :: enum {
/// The player is in a level whose exit switch is not thrown yet.
Playing,
/// The player fell.
Fallen,
/// The player threw the exit switch of a level before the last.
LevelDone,
/// The player threw the exit switch of the last level.
Won,
}
// The game at the start of `LEVELS[index]`, as the build read it.
game_at :: fn(index: i64) -> Game {
level :: LEVELS[index]
return Game {
.level = level,
.lights = light_map(level),
.player = new_player(level),
.index = index,
}
}
/// A game from the start of the first level.
new_game :: fn() -> Game = game_at(0)
// The game in the level after `game`'s, the player keeping their health and
// their bullets.
next_level :: fn(game: Game) -> Game {
next :: game_at(game.index + 1)
next.player.health = game.player.health
next.player.ammo = game.player.ammo
return next
}
// How `game` stands.
progress :: fn(game: Game) -> Progress {
if game.player.health <= 0 {
return .Fallen
}
if !game.level.exited() {
return .Playing
}
if game.index == LEVELS.len() - 1 {
return .Won
}
return .LevelDone
}
// The game Enter starts once `game` is over: the next level after a level done,
// the first level again otherwise.
after :: fn(game: Game) -> Game {
if game.progress() == .LevelDone {
return game.next_level()
}
return new_game()
}
main :: fn() -> i32 {
set_config_flags(FLAG_WINDOW_RESIZABLE)
init_window(WINDOW_WIDTH, WINDOW_HEIGHT, "Vader raycaster")
if !is_window_ready() {
eprintln("raycaster: no window could be opened (raylib says why above)")
return 1
}
defer close_window()
// Escape pauses: only the window's close button and Q leave.
set_exit_key(.Null)
init_audio_device()
defer close_audio_device()
set_master_volume(VOLUME)
set_target_fps(60)
sounds :: load_sounds()
defer unload_sounds(sounds)
icons :: load_icons()
defer unload_icons(icons)
game := new_game()
frame :: new_frame(VIEW_WIDTH, VIEW_HEIGHT)
screen :: load_blank_texture(VIEW_WIDTH, VIEW_HEIGHT)
defer unload_texture(screen)
source :: Rectangle {
.x = 0.0,
.y = 0.0,
.width = f32(VIEW_WIDTH),
.height = f32(VIEW_HEIGHT),
}
show_map := true
phase := Phase.Title
for !window_should_close() {
seconds :: f64(get_frame_time())
level :: game.level
player :: game.player
noises: Noise[]: []
typed :: typed_this_frame()
next :: next_phase(phase, typed)
if next == .Leaving {
break
}
if next != phase {
use_cursor_for(next)
}
phase = next
if phase == .Playing {
if game.progress() != .Playing && is_key_pressed(.Enter) {
game = game.after()
continue
}
// A fallen player's level plays on around them; a level done stops.
if !level.exited() {
play(level, player, seconds, typed, noises)
}
}
if 'm' in typed {
show_map = !show_map
}
if 'f' in typed || is_key_pressed(.F11) {
toggle_borderless_windowed()
}
for noise in noises {
play_sound(sound_of(sounds, noise))
}
render(frame, level, player, game.lights)
if show_map {
draw_minimap(frame, level, player, 4)
}
draw_crosshair(frame)
update_texture(screen, frame.pixels)
view :: fitted_view()
begin_drawing()
clear_background(BLACK)
draw_texture_pro(screen, source, view, Vector2 { .x = 0.0, .y = 0.0 }, 0.0, WHITE)
draw_hud(view, icons, phase, game)
end_drawing()
if 'p' in typed {
take_screenshot("raycaster.png")
}
}
return 0
}
// The characters typed this frame, letters in lower case. They count only on a
// frame where a key went down: a key held down types its character again and
// again, which would toggle the full screen or a door on every repeat.
typed_this_frame :: fn() -> char[] {
pressed := false
for get_key_pressed() != 0 {
pressed = true
}
typed: char[]: []
for {
code :: get_char_pressed()
if code == 0 {
break
}
if pressed {
typed.push(lower_case(code))
}
}
return typed
}
// The character `code` stands for, an upper-case ASCII letter as its lower case.
lower_case :: fn(code: i32) -> char {
if code >= 'A' && code <= 'Z' {
return char(u32(code + 32))
}
return char(u32(code))
}
// The phase after this frame's keys and `typed` characters: from the title,
// Enter plays and Escape leaves; Escape pauses the game, and from the pause plays
// on, or Q leaves.
next_phase :: fn(phase: Phase, typed: char[]) -> Phase {
match phase {
.Title -> {
if is_key_pressed(.Enter) { return .Playing }
if is_key_pressed(.Escape) { return .Leaving }
}
.Playing -> {
if is_key_pressed(.Escape) {
return .Paused
}
}
.Paused -> {
if is_key_pressed(.Escape) { return .Playing }
if 'q' in typed { return .Leaving }
}
.Leaving -> {}
}
return phase
}
// Hide the cursor while the game runs, so that the mouse turns the view; show it
// on the other screens.
use_cursor_for :: fn(phase: Phase) -> void {
if phase == .Playing {
disable_cursor()
return
}
enable_cursor()
}
// Run the game on by `seconds`: the player's keys and the `typed` characters,
// then the doors, the zombies and the player's own clocks.
//
// @param noises what the player hears, appended to.
play :: fn(level: Level, player: Player, seconds: f64, typed: char[], noises: Noise[]) -> void {
if player.health > 0 {
steer(player, level, seconds)
player.collect(level, noises)
if is_mouse_button_down(.Left) || is_key_down(.LeftControl) {
player.fire(level, noises)
}
if 'e' in typed || is_key_pressed(.Space) {
player.use_ahead(level, noises)
}
}
level.update_doors(player, seconds)
level.update_enemies(player, seconds, noises)
level.update_effects(seconds)
player.update_player(seconds)
}
// Move and turn the player from the keys held and the mouse, for a frame of
// `seconds`.
steer :: fn(player: Player, level: Level, seconds: f64) -> void {
forward := 0.0
right := 0.0
if is_key_down(.W) || is_key_down(.Up) { forward += 1.0 }
if is_key_down(.S) || is_key_down(.Down) { forward -= 1.0 }
if is_key_down(.D) { right += 1.0 }
if is_key_down(.A) { right -= 1.0 }
if forward != 0.0 && right != 0.0 {
forward *= DIAGONAL
right *= DIAGONAL
}
stride :: WALK_SPEED * seconds
player.walk(level, forward * stride, right * stride)
angle := f64(get_mouse_delta().x) * MOUSE_TURN
if is_key_down(.Right) { angle += TURN_SPEED * seconds }
if is_key_down(.Left) { angle -= TURN_SPEED * seconds }
player.turn(angle)
}
// Where the window shows the frame: as large as fits, centred, black around it.
fitted_view :: fn() -> Rectangle {
width :: f32(get_screen_width())
height :: f32(get_screen_height())
scale :: min(width / f32(VIEW_WIDTH), height / f32(VIEW_HEIGHT))
return Rectangle {
.x = (width - f32(VIEW_WIDTH) * scale) / 2.0,
.y = (height - f32(VIEW_HEIGHT) * scale) / 2.0,
.width = f32(VIEW_WIDTH) * scale,
.height = f32(VIEW_HEIGHT) * scale,
}
}
// What `phase` shows over `view`: the title, or the health, the bullets and the
// zombies left, the keys, the pause, and what became of the player.
draw_hud :: fn(view: Rectangle, icons: Icons, phase: Phase, game: Game) -> void {
level :: game.level
player :: game.player
if phase != .Playing {
draw_rectangle_rec(view, SHADE)
}
draw_text("${get_fps()} FPS", view.across(1180), view.down(10), view.scaled(20), RAYWHITE)
if phase == .Title {
view.draw_centred("VADER RAYCASTER", 260, 80)
view.draw_centred("Enter: play Esc: quit", 370, 30)
return
}
view.draw_count(icons.health, 16, "${player.health}")
view.draw_count(icons.ammo, 176, "${player.ammo}")
view.draw_count(icons.zombies, 336, "${level.standing()} / ${level.enemies.len()}")
if phase == .Paused {
view.draw_centred("PAUSED", 260, 80)
view.draw_centred("Esc: play on Q: quit", 370, 30)
return
}
draw_text(HELP, view.across(16), view.down(694), view.scaled(20), RAYWHITE)
match game.progress() {
.Fallen -> view.draw_centred("YOU FELL. Enter: start again", 340, 40)
.Won -> {
view.draw_centred("YOU WIN", 300, 80)
view.draw_centred("Every level is done. Enter: play again", 390, 30)
}
.LevelDone -> {
view.draw_centred("LEVEL COMPLETE", 300, 80)
total :: level.enemies.len()
down :: total - level.standing()
view.draw_centred("Zombies down: ${down} / ${total} Enter: next level", 390, 30)
}
.Playing -> {
if level.cleared() {
view.draw_centred("Every zombie is down: find the exit", 60, 30)
}
}
}
}
// Draw `icon` with `text` beside it on the line of counts, `x` HUD pixels from
// `view`'s left edge.
draw_count :: fn(view: Rectangle, icon: Texture2D, x: i32, text: string) -> void {
place :: Rectangle {
.x = f32(view.across(x)),
.y = f32(view.down(STATUS_TOP)),
.width = f32(view.scaled(ICON_SIZE)),
.height = f32(view.scaled(ICON_SIZE)),
}
whole :: Rectangle {
.x = 0.0,
.y = 0.0,
.width = f32(icon.width),
.height = f32(icon.height),
}
draw_texture_pro(icon, whole, place, Vector2 { .x = 0.0, .y = 0.0 }, 0.0, WHITE)
draw_text(
text,
view.across(x + ICON_SIZE + 8),
view.down(STATUS_TOP + 6),
view.scaled(30),
RAYWHITE,
)
}
// Draw `text` centred across `view`, its top `y` HUD pixels down.
draw_centred :: fn(view: Rectangle, text: string, y: i32, size: i32) -> void {
pixels :: view.scaled(size)
left :: i32(view.x + view.width / 2.0) - measure_text(text, pixels) / 2
draw_text(text, left, view.down(y), pixels, RAYWHITE)
}
// `size` HUD pixels, in the window's pixels for `view`.
scaled :: fn(view: Rectangle, size: i32) -> i32 = i32(f32(size) * view.height / f32(WINDOW_HEIGHT))
// The window column `x` HUD pixels from `view`'s left edge.
across :: fn(view: Rectangle, x: i32) -> i32 = i32(view.x) + view.scaled(x)
// The window row `y` HUD pixels below `view`'s top edge.
down :: fn(view: Rectangle, y: i32) -> i32 = i32(view.y) + view.scaled(y)
// ---- icons ----------------------------------------------------------------
// The HUD's icons, as textures.
Icons :: struct {
health: Texture2D
ammo: Texture2D
zombies: Texture2D
}
// Every icon, made into a texture: the window must be open.
load_icons :: fn() -> Icons = Icons {
.health = load_icon(.Health),
.ammo = load_icon(.Ammo),
.zombies = load_icon(.Zombies),
}
// The texture of `icon`, one pixel per texel of its sprite.
load_icon :: fn(icon: Icon) -> Texture2D {
picture :: icon_frame(icon)
texture :: load_blank_texture(picture.width, picture.height)
update_texture(texture, picture.pixels)
return texture
}
unload_icons :: fn(icons: Icons) -> void {
unload_texture(icons.health)
unload_texture(icons.ammo)
unload_texture(icons.zombies)
}
// ---- sounds ---------------------------------------------------------------
// The sounds the game plays, one per noise the engine makes. The zombies' shots
// have a sound of their own, from the same file as the player's, so that the
// two play over each other instead of cutting each other short.
Sounds :: struct {
shot: Sound
enemy_sees: Sound
enemy_hurt: Sound
enemy_dies: Sound
enemy_fires: Sound
player_hurt: Sound
player_dies: Sound
door_opens: Sound
door_closes: Sound
pick_up: Sound
exit: Sound
}
// The sound files, from Freedoom, read by the build.
PISTOL_WAV: u8[]: @comptime read_sound("assets/sounds/dspistol.wav")
SIGHT_WAV: u8[]: @comptime read_sound("assets/sounds/dsposit1.wav")
PAIN_WAV: u8[]: @comptime read_sound("assets/sounds/dspopain.wav")
DEATH_WAV: u8[]: @comptime read_sound("assets/sounds/dspodth1.wav")
PLAYER_PAIN_WAV: u8[]: @comptime read_sound("assets/sounds/dsplpain.wav")
PLAYER_DEATH_WAV: u8[]: @comptime read_sound("assets/sounds/dspldeth.wav")
DOOR_OPENS_WAV: u8[]: @comptime read_sound("assets/sounds/dsdoropn.wav")
DOOR_CLOSES_WAV: u8[]: @comptime read_sound("assets/sounds/dsdorcls.wav")
PICK_UP_WAV: u8[]: @comptime read_sound("assets/sounds/dsitemup.wav")
EXIT_WAV: u8[]: @comptime read_sound("assets/sounds/dsswtchx.wav")
// The bytes of the sound file at `path`, relative to this source file: one that
// is missing stops the build.
read_sound :: fn(path: string) -> u8[] {
data :: read_file_bytes(path)
if data is IOError {
panic("cannot read `${path}`: ${data.message()}")
}
return data
}
// Every sound, decoded for the audio device, which must be open.
load_sounds :: fn() -> Sounds = Sounds {
.shot = decode_sound(PISTOL_WAV),
.enemy_sees = decode_sound(SIGHT_WAV),
.enemy_hurt = decode_sound(PAIN_WAV),
.enemy_dies = decode_sound(DEATH_WAV),
.enemy_fires = decode_sound(PISTOL_WAV),
.player_hurt = decode_sound(PLAYER_PAIN_WAV),
.player_dies = decode_sound(PLAYER_DEATH_WAV),
.door_opens = decode_sound(DOOR_OPENS_WAV),
.door_closes = decode_sound(DOOR_CLOSES_WAV),
.pick_up = decode_sound(PICK_UP_WAV),
.exit = decode_sound(EXIT_WAV),
}
// The sound in the WAV file `data`. The files are the build's, so one that does
// not decode is a broken build.
decode_sound :: fn(data: u8[]) -> Sound =
load_sound_from_memory(".wav", data) ?? panic("a sound baked into the program does not decode")
unload_sounds :: fn(sounds: Sounds) -> void {
unload_sound(sounds.shot)
unload_sound(sounds.enemy_sees)
unload_sound(sounds.enemy_hurt)
unload_sound(sounds.enemy_dies)
unload_sound(sounds.enemy_fires)
unload_sound(sounds.player_hurt)
unload_sound(sounds.player_dies)
unload_sound(sounds.door_opens)
unload_sound(sounds.door_closes)
unload_sound(sounds.pick_up)
unload_sound(sounds.exit)
}
// The sound that plays for `noise`.
sound_of :: fn(sounds: Sounds, noise: Noise) -> Sound = match noise {
.Shot -> sounds.shot
.EnemySees -> sounds.enemy_sees
.EnemyHurt -> sounds.enemy_hurt
.EnemyDies -> sounds.enemy_dies
.EnemyFires -> sounds.enemy_fires
.PlayerHurt -> sounds.player_hurt
.PlayerDies -> sounds.player_dies
.DoorOpens -> sounds.door_opens
.DoorCloses -> sounds.door_closes
.PickUp -> sounds.pick_up
.Exit -> sounds.exit
}shot 1 at (1.5, 1.5), angle 0.3: c4818a89 :-:-:-::-::::::-------:::::---------=------=-=-- :-+=@::#+:#::::#:::---+::---------=-----=-==-=== --+:*:::+:::+::::+::::+---:--:-------=-=======-= -:+#:*:#+:#::::::*::#:+:::::::-:-------:-::--::: -:+*-:::+:::::*:+-----+::::::::::.::..:.......:. ::+---:*+::.::::+::#..+....:...............:.:.: ::+++++++++++++++++++++...................-:.... --:::::::::.:::::::::.............. ......*#.::. -::-::::::...:::::::::::::................=: ... -:::::::........:::.::::::::::::::::::---------- :-::::.:..::::.:.::::::-..:::::::::::::::::-:::- :-::..:.:::::::::::::: -=..::::::::::::::::::::: --:.:::::::::::::::::--..--.:::::::::::::::::::: :::::::::::::::::::::.:----..::::::::::::::::::: shot 2 at (7.5, 3.5), angle 0.0: 2bfd2790 ---======++=++===*-=-==------=-+=-=---=--===---= ==-===-==-====-===.............+=---=---------:- :====-==--===+-==+.=====-----=.-=---=---===-:-=- ===+==+++-=+===+=+.=====-----=.+======--==--=--- ==--=-==-++======+.===+=-=====.+=-----==-=--=--: ==+=-=+======++==*.=====-=====.==-===-----==--=- ----==+===++===-==.=======+===.+-=--=--==------= =++===+++===+--+*+.=====-=====.*--=-===--=--==-- ============+==+==.-=-=---=-=-.+=-=-=-------=--= -==-=--==-=-=====+.+.::+=::.+..-----=-:---=--=-- ===+===+=-====++==--::-#--:::::+-=-===---=-==--- ---------------------- ##------------::::-:::-:: ======-==-=======--==**::+#----------------:--:- ===================-=-++**+---==---------------- shot 3 at (7.5, 3.5), angle 0.0: 756f1bc5 ---======++=++===*-=-==------=-+=-=---=--===---= ==-===-==-====-===+-=-----.....+=---=---------:- :====-==--===+-==+=---:---=====-=---=---===-:-=- ===+==+++-=+===+=+=-------=====+======--==--=--- ==--=-==-++======+=:::::::=====+=-----==-=--=--: ==+=-=+======++==*= . ..=======-===-----==--=- ----==+===++===-==* =====+-=--=--==------= =++===+++===+--+*+= ..====-*--=-===--=--==-- ============+==+===:::::::==-=-+=-=-=-------=--= -==-=--==-=-=====+=:::::::+.=.=-----=-:---=--=-- ===+===+=-====++==--::-#--:::::+-=-===---=-==--- ---------------------- ##------------::::-:::-:: ======-==-=======--==**::+#----------------:--:- ===================-=-++**+---==---------------- shot 4 at (12.5, 6.0), angle 0.15: 2b27c691 -----------------:----==-=---=-==-------===-==== ::::-::-:--:-----:-----:--------:------------::- :::::::::::::::::::::::::::::::::::::::::::::::: :.:.::::::::::::::...:.:::.::.::::::::.:.:.:.::: ...........:-:::::::::::::...................... .. :.::.:::..:::.:.. . ....... .... .::::-:.:.:::-.. ...... .. ...:: . . :--:::::::::.::: . .... . . ....:-. ...........:-::::.:..::...:..................::. ...............:.::.:..:.::.:....::::::.:...:.:. :::::::::::::::::::::::*-:::::::::-:-:-:--:----: ::-:-::-::-::::-::---- *#:-----------------=--=- ------:--------------+*::+*-----=--==--========= ----------------------=++*+--=-===-============= shot 5 at (21.5, 9.5), angle -1.9: d7effed8 ::::-::::-:--:--------------=-==--========-==-== ::::-:::::::--:---:-----------======+=========-- :::::::::::::::::::::--:----==============------ :-:::.::::.::.:::::::::::::-:::-::-:::-::::::::: :::::.:.:::..................................... :::::.::.::..................................... ::::-::.::-..................................... --:::.::-.:.......... ... ..........*..... .... :::::..:............................ ........... :..............:::::::::::::::-------------::::: :::::.:::::::::::::::::=:.:::::-:-:----------:-- :::::::::::::::::::::: ==.:::::::::::::::::::::: :::::::::::::::::::::--..-=::::::::::::::::::::: :::::::::::::::::::::::----..::::::::::::::::::: shot 6 at (5.5, 12.5), angle -1.57: 7f2b628f ====-==-=-==-==-===-========================-==- ----=----------=-----=----------=-----==-----=-= :--------:-:--:---::-:-:--:-:-:--:-:-:---------- :::.:::::::::::::::::::::::::.:::::::.:::-::-.:: ::::-..::::::..::::::-++----::.:::::-:.::::--:.: ::::::.:-:::::.::::::+=::..:::..:::::::.-::-:::: :.::::-::.:::::::::::.-...:. :::::::::::::::-::. :::::::-:::::::-:::::-.=-: ::.:::::::.::::::-::: :::-::--:::-:::-:::-+=:##* .:::-:::--::-:::--::- :::::::::::::::::::::*-##* ::::::::::::::::::::: -::::-::---:::-:---:-+-#-- :-:-:::-----:-::----- ---------------------+ #%-=:-------------------- ---------=-----======*#::*#=-==-=-=-=--=-=------ ==-==-================+**#*--=============-===== shot 7 at (16.5, 13.5), angle 1.0: e0df2a82 ---------====--========+=+===++=+===+=-=======-= ::::--:-:------------=---=--=-==-==-----------:- ::::::::::::::::::::::--:--:-:::-:--:::-:::::::: ..:..........:........:::::.:::.:...::::..:....: ::::::.......................................... ::::::::..... ::...........................:. :::::::::.... ....+##....................:...... :::::::::... -=*:: .....-*-................:. ::...........++................................. .......... ++...:. :::::::: ::::::::::::::::... ::::::::::- --: --.---+::--.-------------:--::: -:------------: --#+:= ++::========-===--------- -------------:*-----#=+..=+:=======-====--=--==- -------------==.-==-*:-==+=::==-=-=--======----- shot 8 at (28.5, 17.5), angle -2.4: 99793d5f ::::-::::-::::-::::-::-:---::::---::--::--:-:::: :::::::.:::::::::::::::::::::::::::::::::::::::: -.:..:.:.:..:::.:.::.::.::::.:..:.:::::::::::::: ---:---- ..............:..........:..........:.- ------:-:::...--::::::::::::....::::------------ ----:-:-:::::.--::::::::::::::::::::-:---------- --------::.:::--::::::::::::::::::::------------ --------:.::::--:::::::::::::::::::::.-.-------- ------:- . --::::::::............- - ------- ................................................ ......::..:::.:::::::::*-::::::::::::::::::::::: :::::::::::::::::::::: *#:-::::::--:-::::::::::: :::::::::::::-:-:----**::+*--------------------- :::::-:----:-:--------=+**+--------------------- fight, step 25: 255ee63d =--=:-----:----------=-------------------------- -=-====:-:-::----::----------:----------:------- -=-=----=-::::::::::::::::::::::::::-::::::::::: =-====--:---:.:..:.::.:.::::.:.:.::::::.:::::::: =--+=---=-:-::::....................:....::::::- ===-==--:::-:::.... . ... ...... ::-::.: ==-=--=---:::::..... :+= . ... ::-:..: =--===--=---::::.... =.@*. . . .:.. ::-:::- -=-==---:---::........+.# ...............::::..: -+--==----..............=:...:....::: :..:...... --*=:::::::::::::::::::#--:::::::::::::::::::::: ::-:::-::------------- #%-=--------------------- --------------===--==*#::*#===-==--===--=------- -----==---============+**#*--===========-===--== fight, step 50: 547e4eb0 =--=:-----:----------=-------------------------- -=-====:-:-::----::----------:----------:------- -=-=----=-::::::::::::::::::::::::::-::::::::::: =-====--:---:.:..:.::.:.::::.:.:.::::::.:::::::: =--+=---=-:-::::....................:....::::::- ===-==--:::-:::.... . ... ...... ::-::.: ==-=--=---:::::..... :+= . ... ::-:..: =--===--=---::::.... =.@*. . . .:.. ::-:::- -=-==---:---::........+.# ...............::::..: -+--==----..............=:...:....::: :..:...... --*=:::::::::::::::::::#--:::::::::::::::::::::: ::-:::-::------------- #%-=--------------------- --------------===--==*#::*#===-==--===--=------- -----==---============+**#*--===========-===--== fight, step 75: 5f9b3a80 =--=:-----:----------=-------------------------- -=-====:-:-::----::----------:----------:------- -=-=----=-::::::::::::::::::::::::::-::::::::::: =-====--:---:.:..:.::.:.::::.:.:.::::::.:::::::: =--+=---=-:-::::....................:....::::::- ===-==--:::-:::.... . ... ...... ::-::.: ==-=--=---:::::..... :+= . ... ::-:..: =--===--=---::::.... =.@*. . . .:.. ::-:::- -=-==---:---::........+.# ...............::::..: -+--==----.............#+:...:....::: :..:...... --*=::::::::::::::::::@===:::::::::::::::::::::: ::-:::-::------------- ::.*+-------------------- --------------===--==*#**#*-+=-==--===--=------- -----==---==========::+***+::===========-===--== fight, step 100: 867c37cf =--=:-----:----------=-------------------------- -=-====:-:-::----::----------:----------:------- -=-=----=-::::::::::::::::::::::::::-::::::::::: =-====--:---:.:..:.::.:.::::.:.:.::::::.:::::::: =--+=---=-:-::::....................:....::::::- ===-==--:::-:::.... . ... ...... ::-::.: ==-=--=---:::::..... :+= . ... ::-:..: =--===--=---::::.... =.@*. . . .:.. ::-:::- -=-==---:---::........+.# ...............::::..: -+--==----..............=:...:....::: :..:...... --*=:::::::::::::::::::#--:::::::::::::::::::::: ::-:::-::------------- #%-=--------------------- --------------===--==*#::*#===-==--===--=------- -----==---============+**#*--===========-===--== fight, step 125: 867c37cf =--=:-----:----------=-------------------------- -=-====:-:-::----::----------:----------:------- -=-=----=-::::::::::::::::::::::::::-::::::::::: =-====--:---:.:..:.::.:.::::.:.:.::::::.:::::::: =--+=---=-:-::::....................:....::::::- ===-==--:::-:::.... . ... ...... ::-::.: ==-=--=---:::::..... :+= . ... ::-:..: =--===--=---::::.... =.@*. . . .:.. ::-:::- -=-==---:---::........+.# ...............::::..: -+--==----..............=:...:....::: :..:...... --*=:::::::::::::::::::#--:::::::::::::::::::::: ::-:::-::------------- #%-=--------------------- --------------===--==*#::*#===-==--===--=------- -----==---============+**#*--===========-===--== fight, step 150: 867c37cf =--=:-----:----------=-------------------------- -=-====:-:-::----::----------:----------:------- -=-=----=-::::::::::::::::::::::::::-::::::::::: =-====--:---:.:..:.::.:.::::.:.:.::::::.:::::::: =--+=---=-:-::::....................:....::::::- ===-==--:::-:::.... . ... ...... ::-::.: ==-=--=---:::::..... :+= . ... ::-:..: =--===--=---::::.... =.@*. . . .:.. ::-:::- -=-==---:---::........+.# ...............::::..: -+--==----..............=:...:....::: :..:...... --*=:::::::::::::::::::#--:::::::::::::::::::::: ::-:::-::------------- #%-=--------------------- --------------===--==*#::*#===-==--===--=------- -----==---============+**#*--===========-===--== health 94, ammo 37 zombies: Dead Idle Idle Idle Idle Idle Idle Idle Idle Idle heard: EnemySees EnemyFires PlayerHurt Shot EnemyHurt Shot EnemyDies Shot