add shop panels

This commit is contained in:
Rokas Puzonas 2026-07-18 20:30:35 +03:00
parent 29b6af5592
commit ca29401911
5 changed files with 1050 additions and 197 deletions

View File

@ -37,6 +37,7 @@ entities: EntitySlotMap,
eternal_flame_id: EntityId,
upgrades: Upgrades,
shop: Shop,
font: Gfx.Font.Id,
idle_animations: AnimationDirections,
@ -227,19 +228,29 @@ const Upgrades = struct {
minion_walk: MinionWalkStats = .{},
tree_station_count: u32 = 0,
tree_health: u32 = 10,
tree_respawn_duration: Nanoseconds = std.time.ns_per_s,
tree_wood_amount: u32 = 1,
tree_level: u32 = 1,
tree_health: u32 = tree_levels[0].health,
tree_respawn_duration: Nanoseconds = tree_levels[0].respawn_duration,
tree_wood_amount: u32 = tree_levels[0].wood_amount,
tree_wood_health: f32 = tree_levels[0].wood_health,
burn_max_stack_size: u32 = 10,
burn_speed: f32 = 10,
burn_wood_health: f32 = 20,
burn_heat_per_wood: u32 = 1,
burn_max_stack_size: u32 = 10,
burn_levels: [2]BurnLevel = .{
.{ .threshold = 10, .multiplier = 2 },
.{ .threshold = 20, .multiplier = 3 },
},
const tree_levels = [_]TreeLevel{
TreeLevel{
.health = 10,
.respawn_duration = std.time.ns_per_s,
.wood_amount = 1,
.wood_health = 20,
}
};
const MinionWalkStats = struct {
friction: f32 = 0.998,
max_velocity: f32 = 50,
@ -250,6 +261,133 @@ const Upgrades = struct {
threshold: u32,
multiplier: u32,
};
const TreeLevel = struct {
health: u32,
respawn_duration: Nanoseconds,
wood_amount: u32,
wood_health: f32,
};
};
const Shop = struct {
minion_upgrades: []Item,
tree_upgrades: []Item,
fire_upgrades: []Item,
const Item = struct {
label: []const u8,
value_ref: ValueRef,
value_scaling: Scaling,
cost: u32,
cost_scaling: Scaling,
unlocked: bool = false,
limit: ?f32 = null,
const ValueType = enum {
u32,
f32,
};
const ValueRef = union(ValueType) {
u32: *u32,
f32: *f32,
pub fn initU32(value: *u32) ValueRef {
return ValueRef{
.u32 = value
};
}
pub fn initF32(value: *f32) ValueRef {
return ValueRef{
.f32 = value
};
}
pub fn deref(self: ValueRef) Value {
return switch(self) {
.u32 => |value| Value{ .u32 = value.* },
.f32 => |value| Value{ .f32 = value.* },
};
}
pub fn set(self: *ValueRef, new_value: Value) void {
assert(@as(ValueType, self.*) == @as(ValueType, new_value));
switch (new_value) {
.u32 => |value| {
self.u32.* = value;
},
.f32 => |value| {
self.f32.* = value;
}
}
}
};
const Value = union(ValueType) {
u32: u32,
f32: f32,
pub fn format(self: Value, writer: *std.Io.Writer) std.Io.Writer.Error!void {
switch (self) {
.u32 => |v| try writer.print("{}", .{ v }),
.f32 => |v| try writer.print("{:.2}", .{ v }),
}
}
};
const Scaling = struct {
fixed: f32,
linear: f32,
quadratic: f32,
pub fn initFixed(fixed: f32) Scaling {
return Scaling{
.fixed = fixed,
.linear = 1,
.quadratic = 0,
};
}
pub fn initLinear(fixed: f32, linear: f32) Scaling {
return Scaling{
.fixed = fixed,
.linear = linear,
.quadratic = 0,
};
}
pub fn initQuadratic(fixed: f32, linear: f32, quadratic: f32) Scaling {
return Scaling{
.fixed = fixed,
.linear = linear,
.quadratic = quadratic,
};
}
pub fn apply(self: Scaling, value: Value) Value {
const value_f32: f32 = switch(value) {
.u32 => |v| @floatFromInt(v),
.f32 => |v| v
};
var new_value_f32: f32 = 0;
new_value_f32 += value_f32 * value_f32 * self.quadratic;
new_value_f32 += value_f32 * self.linear;
new_value_f32 += self.fixed;
return switch(value) {
.u32 => Value{ .u32 = @ceil(new_value_f32) },
.f32 => Value{ .f32 = new_value_f32 }
};
}
};
};
};
fn initImageDataFromSTB(img: STBImage) Gfx.ImageData {
@ -309,9 +447,12 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 {
.camera_pos = .init(0, 0),
.font = font,
.eternal_flame_id = undefined,
.upgrades = .{}
.upgrades = .{},
.shop = undefined,
};
try self.initShop(plt.arena);
self.spawnBurnStation(100);
self.spawnBurnStation(150);
@ -323,6 +464,95 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 {
return null;
}
fn initShop(self: *App, arena: Allocator) !void {
const minion_upgrades = try arena.dupe(Shop.Item, &.{
Shop.Item{
.label = "Minion count",
.limit = 50,
.unlocked = true,
.value_ref = .initU32(&self.upgrades.minion_count),
.value_scaling = .initFixed(1),
.cost = 1,
.cost_scaling = .initFixed(1),
},
Shop.Item{
.label = "Carry capacity",
.unlocked = true,
.value_ref = .initU32(&self.upgrades.minion_carry_capacity),
.value_scaling = .initFixed(1),
.cost = 1,
.cost_scaling = .initFixed(1),
},
});
const tree_upgrades = try arena.dupe(Shop.Item, &.{
Shop.Item{
.label = "Tree count",
.unlocked = true,
.value_ref = .initU32(&self.upgrades.tree_station_count),
.value_scaling = .initFixed(1),
.cost = 1,
.cost_scaling = .initFixed(1),
},
Shop.Item{
.label = "Tree level",
.limit = Upgrades.tree_levels.len,
.unlocked = true,
.value_ref = .initU32(&self.upgrades.tree_level),
.value_scaling = .initFixed(1),
.cost = 1,
.cost_scaling = .initFixed(1),
},
});
const fire_upgrades = try arena.dupe(Shop.Item, &.{
Shop.Item{
.label = "Burn speed",
.unlocked = true,
.value_ref = .initF32(&self.upgrades.burn_speed),
.value_scaling = .initFixed(1),
.cost = 1,
.cost_scaling = .initFixed(1),
},
Shop.Item{
.label = "Heat per wood",
.unlocked = true,
.value_ref = .initU32(&self.upgrades.burn_heat_per_wood),
.value_scaling = .initFixed(1),
.cost = 1,
.cost_scaling = .initFixed(1),
},
Shop.Item{
.label = "Max stack size",
.unlocked = true,
.value_ref = .initU32(&self.upgrades.burn_max_stack_size),
.value_scaling = .initFixed(1),
.cost = 1,
.cost_scaling = .initFixed(1),
},
});
self.shop = .{
.minion_upgrades = minion_upgrades,
.tree_upgrades = tree_upgrades,
.fire_upgrades = fire_upgrades,
};
}
fn spawnEntity(self: *App, e: Entity) ?EntityId {
return self.entities.insert(e) catch {
log.warn("Failed to spawn entity {}, entity limit reached", .{e.type});
@ -391,7 +621,7 @@ fn destroyTree(self: *App, tree_id: EntityId) void {
}
fn spawnWood(self: *App, pos: Vec2) void {
const health = self.upgrades.burn_wood_health;
const health = self.upgrades.tree_wood_health;
_ = self.spawnEntity(Entity{
.type = .wood,
@ -773,6 +1003,318 @@ fn getBurnMultiplier(self: *App) u32 {
return 1;
}
fn buyUpgrade(self: *App, shop_item: *Shop.Item) void {
if (self.heat < shop_item.cost) {
return;
}
self.heat -= shop_item.cost;
shop_item.cost = shop_item.cost_scaling.apply(.{ .u32 = shop_item.cost }).u32;
const value = shop_item.value_ref.deref();
const new_value = shop_item.value_scaling.apply(value);
shop_item.value_ref.set(new_value);
}
fn showShopPanel(
self: *App,
plt: Platform.Frame,
panel_label: []const u8,
panel_pos: Vec2,
shop_items: []Shop.Item
) !void {
var unlocked_count: f32 = 0;
for (shop_items) |*shop_item| {
if (shop_item.unlocked) {
unlocked_count += 1;
}
}
if (unlocked_count == 0) {
return;
}
const panel_size = Vec2.init(400, unlocked_count * 32 + 10);
const border_width = 5;
const border_color = Color.rgb(180, 180, 180);
Gfx.transformPush();
defer Gfx.transformPop();
Gfx.transformTranslate(panel_pos);
Gfx.transformTranslate(.init(-panel_size.x/2, -panel_size.y));
Gfx.drawRectangleOutline(.init(0, 0), panel_size, border_color, border_width, 0);
Gfx.transformTranslate(.init(0, panel_size.y));
{
const panel_label_size = 40;
const panel_label_margin = Vec2.init(15, 10);
const panel_label_width = Gfx.measureText(self.font, panel_label_size, panel_label).size.x;
Gfx.transformPush();
defer Gfx.transformPop();
Gfx.drawText(self.font, .{
.text = panel_label,
.pos = panel_label_margin.add(.init(0, 5)),
.height = panel_label_size
});
Gfx.drawRectangleOutline(
.init(0, border_width),
Vec2.init(panel_label_width, panel_label_size).add(panel_label_margin.multiplyScalar(2)),
border_color,
border_width,
0
);
}
Gfx.transformTranslate(.init(10, -5));
var index: usize = 0;
for (shop_items) |*shop_item| {
if (!shop_item.unlocked) {
continue;
}
defer index += 1;
Gfx.transformPush();
defer Gfx.transformPop();
Gfx.transformTranslate(.init(0, -32 * @as(f32, @floatFromInt(index + 1))));
const hitbox = Rect.init(-10, 0, panel_size.x, 32);
const cost = shop_item.cost;
var limit_reached = false;
if (shop_item.limit) |limit| {
limit_reached = switch (shop_item.value_ref) {
.u32 => |value| @as(f32, @floatFromInt(value.*)) >= limit,
.f32 => |value| value.* >= limit
};
}
var text_color = Color.white;
var is_mouse_hovering = false;
if (limit_reached) {
text_color = .rgb(120, 120, 120);
} else if (plt.input.mouse_position) |screen_mouse| {
const transform = Gfx.getTransform();
const world_mouse = screen_mouse.sub(transform.offset);
if (hitbox.isInside(world_mouse)) {
is_mouse_hovering = true;
}
}
if (is_mouse_hovering) {
if (self.heat >= cost) {
if (plt.input.isMousePressed(.left)) {
self.buyUpgrade(shop_item);
}
if (plt.input.isMouseDown(.left)) {
text_color = .rgb(100, 200, 100);
} else {
text_color = .rgb(180, 255, 180);
}
} else {
text_color = .rgb(255, 180, 180);
}
Gfx.drawLine(
.init(hitbox.left() + 5, hitbox.bottom() - 5),
.init(hitbox.right() - 5, hitbox.bottom() - 5),
text_color,
2
);
}
Gfx.drawText(self.font, .{
.text = shop_item.label,
.pos = .init(0, 0),
.height = 24,
.color = text_color
});
const allocPrint = std.fmt.allocPrint;
Gfx.drawText(self.font, .{
.text = try allocPrint(plt.arena, "{f}", .{shop_item.value_ref.deref()}),
.pos = .init(180, 0),
.height = 24,
.color = text_color
});
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}),
.pos = .init(240, 0),
.height = 24,
.color = text_color
});
}
Gfx.drawText(self.font, .{
.text = try std.fmt.allocPrint(plt.arena, "{}H", .{cost}),
.pos = .init(panel_size.x - 20, 0),
.height = 24,
.color = text_color,
.alignment = .init(1, 0)
});
}
}
fn showUI(self: *App, plt: Platform.Frame) !void {
var heat_pos = Vec2.init(0, -200);
{ // Draw heat counter
var builder = Gfx.TextBuilder.init(plt.arena);
builder.font(self.font, 64);
builder.color = .rgb(200, 20, 20);
try builder.put(try std.fmt.allocPrint(plt.arena, "{}", .{self.heat}));
builder.font(self.font, 32);
builder.color = .rgb(240, 100, 100);
builder.offset(.init(10, 0));
try builder.put("Heat");
builder.draw(heat_pos.sub(builder.bounds.size.divideScalar(2)));
}
try self.showShopPanel(plt, "Minion", Vec2.init(0, -600), self.shop.minion_upgrades);
try self.showShopPanel(plt, "Flame", Vec2.init(600, -600), self.shop.fire_upgrades);
try self.showShopPanel(plt, "Trees", Vec2.init(-600, -600), self.shop.tree_upgrades);
}
fn showDebugScaling(label: []const u8, scaling: *Shop.Item.Scaling) void {
ImGUI.pushID(.{ .string = label });
defer ImGUI.popID();
const field_width = 80;
ImGUI.text("{s}", .{label});
ImGUI.sameLine();
ImGUI.setNextItemWidth(field_width);
_ = ImGUI.inputF32Drag(.{ .label = "fixed", .value = &scaling.fixed });
ImGUI.sameLine();
ImGUI.setNextItemWidth(field_width);
_ = ImGUI.inputF32Drag(.{ .label = "linear", .value = &scaling.linear });
ImGUI.sameLine();
ImGUI.setNextItemWidth(field_width);
_ = ImGUI.inputF32Drag(.{ .label = "quadratic", .value = &scaling.quadratic });
}
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| {
ImGUI.pushID(.{ .int = @intCast(i) });
defer ImGUI.popID();
if (ImGUI.collapsingHeader(shop_item.label)) {
_ = ImGUI.inputU32("cost", &shop_item.cost);
_ = ImGUI.checkbox("unlocked", &shop_item.unlocked);
showDebugScaling("Cost scaling", &shop_item.cost_scaling);
showDebugScaling("Value scaling", &shop_item.value_scaling);
}
}
}
fn showDebug(self: *App, plt: Platform.Frame) !void {
_ = plt; // autofix
if (!ImGUI.beginWindow(.{ .name = "Debug", .alpha = 0.8, .size = .init(500, 200), .collapsed = true })) {
return;
}
defer ImGUI.endWindow();
if (ImGUI.beginTabBar("tab bar")) {
defer ImGUI.endTabBar();
if (ImGUI.beginTabItem("Upgrades")) {
defer ImGUI.endTabItem();
var heat: u32 = @intCast(self.heat);
if (ImGUI.inputU32("heat", &heat)) {
self.heat = @intCast(heat);
}
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.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("level", &self.upgrades.tree_level);
_ = ImGUI.inputU32("station_count", &self.upgrades.tree_station_count);
_ = ImGUI.inputU32("health", &self.upgrades.tree_health);
_ = ImGUI.inputU32("wood_amount", &self.upgrades.tree_wood_amount);
_ = ImGUI.inputF32(.{
.label = "wood_health",
.value = &self.upgrades.tree_wood_health
});
_ = 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.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);
}
}
if (ImGUI.beginTabItem("Shop")) {
defer ImGUI.endTabItem();
self.showDebugShopItems("Minion", self.shop.minion_upgrades);
ImGUI.separator();
self.showDebugShopItems("Tree", self.shop.tree_upgrades);
ImGUI.separator();
self.showDebugShopItems("Fire", self.shop.fire_upgrades);
}
}
}
pub fn frame(self: *App, plt: Platform.Frame) !void {
const input = plt.input;
_ = input; // autofix
@ -790,8 +1332,8 @@ pub fn frame(self: *App, plt: Platform.Frame) !void {
self.camera_pos = self.camera_pos.sub(plt.input.mouse_delta);
}
Gfx.viewTranslate(plt.input.window_size.x/2, plt.input.window_size.y/2);
Gfx.viewTranslate(-self.camera_pos.x, -self.camera_pos.y);
Gfx.transformTranslate(plt.input.window_size.divideScalar(2));
Gfx.transformTranslate(self.camera_pos.multiplyScalar(-1));
Gfx.setClearColor(.rgb(40, 40, 40));
Gfx.drawLine(.init(-800, 0), .init(800, 0), .white, 5);
@ -1186,80 +1728,8 @@ pub fn frame(self: *App, plt: Platform.Frame) !void {
}
}
Gfx.drawText(self.font, .{
.height = 32,
.pos = .init(-50, -200),
.text = try std.fmt.allocPrint(plt.arena, "Heat: {}", .{self.heat}),
});
if (ImGUI.beginWindow(.{ .name = "Debug", .alpha = 0.8, .size = .init(500, 200) })) {
defer ImGUI.endWindow();
var heat: u32 = @intCast(self.heat);
if (ImGUI.inputU32("heat", &heat)) {
self.heat = @intCast(heat);
}
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.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("station_count", &self.upgrades.tree_station_count);
_ = 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);
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);
}
}
try self.showUI(plt);
try self.showDebug(plt);
}
pub fn deinit(self: *App, plt: Platform.Deinit) void {

View File

@ -11,6 +11,9 @@ a: f32,
pub const black = rgb(0, 0, 0);
pub const white = rgb(255, 255, 255);
pub const purple = rgb(255, 0, 255);
pub const red = rgb(255, 0, 0);
pub const blue = rgb(0, 0, 255);
pub const green = rgb(0, 255, 0);
pub fn rgba(r: u8, g: u8, b: u8, a: f32) Color {
assert(0 <= a and a <= 1);

View File

@ -26,7 +26,6 @@ const State = struct {
frame_index: u64,
view: Mat4,
shader: sg.Shader,
pipeline: sg.Pipeline,
bindings: sg.Bindings,
@ -45,6 +44,9 @@ const State = struct {
clear_color: Color,
transforms_buffer: [32]TransformFrame,
transforms: std.ArrayList(TransformFrame) = .empty,
textures_buffer: [Texture.max_textures]Texture.SlotMap.Slot,
textures: Texture.SlotMap,
nil_texture: Texture.Id,
@ -151,7 +153,6 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void {
self.* = State{
.gpa = gpa,
.frame_index = 0,
.view = .initIdentity(),
.shader = shader,
.pipeline = pipeline,
.bindings = bindings,
@ -167,6 +168,9 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void {
.nearest_sampler = nearest_sampler,
.default_sampler = .nearest,
.transforms_buffer = undefined,
.transforms = .initBuffer(&self.transforms_buffer),
.textures_buffer = undefined,
.textures = .init(&self.textures_buffer),
.nil_texture = undefined,
@ -240,7 +244,7 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void {
var pixels: [width * height * 4]u8 = undefined;
@memset(&pixels, 0xFF);
self.default_sprite = initSprite(.{ });
self.default_sprite = initSprite(.{ .padding = 0 });
setSprite(self.default_sprite, .{
.image = ImageData{
.width = width,
@ -330,23 +334,62 @@ fn toSokolColor(color: Color) sokol.gfx.Color {
};
}
pub fn viewTranslate(x: f32, y: f32) void {
flush(.{});
fn getTransformPtr() ?*TransformFrame {
const self = &g_state;
self.view.translate(x, y, 0);
if (self.transforms.items.len == 0) {
return null;
}
return &self.transforms.items[self.transforms.items.len-1];
}
pub fn viewScale(x: f32, y: f32) void {
flush(.{});
pub fn transformTranslate(offset: Vec2) void {
const top_frame = getTransformPtr() orelse return;
top_frame.offset = top_frame.offset.add(offset.multiply(top_frame.scale));
}
pub fn transformScale(scale: Vec2) void {
const top_frame = getTransformPtr() orelse return;
top_frame.scale = top_frame.scale.multiply(scale);
}
pub fn transformPush() void {
const self = &g_state;
self.view.scale(x, y, 1);
const top_frame = getTransformPtr() orelse return;
self.transforms.appendBounded(top_frame.*) catch {
log.warn("Transform stack limit reached!", .{});
};
}
pub fn transformPop() void {
const self = &g_state;
_ = self.transforms.pop();
}
pub fn getTransform() TransformFrame {
if (getTransformPtr()) |top_frame| {
return top_frame.*;
} else {
return TransformFrame{
.offset = .init(0, 0),
.scale = .init(1, 1)
};
}
}
pub fn beginFrame() void {
const self = &g_state;
self.transforms.clearRetainingCapacity();
self.transforms.appendAssumeCapacity(TransformFrame{
.offset = .init(0, 0),
.scale = .init(1, 1),
});
var pass_action: sg.PassAction = .{};
pass_action.colors[0] = .{
.load_action = .CLEAR,
@ -358,8 +401,6 @@ pub fn beginFrame() void {
.swapchain = sglue.swapchain()
});
self.view = .initIdentity();
const default_spritesheet = self.spritesheets.getAssumeExists(self.default_spritesheet);
const spritesheet_texture = self.textures.getAssumeExists(default_spritesheet.texture);
self.bindings.views[shd.VIEW_tex] = spritesheet_texture.view;
@ -448,7 +489,7 @@ pub fn flush(opts: FlushOptions) void {
var vs_params: shd.VsParams = .{
// TODO: Maybe 'view' matrix is not needed.
// We probably should only have a combined 'mvp' matrix
.view = self.view,
.view = .initIdentity(),
.projection = createProjectionMatrix(window_size)
};
@ -480,6 +521,12 @@ pub fn endFrame() void {
while (spritesheet_iter.next()) |spritesheet_id| {
repackSpritesheetIfNeeded(spritesheet_id);
}
if (self.transforms.items.len > 1) {
log.warn("Too many calls to transformPush()", .{});
} else if (self.transforms.items.len == 0) {
log.warn("Too many calls to transformPop()", .{});
}
}
pub fn drawRectangle(pos: Vec2, size: Vec2, color: Color) void {
@ -488,6 +535,54 @@ pub fn drawRectangle(pos: Vec2, size: Vec2, color: Color) void {
drawSprite(self.default_sprite, pos, size, color);
}
pub fn drawRectangleOutline(pos: Vec2, size: Vec2, color: Color, width: f32, alignment: f32) void {
const self = &g_state;
const outer_rect = (Rect{ .pos = pos, .size = size }).grow(Vec2.init(width, width).multiplyScalar(1-alignment));
const inner_rect = (Rect{ .pos = pos, .size = size }).shrink(Vec2.init(width, width).multiplyScalar(alignment));
drawSpriteQuad(
self.default_sprite,
.{
.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(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(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(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
);
}
pub fn drawLine(from: Vec2, to: Vec2, color: Color, width: f32) void {
const self = &g_state;
@ -749,6 +844,12 @@ pub fn draw(opts: DrawOptions) void {
flush(.{});
}
if (getTransformPtr()) |transform| {
for (&quad.vertices) |*vertex| {
vertex.position = transform.apply(vertex.position);
}
}
self.quads.appendAssumeCapacity(quad);
}
@ -1066,11 +1167,17 @@ pub fn getSpriteUVRect(id: Sprite.Id) ?Rect {
assert(sprite.position != null);
const sprite_size = Vec2.initFromInt(u32, sprite_data.width, sprite_data.height);
return Rect{
var uv_rect = Rect{
.pos = sprite.position.?.divide(spritesheet.size),
.size = sprite_size.divide(spritesheet.size),
};
const texel_size = Vec2.init(1, 1).divide(spritesheet.size);
uv_rect = uv_rect.shrink(texel_size.divideScalar(8));
return uv_rect;
}
fn drawSpriteQuad(id: Sprite.Id, points: [4]Vec2, color: Color) void {
@ -1086,19 +1193,10 @@ fn drawSpriteQuad(id: Sprite.Id, points: [4]Vec2, color: Color) void {
var texture: ?Texture.Id = null;
if (id != self.nil_sprite) {
if (getSpriteUVRect(id)) |sprite_uv| {
quad.setUVRect(sprite_uv);
const sprite = self.sprites.getAssumeExists(id);
const spritesheet = self.spritesheets.getAssumeExists(sprite.spritesheet);
const texel_size = Vec2.init(1, 1).divide(spritesheet.size);
_ = texel_size; // autofix
// TODO: I hope this doesn't bite me in the ass.
// The shrink of the UV by half a texel
// This is to avoid linear sampling artifacts at edges
//
// Yes I did regret this. Fuck.
//
// quad.setUVRect(sprite_uv.shrink(texel_size.divideScalar(2)));
quad.setUVRect(sprite_uv);
texture = spritesheet.texture;
}
}
@ -1215,119 +1313,327 @@ pub fn setFont(id: Font.Id, ttf_data: [:0]const u8) void {
font.cache = .init();
}
const DrawTextOptions = struct {
text: []const u8,
pos: Vec2,
height: f32,
color: Color = .white
};
pub fn drawText(id: Font.Id, opts: DrawTextOptions) void {
fn getFont(id: Font.Id) ?*Font {
const self = &g_state;
if (id == self.nil_font) {
return;
return null;
}
const font = self.fonts.get(id) orelse {
log.warn("Attempt to draw text with font that doesn't exist: {}", .{id});
return;
};
const stb_font = font.stb orelse {
log.warn("Attempt to draw text with font that isn't set: {}", .{id});
return;
};
return self.fonts.get(id);
}
const fallback_glyph = stb_font.findGlyphIndex('?') orelse {
log.warn("Failed to find fallback glyph '?'", .{});
return;
};
pub fn getGlyphIndex(id: Font.Id, codepoint: u21) ?GlyphIndex {
const font = getFont(id) orelse return null;
const stb_font = font.stb orelse return null;
const scale = stb_font.scaleForPixelHeight(opts.height);
const scale_x = scale;
const scale_y = scale;
if (stb_font.findGlyphIndex(codepoint)) |index| {
return index;
} else {
// Fallback
return getGlyphIndex(id, '?');
}
}
pub fn getFontPixelScale(id: Font.Id, height: f32) f32 {
const font = getFont(id) orelse return 0;
const stb_font = font.stb orelse return 0;
return stb_font.scaleForPixelHeight(height);
}
pub fn getFontAscent(id: Font.Id, height: f32) f32 {
const font = getFont(id) orelse return 0;
const stb_font = font.stb orelse return 0;
const vmetrics = stb_font.getFontVMetrics();
const baseline = @as(f32, @floatFromInt(vmetrics.ascent)) * scale;
const scale = stb_font.scaleForPixelHeight(height);
return @as(f32, @floatFromInt(vmetrics.ascent)) * scale;
}
var current_point = opts.pos;
current_point.y += baseline;
pub fn getGlyph(id: Font.Id, index: GlyphIndex, scale_x: f32, scale_y: f32) ?*Font.Glyph {
const self = &g_state;
const font = getFont(id) orelse return null;
const stb_font = font.stb orelse return null;
var prev_glyph: ?u32 = null;
var cursor: usize = 0;
while (cursor < opts.text.len) {
const codepoint_len = std.unicode.utf8ByteSequenceLength(opts.text[cursor]) catch |e| {
log.err("Failed to draw text, invalid codepoint at {} in '{s}': {}", .{cursor, opts.text, e});
return;
// TODO: Allow specifying a different scale for x and y
const glyph_key = Font.Glyph.Key{
.index = index,
.scale_x = scale_x,
.scale_y = scale_y
};
var glyph: *Font.Glyph = undefined;
if (font.cache.lookup(glyph_key)) |glyph_id| {
return font.cache.get(glyph_id);
} else {
const glyph_id = font.cache.insert(glyph_key) orelse {
@panic("TODO: render glyph lru_tail");
};
const codepoint_bytes = opts.text[cursor..][0..codepoint_len];
const codepoint = std.unicode.utf8Decode(codepoint_bytes) catch |e| {
log.err("Failed to draw text, invalid codepoint at {} in '{s}': {}", .{cursor, opts.text, e});
return;
};
cursor += codepoint_len;
glyph = font.cache.get(glyph_id);
glyph.box = stb_font.getGlyphBitmapBox(glyph_key.index, glyph_key.scale_x, glyph_key.scale_y);
const glyph_index = stb_font.findGlyphIndex(codepoint) orelse fallback_glyph;
const glyph_key = Font.Glyph.Key{ .index = glyph_index, .scale_x = scale_x, .scale_y = scale_y };
const box_width: u32 = @intCast(glyph.box.x1 - glyph.box.x0);
const box_height: u32 = @intCast(glyph.box.y1 - glyph.box.y0);
if (box_width > 0 and box_height > 0) {
assert(glyph.sprite == self.nil_sprite);
glyph.sprite = initSprite(.{ .spritesheet = font.spritesheet, .padding = 1 });
var glyph: *Font.Glyph = undefined;
if (font.cache.lookup(glyph_key)) |glyph_id| {
glyph = font.cache.get(glyph_id);
} else {
const glyph_id = font.cache.insert(glyph_key) orelse {
@panic("TODO: render glyph lru_tail");
assert(box_width < Font.Glyph.max_width);
assert(box_height < Font.Glyph.max_height);
var bitmap_buffer: [Font.Glyph.max_width * Font.Glyph.max_height]u8 = undefined;
const bitmap = ImageData{
.pixels = .{ .r8 = (&bitmap_buffer).ptr },
.width = box_width,
.height = box_height
};
glyph = font.cache.get(glyph_id);
glyph.box = stb_font.getGlyphBitmapBox(glyph_key.index, glyph_key.scale_x, glyph_key.scale_y);
const width: u32 = @intCast(glyph.box.x1 - glyph.box.x0);
const height: u32 = @intCast(glyph.box.y1 - glyph.box.y0);
if (width > 0 and height > 0) {
assert(glyph.sprite == self.nil_sprite);
glyph.sprite = initSprite(.{ .spritesheet = font.spritesheet, .padding = 1 });
assert(width < Font.Glyph.max_width);
assert(height < Font.Glyph.max_height);
var bitmap_buffer: [Font.Glyph.max_width * Font.Glyph.max_height]u8 = undefined;
const bitmap = ImageData{
.pixels = .{ .r8 = (&bitmap_buffer).ptr },
.width = width,
.height = height
};
stb_font.makeGlyphBitmap(
bitmap.pixels.?.r8,
width, height,
width,
glyph_key.scale_x, glyph_key.scale_y,
glyph_key.index
);
setSprite(glyph.sprite, .{ .image = bitmap });
} else {
glyph.sprite = self.nil_sprite;
}
stb_font.makeGlyphBitmap(
bitmap.pixels.?.r8,
box_width, box_height,
box_width,
glyph_key.scale_x, glyph_key.scale_y,
glyph_key.index
);
setSprite(glyph.sprite, .{ .image = bitmap });
} else {
glyph.sprite = self.nil_sprite;
}
glyph.used_this_frame = true;
}
glyph.used_this_frame = true;
if (prev_glyph != null) {
const kerning = stb_font.getGlyphKernAdvance(prev_glyph.?, glyph_index);
current_point.x += @as(f32, @floatFromInt(kerning)) * scale;
return glyph;
}
pub const TextRunLayout = struct {
font: Font.Id,
scale_x: f32,
scale_y: f32,
prev_glyph: ?GlyphIndex,
pen_pos: Vec2,
const GlyphRect = struct {
rect: Rect,
sprite: Sprite.Id,
};
pub fn init(font: Font.Id, height: f32) TextRunLayout {
const scale = getFontPixelScale(font, height);
return TextRunLayout{
.font = font,
.scale_x = scale,
.scale_y = scale,
.prev_glyph = null,
.pen_pos = .init(0, 0)
};
}
pub fn next(self: *TextRunLayout, glyph_index: GlyphIndex) ?GlyphRect {
const glyph = getGlyph(self.font, glyph_index, self.scale_x, self.scale_y) orelse return null;
const font = getFont(self.font) orelse return null;
const stb_font = font.stb orelse return null;
if (self.prev_glyph) |prev_glyph| {
const kerning = stb_font.getGlyphKernAdvance(prev_glyph, glyph_index);
self.pen_pos.x += @as(f32, @floatFromInt(kerning)) * self.scale_x;
}
const hmetrics = stb_font.getGlyphHMetrics(glyph_index);
const glyph_size = Vec2.initFromInt(i32, glyph.box.x1 - glyph.box.x0, glyph.box.y1 - glyph.box.y0);
if (glyph.sprite != self.nil_sprite) {
var draw_pos = current_point;
draw_pos.x -= @floatFromInt(glyph.box.x0);
draw_pos.x += @as(f32, @floatFromInt(hmetrics.left_side_bearing)) * scale;
draw_pos.y += @floatFromInt(glyph.box.y0);
drawSprite(glyph.sprite, draw_pos, glyph_size, opts.color);
var draw_pos = self.pen_pos;
draw_pos.x -= @floatFromInt(glyph.box.x0);
draw_pos.x += @as(f32, @floatFromInt(hmetrics.left_side_bearing)) * self.scale_x;
draw_pos.y += @floatFromInt(glyph.box.y0);
const draw_rect = Rect{
.pos = draw_pos,
.size = glyph_size
};
self.pen_pos.x += @as(f32, @floatFromInt(hmetrics.advance_width)) * self.scale_x;
self.prev_glyph = glyph_index;
return GlyphRect{
.rect = draw_rect,
.sprite = glyph.sprite
};
}
};
pub fn measureText(id: Font.Id, height: f32, text: []const u8) Rect {
var bounds: Rect = .init(0, 0, 0, 0);
var first_glyph = false;
var layout = TextRunLayout.init(id, height);
var iter = std.unicode.Utf8Iterator{
.bytes = text,
.i = 0
};
while (iter.nextCodepoint()) |codepoint| {
const glyph_index = getGlyphIndex(id, codepoint) orelse continue;
const glyph_layout = layout.next(glyph_index) orelse continue;
if (first_glyph) {
bounds = glyph_layout.rect;
first_glyph = false;
} else {
bounds = bounds.expand(glyph_layout.rect);
}
}
return bounds;
}
pub const DrawTextOptions = struct {
text: []const u8,
pos: Vec2,
height: f32,
color: Color = .white,
alignment: Vec2 = .init(0, 0)
};
pub fn drawText(id: Font.Id, opts: DrawTextOptions) void {
const self = &g_state;
var layout = TextRunLayout.init(id, opts.height);
var pos = opts.pos;
pos.y += getFontAscent(id, opts.height);
if (!opts.alignment.eql(.zero)) {
const bounds = measureText(id, opts.height, opts.text);
pos = pos.sub(bounds.size.multiply(opts.alignment));
}
var iter = std.unicode.Utf8Iterator{
.bytes = opts.text,
.i = 0
};
while (iter.nextCodepoint()) |codepoint| {
const glyph_index = getGlyphIndex(id, codepoint) orelse continue;
const glyph_layout = layout.next(glyph_index) orelse continue;
if (glyph_layout.sprite == self.nil_sprite) {
continue;
}
current_point.x += @as(f32, @floatFromInt(hmetrics.advance_width)) * scale;
prev_glyph = glyph_index;
drawSprite(
glyph_layout.sprite,
glyph_layout.rect.pos.add(pos),
glyph_layout.rect.size,
opts.color
);
}
}
pub const TextBuilder = struct {
gpa: std.mem.Allocator,
glyphs: std.ArrayList(Glyph),
color: Color,
baseline: f32,
layout: ?TextRunLayout,
bounds: Rect,
const Glyph = struct {
sprite: Sprite.Id,
rect: Rect,
color: Color
};
pub fn init(gpa: std.mem.Allocator) TextBuilder {
return TextBuilder{
.gpa = gpa,
.glyphs = .empty,
.color = .white,
.baseline = 0,
.layout = null,
.bounds = .init(0, 0, 0, 0)
};
}
pub fn font(self: *TextBuilder, id: Font.Id, height: f32) void {
var new_layout = TextRunLayout.init(id, height);
if (self.layout) |layout| {
if (new_layout.font == layout.font and
new_layout.scale_x == layout.scale_x and
new_layout.scale_y == layout.scale_y
) {
return;
}
}
const font_baseline = getFontAscent(id, height);
if (font_baseline > self.baseline) {
// Fixup previously drawn glyph to match the new larger baseline
const offset_y = font_baseline - self.baseline;
for (self.glyphs.items) |*glyph| {
glyph.rect.pos.y += offset_y;
}
self.bounds.pos.y += offset_y;
self.baseline = font_baseline;
}
if (self.layout) |layout| {
new_layout.pen_pos = layout.pen_pos;
if (layout.font == new_layout.font) {
new_layout.prev_glyph = layout.prev_glyph;
}
}
self.layout = new_layout;
}
pub fn putCodepoint(self: *TextBuilder, codepoint: u21) !void {
if (self.layout == null) {
return;
}
const layout = &self.layout.?;
const glyph_index = getGlyphIndex(layout.font, codepoint) orelse return;
const glyph_layout = layout.next(glyph_index) orelse return;
var rect = glyph_layout.rect;
rect.pos.y += self.baseline;
try self.glyphs.append(self.gpa, Glyph{
.sprite = glyph_layout.sprite,
.rect = rect,
.color = self.color
});
if (self.bounds.size.x > 0 and self.bounds.size.y > 0) {
self.bounds = self.bounds.expand(rect);
} else {
self.bounds = rect;
}
}
pub fn put(self: *TextBuilder, text: []const u8) !void {
var iter = std.unicode.Utf8Iterator{ .bytes = text, .i = 0 };
while (iter.nextCodepoint()) |codepoint| {
try self.putCodepoint(codepoint);
}
}
pub fn draw(self: *TextBuilder, pos: Vec2) void {
for (self.glyphs.items) |glyph| {
if (glyph.sprite == g_state.nil_sprite) {
continue;
}
drawSprite(glyph.sprite, glyph.rect.pos.add(pos), glyph.rect.size, glyph.color);
}
}
pub fn offset(self: *TextBuilder, vec2: Vec2) void {
if (self.layout) |*layout| {
layout.pen_pos = layout.pen_pos.add(vec2);
}
}
};
pub const ImageData = struct {
const Format = enum {
rgba8,
@ -1947,7 +2253,10 @@ pub const Font = struct {
const max_width: u32 = 512;
const max_height: u32 = 512;
const Index = u32;
const Key = packed struct {
// TODO: Quantize `scale_x` and `scale_y`.
index: u32,
scale_x: f32,
scale_y: f32,
@ -1979,6 +2288,8 @@ pub const Font = struct {
};
};
const GlyphIndex = Font.Glyph.Index;
pub const Vertex = extern struct {
position: Vec2,
texcoord: Vec2,
@ -2023,3 +2334,12 @@ pub const Quad = extern struct {
self.vertices[3].position = pos.add(size);
}
};
pub const TransformFrame = struct {
offset: Vec2,
scale: Vec2,
pub fn apply(self: TransformFrame, position: Vec2) Vec2 {
return position.multiply(self.scale).add(self.offset);
}
};

View File

@ -460,3 +460,46 @@ pub fn setNextItemWidth(width: f32) void {
ig.igSetNextItemWidth(width);
}
pub fn beginTabBar(id: [*c]const u8) bool {
if (isDisabled()) {
return false;
}
return ig.igBeginTabBar(id, ig.ImGuiTabBarFlags_None);
}
pub fn endTabBar() void {
if (isDisabled()) {
return;
}
ig.igEndTabBar();
}
pub fn beginTabItem(label: [*c]const u8) bool {
if (isDisabled()) {
return false;
}
return ig.igBeginTabItem(label, null, ig.ImGuiTabItemFlags_None);
}
pub fn endTabItem() void {
if (isDisabled()) {
return;
}
return ig.igEndTabItem();
}
pub fn collapsingHeader(label: []const u8) bool {
if (isDisabled()) {
return false;
}
return ig.igCollapsingHeader(
formatString("{s}", .{label}),
ig.ImGuiTreeNodeFlags_None
);
}

View File

@ -404,6 +404,19 @@ pub const Rect = struct {
);
}
pub fn expand(self: Rect, other: Rect) Rect {
const left_edge = @min(self.left(), other.left());
const right_edge = @max(self.right(), other.right());
const top_edge = @min(self.top(), other.top());
const bottom_edge = @max(self.bottom(), other.bottom());
return Rect.init(
left_edge,
top_edge,
right_edge - left_edge,
bottom_edge - top_edge
);
}
pub fn left(self: Rect) f32 {
return self.pos.x;
}
@ -447,9 +460,13 @@ pub const Rect = struct {
pub fn shrink(self: Rect, margin: Vec2) Rect {
return Rect{
.pos = self.pos.add(margin),
.size = self.pos.add(margin.multiplyScalar(2))
.size = self.size.sub(margin.multiplyScalar(2))
};
}
pub fn grow(self: Rect, margin: Vec2) Rect {
return self.shrink(margin.multiplyScalar(-1));
}
};
pub const Line = struct {