commit b6efce7dc80d44096c058ba013768601f57b5953
parent 5ec394929f5acb6a022d18a61d772b6f8723cb99
Author: brookjeynes <me@brookjeynes.dev>
Date: Fri, 31 Jul 2026 15:21:01 +1000
feat: upgrades
Signed-off-by: brookjeynes <me@brookjeynes.dev>
Diffstat:
7 files changed, 94 insertions(+), 21 deletions(-)
diff --git a/src/bullet.zig b/src/bullet.zig
@@ -1,7 +1,10 @@
+const std = @import("std");
+
const raylib = @import("raylib");
+
+const constants = @import("./constants.zig");
const Timer = @import("./timer.zig");
-pub const max_bullets: u8 = 255;
pub const radius: f32 = 10.0;
pub const Bullet = struct {
@@ -11,7 +14,7 @@ pub const Bullet = struct {
};
pub const Pool = struct {
- bullets: [255]Bullet,
+ bullets: [constants.max_bullets]Bullet,
count: usize = 0,
fire_timer: Timer,
@@ -25,6 +28,7 @@ pub const Pool = struct {
velocity: raylib.Vector2,
colour: raylib.Color,
) void {
+ if (self.count == self.bullets.len) return;
self.bullets[self.count] = .{
.position = position,
.velocity = velocity,
@@ -34,11 +38,11 @@ pub const Pool = struct {
}
pub fn despawn(self: *Pool, idx: usize) void {
- for (idx..self.count) |i| {
+ for (idx..self.count - 1) |i| {
self.bullets[i] = self.bullets[i + 1];
}
- self.count -= 1;
+ if (self.count > 0) self.count -= 1;
}
pub fn render(self: *Pool, screen_size: raylib.Vector2) void {
@@ -60,6 +64,7 @@ pub const Pool = struct {
}
raylib.DrawCircleV(bullet.position, radius, bullet.color);
+
i += 1;
}
}
diff --git a/src/constants.zig b/src/constants.zig
@@ -1,3 +1,4 @@
+pub const max_bullets: u8 = 255;
pub const max_enemies: u8 = 255;
pub const screen_width: u16 = 800;
pub const screen_height: u16 = 1000;
diff --git a/src/entity.zig b/src/entity.zig
@@ -1,6 +1,7 @@
-const Grunt = @import("./enemy_types/grunt.zig");
const raylib = @import("raylib");
+const Grunt = @import("./enemy_types/grunt.zig");
+
pub const EntityType = enum { grunt };
pub const Entity = union(EntityType) {
diff --git a/src/main.zig b/src/main.zig
@@ -2,12 +2,12 @@ const raylib = @import("raylib");
const raymath = @import("raymath");
const Bullet = @import("./bullet.zig");
+const constants = @import("./constants.zig");
const Grunt = @import("./enemy_types/grunt.zig");
const Player = @import("./player.zig");
const GameState = @import("./state.zig").GameState;
const Timer = @import("./timer.zig");
const Button = @import("./ui.zig").Button;
-const constants = @import("./constants.zig");
const start_button = Button.new(
.{ .x = 100, .y = 70 },
@@ -56,7 +56,6 @@ pub fn main() !void {
.upgrade => {},
.gameplay => {
// -- tick
-
for (0..game_state.enemy_count) |idx| {
game_state.enemies[idx].move(game_state.player.position);
}
@@ -78,7 +77,7 @@ pub fn main() !void {
// Health overlay
const health_percent: f32 = @max(
0,
- @min(1, game_state.player.health / Player.max_health),
+ @min(1, game_state.player.health / game_state.player.max_health),
);
const damage_percent = 1 - health_percent;
raylib.DrawRectangle(
diff --git a/src/player.zig b/src/player.zig
@@ -1,25 +1,25 @@
const raylib = @import("raylib");
const raymath = @import("raymath");
-const std = @import("std");
const bullet = @import("./bullet.zig");
const Timer = @import("./timer.zig");
pub const Player = @This();
-const bullet_speed: f32 = 300.0;
pub const bullet_spawn_cooldown: f32 = 0.4;
const acceleration: f32 = 10000.0;
const friction: f32 = 10000.0;
const max_delta: f32 = 1.0 / 30.0;
-pub const max_health: f32 = 255;
-const health_regen_amount: f32 = 50;
+const starting_max_health: u8 = 255;
movement_speed: f32 = 140.0,
+bullet_speed: f32 = 300.0,
+health_regen_amount: f32 = 50,
size: raylib.Vector2 = .{ .x = 50, .y = 50 },
position: raylib.Vector2 = .{},
velocity: raylib.Vector2 = .{},
-health: f32 = max_health,
+max_health: f32 = starting_max_health,
+health: f32 = starting_max_health,
health_regen_timer: Timer = .{
.duration = 5.0,
},
@@ -41,7 +41,7 @@ pub fn render(self: *Player) void {
pub fn tick(self: *Player) void {
if (self.health_regen_timer.tick(raylib.GetFrameTime())) {
- self.health = @min(self.health + health_regen_amount, max_health);
+ self.health = @min(self.health + self.health_regen_amount, self.max_health);
}
}
@@ -105,8 +105,8 @@ pub fn shoot(self: *Player) void {
.y = mouse_pos.y - center.y,
});
const bullet_vel: raylib.Vector2 = .{
- .x = dir.x * bullet_speed,
- .y = dir.y * bullet_speed,
+ .x = dir.x * self.bullet_speed,
+ .y = dir.y * self.bullet_speed,
};
self.bullet_pool.spawn(center, bullet_vel, raylib.BLUE);
}
diff --git a/src/state.zig b/src/state.zig
@@ -8,6 +8,8 @@ const Entity = @import("./entity.zig").Entity;
const EntityType = @import("./entity.zig").EntityType;
const Player = @import("./player.zig");
const Timer = @import("./timer.zig");
+const UpgradeId = @import("./upgrade.zig").UpgradeId;
+const Upgrades = @import("./upgrade.zig").Upgrades;
pub const Screen = enum(u3) {
title,
@@ -20,6 +22,7 @@ pub const GameState = struct {
time: f32,
player: Player,
+ upgrades: Upgrades,
enemies: [constants.max_enemies]Entity,
enemy_count: u8,
@@ -42,8 +45,10 @@ pub const GameState = struct {
.y = constants.screen_height / 2,
},
},
+ .upgrades = .{ .levels = [_]u8{0} ** UpgradeId.count },
.enemies = undefined,
+ .enemies_killed = 0,
.enemy_count = 0,
};
}
@@ -56,7 +61,7 @@ pub const GameState = struct {
}
pub fn despawn_enemy(self: *GameState, enemy_idx: u8) void {
- for (enemy_idx..self.enemy_count) |i| {
+ for (enemy_idx..self.enemy_count - 1) |i| {
self.enemies[i] = self.enemies[i + 1];
}
@@ -70,7 +75,8 @@ pub const GameState = struct {
self.enemy_count = 0;
self.enemy_spawn_time = .{ .duration = 0.7 };
- self.player.health = Player.max_health;
+ self.upgrades.apply(&self.player);
+ self.player.health = self.player.max_health;
self.player.position = .{
.x = constants.screen_width / 2,
.y = constants.screen_height / 2,
@@ -110,7 +116,7 @@ pub const GameState = struct {
}
var enemy_idx: u8 = 0;
- while (enemy_idx < self.enemy_count) {
+ lbl: while (enemy_idx < self.enemy_count) {
const entity = &self.enemies[enemy_idx];
switch (entity.*) {
@@ -158,9 +164,8 @@ pub const GameState = struct {
if (is_collision) {
self.player.bullet_pool.despawn(player_bullet_idx);
-
self.despawn_enemy(enemy_idx);
- enemy_idx -= 1;
+ continue :lbl;
}
}
},
diff --git a/src/upgrade.zig b/src/upgrade.zig
@@ -0,0 +1,62 @@
+const Player = @import("./player.zig");
+
+pub const UpgradeId = enum(u8) {
+ max_health,
+ movement_speed,
+ bullet_speed,
+ fire_rate,
+ regen,
+
+ pub const count = @typeInfo(UpgradeId).@"enum".fields.len;
+};
+
+const Meta = struct {
+ name: []const u8,
+ base: f32,
+ per_level: f32,
+ max_level: u8,
+ cost: u8,
+};
+
+pub const meta: [UpgradeId.count]Meta = .{
+ .{ .name = "health", .base = 255, .per_level = 50, .max_level = 10, .cost = 5 },
+ .{ .name = "speed", .base = 140, .per_level = 10, .max_level = 10, .cost = 5 },
+ .{ .name = "bullet spd", .base = 300, .per_level = 20, .max_level = 10, .cost = 5 },
+ .{ .name = "fire rate", .base = 0.4, .per_level = -0.02, .max_level = 10, .cost = 5 },
+ .{ .name = "regen", .base = 50, .per_level = 10, .max_level = 10, .cost = 5 },
+};
+
+pub const Upgrades = struct {
+ levels: [UpgradeId.count]u8,
+
+ pub fn level(self: Upgrades, comptime id: UpgradeId) u8 {
+ return self.levels[@intFromEnum(id)];
+ }
+
+ pub fn maxed(self: Upgrades, comptime id: UpgradeId) bool {
+ return self.level(id) >= meta[@intFromEnum(id)].max_level;
+ }
+
+ pub fn stat(self: Upgrades, comptime id: UpgradeId) f32 {
+ const m = meta[@intFromEnum(id)];
+ return m.base + m.per_level * @as(f32, @floatFromInt(self.level(id)));
+ }
+
+ pub fn cost(self: Upgrades, comptime id: UpgradeId) u8 {
+ return meta[@intFromEnum(id)].cost * (self.level(id) + 1);
+ }
+
+ pub fn upgrade(self: *Upgrades, comptime id: UpgradeId) bool {
+ if (self.maxed(id)) return false;
+ self.levels[@intFromEnum(id)] += 1;
+ return true;
+ }
+
+ pub fn apply(self: Upgrades, player: *Player) void {
+ player.max_health = self.stat(.max_health);
+ player.movement_speed = self.stat(.movement_speed);
+ player.bullet_speed = self.stat(.bullet_speed);
+ player.health_regen_amount = self.stat(.regen);
+ player.bullet_pool.fire_timer = .{ .duration = self.stat(.fire_rate) };
+ }
+};