From 1cca8c5806c1014d04f66e2af33e2b99c2491735 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Wed, 15 Jan 2025 01:21:01 +0200 Subject: [PATCH] add switching to simulated mode in gui (freezes on exitting) --- api/schemas/position.zig | 4 + api/server.zig | 8 + gui/app.zig | 498 +++++++++++--------- gui/main.zig | 3 + gui/ui.zig | 970 ++++++++++++++++++++++++++++++++------- lib/artificer.zig | 2 +- lib/sim_clock.zig | 18 +- lib/system_clock.zig | 2 +- 8 files changed, 1120 insertions(+), 385 deletions(-) diff --git a/api/schemas/position.zig b/api/schemas/position.zig index cd56ea2..a15b7d9 100644 --- a/api/schemas/position.zig +++ b/api/schemas/position.zig @@ -20,6 +20,10 @@ pub fn subtract(self: Position, other: Position) Position { return init(self.x - other.x, self.y - other.y); } +pub fn multiply(self: Position, other: Position) Position { + return init(self.x * other.x, self.y * other.y); +} + pub fn distance(self: Position, other: Position) f32 { const dx: f32 = @floatFromInt(self.x - other.x); const dy: f32 = @floatFromInt(self.y - other.y); diff --git a/api/server.zig b/api/server.zig index 54e0b73..a502cef 100644 --- a/api/server.zig +++ b/api/server.zig @@ -459,6 +459,14 @@ pub fn prefetch(self: *Server, allocator: std.mem.Allocator, opts: PrefetchOptio _ = try self.getImage(.map, skin); } } + + inline for (std.meta.fields(Character.Skin)) |field| { + const skin: Character.Skin = @enumFromInt(field.value); + const skin_name = skin.toString(); + if (self.store.images.getId(.character, skin_name) == null) { + _ = try self.getImage(.character, skin_name); + } + } } } diff --git a/gui/app.zig b/gui/app.zig index 043e1b8..acb6165 100644 --- a/gui/app.zig +++ b/gui/app.zig @@ -12,6 +12,7 @@ const rlgl_h = @cImport({ }); const assert = std.debug.assert; +const log = std.log.scoped(.app); const App = @This(); @@ -27,6 +28,7 @@ map_textures: std.ArrayList(MapTexture), map_texture_indexes: std.ArrayList(usize), map_position_min: Api.Position, map_position_max: Api.Position, +character_skin_textures: std.EnumArray(Api.Character.Skin, rl.Texture2D), camera: rl.Camera2D, font_face: FontFace, @@ -35,7 +37,20 @@ blur_texture_horizontal: ?rl.RenderTexture = null, blur_texture_both: ?rl.RenderTexture = null, blur_shader: rl.Shader, -pub fn init(allocator: std.mem.Allocator, store: *Api.Store, server: *Api.Server) !App { +simulation: bool = false, + +system_clock: Artificer.SystemClock, +started_at: i128, +artificer: Artificer.ArtificerApi, +sim_artificer: Artificer.ArtificerSim, +sim_server: Artificer.SimServer, +sim_started_at: i128, +last_sim_timestamp: i128 = 0, + +thread_running: bool = true, +artificer_thread: std.Thread, + +pub fn init(allocator: std.mem.Allocator, store: *Api.Store, server: *Api.Server) !*App { var map_textures = std.ArrayList(MapTexture).init(allocator); errdefer map_textures.deinit(); errdefer { @@ -54,20 +69,10 @@ pub fn init(allocator: std.mem.Allocator, store: *Api.Store, server: *Api.Server 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 + .texture = try loadTextureFromStore(store, image_id) }); } } @@ -107,6 +112,15 @@ pub fn init(allocator: std.mem.Allocator, store: *Api.Store, server: *Api.Server } } + var character_skin_textures = std.EnumArray(Api.Character.Skin, rl.Texture2D).initUndefined(); + + inline for (std.meta.fields(Api.Character.Skin)) |field| { + const skin: Api.Character.Skin = @enumFromInt(field.value); + const skin_image_id = store.images.getId(.character, skin.toString()).?; + const texture = try loadTextureFromStore(store, skin_image_id); + character_skin_textures.set(skin, texture); + } + const blur_shader = rl.loadShaderFromMemory( @embedFile("./base.vsh"), @embedFile("./blur.fsh"), @@ -124,29 +138,75 @@ pub fn init(allocator: std.mem.Allocator, store: *Api.Store, server: *Api.Server return error.LoadFontFromMemory; } - return App{ + const font_face = FontFace{ .font = font }; + + const ui = UI.init(font_face); + + const character_id = store.characters.getId("Blondie").?; + + const sim_server = Artificer.SimServer.init(0, store); + + var app = try allocator.create(App); + errdefer allocator.destroy(app); + + app.* = App{ .store = store, .server = server, - .ui = UI.init(), + .system_clock = .{}, + .sim_server = sim_server, + .started_at = 0, + .sim_started_at = sim_server.clock.nanoTimestamp(), + .ui = ui, .map_textures = map_textures, .map_texture_indexes = map_texture_indexes, .map_position_max = map_position_max, .map_position_min = map_position_min, + .character_skin_textures = character_skin_textures, .blur_shader = blur_shader, - .font_face = .{ .font = font }, + .font_face = font_face, .camera = rl.Camera2D{ .offset = rl.Vector2.zero(), .target = rl.Vector2.zero(), .rotation = 0, .zoom = 1, - } + }, + + .artificer = undefined, + .sim_artificer = undefined, + .artificer_thread = undefined, }; + app.started_at = app.system_clock.nanoTimestamp(); + + app.sim_artificer = try Artificer.ArtificerSim.init(allocator, store, &app.sim_server.clock, &app.sim_server, character_id); + errdefer app.sim_artificer.deinit(allocator); + + app.artificer = try Artificer.ArtificerApi.init(allocator, store, &app.system_clock, server, character_id); + errdefer app.artificer.deinit(allocator); + + app.artificer_thread = try std.Thread.spawn(.{ .allocator = allocator }, artificer_thread_cb, .{ app }); + errdefer { + app.thread_running = false; + app.artificer_thread.join(); + } + + return app; } pub fn deinit(self: *App) void { + const allocator = self.map_textures.allocator; + + self.thread_running = false; + self.artificer_thread.join(); + + self.artificer.deinit(allocator); + self.sim_artificer.deinit(allocator); + for (self.map_textures.items) |map_texture| { map_texture.texture.unload(); } + for (self.character_skin_textures.values) |texture| { + texture.unload(); + } self.map_textures.deinit(); self.map_texture_indexes.deinit(); @@ -161,6 +221,55 @@ pub fn deinit(self: *App) void { if (self.blur_texture_original) |render_texture| { render_texture.unload(); } + + allocator.destroy(self); +} + +fn artificer_thread_cb(app: *App) void { + while (app.thread_running) { + + if (app.simulation) { + const artificer = &app.sim_artificer; + + const expires_in = artificer.timeUntilCooldownExpires(); + if (expires_in > 0) { + artificer.clock.sleep(expires_in); + } + + artificer.tick() catch |e| { + log.err("Error in .tick in thread: {}", .{e}); + app.thread_running = false; + }; + } else { + const artificer = &app.artificer; + + const expires_in = artificer.timeUntilCooldownExpires(); + if (expires_in > 0) { + artificer.clock.sleep(expires_in); + } + + artificer.tick() catch |e| { + log.err("Error in .tick in thread: {}", .{e}); + app.thread_running = false; + }; + } + } +} + +fn loadTextureFromStore(store: *Api.Store, image_id: Api.Store.Id) !rl.Texture2D { + 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; + } + + return texture; } fn cameraControls(camera: *rl.Camera2D) void { @@ -243,206 +352,10 @@ fn createOrGetRenderTexture(maybe_render_texture: *?rl.RenderTexture) !rl.Render return maybe_render_texture.*.?; } -// Modified version of `DrawRectangleRounded` where the UV texture coordiantes are consistent and align -fn drawRectangleRoundedUV(rec: rl.Rectangle, roundness: f32, color: rl.Color) void { - assert(roundness < 1); - - if (roundness <= 0 or rec.width <= 1 or rec.height <= 1) { - rl.drawRectangleRec(rec, color); - return; - } - - const radius: f32 = @min(rec.width, rec.height) * roundness / 2; - if (radius <= 0.0) return; - - // Calculate the maximum angle between segments based on the error rate (usually 0.5f) - const smooth_circle_error_rate = 0.5; - const th: f32 = std.math.acos(2 * std.math.pow(f32, 1 - smooth_circle_error_rate / radius, 2) - 1); - var segments: i32 = @intFromFloat(@ceil(2 * std.math.pi / th) / 4.0); - if (segments <= 0) segments = 4; - - const step_length = 90.0 / @as(f32, @floatFromInt(segments)); - - // Quick sketch to make sense of all of this, - // there are 9 parts to draw, also mark the 12 points we'll use - // - // P0____________________P1 - // /| |\ - // /1| 2 |3\ - // P7 /__|____________________|__\ P2 - // | |P8 P9| | - // | 8 | 9 | 4 | - // | __|____________________|__ | - // P6 \ |P11 P10| / P3 - // \7| 6 |5/ - // \|____________________|/ - // P5 P4 - - // Coordinates of the 12 points that define the rounded rect - const radius_u = radius / rec.width; - const radius_v = radius / rec.height; - const points = [_]rl.Vector2{ - .{ .x = radius_u , .y = 0 }, // P0 - .{ .x = 1 - radius_u , .y = 0 }, // P1 - .{ .x = 1 , .y = radius_v }, // P2 - .{ .x = 1 , .y = 1 - radius_v }, // P3 - .{ .x = 1 - radius_u , .y = 1 }, // P4 - .{ .x = radius_u , .y = 1 }, // P5 - .{ .x = 0 , .y = 1 - radius_v }, // P6 - .{ .x = 0 , .y = radius_v }, // P7 - .{ .x = radius_u , .y = radius_v }, // P8 - .{ .x = 1 - radius_u , .y = radius_v }, // P9 - .{ .x = 1 - radius_u , .y = 1 - radius_v }, // P10 - .{ .x = radius_u , .y = 1 - radius_v }, // P11 - }; - - const texture = rl.getShapesTexture(); - const shape_rect = rl.getShapesTextureRectangle(); - - const texture_width: f32 = @floatFromInt(texture.width); - const texture_height: f32 = @floatFromInt(texture.height); - - rl.gl.rlBegin(rlgl_h.RL_TRIANGLES); - defer rl.gl.rlEnd(); - - rl.gl.rlSetTexture(texture.id); - defer rl.gl.rlSetTexture(0); - - // Draw all of the 4 corners: [1] Upper Left Corner, [3] Upper Right Corner, [5] Lower Right Corner, [7] Lower Left Corner - const centers = [_]rl.Vector2{ points[8], points[9], points[10], points[11] }; - const angles = [_]f32{ 180.0, 270.0, 0.0, 90.0 }; - for (0..4) |k| { - var angle = angles[k]; - const center = centers[k]; - for (0..@intCast(segments)) |_| { - rl.gl.rlColor4ub(color.r, color.g, color.b, color.a); - - const rad_per_deg = std.math.rad_per_deg; - - const triangle = .{ - center, - .{ - .x = center.x + @cos(rad_per_deg*(angle + step_length))*radius_u, - .y = center.y + @sin(rad_per_deg*(angle + step_length))*radius_v - }, - .{ - .x = center.x + @cos(rad_per_deg * angle)*radius_u, - .y = center.y + @sin(rad_per_deg * angle)*radius_v - } - }; - - inline for (triangle) |point| { - rl.gl.rlTexCoord2f( - (shape_rect.x + shape_rect.width * point.x) / texture_width, - (shape_rect.y + shape_rect.height * point.y) / texture_height - ); - rl.gl.rlVertex2f( - rec.x + rec.width * point.x, - rec.y + rec.height * point.y - ); - } - - angle += step_length; - } - } - - // [2] Upper Rectangle - rl.gl.rlColor4ub(color.r, color.g, color.b, color.a); - inline for (.{ 0, 8, 9, 1, 0, 9 }) |index| { - const point = points[index]; - rl.gl.rlTexCoord2f( - (shape_rect.x + shape_rect.width * point.x) / texture_width, - (shape_rect.y + shape_rect.height * point.y) / texture_height - ); - rl.gl.rlVertex2f( - rec.x + rec.width * point.x, - rec.y + rec.height * point.y - ); - } - - // [4] Right Rectangle - rl.gl.rlColor4ub(color.r, color.g, color.b, color.a); - inline for (.{ 9, 10, 3, 2, 9, 3 }) |index| { - const point = points[index]; - rl.gl.rlTexCoord2f( - (shape_rect.x + shape_rect.width * point.x) / texture_width, - (shape_rect.y + shape_rect.height * point.y) / texture_height - ); - rl.gl.rlVertex2f( - rec.x + rec.width * point.x, - rec.y + rec.height * point.y - ); - } - - // [6] Bottom Rectangle - rl.gl.rlColor4ub(color.r, color.g, color.b, color.a); - inline for (.{ 11, 5, 4, 10, 11, 4 }) |index| { - const point = points[index]; - rl.gl.rlTexCoord2f( - (shape_rect.x + shape_rect.width * point.x) / texture_width, - (shape_rect.y + shape_rect.height * point.y) / texture_height - ); - rl.gl.rlVertex2f( - rec.x + rec.width * point.x, - rec.y + rec.height * point.y - ); - } - - // [8] Left Rectangle - rl.gl.rlColor4ub(color.r, color.g, color.b, color.a); - inline for (.{ 7, 6, 11, 8, 7, 11 }) |index| { - const point = points[index]; - rl.gl.rlTexCoord2f( - (shape_rect.x + shape_rect.width * point.x) / texture_width, - (shape_rect.y + shape_rect.height * point.y) / texture_height - ); - rl.gl.rlVertex2f( - rec.x + rec.width * point.x, - rec.y + rec.height * point.y - ); - } - - // [9] Middle Rectangle - rl.gl.rlColor4ub(color.r, color.g, color.b, color.a); - inline for (.{ 8, 11, 10, 9, 8, 10 }) |index| { - const point = points[index]; - rl.gl.rlTexCoord2f( - (shape_rect.x + shape_rect.width * point.x) / texture_width, - (shape_rect.y + shape_rect.height * point.y) / texture_height - ); - rl.gl.rlVertex2f( - rec.x + rec.width * point.x, - rec.y + rec.height * point.y - ); - } -} - -fn drawBlurredWorld(self: *App, rect: rl.Rectangle, color: rl.Color) void { - const blur_both = self.blur_texture_both.?.texture; - - const previous_texture = rl.getShapesTexture(); - const previous_rect = rl.getShapesTextureRectangle(); - defer rl.setShapesTexture(previous_texture, previous_rect); - - const texture_height: f32 = @floatFromInt(blur_both.height); - const shape_rect = rl.Rectangle{ - .x = rect.x, - .y = texture_height - rect.y, - .width = rect.width, - .height = -rect.height, - }; - rl.setShapesTexture(blur_both, shape_rect); - - const border = 2; - const roundness = 0.2; - drawRectangleRoundedUV(rect, roundness, color); - rl.drawRectangleRoundedLinesEx(RectUtils.shrink(rect, border - 1, border - 1), roundness, 0, border, srcery.bright_white.alpha(0.3)); -} - pub fn drawWorld(self: *App) void { rl.clearBackground(srcery.black); - rl.drawCircleV(rl.Vector2.zero(), 5, rl.Color.red); + const tile_size = rl.Vector2.init(224, 224); const map_size = self.map_position_max.subtract(self.map_position_min); for (0..@intCast(map_size.y)) |oy| { @@ -453,11 +366,32 @@ pub fn drawWorld(self: *App) void { 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); } } + + for (self.store.characters.objects.items) |optional_character| { + if (optional_character != .object) continue; + const character = optional_character.object; + const skin_texture = self.character_skin_textures.get(character.skin); + + const position = rl.Vector2{ + .x = @floatFromInt(character.position.x), + .y = @floatFromInt(character.position.y), + }; + + const skin_size = rl.Vector2{ + .x = @floatFromInt(skin_texture.width), + .y = @floatFromInt(skin_texture.height), + }; + + rl.drawTextureV( + skin_texture, + position.addValue(0.5).multiply(tile_size).subtract(skin_size.divide(.{ .x = 2, .y = 2 })), + rl.Color.white + ); + } } pub fn drawWorldAndBlur(self: *App) !void { @@ -490,7 +424,6 @@ pub fn drawWorldAndBlur(self: *App) !void { 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; @@ -601,11 +534,6 @@ fn drawRatelimits(self: *App, box: rl.Rectangle) void { const Category = Api.RateLimit.Category; const ratelimits = self.server.ratelimits; - self.drawBlurredWorld( - box, - srcery.xgray10 - ); - const padding = 16; var stack = UI.Stack.init(RectUtils.shrink(box, padding, padding), .top_to_bottom); stack.gap = 8; @@ -663,16 +591,92 @@ fn drawRatelimits(self: *App, box: rl.Rectangle) void { } } +fn showButton(self: *App, text: []const u8) UI.Interaction { + const key_hash: u16 = @truncate(@intFromPtr(text.ptr)); + var button = self.ui.getOrAppendWidget(key_hash); + button.padding.vertical(8); + button.padding.horizontal(16); + + button.flags.insert(.clickable); + button.size = .{ + .x = .{ .text = {} }, + .y = .{ .text = {} }, + }; + + const interaction = self.ui.getInteraction(button); + var text_color: rl.Color = undefined; + if (interaction.held_down) { + button.background = .{ .color = srcery.hard_black }; + text_color = srcery.white; + } else if (interaction.hovering) { + button.background = .{ .color = srcery.bright_black }; + text_color = srcery.bright_white; + } else { + button.background = .{ .color = srcery.black }; + text_color = srcery.bright_white; + } + + button.text = .{ + .content = text, + .color = text_color + }; + + return interaction; +} + +fn showLabel(self: *App, text: []const u8) UI.Widget.Key { + const key_hash: u16 = @truncate(@intFromPtr(text.ptr)); + var label = self.ui.getOrAppendWidget(key_hash); + + label.text = .{ + .content = text + }; + label.size = .{ + .x = .{ .text = {} }, + .y = .{ .text = {} } + }; + return label.key; +} + +pub fn toggleSimulationMode(self: *App) void { + self.simulation = !self.simulation; + + if (self.simulation) { + const system_clock = self.system_clock; + const time_passed = system_clock.nanoTimestamp() - self.started_at; + + const sim_clock = &self.sim_server.clock; + sim_clock.timestamp_limit = self.sim_started_at + time_passed; + sim_clock.timestamp = sim_clock.timestamp_limit.?; + self.last_sim_timestamp = std.time.nanoTimestamp(); + } +} + pub fn tick(self: *App) !void { for (&self.server.ratelimits.values) |*ratelimit| { ratelimit.update_timers(std.time.milliTimestamp()); } + if (self.simulation) { + const now = std.time.nanoTimestamp(); + const time_passed = now - self.last_sim_timestamp; + self.last_sim_timestamp = now; + + + const sim_clock = &self.sim_server.clock; + sim_clock.timestamp_limit.? += time_passed; + } + 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); + if (!self.ui.isHoveringAnything()) { + cameraControls(&self.camera); + self.ui.disable_mouse_interaction = rl.isMouseButtonDown(.mouse_button_left); + } else { + self.ui.disable_mouse_interaction = false; + } rl.beginDrawing(); defer rl.endDrawing(); @@ -681,9 +685,55 @@ pub fn tick(self: *App) !void { try self.drawWorldAndBlur(); - self.drawRatelimits( - .{ .x = 20, .y = 20, .width = 200, .height = 200 }, - ); + const ui = &self.ui; + if (self.blur_texture_both) |blur_texture_both| { + ui.blur_background = blur_texture_both.texture; + } + ui.begin(); + + ui.topWidget().padding.all(16); + ui.layout().gap = 8; + + { + const panel = ui.getOrAppendWidget(ui.randomWidgetHash()); + panel.size = .{ .x = .fit_children, .y = .fit_children }; + panel.padding.all(16); + panel.layout.gap = 8; + panel.background = .blur_world; + ui.pushWidget(panel.key); + defer ui.popWidget(); + + if (self.simulation) { + const clock = self.sim_server.clock; + const started_at = self.sim_started_at; + const time_passed: f32 = @floatFromInt(clock.nanoTimestamp() - started_at); + var time_passed_buff: [128]u8 = undefined; + const time_passed_str = try std.fmt.bufPrint(&time_passed_buff, "Time passed: {d:.1}s", .{ time_passed / std.time.ns_per_s }); + + _ = self.showLabel(time_passed_str); + if (self.showButton("Turn off simulation").clicked) { + self.toggleSimulationMode(); + } + } else { + const clock = self.system_clock; + const started_at = self.started_at; + const time_passed: f32 = @floatFromInt(clock.nanoTimestamp() - started_at); + var time_passed_buff: [128]u8 = undefined; + const time_passed_str = try std.fmt.bufPrint(&time_passed_buff, "Time passed: {d:.1}s", .{ time_passed / std.time.ns_per_s }); + + _ = self.showLabel(time_passed_str); + if (self.showButton("Turn on simulation").clicked) { + self.toggleSimulationMode(); + } + } + + } + + ui.end(); + + // self.drawRatelimits( + // .{ .x = 20, .y = 20, .width = 200, .height = 200 }, + // ); rl.drawFPS( @as(i32, @intFromFloat(screen_size.x)) - 100, diff --git a/gui/main.zig b/gui/main.zig index 4b2c2ab..c96e283 100644 --- a/gui/main.zig +++ b/gui/main.zig @@ -104,6 +104,9 @@ pub fn main() anyerror!void { try server.prefetchCached(allocator, cache_path, .{ .images = true }); } + const characters = try server.getMyCharacters(allocator); + characters.deinit(); + rl.initWindow(800, 450, "Artificer"); defer rl.closeWindow(); diff --git a/gui/ui.zig b/gui/ui.zig index 11d9f8d..0a368e9 100644 --- a/gui/ui.zig +++ b/gui/ui.zig @@ -1,200 +1,854 @@ +// zig fmt: off const std = @import("std"); const rl = @import("raylib"); const rect_utils = @import("./rect-utils.zig"); -const assert = std.debug.assert; -const SourceLocation = std.builtin.SourceLocation; +const FontFace = @import("./font-face.zig"); +const builtin = @import("builtin"); +const srcery = @import("./srcery.zig"); +const rlgl_h = @cImport({ + @cInclude("rlgl.h"); +}); -// TODO: Implement Id context (I.e. ID parenting) +const Rect = rl.Rectangle; +const Vec2 = rl.Vector2; +const assert = std.debug.assert; const UI = @This(); -const max_stack_depth = 16; -const TransformFrame = struct { - offset: rl.Vector2, - scale: rl.Vector2, +pub const Interaction = struct { + widget: Widget.Key, + + clicked: bool = false, + + hovering: bool = false, + pressed: bool = false, + released: bool = false, + held_down: bool = false, }; -hot_widget: ?Id = null, -active_widget: ?Id = null, +const SemanticSize = union(enum) { + pixels: f32, + percent: f32, + fit_children, + text +}; -transform_stack: std.BoundedArray(TransformFrame, max_stack_depth), +const SemanticVec2 = struct { + x: SemanticSize, + y: SemanticSize, +}; -pub fn init() UI { - var stack = std.BoundedArray(TransformFrame, max_stack_depth).init(0) catch unreachable; - stack.appendAssumeCapacity(TransformFrame{ - .offset = rl.Vector2{ .x = 0, .y = 0 }, - .scale = rl.Vector2{ .x = 1, .y = 1 }, - }); - - return UI{ - .transform_stack = stack +pub const Widget = struct { + const Flag = enum { + clickable, + passthrough }; -} -pub fn isHot(self: *const UI, id: Id) bool { - if (self.hot_widget) |hot_id| { - return hot_id.eql(id); - } - return false; -} + const Flags = std.EnumSet(Flag); -pub fn isActive(self: *const UI, id: Id) bool { - if (self.active_widget) |active_id| { - return active_id.eql(id); - } - return false; -} + pub const Key = packed struct { + hash: u16 = 0, + extra: u16 = 0, -pub fn hashSrc(src: SourceLocation) u64 { - var hash = std.hash.Fnv1a_64.init(); - hash.update(src.file); - hash.update(std.mem.asBytes(&src.line)); - hash.update(std.mem.asBytes(&src.column)); - return hash.value; -} + pub fn fromUsize(number: usize) Key { + return .{ + .hash = @truncate(number), + }; + } -fn getTopFrame(self: *UI) *TransformFrame { - assert(self.transform_stack.len >= 1); - return &self.transform_stack.buffer[self.transform_stack.len-1]; -} + pub fn eql(self: Key, other: Key) bool { + return self.hash == other.hash and self.extra == other.extra; + } -pub fn getMousePosition(self: *UI) rl.Vector2 { - const frame = self.getTopFrame(); - return rl.getMousePosition().subtract(frame.offset).divide(frame.scale); -} + pub fn indexOf(haystack: []const Key, needle: Key) ?usize { + for (0.., haystack) |i, key| { + if (key.eql(needle)) { + return i; + } + } + return null; + } + }; -pub fn getMouseDelta(self: *UI) rl.Vector2 { - const frame = self.getTopFrame(); - return rl.Vector2.multiply(rl.getMouseDelta(), frame.scale); -} + const Sides = struct { + top: f32 = 0, + bottom: f32 = 0, + left: f32 = 0, + right: f32 = 0, -pub fn getMouseWheelMove(self: *UI) f32 { - const frame = self.getTopFrame(); - return rl.getMouseWheelMove() * frame.scale.y; -} + pub fn vertical(self: *Sides, amount: f32) void { + self.top = amount; + self.bottom = amount; + } -pub fn isMouseInside(self: *UI, rect: rl.Rectangle) bool { - return rect_utils.isInsideVec2(rect, self.getMousePosition()); -} + pub fn horizontal(self: *Sides, amount: f32) void { + self.left = amount; + self.right = amount; + } -pub fn transformScale(self: *UI, x: f32, y: f32) void { - const frame = self.getTopFrame(); - frame.scale.x *= x; - frame.scale.y *= y; + pub fn all(self: *Sides, amount: f32) void { + self.top = amount; + self.bottom = amount; + self.left = amount; + self.right = amount; + } + }; - rl.gl.rlScalef(x, y, 1); -} + key: Key = .{}, + parent: Key = .{}, + flags: Flags = .{}, + layout: Layout = .{}, -pub fn transformTranslate(self: *UI, x: f32, y: f32) void { - const frame = self.getTopFrame(); - frame.offset.x += x * frame.scale.x; - frame.offset.y += y * frame.scale.y; + padding: Sides = .{}, + corner_radius: f32 = 12, - rl.gl.rlTranslatef(x, y, 0); -} + size: SemanticVec2 = .{ + .x = .{ .pixels = 0 }, + .y = .{ .pixels = 0 }, + }, -pub fn pushTransform(self: *UI) void { - rl.gl.rlPushMatrix(); - self.transform_stack.appendAssumeCapacity(self.getTopFrame().*); -} + text: ?struct { + content: []const u8, + color: rl.Color = srcery.bright_white, + } = null, -pub fn popTransform(self: *UI) void { - assert(self.transform_stack.len >= 2); - rl.gl.rlPopMatrix(); - _ = self.transform_stack.pop(); -} + background: ?union(enum) { + color: rl.Color, + blur_world, + } = null, -pub fn beginScissorMode(self: *UI, x: f32, y: f32, width: f32, height: f32) void { - const frame = self.getTopFrame(); + computed_relative_position: ?Vec2 = null, + computed_content_size: ?Vec2 = null, + computed_rect: ?Rect = null, - rl.beginScissorMode( - @intFromFloat(x * frame.scale.x + frame.offset.x), - @intFromFloat(y * frame.scale.y + frame.offset.y), - @intFromFloat(width * frame.scale.x), - @intFromFloat(height * frame.scale.y), - ); -} - -pub fn beginScissorModeRect(self: *UI, rect: rl.Rectangle) void { - self.beginScissorMode(rect.x, rect.y, rect.width, rect.height); -} - -pub fn endScissorMode(self: *UI) void { - _ = self; - rl.endScissorMode(); -} - -pub const Id = struct { - location: u64, - extra: u32 = 0, - - pub fn init(comptime src: SourceLocation) Id { - return Id{ .location = comptime hashSrc(src) }; - } - - pub fn eql(a: Id, b: Id) bool { - return a.location == b.location and a.extra == b.extra; + fn computed_size(self: *const Widget) ?Vec2 { + if (self.computed_content_size) |content_size| { + return Vec2{ + .x = content_size.x + self.padding.left + self.padding.right, + .y = content_size.y + self.padding.top + self.padding.bottom, + }; + } else { + return null; + } } }; -pub const Stack = struct { - pub const Direction = enum { - top_to_bottom, - bottom_to_top, - left_to_right +pub const Layout = struct { + pub const Kind = enum { + vertical_column }; - unused_box: rl.Rectangle, - dir: Direction, + kind: Kind = .vertical_column, gap: f32 = 0, - - pub fn init(box: rl.Rectangle, dir: Direction) Stack { - return Stack{ - .unused_box = box, - .dir = dir - }; - } - - pub fn next(self: *Stack, size: f32) rl.Rectangle { - return switch (self.dir) { - .top_to_bottom => { - const next_box = rl.Rectangle.init(self.unused_box.x, self.unused_box.y, self.unused_box.width, size); - self.unused_box.y += size; - self.unused_box.y += self.gap; - return next_box; - }, - .bottom_to_top => { - const next_box = rl.Rectangle.init(self.unused_box.x, self.unused_box.y + self.unused_box.height - size, self.unused_box.width, size); - self.unused_box.height -= size; - self.unused_box.height -= self.gap; - return next_box; - }, - .left_to_right => { - const next_box = rl.Rectangle.init(self.unused_box.x, self.unused_box.y, size, self.unused_box.height); - self.unused_box.x += size; - self.unused_box.x += self.gap; - return next_box; - }, - }; - } + key_extra: u16 = 0 }; -pub const IdIterator = struct { - id: Id, - counter: u32, +const debug = false; - pub fn init(comptime src: SourceLocation) IdIterator { - return IdIterator{ - .id = Id.init(src), - .counter = 0 +const max_widgets = 64; +const Widgets = std.BoundedArray(Widget, max_widgets); + +const random_seed = 0; +const root_widget_key = Widget.Key{ .hash = 0 }; + +prev_widgets: Widgets = .{}, +widgets: Widgets = .{}, +widget_stack: std.BoundedArray(Widget.Key, max_widgets) = .{}, +font_face: FontFace, +random: std.Random.DefaultPrng, +blur_background: ?rl.Texture = null, +disable_mouse_interaction: bool = false, + +// Debug fields +duplicate_keys: std.BoundedArray(Widget.Key, max_widgets) = .{}, + +pub fn init(font_face: FontFace) UI { + const random = std.Random.DefaultPrng.init(random_seed); + return UI{ + .font_face = font_face, + .random = random + }; +} + +pub fn begin(self: *UI) void { + self.prev_widgets = self.widgets; + self.widgets.len = 0; + self.duplicate_keys.len = 0; + self.random = std.Random.DefaultPrng.init(random_seed); + + self.widgets.appendAssumeCapacity(Widget{ + .key = root_widget_key, + .size = .{ + .x = .{ .pixels = @floatFromInt(rl.getScreenWidth()) }, + .y = .{ .pixels = @floatFromInt(rl.getScreenHeight()) }, + }, + .flags = Widget.Flags.initMany(&.{ .passthrough }) + }); + self.pushWidget(root_widget_key); +} + +pub fn end(self: *UI) void { + self.popWidget(); + + const widgets: []Widget = self.widgets.slice(); + + for (widgets) |*widget| { + widget.computed_content_size = Vec2{ .x = 0, .y = 0 }; + widget.computed_relative_position = Vec2{ .x = 0, .y = 0 }; + } + + // (1) Calculate stadalone sizes + for (widgets) |*widget| { + var text_size = Vec2{ .x = 0, .y = 0 }; + if (widget.text) |text| { + text_size = self.font_face.measureText(text.content); + } + + var content_size = &widget.computed_content_size.?; + inline for (.{ "x", "y" }) |axis| { + const semantic_size = @field(widget.size, axis); + + if (semantic_size == .pixels) { + @field(content_size, axis) = semantic_size.pixels; + } else if (semantic_size == .text) { + @field(content_size, axis) = @field(text_size, axis); + } + } + } + + // (2) Upward dependent sizes + for (widgets) |*widget| { + if (widget.key.eql(root_widget_key)) continue; + + const parent = self.getWidget(widget.parent); + const parent_size = parent.computed_size().?; + + var content_size = &widget.computed_content_size.?; + + if (widget.size.x == .percent) { + const percent = widget.size.x.percent; + content_size.x = parent_size.x * percent; + } + + if (widget.size.y == .percent) { + const percent = widget.size.y.percent; + content_size.y = parent_size.y * percent; + } + } + + for (0..widgets.len) |i| { + const widget = &widgets[widgets.len - i - 1]; + var children = self.listChildren(widget); + + // (3) Calculate relative position of each widget + switch (widget.layout.kind) { + .vertical_column => { + var y_offset: f32 = 0; + const children_slice: []*Widget = children.slice(); + for (children_slice) |child| { + child.computed_relative_position.?.y += y_offset; + y_offset += child.computed_size().?.y; + y_offset += widget.layout.gap; + } + } + } + + // (4) Downward dependent sizes + inline for (.{ "x", "y" }) |axis| { + if (@field(widget.size, axis) == .fit_children) { + var min = std.math.floatMax(f32); + var max = std.math.floatMin(f32); + + for (children.constSlice()) |child| { + const child_size = child.computed_size().?; + const child_position = child.computed_relative_position.?; + + min = @min(min, @field(child_position, axis)); + max = @max(max, @field(child_position, axis) + @field(child_size, axis)); + } + + if (children.len > 0) { + @field(widget.computed_content_size.?, axis) = max - min; + } + } + } + } + + for (widgets) |*widget| { + const computed_size = widget.computed_size().?; + const relative_position = widget.computed_relative_position.?; + + var computed_rect = Rect{ + .x = relative_position.x, + .y = relative_position.y, + .width = computed_size.x, + .height = computed_size.y, }; + + if (!widget.key.eql(root_widget_key)) { + const parent = self.getWidget(widget.parent); + computed_rect.x += parent.padding.left; + computed_rect.y += parent.padding.top; + + if (parent.computed_rect) |parent_rect| { + computed_rect.x += parent_rect.x; + computed_rect.y += parent_rect.y; + } + } + + widget.computed_rect = computed_rect; } - pub fn next(self: *IdIterator) Id { - var id = self.id; - id.extra = self.counter; + const duplicate_keys = self.duplicate_keys.constSlice(); + for (widgets) |*widget|{ + const rect = widget.computed_rect orelse continue; - self.counter += 1; - return id; + const padding = widget.padding; + const content_rect = Rect{ + .x = rect.x + padding.left, + .y = rect.y + padding.top, + .width = rect.width - padding.left - padding.right, + .height = rect.height - padding.top - padding.bottom, + }; + + if (widget.background) |background| { + switch (background) { + .color => |bg_color| { + drawRectangleRoundedUV(rect, widget.corner_radius, bg_color); + }, + .blur_world => { + const bg_color = srcery.xgray10; + + if (self.blur_background) |texture| { + const border = 2.5; + + { + const previous_texture = rl.getShapesTexture(); + const previous_rect = rl.getShapesTextureRectangle(); + defer rl.setShapesTexture(previous_texture, previous_rect); + + const texture_height: f32 = @floatFromInt(texture.height); + const shape_rect = rl.Rectangle{ + .x = rect.x, + .y = texture_height - rect.y, + .width = rect.width, + .height = -rect.height, + }; + rl.setShapesTexture(texture, shape_rect); + + drawRectangleRoundedUV(rect, widget.corner_radius, bg_color); + rl.gl.rlDrawRenderBatchActive(); + } + + drawRectangleRoundedLinesEx( + rect, + widget.corner_radius, + border, + srcery.bright_white.alpha(0.25) + ); + rl.gl.rlDrawRenderBatchActive(); + } + } + } + } + + if (widget.text) |text| { + self.font_face.drawTextCenter( + text.content, + rect_utils.center(content_rect), + text.color + ); + } } -}; + + if (builtin.mode == .Debug) { + for (widgets) |widget|{ + const rect = widget.computed_rect orelse continue; + + if (Widget.Key.indexOf(duplicate_keys, widget.key) != null) { + const time: f32 = @floatCast(rl.getTime()); + if (@rem(time, 0.4) < 0.2) { + rl.drawRectangleLinesEx(rect, 3, rl.Color.purple); + } else { + rl.drawRectangleLinesEx(rect, 3, rl.Color.red); + } + } + } + } +} + +pub fn pushWidget(self: *UI, key: Widget.Key) void { + self.widget_stack.appendAssumeCapacity(key); +} + +pub fn popWidget(self: *UI) void { + assert(self.widget_stack.len >= 1); + + _ = self.widget_stack.pop(); +} + +pub fn topWidget(self: *UI) *Widget { + const top_key = self.widget_stack.buffer[self.widget_stack.len - 1]; + return self.getWidget(top_key); +} + +pub fn layout(self: *UI) *Layout { + return &self.topWidget().layout; +} + +pub fn getOrAppendWidget(self: *UI, key_hash: u16) *Widget { + const parent = self.topWidget(); + + const key = Widget.Key{ + .hash = key_hash, + .extra = parent.layout.key_extra + }; + + var found_prev_widget: ?Widget = null; + for (self.prev_widgets.constSlice()) |widget| { + if (widget.key.eql(key)) { + found_prev_widget = widget; + break; + } + } + + for (self.widgets.constSlice()) |widget| { + if (widget.key.eql(key)) { + self.duplicate_keys.appendAssumeCapacity(widget.key); + break; + } + } + + if (found_prev_widget) |prev_widget| { + self.widgets.appendAssumeCapacity(prev_widget); + } else { + self.widgets.appendAssumeCapacity(Widget{ + .key = key + }); + } + + const widget = &self.widgets.buffer[self.widgets.len - 1]; + + assert(self.widget_stack.len >= 1); + widget.parent = self.widget_stack.buffer[self.widget_stack.len-1]; + + return widget; +} + +pub fn getWidget(self: *UI, key: Widget.Key) *Widget { + for (self.widgets.slice()) |*widget| { + if (widget.key.eql(key)) { + return widget; + } + } + + @panic("Failed to find widget"); +} + +pub fn getInteraction(self: *UI, widget: *const Widget) Interaction { + var interaction = Interaction{ + .widget = widget.key + }; + + const rect = widget.computed_rect orelse return interaction; + + if (!self.disable_mouse_interaction) { + const mouse = rl.getMousePosition(); + if (!widget.flags.contains(.passthrough) and rect_utils.isInsideVec2(rect, mouse)) { + interaction.hovering = true; + + if (rl.isMouseButtonPressed(.mouse_button_left)) { + interaction.pressed = true; + } + + if (rl.isMouseButtonReleased(.mouse_button_left)) { + interaction.released = true; + if (widget.flags.contains(.clickable)) { + interaction.clicked = true; + } + } + + if (rl.isMouseButtonDown(.mouse_button_left)) { + interaction.held_down = true; + } + } + } + + return interaction; +} + +pub fn isHoveringAnything(self: *UI) bool { + const widgets: []Widget = self.widgets.slice(); + for (widgets) |*widget|{ + const interaction = self.getInteraction(widget); + if (interaction.hovering) { + return true; + } + } + + return false; +} + +pub fn randomWidgetHash(self: *UI) u16 { + const rng = self.random.random(); + return rng.int(u16); +} + +fn listChildren(self: *UI, parent: *Widget) std.BoundedArray(*Widget, max_widgets) { + var children: std.BoundedArray(*Widget, max_widgets) = .{}; + + const widgets: []Widget = self.widgets.slice(); + for (widgets) |*child| { + if (child == parent) { + continue; + } + + if (child.parent.eql(parent.key)) { + children.appendAssumeCapacity(child); + } + } + + return children; +} + +// Modified version of `DrawRectangleRounded` where the UV texture coordiantes are consistent and align +fn drawRectangleRoundedUV(rec: rl.Rectangle, radius: f32, color: rl.Color) void { + if (radius <= 0 or rec.width <= 1 or rec.height <= 1) { + rl.drawRectangleRec(rec, color); + return; + } + + if (radius <= 0.0) return; + + // Calculate the maximum angle between segments based on the error rate (usually 0.5f) + const smooth_circle_error_rate = 0.5; + const th: f32 = std.math.acos(2 * std.math.pow(f32, 1 - smooth_circle_error_rate / radius, 2) - 1); + var segments: i32 = @intFromFloat(@ceil(2 * std.math.pi / th) / 4.0); + segments = @max(segments, 4); + + const step_length = 90.0 / @as(f32, @floatFromInt(segments)); + + // Quick sketch to make sense of all of this, + // there are 9 parts to draw, also mark the 12 points we'll use + // + // P0____________________P1 + // /| |\ + // /1| 2 |3\ + // P7 /__|____________________|__\ P2 + // | |P8 P9| | + // | 8 | 9 | 4 | + // | __|____________________|__ | + // P6 \ |P11 P10| / P3 + // \7| 6 |5/ + // \|____________________|/ + // P5 P4 + + // Coordinates of the 12 points that define the rounded rect + const radius_u = radius / rec.width; + const radius_v = radius / rec.height; + const points = [_]rl.Vector2{ + .{ .x = radius_u , .y = 0 }, // P0 + .{ .x = 1 - radius_u , .y = 0 }, // P1 + .{ .x = 1 , .y = radius_v }, // P2 + .{ .x = 1 , .y = 1 - radius_v }, // P3 + .{ .x = 1 - radius_u , .y = 1 }, // P4 + .{ .x = radius_u , .y = 1 }, // P5 + .{ .x = 0 , .y = 1 - radius_v }, // P6 + .{ .x = 0 , .y = radius_v }, // P7 + .{ .x = radius_u , .y = radius_v }, // P8 + .{ .x = 1 - radius_u , .y = radius_v }, // P9 + .{ .x = 1 - radius_u , .y = 1 - radius_v }, // P10 + .{ .x = radius_u , .y = 1 - radius_v }, // P11 + }; + + const texture = rl.getShapesTexture(); + const shape_rect = rl.getShapesTextureRectangle(); + + const texture_width: f32 = @floatFromInt(texture.width); + const texture_height: f32 = @floatFromInt(texture.height); + + rl.gl.rlBegin(rlgl_h.RL_TRIANGLES); + defer rl.gl.rlEnd(); + + rl.gl.rlSetTexture(texture.id); + defer rl.gl.rlSetTexture(0); + + // Draw all of the 4 corners: [1] Upper Left Corner, [3] Upper Right Corner, [5] Lower Right Corner, [7] Lower Left Corner + const centers = [_]rl.Vector2{ points[8], points[9], points[10], points[11] }; + const angles = [_]f32{ 180.0, 270.0, 0.0, 90.0 }; + for (0..4) |k| { + var angle = angles[k]; + const center = centers[k]; + for (0..@intCast(segments)) |_| { + rl.gl.rlColor4ub(color.r, color.g, color.b, color.a); + + const rad_per_deg = std.math.rad_per_deg; + + const triangle = .{ + center, + .{ + .x = center.x + @cos(rad_per_deg*(angle + step_length))*radius_u, + .y = center.y + @sin(rad_per_deg*(angle + step_length))*radius_v + }, + .{ + .x = center.x + @cos(rad_per_deg * angle)*radius_u, + .y = center.y + @sin(rad_per_deg * angle)*radius_v + } + }; + + inline for (triangle) |point| { + rl.gl.rlTexCoord2f( + (shape_rect.x + shape_rect.width * point.x) / texture_width, + (shape_rect.y + shape_rect.height * point.y) / texture_height + ); + rl.gl.rlVertex2f( + rec.x + rec.width * point.x, + rec.y + rec.height * point.y + ); + } + + angle += step_length; + } + } + + // [2] Upper Rectangle + rl.gl.rlColor4ub(color.r, color.g, color.b, color.a); + inline for (.{ 0, 8, 9, 1, 0, 9 }) |index| { + const point = points[index]; + rl.gl.rlTexCoord2f( + (shape_rect.x + shape_rect.width * point.x) / texture_width, + (shape_rect.y + shape_rect.height * point.y) / texture_height + ); + rl.gl.rlVertex2f( + rec.x + rec.width * point.x, + rec.y + rec.height * point.y + ); + } + + // [4] Right Rectangle + rl.gl.rlColor4ub(color.r, color.g, color.b, color.a); + inline for (.{ 9, 10, 3, 2, 9, 3 }) |index| { + const point = points[index]; + rl.gl.rlTexCoord2f( + (shape_rect.x + shape_rect.width * point.x) / texture_width, + (shape_rect.y + shape_rect.height * point.y) / texture_height + ); + rl.gl.rlVertex2f( + rec.x + rec.width * point.x, + rec.y + rec.height * point.y + ); + } + + // [6] Bottom Rectangle + rl.gl.rlColor4ub(color.r, color.g, color.b, color.a); + inline for (.{ 11, 5, 4, 10, 11, 4 }) |index| { + const point = points[index]; + rl.gl.rlTexCoord2f( + (shape_rect.x + shape_rect.width * point.x) / texture_width, + (shape_rect.y + shape_rect.height * point.y) / texture_height + ); + rl.gl.rlVertex2f( + rec.x + rec.width * point.x, + rec.y + rec.height * point.y + ); + } + + // [8] Left Rectangle + rl.gl.rlColor4ub(color.r, color.g, color.b, color.a); + inline for (.{ 7, 6, 11, 8, 7, 11 }) |index| { + const point = points[index]; + rl.gl.rlTexCoord2f( + (shape_rect.x + shape_rect.width * point.x) / texture_width, + (shape_rect.y + shape_rect.height * point.y) / texture_height + ); + rl.gl.rlVertex2f( + rec.x + rec.width * point.x, + rec.y + rec.height * point.y + ); + } + + // [9] Middle Rectangle + rl.gl.rlColor4ub(color.r, color.g, color.b, color.a); + inline for (.{ 8, 11, 10, 9, 8, 10 }) |index| { + const point = points[index]; + rl.gl.rlTexCoord2f( + (shape_rect.x + shape_rect.width * point.x) / texture_width, + (shape_rect.y + shape_rect.height * point.y) / texture_height + ); + rl.gl.rlVertex2f( + rec.x + rec.width * point.x, + rec.y + rec.height * point.y + ); + } +} + +// Modified version of `DrawRectangleRoundedLinesEx` where the corner radius is provided +fn drawRectangleRoundedLinesEx(rec: rl.Rectangle, radius: f32, _line_thick: f32, color: rl.Color) void { + var line_thick = _line_thick; + if (line_thick < 0) line_thick = 0; + + // Not a rounded rectangle + if (radius <= 0.0) { + rl.drawRectangleLinesEx(rl.Rectangle{ + .x = rec.x, + .y = rec.y, + .width = rec.width, + .height = rec.height + }, line_thick, color); + return; + } + + // Calculate number of segments to use for the corners + const smooth_circle_error_rate = 0.5; + const th: f32 = std.math.acos(2 * std.math.pow(f32, 1 - smooth_circle_error_rate / radius, 2) - 1); + var segments: i32 = @intFromFloat(@ceil(2 * std.math.pi / th) / 4.0); + segments = @max(segments, 4); + + const step_length = 90.0/@as(f32, @floatFromInt(segments)); + const outer_radius = radius; + const inner_radius = radius - line_thick; + + // Quick sketch to make sense of all of this, + // marks the 16 + 4(corner centers P16-19) points we'll use + // + // P0 ================== P1 + // // P8 P9 \\ + // // \\ + // P7 // P15 P10 \\ P2 + // || *P16 P17* || + // || || + // || P14 P11 || + // P6 \\ *P19 P18* // P3 + // \\ // + // \\ P13 P12 // + // P5 ================== P4 + const points = [_]rl.Vector2{ + .{ .x = outer_radius , .y = 0 }, // P0 + .{ .x = rec.width - outer_radius , .y = 0 }, // P1 + .{ .x = rec.width , .y = outer_radius }, // P2 + .{ .x = rec.width , .y = rec.height - outer_radius }, // P3 + .{ .x = rec.width - outer_radius , .y = rec.height }, // P4 + .{ .x = outer_radius , .y = rec.height }, // P5 + .{ .x = 0 , .y = rec.height - outer_radius }, // P6 + .{ .x = 0 , .y = outer_radius }, // P7 + + .{ .x = outer_radius , .y = line_thick }, // P8 + .{ .x = rec.width - outer_radius , .y = line_thick }, // P9 + .{ .x = rec.width - line_thick , .y = outer_radius }, // P10 + .{ .x = rec.width - line_thick , .y = rec.height - outer_radius }, // P11 + .{ .x = rec.width - outer_radius , .y = rec.height - line_thick }, // P12 + .{ .x = outer_radius , .y = rec.height - line_thick }, // P13 + .{ .x = line_thick , .y = rec.height - outer_radius }, // P14 + .{ .x = line_thick , .y = outer_radius }, // P15 + }; + + const centers = [_]rl.Vector2{ + .{ .x = outer_radius , .y = outer_radius }, // P16 + .{ .x = rec.width - outer_radius , .y = outer_radius }, // P17 + .{ .x = rec.width - outer_radius , .y = rec.height - outer_radius }, // P18 + .{ .x = outer_radius , .y = rec.height - outer_radius }, // P18 + }; + + const angles = [_]f32{ 180.0, 270.0, 0.0, 90.0 }; + + if (line_thick > 1) { + rl.gl.rlBegin(rlgl_h.RL_TRIANGLES); + defer rl.gl.rlEnd(); + + // Draw all of the 4 corners first: Upper Left Corner, Upper Right Corner, Lower Right Corner, Lower Left Corner + for (centers, angles) |center, initialAngle| { + var angle = initialAngle; + for (0..@intCast(segments)) |_| { + + const rad_per_deg = std.math.rad_per_deg; + const next_angle = angle + step_length; + + const rect_points = .{ + rl.Vector2{ + .x = @cos(rad_per_deg * angle) * inner_radius, + .y = @sin(rad_per_deg * angle) * inner_radius + }, + rl.Vector2{ + .x = @cos(rad_per_deg * next_angle) * inner_radius, + .y = @sin(rad_per_deg * next_angle) * inner_radius + }, + rl.Vector2{ + .x = @cos(rad_per_deg * angle) * outer_radius, + .y = @sin(rad_per_deg * angle) * outer_radius + }, + rl.Vector2{ + .x = @cos(rad_per_deg * next_angle) * outer_radius, + .y = @sin(rad_per_deg * next_angle) * outer_radius + } + }; + + rl.gl.rlColor4ub(color.r, color.g, color.b, color.a); + inline for (.{ 0, 1, 2, 1, 3, 2 }) |i| { + rl.gl.rlVertex2f( + rec.x + center.x + rect_points[i].x, + rec.y + center.y + rect_points[i].y + ); + } + + angle = next_angle; + } + } + + // Upper rectangle + rl.gl.rlColor4ub(color.r, color.g, color.b, color.a); + inline for (.{ 0, 8, 9, 1, 0, 9 }) |i| { + rl.gl.rlVertex2f(rec.x + points[i].x, rec.y + points[i].y); + } + + // Right rectangle + rl.gl.rlColor4ub(color.r, color.g, color.b, color.a); + inline for (.{ 10, 11, 3, 2, 10, 3 }) |i| { + rl.gl.rlVertex2f(rec.x + points[i].x, rec.y + points[i].y); + } + + // Lower rectangle + rl.gl.rlColor4ub(color.r, color.g, color.b, color.a); + inline for (.{ 13, 5, 4, 12, 13, 4 }) |i| { + rl.gl.rlVertex2f(rec.x + points[i].x, rec.y + points[i].y); + } + + // Left rectangle + rl.gl.rlColor4ub(color.r, color.g, color.b, color.a); + inline for (.{ 7, 6, 14, 15, 7, 14 }) |i| { + rl.gl.rlVertex2f(rec.x + points[i].x, rec.y + points[i].y); + } + } else { + // Use LINES to draw the outline + rl.gl.rlBegin(rlgl_h.RL_LINES); + defer rl.gl.rlEnd(); + + // Draw all the 4 corners first: Upper Left Corner, Upper Right Corner, Lower Right Corner, Lower Left Corner + for (centers, angles) |center, initialAngle| { + var angle = initialAngle; + + for (0..@intCast(segments)) |_| { + const rad_per_deg = std.math.rad_per_deg; + const next_angle = angle + step_length; + + rl.gl.rlColor4ub(color.r, color.g, color.b, color.a); + rl.gl.rlVertex2f( + rec.x + center.x + @cos(rad_per_deg*angle)*outer_radius, + rec.y + center.y + @sin(rad_per_deg*angle)*outer_radius + ); + rl.gl.rlVertex2f( + rec.x + center.x + @cos(rad_per_deg*next_angle)*outer_radius, + rec.y + center.y + @sin(rad_per_deg*next_angle)*outer_radius + ); + + angle = next_angle; + } + } + + // And now the remaining 4 lines + inline for (.{ 0, 2, 4, 6 }) |i| { + rl.gl.rlColor4ub(color.r, color.g, color.b, color.a); + rl.gl.rlVertex2f(rec.x + points[i + 0].x, rec.y + points[i + 0].y); + rl.gl.rlVertex2f(rec.x + points[i + 1].x, rec.y + points[i + 1].y); + } + } +} diff --git a/lib/artificer.zig b/lib/artificer.zig index b00c1fb..c5d852a 100644 --- a/lib/artificer.zig +++ b/lib/artificer.zig @@ -275,7 +275,7 @@ pub fn ArtificerType(Clock: type, Server: type) type { return slot; } - fn timeUntilCooldownExpires(self: *Self) u64 { + pub fn timeUntilCooldownExpires(self: *Self) u64 { const store = self.server.store; const character = store.characters.get(self.character).?; diff --git a/lib/sim_clock.zig b/lib/sim_clock.zig index 3302900..1937dcc 100644 --- a/lib/sim_clock.zig +++ b/lib/sim_clock.zig @@ -3,9 +3,25 @@ const std = @import("std"); const Clock = @This(); timestamp: i128 = 0, +timestamp_limit: ?i128 = null, pub fn sleep(self: *Clock, nanoseconds: u64) void { - self.timestamp += @intCast(nanoseconds); + const nanoseconds_i128: i128 = @intCast(nanoseconds); + const new_timestamp = self.timestamp + nanoseconds_i128; + + if (self.timestamp_limit != null) { + while (true) { + self.timestamp = @min(self.timestamp_limit.?, new_timestamp); + + if (self.timestamp < self.timestamp_limit.?) { + break; + } + + std.time.sleep(std.time.ms_per_s * 100); + } + } else { + self.timestamp = new_timestamp; + } } pub fn nanoTimestamp(self: Clock) i128 { diff --git a/lib/system_clock.zig b/lib/system_clock.zig index 2d7936c..a93742a 100644 --- a/lib/system_clock.zig +++ b/lib/system_clock.zig @@ -6,7 +6,7 @@ pub fn sleep(self: *Clock, nanoseconds: u64) void { std.time.sleep(nanoseconds); } -pub fn nanoTimestamp(self: *Clock) i128 { +pub fn nanoTimestamp(self: Clock) i128 { _ = self; return std.time.nanoTimestamp(); }