commit 2fe84e94cecc71ae5e67988e8226eb772718d4c0
parent 410b751ad9501886166e35a73a400a597286fce2
Author: brookjeynes <me@brookjeynes.dev>
Date: Fri, 7 Aug 2026 08:58:36 +1000
feat: new enemy type
Signed-off-by: brookjeynes <me@brookjeynes.dev>
Diffstat:
3 files changed, 183 insertions(+), 5 deletions(-)
diff --git a/src/enemy_types/blorb.zig b/src/enemy_types/blorb.zig
@@ -0,0 +1,88 @@
+const std = @import("std");
+
+const raylib = @import("raylib");
+
+const bullet = @import("../bullet.zig");
+
+pub const bullet_damage: f32 = 80.0;
+pub const size: raylib.Vector2 = .{ .x = 60, .y = 60 };
+
+pub const Blorb = @This();
+
+movement_speed: f32 = 20.0,
+bullet_speed: f32 = 50.0,
+direction: raylib.Vector2 = .{},
+position: raylib.Vector2 = .{},
+shoot_offset: u2 = 0,
+
+bullet_pool: bullet.Pool,
+
+pub fn tick(self: *Blorb) void {
+ if (self.bullet_pool.tick()) self.shoot();
+}
+
+pub fn shoot(self: *Blorb) void {
+ const center: raylib.Vector2 = .{
+ .x = self.position.x + (size.x / 2),
+ .y = self.position.y + (size.y / 2),
+ };
+
+ for ([_]u2{ 0, 1, 2, 3 }) |delta| {
+ const side = self.shoot_offset +% delta;
+ const spawn_position: raylib.Vector2 = switch (side) {
+ 0 => .{ .x = center.x, .y = self.position.y },
+ 1 => .{ .x = self.position.x + size.x, .y = center.y },
+ 2 => .{ .x = center.x, .y = self.position.y + size.y },
+ else => .{ .x = self.position.x, .y = center.y },
+ };
+ const bullet_velocity: raylib.Vector2 = switch (side) {
+ 0 => .{ .x = 0, .y = -self.bullet_speed },
+ 1 => .{ .x = self.bullet_speed, .y = 0 },
+ 2 => .{ .x = 0, .y = self.bullet_speed },
+ else => .{ .x = -self.bullet_speed, .y = 0 },
+ };
+ self.bullet_pool.spawn(spawn_position, bullet_velocity, raylib.PURPLE);
+ }
+
+ self.shoot_offset +%= 1;
+}
+
+pub fn setRandomDirection(self: *Blorb) void {
+ const angle: f32 = @as(f32, @floatFromInt(raylib.GetRandomValue(0, 360)));
+ self.direction = .{
+ .x = @cos(angle * std.math.pi / 180.0),
+ .y = @sin(angle * std.math.pi / 180.0),
+ };
+}
+
+pub fn handleMovement(self: *Blorb) void {
+ const delta: f32 = raylib.GetFrameTime();
+
+ self.position.x += self.direction.x * self.movement_speed * delta;
+ self.position.y += self.direction.y * self.movement_speed * delta;
+
+ const max_x: f32 = @as(f32, @floatFromInt(raylib.GetScreenWidth())) - size.x;
+ const max_y: f32 = @as(f32, @floatFromInt(raylib.GetScreenHeight())) - size.y;
+
+ if (self.position.x <= 0 or self.position.x >= max_x) {
+ self.direction.x = -self.direction.x;
+ self.position.x = @max(0, @min(max_x, self.position.x));
+ }
+ if (self.position.y <= 0 or self.position.y >= max_y) {
+ self.direction.y = -self.direction.y;
+ self.position.y = @max(0, @min(max_y, self.position.y));
+ }
+}
+
+pub fn render(self: *Blorb) void {
+ raylib.DrawRectangleV(
+ self.position,
+ size,
+ raylib.PURPLE,
+ );
+
+ self.bullet_pool.render(.{
+ .x = @floatFromInt(raylib.GetScreenWidth()),
+ .y = @floatFromInt(raylib.GetScreenHeight()),
+ });
+}
diff --git a/src/entity.zig b/src/entity.zig
@@ -1,27 +1,32 @@
const raylib = @import("raylib");
const Grunt = @import("./enemy_types/grunt.zig");
+const Blorb = @import("./enemy_types/blorb.zig");
-pub const EntityType = enum { grunt };
+pub const EntityType = enum { grunt, blorb };
pub const Entity = union(EntityType) {
grunt: Grunt,
+ blorb: Blorb,
pub fn render(entity: *Entity) void {
switch (entity.*) {
.grunt => |*grunt| grunt.render(),
+ .blorb => |*blorb| blorb.render(),
}
}
pub fn move(entity: *Entity, target_position: raylib.Vector2) void {
switch (entity.*) {
.grunt => |*grunt| grunt.handleMovement(target_position),
+ .blorb => |*blorb| blorb.handleMovement(),
}
}
pub fn tick(entity: *Entity) void {
switch (entity.*) {
.grunt => |*grunt| grunt.tick(),
+ .blorb => |*blorb| blorb.tick(),
}
}
};
diff --git a/src/state.zig b/src/state.zig
@@ -4,6 +4,7 @@ const raymath = @import("raymath");
const Bullet = @import("./bullet.zig");
const constants = @import("./constants.zig");
const Grunt = @import("./enemy_types/grunt.zig");
+const Blorb = @import("./enemy_types/blorb.zig");
const Entity = @import("./entity.zig").Entity;
const EntityType = @import("./entity.zig").EntityType;
const Player = @import("./player.zig");
@@ -27,7 +28,8 @@ pub const GameState = struct {
enemies: [constants.max_enemies]Entity,
enemy_count: u8,
- enemy_spawn_time: Timer = .{ .duration = 0.7 },
+ grunt_spawn_time: Timer = .{ .duration = 0.7 },
+ blorb_spawn_time: Timer = .{ .duration = 5.0 },
pub fn init() GameState {
return .{
@@ -74,7 +76,9 @@ pub const GameState = struct {
self.time = 0;
self.enemy_count = 0;
- self.enemy_spawn_time = .{ .duration = 0.7 };
+ self.enemies_killed = 0;
+ self.grunt_spawn_time = .{ .duration = 0.7 };
+ self.blorb_spawn_time = .{ .duration = 5.0 };
self.upgrades.apply(&self.player);
self.player.health = self.player.max_health;
@@ -93,7 +97,8 @@ pub const GameState = struct {
const delta = raylib.GetFrameTime();
self.time += delta;
- self.enemy_spawn_time.duration = @max(0.15, 0.7 / difficulty);
+ self.grunt_spawn_time.duration = @max(0.15, 0.7 / difficulty);
+ self.blorb_spawn_time.duration = @max(0.15, 5.0 / difficulty);
self.player.tick();
if (self.player.health <= 0) {
@@ -104,7 +109,7 @@ pub const GameState = struct {
self.enemies[i].tick();
}
- if (self.enemy_spawn_time.tick(delta)) {
+ if (self.grunt_spawn_time.tick(delta)) {
const grunt = createEnemy(
.{ .x = constants.screen_width, .y = constants.screen_height },
self.player,
@@ -116,6 +121,18 @@ pub const GameState = struct {
self.spawn_enemy(grunt);
}
+ if (self.blorb_spawn_time.tick(delta)) {
+ const blorb = createEnemy(
+ .{ .x = constants.screen_width, .y = constants.screen_height },
+ self.player,
+ difficulty,
+ .blorb,
+ Blorb.size,
+ );
+
+ self.spawn_enemy(blorb);
+ }
+
var enemy_idx: u8 = 0;
lbl: while (enemy_idx < self.enemy_count) {
const entity = &self.enemies[enemy_idx];
@@ -171,6 +188,56 @@ pub const GameState = struct {
}
}
},
+ .blorb => |*blorb| {
+ var enemy_bullet_idx = blorb.bullet_pool.count;
+ while (enemy_bullet_idx > 0) {
+ enemy_bullet_idx -= 1;
+
+ const bullet = blorb.bullet_pool.bullets[enemy_bullet_idx];
+ const is_collision = raylib.CheckCollisionCircleRec(
+ .{ .x = bullet.position.x, .y = bullet.position.y },
+ Bullet.radius,
+ .{
+ .x = self.player.position.x,
+ .y = self.player.position.y,
+ .width = self.player.size.x,
+ .height = self.player.size.y,
+ },
+ );
+
+ if (is_collision) {
+ blorb.bullet_pool.despawn(enemy_bullet_idx);
+ self.player.handleDamage(Blorb.bullet_damage);
+ }
+ }
+
+ var player_bullet_idx = self.player.bullet_pool.count;
+ while (player_bullet_idx > 0) {
+ player_bullet_idx -= 1;
+
+ const bullet = self.player.bullet_pool.bullets[player_bullet_idx];
+ const is_collision = raylib.CheckCollisionCircleRec(
+ .{
+ .x = bullet.position.x,
+ .y = bullet.position.y,
+ },
+ Bullet.radius,
+ .{
+ .x = blorb.position.x,
+ .y = blorb.position.y,
+ .width = Blorb.size.x,
+ .height = Blorb.size.y,
+ },
+ );
+
+ if (is_collision) {
+ self.player.bullet_pool.despawn(player_bullet_idx);
+ self.despawn_enemy(enemy_idx);
+ self.enemies_killed += 1;
+ continue :lbl;
+ }
+ }
+ },
}
enemy_idx += 1;
@@ -228,5 +295,23 @@ fn createEnemy(
break :lbl .{ .grunt = grunt };
},
+ .blorb => lbl: {
+ var blorb = Blorb{
+ .bullet_pool = .{
+ .bullets = undefined,
+ .fire_timer = .{
+ .duration = 4.0,
+ },
+ },
+ .position = spawn_position,
+ };
+
+ blorb.movement_speed = @min(160.0, 40.0 * difficulty);
+ blorb.bullet_speed = @min(300.0, 100.0 * difficulty);
+ blorb.bullet_pool.fire_timer.duration = @max(0.3, 2.0 / difficulty);
+ blorb.setRandomDirection();
+
+ break :lbl .{ .blorb = blorb };
+ },
};
}