add basic gui

This commit is contained in:
Rokas Puzonas 2024-09-07 16:37:05 +03:00
parent 45e32424cb
commit f9dc023b90
9 changed files with 476 additions and 19 deletions

View File

@ -44,7 +44,7 @@ air: CombatStats,
equipment: Equipment,
inventory_max_items: i64,
inventory_max_items: u64,
inventory: Inventory,
pub fn parse(api: *Server, obj: json.ObjectMap, allocator: Allocator) !Character {
@ -54,7 +54,14 @@ pub fn parse(api: *Server, obj: json.ObjectMap, allocator: Allocator) !Character
const x = try json_utils.getIntegerRequired(obj, "x");
const y = try json_utils.getIntegerRequired(obj, "y");
const name = (try json_utils.dupeString(allocator, obj, "name")) orelse return error.MissingProperty;
assert(name.len > 0);
if (name.len == 0) {
return error.InvalidName;
}
const inventory_max_items = json_utils.getInteger(obj, "inventory_max_items") orelse return error.MissingProperty;
if (inventory_max_items < 0) {
return error.InvalidInventoryMaxItems;
}
return Character{
.allocator = allocator,
@ -84,7 +91,7 @@ pub fn parse(api: *Server, obj: json.ObjectMap, allocator: Allocator) !Character
.equipment = try Equipment.parse(api, obj),
.inventory_max_items = json_utils.getInteger(obj, "inventory_max_items") orelse return error.MissingProperty,
.inventory_max_items = @intCast(inventory_max_items),
.inventory = try Inventory.parse(api, inventory)
};
}

View File

@ -1,4 +1,5 @@
const std = @import("std");
const builtin = @import("builtin");
const Artificer = @import("artificer");
const Allocator = std.mem.Allocator;
@ -29,6 +30,11 @@ pub fn main() !void {
var artificer = try Artificer.init(allocator, token);
defer artificer.deinit();
if (builtin.mode != .Debug) {
std.log.info("Prefetching server data", .{});
try artificer.server.prefetch();
}
std.log.info("Starting main loop", .{});
while (true) {
const waitUntil = artificer.nextStepAt();

View File

@ -1,18 +1,76 @@
const std = @import("std");
const Artificer = @import("artificer");
const rl = @import("raylib");
const Allocator = std.mem.Allocator;
const srcery = @import("./srcery.zig");
const UI = @import("./ui.zig");
const UIStack = @import("./ui_stack.zig");
const RectUtils = @import("./rect_utils.zig");
fn getAPITokenFromArgs(allocator: Allocator) !?[]u8 {
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
if (args.len < 2) {
return null;
}
const filename = args[1];
const cwd = std.fs.cwd();
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, brain: Artificer.Brain) void {
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);
}
pub fn main() anyerror!void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const token = (try getAPITokenFromArgs(allocator)) orelse return error.MissingToken;
defer allocator.free(token);
var artificer = try Artificer.init(allocator, token);
defer artificer.deinit();
rl.initWindow(800, 450, "Artificer");
defer rl.closeWindow();
rl.setTargetFPS(60);
var ui = UI.init();
defer ui.deinit();
while (!rl.windowShouldClose()) {
if (std.time.timestamp() > artificer.nextStepAt()) {
try artificer.step();
}
const screen_size = rl.Vector2.init(
@floatFromInt(rl.getScreenWidth()),
@floatFromInt(rl.getScreenHeight())
);
rl.beginDrawing();
defer rl.endDrawing();
rl.clearBackground(rl.Color.white);
rl.clearBackground(srcery.black);
rl.drawText("Congrats! You created your first window!", 190, 200, 20, rl.Color.light_gray);
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));
drawCharacterInfo(&ui, info_stack.next(info_width), brain);
}
}
}

142
gui/rect_utils.zig Normal file
View File

