const std = @import("std"); const ArtifactsAPI = @import("artifacts-api"); const Server = ArtifactsAPI.Server; const Allocator = std.mem.Allocator; const Position = Server.Position; const assert = std.debug.assert; const CharacterTask = @import("./task.zig").Task; const CharacterBrain = @This(); const bank_position: Position = .{ .x = 4, .y = 1 }; // TODO: Figure this out dynamically pub const QueuedAction = union(enum) { move: Position, fight, gather, deposit_gold: u64, deposit_item: Server.ItemIdQuantity, withdraw_item: Server.ItemIdQuantity, craft_item: Server.ItemIdQuantity, pub fn perform(self: QueuedAction, api: *Server, name: []const u8, ) !QueuedActionResult { const log = std.log.default; switch (self) { .fight => { log.debug("[{s}] attack", .{name}); return .{ .fight = api.actionFight(name) }; }, .move => |pos| { log.debug("[{s}] move to ({}, {})", .{name, pos.x, pos.y}); return .{ .move = api.actionMove(name, pos.x, pos.y) }; }, .deposit_gold => |quantity| { log.debug("[{s}] deposit {} gold", .{name, quantity}); return .{ .deposit_gold = api.actionBankDepositGold(name, quantity) }; }, .deposit_item => |item| { const code = api.getItemCode(item.id) orelse return error.ItemNotFound; log.debug("[{s}] deposit {s} (x{})", .{name, code, item.quantity}); return .{ .deposit_item = api.actionBankDepositItem(name, code, item.quantity) }; }, .withdraw_item => |item| { const code = api.getItemCode(item.id) orelse return error.ItemNotFound; log.debug("[{s}] withdraw {s} (x{})", .{name, code, item.quantity}); return .{ .withdraw_item = api.actionBankWithdrawItem(name, code, item.quantity) }; }, .gather => { log.debug("[{s}] gather", .{name}); return .{ .gather = api.actionGather(name) }; }, .craft_item => |item| { const code = api.getItemCode(item.id) orelse return error.ItemNotFound; log.debug("[{s}] craft {s} (x{})", .{name, code, item.quantity}); return .{ .craft_item = api.actionCraft(name, code, item.quantity) }; } } } }; const QueuedActionResult = union(enum) { move: Server.MoveError!Server.MoveResult, fight: Server.FightError!Server.FightResult, gather: Server.GatherError!Server.GatherResult, deposit_gold: Server.BankDepositGoldError!Server.GoldTransactionResult, deposit_item: Server.BankDepositItemError!Server.ItemTransactionResult, withdraw_item: Server.BankWithdrawItemError!Server.ItemTransactionResult, craft_item: Server.CraftError!Server.CraftResult, const Tag = @typeInfo(QueuedActionResult).Union.tag_type.?; fn fieldType(comptime kind: Tag) type { const field_type = std.meta.fields(QueuedActionResult)[@intFromEnum(kind)].type; return @typeInfo(field_type).ErrorUnion.payload; } pub fn get(self: QueuedActionResult, comptime kind: Tag) ?fieldType(kind) { return switch (self) { kind => |v| v catch null, else => null }; } }; comptime { assert(@typeInfo(QueuedAction).Union.fields.len == @typeInfo(QueuedActionResult).Union.fields.len); } name: []const u8, action_queue: std.ArrayList(QueuedAction), task: ?*CharacterTask = null, pub fn init(allocator: Allocator, name: []const u8) !CharacterBrain { return CharacterBrain{ .name = try allocator.dupe(u8, name), .action_queue = std.ArrayList(QueuedAction).init(allocator), }; } pub fn deinit(self: CharacterBrain) void { const allocator = self.action_queue.allocator; allocator.free(self.name); self.action_queue.deinit(); } fn currentTime() f64 { const timestamp: f64 = @floatFromInt(std.time.milliTimestamp()); return timestamp / std.time.ms_per_s; } pub fn performNextAction(self: *CharacterBrain, api: *Server) !void { const log = std.log.default; assert(self.action_queue.items.len > 0); const APIError = Server.APIError; const retry_delay = 0.5; // 500ms var character = api.findCharacterPtr(self.name).?; const next_action = self.action_queue.items[0]; const action_result = try next_action.perform(api, self.name); switch (action_result) { .fight => |result| { const FightError = Server.FightError; _ = result catch |err| switch (err) { FightError.CharacterInCooldown, FightError.CharacterIsBusy => { // A bit too eager, retry action character.cooldown_expiration = currentTime() + retry_delay; log.warn("[{s}] retry fighting", .{self.name}); return; }, FightError.CharacterIsFull, FightError.MonsterNotFound => { // Re-evaluate what the character should do, something is not right. log.warn("[{s}] clear action queue", .{self.name}); self.action_queue.clearAndFree(); return; }, FightError.CharacterNotFound, APIError.ServerUnavailable, APIError.RequestFailed, APIError.ParseFailed, APIError.OutOfMemory => { // Welp... Abondon ship. Bail. Bail return err; } }; }, .move => |result| { const MoveError = Server.MoveError; _ = result catch |err| switch (err) { MoveError.CharacterIsBusy, MoveError.CharacterInCooldown => { // A bit too eager, retry action character.cooldown_expiration = currentTime() + retry_delay; log.warn("[{s}] retry moving", .{self.name}); return; }, MoveError.CharacterAtDestination => { // Not great, but I guess the goal achieved? The character is at the desired location. log.warn("[{s}] tried to move, but already at destination", .{self.name}); }, MoveError.MapNotFound => { // Re-evaluate what the character should do, something is not right. log.warn("[{s}] clear action queue", .{self.name}); self.action_queue.clearAndFree(); return; }, MoveError.CharacterNotFound, APIError.ServerUnavailable, APIError.RequestFailed, APIError.ParseFailed, APIError.OutOfMemory => { // Welp... Abondon ship. Bail. Bail return err; } }; }, .deposit_gold => |result| { const BankDepositGoldError = Server.BankDepositGoldError; _ = result catch |err| switch (err) { BankDepositGoldError.BankIsBusy, BankDepositGoldError.CharacterIsBusy, BankDepositGoldError.CharacterInCooldown => { // A bit too eager, retry action character.cooldown_expiration = currentTime() + retry_delay; log.warn("[{s}] retry depositing gold", .{self.name}); return; }, BankDepositGoldError.NotEnoughGold, BankDepositGoldError.BankNotFound => { // Re-evaluate what the character should do, something is not right. log.warn("[{s}] clear action queue", .{self.name}); self.action_queue.clearAndFree(); return; }, BankDepositGoldError.CharacterNotFound, APIError.ServerUnavailable, APIError.RequestFailed, APIError.ParseFailed, APIError.OutOfMemory => { // Welp... Abondon ship. Bail. Bail return err; } }; }, .deposit_item => |result| { const BankDepositItemError = Server.BankDepositItemError; _ = result catch |err| switch (err) { BankDepositItemError.BankIsBusy, BankDepositItemError.CharacterIsBusy, BankDepositItemError.CharacterInCooldown => { // A bit too eager, retry action character.cooldown_expiration = currentTime() + retry_delay; log.warn("[{s}] retry depositing item", .{self.name}); return; }, BankDepositItemError.ItemNotFound, BankDepositItemError.NotEnoughItems, BankDepositItemError.BankNotFound => { // Re-evaluate what the character should do, something is not right. log.warn("[{s}] clear action queue", .{self.name}); self.action_queue.clearAndFree(); return; }, BankDepositItemError.CharacterNotFound, APIError.ServerUnavailable, APIError.RequestFailed, APIError.ParseFailed, APIError.OutOfMemory => { // Welp... Abondon ship. Bail. Bail return err; } }; }, .withdraw_item => |result| { const BankWithdrawItemError = Server.BankWithdrawItemError; _ = result catch |err| switch (err) { BankWithdrawItemError.CharacterIsBusy, BankWithdrawItemError.CharacterInCooldown, BankWithdrawItemError.BankIsBusy => { // A bit too eager, retry action character.cooldown_expiration = currentTime() + retry_delay; log.warn("[{s}] retry withdrawing item", .{self.name}); return; }, BankWithdrawItemError.ItemNotFound, BankWithdrawItemError.NotEnoughItems, BankWithdrawItemError.CharacterIsFull, BankWithdrawItemError.BankNotFound => { // Re-evaluate what the character should do, something is not right. log.warn("[{s}] clear action queue", .{self.name}); self.action_queue.clearAndFree(); return; }, BankWithdrawItemError.CharacterNotFound, APIError.ServerUnavailable, APIError.RequestFailed, APIError.ParseFailed, APIError.OutOfMemory => { // Welp... Abondon ship. Bail. Bail return err; } }; }, .gather => |result| { const GatherError = Server.GatherError; _ = result catch |err| switch (err) { GatherError.CharacterInCooldown, GatherError.CharacterIsBusy => { // A bit too eager, retry action character.cooldown_expiration = currentTime() + retry_delay; log.warn("[{s}] retry withdrawing item", .{self.name}); return; }, GatherError.NotEnoughSkill, GatherError.CharacterIsFull, GatherError.ResourceNotFound => { // Re-evaluate what the character should do, something is not right. log.warn("[{s}] clear action queue", .{self.name}); self.action_queue.clearAndFree(); return; }, GatherError.CharacterNotFound, APIError.ServerUnavailable, APIError.RequestFailed, APIError.ParseFailed, APIError.OutOfMemory => { // Welp... Abondon ship. Bail. Bail return err; } }; }, .craft_item => |result| { const CraftError = Server.CraftError; _ = result catch |err| switch (err) { CraftError.CharacterInCooldown, CraftError.CharacterIsBusy => { // A bit too eager, retry action character.cooldown_expiration = currentTime() + retry_delay; log.warn("[{s}] retry withdrawing item", .{self.name}); return; }, CraftError.RecipeNotFound, CraftError.NotEnoughItems, CraftError.NotEnoughSkill, CraftError.CharacterIsFull, CraftError.WorkshopNotFound => { // Re-evaluate what the character should do, something is not right. log.warn("[{s}] clear action queue", .{self.name}); self.action_queue.clearAndFree(); return; }, CraftError.CharacterNotFound, APIError.ServerUnavailable, APIError.RequestFailed, APIError.ParseFailed, APIError.OutOfMemory => { // Welp... Abondon ship. Bail. Bail return err; } }; } } _ = self.action_queue.orderedRemove(0); self.onActionCompleted(action_result); } fn onActionCompleted(self: *CharacterBrain, result: QueuedActionResult) void { if (self.task == null) return; switch (self.task.?.*) { .fight => |*args| { if (result.get(.fight)) |r| { const fight_result: Server.FightResult = r; const drops = fight_result.fight.drops; args.progress += drops.getQuantity(args.until.item.id); } }, .gather => |*args| { if (result.get(.gather)) |r| { const gather_resutl: Server.GatherResult = r; const items = gather_resutl.details.items; args.progress += items.getQuantity(args.until.item.id); } }, .craft => |*args| { if (result.get(.craft_item)) |r| { const craft_result: Server.CraftResult = r; const items = craft_result.details.items; args.progress += items.getQuantity(args.target.id); } } } } pub fn isTaskFinished(self: *CharacterBrain) bool { if (self.task == null) { return false; } return self.task.?.isComplete(); } pub fn performTask(self: *CharacterBrain, api: *Server) !void { if (self.task == null) { std.log.debug("[{s}] idle", .{self.name}); return; } switch (self.task.?.*) { .fight => |args| { try self.fightRoutine(api, args.at); }, .gather => |args| { try self.gatherRoutine(api, args.at); }, .craft => |args| { try self.craftRoutine(api, args.at, args.target.id, args.target.quantity); } } } fn moveIfNeeded(self: *CharacterBrain, api: *Server, pos: Position) !bool { const character = api.findCharacter(self.name).?; if (character.position.eql(pos)) { return false; } try self.action_queue.append(.{ .move = pos }); return true; } pub fn depositItemsToBank(self: *CharacterBrain, api: *Server) !bool { var character = api.findCharacter(self.name).?; const action_queue = &self.action_queue; // Deposit items and gold to bank if full if (character.getItemCount() == 0) { return false; } if (!character.position.eql(bank_position)) { try action_queue.append(.{ .move = bank_position }); } for (character.inventory.slice()) |slot| { try action_queue.append(.{ .deposit_item = .{ .id = slot.id, .quantity = slot.quantity } }); } return true; } fn depositIfFull(self: *CharacterBrain, api: *Server) !bool { const character = api.findCharacter(self.name).?; if (character.getItemCount() < character.inventory_max_items) { return false; } _ = try self.depositItemsToBank(api); if (character.gold > 0) { try self.action_queue.append(.{ .deposit_gold = @intCast(character.gold) }); } return true; } fn fightRoutine(self: *CharacterBrain, api: *Server, enemy_position: Position) !void { if (try self.depositIfFull(api)) { return; } if (try self.moveIfNeeded(api, enemy_position)) { return; } try self.action_queue.append(.{ .fight = {} }); } fn gatherRoutine(self: *CharacterBrain, api: *Server, resource_position: Position) !void { if (try self.depositIfFull(api)) { return; } if (try self.moveIfNeeded(api, resource_position)) { return; } try self.action_queue.append(.{ .gather = {} }); } fn withdrawFromBank(self: *CharacterBrain, api: *Server, items: []const Server.Slot) !bool { var character = api.findCharacter(self.name).?; var has_all_items = true; for (items) |item_quantity| { const inventory_quantity = character.inventory.getQuantity(item_quantity.id); if(inventory_quantity < item_quantity.quantity) { has_all_items = false; break; } } if (has_all_items) return false; if (try self.moveIfNeeded(api, bank_position)) { return true; } for (items) |item_quantity| { const inventory_quantity = character.inventory.getQuantity(item_quantity.id); if(inventory_quantity < item_quantity.quantity) { try self.action_queue.append(.{ .withdraw_item = .{ .id = item_quantity.id, .quantity = item_quantity.quantity - inventory_quantity, }}); } } return true; } fn craftItem(self: *CharacterBrain, api: *Server, workstation: Position, id: Server.ItemId, quantity: u64) !bool { var character = api.findCharacter(self.name).?; const inventory_quantity = character.inventory.getQuantity(id); if (inventory_quantity >= quantity) { return false; } if (try self.moveIfNeeded(api, workstation)) { return true; } try self.action_queue.append(.{ .craft_item = .{ .id = id, .quantity = quantity - inventory_quantity }}); return true; } fn craftRoutine(self: *CharacterBrain, api: *Server, workstation: Position, id: Server.ItemId, quantity: u64) !void { var character = api.findCharacter(self.name).?; const inventory_quantity = character.inventory.getQuantity(id); if (inventory_quantity >= quantity) { if (try self.depositItemsToBank(api)) { return; } } const code = api.getItemCode(id) orelse return error.InvalidItemId; const target_item = try api.getItem(code) orelse return error.ItemNotFound; if (target_item.craft == null) { return error.NotCraftable; } const recipe = target_item.craft.?; var needed_items = recipe.items; for (needed_items.slice()) |*needed_item| { needed_item.quantity *= quantity; } if (try self.withdrawFromBank(api, needed_items.slice())) { return; } if (try self.craftItem(api, workstation, id, quantity)) { return; } }