From b4173ec95b5e76ae3d01a90dc8ed9cc2fef44fef Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Fri, 17 Jul 2026 02:29:51 +0300 Subject: [PATCH] implement burn multiplier --- src/app.zig | 220 ++++++++++++++++++++++++++++++++++++++------------ src/imgui.zig | 111 ++++++++++++------------- 2 files changed, 223 insertions(+), 108 deletions(-) diff --git a/src/app.zig b/src/app.zig index c0e4ffe..1ef6869 100644 --- a/src/app.zig +++ b/src/app.zig @@ -163,7 +163,8 @@ const Entity = struct { minion_state: MinionState = .wander, minion_chop_cooldown: Nanoseconds = 0, minion_throw_cooldown: Nanoseconds = 0, - minion_carried_wood: ?EntityId = null, + minion_carried_wood_head: ?EntityId = null, + minion_carried_wood_len: u32 = 0, tree_health: u32 = 0, tree_max_health: u32 = 0, @@ -175,6 +176,7 @@ const Entity = struct { wood_burning: bool = false, wood_health: f32 = 0, wood_max_health: f32 = 0, + wood_next_carried: ?EntityId = null, pos: Vec2 = .init(0, 0), vel: Vec2 = .init(0, 0), @@ -214,9 +216,12 @@ const Entity = struct { }; const Upgrades = struct { + minion_count: u32 = 0, minion_chop_damage: u32 = 2, minion_chop_rate: f32 = 1, minion_throw_cooldown: Nanoseconds = std.time.ns_per_s, + minion_throw_strength: u32 = 1, + minion_carry_capacity: u32 = 1, minion_walk: MinionWalkStats = .{}, tree_health: u32 = 10, @@ -227,12 +232,21 @@ const Upgrades = struct { burn_speed: f32 = 10, burn_wood_health: f32 = 20, burn_heat_per_wood: u32 = 1, + burn_levels: [2]BurnLevel = .{ + .{ .threshold = 10, .multiplier = 2 }, + .{ .threshold = 20, .multiplier = 3 }, + }, const MinionWalkStats = struct { friction: f32 = 0.998, max_velocity: f32 = 50, max_speed: f32 = 300, }; + + const BurnLevel = struct { + threshold: u32, + multiplier: u32, + }; }; fn initImageDataFromSTB(img: STBImage) Gfx.ImageData { @@ -295,9 +309,6 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 { .upgrades = .{} }; - self.spawnMinion(.init(0, 0)); - // try self.spawnMinion(.init(32, 0)); - self.spawnWoodStation(-100); self.spawnWoodStation(-150); self.spawnBurnStation(100); @@ -556,6 +567,35 @@ fn countEternalFlameStackSize(self: *App) u32 { return result; } +fn minionPushWood(self: *App, minion_id: EntityId, wood_id: EntityId) void { + const minion = self.entities.get(minion_id) orelse return; + const wood = self.entities.get(wood_id) orelse return; + + wood.wood_carried_by = minion_id; + wood.wood_next_carried = minion.minion_carried_wood_head; + wood.flags.has_kinematics = false; + minion.minion_carried_wood_head = wood_id; + minion.minion_carried_wood_len += 1; +} + +fn minionPopWood(self: *App, minion_id: EntityId) ?EntityId { + const minion = self.entities.get(minion_id) orelse return null; + + if (minion.minion_carried_wood_head) |wood_id| { + minion.minion_carried_wood_len -= 1; + if (self.entities.get(wood_id)) |wood| { + minion.minion_carried_wood_head = wood.wood_next_carried; + wood.wood_carried_by = null; + wood.wood_next_carried = null; + return wood_id; + } else { + minion.minion_carried_wood_head = null; + } + } + + return null; +} + fn minionLogic(self: *App, plt: Platform.Frame, minion_id: EntityId) !void { const minion = self.entities.getAssumeExists(minion_id); assert(minion.type == .minion); @@ -563,7 +603,6 @@ fn minionLogic(self: *App, plt: Platform.Frame, minion_id: EntityId) !void { const goal_distance = 10; var walk_goal: ?Vec2 = null; - self.ensureEntityExists(&minion.minion_carried_wood); minion.minion_chop_cooldown = @max(minion.minion_chop_cooldown - plt.dt, 0); switch (minion.minion_state) { @@ -610,7 +649,7 @@ fn minionLogic(self: *App, plt: Platform.Frame, minion_id: EntityId) !void { if (minion.pos.distance(tree.pos) < goal_distance) { self.unsetEntityTarget(minion_id); - self.minionPickupWood(minion_id, wood_id); + self.minionPushWood(minion_id, wood_id); minion.minion_state = .wander; } else { walk_goal = tree.pos; @@ -620,7 +659,7 @@ fn minionLogic(self: *App, plt: Platform.Frame, minion_id: EntityId) !void { } }, .goto_burn_station => { - if (minion.minion_carried_wood == null) { + if (minion.minion_carried_wood_len == 0) { self.unsetEntityTarget(minion_id); minion.minion_state = .wander; } @@ -637,19 +676,24 @@ fn minionLogic(self: *App, plt: Platform.Frame, minion_id: EntityId) !void { } }, .burn_wood => { - if (minion.minion_carried_wood) |wood_id| { - const wood = self.entities.getAssumeExists(wood_id); - const eternal_flame = self.entities.getAssumeExists(self.eternal_flame_id); - 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; + minion.minion_throw_cooldown = @max(minion.minion_throw_cooldown - plt.dt, 0); + if (self.countEternalFlameStackSize() < self.upgrades.burn_max_stack_size and minion.minion_throw_cooldown == 0) { + + var threw_anything = false; + for (0..self.upgrades.minion_throw_strength) |_| { + if (self.minionPopWood(minion_id)) |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; + threw_anything = true; + } } - } else { - minion.minion_throw_cooldown = @max(minion.minion_throw_cooldown - plt.dt, 0); - if (minion.minion_throw_cooldown == 0) { + + if (threw_anything) { + minion.minion_throw_cooldown = self.upgrades.minion_throw_cooldown; + } else { minion.minion_state = .wander; } } @@ -667,23 +711,37 @@ fn minionLogic(self: *App, plt: Platform.Frame, minion_id: EntityId) !void { } } -fn minionPickupWood(self: *App, minion_id: EntityId, wood_id: EntityId) void { - const minion = self.entities.get(minion_id) orelse return; - assert(minion.type == .minion); - - const wood = self.entities.get(wood_id) orelse return; - assert(wood.type == .wood); - - wood.wood_carried_by = minion_id; - minion.minion_carried_wood = wood_id; -} - fn ensureEntityExists(self: *App, maybe_entity_id: *?EntityId) void { if (maybe_entity_id.* != null and !self.entities.exists(maybe_entity_id.*.?)) { maybe_entity_id.* = null; } } +fn removeMinion(self: *App, minion_id: EntityId) void { + const minion = self.entities.get(minion_id) orelse return; + assert(minion.type == .minion); + + while (self.minionPopWood(minion_id)) |wood_id| { + const wood = self.entities.getAssumeExists(wood_id); + wood.flags.has_kinematics = true; + } + + self.entities.removeAssumeExists(minion_id); +} + +fn getBurnMultiplier(self: *App) u32 { + const stack_size = self.countEternalFlameStackSize(); + + for (0..self.upgrades.burn_levels.len) |i| { + const level = self.upgrades.burn_levels[self.upgrades.burn_levels.len - i - 1]; + if (stack_size >= level.threshold) { + return level.multiplier; + } + } + + return 1; +} + pub fn frame(self: *App, plt: Platform.Frame) !void { const input = plt.input; _ = input; // autofix @@ -707,6 +765,27 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { Gfx.setClearColor(.rgb(40, 40, 40)); Gfx.drawLine(.init(-800, 0), .init(800, 0), .white, 5); + { // Spawn in or delete minions based on self.upgrades.minion_count + var minion_count: u32 = 0; + { + var minion_iter = self.initEntityIteratorByType(.minion); + while (minion_iter.next()) |_| { + minion_count += 1; + } + } + + if (minion_count > self.upgrades.minion_count) { + var minion_iter = self.initEntityIteratorByType(.minion); + for (0..(minion_count-self.upgrades.minion_count)) |_| { + self.removeMinion(minion_iter.next().?); + } + } else if (minion_count < self.upgrades.minion_count) { + for (0..(self.upgrades.minion_count-minion_count)) |_| { + self.spawnMinion(.init(32, 0)); + } + } + } + { // Spawn trees at wood station on a cooldown var wood_station_iter = self.initEntityIteratorByType(.wood_station); while (wood_station_iter.next()) |wood_station_id| { @@ -762,7 +841,7 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { wood.wood_health = @max(wood.wood_health - self.upgrades.burn_speed * dt, 0); if (wood.wood_health == 0) { - self.heat += self.upgrades.burn_heat_per_wood; + self.heat += self.upgrades.burn_heat_per_wood * self.getBurnMultiplier(); self.entities.removeAssumeExists(wood_id); } @@ -770,11 +849,11 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { } // Each wood entity will assign itself to the nearest minion that isn't busy - if (wood.wood_carried_by == null and (wood.targeted_by_entity == null or wood.wood_carried_by == null)) { + if (wood.wood_carried_by == null and wood.targeted_by_entity == null) { var minion_iter = self.initNearestEntityIter(.minion, wood.pos, plt.arena); while (try minion_iter.next()) |minion_id| { const minion = self.entities.getAssumeExists(minion_id); - if (minion.minion_carried_wood != null) { + if (minion.minion_carried_wood_len == self.upgrades.minion_carry_capacity) { continue; } @@ -786,6 +865,10 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { } } } + + if (wood.flags.has_kinematics) { + wood.vel = .init(0, 100); + } } } @@ -793,23 +876,34 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { var tree_iter = self.initEntityIteratorByType(.tree); while (tree_iter.next()) |tree_id| { const tree = self.entities.getAssumeExists(tree_id); - if (tree.targeted_by_entity != null) { - continue; - } - var minion_iter = self.initNearestEntityIter(.minion, tree.pos, plt.arena); while (try minion_iter.next()) |minion_id| { const minion = self.entities.getAssumeExists(minion_id); - if (minion.minion_carried_wood != null) { + if (minion.minion_carried_wood_len == self.upgrades.minion_carry_capacity) { continue; } const state = minion.minion_state; - if (state == .wander or state == .goto_tree_station) { - self.setEntityTarget(minion_id, tree_id); - minion.minion_state = .chop_tree; - break; + if (!(state == .wander or state == .goto_tree_station)) { + continue; } + + if (tree.targeted_by_entity) |targeted_by_minion_id| { + const targeted_by_minion = self.entities.getAssumeExists(targeted_by_minion_id); + if (minion.pos.distanceSqr(tree.pos) > targeted_by_minion.pos.distanceSqr(tree.pos)) { + continue; + } + + // Steal tree if this minion is closer to it. + // This is useful for sitation when a minion far away minion as reserved a tree. + self.unsetEntityTarget(targeted_by_minion_id); + tree.targeted_by_entity = null; + targeted_by_minion.minion_state = .wander; + } + + self.setEntityTarget(minion_id, tree_id); + minion.minion_state = .chop_tree; + break; } } } @@ -821,11 +915,14 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { if (wood_station.targeted_by_entity != null) { continue; } + if (wood_station.station_owned_tree != null) { + continue; + } var minion_iter = self.initNearestEntityIter(.minion, wood_station.pos, plt.arena); while (try minion_iter.next()) |minion_id| { const minion = self.entities.getAssumeExists(minion_id); - if (minion.minion_carried_wood != null) { + if (minion.minion_carried_wood_len == self.upgrades.minion_carry_capacity) { continue; } @@ -850,12 +947,8 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { var minion_iter = self.initNearestEntityIter(.minion, burn_station.pos, plt.arena); while (try minion_iter.next()) |minion_id| { const minion = self.entities.getAssumeExists(minion_id); - if (minion.minion_carried_wood == null) { - continue; - } - const state = minion.minion_state; - if (state == .wander) { + if (state == .wander and minion.minion_carried_wood_len == self.upgrades.minion_carry_capacity) { self.setEntityTarget(minion_id, burn_station_id); minion.minion_state = .goto_burn_station; break; @@ -922,6 +1015,8 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { e.vel = e.vel.add(e.acc.multiplyScalar(dt)); e.pos = e.pos.add(e.vel.multiplyScalar(dt)); + e.pos.y = @min(e.pos.y, 0); + e.acc = .init(0, 0); } @@ -1008,6 +1103,11 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { .pos = eternal_flame.pos.sub(.init(10, 80)), .height = 24 }); + Gfx.drawText(self.font, .{ + .text = try std.fmt.allocPrint(plt.arena, "Mult: {}", .{self.getBurnMultiplier()}), + .pos = eternal_flame.pos.sub(.init(10, 110)), + .height = 24 + }); } } @@ -1029,10 +1129,6 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { if (ImGUI.beginWindow(.{ .name = "Debug", .alpha = 0.8, .size = .init(500, 200) })) { defer ImGUI.endWindow(); - 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); @@ -1040,12 +1136,18 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { ImGUI.separator(); _ = ImGUI.text("Minion upgrades", .{}); + _ = ImGUI.inputU32("count", &self.upgrades.minion_count); + _ = ImGUI.inputU32("carry_capacity", &self.upgrades.minion_carry_capacity); + + _ = ImGUI.inputU32("throw_strength", &self.upgrades.minion_throw_strength); + _ = ImGUI.inputDuration("throw_cooldown", &self.upgrades.minion_throw_cooldown); + _ = 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 @@ -1078,6 +1180,18 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { }); _ = ImGUI.inputU32("heat_per_wood", &self.upgrades.burn_heat_per_wood); _ = ImGUI.inputU32("max_stack_size", &self.upgrades.burn_max_stack_size); + for (1.., &self.upgrades.burn_levels) |i, *level| { + ImGUI.pushID(.{ .int = @intCast(i) }); + defer ImGUI.popID(); + + _ = ImGUI.text("Level {}", .{i}); + ImGUI.sameLine(); + ImGUI.setNextItemWidth(150); + _ = ImGUI.inputU32("threshold", &level.threshold); + ImGUI.sameLine(); + ImGUI.setNextItemWidth(100); + _ = ImGUI.inputU32("multiplier", &level.multiplier); + } } } diff --git a/src/imgui.zig b/src/imgui.zig index c7ee3aa..187ef51 100644 --- a/src/imgui.zig +++ b/src/imgui.zig @@ -155,12 +155,13 @@ pub const WindowOptions = struct { alpha: f32 = 1 }; -pub fn beginWindow(opts: WindowOptions) bool { - if (!build_options.has_imgui) { - return false; - } +inline fn isDisabled() bool { const self = &g_state; - if (!self.enabled) { + return !(build_options.has_imgui and self.enabled); +} + +pub fn beginWindow(opts: WindowOptions) bool { + if (isDisabled()) { return false; } @@ -200,11 +201,7 @@ pub fn beginWindow(opts: WindowOptions) bool { } pub fn endWindow() void { - if (!build_options.has_imgui) { - return; - } - const self = &g_state; - if (!self.enabled) { + if (isDisabled()) { return; } @@ -220,11 +217,7 @@ fn formatString(comptime fmt: []const u8, args: anytype) [:0]const u8 { } pub fn text(comptime fmt: []const u8, args: anytype) void { - if (!build_options.has_imgui) { - return; - } - const self = &g_state; - if (!self.enabled) { + if (isDisabled()) { return; } @@ -232,11 +225,7 @@ pub fn text(comptime fmt: []const u8, args: anytype) void { } pub fn button(label: []const u8) bool { - if (!build_options.has_imgui) { - return false; - } - const self = &g_state; - if (!self.enabled) { + if (isDisabled()) { return false; } @@ -251,11 +240,7 @@ pub const SliderOptions = struct { }; pub fn slider(opts: SliderOptions) bool { - if (!build_options.has_imgui) { - return false; - } - const self = &g_state; - if (!self.enabled) { + if (isDisabled()) { return false; } @@ -278,11 +263,7 @@ const InputF32Options = struct { }; pub fn inputF32(opts: InputF32Options) bool { - if (!build_options.has_imgui) { - return false; - } - const self = &g_state; - if (!self.enabled) { + if (isDisabled()) { return false; } @@ -314,11 +295,7 @@ const InputF32DragOptions = struct { }; pub fn inputF32Drag(opts: InputF32DragOptions) bool { - if (!build_options.has_imgui) { - return false; - } - const self = &g_state; - if (!self.enabled) { + if (isDisabled()) { return false; } @@ -360,11 +337,7 @@ pub fn inputDuration(label: []const u8, duration_ns: *Nanoseconds) bool { } pub fn inputI32(label: []const u8, value: *i32) bool { - if (!build_options.has_imgui) { - return false; - } - const self = &g_state; - if (!self.enabled) { + if (isDisabled()) { return false; } @@ -387,11 +360,7 @@ pub fn inputU32(label: []const u8, value: *u32) bool { } pub fn checkbox(label: []const u8, value: *bool) bool { - if (!build_options.has_imgui) { - return false; - } - const self = &g_state; - if (!self.enabled) { + if (isDisabled()) { return false; } @@ -402,11 +371,7 @@ pub fn checkbox(label: []const u8, value: *bool) bool { } pub fn separator() void { - if (!build_options.has_imgui) { - return; - } - const self = &g_state; - if (!self.enabled) { + if (isDisabled()) { return; } @@ -420,11 +385,7 @@ const ColorEditOptions = struct { }; pub fn colorEdit(opts: ColorEditOptions) bool { - if (!build_options.has_imgui) { - return false; - } - const self = &g_state; - if (!self.enabled) { + if (isDisabled()) { return false; } @@ -459,3 +420,43 @@ fn toImVec2(vec2: Vec2) ig.ImVec2 { .y = vec2.y, }; } + +pub fn sameLine() void { + if (isDisabled()) { + return; + } + + ig.igSameLine(); +} + +pub const ID = union(enum) { + string: []const u8, + int: i32 +}; + +pub fn pushID(id: ID) void { + if (isDisabled()) { + return; + } + + switch (id) { + .string => |str| ig.igPushIDStr(str.ptr, str.ptr + str.len), + .int => |int| ig.igPushIDInt(int) + } +} + +pub fn popID() void { + if (isDisabled()) { + return; + } + + ig.igPopID(); +} + +pub fn setNextItemWidth(width: f32) void { + if (isDisabled()) { + return; + } + + ig.igSetNextItemWidth(width); +}