@ -0,0 +1,142 @@
const rl = @import("raylib");
const Rect = rl.Rectangle;
pub const AlignX = enum { left, center, right };
pub const AlignY = enum { top, center, bottom };
pub fn initCentered(rect: Rect, width: f32, height: f32) Rect {
const unused_width = rect.width - width;
const unused_height = rect.height - height;
return Rect.init(rect.x + unused_width / 2, rect.y + unused_height / 2, width, height);
}
pub fn center(rect: Rect) rl.Vector2 {
return rl.Vector2{
.x = rect.x + rect.width / 2,
.y = rect.y + rect.height / 2,
};
}
pub fn bottomLeft(rect: Rect) rl.Vector2 {
return rl.Vector2{
.x = rect.x,
.y = rect.y + rect.height,
};
}
pub fn bottomRight(rect: Rect) rl.Vector2 {
return rl.Vector2{
.x = rect.x + rect.width,
.y = rect.y + rect.height,
};
}
pub fn topLeft(rect: Rect) rl.Vector2 {
return rl.Vector2{
.x = rect.x,
.y = rect.y,
};
}
pub fn topRight(rect: Rect) rl.Vector2 {
return rl.Vector2{
.x = rect.x + rect.width,
.y = rect.y,
};
}
pub fn aligned(rect: Rect, align_x: AlignX, align_y: AlignY) rl.Vector2 {
const x = switch(align_x) {
.left => rect.x,
.center => rect.x + rect.width/2,
.right => rect.x + rect.width,
};
const y = switch(align_y) {
.top => rect.y,
.center => rect.y + rect.height/2,
.bottom => rect.y + rect.height,
};
return rl.Vector2.init(x, y);
}
pub fn shrink(rect: Rect, x: f32, y: f32) rl.Rectangle {
return Rect.init(rect.x + x, rect.y + y, rect.width - 2 * x, rect.height - 2 * y);
}
pub fn shrinkX(rect: rl.Rectangle, offset: f32) rl.Rectangle {
return shrink(rect, offset, 0);
}
pub fn shrinkY(rect: rl.Rectangle, offset: f32) rl.Rectangle {
return shrink(rect, 0, offset);
}
pub fn shrinkTop(rect: rl.Rectangle, offset: f32) rl.Rectangle {
return Rect.init(rect.x, rect.y + offset, rect.width, rect.height - offset);
}
pub fn grow(rect: Rect, x: f32, y: f32) rl.Rectangle {
return shrink(rect, -x, -y);
}
pub fn growY(rect: Rect, offset: f32) rl.Rectangle {
return grow(rect, 0, offset);
}
pub fn position(rect: rl.Rectangle) rl.Vector2 {
return rl.Vector2.init(rect.x, rect.y);
}
pub fn isInside(rect: rl.Rectangle, x: f32, y: f32) bool {
return (rect.x <= x and x < rect.x + rect.width) and (rect.y < y and y < rect.y + rect.height);
}
pub fn isInsideVec2(rect: rl.Rectangle, vec2: rl.Vector2) bool {
return isInside(rect, vec2.x, vec2.y);
}
pub fn top(rect: rl.Rectangle) f32 {
return rect.y;
}
pub fn bottom(rect: rl.Rectangle) f32 {
return rect.y + rect.height;
}
pub fn left(rect: rl.Rectangle) f32 {
return rect.x;
}
pub fn right(rect: rl.Rectangle) f32 {
return rect.x + rect.width;
}
pub fn verticalSplit(rect: rl.Rectangle, left_side_width: f32) [2]rl.Rectangle {
var left_side = rect;
left_side.width = left_side_width;
var right_side = rect;
right_side.x += left_side_width;
right_side.width -= left_side_width;
return .{
left_side,
right_side
};
}
pub fn horizontalSplit(rect: rl.Rectangle, top_side_height: f32) [2]rl.Rectangle {
var top_side = rect;
top_side.height = top_side_height;
var bottom_side = rect;
bottom_side.y += top_side_height;
bottom_side.height -= top_side_height;
return .{
top_side,
bottom_side
};
}

43
gui/srcery.zig Normal file
View File

@ -0,0 +1,43 @@
const rl = @import("raylib");
fn rgb(r: u8, g: u8, b: u8) rl.Color {
return rl.Color.init(r, g, b, 255);
}
// Primary
pub const black = rgb(28 , 27 , 25 );
pub const red = rgb(239, 47 , 39 );
pub const green = rgb(81 , 159, 80 );
pub const yellow = rgb(251, 184, 41 );
pub const blue = rgb(44 , 120, 191);
pub const magenta = rgb(224, 44 , 109);
pub const cyan = rgb(10 , 174, 179);
pub const white = rgb(186, 166, 127);
pub const bright_black = rgb(145, 129, 117);
pub const bright_red = rgb(247, 83 , 65 );
pub const bright_green = rgb(152, 188, 55 );
pub const bright_yellow = rgb(254, 208, 110);
pub const bright_blue = rgb(104, 168, 228);
pub const bright_magenta = rgb(255, 92 , 143);
pub const bright_cyan = rgb(43 , 228, 208);
pub const bright_white = rgb(252, 232, 195);
// Secondary
pub const orange = rgb(255, 95, 0);
pub const bright_orange = rgb(255, 135, 0);
pub const hard_black = rgb(18, 18, 18);
pub const teal = rgb(0, 128, 128);
// Grays
pub const xgray1 = rgb(38 , 38 , 38 );
pub const xgray2 = rgb(48 , 48 , 48 );
pub const xgray3 = rgb(58 , 58 , 58 );
pub const xgray4 = rgb(68 , 68 , 68 );
pub const xgray5 = rgb(78 , 78 , 78 );
pub const xgray6 = rgb(88 , 88 , 88 );
pub const xgray7 = rgb(98 , 98 , 98 );
pub const xgray8 = rgb(108, 108, 108);
pub const xgray9 = rgb(118, 118, 118);
pub const xgray10 = rgb(128, 128, 128);
pub const xgray11 = rgb(138, 138, 138);
pub const xgray12 = rgb(148, 148, 148);

