290 lines
9.3 KiB
Zig
290 lines
9.3 KiB
Zig
const std = @import("std");
|
|
const assert = std.debug.assert;
|
|
const ArtifactsAPI = @import("artifacts.zig");
|
|
const Allocator = std.mem.Allocator;
|
|
|
|
// pub const std_options = .{ .log_level = .debug };
|
|
|
|
const Position = struct {
|
|
x: i64,
|
|
y: i64,
|
|
|
|
fn init(x: i64, y: i64) Position {
|
|
return Position{
|
|
.x = x,
|
|
.y = y
|
|
};
|
|
}
|
|
|
|
fn eql(self: Position, other: Position) bool {
|
|
return self.x == other.x and self.y == other.y;
|
|
}
|
|
};
|
|
|
|
const QueuedAction = union(enum) {
|
|
move: Position,
|
|
attack,
|
|
gather,
|
|
depositGold: u64,
|
|
depositItem: struct { id: ArtifactsAPI.ItemId, quantity: u64 },
|
|
};
|
|
|
|
const ActionQueue = std.ArrayList(QueuedAction);
|
|
|
|
const ManagedCharacter = struct {
|
|
character: ArtifactsAPI.Character,
|
|
action_queue: ActionQueue,
|
|
cooldown_expires_at: f64,
|
|
};
|
|
|
|
fn currentTime() f64 {
|
|
const timestamp: f64 = @floatFromInt(std.time.milliTimestamp());
|
|
return timestamp / std.time.ms_per_s;
|
|
}
|
|
|
|
const Manager = struct {
|
|
allocator: Allocator,
|
|
characters: std.ArrayList(ManagedCharacter),
|
|
api: *ArtifactsAPI,
|
|
|
|
fn init(allocator: Allocator, api: *ArtifactsAPI) Manager {
|
|
return Manager{
|
|
.allocator = allocator,
|
|
.api = api,
|
|
.characters = std.ArrayList(ManagedCharacter).init(allocator),
|
|
};
|
|
}
|
|
|
|
fn addCharacter(self: *Manager, character: ArtifactsAPI.Character) !void {
|
|
try self.characters.append(ManagedCharacter{
|
|
.character = character,
|
|
.action_queue = std.ArrayList(QueuedAction).init(self.allocator),
|
|
.cooldown_expires_at = character.cooldown_expiration
|
|
});
|
|
}
|
|
|
|
fn poll(self: *Manager) ?*ManagedCharacter {
|
|
if (self.characters.items.len == 0) return null;
|
|
|
|
var earliest_expiration = self.characters.items[0].cooldown_expires_at;
|
|
|
|
var now = currentTime();
|
|
for (self.characters.items) |managed_character| {
|
|
earliest_expiration = @min(earliest_expiration, managed_character.cooldown_expires_at);
|
|
}
|
|
|
|
if (earliest_expiration > now) {
|
|
const duration_s = earliest_expiration - now;
|
|
const duration_ms: u64 = @intFromFloat(@trunc(duration_s * std.time.ms_per_s));
|
|
std.log.debug("waiting for {d:.3}s", .{duration_s});
|
|
std.time.sleep(std.time.ns_per_ms * duration_ms);
|
|
}
|
|
|
|
const cooldown_margin = 0.1; // 100ms
|
|
|
|
now = currentTime();
|
|
for (self.characters.items) |*managed_character| {
|
|
if (now - managed_character.cooldown_expires_at >= -cooldown_margin) {
|
|
return managed_character;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
fn deinit(self: Manager) void {
|
|
for (self.characters.items) |managed_character| {
|
|
managed_character.action_queue.deinit();
|
|
}
|
|
self.characters.deinit();
|
|
}
|
|
};
|
|
|
|
fn depositIfFull(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) {
|
|
return false;
|
|
}
|
|
|
|
var character_pos = Position.init(character.x, character.y);
|
|
const bank_pos = Position{ .x = 4, .y = 1 };
|
|
|
|
if (!character_pos.eql(bank_pos)) {
|
|
try action_queue.append(.{ .move = bank_pos });
|
|
}
|
|
|
|
for (character.inventory.slots) |slot| {
|
|
if (slot.quantity == 0) continue;
|
|
|
|
if (slot.id) |item_id| {
|
|
try action_queue.append(.{
|
|
.depositItem = .{ .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;
|
|
|
|
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)) {
|
|
return;
|
|
}
|
|
|
|
// Go to chickens
|
|
if (!character_pos.eql(chicken_pos)) {
|
|
try action_queue.append(.{ .move = chicken_pos });
|
|
return;
|
|
}
|
|
|
|
// Attack chickens
|
|
try 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)) {
|
|
return;
|
|
}
|
|
|
|
if (!character_pos.eql(resource_pos)) {
|
|
try action_queue.append(.{ .move = resource_pos });
|
|
return;
|
|
}
|
|
|
|
try action_queue.append(.{ .gather = {} });
|
|
}
|
|
|
|
pub fn main() !void {
|
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
|
defer _ = gpa.deinit();
|
|
const allocator = gpa.allocator();
|
|
|
|
var api = try ArtifactsAPI.init(allocator);
|
|
defer api.deinit();
|
|
|
|
const args = try std.process.argsAlloc(allocator);
|
|
defer std.process.argsFree(allocator, args);
|
|
|
|
if (args.len >= 2) {
|
|
const filename = args[1];
|
|
const cwd = std.fs.cwd();
|
|
|
|
var token_buffer: [256]u8 = undefined;
|
|
const token = try cwd.readFile(filename, &token_buffer);
|
|
try api.setToken(std.mem.trim(u8,token,"\n\t "));
|
|
}
|
|
|
|
if (api.token == null) {
|
|
return error.MissingToken;
|
|
}
|
|
|
|
var manager = Manager.init(allocator, &api);
|
|
defer manager.deinit();
|
|
|
|
const characters = try api.listMyCharacters();
|
|
defer characters.deinit();
|
|
|
|
for (characters.items) |character| {
|
|
try manager.addCharacter(character);
|
|
}
|
|
|
|
const status = try api.getServerStatus();
|
|
defer status.deinit();
|
|
|
|
std.log.info("Server status: {s} v{s}", .{ status.status, status.version });
|
|
std.log.info("Characters online: {}", .{ status.characters_online });
|
|
|
|
std.log.info("Starting main loop", .{});
|
|
while (manager.poll()) |char| {
|
|
if (char.action_queue.items.len > 0) {
|
|
const action = char.action_queue.items[0];
|
|
|
|
var cooldown: ArtifactsAPI.Cooldown = undefined;
|
|
switch (action) {
|
|
.attack => {
|
|
std.log.debug("{s} attacks", .{char.character.name});
|
|
var result = try api.actionFight(char.character.name);
|
|
|
|
cooldown = result.cooldown;
|
|
char.character.gold += result.fight.gold;
|
|
for (result.fight.drops.slice()) |item| {
|
|
char.character.inventory.addItem(item.id, @intCast(item.quantity));
|
|
}
|
|
},
|
|
.move => |pos| {
|
|
std.log.debug("move {s} to ({}, {})", .{char.character.name, pos.x, pos.y});
|
|
var result = try api.actionMove(char.character.name, pos.x, pos.y);
|
|
defer result.deinit();
|
|
|
|
cooldown = result.cooldown;
|
|
char.character.x = pos.x;
|
|
char.character.y = pos.y;
|
|
},
|
|
.depositGold => |quantity| {
|
|
std.log.debug("deposit {} gold from {s}", .{quantity, char.character.name});
|
|
var result = try api.actionBankDepositGold(char.character.name, quantity);
|
|
defer result.deinit();
|
|
|
|
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});
|
|
const code = api.getItemCode(item.id) orelse return error.ItemNotFound;
|
|
var result = try api.actionBankDepositItem(char.character.name, code, item.quantity);
|
|
defer result.deinit();
|
|
|
|
cooldown = result.cooldown;
|
|
char.character.inventory.removeItem(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.cooldown_expires_at = cooldown.expiration;
|
|
|
|
_ = char.action_queue.orderedRemove(0);
|
|
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")) {
|
|
try gatherResourceRoutine(char, .{ .x = 2, .y = 0 }); // Copper ore
|
|
} else if (std.mem.eql(u8, char.character.name, "Diana")) {
|
|
try gatherResourceRoutine(char, .{ .x = 4, .y = 2 }); // Gudgeon fish
|
|
} else {
|
|
try attackChickenRoutine(char);
|
|
}
|
|
}
|
|
}
|