diff --git a/src/auto-evo/mutations/CommonMutationFunctions.cs b/src/auto-evo/mutations/CommonMutationFunctions.cs index f706185b8e8..80f92e77c7b 100644 --- a/src/auto-evo/mutations/CommonMutationFunctions.cs +++ b/src/auto-evo/mutations/CommonMutationFunctions.cs @@ -90,7 +90,9 @@ public static MicrobeSpecies GenerateRandomSpecies(MicrobeSpecies mutated, Patch MutationLogicFunctions.ColourNewMicrobeSpecies(random, mutated); } - mutated.ModifiableTolerances.CopyFrom(forPatch.GenerateTolerancesForMicrobe(mutated.Organelles)); + mutated.ModifiableTolerances.CopyFrom(forPatch.GenerateTolerancesForMicrobe(mutated.Organelles, + MicrobeInternalCalculations.CalculateSpecializationBonus(mutated.Organelles, + new Dictionary()))); // Override the default species starting name to have more variability in the names var nameGenerator = SimulationParameters.Instance.NameGenerator; diff --git a/src/auto-evo/simulation/SimulationCache.cs b/src/auto-evo/simulation/SimulationCache.cs index 51a71805446..d49bcd84c99 100644 --- a/src/auto-evo/simulation/SimulationCache.cs +++ b/src/auto-evo/simulation/SimulationCache.cs @@ -140,12 +140,11 @@ public EnergyBalanceInfoSimple GetEnergyBalanceForSpecies(MicrobeSpecies species // TODO: check if caching instances of these objects would be better than always recreating var cached = new EnergyBalanceInfoSimple(); - // Assume here that the species specialization factor may not be up to date, so recalculate here - var specialization = MicrobeInternalCalculations.CalculateSpecializationBonus(species.Organelles, workMemory1); + var totalSpecializationBonus = species.CellTypeSpecializationBonus; // Auto-evo uses the average values of compound during the course of a simulated day ProcessSystem.ComputeEnergyBalanceSimple(species.Organelles, biomeConditions, - GetEnvironmentalTolerances(species, biomeConditions), specialization, species.MembraneType, + GetEnvironmentalTolerances(species, biomeConditions), totalSpecializationBonus, species.MembraneType, maximumMovementDirection, true, species.PlayerSpecies, worldSettings, CompoundAmountType.Average, this, cached); @@ -169,8 +168,13 @@ public float GetSpeedForSpecies(MicrobeSpecies species) return speed; } - var cached = MicrobeInternalCalculations.CalculateSpeed(species.Organelles.Organelles, species.MembraneType, - species.MembraneRigidity, species.IsBacteria, true); + var organelles = species.Organelles; + + // For MicrobeSpecies, Cell Type Specialization = Total Specialization Bonus + var totalSpecializationBonus = species.CellTypeSpecializationBonus; + + var cached = MicrobeInternalCalculations.CalculateSpeed(organelles.Organelles, species.MembraneType, + species.MembraneRigidity, species.IsBacteria, totalSpecializationBonus, true); cachedBaseSpeeds.Add(key, cached); return cached; @@ -202,7 +206,12 @@ public float GetRotationSpeedForSpecies(MicrobeSpecies species) // prey species by multiple predators might benefit ever so slightly, but it seems kind of unlikely). // A more useful thing would be to cache this directly in the species when calculating other movement cached // properties. - return MicrobeInternalCalculations.CalculateRotationSpeed(species.Organelles.Organelles); + var organelles = species.Organelles; + + // For MicrobeSpecies, Cell Type Specialization = Total Specialization Bonus + var totalSpecializationBonus = species.CellTypeSpecializationBonus; + + return MicrobeInternalCalculations.CalculateRotationSpeed(organelles.Organelles, totalSpecializationBonus); } public float GetCompoundConversionScoreForSpecies(CompoundDefinition fromCompound, CompoundDefinition toCompound, @@ -390,9 +399,13 @@ public float GetPredationScore(Species predatorSpecies, Species preySpecies, Bio } } + // This will be used at several points to mimic the effect the specialization bonus has on organelles + var specializationBonus = predator.CellTypeSpecializationBonus; + var preySpecializationBonus = prey.CellTypeSpecializationBonus; + var predatorHexSize = GetBaseHexSizeForSpecies(predator); var preyHexSize = GetBaseHexSizeForSpecies(prey); - var enzymesScore = GetEnzymesScore(predator, prey.MembraneType.DissolverEnzyme); + var enzymesScore = GetEnzymesScore(predator, prey.MembraneType.DissolverEnzyme, specializationBonus); var canDigestPrey = predatorHexSize / preyHexSize > Constants.ENGULF_SIZE_RATIO_REQ && canEngulf && enzymesScore > 0.0f; @@ -446,22 +459,25 @@ public float GetPredationScore(Species predatorSpecies, Species preySpecies, Bio var preyToolScores = GetPredationToolsRawScores(prey); var toxicity = predatorToolScores.AverageToxicity; - var macrolideScore = predatorToolScores.MacrolideScore; - var predatorSlimeJetScore = predatorToolScores.SlimeJetScore; - var pullingCiliaModifier = predatorToolScores.PullingCiliaModifier; + oxytoxyScore *= specializationBonus; + cytotoxinScore *= specializationBonus; + channelInhibitorScore *= specializationBonus; + var macrolideScore = predatorToolScores.MacrolideScore * specializationBonus; + var predatorSlimeJetScore = predatorToolScores.SlimeJetScore * specializationBonus; + var pullingCiliaModifier = predatorToolScores.PullingCiliaModifier * specializationBonus; var strongPullingCiliaModifier = pullingCiliaModifier * pullingCiliaModifier; var predatorToxinResistance = predator.MembraneType.ToxinResistance; var predatorPhysicalResistance = predator.MembraneType.PhysicalResistance; - var preySlimeJetScore = preyToolScores.SlimeJetScore; + var preySlimeJetScore = preyToolScores.SlimeJetScore * preySpecializationBonus; var preyMucocystsScore = preyToolScores.MucocystsScore; var preyPilusScore = preyToolScores.PilusScore; var preyInjectisomeScore = preyToolScores.InjectisomeScore; var preyToxicity = preyToolScores.AverageToxicity; - var preyOxytoxyScore = preyToolScores.OxytoxyScore; - var preyCytotoxinScore = preyToolScores.CytotoxinScore; - var preyMacrolideScore = preyToolScores.MacrolideScore; - var preyChannelInhibitorScore = preyToolScores.ChannelInhibitorScore; + var preyOxytoxyScore = preyToolScores.OxytoxyScore * preySpecializationBonus; + var preyCytotoxinScore = preyToolScores.CytotoxinScore * preySpecializationBonus; + var preyMacrolideScore = preyToolScores.MacrolideScore * preySpecializationBonus; + var preyChannelInhibitorScore = preyToolScores.ChannelInhibitorScore * preySpecializationBonus; var preyOxygenMetabolismInhibitorScore = preyToolScores.OxygenMetabolismInhibitorScore; var defensivePilusScore = preyToolScores.DefensivePilusScore; var defensiveInjectisomeScore = preyToolScores.DefensiveInjectisomeScore; @@ -1222,7 +1238,7 @@ public PredationToolsRawScores GetPredationToolsRawScores(MicrobeSpecies microbe return predationToolsRawScores; } - public float GetEnzymesScore(MicrobeSpecies predator, string dissolverEnzyme) + public float GetEnzymesScore(MicrobeSpecies predator, string dissolverEnzyme, float specializationBonus) { // This is not cached as it is not useful at the present time (as this is only called from places that cache // stuff) @@ -1263,9 +1279,9 @@ public float GetEnzymesScore(MicrobeSpecies predator, string dissolverEnzyme) // If not digestible, mark that as a 0 score if (!isMembraneDigestible) - enzymesScore = 0; + return 0; - return enzymesScore; + return enzymesScore * specializationBonus; } public ResolvedMicrobeTolerances GetEnvironmentalTolerances(MicrobeSpecies species, diff --git a/src/general/GameWorld.cs b/src/general/GameWorld.cs index 0f44de67ab6..fcd51ce202d 100644 --- a/src/general/GameWorld.cs +++ b/src/general/GameWorld.cs @@ -260,7 +260,8 @@ public static void SetSpeciesInitialTolerances(Species species, PatchMap map, Pa if (species is MicrobeSpecies microbeSpecies) { - species.ModifiableTolerances.CopyFrom(patch.GenerateTolerancesForMicrobe(microbeSpecies.Organelles)); + species.ModifiableTolerances.CopyFrom(patch.GenerateTolerancesForMicrobe(microbeSpecies.Organelles, + microbeSpecies.CellTypeSpecializationBonus)); } else if (species is MulticellularSpecies multicellularSpecies) { diff --git a/src/general/base_stage/CreatureStageBase.cs b/src/general/base_stage/CreatureStageBase.cs index 1a8eb08d219..c0154256d06 100644 --- a/src/general/base_stage/CreatureStageBase.cs +++ b/src/general/base_stage/CreatureStageBase.cs @@ -579,7 +579,8 @@ private void CheckPerformanceEnoughForSimulationSpeed() private void AdjustTolerancesToWorkInPatch(MicrobeSpecies species, Patch currentPatch) { - var optimal = currentPatch.GenerateTolerancesForMicrobe(species.Organelles); + var optimal = currentPatch.GenerateTolerancesForMicrobe(species.Organelles, + species.CellTypeSpecializationBonus); var current = MicrobeEnvironmentalToleranceCalculations.CalculateTolerances(species, currentPatch.Biome); diff --git a/src/general/mutation_points/CellTypeEditsFacade.cs b/src/general/mutation_points/CellTypeEditsFacade.cs index a8f951d73d0..f027ec4bf4d 100644 --- a/src/general/mutation_points/CellTypeEditsFacade.cs +++ b/src/general/mutation_points/CellTypeEditsFacade.cs @@ -94,7 +94,7 @@ public bool IsBacteria /// public string? SplitFromTypeName => originalCell.SplitFromTypeName; - public float SpecializationBonus => + public float CellTypeSpecializationBonus => throw new NotSupportedException("This class doesn't dynamically recalculate the specialization bonus"); // TODO: check that this is right (there might sometimes be too many items in removedOrganelles) diff --git a/src/general/mutation_points/MicrobeEditsFacade.cs b/src/general/mutation_points/MicrobeEditsFacade.cs index 20c39432e61..c1dee8f0dc9 100644 --- a/src/general/mutation_points/MicrobeEditsFacade.cs +++ b/src/general/mutation_points/MicrobeEditsFacade.cs @@ -91,7 +91,7 @@ public string CellTypeName /// public string? SplitFromTypeName => microbeSpecies.SplitFromTypeName; - public float SpecializationBonus => + public float CellTypeSpecializationBonus => throw new NotSupportedException("This class doesn't dynamically recalculate the specialization bonus"); public int Count diff --git a/src/general/world_effects/AmmoniaProductionEffect.cs b/src/general/world_effects/AmmoniaProductionEffect.cs index 0919ea16aa7..fc2a8c49676 100644 --- a/src/general/world_effects/AmmoniaProductionEffect.cs +++ b/src/general/world_effects/AmmoniaProductionEffect.cs @@ -55,7 +55,7 @@ public static float GetSpeciesModifiersForEffect(Species species, out ResolvedMi resolvedTolerances = MicrobeEnvironmentalToleranceCalculations.ResolveToleranceValues( MicrobeEnvironmentalToleranceCalculations.CalculateTolerances(microbeSpecies, biome)); - specialization = microbeSpecies.SpecializationBonus; + specialization = microbeSpecies.CellTypeSpecializationBonus; } else if (species is MulticellularSpecies multicellularSpecies) { diff --git a/src/microbe_stage/ICellDefinition.cs b/src/microbe_stage/ICellDefinition.cs index 237f4c5b4e1..f5fab2a57eb 100644 --- a/src/microbe_stage/ICellDefinition.cs +++ b/src/microbe_stage/ICellDefinition.cs @@ -28,6 +28,13 @@ public interface ICellDefinition : IReadOnlyCellDefinition, ISimulationPhotograp public string FormattedName { get; } + /// + /// A multiplier starting from 1 and going up based on how specialized this cell type is. This is eventually + /// applied to and many other systems. + /// Does not include any adjacency bonus effects that may be applied later. + /// + public float CellTypeSpecializationBonus { get; set; } + /// /// Repositions the cell to the origin and recalculates any properties dependent on its position. /// @@ -49,6 +56,8 @@ public interface IReadOnlyCellDefinition public interface ICellTypeDefinition : ICellDefinition, IReadOnlyCellTypeDefinition { public new int MPCost { get; set; } + + public new float CellTypeSpecializationBonus { get; } } public interface IReadOnlyCellTypeDefinition : IReadOnlyCellDefinition, IPlayerReadableName @@ -66,7 +75,7 @@ public interface IReadOnlyCellTypeDefinition : IReadOnlyCellDefinition, IPlayerR /// A multiplier starting from 1 and going up based on how specialized this cell type is. This is eventually /// applied to /// - public float SpecializationBonus { get; } + public float CellTypeSpecializationBonus { get; } } /// @@ -127,7 +136,7 @@ public static void SetupWorldEntities(this ICellDefinition definition, IWorldSim workMemory1, workMemory2) { // For visualization the bonus doesn't matter, but we need to set a valid value - SpecializationBonus = 1, + CellTypeSpecializationBonus = 1, }; species.SetupWorldEntities(worldSimulation); diff --git a/src/microbe_stage/IOrganelleComponent.cs b/src/microbe_stage/IOrganelleComponent.cs index e863d97fd6e..b039b344e12 100644 --- a/src/microbe_stage/IOrganelleComponent.cs +++ b/src/microbe_stage/IOrganelleComponent.cs @@ -19,6 +19,7 @@ public interface IOrganelleComponent /// /// /// Organelle container instance this organelle is inside + /// Organelle effect bonus cell from specialization and adjacency /// Entity reference of the entity that contains this organelle /// /// The simulation this entity is in. Care needs to be taken on what operations are safe to perform here in an @@ -28,7 +29,8 @@ public interface IOrganelleComponent /// /// modifies the amount of ATP to be consumed by organelles /// Time since the last update in seconds - public void UpdateAsync(ref OrganelleContainer organelleContainer, in Entity microbeEntity, + public void UpdateAsync(ref OrganelleContainer organelleContainer, ref SpecializationFactor specializationFactor, + in Entity microbeEntity, IWorldSimulation worldSimulation, float energyCostMultiplier, float delta); /// diff --git a/src/microbe_stage/MicrobeEnvironmentalToleranceCalculations.cs b/src/microbe_stage/MicrobeEnvironmentalToleranceCalculations.cs index 125668ae516..51dd654423e 100644 --- a/src/microbe_stage/MicrobeEnvironmentalToleranceCalculations.cs +++ b/src/microbe_stage/MicrobeEnvironmentalToleranceCalculations.cs @@ -27,7 +27,8 @@ public static double CalculateTotalToleranceScore(MicrobeSpecies species, BiomeC public static ToleranceResult CalculateTolerances(MicrobeSpecies species, IBiomeConditions environment) { - return CalculateTolerances(species.Tolerances, species.Organelles, environment); + return CalculateTolerances(species.Tolerances, species.Organelles, species.CellTypeSpecializationBonus, + environment); } public static ToleranceResult CalculateTolerances(MulticellularSpecies species, IBiomeConditions environment) @@ -40,6 +41,7 @@ public static ToleranceResult CalculateTolerances(MulticellularSpecies species, /// /// Configured tolerances /// Organelles that may affect the tolerances + /// Specialization bonus for this species /// Environment that the tolerances need to match to not get debuffs /// /// If true, excludes perfect adaptation bonuses. This is used to show debuffs in a way that no buffs can get @@ -49,7 +51,8 @@ public static ToleranceResult CalculateTolerances(MulticellularSpecies species, /// /// Calculated tolerance result public static ToleranceResult CalculateTolerances(IReadOnlyEnvironmentalTolerances speciesTolerances, - IReadOnlyList organelles, IBiomeConditions environment, bool excludePositiveBuffs = false) + IReadOnlyList organelles, float totalSpecializationBonus, IBiomeConditions environment, + bool excludePositiveBuffs = false) { var resolvedTolerances = new ToleranceValues { @@ -63,7 +66,7 @@ public static ToleranceResult CalculateTolerances(IReadOnlyEnvironmentalToleranc var noExtraEffects = resolvedTolerances; - ApplyOrganelleEffectsOnTolerances(organelles, ref resolvedTolerances); + ApplyOrganelleEffectsOnTolerances(organelles, totalSpecializationBonus, ref resolvedTolerances); ApplyResultMinimums(ref resolvedTolerances); @@ -95,13 +98,13 @@ public static ToleranceResult CalculateTolerancesWithoutOrganelleModifiers( } public static void ApplyOrganelleEffectsOnTolerances(IReadOnlyList organelles, - ref ToleranceValues tolerances) + float totalSpecializationBonus, ref ToleranceValues tolerances) { - float temperatureChange = 0; - float oxygenChange = 0; - float uvChange = 0; - float pressureMinimumChange = 0; - float pressureToleranceChange = 0; + float totalTemperatureChange = 0; + float totalOxygenChange = 0; + float totalUvChange = 0; + float totalPressureMinimumChange = 0; + float totalPressureToleranceChange = 0; int organelleCount = organelles.Count; for (int i = 0; i < organelleCount; ++i) @@ -110,33 +113,48 @@ public static void ApplyOrganelleEffectsOnTolerances(IReadOnlyList 0) + temperatureChange *= totalSpecializationBonus; + if (oxygenChange > 0) + oxygenChange *= totalSpecializationBonus; + if (uvChange > 0) + uvChange *= totalSpecializationBonus; + if (pressureToleranceChange > 0) + pressureToleranceChange *= totalSpecializationBonus; + // Buffer all changes so that float rounding doesn't cause us issues - temperatureChange += organelleDefinition.ToleranceModifierTemperatureRange; - oxygenChange += organelleDefinition.ToleranceModifierOxygen; - uvChange += organelleDefinition.ToleranceModifierUV; - pressureToleranceChange += organelleDefinition.ToleranceModifierPressureTolerance; + totalTemperatureChange += temperatureChange; + totalOxygenChange += oxygenChange; + totalUvChange += uvChange; + totalPressureToleranceChange += pressureToleranceChange; } } // Then apply all at once - tolerances.TemperatureTolerance += temperatureChange; - tolerances.OxygenResistance += oxygenChange; - tolerances.UVResistance += uvChange; - tolerances.PressureMinimum -= pressureMinimumChange; - tolerances.PressureTolerance += pressureToleranceChange; + tolerances.TemperatureTolerance += totalTemperatureChange; + tolerances.OxygenResistance += totalOxygenChange; + tolerances.UVResistance += totalUvChange; + tolerances.PressureMinimum -= totalPressureMinimumChange; + tolerances.PressureTolerance += totalPressureToleranceChange; } public static void ApplyOrganelleEffectsOnTolerances(IReadOnlyCollection organelles, - ref ToleranceValues tolerances) + ref ToleranceValues tolerances, float totalSpecializationBonus) { // This is a separate overload as this uses an extra enumerator call (and putting in the indexer requirement // to the base-read-only cell layout would require quite expensive operations in facade types). - float temperatureChange = 0; - float oxygenChange = 0; - float uvChange = 0; - float pressureMinimumChange = 0; - float pressureToleranceChange = 0; + float totalTemperatureChange = 0; + float totalOxygenChange = 0; + float totalUvChange = 0; + float totalPressureMinimumChange = 0; + float totalPressureToleranceChange = 0; foreach (var organelle in organelles) { @@ -144,20 +162,35 @@ public static void ApplyOrganelleEffectsOnTolerances(IReadOnlyCollection 0) + temperatureChange *= totalSpecializationBonus; + if (oxygenChange > 0) + oxygenChange *= totalSpecializationBonus; + if (uvChange > 0) + uvChange *= totalSpecializationBonus; + if (pressureToleranceChange > 0) + pressureToleranceChange *= totalSpecializationBonus; + // Buffer all changes so that float rounding doesn't cause us issues - temperatureChange += organelleDefinition.ToleranceModifierTemperatureRange; - oxygenChange += organelleDefinition.ToleranceModifierOxygen; - uvChange += organelleDefinition.ToleranceModifierUV; - pressureToleranceChange += organelleDefinition.ToleranceModifierPressureTolerance; + totalTemperatureChange += temperatureChange; + totalOxygenChange += oxygenChange; + totalUvChange += uvChange; + totalPressureToleranceChange += pressureToleranceChange; } } // Then apply all at once - tolerances.TemperatureTolerance += temperatureChange; - tolerances.OxygenResistance += oxygenChange; - tolerances.UVResistance += uvChange; - tolerances.PressureMinimum -= pressureMinimumChange; - tolerances.PressureTolerance += pressureToleranceChange; + tolerances.TemperatureTolerance += totalTemperatureChange; + tolerances.OxygenResistance += totalOxygenChange; + tolerances.UVResistance += totalUvChange; + tolerances.PressureMinimum -= totalPressureMinimumChange; + tolerances.PressureTolerance += totalPressureToleranceChange; } public static void GenerateToleranceEffectSummariesByOrganelle(IReadOnlyList organelles, @@ -215,6 +248,19 @@ public static void GenerateToleranceEffectSummariesByCell(IndividualHexLayout + /// Calculates effective tolerances given the species tolerances, cells, and environmental conditions. + /// + /// Configured tolerances + /// Organelles that may affect the tolerances + /// Environment that the tolerances need to match to not get debuffs + /// + /// If true, excludes perfect adaptation bonuses. This is used to show debuffs in a way that no buffs can get + /// mixed in. Note that for the tooltips we separately generate "good enough" tolerances to not get bonuses or + /// debuffs instead of using this flag. So TODO: it would be nice to combine these two approaches that are almost + /// the same. But to get the new tolerance GUI visuals done, these two systems were left as separate (for now). + /// + /// Calculated tolerance result public static ToleranceResult CalculateTolerances(IReadOnlyEnvironmentalTolerances speciesTolerances, IndividualHexLayout cells, IBiomeConditions environment, bool excludePositiveBuffs = false) { @@ -250,11 +296,14 @@ public static void ApplyCellEffectsOnTolerances(IndividualHexLayout organelles) + public static float GetTotalNominalCapacity(IEnumerable organelles, + float totalSpecializationBonus) { - return organelles.Sum(o => GetNominalCapacityForOrganelle(o.Definition, o.Upgrades)); + return organelles.Sum(o => GetNominalCapacityForOrganelle(o.Definition, o.Upgrades, + totalSpecializationBonus)); } public static Dictionary GetTotalSpecificCapacity(IReadOnlyList organelles, - out float nominalCapacity) + float totalSpecializationBonus, out float nominalCapacity) { - var totalNominalCap = GetTotalNominalCapacity(organelles); + var totalNominalCap = GetTotalNominalCapacity(organelles, totalSpecializationBonus); nominalCapacity = totalNominalCap; var capacities = new Dictionary(); - AddSpecificCapacity(organelles, capacities); + AddSpecificCapacity(organelles, capacities, totalSpecializationBonus); return capacities; } public static void AddSpecificCapacity(IReadOnlyList organelles, - Dictionary capacities) + Dictionary capacities, float totalSpecializationBonus) { var count = organelles.Count; @@ -74,7 +76,8 @@ public static void AddSpecificCapacity(IReadOnlyList organell { var organelle = organelles[i]; - var specificCapacity = GetAdditionalCapacityForOrganelle(organelle.Definition, organelle.Upgrades); + var specificCapacity = GetAdditionalCapacityForOrganelle(organelle.Definition, organelle.Upgrades, + totalSpecializationBonus); if (specificCapacity.Compound == Compound.Invalid) continue; @@ -86,13 +89,17 @@ public static void AddSpecificCapacity(IReadOnlyList organell } /// - /// Variant of to update - /// spawned microbe stats. The used must already have the correct nominal capacity set - /// for this to work correctly. + /// Variant of to + /// update spawned microbe stats. The used must already have the correct nominal + /// capacity set for this to work correctly. /// /// Target compound bag to set info in (this doesn't update nominal capacity) /// Organelles to find specific capacity from - public static void UpdateSpecificCapacities(CompoundBag compoundBag, IReadOnlyList organelles) + /// + /// The cell specialization bonus for this microbe/cell, including adjacency when relevant + /// + public static void UpdateSpecificCapacities(CompoundBag compoundBag, IReadOnlyList organelles, + float totalSpecializationBonus) { compoundBag.ClearSpecificCapacities(); @@ -100,7 +107,8 @@ public static void UpdateSpecificCapacities(CompoundBag compoundBag, IReadOnlyLi for (int i = 0; i < count; ++i) { var organelle = organelles[i]; - var specificCapacity = GetAdditionalCapacityForOrganelle(organelle.Definition, organelle.Upgrades); + var specificCapacity = GetAdditionalCapacityForOrganelle(organelle.Definition, organelle.Upgrades, + totalSpecializationBonus); if (specificCapacity.Compound == Compound.Invalid) continue; @@ -110,7 +118,7 @@ public static void UpdateSpecificCapacities(CompoundBag compoundBag, IReadOnlyLi } public static float GetNominalCapacityForOrganelle(OrganelleDefinition definition, - IReadOnlyOrganelleUpgrades? upgrades) + IReadOnlyOrganelleUpgrades? upgrades, float totalSpecializationBonus) { if (upgrades?.CustomUpgradeData is StorageComponentUpgrades storage && storage.SpecializedFor != Compound.Invalid) @@ -121,11 +129,12 @@ public static float GetNominalCapacityForOrganelle(OrganelleDefinition definitio if (definition.Components.Storage == null) return 0; - return definition.Components.Storage!.Capacity; + return definition.Components.Storage!.Capacity * totalSpecializationBonus; } public static (Compound Compound, float Capacity) - GetAdditionalCapacityForOrganelle(OrganelleDefinition definition, IReadOnlyOrganelleUpgrades? upgrades) + GetAdditionalCapacityForOrganelle(OrganelleDefinition definition, IReadOnlyOrganelleUpgrades? upgrades, + float totalSpecializationBonus) { if (definition.Components.Storage == null) return (Compound.Invalid, 0); @@ -136,7 +145,7 @@ public static (Compound Compound, float Capacity) var specialization = storage.SpecializedFor; var capacity = definition.Components.Storage!.Capacity; var extraCapacity = capacity * Constants.VACUOLE_SPECIALIZED_MULTIPLIER; - return (specialization, extraCapacity); + return (specialization, extraCapacity * totalSpecializationBonus); } return (Compound.Invalid, 0); @@ -150,7 +159,7 @@ public static float CalculateCapacity(IEnumerable organelles) // TODO: maybe this should return a ValueTask as this is getting pretty computation intensive public static float CalculateSpeed(IReadOnlyList organelles, MembraneType membraneType, - float membraneRigidity, bool isBacteria, bool useEstimate = false) + float membraneRigidity, bool isBacteria, float totalSpecializationBonus, bool useEstimate = false) { float shapeMass = 0; @@ -252,6 +261,9 @@ public static float CalculateSpeed(IReadOnlyList organelles, var finalMass = useEstimate ? massEstimate : shapeMass; + // Apply cell specialization bonus + organelleMovementForce *= totalSpecializationBonus; + float finalSpeed = (baseMovementForce + organelleMovementForce) / finalMass; return finalSpeed; @@ -295,10 +307,12 @@ public static float CalculateHealth(ResolvedMicrobeTolerances tolerances, Membra /// Calculates the rotation speed for a cell. Note that higher value means slower rotation. /// /// The organelles the cell has with their positions for the calculations + /// Cell specialization bonus, including adjacency if relevant /// /// The rotation speed value for putting in /// - public static float CalculateRotationSpeed(IReadOnlyList organelles) + public static float CalculateRotationSpeed(IReadOnlyList organelles, + float totalSpecializationBonus) { // TODO: it would be very nice to be able to switch this back to a more physically accurate calculation using // the real physics shape here @@ -327,6 +341,8 @@ public static float CalculateRotationSpeed(IReadOnlyList o } } + ciliaFactor *= totalSpecializationBonus; + return inertia / (Constants.CELL_ROTATION_INFLECTION_INERTIA + ciliaFactor + inertia) * Constants.CELL_MAX_ROTATION + Constants.CELL_MIN_ROTATION; } @@ -359,15 +375,16 @@ public static float CalculateAverageDensity(IEnumerable or return density / totalVolume; } - public static float CalculateDigestionSpeed(int enzymeCount) + public static float CalculateDigestionSpeed(int enzymeCount, float totalSpecializationBonus) { var amount = Constants.ENGULF_COMPOUND_ABSORBING_PER_SECOND; - var buff = amount * Constants.ENZYME_DIGESTION_SPEED_UP_FRACTION * enzymeCount; + var buff = amount * Constants.ENZYME_DIGESTION_SPEED_UP_FRACTION * enzymeCount * totalSpecializationBonus; return amount + buff; } - public static float CalculateTotalDigestionSpeed(IEnumerable organelles) + public static float CalculateTotalDigestionSpeed(IEnumerable organelles, + float totalSpecializationBonus) { var multiplier = 0; foreach (var organelle in organelles) @@ -376,13 +393,14 @@ public static float CalculateTotalDigestionSpeed(IEnumerable ++multiplier; } - return CalculateDigestionSpeed(multiplier); + return CalculateDigestionSpeed(multiplier, totalSpecializationBonus); } - public static float CalculateDigestionEfficiency(int enzymeCount) + public static float CalculateDigestionEfficiency(int enzymeCount, float totalSpecializationBonus) { var absorption = Constants.ENGULF_BASE_COMPOUND_ABSORPTION_YIELD; - var buff = absorption * Constants.ENZYME_DIGESTION_EFFICIENCY_BUFF_FRACTION * enzymeCount; + var buff = absorption * Constants.ENZYME_DIGESTION_EFFICIENCY_BUFF_FRACTION * enzymeCount * + totalSpecializationBonus; return Math.Clamp(absorption + buff, 0.0f, Constants.ENZYME_DIGESTION_EFFICIENCY_MAXIMUM); } @@ -390,7 +408,8 @@ public static float CalculateDigestionEfficiency(int enzymeCount) /// /// Returns the efficiency of all enzymes present in the given organelles. /// - public static Dictionary CalculateDigestionEfficiencies(IEnumerable organelles) + public static Dictionary CalculateDigestionEfficiencies(IEnumerable organelles, + float totalSpecializationBonus) { var enzymes = new Dictionary(); var result = new Dictionary(); @@ -410,11 +429,11 @@ public static Dictionary CalculateDigestionEfficiencies(IEnumerab enzymes[enzyme] = count + 1; } - result[lipase] = CalculateDigestionEfficiency(0); + result[lipase] = CalculateDigestionEfficiency(0, totalSpecializationBonus); foreach (var enzyme in enzymes) { - result[enzyme.Key] = CalculateDigestionEfficiency(enzyme.Value); + result[enzyme.Key] = CalculateDigestionEfficiency(enzyme.Value, totalSpecializationBonus); } return result; @@ -486,31 +505,29 @@ public static void GiveNearNightInitialCompoundBuff(CompoundBag compoundReceiver /// public static Dictionary CalculateDayVaryingCompoundsFillTimes( IReadOnlyList organelles, MembraneType membraneType, bool moving, bool playerSpecies, - BiomeConditions biomeConditions, ResolvedMicrobeTolerances environmentalTolerances, - WorldGenerationSettings worldSettings) + float totalSpecializationBonus, BiomeConditions biomeConditions, + ResolvedMicrobeTolerances environmentalTolerances, WorldGenerationSettings worldSettings) { var energyBalance = new EnergyBalanceInfoSimple(); - // Note this assumes this is only used just for single cell types or microbe species! - var specialization = CalculateSpecializationBonus(organelles, new Dictionary()); - var maximumMovementDirection = MaximumSpeedDirection(organelles); - ProcessSystem.ComputeEnergyBalanceSimple(organelles, biomeConditions, environmentalTolerances, specialization, - membraneType, maximumMovementDirection, moving, playerSpecies, worldSettings, CompoundAmountType.Biome, - null, energyBalance); + ProcessSystem.ComputeEnergyBalanceSimple(organelles, biomeConditions, environmentalTolerances, + totalSpecializationBonus, membraneType, maximumMovementDirection, moving, playerSpecies, + worldSettings, CompoundAmountType.Biome, null, energyBalance); var compoundBalances = new Dictionary(); ProcessSystem.ComputeCompoundBalanceAtEquilibrium(organelles, biomeConditions, environmentalTolerances, - specialization, CompoundAmountType.Biome, energyBalance, compoundBalances); + totalSpecializationBonus, CompoundAmountType.Biome, energyBalance, compoundBalances); // TODO: is it fine to use energy balance calculated with the biome numbers here? var minimums = new Dictionary(); ProcessSystem.ComputeCompoundBalanceAtEquilibrium(organelles, biomeConditions, environmentalTolerances, - specialization, CompoundAmountType.Minimum, energyBalance, minimums); + totalSpecializationBonus, CompoundAmountType.Minimum, energyBalance, minimums); - var cachedCapacities = GetTotalSpecificCapacity(organelles, out var cachedCapacity); + var cachedCapacities = + GetTotalSpecificCapacity(organelles, totalSpecializationBonus, out var cachedCapacity); var result = new Dictionary(); @@ -586,39 +603,38 @@ public static bool UsesDayVaryingCompounds(IReadOnlyCollection public static (bool CanSurvive, Dictionary RequiredStorage) CalculateNightStorageRequirements( IReadOnlyList organelles, MembraneType membraneType, bool moving, bool playerSpecies, - BiomeConditions biomeConditions, ResolvedMicrobeTolerances environmentalTolerances, - WorldGenerationSettings worldSettings, + float totalSpecializationBonus, BiomeConditions biomeConditions, + ResolvedMicrobeTolerances environmentalTolerances, WorldGenerationSettings worldSettings, ref Dictionary? dayCompoundBalances) { - // Note this assumes this is only used just for single cell types or microbe species! - var specialization = CalculateSpecializationBonus(organelles, new Dictionary()); - if (dayCompoundBalances == null) { var energyBalance = new EnergyBalanceInfoSimple(); ProcessSystem.ComputeEnergyBalanceSimple(organelles, biomeConditions, environmentalTolerances, - specialization, membraneType, Vector3.Forward, moving, playerSpecies, worldSettings, + totalSpecializationBonus, membraneType, Vector3.Forward, moving, playerSpecies, worldSettings, CompoundAmountType.Biome, null, energyBalance); dayCompoundBalances = new Dictionary(); ProcessSystem.ComputeCompoundBalanceAtEquilibrium(organelles, biomeConditions, environmentalTolerances, - specialization, CompoundAmountType.Biome, energyBalance, dayCompoundBalances); + totalSpecializationBonus, CompoundAmountType.Biome, energyBalance, dayCompoundBalances); } var energyBalanceAtMinimum = new EnergyBalanceInfoSimple(); - ProcessSystem.ComputeEnergyBalanceSimple(organelles, biomeConditions, environmentalTolerances, specialization, - membraneType, Vector3.Forward, moving, playerSpecies, worldSettings, CompoundAmountType.Minimum, null, + ProcessSystem.ComputeEnergyBalanceSimple(organelles, biomeConditions, environmentalTolerances, + totalSpecializationBonus, membraneType, Vector3.Forward, moving, playerSpecies, + worldSettings, CompoundAmountType.Minimum, null, energyBalanceAtMinimum); var minimums = new Dictionary(); ProcessSystem.ComputeCompoundBalanceAtEquilibrium(organelles, biomeConditions, environmentalTolerances, - specialization, CompoundAmountType.Minimum, energyBalanceAtMinimum, minimums); + totalSpecializationBonus, CompoundAmountType.Minimum, energyBalanceAtMinimum, minimums); - var cachedCapacities = GetTotalSpecificCapacity(organelles, out var cachedCapacity); + var cachedCapacities = + GetTotalSpecificCapacity(organelles, totalSpecializationBonus, out var cachedCapacity); var nightSeconds = worldSettings.DayLength * (1 - worldSettings.DaytimeFraction); @@ -885,7 +901,8 @@ public static int CalculateMostCommonSpecializationOrganelle(IReadOnlyList - /// Calculates a specialization bonus for a cell type based on its organelles. + /// Calculates a specialization bonus for a cell type based on its organelles. In case of a multicellular + /// organism, this will need to be multiplied by the adjacency bonus for most purposes. /// /// A multiplier starting from 1 and going up as specialization improves public static float CalculateSpecializationBonus(IReadOnlyList organelles, diff --git a/src/microbe_stage/MicrobeSpecies.cs b/src/microbe_stage/MicrobeSpecies.cs index 3066763f5d2..efead7af38d 100644 --- a/src/microbe_stage/MicrobeSpecies.cs +++ b/src/microbe_stage/MicrobeSpecies.cs @@ -78,7 +78,8 @@ public Color Colour // Base refers here to the fact that these are the values when a cell is freshly spawned and has no // reproduction progress. public float BaseSpeed => - MicrobeInternalCalculations.CalculateSpeed(Organelles.Organelles, MembraneType, MembraneRigidity, IsBacteria); + MicrobeInternalCalculations.CalculateSpeed(Organelles.Organelles, MembraneType, MembraneRigidity, IsBacteria, + CellTypeSpecializationBonus); public float BaseRotationSpeed { get; set; } @@ -117,7 +118,8 @@ public float BaseHexSize { get { - var specific = MicrobeInternalCalculations.GetTotalSpecificCapacity(Organelles, out var nominal); + var specific = MicrobeInternalCalculations.GetTotalSpecificCapacity(Organelles, + CellTypeSpecializationBonus, out var nominal); return (nominal, specific); } } @@ -142,7 +144,7 @@ public float BaseHexSize /// /// Cached specialization bonus for this species. /// - public float SpecializationBonus { get; set; } + public float CellTypeSpecializationBonus { get; set; } public override Stage StageForDisplay => Stage.MicrobeStage; @@ -183,13 +185,13 @@ public static MicrobeSpecies ReadFromArchive(ISArchiveReader reader, ushort vers if (version > 1) { - instance.SpecializationBonus = reader.ReadFloat(); + instance.CellTypeSpecializationBonus = reader.ReadFloat(); } else { // Assume older microbes won't have specialization for now. And the next editor / auto-evo cycle can sort // them out. - instance.SpecializationBonus = 1; + instance.CellTypeSpecializationBonus = 1; } return instance; @@ -205,7 +207,7 @@ public override void WriteToArchive(ISArchiveWriter writer) writer.WriteObject(Organelles); writer.Write(BaseRotationSpeed); - writer.Write(SpecializationBonus); + writer.Write(CellTypeSpecializationBonus); } public void UpdateIsBacteria() @@ -244,14 +246,12 @@ public override void OnAttemptedInAutoEvo(bool refreshCache) { base.OnAttemptedInAutoEvo(refreshCache); + CellTypeSpecializationBonus = MicrobeInternalCalculations.CalculateSpecializationBonus(Organelles, + new Dictionary()); UpdateInitialCompounds(); UpdateIsBacteria(); cachedFillTimes.Clear(); - - SpecializationBonus = - MicrobeInternalCalculations.CalculateSpecializationBonus(Organelles, - new Dictionary()); } public override bool RepositionToOrigin() @@ -348,7 +348,8 @@ public override void HandleNightSpawnCompounds(CompoundBag targetStorage, ISpawn { // TODO: should moving be false in some cases? compoundTimes = MicrobeInternalCalculations.CalculateDayVaryingCompoundsFillTimes(Organelles, - MembraneType, true, PlayerSpecies, biome, resolvedTolerances, spawnEnvironment.WorldSettings); + MembraneType, true, PlayerSpecies, CellTypeSpecializationBonus, biome, resolvedTolerances, + spawnEnvironment.WorldSettings); cachedFillTimes[biome] = compoundTimes; } } @@ -376,7 +377,7 @@ public override void ApplyMutation(Species mutation) IsBacteria = casted.IsBacteria; MembraneType = casted.MembraneType; MembraneRigidity = casted.MembraneRigidity; - SpecializationBonus = casted.SpecializationBonus; + CellTypeSpecializationBonus = casted.CellTypeSpecializationBonus; cachedFillTimes.Clear(); } @@ -415,7 +416,7 @@ public MicrobeSpecies Clone(bool cloneOrganelles) result.IsBacteria = IsBacteria; result.MembraneType = MembraneType; result.MembraneRigidity = MembraneRigidity; - result.SpecializationBonus = SpecializationBonus; + result.CellTypeSpecializationBonus = CellTypeSpecializationBonus; if (cloneOrganelles) { @@ -484,6 +485,7 @@ protected override Dictionary CalculateTotalReproductionCost() private void CalculateRotationSpeed() { - BaseRotationSpeed = MicrobeInternalCalculations.CalculateRotationSpeed(Organelles.Organelles); + BaseRotationSpeed = MicrobeInternalCalculations.CalculateRotationSpeed(Organelles.Organelles, + CellTypeSpecializationBonus); } } diff --git a/src/microbe_stage/MicrobeStage.cs b/src/microbe_stage/MicrobeStage.cs index be0dfff1f21..1603adcd030 100644 --- a/src/microbe_stage/MicrobeStage.cs +++ b/src/microbe_stage/MicrobeStage.cs @@ -962,13 +962,14 @@ public override void OnReturnFromEditor() earlySpeciesType.MulticellularCellType = earlySpeciesType.Species.ModifiableGameplayCells[0].ModifiableCellType; - environmentalEffects.ApplyEffects(resolvedTolerances, - earlySpeciesType.MulticellularCellType.SpecializationBonus * - earlySpeciesType.Species.GetAdjacencySpecializationBonus(0), ref bioProcesses); + var totalSpecializationBonus = earlySpeciesType.MulticellularCellType.CellTypeSpecializationBonus * + earlySpeciesType.Species.GetAdjacencySpecializationBonus(0); + + environmentalEffects.ApplyEffects(resolvedTolerances, totalSpecializationBonus, ref bioProcesses); cellProperties.ReApplyCellTypeProperties(ref environmentalEffects, Player, - earlySpeciesType.MulticellularCellType, earlySpeciesType.Species, WorldSimulation, workData1, - workData2); + earlySpeciesType.MulticellularCellType, earlySpeciesType.Species, totalSpecializationBonus, + WorldSimulation, workData1, workData2); } else { @@ -977,11 +978,11 @@ public override void OnReturnFromEditor() var resolvedTolerances = MicrobeEnvironmentalToleranceCalculations.ResolveToleranceValues( MicrobeEnvironmentalToleranceCalculations.CalculateTolerances(species.Species, CurrentBiome)); - environmentalEffects.ApplyEffects(resolvedTolerances, species.Species.SpecializationBonus, + environmentalEffects.ApplyEffects(resolvedTolerances, species.Species.CellTypeSpecializationBonus, ref bioProcesses); cellProperties.ReApplyCellTypeProperties(ref environmentalEffects, Player, - species.Species, species.Species, WorldSimulation, + species.Species, species.Species, species.Species.CellTypeSpecializationBonus, WorldSimulation, workData1, workData2); foreach (var organelle in species.Species.Organelles) diff --git a/src/microbe_stage/MicrobeVisualOnlySimulation.cs b/src/microbe_stage/MicrobeVisualOnlySimulation.cs index b1081d90838..a503e5ce2da 100644 --- a/src/microbe_stage/MicrobeVisualOnlySimulation.cs +++ b/src/microbe_stage/MicrobeVisualOnlySimulation.cs @@ -202,10 +202,12 @@ public void ApplyNewVisualisationMicrobeSpecies(Entity microbe, MicrobeSpecies s ProcessSpeedModifier = 1, }; + var dummySpecializationBonus = 1; + // Perform a full update apply with the general code method. ref var cellProperties = ref microbe.Get(); cellProperties.ReApplyCellTypeProperties(ref dummyEffects, microbe, species, - species, this, hexWorkData1, hexWorkData2); + species, dummySpecializationBonus, this, hexWorkData1, hexWorkData2); // TODO: update species member component if species changed? } diff --git a/src/microbe_stage/MicrobeWorldSimulation.cs b/src/microbe_stage/MicrobeWorldSimulation.cs index a2c40ea6c48..4f29ffc3847 100644 --- a/src/microbe_stage/MicrobeWorldSimulation.cs +++ b/src/microbe_stage/MicrobeWorldSimulation.cs @@ -1,4 +1,6 @@ -using Arch.Core; +using System.Collections.Generic; +using Arch.Core; +using Arch.Core.Extensions; using Components; using Godot; using SharedBase.Archive; @@ -11,7 +13,7 @@ /// public partial class MicrobeWorldSimulation : WorldSimulationWithPhysics { - public const ushort SERIALIZATION_VERSION = 1; + public const ushort SERIALIZATION_VERSION = 2; // Base systems private AnimationControlSystem animationControlSystem = null!; @@ -151,6 +153,28 @@ public static MicrobeWorldSimulation ReadFromArchive(ISArchiveReader reader, ush reader.ReadObjectProperties(instance.FluidCurrentsSystem); instance.DeactivateWorldOnReadContext(reader); + + if (version < 2) + { + // Add new SpecializationFactor component to all microbes as that is required to support older saves + + var microbes = new List(); + instance.entities.Query(new QueryDescription().WithAll(), microbes.Add); + + foreach (var microbe in microbes) + { + if (!microbe.Has()) + { + microbe.Add(new SpecializationFactor + { + // We use 1 as a placeholder to not apply any bonuses. Things will get corrected the next time + // the player goes to the editor + TotalSpecializationBonus = 1, + }); + } + } + } + return instance; } diff --git a/src/microbe_stage/Patch.cs b/src/microbe_stage/Patch.cs index c2d60380f84..b8306e25b0e 100644 --- a/src/microbe_stage/Patch.cs +++ b/src/microbe_stage/Patch.cs @@ -597,13 +597,15 @@ public void UpdateCurrentSunlight(float multiplier) /// negative effects. To balance out organelle tolerance debuffs, needs to be given the current organelles. /// /// Set of tolerances that can survive well in the current patch - public EnvironmentalTolerances GenerateTolerancesForMicrobe(IReadOnlyList organelles) + public EnvironmentalTolerances GenerateTolerancesForMicrobe(IReadOnlyList organelles, + float totalSpecializationBonus) { // To guarantee perfect tolerance, we need to apply reverse of the organelle effects so that when the organelle // effects are applied, the final tolerances are well adapted var organelleEffects = default(MicrobeEnvironmentalToleranceCalculations.ToleranceValues); - MicrobeEnvironmentalToleranceCalculations.ApplyOrganelleEffectsOnTolerances(organelles, ref organelleEffects); + MicrobeEnvironmentalToleranceCalculations.ApplyOrganelleEffectsOnTolerances(organelles, + totalSpecializationBonus, ref organelleEffects); var result = GenerateOptimalTolerances(organelleEffects); @@ -611,7 +613,8 @@ public EnvironmentalTolerances GenerateTolerancesForMicrobe(IReadOnlyList 1 + MathUtils.EPSILON) { diff --git a/src/microbe_stage/Spawners.cs b/src/microbe_stage/Spawners.cs index 2172671975d..3abae53d2d5 100644 --- a/src/microbe_stage/Spawners.cs +++ b/src/microbe_stage/Spawners.cs @@ -73,7 +73,7 @@ public static class SpawnHelpers typeof(MicrobeTemporaryEffects), typeof(CollisionManagement), typeof(PhysicsShapeHolder), typeof(MicrobeControl), typeof(ManualPhysicsControl), typeof(MicrobeStatus), typeof(Health), typeof(MicrobeEnvironmentalEffects), typeof(CommandSignaler), typeof(StrainAffected), typeof(CurrentAffected), - typeof(Selectable), typeof(ReadableName), + typeof(Selectable), typeof(ReadableName), typeof(SpecializationFactor), // Player-specific components typeof(PlayerMarker), typeof(SoundListener), typeof(EntityLight), @@ -92,7 +92,7 @@ public static class SpawnHelpers typeof(MicrobeTemporaryEffects), typeof(CollisionManagement), typeof(PhysicsShapeHolder), typeof(MicrobeControl), typeof(ManualPhysicsControl), typeof(MicrobeStatus), typeof(Health), typeof(MicrobeEnvironmentalEffects), typeof(CommandSignaler), typeof(StrainAffected), typeof(CurrentAffected), - typeof(Selectable), typeof(ReadableName), + typeof(Selectable), typeof(ReadableName), typeof(SpecializationFactor), // AI-specific components typeof(MicrobeAI), typeof(SurvivalStatistics), @@ -111,7 +111,7 @@ public static class SpawnHelpers typeof(MicrobeTemporaryEffects), typeof(CollisionManagement), typeof(PhysicsShapeHolder), typeof(MicrobeControl), typeof(ManualPhysicsControl), typeof(MicrobeStatus), typeof(Health), typeof(MicrobeEnvironmentalEffects), typeof(CommandSignaler), typeof(StrainAffected), typeof(CurrentAffected), - typeof(Selectable), typeof(ReadableName), + typeof(Selectable), typeof(ReadableName), typeof(SpecializationFactor), // Player-specific components typeof(PlayerMarker), typeof(SoundListener), typeof(EntityLight), @@ -130,7 +130,7 @@ public static class SpawnHelpers typeof(MicrobeTemporaryEffects), typeof(CollisionManagement), typeof(PhysicsShapeHolder), typeof(MicrobeControl), typeof(ManualPhysicsControl), typeof(MicrobeStatus), typeof(Health), typeof(MicrobeEnvironmentalEffects), typeof(CommandSignaler), typeof(StrainAffected), typeof(CurrentAffected), - typeof(Selectable), typeof(ReadableName), + typeof(Selectable), typeof(ReadableName), typeof(SpecializationFactor), // AI-specific components typeof(MicrobeAI), typeof(SurvivalStatistics), @@ -705,6 +705,9 @@ public static float SpawnMicrobeWithoutFinalizing(IWorldSimulation worldSimulati ProcessStatistics = !aiControlled ? new ProcessStatistics() : null, }; + // Needed for later calculations + float totalSpecializationBonus; + if (species is MulticellularSpecies multicellularSpecies) { var multicellularTolerances = spawnEnvironment.GetSpeciesTolerances(multicellularSpecies); @@ -728,10 +731,15 @@ public static float SpawnMicrobeWithoutFinalizing(IWorldSimulation worldSimulati membraneType = properties.MembraneType; recorder.Set(entity, properties); - environmentalEffects.ApplyEffects(multicellularTolerances, - resolvedCellType.SpecializationBonus * - multicellularSpecies.GetAdjacencySpecializationBonus(multicellularData.CellBodyPlanIndex), - ref bioProcesses); + totalSpecializationBonus = resolvedCellType.CellTypeSpecializationBonus * + multicellularSpecies.GetAdjacencySpecializationBonus(multicellularData.CellBodyPlanIndex); + + recorder.Set(entity, new SpecializationFactor + { + TotalSpecializationBonus = totalSpecializationBonus, + }); + + environmentalEffects.ApplyEffects(multicellularTolerances, totalSpecializationBonus, ref bioProcesses); // TODO: should this also be given MulticellularGrowth to allow this to grow fully if the colony splits } @@ -754,10 +762,15 @@ public static float SpawnMicrobeWithoutFinalizing(IWorldSimulation worldSimulati // this one more variable) recorder.Add(entity, new MulticellularGrowth(multicellularSpecies)); - environmentalEffects.ApplyEffects(multicellularTolerances, - resolvedCellType.SpecializationBonus * - multicellularSpecies.GetAdjacencySpecializationBonus(multicellularData.CellBodyPlanIndex), - ref bioProcesses); + totalSpecializationBonus = resolvedCellType.CellTypeSpecializationBonus * + multicellularSpecies.GetAdjacencySpecializationBonus(multicellularData.CellBodyPlanIndex); + + recorder.Set(entity, new SpecializationFactor + { + TotalSpecializationBonus = totalSpecializationBonus, + }); + + environmentalEffects.ApplyEffects(multicellularTolerances, totalSpecializationBonus, ref bioProcesses); } #if DEBUG @@ -772,9 +785,6 @@ public static float SpawnMicrobeWithoutFinalizing(IWorldSimulation worldSimulati } else if (species is MicrobeSpecies microbeSpecies) { - environmentalEffects.ApplyEffects(spawnEnvironment.GetSpeciesTolerances(microbeSpecies), - microbeSpecies.SpecializationBonus, ref bioProcesses); - recorder.Set(entity, new MicrobeSpeciesMember { Species = microbeSpecies, @@ -785,6 +795,17 @@ public static float SpawnMicrobeWithoutFinalizing(IWorldSimulation worldSimulati membraneType = properties.MembraneType; recorder.Set(entity, properties); + // No cells to be adjacent to, so total specialization = cell type specialization + totalSpecializationBonus = usedCellDefinition.CellTypeSpecializationBonus; + + recorder.Set(entity, new SpecializationFactor + { + TotalSpecializationBonus = totalSpecializationBonus, + }); + + environmentalEffects.ApplyEffects(spawnEnvironment.GetSpeciesTolerances(microbeSpecies), + totalSpecializationBonus, ref bioProcesses); + if (multicellularData.MulticellularCellType != null) GD.PrintErr("Multicellular cell type may not be set when spawning a MicrobeSpecies instance"); } @@ -818,7 +839,7 @@ public static float SpawnMicrobeWithoutFinalizing(IWorldSimulation worldSimulati // Run the storage update logic for the first time (to ensure consistency with later updates) // This has to be called as CreateOrganelleLayout doesn't do this automatically - container.UpdateCompoundBagStorageFromOrganelles(ref storage); + container.UpdateCompoundBagStorageFromOrganelles(ref storage, totalSpecializationBonus); var engulfable = new Engulfable(PhagocytosisPhase.None, Entity.Null) { diff --git a/src/microbe_stage/components/CellProperties.cs b/src/microbe_stage/components/CellProperties.cs index 7f0b6b7e256..35c0a21f9e8 100644 --- a/src/microbe_stage/components/CellProperties.cs +++ b/src/microbe_stage/components/CellProperties.cs @@ -481,6 +481,9 @@ public static Vector3 CalculateExternalOrganellePosition(this ref CellProperties /// ( applies instead). Note if species object instance changes from what it /// was before, the code calling this method must do that adjustment manually. /// + /// + /// The specialization bonus the cell should use, including any adjacency effects. + /// /// /// Needed when resetting multicellular growth as that needs to delete colony cells /// @@ -488,8 +491,8 @@ public static Vector3 CalculateExternalOrganellePosition(this ref CellProperties /// More temporary memory public static void ReApplyCellTypeProperties(this ref CellProperties cellProperties, ref readonly MicrobeEnvironmentalEffects environmentalEffects, in Entity entity, - ICellDefinition newDefinition, Species baseReproductionCostFrom, IWorldSimulation worldSimulation, - List workMemory1, List workMemory2) + ICellDefinition newDefinition, Species baseReproductionCostFrom, float totalSpecializationBonus, + IWorldSimulation worldSimulation, List workMemory1, List workMemory2) { // Copy new cell type properties cellProperties.MembraneType = newDefinition.MembraneType; @@ -513,11 +516,15 @@ public static void ReApplyCellTypeProperties(this ref CellProperties cellPropert ref var organelleContainer = ref entity.Get(); + // Reset Specialization factor to the one in new species' data + ref var specialization = ref entity.Get(); + specialization.TotalSpecializationBonus = totalSpecializationBonus; + // Reset all the duplicate organelles / reproduction progress of the entity // This also resets multicellular creature's reproduction progress organelleContainer.ResetOrganelleLayout(ref entity.Get(), ref entity.Get(), - in environmentalEffects, entity, newDefinition, baseReproductionCostFrom, worldSimulation, workMemory1, - workMemory2); + ref specialization, in environmentalEffects, entity, newDefinition, baseReproductionCostFrom, + worldSimulation, workMemory1, workMemory2); // Reset runtime colour if (entity.Has()) diff --git a/src/microbe_stage/components/MicrobeColony.cs b/src/microbe_stage/components/MicrobeColony.cs index be2d2492541..75535309fb3 100644 --- a/src/microbe_stage/components/MicrobeColony.cs +++ b/src/microbe_stage/components/MicrobeColony.cs @@ -1002,7 +1002,8 @@ public static void CalculateRotationSpeed(this ref MicrobeColony colony) // When changing this method's logic also update the corresponding method in CellBodyPlanInternalCalculations float colonyRotation = MicrobeInternalCalculations - .CalculateRotationSpeed(colony.Leader.Get().Organelles!.Organelles); + .CalculateRotationSpeed(colony.Leader.Get().Organelles!.Organelles, + colony.Leader.Get().TotalSpecializationBonus); foreach (var colonyMember in colony.ColonyMembers) { @@ -1023,7 +1024,8 @@ public static void CalculateRotationSpeed(this ref MicrobeColony colony) // This relies on the bounding of the cell rotation, as a colony can never be faster than the // fastest cell inside it var memberRotation = MicrobeInternalCalculations - .CalculateRotationSpeed(colonyMember.Get().Organelles!.Organelles) + .CalculateRotationSpeed(colonyMember.Get().Organelles!.Organelles, + colonyMember.Get().TotalSpecializationBonus) * (1 + 0.007f * distanceSquared); colonyRotation += memberRotation; diff --git a/src/microbe_stage/components/OrganelleContainer.cs b/src/microbe_stage/components/OrganelleContainer.cs index 363910807a8..ca152d31ae2 100644 --- a/src/microbe_stage/components/OrganelleContainer.cs +++ b/src/microbe_stage/components/OrganelleContainer.cs @@ -349,7 +349,7 @@ public static void CreateOrganelleLayout(this ref OrganelleContainer container, }, workMemory1, workMemory2); } - container.CalculateOrganelleLayoutStatistics(); + container.CalculateOrganelleLayoutStatistics(cellDefinition.CellTypeSpecializationBonus); container.AllOrganellesDivided = false; @@ -369,9 +369,9 @@ public static void CreateOrganelleLayout(this ref OrganelleContainer container, /// public static void ResetOrganelleLayout(this ref OrganelleContainer container, ref CompoundStorage storageToUpdate, ref BioProcesses bioProcessesToUpdate, - ref readonly MicrobeEnvironmentalEffects effectsToRead, in Entity entity, - ICellDefinition cellDefinition, Species baseReproductionCostFrom, IWorldSimulation worldSimulation, - List workMemory1, List workMemory2) + ref SpecializationFactor specializationFactor, ref readonly MicrobeEnvironmentalEffects effectsToRead, + in Entity entity, ICellDefinition cellDefinition, Species baseReproductionCostFrom, + IWorldSimulation worldSimulation, List workMemory1, List workMemory2) { container.CreateOrganelleLayout(cellDefinition, workMemory1, workMemory2); container.UpdateEngulfingSizeData(ref entity.Get(), ref entity.Get(), @@ -421,7 +421,8 @@ public static void ResetOrganelleLayout(this ref OrganelleContainer container, growth.ResetMulticellularProgress(entity, worldSimulation); } - container.UpdateCompoundBagStorageFromOrganelles(ref storageToUpdate); + container.UpdateCompoundBagStorageFromOrganelles(ref storageToUpdate, + specializationFactor.TotalSpecializationBonus); container.RecalculateOrganelleBioProcesses(ref bioProcessesToUpdate); @@ -442,14 +443,17 @@ public static void ResetOrganelleLayout(this ref OrganelleContainer container, /// public static void OnOrganellesChanged(this ref OrganelleContainer container, ref CompoundStorage storage, ref BioProcesses bioProcesses, ref Engulfer engulfer, ref Engulfable engulfable, - ref CellProperties cellProperties) + ref CellProperties cellProperties, ref SpecializationFactor specializationFactor) { container.OrganelleVisualsCreated = false; container.OrganelleComponentsCached = false; - container.CalculateOrganelleLayoutStatistics(); + // This includes any relevant adjacency effects + var totalSpecializationBonus = specializationFactor.TotalSpecializationBonus; + + container.CalculateOrganelleLayoutStatistics(totalSpecializationBonus); container.UpdateEngulfingSizeData(ref engulfer, ref engulfable, cellProperties.IsBacteria); - container.UpdateCompoundBagStorageFromOrganelles(ref storage); + container.UpdateCompoundBagStorageFromOrganelles(ref storage, totalSpecializationBonus); container.RecalculateOrganelleBioProcesses(ref bioProcesses); } @@ -557,7 +561,8 @@ public static void RecalculateOrganelleBioProcesses(this ref OrganelleContainer return detections; } - public static void CalculateOrganelleLayoutStatistics(this ref OrganelleContainer container) + public static void CalculateOrganelleLayoutStatistics(this ref OrganelleContainer container, + float totalSpecializationBonus) { if (container.AvailableEnzymes == null) { @@ -682,7 +687,7 @@ public static void CalculateOrganelleLayoutStatistics(this ref OrganelleContaine container.OrganellesCapacity += MicrobeInternalCalculations.GetNominalCapacityForOrganelle(organelleDefinition, - organelle.Upgrades); + organelle.Upgrades, totalSpecializationBonus); var enzymes = organelle.GetEnzymes(); @@ -732,8 +737,9 @@ public static void UpdateEngulfingSizeData(this ref OrganelleContainer container /// /// Organelle data /// Target compound storage to update + /// Bonus modifier from cell specialization, plus adjacency effects public static void UpdateCompoundBagStorageFromOrganelles(this ref OrganelleContainer container, - ref CompoundStorage compoundStorage) + ref CompoundStorage compoundStorage, float totalSpecializationBonus) { if (container.Organelles == null) throw new InvalidOperationException("Organelle list needs to be initialized first"); @@ -742,7 +748,8 @@ public static void UpdateCompoundBagStorageFromOrganelles(this ref OrganelleCont compounds.NominalCapacity = container.OrganellesCapacity; - MicrobeInternalCalculations.UpdateSpecificCapacities(compounds, container.Organelles.Organelles); + MicrobeInternalCalculations.UpdateSpecificCapacities(compounds, container.Organelles.Organelles, + totalSpecializationBonus); } /// diff --git a/src/microbe_stage/components/SpecializationFactor.cs b/src/microbe_stage/components/SpecializationFactor.cs new file mode 100644 index 00000000000..7ce6b7e9538 --- /dev/null +++ b/src/microbe_stage/components/SpecializationFactor.cs @@ -0,0 +1,36 @@ +namespace Components; + +using SharedBase.Archive; + +[ComponentIsReadByDefault] +public struct SpecializationFactor : IArchivableComponent +{ + public const ushort SERIALIZATION_VERSION = 1; + + /// + /// Total applied specialization bonus for this specific cell, including any adjacency effects. + /// + public float TotalSpecializationBonus; + + public ushort CurrentArchiveVersion => SERIALIZATION_VERSION; + public ThriveArchiveObjectType ArchiveObjectType => ThriveArchiveObjectType.ComponentSpecializationFactor; + + public void WriteToArchive(ISArchiveWriter writer) + { + writer.Write(TotalSpecializationBonus); + } +} + +public static class SpecializationFactorHelpers +{ + public static SpecializationFactor ReadFromArchive(ISArchiveReader reader, ushort version) + { + if (version is > SpecializationFactor.SERIALIZATION_VERSION or <= 0) + throw new InvalidArchiveVersionException(version, SpecializationFactor.SERIALIZATION_VERSION); + + return new SpecializationFactor + { + TotalSpecializationBonus = reader.ReadFloat(), + }; + } +} diff --git a/src/microbe_stage/editor/CellEditorComponent.cs b/src/microbe_stage/editor/CellEditorComponent.cs index 271beba5a9d..cf4725e3908 100644 --- a/src/microbe_stage/editor/CellEditorComponent.cs +++ b/src/microbe_stage/editor/CellEditorComponent.cs @@ -1062,7 +1062,15 @@ public void OnFinishEditing(bool shouldUpdatePosition) } if (shouldUpdatePosition) + { editedProperties.RepositionToOrigin(); + } + else + { + // Even if not repositioning to origin, we still need to update this + editedProperties.CellTypeSpecializationBonus = MicrobeInternalCalculations.CalculateSpecializationBonus( + editedProperties.ModifiableOrganelles, new Dictionary()); + } // Update bacteria status editedProperties.IsBacteria = !HasNucleus; @@ -1300,8 +1308,12 @@ public ToleranceResult CalculateRawTolerances(bool excludePositiveBuffs = false) "In multicellular, the cell editor is not responsible for tolerances data"); } + // Treats cellTypeSpecializationBonus as totalSpecializationBonus, because adjacency is ignored in this editor. + var specialization = MicrobeInternalCalculations.CalculateSpecializationBonus( + editedMicrobeOrganelles.Organelles, tempMemory3); + return MicrobeEnvironmentalToleranceCalculations.CalculateTolerances(tolerancesEditor.CurrentTolerances, - editedMicrobeOrganelles, Editor.CurrentPatch.Biome, excludePositiveBuffs); + editedMicrobeOrganelles, specialization, Editor.CurrentPatch.Biome, excludePositiveBuffs); } public void UpdatePatchDependentBalanceData() @@ -1510,13 +1522,21 @@ public override void OnValidAction(IEnumerable actions) public float CalculateSpeed() { + // Treats cellTypeSpecializationBonus as totalSpecializationBonus, because adjacency is ignored in this editor. + var specialization = MicrobeInternalCalculations.CalculateSpecializationBonus( + editedMicrobeOrganelles.Organelles, tempMemory3); + return MicrobeInternalCalculations.CalculateSpeed(editedMicrobeOrganelles.Organelles, Membrane, Rigidity, - !HasNucleus); + !HasNucleus, specialization); } public float CalculateRotationSpeed() { - return MicrobeInternalCalculations.CalculateRotationSpeed(editedMicrobeOrganelles.Organelles); + // Treats cellTypeSpecializationBonus as totalSpecializationBonus, because adjacency is ignored in this editor. + var specialization = MicrobeInternalCalculations.CalculateSpecializationBonus( + editedMicrobeOrganelles.Organelles, tempMemory3); + + return MicrobeInternalCalculations.CalculateRotationSpeed(editedMicrobeOrganelles.Organelles, specialization); } public float CalculateHitpoints() @@ -1526,17 +1546,31 @@ public float CalculateHitpoints() public Dictionary GetAdditionalCapacities(out float nominalCapacity) { - return MicrobeInternalCalculations.GetTotalSpecificCapacity(editedMicrobeOrganelles, out nominalCapacity); + // Treats cellTypeSpecializationBonus as totalSpecializationBonus, because adjacency is ignored in this editor. + var totalSpecializationBonus = + MicrobeInternalCalculations.CalculateSpecializationBonus(editedMicrobeOrganelles.Organelles, tempMemory3); + + return MicrobeInternalCalculations.GetTotalSpecificCapacity(editedMicrobeOrganelles, + totalSpecializationBonus, out nominalCapacity); } public float CalculateTotalDigestionSpeed() { - return MicrobeInternalCalculations.CalculateTotalDigestionSpeed(editedMicrobeOrganelles); + // Treats cellTypeSpecializationBonus as totalSpecializationBonus, because adjacency is ignored in this editor. + var totalSpecializationBonus = + MicrobeInternalCalculations.CalculateSpecializationBonus(editedMicrobeOrganelles.Organelles, tempMemory3); + + return MicrobeInternalCalculations.CalculateTotalDigestionSpeed(editedMicrobeOrganelles, + totalSpecializationBonus); } public Dictionary CalculateDigestionEfficiencies() { - return MicrobeInternalCalculations.CalculateDigestionEfficiencies(editedMicrobeOrganelles); + // Treats cellTypeSpecializationBonus as totalSpecializationBonus, because adjacency is ignored in this editor. + var specialization = + MicrobeInternalCalculations.CalculateSpecializationBonus(editedMicrobeOrganelles.Organelles, tempMemory3); + + return MicrobeInternalCalculations.CalculateDigestionEfficiencies(editedMicrobeOrganelles, specialization); } public (int AmmoniaCost, int PhosphatesCost) CalculateOrganellesCosts() @@ -1832,7 +1866,7 @@ private bool CreatePreviewMicrobeIfNeeded() IsBacteria = false, // Doesn't matter for visualization, but we want to set a valid value - SpecializationBonus = 1, + CellTypeSpecializationBonus = 1, }; previewMicrobe = previewSimulation.CreateVisualisationMicrobe(previewMicrobeSpecies); @@ -2149,6 +2183,7 @@ private void CalculateEnergyAndCompoundBalance(IReadOnlyList var maximumMovementDirection = MicrobeInternalCalculations.MaximumSpeedDirection(organelles); + // Treats cellTypeSpecializationBonus as totalSpecializationBonus, because adjacency is ignored in this editor. var specialization = MicrobeInternalCalculations.CalculateSpecializationBonus(organelles, tempMemory3); var tolerances = CalculateLatestTolerances(); @@ -2214,7 +2249,8 @@ private Dictionary CalculateCompoundBalanceWithMethod goto case BalanceDisplayType.EnergyEquilibrium; } - specificStorages ??= MicrobeInternalCalculations.GetTotalSpecificCapacity(organelles, out nominalStorage); + specificStorages ??= MicrobeInternalCalculations.GetTotalSpecificCapacity(organelles, + specializationBonus, out nominalStorage); return ProcessSystem.ComputeCompoundFillTimes(compoundBalanceData, nominalStorage, specificStorages); } @@ -2967,6 +3003,10 @@ private void CopyEditedPropertiesToSpecies(MicrobeSpecies target) target.ModifiableBehaviour = ((IReadOnlyBehaviourDictionary)overwriteBehaviourForCalculations).Clone(); } + target.CellTypeSpecializationBonus = + MicrobeInternalCalculations.CalculateSpecializationBonus(target.Organelles, + new Dictionary()); + // Copy tolerances target.ModifiableTolerances.CopyFrom(tolerancesEditor.CurrentTolerances); } @@ -3432,6 +3472,9 @@ public OrganelleSuggestionCalculation(MicrobeSpecies initialSpeciesToCopy, { pristineSpeciesCopy = initialSpeciesToCopy; calculationSpecies = initialSpeciesToCopy.Clone(true); + calculationSpecies.CellTypeSpecializationBonus = + MicrobeInternalCalculations.CalculateSpecializationBonus(initialSpeciesToCopy.Organelles, + new Dictionary()); this.applyLatestEditsToSpecies = applyLatestEditsToSpecies; this.currentGameProperties = currentGameProperties; editorOpenedForSpecies = editedSpecies; diff --git a/src/microbe_stage/editor/MicrobeEditor.cs b/src/microbe_stage/editor/MicrobeEditor.cs index cc2254e3716..b8077525bf5 100644 --- a/src/microbe_stage/editor/MicrobeEditor.cs +++ b/src/microbe_stage/editor/MicrobeEditor.cs @@ -238,13 +238,23 @@ public void OnTolerancesChanged(EnvironmentalTolerances newTolerances) public EnvironmentalTolerances GetOptimalTolerancesForCurrentPatch() { - return CurrentPatch.GenerateTolerancesForMicrobe(EditedCellOrganelles); + // This is a microbe, so totalSpecializationBonus = cell specialization bonus + var totalSpecializationBonus = + MicrobeInternalCalculations.CalculateSpecializationBonus(EditedCellOrganelles, + tempMemory1); + + return CurrentPatch.GenerateTolerancesForMicrobe(EditedCellOrganelles, totalSpecializationBonus); } public ToleranceResult CalculateCurrentTolerances(EnvironmentalTolerances calculationTolerances) { + // This is a microbe, so totalSpecializationBonus = cell specialization bonus + var totalSpecializationBonus = + MicrobeInternalCalculations.CalculateSpecializationBonus(EditedCellOrganelles, + tempMemory1); + return MicrobeEnvironmentalToleranceCalculations.CalculateTolerances(calculationTolerances, - EditedCellOrganelles, CurrentPatch.Biome); + EditedCellOrganelles, totalSpecializationBonus, CurrentPatch.Biome); } public void GetCurrentToleranceSummaryByElement(ToleranceModifier toleranceCategory, @@ -257,8 +267,13 @@ public void GetCurrentToleranceSummaryByElement(ToleranceModifier toleranceCateg public void CalculateBodyEffectOnTolerances( ref MicrobeEnvironmentalToleranceCalculations.ToleranceValues modifiedTolerances) { + // This is a microbe, so totalSpecializationBonus = cell specialization bonus + var totalSpecializationBonus = + MicrobeInternalCalculations.CalculateSpecializationBonus(EditedCellOrganelles, + tempMemory1); + MicrobeEnvironmentalToleranceCalculations.ApplyOrganelleEffectsOnTolerances(EditedCellOrganelles, - ref modifiedTolerances); + totalSpecializationBonus, ref modifiedTolerances); } protected override void ResolveDerivedTypeNodeReferences() @@ -661,7 +676,8 @@ public float Speed return MicrobeInternalCalculations.SpeedToUserReadableNumber(MicrobeInternalCalculations.CalculateSpeed( CellDefinition.ModifiableOrganelles.Organelles, CellDefinition.MembraneType, - CellDefinition.MembraneRigidity, CellDefinition.IsBacteria)); + CellDefinition.MembraneRigidity, CellDefinition.IsBacteria, + CellDefinition.CellTypeSpecializationBonus)); } } } diff --git a/src/microbe_stage/editor/TolerancesEditorSubComponent.cs b/src/microbe_stage/editor/TolerancesEditorSubComponent.cs index 25acdd0ccc4..2ec361fcd45 100644 --- a/src/microbe_stage/editor/TolerancesEditorSubComponent.cs +++ b/src/microbe_stage/editor/TolerancesEditorSubComponent.cs @@ -338,9 +338,15 @@ public void UpdateToolTipStats() if (enableMicrobeDebugCode) { tempTolerances.CopyFrom(optimal); + + // Note this assumes this is only used just for single cell types or microbe species! + var specialization = MicrobeInternalCalculations.CalculateSpecializationBonus( + Editor.EditedCellOrganelles ?? throw new Exception("Organelles not set"), + new Dictionary()); + var optimalTest = MicrobeEnvironmentalToleranceCalculations.CalculateTolerances(tempTolerances, - Editor.EditedCellOrganelles ?? throw new Exception("Organelles not set"), + Editor.EditedCellOrganelles, specialization, Editor.CurrentPatch.Biome); if (optimalTest.OverallScore is < 1 or > 1 + MathUtils.EPSILON) diff --git a/src/microbe_stage/organelle_components/BioluminescenceComponent.cs b/src/microbe_stage/organelle_components/BioluminescenceComponent.cs index 879108a6205..a7df0c30acd 100644 --- a/src/microbe_stage/organelle_components/BioluminescenceComponent.cs +++ b/src/microbe_stage/organelle_components/BioluminescenceComponent.cs @@ -50,8 +50,8 @@ public void OnAttachToCell(PlacedOrganelle organelle) parentOrganelle = organelle; } - public void UpdateAsync(ref OrganelleContainer organelleContainer, in Entity microbeEntity, - IWorldSimulation worldSimulation, float energycostMultiplier, float delta) + public void UpdateAsync(ref OrganelleContainer organelleContainer, ref SpecializationFactor specializationFactor, + in Entity microbeEntity, IWorldSimulation worldSimulation, float energyCostMultiplier, float delta) { // Drain luciferase var compounds = microbeEntity.Get().Compounds; diff --git a/src/microbe_stage/organelle_components/ChemoreceptorComponent.cs b/src/microbe_stage/organelle_components/ChemoreceptorComponent.cs index 3f3d62cc719..2cadec96cc2 100644 --- a/src/microbe_stage/organelle_components/ChemoreceptorComponent.cs +++ b/src/microbe_stage/organelle_components/ChemoreceptorComponent.cs @@ -36,8 +36,8 @@ public void OnAttachToCell(PlacedOrganelle organelle) GD.PrintErr("Chemoreceptor has no target compound or species, invalid configuration"); } - public void UpdateAsync(ref OrganelleContainer organelleContainer, in Entity microbeEntity, - IWorldSimulation worldSimulation, float energycostMultiplier, float delta) + public void UpdateAsync(ref OrganelleContainer organelleContainer, ref SpecializationFactor specializationFactor, + in Entity microbeEntity, IWorldSimulation worldSimulation, float energyCostMultiplier, float delta) { if (targetCompound != Compound.Invalid) { diff --git a/src/microbe_stage/organelle_components/CiliaComponent.cs b/src/microbe_stage/organelle_components/CiliaComponent.cs index 62597b64080..b41e9bbefe4 100644 --- a/src/microbe_stage/organelle_components/CiliaComponent.cs +++ b/src/microbe_stage/organelle_components/CiliaComponent.cs @@ -38,8 +38,8 @@ public void OnAttachToCell(PlacedOrganelle organelle) usesPullingCilia = true; } - public void UpdateAsync(ref OrganelleContainer organelleContainer, in Entity microbeEntity, - IWorldSimulation worldSimulation, float energycostMultiplier, float delta) + public void UpdateAsync(ref OrganelleContainer organelleContainer, ref SpecializationFactor specializationFactor, + in Entity microbeEntity, IWorldSimulation worldSimulation, float energyCostMultiplier, float delta) { // Stop animating when being engulfed if (microbeEntity.Get().PhagocytosisStep != PhagocytosisPhase.None) @@ -80,7 +80,8 @@ public void UpdateAsync(ref OrganelleContainer organelleContainer, in Entity mic // Update pulling cilia state and logic. This is fine to be rate limited here as we take that into // account in the impulse size calculation. - UpdatePullingCilia(ref organelleContainer, microbeEntity, worldSimulation, timeSinceRotationSample); + UpdatePullingCilia(ref organelleContainer, ref specializationFactor, microbeEntity, worldSimulation, + timeSinceRotationSample); } else { @@ -101,7 +102,7 @@ public void UpdateAsync(ref OrganelleContainer organelleContainer, in Entity mic var requiredEnergy = cost * timeSinceRotationSample; - requiredEnergy *= energycostMultiplier; + requiredEnergy *= energyCostMultiplier; var compounds = microbeEntity.Get().Compounds; @@ -138,7 +139,8 @@ private void SetSpeedFactor(float speed) animationDirty = true; } - private void UpdatePullingCilia(ref OrganelleContainer organelleContainer, in Entity microbeEntity, + private void UpdatePullingCilia(ref OrganelleContainer organelleContainer, + ref SpecializationFactor specializationFactor, in Entity microbeEntity, IWorldSimulation worldSimulation, float delta) { if (sharedPullData == null) @@ -193,6 +195,9 @@ private void UpdatePullingCilia(ref OrganelleContainer organelleContainer, in En float ciliaCountForForce = sharedPullData.CiliaCount; sharedPullData.CiliaCount = 1; + // Cell specialization bonus is applied in several places + var totalSpecializationBonus = specializationFactor.TotalSpecializationBonus; + if (!microbeEntity.Has()) { // Cilia initialization @@ -271,8 +276,8 @@ private void UpdatePullingCilia(ref OrganelleContainer organelleContainer, in En if (distance < MathUtils.EPSILON) return; - float force = Constants.CILIA_PULLING_FORCE * ciliaCountForForce * delta / - MathF.Max(1.0f, distance * Constants.CILIA_PULLING_FORCE_FALLOFF_FACTOR); + float force = Constants.CILIA_PULLING_FORCE * ciliaCountForForce * totalSpecializationBonus * delta + / MathF.Max(1.0f, distance * Constants.CILIA_PULLING_FORCE_FALLOFF_FACTOR); ref var targetPhysics = ref pulledEntity.Get(); targetPhysics.ImpulseToGive += @@ -332,8 +337,8 @@ private PhysicsShape CreateCiliaDetectorShape(int count) sharedPullData.SizeCreatedWithCilia = count; // TODO: this will need a size parameter if cilia can ever be placed on prokaryotes - return PhysicsShape.CreateSphere(Constants.CILIA_PULLING_FORCE_FIELD_RADIUS + - count * Constants.CILIA_PULL_RADIUS_PER_CILIA); + return PhysicsShape.CreateSphere(Constants.CILIA_PULLING_FORCE_FIELD_RADIUS + count + * Constants.CILIA_PULL_RADIUS_PER_CILIA); } /// diff --git a/src/microbe_stage/organelle_components/EmptyOrganelleComponent.cs b/src/microbe_stage/organelle_components/EmptyOrganelleComponent.cs index aac0367cfda..1950165f762 100644 --- a/src/microbe_stage/organelle_components/EmptyOrganelleComponent.cs +++ b/src/microbe_stage/organelle_components/EmptyOrganelleComponent.cs @@ -14,8 +14,8 @@ public void OnAttachToCell(PlacedOrganelle organelle) { } - public void UpdateAsync(ref OrganelleContainer organelleContainer, in Entity microbeEntity, - IWorldSimulation worldSimulation, float energycostMultiplier, float delta) + public void UpdateAsync(ref OrganelleContainer organelleContainer, ref SpecializationFactor specializationFactor, + in Entity microbeEntity, IWorldSimulation worldSimulation, float energyCostMultiplier, float delta) { } diff --git a/src/microbe_stage/organelle_components/LysosomeComponent.cs b/src/microbe_stage/organelle_components/LysosomeComponent.cs index eee148415f2..63977109d91 100644 --- a/src/microbe_stage/organelle_components/LysosomeComponent.cs +++ b/src/microbe_stage/organelle_components/LysosomeComponent.cs @@ -54,8 +54,8 @@ public void OnAttachToCell(PlacedOrganelle organelle) organelle.OverriddenEnzymes = enzymes; } - public void UpdateAsync(ref OrganelleContainer organelleContainer, in Entity microbeEntity, - IWorldSimulation worldSimulation, float energycostMultiplier, float delta) + public void UpdateAsync(ref OrganelleContainer organelleContainer, ref SpecializationFactor specializationFactor, + in Entity microbeEntity, IWorldSimulation worldSimulation, float energyCostMultiplier, float delta) { // TODO: Animate lysosomes sticking onto phagosomes (if possible). This probably should happen in the // engulfing system (this at least can't happen here as Godot data update needs to happen in sync update) diff --git a/src/microbe_stage/organelle_components/MovementComponent.cs b/src/microbe_stage/organelle_components/MovementComponent.cs index 3393b93af2f..dfa6b5e81fd 100644 --- a/src/microbe_stage/organelle_components/MovementComponent.cs +++ b/src/microbe_stage/organelle_components/MovementComponent.cs @@ -51,8 +51,8 @@ public void OnAttachToCell(PlacedOrganelle organelle) force = CalculateForce(organelle.Position, momentum); } - public void UpdateAsync(ref OrganelleContainer organelleContainer, in Entity microbeEntity, - IWorldSimulation worldSimulation, float energyCostMultiplier, float delta) + public void UpdateAsync(ref OrganelleContainer organelleContainer, ref SpecializationFactor specializationFactor, + in Entity microbeEntity, IWorldSimulation worldSimulation, float energyCostMultiplier, float delta) { // Stop animating when being engulfed if (microbeEntity.Get().PhagocytosisStep != PhagocytosisPhase.None) diff --git a/src/microbe_stage/organelle_components/SlimeJetComponent.cs b/src/microbe_stage/organelle_components/SlimeJetComponent.cs index 97cf40e4017..856ee98bb41 100644 --- a/src/microbe_stage/organelle_components/SlimeJetComponent.cs +++ b/src/microbe_stage/organelle_components/SlimeJetComponent.cs @@ -53,8 +53,8 @@ public void OnAttachToCell(PlacedOrganelle organelle) IsMucocyst = true; } - public void UpdateAsync(ref OrganelleContainer organelleContainer, in Entity microbeEntity, - IWorldSimulation worldSimulation, float energycostMultiplier, float delta) + public void UpdateAsync(ref OrganelleContainer organelleContainer, ref SpecializationFactor specializationFactor, + in Entity microbeEntity, IWorldSimulation worldSimulation, float energyCostMultiplier, float delta) { // All the logic for this ended up in MicrobeEmissionSystem and MicrobeMovementSystem, just the animation // applying is here anymore... diff --git a/src/microbe_stage/systems/EndosymbiontOrganelleSystem.cs b/src/microbe_stage/systems/EndosymbiontOrganelleSystem.cs index 2c52d5e23ed..905bc8767b7 100644 --- a/src/microbe_stage/systems/EndosymbiontOrganelleSystem.cs +++ b/src/microbe_stage/systems/EndosymbiontOrganelleSystem.cs @@ -15,6 +15,7 @@ /// [ReadsComponent(typeof(SpeciesMember))] [ReadsComponent(typeof(CellProperties))] +[ReadsComponent(typeof(SpecializationFactor))] [RunsBefore(typeof(MicrobeReproductionSystem))] [RunsBefore(typeof(MicrobePhysicsCreationAndSizeSystem))] [RunsBefore(typeof(MicrobeVisualsSystem))] @@ -82,7 +83,7 @@ private void Update(ref TemporaryEndosymbiontInfo endosymbiontInfo, ref Organell // times and when not empty, mostly just once organelleContainer.OnOrganellesChanged(ref entity.Get(), ref entity.Get(), ref entity.Get(), ref entity.Get(), - ref entity.Get()); + ref entity.Get(), ref entity.Get()); endosymbiontInfo.CreatedOrganelleInstancesFor.Add(symbiontSpecies); } diff --git a/src/microbe_stage/systems/EngulfedDigestionSystem.cs b/src/microbe_stage/systems/EngulfedDigestionSystem.cs index 8cfddd7c7df..ee947756847 100644 --- a/src/microbe_stage/systems/EngulfedDigestionSystem.cs +++ b/src/microbe_stage/systems/EngulfedDigestionSystem.cs @@ -29,6 +29,7 @@ [ReadsComponent(typeof(WorldPosition))] [ReadsComponent(typeof(MicrobeEventCallbacks))] [ReadsComponent(typeof(SpeciesMember))] +[ReadsComponent(typeof(SpecializationFactor))] [RunsAfter(typeof(EngulfingSystem))] [RuntimeCost(2)] public partial class EngulfedDigestionSystem : BaseSystem @@ -101,6 +102,8 @@ private void HandleDigestion(in Entity entity, ref Engulfer engulfer, ref Organe ref var cellProperties = ref entity.Get(); ref var position = ref entity.Get(); + var totalSpecializationBonus = entity.Get().TotalSpecializationBonus; + for (int i = engulfer.EngulfedObjects!.Count - 1; i >= 0; --i) { var engulfedObject = engulfer.EngulfedObjects![i]; @@ -240,14 +243,16 @@ private void HandleDigestion(in Entity entity, ref Engulfer engulfer, ref Organe continue; var amount = - MicrobeInternalCalculations.CalculateDigestionSpeed(organelles.AvailableEnzymes[usedEnzyme]); + MicrobeInternalCalculations.CalculateDigestionSpeed(organelles.AvailableEnzymes[usedEnzyme], + totalSpecializationBonus); amount *= delta; // Efficiency starts from Constants.ENGULF_BASE_COMPOUND_ABSORPTION_YIELD up to // Constants.ENZYME_DIGESTION_EFFICIENCY_MAXIMUM. This means at least 7 lysosomes // are needed to achieve "maximum" efficiency var efficiency = - MicrobeInternalCalculations.CalculateDigestionEfficiency(organelles.AvailableEnzymes[usedEnzyme]); + MicrobeInternalCalculations.CalculateDigestionEfficiency(organelles.AvailableEnzymes[usedEnzyme], + totalSpecializationBonus); var taken = MathF.Min(totalAvailable, amount); diff --git a/src/microbe_stage/systems/MicrobeEmissionSystem.cs b/src/microbe_stage/systems/MicrobeEmissionSystem.cs index 683bf1f08f1..5bd7275e15c 100644 --- a/src/microbe_stage/systems/MicrobeEmissionSystem.cs +++ b/src/microbe_stage/systems/MicrobeEmissionSystem.cs @@ -97,7 +97,8 @@ private static Vector3 FacingDirection(in Entity entity, ref WorldPosition posit [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Update([Data] in float delta, ref MicrobeControl control, ref OrganelleContainer organelles, ref CellProperties cellProperties, ref WorldPosition position, ref SoundEffectPlayer soundEffectPlayer, - ref CompoundStorage compoundStorage, ref Engulfable engulfable, in Entity entity) + ref CompoundStorage compoundStorage, ref Engulfable engulfable, ref SpecializationFactor specializationFactor, + in Entity entity) { DecreaseTimeCountdownValue(ref control.AgentEmissionCooldown, delta); DecreaseTimeCountdownValue(ref control.SlimeSecretionCooldown, delta); @@ -110,20 +111,20 @@ private void Update([Data] in float delta, ref MicrobeControl control, ref Organ if (control.QueuedToxinToEmit != Compound.Invalid) { EmitProjectile(entity, ref control, ref organelles, ref cellProperties, ref soundEffectPlayer, ref position, - control.QueuedToxinToEmit, compounds, engulfed, false); + control.QueuedToxinToEmit, compounds, engulfed, false, specializationFactor.TotalSpecializationBonus); control.QueuedToxinToEmit = Compound.Invalid; } if (control.QueuedSiderophoreToEmit) { EmitProjectile(entity, ref control, ref organelles, ref cellProperties, ref soundEffectPlayer, ref position, - Compound.Invalid, null, engulfed, true); + Compound.Invalid, null, engulfed, true, specializationFactor.TotalSpecializationBonus); control.QueuedSiderophoreToEmit = false; } // This method itself checks for the preconditions on emitting slime HandleSlimeSecretion(entity, ref control, ref organelles, ref cellProperties, ref soundEffectPlayer, - ref position, compounds, engulfed, delta); + ref position, compounds, engulfed, specializationFactor.TotalSpecializationBonus, delta); } /// @@ -131,7 +132,7 @@ private void Update([Data] in float delta, ref MicrobeControl control, ref Organ /// private void EmitProjectile(in Entity entity, ref MicrobeControl control, ref OrganelleContainer organelles, ref CellProperties cellProperties, ref SoundEffectPlayer soundEffectPlayer, ref WorldPosition position, - Compound agentType, CompoundBag? compounds, bool engulfed, bool siderophore) + Compound agentType, CompoundBag? compounds, bool engulfed, bool siderophore, float specializationBonus) { if (engulfed) return; @@ -254,28 +255,30 @@ private void EmitProjectile(in Entity entity, ref MicrobeControl control, ref Or // TODO: some of the checks above part are already implemented as extension for PlayerMicrobeInput // (so could share a bit of code for checking if ready to shoot yet) - // The cooldown time is inversely proportional to the amount of agent vacuoles. + // Cooldown time is inversely proportional to the amount of agent vacuoles (with specialization multiplier). control.AgentEmissionCooldown = - ToxinCooldownWithToxicity(organelles.AgentVacuoleCount, organelles.AverageToxinToxicity); + ToxinCooldownWithToxicity(organelles.AgentVacuoleCount, organelles.AverageToxinToxicity, + specializationBonus); } } - private float ToxinCooldownWithToxicity(int vacuoleCount, float toxicity) + private float ToxinCooldownWithToxicity(int vacuoleCount, float toxicity, float specializationBonus) { if (toxicity < 0) { // High-firerate - return Constants.AGENT_EMISSION_COOLDOWN / vacuoleCount * (1.0f - (0.5f * Math.Abs(toxicity))); + return Constants.AGENT_EMISSION_COOLDOWN / (vacuoleCount * specializationBonus) * + (1.0f - 0.5f * Math.Abs(toxicity)); } if (toxicity > 0) { // Low-firerate - return Constants.AGENT_EMISSION_COOLDOWN / vacuoleCount * (1 + toxicity); + return Constants.AGENT_EMISSION_COOLDOWN / (vacuoleCount * specializationBonus) * (1 + toxicity); } // No modification from default - return Constants.AGENT_EMISSION_COOLDOWN / vacuoleCount; + return Constants.AGENT_EMISSION_COOLDOWN / (vacuoleCount * specializationBonus); } private float EmissionAmountWithToxicity(float toxicity) @@ -298,7 +301,7 @@ private float EmissionAmountWithToxicity(float toxicity) private void HandleSlimeSecretion(in Entity entity, ref MicrobeControl control, ref OrganelleContainer organelles, ref CellProperties cellProperties, ref SoundEffectPlayer soundEffectPlayer, ref WorldPosition worldPosition, - CompoundBag compounds, bool engulfed, float delta) + CompoundBag compounds, bool engulfed, float specializationBonus, float delta) { // Ignore if we have no slime jets if (organelles.SlimeJets == null) @@ -329,7 +332,7 @@ private void HandleSlimeSecretion(in Entity entity, ref MicrobeControl control, foreach (var jet in organelles.SlimeJets) { // Secrete the slime - float slimeToSecrete = Math.Min(Constants.COMPOUNDS_TO_VENT_PER_SECOND * delta, + float slimeToSecrete = Math.Min(Constants.COMPOUNDS_TO_VENT_PER_SECOND * specializationBonus * delta, compounds.GetCompoundAmount(Compound.Mucilage)); var direction = jet.GetDirection(); diff --git a/src/microbe_stage/systems/MicrobeMovementSystem.cs b/src/microbe_stage/systems/MicrobeMovementSystem.cs index 9de05f75642..5d44704279c 100644 --- a/src/microbe_stage/systems/MicrobeMovementSystem.cs +++ b/src/microbe_stage/systems/MicrobeMovementSystem.cs @@ -83,7 +83,8 @@ private static float CalculateRotationSpeed(in Entity entity, ref OrganelleConta private void Update([Data] in float delta, ref Physics physics, ref OrganelleContainer organelles, ref MicrobeControl control, ref StrainAffected strainAffected, ref Health health, ref WorldPosition position, ref CompoundStorage compoundStorage, ref CellProperties cellProperties, - ref MicrobeTemporaryEffects microbeTemporaryEffects, in SpeciesMember speciesMember, Entity entity) + ref MicrobeTemporaryEffects microbeTemporaryEffects, ref SpecializationFactor specializationFactor, + in SpeciesMember speciesMember, Entity entity) { if (!physics.IsBodyEffectivelyEnabled()) return; @@ -171,7 +172,7 @@ private void Update([Data] in float delta, ref Physics physics, ref OrganelleCon var movementImpulse = CalculateMovementForce(entity, ref control, ref cellProperties, ref position, ref organelles, ref microbeTemporaryEffects, ref strainAffected, compoundStorage.Compounds, - energyCostMultiplier, delta); + specializationFactor.TotalSpecializationBonus, energyCostMultiplier, delta); if (control.State == MicrobeState.MucocystShield) { @@ -185,7 +186,7 @@ private void Update([Data] in float delta, ref Physics physics, ref OrganelleCon private Vector3 CalculateMovementForce(in Entity entity, ref MicrobeControl control, ref CellProperties cellProperties, ref WorldPosition position, ref OrganelleContainer organelles, ref MicrobeTemporaryEffects temporaryEffects, ref StrainAffected strain, - CompoundBag compounds, float energyCostMultiplier, float delta) + CompoundBag compounds, float totalSpecializationBonus, float energyCostMultiplier, float delta) { float strainMultiplier; @@ -209,7 +210,7 @@ private Vector3 CalculateMovementForce(in Entity entity, ref MicrobeControl cont } // Slime jets work even when not holding down any movement keys - var jetMovement = CalculateMovementFromSlimeJets(ref organelles); + var jetMovement = CalculateMovementFromSlimeJets(ref organelles, totalSpecializationBonus); if (entity.Has()) jetMovement += CalculateColonyMovementFromSlimeJets(entity); @@ -273,15 +274,19 @@ private Vector3 CalculateMovementForce(in Entity entity, ref MicrobeControl cont } // Speed from flagella (these also take ATP otherwise they won't work) + var thrustForce = 0.0f; + if (organelles.ThrustComponents != null && control.MovementDirection != Vector3.Zero) { foreach (var flagellum in organelles.ThrustComponents) { - force += flagellum.UseForMovement(control.MovementDirection, compounds, Quaternion.Identity, + thrustForce += flagellum.UseForMovement(control.MovementDirection, compounds, Quaternion.Identity, cellProperties.IsBacteria, energyCostMultiplier, delta); } } + force += thrustForce * totalSpecializationBonus; + force *= cellProperties.MembraneType.MovementFactor - cellProperties.MembraneRigidity * Constants.MEMBRANE_RIGIDITY_BASE_MOBILITY_MODIFIER; @@ -339,7 +344,7 @@ private Vector3 CalculateMovementForce(in Entity entity, ref MicrobeControl cont // Speed from jets (these are related to a non-rotated state of the cell so this is done before rotating // by the transform) - movementVector += CalculateMovementFromSlimeJets(ref organelles); + movementVector += CalculateMovementFromSlimeJets(ref organelles, totalSpecializationBonus); // Handle colony jets if (hasColony) @@ -356,7 +361,7 @@ private float GetStrainAtpMultiplier(ref StrainAffected strain) return strainFraction * Constants.STRAIN_TO_ATP_USAGE_COEFFICIENT + 1.0f; } - private Vector3 CalculateMovementFromSlimeJets(ref OrganelleContainer organelles) + private Vector3 CalculateMovementFromSlimeJets(ref OrganelleContainer organelles, float specializationBonus) { var movementVector = Vector3.Zero; @@ -370,7 +375,7 @@ private Vector3 CalculateMovementFromSlimeJets(ref OrganelleContainer organelles // It might be better to consume the queued force always, but this probably results at most in just // one extra frame of thrust whenever the jets are engaged jet.ConsumeMovementForce(out var jetForce); - movementVector += jetForce; + movementVector += jetForce * specializationBonus; } } @@ -400,8 +405,10 @@ private Vector3 CalculateColonyMovementFromSlimeJets(in Entity entity) } ref var memberOrganelles = ref colonyMember.Get(); + ref var specializationFactor = ref colonyMember.Get(); - movementVector += CalculateMovementFromSlimeJets(ref memberOrganelles); + movementVector += CalculateMovementFromSlimeJets(ref memberOrganelles, + specializationFactor.TotalSpecializationBonus); } } catch (Exception e) diff --git a/src/microbe_stage/systems/MicrobePhysicsCreationAndSizeSystem.cs b/src/microbe_stage/systems/MicrobePhysicsCreationAndSizeSystem.cs index 5552505eda2..0dcbfe60bc6 100644 --- a/src/microbe_stage/systems/MicrobePhysicsCreationAndSizeSystem.cs +++ b/src/microbe_stage/systems/MicrobePhysicsCreationAndSizeSystem.cs @@ -29,6 +29,7 @@ [ReadsComponent(typeof(OrganelleContainer))] [ReadsComponent(typeof(AttachedToEntity))] [ReadsComponent(typeof(SpatialAnimation))] +[ReadsComponent(typeof(SpecializationFactor))] [RunsAfter(typeof(MicrobeVisualsSystem))] [RunsBefore(typeof(PhysicsBodyCreationSystem))] [RunsBefore(typeof(MicrobeReproductionSystem))] @@ -182,10 +183,12 @@ private void Update(ref CellProperties cellProperties, ref PhysicsShapeHolder sh var oldShape = shapeHolder.Shape; + ref var specializationFactor = ref entity.Get(); + if (!requiresCompoundShape) { shapeHolder.Shape = CreateSimpleMicrobeShape(ref extraData, ref organelles, ref cellProperties, - rawData, count); + ref specializationFactor, rawData, count); } else { @@ -197,13 +200,14 @@ private void Update(ref CellProperties cellProperties, ref PhysicsShapeHolder sh { var memberOrganelles = temporaryColonyMemberOrganelles.Value!; shapeHolder.Shape = CreateCompoundMicrobeShape(ref extraData, ref organelles, - ref cellProperties, entity, combinedData, memberOrganelles, + ref cellProperties, ref specializationFactor, entity, combinedData, memberOrganelles, rawData, count, colonyMembranes); } else { shapeHolder.Shape = CreateCompoundMicrobeShape(ref extraData, ref organelles, - ref cellProperties, entity, combinedData, null, rawData, count, + ref cellProperties, ref specializationFactor, entity, combinedData, null, rawData, + count, colonyMembranes); } @@ -237,9 +241,9 @@ private void Update(ref CellProperties cellProperties, ref PhysicsShapeHolder sh private PhysicsShape CreateSimpleMicrobeShape(ref MicrobePhysicsExtraData extraData, ref OrganelleContainer organelles, ref CellProperties cellProperties, - Vector2[] membraneVertices, int vertexCount) + ref SpecializationFactor specializationFactor, Vector2[] membraneVertices, int vertexCount) { - UpdateRotationRate(ref organelles); + UpdateRotationRate(ref organelles, specializationFactor.TotalSpecializationBonus); var shape = PhysicsShape.GetOrCreateMicrobeShape(membraneVertices, vertexCount, MicrobeInternalCalculations.CalculateAverageDensity(organelles.Organelles!), @@ -271,13 +275,13 @@ private PhysicsShape CreateColonyMemberBaseShape(ref MicrobePhysicsExtraData ext } private PhysicsShape CreateCompoundMicrobeShape(ref MicrobePhysicsExtraData extraData, - ref OrganelleContainer organelles, ref CellProperties cellProperties, in Entity entity, - List<(PhysicsShape Shape, Vector3 Position, Quaternion Rotation)> combinedData, - List<(OrganelleLayout Organelles, Vector3 ExtraOffset, Quaternion ExtraRotation)>? - memberOrganelles, Vector2[] membraneVertices, int vertexCount, + ref OrganelleContainer organelles, ref CellProperties cellProperties, + ref SpecializationFactor specializationFactor, in Entity entity, List<(PhysicsShape Shape, Vector3 Position, + Quaternion Rotation)> combinedData, List<(OrganelleLayout Organelles, Vector3 ExtraOffset, + Quaternion ExtraRotation)>? memberOrganelles, Vector2[] membraneVertices, int vertexCount, List<(Membrane Membrane, bool Bacteria)>? colonyMembranes) { - UpdateRotationRate(ref organelles); + UpdateRotationRate(ref organelles, specializationFactor.TotalSpecializationBonus); #if DEBUG if (combinedData.Count > 0) @@ -291,7 +295,8 @@ private PhysicsShape CreateCompoundMicrobeShape(ref MicrobePhysicsExtraData extr // Base microbe shape is always first combinedData.Add(( - CreateSimpleMicrobeShape(ref extraData, ref organelles, ref cellProperties, membraneVertices, + CreateSimpleMicrobeShape(ref extraData, ref organelles, ref cellProperties, ref specializationFactor, + membraneVertices, vertexCount), Vector3.Zero, Quaternion.Identity)); // Then the (potential) colony members @@ -481,7 +486,7 @@ private PhysicsShape CreateCompoundMicrobeShape(ref MicrobePhysicsExtraData extr /// Note that the PhysicsShape is not currently used in rotation calculations, and this code is here due to /// an earlier version requiring it. /// - private void UpdateRotationRate(ref OrganelleContainer organelleContainer) + private void UpdateRotationRate(ref OrganelleContainer organelleContainer, float totalSpecializationBonus) { if (organelleContainer.Organelles == null) { @@ -490,7 +495,8 @@ private void UpdateRotationRate(ref OrganelleContainer organelleContainer) } organelleContainer.RotationSpeed = - MicrobeInternalCalculations.CalculateRotationSpeed(organelleContainer.Organelles.Organelles); + MicrobeInternalCalculations.CalculateRotationSpeed(organelleContainer.Organelles.Organelles, + totalSpecializationBonus); } private PhysicsShape CreatePilusShape(float size) diff --git a/src/microbe_stage/systems/MicrobeReproductionSystem.cs b/src/microbe_stage/systems/MicrobeReproductionSystem.cs index 5b266d5fa67..6e260afe5fb 100644 --- a/src/microbe_stage/systems/MicrobeReproductionSystem.cs +++ b/src/microbe_stage/systems/MicrobeReproductionSystem.cs @@ -37,6 +37,7 @@ [ReadsComponent(typeof(SoundEffectPlayer))] [ReadsComponent(typeof(MicrobeControl))] [ReadsComponent(typeof(MicrobeEnvironmentalEffects))] +[ReadsComponent(typeof(SpecializationFactor))] [RunsAfter(typeof(OsmoregulationAndHealingSystem))] [RunsAfter(typeof(ProcessSystem))] [RuntimeCost(10)] @@ -514,7 +515,7 @@ private void SplitQueuedOrganelles(List organellesToAdd, // help to complicate things by trying to fetch these before the loop organelles.OnOrganellesChanged(ref storage, ref entity.Get(), ref entity.Get(), ref entity.Get(), - ref entity.Get()); + ref entity.Get(), ref entity.Get()); if (entity.Has()) { @@ -605,8 +606,8 @@ private void ReadyToReproduce(in Entity entity, ref OrganelleContainer organelle // Return the first cell to its normal, non-duplicated cell arrangement and spawn a daughter cell organelles.ResetOrganelleLayout(ref entity.Get(), - ref entity.Get(), ref environmentalEffects, - entity, species, species, worldSimulation, workData1, workData2); + ref entity.Get(), ref entity.Get(), + in environmentalEffects, entity, species, species, worldSimulation, workData1, workData2); // This is purely inside this lock to suppress a warning on worldSimulation cellProperties.Divide(ref organelles, entity, species, worldSimulation, spawnEnvironment, diff --git a/src/microbe_stage/systems/OrganelleTickSystem.cs b/src/microbe_stage/systems/OrganelleTickSystem.cs index 4f8399090a6..7468d771347 100644 --- a/src/microbe_stage/systems/OrganelleTickSystem.cs +++ b/src/microbe_stage/systems/OrganelleTickSystem.cs @@ -78,7 +78,7 @@ public override void AfterUpdate(in float delta) [All] [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Update([Data] in float delta, ref OrganelleContainer organelleContainer, - in SpeciesMember speciesMember, in Entity entity) + ref SpecializationFactor specializationFactor, in SpeciesMember speciesMember, in Entity entity) { if (organelleContainer.Organelles == null) return; @@ -106,7 +106,8 @@ private void Update([Data] in float delta, ref OrganelleContainer organelleConta var component = components[j]; // Organelles can do various things which is why we have the above "All" attribute - component.UpdateAsync(ref organelleContainer, entity, worldSimulation, energyCostMultiplier, delta); + component.UpdateAsync(ref organelleContainer, ref specializationFactor, entity, worldSimulation, + energyCostMultiplier, delta); if (component.UsesSyncProcess) queuedSyncRuns.Push((component, entity)); diff --git a/src/microbe_stage/systems/ProcessSystem.cs b/src/microbe_stage/systems/ProcessSystem.cs index 0346a1d4db9..3099ae11fc3 100644 --- a/src/microbe_stage/systems/ProcessSystem.cs +++ b/src/microbe_stage/systems/ProcessSystem.cs @@ -218,7 +218,10 @@ public static Dictionary ComputeOrganelleProcessEff /// The organelles to compute the balance with /// The conditions the organelles are simulated in /// Environmental tolerances that affect the processes - /// Cell type specialization factor (1 is default) + /// + /// Cell type specialization factor (1 is default). This should include any adjacency bonuses when relevant. + /// (cells in a multicellular species) + /// /// The membrane type to adjust the energy balance with /// /// Only movement organelles that can move in this (cell origin relative) direction are calculated. Other @@ -238,7 +241,7 @@ public static Dictionary ComputeOrganelleProcessEff /// The resulting energy balance. /// public static void ComputeEnergyBalanceSimple(IReadOnlyList organelles, - IBiomeConditions biome, in ResolvedMicrobeTolerances environmentTolerances, float specializationFactor, + IBiomeConditions biome, in ResolvedMicrobeTolerances environmentTolerances, float totalSpecializationBonus, MembraneType membrane, Vector3 onlyMovementInDirection, bool includeMovementCost, bool isPlayerSpecies, WorldGenerationSettings worldSettings, CompoundAmountType amountType, SimulationCache? cache, @@ -255,7 +258,7 @@ public static void ComputeEnergyBalanceSimple(IReadOnlyList o } #endif - CalculateSimplePartOfEnergyBalance(organelles, biome, environmentTolerances, specializationFactor, membrane, + CalculateSimplePartOfEnergyBalance(organelles, biome, environmentTolerances, totalSpecializationBonus, membrane, onlyMovementInDirection, includeMovementCost, isPlayerSpecies, worldSettings, amountType, cache, result); } diff --git a/src/multicellular_stage/CellBodyPlanInternalCalculations.cs b/src/multicellular_stage/CellBodyPlanInternalCalculations.cs index d1e2cbc9a81..97f89f9bb2b 100644 --- a/src/multicellular_stage/CellBodyPlanInternalCalculations.cs +++ b/src/multicellular_stage/CellBodyPlanInternalCalculations.cs @@ -5,7 +5,7 @@ public static class CellBodyPlanInternalCalculations { - public static Dictionary GetTotalSpecificCapacity(IEnumerable cells, + public static Dictionary GetTotalSpecificCapacity(IReadOnlyList> cells, out float nominalCapacity) { nominalCapacity = 0.0f; @@ -13,12 +13,19 @@ public static Dictionary GetTotalSpecificCapacity(IEnumerable(); // TODO: Check if it's possible to do those calculations per cell type and multiply by the types' cell counts - foreach (var cell in cells) + foreach (var hex in cells) { - var totalNominalCap = MicrobeInternalCalculations.GetTotalNominalCapacity(cell.ModifiableOrganelles); + var cell = hex.Data!; + + var totalSpecializationBonus = cell.CellTypeSpecializationBonus * + GetAdjacencySpecializationBonusFromBodyPlan(cell, cells); + + var totalNominalCap = MicrobeInternalCalculations.GetTotalNominalCapacity(cell.ModifiableOrganelles, + totalSpecializationBonus); nominalCapacity += totalNominalCap; - MicrobeInternalCalculations.AddSpecificCapacity(cell.ModifiableOrganelles, capacities); + MicrobeInternalCalculations.AddSpecificCapacity(cell.ModifiableOrganelles, capacities, + totalSpecializationBonus); } return capacities; @@ -32,8 +39,10 @@ public static float CalculateSpeed(IReadOnlyList> cell { var leader = cells[0].Data!; + var leaderTotalSpecializationBonus = leader.CellTypeSpecializationBonus * + GetAdjacencySpecializationBonusFromBodyPlan(leader.Data, cells); var speed = MicrobeInternalCalculations.CalculateSpeed(leader.ModifiableOrganelles, leader.MembraneType, - leader.MembraneRigidity, leader.IsBacteria); + leader.MembraneRigidity, leader.IsBacteria, leaderTotalSpecializationBonus); if (cells.Count == 1) return speed; @@ -71,6 +80,12 @@ public static float CalculateSpeed(IReadOnlyList> cell if (!cell.IsBacteria) flagellumForce *= Constants.EUKARYOTIC_MOVEMENT_FORCE_MULTIPLIER; + // Apply cell specialization bonus + var totalSpecializationBonus = cell.CellTypeSpecializationBonus * + GetAdjacencySpecializationBonusFromBodyPlan(cell, cells); + + flagellumForce *= totalSpecializationBonus; + addedSpeed += flagellumForce; } } @@ -124,7 +139,10 @@ public static float CalculateRotationSpeed(IReadOnlyList ModifiableCellType.MembraneType = value; } + /// + /// Cached specialization bonus for this cell type (excluding any adjacency effects!). + /// + public float CellTypeSpecializationBonus + { + get => ModifiableCellType.CellTypeSpecializationBonus; + set => ModifiableCellType.CellTypeSpecializationBonus = value; + } + public float MembraneRigidity { get => ModifiableCellType.MembraneRigidity; diff --git a/src/multicellular_stage/CellType.cs b/src/multicellular_stage/CellType.cs index 2b90973843d..0242f83e5f2 100644 --- a/src/multicellular_stage/CellType.cs +++ b/src/multicellular_stage/CellType.cs @@ -57,9 +57,9 @@ public CellType(MicrobeSpecies microbeSpecies, List workMemory1, List public string? SplitFromTypeName { get; set; } /// - /// Cached specialization bonus for this cell type. + /// Cached specialization bonus for this cell type (excluding any adjacency effects!). /// - public float SpecializationBonus { get; set; } + public float CellTypeSpecializationBonus { get; set; } public MembraneType MembraneType { get; set; } public float MembraneRigidity { get; set; } @@ -107,12 +107,12 @@ public static CellType ReadFromArchive(ISArchiveReader reader, ushort version, i if (version > 2) { - result.SpecializationBonus = reader.ReadFloat(); + result.CellTypeSpecializationBonus = reader.ReadFloat(); } else { // Just like microbes, older cell types will get eventually updated by something to have a valid value - result.SpecializationBonus = 1; + result.CellTypeSpecializationBonus = 1; } return result; @@ -132,17 +132,18 @@ public void WriteToArchive(ISArchiveWriter writer) writer.Write(SplitFromTypeName); - writer.Write(SpecializationBonus); + writer.Write(CellTypeSpecializationBonus); } public bool RepositionToOrigin() { var changes = ModifiableOrganelles.RepositionToOrigin(); - CalculateRotationSpeed(); // We don't have another on-edit callback, so we do this update here CalculateSpecialization(); + CalculateRotationSpeed(); + return changes; } @@ -189,7 +190,7 @@ public bool IsMuscularTissueType() public void CalculateSpecialization() { - SpecializationBonus = + CellTypeSpecializationBonus = MicrobeInternalCalculations.CalculateSpecializationBonus(ModifiableOrganelles, new Dictionary()); } @@ -242,7 +243,7 @@ public void CopyFrom(CellType otherType, List hexTemporaryMemory, List Colour = otherType.Colour; MembraneRigidity = otherType.MembraneRigidity; - SpecializationBonus = otherType.SpecializationBonus; + CellTypeSpecializationBonus = otherType.CellTypeSpecializationBonus; } public object Clone() @@ -254,7 +255,7 @@ public object Clone() MembraneRigidity = MembraneRigidity, Colour = Colour, IsBacteria = IsBacteria, - SpecializationBonus = SpecializationBonus, + CellTypeSpecializationBonus = CellTypeSpecializationBonus, }; var workMemory1 = new List(); @@ -322,6 +323,7 @@ public override string ToString() private void CalculateRotationSpeed() { // TODO: switch this to use a read only interface - BaseRotationSpeed = MicrobeInternalCalculations.CalculateRotationSpeed(ModifiableOrganelles.Organelles); + BaseRotationSpeed = MicrobeInternalCalculations.CalculateRotationSpeed(ModifiableOrganelles.Organelles, + CellTypeSpecializationBonus); } } diff --git a/src/multicellular_stage/MulticellularSpecies.cs b/src/multicellular_stage/MulticellularSpecies.cs index 7b25225ff63..412e5a3c3b4 100644 --- a/src/multicellular_stage/MulticellularSpecies.cs +++ b/src/multicellular_stage/MulticellularSpecies.cs @@ -279,10 +279,14 @@ public override void HandleNightSpawnCompounds(CompoundBag targetStorage, ISpawn { if (!cachedFillTimes.TryGetValue(biome, out compoundTimes)) { + // This does not use the real specialization bonus to avoid underestimating how much single cells in + // unusual spawn situations (such as spores) need to survive the night + var totalSpecializationBonus = 1; + // TODO: should moving be false in some cases? compoundTimes = MicrobeInternalCalculations.CalculateDayVaryingCompoundsFillTimes( cellType.ModifiableOrganelles, cellType.MembraneType, true, PlayerSpecies, - microbeSpawnEnvironment.CurrentBiome, resolvedTolerances, + totalSpecializationBonus, microbeSpawnEnvironment.CurrentBiome, resolvedTolerances, microbeSpawnEnvironment.WorldSettings); cachedFillTimes[biome] = compoundTimes; } @@ -342,7 +346,7 @@ public float CalculateAverageSpecialization() for (int i = 0; i < count; ++i) { var cell = ModifiableGameplayCells[i]; - score += cell.CellType.SpecializationBonus * GetAdjacencySpecializationBonus(i); + score += cell.CellType.CellTypeSpecializationBonus * GetAdjacencySpecializationBonus(i); } return score / count; diff --git a/src/multicellular_stage/editor/CellBodyPlanEditorComponent.cs b/src/multicellular_stage/editor/CellBodyPlanEditorComponent.cs index 5fe2672ea30..78826e212af 100644 --- a/src/multicellular_stage/editor/CellBodyPlanEditorComponent.cs +++ b/src/multicellular_stage/editor/CellBodyPlanEditorComponent.cs @@ -682,9 +682,7 @@ public bool ShowCellOptions() public Dictionary GetAdditionalCapacities(out float nominalCapacity) { - return CellBodyPlanInternalCalculations.GetTotalSpecificCapacity( - editedMicrobeCells.AsModifiable().Select(o => o.Data!), - out nominalCapacity); + return CellBodyPlanInternalCalculations.GetTotalSpecificCapacity(editedMicrobeCells, out nominalCapacity); } public void OnCurrentPatchUpdated(Patch patch) @@ -1317,18 +1315,19 @@ private void UpdateCellTypeTooltipAndWarning(CellTypeTooltip tooltip, CellTypeSe var maximumMovementDirection = MicrobeInternalCalculations.MaximumSpeedDirection(cellType.ModifiableOrganelles); - var specialization = + // Deliberately uses cell type specialization bonus without adjacency, + // because this is for editor cell type tooltips + var totalSpecializationBonus = MicrobeInternalCalculations.CalculateSpecializationBonus(cellType.ModifiableOrganelles, tempMemory3); ProcessSystem.ComputeEnergyBalanceFull(cellType.ModifiableOrganelles, Editor.CurrentPatch.Biome, - environmentalTolerances, specialization, - cellType.MembraneType, - maximumMovementDirection, moving, true, Editor.CurrentGame.GameWorld.WorldSettings, + environmentalTolerances, totalSpecializationBonus, cellType.MembraneType, maximumMovementDirection, + moving, true, Editor.CurrentGame.GameWorld.WorldSettings, organismStatisticsPanel.CompoundAmountType, null, energyBalanceInfo); AddCellTypeCompoundBalance(balances, cellType.ModifiableOrganelles, organismStatisticsPanel.BalanceDisplayType, organismStatisticsPanel.CompoundAmountType, Editor.CurrentPatch.Biome, energyBalanceInfo, - environmentalTolerances, specialization); + environmentalTolerances, totalSpecializationBonus); tooltip.DisplayName = cellType.CellTypeName; tooltip.MutationPointCost = Math.Min(cellType.MPCost * Editor.CurrentGame.GameWorld.WorldSettings.MPMultiplier, @@ -1336,7 +1335,7 @@ private void UpdateCellTypeTooltipAndWarning(CellTypeTooltip tooltip, CellTypeSe tempCompoundSources.Clear(); ProcessSystem.CalculateInputCompoundsNeededForOutputs(cellType.ModifiableOrganelles, Editor.CurrentPatch.Biome, - environmentalTolerances, specialization, + environmentalTolerances, totalSpecializationBonus, organismStatisticsPanel.CompoundAmountType, true, tempCompoundSources); Editor.CurrentPatch.Biome.GetProducedCompoundsThatDependOnVarying(tempCompoundSources, @@ -1349,18 +1348,19 @@ private void UpdateCellTypeTooltipAndWarning(CellTypeTooltip tooltip, CellTypeSe tooltip.UpdateHealthIndicator(MicrobeInternalCalculations.CalculateHealth(environmentalTolerances, cellType.MembraneType, cellType.MembraneRigidity)); - tooltip.UpdateStorageIndicator( - MicrobeInternalCalculations.GetTotalNominalCapacity(cellType.ModifiableOrganelles)); + tooltip.UpdateStorageIndicator(MicrobeInternalCalculations.GetTotalNominalCapacity( + cellType.ModifiableOrganelles, totalSpecializationBonus)); tooltip.UpdateSpeedIndicator(MicrobeInternalCalculations.CalculateSpeed(cellType.ModifiableOrganelles, - cellType.MembraneType, cellType.MembraneRigidity, cellType.IsBacteria, false)); + cellType.MembraneType, cellType.MembraneRigidity, cellType.IsBacteria, totalSpecializationBonus, + false)); - tooltip.UpdateRotationSpeedIndicator( - MicrobeInternalCalculations.CalculateRotationSpeed(cellType.ModifiableOrganelles)); + tooltip.UpdateRotationSpeedIndicator(MicrobeInternalCalculations.CalculateRotationSpeed( + cellType.ModifiableOrganelles, totalSpecializationBonus)); tooltip.UpdateSizeIndicator(cellType.Organelles.Sum(o => o.Definition.HexCount)); - tooltip.UpdateDigestionSpeedIndicator( - MicrobeInternalCalculations.CalculateTotalDigestionSpeed(cellType.ModifiableOrganelles)); + tooltip.UpdateDigestionSpeedIndicator(MicrobeInternalCalculations.CalculateTotalDigestionSpeed( + cellType.ModifiableOrganelles, totalSpecializationBonus)); button.ShowInsufficientATPWarning = energyBalanceInfo.TotalProduction < energyBalanceInfo.TotalConsumption; @@ -1585,8 +1585,7 @@ private Dictionary CalculateCompoundBalanceWithMethod amountType, biome, energyBalance, tolerances, totalSpecialization); } - specificStorages ??= CellBodyPlanInternalCalculations.GetTotalSpecificCapacity(cells.Select(o => o.Data!), - out nominalStorage); + specificStorages ??= CellBodyPlanInternalCalculations.GetTotalSpecificCapacity(cells, out nominalStorage); return ProcessSystem.ComputeCompoundFillTimes(compoundBalanceData, nominalStorage, specificStorages); } diff --git a/src/saving/ThriveArchiveObjectType.cs b/src/saving/ThriveArchiveObjectType.cs index 11b18c89988..0d676124c7d 100644 --- a/src/saving/ThriveArchiveObjectType.cs +++ b/src/saving/ThriveArchiveObjectType.cs @@ -316,6 +316,7 @@ public enum ThriveArchiveObjectType : uint ComponentSpatialAnimation = 4396, JukeboxPlaybackState = 4397, ThriveopediaGameData = 4398, + ComponentSpecializationFactor = 4399, // Special flag types ExtendedOrganelleLayout = OrganelleLayout | ArchiveObjectType.ExtendedTypeFlag, diff --git a/src/saving/serializers/IArchivableComponent.cs b/src/saving/serializers/IArchivableComponent.cs index 7540cc51261..e3911f735de 100644 --- a/src/saving/serializers/IArchivableComponent.cs +++ b/src/saving/serializers/IArchivableComponent.cs @@ -286,6 +286,9 @@ public static bool ReadComponentToEntity(ISArchiveReader reader, Entity entity, case ThriveArchiveObjectType.ComponentSpatialAnimation: entity.Add(SpatialAnimationHelpers.ReadFromArchive(reader, version)); return true; + case ThriveArchiveObjectType.ComponentSpecializationFactor: + entity.Add(SpecializationFactorHelpers.ReadFromArchive(reader, version)); + return true; } return false;