// zig fmt: off const std = @import("std"); const Api = @import("artifacts-api"); const Artificer = @import("artificer"); const UI = @import("./ui.zig"); const RectUtils = @import("./rect-utils.zig"); const rl = @import("raylib"); const FontFace = @import("./font-face.zig"); const srcery = @import("./srcery.zig"); const rlgl_h = @cImport({ @cInclude("rlgl.h"); }); const assert = std.debug.assert; const App = @This(); const MapTexture = struct { name: Api.Map.Skin, texture: rl.Texture2D, }; ui: UI, server: *Api.Server, store: *Api.Store, map_textures: std.ArrayList(MapTexture), map_texture_indexes: std.ArrayList(usize), map_position_min: Api.Position, map_position_max: Api.Position, camera: rl.Camera2D, font_face: FontFace, 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, store: *Api.Store, server: *Api.Server) !App { 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; } var fontChars: [95]i32 = undefined; for (0..fontChars.len) |i| { fontChars[i] = 32 + @as(i32, @intCast(i)); } var font = rl.loadFontFromMemory(".ttf", @embedFile("./roboto-font/Roboto-Medium.ttf"), 16, &fontChars); if (!font.isReady()) { return error.LoadFontFromMemory; } return App{ .store = store, .server = server, .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, .font_face = .{ .font = font }, .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.map_textures.deinit(); self.map_texture_indexes.deinit(); self.font_face.font.unload(); 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.*.?; } // 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 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); } } } pub fn drawWorldAndBlur(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(); self.camera.begin(); defer self.camera.end(); self.drawWorld(); } // 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 ); } 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; inline for (.{ .{ "Account creation", Category.account_creation }, .{ "Token", Category.token }, .{ "Data", Category.data }, .{ "Actions", Category.actions }, }) |ratelimit_bar| { const title = ratelimit_bar[0]; const category = ratelimit_bar[1]; const ratelimit = ratelimits.get(category); const ratelimit_box = stack.next(24); rl.drawRectangleRec(ratelimit_box, rl.Color.white); inline for (.{ .{ ratelimit.hours , std.time.ms_per_hour, srcery.red }, .{ ratelimit.minutes, std.time.ms_per_min , srcery.blue }, .{ ratelimit.seconds, std.time.ms_per_s , srcery.green }, }) |limit_spec| { const maybe_timespan = limit_spec[0]; const timespan_size = limit_spec[1]; const color = limit_spec[2]; if (maybe_timespan) |timespan| { const limit_f32: f32 = @floatFromInt(timespan.limit); const counter_f32: f32 = @floatFromInt(timespan.counter); const timer_f32: f32 = @floatFromInt(timespan.timer_ms); const ms_per_request = timespan_size / limit_f32; const progress = std.math.clamp((counter_f32 - timer_f32 / ms_per_request) / limit_f32, 0, 1); var progress_bar = ratelimit_box; progress_bar.width *= progress; rl.drawRectangleRec(progress_bar, color); } } if (self.ui.isMouseInside(ratelimit_box)) { // TODO: Draw more detailed info about rate limits. // Show how many requests have occured } else { const title_size = self.font_face.measureText(title); self.font_face.drawText( title, .{ .x = ratelimit_box.x + 8, .y = ratelimit_box.y + title_size.y/2, }, srcery.white ); } } } pub fn tick(self: *App) !void { for (&self.server.ratelimits.values) |*ratelimit| { ratelimit.update_timers(std.time.milliTimestamp()); } 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.drawWorldAndBlur(); self.drawRatelimits( .{ .x = 20, .y = 20, .width = 200, .height = 200 }, ); 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); // } }