diff --git a/src/combat_screen.zig b/src/combat_screen.zig new file mode 100644 index 0000000..79d70b2 --- /dev/null +++ b/src/combat_screen.zig @@ -0,0 +1,444 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const Assets = @import("./assets.zig"); + +const Engine = @import("./engine/root.zig"); +const Nanoseconds = Engine.Nanoseconds; +const Vec2 = Engine.Vec2; +const Rect = Engine.Math.Rect; +const Range = Engine.Math.Range; +const rgb = Engine.Math.rgb; +const rgba = Engine.Math.rgba; + +const CombatScreen = @This(); + +const RNGState = std.Random.DefaultPrng; + +const Kinetic = struct { + pos: Vec2 = .zero, + vel: Vec2 = .zero, + acc: Vec2 = .zero, + + const UpdateOptions = struct { + max_speed: ?f32 = null, + friction: ?f32 = null, + }; + + pub fn update(self: *Kinetic, dt: f32, opts: UpdateOptions) void { + self.vel = self.vel.add(self.acc.multiplyScalar(dt)); + if (opts.max_speed) |max_speed| { + self.vel = self.vel.limitLength(max_speed); + } + if (opts.friction) |friction| { + const friction_force = std.math.pow(f32, 1 - friction, dt); + self.vel = self.vel.multiplyScalar(friction_force); + } + self.pos = self.pos.add(self.vel.multiplyScalar(dt)); + } +}; + +const Player = struct { + kinetic: Kinetic = .{}, + money: u32 = 0, + health: u32 = 0, + max_health: u32 = 0, + last_shot_at: ?Nanoseconds = null, + invincible_until: ?Nanoseconds = null, + + pub fn getRect(self: Player) Rect { + return getCenteredRect(self.kinetic.pos, 16); + } +}; + +const Bullet = struct { + kinetic: Kinetic, + size: f32 = 5, + dir: Vec2, + speed: f32, + + dead: bool = false +}; + +const Enemy = struct { + kinetic: Kinetic, + speed: f32, + size: f32, + + 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 Pickup = struct { + const Kind = enum { + money + }; + + pos: Vec2, + kind: Kind, +}; + +const world_size = Vec2.init(20 * 16, 15 * 16); +const invincibility_duration_s = 0.5; +const pickup_spawn_duration_s = Range.init(1, 5); +const wave_infos = [_]Wave.Info{ + .{ + .enemies = 10, + .duration_s = 10, + .starts_at_s = 0 + } +}; + +gpa: Allocator, +assets: *Assets, +rng: RNGState, + +player: Player = .{}, +bullets: std.ArrayList(Bullet) = .empty, +enemies: std.ArrayList(Enemy) = .empty, + +pickups: std.ArrayList(Pickup) = .empty, +next_pickup_spawn_at: Nanoseconds, + +wave_timer: Nanoseconds = 0, + +waves: std.ArrayList(Wave) = .empty, +spawned_waves: std.ArrayList(usize) = .empty, + +pub fn init(gpa: Allocator, seed: u64, assets: *Assets) CombatScreen { + var rng = RNGState.init(seed); + + const next_pickup_spawn_at_s = pickup_spawn_duration_s.random(rng.random()); + const next_pickup_spawn_at: Nanoseconds = @intFromFloat(next_pickup_spawn_at_s * std.time.ns_per_s); + + return CombatScreen{ + .gpa = gpa, + .assets = assets, + .next_pickup_spawn_at = next_pickup_spawn_at, + .player = .{ + .kinetic = .{ + .pos = .init(50, 50), + }, + .health = 3, + .max_health = 3 + }, + .rng = rng + }; +} + +pub fn deinit(self: *CombatScreen) void { + self.bullets.deinit(self.gpa); + self.enemies.deinit(self.gpa); + self.waves.deinit(self.gpa); + self.pickups.deinit(self.gpa); + self.spawned_waves.deinit(self.gpa); +} + +pub fn spawnEnemy(self: *CombatScreen) !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 spawnPickup(self: *CombatScreen) !void { + const margin = 10; + const spawn_area = (Rect{ .pos = .zero, .size = world_size }).grow(-margin); + const pos = Vec2.initRandomRect(self.rng.random(), spawn_area); + + try self.pickups.append(self.gpa, Pickup{ + .pos = pos, + .kind = .money + }); +} + +pub fn tick(self: *CombatScreen, frame: *Engine.Frame) !void { + const dt = frame.deltaTime(); + + 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 = @as(u64, 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; + } + } + } + + if (self.wave_timer >= self.next_pickup_spawn_at) { + const next_pickup_spawn_at_s = pickup_spawn_duration_s.random(self.rng.random()); + const next_pickup_spawn_at: Nanoseconds = @intFromFloat(next_pickup_spawn_at_s * std.time.ns_per_s); + self.next_pickup_spawn_at = self.wave_timer + next_pickup_spawn_at; + try self.spawnPickup(); + } + + frame.graphics.canvas_size = world_size; + + frame.drawRectangle(.{ + .rect = .init(0, 0, world_size.x, world_size.y), + .color = rgb(20, 20, 20) + }); + + var dir = Vec2.init(0, 0); + if (frame.isKeyDown(.W)) { + dir.y -= 1; + } + if (frame.isKeyDown(.S)) { + dir.y += 1; + } + if (frame.isKeyDown(.A)) { + dir.x -= 1; + } + if (frame.isKeyDown(.D)) { + dir.x += 1; + } + dir = dir.normalized(); + + const acceleration = 1500; + + self.player.kinetic.acc = dir.multiplyScalar(acceleration); + self.player.kinetic.update(dt, .{ + .friction = 0.99988, + .max_speed = 400 + }); + + if (frame.input.mouse_position) |mouse| { + const cursor_size = Vec2.init(10, 10); + frame.drawRectangle(.{ + .rect = .{ + .pos = mouse.sub(cursor_size.divideScalar(2)), + .size = cursor_size, + }, + .color = rgba(255, 200, 200, 0.5) + }); + + const bullet_dir = mouse.sub(self.player.kinetic.pos).normalized(); + + var cooldown_complete = true; + if (self.player.last_shot_at) |last_shot_at| { + const cooldown_ns = std.time.ns_per_ms * 500; + cooldown_complete = frame.time_ns > last_shot_at + cooldown_ns; + } + + if (frame.isMouseDown(.left) and cooldown_complete) { + self.player.last_shot_at = frame.time_ns; + try self.bullets.append(self.gpa, .{ + .kinetic = .{ + .pos = self.player.kinetic.pos, + }, + .dir = bullet_dir, + .speed = 50 + }); + } + } + + frame.drawRectangle(.{ + .rect = self.player.getRect(), + .color = rgb(255, 255, 255) + }); + + for (self.bullets.items) |*bullet| { + bullet.kinetic.vel = bullet.dir.multiplyScalar(bullet.speed); + bullet.kinetic.update(dt, .{}); + + const bullet_rect = getCenteredRect(bullet.kinetic.pos, bullet.size); + + for (self.enemies.items) |*enemy| { + const enemy_rect = getCenteredRect(enemy.kinetic.pos, enemy.size); + if (enemy_rect.hasOverlap(bullet_rect)) { + enemy.dead = true; + bullet.dead = true; + } + } + + frame.drawRectangle(.{ + .rect = bullet_rect, + .color = rgb(200, 20, 255) + }); + } + + for (self.enemies.items) |*enemy| { + const dir_to_player = self.player.kinetic.pos.sub(enemy.kinetic.pos).normalized(); + enemy.kinetic.vel = dir_to_player.multiplyScalar(50); + enemy.kinetic.update(dt, .{}); + + const enemy_rect = getCenteredRect(enemy.kinetic.pos, enemy.size); + + if (enemy_rect.hasOverlap(self.player.getRect())) { + var is_invincible = false; + if (self.player.invincible_until) |invincible_until| { + is_invincible = frame.time_ns < invincible_until; + } + + if (self.player.health > 0 and !is_invincible) { + self.player.health -= 1; + + var invincible_until = frame.time_ns; + invincible_until += @as(Nanoseconds, @intFromFloat(invincibility_duration_s * std.time.ns_per_s)); + self.player.invincible_until = invincible_until; + } + } + + frame.drawRectangle(.{ + .rect = enemy_rect, + .color = rgb(20, 200, 20) + }); + } + + { + var index: usize = 0; + while (index < self.pickups.items.len) { + var destroy: bool = false; + const pickup = self.pickups.items[index]; + + const pickup_rect = Rect.initCentered(pickup.pos.x, pickup.pos.y, 10, 10); + + if (pickup_rect.hasOverlap(self.player.getRect())) { + switch (pickup.kind) { + .money => { + self.player.money += 1; + } + } + destroy = true; + } + + frame.drawRectangle(.{ + .rect = pickup_rect, + .color = rgb(20, 20, 200) + }); + + if (destroy) { + _ = self.pickups.swapRemove(index); + } else { + index += 1; + } + } + } + + { + var i: usize = 0; + while (i < self.bullets.items.len) { + if (self.bullets.items[i].dead) { + _ = self.bullets.swapRemove(i); + } else { + i += 1; + } + } + } + + { + var i: usize = 0; + while (i < self.enemies.items.len) { + if (self.enemies.items[i].dead) { + _ = self.enemies.swapRemove(i); + } else { + i += 1; + } + } + } + + 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) + }); + + frame.drawTextFormat(.init(10, 30), text_opts, "{d}", .{ self.player.money }); + frame.drawTextFormat(.init(10, 50), text_opts, "{d}/{d}", .{ self.player.health, self.player.max_health }); +} + +fn getCenteredRect(pos: Vec2, size: f32) Rect { + return .{ + .pos = pos.sub(Vec2.init(size, size).multiplyScalar(0.5)), + .size = .init(size, size), + }; +} diff --git a/src/game.zig b/src/game.zig index 5e02801..ac4d182 100644 --- a/src/game.zig +++ b/src/game.zig @@ -25,450 +25,47 @@ const Sprite = Engine.Graphics.Sprite; const RaycastTileIterator = @import("./raycast_tile_iterator.zig"); +const CombatScreen = @import("./combat_screen.zig"); + const Game = @This(); const world_size = Vec2.init(20 * 16, 15 * 16); const RNGState = std.Random.DefaultPrng; -const Kinetic = struct { - pos: Vec2 = .zero, - vel: Vec2 = .zero, - acc: Vec2 = .zero, - - const UpdateOptions = struct { - max_speed: ?f32 = null, - friction: ?f32 = null, - }; - - pub fn update(self: *Kinetic, dt: f32, opts: UpdateOptions) void { - self.vel = self.vel.add(self.acc.multiplyScalar(dt)); - if (opts.max_speed) |max_speed| { - self.vel = self.vel.limitLength(max_speed); - } - if (opts.friction) |friction| { - const friction_force = std.math.pow(f32, 1 - friction, dt); - self.vel = self.vel.multiplyScalar(friction_force); - } - self.pos = self.pos.add(self.vel.multiplyScalar(dt)); - } -}; - -const Player = struct { - kinetic: Kinetic = .{}, - money: u32 = 0, - health: u32 = 0, - max_health: u32 = 0, - last_shot_at: ?Nanoseconds = null, - invincible_until: ?Nanoseconds = null, - - pub fn getRect(self: Player) Rect { - return getCenteredRect(self.kinetic.pos, 16); - } -}; - -const Bullet = struct { - kinetic: Kinetic, - size: f32 = 5, - dir: Vec2, - speed: f32, - - dead: bool = false -}; - -const Enemy = struct { - kinetic: Kinetic, - speed: f32, - size: f32, - - 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 Pickup = struct { - const Kind = enum { - money - }; - - pos: Vec2, - kind: Kind, -}; - -const invincibility_duration_s = 0.5; -const pickup_spawn_duration_s = Range.init(1, 5); -const wave_infos = [_]Wave.Info{ - .{ - .enemies = 10, - .duration_s = 10, - .starts_at_s = 0 - } -}; - arena: std.heap.ArenaAllocator, gpa: Allocator, -rng: RNGState, assets: *Assets, -player: Player = .{}, -bullets: std.ArrayList(Bullet) = .empty, -enemies: std.ArrayList(Enemy) = .empty, - -pickups: std.ArrayList(Pickup) = .empty, -next_pickup_spawn_at: Nanoseconds, - -wave_timer: Nanoseconds = 0, - -waves: std.ArrayList(Wave) = .empty, -spawned_waves: std.ArrayList(usize) = .empty, +combat_screen: CombatScreen, pub fn init(gpa: Allocator, seed: u64, assets: *Assets) !Game { var arena = std.heap.ArenaAllocator.init(gpa); errdefer arena.deinit(); - var rng = RNGState.init(seed); - - const next_pickup_spawn_at_s = pickup_spawn_duration_s.random(rng.random()); - const next_pickup_spawn_at: Nanoseconds = @intFromFloat(next_pickup_spawn_at_s * std.time.ns_per_s); - return Game{ .arena = arena, .gpa = gpa, .assets = assets, - .next_pickup_spawn_at = next_pickup_spawn_at, - .player = .{ - .kinetic = .{ - .pos = .init(50, 50), - }, - .health = 3, - .max_health = 3 - }, - .rng = rng + + .combat_screen = .init(gpa, seed, assets), }; } 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.pickups.deinit(self.gpa); - self.spawned_waves.deinit(self.gpa); + self.combat_screen.deinit(); } -fn getCenteredRect(pos: Vec2, size: f32) Rect { - return .{ - .pos = pos.sub(Vec2.init(size, size).multiplyScalar(0.5)), - .size = .init(size, size), - }; -} - -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 - }); -} - -fn spawnPickup(self: *Game) !void { - const margin = 10; - const spawn_area = (Rect{ .pos = .zero, .size = world_size }).grow(-margin); - const pos = Vec2.initRandomRect(self.rng.random(), spawn_area); - - try self.pickups.append(self.gpa, Pickup{ - .pos = pos, - .kind = .money - }); -} - pub fn tick(self: *Game, frame: *Engine.Frame) !void { - const dt = frame.deltaTime(); - - 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 = @as(u64, wave_info.duration_s) * std.time.ns_per_s, - .enemies_spawned = 0, - .total_enemies = wave_info.enemies - }); + if (frame.isKeyPressed(.ESCAPE)) { + sapp.requestQuit(); } - { - 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; - } - } - } - - if (self.wave_timer >= self.next_pickup_spawn_at) { - const next_pickup_spawn_at_s = pickup_spawn_duration_s.random(self.rng.random()); - const next_pickup_spawn_at: Nanoseconds = @intFromFloat(next_pickup_spawn_at_s * std.time.ns_per_s); - self.next_pickup_spawn_at = self.wave_timer + next_pickup_spawn_at; - try self.spawnPickup(); - } - - frame.graphics.canvas_size = world_size; - if (frame.isKeyPressed(.F3)) { frame.show_debug = !frame.show_debug; } - if (frame.isKeyPressed(.ESCAPE)) { - sapp.requestQuit(); - } - - frame.drawRectangle(.{ - .rect = .init(0, 0, world_size.x, world_size.y), - .color = rgb(20, 20, 20) - }); - - var dir = Vec2.init(0, 0); - if (frame.isKeyDown(.W)) { - dir.y -= 1; - } - if (frame.isKeyDown(.S)) { - dir.y += 1; - } - if (frame.isKeyDown(.A)) { - dir.x -= 1; - } - if (frame.isKeyDown(.D)) { - dir.x += 1; - } - dir = dir.normalized(); - - const acceleration = 1500; - - self.player.kinetic.acc = dir.multiplyScalar(acceleration); - self.player.kinetic.update(dt, .{ - .friction = 0.99988, - .max_speed = 400 - }); - - if (frame.input.mouse_position) |mouse| { - const cursor_size = Vec2.init(10, 10); - frame.drawRectangle(.{ - .rect = .{ - .pos = mouse.sub(cursor_size.divideScalar(2)), - .size = cursor_size, - }, - .color = rgba(255, 200, 200, 0.5) - }); - - const bullet_dir = mouse.sub(self.player.kinetic.pos).normalized(); - - var cooldown_complete = true; - if (self.player.last_shot_at) |last_shot_at| { - const cooldown_ns = std.time.ns_per_ms * 500; - cooldown_complete = frame.time_ns > last_shot_at + cooldown_ns; - } - - if (frame.isMouseDown(.left) and cooldown_complete) { - self.player.last_shot_at = frame.time_ns; - try self.bullets.append(self.gpa, .{ - .kinetic = .{ - .pos = self.player.kinetic.pos, - }, - .dir = bullet_dir, - .speed = 50 - }); - } - } - - frame.drawRectangle(.{ - .rect = self.player.getRect(), - .color = rgb(255, 255, 255) - }); - - for (self.bullets.items) |*bullet| { - bullet.kinetic.vel = bullet.dir.multiplyScalar(bullet.speed); - bullet.kinetic.update(dt, .{}); - - const bullet_rect = getCenteredRect(bullet.kinetic.pos, bullet.size); - - for (self.enemies.items) |*enemy| { - const enemy_rect = getCenteredRect(enemy.kinetic.pos, enemy.size); - if (enemy_rect.hasOverlap(bullet_rect)) { - enemy.dead = true; - bullet.dead = true; - } - } - - frame.drawRectangle(.{ - .rect = bullet_rect, - .color = rgb(200, 20, 255) - }); - } - - for (self.enemies.items) |*enemy| { - const dir_to_player = self.player.kinetic.pos.sub(enemy.kinetic.pos).normalized(); - enemy.kinetic.vel = dir_to_player.multiplyScalar(50); - enemy.kinetic.update(dt, .{}); - - const enemy_rect = getCenteredRect(enemy.kinetic.pos, enemy.size); - - if (enemy_rect.hasOverlap(self.player.getRect())) { - var is_invincible = false; - if (self.player.invincible_until) |invincible_until| { - is_invincible = frame.time_ns < invincible_until; - } - - if (self.player.health > 0 and !is_invincible) { - self.player.health -= 1; - - var invincible_until = frame.time_ns; - invincible_until += @as(Nanoseconds, @intFromFloat(invincibility_duration_s * std.time.ns_per_s)); - self.player.invincible_until = invincible_until; - } - } - - frame.drawRectangle(.{ - .rect = enemy_rect, - .color = rgb(20, 200, 20) - }); - } - - { - var index: usize = 0; - while (index < self.pickups.items.len) { - var destroy: bool = false; - const pickup = self.pickups.items[index]; - - const pickup_rect = Rect.initCentered(pickup.pos.x, pickup.pos.y, 10, 10); - - if (pickup_rect.hasOverlap(self.player.getRect())) { - switch (pickup.kind) { - .money => { - self.player.money += 1; - } - } - destroy = true; - } - - frame.drawRectangle(.{ - .rect = pickup_rect, - .color = rgb(20, 20, 200) - }); - - if (destroy) { - _ = self.pickups.swapRemove(index); - } else { - index += 1; - } - } - } - - { - var i: usize = 0; - while (i < self.bullets.items.len) { - if (self.bullets.items[i].dead) { - _ = self.bullets.swapRemove(i); - } else { - i += 1; - } - } - } - - { - var i: usize = 0; - while (i < self.enemies.items.len) { - if (self.enemies.items[i].dead) { - _ = self.enemies.swapRemove(i); - } else { - i += 1; - } - } - } - - 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) - }); - - frame.drawTextFormat(.init(10, 30), text_opts, "{d}", .{ self.player.money }); - frame.drawTextFormat(.init(10, 50), text_opts, "{d}/{d}", .{ self.player.health, self.player.max_health }); + try self.combat_screen.tick(frame); } pub fn debug(self: *Game) !void { @@ -482,13 +79,15 @@ pub fn debug(self: *Game) !void { defer imgui.endWindow(); if (imgui.button("Spawn enemy")) { - try self.spawnEnemy(); + try self.combat_screen.spawnEnemy(); } - const time_left_til_pickup = self.next_pickup_spawn_at - self.wave_timer; + const screen = &self.combat_screen; - imgui.textFmt("Waves: {}\n", .{self.waves.items.len}); - imgui.textFmt("Bullets: {}\n", .{self.bullets.items.len}); - imgui.textFmt("Enemies: {}\n", .{self.enemies.items.len}); + const time_left_til_pickup = screen.next_pickup_spawn_at - screen.wave_timer; + + imgui.textFmt("Waves: {}\n", .{screen.waves.items.len}); + imgui.textFmt("Bullets: {}\n", .{screen.bullets.items.len}); + imgui.textFmt("Enemies: {}\n", .{screen.enemies.items.len}); imgui.textFmt("Time until next pickup: {d:.2}s\n", .{@as(f32, @floatFromInt(time_left_til_pickup)) / std.time.ns_per_s}); }