riversong code showcase

This commit is contained in:
Daniele Marotta
2026-05-21 15:52:18 +02:00
commit 4c9eea1c02
462 changed files with 23406 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public struct BuildingUpgradedSignal
{
public Building Building;
public BuildingUpgradedSignal(Building building)
{
Building = building;
}
}
}

View File

@@ -0,0 +1,85 @@
using System;
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class HouseTierCountTrackingSystem : GameSystem, IInitializable, IDisposable
{
[InjectService]
private ISignalBus _signalBus;
[InjectService]
private World _world;
public HouseTierCountTrackingSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public UniTask InitializeAsync()
{
_signalBus.Subscribe<BuildingCreatedSignal>(OnBuildingCreated);
_signalBus.Subscribe<BuildingDeletedSignal>(OnBuildingDeleted);
_signalBus.Subscribe<BuildingUpgradedSignal>(OnBuildingUpgraded);
return UniTask.CompletedTask;
}
public void Dispose()
{
_signalBus.Unsubscribe<BuildingCreatedSignal>(OnBuildingCreated);
_signalBus.Unsubscribe<BuildingDeletedSignal>(OnBuildingDeleted);
_signalBus.Unsubscribe<BuildingUpgradedSignal>(OnBuildingUpgraded);
}
private void OnBuildingCreated(BuildingCreatedSignal signal)
{
var building = signal.Building;
if (!building.Definition.IsHouse) return;
UpdateTierCount(building.TierIndex, 1);
}
private void OnBuildingDeleted(BuildingDeletedSignal signal)
{
var building = signal.Building;
if (!building.Definition.IsHouse) return;
UpdateTierCount(building.TierIndex, -1);
}
private void OnBuildingUpgraded(BuildingUpgradedSignal signal)
{
var building = signal.Building;
if (!building.Definition.IsHouse) return;
if (building.TierIndex > 0) UpdateTierCount(building.TierIndex - 1, -1);
UpdateTierCount(building.TierIndex, 1);
}
private void UpdateTierCount(int tierIndex, int delta)
{
if (!ValidateTierIndex(tierIndex)) return;
var countsByTier = _world.PopulationState.HouseCountsByTier;
var updatedCount = countsByTier[tierIndex] + delta;
if (updatedCount < 0)
{
Debug.LogError($"Attempted to decrease house count for tier {tierIndex} that was already at 0");
return;
}
countsByTier[tierIndex] = updatedCount;
}
private bool ValidateTierIndex(int tierIndex)
{
if (tierIndex is >= 0 and < WorldPopulationState.MaxBuildingTier) return true;
Debug.LogError($"Invalid tier index {tierIndex}");
return false;
}
}
}

View File

@@ -0,0 +1,12 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public struct PopulationChangedSignal
{
public int Population;
public PopulationChangedSignal(int population)
{
Population = population;
}
}
}

View File

@@ -0,0 +1,21 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public struct PopulationNeed
{
public byte TierIndex;
public PopulationNeedType Type;
public int ProductHandle;
public ushort HappinessScore;
public byte ConsumptionRate;
public byte FetchThreshold;
public ushort YieldOnFetch;
public ushort Current;
}
}

View File

@@ -0,0 +1,20 @@
using System;
namespace DanieleMarotta.RiversongCodeShowcase
{
[Serializable]
public class PopulationNeedAuthoring
{
public PopulationNeedType Type;
public ProductDefinition Product;
public int HappinessScore = 20;
public int ConsumptionRate;
public int FetchThreshold;
public int YieldOnFetch;
}
}

View File

@@ -0,0 +1,7 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public enum PopulationNeedType : byte
{
Product
}
}

View File

@@ -0,0 +1,21 @@
using Unity.Collections;
namespace DanieleMarotta.RiversongCodeShowcase
{
public struct PopulationNeedsState
{
public FixedList128Bytes<PopulationNeed> Needs;
public TierUpgradeState UpgradeState;
public bool AllNeedsMet;
public int NeedsMetForWeeks;
public float Happiness;
public float MaxHappinessScore;
public float OverallHappinessWeight;
}
}

View File

