artificer/lib/sim_server.zig

228 lines
6.6 KiB
Zig

// zig fmt: off
const std = @import("std");
const Api = @import("./api/root.zig");
const SimClock = @import("./sim_clock.zig");
const Server = @This();
const max_level = 40;
// https://docs.artifactsmmo.com/concepts/skills#experience-to-level
const max_xp_per_level = [max_level]u64{
150, // level 1
250,
350,
450,
700,
950,
1200,
1450,
1700,
2100, // level 10
2500,
2900,
3300,
3700,
4400,
5100,
5800,
6500,
7200,
8200, // level 20
9200,
10200,
11200,
12200,
13400,
14600,
15800,
17000,
18200,
19700, // level 30
21200,
22700,
24200,
25700,
27200,
28700,
30500,
32300,
34100,
35900, // level 40
};
store: *Api.Store,
clock: SimClock = .{},
rng: std.Random.DefaultPrng,
pub fn init(seed: u64, store: *Api.Store) Server {
return Server{
.rng = std.Random.DefaultPrng.init(seed),
.store = store
};
}
fn sleepNorm(self: *Server, stddev_ns: u64, mean_ns: u64) void {
const stddev_ns_f64: f64 = @floatFromInt(stddev_ns);
const mean_ns_f64: f64 = @floatFromInt(mean_ns);
var rng = self.rng.random();
const duration = rng.floatNorm(f64) * stddev_ns_f64 + mean_ns_f64;
self.clock.sleep(@intFromFloat(duration));
}
fn sleepRequestBegin(self: *Server) void {
self.sleepNorm(
100 * std.time.ns_per_ms,
350 * std.time.ns_per_ms,
);
}
fn sleepRequestEnd(self: *Server) void {
self.sleepNorm(
10 * std.time.ns_per_ms,
300 * std.time.ns_per_ms,
);
}
pub fn move(self: *Server, character_name: []const u8, position: Api.Position) Api.MoveError!Api.MoveResult {
self.sleepRequestBegin();
defer self.sleepRequestEnd();
const map: *Api.Map = self.store.getMap(position) orelse return Api.MoveError.MapNotFound;
const character_id = self.store.characters.getId(character_name) orelse return Api.MoveError.CharacterNotFound;
const character: *Api.Character = self.store.characters.get(character_id).?;
if (character.position.eql(position)) {
return Api.MoveError.CharacterAlreadyMap;
}
const now = @as(f64, @floatFromInt(self.clock.nanoTimestamp())) / std.time.ns_per_s;
if (character.cooldown_expiration) |cooldown_expiration| {
if (cooldown_expiration > now) {
return Api.MoveError.CharacterInCooldown;
}
}
const duration_i64 = (@abs(position.x - character.position.x) + @abs(position.y - character.position.y)) * 5;
const duration_f64: f64 = @floatFromInt(duration_i64);
const expiration = now + duration_f64;
character.cooldown_expiration = expiration;
character.position = position;
return Api.MoveResult{
.cooldown = Api.Cooldown{
.reason = .movement,
.started_at = now,
.expiration = expiration
},
.character = character.*,
.destination = map.*
};
}
pub fn gather(self: *Server, character_name: []const u8) Api.GatherError!Api.GatherResult {
self.sleepRequestBegin();
defer self.sleepRequestEnd();
const character_id = self.store.characters.getId(character_name) orelse return Api.GatherError.CharacterNotFound;
const character: *Api.Character = self.store.characters.get(character_id).?;
const now = @as(f64, @floatFromInt(self.clock.nanoTimestamp())) / std.time.ns_per_s;
if (character.cooldown_expiration) |cooldown_expiration| {
if (cooldown_expiration > now) {
return Api.GatherError.CharacterInCooldown;
}
}
const map: *Api.Map = self.store.getMap(character.position) orelse return Api.GatherError.FatalError;
const map_content = map.content orelse return Api.GatherError.MapContentNotFound;
if (map_content.type != .resource) {
return Api.GatherError.MapContentNotFound;
}
const map_content_code = map_content.code.slice();
const resource_id = self.store.resources.getId(map_content_code) orelse return Api.GatherError.MapContentNotFound;
const resource = self.store.resources.get(resource_id) orelse return Api.GatherError.FatalError;
const character_skill = character.skills.getPtr(resource.skill.toCharacterSkill());
if (character_skill.level < resource.level) {
return Api.GatherError.CharacterNotSkillLevelRequired;
}
const duration = 25; // TODO: Update duration calculation
const expiration = now + duration;
var items: Api.GatherResult.Details.Items = .{};
const xp = 8; // TODO: Update xp calculation
var rng = self.rng.random();
for (resource.drops.slice()) |_drop| {
const drop: Api.Resource.Drop = _drop;
if (rng.uintLessThan(u64, drop.rate) == 0) {
const quantity = rng.intRangeAtMost(u64, drop.min_quantity, drop.max_quantity);
items.add(drop.item, quantity) catch return Api.GatherError.FatalError;
}
}
var inventory = character.inventory;
inventory.addSlice(items.slice()) catch return Api.GatherError.CharacterInventoryFull;
if (inventory.totalQuantity() > character.inventory_max_items) {
return Api.GatherError.CharacterInventoryFull;
}
character.inventory = inventory;
character_skill.xp += xp;
while (character_skill.xp > character_skill.max_xp and character_skill.level < max_level) {
character_skill.level += 1;
character_skill.max_xp = max_xp_per_level[character_skill.level - 1];
}
if (character_skill.level == max_level) {
character_skill.xp = 0;
character_skill.max_xp = 0;
}
character.cooldown_expiration = expiration;
return Api.GatherResult{
.character = character.*,
.cooldown = Api.Cooldown{
.reason = .gathering,
.started_at = now,
.expiration = expiration
},
.details = Api.GatherResult.Details{
.xp = xp,
.items = items
}
};
}
pub fn craft(self: *Server, character: []const u8, item: []const u8, quantity: u64) Api.CraftError!Api.CraftResult {
_ = self;
_ = character;
_ = item;
_ = quantity;
return Api.FetchError.FatalError;
}
pub fn equip(self: *Server, character: []const u8, slot: Api.Equipment.SlotId, item: []const u8, quantity: u64) Api.EquipError!Api.EquipResult {
_ = self;
_ = character;
_ = item;
_ = slot;
_ = quantity;
return Api.FetchError.FatalError;
}
pub fn unequip(self: *Server, character: []const u8, slot: Api.Equipment.SlotId, quantity: u64) Api.EquipError!Api.EquipResult {
_ = self;
_ = character;
_ = slot;
_ = quantity;
return Api.FetchError.FatalError;
}