render trees

This commit is contained in:
Rokas Puzonas 2026-07-19 02:19:04 +03:00
parent 892fbb38be
commit b356038f1f
2 changed files with 195 additions and 78 deletions

View File

@ -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, .{

View File

@ -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,