@@ -0,0 +1,126 @@
using System;
using Cysharp.Threading.Tasks;
using Unity.Mathematics;
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class PopulationNeedsSystem : GameSystem, IInitializable, IDisposable, IUpdatable
{
private const float MaxHappinessScoreChangeRate = 0.1f;
[InjectService]
private ISignalBus _signalBus;
[InjectService]
private IEntityCache _entityCache;
[InjectService]
private World _world;
[InjectService]
private GameConfig _config;
public PopulationNeedsSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public UniTask InitializeAsync()
{
_signalBus.Subscribe<EndOfWeekSignal>(OnEndOfWeek);
return UniTask.CompletedTask;
}
public void Dispose()
{
_signalBus.Unsubscribe<EndOfWeekSignal>(OnEndOfWeek);
}
public void Update()
{
if (_world.TimeState.DayNightCycleStep != DayNightCycleStep.Day) return;
foreach (var house in _entityCache.GetHouses())
{
ref var needsState = ref house.GetNeedsStateRW();
ref var storage = ref house.GetStorageRW();
var targetHappiness = 1f;
var happinessScore = 0;
var maxHappinessScore = 0;
needsState.AllNeedsMet = true;
for (var i = 0; i < needsState.Needs.Length; i++)
{
var need = needsState.Needs[i];
if (need.TierIndex > house.TierIndex) break;
if (need.Type != PopulationNeedType.Product) throw new NotImplementedException();
if (need.Current < need.YieldOnFetch)
{
var productCount = storage.AvailableNow(need.ProductHandle);
if (productCount > 0)
{
storage.Take(need.ProductHandle, productCount);
need.Current += (ushort)math.min(productCount * need.YieldOnFetch, ushort.MaxValue);
}
}
var needMet = need.Current > 0;
if (needMet) happinessScore += need.HappinessScore;
maxHappinessScore += need.HappinessScore;
needsState.AllNeedsMet &= needMet;
needsState.Needs[i] = need;
}
if (maxHappinessScore > 0)
{
needsState.MaxHappinessScore += MaxHappinessScoreChangeRate * Time.deltaTime;
needsState.MaxHappinessScore = math.min(needsState.MaxHappinessScore, maxHappinessScore);
targetHappiness = needsState.MaxHappinessScore > 0 ? math.saturate(happinessScore / needsState.MaxHappinessScore) : 1;
}
else
{
needsState.MaxHappinessScore = 0;
}
needsState.Happiness = Mathf.MoveTowards(needsState.Happiness, targetHappiness, _config.Population.OverallHappinessChangeRate * Time.deltaTime);
needsState.Happiness = math.saturate(needsState.Happiness);
needsState.OverallHappinessWeight = math.saturate((float)(_world.TimeState.TotalWeeks - house.WeekCreated) / _config.Population.HouseWeightRampUpWeekCount);
}
}
private void OnEndOfWeek(EndOfWeekSignal signal)
{
foreach (var house in _entityCache.GetHouses())
{
ref var needsState = ref house.GetNeedsStateRW();
if (needsState.AllNeedsMet)
needsState.NeedsMetForWeeks++;
else
needsState.NeedsMetForWeeks = 0;
for (var i = 0; i < needsState.Needs.Length; i++)
{
var need = needsState.Needs[i];
if (need.TierIndex > house.TierIndex) break;
if (need.Type != PopulationNeedType.Product) continue;
if (need.Current >= need.ConsumptionRate)
need.Current -= need.ConsumptionRate;
else
need.Current = 0;
needsState.Needs[i] = need;
}
}
}
}
}

View File

@@ -0,0 +1,89 @@
using Cysharp.Threading.Tasks;
using Unity.Mathematics;
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
[UpdateAfter(typeof(PopulationNeedsSystem))]
public class PopulationUpdateSystem : GameSystem, IInitializable, IUpdatable
{
[InjectService]
private ISignalBus _signalBus;
[InjectService]
private World _world;
[InjectService]
private IEntityCache _entityCache;
[InjectService]
private GameConfig _config;
public PopulationUpdateSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public UniTask InitializeAsync()
{
_world.PopulationState.Happiness = _config.Population.InitialeOverallHappiness;
return UniTask.CompletedTask;
}
public void Update()
{
if (_world.TimeState.DayNightCycleStep != DayNightCycleStep.Day) return;
UpdateOverallHappiness();
UpdatePopulation();
}
private void UpdateOverallHappiness()
{
var targetHappiness = 0f;
var totalWeight = 0f;
foreach (var house in _entityCache.GetHouses())
{
ref var needsState = ref house.GetNeedsStateRW();
targetHappiness += needsState.OverallHappinessWeight * needsState.Happiness;
totalWeight += needsState.OverallHappinessWeight;
}
if (totalWeight > 0) targetHappiness = math.saturate(targetHappiness / totalWeight);
var grace = _world.TimeState.TotalWeeks < _config.Population.GraceWeekCount;
if (grace) targetHappiness = math.max(targetHappiness, _config.Population.GraceMinHappiness);
var populationState = _world.PopulationState;
var delta = math.abs(targetHappiness - populationState.Happiness);
const float halfOnePercent = 0.005f;
populationState.Sentiment = delta > halfOnePercent ? math.sign(targetHappiness - populationState.Happiness) : 0;
populationState.Happiness = Mathf.MoveTowards(populationState.Happiness, targetHappiness, _config.Population.OverallHappinessChangeRate * Time.deltaTime);
populationState.Happiness = math.saturate(populationState.Happiness);
}
private void UpdatePopulation()
{
var populationState = _world.PopulationState;
populationState.PopulationCapacity = 0;
foreach (var house in _entityCache.GetHouses()) populationState.PopulationCapacity += house.PopulationCapacity;
var growthRate = _config.Population.GrowthRatePeakValue * _config.Population.GrowthRateCurve.Evaluate(populationState.Happiness);
if (populationState.Sentiment > 0) growthRate = math.max(growthRate, 0);
populationState.GrowthAccumulator += growthRate * Time.deltaTime;
if (math.abs(populationState.GrowthAccumulator) < 1) return;
var delta = (int)math.sign(populationState.GrowthAccumulator);
populationState.GrowthAccumulator -= delta;
var updatedPopulation = math.clamp(populationState.Population + delta, 0, populationState.PopulationCapacity);
if (updatedPopulation == populationState.Population) return;
populationState.Population = updatedPopulation;
_signalBus.Raise(new PopulationChangedSignal(updatedPopulation));
}
}
}

