diff --git a/src/app.zig b/src/app.zig index 2a89bc9..5ea43e1 100644 --- a/src/app.zig +++ b/src/app.zig @@ -15,6 +15,8 @@ const Platform = @import("./platform.zig"); const Gfx = Platform.Gfx; const ImGUI = Platform.ImGUI; const Rect = Platform.Rect; +const Color = Platform.Color; +const Nanoseconds = Platform.Nanoseconds; const SlotMapType = @import("./slot_map.zig").SlotMapType; @@ -28,6 +30,7 @@ prng: Prng, heat: u64 = 0, +entities_buffer: [2048]EntitySlotMap.Slot, entities: EntitySlotMap, font: Gfx.Font.Id, @@ -149,14 +152,21 @@ const Entity = struct { type: Type = .nil, flags: Flags = .{}, - minion_state: MinionState = .chop, - minion_target_station: ?EntityId = null, - work_progress: f32 = 0, - minion_chop_speed: f32 = 1, + target_entity: ?EntityId = null, + targeted_by_entity: ?EntityId = null, + + minion_state: MinionState = .goto_tree_station, + minion_work_progress: f32 = 0, + minion_chop_cooldown: Nanoseconds = 0, + minion_chop_damage: u32 = 2, minion_burn_speed: f32 = 1, + tree_health: u32 = 0, + tree_max_health: u32 = 0, + station_subtype: StationSubtype = .chop, - station_occupant: ?EntityId = null, + station_spawn_tree_cooldown: Nanoseconds = 0, + station_owned_tree: ?EntityId = null, pos: Vec2 = .init(0, 0), vel: Vec2 = .init(0, 0), @@ -168,10 +178,12 @@ const Entity = struct { animation: AnimationName = .idle, animation_dir: AnimationDirection = .down, animation_index: u32 = 0, - animation_timer: Platform.Nanoseconds = 0, + animation_timer: Nanoseconds = 0, const MinionState = enum { - chop, + goto_tree_station, + chop_tree, + pickup_wood, burn }; @@ -179,6 +191,8 @@ const Entity = struct { nil, minion, station, + tree, + wood, }; const StationSubtype = enum { @@ -243,7 +257,8 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 { .idle_animations = try AnimationDirections.init(plt.arena, idle_sheet, ns_per_s / idle_fps, 2), .walk_animations = try AnimationDirections.init(plt.arena, walk_sheet, ns_per_s / walk_fps, 4), .death_animations = try AnimationDirections.init(plt.arena, death_sheet, ns_per_s / death_fps, 3), - .entities = .init(.empty), + .entities_buffer = undefined, + .entities = .init(&self.entities_buffer), .prng = Prng.init(@truncate(@as(u96, @intCast(now.nanoseconds)))), .camera_pos = .init(0, 0), .font = font @@ -261,15 +276,12 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 { } pub fn spawnMinion(self: *App, pos: Vec2) !void { - const minion_id = try self.entities.insert(self.gpa); - const minion = self.entities.getAssumeExists(minion_id); - minion.* = Entity{ + _ = try self.entities.insert(Entity{ .type = .minion, .flags = .{ .has_animation = true, .has_kinematics = true }, - .minion_state = .chop, .pos = pos, .friction = 0.998, @@ -277,18 +289,34 @@ pub fn spawnMinion(self: *App, pos: Vec2) !void { .max_walk_speed = 300, .animation = .idle, - }; + }); } pub fn spawnStation(self: *App, pos: f32, subtype: Entity.StationSubtype) !void { - const id = try self.entities.insert(self.gpa); - const station = self.entities.getAssumeExists(id); - station.* = Entity{ + _ = try self.entities.insert(Entity{ .type = .station, .station_subtype = subtype, .pos = .init(pos, 0), - }; + }); +} + +fn spawnTree(self: *App, station_id: EntityId) !void { + const station = self.entities.get(station_id) orelse return; + assert(station.type == .station); + if (station.station_owned_tree != null) { + return; + } + + const health = 10; + const id = try self.entities.insert(Entity{ + .type = .tree, + .tree_health = health, + .tree_max_health = health, + + .pos = station.pos + }); + station.station_owned_tree = id; } fn getNearestFreeStation(self: *App, from: Vec2, subtype: Entity.StationSubtype) ?EntityId { @@ -304,7 +332,7 @@ fn getNearestFreeStation(self: *App, from: Vec2, subtype: Entity.StationSubtype) if (station.station_subtype != subtype) { continue; } - if (station.station_occupant != null) { + if (station.targeted_by_entity != null) { continue; } @@ -319,29 +347,57 @@ fn getNearestFreeStation(self: *App, from: Vec2, subtype: Entity.StationSubtype) return result; } -fn bindEntityToStation(self: *App, other_id: EntityId, station_id: EntityId) void { - const station = self.entities.get(station_id) orelse return; - assert(station.type == .station); +fn getNearestFreeTree(self: *App, from: Vec2) ?EntityId { + var result: ?EntityId = null; + var result_distance: f32 = 0; - const other = self.entities.get(other_id) orelse return; - assert(other.type != .station); + var station_iter = self.entities.iterator(); + while (station_iter.next()) |id| { + const station = self.entities.getAssumeExists(id); + if (station.type != .tree) { + continue; + } + if (station.targeted_by_entity != null) { + continue; + } - station.station_occupant = other_id; - other.minion_target_station = station_id; -} + const distance = station.pos.distanceSqr(from); -fn unbindEntityFromStation(self: *App, station_id: EntityId) void { - const station = self.entities.get(station_id) orelse return; - if (station.type != .station) { - return; - } - - if (station.station_occupant) |occupant_id| { - station.station_occupant = null; - if (self.entities.get(occupant_id)) |occupant| { - occupant.minion_target_station = null; + if (result == null or result_distance > distance) { + result = id; + result_distance = distance; } } + + return result; +} + +fn setEntityTarget(self: *App, entity_id: EntityId, target_id: EntityId) void { + const entity = self.entities.get(entity_id) orelse return; + const target = self.entities.get(target_id) orelse return; + + self.unsetEntityTarget(entity_id); + entity.target_entity = target_id; + target.targeted_by_entity = entity_id; +} + +fn unsetEntityTarget(self: *App, entity_id: EntityId) void { + const entity = self.entities.get(entity_id) orelse return; + + if (entity.target_entity) |target_id| { + if (self.entities.get(target_id)) |target| { + if (target.targeted_by_entity == entity_id) { + target.targeted_by_entity = null; + } + } + + entity.target_entity = null; + } +} + +fn drawProgressBar(progress: f32, center: Vec2, size: Vec2, color: Color) void { + const from = center.sub(.init(size.x/2, 0)); + Gfx.drawLine(from, from.add(.init(size.x * progress, 0)), color, size.y); } pub fn frame(self: *App, plt: Platform.Frame) !void { @@ -368,21 +424,56 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { Gfx.drawLine(.init(-800, 0), .init(800, 0), .white, 5); var station_iter = self.entities.iterator(); - while (station_iter.next()) |minion_id| { - const station = self.entities.getAssumeExists(minion_id); + while (station_iter.next()) |station_id| { + const station = self.entities.getAssumeExists(station_id); if (station.type != .station) { continue; } - if (station.station_occupant) |station_occupant| { - if (!self.entities.exists(station_occupant)) { - station.station_occupant = null; + if (station.station_owned_tree) |station_owned_tree| { + if (!self.entities.exists(station_owned_tree)) { + station.station_owned_tree = null; + station.station_spawn_tree_cooldown = std.time.ns_per_s; + } + } + + if (station.station_subtype == .chop) { + station.station_spawn_tree_cooldown = @max(station.station_spawn_tree_cooldown - plt.dt, 0); + if (station.station_spawn_tree_cooldown == 0 and station.station_owned_tree == null) { + try self.spawnTree(station_id); } } } const character_size = Vec2.init(96, 96); + { + var entity_iter = self.entities.iterator(); + while (entity_iter.next()) |entity_id| { + const e = self.entities.getAssumeExists(entity_id); + + if (e.target_entity) |other_id| { + if (self.entities.get(other_id)) |other| { + if (other.targeted_by_entity != entity_id) { + e.target_entity = null; + } + } else { + e.target_entity = null; + } + } + + if (e.targeted_by_entity) |other_id| { + if (self.entities.get(other_id)) |other| { + if (other.target_entity != entity_id) { + e.targeted_by_entity = null; + } + } else { + e.targeted_by_entity = null; + } + } + } + } + var minion_iter = self.entities.iterator(); while (minion_iter.next()) |minion_id| { const minion = self.entities.getAssumeExists(minion_id); @@ -390,74 +481,114 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { continue; } - if (minion.minion_target_station) |minion_target_station| { - if (!self.entities.exists(minion_target_station)) { - minion.minion_target_station = null; - } - } - const goal_distance_threshold = 10; var walk_goal: ?Vec2 = null; var station_goal: ?Entity.StationSubtype = null; - var is_at_station = false; - if (minion.minion_target_station) |station_id| { - const station = self.entities.getAssumeExists(station_id); - if (station.pos.distance(minion.pos) < goal_distance_threshold) { - is_at_station = true; - } - } - - if (!is_at_station) { - minion.work_progress = 0; - } - switch (minion.minion_state) { - .chop => { - station_goal = .chop; + .goto_tree_station => { + // TODO: Steal tree is it is nearer to you compared to the current minion that has it reserved. + if (self.getNearestFreeTree(minion.pos)) |tree_id| { + self.setEntityTarget(minion_id, tree_id); + minion.minion_state = .chop_tree; + } else { + station_goal = .chop; - if (is_at_station) { - minion.work_progress += minion.minion_chop_speed * dt; + if (minion.target_entity) |station_id| { + const station = self.entities.getAssumeExists(station_id); + if (station.type != .station) { + self.unsetEntityTarget(minion_id); + } + } + } + }, + .chop_tree => { + if (minion.target_entity) |tree_id| { + if (self.entities.getAssumeExists(tree_id).type != .tree) { + self.unsetEntityTarget(minion_id); + minion.minion_state = .goto_tree_station; + } } - minion.work_progress = std.math.clamp(minion.work_progress, 0, 1); - if (minion.work_progress == 1) { - minion.minion_state = .burn; - minion.work_progress = 0; + var is_at_tree = false; + if (minion.target_entity) |tree_id| { + const tree = self.entities.getAssumeExists(tree_id); + + is_at_tree = minion.pos.distance(tree.pos) < goal_distance_threshold; + if (is_at_tree) { + minion.minion_chop_cooldown = @max(minion.minion_chop_cooldown - plt.dt, 0); + if (minion.minion_chop_cooldown == 0) { + minion.minion_chop_cooldown = std.time.ns_per_ms * 500; + tree.tree_health -= minion.minion_chop_damage; + } + + if (tree.tree_health <= 0) { + minion.minion_state = .burn; + + self.unsetEntityTarget(minion_id); + self.entities.removeAssumeExists(tree_id); + } + } else { + walk_goal = tree.pos; + } } + + if (!is_at_tree) { + minion.minion_work_progress = 0; + } + }, + .pickup_wood => { }, .burn => { station_goal = .burn; - if (is_at_station) { - minion.work_progress += minion.minion_burn_speed * dt; + var is_at_station = false; + if (minion.target_entity) |station_id| { + const station = self.entities.getAssumeExists(station_id); + if (station.pos.distance(minion.pos) < goal_distance_threshold) { + is_at_station = true; + } } - minion.work_progress = std.math.clamp(minion.work_progress, 0, 1); - if (minion.work_progress == 1) { - minion.minion_state = .chop; - minion.work_progress = 0; + if (!is_at_station) { + minion.minion_work_progress = 0; + } + + if (is_at_station) { + minion.minion_work_progress += minion.minion_burn_speed * dt; + } + + minion.minion_work_progress = std.math.clamp(minion.minion_work_progress, 0, 1); + if (minion.minion_work_progress == 1) { + minion.minion_state = .goto_tree_station; + minion.minion_work_progress = 0; self.heat += 1; } }, } if (station_goal) |station_type| { - if (minion.minion_target_station) |station_id| { + if (minion.target_entity) |entity_id| { + if (self.entities.get(entity_id)) |station| { + if (station.type != .station) { + } + } + } + + if (minion.target_entity) |station_id| { const station = self.entities.getAssumeExists(station_id); if (station.station_subtype != station_type) { - self.unbindEntityFromStation(station_id); - minion.minion_target_station = null; + self.unsetEntityTarget(minion_id); } } - if (minion.minion_target_station == null) { + if (minion.target_entity == null) { if (self.getNearestFreeStation(minion.pos, station_type)) |station_id| { - self.bindEntityToStation(minion_id, station_id); + self.setEntityTarget(minion_id, station_id); } } - if (minion.minion_target_station) |station_id| { + if (minion.target_entity) |station_id| { const station = self.entities.getAssumeExists(station_id); walk_goal = station.pos; } @@ -484,6 +615,21 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { } } + { + var tree_iter = self.entities.iterator(); + while (tree_iter.next()) |id| { + const e = self.entities.getAssumeExists(id); + + if (e.type == .tree) { + const tree_size = Vec2.init(40, 100); + Gfx.drawRectangle(e.pos.sub(.init(tree_size.x/2, tree_size.y)), tree_size, .rgb(40, 200, 40)); + + const progress = @as(f32, @floatFromInt(e.tree_health)) / @as(f32, @floatFromInt(e.tree_max_health)); + drawProgressBar(progress, e.pos.add(.init(0, 20)), .init(40, 10), .rgb(0, 100, 20)); + } + } + } + var entity_iter = self.entities.iterator(); while (entity_iter.next()) |id| { const e = self.entities.getAssumeExists(id); @@ -542,12 +688,9 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { ); Gfx.drawSprite(sprite, pos, size, .white); - const is_working = e.minion_state == .chop or e.minion_state == .burn; - if (e.type == .minion and is_working and e.work_progress > 0) { - const progressbar_size = 100; - - const from = e.pos.add(.init(-progressbar_size/2, 20)); - Gfx.drawLine(from, from.add(.init(progressbar_size * e.work_progress, 0)), .rgb(0, 200, 20), 10); + const is_working = e.minion_state == .chop_tree or e.minion_state == .burn; + if (e.type == .minion and is_working and e.minion_work_progress > 0) { + drawProgressBar(e.minion_work_progress, e.pos.add(.init(0, 20)), .init(100, 10), .rgb(0, 200, 20)); } } @@ -557,6 +700,12 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { } else { Gfx.drawRectangle(e.pos.sub(.init(5, 5)), .init(10, 10), .rgb(200, 20, 20)); } + } else if (e.type == .minion) { + Gfx.drawText(self.font, .{ + .pos = e.pos.sub(.init(20, 50)), + .height = 16, + .text = try std.fmt.allocPrint(plt.arena, "{}", .{e.minion_state}) + }); } } @@ -576,6 +725,6 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { } pub fn deinit(self: *App, plt: Platform.Deinit) void { - self.entities.deinit(self.gpa); + _ = self; // autofix _ = plt; // autofix } diff --git a/src/graphics.zig b/src/graphics.zig index 8f1085f..04986c3 100644 --- a/src/graphics.zig +++ b/src/graphics.zig @@ -168,19 +168,19 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void { .default_sampler = .nearest, .textures_buffer = undefined, - .textures = .init(.initBuffer(&self.textures_buffer)), + .textures = .init(&self.textures_buffer), .nil_texture = undefined, .sprites_buffer = undefined, - .sprites = .init(.initBuffer(&self.sprites_buffer)), + .sprites = .init(&self.sprites_buffer), .nil_sprite = undefined, .spritesheets_buffer = undefined, - .spritesheets = .init(.initBuffer(&self.spritesheets_buffer)), + .spritesheets = .init(&self.spritesheets_buffer), .nil_spritesheet = undefined, .fonts_buffer = undefined, - .fonts = .init(.initBuffer(&self.fonts_buffer)), + .fonts = .init(&self.fonts_buffer), .nil_font = undefined, }; @@ -511,7 +511,7 @@ const TextureOptions = struct { pub fn initTexture(opts: TextureOptions) Texture.Id { const self = &g_state; - const id = self.textures.insertBounded() catch { + const id = self.textures.insertUndefined() catch { log.warn("Failed to create texture, limit reached! limit: {}", .{self.textures.slots.capacity}); return self.nil_texture; }; @@ -765,7 +765,7 @@ pub fn getNilSprite() Sprite.Id { pub fn initSprite(opts: SpriteOptions) Sprite.Id { const self = &g_state; - const id = self.sprites.insertBounded() catch { + const id = self.sprites.insertUndefined() catch { log.warn("Failed to create sprite, limit reached! limit: {}", .{self.sprites.slots.capacity}); return self.nil_sprite; }; @@ -1125,7 +1125,7 @@ pub const SpriteSheetOptions = struct { pub fn initSpritesheet(opts: SpriteSheetOptions) Spritesheet.Id { const self = &g_state; - const id = self.spritesheets.insertBounded() catch { + const id = self.spritesheets.insertUndefined() catch { log.warn("Failed to create spritesheet, limit reached! limit: {}", .{self.spritesheets.slots.capacity}); return self.nil_spritesheet; }; @@ -1159,7 +1159,7 @@ pub fn deinitSpritesheet(id: Spritesheet.Id) void { pub fn initFont() Font.Id { const self = &g_state; - const id = self.fonts.insertBounded() catch { + const id = self.fonts.insertUndefined() catch { log.warn("Failed to create font, limit reached! limit: {}", .{self.fonts.slots.capacity}); return self.nil_font; }; diff --git a/src/platform.zig b/src/platform.zig index f93f3b8..934881a 100644 --- a/src/platform.zig +++ b/src/platform.zig @@ -287,6 +287,13 @@ fn PlatformType(App: type) type { } } + if (e == .mouse_leave and input.mouse_position == null) { + return; + } + if (e == .mouse_enter and input.mouse_position != null) { + return; + } + self.appendEvent(e); } diff --git a/src/slot_map.zig b/src/slot_map.zig index 5eb1cd2..0ee00cb 100644 --- a/src/slot_map.zig +++ b/src/slot_map.zig @@ -78,10 +78,7 @@ pub fn SlotMapType(Index: type, Generation: type, Value: type) type { }; pub fn clearRetainingCapacity(self: *Self) void { - var slots = self.slots; - slots.clearRetainingCapacity(); - - self.* = .init(slots); + self.* = .init(self.slots.items); } fn insertHole(self: *Self, index: Index) void { @@ -121,19 +118,6 @@ pub fn SlotMapType(Index: type, Generation: type, Value: type) type { return null; } - pub fn ensureUnusedCapacity(self: *Self, gpa: Allocator, additional_count: usize) Allocator.Error!void { - if (additional_count > self.hole_count) { - const new_capacity, const overflow = @addWithOverflow( - self.slots.capacity, - additional_count - self.hole_count - ); - if (overflow != 0) return error.OutOfMemory; - if (new_capacity >= std.math.maxInt(Index)) return error.OutOfMemory; - - try self.slots.ensureTotalCapacity(gpa, new_capacity); - } - } - pub fn unusedCapacity(self: *Self) usize { const capacity = @min(self.slots.capacity, std.math.maxInt(Index)); return capacity - self.slots.items.len + self.hole_count; @@ -151,18 +135,19 @@ pub fn SlotMapType(Index: type, Generation: type, Value: type) type { }; } - pub fn insert(self: *Self, gpa: Allocator) Allocator.Error!Id { - try self.ensureUnusedCapacity(gpa, 1); - return self.insertAssumeCapacity(); - } - - pub fn insertBounded(self: *Self) Allocator.Error!Id { + pub fn insertUndefined(self: *Self) Allocator.Error!Id { if (self.unusedCapacity() == 0) { return error.OutOfMemory; } return self.insertAssumeCapacity(); } + pub fn insert(self: *Self, value: Value) Allocator.Error!Id { + const id = try self.insertUndefined(); + self.getAssumeExists(id).* = value; + return id; + } + pub fn exists(self: *Self, id: Id) bool { if (id.index >= self.slots.items.len) { return false; @@ -211,15 +196,11 @@ pub fn SlotMapType(Index: type, Generation: type, Value: type) type { }; } - pub fn init(slots: std.ArrayList(Slot)) Self { + pub fn init(slots: []Slot) Self { var self: Self = .empty; - self.slots = slots; + self.slots = .initBuffer(slots); return self; } - - pub fn deinit(self: *Self, gpa: Allocator) void { - self.slots.deinit(gpa); - } }; } @@ -234,13 +215,13 @@ test "insert & remove" { var map: TestMap = .empty; defer map.deinit(gpa); - const id1 = try map.insert(gpa); + const id1 = try map.insertUndefined(gpa); try expect(map.exists(id1)); try expect(map.remove(id1)); try expect(!map.exists(id1)); try expect(!map.remove(id1)); - const id2 = try map.insert(gpa); + const id2 = try map.insertUndefined(gpa); try expect(map.exists(id2)); try expect(!map.exists(id1)); } @@ -253,14 +234,14 @@ test "generation wrap around" { defer map.deinit(gpa); // Grow array list so that at least 1 slot exists - const id1 = try map.insert(gpa); + const id1 = try map.insertUndefined(gpa); map.removeAssumeExists(id1); // Artificially increase generation count map.slots.items[id1.index].generation = std.math.maxInt(@FieldType(TestMap.Id, "generation")); // Check if generation wraps around - const id2 = try map.insert(gpa); + const id2 = try map.insertUndefined(gpa); map.removeAssumeExists(id2); try expectEqual(id1.index, id2.index); try expectEqual(0, map.slots.items[id1.index].generation); @@ -274,9 +255,9 @@ test "iterator" { defer map.deinit(gpa); // Create array which has a hole - const id1 = try map.insert(gpa); - const id2 = try map.insert(gpa); - const id3 = try map.insert(gpa); + const id1 = try map.insertUndefined(gpa); + const id2 = try map.insertUndefined(gpa); + const id3 = try map.insertUndefined(gpa); map.removeAssumeExists(id2); @@ -294,11 +275,11 @@ test "clear retaining capacity" { var map: TestMap = .empty; defer map.deinit(gpa); - const id1 = try map.insert(gpa); + const id1 = try map.insertUndefined(gpa); try expect(map.exists(id1)); map.clearRetainingCapacity(); - const id2 = try map.insert(gpa); + const id2 = try map.insertUndefined(gpa); try expect(map.exists(id2)); try expectEqual(id1, id2);