add laser weapon

This commit is contained in:
Rokas Puzonas 2026-01-31 18:19:15 +02:00
parent 8d5e7da6a6
commit ba264d7bc5
4 changed files with 257 additions and 19 deletions

View File

@ -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,6 +347,10 @@ 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;
if (self.player.gun) |gun| {
switch (gun) {
.pistol => {
try self.bullets.append(self.gpa, .{
.kinetic = .{
.pos = self.player.kinetic.pos,
@ -326,6 +358,20 @@ pub fn tick(self: *CombatScreen, state: *State, frame: *Engine.Frame) !TickResul
.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) {

View File

@ -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 {

View File

@ -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 {

View File

@ -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;
}