159
gui/ui.zig Normal file
View File

@ -0,0 +1,159 @@
const rl = @import("raylib");
const std = @import("std");
const UI = @This();
font: rl.Font,
pub fn init() UI {
return UI{
.font = rl.getFontDefault()
};
}
pub fn deinit(self: UI) void {
rl.unloadFont(self.font);
}
// Reimplementation of `GetGlyphIndex` from raylib in src/rtext.c
fn GetGlyphIndex(font: rl.Font, codepoint: i32) usize {
var index: usize = 0;
var fallbackIndex: usize = 0; // Get index of fallback glyph '?'
for (0..@intCast(font.glyphCount), font.glyphs) |i, glyph| {
if (glyph.value == '?') fallbackIndex = i;
if (glyph.value == codepoint)
{
index = i;
break;
}
}
if ((index == 0) and (font.glyphs[0].value != codepoint)) index = fallbackIndex;
return index;
}
fn GetCodePointNext(text: []const u8, next: *usize) i32 {
var letter: i32 = '?';
if (std.unicode.utf8ByteSequenceLength(text[0])) |codepointSize| {
next.* = codepointSize;
if (std.unicode.utf8Decode(text[0..codepointSize])) |codepoint| {
letter = @intCast(codepoint);
} else |_| {}
} else |_| {}
return letter;
}
// NOTE: Line spacing is a global variable, use SetTextLineSpacing() to setup
const textLineSpacing = 2; // TODO: Assume that line spacing is not changed.
// Reimplementation of `rl.drawTextEx`, so a null terminated would not be required
pub fn drawTextEx(font: rl.Font, text: []const u8, position: rl.Vector2, font_size: f32, spacing: f32, tint: rl.Color) void {
var used_font = font;
if (font.texture.id == 0) {
used_font = rl.getFontDefault();
}
var text_offset_y: f32 = 0;
var text_offset_x: f32 = 0;
const scale_factor = font_size / @as(f32, @floatFromInt(used_font.baseSize));
var i: usize = 0;
while (i < text.len) {
var next: usize = 0;
const letter = GetCodePointNext(text[i..], &next);
const index = GetGlyphIndex(font, letter);
i += next;
if (letter == '\n') {
text_offset_x = 0;
text_offset_y += (font_size + textLineSpacing);
} else {
if (letter != ' ' and letter != '\t') {
rl.drawTextCodepoint(font, letter, .{
.x = position.x + text_offset_x,
.y = position.y + text_offset_y,
}, font_size, tint);
}
if (font.glyphs[index].advanceX == 0) {
text_offset_x += font.recs[index].width*scale_factor + spacing;
} else {
text_offset_x += @as(f32, @floatFromInt(font.glyphs[index].advanceX))*scale_factor + spacing;
}
}
}
}
// Reimplementation of `rl.measureTextEx`, so a null terminated would not be required
pub fn measureTextEx(font: rl.Font, text: []const u8, fontSize: f32, spacing: f32) rl.Vector2 {
var textSize = rl.Vector2.init(0, 0);
if (font.texture.id == 0) return textSize; // Security check
var tempByteCounter: i32 = 0; // Used to count longer text line num chars
var byteCounter: i32 = 0;
var textWidth: f32 = 0;
var tempTextWidth: f32 = 0; // Used to count longer text line width
var textHeight: f32 = fontSize;
const scaleFactor: f32 = fontSize/@as(f32, @floatFromInt(font.baseSize));
var i: usize = 0;
while (i < text.len)
{
byteCounter += 1;
var next: usize = 0;
const letter = GetCodePointNext(text[i..], &next);
const index = GetGlyphIndex(font, letter);
i += next;
if (letter != '\n')
{
if (font.glyphs[index].advanceX != 0) {
textWidth += @floatFromInt(font.glyphs[index].advanceX);
} else {
textWidth += font.recs[index].width;
textWidth += @floatFromInt(font.glyphs[index].offsetX);
}
}
else
{
if (tempTextWidth < textWidth) tempTextWidth = textWidth;
byteCounter = 0;
textWidth = 0;
textHeight += (fontSize + textLineSpacing);
}
if (tempByteCounter < byteCounter) tempByteCounter = byteCounter;
}
if (tempTextWidth < textWidth) tempTextWidth = textWidth;
textSize.x = tempTextWidth*scaleFactor + @as(f32, @floatFromInt(tempByteCounter - 1)) * spacing;
textSize.y = textHeight;
return textSize;
}
pub fn drawTextCentered(font: rl.Font, text: []const u8, position: rl.Vector2, font_size: f32, spacing: f32, color: rl.Color) void {
const text_size = measureTextEx(font, text, font_size, spacing);
const adjusted_position = rl.Vector2{
.x = position.x - text_size.x/2,
.y = position.y - text_size.y/2,
};
drawTextEx(font, text, adjusted_position, font_size, spacing, color);
}

