diff --git a/src/assets.zig b/src/assets.zig index ba35d98..a6909ca 100644 --- a/src/assets.zig +++ b/src/assets.zig @@ -42,6 +42,7 @@ pistol_mask: Gfx.TextureId, bomb_mask: Gfx.TextureId, laser_mask: Gfx.TextureId, bullet: Gfx.TextureId, +shadow: Gfx.TextureId, dungeon_tilemap: Tilemap, skeleton: Gfx.Sprite, @@ -176,6 +177,16 @@ pub fn init(gpa: std.mem.Allocator) !Assets { } }); + const shadow_image = try STBImage.load(@embedFile("assets/shadow.png")); + defer shadow_image.deinit(); + const shadow_texture = try Gfx.addTexture(&.{ + .{ + .width = shadow_image.width, + .height = shadow_image.height, + .rgba = shadow_image.rgba8_pixels + } + }); + return Assets{ .arena = arena, .font_id = font_id_array, @@ -188,6 +199,7 @@ pub fn init(gpa: std.mem.Allocator) !Assets { .map = map, .tilesets = tilesets, .bullet = bullet_texture, + .shadow = shadow_texture, .dungeon_tilemap = .{ .texture = dungeon_texture, .tile_size = .init(16, 16) diff --git a/src/assets/shadow.png b/src/assets/shadow.png new file mode 100644 index 0000000..c85c4b4 Binary files /dev/null and b/src/assets/shadow.png differ diff --git a/src/combat_screen.zig b/src/combat_screen.zig index 9d30d8c..827fede 100644 --- a/src/combat_screen.zig +++ b/src/combat_screen.zig @@ -58,6 +58,7 @@ const Player = struct { invincible_until: ?Nanoseconds = null, gun: Gun = .pistol, facing_left: bool = false, + started_walk_at: ?Nanoseconds = null, pub fn getRect(self: Player) Rect { const pos = self.kinetic.pos; @@ -95,6 +96,9 @@ const Enemy = struct { size: f32, sprite: Sprite, facing_left: bool = false, + created_at: Nanoseconds, + animation_offset: f32, + animation_kind: AnimationKind, target: ?Vec2 = null, distance_to_target: ?f32 = null, @@ -104,6 +108,12 @@ const Enemy = struct { dead: bool = false, + const AnimationKind = enum { + walk, + float, + slide + }; + const List = GenerationalArrayList(Enemy); const Id = List.Id; }; @@ -197,6 +207,8 @@ wave_timer: Nanoseconds = 0, waves: std.ArrayList(Wave) = .empty, spawned_waves: std.ArrayList(usize) = .empty, +show_colliders: bool = false, + pub fn init(gpa: Allocator, seed: u64, assets: *Assets, state: State) CombatScreen { return CombatScreen{ .gpa = gpa, @@ -230,8 +242,10 @@ pub fn deinit(self: *CombatScreen) void { const EnemyOptions = struct { pos: ?Vec2 = null, + time_ns: Nanoseconds, sprite: Sprite, - size: f32 = 20 + size: f32 = 20, + animation_kind: Enemy.AnimationKind, }; fn pickSpawnLocation(rand: std.Random, margin: f32, size: f32) Vec2 { @@ -278,7 +292,10 @@ pub fn spawnEnemy(self: *CombatScreen, opts: EnemyOptions) !Enemy.Id { .kinetic = .{ .pos = pos }, .speed = 50, .size = opts.size, - .sprite = opts.sprite + .sprite = opts.sprite, + .created_at = opts.time_ns, + .animation_offset = self.rng.random().float(f32), + .animation_kind = opts.animation_kind }); } @@ -376,7 +393,9 @@ pub fn tick(self: *CombatScreen, state: *State, frame: *Engine.Frame) !TickResul switch (wave.kind) { .regular => { _ = try self.spawnEnemy(.{ - .sprite = self.assets.skeleton + .sprite = self.assets.skeleton, + .time_ns = frame.time_ns, + .animation_kind = .walk }); }, .snake => { @@ -388,7 +407,9 @@ pub fn tick(self: *CombatScreen, state: *State, frame: *Engine.Frame) !TickResul const snake_length = rand.intRangeAtMost(u32, wave.min_group_size, wave.max_group_size); const head_id = try self.spawnEnemy(.{ - .sprite = self.assets.snake + .sprite = self.assets.snake, + .time_ns = frame.time_ns, + .animation_kind = .slide }); try enemies.append(arena.allocator(), head_id); @@ -401,8 +422,10 @@ pub fn tick(self: *CombatScreen, state: *State, frame: *Engine.Frame) !TickResul for (0..snake_length) |i| { const tail_pos = head_pos.sub(dir_to_center.multiplyScalar(@floatFromInt(i * gap))); const tail_id = try self.spawnEnemy(.{ + .time_ns = frame.time_ns, .sprite = self.assets.snake, - .pos = tail_pos + .pos = tail_pos, + .animation_kind = .slide }); try enemies.append(arena.allocator(), tail_id); } @@ -429,9 +452,11 @@ pub fn tick(self: *CombatScreen, state: *State, frame: *Engine.Frame) !TickResul const distance = min_cluster_radius + rand.float(f32) * (max_cluster_radius - min_cluster_radius); const enemy_id = try self.spawnEnemy(.{ + .time_ns = frame.time_ns, .pos = center.add(Vec2.initAngle(angle).multiplyScalar(distance)), .sprite = self.assets.fire_spirit, - .size = 10 + .size = 10, + .animation_kind = .float }); try enemies.append(arena.allocator(), enemy_id); } @@ -462,6 +487,38 @@ pub fn tick(self: *CombatScreen, state: *State, frame: *Engine.Frame) !TickResul self.drawMap(frame); + var shadows: std.ArrayList(Vec2) = .empty; + try shadows.append( + frame.arena.allocator(), + self.player.kinetic.pos.add(.{ .x = 0, .y = 4 }) + ); + + { + var enemies_iter = self.enemies.iterator(); + while (enemies_iter.nextItem()) |enemy| { + try shadows.append(frame.arena.allocator(), enemy.kinetic.pos); + } + } + + for (shadows.items) |shadow_caster| { + const shadow_size = Engine.Graphics.getTextureSize(self.assets.shadow); + frame.drawRectangle(.{ + .rect = .{ + .pos = shadow_caster.add(.{ + .x = -shadow_size.x/2, + .y = shadow_size.y/2, + }), + .size = shadow_size, + }, + .color = rgba(255, 255, 255, 0.5), + .sprite = .{ + .texture = self.assets.shadow, + .uv = .unit + } + }); + } + + var dir = Vec2.init(0, 0); if (frame.isKeyDown(.W)) { dir.y -= 1; @@ -479,6 +536,11 @@ pub fn tick(self: *CombatScreen, state: *State, frame: *Engine.Frame) !TickResul if (dir.x != 0) { self.player.facing_left = dir.x < 0; + if (self.player.started_walk_at == null) { + self.player.started_walk_at = frame.time_ns; + } + } else { + self.player.started_walk_at = null; } if (frame.isKeyPressed(._1)) { @@ -496,17 +558,19 @@ pub fn tick(self: *CombatScreen, state: *State, frame: *Engine.Frame) !TickResul .friction = 0.99988, .max_speed = 400 }); + const player_size = self.player.getRect().size; + self.player.kinetic.pos.x = std.math.clamp( + self.player.kinetic.pos.x, + player_size.x/2, + world_size.x - player_size.x/2 + ); + self.player.kinetic.pos.y = std.math.clamp( + self.player.kinetic.pos.y, + player_size.y/2, + world_size.y - player_size.y/2 + ); 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; @@ -543,17 +607,23 @@ pub fn tick(self: *CombatScreen, state: *State, frame: *Engine.Frame) !TickResul try self.lasers.append(self.gpa, .{ .origin = self.player.kinetic.pos, .dir = bullet_dir, - .size = 10, + .size = 3, .created_at = frame.time_ns, - .duration = std.time.ns_per_s + .duration = std.time.ns_per_ms * 10 }); } } } } - { + var walk_animation: f32 = 0; + if (self.player.started_walk_at) |started_walk_at| { + const walk_duration = frame.time_ns - started_walk_at; + const duration_ns = std.time.ns_per_ms * 250; + walk_animation = @as(f32, @floatFromInt(@mod(walk_duration, duration_ns))) / duration_ns; + } + var sprite = Engine.Graphics.Sprite{ .texture = switch (self.player.gun) { .pistol => self.assets.pistol_mask, @@ -569,15 +639,24 @@ pub fn tick(self: *CombatScreen, state: *State, frame: *Engine.Frame) !TickResul } const pos = self.player.kinetic.pos; frame.drawRectangle(.{ - .rect = Rect.initCentered(pos.x, pos.y, sprite_size.x, sprite_size.y), + .rect = Rect.initCentered( + pos.x, + pos.y - @sin(walk_animation * std.math.pi) * 4, + sprite_size.x, + sprite_size.y + ), + .rotation = @sin((walk_animation-0.5)*2 * std.math.pi)*0.1, + .origin = .init(sprite_size.x/2, sprite_size.y), .color = rgb(255, 255, 255), .sprite = sprite }); - frame.drawRectangle(.{ - .rect = self.player.getRect(), - .color = rgba(255, 255, 255, 0.5), - }); + if (self.show_colliders) { + frame.drawRectangle(.{ + .rect = self.player.getRect(), + .color = rgba(255, 255, 255, 0.5), + }); + } } for (self.bullets.items) |*bullet| { @@ -833,16 +912,50 @@ pub fn tick(self: *CombatScreen, state: *State, frame: *Engine.Frame) !TickResul if (enemy.facing_left) { sprite = sprite.flipHorizontal(); } + var enemy_sprite_rect = Rect.initCentered( + pos.x, + pos.y, + sprite_size.x, + sprite_size.y + ); + var rotation: f32 = 0; + + const walk_duration = frame.time_ns - enemy.created_at; + if (enemy.animation_kind == .walk) { + const duration_ns = std.time.ns_per_ms * 250; + var walk_animation = @as(f32, @floatFromInt(@mod(walk_duration, duration_ns))) / duration_ns; + walk_animation = @mod(walk_animation + enemy.animation_offset, 1); + + enemy_sprite_rect.pos.y -= @sin(walk_animation * std.math.pi) * 2; + rotation = @sin((walk_animation-0.5)*2 * std.math.pi)*0.1; + } else if (enemy.animation_kind == .float) { + const duration_ns = std.time.ns_per_s; + var walk_animation = @as(f32, @floatFromInt(@mod(walk_duration, duration_ns))) / duration_ns; + walk_animation = @mod(walk_animation + enemy.animation_offset, 1); + + enemy_sprite_rect.pos.y += @sin((walk_animation-0.5)*2 * std.math.pi) * 2; + } else if (enemy.animation_kind == .slide) { + const duration_ns = std.time.ns_per_s; + var walk_animation = @as(f32, @floatFromInt(@mod(walk_duration, duration_ns))) / duration_ns; + walk_animation = @mod(walk_animation + enemy.animation_offset, 1); + + enemy_sprite_rect.pos.x += @sin((walk_animation-0.5)*2 * std.math.pi) * 2; + } + frame.drawRectangle(.{ - .rect = Rect.initCentered(pos.x, pos.y, sprite_size.x, sprite_size.y), + .rect = enemy_sprite_rect, + .rotation = rotation, + .origin = .init(sprite_size.x/2, sprite_size.y), .color = rgb(255, 255, 255), .sprite = sprite }); - frame.drawRectangle(.{ - .rect = enemy_rect, - .color = rgba(20, 20, 200, 0.2), - }); + if (self.show_colliders) { + frame.drawRectangle(.{ + .rect = enemy_rect, + .color = rgba(20, 20, 200, 0.2), + }); + } } } diff --git a/src/game.zig b/src/game.zig index 0f419c7..3906428 100644 --- a/src/game.zig +++ b/src/game.zig @@ -128,6 +128,10 @@ pub fn debug(self: *Game) !void { try self.restartAndShowCombatScreen(); } + if (imgui.button("Toggle colliders")) { + self.combat_screen.show_colliders = !self.combat_screen.show_colliders; + } + if (self.show_shop) { if (imgui.button("Swap to combat")) { self.show_shop = false;