Added the ability to directly install blueprints onto hardpoints in the colony tab Added the ability to customize production of factories This stuff is still kinda buggy
1028 lines
No EOL
40 KiB
C#
1028 lines
No EOL
40 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Unity.Mathematics;
|
|
using static Unity.Mathematics.math;
|
|
using float2 = Unity.Mathematics.float2;
|
|
using Random = Unity.Mathematics.Random;
|
|
using JM.LinqFaster;
|
|
using float4 = Unity.Mathematics.float4;
|
|
|
|
public class GameContext
|
|
{
|
|
public Random Random = new Random((uint) (DateTime.Now.Ticks%uint.MaxValue));
|
|
public Dictionary<Guid, Guid[]> ZonePlanets = new Dictionary<Guid, Guid[]>();
|
|
public Dictionary<Guid, Dictionary<Guid, Entity>> ZoneEntities = new Dictionary<Guid, Dictionary<Guid, Entity>>();
|
|
public Dictionary<string, GalaxyMapLayerData> MapLayers = new Dictionary<string, GalaxyMapLayerData>();
|
|
public Dictionary<Guid, List<IController>> CorporationControllers = new Dictionary<Guid, List<IController>>();
|
|
public Dictionary<Guid, SimplifiedZoneData> GalaxyZones;
|
|
|
|
private Action<string> _logger;
|
|
|
|
private Dictionary<BlueprintStatEffect, PerformanceStat> AffectedStats =
|
|
new Dictionary<BlueprintStatEffect, PerformanceStat>();
|
|
|
|
private double _time;
|
|
private float _deltaTime;
|
|
private GlobalData _globalData;
|
|
private HashSet<Guid> _loadedZones = new HashSet<Guid>();
|
|
private HashSet<Guid> _orbits = new HashSet<Guid>();
|
|
private Dictionary<Guid, float2> _orbitVelocities = new Dictionary<Guid, float2>();
|
|
private Dictionary<Guid, float2> _orbitPositions = new Dictionary<Guid, float2>();
|
|
private Dictionary<Guid, float2> _previousOrbitPositions = new Dictionary<Guid, float2>();
|
|
private Dictionary<Guid, float4[]> _asteroidTransforms = new Dictionary<Guid, float4[]>(); // x, y, rotation, scale
|
|
private Dictionary<Guid, float4[]> _previousAsteroidTransforms = new Dictionary<Guid, float4[]>(); // x, y, rotation, scale
|
|
private Dictionary<(Guid, int), float> _asteroidRespawnTimers = new Dictionary<(Guid, int), float>();
|
|
private Dictionary<(Guid, int), float> _asteroidDamage = new Dictionary<(Guid, int), float>();
|
|
private Dictionary<(Guid, Guid, int), float> _asteroidMiningAccumulator = new Dictionary<(Guid, Guid, int), float>();
|
|
private Guid _forceLoadZone;
|
|
private Type[] _statObjects;
|
|
|
|
public GlobalData GlobalData => _globalData ?? (_globalData = Cache.GetAll<GlobalData>().FirstOrDefault());
|
|
public DatabaseCache Cache { get; }
|
|
|
|
public Type[] StatObjects => _statObjects ?? (_statObjects = typeof(BehaviorData).GetAllChildClasses()
|
|
.Concat(typeof(EquippableItemData).GetAllChildClasses()).ToArray());
|
|
|
|
public Guid ForceLoadZone
|
|
{
|
|
get => _forceLoadZone;
|
|
set
|
|
{
|
|
if(_forceLoadZone != Guid.Empty && ZoneEntities[_forceLoadZone].Count == 0)
|
|
UnloadZone(_forceLoadZone);
|
|
_forceLoadZone = value;
|
|
if(!_loadedZones.Contains(_forceLoadZone))
|
|
LoadZone(_forceLoadZone);
|
|
}
|
|
}
|
|
|
|
public double Time
|
|
{
|
|
get => _time;
|
|
set
|
|
{
|
|
_deltaTime = (float) (value - _time);
|
|
_time = value;
|
|
//Log($"GameContext delta time: {_deltaTime}");
|
|
}
|
|
}
|
|
|
|
public float2 WormholePosition(Guid zone, Guid other)
|
|
{
|
|
var zoneData = Cache.Get<ZoneData>(zone);
|
|
var direction = Cache.Get<ZoneData>(other).Position - zoneData.Position;
|
|
return normalize(direction) * zoneData.Radius * .95f;
|
|
}
|
|
|
|
// private readonly Dictionary<CraftedItemData, int> Tier = new Dictionary<CraftedItemData, int>();
|
|
|
|
public GameContext(DatabaseCache cache, Action<string> logger)
|
|
{
|
|
Cache = cache;
|
|
_logger = logger;
|
|
// var globalData = Cache.GetAll<GlobalData>().FirstOrDefault();
|
|
// if (globalData == null)
|
|
// {
|
|
// globalData = new GlobalData();
|
|
// Cache.Add(globalData);
|
|
// }
|
|
}
|
|
|
|
public void Log(string s)
|
|
{
|
|
_logger(s);
|
|
}
|
|
|
|
public void Update()
|
|
{
|
|
_previousOrbitPositions = _orbitPositions;
|
|
_orbitPositions = new Dictionary<Guid, float2>(_orbits.Count);
|
|
|
|
foreach(var orbit in _orbits)
|
|
{
|
|
_orbitPositions[orbit] = GetOrbitPosition(orbit);
|
|
_orbitVelocities[orbit] = _previousOrbitPositions.ContainsKey(orbit)
|
|
? (_orbitPositions[orbit] - _previousOrbitPositions[orbit]) / _deltaTime
|
|
: float2.zero;
|
|
}
|
|
|
|
foreach (var asteroids in _asteroidTransforms.Keys)
|
|
UpdateAsteroidTransforms(asteroids);
|
|
|
|
foreach (var corporation in Cache.GetAll<Corporation>())
|
|
{
|
|
foreach (var tasks in corporation.Tasks
|
|
.Select(id => Cache.Get<AgentTask>(id)) // Fetch the tasks from the database cache
|
|
.Where(task => !task.Reserved) // Filter out tasks that have already been reserved
|
|
.GroupBy(task => task.Type)) // Group tasks by type
|
|
{
|
|
// Create a list of available controllers for this task type
|
|
var availableControllers = CorporationControllers[corporation.ID]
|
|
.Where(controller => controller.TaskType == tasks.Key && controller.Available).ToList();
|
|
|
|
// Iterate over the highest priority tasks for which controllers are available
|
|
foreach (var task in tasks.OrderByDescending(task => task.Priority).Take(availableControllers.Count))
|
|
{
|
|
// Find the nearest controller for this task
|
|
IController nearestController = availableControllers[0];
|
|
List<SimplifiedZoneData> nearestControllerPath = FindPath(GalaxyZones[availableControllers.First().Zone], GalaxyZones[task.Zone], true);
|
|
foreach (var controller in availableControllers.Skip(1))
|
|
{
|
|
var path = FindPath(GalaxyZones[controller.Zone], GalaxyZones[task.Zone], true);
|
|
if (path.Count < nearestControllerPath.Count)
|
|
{
|
|
nearestControllerPath = path;
|
|
nearestController = controller;
|
|
}
|
|
}
|
|
task.Reserved = true;
|
|
nearestController.AssignTask(task.ID);
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (var kvp in ZoneEntities)
|
|
{
|
|
foreach (var entity in kvp.Value.Values) entity.Update(_deltaTime);
|
|
}
|
|
}
|
|
|
|
public IEnumerable<Entity> GetEntities(Guid zone)
|
|
{
|
|
return ZoneEntities[zone].Values;
|
|
}
|
|
|
|
public void LoadZone(Guid zone)
|
|
{
|
|
var zoneData = Cache.Get<ZoneData>(zone);
|
|
|
|
_loadedZones.Add(zone);
|
|
|
|
foreach (var orbit in zoneData.Orbits) _orbits.Add(orbit);
|
|
|
|
// TODO: Associate planets with stored entities for planetary colonies
|
|
ZonePlanets[zone] = zoneData.Planets;
|
|
|
|
foreach (var belt in zoneData.Planets
|
|
.Select(id => Cache.Get<PlanetData>(id))
|
|
.Where(p => p.Belt))
|
|
{
|
|
_asteroidTransforms[belt.ID] = new float4[belt.Asteroids.Length];
|
|
_previousAsteroidTransforms[belt.ID] = new float4[belt.Asteroids.Length];
|
|
}
|
|
|
|
// TODO: Load stored entities
|
|
ZoneEntities[zone] = new Dictionary<Guid, Entity>();
|
|
}
|
|
|
|
public void UnloadZone(Guid zone)
|
|
{
|
|
var zoneData = Cache.Get<ZoneData>(zone);
|
|
|
|
_loadedZones.Remove(zone);
|
|
ZoneEntities.Remove(zone);
|
|
ZonePlanets.Remove(zone);
|
|
foreach (var orbit in zoneData.Orbits) _orbits.Remove(orbit);
|
|
foreach (var belt in zoneData.Planets
|
|
.Select(id => Cache.Get<PlanetData>(id))
|
|
.Where(p => p.Belt))
|
|
{
|
|
_asteroidTransforms.Remove(belt.ID);
|
|
_previousAsteroidTransforms.Remove(belt.ID);
|
|
}
|
|
}
|
|
|
|
public void Warp(Entity entity, Guid targetZone)
|
|
{
|
|
if (length(entity.Position - WormholePosition(entity.Zone, targetZone)) < GlobalData.WarpDistance)
|
|
{
|
|
var sourceZone = entity.Zone;
|
|
|
|
// Remove entity from source zone
|
|
ZoneEntities[sourceZone].Remove(entity.ID);
|
|
|
|
// Unload source zone if empty and unobserved
|
|
if(ForceLoadZone != sourceZone && ZoneEntities[sourceZone].Count == 0)
|
|
UnloadZone(sourceZone);
|
|
|
|
// Load target zone if not yet loaded
|
|
if(!_loadedZones.Contains(targetZone))
|
|
LoadZone(targetZone);
|
|
|
|
entity.Zone = targetZone;
|
|
entity.Position = WormholePosition(targetZone, sourceZone);
|
|
ZoneEntities[targetZone][entity.ID] = entity;
|
|
}
|
|
}
|
|
|
|
// Determine orbital position recursively, caching parent positions to avoid repeated calculations
|
|
public float2 GetOrbitPosition(Guid orbit)
|
|
{
|
|
// Root orbit is fixed at center
|
|
if(orbit==Guid.Empty)
|
|
return float2.zero;
|
|
|
|
if (!_orbitPositions.ContainsKey(orbit))
|
|
{
|
|
var orbitData = Cache.Get<OrbitData>(orbit);
|
|
float2 pos = float2.zero;
|
|
if (orbitData.Period > .01f)
|
|
{
|
|
var phase = (float) frac(Time / orbitData.Period * GlobalData.OrbitSpeedMultiplier);
|
|
pos = OrbitData.Evaluate(frac(phase + orbitData.Phase)) * orbitData.Distance;
|
|
}
|
|
|
|
_orbitPositions[orbit] = GetOrbitPosition(orbitData.Parent) + pos;
|
|
}
|
|
|
|
var position = _orbitPositions[orbit];
|
|
if (float.IsNaN(position.x))
|
|
{
|
|
Log("Orbit position is NaN, something went very wrong!");
|
|
return float2.zero;
|
|
}
|
|
return _orbitPositions[orbit];
|
|
}
|
|
|
|
public float2 GetOrbitVelocity(Guid orbit)
|
|
{
|
|
return _orbitVelocities.ContainsKey(orbit) ? _orbitVelocities[orbit] : float2.zero;
|
|
}
|
|
|
|
public int NearestAsteroid(Guid planetDataID, float2 position)
|
|
{
|
|
var planetData = Cache.Get<PlanetData>(planetDataID);
|
|
|
|
var asteroidPositions = GetAsteroidTransforms(planetDataID);
|
|
|
|
int nearest = 0;
|
|
float nearestDistance = Single.MaxValue;
|
|
for (int i = 0; i < planetData.Asteroids.Length; i++)
|
|
{
|
|
var dist = lengthsq(asteroidPositions[i].xy - position);
|
|
if (AsteroidExists(planetDataID, i) && dist < nearestDistance)
|
|
{
|
|
nearest = i;
|
|
nearestDistance = dist;
|
|
}
|
|
}
|
|
|
|
return nearest;
|
|
}
|
|
|
|
public bool AsteroidExists(Guid planetDataID, int asteroid) =>
|
|
!_asteroidRespawnTimers.ContainsKey((planetDataID, asteroid));
|
|
|
|
public float2 GetAsteroidVelocity(Guid planetDataID, int asteroid)
|
|
{
|
|
return (_asteroidTransforms[planetDataID][asteroid].xy - _previousAsteroidTransforms[planetDataID][asteroid].xy) / _deltaTime;
|
|
}
|
|
|
|
public float4 GetAsteroidTransform(Guid planetDataID, int asteroid)
|
|
{
|
|
return _asteroidTransforms[planetDataID][asteroid];
|
|
}
|
|
|
|
public float4[] GetAsteroidTransforms(Guid planetDataID)
|
|
{
|
|
return _asteroidTransforms[planetDataID];
|
|
}
|
|
|
|
private void UpdateAsteroidTransforms(Guid planetDataID)
|
|
{
|
|
var planetData = Cache.Get<PlanetData>(planetDataID);
|
|
|
|
Array.Copy(_asteroidTransforms[planetDataID], _previousAsteroidTransforms[planetDataID], planetData.Asteroids.Length);
|
|
|
|
var orbitData = Cache.Get<OrbitData>(planetData.Orbit);
|
|
var orbitPosition = GetOrbitPosition(orbitData.Parent);
|
|
for (var i = 0; i < planetData.Asteroids.Length; i++)
|
|
{
|
|
var size = planetData.Asteroids[i].Size;
|
|
if(_asteroidRespawnTimers.ContainsKey((planetDataID, i))) size = 0;
|
|
else if (_asteroidDamage.ContainsKey((planetDataID, i)))
|
|
{
|
|
var hpLerp = unlerp(GlobalData.AsteroidSizeMin, GlobalData.AsteroidSizeMax, size);
|
|
var asteroidHitpoints = lerp(GlobalData.AsteroidHitpointsMin, GlobalData.AsteroidHitpointsMax, pow(hpLerp, GlobalData.AsteroidHitpointsPower));
|
|
var sizeLerp = (asteroidHitpoints - _asteroidDamage[(planetDataID, i)]) / asteroidHitpoints;
|
|
size = lerp(GlobalData.AsteroidSizeMin, size, sizeLerp);
|
|
}
|
|
|
|
_asteroidTransforms[planetDataID][i] = float4(
|
|
OrbitData.Evaluate((float) frac(Time / OrbitalPeriod(planetData.Asteroids[i].Distance) * GlobalData.OrbitSpeedMultiplier +
|
|
planetData.Asteroids[i].Phase)) * planetData.Asteroids[i].Distance + orbitPosition,
|
|
(float) (Time * planetData.Asteroids[i].RotationSpeed % 360.0), size);
|
|
}
|
|
}
|
|
|
|
public OrbitData CreateOrbit(Guid zone, Guid parent, float2 position)
|
|
{
|
|
var parentPosition = GetOrbitPosition(parent);
|
|
var delta = position - parentPosition;
|
|
var distance = length(delta);
|
|
var period = OrbitalPeriod(distance);
|
|
var phase = atan2(delta.y, delta.x) / (PI * 2);
|
|
var currentPhase = frac(Time / period * GlobalData.OrbitSpeedMultiplier);
|
|
var storedPhase = (float) frac(phase - currentPhase);
|
|
|
|
var orbit = new OrbitData
|
|
{
|
|
Context = this,
|
|
Distance = distance,
|
|
Parent = parent,
|
|
Period = period,
|
|
Phase = storedPhase,
|
|
Zone = zone
|
|
};
|
|
Cache.Add(orbit);
|
|
_orbits.Add(orbit.ID);
|
|
return orbit;
|
|
}
|
|
|
|
public float OrbitalPeriod(float distance)
|
|
{
|
|
return pow(distance, GlobalData.OrbitPeriodExponent) * GlobalData.OrbitPeriodMultiplier;
|
|
}
|
|
|
|
public void PlaceMegas()
|
|
{
|
|
var megas = Cache.GetAll<MegaCorporation>();
|
|
|
|
var availableZones = Cache.GetAll<ZoneData>()
|
|
.Where(z => megas.All(m => m.HomeZone != z.ID))
|
|
.ToList();
|
|
|
|
foreach(var megaPlacement in megas
|
|
.OrderBy(m=>m.PlacementType)
|
|
.GroupBy(m=>m.PlacementType))
|
|
switch (megaPlacement.Key)
|
|
{
|
|
case MegaPlacementType.Mass:
|
|
foreach (var mega in megaPlacement)
|
|
{
|
|
var zone = availableZones
|
|
.MaxBy(z => z.Mass);
|
|
availableZones.Remove(zone);
|
|
mega.HomeZone = zone.ID;
|
|
}
|
|
break;
|
|
case MegaPlacementType.Planets:
|
|
foreach (var mega in megaPlacement)
|
|
{
|
|
var zone = availableZones
|
|
.MaxBy(z => z.Planets.Length);
|
|
availableZones.Remove(zone);
|
|
mega.HomeZone = zone.ID;
|
|
}
|
|
break;
|
|
case MegaPlacementType.Resources:
|
|
foreach (var mega in megaPlacement)
|
|
{
|
|
var zone = availableZones
|
|
.MaxBy(z => z.Planets
|
|
.SelectMany(id=>Cache.Get<PlanetData>(id).Resources
|
|
.Select(r=>r.Key))
|
|
.Distinct()
|
|
.Count());
|
|
availableZones.Remove(zone);
|
|
mega.HomeZone = zone.ID;
|
|
}
|
|
break;
|
|
case MegaPlacementType.Connected:
|
|
foreach (var mega in megaPlacement)
|
|
{
|
|
var zone = availableZones
|
|
.OrderByDescending(z => z.Wormholes.Count)
|
|
.ThenBy(z=>megas
|
|
.Sum(m=>m.HomeZone==Guid.Empty?0:lengthsq(Cache.Get<ZoneData>(m.HomeZone).Position - z.Position)))
|
|
.First();
|
|
availableZones.Remove(zone);
|
|
mega.HomeZone = zone.ID;
|
|
}
|
|
break;
|
|
case MegaPlacementType.Isolated:
|
|
foreach (var mega in megaPlacement)
|
|
{
|
|
var zone = availableZones
|
|
.Where(z=>z.Wormholes.Count==1)
|
|
.MaxBy(z =>
|
|
{
|
|
int soloChain = 0;
|
|
var previous = z;
|
|
var next = Cache.Get<ZoneData>(z.Wormholes.First());
|
|
while (next.Wormholes.Count == 2)
|
|
{
|
|
var temp = next;
|
|
next = Cache.Get<ZoneData>(next.Wormholes.First(w=>w != previous.ID));
|
|
previous = temp;
|
|
soloChain++;
|
|
}
|
|
|
|
return soloChain;
|
|
});
|
|
availableZones.Remove(zone);
|
|
mega.HomeZone = zone.ID;
|
|
}
|
|
break;
|
|
default:
|
|
throw new ArgumentOutOfRangeException();
|
|
}
|
|
}
|
|
|
|
// public int ItemTier(CraftedItemData itemData)
|
|
// {
|
|
// if (Tier.ContainsKey(itemData)) return Tier[itemData];
|
|
//
|
|
// Tier[itemData] = itemData.Ingredients.Keys.Max(ci => _cache.Get<ItemData>(ci) is CraftedItemData craftableIngredient ? ItemTier(craftableIngredient) : 0);
|
|
//
|
|
// return Tier[itemData];
|
|
// }
|
|
|
|
public ItemData GetData(ItemInstance item)
|
|
{
|
|
return Cache.Get<ItemData>(item.Data);
|
|
}
|
|
|
|
public SimpleCommodityData GetData(SimpleCommodity item)
|
|
{
|
|
return Cache.Get<SimpleCommodityData>(item.Data);
|
|
}
|
|
|
|
public CraftedItemData GetData(CraftedItemInstance item)
|
|
{
|
|
return Cache.Get<CraftedItemData>(item.Data);
|
|
}
|
|
|
|
public EquippableItemData GetData(Gear gear)
|
|
{
|
|
return Cache.Get<EquippableItemData>(gear.Data);
|
|
}
|
|
|
|
public float GetMass(ItemInstance item)
|
|
{
|
|
var data = GetData(item);
|
|
switch (item)
|
|
{
|
|
case CraftedItemInstance _:
|
|
return data.Mass;
|
|
case SimpleCommodity commodity:
|
|
return data.Mass * commodity.Quantity;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
public float GetSize(ItemInstance item)
|
|
{
|
|
var data = GetData(item);
|
|
switch (item)
|
|
{
|
|
case CraftedItemInstance _:
|
|
return data.Size;
|
|
case SimpleCommodity commodity:
|
|
return data.Size * commodity.Quantity;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
public float GetHeatCapacity(ItemInstance item)
|
|
{
|
|
var data = GetData(item);
|
|
switch (item)
|
|
{
|
|
case CraftedItemInstance _:
|
|
return data.Mass * data.SpecificHeat;
|
|
case SimpleCommodity commodity:
|
|
return data.Mass * data.SpecificHeat * commodity.Quantity;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
public PerformanceStat GetAffectedStat(BlueprintData blueprint, BlueprintStatEffect effect)
|
|
{
|
|
// We've already cached this effect's stat, return it directly
|
|
if (AffectedStats.ContainsKey(effect)) return AffectedStats[effect];
|
|
|
|
// Get the data for the item the blueprint is for, return null if not found or not equippable
|
|
var blueprintItem = Cache.Get<EquippableItemData>(blueprint.Item);
|
|
if (blueprintItem == null)
|
|
{
|
|
_logger($"Attempted to get stat effect but Blueprint {blueprint.ID} is not for an equippable item!");
|
|
return AffectedStats[effect] = null;
|
|
}
|
|
|
|
// Get the first behavior of the type specified in the blueprint stat effect, return null if not found
|
|
var effectObject = effect.StatReference.Target == blueprintItem.Name ? (object) blueprintItem :
|
|
blueprintItem.Behaviors.FirstOrDefault(b => b.GetType().Name == effect.StatReference.Target);
|
|
if (effectObject == null)
|
|
{
|
|
_logger($"Attempted to get stat effect for Blueprint {blueprint.ID} but stat references missing behavior \"{effect.StatReference.Target}\"!");
|
|
return AffectedStats[effect] = null;
|
|
}
|
|
|
|
// Get the first field in the behavior matching the name specified in the stat effect, return null if not found
|
|
var type = effectObject.GetType();
|
|
var field = type.GetField(effect.StatReference.Stat);
|
|
if (field == null)
|
|
{
|
|
_logger($"Attempted to get stat effect for Blueprint {blueprint.ID} but object {effect.StatReference.Target} does not have a stat named \"{effect.StatReference.Stat}\"!");
|
|
return AffectedStats[effect] = null;
|
|
}
|
|
|
|
// Finally we've confirmed the stat effect is valid, return the affected stat
|
|
return AffectedStats[effect] = field.GetValue(effectObject) as PerformanceStat;
|
|
}
|
|
|
|
private readonly Dictionary<CraftedItemInstance, float> ItemQuality = new Dictionary<CraftedItemInstance, float>();
|
|
|
|
public float CompoundQuality(CraftedItemInstance item)
|
|
{
|
|
if (ItemQuality.ContainsKey(item)) return ItemQuality[item];
|
|
|
|
var quality = item.Quality;
|
|
|
|
var craftedIngredients = item.Ingredients.Where(i => i is CraftedItemInstance).ToArray();
|
|
if (craftedIngredients.Length > 0)
|
|
{
|
|
var ingredientQualityWeight = Cache.Get<CraftedItemData>(item.Data).IngredientQualityWeight;
|
|
quality = quality * (1 - ingredientQualityWeight) +
|
|
craftedIngredients.Cast<CraftedItemInstance>().Average(CompoundQuality) * ingredientQualityWeight;
|
|
}
|
|
|
|
ItemQuality[item] = quality;
|
|
|
|
return ItemQuality[item];
|
|
}
|
|
|
|
// Determine quality of either the item itself or the specific ingredient this stat depends on
|
|
public float Quality(PerformanceStat stat, Gear item)
|
|
{
|
|
var blueprint = Cache.Get<BlueprintData>(item.Blueprint);
|
|
var activeEffects = blueprint.StatEffects.Where(x => GetAffectedStat(blueprint, x) == stat).ToArray();
|
|
float quality;
|
|
if (!activeEffects.Any())
|
|
quality = CompoundQuality(item);
|
|
else
|
|
{
|
|
var ingredientInstances = item.Ingredients.Select(i => Cache.Get<ItemInstance>(i)).ToArray();
|
|
var ingredients = ingredientInstances.Where(i => activeEffects.Any(e => e.Ingredient == i.Data)).ToArray();
|
|
if(ingredients.Length != activeEffects.Length)
|
|
{
|
|
_logger($"Item {item.ID} does not have the ingredients specified by the stat effects of its blueprint!");
|
|
return 0;
|
|
}
|
|
|
|
float sum = 0;
|
|
foreach (var i in ingredients)
|
|
{
|
|
if (i is CraftedItemInstance ci)
|
|
sum += CompoundQuality(ci);
|
|
else _logger($"Blueprint stat effect for item {item.ID} specifies invalid (non crafted) ingredient!");
|
|
}
|
|
|
|
quality = sum / ingredients.Length;
|
|
}
|
|
|
|
return quality;
|
|
}
|
|
|
|
// Returns stat when not equipped
|
|
public float Evaluate(PerformanceStat stat, Gear item)
|
|
{
|
|
var quality = pow(Quality(stat, item), stat.QualityExponent);
|
|
|
|
var result = lerp(stat.Min, stat.Max, quality);
|
|
|
|
if (float.IsNaN(result))
|
|
return stat.Min;
|
|
|
|
return result;
|
|
}
|
|
|
|
// Returns stat using ship temperature and modifiers
|
|
public float Evaluate(PerformanceStat stat, Gear item, Entity entity)
|
|
{
|
|
var itemData = GetData(item);
|
|
|
|
var heat = !stat.HeatDependent ? 1 : pow(itemData.Performance(entity.Temperature), Evaluate(itemData.HeatExponent,item));
|
|
var durability = !stat.DurabilityDependent ? 1 : pow(item.Durability / Evaluate(itemData.Durability, item), Evaluate(itemData.DurabilityExponent,item));
|
|
var quality = pow(Quality(stat, item), stat.QualityExponent);
|
|
|
|
var scaleModifier = stat.GetScaleModifiers(entity).Values.Aggregate(1.0f, (current, mod) => current * mod);
|
|
|
|
var constantModifier = stat.GetConstantModifiers(entity).Values.Sum();
|
|
|
|
var result = lerp(stat.Min, stat.Max, heat * durability * quality) * scaleModifier + constantModifier;
|
|
if (float.IsNaN(result))
|
|
return stat.Min;
|
|
return result;
|
|
}
|
|
|
|
public Entity CreateEntity(Guid zone, Guid corporation, Guid loadout)
|
|
{
|
|
if (!(Cache.Get(loadout) is LoadoutData loadoutData))
|
|
{
|
|
_logger("Attempted to spawn invalid loadout ID");
|
|
return null;
|
|
}
|
|
|
|
if (!(Cache.Get(loadoutData.Hull) is HullData hullData))
|
|
{
|
|
_logger("Attempted to spawn loadout with invalid hull ID");
|
|
return null;
|
|
}
|
|
|
|
if (!(Cache.Get(zone) is ZoneData zoneData))
|
|
{
|
|
_logger("Attempted to spawn entity with invalid zone ID");
|
|
return null;
|
|
}
|
|
|
|
if (!(Cache.Get(corporation) is Corporation corpData))
|
|
{
|
|
_logger("Attempted to spawn entity with invalid corporation ID");
|
|
return null;
|
|
}
|
|
|
|
var gearData = loadoutData.Items
|
|
.Take(hullData.Hardpoints.Count)
|
|
.Where(id => id != Guid.Empty)
|
|
.Select(id => Cache.Get<GearData>(id))
|
|
.ToArray();
|
|
|
|
if (gearData.Any(g=>g==null))
|
|
{
|
|
_logger("Attempted to spawn loadout with invalid gear ID");
|
|
return null;
|
|
}
|
|
|
|
var cargoData = loadoutData.Items
|
|
.Skip(hullData.Hardpoints.Count)
|
|
.Where(id => id != Guid.Empty)
|
|
.Select(id => Cache.Get<CraftedItemData>(id))
|
|
.ToArray();
|
|
|
|
if (cargoData.Any(g=>g==null))
|
|
{
|
|
_logger("Attempted to spawn loadout with invalid cargo ID");
|
|
return null;
|
|
}
|
|
|
|
var gear = gearData
|
|
.Select(gd => CreateInstance(gd.ID, GlobalData.StartingGearQualityMin, GlobalData.StartingGearQualityMax))
|
|
.ToArray();
|
|
|
|
if (gear.Any(g=>g==null))
|
|
return null;
|
|
|
|
foreach(var g in gear)
|
|
Cache.Add(g);
|
|
|
|
var cargo = cargoData
|
|
.Select(gd => CreateInstance(gd.ID, GlobalData.StartingGearQualityMin, GlobalData.StartingGearQualityMax))
|
|
.ToArray();
|
|
|
|
if (cargo.Any(g=>g==null))
|
|
return null;
|
|
|
|
foreach(var c in cargo)
|
|
Cache.Add(c);
|
|
|
|
var simpleCargo = loadoutData.SimpleCargo?
|
|
.Select(x => CreateInstance(x.Key, x.Value))
|
|
.ToArray() ?? new SimpleCommodity[0];
|
|
|
|
if (simpleCargo.Any(g=>g==null))
|
|
return null;
|
|
|
|
var hull = CreateInstance(loadoutData.Hull, GlobalData.StartingGearQualityMin, GlobalData.StartingGearQualityMax);
|
|
|
|
if(hull==null)
|
|
return null;
|
|
|
|
// Load target zone if not yet loaded
|
|
if (!_loadedZones.Contains(zone))
|
|
LoadZone(zone);
|
|
|
|
if (hullData.HullType == HullType.Ship)
|
|
{
|
|
var entity = new Ship(this, hull.ID, gear.Select(g => g.ID),
|
|
cargo.Select(c => c.ID).Concat(simpleCargo.Select(c => c.ID)), zone, corporation)
|
|
{
|
|
Name = $"{loadoutData.Name} {Random.NextInt(1,255):X}"
|
|
};
|
|
Cache.Add(entity);
|
|
|
|
ZoneEntities[zone][entity.ID] = entity;
|
|
return entity;
|
|
}
|
|
|
|
if (hullData.HullType == HullType.Station)
|
|
{
|
|
var distance = lerp(.2f, .8f, Random.NextFloat()) * zoneData.Radius;
|
|
var orbit = new OrbitData
|
|
{
|
|
Context = this,
|
|
Distance = distance,
|
|
Parent = _orbits
|
|
.Select(id => Cache.Get<OrbitData>(id))
|
|
.First(orbitData => orbitData.Zone == zone && orbitData.Parent == Guid.Empty).ID,
|
|
Period = OrbitalPeriod(distance),
|
|
Phase = Random.NextFloat(),
|
|
Zone = zone
|
|
};
|
|
Cache.Add(orbit);
|
|
_orbits.Add(orbit.ID);
|
|
|
|
var entity = new OrbitalEntity(this, hull.ID, gear.Select(g => g.ID),
|
|
cargo.Select(c => c.ID).Concat(simpleCargo.Select(c => c.ID)), orbit.ID, zone, corporation)
|
|
{
|
|
Name = $"{loadoutData.Name} {Random.NextInt(1,255):X}",
|
|
Temperature = 293,
|
|
Population = 1
|
|
};
|
|
var parentCorp = Cache.Get<MegaCorporation>(corpData.Parent);
|
|
foreach (var attribute in parentCorp.Personality)
|
|
entity.Personality[attribute.Key] = attribute.Value;
|
|
Cache.Add(entity);
|
|
|
|
ZoneEntities[zone][entity.ID] = entity;
|
|
return entity;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public SimpleCommodity CreateInstance(Guid data, int count)
|
|
{
|
|
var item = Cache.Get<SimpleCommodityData>(data);
|
|
if (item != null)
|
|
{
|
|
var newItem = new SimpleCommodity
|
|
{
|
|
Context = this,
|
|
Data = data,
|
|
Quantity = count
|
|
};
|
|
Cache.Add(newItem);
|
|
return newItem;
|
|
}
|
|
|
|
_logger("Attempted to create Simple Commodity instance using missing or incorrect item id");
|
|
return null;
|
|
}
|
|
|
|
public CraftedItemInstance CreateInstance(Guid data, float qualityMin, float qualityMax)
|
|
{
|
|
var item = Cache.Get<CraftedItemData>(data);
|
|
if (item == null)
|
|
{
|
|
_logger("Attempted to create crafted item instance using missing or incorrect item id!");
|
|
return null;
|
|
}
|
|
|
|
var blueprint = Cache.GetAll<BlueprintData>().FirstOrDefault(b => b.Item == data);
|
|
if (blueprint == null)
|
|
{
|
|
_logger("Attempted to create crafted item instance which has no blueprint!");
|
|
return null;
|
|
}
|
|
|
|
var ingredients = blueprint.Ingredients.SelectMany(ci =>
|
|
{
|
|
var ingredient = Cache.Get(ci.Key);
|
|
return ingredient is SimpleCommodityData
|
|
? (IEnumerable<ItemInstance>) new[] {CreateInstance(ci.Key, ci.Value)}
|
|
: Enumerable.Range(0, ci.Value).Select(i => CreateInstance(ci.Key, qualityMin, qualityMax));
|
|
})
|
|
.ToList();
|
|
|
|
Cache.AddAll(ingredients);
|
|
|
|
if (item is EquippableItemData equippableItemData)
|
|
{
|
|
var newGear = new Gear
|
|
{
|
|
Context = this,
|
|
Data = data,
|
|
Ingredients = ingredients.Select(i=>i.ID).ToList(),
|
|
Quality = Random.NextFloat(qualityMin, qualityMax),
|
|
Blueprint = blueprint.ID,
|
|
Name = $"Generic {item.Name}"
|
|
};
|
|
newGear.Durability = Evaluate(equippableItemData.Durability, newGear);
|
|
Cache.Add(newGear);
|
|
return newGear;
|
|
}
|
|
|
|
var newCommodity = new CompoundCommodity
|
|
{
|
|
Context = this,
|
|
Data = data,
|
|
Ingredients = ingredients.Select(i=>i.ID).ToList(),
|
|
Quality = Random.NextFloat(qualityMin, qualityMax),
|
|
Blueprint = blueprint.ID,
|
|
Name = $"Generic {item.Name}"
|
|
};
|
|
Cache.Add(newCommodity);
|
|
return newCommodity;
|
|
}
|
|
|
|
class DijkstraNode
|
|
{
|
|
public float Cost;
|
|
public DijkstraNode Parent;
|
|
public SimplifiedZoneData Zone;
|
|
}
|
|
|
|
public List<SimplifiedZoneData> FindPath(SimplifiedZoneData source, SimplifiedZoneData target, bool bestFirst = false)
|
|
{
|
|
SortedList<float,DijkstraNode> members = new SortedList<float,DijkstraNode>{{0,new DijkstraNode{Zone = source}}};
|
|
List<DijkstraNode> searched = new List<DijkstraNode>();
|
|
while (true)
|
|
{
|
|
var s = members.FirstOrDefault(m => !searched.Contains(m.Value)).Value; // Lowest cost unsearched node
|
|
if (s == null) return null; // No vertices left unsearched
|
|
if (s.Zone == target) // We found the path
|
|
{
|
|
Stack<DijkstraNode> path = new Stack<DijkstraNode>(); // Since we start at the end, use a LIFO collection
|
|
path.Push(s);
|
|
while(path.Peek().Parent!=null) // Keep pushing until we reach the start, which has no parent
|
|
path.Push(path.Peek().Parent);
|
|
return path.Select(dv => dv.Zone).ToList();
|
|
}
|
|
// For each adjacent star (filter already visited zones unless heuristic is in use)
|
|
foreach (var dijkstraStar in s.Zone.Links.WhereSelectF(i => !bestFirst || members.All(m => m.Value.Zone != GalaxyZones[i]),
|
|
// Cost is parent cost plus distance
|
|
i => new DijkstraNode {Parent = s, Zone = GalaxyZones[i], Cost = s.Cost + length(s.Zone.Position - GalaxyZones[i].Position)}))
|
|
// Add new member to list, sorted by cost plus optional heuristic
|
|
members.Add(bestFirst ? dijkstraStar.Cost + length(dijkstraStar.Zone.Position - target.Position) : dijkstraStar.Cost, dijkstraStar);
|
|
searched.Add(s);
|
|
}
|
|
}
|
|
|
|
public void SetParent(Entity child, Entity parent)
|
|
{
|
|
child.Parent = parent.ID;
|
|
parent.Children.Add(child.ID);
|
|
parent.RecalculateMass();
|
|
}
|
|
|
|
public void RemoveParent(Entity child)
|
|
{
|
|
if (child.Parent == Guid.Empty)
|
|
return;
|
|
|
|
var parent = Cache.Get<Entity>(child.Parent);
|
|
parent.Children.Remove(child.ID);
|
|
parent.RecalculateMass();
|
|
child.Parent = Guid.Empty;
|
|
}
|
|
|
|
public void MineAsteroid(Entity miner, Guid asteroidBelt, int asteroid, float damage, float efficiency, float penetration)
|
|
{
|
|
var planetData = Cache.Get<PlanetData>(asteroidBelt);
|
|
var asteroidTransform = GetAsteroidTransform(asteroidBelt, asteroid);
|
|
var hpLerp = unlerp(GlobalData.AsteroidSizeMin, GlobalData.AsteroidSizeMax, asteroidTransform.w);
|
|
var asteroidHitpoints = lerp(GlobalData.AsteroidHitpointsMin, GlobalData.AsteroidHitpointsMax, pow(hpLerp, GlobalData.AsteroidHitpointsPower));
|
|
|
|
if (!_asteroidDamage.ContainsKey((asteroidBelt, asteroid)))
|
|
_asteroidDamage[(asteroidBelt, asteroid)] = 0;
|
|
_asteroidDamage[(asteroidBelt, asteroid)] = _asteroidDamage[(asteroidBelt, asteroid)] + damage;
|
|
|
|
if (!_asteroidMiningAccumulator.ContainsKey((asteroidBelt, miner.ID, asteroid)))
|
|
_asteroidMiningAccumulator[(asteroidBelt, miner.ID, asteroid)] = 0;
|
|
_asteroidMiningAccumulator[(asteroidBelt, miner.ID, asteroid)] = _asteroidMiningAccumulator[(asteroidBelt, miner.ID, asteroid)] + damage;
|
|
|
|
if (_asteroidDamage[(asteroidBelt, asteroid)] > asteroidHitpoints)
|
|
{
|
|
_asteroidRespawnTimers[(asteroidBelt, asteroid)] =
|
|
lerp(GlobalData.AsteroidRespawnMin, GlobalData.AsteroidRespawnMax, hpLerp);
|
|
_asteroidDamage.Remove((asteroidBelt, asteroid));
|
|
_asteroidMiningAccumulator.Remove((asteroidBelt, miner.ID, asteroid));
|
|
return;
|
|
}
|
|
|
|
var resourceCount = planetData.Resources.Sum(x => x.Value);
|
|
var resource = planetData.Resources.MaxBy(x => pow(x.Value, 1f / penetration) * Random.NextFloat());
|
|
if (efficiency * Random.NextFloat() * _asteroidMiningAccumulator[(asteroidBelt, miner.ID, asteroid)] * resourceCount / GlobalData.MiningDifficulty > 1 && miner.OccupiedCapacity < miner.Capacity - 1)
|
|
{
|
|
_asteroidMiningAccumulator.Remove((asteroidBelt, miner.ID, asteroid));
|
|
var simpleCommodityTarget = miner.Cargo
|
|
.Select(i => Cache.Get<ItemInstance>(i))
|
|
.FirstOrDefault(i => i.Data == resource.Key) as SimpleCommodity;
|
|
if (simpleCommodityTarget == null)
|
|
{
|
|
var newSimpleCommodity = new SimpleCommodity
|
|
{
|
|
Context = this,
|
|
Data = resource.Key,
|
|
Quantity = 1
|
|
};
|
|
Cache.Add(newSimpleCommodity);
|
|
miner.Cargo.Add(newSimpleCommodity.ID);
|
|
miner.RecalculateMass();
|
|
}
|
|
else
|
|
{
|
|
simpleCommodityTarget.Quantity++;
|
|
miner.RecalculateMass();
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool MoveCargo(Entity source, Entity target, ItemInstance item, int quantity = int.MaxValue)
|
|
{
|
|
if (item is CraftedItemInstance craftedItemInstance)
|
|
{
|
|
var craftedItemData = Cache.Get<CraftedItemData>(craftedItemInstance.Data);
|
|
if (target.Capacity - target.OccupiedCapacity > craftedItemData.Size)
|
|
{
|
|
// Target has the cargo capacity for the item, simply move the instance
|
|
source.Cargo.Remove(item.ID);
|
|
target.Cargo.Add(item.ID);
|
|
source.RecalculateMass();
|
|
target.RecalculateMass();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (item is SimpleCommodity simpleCommoditySource)
|
|
{
|
|
quantity = min(quantity, simpleCommoditySource.Quantity);
|
|
var simpleCommodityData = Cache.Get<SimpleCommodityData>(simpleCommoditySource.Data);
|
|
var spareCapacity = (int) (target.Capacity - target.OccupiedCapacity);
|
|
var simpleCommodityTarget = target.Cargo
|
|
.Select(i => Cache.Get<ItemInstance>(i))
|
|
.FirstOrDefault(i => i.Data == simpleCommodityData.ID) as SimpleCommodity;
|
|
var success = spareCapacity >= quantity;
|
|
quantity = min(quantity, spareCapacity);
|
|
if (simpleCommodityTarget == null)
|
|
{
|
|
if (quantity == simpleCommoditySource.Quantity)
|
|
{
|
|
// Target has the cargo capacity to hold the full quantity,
|
|
// and the target has no matching item instance;
|
|
// simply move the existing item instance to the target
|
|
source.Cargo.Remove(simpleCommoditySource.ID);
|
|
target.Cargo.Add(simpleCommoditySource.ID);
|
|
source.RecalculateMass();
|
|
target.RecalculateMass();
|
|
return success;
|
|
}
|
|
else
|
|
{
|
|
// Target has the cargo capacity to hold the desired quantity
|
|
// and the target has no matching item instance;
|
|
// Create a new item instance on the target and decrement the quantity of the source instance
|
|
var newSimpleCommodity = new SimpleCommodity
|
|
{
|
|
Context = this,
|
|
Data = simpleCommodityData.ID,
|
|
Quantity = simpleCommoditySource.Quantity
|
|
};
|
|
Cache.Add(newSimpleCommodity);
|
|
target.Cargo.Add(newSimpleCommodity.ID);
|
|
simpleCommoditySource.Quantity -= quantity;
|
|
source.RecalculateMass();
|
|
target.RecalculateMass();
|
|
return success;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (quantity == simpleCommoditySource.Quantity)
|
|
{
|
|
// Target has the cargo capacity to hold the full quantity,
|
|
// and there is already a matching item instance;
|
|
// delete the source entity's item instance and increment the target's quantity
|
|
source.Cargo.Remove(simpleCommoditySource.ID);
|
|
Cache.Delete(simpleCommoditySource);
|
|
simpleCommodityTarget.Quantity += quantity;
|
|
source.RecalculateMass();
|
|
target.RecalculateMass();
|
|
return success;
|
|
}
|
|
else
|
|
{
|
|
// Target has the cargo capacity to hold the desired quantity,
|
|
// and there is already a matching item instance;
|
|
// decrement the source's quantity and increment the target's quantity
|
|
simpleCommoditySource.Quantity -= quantity;
|
|
simpleCommodityTarget.Quantity += quantity;
|
|
source.RecalculateMass();
|
|
target.RecalculateMass();
|
|
return success;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
}
|
|
} |