42
gui/ui_stack.zig Normal file
View File

@ -0,0 +1,42 @@
const rl = @import("raylib");
const Stack = @This();
pub const Direction = enum {
top_to_bottom,
bottom_to_top,
left_to_right
};
unused_box: rl.Rectangle,
dir: Direction,
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;
},
};
}

View File

@ -400,7 +400,7 @@ pub fn isTaskFinished(self: *CharacterBrain) bool {
pub fn performTask(self: *CharacterBrain, api: *Server) !void {
if (self.task == null) {
std.log.debug("[{s}] idle", .{self.name});
// TODO: std.log.debug("[{s}] idle", .{self.name});
return;
}

View File

@ -2,7 +2,7 @@ const std = @import("std");
const Api = @import("artifacts-api");
const Allocator = std.mem.Allocator;
const CharacterBrain = @import("./brain.zig");
pub const Brain = @import("./brain.zig");
const Artificer = @This();
@ -10,7 +10,7 @@ const expiration_margin: u64 = 100 * std.time.ns_per_ms; // 100ms
const server_down_retry_interval = 5; // minutes
server: Api.Server,
characters: std.ArrayList(CharacterBrain),
characters: std.ArrayList(Brain),
paused_until: ?i64 = null,
@ -20,16 +20,15 @@ pub fn init(allocator: Allocator, token: []const u8) !Artificer {
try server.setToken(token);
var characters = std.ArrayList(CharacterBrain).init(allocator);
defer characters.deinit();
// TODO: Add character deinit
var characters = std.ArrayList(Brain).init(allocator);
errdefer characters.deinit(); // TODO: Add character deinit
// const chars = try api.listMyCharacters();
// defer chars.deinit();
const chars = try server.listMyCharacters();
defer chars.deinit();
// for (chars.items) |char| {
// try scheduler.addCharacter(char.name);
// }
for (chars.items) |char| {
try characters.append(try Brain.init(allocator, char.name));
}
return Artificer{
.server = server,
@ -37,11 +36,12 @@ pub fn init(allocator: Allocator, token: []const u8) !Artificer {
};
}
pub fn deinit(self: Artificer) void {
pub fn deinit(self: *Artificer) void {
for (self.characters.items) |brain| {
brain.deinit();
}
self.characters.deinit();
self.server.deinit();
}
pub fn step(self: *Artificer) !void {
@ -93,7 +93,7 @@ pub fn nextStepAt(self: *Artificer) i64 {
return earliestCooldown(self.characters.items, &self.server) orelse 0;
}
fn earliestCooldown(characters: []CharacterBrain, api: *Api.Server) ?i64 {
fn earliestCooldown(characters: []Brain, api: *Api.Server) ?i64 {
var earliest_cooldown: ?i64 = null;
for (characters) |*brain| {
if (brain.action_queue.items.len == 0) continue;
@ -109,7 +109,7 @@ fn earliestCooldown(characters: []CharacterBrain, api: *Api.Server) ?i64 {
return earliest_cooldown;
}
fn runNextActions(characters: []CharacterBrain, api: *Api.Server) !void {
fn runNextActions(characters: []Brain, api: *Api.Server) !void {
const maybe_earliest_cooldown = earliestCooldown(characters, api);
if (maybe_earliest_cooldown == null) return;