228 lines
6.6 KiB
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;
|
|
}
|