summary refs log tree commit diff
diff options
context:
space:
mode:
authorzlago2024-10-28 10:27:19 +0100
committerzlago2024-10-28 10:29:08 +0100
commitf7e4de7c45ed848312690ce6cfb62ae562e8347c (patch)
tree23d4afd3af64690c789249ce7eebb30137581657
parentee316a07cdfb01e52694edef2cc998e672e2885b (diff)
pacer enemy [wip]
-rw-r--r--src/disk.c2
-rw-r--r--src/funsinit.c3
-rw-r--r--src/gun.h7
-rw-r--r--src/hvy_guns.c449
-rw-r--r--src/pacer.c330
-rw-r--r--src/res/big.tmx29
-rw-r--r--src/res/pacer.asebin0 -> 865 bytes
-rw-r--r--src/save.c1
8 files changed, 807 insertions, 14 deletions
diff --git a/src/disk.c b/src/disk.c
index 5231953..084533f 100644
--- a/src/disk.c
+++ b/src/disk.c
@@ -93,7 +93,7 @@ struct entity *save_new(struct entities *entities) {
 		.timer = 0,
 		.ext = NULL,
 	};
-	self->anim = save_anims[SAVE_A_SPIN];
+	self->anim = save_anims[SAVE_A_IDLE];
 	self->texture = res_get_texture("save").data;
 	entities->enemies++;
 	return self;
diff --git a/src/funsinit.c b/src/funsinit.c
index 50469c6..a323b1a 100644
--- a/src/funsinit.c
+++ b/src/funsinit.c
@@ -8,6 +8,8 @@ void *walker_new(struct entities *entities);
 int walker_property(void *const restrict entity, char const *const restrict property, char const *const restrict value);
 void *flier_new(struct entities *entities);
 int flier_property(void *const restrict entity, char const *const restrict property, char const *const restrict value);
+void *pacer_new(struct entities *entities);
+int pacer_property(void *const restrict entity, char const *const restrict property, char const *const restrict value);
 
 void *warp_new(struct entities *entities);
 int warp_property(void *const restrict entity, char const *const restrict property, char const *const restrict value);
@@ -16,5 +18,6 @@ void funs_init(void) {
 	res_push_fun(save_new, save_property, "save");
 	res_push_fun(walker_new, walker_property, "walker");
 	res_push_fun(flier_new, flier_property, "flier");
+	res_push_fun(pacer_new, pacer_property, "pacer");
 	res_push_fun(warp_new, warp_property, "warp");
 }
