From ba264d7bc5f052bce286325b6f51308097c1343b Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Sat, 31 Jan 2026 18:19:15 +0200 Subject: [PATCH] add laser weapon --- src/combat_screen.zig | 86 +++++++++++++++++++-- src/engine/graphics.zig | 21 +++-- src/engine/math.zig | 165 +++++++++++++++++++++++++++++++++++++++- src/shop_screen.zig | 4 +- 4 files changed, 257 insertions(+), 19 deletions(-) diff --git a/src/combat_screen.zig b/src/combat_screen.zig index d2fcffa..c725907 100644 --- a/src/combat_screen.zig +++ b/src/combat_screen.zig @@ -7,6 +7,7 @@ const State = @import("./state.zig"); const Engine = @import("./engine/root.zig"); const Nanoseconds = Engine.Nanoseconds; const Vec2 = Engine.Vec2; +const Line = Engine.Math.Line; const Rect = Engine.Math.Rect; const Range = Engine.Math.Range; const rgb = Engine.Math.rgb; @@ -39,12 +40,19 @@ const Kinetic = struct { } }; +const Gun = enum { + pistol, + bomb, + laser, +}; + const Player = struct { kinetic: Kinetic = .{}, health: u32 = 0, max_health: u32 = 0, last_shot_at: ?Nanoseconds = null, invincible_until: ?Nanoseconds = null, + gun: ?Gun = null, pub fn getRect(self: Player) Rect { return getCenteredRect(self.kinetic.pos, 16); @@ -60,6 +68,14 @@ const Bullet = struct { dead: bool = false }; +const Laser = struct { + origin: Vec2, + dir: Vec2, + size: f32, + created_at: Nanoseconds, + duration: Nanoseconds +}; + const Enemy = struct { kinetic: Kinetic, speed: f32, @@ -105,9 +121,11 @@ gpa: Allocator, assets: *Assets, rng: RNGState, +enemies: std.ArrayList(Enemy) = .empty, + player: Player = .{}, bullets: std.ArrayList(Bullet) = .empty, -enemies: std.ArrayList(Enemy) = .empty, +lasers: std.ArrayList(Laser) = .empty, pickups: std.ArrayList(Pickup) = .empty, next_pickup_spawn_at: Nanoseconds, @@ -131,6 +149,7 @@ pub fn init(gpa: Allocator, seed: u64, assets: *Assets, state: State) CombatScre .kinetic = .{ .pos = world_size.divideScalar(2) }, + .gun = .pistol, .health = state.max_health, .max_health = state.max_health, }, @@ -144,6 +163,7 @@ pub fn deinit(self: *CombatScreen) void { self.waves.deinit(self.gpa); self.pickups.deinit(self.gpa); self.spawned_waves.deinit(self.gpa); + self.lasers.deinit(self.gpa); } pub fn spawnEnemy(self: *CombatScreen) !void { @@ -291,6 +311,14 @@ pub fn tick(self: *CombatScreen, state: *State, frame: *Engine.Frame) !TickResul } dir = dir.normalized(); + if (frame.isKeyPressed(._1)) { + self.player.gun = .pistol; + } else if (frame.isKeyPressed(._2)) { + self.player.gun = .bomb; + } else if (frame.isKeyPressed(._3)) { + self.player.gun = .laser; + } + const acceleration = 1500; self.player.kinetic.acc = dir.multiplyScalar(acceleration); @@ -319,13 +347,31 @@ pub fn tick(self: *CombatScreen, state: *State, frame: *Engine.Frame) !TickResul 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 - }); + + if (self.player.gun) |gun| { + switch (gun) { + .pistol => { + try self.bullets.append(self.gpa, .{ + .kinetic = .{ + .pos = self.player.kinetic.pos, + }, + .dir = bullet_dir, + .speed = 50 + }); + }, + .bomb => { + }, + .laser => { + try self.lasers.append(self.gpa, .{ + .origin = self.player.kinetic.pos, + .dir = bullet_dir, + .size = 10, + .created_at = frame.time_ns, + .duration = std.time.ns_per_s + }); + } + } + } } } @@ -354,6 +400,29 @@ pub fn tick(self: *CombatScreen, state: *State, frame: *Engine.Frame) !TickResul }); } + for (self.lasers.items) |*laser| { + const laser_length = 1000; + const laser_line = Line{ + .p0 = laser.origin, + .p1 = laser.origin.add(laser.dir.multiplyScalar(laser_length)) + }; + const laser_quad = laser_line.getQuad(laser.size); + + for (self.enemies.items) |*enemy| { + const enemy_rect = getCenteredRect(enemy.kinetic.pos, enemy.size); + if (laser_quad.isRectOverlap(enemy_rect)) { + enemy.dead = true; + } + } + + frame.drawLine( + laser_line.p0, + laser_line.p1, + rgb(200, 20, 255), + laser.size + ); + } + 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); @@ -444,6 +513,7 @@ pub fn tick(self: *CombatScreen, state: *State, frame: *Engine.Frame) !TickResul frame.drawTextFormat(.init(10, 30), text_opts, "{d}", .{ state.money }); frame.drawTextFormat(.init(10, 50), text_opts, "{d}/{d}", .{ self.player.health, self.player.max_health }); + frame.drawTextFormat(.init(10, 70), text_opts, "{?}", .{ self.player.gun }); result.player_died = (self.player.health == 0); if (self.enemies.items.len == 0 and self.waves.items.len == 0 and self.spawned_waves.items.len == wave_infos.len) { diff --git a/src/engine/graphics.zig b/src/engine/graphics.zig index 59de1ba..05adc99 100644 --- a/src/engine/graphics.zig +++ b/src/engine/graphics.zig @@ -7,6 +7,7 @@ const simgui = sokol.imgui; const sgl = sokol.gl; const Math = @import("./math.zig"); +const Line = Math.Line; const Vec2 = Math.Vec2; const Vec4 = Math.Vec4; const rgb = Math.rgb; @@ -315,14 +316,22 @@ fn drawRectangle(opts: Command.DrawRectangle) void { } fn drawLine(from: Vec2, to: Vec2, color: Vec4, width: f32) void { - const step = to.sub(from).normalized().multiplyScalar(width / 2); + const line = Line{ + .p0 = from, + .p1 = to, + }; - const top_left = from.add(step.rotateLeft90()); - const bottom_left = from.add(step.rotateRight90()); - const top_right = to.add(step.rotateLeft90()); - const bottom_right = to.add(step.rotateRight90()); + const quad = line.getQuad(width); - drawQuadNoUVs(.{ top_right, top_left, bottom_left, bottom_right }, color); + drawQuadNoUVs( + .{ + quad.top_right, + quad.top_left, + quad.bottom_left, + quad.bottom_right + }, + color + ); } pub fn addFont(name: [*c]const u8, data: []const u8) !Font.Id { diff --git a/src/engine/math.zig b/src/engine/math.zig index 412c75d..9ac8636 100644 --- a/src/engine/math.zig +++ b/src/engine/math.zig @@ -397,6 +397,33 @@ pub const Rect = struct { ); } + pub fn getTopEdge(self: Rect) Line { + return Line{ + .p0 = .init(self.left(), self.top()), + .p1 = .init(self.right(), self.top()), + }; + } + pub fn getBottomEdge(self: Rect) Line { + return Line{ + .p0 = .init(self.left(), self.bottom()), + .p1 = .init(self.right(), self.bottom()), + }; + } + + pub fn getLeftEdge(self: Rect) Line { + return Line{ + .p0 = .init(self.left(), self.top()), + .p1 = .init(self.left(), self.bottom()), + }; + } + + pub fn getRightEdge(self: Rect) Line { + return Line{ + .p0 = .init(self.right(), self.top()), + .p1 = .init(self.right(), self.bottom()), + }; + } + pub fn left(self: Rect) f32 { return self.pos.x; } @@ -431,12 +458,20 @@ pub const Rect = struct { }; } - pub fn isInside(self: Rect, pos: Vec2) bool { + pub fn isPointInside(self: Rect, pos: Vec2) bool { const x_overlap = self.pos.x <= pos.x and pos.x < self.pos.x + self.size.x; const y_overlap = self.pos.y <= pos.y and pos.y < self.pos.y + self.size.y; return x_overlap and y_overlap; } + pub fn checkEdgeLineOverlap(self: Rect, line: Line) bool { + const left_overlap = line.hasOverlap(self.getLeftEdge()); + const right_overlap = line.hasOverlap(self.getRightEdge()); + const top_overlap = line.hasOverlap(self.getTopEdge()); + const bottom_overlap = line.hasOverlap(self.getBottomEdge()); + return left_overlap or right_overlap or top_overlap or bottom_overlap; + } + pub fn hasOverlap(lhs: Rect, rhs: Rect) bool { return (lhs.left() < rhs.right() and lhs.right() > rhs.left()) and (lhs.top() < rhs.bottom() and lhs.bottom() > rhs.top()); @@ -461,9 +496,133 @@ pub const Rect = struct { } }; +pub const Quad = struct { + top_left: Vec2, + bottom_left: Vec2, + top_right: Vec2, + bottom_right: Vec2, + + pub fn isRectOverlap(self: Quad, rect: Rect) bool { + const vertices = [4]Vec2{ + self.top_right, + self.top_left, + self.bottom_left, + self.bottom_right, + }; + const polygon = Polygon{ + .vertices = &vertices + }; + return polygon.isRectOverlap(rect); + } +}; + +pub const Polygon = struct { + vertices: []const Vec2, + + pub fn isPointInside(self: Polygon, point: Vec2) bool { + var collision = false; + + const py = point.y; + const px = point.x; + + for (0..self.vertices.len) |i| { + const next_i = @mod(i + 1, self.vertices.len); + // get the PVectors at our current position + // this makes our if statement a little cleaner + const vc = self.vertices[i]; // c for "current" + const vn = self.vertices[next_i]; // n for "next" + + // compare position, flip 'collision' variable + // back and forth + if ( + ((vc.y > py and vn.y < py) or (vc.y < py and vn.y > py)) + and + (px < (vn.x-vc.x)*(py-vc.y) / (vn.y-vc.y)+vc.x) + ) { + collision = !collision; + } + } + + return collision; + } + + pub fn isRectOverlap(self: Polygon, rect: Rect) bool { + // go through each of the vertices, plus the next + // vertex in the list + for (0..self.vertices.len) |i| { + const next_i = @mod(i + 1, self.vertices.len); + + // get the PVectors at our current position + // this makes our if statement a little cleaner + const vc = self.vertices[i]; // c for "current" + const vn = self.vertices[next_i]; // n for "next" + + const polygon_edge = Line{ + .p0 = vc, + .p1 = vn, + }; + + // check against all four sides of the rectangle + if (rect.checkEdgeLineOverlap(polygon_edge)) { + return true; + } + } + + // optional: test if the rectangle is INSIDE the polygon + // note that this iterates all sides of the polygon + // again, so only use this if you need to + if (self.isPointInside(rect.pos)) { + return true; + } + + return false; + } +}; + pub const Line = struct { p0: Vec2, - p1: Vec2 + p1: Vec2, + + pub fn hasOverlap(lhs: Line, rhs: Line) bool { + const x1 = lhs.p0.x; + const y1 = lhs.p0.y; + const x2 = lhs.p1.x; + const y2 = lhs.p1.y; + const x3 = rhs.p0.x; + const y3 = rhs.p0.y; + const x4 = rhs.p1.x; + const y4 = rhs.p1.y; + + // calculate the direction of the lines + const uA = ((x4-x3)*(y1-y3) - (y4-y3)*(x1-x3)) / ((y4-y3)*(x2-x1) - (x4-x3)*(y2-y1)); + const uB = ((x2-x1)*(y1-y3) - (y2-y1)*(x1-x3)) / ((y4-y3)*(x2-x1) - (x4-x3)*(y2-y1)); + + // if uA and uB are between 0-1, lines are colliding + if (uA >= 0 and uA <= 1 and uB >= 0 and uB <= 1) { + return true; + } + + return false; + } + + pub fn getQuad(self: Line, width: f32) Quad { + const to = self.p1; + const from = self.p0; + + const step = to.sub(from).normalized().multiplyScalar(width / 2); + + const top_left = from.add(step.rotateLeft90()); + const bottom_left = from.add(step.rotateRight90()); + const top_right = to.add(step.rotateLeft90()); + const bottom_right = to.add(step.rotateRight90()); + + return Quad{ + .top_left = top_left, + .bottom_left = bottom_left, + .top_right = top_right, + .bottom_right = bottom_right + }; + } }; pub fn isInsideRect(rect_pos: Vec2, rect_size: Vec2, pos: Vec2) bool { @@ -471,7 +630,7 @@ pub fn isInsideRect(rect_pos: Vec2, rect_size: Vec2, pos: Vec2) bool { .pos = rect_pos, .size = rect_size }; - return rect.isInside(pos); + return rect.isPointInside(pos); } pub fn rgba(r: u8, g: u8, b: u8, a: f32) Vec4 { diff --git a/src/shop_screen.zig b/src/shop_screen.zig index 08aef73..49ce86d 100644 --- a/src/shop_screen.zig +++ b/src/shop_screen.zig @@ -134,7 +134,7 @@ pub fn tick(self: *ShopScreen, state: *State, frame: *Frame) !TickResult { for (self.nodes.items) |*node| { const node_rect = Rect.initCentered(node.pos.x, node.pos.y, node_size.x, node_size.y); - const is_mouse_inside = mouse != null and node_rect.isInside(mouse.?); + const is_mouse_inside = mouse != null and node_rect.isPointInside(mouse.?); const has_enough_money = state.money >= node.money_cost; if (has_enough_money and is_mouse_inside and frame.isMousePressed(.left)) { @@ -173,7 +173,7 @@ pub fn tick(self: *ShopScreen, state: *State, frame: *Frame) !TickResult { .color = rgb(255, 255, 255) }); - if (frame.input.mouse_position != null and back_rect.isInside(frame.input.mouse_position.?) and frame.isMousePressed(.left)) { + if (frame.input.mouse_position != null and back_rect.isPointInside(frame.input.mouse_position.?) and frame.isMousePressed(.left)) { result.back_to_combat = true; }