diff --git a/src/app.zig b/src/app.zig index ac8ca4c..c0e4ffe 100644 --- a/src/app.zig +++ b/src/app.zig @@ -36,6 +36,8 @@ entities_buffer: [max_entities]EntitySlotMap.Slot, entities: EntitySlotMap, eternal_flame_id: EntityId, +upgrades: Upgrades, + font: Gfx.Font.Id, idle_animations: AnimationDirections, walk_animations: AnimationDirections, @@ -160,7 +162,6 @@ const Entity = struct { minion_state: MinionState = .wander, minion_chop_cooldown: Nanoseconds = 0, - minion_chop_damage: u32 = 2, minion_throw_cooldown: Nanoseconds = 0, minion_carried_wood: ?EntityId = null, @@ -178,9 +179,6 @@ const Entity = struct { pos: Vec2 = .init(0, 0), vel: Vec2 = .init(0, 0), acc: Vec2 = .init(0, 0), - max_velocity: f32 = 0, - friction: f32 = 0, - max_walk_speed: f32 = 0, animation: AnimationName = .idle, animation_dir: AnimationDirection = .down, @@ -215,6 +213,28 @@ const Entity = struct { }; }; +const Upgrades = struct { + minion_chop_damage: u32 = 2, + minion_chop_rate: f32 = 1, + minion_throw_cooldown: Nanoseconds = std.time.ns_per_s, + minion_walk: MinionWalkStats = .{}, + + tree_health: u32 = 10, + tree_respawn_duration: Nanoseconds = std.time.ns_per_s, + tree_wood_amount: u32 = 1, + + burn_max_stack_size: u32 = 10, + burn_speed: f32 = 10, + burn_wood_health: f32 = 20, + burn_heat_per_wood: u32 = 1, + + const MinionWalkStats = struct { + friction: f32 = 0.998, + max_velocity: f32 = 50, + max_speed: f32 = 300, + }; +}; + fn initImageDataFromSTB(img: STBImage) Gfx.ImageData { return Gfx.ImageData{ .width = img.width, @@ -272,6 +292,7 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 { .camera_pos = .init(0, 0), .font = font, .eternal_flame_id = undefined, + .upgrades = .{} }; self.spawnMinion(.init(0, 0)); @@ -306,9 +327,6 @@ pub fn spawnMinion(self: *App, pos: Vec2) void { }, .pos = pos, - .friction = 0.998, - .max_velocity = 50, - .max_walk_speed = 300, .animation = .idle, }); @@ -337,7 +355,7 @@ fn spawnTree(self: *App, station_id: EntityId) void { return; } - const health = 10; + const health = self.upgrades.tree_health; if (self.spawnEntity(Entity{ .type = .tree, .tree_health = health, @@ -353,13 +371,15 @@ fn destroyTree(self: *App, tree_id: EntityId) void { const tree = self.entities.get(tree_id) orelse return; assert(tree.type == .tree); - self.spawnWood(tree.pos); + for (0..self.upgrades.tree_wood_amount) |_| { + self.spawnWood(tree.pos); + } self.entities.removeAssumeExists(tree_id); } fn spawnWood(self: *App, pos: Vec2) void { - const health = 20; + const health = self.upgrades.burn_wood_health; _ = self.spawnEntity(Entity{ .type = .wood, @@ -522,6 +542,20 @@ fn drawProgressBar(progress: f32, center: Vec2, size: Vec2, color: Color) void { Gfx.drawLine(from, from.add(.init(size.x * progress, 0)), color, size.y); } +fn countEternalFlameStackSize(self: *App) u32 { + var result: u32 = 0; + + var wood_iter = self.initEntityIteratorByType(.wood); + while (wood_iter.next()) |wood_id| { + const wood = self.entities.getAssumeExists(wood_id); + if (wood.wood_burning) { + result += 1; + } + } + + return result; +} + fn minionLogic(self: *App, plt: Platform.Frame, minion_id: EntityId) !void { const minion = self.entities.getAssumeExists(minion_id); assert(minion.type == .minion); @@ -549,11 +583,16 @@ fn minionLogic(self: *App, plt: Platform.Frame, minion_id: EntityId) !void { if (minion.pos.distance(tree.pos) < goal_distance) { if (minion.minion_chop_cooldown == 0) { - minion.minion_chop_cooldown = std.time.ns_per_ms * 500; - tree.tree_health -= minion.minion_chop_damage; + minion.minion_chop_cooldown = @intFromFloat(std.time.ns_per_s / self.upgrades.minion_chop_rate); + + if (tree.tree_health >= self.upgrades.minion_chop_damage) { + tree.tree_health -= self.upgrades.minion_chop_damage; + } else { + tree.tree_health = 0; + } } - if (tree.tree_health <= 0) { + if (tree.tree_health == 0) { self.unsetEntityTarget(minion_id); self.destroyTree(tree_id); minion.minion_state = .wander; @@ -601,12 +640,13 @@ fn minionLogic(self: *App, plt: Platform.Frame, minion_id: EntityId) !void { if (minion.minion_carried_wood) |wood_id| { const wood = self.entities.getAssumeExists(wood_id); const eternal_flame = self.entities.getAssumeExists(self.eternal_flame_id); - - wood.pos = eternal_flame.pos; - wood.wood_burning = true; - wood.wood_carried_by = null; - minion.minion_carried_wood = null; - minion.minion_throw_cooldown = std.time.ns_per_s; + if (self.countEternalFlameStackSize() < self.upgrades.burn_max_stack_size) { + wood.pos = eternal_flame.pos; + wood.wood_burning = true; + wood.wood_carried_by = null; + minion.minion_carried_wood = null; + minion.minion_throw_cooldown = self.upgrades.minion_throw_cooldown; + } } else { minion.minion_throw_cooldown = @max(minion.minion_throw_cooldown - plt.dt, 0); if (minion.minion_throw_cooldown == 0) { @@ -617,11 +657,12 @@ fn minionLogic(self: *App, plt: Platform.Frame, minion_id: EntityId) !void { } if (walk_goal) |pos| { + const stats = self.upgrades.minion_walk; const diff = minion.pos.x - pos.x; if (diff < -goal_distance) { - minion.acc = .init(minion.max_walk_speed, 0); + minion.acc = .init(stats.max_speed, 0); } else if (diff > goal_distance) { - minion.acc = .init(-minion.max_walk_speed, 0); + minion.acc = .init(-stats.max_speed, 0); } } } @@ -674,7 +715,7 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { if (wood_station.station_owned_tree) |station_owned_tree| { if (!self.entities.exists(station_owned_tree)) { wood_station.station_owned_tree = null; - wood_station.station_spawn_tree_cooldown = std.time.ns_per_s; + wood_station.station_spawn_tree_cooldown = self.upgrades.tree_respawn_duration; } } @@ -710,8 +751,6 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { } { - const burn_speed = 10; - var wood_iter = self.initEntityIteratorByType(.wood); while (wood_iter.next()) |wood_id| { const wood = self.entities.getAssumeExists(wood_id); @@ -720,10 +759,10 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { // Apply burn if (wood.wood_burning) { - wood.wood_health = @max(wood.wood_health - burn_speed * dt, 0); + wood.wood_health = @max(wood.wood_health - self.upgrades.burn_speed * dt, 0); if (wood.wood_health == 0) { - self.heat += 1; + self.heat += self.upgrades.burn_heat_per_wood; self.entities.removeAssumeExists(wood_id); } @@ -881,15 +920,21 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { if (e.flags.has_kinematics) { e.vel = e.vel.add(e.acc.multiplyScalar(dt)); - e.vel = e.vel.limitLength(e.max_velocity); e.pos = e.pos.add(e.vel.multiplyScalar(dt)); - const friction_force = std.math.pow(f32, 1 - e.friction, dt); - e.vel = e.vel.multiplyScalar(friction_force); - e.acc = .init(0, 0); } + if (e.type == .minion) { + const stats = self.upgrades.minion_walk; + e.vel = e.vel.limitLength(stats.max_velocity); + + if (0 <= stats.friction and stats.friction <= 1) { + const friction_force = std.math.pow(f32, 1 - stats.friction, dt); + e.vel = e.vel.multiplyScalar(friction_force); + } + } + if (e.flags.has_animation) { const animations = switch (e.animation) { .idle => self.idle_animations, @@ -958,6 +1003,11 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { const eternal_flame = self.entities.getAssumeExists(id); Gfx.drawRectangle(eternal_flame.pos.sub(.init(10, 50)), .init(30, 50), .rgb(150, 30, 20)); + Gfx.drawText(self.font, .{ + .text = try std.fmt.allocPrint(plt.arena, "Stack: {}", .{self.countEternalFlameStackSize()}), + .pos = eternal_flame.pos.sub(.init(10, 80)), + .height = 24 + }); } } @@ -976,12 +1026,58 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { .text = try std.fmt.allocPrint(plt.arena, "Heat: {}", .{self.heat}), }); - if (ImGUI.beginWindow(.{ .name = "Debug", .size = .init(200, 100) })) { + if (ImGUI.beginWindow(.{ .name = "Debug", .alpha = 0.8, .size = .init(500, 200) })) { defer ImGUI.endWindow(); - if (ImGUI.button("spawn")) { + if (ImGUI.button("spawn minion")) { self.spawnMinion(.init(32, 0)); } + + var heat: u32 = @intCast(self.heat); + if (ImGUI.inputU32("heat", &heat)) { + self.heat = @intCast(heat); + } + + ImGUI.separator(); + _ = ImGUI.text("Minion upgrades", .{}); + _ = ImGUI.inputU32("chop_damage", &self.upgrades.minion_chop_damage); + _ = ImGUI.inputF32Drag(.{ + .label = "chop_rate", + .value = &self.upgrades.minion_chop_rate + }); + _ = ImGUI.inputDuration("throw_cooldown", &self.upgrades.minion_throw_cooldown); + _ = ImGUI.inputF32Drag(.{ + .label = "walk_max_speed", + .value = &self.upgrades.minion_walk.max_speed + }); + _ = ImGUI.inputF32Drag(.{ + .label = "walk_max_vel", + .value = &self.upgrades.minion_walk.max_velocity + }); + _ = ImGUI.inputF32Drag(.{ + .label = "walk_friction", + .value = &self.upgrades.minion_walk.friction, + .ex = .{ .min = 0, .max = 1, .speed = 0.0001 } + }); + + ImGUI.separator(); + _ = ImGUI.text("Tree upgrades", .{}); + _ = ImGUI.inputU32("health", &self.upgrades.tree_health); + _ = ImGUI.inputU32("wood_amount", &self.upgrades.tree_wood_amount); + _ = ImGUI.inputDuration("respawn_duration", &self.upgrades.tree_respawn_duration); + + ImGUI.separator(); + _ = ImGUI.text("Burn upgrades", .{}); + _ = ImGUI.inputF32Drag(.{ + .label = "speed", + .value = &self.upgrades.burn_speed + }); + _ = ImGUI.inputF32(.{ + .label = "wood_health", + .value = &self.upgrades.burn_wood_health + }); + _ = ImGUI.inputU32("heat_per_wood", &self.upgrades.burn_heat_per_wood); + _ = ImGUI.inputU32("max_stack_size", &self.upgrades.burn_max_stack_size); } } diff --git a/src/imgui.zig b/src/imgui.zig index 87fdb02..c7ee3aa 100644 --- a/src/imgui.zig +++ b/src/imgui.zig @@ -15,6 +15,8 @@ const simgui = sokol.imgui; const ig = @import("cimgui"); +const Nanoseconds = @import("./platform.zig").Nanoseconds; + const ImGUI = @This(); const State = struct { @@ -150,6 +152,7 @@ pub const WindowOptions = struct { size: ?Vec2 = null, collapsed: ?bool = null, open: ?*bool = null, + alpha: f32 = 1 }; pub fn beginWindow(opts: WindowOptions) bool { @@ -175,7 +178,7 @@ pub fn beginWindow(opts: WindowOptions) bool { ig.igSetNextWindowCollapsed(collapsed, ig.ImGuiCond_Once); } - ig.igSetNextWindowBgAlpha(1); + ig.igSetNextWindowBgAlpha(opts.alpha); const frame = g_state.frame.?; const namez = frame.dupeSentinel(u8, opts.name, 0) catch blk: { @@ -264,6 +267,125 @@ pub fn slider(opts: SliderOptions) bool { ); } +const InputF32Options = struct { + label: []const u8, + value: *f32, + ex: ?struct { + step: f32, + step_fast: f32, + format: [*c]const u8 = null, + } = null +}; + +pub fn inputF32(opts: InputF32Options) bool { + if (!build_options.has_imgui) { + return false; + } + const self = &g_state; + if (!self.enabled) { + return false; + } + + if (opts.ex) |ex| { + return ig.igInputFloatEx( + formatString("{s}", .{opts.label}), + opts.value, + ex.step, + ex.step_fast, + ex.format, + ig.ImGuiInputTextFlags_None + ); + } else { + return ig.igInputFloat( + formatString("{s}", .{opts.label}), + opts.value, + ); + } +} + +const InputF32DragOptions = struct { + label: []const u8, + value: *f32, + ex: ?struct { + speed: f32 = 1, + min: f32 = 0, + max: f32 = 10 + } = null, +}; + +pub fn inputF32Drag(opts: InputF32DragOptions) bool { + if (!build_options.has_imgui) { + return false; + } + const self = &g_state; + if (!self.enabled) { + return false; + } + + if (opts.ex) |ex| { + return ig.igDragFloatEx( + formatString("{s}", .{opts.label}), + opts.value, + ex.speed, + ex.min, + ex.max, + null, + ig.ImGuiSliderFlags_None + ); + } else { + return ig.igDragFloat( + formatString("{s}", .{opts.label}), + opts.value, + ); + } +} + +pub fn inputDuration(label: []const u8, duration_ns: *Nanoseconds) bool { + const duration_ms: f32 = @floatFromInt(@divTrunc(duration_ns.*, std.time.ns_per_ms)); + var duration_s = duration_ms / std.time.ms_per_s; + if (ImGUI.inputF32(.{ + .label = label, + .value = &duration_s, + .ex = .{ + .step = 0.01, + .step_fast = 0.1, + .format = "%0.3f s" + } + })) { + duration_ns.* = @trunc(duration_s * std.time.ns_per_s); + return true; + } + + return false; +} + +pub fn inputI32(label: []const u8, value: *i32) bool { + if (!build_options.has_imgui) { + return false; + } + const self = &g_state; + if (!self.enabled) { + return false; + } + + return ig.igInputInt( + formatString("{s}", .{label}), + value, + ); +} + +pub fn inputU32(label: []const u8, value: *u32) bool { + var value_i32: i32 = @intCast(value.*); + if (inputI32(label, &value_i32)) { + if (value_i32 >= 0) { + value.* = @intCast(value_i32); + return true; + } + } + + return false; +} + pub fn checkbox(label: []const u8, value: *bool) bool { if (!build_options.has_imgui) { return false; diff --git a/src/platform.zig b/src/platform.zig index 934881a..44ff152 100644 --- a/src/platform.zig +++ b/src/platform.zig @@ -164,68 +164,101 @@ fn PlatformType(App: type) type { // TODO: I don't like the names of these 'event' related functions. // All of them "append an event", but they do different things. - fn applyEventToState(self: *Self, e: Input.Event) void { + fn applyEventToState(self: *Self, e: Input.Event) bool { const input = &self.input; switch (e) { .key_pressed => |key| { - assert(!input.isKeyDown(key)); + if (input.isKeyDown(key)) { + return false; + } + input.keyboard.press(key); self.last_key_pressed = key; }, .key_released => |key| { - assert(input.isKeyDown(key)); + if (!input.isKeyDown(key)) { + return false; + } + input.keyboard.release(key); }, .mouse_pressed => |button| { - assert(input.mouse_position != null); - assert(!input.isMouseDown(button)); + if (input.mouse_position == null or input.isMouseDown(button)) { + return false; + } + input.mouse_buttons.press(button); }, .mouse_released => |button| { - assert(input.mouse_position != null); - assert(input.isMouseDown(button)); + if (input.mouse_position == null or !input.isMouseDown(button)) { + return false; + } + input.mouse_buttons.release(button); }, .mouse_enter => |pos| { - assert(input.mouse_position == null); + if (input.mouse_position != null) { + return false; + } + input.mouse_position = pos; }, .mouse_move => |pos| { - assert(input.mouse_position != null); + if (input.mouse_position == null) { + return false; + } + input.mouse_position = pos; }, .mouse_leave => { - assert(input.mouse_position != null); + if (input.mouse_position == null) { + return false; + } + input.mouse_position = null; }, .char => |char| { // A 'key_pressed' event always occurs before a 'char' event. // This allows us to differentiate between keycodes and scan codes. // And how to map key codes to character codes. - assert(self.last_key_pressed != null); + if (self.last_key_pressed == null) { + return false; + } + input.key_code_mapping.put(self.last_key_pressed.?, char); self.last_key_pressed = null; }, .mouse_scroll => |offset| { - assert(input.mouse_position != null); + if (input.mouse_position == null) { + return false; + } + input.mouse_scroll = input.mouse_scroll.add(offset); }, .window_resize => |window_size| { self.input.window_size = window_size; }, .focused => { - assert(!input.focused); + if (input.focused) { + return false; + } + input.focused = true; }, .unfocused => { - assert(input.focused); + if (!input.focused) { + return false; + } + assert(input.mouse_position == null); assert(input.keyboard.down.eql(.empty)); assert(input.mouse_buttons.down.eql(.empty)); input.focused = false; } } + + return true; } fn appendEvent(self: *Self, e: Input.Event) void { @@ -237,8 +270,9 @@ fn PlatformType(App: type) type { return; } - self.events.appendAssumeCapacity(e); - self.applyEventToState(e); + if (self.applyEventToState(e)) { + self.events.appendAssumeCapacity(e); + } } fn pushEvent(self: *Self, e: Input.Event) void { @@ -249,16 +283,6 @@ fn PlatformType(App: type) type { return; } - if (e == .key_pressed and input.isKeyDown(e.key_pressed)) { - return; - } - if (e == .key_released and !input.isKeyDown(e.key_released)) { - return; - } - if (e == .mouse_move and input.mouse_position.?.eql(e.mouse_move)) { - return; - } - if (e == .unfocused) { var key_iter = input.keyboard.down.iterator(); while (key_iter.next()) |key| { @@ -268,9 +292,7 @@ fn PlatformType(App: type) type { while (mouse_buttons_iter.next()) |button| { self.appendEvent(.{ .mouse_released = button }); } - if (input.mouse_position != null) { - self.appendEvent(.{ .mouse_leave = {} }); - } + self.appendEvent(.{ .mouse_leave = {} }); } } else { @@ -287,13 +309,6 @@ 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); }