View File

@@ -0,0 +1,13 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public enum TierUpgradeState
{
NotReady,
FetchingMaterials,
AllMaterialsFetched,
MaxedOut
}
}

View File

@@ -0,0 +1,93 @@
using System;
using Cysharp.Threading.Tasks;
using Unity.Mathematics;
namespace DanieleMarotta.RiversongCodeShowcase
{
[InitializeAfter(typeof(PopulationNeedsSystem))]
public class TierUpgradeSystem : GameSystem, IInitializable, IDisposable, IUpdatable
{
[InjectService]
private IEntityCache _entityCache;
[InjectService]
private IProductStorageCommonLogic _storageCommonLogic;
[InjectService]
private ISignalBus _signalBus;
[InjectService]
private GameConfig _config;
public TierUpgradeSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public UniTask InitializeAsync()
{
_signalBus.Subscribe<EndOfWeekSignal>(OnEndOfWeek);
return UniTask.CompletedTask;
}
public void Dispose()
{
_signalBus.Unsubscribe<EndOfWeekSignal>(OnEndOfWeek);
}
private void OnEndOfWeek(EndOfWeekSignal signal)
{
var config = _config.Buildings;
foreach (var house in _entityCache.GetHouses())
{
ref var needsState = ref house.GetNeedsStateRW();
if (needsState.UpgradeState != TierUpgradeState.NotReady || !needsState.AllNeedsMet || needsState.NeedsMetForWeeks < config.WeeksWithNeedsMetToUpgrade) continue;
needsState.UpgradeState = TierUpgradeState.FetchingMaterials;
}
}
public void Update()
{
foreach (var house in _entityCache.GetHouses())
{
ref var needsState = ref house.GetNeedsStateRW();
if (needsState.UpgradeState != TierUpgradeState.FetchingMaterials) continue;
var products = house.Definition.HouseTiers[house.TierIndex].UpgradeMaterials;
ref var storage = ref house.GetStorageRW();
if (!_storageCommonLogic.TakeAllOrNothing(ref storage, products)) continue;
needsState.UpgradeState = TierUpgradeState.AllMaterialsFetched;
_ = UpgradeBuildingAsync(house);
}
}
private async UniTask UpgradeBuildingAsync(Building house)
{
await UniTask.WaitForSeconds(1);
house.TierIndex++;
var tiers = house.Definition.HouseTiers;
house.PopulationCapacity = tiers[math.min(house.TierIndex, tiers.Count - 1)].Capacity;
_signalBus.Raise(new BuildingUpgradedSignal(house));
OnBuildingUpgraded(house);
}
private void OnBuildingUpgraded(Building house)
{
var maxTier = house.Definition.HouseTiers.Count - 1;
ref var needsState = ref house.GetNeedsStateRW();
needsState.UpgradeState = house.TierIndex < maxTier ? TierUpgradeState.NotReady : TierUpgradeState.MaxedOut;
}
}
}

View File

@@ -0,0 +1,19 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public class WorldPopulationState
{
public const int MaxBuildingTier = 10;
public int Population { get; set; }
public int PopulationCapacity { get; set; }
public float GrowthAccumulator { get; set; }
public float Happiness { get; set; }
public float Sentiment { get; set; }
public int[] HouseCountsByTier { get; } = new int[MaxBuildingTier];
}
}