add item crating

This commit is contained in:
Rokas Puzonas 2024-08-10 15:11:21 +03:00
parent 7a1b7971a9
commit b33754efb8
3 changed files with 528 additions and 129 deletions

View File

@ -160,6 +160,38 @@ pub const EquipmentSlot = enum {
}
};
pub const Skill = enum {
weaponcrafting,
gearcrafting,
jewelrycrafting,
cooking,
woodcutting,
mining,
fn parse(str: []const u8) ?Skill {
const eql = std.mem.eql;
const mapping = .{
.{ "weaponcrafting" , .weaponcrafting },
.{ "gearcrafting" , .gearcrafting },
.{ "jewelrycrafting", .jewelrycrafting },
.{ "cooking" , .cooking },
.{ "woodcutting" , .woodcutting },
.{ "mining" , .mining },
};
if (mapping.len != @typeInfo(Skill).Enum.fields.len) {
@compileLog("Mapping is not exhaustive");
}
inline for (mapping) |mapping_entry| {
if (eql(u8, str, mapping_entry[0])) {
return mapping_entry[1];
}
}
return null;
}
};
const ServerStatus = struct {
allocator: Allocator,
status: []const u8,
@ -250,35 +282,32 @@ pub const Cooldown = struct {
fn parse(str: []const u8) ?Reason {
const eql = std.mem.eql;
if (eql(u8, str, "movement")) {
return .movement;
} else if (eql(u8, str, "fight")) {
return .fight;
} else if (eql(u8, str, "crafting")) {
return .crafting;
} else if (eql(u8, str, "gathering")) {
return .gathering;
} else if (eql(u8, str, "buy_ge")) {
return .buy_ge;
} else if (eql(u8, str, "sell_ge")) {
return .sell_ge;
} else if (eql(u8, str, "delete_item")) {
return .delete_item;
} else if (eql(u8, str, "deposit_bank")) {
return .deposit_bank;
} else if (eql(u8, str, "withdraw_bank")) {
return .withdraw_bank;
} else if (eql(u8, str, "equip")) {
return .equip;
} else if (eql(u8, str, "unequip")) {
return .unequip;
} else if (eql(u8, str, "task")) {
return .task;
} else if (eql(u8, str, "recycling")) {
return .recycling;
} else {
return null;
const mapping = .{
.{ "movement" , .movement },
.{ "fight" , .fight },
.{ "crafting" , .crafting },
.{ "gathering" , .gathering },
.{ "buy_ge" , .buy_ge },
.{ "sell_ge" , .sell_ge },
.{ "delete_item" , .delete_item },
.{ "deposit_bank" , .deposit_bank },
.{ "withdraw_bank", .withdraw_bank },
.{ "equip" , .equip },
.{ "unequip" , .unequip },
.{ "task" , .task },
.{ "recycling" , .recycling },
};
if (mapping.len != @typeInfo(Reason).Enum.fields.len) {
@compileLog("Mapping is not exhaustive");
}
inline for (mapping) |mapping_entry| {
if (eql(u8, str, mapping_entry[0])) {
return mapping_entry[1];
}
}
return null;
}
};
@ -365,44 +394,52 @@ pub const FightResult = struct {
}
};
pub const ItemQuantity = struct {
pub const ItemIdQuantity = struct {
id: ItemId,
quantity: u64,
pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !ItemQuantity {
pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !ItemIdQuantity {
const code = try json_utils.getStringRequired(obj, "code");
const quantity = try json_utils.getIntegerRequired(obj, "quantity");
if (quantity < 1) return error.InvalidQuantity;
return ItemQuantity{
return ItemIdQuantity{
.id = try api.getItemId(code),
.quantity = @intCast(quantity)
};
}
};
pub const SkillDetails = struct {
xp: i64,
items: std.BoundedArray(ItemQuantity, 8),
const BoundedItems = std.BoundedArray(ItemIdQuantity, 8);
fn parseSimpleItemList(api: *ArtifactsAPI, array: json.Array) !BoundedItems {
var items = BoundedItems.init(0) catch unreachable;
fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !SkillDetails {
var items = std.BoundedArray(ItemQuantity, 8).init(0) catch unreachable;
const items_obj = json_utils.getArray(obj, "items") orelse return error.MissingProperty;
for (items_obj.items) |item_value| {
for (array.items) |item_value| {
const item_obj = json_utils.asObject(item_value) orelse return error.MissingProperty;
try items.append(ItemQuantity.parse(api, item_obj));
try items.append(try ItemIdQuantity.parse(api, item_obj));
}
return SkillDetails{
return items;
}
pub const SkillResultDetails = struct {
xp: i64,
items: BoundedItems,
fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !SkillResultDetails {
const items = json_utils.getArray(obj, "items") orelse return error.MissingProperty;
return SkillResultDetails{
.xp = try json_utils.getIntegerRequired(obj, "xp"),
.items = items,
.items = try parseSimpleItemList(api, items),
};
}
};
pub const GatherResult = struct {
cooldown: Cooldown,
details: SkillDetails,
details: SkillResultDetails,
pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !GatherResult {
const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty;
@ -410,7 +447,7 @@ pub const GatherResult = struct {
return GatherResult{
.cooldown = try Cooldown.parse(cooldown),
.details = try SkillDetails.parse(api, details)
.details = try SkillResultDetails.parse(api, details)
};
}
@ -531,15 +568,11 @@ pub const ItemTransactionResult = struct {
else => null
};
}
pub fn deinit(self: ItemTransactionResult) void {
_ = self;
}
};
pub const CraftResult = struct {
cooldown: Cooldown,
details: SkillDetails,
details: SkillResultDetails,
pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !CraftResult {
const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty;
@ -547,7 +580,7 @@ pub const CraftResult = struct {
return CraftResult{
.cooldown = try Cooldown.parse(cooldown),
.details = try SkillDetails.parse(api, details)
.details = try SkillResultDetails.parse(api, details)
};
}
@ -670,6 +703,72 @@ pub const MapResult = struct {
}
};
pub const Item = struct {
pub const Recipe = struct {
skill: Skill,
level: u64,
quantity: u64,
items: BoundedItems,
pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap) !Recipe {
const skill = json_utils.getString(obj, "skill") orelse return error.MissingProperty;
const level = json_utils.getInteger(obj, "level") orelse return error.MissingProperty;
if (level < 1) return error.InvalidLevel;
const quantity = json_utils.getInteger(obj, "quantity") orelse return error.MissingProperty;
if (quantity < 1) return error.InvalidQuantity;
const items = json_utils.getArray(obj, "items") orelse return error.MissingProperty;
return Recipe{
.skill = Skill.parse(skill) orelse return error.InvalidSkill,
.level = @intCast(level),
.quantity = @intCast(quantity),
.items = try parseSimpleItemList(api, items)
};
}
};
allocator: Allocator,
name: []u8,
code: []u8,
level: u64,
type: []u8,
subtype: []u8,
description: []u8,
craft: ?Recipe,
// TODO: effects
// TODO: Grand exchange
pub fn parse(api: *ArtifactsAPI, obj: json.ObjectMap, allocator: Allocator) !Item {
const item_obj = json_utils.getObject(obj, "item") orelse return error.MissingProperty;
const level = json_utils.getInteger(item_obj, "level") orelse return error.MissingProperty;
if (level < 1) return error.InvalidLevel;
const craft = json_utils.getObject(item_obj, "craft");
return Item{
.allocator = allocator,
.name = (try json_utils.dupeString(allocator, item_obj, "name")) orelse return error.MissingProperty,
.code = (try json_utils.dupeString(allocator, item_obj, "code")) orelse return error.MissingProperty,
.level = @intCast(level),
.type = (try json_utils.dupeString(allocator, item_obj, "type")) orelse return error.MissingProperty,
.subtype = (try json_utils.dupeString(allocator, item_obj, "subtype")) orelse return error.MissingProperty,
.description = (try json_utils.dupeString(allocator, item_obj, "description")) orelse return error.MissingProperty,
.craft = if (craft != null) try Recipe.parse(api, craft.?) else null
};
}
pub fn deinit(self: Item) void {
self.allocator.free(self.name);
self.allocator.free(self.code);
self.allocator.free(self.type);
self.allocator.free(self.subtype);
self.allocator.free(self.description);
}
};
pub const ArtifactsFetchResult = struct {
arena: std.heap.ArenaAllocator,
status: std.http.Status,
@ -711,6 +810,8 @@ const FetchOptions = struct {
path: []const u8,
payload: ?[]const u8 = null,
query: ?[]const u8 = null,
page: ?u64 = null,
page_size: ?u64 = null,
paginated: bool = false
@ -743,12 +844,23 @@ fn fetch(self: *ArtifactsAPI, options: FetchOptions) APIError!ArtifactsFetchResu
var fetch_results = std.ArrayList(json.Value).init(arena.allocator());
while (true) : (current_page += 1) {
var query_params: ?[]u8 = null;
defer if (query_params) |str| self.allocator.free(str);
var pagination_params: ?[]u8 = null;
defer if (pagination_params) |str| self.allocator.free(str);
if (options.paginated) {
query_params = try allocPaginationParams(self.allocator, current_page, options.page_size);
uri.query = .{ .raw = query_params.? };
pagination_params = try allocPaginationParams(self.allocator, current_page, options.page_size);
}
if (options.query != null and pagination_params != null) {
const combined = try std.mem.join(self.allocator, "&", &.{ options.query.?, pagination_params.? });
self.allocator.free(pagination_params.?);
pagination_params = combined;
uri.query = .{ .raw = combined };
} else if (pagination_params != null) {
uri.query = .{ .raw = pagination_params.? };
} else if (options.query != null) {
uri.query = .{ .raw = options.query.? };
}
var response_storage = std.ArrayList(u8).init(arena.allocator());
@ -791,6 +903,7 @@ fn fetch(self: *ArtifactsAPI, options: FetchOptions) APIError!ArtifactsFetchResu
if (options.paginated) {
const total_pages_i64 = json_utils.getInteger(parsed.object, "pages") orelse return APIError.ParseFailed;
if (total_pages_i64 < 1) return APIError.ParseFailed;
total_pages = @intCast(total_pages_i64);
const page_results = json_utils.getArray(parsed.object, "data") orelse return APIError.ParseFailed;
@ -835,13 +948,6 @@ fn handleFetchError(
}
}
if (status == .not_found) {
return null;
}
if (status != .ok) {
return APIError.RequestFailed;
}
return null;
}
@ -864,6 +970,12 @@ fn fetchOptionalObject(
if (handleFetchError(result.status, Error, parseError)) |error_value| {
return error_value;
}
if (result.status == .not_found) {
return null;
}
if (result.status != .ok) {
return APIError.RequestFailed;
}
if (result.body == null) {
return APIError.ParseFailed;
@ -886,7 +998,26 @@ fn fetchObject(
return result orelse return APIError.RequestFailed;
}
fn fetchArray(
fn ObjectList(Object: type) type {
return struct {
list: std.ArrayList(Object),
pub fn deinit(self: @This()) void {
for (self.list.items) |*item| {
if (std.meta.hasMethod(@TypeOf(item), "deinit")) {
item.deinit();
}
}
self.deinitList();
}
pub fn deinitList(self: @This()) void {
self.list.deinit();
}
};
}
fn fetchOptionalArray(
self: *ArtifactsAPI,
allocator: Allocator,
Error: type,
@ -895,7 +1026,7 @@ fn fetchArray(
parseObject: anytype,
parseObjectArgs: anytype,
fetchOptions: FetchOptions
) Error!std.ArrayList(Object) {
) Error!?ObjectList(Object) {
if (@typeInfo(@TypeOf(parseObject)) != .Fn) {
@compileError("`parseObject` must be a function");
}
@ -906,23 +1037,56 @@ fn fetchArray(
if (handleFetchError(result.status, Error, parseError)) |error_value| {
return error_value;
}
if (result.status == .not_found) {
return null;
}
if (result.status != .ok) {
return APIError.RequestFailed;
}
if (result.body == null) {
return APIError.ParseFailed;
}
var array = std.ArrayList(Object).init(allocator);
errdefer array.deinit();
var object_array = ObjectList(Object){
.list = std.ArrayList(Object).init(allocator)
};
errdefer object_array.deinit();
// var array = std.ArrayList(Object).init(allocator);
// errdefer {
// if (std.meta.hasFn(Object, "deinit")) {
// for (array.items) |item| {
// if (@typeInfo(Object.deinit).Fn.args.len == 1) {
// item.deinit();
// }
// }
// }
// array.deinit();
// }
const result_data = json_utils.asArray(result.body.?) orelse return APIError.ParseFailed;
for (result_data.items) |result_item| {
const item_obj = json_utils.asObject(result_item) orelse return APIError.ParseFailed;
const parsed_item = @call(.auto, parseObject, .{ self, item_obj } ++ parseObjectArgs) catch return APIError.ParseFailed;
array.append(parsed_item) catch return APIError.OutOfMemory;
object_array.list.append(parsed_item) catch return APIError.OutOfMemory;
}
return array;
return object_array;
}
fn fetchArray(
self: *ArtifactsAPI,
allocator: Allocator,
Error: type,
parseError: ?fn (status: std.http.Status) ?Error,
Object: type,
parseObject: anytype,
parseObjectArgs: anytype,
fetchOptions: FetchOptions
) Error!ObjectList(Object) {
const result = try self.fetchOptionalArray(allocator, Error, parseError, Object, parseObject, parseObjectArgs, fetchOptions);
return result orelse return APIError.RequestFailed;
}
pub fn setServer(self: *ArtifactsAPI, url: []const u8) !void {
@ -995,7 +1159,7 @@ pub fn getCharacter(self: *ArtifactsAPI, allocator: Allocator, name: []const u8)
);
}
pub fn listMyCharacters(self: *ArtifactsAPI, allocator: Allocator) APIError!std.ArrayList(Character) {
pub fn listMyCharacters(self: *ArtifactsAPI, allocator: Allocator) APIError!ObjectList(Character) {
return self.fetchArray(
allocator,
APIError,
@ -1205,20 +1369,48 @@ pub fn getBankGold(self: *ArtifactsAPI) APIError!u64 {
const data = json_utils.asObject(result.body.?) orelse return APIError.RequestFailed;
const quantity = json_utils.getInteger(data, "quantity") orelse return APIError.ParseFailed;
if (quantity < 0) return APIError.ParseFailed;
return @intCast(quantity);
}
pub fn getBankItems(self: *ArtifactsAPI, allocator: Allocator) APIError!std.ArrayList(ItemQuantity) {
pub fn getBankItems(self: *ArtifactsAPI, allocator: Allocator) APIError!ObjectList(ItemIdQuantity) {
return self.fetchArray(
allocator,
APIError,
null,
ItemQuantity,
ItemQuantity.parse, .{},
ItemIdQuantity,
ItemIdQuantity.parse, .{},
.{ .method = .GET, .path = "/my/bank/items", .paginated = true }
);
}
pub fn getBankItemQuantity(self: *ArtifactsAPI, code: []const u8) APIError!?u64 {
const query = try std.fmt.allocPrint(self.allocator, "item_code={s}", .{code});
defer self.allocator.free(query);
const maybe_items = try self.fetchOptionalArray(
self.allocator,
APIError,
null,
ItemIdQuantity,
ItemIdQuantity.parse, .{},
.{ .method = .GET, .path = "/my/bank/items", .query = query, .paginated = true }
);
if (maybe_items == null) {
return null;
}
const items = maybe_items.?;
defer items.deinit();
const list_items = items.list.items;
assert(list_items.len == 1);
assert(list_items[0].id == try self.getItemId(code));
return list_items[0].quantity;
}
pub fn getMap(self: *ArtifactsAPI, allocator: Allocator, x: i64, y: i64) APIError!?MapResult {
const path = try std.fmt.allocPrint(self.allocator, "/maps/{}/{}", .{x, y});
defer self.allocator.free(path);
@ -1243,6 +1435,19 @@ pub fn getMaps(self: *ArtifactsAPI, allocator: Allocator) APIError!ObjectList(Ma
);
}
pub fn getItem(self: *ArtifactsAPI, allocator: Allocator, code: []const u8) APIError!?Item {
const path = try std.fmt.allocPrint(self.allocator, "/items/{s}", .{code});
defer self.allocator.free(path);
return self.fetchOptionalObject(
APIError,
null,
Item,
Item.parse, .{ allocator },
.{ .method = .GET, .path = path }
);
}
test "parse date time" {
try std.testing.expectEqual(1723069394.105, parseDateTime("2024-08-07T22:23:14.105Z").?);
}

View File

@ -102,12 +102,15 @@ pub const Inventory = struct {
pub const Slot = struct {
id: ?ItemId,
quantity: i64,
quantity: u64,
fn parse(api: *ArtifactsAPI, slot_obj: json.ObjectMap) !Slot {
const quantity = try json_utils.getIntegerRequired(slot_obj, "quantity");
if (quantity < 0) return error.InvalidQuantity;
return Slot{
.id = try getItemId(api, slot_obj, "code"),
.quantity = try json_utils.getIntegerRequired(slot_obj, "quantity"),
.quantity = @intCast(quantity),
};
}
};
@ -140,7 +143,7 @@ pub const Inventory = struct {
const slot = self.findSlot(id) orelse unreachable;
assert(slot.quantity >= quantity);
slot.quantity -= @intCast(quantity);
slot.quantity -= quantity;
if (slot.quantity == 0) {
slot.id = null;
}
@ -148,7 +151,7 @@ pub const Inventory = struct {
pub fn addItem(self: *Inventory, id: ItemId, quantity: u64) void {
if (self.findSlot(id)) |slot| {
slot.quantity += @intCast(quantity);
slot.quantity += quantity;
} else {
var empty_slot: ?*Slot = null;
for (&self.slots) |*slot| {
@ -159,9 +162,29 @@ pub const Inventory = struct {
assert(empty_slot != null);
empty_slot.?.id = id;
empty_slot.?.quantity = @intCast(quantity);
empty_slot.?.quantity = quantity;
}
}
pub fn addItems(self: *Inventory, items: []const ArtifactsAPI.ItemIdQuantity) void {
for (items) |item| {
self.addItem(item.id, item.quantity);
}
}
pub fn removeItems(self: *Inventory, items: []const ArtifactsAPI.ItemIdQuantity) void {
for (items) |item| {
self.removeItem(item.id, item.quantity);
}
}
pub fn getItem(self: *Inventory, id: ItemId) u64 {
if (self.findSlot(id)) |slot| {
return slot.quantity;
}
return 0;
}
};
arena: *std.heap.ArenaAllocator,
@ -249,10 +272,10 @@ pub fn deinit(self: *Character) void {
child_allocator.destroy(self.arena);
}
pub fn getItemCount(self: *const Character) u32 {
var count: u32 = 0;
pub fn getItemCount(self: *const Character) u64 {
var count: u64 = 0;
for (self.inventory.slots) |slot| {
count += @intCast(slot.quantity);
count += slot.quantity;
}
return count;
}

View File

@ -25,8 +25,10 @@ const QueuedAction = union(enum) {
move: Position,
attack,
gather,
depositGold: u64,
depositItem: struct { id: ArtifactsAPI.ItemId, quantity: u64 },
deposit_gold: u64,
deposit_item: ArtifactsAPI.ItemIdQuantity,
withdraw_item: ArtifactsAPI.ItemIdQuantity,
craft_item: ArtifactsAPI.ItemIdQuantity,
};
const ActionQueue = std.ArrayList(QueuedAction);
@ -35,6 +37,10 @@ const ManagedCharacter = struct {
character: ArtifactsAPI.Character,
action_queue: ActionQueue,
cooldown_expires_at: f64,
pub fn position(self: *const ManagedCharacter) Position {
return Position{ .x = self.character.x, .y = self.character.y };
}
};
fn currentTime() f64 {
@ -46,11 +52,15 @@ const Manager = struct {
allocator: Allocator,
characters: std.ArrayList(ManagedCharacter),
api: *ArtifactsAPI,
known_items: std.StringHashMap(ArtifactsAPI.Item),
bank_position: Position = .{ .x = 4, .y = 1 },
fn init(allocator: Allocator, api: *ArtifactsAPI) Manager {
return Manager{
.allocator = allocator,
.api = api,
.known_items = std.StringHashMap(ArtifactsAPI.Item).init(allocator),
.characters = std.ArrayList(ManagedCharacter).init(allocator),
};
}
@ -92,28 +102,74 @@ const Manager = struct {
return null;
}
fn deinit(self: Manager) void {
fn getItem(self: *Manager, code: []const u8) !?ArtifactsAPI.Item {
if (self.known_items.get(code)) |item| {
return item;
}
const maybe_item = try self.api.getItem(self.allocator, code);
if (maybe_item == null) {
std.log.warn("attempt to get item '{s}' which does not exist", .{code});
return null;
}
const item = maybe_item.?;
try self.known_items.putNoClobber(item.code, item);
return item;
}
fn deinit(self: *Manager) void {
for (self.characters.items) |managed_character| {
managed_character.action_queue.deinit();
}
self.characters.deinit();
var known_items_iter = self.known_items.valueIterator();
while (known_items_iter.next()) |item| {
item.deinit();
}
self.known_items.deinit();
}
fn getWorkstation(self: *const Manager, skill: ArtifactsAPI.Skill) Position {
_ = self;
// TODO: Find workstation using map endpoint
return switch (skill) {
.weaponcrafting => Position{ .x = 2, .y = 1 },
.gearcrafting => Position{ .x = 3, .y = 1 },
.jewelrycrafting => Position{ .x = 1, .y = 3 },
.cooking => Position{ .x = 1, .y = 1 },
.woodcutting => Position{ .x = -2, .y = -3 },
.mining => Position{ .x = 1, .y = 5 },
};
}
};
fn depositIfFull(managed_char: *ManagedCharacter) !bool {
fn moveIfNeeded(char: *ManagedCharacter, pos: Position) !bool {
if (char.position().eql(pos)) {
return false;
}
try char.action_queue.append(.{ .move = pos });
return true;
}
fn depositItemsToBank(manager: *Manager, managed_char: *ManagedCharacter) !bool {
const character = managed_char.character;
const action_queue = &managed_char.action_queue;
// Deposit items and gold to bank if full
if (character.getItemCount() < character.inventory_max_items) {
if (character.getItemCount() == 0) {
return false;
}
var character_pos = Position.init(character.x, character.y);
const bank_pos = Position{ .x = 4, .y = 1 };
var character_pos = managed_char.position();
if (!character_pos.eql(bank_pos)) {
try action_queue.append(.{ .move = bank_pos });
if (!character_pos.eql(manager.bank_position)) {
try action_queue.append(.{ .move = manager.bank_position });
}
for (character.inventory.slots) |slot| {
@ -121,58 +177,140 @@ fn depositIfFull(managed_char: *ManagedCharacter) !bool {
if (slot.id) |item_id| {
try action_queue.append(.{
.depositItem = .{ .id = item_id, .quantity = @intCast(slot.quantity) }
.deposit_item = .{ .id = item_id, .quantity = @intCast(slot.quantity) }
});
}
}
if (character.gold > 0) {
try action_queue.append(.{ .depositGold = @intCast(character.gold) });
}
return true;
}
fn attackChickenRoutine(managed_char: *ManagedCharacter) !void {
const character = managed_char.character;
const action_queue = &managed_char.action_queue;
fn depositIfFull(manager: *Manager, char: *ManagedCharacter) !bool {
const character = char.character;
if (character.getItemCount() < character.inventory_max_items) {
return false;
}
_ = try depositItemsToBank(manager, char);
if (character.gold > 0) {
try char.action_queue.append(.{ .deposit_gold = @intCast(character.gold) });
}
}
fn attackChickenRoutine(manager: *Manager, managed_char: *ManagedCharacter) !void {
const chicken_pos = Position{ .x = 0, .y = 1 };
var character_pos = Position.init(character.x, character.y);
// Deposit items and gold to bank if full
if (try depositIfFull(managed_char)) {
if (try depositIfFull(manager, managed_char)) {
return;
}
// Go to chickens
if (!character_pos.eql(chicken_pos)) {
try action_queue.append(.{ .move = chicken_pos });
if (try moveIfNeeded(managed_char, chicken_pos)) {
return;
}
// Attack chickens
try action_queue.append(.{ .attack = {} });
try managed_char.action_queue.append(.{ .attack = {} });
}
fn gatherResourceRoutine(managed_char: *ManagedCharacter, resource_pos: Position) !void {
const character = managed_char.character;
const action_queue = &managed_char.action_queue;
var character_pos = Position.init(character.x, character.y);
// Deposit items and gold to bank if full
if (try depositIfFull(managed_char)) {
fn gatherResourceRoutine(manager: *Manager, managed_char: *ManagedCharacter, resource_pos: Position) !void {
if (try depositIfFull(manager, managed_char)) {
return;
}
if (!character_pos.eql(resource_pos)) {
try action_queue.append(.{ .move = resource_pos });
if (try moveIfNeeded(managed_char, resource_pos)) {
return;
}
try action_queue.append(.{ .gather = {} });
try managed_char.action_queue;.append(.{ .gather = {} });
}
fn withdrawFromBank(manager: *Manager, char: *ManagedCharacter, items: []const ArtifactsAPI.ItemIdQuantity) !bool {
var has_all_items = true;
for (items) |item_quantity| {
const inventory_quantity = char.character.inventory.getItem(item_quantity.id);
if(inventory_quantity < item_quantity.quantity) {
has_all_items = false;
break;
}
}
if (has_all_items) return false;
if (try moveIfNeeded(char, manager.bank_position)) {
return true;
}
for (items) |item_quantity| {
const inventory_quantity = char.character.inventory.getItem(item_quantity.id);
if(inventory_quantity < item_quantity.quantity) {
try char.action_queue.append(.{ .withdraw_item = .{
.id = item_quantity.id,
.quantity = item_quantity.quantity - inventory_quantity,
}});
}
}
return true;
}
fn craftItem(manager: *Manager, char: *ManagedCharacter, id: ArtifactsAPI.ItemId, quantity: u64) !bool {
const inventory_quantity = char.character.inventory.getItem(id);
if (inventory_quantity >= quantity) {
return false;
}
const code = manager.api.getItemCode(id) orelse return error.InvalidItemId;
const item = try manager.getItem(code) orelse return error.ItemNotFound;
if (item.craft == null) {
return error.NotCraftable;
}
const recipe = item.craft.?;
const workstation = manager.getWorkstation(recipe.skill);
if (try moveIfNeeded(char, workstation)) {
return true;
}
try char.action_queue.append(.{ .craft_item = .{
.id = id,
.quantity = quantity - inventory_quantity
}});
return true;
}
fn craftItemFromBank(manager: *Manager, char: *ManagedCharacter, id: ArtifactsAPI.ItemId, quantity: u64) !bool {
const inventory_quantity = char.character.inventory.getItem(id);
if (inventory_quantity >= quantity) {
return false;
}
const code = manager.api.getItemCode(id) orelse return error.InvalidItemId;
const target_item = try manager.getItem(code) orelse return error.ItemNotFound;
if (target_item.craft == null) {
return error.NotCraftable;
}
const recipe = target_item.craft.?;
assert(recipe.quantity == 1); // TODO: Add support for recipe which produce multiple items
var needed_items = recipe.items;
for (needed_items.slice()) |*needed_item| {
needed_item.quantity *= quantity;
}
if (try withdrawFromBank(manager, char, needed_items.constSlice())) {
return true;
}
if (try craftItem(manager, char, id, quantity)) {
return true;
}
return true;
}
pub fn main() !void {
@ -208,14 +346,13 @@ pub fn main() !void {
std.log.info("Server status: {s} v{s}", .{ status.status, status.version });
std.log.info("Characters online: {}", .{ status.characters_online });
const characters = try api.listMyCharacters();
const characters = try api.listMyCharacters(allocator);
defer characters.deinit();
for (characters.items) |character| {
try manager.addCharacter(character);
}
// for (characters.list.items) |character| {
// try manager.addCharacter(character);
// }
try manager.addCharacter(characters.list.items[0]);
std.log.info("Starting main loop", .{});
while (manager.poll()) |char| {
@ -243,32 +380,56 @@ pub fn main() !void {
char.character.x = pos.x;
char.character.y = pos.y;
},
.depositGold => |quantity| {
.deposit_gold => |quantity| {
std.log.debug("deposit {} gold from {s}", .{quantity, char.character.name});
var result = try api.actionBankDepositGold(char.character.name, quantity);
defer result.deinit();
const result = try api.actionBankDepositGold(char.character.name, quantity);
cooldown = result.cooldown;
char.character.gold -= @intCast(quantity);
assert(char.character.gold >= 0);
},
.depositItem => |item| {
std.log.debug("deposit {s}(x{}) from {s}", .{api.getItemCode(item.id).?, item.quantity, char.character.name});
.deposit_item => |item| {
const code = api.getItemCode(item.id) orelse return error.ItemNotFound;
var result = try api.actionBankDepositItem(char.character.name, code, item.quantity);
defer result.deinit();
std.log.debug("deposit {s} (x{}) from {s}", .{code, item.quantity, char.character.name});
const result = try api.actionBankDepositItem(char.character.name, code, item.quantity);
cooldown = result.cooldown;
char.character.inventory.removeItem(item.id, item.quantity);
},
.withdraw_item => |item| {
const code = api.getItemCode(item.id) orelse return error.ItemNotFound;
std.log.debug("withdraw {s} (x{}) from {s}", .{code, item.quantity, char.character.name});
const result = try api.actionBankWithdrawItem(char.character.name, code, item.quantity);
cooldown = result.cooldown;
char.character.inventory.addItem(item.id, item.quantity);
},
.gather => {
std.log.debug("{s} gathers", .{char.character.name});
var result = try api.actionGather(char.character.name);
cooldown = result.cooldown;
for (result.details.items.slice()) |item| {
char.character.inventory.addItem(item.id, @intCast(item.quantity));
char.character.inventory.addItems(result.details.items.slice());
},
.craft_item => |item| {
const code = api.getItemCode(item.id) orelse return error.ItemNotFound;
std.log.debug("craft {s} (x{}) from {s}", .{code, item.quantity, char.character.name});
var result = try api.actionCraft(char.character.name, code, item.quantity);
cooldown = result.cooldown;
var inventory = &char.character.inventory;
const item_details = (try manager.getItem(code)) orelse return error.ItemNotFound;
const recipe = item_details.craft orelse return error.RecipeNotFound;
for (recipe.items.slice()) |recipe_item| {
inventory.removeItem(recipe_item.id, recipe_item.quantity * item.quantity);
}
inventory.addItems(result.details.items.slice());
}
}
@ -278,6 +439,16 @@ pub fn main() !void {
continue;
}
// TODO: Add checking if character state is in sync. Debug mode only
// if (try craftItemFromBank(&manager, char, try api.getItemId("copper"), 10)) {
// continue;
// }
//
// if (try depositItemsToBank(&manager, char)) {
// continue;
// }
if (std.mem.eql(u8, char.character.name, "Devin")) {
try gatherResourceRoutine(char, .{ .x = -1, .y = 0 }); // Ash trees
} else if (std.mem.eql(u8, char.character.name, "Dawn")) {