From b80a356739ab3b8d70134f49e6c3ec5232af0aa4 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Sun, 29 Dec 2024 03:03:00 +0200 Subject: [PATCH] add background blur --- gui/app.zig | 442 +++++++++++++++++++++++++++++++++++++++++++++++++++ gui/base.vsh | 24 +++ gui/blur.fsh | 45 ++++++ gui/main.zig | 150 +++++++++-------- 4 files changed, 597 insertions(+), 64 deletions(-) create mode 100644 gui/app.zig create mode 100644 gui/base.vsh create mode 100644 gui/blur.fsh diff --git a/gui/app.zig b/gui/app.zig new file mode 100644 index 0000000..e22046c --- /dev/null +++ b/gui/app.zig @@ -0,0 +1,442 @@ +// zig fmt: off +const std = @import("std"); +const Api = @import("artifacts-api"); +const Artificer = @import("artificer"); +const UI = @import("./ui.zig"); +const UIStack = @import("./ui_stack.zig"); +const RectUtils = @import("./rect_utils.zig"); +const rl = @import("raylib"); +const srcery = @import("./srcery.zig"); + +const assert = std.debug.assert; + +const App = @This(); + +const MapTexture = struct { + name: Api.Map.Skin, + texture: rl.Texture2D, +}; + +ui: UI, +artificer: *Artificer, +map_textures: std.ArrayList(MapTexture), +map_texture_indexes: std.ArrayList(usize), +map_position_min: Api.Position, +map_position_max: Api.Position, +camera: rl.Camera2D, + +blur_texture_original: ?rl.RenderTexture = null, +blur_texture_horizontal: ?rl.RenderTexture = null, +blur_texture_both: ?rl.RenderTexture = null, +blur_shader: rl.Shader, + +pub fn init(allocator: std.mem.Allocator, artificer: *Artificer) !App { + const store = artificer.server.store; + + var map_textures = std.ArrayList(MapTexture).init(allocator); + errdefer map_textures.deinit(); + errdefer { + for (map_textures.items) |map_texture| { + map_texture.texture.unload(); + } + } + + var map_texture_indexes = std.ArrayList(usize).init(allocator); + errdefer map_texture_indexes.deinit(); + + // Load all map textures from api store + { + const map_image_ids = store.images.category_mapping.get(.map).items; + try map_textures.ensureTotalCapacity(map_image_ids.len); + + for (map_image_ids) |image_id| { + const image = store.images.get(image_id).?; + const texture = rl.loadTextureFromImage(rl.Image{ + .width = @intCast(image.width), + .height = @intCast(image.height), + .data = store.images.getRGBA(image_id).?.ptr, + .mipmaps = 1, + .format = rl.PixelFormat.pixelformat_uncompressed_r8g8b8a8 + }); + if (!rl.isTextureReady(texture)) { + return error.LoadMapTextureFromImage; + } + + map_textures.appendAssumeCapacity(MapTexture{ + .name = try Api.Map.Skin.fromSlice(image.code.slice()), + .texture = texture + }); + } + } + + var map_position_max = Api.Position.zero(); + var map_position_min = Api.Position.zero(); + for (store.maps.items) |map| { + map_position_min.x = @min(map_position_min.x, map.position.x); + map_position_min.y = @min(map_position_min.y, map.position.y); + + map_position_max.x = @max(map_position_max.x, map.position.x); + map_position_max.y = @max(map_position_max.y, map.position.y); + } + const map_size = map_position_max.subtract(map_position_min); + + try map_texture_indexes.ensureTotalCapacity(@intCast(map_size.x * map_size.y)); + + for (0..@intCast(map_size.y)) |oy| { + for (0..@intCast(map_size.x)) |ox| { + const x = map_position_min.x + @as(i64, @intCast(ox)); + const y = map_position_min.y + @as(i64, @intCast(oy)); + const map = store.getMap(.{ .x = x, .y = y }).?; + + var found_texture = false; + const map_skin = map.skin.slice(); + for (0.., map_textures.items) |i, map_texture| { + if (std.mem.eql(u8, map_skin, map_texture.name.slice())) { + map_texture_indexes.appendAssumeCapacity(i); + found_texture = true; + break; + } + } + + if (!found_texture) { + return error.MapImageNotFound; + } + } + } + + const blur_shader = rl.loadShaderFromMemory( + @embedFile("./base.vsh"), + @embedFile("./blur.fsh"), + ); + if (!rl.isShaderReady(blur_shader)) { + return error.LoadShaderFromMemory; + } + + return App{ + .artificer = artificer, + .ui = UI.init(), + .map_textures = map_textures, + .map_texture_indexes = map_texture_indexes, + .map_position_max = map_position_max, + .map_position_min = map_position_min, + .blur_shader = blur_shader, + .camera = rl.Camera2D{ + .offset = rl.Vector2.zero(), + .target = rl.Vector2.zero(), + .rotation = 0, + .zoom = 1, + } + }; +} + +pub fn deinit(self: *App) void { + for (self.map_textures.items) |map_texture| { + map_texture.texture.unload(); + } + + self.ui.deinit(); + self.map_textures.deinit(); + self.map_texture_indexes.deinit(); + + if (self.blur_texture_horizontal) |render_texture| { + render_texture.unload(); + } + if (self.blur_texture_both) |render_texture| { + render_texture.unload(); + } + if (self.blur_texture_original) |render_texture| { + render_texture.unload(); + } +} + +fn cameraControls(camera: *rl.Camera2D) void { + if (rl.isMouseButtonDown(.mouse_button_left)) { + const mouse_delta = rl.getMouseDelta(); + camera.target.x -= mouse_delta.x / camera.zoom; + camera.target.y -= mouse_delta.y / camera.zoom; + } + + const zoom_speed = 0.2; + const min_zoom = 0.1; + const max_zoom = 2; + const zoom_delta = rl.getMouseWheelMove(); + if (zoom_delta != 0) { + const mouse_screen = rl.getMousePosition(); + + // Get the world point that is under the mouse + const mouse_world = rl.getScreenToWorld2D(mouse_screen, camera.*); + + // Set the offset to where the mouse is + camera.offset = mouse_screen; + + // Set the target to match, so that the camera maps the world space point + // under the cursor to the screen space point under the cursor at any zoom + camera.target = mouse_world; + + // Zoom increment + camera.zoom *= (1 + zoom_delta * zoom_speed); + camera.zoom = std.math.clamp(camera.zoom, min_zoom, max_zoom); + } +} + +// fn drawCharacterInfo(ui: *UI, rect: rl.Rectangle, artificer: *Artificer, brain: *Artificer.Brain) !void { +// var buffer: [256]u8 = undefined; +// +// const name_height = 20; +// UI.drawTextCentered(ui.font, brain.name, .{ .x = RectUtils.center(rect).x, .y = rect.y + name_height / 2 }, 20, 2, srcery.bright_white); +// +// var label_stack = UIStack.init(RectUtils.shrinkTop(rect, name_height + 4), .top_to_bottom); +// label_stack.gap = 4; +// +// const now = std.time.milliTimestamp(); +// const cooldown = brain.cooldown(&artificer.server); +// const seconds_left: f32 = @as(f32, @floatFromInt(@max(cooldown - now, 0))) / std.time.ms_per_s; +// const cooldown_label = try std.fmt.bufPrint(&buffer, "Cooldown: {d:.3}", .{seconds_left}); +// UI.drawTextEx(ui.font, cooldown_label, RectUtils.topLeft(label_stack.next(16)), 16, 2, srcery.bright_white); +// +// var task_label: []u8 = undefined; +// if (brain.task) |task| { +// task_label = try std.fmt.bufPrint(&buffer, "Task: {s}", .{@tagName(task)}); +// } else { +// task_label = try std.fmt.bufPrint(&buffer, "Task: -", .{}); +// } +// UI.drawTextEx(ui.font, task_label, RectUtils.topLeft(label_stack.next(16)), 16, 2, srcery.bright_white); +// +// const actions_label = try std.fmt.bufPrint(&buffer, "Actions: {}", .{brain.action_queue.items.len}); +// UI.drawTextEx(ui.font, actions_label, RectUtils.topLeft(label_stack.next(16)), 16, 2, srcery.bright_white); +// } + +fn createOrGetRenderTexture(maybe_render_texture: *?rl.RenderTexture) !rl.RenderTexture { + const screen_width = rl.getScreenWidth(); + const screen_height = rl.getScreenHeight(); + + if (maybe_render_texture.*) |render_texture| { + if (render_texture.texture.width != screen_width or render_texture.texture.height != screen_height) { + render_texture.unload(); + maybe_render_texture.* = null; + } + } + + if (maybe_render_texture.* == null) { + const render_texture = rl.loadRenderTexture(screen_width, screen_height); + if (!rl.isRenderTextureReady(render_texture)) { + return error.LoadRenderTexture; + } + + maybe_render_texture.* = render_texture; + } + + return maybe_render_texture.*.?; +} + +fn drawRectangleRoundedUV(rect: rl.Rectangle, roundness: f32, color: rl.Color) void { + if (roundness == 0) { + rl.drawRectangleRec(rect, color); + } +} + +fn drawBlurredWorld(self: *App, rect: rl.Rectangle, roundness: f32, color: rl.Color) !void { + const blur_both = try createOrGetRenderTexture(&self.blur_texture_both); + + const previous_texture = rl.getShapesTexture(); + const previous_rect = rl.getShapesTextureRectangle(); + defer rl.setShapesTexture(previous_texture, previous_rect); + + const texture_height: f32 = @floatFromInt(blur_both.texture.height); + const shape_rect = rl.Rectangle{ + .x = rect.x, + .y = texture_height - rect.y, + .width = rect.width, + .height = -rect.height, + }; + rl.setShapesTexture(blur_both.texture, shape_rect); + drawRectangleRoundedUV(rect, roundness, 0, color); +} + +pub fn drawWorld(self: *App) !void { + const screen_width = rl.getScreenWidth(); + const screen_height = rl.getScreenHeight(); + const screen_size = rl.Vector2.init(@floatFromInt(screen_width), @floatFromInt(screen_height)); + + const blur_original = try createOrGetRenderTexture(&self.blur_texture_original); + const blur_horizontal = try createOrGetRenderTexture(&self.blur_texture_horizontal); + const blur_both = try createOrGetRenderTexture(&self.blur_texture_both); + + // 1 pass. Draw the all of the sprites + { + blur_original.begin(); + defer blur_original.end(); + + rl.clearBackground(rl.Color.black.alpha(0)); + + self.camera.begin(); + defer self.camera.end(); + + rl.drawCircleV(rl.Vector2.zero(), 5, rl.Color.red); + + const map_size = self.map_position_max.subtract(self.map_position_min); + for (0..@intCast(map_size.y)) |oy| { + for (0..@intCast(map_size.x)) |ox| { + const map_index = @as(usize, @intCast(map_size.x)) * oy + ox; + const x = self.map_position_min.x + @as(i64, @intCast(ox)); + const y = self.map_position_min.y + @as(i64, @intCast(oy)); + const texture_index = self.map_texture_indexes.items[map_index]; + const texture = self.map_textures.items[texture_index].texture; + + const tile_size = rl.Vector2.init(224, 224); + const position = rl.Vector2.init(@floatFromInt(x), @floatFromInt(y)).multiply(tile_size); + rl.drawTextureV(texture, position, rl.Color.white); + } + } + } + + // 2 pass. Apply horizontal blur + const kernel_radius: i32 = 16; + var kernel_coeffs: [kernel_radius * 2 + 1]f32 = undefined; + { + const sigma = 10; + + for (0..kernel_coeffs.len) |i| { + const i_i32: i32 = @intCast(i); + const io: f32 = @floatFromInt(i_i32 - kernel_radius); + kernel_coeffs[i] = @exp(-(io * io) / (sigma * sigma)); + // kernel_coeffs[i] /= @floatFromInt(kernel_coeffs.len); + } + + var kernel_sum: f32 = 0; + for (kernel_coeffs) |coeff| { + kernel_sum += coeff; + } + + for (&kernel_coeffs) |*coeff| { + coeff.* /= kernel_sum; + } + } + + { + const texture_size_loc = rl.getShaderLocation(self.blur_shader, "textureSize"); + assert(texture_size_loc != -1); + + rl.setShaderValue(self.blur_shader, texture_size_loc, &screen_size, .shader_uniform_vec2); + } + + { + const kernel_radius_loc = rl.getShaderLocation(self.blur_shader, "kernelRadius"); + assert(kernel_radius_loc != -1); + + rl.setShaderValue(self.blur_shader, kernel_radius_loc, &kernel_radius, .shader_uniform_int); + } + + { + const coeffs_loc = rl.getShaderLocation(self.blur_shader, "coeffs"); + assert(coeffs_loc != -1); + + rl.setShaderValueV(self.blur_shader, coeffs_loc, &kernel_coeffs, .shader_uniform_float, kernel_coeffs.len); + } + + { + const kernel_direction_loc = rl.getShaderLocation(self.blur_shader, "kernelDirection"); + assert(kernel_direction_loc != -1); + + const kernel_direction = rl.Vector2.init(1, 0); + rl.setShaderValue(self.blur_shader, kernel_direction_loc, &kernel_direction, .shader_uniform_vec2); + } + + { + blur_horizontal.begin(); + defer blur_horizontal.end(); + + rl.clearBackground(rl.Color.black.alpha(0)); + + self.blur_shader.activate(); + defer self.blur_shader.deactivate(); + + rl.drawTextureRec( + blur_original.texture, + .{ + .x = 0, + .y = 0, + .width = screen_size.x, + .height = -screen_size.y, + }, + rl.Vector2.zero(), + rl.Color.white + ); + } + + // 3 pass. Apply vertical blur + { + const kernel_direction_loc = rl.getShaderLocation(self.blur_shader, "kernelDirection"); + assert(kernel_direction_loc != -1); + + const kernel_direction = rl.Vector2.init(0, 1); + rl.setShaderValue(self.blur_shader, kernel_direction_loc, &kernel_direction, .shader_uniform_vec2); + } + + { + blur_both.begin(); + defer blur_both.end(); + + self.blur_shader.activate(); + defer self.blur_shader.deactivate(); + + rl.drawTextureRec( + blur_horizontal.texture, + .{ + .x = 0, + .y = 0, + .width = screen_size.x, + .height = -screen_size.y, + }, + rl.Vector2.zero(), + rl.Color.white + ); + } + + // Last thing, draw world without blur + rl.drawTextureRec( + blur_original.texture, + .{ + .x = 0, + .y = 0, + .width = @floatFromInt(blur_original.texture.width), + .height = @floatFromInt(-blur_original.texture.height), + }, + rl.Vector2.zero(), + rl.Color.white + ); +} + +pub fn tick(self: *App) !void { + try self.artificer.tick(); + + const screen_width = rl.getScreenWidth(); + const screen_height = rl.getScreenHeight(); + const screen_size = rl.Vector2.init(@floatFromInt(screen_width), @floatFromInt(screen_height)); + + cameraControls(&self.camera); + + rl.beginDrawing(); + defer rl.endDrawing(); + + rl.clearBackground(srcery.black); + + try self.drawWorld(); + + try self.drawBlurredWorld( + .{ .x = 20, .y = 20, .width = 200, .height = 200 }, + 0.5, + rl.Color.gray + ); + + rl.drawFPS( + @as(i32, @intFromFloat(screen_size.x)) - 100, + @as(i32, @intFromFloat(screen_size.y)) - 24 + ); + + // var info_stack = UIStack.init(rl.Rectangle.init(0, 0, screen_size.x, screen_size.y), .left_to_right); + // for (artificer.characters.items) |*brain| { + // const info_width = screen_size.x / @as(f32, @floatFromInt(artificer.characters.items.len)); + // try drawCharacterInfo(&ui, info_stack.next(info_width), &artificer, brain); + // } +} diff --git a/gui/base.vsh b/gui/base.vsh new file mode 100644 index 0000000..dd14387 --- /dev/null +++ b/gui/base.vsh @@ -0,0 +1,24 @@ +#version 330 + +// Input vertex attributes +in vec3 vertexPosition; +in vec2 vertexTexCoord; +in vec3 vertexNormal; +in vec4 vertexColor; + +// Input uniform values +uniform mat4 mvp; + +// Output vertex attributes (to fragment shader) +out vec2 fragTexCoord; +out vec4 fragColor; + +void main() +{ + // Send vertex attributes to fragment shader + fragTexCoord = vertexTexCoord; + fragColor = vertexColor; + + // Calculate final vertex position + gl_Position = mvp * vec4(vertexPosition, 1.0); +} diff --git a/gui/blur.fsh b/gui/blur.fsh new file mode 100644 index 0000000..3724b7f --- /dev/null +++ b/gui/blur.fsh @@ -0,0 +1,45 @@ +#version 330 + +#define MAX_KERNEL_RADIUS 16 +#define MAX_KERNEL_COEFFS 2*MAX_KERNEL_RADIUS + 1 + +// Input vertex attributes (from vertex shader) +in vec2 fragTexCoord; +in vec4 fragColor; + +// Input uniform values +uniform sampler2D texture0; +uniform vec4 colDiffuse; + +uniform vec2 textureSize; + +uniform float coeffs[MAX_KERNEL_COEFFS]; +uniform int kernelRadius; +uniform vec2 kernelDirection; + +// Output fragment color +out vec4 finalColor; + +void main() +{ + vec2 texel = 1.0 / textureSize; + + vec4 texelColor = vec4(0); + float alphaCorrection = 0; + + for (int i = 0; i < 2*kernelRadius + 1; i++) + { + vec2 offset = kernelDirection * vec2(i - kernelRadius, i - kernelRadius) * texel; + vec2 sampleCoord = fragTexCoord + offset; + + if ((0 <= sampleCoord.x && sampleCoord.x <= 1) && (0 <= sampleCoord.y && sampleCoord.y <= 1)) { + vec4 sample = texture(texture0, sampleCoord); + texelColor += sample * coeffs[i]; + alphaCorrection += sample.a * coeffs[i]; + } + } + + texelColor /= alphaCorrection; + + finalColor = texelColor * colDiffuse * fragColor; +} diff --git a/gui/main.zig b/gui/main.zig index 8065c25..9e8a21d 100644 --- a/gui/main.zig +++ b/gui/main.zig @@ -1,15 +1,65 @@ +// zig fmt: off const std = @import("std"); const Artificer = @import("artificer"); +const Api = @import("artifacts-api"); const rl = @import("raylib"); -const Allocator = std.mem.Allocator; +const raylib_h = @cImport({ + @cInclude("stdio.h"); + @cInclude("raylib.h"); +}); +const App = @import("./app.zig"); -const srcery = @import("./srcery.zig"); +pub const std_options = .{ + .log_scope_levels = &[_]std.log.ScopeLevel{ + .{ .scope = .api, .level = .info }, + .{ .scope = .raylib, .level = .warn }, + } +}; -const UI = @import("./ui.zig"); -const UIStack = @import("./ui_stack.zig"); -const RectUtils = @import("./rect_utils.zig"); +fn toRaylibTraceLogLevel(log_level: std.log.Level) rl.TraceLogLevel { + return switch (log_level) { + .err => rl.TraceLogLevel.log_error, + .warn => rl.TraceLogLevel.log_warning, + .info => rl.TraceLogLevel.log_info, + .debug => rl.TraceLogLevel.log_trace, + }; +} -fn getAPITokenFromArgs(allocator: Allocator) !?[]u8 { +fn toZigLogLevel(log_type: c_int) ?std.log.Level { + return switch (log_type) { + @intFromEnum(rl.TraceLogLevel.log_trace) => std.log.Level.debug, + @intFromEnum(rl.TraceLogLevel.log_debug) => std.log.Level.debug, + @intFromEnum(rl.TraceLogLevel.log_info) => std.log.Level.info, + @intFromEnum(rl.TraceLogLevel.log_warning) => std.log.Level.warn, + @intFromEnum(rl.TraceLogLevel.log_error) => std.log.Level.err, + @intFromEnum(rl.TraceLogLevel.log_fatal) => std.log.Level.err, + else => null, + }; +} + +fn raylibTraceLogCallback(logType: c_int, text: [*c]const u8, args: [*c]raylib_h.struct___va_list_tag_1) callconv(.C) void { + const log_level = toZigLogLevel(logType) orelse return; + + const scope = .raylib; + const raylib_log = std.log.scoped(scope); + + const max_tracelog_msg_length = 256; // from utils.c in raylib + var buffer: [max_tracelog_msg_length:0]u8 = undefined; + + inline for (std.meta.fields(std.log.Level)) |field| { + const message_level: std.log.Level = @enumFromInt(field.value); + if (std.log.logEnabled(message_level, scope) and log_level == message_level) { + @memset(&buffer, 0); + const text_length = raylib_h.vsnprintf(&buffer, buffer.len, text, args); + const formatted_text = buffer[0..@intCast(text_length)]; + + const log_function = @field(raylib_log, field.name); + @call(.auto, log_function, .{ "{s}", .{formatted_text} }); + } + } +} + +fn getAPITokenFromArgs(allocator: std.mem.Allocator) !?[]u8 { const args = try std.process.argsAlloc(allocator); defer std.process.argsFree(allocator, args); @@ -22,37 +72,7 @@ fn getAPITokenFromArgs(allocator: Allocator) !?[]u8 { var token_buffer: [256]u8 = undefined; const token = try cwd.readFile(filename, &token_buffer); - return try allocator.dupe(u8, std.mem.trim(u8,token,"\n\t ")); -} - -fn drawCharacterInfo(ui: *UI, rect: rl.Rectangle, artificer: *Artificer, brain: *Artificer.Brain) !void { - var buffer: [256]u8 = undefined; - - const name_height = 20; - UI.drawTextCentered(ui.font, brain.name, .{ - .x = RectUtils.center(rect).x, - .y = rect.y + name_height/2 - }, 20, 2, srcery.bright_white); - - var label_stack = UIStack.init(RectUtils.shrinkTop(rect, name_height + 4), .top_to_bottom); - label_stack.gap = 4; - - const now = std.time.milliTimestamp(); - const cooldown = brain.cooldown(&artificer.server); - const seconds_left: f32 = @as(f32, @floatFromInt(@max(cooldown - now, 0))) / std.time.ms_per_s; - const cooldown_label = try std.fmt.bufPrint(&buffer, "Cooldown: {d:.3}", .{ seconds_left }); - UI.drawTextEx(ui.font, cooldown_label, RectUtils.topLeft(label_stack.next(16)), 16, 2, srcery.bright_white); - - var task_label: []u8 = undefined; - if (brain.task) |task| { - task_label = try std.fmt.bufPrint(&buffer, "Task: {s}", .{ @tagName(task) }); - } else { - task_label = try std.fmt.bufPrint(&buffer, "Task: -", .{ }); - } - UI.drawTextEx(ui.font, task_label, RectUtils.topLeft(label_stack.next(16)), 16, 2, srcery.bright_white); - - const actions_label = try std.fmt.bufPrint(&buffer, "Actions: {}", .{ brain.action_queue.items.len }); - UI.drawTextEx(ui.font, actions_label, RectUtils.topLeft(label_stack.next(16)), 16, 2, srcery.bright_white); + return try allocator.dupe(u8, std.mem.trim(u8, token, "\n\t ")); } pub fn main() anyerror!void { @@ -60,45 +80,47 @@ pub fn main() anyerror!void { defer _ = gpa.deinit(); const allocator = gpa.allocator(); + raylib_h.SetTraceLogCallback(raylibTraceLogCallback); + rl.setTraceLogLevel(toRaylibTraceLogLevel(std.log.default_level)); + const token = (try getAPITokenFromArgs(allocator)) orelse return error.MissingToken; defer allocator.free(token); - var artificer = try Artificer.init(allocator, token); + var store = try Api.Store.init(allocator); + defer store.deinit(allocator); + + var server = try Api.Server.init(allocator, &store); + defer server.deinit(); + + try server.setToken(token); + + var artificer = try Artificer.init(allocator, &server); defer artificer.deinit(); - const cache_path = try std.fs.cwd().realpathAlloc(allocator, "api-store.bin"); - defer allocator.free(cache_path); - std.log.info("Prefetching server data", .{}); - try artificer.server.prefetchCached(cache_path); + { + const cwd_path = try std.fs.cwd().realpathAlloc(allocator, "."); + defer allocator.free(cwd_path); + + const cache_path = try std.fs.path.resolve(allocator, &.{ cwd_path, "./api-store.bin" }); + defer allocator.free(cache_path); + + try server.prefetchCached(allocator, cache_path); + } rl.initWindow(800, 450, "Artificer"); defer rl.closeWindow(); - rl.setTargetFPS(60); + rl.setWindowMinSize(200, 200); + rl.setWindowState(.{ + .vsync_hint = true, + // .window_resizable = true + }); - var ui = UI.init(); - defer ui.deinit(); + var app = try App.init(allocator, &artificer); + defer app.deinit(); while (!rl.windowShouldClose()) { - if (std.time.milliTimestamp() > artificer.nextStepAt()) { - try artificer.step(); - } - - const screen_size = rl.Vector2.init( - @floatFromInt(rl.getScreenWidth()), - @floatFromInt(rl.getScreenHeight()) - ); - - rl.beginDrawing(); - defer rl.endDrawing(); - - rl.clearBackground(srcery.black); - - var info_stack = UIStack.init(rl.Rectangle.init(0, 0, screen_size.x, screen_size.y), .left_to_right); - for (artificer.characters.items) |*brain| { - const info_width = screen_size.x / @as(f32, @floatFromInt(artificer.characters.items.len)); - try drawCharacterInfo(&ui, info_stack.next(info_width), &artificer, brain); - } + try app.tick(); } }