diff --git a/src/gun.h b/src/gun.h
new file mode 100644
index 0000000..6dac3d7
--- /dev/null
+++ b/src/gun.h
@@ -0,0 +1,7 @@
+#pragma once
+
+struct gun {
+	int (*update)(struct gun *self, struct entity *parent, struct entity *target);
+	int timer, counter;
+	int x, y; // offset from the parents center
+};
diff --git a/src/hvy_guns.c b/src/hvy_guns.c
new file mode 100644
index 0000000..43fd1f1
--- /dev/null
+++ b/src/hvy_guns.c
@@ -0,0 +1,449 @@
+#include "loader.h"
+#include "tilemap.h"
+#include <math.h>
+#include "particles.h"
+#include "gun.h"
+
+#define POS_X (parent->x + self->x * parent->facing)
+#define POS_Y (parent->y + self->y)
+
+#define HVY_BLASTER_RELOAD 120
+
+#define HVY_SHOTGUN_RELOAD 150
+
+#define HVY_REPEATER_RELOAD 60
+#define HVY_REPEATER_ROUNDS 3
+
+#define HVY_CHAINGUN_RELOAD 30
+#define HVY_CHAINGUN_MIN_RELOAD 5
+
+static int bullet_update(struct projectile *self) {
+	self->x += self->velocity.x;
+	if (collision_solid(tilemap_tile(tilemap, from_fixed(self->x), from_fixed(self->y)))) {
+		self->velocity.x = -self->velocity.x;
+		self->x += self->velocity.x;
+	}
+		
+	self->y += self->velocity.y;
+	if (collision_solid(tilemap_tile(tilemap, from_fixed(self->x), from_fixed(self->y)))) {
+		self->velocity.y = -self->velocity.y;
+		self->y += self->velocity.y;
+	}
+
+	self->hp--;
+	int x = from_fixed(self->x);
+	int y = from_fixed(self->y);
+	self->hitbox = (struct hitbox) {.left = x, .right = x, .top = y, .bottom = y};
+	if (hitbox_overlap(self->hitbox, entities.player[0].hitbox)) {
+		if (entities.player[0].hurt(entities.player + 0, (struct damage) {1, 60})) {
+			//return 1;
+		}
+	}
+	if (self->hp == 0) {
+		return 1;
+		self->state = 0;
+	}
+	return 0;
+}
+
+static int projectile_update(struct projectile *self) {
+	self->x += self->velocity.x;
+	if (collision_solid(tilemap_tile(tilemap, from_fixed(self->x), from_fixed(self->y)))) {
+		goto explod;
+	}
+		
+	self->y += self->velocity.y;
+	if (collision_solid(tilemap_tile(tilemap, from_fixed(self->x), from_fixed(self->y)))) {
+		goto explod;
+	}
+
+	self->hp--;
+	int x = from_fixed(self->x);
+	int y = from_fixed(self->y);
+	self->hitbox = (struct hitbox) {.left = x, .right = x, .top = y, .bottom = y};
+	if (hitbox_overlap(self->hitbox, entities.player[0].hitbox)) {
+		entities.player[0].hurt(entities.player + 0, (struct damage) {1, 60});
+		goto explod;
+	}
+	if (self->hp == 0) {
+		goto explod;
+	}
+	return 0;
+	explod:
+	for (float r = 0; r < M_PI * 2; r += M_PI / 6) {
+		struct particle *part = entities.particle + entities.particles;
+		part->x = self->x;
+		part->y = self->y;
+		part->velocity = (struct vec2) {sin(r) * 24, cos(r) * 24};
+		part->rect = particle_red;
+		part->acceleration = (struct vec2) {0, 0};
+		part->hp = 15;
+		entities.particles++;
+	}
+	return 1;
+}
+
+static int laser_update(struct projectile *self) {
+	for (int i = 0; i < 15; i++) {
+	self->x += self->velocity.x;
+	if (collision_solid(tilemap_tile(tilemap, from_fixed(self->x), from_fixed(self->y)))) {
+		self->velocity.x = -self->velocity.x;
+		self->x += self->velocity.x;
+	}
+	
+	self->y += self->velocity.y;
+	if (collision_solid(tilemap_tile(tilemap, from_fixed(self->x), from_fixed(self->y)))) {
+		self->velocity.y = -self->velocity.y;
+		self->y += self->velocity.y;
+	}
+
+	int x = from_fixed(self->x);
+	int y = from_fixed(self->y);
+	self->hitbox = (struct hitbox) {.left = x, .right = x, .top = y, .bottom = y};
+	if (hitbox_overlap(self->hitbox, entities.player[0].hitbox)) {
+		if (entities.player[0].hurt(entities.player + 0, (struct damage) {1, 60})) {
+			return 1;
+		}
+	}
+		struct particle *part = entities.particle + entities.particles;
+		part->x = self->x;
+		part->y = self->y;
+		part->velocity = (struct vec2) {0, 0};
+		part->rect = particle_red;
+		part->acceleration = (struct vec2) {0, 0};
+		part->hp = i & 15;
+		entities.particles++;
+	}
+	self->hp -= 15;
+	if (self->hp <= 0) {
+		return 1;
+		self->state = 0;
+	}
+	return 0;
+}
+
+static int bullet_draw(struct projectile *self, int camX, int camY) {
+	SDL_Rect const *rect;
+	if (self->hp & 0x2) {
+		rect = &particle_red;
+	} else {
+		rect = &particle_white;
+	}
+	SDL_RenderCopy(renderer, self->texture, rect, &(SDL_Rect) {from_fixed(self->x) - camX - 1, from_fixed(self->y) - camY - 1, 4, 4});
+	SDL_RenderCopy(renderer, self->texture, rect, &(SDL_Rect) {from_fixed(self->x - self->velocity.x) - camX - 1, from_fixed(self->y - self->velocity.y) - camY - 1, 4, 4});
+	//SDL_RenderFillRect(renderer, &(SDL_Rect) {from_fixed(self->x) - camX - 8, from_fixed(self->y) - camY - 12, 16, 16});
+	return 0;
+}
+
+static void bullet_free(struct projectile *self) {}
+
+int hvy_blaster(struct gun *self, struct entity *parent, struct entity *target) {
+	if (self->timer > 0) {
+		self->timer--;
+		return 0;
+	}
+	if (target != NULL) {
+		entities.projectile[entities.projectiles] = (struct projectile) {
+			.update = bullet_update,
+			.draw = bullet_draw,
+			.free = bullet_free,
+			.x = POS_X, .y = POS_Y,
+			.velocity = (struct vec2) {parent->velocity.x + to_fixed(parent->facing), 1},
+			.hitbox = {
+				0, 0, 0, 0,
+			},
+			.state = 1,
+			.hp = 300,
+			.timer = 0,
+			.facing = parent->facing,
+			.faction = parent->faction,
+			.iframes = 0,
+			.texture = NULL,
+			.ext = NULL,
+		};
+		//anim(entities->projectile + entities->projectiles, PACER_A_IDLE);
+		entities.projectile[entities.projectiles].texture = res_get_texture("particles").data;
+		entities.projectiles++;
+		self->timer = HVY_BLASTER_RELOAD;
+	}
+	return 1;
+}
+
+void hvy_blaster_new(struct gun *self, int x, int y) {
+	self->update = hvy_blaster;
+	self->timer = HVY_BLASTER_RELOAD;
+	self->counter = 0;
+	self->x = to_fixed(x);
+	self->y = to_fixed(y);
+}
+
+int hvy_shotgun(struct gun *self, struct entity *parent, struct entity *target) {
+	if (self->timer > 0) {
+		self->timer--;
+		return 0;
+	}
+	if (target != NULL) {
+		float angle = atan2(target->x - POS_X, target->y - POS_Y);
+		for (int i = -2; i <= 2; i++) {
+			entities.projectile[entities.projectiles] = (struct projectile) {
+				.update = bullet_update,
+				.draw = bullet_draw,
+				.free = bullet_free,
+				.x = POS_X, .y = POS_Y,
+				.velocity = (struct vec2) {parent->velocity.x + to_fixed(sin(angle + i * 0.1)), to_fixed(cos(angle + i * 0.1))},
+				.hitbox = {
+					0, 0, 0, 0,
+				},
+				.state = 1,
+				.hp = 300,
+				.timer = 0,
+				.facing = parent->facing,
+				.faction = parent->faction,
+				.iframes = 0,
+				.texture = NULL,
+				.ext = NULL,
+			};
+			//anim(entities->projectile + entities->projectiles, PACER_A_IDLE);
+			entities.projectile[entities.projectiles].texture = res_get_texture("particles").data;
+			entities.projectiles++;
+		}
+		self->timer = HVY_SHOTGUN_RELOAD;
+	}
+	return 1;
+}
+
+void hvy_shotgun_new(struct gun *self, int x, int y) {
+	self->update = hvy_shotgun;
+	self->timer = HVY_SHOTGUN_RELOAD;
+	self->counter = 0;
+	self->x = to_fixed(x);
+	self->y = to_fixed(y);
+}
+
+int hvy_repeater(struct gun *self, struct entity *parent, struct entity *target) {
+	if (self->timer > 0) {
+		self->timer--;
+		return 0;
+	}
+	if (target != NULL && self->counter == 0) {
+		self->counter = HVY_REPEATER_ROUNDS;
+	}
+	if (self->counter > 0) {
+		entities.projectile[entities.projectiles] = (struct projectile) {
+			.update = bullet_update,
+			.draw = bullet_draw,
+			.free = bullet_free,
+			.x = POS_X, .y = POS_Y,
+			.velocity = (struct vec2) {parent->velocity.x + to_fixed(parent->facing), (int []) {0, 1, 0, -1}[self->counter % 4]},
+			.hitbox = {
+				0, 0, 0, 0,
+			},
+			.state = 1,
+			.hp = 300,
+			.timer = 0,
+			.facing = parent->facing,
+			.faction = parent->faction,
+			.iframes = 0,
+			.texture = NULL,
+			.ext = NULL,
+		};
+		//anim(entities->projectile + entities->projectiles, PACER_A_IDLE);
+		entities.projectile[entities.projectiles].texture = res_get_texture("particles").data;
+		entities.projectiles++;
+		self->counter--;
+		if (self->counter > 0) {
+			self->timer = 6;
+		} else {
+			self->timer = HVY_REPEATER_RELOAD;
+			if (target != NULL) {
+				return 1;
+			}
+		}
+		return 0;
+	}
+	return 1;
+}
+
+void hvy_repeater_new(struct gun *self, int x, int y) {
+	self->update = hvy_repeater;
+	self->timer = HVY_REPEATER_RELOAD;
+	self->counter = 0;
+	self->x = to_fixed(x);
+	self->y = to_fixed(y);
+}
+
+int hvy_chaingun(struct gun *self, struct entity *parent, struct entity *target) {
+	if (self->timer > 0) {
+		self->timer--;
+		return 0;
+	}
+	if (target != NULL) {
+		entities.projectile[entities.projectiles] = (struct projectile) {
+			.update = bullet_update,
+			.draw = bullet_draw,
+			.free = bullet_free,
+			.x = POS_X, .y = POS_Y,
+			.velocity = (struct vec2) {parent->velocity.x + to_fixed(parent->facing), (int []) {0, 1, 0, -1}[self->counter % 4]},
+			.hitbox = {
+				0, 0, 0, 0,
+			},
+			.state = 1,
+			.hp = 300,
+			.timer = 0,
+			.facing = parent->facing,
+			.faction = parent->faction,
+			.iframes = 0,
+			.texture = NULL,
+			.ext = NULL,
+		};
+		//anim(entities->projectile + entities->projectiles, PACER_A_IDLE);
+		entities.projectile[entities.projectiles].texture = res_get_texture("particles").data;
+		entities.projectiles++;
+		if (self->counter > HVY_CHAINGUN_MIN_RELOAD) {
+			self->counter--;
+		}
+		self->timer = self->counter;
+	} else {
+		self->counter = HVY_CHAINGUN_RELOAD;
+	}
+	return 1;
+}
+
+void hvy_chaingun_new(struct gun *self, int x, int y) {
+	self->update = hvy_chaingun;
+	self->timer = HVY_CHAINGUN_RELOAD;
+	self->counter = 0;
+	self->x = to_fixed(x);
+	self->y = to_fixed(y);
+}
+
+int hvy_launcher(struct gun *self, struct entity *parent, struct entity *target) {
+	if (self->timer > 0) {
+		self->timer--;
+		return 0;
+	}
+	if (target != NULL) {
+		entities.projectile[entities.projectiles] = (struct projectile) {
+			.update = projectile_update,
+			.draw = bullet_draw,
+			.free = bullet_free,
+			.x = POS_X, .y = POS_Y,
+			.velocity = (struct vec2) {parent->velocity.x + to_fixed(parent->facing), 1},
+			.hitbox = {
+				0, 0, 0, 0,
+			},
+			.state = 1,
+			.hp = 300,
+			.timer = 0,
+			.facing = parent->facing,
+			.faction = parent->faction,
+			.iframes = 0,
+			.texture = NULL,
+			.ext = NULL,
+		};
+		//anim(entities->projectile + entities->projectiles, PACER_A_IDLE);
+		entities.projectile[entities.projectiles].texture = res_get_texture("particles").data;
+		entities.projectiles++;
+		self->timer = HVY_BLASTER_RELOAD;
+	}
+	return 1;
+}
+
+void hvy_launcher_new(struct gun *self, int x, int y) {
+	self->update = hvy_launcher;
+	self->timer = HVY_BLASTER_RELOAD;
+	self->counter = 0;
+	self->x = to_fixed(x);
+	self->y = to_fixed(y);
+}
+
+int hvy_laser(struct gun *self, struct entity *parent, struct entity *target) {
+	if (self->counter == 0) {
+		if (self->timer > 0) {
+			self->timer--;
+			return 0;
+		}
+		if (target != NULL) {
+			float angle = atan2(target->x - POS_X, target->y - POS_Y);
+			#if 0
+			entities.projectile[entities.projectiles] = (struct projectile) {
+				.update = laser_update,
+				.draw = bullet_draw,
+				.free = bullet_free,
+				.x = POS_X, .y = POS_Y,
+				.velocity = (struct vec2) {parent->velocity.x + to_fixed(sin(angle)), to_fixed(cos(angle))},
+				.hitbox = {
+					0, 0, 0, 0,
+				},
+				.state = 1,
+				.hp = 500,
+				.timer = 0,
+				.facing = parent->facing,
+				.faction = parent->faction,
+				.iframes = 0,
+				.texture = NULL,
+				.ext = NULL,
+			};
+			//anim(entities->projectile + entities->projectiles, PACER_A_IDLE);
+			entities.projectile[entities.projectiles].texture = res_get_texture("particles").data;
+			entities.projectiles++;
+			self->timer = HVY_BLASTER_RELOAD;
+			#endif
+			self->counter = 25;
+			self->timer = angle * 128;
+			return 0;
+		}
+	} else {
+		if (target != NULL) {
+			self->counter--;
+			float angle = self->timer / 128.0;
+			if (self->counter != 0) {
+				float r = angle + (self->counter & 1? self->counter: -self->counter) * 0.05;
+				int const x = sin(r) * 16, y = cos(r) * 16;
+				struct particle *part = entities.particle + entities.particles;
+				part->x = POS_X + x * self->counter;
+				part->y = POS_Y + y * self->counter;
+				part->velocity = (struct vec2) {-x, -y};
+				part->rect = particle_white;
+				part->acceleration = (struct vec2) {0, 0};
+				part->hp = self->counter;
+				entities.particles++;
+				return 0;
+			}
+			entities.projectile[entities.projectiles] = (struct projectile) {
+				.update = laser_update,
+				.draw = bullet_draw,
+				.free = bullet_free,
+				.x = POS_X, .y = POS_Y,
+				.velocity = (struct vec2) {parent->velocity.x + to_fixed(sin(angle)), to_fixed(cos(angle))},
+				.hitbox = {
+					0, 0, 0, 0,
+				},
+				.state = 1,
+				.hp = 500,
+				.timer = 0,
+				.facing = parent->facing,
+				.faction = parent->faction,
+				.iframes = 0,
+				.texture = NULL,
+				.ext = NULL,
+			};
+			//anim(entities->projectile + entities->projectiles, PACER_A_IDLE);
+			entities.projectile[entities.projectiles].texture = res_get_texture("particles").data;
+			entities.projectiles++;
+			self->timer = HVY_BLASTER_RELOAD;
+		} else {
+			self->counter = 0;
+			self->timer = 0;
+		}
+	}
+	return 1;
+}
+
+void hvy_laser_new(struct gun *self, int x, int y) {
+	self->update = hvy_laser;
+	self->timer = HVY_BLASTER_RELOAD;
+	self->counter = 0;
+	self->x = to_fixed(x);
+	self->y = to_fixed(y);
+}
diff --git a/src/pacer.c b/src/pacer.c
new file mode 100644
index 0000000..9bbcfb6
--- /dev/null
+++ b/src/pacer.c
@@ -0,0 +1,330 @@
+#include "main.h"
+#include "entity.h"
+#include "loader.h"
+#include "tilemap.h"
+#include <stdbool.h>
+#include <math.h>
+#include "particles.h"
+#include "gun.h"
+
+#define HP 15
+#define WIDTH 8
+#define HEIGHT 10
+#define ACCELERATION 1
+#define FRICTION 2
+#define MAX_SPEED 8
+#define GRAVITY 1
+#define WALKABLE_CHECK_DISTANCE 8
+#define DETECTION_RANGE to_fixed(80)
+#define GIVE_UP_RANGE to_fixed(128)
+#define FAR_RANGE to_fixed(56)
+#define NEAR_RANGE to_fixed(40)
+#define COLLISION_DAMAGE 1
+#define COLLISION_IFRAMES 60
+
+struct pacer_ext {
+	unsigned detection_range, give_up_range;
+	signed facing;
+	struct gun primary, secondary;
+};
+
+enum {
+	PACER_NONE,
+	PACER_FALL,
+	PACER_IDLE,
+	PACER_PATROL,
+	PACER_ALERT_NEAR,
+	PACER_ALERT_APPROACH,
+	PACER_ALERT_HALT_FAR,
+	PACER_ALERT_FAR,
+	PACER_ALERT_BACK_AWAY,
+	PACER_ALERT_HALT_NEAR,
+};
+
+enum {
+	PACER_A_IDLE,
+	PACER_A_IDLE2,
+	PACER_A_REST,
+	PACER_A_WALK,
+	PACER_A_WALK2,
+	PACER_A_WALK3,
+	PACER_A_WALK4,
+};
+
+static struct anim pacer_anims[] = {
+	{PACER_A_IDLE2, {0, 0, 16, 16}, 300},
+	{PACER_A_IDLE, {16, 0, 16, 16}, 2},
+	{PACER_A_REST, {16, 0, 16, 16}, 300},
+	{PACER_A_WALK2, {0, 0, 16, 16}, 6},
+	{PACER_A_WALK3, {32, 0, 16, 16}, 6},
+	{PACER_A_WALK4, {0, 0, 16, 16}, 6},
+	{PACER_A_WALK, {48, 0, 16, 16}, 6},
+};
+
+void hvy_blaster_new(struct gun *self, int x, int y);
+void hvy_shotgun_new(struct gun *self, int x, int y);
+void hvy_repeater_new(struct gun *self, int x, int y);
+void hvy_chaingun_new(struct gun *self, int x, int y);
+void hvy_launcher_new(struct gun *self, int x, int y);
+void hvy_laser_new(struct gun *self, int x, int y);
+
+static collision_T collide(struct entity *self) {
+	return tilemap_area(tilemap, from_fixed(self->x) - WIDTH / 2, from_fixed(self->y) - HEIGHT, from_fixed(self->x) + WIDTH / 2, from_fixed(self->y));
+}
+
+static int move(struct entity *self, signed direction) {
+	int on_ground = false;
+	int const dx = direction * ACCELERATION;
+	self->velocity.x += dx;
+	// deaccel
+	if (dx == 0) {
+		if (self->velocity.x < -FRICTION) {
+			self->velocity.x += FRICTION;
+		} else if (self->velocity.x > FRICTION) {
+			self->velocity.x -= FRICTION;
+		} else {
+			self->velocity.x = 0;
+		}
+	}
+	// speed cap
+	if (self->velocity.x > MAX_SPEED) {
+		self->velocity.x = MAX_SPEED;
+	} else if (self->velocity.x < -MAX_SPEED) {
+		self->velocity.x = -MAX_SPEED;
+	}
+	// x collision
+	self->x += self->velocity.x;
+	collision_T const cx = collide(self);
+	if (collision_solid(cx)) {
+		if (self->velocity.x < 0) {
+			self->x += to_fixed(8) - ((self->x - to_fixed(WIDTH / 2)) % to_fixed(8)); // left
+		} else if (self->velocity.x == 0) {
+			//fputs("what?\n", stderr);
+		} else {
+			self->x -= ((self->x + to_fixed(WIDTH / 2)) % to_fixed(8)); // right
+		}
+		self->velocity.x = 0;
+	}
+	self->velocity.y += GRAVITY;
+	// y collision
+	self->y += self->velocity.y;
+	collision_T const cy = collide(self);
+	if (collision_solid(cy)) {
+		if (self->velocity.y < 0) {
+			self->y += to_fixed(8) - ((self->y - to_fixed(HEIGHT)) % to_fixed(8)); // up
+			self->velocity.y = 0;
+		} else if (self->velocity.y == 0) {
+			//fputs("what?\n", stderr);
+		} else {
+			self->y -= ((self->y) % to_fixed(8)); // down
+			self->velocity.y = to_fixed(1); // crazy but it works
+			on_ground = true;
+		}
+	}
+	
+	if (collision_hazard(cx | cy)) {
+		self->hurt(self, (struct damage) {1, 60});
+	}
+	
+	self->hitbox.left = from_fixed(self->x) - WIDTH / 2;
+	self->hitbox.right = from_fixed(self->x) + WIDTH / 2;
+	self->hitbox.top = from_fixed(self->y) - HEIGHT;
+	self->hitbox.bottom = from_fixed(self->y);
+	return on_ground;
+}
+
+static void anim(struct entity *self, unsigned anim) {
+	self->anim = pacer_anims[anim];
+}
+
+static inline int is_walkable(int const x, int const y) {
+	collision_T const front1 = tilemap_tile(tilemap, from_fixed(x), from_fixed(y) - 4);
+	collision_T const front2 = tilemap_tile(tilemap, from_fixed(x), from_fixed(y) - 10);
+	collision_T const floor = tilemap_tile(tilemap, from_fixed(x), from_fixed(y) + 4);
+	return !collision_solid(front1) && !collision_solid(front2) && collision_solid(floor) && !collision_hazard(floor);
+}
+
+static void pacer_free(struct entity *self) {
+	self->state = 0;
+	free(self->ext), self->ext = NULL;
+}
+
+static int pacer_update(struct entity *self) {
+	struct pacer_ext *const ext = self->ext;
+	if (self->hp <= 0) {
+		pacer_free(self);
+		entities.enemies--;
+		return 1;
+	}
+	switch (self->state) {
+		case PACER_IDLE:
+			move(self, 0);
+			ext->primary.update(&ext->primary, self, NULL);
+			ext->secondary.update(&ext->secondary, self, NULL);
+			if (abs(entities.player[0].x - self->x) + abs(entities.player[0].y - self->y) < ext->detection_range) {
+				self->facing = self->x > entities.player[0].x? -1: 1;
+				anim(self, PACER_A_WALK);
+				self->state = PACER_ALERT_APPROACH;
+			}
+			break;
+		
+		case PACER_ALERT_NEAR:
+			move(self, 0);
+			self->facing = self->x > entities.player[0].x? -1: 1;
+			if (ext->primary.update(&ext->primary, self, NULL)) {
+				anim(self, PACER_A_WALK);
+				self->state = PACER_ALERT_BACK_AWAY;
+			}
+			if (abs(entities.player[0].x - self->x) + abs(entities.player[0].y - self->y) > ext->give_up_range) {
+				anim(self, PACER_A_REST);
+				self->state = PACER_IDLE;
+			}
+			break;
+
+		case PACER_ALERT_APPROACH:
+			move(self, self->facing);
+			if (hitbox_overlap(self->hitbox, entities.player[0].hitbox)) {
+				entities.player[0].hurt(entities.player + 0, (struct damage) {COLLISION_DAMAGE, COLLISION_IFRAMES});
+			}
+			if (abs(entities.player[0].x - self->x) < NEAR_RANGE || !is_walkable(self->x + to_fixed(self->facing * WALKABLE_CHECK_DISTANCE), self->y)) {
+				anim(self, PACER_A_IDLE);
+				self->state = PACER_ALERT_HALT_NEAR;
+			}
+			break;
+
+		case PACER_ALERT_HALT_NEAR: // velocity affects bullets
+			move(self, 0);
+			if (self->velocity.x == 0) {
+				if (ext->primary.update(&ext->primary, self, entities.player)) {
+					anim(self, PACER_A_IDLE);
+					self->state = PACER_ALERT_NEAR;
+				}
+			}
+			break;
+
+		case PACER_ALERT_FAR:
+			move(self, 0);
+			self->facing = self->x > entities.player[0].x? -1: 1;
+			if (ext->secondary.update(&ext->secondary, self, NULL)) {
+				anim(self, PACER_A_WALK);
+				self->state = PACER_ALERT_APPROACH;
+			}
+			if (abs(entities.player[0].x - self->x) + abs(entities.player[0].y - self->y) > ext->give_up_range) {
+				anim(self, PACER_A_REST);
+				self->state = PACER_IDLE;
+			}
+			break;
+
+		case PACER_ALERT_BACK_AWAY:
+			move(self, -self->facing);
+			if (hitbox_overlap(self->hitbox, entities.player[0].hitbox)) {
+				entities.player[0].hurt(entities.player + 0, (struct damage) {COLLISION_DAMAGE, COLLISION_IFRAMES});
+			}
+			if (abs(entities.player[0].x - self->x) > FAR_RANGE || !is_walkable(self->x + to_fixed(-self->facing * WALKABLE_CHECK_DISTANCE), self->y)) {
+				anim(self, PACER_A_IDLE);
+				self->state = PACER_ALERT_HALT_FAR;
+			}
+			break;
+
+		case PACER_ALERT_HALT_FAR: // velocity affects bullets
+			move(self, 0);
+			if (self->velocity.x == 0) {
+				if (ext->secondary.update(&ext->secondary, self, entities.player)) {
+					anim(self, PACER_A_IDLE);
+					self->state = PACER_ALERT_FAR;
+				}
+			}
+			break;
+	}
+	if (self->iframes > 0) {
+		self->iframes--;
+	}
+	self->anim.length--;
+	if (self->anim.length == 0) {
+		anim(self, self->anim.frame);
+	}
+	return 0;
+}
+
+static int pacer_hurt(struct entity *self, struct damage damage) {
+	if (self->iframes == 0) {
+		self->hp -= damage.damage;
+		self->iframes = damage.iframes;
+		for (int x = -1; x <= 1; x += 2) {
+			for (int y = -1; y <= 1; y += 2) {
+				struct particle *part = entities.particle + entities.particles;
+				part->x = self->x;
+				part->y = self->y;
+				part->velocity = (struct vec2) {to_fixed(x) * 1.5, to_fixed(y) * 1.5};
+				part->rect = particle_white;
+				part->acceleration = (struct vec2) {-x, -y};
+				part->hp = 24;
+				entities.particles++;
+			}
+		}
+	}
+	return 0;
+}
+
+static int pacer_draw(struct entity *self, int camX, int camY) {
+	if (!(self->iframes & 0x4)) {
+		SDL_Rect rect = self->anim.rect;
+		if (self->facing == -1) {
+			rect.y += 16;
+		}
+		SDL_RenderCopy(renderer, self->texture, &rect, &(SDL_Rect) {from_fixed(self->x) - camX - 8, from_fixed(self->y) - camY - 16, 16, 16});
+	}
+	return 0;
+}
+
+struct entity *pacer_new(struct entities *entities) {
+	struct entity *self = entities->enemy + entities->enemies;
+	*self = (struct entity) {
+		.update = pacer_update,
+		.hurt = pacer_hurt,
+		.draw = pacer_draw,
+		.free = pacer_free,
+		.x = 0, .y = 0,
+		.velocity = {.x = 0, .y = 0},
+		.hitbox = {
+			0, 0, 0, 0,
+		},
+		.state = PACER_IDLE,
+		.hp = HP,
+		.timer = 0,
+		.facing = 1,
+		.faction = FACTION_ENEMY,
+		.iframes = 0,
+		.texture = NULL,
+		.ext = NULL,
+	};
+	self->ext = malloc(sizeof (struct pacer_ext));
+	*(struct pacer_ext *) self->ext = (struct pacer_ext) {
+		.detection_range = DETECTION_RANGE,
+		.give_up_range = GIVE_UP_RANGE,
+		.facing = 1,
+	};
+	struct pacer_ext *ext = self->ext;
+	hvy_repeater_new(&ext->primary, 5, -6);
+	hvy_blaster_new(&ext->secondary, 5, -6);
+	anim(self, PACER_A_REST);
+	self->texture = res_get_texture("pacer").data;
+	entities->enemies++;
+	return self;
+}
+
+int pacer_property(struct entity *const restrict self, char const *const restrict property, char const *const restrict value) {
+	struct pacer_ext *const ext = self->ext;
+	if (strcmp(property, "x") == 0) {
+		self->x = to_fixed(atoi(value));
+	} else if (strcmp(property, "y") == 0) {
+		self->y = to_fixed(atoi(value));
+	} else if (strcmp(property, "detection range") == 0) {
+		ext->detection_range = to_fixed(atoi(value));
+	} else if (strcmp(property, "give up range") == 0) {
+		ext->give_up_range = to_fixed(atoi(value));
+	} else {
+		return 1;
+	}
+	return 0;
+}
diff --git a/src/res/big.tmx b/src/res/big.tmx
index d9d35ce..8c10ff6 100644
--- a/src/res/big.tmx
+++ b/src/res/big.tmx
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<map version="1.10" tiledversion="1.11.0" orientation="orthogonal" renderorder="right-down" width="65" height="33" tilewidth="8" tileheight="8" infinite="0" backgroundcolor="#1d2b79" nextlayerid="5" nextobjectid="7">
+<map version="1.10" tiledversion="1.11.0" orientation="orthogonal" renderorder="right-down" width="65" height="33" tilewidth="8" tileheight="8" infinite="0" backgroundcolor="#1d2b79" nextlayerid="5" nextobjectid="8">
  <tileset firstgid="1" source="autotiles.tsx"/>
  <tileset firstgid="33" source="tileset.tsx"/>
  <group id="3" name="Group Layer 1">
