From e4307fb543785aa9d4e21cf88689417eb4e518b4 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Sun, 19 Jul 2026 10:48:36 +0300 Subject: [PATCH] implement upgrades --- src/app.zig | 384 +++++++++++++++++++++++++++++++++++++---------- src/graphics.zig | 2 +- 2 files changed, 306 insertions(+), 80 deletions(-) diff --git a/src/app.zig b/src/app.zig index a96a25d..2418467 100644 --- a/src/app.zig +++ b/src/app.zig @@ -53,6 +53,10 @@ terrain_tiles: []Tile, show_minion_state: bool = false, +visual_progress: f32 = 0, +show_multiplier: bool = false, +show_heat: bool = false, + const terrain_origin_x: i32 = -80; const terrain_origin_y: i32 = -60; const terrain_width = 160; @@ -251,12 +255,15 @@ const Entity = struct { const Upgrades = struct { minion_count: u32 = 1, - 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_chop_level: u32 = 1, + minion_chop_damage: u32 = chop_levels[0].damage, + minion_chop_rate: f32 = chop_levels[0].rate, + minion_throw_level: u32 = 1, + minion_throw_cooldown: Nanoseconds = throw_levels[0].cooldown, + minion_throw_strength: u32 = throw_levels[0].strength, minion_carry_capacity: u32 = 1, - minion_walk: MinionWalkStats = .{}, + minion_walk_level: u32 = 1, + minion_walk: MinionWalkStats = walk_levels[0], tree_station_count: u32 = 1, tree_level: u32 = 1, @@ -265,27 +272,106 @@ const Upgrades = struct { tree_wood_amount: u32 = tree_levels[0].wood_amount, tree_wood_health: f32 = tree_levels[0].wood_health, - burn_speed: f32 = 10, + burn_speed: u32 = 10, burn_heat_per_wood: u32 = 1, burn_max_stack_size: u32 = 10, burn_levels: [2]BurnLevel = .{ - .{ .threshold = 10, .multiplier = 2 }, - .{ .threshold = 20, .multiplier = 3 }, + .{ .threshold = 5 , .multiplier = 2 }, + .{ .threshold = 15, .multiplier = 3 }, }, const tree_levels = [_]TreeLevel{ TreeLevel{ .health = 10, - .respawn_duration = std.time.ns_per_s, + .respawn_duration = std.time.ns_per_s * 5, .wood_amount = 1, .wood_health = 20, + }, + TreeLevel{ + .health = 30, + .respawn_duration = std.time.ns_per_s * 3, + .wood_amount = 3, + .wood_health = 30, + }, + TreeLevel{ + .health = 60, + .respawn_duration = std.time.ns_per_s * 1, + .wood_amount = 6, + .wood_health = 50, + }, + TreeLevel{ + .health = 120, + .respawn_duration = std.time.ns_per_s * 0.5, + .wood_amount = 20, + .wood_health = 80, + } + }; + + const chop_levels = [_]ChopLevel{ + ChopLevel{ + .damage = 2, + .rate = 1 + }, + ChopLevel{ + .damage = 5, + .rate = 2, + }, + ChopLevel{ + .damage = 10, + .rate = 4, + }, + ChopLevel{ + .damage = 40, + .rate = 8 + }, + }; + + const walk_levels = [_]MinionWalkStats{ + MinionWalkStats{ + .friction = 0.998, + .max_velocity = 50, + .max_speed = 300, + }, + MinionWalkStats{ + .friction = 0.998, + .max_velocity = 200, + .max_speed = 800, + }, + MinionWalkStats{ + .friction = 0.996, + .max_velocity = 400, + .max_speed = 1600, + }, + MinionWalkStats{ + .friction = 0.996, + .max_velocity = 1000, + .max_speed = 2800, + } + }; + + const throw_levels = [_]ThrowLevel{ + ThrowLevel{ + .cooldown = std.time.ns_per_s, + .strength = 1, + }, + ThrowLevel{ + .cooldown = std.time.ns_per_s*0.5, + .strength = 2, + }, + ThrowLevel{ + .cooldown = std.time.ns_per_s*0.25, + .strength = 4, + }, + ThrowLevel{ + .cooldown = std.time.ns_per_s*0.1, + .strength = 8, } }; const MinionWalkStats = struct { - friction: f32 = 0.998, - max_velocity: f32 = 50, - max_speed: f32 = 300, + friction: f32, + max_velocity: f32, + max_speed: f32, }; const BurnLevel = struct { @@ -299,11 +385,24 @@ const Upgrades = struct { wood_amount: u32, wood_health: f32, }; + + const ChopLevel = struct { + damage: u32, + rate: f32, + }; + + const ThrowLevel = struct { + cooldown: Nanoseconds, + strength: u32, + }; }; const Shop = struct { minion_count: Shop.Item, minion_carry_capacity: Shop.Item, + minion_chop_level: Shop.Item, + minion_walk_level: Shop.Item, + minion_throw_level: Shop.Item, tree_count: Shop.Item, tree_level: Shop.Item, @@ -572,11 +671,12 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 { self.spawnBurnStation(100); self.spawnBurnStation(150); + self.spawnBurnStation(200); self.eternal_flame_id = self.spawnEntity(Entity{ .type = .eternal_flame, - .pos = .init(250, 0), - .eternal_flame_size = .large, + .pos = .init(300, 0), + .eternal_flame_size = .small, }).?; return null; @@ -586,33 +686,64 @@ fn initShop(self: *App, arena: Allocator) !void { self.shop = .{ .minion_count = Shop.Item{ .label = "Minion count", - .limit = 50, - .unlocked = true, + .limit = 30, .value_ref = .initU32(&self.upgrades.minion_count), .value_scaling = .initFixed(1), .cost = 1, - .cost_scaling = .initFixed(1), + .cost_scaling = .initLinear(2, 1.25), }, .minion_carry_capacity = Shop.Item{ .label = "Carry capacity", + .limit = 8, .value_ref = .initU32(&self.upgrades.minion_carry_capacity), .value_scaling = .initFixed(1), - .cost = 1, - .cost_scaling = .initFixed(1), + .cost = 2, + .cost_scaling = .initLinear(10, 2), + }, + .minion_chop_level = Shop.Item{ + .label = "Chop level", + .limit = Upgrades.chop_levels.len, + + .value_ref = .initU32(&self.upgrades. minion_chop_level), + .value_scaling = .initFixed(1), + + .cost = 2, + .cost_scaling = .initLinear(20, 2), + }, + .minion_walk_level = Shop.Item{ + .label = "Walk speed", + .limit = Upgrades.walk_levels.len, + + .value_ref = .initU32(&self.upgrades.minion_walk_level), + .value_scaling = .initFixed(1), + + .cost = 3, + .cost_scaling = .initLinear(10, 2), + }, + .minion_throw_level = Shop.Item{ + .label = "Throw wood level", + .limit = Upgrades.throw_levels.len, + + .value_ref = .initU32(&self.upgrades.minion_throw_level), + .value_scaling = .initFixed(1), + + .cost = 8, + .cost_scaling = .initLinear(20, 3.0), }, .tree_count = Shop.Item{ .label = "Tree count", + .limit = 15, .value_ref = .initU32(&self.upgrades.tree_station_count), .value_scaling = .initFixed(1), - .cost = 1, - .cost_scaling = .initFixed(1), + .cost = 3, + .cost_scaling = .initLinear(1, 1.5), }, .tree_level = Shop.Item{ .label = "Tree level", @@ -621,50 +752,56 @@ fn initShop(self: *App, arena: Allocator) !void { .value_ref = .initU32(&self.upgrades.tree_level), .value_scaling = .initFixed(1), - .cost = 1, - .cost_scaling = .initFixed(1), + .cost = 10, + .cost_scaling = .initLinear(2, 1.3), }, .fire_speed = Shop.Item{ - .label = "Burn speed", + .label = "Burn wood speed", + .limit = 50, - .value_ref = .initF32(&self.upgrades.burn_speed), - .value_scaling = .initFixed(1), + .value_ref = .initU32(&self.upgrades.burn_speed), + .value_scaling = .initLinear(0, 1.5), - .cost = 1, - .cost_scaling = .initFixed(1), + .cost = 10, + .cost_scaling = .initLinear(5, 1.5), }, .fire_heat_per_wood = Shop.Item{ .label = "Heat per wood", + .limit = 5, .value_ref = .initU32(&self.upgrades.burn_heat_per_wood), .value_scaling = .initFixed(1), - .cost = 1, - .cost_scaling = .initFixed(1), + .cost = 30, + .cost_scaling = .initLinear(1, 2), }, .fire_stack_size = Shop.Item{ .label = "Stack size", + .limit = 25, .value_ref = .initU32(&self.upgrades.burn_max_stack_size), - .value_scaling = .initFixed(1), + .value_scaling = .initFixed(10), - .cost = 1, - .cost_scaling = .initFixed(1), + .cost = 20, + .cost_scaling = .initLinear(2, 2.5), }, .minion_upgrades = try arena.dupe(*Shop.Item, &.{ &self.shop.minion_count, - &self.shop.minion_carry_capacity + &self.shop.minion_carry_capacity, + &self.shop.minion_chop_level, + &self.shop.minion_walk_level, + &self.shop.minion_throw_level }), .tree_upgrades = try arena.dupe(*Shop.Item, &.{ &self.shop.tree_count, &self.shop.tree_level }), .fire_upgrades = try arena.dupe(*Shop.Item, &.{ + &self.shop.fire_stack_size, &self.shop.fire_speed, &self.shop.fire_heat_per_wood, - &self.shop.fire_stack_size, }), }; } @@ -1207,16 +1344,25 @@ fn removeWoodStation(self: *App, id: EntityId) void { self.entities.removeAssumeExists(id); } -fn getBurnMultiplier(self: *App) u32 { +fn getBurnLevelIndex(self: *App) ?usize { 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]; + for (0..self.upgrades.burn_levels.len) |oi| { + const i = self.upgrades.burn_levels.len - oi - 1; + const level = self.upgrades.burn_levels[i]; if (stack_size >= level.threshold) { - return level.multiplier; + return i; } } + return null; +} + +fn getBurnMultiplier(self: *App) u32 { + if (self.getBurnLevelIndex()) |index| { + return self.upgrades.burn_levels[index].multiplier; + } + return 1; } @@ -1252,7 +1398,7 @@ fn showShopPanel( return; } - const panel_size = Vec2.init(400, unlocked_count * 32 + 10); + const panel_size = Vec2.init(420, unlocked_count * 32 + 10); const border_width = 5; const border_color = Color.rgb(180, 180, 180); @@ -1353,8 +1499,8 @@ fn showShopPanel( const allocPrint = std.fmt.allocPrint; Gfx.drawText(self.font, .{ - .text = try allocPrint(plt.arena, "{f}", .{shop_item.value_ref.deref()}), - .pos = .init(180, 0), + .text = try allocPrint(plt.frame, "{f}", .{shop_item.value_ref.deref()}), + .pos = .init(200, 0), .height = 24, .color = text_color }); @@ -1362,7 +1508,7 @@ fn showShopPanel( if (is_mouse_hovering) { const new_value = shop_item.value_scaling.apply(shop_item.value_ref.deref()); Gfx.drawText(self.font, .{ - .text = try allocPrint(plt.arena, "> {f}", .{new_value}), + .text = try allocPrint(plt.frame, "> {f}", .{new_value}), .pos = .init(250, 0), .height = 24, .color = text_color @@ -1370,7 +1516,7 @@ fn showShopPanel( } Gfx.drawText(self.font, .{ - .text = if (limit_reached) "MAX" else try std.fmt.allocPrint(plt.arena, "{}H", .{cost}), + .text = if (limit_reached) "MAX" else try std.fmt.allocPrint(plt.frame, "{}H", .{cost}), .pos = .init(panel_size.x - 20, 0), .height = 24, .color = text_color, @@ -1382,12 +1528,12 @@ fn showShopPanel( fn showUI(self: *App, plt: Platform.Frame) !void { var heat_pos = Vec2.init(50, -200); - { // Draw heat counter - var builder = Gfx.TextBuilder.init(plt.arena); + if (self.show_heat) { // Draw heat counter + var builder = Gfx.TextBuilder.init(plt.frame); builder.font(self.font, 64); builder.color = .rgb(200, 20, 20); - try builder.put(try std.fmt.allocPrint(plt.arena, "{}", .{self.heat})); + try builder.put(try std.fmt.allocPrint(plt.frame, "{}", .{self.heat})); builder.font(self.font, 32); builder.color = .rgb(240, 100, 100); @@ -1395,6 +1541,61 @@ fn showUI(self: *App, plt: Platform.Frame) !void { try builder.put("Heat"); builder.draw(heat_pos.sub(builder.bounds.size.divideScalar(2))); + } else { + self.show_heat = self.heat > 0; + } + + if (self.show_multiplier) { + Gfx.transformPush(); + defer Gfx.transformPop(); + + Gfx.transformTranslate(heat_pos); + Gfx.transformTranslate(.init(-10, 56)); + + var lower_threshold: u32 = 0; + var upper_threshold: u32 = 0; + if (self.getBurnLevelIndex()) |index| { + lower_threshold = self.upgrades.burn_levels[index].threshold; + if (index+1 < self.upgrades.burn_levels.len) { + upper_threshold = self.upgrades.burn_levels[index+1].threshold; + } else { + upper_threshold = lower_threshold; + } + } else { + upper_threshold = self.upgrades.burn_levels[0].threshold; + } + + const stack_size = self.countEternalFlameStackSize(); + + var progress: f32 = 1; + if (lower_threshold < upper_threshold ) { + progress = @as(f32, @floatFromInt(stack_size-lower_threshold)) / @as(f32, @floatFromInt(upper_threshold-lower_threshold)); + } + + self.visual_progress = std.math.lerp(self.visual_progress, progress, 0.03); + + const progress_bar_size = Vec2.init(80, 10); + drawProgressBar(1, .init(0, 0), progress_bar_size, .rgb(30, 30, 30)); + drawProgressBar(self.visual_progress, .init(0, 0), progress_bar_size, .rgb(255, 255, 255)); + + Gfx.drawText(self.font, .{ + .height = 18, + .pos = .init(progress_bar_size.x/2 + 10, -9), + .text = try std.fmt.allocPrint(plt.frame, "{}x", .{self.getBurnMultiplier()}) + }); + } else { + self.show_multiplier = self.heat > 2; + } + + inline for (&.{ self.shop.minion_upgrades, self.shop.tree_upgrades, self.shop.fire_upgrades }) |upgrades| { + for (upgrades) |upgrade| { + if (!upgrade.unlocked) { + upgrade.unlocked = self.heat >= upgrade.cost; + } + } + } + if (!self.shop.minion_chop_level.unlocked) { + self.shop.minion_chop_level.unlocked = self.upgrades.tree_level > 1; } try self.showShopPanel(plt, "Minion", Vec2.init(0, -600), self.shop.minion_upgrades); @@ -1502,10 +1703,7 @@ fn showDebug(self: *App, plt: Platform.Frame) !void { ImGUI.separator(); _ = ImGUI.text("Burn upgrades", .{}); - _ = ImGUI.inputF32Drag(.{ - .label = "speed", - .value = &self.upgrades.burn_speed - }); + _ = ImGUI.inputU32("speed", &self.upgrades.burn_speed); _ = 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| { @@ -1799,7 +1997,8 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { // Apply burn if (wood.wood_burning) { - wood.wood_health = @max(wood.wood_health - self.upgrades.burn_speed * dt, 0); + const burn_speed: f32 = @floatFromInt(self.upgrades.burn_speed); + wood.wood_health = @max(wood.wood_health - burn_speed * dt, 0); if (wood.wood_health == 0) { self.heat += self.upgrades.burn_heat_per_wood * self.getBurnMultiplier(); @@ -1811,7 +2010,7 @@ 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) { - var minion_iter = self.initNearestEntityIter(.minion, wood.pos, plt.arena); + var minion_iter = self.initNearestEntityIter(.minion, wood.pos, plt.frame); while (try minion_iter.next()) |minion_id| { const minion = self.entities.getAssumeExists(minion_id); if (minion.minion_carried_wood_len == self.upgrades.minion_carry_capacity) { @@ -1841,7 +2040,7 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { continue; } - var minion_iter = self.initNearestEntityIter(.minion, burn_station.pos, plt.arena); + var minion_iter = self.initNearestEntityIter(.minion, burn_station.pos, plt.frame); while (try minion_iter.next()) |minion_id| { const minion = self.entities.getAssumeExists(minion_id); const state = minion.minion_state; @@ -1858,7 +2057,7 @@ 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); - var minion_iter = self.initNearestEntityIter(.minion, tree.pos, plt.arena); + var minion_iter = self.initNearestEntityIter(.minion, tree.pos, plt.frame); while (try minion_iter.next()) |minion_id| { const minion = self.entities.getAssumeExists(minion_id); if (minion.minion_carried_wood_len == self.upgrades.minion_carry_capacity) { @@ -1901,7 +2100,7 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { continue; } - var minion_iter = self.initNearestEntityIter(.minion, wood_station.pos, plt.arena); + var minion_iter = self.initNearestEntityIter(.minion, wood_station.pos, plt.frame); while (try minion_iter.next()) |minion_id| { const minion = self.entities.getAssumeExists(minion_id); if (minion.minion_carried_wood_len == self.upgrades.minion_carry_capacity) { @@ -2087,6 +2286,14 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { ); } + if (self.getBurnLevelIndex() == null) { + eternal_flame.eternal_flame_size = .small; + } else if (self.getBurnLevelIndex() == 0) { + eternal_flame.eternal_flame_size = .medium; + } else { + eternal_flame.eternal_flame_size = .large; + } + switch (eternal_flame.eternal_flame_size) { .small => { const flame_size = Vec2.init(16, 32).multiplyScalar(4); @@ -2118,10 +2325,6 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { self.drawEntityAnimation(e, plt.dt); } - if (e.type == .minion and e.minion_state == .burn_wood and e.minion_throw_cooldown > 0) { - Gfx.drawRectangle(e.pos.sub(.init(5, 65)), .init(10, 10), .rgb(200, 20, 20)); - } - if (e.type == .burn_station) { Gfx.drawSprite( self.terrain_sheet.get(123, 81), @@ -2134,31 +2337,13 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { if (e.type == .minion and self.show_minion_state) { Gfx.drawText(self.font, .{ .height = 16, - .text = try std.fmt.allocPrint(plt.arena, "{}", .{e.minion_state}), + .text = try std.fmt.allocPrint(plt.frame, "{}", .{e.minion_state}), .pos = e.pos.add(.init(0, 20)), }); } } } - { // Draw eternal flame stats - var eternal_flame_iter = self.initEntityIteratorByType(.eternal_flame); - while (eternal_flame_iter.next()) |id| { - const eternal_flame = self.entities.getAssumeExists(id); - - Gfx.drawText(self.font, .{ - .text = try std.fmt.allocPrint(plt.arena, "Stack: {}", .{self.countEternalFlameStackSize()}), - .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 - }); - } - } - { // Draw wood var wood_iter = self.initEntityIteratorByType(.wood); while (wood_iter.next()) |id| { @@ -2187,8 +2372,49 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { } } + const previous_tree_level = self.upgrades.tree_level; + const previous_chop_level = self.upgrades.minion_chop_level; + const previous_walk_level = self.upgrades.minion_walk_level; + const previous_throw_level = self.upgrades.minion_throw_level; try self.showUI(plt); try self.showDebug(plt); + + if (previous_tree_level != self.upgrades.tree_level) { + if (self.upgrades.tree_level >= Upgrades.tree_levels.len) { + self.upgrades.tree_level = Upgrades.tree_levels.len; + } + const level = Upgrades.tree_levels[self.upgrades.tree_level-1]; + self.upgrades.tree_health = level.health; + self.upgrades.tree_respawn_duration = level.respawn_duration; + self.upgrades.tree_wood_amount = level.wood_amount; + self.upgrades.tree_wood_health = level.wood_health; + } + + if (previous_chop_level != self.upgrades.minion_chop_level) { + if (self.upgrades.minion_chop_level >= Upgrades.chop_levels.len) { + self.upgrades.minion_chop_level = Upgrades.chop_levels.len; + } + const level = Upgrades.chop_levels[self.upgrades.minion_chop_level-1]; + self.upgrades.minion_chop_damage = level.damage; + self.upgrades.minion_chop_rate = level.rate; + } + + if (previous_walk_level != self.upgrades.minion_walk_level) { + if (self.upgrades.minion_walk_level >= Upgrades.walk_levels.len) { + self.upgrades.minion_walk_level = Upgrades.walk_levels.len; + } + const level = Upgrades.walk_levels[self.upgrades.minion_walk_level-1]; + self.upgrades.minion_walk = level; + } + + if (previous_throw_level != self.upgrades.minion_throw_level) { + if (self.upgrades.minion_throw_level >= Upgrades.throw_levels.len) { + self.upgrades.minion_throw_level = Upgrades.throw_levels.len; + } + const level = Upgrades.throw_levels[self.upgrades.minion_throw_level-1]; + self.upgrades.minion_throw_cooldown = level.cooldown; + self.upgrades.minion_throw_strength = level.strength; + } } pub fn deinit(self: *App, plt: Platform.Deinit) void { diff --git a/src/graphics.zig b/src/graphics.zig index 618b628..c6ec261 100644 --- a/src/graphics.zig +++ b/src/graphics.zig @@ -1818,7 +1818,7 @@ pub const Sprite = struct { position: ?Vec2, padding: u32, - const max_sprites = 128; + const max_sprites = 256; const SlotMap = SlotMapType(std.math.IntFittingRange(0, max_sprites-1), u8, Sprite); pub const Id = SlotMap.Id; };