diff --git a/src/engine/frame.zig b/src/engine/frame.zig index 4b4f2bf..fc800cf 100644 --- a/src/engine/frame.zig +++ b/src/engine/frame.zig @@ -257,7 +257,7 @@ pub const DrawTextOptions = struct { color: Vec4 = rgb(255, 255, 255), }; -pub fn drawText(self: *Frame, position: Vec2, text: []const u8, opts: DrawTextOptions) void { +pub fn drawText(self: *Frame, position: Vec2, opts: DrawTextOptions, text: []const u8) void { const arena = self.arena.allocator(); const text_dupe = arena.dupe(u8, text) catch |e| { log.warn("Failed to draw text: {}", .{e}); @@ -275,6 +275,30 @@ pub fn drawText(self: *Frame, position: Vec2, text: []const u8, opts: DrawTextOp }); } +pub fn drawTextFormat( + self: *Frame, + position: Vec2, + opts: DrawTextOptions, + comptime fmt: []const u8, + args: anytype +) void { + const arena = self.arena.allocator(); + const text = std.fmt.allocPrint(arena, fmt, args) catch |e| { + log.warn("Failed to draw text: {}", .{e}); + return; + }; + + self.pushGraphicsCommand(.{ + .draw_text = .{ + .pos = position, + .text = text, + .size = opts.size, + .font = opts.font, + .color = opts.color, + } + }); +} + pub fn pushTransform(self: *Frame, translation: Vec2, scale: Vec2) void { self.pushGraphicsCommand(.{ .push_transformation = .{ diff --git a/src/engine/math.zig b/src/engine/math.zig index f9861e5..7933229 100644 --- a/src/engine/math.zig +++ b/src/engine/math.zig @@ -48,6 +48,12 @@ pub const Vec2 = extern struct { return .init(@floatFromInt(x), @floatFromInt(y)); } + pub fn initRandomRect(rng: std.Random, rect: Rect) Vec2 { + const x = Range.init(rect.left(), rect.right()).random(rng); + const y = Range.init(rect.top(), rect.bottom()).random(rng); + return init(x, y); + } + pub fn initAngle(angle: f32) Vec2 { return Vec2{ .x = @cos(angle), @@ -431,6 +437,24 @@ pub const Rect = struct { return (lhs.left() < rhs.right() and lhs.right() > rhs.left()) and (lhs.top() < rhs.bottom() and lhs.bottom() > rhs.top()); } + + pub fn growX(self: Rect, amount: f32) Rect { + return Rect{ + .pos = .init(self.pos.x - amount, self.pos.y), + .size = .init(self.size.x + 2*amount, self.size.y) + }; + } + + pub fn growY(self: Rect, amount: f32) Rect { + return Rect{ + .pos = .init(self.pos.x, self.pos.y - amount), + .size = .init(self.size.x, self.size.y + 2*amount) + }; + } + + pub fn grow(self: Rect, amount: f32) Rect { + return self.growX(self.growY(amount), amount); + } }; pub const Line = struct { diff --git a/src/game.zig b/src/game.zig index e8669d6..13c3779 100644 --- a/src/game.zig +++ b/src/game.zig @@ -26,6 +26,7 @@ const Sprite = Engine.Graphics.Sprite; const RaycastTileIterator = @import("./raycast_tile_iterator.zig"); const Game = @This(); +const world_size = Vec2.init(20 * 16, 15 * 16); const RNGState = std.Random.DefaultPrng; @@ -74,6 +75,27 @@ const Enemy = struct { dead: bool = false }; +const Wave = struct { + started_at: Nanoseconds, + duration: Nanoseconds, + enemies_spawned: u32, + total_enemies: u32, + + const Info = struct { + enemies: u32, + duration_s: u32, + starts_at_s: u32, + }; +}; + +const wave_infos = [_]Wave.Info{ + .{ + .enemies = 10, + .duration_s = 1, + .starts_at_s = 0 + } +}; + arena: std.heap.ArenaAllocator, gpa: Allocator, rng: RNGState, @@ -83,6 +105,11 @@ player: Player = .{}, bullets: std.ArrayList(Bullet) = .empty, enemies: std.ArrayList(Enemy) = .empty, +wave_timer: Nanoseconds = 0, + +waves: std.ArrayList(Wave) = .empty, +spawned_waves: std.ArrayList(usize) = .empty, + pub fn init(gpa: Allocator, seed: u64, assets: *Assets) !Game { var arena = std.heap.ArenaAllocator.init(gpa); errdefer arena.deinit(); @@ -104,6 +131,8 @@ pub fn deinit(self: *Game) void { self.arena.deinit(); self.bullets.deinit(self.gpa); self.enemies.deinit(self.gpa); + self.waves.deinit(self.gpa); + self.spawned_waves.deinit(self.gpa); } fn getCenteredRect(pos: Vec2, size: f32) Rect { @@ -113,11 +142,102 @@ fn getCenteredRect(pos: Vec2, size: f32) Rect { }; } +fn spawnEnemy(self: *Game) !void { + const spawn_area_margin = 20; + const spawn_area_size = 10; + + const top_spawn_area = (Rect{ + .pos = .init(0, -spawn_area_size - spawn_area_margin), + .size = .init(world_size.x, spawn_area_size) + }).growX(spawn_area_size); + + const bottom_spawn_area = (Rect{ + .pos = .init(0, world_size.y + spawn_area_margin), + .size = .init(world_size.x, spawn_area_size) + }).growX(spawn_area_size); + + const left_spawn_area = (Rect{ + .pos = .init(-spawn_area_margin-spawn_area_size, 0), + .size = .init(spawn_area_size, world_size.y) + }).growY(spawn_area_size); + + const right_spawn_area = (Rect{ + .pos = .init(world_size.x + spawn_area_margin, 0), + .size = .init(spawn_area_size, world_size.y) + }).growY(spawn_area_size); + + const spawn_areas = [_]Rect{ + top_spawn_area, + bottom_spawn_area, + left_spawn_area, + right_spawn_area + }; + + const rand = self.rng.random(); + const spawn_area = spawn_areas[rand.uintLessThan(usize, spawn_areas.len)]; + const pos = Vec2.initRandomRect(rand, spawn_area); + + try self.enemies.append(self.gpa, .{ + .kinetic = .{ .pos = pos }, + .speed = 10, + .size = 20 + }); +} + pub fn tick(self: *Game, frame: *Engine.Frame) !void { const dt = frame.deltaTime(); - const canvas_size = Vec2.init(20 * 16, 15 * 16); - frame.graphics.canvas_size = canvas_size; + self.wave_timer += frame.dt_ns; + const wave_timer_s = @divFloor(self.wave_timer, std.time.ns_per_s); + + for (0.., wave_infos) |i, wave_info| { + if (std.mem.indexOfScalar(usize, self.spawned_waves.items, i) != null) { + continue; + } + + if (wave_info.starts_at_s > wave_timer_s) { + continue; + } + + try self.spawned_waves.append(self.gpa, i); + try self.waves.append(self.gpa, .{ + .started_at = self.wave_timer, + .duration = wave_info.duration_s * std.time.ns_per_s, + .enemies_spawned = 0, + .total_enemies = wave_info.enemies + }); + } + + { + var index: usize = 0; + while (index < self.waves.items.len) { + var wave_complete = false; + const wave = &self.waves.items[index]; + + const wave_time_passed: f32 = @floatFromInt(self.wave_timer - wave.started_at); + const wave_duration: f32 = @floatFromInt(wave.duration); + const percent_complete = std.math.clamp(wave_time_passed / wave_duration, 0, 1); + const wave_total_enemies: f32 = @floatFromInt(wave.total_enemies); + const expected_enemies: u32 = @intFromFloat(wave_total_enemies * percent_complete); + + while (wave.enemies_spawned < expected_enemies) { + try self.spawnEnemy(); + wave.enemies_spawned += 1; + } + + if (wave.enemies_spawned == wave.total_enemies) { + wave_complete = true; + } + + if (wave_complete) { + _ = self.waves.swapRemove(index); + } else { + index += 1; + } + } + } + + frame.graphics.canvas_size = world_size; if (frame.isKeyPressed(.F3)) { frame.show_debug = !frame.show_debug; @@ -128,7 +248,7 @@ pub fn tick(self: *Game, frame: *Engine.Frame) !void { } frame.drawRectangle(.{ - .rect = .init(0, 0, canvas_size.x, canvas_size.y), + .rect = .init(0, 0, world_size.x, world_size.y), .color = rgb(20, 20, 20) }); @@ -183,16 +303,6 @@ pub fn tick(self: *Game, frame: *Engine.Frame) !void { .speed = 50 }); } - - if (frame.isMousePressed(.right)) { - try self.enemies.append(self.gpa, .{ - .kinetic = .{ - .pos = mouse, - }, - .speed = 10, - .size = 20 - }); - } } frame.drawRectangle(.{ @@ -253,6 +363,14 @@ pub fn tick(self: *Game, frame: *Engine.Frame) !void { } } } + + const text_opts = Engine.Frame.DrawTextOptions{ + .font = self.assets.font_id.get(.regular) + }; + frame.drawTextFormat(.init(10, 10), text_opts, "{d:02}:{d:02}", .{ + @divFloor(wave_timer_s, 60), + @mod(wave_timer_s, 60) + }); } pub fn debug(self: *Game) !void { @@ -265,5 +383,10 @@ pub fn debug(self: *Game) !void { } defer imgui.endWindow(); - _ = self; + if (imgui.button("Spawn enemy")) { + try self.spawnEnemy(); + } + + imgui.textFmt("Waves: {}\n", .{self.waves.items.len}); + imgui.textFmt("Enemies: {}\n", .{self.enemies.items.len}); }