@@ -23,7 +23,7 @@
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,37,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
-0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,65,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,0,0,0,0,0,0,65,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
@@ -56,16 +56,16 @@
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
-0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
-0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
-0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
-0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
-0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
-0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
-0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,13,13,13,5,0,0,0,0,0,0,0,0,0,0,9,13,13,13,13,13,13,5,0,0,0,0,0,0,0,0,0,
-0,9,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,5,9,13,13,13,13,13,13,13,13,13,13,13,13,13,13,15,8,4,12,14,13,5,9,13,13,13,13,13,13,13,15,8,4,4,4,4,12,14,13,5,0,0,0,0,0,0,0,
-0,3,4,4,4,4,4,4,4,4,4,4,4,4,4,4,12,14,15,8,4,4,4,4,4,4,4,4,4,4,4,4,4,4,2,0,3,4,12,14,15,8,4,4,4,4,4,4,4,2,0,0,0,0,3,4,4,2,0,0,0,0,0,0,0,
-0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,4,4,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,4,4,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,5,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,8,4,4,4,4,4,4,4,4,4,4,4,4,4,12,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,6,0,0,0,0,0,0,0,0,0,0,0,0,0,11,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,6,0,0,0,0,0,0,0,0,0,0,0,0,0,11,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,6,0,0,0,0,0,0,0,0,0,0,0,0,0,11,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,2,9,5,0,0,0,0,0,0,0,0,0,9,5,3,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,9,5,3,2,0,0,0,0,0,0,0,0,0,3,2,9,13,13,13,5,0,0,0,0,0,0,0,0,0,0,9,13,13,13,13,13,13,5,0,0,0,0,0,0,0,0,0,
+0,9,13,13,13,13,13,13,13,13,13,13,13,13,5,9,13,13,15,14,13,13,13,13,13,13,13,13,13,13,13,13,13,15,8,4,12,14,13,5,9,13,13,13,13,13,13,13,15,8,4,4,4,4,12,14,13,5,0,9,13,13,13,5,0,
+0,3,4,4,4,4,4,4,4,4,4,4,4,12,14,15,8,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,2,0,3,4,12,14,15,8,4,4,4,4,4,4,4,2,0,0,0,0,3,4,4,2,0,3,4,4,4,2,0,
+0,0,0,0,0,0,0,0,0,0,0,0,0,3,4,4,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,4,4,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
@@ -98,11 +98,14 @@
   <object id="4" name="walky" type="walker" x="408" y="96">
    <point/>
   </object>
-  <object id="5" name="walky" type="walker" x="200" y="112">
+  <object id="5" name="pacer" type="pacer" x="200" y="112">
    <point/>
   </object>
   <object id="6" name="disk" type="save" x="448" y="120">
    <point/>
   </object>
+  <object id="7" name="pacer" type="pacer" x="488" y="112">
+   <point/>
+  </object>
  </objectgroup>
 </map>
diff --git a/src/res/pacer.ase b/src/res/pacer.ase
new file mode 100644
index 0000000..7d8e666
--- /dev/null
+++ b/src/res/pacer.ase
Binary files differdiff --git a/src/save.c b/src/save.c
index 0264dfa..9baf981 100644
--- a/src/save.c
+++ b/src/save.c
@@ -1,5 +1,6 @@
 #include <stdio.h>
 #include <stdlib.h>
+#include <string.h>
 #include "main.h"
 #include "util.h"
 #include "util.h"