Compare commits
No commits in common. "82e4ae8a81dd38341cee00b88a71581edc2d2d1a" and "0b15fb02d20ef1dede2a61a0d3512383e307ee96" have entirely different histories.
82e4ae8a81
...
0b15fb02d2
File diff suppressed because it is too large
Load Diff
@ -1,24 +1,24 @@
|
|||||||
using MongoDB.Bson.Serialization.Attributes;
|
using MongoDB.Bson.Serialization.Attributes;
|
||||||
using MongoDB.Bson;
|
using MongoDB.Bson;
|
||||||
|
|
||||||
namespace CharacterApi.Models;
|
namespace CharacterApi.Models;
|
||||||
|
|
||||||
[BsonIgnoreExtraElements]
|
[BsonIgnoreExtraElements]
|
||||||
public class VisibleLocation
|
public class VisibleLocation
|
||||||
{
|
{
|
||||||
[BsonId]
|
[BsonId]
|
||||||
[BsonRepresentation(BsonType.ObjectId)]
|
[BsonRepresentation(BsonType.ObjectId)]
|
||||||
public string? Id { get; set; }
|
public string? Id { get; set; }
|
||||||
|
|
||||||
[BsonElement("name")]
|
[BsonElement("name")]
|
||||||
public string Name { get; set; } = string.Empty;
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
[BsonElement("coord")]
|
[BsonElement("coord")]
|
||||||
public LocationCoord Coord { get; set; } = new();
|
public LocationCoord Coord { get; set; } = new();
|
||||||
|
|
||||||
[BsonElement("biomeKey")]
|
[BsonElement("biomeKey")]
|
||||||
public string BiomeKey { get; set; } = "plains";
|
public string BiomeKey { get; set; } = "plains";
|
||||||
|
|
||||||
[BsonElement("locationObject")]
|
[BsonElement("resources")]
|
||||||
public VisibleLocationObject? LocationObject { get; set; }
|
public List<VisibleLocationResource> Resources { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,21 +0,0 @@
|
|||||||
using MongoDB.Bson.Serialization.Attributes;
|
|
||||||
|
|
||||||
namespace CharacterApi.Models;
|
|
||||||
|
|
||||||
public class VisibleLocationObject
|
|
||||||
{
|
|
||||||
[BsonElement("id")]
|
|
||||||
public string Id { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
[BsonElement("objectType")]
|
|
||||||
public string ObjectType { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
[BsonElement("objectKey")]
|
|
||||||
public string ObjectKey { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
[BsonElement("name")]
|
|
||||||
public string Name { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
[BsonElement("state")]
|
|
||||||
public VisibleLocationObjectState State { get; set; } = new();
|
|
||||||
}
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
using MongoDB.Bson.Serialization.Attributes;
|
|
||||||
|
|
||||||
namespace CharacterApi.Models;
|
|
||||||
|
|
||||||
public class VisibleLocationObjectState
|
|
||||||
{
|
|
||||||
[BsonElement("itemKey")]
|
|
||||||
public string ItemKey { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
[BsonElement("remainingQuantity")]
|
|
||||||
public int RemainingQuantity { get; set; }
|
|
||||||
|
|
||||||
[BsonElement("gatherQuantity")]
|
|
||||||
public int GatherQuantity { get; set; } = 1;
|
|
||||||
}
|
|
||||||
@ -1,9 +1,9 @@
|
|||||||
using CharacterApi.Models;
|
using CharacterApi.Models;
|
||||||
using MongoDB.Bson;
|
using MongoDB.Bson;
|
||||||
using MongoDB.Driver;
|
using MongoDB.Driver;
|
||||||
|
|
||||||
namespace CharacterApi.Services;
|
namespace CharacterApi.Services;
|
||||||
|
|
||||||
public class CharacterStore
|
public class CharacterStore
|
||||||
{
|
{
|
||||||
private readonly IMongoCollection<Character> _col;
|
private readonly IMongoCollection<Character> _col;
|
||||||
@ -13,45 +13,87 @@ public class CharacterStore
|
|||||||
private static readonly string[] BiomeOrder = ["plains", "forest", "wetlands", "rocky", "desert"];
|
private static readonly string[] BiomeOrder = ["plains", "forest", "wetlands", "rocky", "desert"];
|
||||||
private static readonly Dictionary<string, BiomeDefinition> Biomes = new()
|
private static readonly Dictionary<string, BiomeDefinition> Biomes = new()
|
||||||
{
|
{
|
||||||
["plains"] = new("plains", 3.0, new()
|
["plains"] = new BiomeDefinition(
|
||||||
{
|
"plains",
|
||||||
["forest"] = 1.7,
|
3.0,
|
||||||
["wetlands"] = 0.9,
|
new Dictionary<string, double>
|
||||||
["rocky"] = 0.8,
|
{
|
||||||
["desert"] = 0.4
|
["forest"] = 1.7,
|
||||||
}),
|
["wetlands"] = 0.9,
|
||||||
["forest"] = new("forest", 3.4, new()
|
["rocky"] = 0.8,
|
||||||
{
|
["desert"] = 0.4
|
||||||
["plains"] = 1.6,
|
},
|
||||||
["wetlands"] = 1.3,
|
new[]
|
||||||
["rocky"] = 0.5,
|
{
|
||||||
["desert"] = 0.1
|
new ResourceRule("grass", 180, 420, 12),
|
||||||
}),
|
new ResourceRule("wood", 25, 90, 3),
|
||||||
["wetlands"] = new("wetlands", 3.1, new()
|
new ResourceRule("stone", 10, 40, 2)
|
||||||
{
|
}),
|
||||||
["forest"] = 1.5,
|
["forest"] = new BiomeDefinition(
|
||||||
["plains"] = 1.1,
|
"forest",
|
||||||
["rocky"] = 0.2,
|
3.4,
|
||||||
["desert"] = 0.05
|
new Dictionary<string, double>
|
||||||
}),
|
{
|
||||||
["rocky"] = new("rocky", 3.0, new()
|
["plains"] = 1.6,
|
||||||
{
|
["wetlands"] = 1.3,
|
||||||
["plains"] = 1.2,
|
["rocky"] = 0.5,
|
||||||
["forest"] = 0.6,
|
["desert"] = 0.1
|
||||||
["desert"] = 1.1,
|
},
|
||||||
["wetlands"] = 0.1
|
new[]
|
||||||
}),
|
{
|
||||||
["desert"] = new("desert", 3.2, new()
|
new ResourceRule("wood", 220, 520, 6),
|
||||||
{
|
new ResourceRule("grass", 90, 220, 8),
|
||||||
["rocky"] = 1.4,
|
new ResourceRule("stone", 15, 45, 2)
|
||||||
["plains"] = 0.8,
|
}),
|
||||||
["forest"] = 0.1,
|
["wetlands"] = new BiomeDefinition(
|
||||||
["wetlands"] = 0.05
|
"wetlands",
|
||||||
})
|
3.1,
|
||||||
|
new Dictionary<string, double>
|
||||||
|
{
|
||||||
|
["forest"] = 1.5,
|
||||||
|
["plains"] = 1.1,
|
||||||
|
["rocky"] = 0.2,
|
||||||
|
["desert"] = 0.05
|
||||||
|
},
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
new ResourceRule("grass", 260, 600, 15),
|
||||||
|
new ResourceRule("wood", 40, 120, 4)
|
||||||
|
}),
|
||||||
|
["rocky"] = new BiomeDefinition(
|
||||||
|
"rocky",
|
||||||
|
3.0,
|
||||||
|
new Dictionary<string, double>
|
||||||
|
{
|
||||||
|
["plains"] = 1.2,
|
||||||
|
["forest"] = 0.6,
|
||||||
|
["desert"] = 1.1,
|
||||||
|
["wetlands"] = 0.1
|
||||||
|
},
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
new ResourceRule("stone", 220, 540, 8),
|
||||||
|
new ResourceRule("wood", 10, 40, 2),
|
||||||
|
new ResourceRule("grass", 20, 70, 4)
|
||||||
|
}),
|
||||||
|
["desert"] = new BiomeDefinition(
|
||||||
|
"desert",
|
||||||
|
3.2,
|
||||||
|
new Dictionary<string, double>
|
||||||
|
{
|
||||||
|
["rocky"] = 1.4,
|
||||||
|
["plains"] = 0.8,
|
||||||
|
["forest"] = 0.1,
|
||||||
|
["wetlands"] = 0.05
|
||||||
|
},
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
new ResourceRule("stone", 80, 220, 5),
|
||||||
|
new ResourceRule("grass", 5, 25, 2)
|
||||||
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
public sealed record VisibleLocationResult(List<VisibleLocation> Locations, int GeneratedCount);
|
public sealed record VisibleLocationResult(List<VisibleLocation> Locations, int GeneratedCount);
|
||||||
private sealed record BiomeDefinition(string Key, double ContinuationWeight, Dictionary<string, double> TransitionWeights);
|
|
||||||
|
|
||||||
public CharacterStore(IConfiguration cfg)
|
public CharacterStore(IConfiguration cfg)
|
||||||
{
|
{
|
||||||
@ -66,9 +108,9 @@ public class CharacterStore
|
|||||||
var ownerIndex = Builders<Character>.IndexKeys.Ascending(c => c.OwnerUserId);
|
var ownerIndex = Builders<Character>.IndexKeys.Ascending(c => c.OwnerUserId);
|
||||||
_col.Indexes.CreateOne(new CreateIndexModel<Character>(ownerIndex));
|
_col.Indexes.CreateOne(new CreateIndexModel<Character>(ownerIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task CreateAsync(Character character) => _col.InsertOneAsync(character);
|
public Task CreateAsync(Character character) => _col.InsertOneAsync(character);
|
||||||
|
|
||||||
public Task<List<Character>> GetForOwnerAsync(string ownerUserId) =>
|
public Task<List<Character>> GetForOwnerAsync(string ownerUserId) =>
|
||||||
_col.Find(c => c.OwnerUserId == ownerUserId).ToListAsync();
|
_col.Find(c => c.OwnerUserId == ownerUserId).ToListAsync();
|
||||||
|
|
||||||
@ -83,8 +125,10 @@ public class CharacterStore
|
|||||||
return result.ModifiedCount > 0 || result.MatchedCount > 0;
|
return result.ModifiedCount > 0 || result.MatchedCount > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<List<VisibleLocation>> GetVisibleLocationsAsync(Character character) =>
|
public Task<List<VisibleLocation>> GetVisibleLocationsAsync(Character character)
|
||||||
GetVisibleLocationsInternalAsync(character, ensureGenerated: false);
|
{
|
||||||
|
return GetVisibleLocationsInternalAsync(character, ensureGenerated: false);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<VisibleLocationResult> GetOrCreateVisibleLocationsAsync(Character character)
|
public async Task<VisibleLocationResult> GetOrCreateVisibleLocationsAsync(Character character)
|
||||||
{
|
{
|
||||||
@ -109,13 +153,6 @@ public class CharacterStore
|
|||||||
);
|
);
|
||||||
|
|
||||||
var documents = await _locations.Find(filter).ToListAsync();
|
var documents = await _locations.Find(filter).ToListAsync();
|
||||||
if (ensureGenerated)
|
|
||||||
{
|
|
||||||
foreach (var document in documents)
|
|
||||||
await BackfillLocationStateAsync(document);
|
|
||||||
documents = await _locations.Find(filter).ToListAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
return documents.Select(MapVisibleLocation).ToList();
|
return documents.Select(MapVisibleLocation).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,6 +173,39 @@ public class CharacterStore
|
|||||||
return generatedCount;
|
return generatedCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string DefaultLocationName(int x, int y)
|
||||||
|
{
|
||||||
|
if (x == 0 && y == 0)
|
||||||
|
return "Origin";
|
||||||
|
return $"Location {x},{y}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static VisibleLocation MapVisibleLocation(BsonDocument document)
|
||||||
|
{
|
||||||
|
var coord = document.GetValue("coord", new BsonDocument()).AsBsonDocument;
|
||||||
|
var idValue = document.GetValue("_id", BsonNull.Value);
|
||||||
|
string? id = null;
|
||||||
|
if (!idValue.IsBsonNull)
|
||||||
|
{
|
||||||
|
id = idValue.BsonType == BsonType.ObjectId
|
||||||
|
? idValue.AsObjectId.ToString()
|
||||||
|
: idValue.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new VisibleLocation
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
Name = document.GetValue("name", "").AsString,
|
||||||
|
Coord = new LocationCoord
|
||||||
|
{
|
||||||
|
X = coord.GetValue("x", 0).ToInt32(),
|
||||||
|
Y = coord.GetValue("y", 0).ToInt32()
|
||||||
|
},
|
||||||
|
BiomeKey = document.GetValue("biomeKey", "plains").AsString,
|
||||||
|
Resources = MapVisibleLocationResources(document)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<bool> EnsureLocationStateAsync(int x, int y)
|
private async Task<bool> EnsureLocationStateAsync(int x, int y)
|
||||||
{
|
{
|
||||||
var filter = Builders<BsonDocument>.Filter.And(
|
var filter = Builders<BsonDocument>.Filter.And(
|
||||||
@ -160,8 +230,7 @@ public class CharacterStore
|
|||||||
{ "y", y }
|
{ "y", y }
|
||||||
})
|
})
|
||||||
.SetOnInsert("biomeKey", biomeKey)
|
.SetOnInsert("biomeKey", biomeKey)
|
||||||
.SetOnInsert("locationObject", CreateLocationObjectValueForBiome(biomeKey, x, y))
|
.SetOnInsert("resources", BuildResourcesDocument(biomeKey, x, y))
|
||||||
.SetOnInsert("locationObjectResolved", true)
|
|
||||||
.SetOnInsert("createdUtc", DateTime.UtcNow);
|
.SetOnInsert("createdUtc", DateTime.UtcNow);
|
||||||
|
|
||||||
try
|
try
|
||||||
@ -185,17 +254,11 @@ public class CharacterStore
|
|||||||
var biomeKey = document.GetValue("biomeKey", BsonNull.Value).IsBsonNull
|
var biomeKey = document.GetValue("biomeKey", BsonNull.Value).IsBsonNull
|
||||||
? await DetermineBiomeKeyAsync(x, y)
|
? await DetermineBiomeKeyAsync(x, y)
|
||||||
: document.GetValue("biomeKey", "plains").AsString;
|
: document.GetValue("biomeKey", "plains").AsString;
|
||||||
var objectResolved = document.TryGetValue("locationObjectResolved", out var resolvedValue) &&
|
|
||||||
resolvedValue.ToBoolean();
|
|
||||||
|
|
||||||
if (!document.Contains("biomeKey"))
|
if (!document.Contains("biomeKey"))
|
||||||
updates.Add(Builders<BsonDocument>.Update.Set("biomeKey", biomeKey));
|
updates.Add(Builders<BsonDocument>.Update.Set("biomeKey", biomeKey));
|
||||||
if (!objectResolved)
|
if (!document.Contains("resources"))
|
||||||
{
|
updates.Add(Builders<BsonDocument>.Update.Set("resources", BuildResourcesDocument(biomeKey, x, y)));
|
||||||
var locationObject = TryMigrateLegacyResource(document) ?? CreateLocationObjectValueForBiome(biomeKey, x, y);
|
|
||||||
updates.Add(Builders<BsonDocument>.Update.Set("locationObject", locationObject));
|
|
||||||
updates.Add(Builders<BsonDocument>.Update.Set("locationObjectResolved", true));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updates.Count == 0)
|
if (updates.Count == 0)
|
||||||
return;
|
return;
|
||||||
@ -254,7 +317,14 @@ public class CharacterStore
|
|||||||
|
|
||||||
private async Task<List<string>> LoadNeighborBiomeKeysAsync(int x, int y)
|
private async Task<List<string>> LoadNeighborBiomeKeysAsync(int x, int y)
|
||||||
{
|
{
|
||||||
var coords = new[] { (x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1) };
|
var coords = new[]
|
||||||
|
{
|
||||||
|
(x - 1, y),
|
||||||
|
(x + 1, y),
|
||||||
|
(x, y - 1),
|
||||||
|
(x, y + 1)
|
||||||
|
};
|
||||||
|
|
||||||
var filters = coords.Select(coord =>
|
var filters = coords.Select(coord =>
|
||||||
Builders<BsonDocument>.Filter.And(
|
Builders<BsonDocument>.Filter.And(
|
||||||
Builders<BsonDocument>.Filter.Eq("coord.x", coord.Item1),
|
Builders<BsonDocument>.Filter.Eq("coord.x", coord.Item1),
|
||||||
@ -287,131 +357,68 @@ public class CharacterStore
|
|||||||
return "plains";
|
return "plains";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static BsonValue CreateLocationObjectValueForBiome(string biomeKey, int x, int y)
|
private static BsonArray BuildResourcesDocument(string biomeKey, int x, int y)
|
||||||
{
|
{
|
||||||
var roll = StableNoise(x, y, 401);
|
if (!Biomes.TryGetValue(biomeKey, out var biome))
|
||||||
return biomeKey switch
|
biome = Biomes["plains"];
|
||||||
{
|
|
||||||
"forest" => roll switch
|
|
||||||
{
|
|
||||||
< 0.35 => BsonNull.Value,
|
|
||||||
< 0.80 => CreateGatherableObjectDocument("wood", 60, 3),
|
|
||||||
< 0.95 => CreateGatherableObjectDocument("grass", 120, 10),
|
|
||||||
_ => CreateGatherableObjectDocument("stone", 40, 2)
|
|
||||||
},
|
|
||||||
"rocky" => roll switch
|
|
||||||
{
|
|
||||||
< 0.60 => BsonNull.Value,
|
|
||||||
< 0.90 => CreateGatherableObjectDocument("stone", 40, 2),
|
|
||||||
_ => CreateGatherableObjectDocument("wood", 60, 3)
|
|
||||||
},
|
|
||||||
"wetlands" => roll switch
|
|
||||||
{
|
|
||||||
< 0.40 => BsonNull.Value,
|
|
||||||
< 0.90 => CreateGatherableObjectDocument("grass", 120, 10),
|
|
||||||
_ => CreateGatherableObjectDocument("wood", 60, 3)
|
|
||||||
},
|
|
||||||
"desert" => roll switch
|
|
||||||
{
|
|
||||||
< 0.70 => BsonNull.Value,
|
|
||||||
< 0.95 => CreateGatherableObjectDocument("stone", 40, 2),
|
|
||||||
_ => CreateGatherableObjectDocument("wood", 60, 3)
|
|
||||||
},
|
|
||||||
_ => roll switch
|
|
||||||
{
|
|
||||||
< 0.50 => BsonNull.Value,
|
|
||||||
< 0.85 => CreateGatherableObjectDocument("grass", 120, 10),
|
|
||||||
_ => CreateGatherableObjectDocument("wood", 60, 3)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static BsonDocument? TryMigrateLegacyResource(BsonDocument document)
|
var resources = new BsonArray();
|
||||||
{
|
foreach (var rule in biome.ResourceRules)
|
||||||
if (!document.TryGetValue("resources", out var resourcesValue) || resourcesValue is not BsonArray resources)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
foreach (var resourceValue in resources)
|
|
||||||
{
|
{
|
||||||
if (resourceValue is not BsonDocument resource)
|
var roll = StableNoise(x, y, StableHash(rule.ItemKey));
|
||||||
|
var quantity = rule.MinQuantity + (int)Math.Round(roll * (rule.MaxQuantity - rule.MinQuantity));
|
||||||
|
if (quantity <= 0)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var remainingQuantity = resource.GetValue("remainingQuantity", 0).ToInt32();
|
resources.Add(new BsonDocument
|
||||||
if (remainingQuantity <= 0)
|
{
|
||||||
|
{ "itemKey", rule.ItemKey },
|
||||||
|
{ "remainingQuantity", quantity },
|
||||||
|
{ "gatherQuantity", rule.GatherQuantity }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return resources;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double StableNoise(int x, int y, int salt)
|
||||||
|
{
|
||||||
|
var value = Math.Sin((x * 12.9898) + (y * 78.233) + ((WorldSeed + salt) * 0.1597)) * 43758.5453;
|
||||||
|
return value - Math.Floor(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int StableHash(string value)
|
||||||
|
{
|
||||||
|
unchecked
|
||||||
|
{
|
||||||
|
var hash = 23;
|
||||||
|
foreach (var c in value)
|
||||||
|
hash = (hash * 31) + c;
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<VisibleLocationResource> MapVisibleLocationResources(BsonDocument document)
|
||||||
|
{
|
||||||
|
if (!document.TryGetValue("resources", out var resourcesValue) || resourcesValue.BsonType != BsonType.Array)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
var results = new List<VisibleLocationResource>();
|
||||||
|
foreach (var value in resourcesValue.AsBsonArray)
|
||||||
|
{
|
||||||
|
if (value.BsonType != BsonType.Document)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var itemKey = NormalizeItemKey(resource.GetValue("itemKey", "").AsString);
|
var resource = value.AsBsonDocument;
|
||||||
var gatherQuantity = Math.Max(1, resource.GetValue("gatherQuantity", 1).ToInt32());
|
results.Add(new VisibleLocationResource
|
||||||
return CreateGatherableObjectDocument(itemKey, remainingQuantity, gatherQuantity);
|
{
|
||||||
|
ItemKey = resource.GetValue("itemKey", "").AsString,
|
||||||
|
RemainingQuantity = resource.GetValue("remainingQuantity", 0).ToInt32(),
|
||||||
|
GatherQuantity = resource.GetValue("gatherQuantity", 1).ToInt32()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return results;
|
||||||
}
|
|
||||||
|
|
||||||
private static BsonDocument CreateGatherableObjectDocument(string itemKey, int remainingQuantity, int gatherQuantity)
|
|
||||||
{
|
|
||||||
var normalizedItemKey = NormalizeItemKey(itemKey);
|
|
||||||
return new BsonDocument
|
|
||||||
{
|
|
||||||
{ "id", Guid.NewGuid().ToString("N") },
|
|
||||||
{ "objectType", "gatherable" },
|
|
||||||
{ "objectKey", $"{normalizedItemKey}_node" },
|
|
||||||
{ "name", HumanizeItemKey(normalizedItemKey) },
|
|
||||||
{
|
|
||||||
"state", new BsonDocument
|
|
||||||
{
|
|
||||||
{ "itemKey", normalizedItemKey },
|
|
||||||
{ "remainingQuantity", remainingQuantity },
|
|
||||||
{ "gatherQuantity", gatherQuantity }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static VisibleLocation MapVisibleLocation(BsonDocument document)
|
|
||||||
{
|
|
||||||
var coord = document.GetValue("coord", new BsonDocument()).AsBsonDocument;
|
|
||||||
var locationObject = MapVisibleLocationObject(document.GetValue("locationObject", BsonNull.Value));
|
|
||||||
var idValue = document.GetValue("_id", BsonNull.Value);
|
|
||||||
string? id = null;
|
|
||||||
if (!idValue.IsBsonNull)
|
|
||||||
{
|
|
||||||
id = idValue.BsonType == BsonType.ObjectId ? idValue.AsObjectId.ToString() : idValue.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new VisibleLocation
|
|
||||||
{
|
|
||||||
Id = id,
|
|
||||||
Name = document.GetValue("name", "").AsString,
|
|
||||||
Coord = new LocationCoord
|
|
||||||
{
|
|
||||||
X = coord.GetValue("x", 0).ToInt32(),
|
|
||||||
Y = coord.GetValue("y", 0).ToInt32()
|
|
||||||
},
|
|
||||||
BiomeKey = document.GetValue("biomeKey", "plains").AsString,
|
|
||||||
LocationObject = locationObject
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static VisibleLocationObject? MapVisibleLocationObject(BsonValue value)
|
|
||||||
{
|
|
||||||
if (value.IsBsonNull || value is not BsonDocument document)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var stateDoc = document.GetValue("state", new BsonDocument()).AsBsonDocument;
|
|
||||||
return new VisibleLocationObject
|
|
||||||
{
|
|
||||||
Id = document.GetValue("id", "").AsString,
|
|
||||||
ObjectType = document.GetValue("objectType", "").AsString,
|
|
||||||
ObjectKey = document.GetValue("objectKey", "").AsString,
|
|
||||||
Name = document.GetValue("name", "").AsString,
|
|
||||||
State = new VisibleLocationObjectState
|
|
||||||
{
|
|
||||||
ItemKey = stateDoc.GetValue("itemKey", "").AsString,
|
|
||||||
RemainingQuantity = stateDoc.GetValue("remainingQuantity", 0).ToInt32(),
|
|
||||||
GatherQuantity = stateDoc.GetValue("gatherQuantity", 1).ToInt32()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EnsureLocationCoordIndexes()
|
private void EnsureLocationCoordIndexes()
|
||||||
@ -428,7 +435,11 @@ public class CharacterStore
|
|||||||
_locations.Indexes.DropOne(name);
|
_locations.Indexes.DropOne(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
var coordIndex = new BsonDocument { { "coord.x", 1 }, { "coord.y", 1 } };
|
var coordIndex = new BsonDocument
|
||||||
|
{
|
||||||
|
{ "coord.x", 1 },
|
||||||
|
{ "coord.y", 1 }
|
||||||
|
};
|
||||||
var coordIndexOptions = new CreateIndexOptions { Unique = true, Name = CoordIndexName };
|
var coordIndexOptions = new CreateIndexOptions { Unique = true, Name = CoordIndexName };
|
||||||
_locations.Indexes.CreateOne(new CreateIndexModel<BsonDocument>(coordIndex, coordIndexOptions));
|
_locations.Indexes.CreateOne(new CreateIndexModel<BsonDocument>(coordIndex, coordIndexOptions));
|
||||||
}
|
}
|
||||||
@ -451,42 +462,22 @@ public class CharacterStore
|
|||||||
{
|
{
|
||||||
var filter = Builders<Character>.Filter.Eq(c => c.Id, id);
|
var filter = Builders<Character>.Filter.Eq(c => c.Id, id);
|
||||||
if (!allowAnyOwner)
|
if (!allowAnyOwner)
|
||||||
filter = Builders<Character>.Filter.And(filter, Builders<Character>.Filter.Eq(c => c.OwnerUserId, ownerUserId));
|
{
|
||||||
|
filter = Builders<Character>.Filter.And(
|
||||||
|
filter,
|
||||||
|
Builders<Character>.Filter.Eq(c => c.OwnerUserId, ownerUserId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
var result = await _col.DeleteOneAsync(filter);
|
var result = await _col.DeleteOneAsync(filter);
|
||||||
return result.DeletedCount > 0;
|
return result.DeletedCount > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string DefaultLocationName(int x, int y)
|
private sealed record ResourceRule(string ItemKey, int MinQuantity, int MaxQuantity, int GatherQuantity);
|
||||||
{
|
|
||||||
if (x == 0 && y == 0)
|
|
||||||
return "Origin";
|
|
||||||
return $"Location {x},{y}";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string NormalizeItemKey(string itemKey) => itemKey.Trim().ToLowerInvariant();
|
private sealed record BiomeDefinition(
|
||||||
|
string Key,
|
||||||
private static string HumanizeItemKey(string itemKey)
|
double ContinuationWeight,
|
||||||
{
|
Dictionary<string, double> TransitionWeights,
|
||||||
return string.Join(" ", itemKey.Split('_', StringSplitOptions.RemoveEmptyEntries)
|
IReadOnlyList<ResourceRule> ResourceRules);
|
||||||
.Where(part => part.Length > 0)
|
|
||||||
.Select(part => char.ToUpperInvariant(part[0]) + part[1..]));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double StableNoise(int x, int y, int salt)
|
|
||||||
{
|
|
||||||
var value = Math.Sin((x * 12.9898) + (y * 78.233) + ((WorldSeed + salt) * 0.1597)) * 43758.5453;
|
|
||||||
return value - Math.Floor(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int StableHash(string value)
|
|
||||||
{
|
|
||||||
unchecked
|
|
||||||
{
|
|
||||||
var hash = 17;
|
|
||||||
foreach (var ch in value)
|
|
||||||
hash = (hash * 31) + ch;
|
|
||||||
return hash;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -94,42 +94,40 @@ public class LocationsController : ControllerBase
|
|||||||
return Ok("Updated");
|
return Ok("Updated");
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{id}/interact")]
|
[HttpPost("{id}/gather")]
|
||||||
[Authorize(Roles = "USER,SUPER")]
|
[Authorize(Roles = "USER,SUPER")]
|
||||||
public async Task<IActionResult> Interact(string id, [FromBody] InteractLocationObjectRequest req)
|
public async Task<IActionResult> Gather(string id, [FromBody] GatherResourceRequest req)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(req.CharacterId))
|
if (string.IsNullOrWhiteSpace(req.CharacterId))
|
||||||
return BadRequest("characterId required");
|
return BadRequest("characterId required");
|
||||||
if (string.IsNullOrWhiteSpace(req.ObjectId))
|
if (string.IsNullOrWhiteSpace(req.ResourceKey))
|
||||||
return BadRequest("objectId required");
|
return BadRequest("resourceKey required");
|
||||||
|
|
||||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
if (string.IsNullOrWhiteSpace(userId))
|
if (string.IsNullOrWhiteSpace(userId))
|
||||||
return Unauthorized();
|
return Unauthorized();
|
||||||
|
|
||||||
var allowAnyOwner = User.IsInRole("SUPER");
|
var allowAnyOwner = User.IsInRole("SUPER");
|
||||||
var interact = await _locations.InteractWithObjectAsync(id, req.CharacterId, req.ObjectId, userId, allowAnyOwner);
|
var gather = await _locations.GatherResourceAsync(id, req.CharacterId, req.ResourceKey, userId, allowAnyOwner);
|
||||||
if (interact.Status == InteractStatus.LocationNotFound)
|
if (gather.Status == GatherStatus.LocationNotFound)
|
||||||
return NotFound("Location not found");
|
return NotFound("Location not found");
|
||||||
if (interact.Status == InteractStatus.CharacterNotFound)
|
if (gather.Status == GatherStatus.CharacterNotFound)
|
||||||
return NotFound("Character not found");
|
return NotFound("Character not found");
|
||||||
if (interact.Status == InteractStatus.Forbidden)
|
if (gather.Status == GatherStatus.Forbidden)
|
||||||
return Forbid();
|
return Forbid();
|
||||||
if (interact.Status == InteractStatus.Invalid)
|
if (gather.Status == GatherStatus.Invalid)
|
||||||
return BadRequest("Character is not at the target location");
|
return BadRequest("Character is not at the target location");
|
||||||
if (interact.Status == InteractStatus.ObjectNotFound)
|
if (gather.Status == GatherStatus.ResourceNotFound)
|
||||||
return NotFound("Location object not found");
|
return NotFound("Resource not found at location");
|
||||||
if (interact.Status == InteractStatus.UnsupportedObjectType)
|
if (gather.Status == GatherStatus.ResourceDepleted)
|
||||||
return BadRequest("Location object type is not supported");
|
return Conflict("Resource is depleted");
|
||||||
if (interact.Status == InteractStatus.ObjectConsumed)
|
|
||||||
return Conflict("Location object is consumed");
|
|
||||||
|
|
||||||
var inventoryBaseUrl = (_configuration["Services:InventoryApiBaseUrl"] ?? "http://localhost:5003").TrimEnd('/');
|
var inventoryBaseUrl = (_configuration["Services:InventoryApiBaseUrl"] ?? "http://localhost:5003").TrimEnd('/');
|
||||||
var token = Request.Headers.Authorization.ToString();
|
var token = Request.Headers.Authorization.ToString();
|
||||||
var grantBody = JsonSerializer.Serialize(new
|
var grantBody = JsonSerializer.Serialize(new
|
||||||
{
|
{
|
||||||
itemKey = interact.ItemKey,
|
itemKey = gather.ResourceKey,
|
||||||
quantity = interact.QuantityGranted
|
quantity = gather.QuantityGranted
|
||||||
});
|
});
|
||||||
|
|
||||||
var client = _httpClientFactory.CreateClient();
|
var client = _httpClientFactory.CreateClient();
|
||||||
@ -144,21 +142,17 @@ public class LocationsController : ControllerBase
|
|||||||
var responseBody = await response.Content.ReadAsStringAsync();
|
var responseBody = await response.Content.ReadAsStringAsync();
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
if (interact.PreviousObject is not null)
|
await _locations.RestoreGatheredResourceAsync(id, gather.ResourceKey, gather.QuantityGranted);
|
||||||
await _locations.RestoreObjectInteractionAsync(id, interact.PreviousObject);
|
|
||||||
return StatusCode((int)response.StatusCode, responseBody);
|
return StatusCode((int)response.StatusCode, responseBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(new InteractLocationObjectResponse
|
return Ok(new GatherResourceResponse
|
||||||
{
|
{
|
||||||
LocationId = id,
|
LocationId = id,
|
||||||
CharacterId = req.CharacterId,
|
CharacterId = req.CharacterId,
|
||||||
ObjectId = interact.ObjectId,
|
ResourceKey = gather.ResourceKey,
|
||||||
ObjectType = interact.ObjectType,
|
QuantityGranted = gather.QuantityGranted,
|
||||||
ItemKey = interact.ItemKey,
|
RemainingQuantity = gather.RemainingQuantity,
|
||||||
QuantityGranted = interact.QuantityGranted,
|
|
||||||
RemainingQuantity = interact.RemainingQuantity,
|
|
||||||
Consumed = interact.Consumed,
|
|
||||||
InventoryResponseJson = responseBody
|
InventoryResponseJson = responseBody
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +0,0 @@
|
|||||||
namespace LocationsApi.Models;
|
|
||||||
|
|
||||||
public class InteractLocationObjectRequest
|
|
||||||
{
|
|
||||||
public string CharacterId { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
public string ObjectId { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
namespace LocationsApi.Models;
|
|
||||||
|
|
||||||
public class InteractLocationObjectResponse
|
|
||||||
{
|
|
||||||
public string LocationId { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
public string CharacterId { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
public string ObjectId { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
public string ObjectType { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
public string ItemKey { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
public int QuantityGranted { get; set; }
|
|
||||||
|
|
||||||
public int RemainingQuantity { get; set; }
|
|
||||||
|
|
||||||
public bool Consumed { get; set; }
|
|
||||||
|
|
||||||
public string InventoryResponseJson { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
@ -1,17 +1,17 @@
|
|||||||
using MongoDB.Bson;
|
using MongoDB.Bson;
|
||||||
using MongoDB.Bson.Serialization.Attributes;
|
using MongoDB.Bson.Serialization.Attributes;
|
||||||
|
|
||||||
namespace LocationsApi.Models;
|
namespace LocationsApi.Models;
|
||||||
|
|
||||||
public class Location
|
public class Location
|
||||||
{
|
{
|
||||||
[BsonId]
|
[BsonId]
|
||||||
[BsonRepresentation(BsonType.ObjectId)]
|
[BsonRepresentation(BsonType.ObjectId)]
|
||||||
public string? Id { get; set; }
|
public string? Id { get; set; }
|
||||||
|
|
||||||
[BsonElement("name")]
|
[BsonElement("name")]
|
||||||
public string Name { get; set; } = string.Empty;
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
[BsonElement("coord")]
|
[BsonElement("coord")]
|
||||||
public required Coord Coord { get; set; }
|
public required Coord Coord { get; set; }
|
||||||
|
|
||||||
@ -21,12 +21,6 @@ public class Location
|
|||||||
[BsonElement("resources")]
|
[BsonElement("resources")]
|
||||||
public List<LocationResource> Resources { get; set; } = [];
|
public List<LocationResource> Resources { get; set; } = [];
|
||||||
|
|
||||||
[BsonElement("locationObject")]
|
[BsonElement("createdUtc")]
|
||||||
public LocationObject? LocationObject { get; set; }
|
public DateTime CreatedUtc { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
[BsonElement("locationObjectResolved")]
|
|
||||||
public bool LocationObjectResolved { get; set; }
|
|
||||||
|
|
||||||
[BsonElement("createdUtc")]
|
|
||||||
public DateTime CreatedUtc { get; set; } = DateTime.UtcNow;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,21 +0,0 @@
|
|||||||
using MongoDB.Bson.Serialization.Attributes;
|
|
||||||
|
|
||||||
namespace LocationsApi.Models;
|
|
||||||
|
|
||||||
public class LocationObject
|
|
||||||
{
|
|
||||||
[BsonElement("id")]
|
|
||||||
public string Id { get; set; } = Guid.NewGuid().ToString("N");
|
|
||||||
|
|
||||||
[BsonElement("objectType")]
|
|
||||||
public string ObjectType { get; set; } = "gatherable";
|
|
||||||
|
|
||||||
[BsonElement("objectKey")]
|
|
||||||
public string ObjectKey { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
[BsonElement("name")]
|
|
||||||
public string Name { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
[BsonElement("state")]
|
|
||||||
public LocationObjectState State { get; set; } = new();
|
|
||||||
}
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
using MongoDB.Bson.Serialization.Attributes;
|
|
||||||
|
|
||||||
namespace LocationsApi.Models;
|
|
||||||
|
|
||||||
public class LocationObjectState
|
|
||||||
{
|
|
||||||
[BsonElement("itemKey")]
|
|
||||||
public string ItemKey { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
[BsonElement("remainingQuantity")]
|
|
||||||
public int RemainingQuantity { get; set; }
|
|
||||||
|
|
||||||
[BsonElement("gatherQuantity")]
|
|
||||||
public int GatherQuantity { get; set; } = 1;
|
|
||||||
}
|
|
||||||
@ -77,41 +77,11 @@ public class LocationStore
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"locationObject", new BsonDocument
|
|
||||||
{
|
|
||||||
{ "bsonType", new BsonArray { "object", "null" } },
|
|
||||||
{
|
|
||||||
"properties", new BsonDocument
|
|
||||||
{
|
|
||||||
{ "id", new BsonDocument { { "bsonType", "string" } } },
|
|
||||||
{ "objectType", new BsonDocument { { "bsonType", "string" } } },
|
|
||||||
{ "objectKey", new BsonDocument { { "bsonType", "string" } } },
|
|
||||||
{ "name", new BsonDocument { { "bsonType", "string" } } },
|
|
||||||
{
|
|
||||||
"state", new BsonDocument
|
|
||||||
{
|
|
||||||
{ "bsonType", new BsonArray { "object", "null" } },
|
|
||||||
{
|
|
||||||
"properties", new BsonDocument
|
|
||||||
{
|
|
||||||
{ "itemKey", new BsonDocument { { "bsonType", "string" } } },
|
|
||||||
{ "remainingQuantity", new BsonDocument { { "bsonType", "int" }, { "minimum", 0 } } },
|
|
||||||
{ "gatherQuantity", new BsonDocument { { "bsonType", "int" }, { "minimum", 1 } } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ "locationObjectResolved", new BsonDocument { { "bsonType", new BsonArray { "bool", "null" } } } },
|
|
||||||
{ "createdUtc", new BsonDocument { { "bsonType", "date" } } }
|
{ "createdUtc", new BsonDocument { { "bsonType", "date" } } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var collections = db.ListCollectionNames().ToList();
|
var collections = db.ListCollectionNames().ToList();
|
||||||
@ -156,79 +126,54 @@ public class LocationStore
|
|||||||
return result.ModifiedCount > 0;
|
return result.ModifiedCount > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record InteractResult(
|
public sealed record GatherResult(GatherStatus Status, string ResourceKey = "", int QuantityGranted = 0, int RemainingQuantity = 0);
|
||||||
InteractStatus Status,
|
|
||||||
string ObjectId = "",
|
|
||||||
string ObjectType = "",
|
|
||||||
string ItemKey = "",
|
|
||||||
int QuantityGranted = 0,
|
|
||||||
int RemainingQuantity = 0,
|
|
||||||
bool Consumed = false,
|
|
||||||
LocationObject? PreviousObject = null);
|
|
||||||
|
|
||||||
public async Task<InteractResult> InteractWithObjectAsync(string locationId, string characterId, string objectId, string userId, bool allowAnyOwner)
|
public async Task<GatherResult> GatherResourceAsync(string locationId, string characterId, string resourceKey, string userId, bool allowAnyOwner)
|
||||||
{
|
{
|
||||||
|
var normalizedKey = resourceKey.Trim().ToLowerInvariant();
|
||||||
var location = await _col.Find(l => l.Id == locationId).FirstOrDefaultAsync();
|
var location = await _col.Find(l => l.Id == locationId).FirstOrDefaultAsync();
|
||||||
if (location is null)
|
if (location is null)
|
||||||
return new InteractResult(InteractStatus.LocationNotFound);
|
return new GatherResult(GatherStatus.LocationNotFound);
|
||||||
|
|
||||||
location = await EnsureLocationMetadataAsync(location);
|
|
||||||
|
|
||||||
var character = await _characters.Find(c => c.Id == characterId).FirstOrDefaultAsync();
|
var character = await _characters.Find(c => c.Id == characterId).FirstOrDefaultAsync();
|
||||||
if (character is null)
|
if (character is null)
|
||||||
return new InteractResult(InteractStatus.CharacterNotFound);
|
return new GatherResult(GatherStatus.CharacterNotFound);
|
||||||
if (!allowAnyOwner && character.OwnerUserId != userId)
|
if (!allowAnyOwner && character.OwnerUserId != userId)
|
||||||
return new InteractResult(InteractStatus.Forbidden);
|
return new GatherResult(GatherStatus.Forbidden);
|
||||||
if (character.Coord.X != location.Coord.X || character.Coord.Y != location.Coord.Y)
|
if (character.Coord.X != location.Coord.X || character.Coord.Y != location.Coord.Y)
|
||||||
return new InteractResult(InteractStatus.Invalid);
|
return new GatherResult(GatherStatus.Invalid);
|
||||||
|
|
||||||
var locationObject = location.LocationObject;
|
var resource = location.Resources.FirstOrDefault(r => NormalizeItemKey(r.ItemKey) == normalizedKey);
|
||||||
if (locationObject is null)
|
if (resource is null)
|
||||||
return new InteractResult(InteractStatus.ObjectNotFound);
|
return new GatherResult(GatherStatus.ResourceNotFound);
|
||||||
if (!string.Equals(locationObject.Id, objectId, StringComparison.Ordinal))
|
if (resource.RemainingQuantity <= 0)
|
||||||
return new InteractResult(InteractStatus.ObjectNotFound);
|
return new GatherResult(GatherStatus.ResourceDepleted);
|
||||||
if (!string.Equals(locationObject.ObjectType, "gatherable", StringComparison.OrdinalIgnoreCase))
|
|
||||||
return new InteractResult(InteractStatus.UnsupportedObjectType);
|
|
||||||
if (locationObject.State.RemainingQuantity <= 0)
|
|
||||||
return new InteractResult(InteractStatus.ObjectConsumed);
|
|
||||||
|
|
||||||
var quantityGranted = Math.Min(locationObject.State.GatherQuantity, locationObject.State.RemainingQuantity);
|
var quantityGranted = Math.Min(resource.GatherQuantity, resource.RemainingQuantity);
|
||||||
var remainingQuantity = locationObject.State.RemainingQuantity - quantityGranted;
|
var filter = Builders<Location>.Filter.And(
|
||||||
var objectFilter = Builders<Location>.Filter.And(
|
|
||||||
Builders<Location>.Filter.Eq(l => l.Id, locationId),
|
Builders<Location>.Filter.Eq(l => l.Id, locationId),
|
||||||
Builders<Location>.Filter.Eq("locationObject.id", locationObject.Id),
|
Builders<Location>.Filter.ElemMatch(l => l.Resources, r => r.ItemKey == resource.ItemKey && r.RemainingQuantity >= quantityGranted)
|
||||||
Builders<Location>.Filter.Eq("locationObject.state.remainingQuantity", locationObject.State.RemainingQuantity)
|
|
||||||
);
|
);
|
||||||
|
var update = Builders<Location>.Update.Inc("resources.$.remainingQuantity", -quantityGranted);
|
||||||
UpdateDefinition<Location> update;
|
var result = await _col.UpdateOneAsync(filter, update);
|
||||||
if (remainingQuantity <= 0)
|
|
||||||
{
|
|
||||||
update = Builders<Location>.Update.Unset("locationObject");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
update = Builders<Location>.Update.Set("locationObject.state.remainingQuantity", remainingQuantity);
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = await _col.UpdateOneAsync(objectFilter, update);
|
|
||||||
if (result.ModifiedCount == 0)
|
if (result.ModifiedCount == 0)
|
||||||
return new InteractResult(InteractStatus.ObjectConsumed);
|
return new GatherResult(GatherStatus.ResourceDepleted);
|
||||||
|
|
||||||
return new InteractResult(
|
return new GatherResult(
|
||||||
InteractStatus.Ok,
|
GatherStatus.Ok,
|
||||||
locationObject.Id,
|
resource.ItemKey,
|
||||||
locationObject.ObjectType,
|
|
||||||
locationObject.State.ItemKey,
|
|
||||||
quantityGranted,
|
quantityGranted,
|
||||||
Math.Max(0, remainingQuantity),
|
resource.RemainingQuantity - quantityGranted);
|
||||||
remainingQuantity <= 0,
|
|
||||||
CloneLocationObject(locationObject));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RestoreObjectInteractionAsync(string locationId, LocationObject previousObject)
|
public async Task RestoreGatheredResourceAsync(string locationId, string resourceKey, int quantity)
|
||||||
{
|
{
|
||||||
var filter = Builders<Location>.Filter.Eq(l => l.Id, locationId);
|
var normalizedKey = NormalizeItemKey(resourceKey);
|
||||||
var update = Builders<Location>.Update.Set(l => l.LocationObject, previousObject);
|
var filter = Builders<Location>.Filter.And(
|
||||||
|
Builders<Location>.Filter.Eq(l => l.Id, locationId),
|
||||||
|
Builders<Location>.Filter.ElemMatch(l => l.Resources, r => r.ItemKey == normalizedKey)
|
||||||
|
);
|
||||||
|
var update = Builders<Location>.Update.Inc("resources.$.remainingQuantity", quantity);
|
||||||
await _col.UpdateOneAsync(filter, update);
|
await _col.UpdateOneAsync(filter, update);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -273,19 +218,35 @@ public class LocationStore
|
|||||||
{
|
{
|
||||||
var filter = Builders<Location>.Filter.And(
|
var filter = Builders<Location>.Filter.And(
|
||||||
Builders<Location>.Filter.Eq(l => l.Coord.X, 0),
|
Builders<Location>.Filter.Eq(l => l.Coord.X, 0),
|
||||||
Builders<Location>.Filter.Eq(l => l.Coord.Y, 0)
|
Builders<Location>.Filter.Eq(l => l.Coord.Y, 0)
|
||||||
);
|
);
|
||||||
var existing = _col.Find(filter).FirstOrDefault();
|
var existing = _col.Find(filter).FirstOrDefault();
|
||||||
if (existing is not null)
|
if (existing is not null)
|
||||||
|
{
|
||||||
|
var updates = new List<UpdateDefinition<Location>>();
|
||||||
|
if (string.IsNullOrWhiteSpace(existing.BiomeKey))
|
||||||
|
updates.Add(Builders<Location>.Update.Set(l => l.BiomeKey, "plains"));
|
||||||
|
if (existing.Resources.Count == 0)
|
||||||
|
updates.Add(Builders<Location>.Update.Set(l => l.Resources, new List<LocationResource>
|
||||||
|
{
|
||||||
|
new() { ItemKey = "wood", RemainingQuantity = 100, GatherQuantity = 3 },
|
||||||
|
new() { ItemKey = "grass", RemainingQuantity = 500, GatherQuantity = 10 }
|
||||||
|
}));
|
||||||
|
if (updates.Count > 0)
|
||||||
|
_col.UpdateOne(filter, Builders<Location>.Update.Combine(updates));
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var origin = new Location
|
var origin = new Location
|
||||||
{
|
{
|
||||||
Name = "Origin",
|
Name = "Origin",
|
||||||
Coord = new Coord { X = 0, Y = 0 },
|
Coord = new Coord { X = 0, Y = 0 },
|
||||||
BiomeKey = DetermineBiomeKey(0, 0),
|
BiomeKey = "plains",
|
||||||
LocationObject = CreateLocationObjectForBiome(DetermineBiomeKey(0, 0), 0, 0),
|
Resources =
|
||||||
LocationObjectResolved = true,
|
[
|
||||||
|
new LocationResource { ItemKey = "wood", RemainingQuantity = 100, GatherQuantity = 3 },
|
||||||
|
new LocationResource { ItemKey = "grass", RemainingQuantity = 500, GatherQuantity = 10 }
|
||||||
|
],
|
||||||
CreatedUtc = DateTime.UtcNow
|
CreatedUtc = DateTime.UtcNow
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -301,150 +262,6 @@ public class LocationStore
|
|||||||
|
|
||||||
private static string NormalizeItemKey(string itemKey) => itemKey.Trim().ToLowerInvariant();
|
private static string NormalizeItemKey(string itemKey) => itemKey.Trim().ToLowerInvariant();
|
||||||
|
|
||||||
private async Task<Location> EnsureLocationMetadataAsync(Location location)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(location.BiomeKey) && location.LocationObjectResolved)
|
|
||||||
return location;
|
|
||||||
|
|
||||||
var biomeKey = location.BiomeKey;
|
|
||||||
if (string.IsNullOrWhiteSpace(biomeKey))
|
|
||||||
biomeKey = DetermineBiomeKey(location.Coord.X, location.Coord.Y);
|
|
||||||
|
|
||||||
var migratedObject = TryMigrateLegacyResources(location) ?? CreateLocationObjectForBiome(biomeKey, location.Coord.X, location.Coord.Y);
|
|
||||||
var filter = Builders<Location>.Filter.And(
|
|
||||||
Builders<Location>.Filter.Eq(l => l.Id, location.Id)
|
|
||||||
);
|
|
||||||
var update = Builders<Location>.Update
|
|
||||||
.Set(l => l.BiomeKey, biomeKey)
|
|
||||||
.Set(l => l.LocationObjectResolved, true)
|
|
||||||
.Set(l => l.LocationObject, migratedObject);
|
|
||||||
await _col.UpdateOneAsync(filter, update);
|
|
||||||
location.BiomeKey = biomeKey;
|
|
||||||
location.LocationObject = migratedObject;
|
|
||||||
location.LocationObjectResolved = true;
|
|
||||||
return location;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static LocationObject? TryMigrateLegacyResources(Location location)
|
|
||||||
{
|
|
||||||
var legacyResource = location.Resources.FirstOrDefault(r => r.RemainingQuantity > 0);
|
|
||||||
if (legacyResource is null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return CreateGatherableObject(
|
|
||||||
legacyResource.ItemKey,
|
|
||||||
legacyResource.RemainingQuantity,
|
|
||||||
legacyResource.GatherQuantity);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string DetermineBiomeKey(int x, int y)
|
|
||||||
{
|
|
||||||
if (x == 0 && y == 0)
|
|
||||||
return "plains";
|
|
||||||
|
|
||||||
var regionX = FloorDiv(x, 4);
|
|
||||||
var regionY = FloorDiv(y, 4);
|
|
||||||
var roll = Math.Abs(HashCode.Combine(regionX, regionY, 7919)) % 100;
|
|
||||||
if (roll < 35)
|
|
||||||
return "plains";
|
|
||||||
if (roll < 60)
|
|
||||||
return "forest";
|
|
||||||
if (roll < 80)
|
|
||||||
return "rocky";
|
|
||||||
if (roll < 92)
|
|
||||||
return "wetlands";
|
|
||||||
return "desert";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static LocationObject? CreateLocationObjectForBiome(string biomeKey, int x, int y)
|
|
||||||
{
|
|
||||||
var roll = Math.Abs(HashCode.Combine(x, y, 1543)) % 100;
|
|
||||||
return biomeKey switch
|
|
||||||
{
|
|
||||||
"forest" => roll switch
|
|
||||||
{
|
|
||||||
< 35 => null,
|
|
||||||
< 80 => CreateGatherableObject("wood", 60, 3),
|
|
||||||
< 95 => CreateGatherableObject("grass", 120, 10),
|
|
||||||
_ => CreateGatherableObject("stone", 40, 2)
|
|
||||||
},
|
|
||||||
"rocky" => roll switch
|
|
||||||
{
|
|
||||||
< 60 => null,
|
|
||||||
< 90 => CreateGatherableObject("stone", 40, 2),
|
|
||||||
_ => CreateGatherableObject("wood", 60, 3)
|
|
||||||
},
|
|
||||||
"wetlands" => roll switch
|
|
||||||
{
|
|
||||||
< 40 => null,
|
|
||||||
< 90 => CreateGatherableObject("grass", 120, 10),
|
|
||||||
_ => CreateGatherableObject("wood", 60, 3)
|
|
||||||
},
|
|
||||||
"desert" => roll switch
|
|
||||||
{
|
|
||||||
< 70 => null,
|
|
||||||
< 95 => CreateGatherableObject("stone", 40, 2),
|
|
||||||
_ => CreateGatherableObject("wood", 60, 3)
|
|
||||||
},
|
|
||||||
_ => roll switch
|
|
||||||
{
|
|
||||||
< 50 => null,
|
|
||||||
< 85 => CreateGatherableObject("grass", 120, 10),
|
|
||||||
_ => CreateGatherableObject("wood", 60, 3)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static LocationObject CreateGatherableObject(string itemKey, int remainingQuantity, int gatherQuantity)
|
|
||||||
{
|
|
||||||
var normalizedItemKey = NormalizeItemKey(itemKey);
|
|
||||||
return new LocationObject
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid().ToString("N"),
|
|
||||||
ObjectType = "gatherable",
|
|
||||||
ObjectKey = $"{normalizedItemKey}_node",
|
|
||||||
Name = HumanizeItemKey(normalizedItemKey),
|
|
||||||
State = new LocationObjectState
|
|
||||||
{
|
|
||||||
ItemKey = normalizedItemKey,
|
|
||||||
RemainingQuantity = remainingQuantity,
|
|
||||||
GatherQuantity = gatherQuantity
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string HumanizeItemKey(string itemKey)
|
|
||||||
{
|
|
||||||
return string.Join(" ", itemKey.Split('_', StringSplitOptions.RemoveEmptyEntries)
|
|
||||||
.Select(part => char.ToUpperInvariant(part[0]) + part[1..]));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int FloorDiv(int value, int divisor)
|
|
||||||
{
|
|
||||||
var quotient = value / divisor;
|
|
||||||
var remainder = value % divisor;
|
|
||||||
if (remainder != 0 && ((remainder < 0) != (divisor < 0)))
|
|
||||||
quotient -= 1;
|
|
||||||
return quotient;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static LocationObject CloneLocationObject(LocationObject source)
|
|
||||||
{
|
|
||||||
return new LocationObject
|
|
||||||
{
|
|
||||||
Id = source.Id,
|
|
||||||
ObjectType = source.ObjectType,
|
|
||||||
ObjectKey = source.ObjectKey,
|
|
||||||
Name = source.Name,
|
|
||||||
State = new LocationObjectState
|
|
||||||
{
|
|
||||||
ItemKey = source.State.ItemKey,
|
|
||||||
RemainingQuantity = source.State.RemainingQuantity,
|
|
||||||
GatherQuantity = source.State.GatherQuantity
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
[MongoDB.Bson.Serialization.Attributes.BsonIgnoreExtraElements]
|
[MongoDB.Bson.Serialization.Attributes.BsonIgnoreExtraElements]
|
||||||
private class CharacterDocument
|
private class CharacterDocument
|
||||||
{
|
{
|
||||||
@ -458,14 +275,13 @@ public class LocationStore
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum InteractStatus
|
public enum GatherStatus
|
||||||
{
|
{
|
||||||
Ok,
|
Ok,
|
||||||
LocationNotFound,
|
LocationNotFound,
|
||||||
CharacterNotFound,
|
CharacterNotFound,
|
||||||
Forbidden,
|
Forbidden,
|
||||||
Invalid,
|
Invalid,
|
||||||
ObjectNotFound,
|
ResourceNotFound,
|
||||||
UnsupportedObjectType,
|
ResourceDepleted
|
||||||
ObjectConsumed
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user