Compare commits
2 Commits
805e2dcabc
...
ee563e6ba6
| Author | SHA1 | Date | |
|---|---|---|---|
| ee563e6ba6 | |||
| 8cd5fb44c8 |
@ -11,7 +11,9 @@ const assert = std.debug.assert;
|
|||||||
const SkillStats = @import("./skill_stats.zig");
|
const SkillStats = @import("./skill_stats.zig");
|
||||||
const CombatStats = @import("./combat_stats.zig");
|
const CombatStats = @import("./combat_stats.zig");
|
||||||
const Equipment = @import("./equipment.zig");
|
const Equipment = @import("./equipment.zig");
|
||||||
const Inventory = @import("./inventory.zig");
|
const BoundedSlotsArray = @import("./slot_array.zig").BoundedSlotsArray;
|
||||||
|
|
||||||
|
const Inventory = BoundedSlotsArray(20);
|
||||||
|
|
||||||
const Character = @This();
|
const Character = @This();
|
||||||
|
|
||||||
@ -94,11 +96,7 @@ pub fn deinit(self: *Character) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn getItemCount(self: *const Character) u64 {
|
pub fn getItemCount(self: *const Character) u64 {
|
||||||
var count: u64 = 0;
|
return self.inventory.totalQuantity();
|
||||||
for (self.inventory.slots) |slot| {
|
|
||||||
count += slot.quantity;
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn format(
|
pub fn format(
|
||||||
|
|||||||
@ -1,96 +0,0 @@
|
|||||||
const std = @import("std");
|
|
||||||
const json_utils = @import("json_utils.zig");
|
|
||||||
const Server = @import("./server.zig");
|
|
||||||
const ItemId = Server.ItemId;
|
|
||||||
const assert = std.debug.assert;
|
|
||||||
const json = std.json;
|
|
||||||
|
|
||||||
const Inventory = @This();
|
|
||||||
|
|
||||||
const slot_count = 20;
|
|
||||||
|
|
||||||
pub const Slot = struct {
|
|
||||||
id: ?ItemId,
|
|
||||||
quantity: u64,
|
|
||||||
|
|
||||||
fn parse(api: *Server, slot_obj: json.ObjectMap) !Slot {
|
|
||||||
const quantity = try json_utils.getIntegerRequired(slot_obj, "quantity");
|
|
||||||
if (quantity < 0) return error.InvalidQuantity;
|
|
||||||
|
|
||||||
return Slot{
|
|
||||||
.id = try api.getItemIdJson(slot_obj, "code"),
|
|
||||||
.quantity = @intCast(quantity),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
slots: [slot_count]Slot,
|
|
||||||
|
|
||||||
pub fn parse(api: *Server, slots_array: json.Array) !Inventory {
|
|
||||||
assert(slots_array.items.len == Inventory.slot_count);
|
|
||||||
|
|
||||||
var inventory: Inventory = undefined;
|
|
||||||
|
|
||||||
for (0.., slots_array.items) |i, slot_value| {
|
|
||||||
const slot_obj = json_utils.asObject(slot_value) orelse return error.InvalidType;
|
|
||||||
inventory.slots[i] = try Slot.parse(api, slot_obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
return inventory;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn findSlot(self: *Inventory, id: ItemId) ?*Slot {
|
|
||||||
for (&self.slots) |*slot| {
|
|
||||||
if (slot.id == id) {
|
|
||||||
return slot;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn removeItem(self: *Inventory, id: ItemId, quantity: u64) void {
|
|
||||||
const slot = self.findSlot(id) orelse unreachable;
|
|
||||||
assert(slot.quantity >= quantity);
|
|
||||||
|
|
||||||
slot.quantity -= quantity;
|
|
||||||
if (slot.quantity == 0) {
|
|
||||||
slot.id = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn addItem(self: *Inventory, id: ItemId, quantity: u64) void {
|
|
||||||
if (self.findSlot(id)) |slot| {
|
|
||||||
slot.quantity += quantity;
|
|
||||||
} else {
|
|
||||||
var empty_slot: ?*Slot = null;
|
|
||||||
for (&self.slots) |*slot| {
|
|
||||||
if (slot.id == null) {
|
|
||||||
empty_slot = slot;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(empty_slot != null);
|
|
||||||
empty_slot.?.id = id;
|
|
||||||
empty_slot.?.quantity = quantity;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn addItems(self: *Inventory, items: []const Server.ItemIdQuantity) void {
|
|
||||||
for (items) |item| {
|
|
||||||
self.addItem(item.id, item.quantity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn removeItems(self: *Inventory, items: []const Server.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;
|
|
||||||
}
|
|
||||||
@ -8,6 +8,9 @@ pub const parseDateTime = @import("../date_time/parse.zig").parseDateTime;
|
|||||||
|
|
||||||
const json_utils = @import("json_utils.zig");
|
const json_utils = @import("json_utils.zig");
|
||||||
pub const Character = @import("character.zig");
|
pub const Character = @import("character.zig");
|
||||||
|
const BoundedSlotsArray = @import("./slot_array.zig").BoundedSlotsArray;
|
||||||
|
pub const Slot = @import("./slot.zig");
|
||||||
|
pub const Position = @import("./position.zig");
|
||||||
|
|
||||||
// Specification: https://api.artifactsmmo.com/docs
|
// Specification: https://api.artifactsmmo.com/docs
|
||||||
|
|
||||||
@ -252,16 +255,12 @@ pub const Cooldown = struct {
|
|||||||
|
|
||||||
pub const FightResult = struct {
|
pub const FightResult = struct {
|
||||||
const Details = struct {
|
const Details = struct {
|
||||||
const DroppedItem = struct {
|
|
||||||
id: ItemId,
|
|
||||||
quantity: i64
|
|
||||||
};
|
|
||||||
|
|
||||||
const Result = enum { win, lose };
|
const Result = enum { win, lose };
|
||||||
|
const Drops = BoundedSlotsArray(8);
|
||||||
|
|
||||||
xp: i64,
|
xp: i64,
|
||||||
gold: i64,
|
gold: i64,
|
||||||
drops: std.BoundedArray(DroppedItem, 8),
|
drops: Drops,
|
||||||
result: Result,
|
result: Result,
|
||||||
|
|
||||||
fn parse(api: *Server, obj: json.ObjectMap) !Details {
|
fn parse(api: *Server, obj: json.ObjectMap) !Details {
|
||||||
@ -271,24 +270,16 @@ pub const FightResult = struct {
|
|||||||
result_enum = .win;
|
result_enum = .win;
|
||||||
} else if (std.mem.eql(u8, result, "win")) {
|
} else if (std.mem.eql(u8, result, "win")) {
|
||||||
result_enum = .lose;
|
result_enum = .lose;
|
||||||
|
} else {
|
||||||
|
return error.InvalidProperty;
|
||||||
}
|
}
|
||||||
|
|
||||||
var drops = std.BoundedArray(DroppedItem, 8).init(0) catch unreachable;
|
|
||||||
const drops_obj = json_utils.getArray(obj, "drops") orelse return error.MissingProperty;
|
const drops_obj = json_utils.getArray(obj, "drops") orelse return error.MissingProperty;
|
||||||
for (drops_obj.items) |drop_value| {
|
|
||||||
const drop_obj = json_utils.asObject(drop_value) orelse return error.MissingProperty;
|
|
||||||
const code = try json_utils.getStringRequired(drop_obj, "code");
|
|
||||||
|
|
||||||
try drops.append(DroppedItem{
|
|
||||||
.id = try api.getItemId(code),
|
|
||||||
.quantity = try json_utils.getIntegerRequired(drop_obj, "quantity")
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return Details{
|
return Details{
|
||||||
.xp = try json_utils.getIntegerRequired(obj, "xp"),
|
.xp = try json_utils.getIntegerRequired(obj, "xp"),
|
||||||
.gold = try json_utils.getIntegerRequired(obj, "gold"),
|
.gold = try json_utils.getIntegerRequired(obj, "gold"),
|
||||||
.drops = drops,
|
.drops = try Drops.parse(api, drops_obj),
|
||||||
.result = result_enum,
|
.result = result_enum,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -322,6 +313,7 @@ pub const FightResult = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: Replace this with ItemSlot struct
|
||||||
pub const ItemIdQuantity = struct {
|
pub const ItemIdQuantity = struct {
|
||||||
id: ItemId,
|
id: ItemId,
|
||||||
quantity: u64,
|
quantity: u64,
|
||||||
@ -338,29 +330,18 @@ pub const ItemIdQuantity = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const BoundedItems = std.BoundedArray(ItemIdQuantity, 8);
|
|
||||||
fn parseSimpleItemList(api: *Server, array: json.Array) !BoundedItems {
|
|
||||||
var items = BoundedItems.init(0) catch unreachable;
|
|
||||||
|
|
||||||
for (array.items) |item_value| {
|
|
||||||
const item_obj = json_utils.asObject(item_value) orelse return error.MissingProperty;
|
|
||||||
|
|
||||||
try items.append(try ItemIdQuantity.parse(api, item_obj));
|
|
||||||
}
|
|
||||||
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const SkillResultDetails = struct {
|
pub const SkillResultDetails = struct {
|
||||||
|
const Items = BoundedSlotsArray(8);
|
||||||
|
|
||||||
xp: i64,
|
xp: i64,
|
||||||
items: BoundedItems,
|
items: Items,
|
||||||
|
|
||||||
fn parse(api: *Server, obj: json.ObjectMap) !SkillResultDetails {
|
fn parse(api: *Server, obj: json.ObjectMap) !SkillResultDetails {
|
||||||
const items = json_utils.getArray(obj, "items") orelse return error.MissingProperty;
|
const items = json_utils.getArray(obj, "items") orelse return error.MissingProperty;
|
||||||
|
|
||||||
return SkillResultDetails{
|
return SkillResultDetails{
|
||||||
.xp = try json_utils.getIntegerRequired(obj, "xp"),
|
.xp = try json_utils.getIntegerRequired(obj, "xp"),
|
||||||
.items = try parseSimpleItemList(api, items),
|
.items = try Items.parse(api, items),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -640,10 +621,12 @@ pub const MapResult = struct {
|
|||||||
|
|
||||||
pub const Item = struct {
|
pub const Item = struct {
|
||||||
pub const Recipe = struct {
|
pub const Recipe = struct {
|
||||||
|
const Items = BoundedSlotsArray(8);
|
||||||
|
|
||||||
skill: Skill,
|
skill: Skill,
|
||||||
level: u64,
|
level: u64,
|
||||||
quantity: u64,
|
quantity: u64,
|
||||||
items: BoundedItems,
|
items: Items,
|
||||||
|
|
||||||
pub fn parse(api: *Server, obj: json.ObjectMap) !Recipe {
|
pub fn parse(api: *Server, obj: json.ObjectMap) !Recipe {
|
||||||
const skill = json_utils.getString(obj, "skill") orelse return error.MissingProperty;
|
const skill = json_utils.getString(obj, "skill") orelse return error.MissingProperty;
|
||||||
@ -659,7 +642,7 @@ pub const Item = struct {
|
|||||||
.skill = Skill.parse(skill) orelse return error.InvalidSkill,
|
.skill = Skill.parse(skill) orelse return error.InvalidSkill,
|
||||||
.level = @intCast(level),
|
.level = @intCast(level),
|
||||||
.quantity = @intCast(quantity),
|
.quantity = @intCast(quantity),
|
||||||
.items = try parseSimpleItemList(api, items)
|
.items = try Items.parse(api, items)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -1094,6 +1077,15 @@ pub fn findCharacter(self: *const Server, name: []const u8) ?Character {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn findCharacterPtr(self: *Server, name: []const u8) ?*Character {
|
||||||
|
if (self.findCharacterIndex(name)) |index| {
|
||||||
|
return &self.characters.items[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn findItem(self: *const Server, name: []const u8) ?Item {
|
pub fn findItem(self: *const Server, name: []const u8) ?Item {
|
||||||
return self.items.get(name);
|
return self.items.get(name);
|
||||||
}
|
}
|
||||||
|
|||||||
23
src/api/slot.zig
Normal file
23
src/api/slot.zig
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const json_utils = @import("json_utils.zig");
|
||||||
|
const Server = @import("./server.zig");
|
||||||
|
const ItemId = Server.ItemId;
|
||||||
|
const json = std.json;
|
||||||
|
|
||||||
|
const ItemSlot = @This();
|
||||||
|
|
||||||
|
id: ItemId,
|
||||||
|
quantity: u64,
|
||||||
|
|
||||||
|
pub fn parse(api: *Server, slot_obj: json.ObjectMap) !?ItemSlot {
|
||||||
|
const code = try json_utils.getStringRequired(slot_obj, "code");
|
||||||
|
if (code.len == 0) return null;
|
||||||
|
|
||||||
|
const quantity = try json_utils.getIntegerRequired(slot_obj, "quantity");
|
||||||
|
if (quantity < 0) return error.InvalidQuantity;
|
||||||
|
|
||||||
|
return ItemSlot{
|
||||||
|
.id = try api.getItemId(code),
|
||||||
|
.quantity = @intCast(quantity),
|
||||||
|
};
|
||||||
|
}
|
||||||
115
src/api/slot_array.zig
Normal file
115
src/api/slot_array.zig
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const json_utils = @import("json_utils.zig");
|
||||||
|
const Server = @import("./server.zig");
|
||||||
|
const ItemId = Server.ItemId;
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const json = std.json;
|
||||||
|
|
||||||
|
const Slot = @import("./slot.zig");
|
||||||
|
|
||||||
|
pub fn BoundedSlotsArray(comptime slot_count: u32) type {
|
||||||
|
const Slots = std.BoundedArray(Slot, slot_count);
|
||||||
|
|
||||||
|
return struct {
|
||||||
|
slots: Slots,
|
||||||
|
|
||||||
|
fn init() @This() {
|
||||||
|
return @This(){
|
||||||
|
.slots = Slots.init(0) catch unreachable
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(api: *Server, slots_array: json.Array) !@This() {
|
||||||
|
var slots = Slots.init(0) catch unreachable;
|
||||||
|
|
||||||
|
for (slots_array.items) |slot_value| {
|
||||||
|
const slot_obj = json_utils.asObject(slot_value) orelse return error.InvalidType;
|
||||||
|
|
||||||
|
if (try Slot.parse(api, slot_obj)) |slot| {
|
||||||
|
try slots.append(slot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return @This(){ .slots = slots };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn findSlotIndex(self: *const @This(), id: ItemId) ?usize {
|
||||||
|
for (0.., self.slots.slice()) |i, *slot| {
|
||||||
|
if (slot.id == id) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn findSlot(self: *@This(), id: ItemId) ?*Slot {
|
||||||
|
if (self.findSlotIndex(id)) |index| {
|
||||||
|
return &self.slots.buffer[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove(self: *@This(), id: ItemId, quantity: u64) void {
|
||||||
|
const slot_index = self.findSlotIndex(id) orelse unreachable;
|
||||||
|
const slot = self.slots.get(slot_index);
|
||||||
|
assert(slot.quantity >= quantity);
|
||||||
|
|
||||||
|
slot.quantity -= quantity;
|
||||||
|
if (slot.quantity == 0) {
|
||||||
|
self.slots.swapRemove(slot_index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add(self: *@This(), id: ItemId, quantity: u64) void {
|
||||||
|
if (self.findSlot(id)) |slot| {
|
||||||
|
slot.quantity += quantity;
|
||||||
|
} else {
|
||||||
|
var empty_slot: ?*Slot = null;
|
||||||
|
for (&self.slots) |*slot| {
|
||||||
|
if (slot.id == null) {
|
||||||
|
empty_slot = slot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(empty_slot != null);
|
||||||
|
empty_slot.?.id = id;
|
||||||
|
empty_slot.?.quantity = quantity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn addSlice(self: *@This(), items: []const Server.ItemIdQuantity) void {
|
||||||
|
for (items) |item| {
|
||||||
|
self.add(item.id, item.quantity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn removeSlice(self: *@This(), items: []const Server.ItemIdQuantity) void {
|
||||||
|
for (items) |item| {
|
||||||
|
self.remove(item.id, item.quantity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getQuantity(self: *const @This(), id: ItemId) u64 {
|
||||||
|
if (self.findSlotIndex(id)) |index| {
|
||||||
|
return self.slots.get(index).quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn totalQuantity(self: *const @This()) u64 {
|
||||||
|
var count: u64 = 0;
|
||||||
|
for (self.slots.constSlice()) |slot| {
|
||||||
|
count += slot.quantity;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn slice(self: *@This()) []Slot {
|
||||||
|
return self.slots.slice();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
612
src/character_brain.zig
Normal file
612
src/character_brain.zig
Normal file
@ -0,0 +1,612 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Server = @import("./api/server.zig");
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const Position = Server.Position;
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
|
||||||
|
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,
|
||||||
|
routine: union (enum) {
|
||||||
|
idle,
|
||||||
|
|
||||||
|
fight: struct {
|
||||||
|
at: Position,
|
||||||
|
target: Server.ItemIdQuantity,
|
||||||
|
progress: u64 = 0,
|
||||||
|
},
|
||||||
|
gather: struct {
|
||||||
|
at: Position,
|
||||||
|
target: Server.ItemIdQuantity,
|
||||||
|
progress: u64 = 0,
|
||||||
|
},
|
||||||
|
craft: struct {
|
||||||
|
target: Server.ItemIdQuantity,
|
||||||
|
progress: u64 = 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
action_queue: std.ArrayList(QueuedAction),
|
||||||
|
|
||||||
|
pub fn init(allocator: Allocator, name: []const u8) !CharacterBrain {
|
||||||
|
return CharacterBrain{
|
||||||
|
.name = try allocator.dupe(u8, name),
|
||||||
|
.routine = .idle,
|
||||||
|
.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 {
|
||||||
|
switch (self.routine) {
|
||||||
|
.idle => {},
|
||||||
|
|
||||||
|
.fight => |*args| {
|
||||||
|
if (result.get(.fight)) |r| {
|
||||||
|
const fight_result: Server.FightResult = r;
|
||||||
|
const drops = fight_result.fight.drops;
|
||||||
|
|
||||||
|
args.progress += drops.getQuantity(args.target.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.target.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 isRoutineFinished(self: *CharacterBrain) bool {
|
||||||
|
return switch (self.routine) {
|
||||||
|
.idle => false,
|
||||||
|
|
||||||
|
.fight => |args| {
|
||||||
|
return args.progress >= args.target.quantity;
|
||||||
|
},
|
||||||
|
.gather => |args| {
|
||||||
|
return args.progress >= args.target.quantity;
|
||||||
|
},
|
||||||
|
.craft => |args| {
|
||||||
|
return args.progress >= args.target.quantity;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn performRoutine(self: *CharacterBrain, api: *Server) !void {
|
||||||
|
switch (self.routine) {
|
||||||
|
.idle => {
|
||||||
|
std.log.debug("[{s}] idle", .{self.name});
|
||||||
|
},
|
||||||
|
.fight => |args| {
|
||||||
|
try self.fightRoutine(api, args.at);
|
||||||
|
},
|
||||||
|
.gather => |args| {
|
||||||
|
try self.gatherRoutine(api, args.at);
|
||||||
|
},
|
||||||
|
.craft => |args| {
|
||||||
|
try self.craftRoutine(api, 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, 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const code = api.getItemCode(id) orelse return error.InvalidItemId;
|
||||||
|
const item = try api.getItem(code) orelse return error.ItemNotFound;
|
||||||
|
if (item.craft == null) {
|
||||||
|
return error.NotCraftable;
|
||||||
|
}
|
||||||
|
|
||||||
|
const recipe = item.craft.?;
|
||||||
|
|
||||||
|
// TODO: Figure this out dynamically
|
||||||
|
const workstation = switch (recipe.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 },
|
||||||
|
};
|
||||||
|
|
||||||
|
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, 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.?;
|
||||||
|
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 self.withdrawFromBank(api, needed_items.slice())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (try self.craftItem(api, id, quantity)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
337
src/main.zig
337
src/main.zig
@ -4,98 +4,10 @@ const assert = std.debug.assert;
|
|||||||
|
|
||||||
const Position = @import("./api/position.zig");
|
const Position = @import("./api/position.zig");
|
||||||
const Server = @import("./api/server.zig");
|
const Server = @import("./api/server.zig");
|
||||||
|
const CharacterBrain = @import("./character_brain.zig");
|
||||||
|
|
||||||
// pub const std_options = .{ .log_level = .debug };
|
// pub const std_options = .{ .log_level = .debug };
|
||||||
|
|
||||||
const QueuedAction = union(enum) {
|
|
||||||
move: Position,
|
|
||||||
fight,
|
|
||||||
gather,
|
|
||||||
deposit_gold: u64,
|
|
||||||
deposit_item: Server.ItemIdQuantity,
|
|
||||||
withdraw_item: Server.ItemIdQuantity,
|
|
||||||
craft_item: Server.ItemIdQuantity,
|
|
||||||
};
|
|
||||||
|
|
||||||
const CharacterBrain = struct {
|
|
||||||
name: []const u8,
|
|
||||||
routine: union (enum) {
|
|
||||||
idle,
|
|
||||||
|
|
||||||
fight: struct {
|
|
||||||
at: Position,
|
|
||||||
target: Server.ItemIdQuantity,
|
|
||||||
progress: u64 = 0,
|
|
||||||
},
|
|
||||||
gather: struct {
|
|
||||||
at: Position,
|
|
||||||
target: Server.ItemIdQuantity,
|
|
||||||
progress: u64 = 0,
|
|
||||||
},
|
|
||||||
craft: struct {
|
|
||||||
target: Server.ItemIdQuantity,
|
|
||||||
progress: u64 = 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
action_queue: std.ArrayList(QueuedAction),
|
|
||||||
|
|
||||||
fn init(allocator: Allocator, name: []const u8) !CharacterBrain {
|
|
||||||
return CharacterBrain{
|
|
||||||
.name = try allocator.dupe(u8, name),
|
|
||||||
.routine = .idle,
|
|
||||||
.action_queue = std.ArrayList(QueuedAction).init(allocator),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deinit(self: CharacterBrain) void {
|
|
||||||
const allocator = self.action_queue.allocator;
|
|
||||||
allocator.free(self.name);
|
|
||||||
self.action_queue.deinit();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn performNextAction(self: *CharacterBrain, api: *Server) !void {
|
|
||||||
assert(self.action_queue.items.len > 0);
|
|
||||||
|
|
||||||
const log = std.log.default;
|
|
||||||
|
|
||||||
switch (self.action_queue.items[0]) {
|
|
||||||
.fight => {
|
|
||||||
log.debug("{s} attacks", .{self.name});
|
|
||||||
_ = try api.actionFight(self.name);
|
|
||||||
},
|
|
||||||
.move => |pos| {
|
|
||||||
log.debug("move {s} to ({}, {})", .{self.name, pos.x, pos.y});
|
|
||||||
_ = try api.actionMove(self.name, pos.x, pos.y);
|
|
||||||
},
|
|
||||||
.deposit_gold => |quantity| {
|
|
||||||
log.debug("deposit {} gold from {s}", .{quantity, self.name});
|
|
||||||
_ = try api.actionBankDepositGold(self.name, quantity);
|
|
||||||
},
|
|
||||||
.deposit_item => |item| {
|
|
||||||
const code = api.getItemCode(item.id) orelse return error.ItemNotFound;
|
|
||||||
log.debug("deposit {s} (x{}) from {s}", .{code, item.quantity, self.name});
|
|
||||||
_ = try api.actionBankDepositItem(self.name, code, item.quantity);
|
|
||||||
},
|
|
||||||
.withdraw_item => |item| {
|
|
||||||
const code = api.getItemCode(item.id) orelse return error.ItemNotFound;
|
|
||||||
log.debug("withdraw {s} (x{}) from {s}", .{code, item.quantity, self.name});
|
|
||||||
_ = try api.actionBankWithdrawItem(self.name, code, item.quantity);
|
|
||||||
},
|
|
||||||
.gather => {
|
|
||||||
log.debug("{s} gathers", .{self.name});
|
|
||||||
_ = try api.actionGather(self.name);
|
|
||||||
},
|
|
||||||
.craft_item => |item| {
|
|
||||||
const code = api.getItemCode(item.id) orelse return error.ItemNotFound;
|
|
||||||
log.debug("craft {s} (x{}) from {s}", .{code, item.quantity, self.name});
|
|
||||||
_ = try api.actionCraft(self.name, code, item.quantity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = self.action_queue.orderedRemove(0);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fn currentTime() f64 {
|
fn currentTime() f64 {
|
||||||
const timestamp: f64 = @floatFromInt(std.time.milliTimestamp());
|
const timestamp: f64 = @floatFromInt(std.time.milliTimestamp());
|
||||||
return timestamp / std.time.ms_per_s;
|
return timestamp / std.time.ms_per_s;
|
||||||
@ -154,8 +66,6 @@ const GoalManager = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const bank_position: Position = .{ .x = 4, .y = 1 }; // TODO: Figure this out dynamically
|
|
||||||
|
|
||||||
fn getAPITokenFromArgs(allocator: Allocator) !?[]u8 {
|
fn getAPITokenFromArgs(allocator: Allocator) !?[]u8 {
|
||||||
const args = try std.process.argsAlloc(allocator);
|
const args = try std.process.argsAlloc(allocator);
|
||||||
defer std.process.argsFree(allocator, args);
|
defer std.process.argsFree(allocator, args);
|
||||||
@ -172,183 +82,6 @@ fn getAPITokenFromArgs(allocator: Allocator) !?[]u8 {
|
|||||||
return try allocator.dupe(u8, std.mem.trim(u8,token,"\n\t "));
|
return try allocator.dupe(u8, std.mem.trim(u8,token,"\n\t "));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn moveIfNeeded(api: *Server, brain: *CharacterBrain, pos: Position) !bool {
|
|
||||||
const character = api.findCharacter(brain.name).?;
|
|
||||||
|
|
||||||
if (character.position.eql(pos)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try brain.action_queue.append(.{ .move = pos });
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn depositItemsToBank(api: *Server, brain: *CharacterBrain) !bool {
|
|
||||||
const character = api.findCharacter(brain.name).?;
|
|
||||||
const action_queue = &brain.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.slots) |slot| {
|
|
||||||
if (slot.quantity == 0) continue;
|
|
||||||
|
|
||||||
if (slot.id) |item_id| {
|
|
||||||
try action_queue.append(.{
|
|
||||||
.deposit_item = .{ .id = item_id, .quantity = @intCast(slot.quantity) }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn depositIfFull(api: *Server, brain: *CharacterBrain) !bool {
|
|
||||||
const character = api.findCharacter(brain.name).?;
|
|
||||||
if (character.getItemCount() < character.inventory_max_items) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = try depositItemsToBank(api, brain);
|
|
||||||
|
|
||||||
if (character.gold > 0) {
|
|
||||||
try brain.action_queue.append(.{ .deposit_gold = @intCast(character.gold) });
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fightRoutine(api: *Server, brain: *CharacterBrain, enemy_position: Position) !void {
|
|
||||||
if (try depositIfFull(api, brain)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (try moveIfNeeded(api, brain, enemy_position)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try brain.action_queue.append(.{ .fight = {} });
|
|
||||||
}
|
|
||||||
|
|
||||||
fn gatherRoutine(api: *Server, brain: *CharacterBrain, resource_position: Position) !void {
|
|
||||||
if (try depositIfFull(api, brain)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (try moveIfNeeded(api, brain, resource_position)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try brain.action_queue.append(.{ .gather = {} });
|
|
||||||
}
|
|
||||||
|
|
||||||
fn withdrawFromBank(api: *Server, brain: *CharacterBrain, items: []const Server.ItemIdQuantity) !bool {
|
|
||||||
var character = api.findCharacter(brain.name).?;
|
|
||||||
|
|
||||||
var has_all_items = true;
|
|
||||||
for (items) |item_quantity| {
|
|
||||||
const inventory_quantity = 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(api, brain, bank_position)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (items) |item_quantity| {
|
|
||||||
const inventory_quantity = character.inventory.getItem(item_quantity.id);
|
|
||||||
if(inventory_quantity < item_quantity.quantity) {
|
|
||||||
try brain.action_queue.append(.{ .withdraw_item = .{
|
|
||||||
.id = item_quantity.id,
|
|
||||||
.quantity = item_quantity.quantity - inventory_quantity,
|
|
||||||
}});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn craftItem(api: *Server, brain: *CharacterBrain, id: Server.ItemId, quantity: u64) !bool {
|
|
||||||
var character = api.findCharacter(brain.name).?;
|
|
||||||
|
|
||||||
const inventory_quantity = character.inventory.getItem(id);
|
|
||||||
if (inventory_quantity >= quantity) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const code = api.getItemCode(id) orelse return error.InvalidItemId;
|
|
||||||
const item = try api.getItem(code) orelse return error.ItemNotFound;
|
|
||||||
if (item.craft == null) {
|
|
||||||
return error.NotCraftable;
|
|
||||||
}
|
|
||||||
|
|
||||||
const recipe = item.craft.?;
|
|
||||||
|
|
||||||
// TODO: Figure this out dynamically
|
|
||||||
const workstation = switch (recipe.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 },
|
|
||||||
};
|
|
||||||
|
|
||||||
if (try moveIfNeeded(api, brain, workstation)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
try brain.action_queue.append(.{ .craft_item = .{
|
|
||||||
.id = id,
|
|
||||||
.quantity = quantity - inventory_quantity
|
|
||||||
}});
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn craftRoutine(api: *Server, brain: *CharacterBrain, id: Server.ItemId, quantity: u64) !void {
|
|
||||||
var character = api.findCharacter(brain.name).?;
|
|
||||||
const inventory_quantity = character.inventory.getItem(id);
|
|
||||||
if (inventory_quantity >= quantity) {
|
|
||||||
if (try depositItemsToBank(api, brain)) {
|
|
||||||
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.?;
|
|
||||||
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(api, brain, needed_items.constSlice())) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (try craftItem(api, brain, id, quantity)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn main() !void {
|
pub fn main() !void {
|
||||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||||
defer _ = gpa.deinit();
|
defer _ = gpa.deinit();
|
||||||
@ -375,66 +108,78 @@ pub fn main() !void {
|
|||||||
goal_manager.characters.items[0].routine = .{
|
goal_manager.characters.items[0].routine = .{
|
||||||
.fight = .{
|
.fight = .{
|
||||||
.at = Position.init(0, 1),
|
.at = Position.init(0, 1),
|
||||||
.target = undefined
|
.target = .{
|
||||||
|
.id = try api.getItemId("egg"),
|
||||||
|
.quantity = 1
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
goal_manager.characters.items[1].routine = .{
|
goal_manager.characters.items[1].routine = .{
|
||||||
.gather = .{
|
.gather = .{
|
||||||
.at = Position.init(-1, 0),
|
.at = Position.init(-1, 0),
|
||||||
.target = undefined
|
.target = .{
|
||||||
|
.id = try api.getItemId("ash_wood"),
|
||||||
|
.quantity = 3
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
goal_manager.characters.items[2].routine = .{
|
goal_manager.characters.items[2].routine = .{
|
||||||
.gather = .{
|
.gather = .{
|
||||||
.at = Position.init(2, 0),
|
.at = Position.init(2, 0),
|
||||||
.target = undefined
|
.target = .{
|
||||||
|
.id = try api.getItemId("copper_ore"),
|
||||||
|
.quantity = 3
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
goal_manager.characters.items[3].routine = .{
|
goal_manager.characters.items[3].routine = .{
|
||||||
.gather = .{
|
.gather = .{
|
||||||
.at = Position.init(4, 2),
|
.at = Position.init(4, 2),
|
||||||
.target = undefined
|
.target = .{
|
||||||
|
.id = try api.getItemId("gudgeon"),
|
||||||
|
.quantity = 3
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
goal_manager.characters.items[4].routine = .{
|
goal_manager.characters.items[4].routine = .{
|
||||||
.fight = .{
|
.fight = .{
|
||||||
.at = Position.init(0, 1),
|
.at = Position.init(0, 1),
|
||||||
.target = undefined
|
.target = .{
|
||||||
|
.id = try api.getItemId("raw_chicken"),
|
||||||
|
.quantity = 1
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// goal_manager.characters.items[2].routine = .{
|
const APIError = Server.APIError;
|
||||||
// .craft = .{
|
|
||||||
// .target = .{
|
|
||||||
// .quantity = 3,
|
|
||||||
// .id = try api.getItemId("copper"),
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
std.log.info("Starting main loop", .{});
|
std.log.info("Starting main loop", .{});
|
||||||
while (true) {
|
while (true) {
|
||||||
try goal_manager.runNextAction();
|
goal_manager.runNextAction() catch |err| switch (err) {
|
||||||
|
APIError.ServerUnavailable => {
|
||||||
for (goal_manager.characters.items) |*character| {
|
// If the server is down, wait for a moment and try again.
|
||||||
if (character.action_queue.items.len > 0) continue;
|
std.time.sleep(std.time.ns_per_min * 5);
|
||||||
|
continue;
|
||||||
switch (character.routine) {
|
|
||||||
.idle => {},
|
|
||||||
.fight => |args| {
|
|
||||||
try fightRoutine(&api, character, args.at);
|
|
||||||
},
|
},
|
||||||
.gather => |args| {
|
|
||||||
try gatherRoutine(&api, character, args.at);
|
// TODO: Log all other error to a file or something. So it could be review later on.
|
||||||
},
|
else => return err
|
||||||
.craft => |args| {
|
};
|
||||||
try craftRoutine(&api, character, args.target.id, args.target.quantity);
|
|
||||||
|
for (goal_manager.characters.items) |*brain| {
|
||||||
|
if (brain.action_queue.items.len > 0) continue;
|
||||||
|
|
||||||
|
if (brain.isRoutineFinished()) {
|
||||||
|
if (!try brain.depositItemsToBank(&api)) {
|
||||||
|
brain.routine = .idle;
|
||||||
}
|
}
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try brain.performRoutine(&api);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user