// 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; }