add accepting task and fighting monster

This commit is contained in:
Rokas Puzonas 2024-09-08 11:57:14 +03:00
parent cc289194a8
commit 429b1dcd6e
15 changed files with 239 additions and 77 deletions

View File

@ -43,11 +43,13 @@ const MapNotFound = ErrorDefinition.init("MapNotFound", 404);
const ItemNotFound = ErrorDefinition.init("ItemNotFound", 404);
const RecipeNotFound = ErrorDefinition.init("RecipeNotFound", 404);
const BankIsBusy = ErrorDefinition.init("BankIsBusy", 461);
const NotEnoughItems = ErrorDefinition.init("NotEnoughItems", 478);
const SlotIsFull = ErrorDefinition.init("SlotIsFull", 485);
const CharacterIsBusy = ErrorDefinition.init("CharacterIsBusy", 486);
const AlreadyHasTask = ErrorDefinition.init("AlreadyHasTask", 486);
const BankIsBusy = ErrorDefinition.init("BankIsBusy", 461);
const NotEnoughItems = ErrorDefinition.init("NotEnoughItems", 478);
const SlotIsFull = ErrorDefinition.init("SlotIsFull", 485);
const CharacterIsBusy = ErrorDefinition.init("CharacterIsBusy", 486);
const AlreadyHasTask = ErrorDefinition.init("AlreadyHasTask", 486);
const HasNoTask = ErrorDefinition.init("HasNoTask", 487);
const TaskNotCompleted = ErrorDefinition.init("TaskNotCompleted", 488);
const CharacterAtDestination = ErrorDefinition.init("CharacterAtDestination", 490);
const SlotIsEmpty = ErrorDefinition.init("SlotIsEmpty", 491);
@ -183,12 +185,24 @@ const EquipErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{
pub const EquipError = FetchError || EquipErrorDef.ErrorSet;
pub const parseEquipError = EquipErrorDef.parse;
const TaskAcceptErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{
const AcceptTaskErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{
CharacterIsBusy,
AlreadyHasTask,
CharacterNotFound,
CharacterInCooldown,
TaskMasterNotFound
});
pub const TaskAcceptError = FetchError || TaskAcceptErrorDef.ErrorSet;
pub const parseTaskAcceptError = TaskAcceptErrorDef.parse;
pub const AcceptTaskError = FetchError || AcceptTaskErrorDef.ErrorSet;
pub const parseAcceptTaskError = AcceptTaskErrorDef.parse;
const TaskCompleteErrorDef = ErrorDefinitionList(&[_]ErrorDefinition{
CharacterIsBusy,
HasNoTask,
TaskNotCompleted,
CharacterIsFull,
CharacterNotFound,
CharacterInCooldown,
TaskMasterNotFound
});
pub const TaskCompleteError = FetchError || TaskCompleteErrorDef.ErrorSet;
pub const parseTaskCompleteError = TaskCompleteErrorDef.parse;

View File

@ -4,6 +4,7 @@ pub const parseDateTime = @import("./date_time/parse.zig").parseDateTime;
pub const Server = @import("server.zig");
pub const Store = @import("store.zig");
pub const Character = @import("./schemas/character.zig");
pub const Map = @import("./schemas/map.zig");
pub const Position = @import("position.zig");
pub const BoundedSlotsArray = @import("schemas/slot_array.zig").BoundedSlotsArray;
@ -20,3 +21,4 @@ pub const BankDepositGoldError = errors.BankDepositGoldError;
pub const BankDepositItemError = errors.BankDepositItemError;
pub const BankWithdrawItemError = errors.BankWithdrawItemError;
pub const CraftError = errors.CraftError;
pub const AcceptTaskError = errors.AcceptTaskError;

View File

@ -10,6 +10,7 @@ const assert = std.debug.assert;
const SkillStats = @import("./skill_stats.zig");
const CombatStats = @import("./combat_stats.zig");
const Equipment = @import("./equipment.zig");
const Task = @import("./task.zig");
const BoundedSlotsArray = @import("./slot_array.zig").BoundedSlotsArray;
const Inventory = BoundedSlotsArray(20);
@ -17,10 +18,32 @@ const Inventory = BoundedSlotsArray(20);
const Character = @This();
const TaskMasterTask = struct {
target: []u8,
type: []u8,
target_id: Store.CodeId,
type: Task.Type,
progress: u64,
total: u64,
fn parse(store: *Store, obj: json.ObjectMap) !TaskMasterTask {
const task_target = try json_utils.getStringRequired(obj, "task");
const task_type = try json_utils.getStringRequired(obj, "task_type");
const progress = try json_utils.getIntegerRequired(obj, "task_progress");
if (progress < 0) {
return error.InvalidTaskProgress;
}
const total = try json_utils.getIntegerRequired(obj, "task_total");
if (total < 0) {
return error.InvalidTaskTotal;
}
return TaskMasterTask{
.target_id = try store.getCodeId(task_target),
.type = Task.TypeUtils.fromString(task_type) orelse return error.InvalidTaskType,
.total = @intCast(total),
.progress = @intCast(progress),
};
}
};
allocator: Allocator,
@ -74,22 +97,7 @@ pub fn parse(store: *Store, obj: json.ObjectMap, allocator: Allocator) !Characte
var task: ?TaskMasterTask = null;
const task_target = try json_utils.getStringRequired(obj, "task");
if (task_target.len > 0) {
const progress = try json_utils.getIntegerRequired(obj, "task_progress");
if (progress < 0) {
return error.InvalidTaskProgress;
}
const total = try json_utils.getIntegerRequired(obj, "task_total");
if (total < 0) {
return error.InvalidTaskTotal;
}
task = TaskMasterTask{
.target = try allocator.dupe(u8, task_target),
.type = try json_utils.dupeStringRequired(allocator, obj, "task_type"),
.total = @intCast(total),
.progress = @intCast(progress),
};
task = try TaskMasterTask.parse(store, obj);
}
return Character{
@ -131,11 +139,6 @@ pub fn deinit(self: *Character) void {
if (self.account) |str| self.allocator.free(str);
self.allocator.free(self.name);
self.allocator.free(self.skin);
if (self.task) |task| {
self.allocator.free(task.type);
self.allocator.free(task.target);
}
}
pub fn getItemCount(self: *const Character) u64 {

View File

@ -24,7 +24,7 @@ pub fn parse(store: *Store, obj: json.ObjectMap, allocator: Allocator) !Map {
.name = (try json_utils.dupeString(allocator, obj, "name")) orelse return error.MissingProperty,
.skin = (try json_utils.dupeString(allocator, obj, "skin")) orelse return error.MissingProperty,
.position = Position.init(x, y),
.content = try MapContent.parse(store, content)
.content = if (content) |c| try MapContent.parse(store, c) else null
};
}

View File

@ -27,11 +27,11 @@ pub const TypeUtils = EnumStringUtils(Type, .{
type: Type,
code_id: Store.CodeId,
pub fn parse(store: *Store, obj: json.ObjectMap) MapContent {
pub fn parse(store: *Store, obj: json.ObjectMap) !MapContent {
const content_type = json_utils.getString(obj, "type") orelse return error.MissingProperty;
return MapContent{
.type = TypeUtils.fromString(content_type) orelse return error.InvalidContentType,
.code = (try store.getCodeIdJson(obj, "code")) orelse return error.MissingProperty
.code_id = (try store.getCodeIdJson(obj, "code")) orelse return error.MissingProperty
};
}

View File

@ -8,14 +8,18 @@ const Items = BoundedSlotsArray(8);
const SkillInfo = @This();
xp: i64,
xp: u64,
items: Items,
pub fn parse(store: *Store, obj: json.ObjectMap) !SkillInfo {
const items = json_utils.getArray(obj, "items") orelse return error.MissingProperty;
const xp = try json_utils.getIntegerRequired(obj, "xp");
if (xp < 0) {
return error.InvalidXp;
}
return SkillInfo{
.xp = try json_utils.getIntegerRequired(obj, "xp"),
.xp = @intCast(xp),
.items = try Items.parse(store, items),
};
}

View File

@ -0,0 +1,27 @@
const std = @import("std");
const Store = @import("../store.zig");
const json_utils = @import("../json_utils.zig");
const json = std.json;
const Allocator = std.mem.Allocator;
const Cooldown = @import("./cooldown.zig");
const Character = @import("./character.zig");
const ItemQuantity = @import("./item_quantity.zig");
const TaskRewardData = @This();
cooldown: Cooldown,
character: Character,
reward: ItemQuantity,
pub fn parse(store: *Store, obj: json.ObjectMap, allocator: Allocator) !TaskRewardData {
const cooldown = json_utils.getObject(obj, "cooldown") orelse return error.MissingProperty;
const character = json_utils.getObject(obj, "character") orelse return error.MissingProperty;
const task = json_utils.getObject(obj, "task") orelse return error.MissingProperty;
return TaskRewardData{
.cooldown = try Cooldown.parse(cooldown),
.character = try Character.parse(store, character, allocator),
.reward = (try ItemQuantity.parse(store, task)) orelse return error.MissinReward
};
}

View File

@ -45,6 +45,7 @@ pub const UnequipResult = @import("./schemas/equip_request.zig");
pub const EquipResult = @import("./schemas/equip_request.zig");
const DropRate = @import("./schemas/drop_rate.zig");
pub const AcceptTaskResult = @import("./schemas/task_data.zig");
pub const CompleteTaskResult = @import("./schemas/task_reward_data.zig");
pub const MapContent = @import("./schemas/map_content.zig");
pub const MapTile = @import("./schemas/map.zig");
pub const Item = @import("./schemas/item.zig");
@ -1028,13 +1029,13 @@ pub fn getMonsters(self: *Server, opts: MonsterOptions) FetchError!std.ArrayList
return result;
}
pub fn acceptTask(self: *Server, name: []const u8) errors.TaskAcceptError!AcceptTaskResult {
pub fn acceptTask(self: *Server, name: []const u8) errors.AcceptTaskError!AcceptTaskResult {
const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/task/new", .{name});
defer self.allocator.free(path);
const result = try self.fetchObject(
errors.TaskAcceptError,
errors.parseTaskAcceptError,
errors.AcceptTaskError,
errors.parseAcceptTaskError,
AcceptTaskResult,
AcceptTaskResult.parse, .{ self.allocator },
.{ .method = .POST, .path = path }
@ -1043,3 +1044,19 @@ pub fn acceptTask(self: *Server, name: []const u8) errors.TaskAcceptError!Accept
return result;
}
pub fn completeTask(self: *Server, name: []const u8) errors.TaskCompleteError!CompleteTaskResult {
const path = try std.fmt.allocPrint(self.allocator, "/my/{s}/action/task/complete", .{name});
defer self.allocator.free(path);
const result = try self.fetchObject(
errors.TaskCompleteError,
errors.parseTaskCompleteError,
CompleteTaskResult,
CompleteTaskResult.parse, .{ self.allocator },
.{ .method = .POST, .path = path }
);
try self.store.putCharacter(result.character);
return result;
}

View File

@ -155,7 +155,9 @@ pub fn getMaps(self: *Store, opts: Server.MapOptions) !std.ArrayList(Map) {
}
if (opts.code) |content_code| {
if (map.content == null) continue;
if (!std.mem.eql(u8, map.content.?.code, content_code)) continue;
const map_content_code = self.getCode(map.content.?.code_id).?;
if (!std.mem.eql(u8, map_content_code, content_code)) continue;
}
try found.append(map.*);

View File

@ -38,7 +38,7 @@ pub fn main() !void {
std.log.info("Starting main loop", .{});
while (true) {
const waitUntil = artificer.nextStepAt();
const duration = waitUntil - std.time.timestamp();
const duration = waitUntil - std.time.milliTimestamp();
if (duration > 0) {
std.time.sleep(@intCast(duration));
}

View File

@ -25,12 +25,34 @@ fn getAPITokenFromArgs(allocator: Allocator) !?[]u8 {
return try allocator.dupe(u8, std.mem.trim(u8,token,"\n\t "));
}
fn drawCharacterInfo(ui: *UI, rect: rl.Rectangle, brain: Artificer.Brain) void {
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);
}
pub fn main() anyerror!void {
@ -53,7 +75,7 @@ pub fn main() anyerror!void {
defer ui.deinit();
while (!rl.windowShouldClose()) {
if (std.time.timestamp() > artificer.nextStepAt()) {
if (std.time.milliTimestamp() > artificer.nextStepAt()) {
try artificer.step();
}
@ -68,9 +90,9 @@ pub fn main() anyerror!void {
rl.clearBackground(srcery.black);
var info_stack = UIStack.init(rl.Rectangle.init(0, 0, screen_size.x, screen_size.y), .left_to_right);
for (artificer.characters.items) |brain| {
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);
try drawCharacterInfo(&ui, info_stack.next(info_width), &artificer, brain);
}
}
}

View File

@ -12,6 +12,7 @@ pub const Action = union(enum) {
deposit_item: Api.ItemQuantity,
withdraw_item: Api.ItemQuantity,
craft_item: Api.ItemQuantity,
accept_task,
pub fn perform(self: Action, api: *Server, name: []const u8) !ActionResult {
const log = std.log.default;
@ -61,6 +62,12 @@ pub const Action = union(enum) {
return .{
.craft_item = api.actionCraft(name, code, item.quantity)
};
},
.accept_task => {
log.debug("[{s}] accept task", .{name});
return .{
.accept_task = api.acceptTask(name)
};
}
}
}
@ -88,6 +95,7 @@ pub const ActionResult = union(enum) {
deposit_item: Api.BankDepositItemError!Server.ItemTransactionResult,
withdraw_item: Api.BankWithdrawItemError!Server.ItemTransactionResult,
craft_item: Api.CraftError!Server.CraftResult,
accept_task: Api.AcceptTaskError!Server.AcceptTaskResult,
const AnyError = Server.MoveError;
@ -127,6 +135,9 @@ pub const ActionResult = union(enum) {
},
.craft_item => |result| {
_ = try result;
},
.accept_task => |result| {
_ = try result;
}
}
}
@ -149,6 +160,8 @@ pub const ActionResult = union(enum) {
error.ItemNotFound,
error.NotEnoughItems,
error.RecipeNotFound,
error.AlreadyHasTask,
error.TaskMasterNotFound,
error.WorkshopNotFound => return ErrorResponse.restart,
error.CharacterNotFound,

View File

@ -14,7 +14,7 @@ const Brain = @This();
name: []const u8,
action_queue: std.ArrayList(QueuedAction),
task: ?CharacterTask = null,
paused_until: ?i64 = null,
paused_until: ?i64 = null, // ms
pub fn init(allocator: Allocator, name: []const u8) !Brain {
return Brain{
@ -33,7 +33,7 @@ pub fn performNextAction(self: *Brain, api: *Server) !void {
const log = std.log.default;
assert(self.action_queue.items.len > 0);
const retry_delay = std.time.ns_per_ms * 500; // 500ms
const retry_delay = 500; // 500ms
const next_action = self.action_queue.items[0];
const action_result = try next_action.perform(api, self.name);
@ -41,7 +41,7 @@ pub fn performNextAction(self: *Brain, api: *Server) !void {
if (action_result.getErrorResponse()) |error_response| {
switch (error_response) {
.retry => {
self.paused_until = std.time.timestamp() + retry_delay;
self.paused_until = std.time.milliTimestamp() + retry_delay;
log.warn("[{s}] retry action", .{self.name});
return;
},
@ -51,6 +51,7 @@ pub fn performNextAction(self: *Brain, api: *Server) !void {
return;
},
.abort => {
log.warn("[{s}] abort action {s}", .{ self.name, @tagName(next_action) });
try action_result.getError();
// The error above should always return
@ -69,7 +70,7 @@ pub fn performNextAction(self: *Brain, api: *Server) !void {
pub fn step(self: *Brain, api: *Api.Server) !void {
if (self.paused_until) |paused_until| {
if (std.time.timestamp() < paused_until) {
if (std.time.milliTimestamp() < paused_until) {
return;
}
self.paused_until = null;
@ -93,7 +94,7 @@ pub fn step(self: *Brain, api: *Api.Server) !void {
pub fn cooldown(self: *Brain, api: *Server) i64 {
const character = api.store.getCharacter(self.name).?;
const cooldown_expiration: i64 = @intFromFloat(character.cooldown_expiration * std.time.ns_per_s);
const cooldown_expiration: i64 = @intFromFloat(character.cooldown_expiration * std.time.ms_per_s);
if (self.paused_until) |pause_until| {
return @max(cooldown_expiration, pause_until);

View File

@ -14,7 +14,7 @@ server: Api.Server,
characters: std.ArrayList(Brain),
task_graph: TaskGraph,
paused_until: ?i64 = null,
paused_until: ?i64 = null, // ms
pub fn init(allocator: Allocator, token: []const u8) !Artificer {
var server = try Api.Server.init(allocator);
@ -49,7 +49,7 @@ pub fn deinit(self: *Artificer) void {
pub fn step(self: *Artificer) !void {
if (self.paused_until) |paused_until| {
if (std.time.timestamp() < paused_until) {
if (std.time.milliTimestamp() < paused_until) {
return;
}
self.paused_until = null;
@ -57,7 +57,7 @@ pub fn step(self: *Artificer) !void {
runNextActions(self.characters.items, &self.server) catch |err| switch (err) {
Api.FetchError.ServerUnavailable => {
self.paused_until = std.time.timestamp() + std.time.ns_per_min * server_down_retry_interval;
self.paused_until = std.time.milliTimestamp() + std.time.ms_per_min * server_down_retry_interval;
std.log.warn("Server is down, retrying in {}min", .{ server_down_retry_interval });
return;
},
@ -65,14 +65,40 @@ pub fn step(self: *Artificer) !void {
};
for (self.characters.items) |*brain| {
if (brain.task == null) {
const character = self.server.store.getCharacter(brain.name).?;
if (character.task == null) {
brain.task = .{ .accept_task = .{} };
}
if (brain.task != null) {
try brain.step(&self.server);
continue;
}
try brain.step(&self.server);
const character = self.server.store.getCharacter(brain.name).?;
if (character.task) |taskmaster_task| {
if (taskmaster_task.total > taskmaster_task.progress) {
switch (taskmaster_task.type) {
.monsters => {
const monster_code = self.server.store.getCode(taskmaster_task.target_id).?;
const maps = try self.server.getMaps(.{ .code = monster_code });
defer maps.deinit();
if (maps.items.len > 0) {
const resource_map: Api.Map = maps.items[0];
std.debug.print("fight at {}\n", .{resource_map.position});
brain.task = .{
.fight = .{
.at = resource_map.position,
.until = .{ .quantity = taskmaster_task.total - taskmaster_task.progress },
}
};
}
},
.crafts => {},
.resources => {},
}
}
} else {
brain.task = .{ .accept_task = .{} };
}
}
}
@ -99,17 +125,11 @@ fn earliestCooldown(characters: []Brain, api: *Api.Server) ?i64 {
}
fn runNextActions(characters: []Brain, api: *Api.Server) !void {
const maybe_earliest_cooldown = earliestCooldown(characters, api);
if (maybe_earliest_cooldown == null) return;
const earliest_cooldown = maybe_earliest_cooldown.?;
if (earliest_cooldown < std.time.timestamp()) return;
for (characters) |*brain| {
if (brain.action_queue.items.len == 0) continue;
const cooldown = brain.cooldown(api);
if (earliest_cooldown > cooldown) {
if (std.time.milliTimestamp() >= cooldown) {
try brain.performNextAction(api);
}
}

View File

@ -7,11 +7,22 @@ const ActionResult = @import("./action.zig").ActionResult;
const CodeId = Api.CodeId;
const ItemQuantity = Api.ItemQuantity;
const bank_position: Position = .{ .x = 4, .y = 1 }; // TODO: Figure this out dynamically
const bank_position = Position{ .x = 4, .y = 1 }; // TODO: Figure this out dynamically
const task_master_position = Position{ .x = 1, .y = 2 }; // TODO: Figure this out dynamically
pub const UntilCondition = union(enum) {
xp: u64,
item: Api.ItemQuantity,
quantity: u64,
fn isComplete(self: UntilCondition, progress: u64) bool {
return switch (self) {
.xp => |xp| progress >= xp,
.item => |item| progress >= item.quantity,
.quantity => |quantity| progress >= quantity,
};
}
};
pub const Task = union(enum) {
@ -36,8 +47,8 @@ pub const Task = union(enum) {
pub fn isComplete(self: Task) bool {
return switch (self) {
.fight => |args| args.progress >= args.until.item.quantity,
.gather => |args| args.progress >= args.until.item.quantity,
.fight => |args| args.until.isComplete(args.progress),
.gather => |args| args.until.isComplete(args.progress),
.craft => |args| args.progress >= args.target.quantity,
.accept_task => |args| args.done
};
@ -48,17 +59,37 @@ pub const Task = union(enum) {
.fight => |*args| {
if (result.get(.fight)) |r| {
const fight_result: Api.Server.FightResult = r;
const drops = fight_result.fight.drops;
args.progress += drops.getQuantity(args.until.item.id);
switch (args.until) {
.xp => {
args.progress += fight_result.fight.xp;
},
.item => {
const drops = fight_result.fight.drops;
args.progress += drops.getQuantity(args.until.item.id);
},
.quantity => {
args.progress += 1;
}
}
}
},
.gather => |*args| {
if (result.get(.gather)) |r| {
const gather_resutl: Api.Server.GatherResult = r;
const items = gather_resutl.details.items;
const gather_result: Api.Server.GatherResult = r;
args.progress += items.getQuantity(args.until.item.id);
switch (args.until) {
.xp => {
args.progress += gather_result.details.xp;
},
.item => {
const items = gather_result.details.items;
args.progress += items.getQuantity(args.until.item.id);
},
.quantity => {
args.progress += 1;
}
}
}
},
.craft => |*args| {
@ -69,8 +100,10 @@ pub const Task = union(enum) {
args.progress += items.getQuantity(args.target.id);
}
},
.accept_task => {
// TODO:
.accept_task => |*args| {
if (result.get(.accept_task)) |_| {
args.done = true;
}
}
}
}
@ -93,7 +126,11 @@ pub const Task = union(enum) {
try ctx.craftRoutine(args.at, args.target.id, args.target.quantity);
},
.accept_task => {
// TODO:
if (try ctx.moveIfNeeded(task_master_position)) {
return;
}
try ctx.action_queue.append(.{ .accept_task = {} });
}
}
}