From b356038f1f453ad0d36bf2ab54e7122f7eff2242 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Sun, 19 Jul 2026 02:19:04 +0300 Subject: [PATCH] render trees --- src/app.zig | 177 +++++++++++++++++++++++++++++++++++------------ src/graphics.zig | 96 ++++++++++++++++--------- 2 files changed, 195 insertions(+), 78 deletions(-) diff --git a/src/app.zig b/src/app.zig index 57c3bef..33972ed 100644 --- a/src/app.zig +++ b/src/app.zig @@ -51,6 +51,7 @@ const terrain_origin_x: i32 = -80; const terrain_origin_y: i32 = -60; const terrain_width = 160; const terrain_height = 70; +const terrain_tile_size = Vec2.init(32, 32); const EntitySlotMap = SlotMapType(u16, u16, Entity); const EntityId = EntitySlotMap.Id; @@ -179,6 +180,7 @@ const Entity = struct { tree_health: u32 = 0, tree_max_health: u32 = 0, + tree_seed: u64 = 0, station_spawn_tree_cooldown: Nanoseconds = 0, station_owned_tree: ?EntityId = null, @@ -279,9 +281,19 @@ const Upgrades = struct { }; const Shop = struct { - minion_upgrades: []Item, - tree_upgrades: []Item, - fire_upgrades: []Item, + minion_count: Shop.Item, + minion_carry_capacity: Shop.Item, + + tree_count: Shop.Item, + tree_level: Shop.Item, + + fire_speed: Shop.Item, + fire_heat_per_wood: Shop.Item, + fire_stack_size: Shop.Item, + + minion_upgrades: []*Item, + tree_upgrades: []*Item, + fire_upgrades: []*Item, const Item = struct { label: []const u8, @@ -491,8 +503,8 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 { } fn initShop(self: *App, arena: Allocator) !void { - const minion_upgrades = try arena.dupe(Shop.Item, &.{ - Shop.Item{ + self.shop = .{ + .minion_count = Shop.Item{ .label = "Minion count", .limit = 50, .unlocked = true, @@ -503,9 +515,8 @@ fn initShop(self: *App, arena: Allocator) !void { .cost = 1, .cost_scaling = .initFixed(1), }, - Shop.Item{ + .minion_carry_capacity = Shop.Item{ .label = "Carry capacity", - .unlocked = true, .value_ref = .initU32(&self.upgrades.minion_carry_capacity), .value_scaling = .initFixed(1), @@ -513,12 +524,9 @@ fn initShop(self: *App, arena: Allocator) !void { .cost = 1, .cost_scaling = .initFixed(1), }, - }); - const tree_upgrades = try arena.dupe(Shop.Item, &.{ - Shop.Item{ + .tree_count = Shop.Item{ .label = "Tree count", - .unlocked = true, .value_ref = .initU32(&self.upgrades.tree_station_count), .value_scaling = .initFixed(1), @@ -526,10 +534,9 @@ fn initShop(self: *App, arena: Allocator) !void { .cost = 1, .cost_scaling = .initFixed(1), }, - Shop.Item{ + .tree_level = Shop.Item{ .label = "Tree level", .limit = Upgrades.tree_levels.len, - .unlocked = true, .value_ref = .initU32(&self.upgrades.tree_level), .value_scaling = .initFixed(1), @@ -537,12 +544,9 @@ fn initShop(self: *App, arena: Allocator) !void { .cost = 1, .cost_scaling = .initFixed(1), }, - }); - const fire_upgrades = try arena.dupe(Shop.Item, &.{ - Shop.Item{ + .fire_speed = Shop.Item{ .label = "Burn speed", - .unlocked = true, .value_ref = .initF32(&self.upgrades.burn_speed), .value_scaling = .initFixed(1), @@ -550,9 +554,8 @@ fn initShop(self: *App, arena: Allocator) !void { .cost = 1, .cost_scaling = .initFixed(1), }, - Shop.Item{ + .fire_heat_per_wood = Shop.Item{ .label = "Heat per wood", - .unlocked = true, .value_ref = .initU32(&self.upgrades.burn_heat_per_wood), .value_scaling = .initFixed(1), @@ -560,9 +563,8 @@ fn initShop(self: *App, arena: Allocator) !void { .cost = 1, .cost_scaling = .initFixed(1), }, - Shop.Item{ - .label = "Max stack size", - .unlocked = true, + .fire_stack_size = Shop.Item{ + .label = "Stack size", .value_ref = .initU32(&self.upgrades.burn_max_stack_size), .value_scaling = .initFixed(1), @@ -570,12 +572,20 @@ fn initShop(self: *App, arena: Allocator) !void { .cost = 1, .cost_scaling = .initFixed(1), }, - }); - self.shop = .{ - .minion_upgrades = minion_upgrades, - .tree_upgrades = tree_upgrades, - .fire_upgrades = fire_upgrades, + .minion_upgrades = try arena.dupe(*Shop.Item, &.{ + &self.shop.minion_count, + &self.shop.minion_carry_capacity + }), + .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_speed, + &self.shop.fire_heat_per_wood, + &self.shop.fire_stack_size, + }), }; } @@ -617,6 +627,28 @@ fn initTerrain(self: *App) void { tile.sprite = self.pickRandom(Gfx.Sprite.Id, underground_tiles); } } + + const grass_tiles = &[_]Gfx.Sprite.Id{ + self.terrain_sheet.get(113, 45), + self.terrain_sheet.get(114, 45), + self.terrain_sheet.get(115, 45), + self.terrain_sheet.get(116, 45), + self.terrain_sheet.get(117, 45), + self.terrain_sheet.get(122, 45), + self.terrain_sheet.get(123, 45), + self.terrain_sheet.get(124, 45), + self.terrain_sheet.get(125, 45), + Gfx.getNilSprite(), + Gfx.getNilSprite(), + Gfx.getNilSprite(), + Gfx.getNilSprite(), + Gfx.getNilSprite(), + Gfx.getNilSprite(), + }; + for (0..terrain_width) |x| { + const tile = self.getTerrainTile(@as(i32, @intCast(x)) + terrain_origin_x, -1); + tile.sprite = self.pickRandom(Gfx.Sprite.Id, grass_tiles); + } } fn getTerrainTile(self: *App, x: i32, y: i32) *Tile { @@ -633,8 +665,6 @@ fn getTerrainTile(self: *App, x: i32, y: i32) *Tile { return &self.terrain_tiles[y_u32 * terrain_width + x_u32]; } -// fn getTerrainEdgeLeft(self: *App) - fn spawnEntity(self: *App, e: Entity) ?EntityId { return self.entities.insert(e) catch { log.warn("Failed to spawn entity {}, entity limit reached", .{e.type}); @@ -679,11 +709,14 @@ fn spawnTree(self: *App, station_id: EntityId) void { return; } + const rng = self.prng.random(); + const health = self.upgrades.tree_health; if (self.spawnEntity(Entity{ .type = .tree, .tree_health = health, .tree_max_health = health, + .tree_seed = rng.int(u64), .pos = station.pos })) |id| { @@ -1104,10 +1137,10 @@ fn showShopPanel( plt: Platform.Frame, panel_label: []const u8, panel_pos: Vec2, - shop_items: []Shop.Item + shop_items: []*Shop.Item ) !void { var unlocked_count: f32 = 0; - for (shop_items) |*shop_item| { + for (shop_items) |shop_item| { if (shop_item.unlocked) { unlocked_count += 1; } @@ -1154,7 +1187,7 @@ fn showShopPanel( Gfx.transformTranslate(.init(10, -5)); var index: usize = 0; - for (shop_items) |*shop_item| { + for (shop_items) |shop_item| { if (!shop_item.unlocked) { continue; } @@ -1228,7 +1261,7 @@ fn showShopPanel( 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}), - .pos = .init(240, 0), + .pos = .init(250, 0), .height = 24, .color = text_color }); @@ -1245,7 +1278,7 @@ fn showShopPanel( } fn showUI(self: *App, plt: Platform.Frame) !void { - var heat_pos = Vec2.init(0, -200); + var heat_pos = Vec2.init(50, -200); { // Draw heat counter var builder = Gfx.TextBuilder.init(plt.arena); @@ -1285,13 +1318,13 @@ fn showDebugScaling(label: []const u8, scaling: *Shop.Item.Scaling) void { _ = ImGUI.inputF32Drag(.{ .label = "quadratic", .value = &scaling.quadratic }); } -fn showDebugShopItems(self: *App, label: []const u8, shop_items: []Shop.Item) void { +fn showDebugShopItems(self: *App, label: []const u8, shop_items: []*Shop.Item) void { _ = self; // autofix ImGUI.pushID(.{ .string = label }); defer ImGUI.popID(); _ = ImGUI.text("{s}", .{label}); - for (0.., shop_items) |i, *shop_item| { + for (0.., shop_items) |i, shop_item| { ImGUI.pushID(.{ .int = @intCast(i) }); defer ImGUI.popID(); @@ -1397,6 +1430,66 @@ fn showDebug(self: *App, plt: Platform.Frame) !void { } } +fn drawTree(self: *App, seed: u64, pos: Vec2) void { + var prng = Prng.init(seed); + const rng = prng.random(); + + Gfx.transformPush(); + defer Gfx.transformPop(); + + Gfx.transformTranslate(pos.sub(.init(terrain_tile_size.x/2, terrain_tile_size.y))); + Gfx.transformScale(terrain_tile_size); + + Gfx.transformTranslate(.init(rng.float(f32)-0.5, 0)); + + const tree_middle = &[_]Gfx.Sprite.Id{ + self.terrain_sheet.get(125, 41), + self.terrain_sheet.get(125, 40), + self.terrain_sheet.get(125, 39), + }; + + const trunk_height = rng.uintAtMost(u32, 4) + 2; + + Gfx.drawSprite(self.terrain_sheet.get(125, 42), .init(0, 0), .init(1, 1), .white); + for (0..trunk_height) |i| { + const sprite = tree_middle[rng.uintLessThan(u32, tree_middle.len)]; + var trunk_pos = Vec2.init(0, -@as(f32, @floatFromInt(i))); + trunk_pos.x += (rng.float(f32)-0.5)*2 * 0.15; + Gfx.drawSprite(sprite, trunk_pos, .init(1, 1), .white); + } + Gfx.transformTranslate(Vec2.initFromInt(u32, 0, trunk_height).multiplyScalar(-1)); + Gfx.drawSprite( + self.terrain_sheet.get(125, 38), + .init(0, 0), + .init(1, 1), + .white + ); + + const inner_leaves = self.terrain_sheet.get(123, 42); + const bottom_leaves = self.terrain_sheet.get(124, 39); + const left_leaves = self.terrain_sheet.get(123, 38); + const corner_leaves = self.terrain_sheet.get(123, 39); + + Gfx.drawSprite(bottom_leaves, .init(0, -1), .init(1, 1), .white); + Gfx.drawSprite(corner_leaves, .init(-1, -1), .init(1, 1), .white); + Gfx.drawSprite(corner_leaves, .init(2, -1), .init(-1, 1), .white); + + const leaves_height = rng.uintAtMost(u32, 3) + 1; + + for (0..leaves_height) |i| { + var base = Vec2.initFromInt(usize, 0, i).multiplyScalar(-1); + base.x += (rng.float(f32)-0.5)*2 * 0.2; + Gfx.drawSprite(inner_leaves, base.add(.init(0, -2)), .init(1, 1), .white); + Gfx.drawSprite(left_leaves, base.add(.init(2, -2)), .init(-1, 1), .white); + Gfx.drawSprite(left_leaves, base.add(.init(-1, -2)), .init(1, 1), .white); + } + Gfx.transformTranslate(Vec2.initFromInt(u32, 0, leaves_height).multiplyScalar(-1)); + + Gfx.drawSprite(bottom_leaves, .init(0, -1), .init(1, -1), .white); + Gfx.drawSprite(corner_leaves, .init(-1, -1), .init(1, -1), .white); + Gfx.drawSprite(corner_leaves, .init(2, -1), .init(-1, -1), .white); +} + pub fn frame(self: *App, plt: Platform.Frame) !void { const input = plt.input; _ = input; // autofix @@ -1456,7 +1549,7 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { } const wood_station_first_pos = -100; - const wood_station_step = -50; + const wood_station_step = -70; if (station_count > self.upgrades.tree_station_count) { for (self.upgrades.tree_station_count..station_count) |i| { @@ -1695,9 +1788,8 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { continue; } - const tile_size = Vec2.init(32, 32); const tile_pos = Vec2.initFromInt(usize, x, y).add(.initFromInt(i32, terrain_origin_x, terrain_origin_y)); - Gfx.drawSprite(tile.sprite, tile_pos.multiply(tile_size), tile_size, .white); + Gfx.drawSprite(tile.sprite, tile_pos.multiply(terrain_tile_size), terrain_tile_size, .white); } } } @@ -1707,8 +1799,7 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { while (tree_iter.next()) |id| { const e = self.entities.getAssumeExists(id); - 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)); + self.drawTree(e.tree_seed, e.pos); 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)); @@ -1788,9 +1879,7 @@ pub fn frame(self: *App, plt: Platform.Frame) !void { } } - if (e.type == .wood_station) { - Gfx.drawRectangle(e.pos.sub(.init(5, 5)), .init(10, 10), .rgb(20, 200, 20)); - } else if (e.type == .burn_station) { + if (e.type == .burn_station) { Gfx.drawRectangle(e.pos.sub(.init(5, 5)), .init(10, 10), .rgb(200, 20, 20)); } else if (e.type == .minion) { Gfx.drawText(self.font, .{ diff --git a/src/graphics.zig b/src/graphics.zig index 907ce9a..5dabf8a 100644 --- a/src/graphics.zig +++ b/src/graphics.zig @@ -39,8 +39,8 @@ const State = struct { nearest_sampler: sg.Sampler, default_sampler: Sampler, - quads_buffer: [2048]Quad, - quads: std.ArrayList(Quad), + quads_buffer: [2048]VertexQuad, + quads: std.ArrayList(VertexQuad), clear_color: Color, @@ -126,7 +126,7 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void { const max_quads = self.quads_buffer.len; bindings.vertex_buffers[0] = sg.makeBuffer(.{ - .size = @sizeOf(Quad) * max_quads, + .size = @sizeOf(VertexQuad) * max_quads, .usage = .{ .vertex_buffer = true, .stream_update = true }, .label = "quad-vertices" }); @@ -543,42 +543,42 @@ pub fn drawRectangleOutline(pos: Vec2, size: Vec2, color: Color, width: f32, ali drawSpriteQuad( self.default_sprite, - .{ + .init(.{ .init(outer_rect.right(), outer_rect.top()), .init(outer_rect.left(), outer_rect.top()), .init(inner_rect.right(), inner_rect.top()), .init(inner_rect.left(), inner_rect.top()), - }, + }), color ); drawSpriteQuad( self.default_sprite, - .{ + .init(.{ .init(outer_rect.right(), outer_rect.top()), .init(inner_rect.right(), inner_rect.top()), .init(outer_rect.right(), outer_rect.bottom()), .init(inner_rect.right(), inner_rect.bottom()), - }, + }), color ); drawSpriteQuad( self.default_sprite, - .{ + .init(.{ .init(outer_rect.left(), outer_rect.top()), .init(inner_rect.left(), inner_rect.top()), .init(outer_rect.left(), outer_rect.bottom()), .init(inner_rect.left(), inner_rect.bottom()), - }, + }), color ); drawSpriteQuad( self.default_sprite, - .{ + .init(.{ .init(outer_rect.right(), outer_rect.bottom()), .init(outer_rect.left(), outer_rect.bottom()), .init(inner_rect.right(), inner_rect.bottom()), .init(inner_rect.left(), inner_rect.bottom()), - }, + }), color ); } @@ -588,14 +588,14 @@ pub fn drawLine(from: Vec2, to: Vec2, color: Color, width: f32) void { const step = to.sub(from).normalized().multiplyScalar(width/2); - const top_left = from.add(step.rotateLeft90()); - const bottom_left = from.add(step.rotateRight90()); + const top_left = from.add(step.rotateLeft90()); + const bottom_left = from.add(step.rotateRight90()); const top_right = to.add(step.rotateLeft90()); const bottom_right = to.add(step.rotateRight90()); drawSpriteQuad( self.default_sprite, - .{ top_right, top_left, bottom_right, bottom_left }, + .init(.{ top_right, top_left, bottom_right, bottom_left }), color ); } @@ -755,7 +755,7 @@ pub fn setTexture(id: Texture.Id, texture_data: ImageData) void { } pub fn drawTexture(id: Texture.Id, pos: Vec2, size: Vec2, color: Color) void { - var quad: Quad = undefined; + var quad: VertexQuad = undefined; quad.setColor(color); quad.setRect(.{ .pos = pos, .size = size }); quad.setUVRect(.init(0, 0, 1, 1)); @@ -795,7 +795,7 @@ pub const Sampler = enum { const DrawOptions = struct { sampler: ?Sampler = null, texture: Texture.Id, - quad: Quad + quad: VertexQuad }; pub fn draw(opts: DrawOptions) void { @@ -1180,15 +1180,15 @@ pub fn getSpriteUVRect(id: Sprite.Id) ?Rect { return uv_rect; } -fn drawSpriteQuad(id: Sprite.Id, points: [4]Vec2, color: Color) void { +pub fn drawSpriteQuad(id: Sprite.Id, points: Quad, color: Color) void { const self = &g_state; - var quad: Quad = undefined; + var quad: VertexQuad = undefined; quad.setColor(color); - quad.vertices[0].position = points[0]; - quad.vertices[1].position = points[1]; - quad.vertices[2].position = points[2]; - quad.vertices[3].position = points[3]; + quad.vertices[0].position = points.positions[0]; + quad.vertices[1].position = points.positions[1]; + quad.vertices[2].position = points.positions[2]; + quad.vertices[3].position = points.positions[3]; var texture: ?Texture.Id = null; if (id != self.nil_sprite) { @@ -1208,12 +1208,7 @@ fn drawSpriteQuad(id: Sprite.Id, points: [4]Vec2, color: Color) void { } pub fn drawSprite(id: Sprite.Id, pos: Vec2, size: Vec2, color: Color) void { - const rect = .{ - pos, - pos.add(.init(size.x, 0)), - pos.add(.init(0, size.y)), - pos.add(size), - }; + const rect = Quad.initRect(pos, size); drawSpriteQuad(id, rect, color); } @@ -2296,16 +2291,16 @@ pub const Vertex = extern struct { color: Vec4, }; -pub const Quad = extern struct { +pub const VertexQuad = extern struct { vertices: [4]Vertex, - pub fn init(vertices: [4]Vertex) Quad { - return Quad{ + pub fn init(vertices: [4]Vertex) VertexQuad { + return VertexQuad{ .vertices = vertices }; } - pub fn setColor(self: *Quad, color: Color) void { + pub fn setColor(self: *VertexQuad, color: Color) void { for (&self.vertices) |*vertex| { vertex.color = Vec4{ .x = color.r, @@ -2316,7 +2311,7 @@ pub const Quad = extern struct { } } - pub fn setUVRect(self: *Quad, rect: Rect) void { + pub fn setUVRect(self: *VertexQuad, rect: Rect) void { const pos = rect.pos; const size = rect.size; self.vertices[0].texcoord = pos; @@ -2325,7 +2320,7 @@ pub const Quad = extern struct { self.vertices[3].texcoord = pos.add(size); } - pub fn setRect(self: *Quad, rect: Rect) void { + pub fn setRect(self: *VertexQuad, rect: Rect) void { const pos = rect.pos; const size = rect.size; self.vertices[0].position = pos; @@ -2335,6 +2330,39 @@ pub const Quad = extern struct { } }; +pub const Quad = struct { + positions: [4]Vec2, + + pub fn init(positions: [4]Vec2) Quad { + return Quad{ + .positions = positions + }; + } + + pub fn initRect(pos: Vec2, size: Vec2) Quad { + return Quad{ + .positions = .{ + pos, + pos.add(.init(size.x, 0)), + pos.add(.init(0, size.y)), + pos.add(size), + } + }; + } + + pub fn rotate(self: *Quad, rad: f32, origin: Vec2) void { + for (&self.positions) |*pos| { + pos.* = pos.sub(origin).rotate(rad).add(origin); + } + } + + pub fn offset(self: *Quad, vec2: Vec2) void { + for (&self.positions) |*pos| { + pos.* = pos.add(vec2); + } + } +}; + pub const TransformFrame = struct { offset: Vec2, scale: Vec2,