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,47 @@
using Unity.Properties;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class BuildMenuBuildingModel : UIModel
{
private BuildingDefinition _building;
private bool _isUnlocked;
private string _unlockConditions;
public BuildingDefinition Building
{
get => _building;
set
{
SetProperty(ref _building, value);
if (!_building) return;
BuildingName = _building.BuildingName;
NotifyPropertyChanged(nameof(BuildingName));
BuildingDescription = _building.BuildingDescription;
NotifyPropertyChanged(nameof(BuildingDescription));
}
}
[CreateProperty] public string BuildingName { get; private set; }
[CreateProperty] public string BuildingDescription { get; private set; }
public bool IsUnlocked
{
get => _isUnlocked;
set => SetProperty(ref _isUnlocked, value);
}
[CreateProperty]
public string UnlockConditions
{
get => _unlockConditions;
set => SetProperty(ref _unlockConditions, value);
}
}
}

View File

@@ -0,0 +1,89 @@
using Cysharp.Threading.Tasks;
using PrimeTween;
using UnityEngine;
using UnityEngine.UIElements;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class BuildMenuButtonUIView : UIView<BuildingDefinition>
{
private static readonly Color UnlockGlowColor = new(0, 1, 0.7f);
private VisualElement _icon;
private Material _iconMaterial;
public override UniTask InitializeAsync(UIService uiService, VisualElement rootElement)
{
base.InitializeAsync(uiService, rootElement);
_icon = RootElement.Q(className: "build-menu__button-icon");
return UniTask.CompletedTask;
}
public async UniTask PlayRevealAnimationAsync()
{
await Tween.Custom(0, 1, 1, value => RootElement.SetScale(value), Ease.OutBounce, useUnscaledTime: true);
}
public async UniTask PlayUnlockAnimationAsync()
{
CacheIconMaterial();
_ = PlayUnlockVfxAsync();
await Sequence.Create(useUnscaledTime: true)
.Group(
Tween.Custom(
_iconMaterial.GetColor(ShaderProperties.Color),
UnlockGlowColor,
1.2f,
value => _iconMaterial.SetColor(ShaderProperties.Color, value),
Ease.OutQuad))
.Chain(Tween.Custom(1, 0, 0.8f, value => _iconMaterial.SetFloat(ShaderProperties.Amount, value)));
}
private async UniTask PlayUnlockVfxAsync()
{
var vfx = new VisualElement();
RootElement.panel.visualTree.Q(className: "vfx-layer").Add(vfx);
vfx.AddToClassList("vfx__build-menu-button-unlock");
vfx.pickingMode = PickingMode.Ignore;
var position = RootElement.Q<Image>().worldBound.center;
vfx.style.left = position.x;
vfx.style.top = position.y;
vfx.SetScale(0);
await UniTask.NextFrame();
var material = vfx.resolvedStyle.unityMaterial.material;
material.SetColor(ShaderProperties.Color, UnlockGlowColor);
await Sequence.Create(useUnscaledTime: true)
.Group(Tween.Custom(0.25f, 1, 1, value => vfx.SetScale(value), Ease.OutQuad))
.Group(Tween.Custom(0, 1, 0.25f, value => vfx.style.opacity = value, Ease.OutQuad))
.Group(Tween.Custom(1, 0, 0.25f, value => vfx.style.opacity = value, startDelay: 0.75f));
vfx.RemoveFromHierarchy();
}
private Material CacheIconMaterial()
{
if (_iconMaterial) return _iconMaterial;
var sourceMaterial = _icon.resolvedStyle.unityMaterial.material;
if (!sourceMaterial)
{
Debug.LogError($"Icon material in {nameof(BuildMenuButtonUIView)} is null");
return null;
}
_iconMaterial = new Material(sourceMaterial);
_icon.style.unityMaterial = _iconMaterial;
return _iconMaterial;
}
}
}

View File

@@ -0,0 +1,6 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public struct BuildMenuButtonUnlockAnimationStartedSignal
{
}
}

View File

@@ -0,0 +1,17 @@
using System.Collections.Generic;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class BuildMenuModel : UIModel
{
private BuildingDefinition _selectedBuilding;
public List<BuildMenuBuildingModel> Buildings { get; } = new();
public BuildingDefinition SelectedBuilding
{
get => _selectedBuilding;
set => SetProperty(ref _selectedBuilding, value);
}
}
}

View File

@@ -0,0 +1,64 @@
using Cysharp.Threading.Tasks;
using UnityEngine.UIElements;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class BuildMenuTooltipUIView : UIView<BuildMenuBuildingModel>
{
private VisualElement _lockedContent;
private VisualElement _unlockedContent;
private VisualElement _products;
public override UniTask InitializeAsync(UIService uiService, VisualElement rootElement)
{
base.InitializeAsync(uiService, rootElement);
_lockedContent = rootElement.Q(className: "build-menu__tooltip-content__locked");
_unlockedContent = rootElement.Q(className: "build-menu__tooltip-content__unlocked");
_products = rootElement.Q(className: "build-menu__tooltip-products");
return UniTask.CompletedTask;
}
protected override void OnNewModel(BuildMenuBuildingModel model)
{
base.OnNewModel(model);
Update();
}
protected override void OnModelPropertyChanged(object sender, BindablePropertyChangedEventArgs e)
{
base.OnModelPropertyChanged(sender, e);
Update();
}
private void Update()
{
if (!Model.Building) return;
_lockedContent.style.display = Model.IsUnlocked ? DisplayStyle.None : DisplayStyle.Flex;
_unlockedContent.style.display = Model.IsUnlocked ? DisplayStyle.Flex : DisplayStyle.None;
UpdateProducts();
}
private void UpdateProducts()
{
_products.Clear();
var productTemplate = UIService.TemplateLibrary.Common.ProductAmount;
foreach (var productAmount in Model.Building.BuildingMaterials)
{
var element = productTemplate.CloneTree();
element.dataSource = productAmount;
_products.Add(element);
}
}
}
}

View File

@@ -0,0 +1,217 @@
using System;
using System.Collections.Generic;
using System.Text;
using Cysharp.Threading.Tasks;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class BuildMenuUIController : UIControllerSystem<BuildMenuUIView>, IUpdatable, IDisposable
{
[InjectService]
private IGameDatabase _gameDatabase;
[InjectService]
private IEditingService _editingService;
[InjectService]
private ISignalBus _signalBus;
[InjectService]
private World _world;
[InjectService]
private TextFormatHelper _textFormatHelper;
[InjectService]
private ICancelAction _cancelAction;
private BuildMenuModel _model;
private List<BuildingDefinition> _pendingBuildings = new();
private List<BuildingDefinition> _pendingTeasers = new();
private bool _isPlayingButtonAnimation;
private Action _onUnlockAnimationStarted;
public BuildMenuUIController(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
protected override BuildMenuUIView View => UIRoot.GetView<BuildMenuUIView>();
public override async UniTask InitializeAsync()
{
await base.InitializeAsync();
var buildings = _gameDatabase.OfType<BuildingDefinition>();
buildings.Sort((x, y) => x.UIOrder.CompareTo(y.UIOrder));
_model = new BuildMenuModel();
var sb = new StringBuilder();
foreach (var building in buildings)
{
if (_world.UnlocksState.TryGetBuildingUnlock(building, out var unlockId))
{
var unlock = _gameDatabase.WithId<UnlockDefinition>(unlockId);
_textFormatHelper.FormatUnlockConditions(unlock, sb);
}
var buildingModel = new BuildMenuBuildingModel
{
Building = building,
UnlockConditions = sb.ToString()
};
_model.Buildings.Add(buildingModel);
sb.Clear();
}
View.SetModel(_model);
View.ButtonClick += OnButtonClick;
_editingService.ActiveToolChanged += OnActiveToolChanged;
_signalBus.Subscribe<UnlockUnlockedSignal>(OnUnlockUnlocked);
foreach (var unlockId in _world.UnlocksState.Unlocked)
{
var unlock = _gameDatabase.WithId<UnlockDefinition>(unlockId);
OnUnlockUnlocked(unlock);
}
_cancelAction.AddHandler(
(int)CancelActions.CloseBuildMenu,
_ =>
{
if (!View.IsOpen()) return false;
CloseView();
return true;
});
}
public void Dispose()
{
View.ButtonClick -= OnButtonClick;
_editingService.ActiveToolChanged -= OnActiveToolChanged;
_signalBus.Unsubscribe<UnlockUnlockedSignal>(OnUnlockUnlocked);
}
private void OnButtonClick(BuildingDefinition building)
{
if (!_world.UnlocksState.UnlockedBuildings.Contains(building.RuntimeId)) return;
var buildTool = _editingService.EditingState.BuildTool;
buildTool.Building = building;
_editingService.ActivateTool(buildTool);
}
private void OnActiveToolChanged(EditTool tool)
{
BuildingDefinition selectedBuilding = null;
if (tool is BuildTool buildTool) selectedBuilding = buildTool.Building;
_model.SelectedBuilding = selectedBuilding;
}
private void OnUnlockUnlocked(UnlockDefinition unlock)
{
if (unlock.Building)
{
_pendingBuildings.Add(unlock.Building);
_pendingBuildings.Sort();
}
if (unlock.TeasedBuildings.Count > 0)
{
foreach (var teasedBuilding in unlock.TeasedBuildings) _pendingTeasers.Add(teasedBuilding);
_pendingTeasers.Sort();
}
}
private void OnUnlockUnlocked(UnlockUnlockedSignal signal)
{
OnUnlockUnlocked(signal.Unlock);
}
public void Update()
{
UpdateModels();
UpdateButtons();
}
private void UpdateModels()
{
foreach (var buildingModel in _model.Buildings) buildingModel.IsUnlocked = _world.UnlocksState.UnlockedBuildings.Contains(buildingModel.Building.RuntimeId);
}
private void UpdateButtons()
{
if (!View.IsOpen() || _isPlayingButtonAnimation || (_pendingBuildings.Count <= 0 && _pendingTeasers.Count <= 0)) return;
if (_pendingBuildings.Count > 0)
{
var building = _pendingBuildings[0];
_pendingBuildings.RemoveAt(0);
_ = UnlockButtonAsync(building);
return;
}
while (_pendingTeasers.Count > 0)
{
var teaserBuilding = _pendingTeasers[0];
_pendingTeasers.RemoveAt(0);
if (_world.UnlocksState.UnlockedBuildings.Contains(teaserBuilding.RuntimeId)) continue;
_ = CreateTeaserButtonAsync(teaserBuilding);
break;
}
}
private async UniTask UnlockButtonAsync(BuildingDefinition building)
{
_isPlayingButtonAnimation = true;
try
{
_onUnlockAnimationStarted ??= () => _signalBus.Raise(new BuildMenuButtonUnlockAnimationStartedSignal());
await View.UnlockOrCreateUnlockedButtonAsync(building, _onUnlockAnimationStarted);
await UniTask.WaitForSeconds(0.5f, true);
}
finally
{
_isPlayingButtonAnimation = false;
}
}
private async UniTask CreateTeaserButtonAsync(BuildingDefinition building)
{
_isPlayingButtonAnimation = true;
try
{
await View.CreateTeaserButtonAsync(building);
await UniTask.WaitForSeconds(0.5f, true);
}
finally
{
_isPlayingButtonAnimation = false;
}
}
protected override void CloseView(bool animate = true)
{
if (_isPlayingButtonAnimation) return;
base.CloseView(animate);
}
}
}

View File

@@ -0,0 +1,141 @@
using System;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using UnityEngine.Device;
using UnityEngine.UIElements;
namespace DanieleMarotta.RiversongCodeShowcase
{
[UIView("build-menu")]
public class BuildMenuUIView : UIView<BuildMenuModel>
{
private VisualElement _buttons;
private Dictionary<int, BuildMenuButtonUIView> _buttonViews = new();
private BuildMenuTooltipUIView _tooltip;
public event Action<BuildingDefinition> ButtonClick;
public override async UniTask InitializeAsync(UIService uiService, VisualElement rootElement)
{
await base.InitializeAsync(uiService, rootElement);
_buttons = rootElement.Q(className: "build-menu__buttons");
_tooltip = (BuildMenuTooltipUIView)await uiService.CreateView(typeof(BuildMenuTooltipUIView), rootElement.Q(className: "build-menu__tooltip"));
_tooltip.Show(false);
}
private void OnButtonClick(ClickEvent evt)
{
var element = (VisualElement)evt.currentTarget;
var building = (BuildingDefinition)element.dataSource;
ButtonClick?.Invoke(building);
}
private void OnButtonEnter(PointerEnterEvent evt)
{
var element = (VisualElement)evt.currentTarget;
var building = (BuildingDefinition)element.dataSource;
foreach (var buildingModel in Model.Buildings)
{
if (!ReferenceEquals(buildingModel.Building, building)) continue;
_tooltip.SetModel(buildingModel);
break;
}
_tooltip.Show(true);
var position = RootElement.WorldToLocal(element.worldBound.center);
_tooltip.RootElement.style.left = position.x;
_tooltip.RootElement.style.top = position.y;
_tooltip.RootElement.schedule.Execute(() =>
{
var left = _tooltip.RootElement.resolvedStyle.left;
var rect = _tooltip.RootElement.worldBound;
if (rect.xMin < 0) left -= rect.xMin;
if (rect.xMax > Screen.width) left -= rect.xMax - Screen.width;
_tooltip.RootElement.style.left = left;
});
}
private void OnButtonLeave(PointerLeaveEvent evt)
{
_tooltip.Show(false);
}
public async UniTask<BuildMenuButtonUIView> CreateTeaserButtonAsync(BuildingDefinition building)
{
var view = await CreateButtonAsync(building);
view.RootElement.RegisterCallback<PointerEnterEvent>(OnButtonEnter);
view.RootElement.RegisterCallback<PointerLeaveEvent>(OnButtonLeave);
await view.PlayRevealAnimationAsync();
return view;
}
public async UniTask UnlockOrCreateUnlockedButtonAsync(BuildingDefinition building, Action onUnlockAnimationStarted = null)
{
if (!_buttonViews.TryGetValue(building.RuntimeId, out var view))
{
view = await CreateTeaserButtonAsync(building);
await UniTask.WaitForSeconds(0.5f, true);
}
await UniTask.WaitForSeconds(0.5f, true);
onUnlockAnimationStarted?.Invoke();
await view.PlayUnlockAnimationAsync();
view.RootElement.RegisterCallback<ClickEvent>(OnButtonClick);
}
private async UniTask<BuildMenuButtonUIView> CreateButtonAsync(BuildingDefinition building)
{
var buttonTemplate = UIService.TemplateLibrary.BuildMenu.Button;
var buttonElement = buttonTemplate.CloneTree();
_buttons.Add(buttonElement);
var view = (BuildMenuButtonUIView)await UIService.CreateView(typeof(BuildMenuButtonUIView), buttonElement);
_buttonViews.Add(building.RuntimeId, view);
view.SetModel(building);
return view;
}
protected override void OnModelPropertyChanged(object sender, BindablePropertyChangedEventArgs e)
{
base.OnModelPropertyChanged(sender, e);
switch (e.propertyName)
{
case nameof(BuildMenuModel.SelectedBuilding):
UpdateButtonsSelectedState();
break;
}
}
private void UpdateButtonsSelectedState()
{
foreach (var element in _buttons.Children())
{
var button = element.Q(className: "build-menu__button");
if (ReferenceEquals(element.dataSource, Model.SelectedBuilding))
button.AddToClassList("selected");
else
button.RemoveFromClassList("selected");
}
}
}
}

View File

@@ -0,0 +1,64 @@
using Unity.Properties;
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class BuildingPanelModel : UIModel
{
private Building _building;
private string _buildingName;
private string _buildingDescription;
private Sprite _buildingIcon;
private bool _showNoHouseNearbyWarning;
public Building Building
{
get => _building;
private set => SetProperty(ref _building, value);
}
[CreateProperty]
public string BuildingDescription
{
get => _buildingDescription;
private set => SetProperty(ref _buildingDescription, value);
}
[CreateProperty]
public string BuildingName
{
get => _buildingName;
private set => SetProperty(ref _buildingName, value);
}
[CreateProperty]
public Sprite BuildingIcon
{
get => _buildingIcon;
private set => SetProperty(ref _buildingIcon, value);
}
[CreateProperty]
public bool ShowNoHouseNearbyWarning
{
get => _showNoHouseNearbyWarning;
set => SetProperty(ref _showNoHouseNearbyWarning, value);
}
public HousePanelModel HouseModel { get; } = new();
public StoragePanelModel StorageModel { get; } = new();
public void SetBuilding(Building building)
{
Building = building;
BuildingName = Building?.Definition.BuildingName;
BuildingDescription = Building?.Definition.BuildingDescription;
BuildingIcon = Building?.Definition.Icon;
}
}
}

View File

@@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class BuildingPanelUIController : UIControllerSystem<BuildingPanelUIView>, IDisposable, IUpdatable
{
[InjectService]
private ISignalBus _signalBus;
[InjectService]
private IProductCatalog _productCatalog;
[InjectService]
private UIState _uiState;
[InjectService]
private World _world;
private BuildingPanelModel _model;
private List<IBuildingPanelSectionPresenter> _sectionPresenters;
public BuildingPanelUIController(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
protected override BuildingPanelUIView View => UIRoot.GetView<BuildingPanelUIView>();
public override async UniTask InitializeAsync()
{
await base.InitializeAsync();
_model = new BuildingPanelModel();
_sectionPresenters = new List<IBuildingPanelSectionPresenter>
{
new HouseBuildingPanelSectionPresenter(_productCatalog),
new StorageBuildingPanelSectionPresenter(_productCatalog)
};
View.SetModel(_model);
View.Show(false);
_signalBus.Subscribe<SelectedBuildingChangedSignal>(OnSelectedBuildingChanged);
_signalBus.Subscribe<BuildingUpgradedSignal>(OnBuildingUpgraded);
}
public void Dispose()
{
_signalBus.Unsubscribe<SelectedBuildingChangedSignal>(OnSelectedBuildingChanged);
_signalBus.Unsubscribe<BuildingUpgradedSignal>(OnBuildingUpgraded);
}
private void OnSelectedBuildingChanged(SelectedBuildingChangedSignal signal)
{
var selectedBuilding = signal.NewSelection;
if (selectedBuilding != null)
{
_model.SetBuilding(selectedBuilding);
InitializeSections(selectedBuilding);
UpdateWorkerHousingWarning(selectedBuilding);
}
View.Show(selectedBuilding != null, true);
}
private void InitializeSections(Building building)
{
foreach (var sectionPresenter in _sectionPresenters)
{
if (!sectionPresenter.IsSectionRelevant(building)) continue;
sectionPresenter.InitializeSection(_model, building);
}
}
public void Update()
{
var building = _model.Building;
if (building == null) return;
foreach (var sectionPresenter in _sectionPresenters)
{
if (!sectionPresenter.IsSectionRelevant(building)) continue;
sectionPresenter.UpdateSection(_model, building);
}
UpdateWorkerHousingWarning(building);
}
private void OnBuildingUpgraded(BuildingUpgradedSignal signal)
{
if (signal.Building != _uiState.SelectedBuilding) return;
InitializeSections(signal.Building);
}
private void UpdateWorkerHousingWarning(Building building)
{
_model.ShowNoHouseNearbyWarning = false;
if (building == null || _world.TimeState.DayNightCycleStep != DayNightCycleStep.Day) return;
ref var sleepState = ref building.GetSleepStateRW();
_model.ShowNoHouseNearbyWarning = sleepState.HasHomelessWorkers;
}
}
}

View File

@@ -0,0 +1,57 @@
using Cysharp.Threading.Tasks;
using UnityEngine.UIElements;
namespace DanieleMarotta.RiversongCodeShowcase
{
[UIView("building-panel")]
public class BuildingPanelUIView : UIView<BuildingPanelModel>
{
private VisualElement _noHouseNearbyWarning;
private HousePanelUIView _housePanel;
private StoragePanelUIView _storagePanel;
public override async UniTask InitializeAsync(UIService uiService, VisualElement rootElement)
{
await base.InitializeAsync(uiService, rootElement);
_noHouseNearbyWarning = rootElement.Q<VisualElement>(className: "building-panel__no-house-nearby-warning");
_housePanel = (HousePanelUIView)await uiService.CreateView(typeof(HousePanelUIView), rootElement.Q(className: "building-panel__house-panel"));
_storagePanel = (StoragePanelUIView)await uiService.CreateView(typeof(StoragePanelUIView), rootElement.Q(className: "building-panel__storage-panel"));
}
protected override void OnNewModel(BuildingPanelModel model)
{
base.OnNewModel(model);
_housePanel.SetModel(model.HouseModel);
_storagePanel.SetModel(model.StorageModel);
}
protected override void OnModelPropertyChanged(object sender, BindablePropertyChangedEventArgs e)
{
base.OnModelPropertyChanged(sender, e);
switch (e.propertyName)
{
case nameof(BuildingPanelModel.Building):
_housePanel.Show(Model.Building != null && Model.Building.Definition.IsHouse);
_storagePanel.Show(Model.Building != null && Model.Building.Definition.IsStorage);
UpdateNoHouseNearbyWarning();
break;
case nameof(BuildingPanelModel.ShowNoHouseNearbyWarning):
UpdateNoHouseNearbyWarning();
break;
}
}
private void UpdateNoHouseNearbyWarning()
{
_noHouseNearbyWarning.style.display = Model.Building != null && Model.ShowNoHouseNearbyWarning ? DisplayStyle.Flex : DisplayStyle.None;
}
}
}

View File

@@ -0,0 +1,92 @@
using Unity.Mathematics;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class HouseBuildingPanelSectionPresenter : IBuildingPanelSectionPresenter
{
private readonly IProductCatalog _productCatalog;
public HouseBuildingPanelSectionPresenter(IProductCatalog productCatalog)
{
_productCatalog = productCatalog;
}
public bool IsSectionRelevant(Building building)
{
return building.Definition.IsHouse;
}
public void InitializeSection(BuildingPanelModel model, Building building)
{
var tiers = building.Definition.HouseTiers;
var tierIndex = building.TierIndex;
var maxTier = math.min(tierIndex, tiers.Count - 1);
ref var needsState = ref building.GetNeedsStateRW();
model.HouseModel.Needs.Clear();
var rowIndex = 0;
for (var i = 0; i <= maxTier; i++)
{
var tier = tiers[i];
foreach (var need in tier.Needs)
{
var needModel = new HousePanelNeedModel();
switch (need.Type)
{
case PopulationNeedType.Product:
needModel.Initialize(rowIndex++, need.Product.ProductName, need.Product.Icon, 0, need.YieldOnFetch);
break;
}
model.HouseModel.Needs.Add(needModel);
}
}
model.HouseModel.AllNeedsMet = needsState.AllNeedsMet;
model.HouseModel.UpgradeState = needsState.UpgradeState;
model.HouseModel.UpgradeMaterials.Clear();
if (tierIndex < tiers.Count)
foreach (var productAmount in tiers[tierIndex].UpgradeMaterials)
model.HouseModel.UpgradeMaterials.Add(new HousePanelUpgradeMaterialModel(productAmount));
model.HouseModel.NotifyChanged();
}
public void UpdateSection(BuildingPanelModel model, Building building)
{
ref var needsState = ref building.GetNeedsStateRW();
ref var storage = ref building.GetStorageRW();
for (var i = 0; i < needsState.Needs.Length; i++)
{
var need = needsState.Needs[i];
if (need.TierIndex > building.TierIndex) break;
var needModel = model.HouseModel.Needs[i];
switch (need.Type)
{
case PopulationNeedType.Product:
needModel.UpdateValue(need.Current);
break;
}
}
model.HouseModel.AllNeedsMet = needsState.AllNeedsMet;
model.HouseModel.UpgradeState = needsState.UpgradeState;
foreach (var upgradeMaterialModel in model.HouseModel.UpgradeMaterials)
{
var productHandle = _productCatalog.GetHandle(upgradeMaterialModel.Product);
upgradeMaterialModel.Remaining = math.max(upgradeMaterialModel.Needed - storage.AvailableNow(productHandle), 0);
upgradeMaterialModel.Done &= needsState.UpgradeState != TierUpgradeState.NotReady;
upgradeMaterialModel.Done |= upgradeMaterialModel.Remaining <= 0;
}
}
}
}

View File

@@ -0,0 +1,27 @@
using System.Collections.Generic;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class HousePanelModel : UIModel
{
private bool _allNeedsMet;
private TierUpgradeState _upgradeState;
public List<HousePanelNeedModel> Needs { get; } = new();
public bool AllNeedsMet
{
get => _allNeedsMet;
set => SetProperty(ref _allNeedsMet, value);
}
public TierUpgradeState UpgradeState
{
get => _upgradeState;
set => SetProperty(ref _upgradeState, value);
}
public List<HousePanelUpgradeMaterialModel> UpgradeMaterials { get; } = new();
}
}

View File

@@ -0,0 +1,43 @@
using Unity.Properties;
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class HousePanelNeedModel : UIModel
{
private int _value;
private int _max;
public int RowIndex { get; set; }
[CreateProperty] public string Name { get; set; }
[CreateProperty] public Sprite Icon { get; set; }
[CreateProperty] public int Value { get; set; }
[CreateProperty] public int Max { get; set; }
[CreateProperty] public string ValueString => $"{Mathf.RoundToInt(100 * Mathf.Clamp01(Value / (float)Max))}%";
public void Initialize(int rowIndex, string name, Sprite icon, int value, int max)
{
RowIndex = rowIndex;
Name = name;
Icon = icon;
Value = value;
Max = max;
}
public void UpdateValue(int value)
{
if (Value == value) return;
Value = value;
NotifyPropertyChanged(nameof(Value));
NotifyPropertyChanged(nameof(ValueString));
}
}
}

View File

@@ -0,0 +1,47 @@
using Cysharp.Threading.Tasks;
using Unity.Mathematics;
using UnityEngine.UIElements;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class HousePanelNeedUIView : UIView<HousePanelNeedModel>
{
private VisualElement _valueBarFill;
public override UniTask InitializeAsync(UIService uiService, VisualElement rootElement)
{
base.InitializeAsync(uiService, rootElement);
_valueBarFill = rootElement.Q<VisualElement>(className: "house-panel__need-value-bar-fill");
return UniTask.CompletedTask;
}
protected override void OnNewModel(HousePanelNeedModel model)
{
base.OnNewModel(model);
if (model.RowIndex % 2 == 0) RootElement.Q(className: "house-panel__need").AddToClassList("alt");
UpdateValueBar();
}
protected override void OnModelPropertyChanged(object sender, BindablePropertyChangedEventArgs e)
{
base.OnModelPropertyChanged(sender, e);
switch (e.propertyName)
{
case nameof(HousePanelNeedModel.Value):
UpdateValueBar();
break;
}
}
private void UpdateValueBar()
{
var fillPercent = math.clamp((float)Model.Value / Model.Max, 0, 1) * 100;
_valueBarFill.style.width = Length.Percent(fillPercent);
}
}
}

View File

@@ -0,0 +1,116 @@
using Cysharp.Threading.Tasks;
using UnityEngine.UIElements;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class HousePanelUIView : UIView<HousePanelModel>
{
private VisualElement _needs;
private Label _notReadyNeedsNotMetLabel;
private Label _notReadyNeedsMetLabel;
private Label _fetchingMaterialsLabel;
private Label _maxedOutLabel;
private VisualElement _upgradeMaterials;
public override UniTask InitializeAsync(UIService uiService, VisualElement rootElement)
{
base.InitializeAsync(uiService, rootElement);
_needs = rootElement.Q<VisualElement>(className: "house-panel__needs");
_notReadyNeedsNotMetLabel = rootElement.Q<Label>(className: "house-panel__not-ready-needs-not-met");
_notReadyNeedsMetLabel = rootElement.Q<Label>(className: "house-panel__not-ready-needs-met");
_fetchingMaterialsLabel = rootElement.Q<Label>(className: "house-panel__fetching-materials");
_maxedOutLabel = rootElement.Q<Label>(className: "house-panel__maxed-out");
_upgradeMaterials = rootElement.Q<VisualElement>(className: "house-panel__upgrade-materials");
_ = AnimateHouseUpgradeStatusAsync();
return UniTask.CompletedTask;
}
protected override void OnModelChanged()
{
base.OnModelChanged();
_ = CreateNeedViewsAsync();
_ = CreateUpgradeMaterialViewsAsync();
UpdateUpgradeState();
}
private async UniTask CreateNeedViewsAsync()
{
var template = UIService.TemplateLibrary.BuildingPanel.Need;
await UIService.CreateViews<HousePanelNeedModel, HousePanelNeedUIView>(Model.Needs, _needs, template);
}
private async UniTask CreateUpgradeMaterialViewsAsync()
{
var template = UIService.TemplateLibrary.BuildingPanel.UpgradeMaterial;
await UIService.CreateViews<HousePanelUpgradeMaterialModel, HousePanelUpgradeMaterialUIView>(Model.UpgradeMaterials, _upgradeMaterials, template);
}
private void UpdateUpgradeState()
{
_notReadyNeedsNotMetLabel.style.display = DisplayStyle.None;
_notReadyNeedsMetLabel.style.display = DisplayStyle.None;
_fetchingMaterialsLabel.style.display = DisplayStyle.None;
_maxedOutLabel.style.display = DisplayStyle.None;
switch (Model.UpgradeState)
{
case TierUpgradeState.NotReady:
_notReadyNeedsNotMetLabel.style.display = Model.AllNeedsMet ? DisplayStyle.None : DisplayStyle.Flex;
_notReadyNeedsMetLabel.style.display = Model.AllNeedsMet ? DisplayStyle.Flex : DisplayStyle.None;
break;
case TierUpgradeState.FetchingMaterials:
case TierUpgradeState.AllMaterialsFetched:
_fetchingMaterialsLabel.style.display = DisplayStyle.Flex;
break;
case TierUpgradeState.MaxedOut:
_maxedOutLabel.style.display = DisplayStyle.Flex;
break;
}
}
protected override void OnModelPropertyChanged(object sender, BindablePropertyChangedEventArgs e)
{
base.OnModelPropertyChanged(sender, e);
switch (e.propertyName)
{
case nameof(HousePanelModel.AllNeedsMet):
case nameof(HousePanelModel.UpgradeState):
UpdateUpgradeState();
break;
}
}
private async UniTask AnimateHouseUpgradeStatusAsync()
{
_fetchingMaterialsLabel.text = _fetchingMaterialsLabel.text.Replace(".", string.Empty);
var i = 0;
while (_fetchingMaterialsLabel.panel != null)
{
await UniTask.WaitForSeconds(1, true);
var text = _fetchingMaterialsLabel.text;
text = text.Substring(0, text.Length - i);
i = (i + 1) % 4;
if (i > 0) text += new string('.', i);
_fetchingMaterialsLabel.text = text;
}
}
}
}

View File

@@ -0,0 +1,42 @@
using Unity.Properties;
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class HousePanelUpgradeMaterialModel : UIModel
{
private int _remaining;
private bool _done;
public HousePanelUpgradeMaterialModel(ProductDefinition product, int needed)
{
Product = product;
Needed = needed;
}
public HousePanelUpgradeMaterialModel(IProductAmount productAmount) : this(productAmount.Product, productAmount.Amount)
{
}
public ProductDefinition Product { get; }
[CreateProperty] public Sprite Icon => Product.Icon;
[CreateProperty]
public int Remaining
{
get => _remaining;
set => SetProperty(ref _remaining, value);
}
[CreateProperty] public int Needed { get; }
[CreateProperty]
public bool Done
{
get => _done;
set => SetProperty(ref _done, value);
}
}
}

View File

@@ -0,0 +1,37 @@
using Cysharp.Threading.Tasks;
using UnityEngine.UIElements;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class HousePanelUpgradeMaterialUIView : UIView<HousePanelUpgradeMaterialModel>
{
private VisualElement _remaining;
private VisualElement _checkIcon;
public override UniTask InitializeAsync(UIService uiService, VisualElement rootElement)
{
base.InitializeAsync(uiService, rootElement);
_remaining = rootElement.Q<VisualElement>(className: "house-panel__remaining");
_checkIcon = rootElement.Q<VisualElement>(className: "house-panel__upgrade-material-check");
_checkIcon.style.display = DisplayStyle.None;
return UniTask.CompletedTask;
}
protected override void OnModelPropertyChanged(object sender, BindablePropertyChangedEventArgs e)
{
base.OnModelPropertyChanged(sender, e);
switch (e.propertyName)
{
case nameof(HousePanelUpgradeMaterialModel.Done):
_remaining.style.display = Model.Done ? DisplayStyle.None : DisplayStyle.Flex;
_checkIcon.style.display = Model.Done ? DisplayStyle.Flex : DisplayStyle.None;
break;
}
}
}
}

View File

@@ -0,0 +1,11 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public interface IBuildingPanelSectionPresenter
{
bool IsSectionRelevant(Building building);
void InitializeSection(BuildingPanelModel model, Building building);
void UpdateSection(BuildingPanelModel model, Building building);
}
}

View File

@@ -0,0 +1,95 @@
using UnityEngine;
using UnityEngine.InputSystem;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class StorageBuildingPanelSectionPresenter : IBuildingPanelSectionPresenter
{
private const int RequestedAmountLargeDelta = 5;
private readonly IProductCatalog _productCatalog;
public StorageBuildingPanelSectionPresenter(IProductCatalog productCatalog)
{
_productCatalog = productCatalog;
}
public bool IsSectionRelevant(Building building)
{
return building.Definition.IsStorage;
}
public void InitializeSection(BuildingPanelModel model, Building building)
{
model.StorageModel.Products.Clear();
for (var productHandle = 0; productHandle < _productCatalog.ProductTypeCount; productHandle++)
{
var productModel = new StoragePanelProductModel();
productModel.Initialize(
productHandle,
productHandle,
_productCatalog.GetProduct(productHandle),
() => ToggleAllowTaking(building, productModel),
() => ToggleCanFulfillRequests(building, productModel),
() => ChangeRequestedAmount(building, productModel, Keyboard.current.shiftKey.isPressed ? -RequestedAmountLargeDelta : -1),
() => ChangeRequestedAmount(building, productModel, Keyboard.current.shiftKey.isPressed ? RequestedAmountLargeDelta : 1));
model.StorageModel.Products.Add(productModel);
}
UpdateSection(model, building);
model.StorageModel.NotifyChanged();
}
public void UpdateSection(BuildingPanelModel model, Building building)
{
ref var storage = ref building.GetStorageRW();
ref var storagePolicy = ref building.GetProductStoragePolicyRW();
foreach (var productModel in model.StorageModel.Products)
{
var productHandle = productModel.ProductHandle;
productModel.AvailableNow = storage.AvailableNow(productHandle);
productModel.InputBlocked = !storagePolicy.IsTakingAllowed(productHandle);
productModel.CanFulfillRequests = storagePolicy.CanFulfillRequests(productHandle);
productModel.RequestedAmount = storagePolicy.GetRequestedAmount(productHandle);
}
}
private static void ToggleAllowTaking(Building building, StoragePanelProductModel productModel)
{
ref var storagePolicy = ref building.GetProductStoragePolicyRW();
var isTakingAllowed = storagePolicy.IsTakingAllowed(productModel.ProductHandle);
storagePolicy.SetAllowTaking(productModel.ProductHandle, !isTakingAllowed);
productModel.InputBlocked = isTakingAllowed;
}
private static void ToggleCanFulfillRequests(Building building, StoragePanelProductModel productModel)
{
ref var storagePolicy = ref building.GetProductStoragePolicyRW();
var canFulfillRequests = storagePolicy.CanFulfillRequests(productModel.ProductHandle);
storagePolicy.SetCanFulfillRequests(productModel.ProductHandle, !canFulfillRequests);
productModel.CanFulfillRequests = !canFulfillRequests;
}
private static void ChangeRequestedAmount(Building building, StoragePanelProductModel productModel, int delta)
{
ref var storage = ref building.GetStorageRW();
ref var storagePolicy = ref building.GetProductStoragePolicyRW();
var requestedAmount = storagePolicy.GetRequestedAmount(productModel.ProductHandle) + delta;
requestedAmount = Mathf.Clamp(requestedAmount, 0, storage.Capacity);
storagePolicy.SetRequestedAmount(productModel.ProductHandle, requestedAmount);
productModel.RequestedAmount = requestedAmount;
}
}
}

View File

@@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class StoragePanelModel : UIModel
{
public List<StoragePanelProductModel> Products { get; } = new();
}
}

View File

@@ -0,0 +1,77 @@
using System;
using Unity.Properties;
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class StoragePanelProductModel : UIModel
{
private int _availableNow;
private bool _inputBlocked;
private bool _canFulfillRequests;
private int _requestedAmount;
public int RowIndex { get; set; }
public int ProductHandle { get; private set; }
[CreateProperty] public string Name { get; private set; }
[CreateProperty] public Sprite Icon { get; private set; }
[CreateProperty]
public int AvailableNow
{
get => _availableNow;
set => SetProperty(ref _availableNow, value);
}
public bool InputBlocked
{
get => _inputBlocked;
set => SetProperty(ref _inputBlocked, value);
}
public bool CanFulfillRequests
{
get => _canFulfillRequests;
set => SetProperty(ref _canFulfillRequests, value);
}
[CreateProperty]
public int RequestedAmount
{
get => _requestedAmount;
set => SetProperty(ref _requestedAmount, value);
}
public Action ToggleInputBlockedCallback { get; private set; }
public Action ToggleCanFulfillRequestsCallback { get; private set; }
public Action DecreaseRequestedAmountCallback { get; private set; }
public Action IncreaseRequestedAmountCallback { get; private set; }
public void Initialize(int rowIndex,
int productHandle,
ProductDefinition product,
Action toggleAllowTakingCallback,
Action toggleCanFulfillRequestsCallback,
Action decreaseRequestedAmountCallback,
Action increaseRequestedAmountCallback)
{
RowIndex = rowIndex;
ProductHandle = productHandle;
Name = product.ProductName;
Icon = product.Icon;
ToggleInputBlockedCallback = toggleAllowTakingCallback;
ToggleCanFulfillRequestsCallback = toggleCanFulfillRequestsCallback;
DecreaseRequestedAmountCallback = decreaseRequestedAmountCallback;
IncreaseRequestedAmountCallback = increaseRequestedAmountCallback;
}
}
}

View File

@@ -0,0 +1,89 @@
using Cysharp.Threading.Tasks;
using UnityEngine.UIElements;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class StoragePanelProductUIView : UIView<StoragePanelProductModel>
{
private VisualElement _inputBlockedButton;
private VisualElement _fulfillRequestsButton;
private Button _decreaseRequestedAmountButton;
private Button _increaseRequestedAmountButton;
public override UniTask InitializeAsync(UIService uiService, VisualElement rootElement)
{
base.InitializeAsync(uiService, rootElement);
_inputBlockedButton = rootElement.Q(className: "storage-panel__product-input-block-button");
_fulfillRequestsButton = rootElement.Q(className: "storage-panel__product-fulfill-requests-button");
_decreaseRequestedAmountButton = rootElement.Q<Button>(className: "storage-panel__product-requested-amount-button-minus");
_increaseRequestedAmountButton = rootElement.Q<Button>(className: "storage-panel__product-requested-amount-button-plus");
_inputBlockedButton.RegisterCallback<ClickEvent>(OnInputBlockedClick);
_fulfillRequestsButton.RegisterCallback<ClickEvent>(OnFulfillRequestsClick);
_decreaseRequestedAmountButton.RegisterCallback<ClickEvent>(OnDecreaseRequestedAmountClick);
_increaseRequestedAmountButton.RegisterCallback<ClickEvent>(OnIncreaseRequestedAmountClick);
return UniTask.CompletedTask;
}
protected override void OnNewModel(StoragePanelProductModel model)
{
base.OnNewModel(model);
if (model.RowIndex % 2 == 0) RootElement.Q(className: "storage-panel__product").AddToClassList("alt");
UpdateSelectedState();
}
protected override void OnModelPropertyChanged(object sender, BindablePropertyChangedEventArgs e)
{
base.OnModelPropertyChanged(sender, e);
switch (e.propertyName)
{
case nameof(StoragePanelProductModel.InputBlocked):
case nameof(StoragePanelProductModel.CanFulfillRequests):
UpdateSelectedState();
break;
}
}
private void OnInputBlockedClick(ClickEvent evt)
{
Model.ToggleInputBlockedCallback?.Invoke();
}
private void OnFulfillRequestsClick(ClickEvent evt)
{
Model.ToggleCanFulfillRequestsCallback?.Invoke();
}
private void OnDecreaseRequestedAmountClick(ClickEvent evt)
{
Model.DecreaseRequestedAmountCallback?.Invoke();
}
private void OnIncreaseRequestedAmountClick(ClickEvent evt)
{
Model.IncreaseRequestedAmountCallback?.Invoke();
}
private void UpdateSelectedState()
{
UpdateSelected(_inputBlockedButton, Model.InputBlocked);
UpdateSelected(_fulfillRequestsButton, Model.CanFulfillRequests);
}
private static void UpdateSelected(VisualElement element, bool isSelected)
{
if (isSelected)
element.AddToClassList("selected");
else
element.RemoveFromClassList("selected");
}
}
}

View File

@@ -0,0 +1,27 @@
using Cysharp.Threading.Tasks;
using UnityEngine.UIElements;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class StoragePanelUIView : UIView<StoragePanelModel>
{
private VisualElement _products;
public override UniTask InitializeAsync(UIService uiService, VisualElement rootElement)
{
base.InitializeAsync(uiService, rootElement);
_products = rootElement.Q<VisualElement>(className: "storage-panel__products");
return UniTask.CompletedTask;
}
protected override void OnModelChanged()
{
base.OnModelChanged();
var template = UIService.TemplateLibrary.BuildingPanel.StorageProduct;
_ = UIService.CreateViews<StoragePanelProductModel, StoragePanelProductUIView>(Model.Products, _products, template);
}
}
}

View File

@@ -0,0 +1,81 @@
using System;
using Cysharp.Threading.Tasks;
using UnityEngine.InputSystem;
using UnityEngine.UIElements;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class BuildingPlacementTooltipController : UIControllerSystem<BuildingPlacementTooltipUIView>, IUpdatable
{
[InjectService]
private IEditingService _editingService;
[InjectService]
private IPointerService _pointerService;
[InjectService]
private TextFormatHelper _textFormatHelper;
public BuildingPlacementTooltipController(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
protected override BuildingPlacementTooltipUIView View => UIRoot.GetView<BuildingPlacementTooltipUIView>();
public override async UniTask InitializeAsync()
{
await base.InitializeAsync();
View.RootElement.SetPickingModeRecursive(PickingMode.Ignore);
}
public void Update()
{
if (_pointerService.IsPointerOverUI)
{
View.Clear();
return;
}
var editingState = _editingService.EditingState;
if (editingState.ActiveTool != editingState.BuildTool)
{
View.Clear();
return;
}
var validation = editingState.BuildTool.GetLastValidationResult();
if (validation == EditToolValidationResult.Success)
{
View.Clear();
return;
}
var buildingName = _textFormatHelper.FormatImportantText(editingState.BuildTool.Building.BuildingName);
var errorFormat = GetFailedValidationFormat(validation);
var text = string.Format(errorFormat, buildingName);
View.SetText(text);
View.RootElement.SetAbsoluteScreenPosition(Mouse.current.position.ReadValue());
}
private string GetFailedValidationFormat(EditToolValidationResult validation)
{
switch (validation)
{
case EditToolValidationResult.BlockedTile:
return "There is an obstacle here";
case EditToolValidationResult.CanOnlyBePlacedNearWater:
return "The {0} must be placed near water";
case EditToolValidationResult.CanOnlyBePlacedOnFertileGround:
return "The {0} must be placed on fertile ground";
default:
throw new ArgumentOutOfRangeException();
}
}
}
}

View File

@@ -0,0 +1,31 @@
using Cysharp.Threading.Tasks;
using UnityEngine.UIElements;
namespace DanieleMarotta.RiversongCodeShowcase
{
[UIView("building-placement-tooltip")]
public class BuildingPlacementTooltipUIView : UIView
{
private Label _tooltipText;
public override UniTask InitializeAsync(UIService uiService, VisualElement rootElement)
{
base.InitializeAsync(uiService, rootElement);
_tooltipText = rootElement.Q<Label>();
return UniTask.CompletedTask;
}
public void SetText(string text)
{
_tooltipText.text = text;
Show(true, true);
}
public void Clear()
{
Show(false, true);
}
}
}

View File

@@ -0,0 +1,277 @@
using System;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.TextCore.Text;
using UnityEngine.UIElements;
namespace DanieleMarotta.RiversongCodeShowcase
{
[GameSystemGroup(typeof(UISystemGroup))]
[InitializeAfter(typeof(UIInitializationSystem))]
public class DebugPanelUIControllerSystem : UIControllerSystem, IUpdatable, IDisposable
{
[InjectService]
private GameConfig _config;
[InjectService]
private ISignalBus _signalBus;
[InjectService]
private IPointerService _pointerService;
[InjectService]
private ITileSpace _tileSpace;
[InjectService]
private World _world;
private UIElementReferences _elements = new();
private FontAsset _font;
private Building _selectedBuilding;
private bool _isVisible;
public DebugPanelUIControllerSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public override async UniTask InitializeAsync()
{
await base.InitializeAsync();
_font = await _config.UI.DebugFont.LoadAssetAsync<FontAsset>();
_elements.PanelRoot = CreatePanelRoot();
InitializePanelContent();
UIRoot.RootVisualElement.Add(_elements.PanelRoot);
_signalBus.Subscribe<SelectedBuildingChangedSignal>(OnSelectedBuildingChanged);
}
public void Dispose()
{
_signalBus.Unsubscribe<SelectedBuildingChangedSignal>(OnSelectedBuildingChanged);
}
private void InitializePanelContent()
{
_elements.PanelRoot.Add(CreateHeader("DEBUG PANEL"));
InitializePointerSection();
InitializePopulationSection();
InitializeSelectedBuildingSection();
}
private void InitializePointerSection()
{
_elements.TileCoordsLabel = CreateLabel("Tile: ");
_elements.PanelRoot.Add(_elements.TileCoordsLabel);
}
private void InitializePopulationSection()
{
_elements.PanelRoot.Add(CreateHeader("POPULATION"));
_elements.PopulationCapacityLabel = CreateLabel("Capacity: ");
_elements.PanelRoot.Add(_elements.PopulationCapacityLabel);
_elements.PopulationGrowthAccumulatorLabel = CreateLabel("Growth Accumulator: ");
_elements.PanelRoot.Add(_elements.PopulationGrowthAccumulatorLabel);
}
private void InitializeSelectedBuildingSection()
{
_elements.PanelRoot.Add(CreateHeader("SELECTED BUILDING"));
_elements.SelectedBuildingFallbackLabel = CreateLabel("No building selected");
_elements.PanelRoot.Add(_elements.SelectedBuildingFallbackLabel);
_elements.SelectedBuildingNameLabel = CreateLabel("Building: ");
_elements.PanelRoot.Add(_elements.SelectedBuildingNameLabel);
InitializeSelectedHouseContent();
}
private void InitializeSelectedHouseContent()
{
_elements.SelectedHouseContent = new VisualElement { style = { display = DisplayStyle.None } };
_elements.SelectedBuildingTierLabel = CreateLabel("Tier: ");
_elements.SelectedHouseContent.Add(_elements.SelectedBuildingTierLabel);
_elements.SelectedBuildingHappinessLabel = CreateLabel("Happiness: ");
_elements.SelectedHouseContent.Add(_elements.SelectedBuildingHappinessLabel);
_elements.SelectedBuildingMaxHappinessScoreLabel = CreateLabel("Max Happiness Score: ");
_elements.SelectedHouseContent.Add(_elements.SelectedBuildingMaxHappinessScoreLabel);
_elements.SelectedBuildingOverallWeightLabel = CreateLabel("Overall Weight: ");
_elements.SelectedHouseContent.Add(_elements.SelectedBuildingOverallWeightLabel);
_elements.SelectedBuildingAllNeedsMetLabel = CreateLabel("All Needs Met: ");
_elements.SelectedHouseContent.Add(_elements.SelectedBuildingAllNeedsMetLabel);
_elements.SelectedBuildingNeedsMetForWeeksLabel = CreateLabel("Needs Met For Weeks: ");
_elements.SelectedHouseContent.Add(_elements.SelectedBuildingNeedsMetForWeeksLabel);
_elements.PanelRoot.Add(_elements.SelectedHouseContent);
}
public void Update()
{
if (Keyboard.current.f9Key.wasPressedThisFrame)
{
_isVisible = !_isVisible;
_elements.PanelRoot.style.display = _isVisible ? DisplayStyle.Flex : DisplayStyle.None;
}
if (!_isVisible) return;
UpdatePointerSection();
UpdatePopulationSection();
UpdateSelectedBuildingSection();
}
private void UpdatePointerSection()
{
if (_pointerService.TryGetPositionOnTerrain(out var position))
{
var tile = _tileSpace.WorldToTile(position);
_elements.TileCoordsLabel.text = $"Tile: {tile.x}, {tile.y}";
}
else
{
_elements.TileCoordsLabel.text = "Tile: -";
}
}
private void UpdatePopulationSection()
{
var populationState = _world.PopulationState;
_elements.PopulationCapacityLabel.text = $"Capacity: {populationState.PopulationCapacity}";
_elements.PopulationGrowthAccumulatorLabel.text = $"Growth Accumulator: {populationState.GrowthAccumulator:0.000}";
}
private void UpdateSelectedBuildingSection()
{
var anyBuildingSelected = _selectedBuilding != null;
var houseSelected = anyBuildingSelected && _selectedBuilding.Definition.IsHouse;
_elements.SelectedBuildingFallbackLabel.style.display = anyBuildingSelected ? DisplayStyle.None : DisplayStyle.Flex;
_elements.SelectedBuildingNameLabel.style.display = anyBuildingSelected ? DisplayStyle.Flex : DisplayStyle.None;
_elements.SelectedHouseContent.style.display = houseSelected ? DisplayStyle.Flex : DisplayStyle.None;
if (!anyBuildingSelected) return;
_elements.SelectedBuildingNameLabel.text = $"Building: {_selectedBuilding.Definition.BuildingName} ({_selectedBuilding.Id})";
if (houseSelected) UpdateSelectedHouseInfo();
}
private void UpdateSelectedHouseInfo()
{
ref var needsState = ref _selectedBuilding.GetNeedsStateRW();
_elements.SelectedBuildingTierLabel.text = $"Tier: {_selectedBuilding.TierIndex}";
_elements.SelectedBuildingHappinessLabel.text = $"Happiness: {needsState.Happiness:0.000}";
_elements.SelectedBuildingMaxHappinessScoreLabel.text = $"Max Happiness Score: {Mathf.FloorToInt(needsState.MaxHappinessScore)}";
_elements.SelectedBuildingOverallWeightLabel.text = $"Overall Weight: {needsState.OverallHappinessWeight:0.000}";
_elements.SelectedBuildingAllNeedsMetLabel.text = $"All Needs Met: {needsState.AllNeedsMet}";
_elements.SelectedBuildingNeedsMetForWeeksLabel.text = $"Needs Met For Weeks: {needsState.NeedsMetForWeeks}";
}
private void OnSelectedBuildingChanged(SelectedBuildingChangedSignal signal)
{
_selectedBuilding = signal.NewSelection;
UpdateSelectedBuildingSection();
}
private VisualElement CreatePanelRoot()
{
const int panelWidth = 350;
var panelRoot = new VisualElement
{
name = "debug-panel",
pickingMode = PickingMode.Position,
style =
{
position = Position.Absolute,
top = 16,
left = Screen.width - panelWidth - 16,
width = panelWidth,
paddingTop = 12,
paddingRight = 12,
paddingBottom = 12,
paddingLeft = 12,
backgroundColor = new StyleColor(new Color(0, 0, 0, 0.7f)),
borderTopLeftRadius = 8,
borderTopRightRadius = 8,
borderBottomLeftRadius = 8,
borderBottomRightRadius = 8,
display = DisplayStyle.None
}
};
panelRoot.AddToClassList("drag-target");
UIRoot.MakeDraggable(panelRoot);
return panelRoot;
}
private Label CreateLabel(string text, FontAsset font = null, int fontSize = 14, FontStyle fontStyle = FontStyle.Normal, int marginBottom = 0)
{
var label = new Label(text);
label.pickingMode = PickingMode.Ignore;
label.style.color = Color.white;
label.style.unityFontDefinition = FontDefinition.FromSDFFont(font ?? _font);
label.style.unityFontStyleAndWeight = fontStyle;
label.style.fontSize = fontSize;
label.style.marginBottom = marginBottom;
return label;
}
private Label CreateHeader(string text)
{
var header = CreateLabel(text, fontSize: 16, fontStyle: FontStyle.Bold, marginBottom: 8);
header.pickingMode = PickingMode.Position;
header.AddToClassList("drag-handle");
return header;
}
private class UIElementReferences
{
public VisualElement PanelRoot;
public Label TileCoordsLabel;
public Label PopulationCapacityLabel;
public Label PopulationGrowthAccumulatorLabel;
public Label SelectedBuildingFallbackLabel;
public VisualElement SelectedHouseContent;
public Label SelectedBuildingNameLabel;
public Label SelectedBuildingTierLabel;
public Label SelectedBuildingHappinessLabel;
public Label SelectedBuildingMaxHappinessScoreLabel;
public Label SelectedBuildingOverallWeightLabel;
public Label SelectedBuildingAllNeedsMetLabel;
public Label SelectedBuildingNeedsMetForWeeksLabel;
}
}
}

View File

@@ -0,0 +1,45 @@
using System;
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class DemoPanelUIController : UIControllerSystem<DemoPanelUIView>, IDisposable
{
[InjectService]
private ISignalBus _signalBus;
[InjectService]
private World _world;
public DemoPanelUIController(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
protected override DemoPanelUIView View => UIRoot.GetView<DemoPanelUIView>();
public override async UniTask InitializeAsync()
{
await base.InitializeAsync();
View.FeedbackButtonClick += OnFeedbackButtonClick;
_signalBus.Subscribe<DemoCompletedSignal>(OnDemoCompleted);
}
public void Dispose()
{
View.FeedbackButtonClick -= OnFeedbackButtonClick;
_signalBus.Unsubscribe<DemoCompletedSignal>(OnDemoCompleted);
}
private void OnDemoCompleted(DemoCompletedSignal signal)
{
View.OnDemoCompleted(_world.PopulationState.Population, signal.GameTime);
}
private void OnFeedbackButtonClick()
{
Application.OpenURL(AppLinks.DemoFeedbackUrl);
}
}
}

View File

@@ -0,0 +1,64 @@
using System;
using Cysharp.Threading.Tasks;
using UnityEngine.UIElements;
namespace DanieleMarotta.RiversongCodeShowcase
{
[UIView("demo-panel")]
public class DemoPanelUIView : UIView
{
private Button _closeButton;
private Button _feedbackButton;
private Label _populationLabel;
private Label _gameTimeLabel;
public event Action FeedbackButtonClick;
public override UniTask InitializeAsync(UIService uiService, VisualElement rootElement)
{
base.InitializeAsync(uiService, rootElement);
_populationLabel = rootElement.Q<Label>(className: "demo-panel__population");
_gameTimeLabel = rootElement.Q<Label>(className: "demo-panel__game-time");
_feedbackButton = rootElement.Q<Button>(className: "demo-panel__feedback-button");
_feedbackButton.RegisterCallbackOnce<ClickEvent>(_ => FeedbackButtonClick?.Invoke());
_closeButton = rootElement.Q<Button>(className: "demo-panel__close-button");
_closeButton.RegisterCallbackOnce<ClickEvent>(_ => Show(false, true));
Show(false);
return UniTask.CompletedTask;
}
public void OnDemoCompleted(int population, float gameTime)
{
FormatPanel(population, gameTime);
Show(true, true);
}
private void FormatPanel(int population, float gameTime)
{
_populationLabel.text = string.Format(_populationLabel.text, population);
_gameTimeLabel.text = string.Format(_gameTimeLabel.text, MakeGameTimeString(gameTime));
}
private string MakeGameTimeString(float gameTime)
{
var formatHelper = UIService.TextFormatHelper;
var totalSeconds = (int)gameTime;
var totalMinutes = totalSeconds / 60;
var hours = totalMinutes / 60;
var minutes = totalMinutes % 60;
return hours > 0
? $"{hours} {formatHelper.Pluralize(hours, "hour", "hours")} {minutes} {formatHelper.Pluralize(minutes, "minute", "minutes")}"
: $"{minutes} {formatHelper.Pluralize(minutes, "minute", "minutes")}";
}
}
}

View File

@@ -0,0 +1,29 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public class MainToolbarModel : UIModel
{
private bool _isBuildMenuOpen;
private bool _isRoadToolActive;
private bool _isDeleteToolActive;
public bool IsBuildMenuOpen
{
get => _isBuildMenuOpen;
set => SetProperty(ref _isBuildMenuOpen, value);
}
public bool IsDeleteToolActive
{
get => _isDeleteToolActive;
set => SetProperty(ref _isDeleteToolActive, value);
}
public bool IsRoadToolActive
{
get => _isRoadToolActive;
set => SetProperty(ref _isRoadToolActive, value);
}
}
}

View File

@@ -0,0 +1,71 @@
using System;
using Cysharp.Threading.Tasks;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class MainToolbarUIController : UIControllerSystem<MainToolbarUIView>, IDisposable
{
[InjectService]
private IEditingService _editingService;
private MainToolbarModel _model;
public MainToolbarUIController(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
protected override MainToolbarUIView View => UIRoot.GetView<MainToolbarUIView>();
public override async UniTask InitializeAsync()
{
await base.InitializeAsync();
_model = new MainToolbarModel { IsBuildMenuOpen = true };
View.SetModel(_model);
View.BuildMenuButtonClicked += OnBuildMenuButtonClicked;
View.DeleteToolButtonClicked += OnDeleteToolButtonClicked;
View.RoadToolButtonClicked += OnRoadToolButtonClicked;
_editingService.ActiveToolChanged += OnActiveToolChanged;
var buildMenu = UIRoot.GetView<BuildMenuUIView>();
buildMenu.OpenedOrClosed += OnBuildMenuOpenedOrClosed;
}
public void Dispose()
{
View.BuildMenuButtonClicked -= OnBuildMenuButtonClicked;
View.DeleteToolButtonClicked -= OnDeleteToolButtonClicked;
View.RoadToolButtonClicked -= OnRoadToolButtonClicked;
_editingService.ActiveToolChanged -= OnActiveToolChanged;
UIRoot.GetView<BuildMenuUIView>().OpenedOrClosed -= OnBuildMenuOpenedOrClosed;
}
private void OnBuildMenuButtonClicked()
{
UIRoot.GetView<BuildMenuUIView>().Toggle(true);
}
private void OnBuildMenuOpenedOrClosed(bool isOpen)
{
_model.IsBuildMenuOpen = isOpen;
}
private void OnDeleteToolButtonClicked()
{
_editingService.ActivateTool(_editingService.EditingState.DeleteTool);
}
private void OnRoadToolButtonClicked()
{
_editingService.ActivateTool(_editingService.EditingState.RoadTool);
}
private void OnActiveToolChanged(EditTool tool)
{
var editingState = _editingService.EditingState;
_model.IsDeleteToolActive = editingState.ActiveTool == editingState.DeleteTool;
_model.IsRoadToolActive = editingState.ActiveTool == editingState.RoadTool;
}
}
}

View File

@@ -0,0 +1,74 @@
using System;
using Cysharp.Threading.Tasks;
using UnityEngine.UIElements;
namespace DanieleMarotta.RiversongCodeShowcase
{
[UIView("main-toolbar")]
public class MainToolbarUIView : UIView<MainToolbarModel>
{
private Button _deleteToolButton;
private Button _buildMenuButton;
private Button _roadToolButton;
public event Action BuildMenuButtonClicked
{
add => _buildMenuButton.clicked += value;
remove => _buildMenuButton.clicked -= value;
}
public event Action DeleteToolButtonClicked
{
add => _deleteToolButton.clicked += value;
remove => _deleteToolButton.clicked -= value;
}
public event Action RoadToolButtonClicked
{
add => _roadToolButton.clicked += value;
remove => _roadToolButton.clicked -= value;
}
public override UniTask InitializeAsync(UIService uiService, VisualElement rootElement)
{
base.InitializeAsync(uiService, rootElement);
_buildMenuButton = rootElement.Q<Button>(className: "main-toolbar__build-menu-button");
_deleteToolButton = rootElement.Q<Button>(className: "main-toolbar__delete-tool-button");
_roadToolButton = rootElement.Q<Button>(className: "main-toolbar__road-tool-button");
return UniTask.CompletedTask;
}
protected override void OnNewModel(MainToolbarModel model)
{
base.OnNewModel(model);
UpdateButtons();
}
protected override void OnModelPropertyChanged(object sender, BindablePropertyChangedEventArgs e)
{
base.OnModelPropertyChanged(sender, e);
UpdateButtons();
}
private void UpdateButtons()
{
UpdateButton(Model.IsBuildMenuOpen, _buildMenuButton);
UpdateButton(Model.IsDeleteToolActive, _deleteToolButton);
UpdateButton(Model.IsRoadToolActive, _roadToolButton);
}
private void UpdateButton(bool isSelected, VisualElement element)
{
if (isSelected)
element.AddToClassList("selected");
else
element.RemoveFromClassList("selected");
}
}
}

View File

@@ -0,0 +1,88 @@
using System;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class OnboardingPanelUIController : UIControllerSystem<OnboardingPanelUIView>, IDisposable, IUpdatable
{
[InjectService]
private ISignalBus _signalBus;
[InjectService]
private GameConfig _config;
[InjectService]
private ISoundPlayer _soundPlayer;
private readonly Queue<OnboardingEvents> _completedEvents = new();
private bool _isWaitingToShowMessage;
private float _currentMessageTime;
public OnboardingPanelUIController(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
protected override OnboardingPanelUIView View => UIRoot.GetView<OnboardingPanelUIView>();
public override async UniTask InitializeAsync()
{
await base.InitializeAsync();
_signalBus.Subscribe<OnboardingEventCompleted>(OnOnboardingEventCompleted);
}
public void Dispose()
{
_signalBus.Unsubscribe<OnboardingEventCompleted>(OnOnboardingEventCompleted);
}
public void Update()
{
if (View.IsShowingAnyMessage())
{
_currentMessageTime += Time.unscaledDeltaTime;
if (_currentMessageTime > _config.Onboarding.MessageDuration) View.CloseCurrentMessage();
return;
}
_currentMessageTime = 0;
if (_isWaitingToShowMessage || !_completedEvents.TryDequeue(out var completedEvent)) return;
_ = ShowMessageAsync(completedEvent);
}
private async UniTask ShowMessageAsync(OnboardingEvents completedEvent)
{
_isWaitingToShowMessage = true;
await UniTask.WaitForSeconds(1, true);
View.ShowMessage(GetMessage(completedEvent));
_soundPlayer.Play(SystemSoundId.OnboardingMessage);
_isWaitingToShowMessage = false;
}
private string GetMessage(OnboardingEvents completedEvent)
{
foreach (var entry in _config.Onboarding.Messages.Entries)
{
if (entry.Event != completedEvent) continue;
return entry.Text ?? $"Unknown Event {completedEvent}";
}
return $"Unknown Event {completedEvent}";
}
private void OnOnboardingEventCompleted(OnboardingEventCompleted signal)
{
_completedEvents.Enqueue(signal.Event);
}
}
}

View File

@@ -0,0 +1,92 @@
using System.Text;
using Cysharp.Threading.Tasks;
using PrimeTween;
using UnityEngine.UIElements;
namespace DanieleMarotta.RiversongCodeShowcase
{
[UIView("onboarding-panel")]
public class OnboardingPanelUIView : UIView
{
private VisualElement _messages;
public override UniTask InitializeAsync(UIService uiService, VisualElement rootElement)
{
base.InitializeAsync(uiService, rootElement);
_messages = rootElement.Q<VisualElement>(className: "onboarding-panel__messages");
return UniTask.CompletedTask;
}
public bool IsShowingAnyMessage()
{
return _messages.childCount > 0;
}
public void ShowMessage(string message)
{
var element = UIService.TemplateLibrary.OnboardingPanel.Message.CloneTree();
element.Q<Button>().RegisterCallbackOnce<ClickEvent>(_ => CloseCurrentMessage());
_messages.Add(element);
ShowMessageAsync(element, message).Forget();
}
private async UniTask ShowMessageAsync(VisualElement element, string message)
{
UIVisibilityAnimation.PlayShowTween(element).ToUniTask().Forget();
await AnimateTextAsync(element, message);
PlayReminderAnimationAsync(element).Forget();
}
private async UniTask AnimateTextAsync(VisualElement element, string message)
{
var label = element.Q<Label>(className: "onboarding-message__body");
label.text = string.Empty;
var visibleText = new StringBuilder(message.Length);
for (var i = 0; i < message.Length; i++)
{
if (message[i] == '<')
{
var tagEnd = message.IndexOf('>', i + 1);
if (tagEnd >= 0)
{
visibleText.Append(message, i, tagEnd - i + 1);
label.text = visibleText.ToString();
i = tagEnd;
continue;
}
}
visibleText.Append(message[i]);
label.text = visibleText.ToString();
await UniTask.WaitForSeconds(0.03f, true);
}
}
private async UniTask PlayReminderAnimationAsync(VisualElement message)
{
while (message.panel != null)
{
await UniTask.WaitForSeconds(5, true);
await Sequence.Create(useUnscaledTime: true)
.Chain(Tween.Custom(0, 3, 0.1f, value => message.style.translate = new Translate(value, 0), Ease.OutQuad))
.Chain(Tween.Custom(3, -2.5f, 0.14f, value => message.style.translate = new Translate(value, 0), Ease.InOutQuad))
.Chain(Tween.Custom(-2.5f, 1.5f, 0.12f, value => message.style.translate = new Translate(value, 0), Ease.InOutQuad))
.Chain(Tween.Custom(1.5f, 0, 0.1f, value => message.style.translate = new Translate(value, 0), Ease.OutQuad));
}
}
public void CloseCurrentMessage()
{
if (_messages.childCount == 0) return;
_messages[0].RemoveFromHierarchy();
}
}
}

View File

@@ -0,0 +1,62 @@
using System;
using Cysharp.Threading.Tasks;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace DanieleMarotta.RiversongCodeShowcase
{
public class PausePopupUIController : UIControllerSystem<PausePopupUIView>, IDisposable
{
[InjectService]
private ICancelAction _cancelAction;
public PausePopupUIController(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
protected override PausePopupUIView View => UIRoot.GetView<PausePopupUIView>();
public override async UniTask InitializeAsync()
{
await base.InitializeAsync();
View.Show(false);
View.FeedbackButtonClick += OnFeedbackButtonClick;
View.QuitButtonClick += OnQuitButtonClick;
_cancelAction.AddHandler(
(int)CancelActions.PauseMenu,
cancelActionType =>
{
if (cancelActionType != CancelActionType.EscapeKey) return false;
View.Toggle(true);
return true;
});
}
public void Dispose()
{
View.FeedbackButtonClick -= OnFeedbackButtonClick;
View.QuitButtonClick -= OnQuitButtonClick;
}
private void OnFeedbackButtonClick()
{
Application.OpenURL(AppLinks.DemoFeedbackUrl);
}
private void OnQuitButtonClick()
{
#if UNITY_EDITOR
EditorApplication.isPlaying = false;
#else
Application.Quit();
#endif
}
}
}

View File

@@ -0,0 +1,43 @@
using System;
using Cysharp.Threading.Tasks;
using UnityEngine.UIElements;
namespace DanieleMarotta.RiversongCodeShowcase
{
[UIView("pause-popup")]
public class PausePopupUIView : UIView
{
private Button _feedbackButton;
private Button _quitButton;
private EventCallback<ClickEvent> _onFeedbackButtonClick;
private EventCallback<ClickEvent> _onQuitButtonClick;
public event Action FeedbackButtonClick;
public event Action QuitButtonClick;
public override UniTask InitializeAsync(UIService uiService, VisualElement rootElement)
{
base.InitializeAsync(uiService, rootElement);
_feedbackButton = rootElement.Q<Button>(className: "pause-popup__feedback-button");
_onFeedbackButtonClick = _ => FeedbackButtonClick?.Invoke();
_feedbackButton.RegisterCallback(_onFeedbackButtonClick);
_quitButton = rootElement.Q<Button>(className: "pause-popup__quit-button");
_onQuitButtonClick = _ => QuitButtonClick?.Invoke();
_quitButton.RegisterCallback(_onQuitButtonClick);
return UniTask.CompletedTask;
}
public override void Dispose()
{
_feedbackButton?.UnregisterCallback(_onFeedbackButtonClick);
_quitButton?.UnregisterCallback(_onQuitButtonClick);
}
}
}

View File

@@ -0,0 +1,32 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public class PopulationPanelModel : UIModel
{
public int Population { get; set; }
public int Happiness { get; set; }
public LaborTier LaborTier { get; set; }
public void Update(int population, int happiness, LaborTier laborTier)
{
if (Population != population)
{
Population = population;
NotifyPropertyChanged(nameof(Population));
}
if (Happiness != happiness)
{
Happiness = happiness;
NotifyPropertyChanged(nameof(Happiness));
}
if (LaborTier != laborTier)
{
LaborTier = laborTier;
NotifyPropertyChanged(nameof(LaborTier));
}
}
}
}

View File

@@ -0,0 +1,36 @@
using Cysharp.Threading.Tasks;
using Unity.Mathematics;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class PopulationPanelUIController : UIControllerSystem<PopulationPanelUIView>, IUpdatable
{
[InjectService]
private World _world;
private PopulationPanelModel _model;
public PopulationPanelUIController(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
protected override PopulationPanelUIView View => UIRoot.GetView<PopulationPanelUIView>();
public override async UniTask InitializeAsync()
{
await base.InitializeAsync();
_model = new PopulationPanelModel();
View.SetModel(_model);
}
public void Update()
{
var population = _world.PopulationState.Population;
var happiness = math.clamp((int)math.round(_world.PopulationState.Happiness * 100), 0, 100);
_model.Update(population, happiness, _world.ProductionState.LaborTier);
View.UpdateHappinessIconAnimation();
}
}
}

View File

@@ -0,0 +1,113 @@
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.UIElements;
namespace DanieleMarotta.RiversongCodeShowcase
{
[UIView("population-panel")]
public class PopulationPanelUIView : UIView<PopulationPanelModel>
{
private Label _populationLabel;
private VisualElement _happinessIcon;
private Label _happinessLabel;
private List<VisualElement> _laborTier;
private float _happinessIconScale = 1;
public override UniTask InitializeAsync(UIService uiService, VisualElement rootElement)
{
base.InitializeAsync(uiService, rootElement);
_populationLabel = rootElement.Q<Label>(className: "population-panel__population-label");
_happinessIcon = rootElement.Q<VisualElement>(className: "population-panel__happiness-icon");
_happinessLabel = rootElement.Q<Label>(className: "population-panel__happiness-label");
_laborTier = rootElement.Query<VisualElement>(className: "population-panel__labor-dot").ToList();
return UniTask.CompletedTask;
}
protected override void OnNewModel(PopulationPanelModel model)
{
base.OnNewModel(model);
UpdatePopulationLabel();
UpdateHappinessLabel();
UpdateLaborRating();
}
protected override void OnModelPropertyChanged(object sender, BindablePropertyChangedEventArgs e)
{
base.OnModelPropertyChanged(sender, e);
switch (e.propertyName)
{
case nameof(PopulationPanelModel.Population):
UpdatePopulationLabel();
break;
case nameof(PopulationPanelModel.Happiness):
UpdateHappinessLabel();
break;
case nameof(PopulationPanelModel.LaborTier):
UpdateLaborRating();
break;
}
}
private void UpdatePopulationLabel()
{
_populationLabel.text = Model.Population.ToString();
}
private void UpdateHappinessLabel()
{
_happinessLabel.text = Model.Happiness.ToString();
}
private void UpdateLaborRating()
{
var ratingClass = Model.LaborTier switch
{
LaborTier.Medium => "medium",
LaborTier.High => "high",
_ => "low"
};
foreach (var element in _laborTier)
{
element.RemoveFromClassList("low");
element.RemoveFromClassList("medium");
element.RemoveFromClassList("high");
element.AddToClassList(ratingClass);
}
var rating = math.clamp((int)Model.LaborTier, 0, _laborTier.Count);
for (var i = 0; i < _laborTier.Count; i++) _laborTier[i].EnableInClassList("empty", i >= rating);
}
public void UpdateHappinessIconAnimation()
{
var normalizedHappiness = Model.Happiness * 0.01f;
const float minSpeed = 1.8f;
const float maxSpeed = 2.4f;
var speed = math.lerp(minSpeed, maxSpeed, normalizedHappiness);
const float minAmplitude = 0.03f;
const float maxAmplitude = 0.06f;
var amplitude = math.lerp(minAmplitude, maxAmplitude, normalizedHappiness);
if (normalizedHappiness > 0.98f) amplitude *= 1.15f;
var targetScale = 1 + amplitude * 0.5f * (1 + math.sin(Time.unscaledTime * speed));
_happinessIconScale = math.lerp(_happinessIconScale, targetScale, math.saturate(Time.unscaledDeltaTime * 4));
_happinessIcon.style.scale = new StyleScale(new Scale(Vector2.one * _happinessIconScale));
}
}
}

View File

@@ -0,0 +1,54 @@
using System;
using Cysharp.Threading.Tasks;
using UnityEngine.UIElements;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class RuntimeTooltipUIController : UIControllerSystem<RuntimeTooltipUIView>, IDisposable
{
private VisualElement _currentSource;
public RuntimeTooltipUIController(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
protected override RuntimeTooltipUIView View => UIRoot.GetView<RuntimeTooltipUIView>();
public override async UniTask InitializeAsync()
{
await base.InitializeAsync();
View.Show(false);
UIRoot.RootVisualElement.RegisterCallback<PointerEnterEvent>(OnPointerEnter, TrickleDown.TrickleDown);
UIRoot.RootVisualElement.RegisterCallback<PointerLeaveEvent>(OnPointerLeave, TrickleDown.TrickleDown);
}
public void Dispose()
{
UIRoot.RootVisualElement.UnregisterCallback<PointerEnterEvent>(OnPointerEnter, TrickleDown.TrickleDown);
UIRoot.RootVisualElement.UnregisterCallback<PointerLeaveEvent>(OnPointerLeave, TrickleDown.TrickleDown);
}
private void OnPointerEnter(PointerEnterEvent evt)
{
if (evt.target is not VisualElement element) return;
var source = element.GetFirstAncestorOrSelf(static candidate => !string.IsNullOrEmpty(candidate.tooltip));
if (source == null || ReferenceEquals(source, _currentSource)) return;
_currentSource = source;
View.Show(true);
View.AnchorTooltip(source, source.tooltip);
}
private void OnPointerLeave(PointerLeaveEvent evt)
{
if (!ReferenceEquals(evt.target, _currentSource)) return;
_currentSource = null;
View.Show(false);
}
}
}

View File

@@ -0,0 +1,52 @@
using System;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.UIElements;
namespace DanieleMarotta.RiversongCodeShowcase
{
[UIView("runtime-tooltip")]
public class RuntimeTooltipUIView : UIView
{
private Label _content;
public override UniTask InitializeAsync(UIService uiService, VisualElement rootElement)
{
base.InitializeAsync(uiService, rootElement);
_content = rootElement.Q<Label>(className: "runtime-tooltip__content");
RootElement.SetPickingModeRecursive(PickingMode.Ignore);
return UniTask.CompletedTask;
}
public void AnchorTooltip(VisualElement anchor, string text)
{
_content.text = text;
var placeAbove = anchor.worldBound.center.y > Screen.height * 0.2f;
RootElement.EnableInClassList("runtime-tooltip--above", placeAbove);
RootElement.EnableInClassList("runtime-tooltip--below", !placeAbove);
RootElement.schedule.Execute(() => ClampToScreen(anchor.worldBound, placeAbove));
}
private void ClampToScreen(Rect anchor, bool placeAbove)
{
var width = RootElement.resolvedStyle.width;
var height = RootElement.resolvedStyle.height;
if (width <= 0 || height <= 0) return;
var left = Mathf.Clamp(anchor.center.x - width / 2, 0, Mathf.Max(0, Screen.width - width));
const float verticalGap = 32;
var top = placeAbove ? anchor.yMin - verticalGap - height : anchor.yMax + verticalGap;
top = Mathf.Clamp(top, 0, Mathf.Max(0, Screen.height - height));
RootElement.style.left = left + width / 2;
RootElement.style.top = placeAbove ? top + height : top;
}
}
}

View File

@@ -0,0 +1,16 @@
using Unity.Properties;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class SpeedControlsPanelModel : UIModel
{
private int _speedLevel;
[CreateProperty]
public int SpeedLevel
{
get => _speedLevel;
set => SetProperty(ref _speedLevel, value);
}
}
}

View File

@@ -0,0 +1,44 @@
using System;
using Cysharp.Threading.Tasks;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class SpeedControlsPanelUIController : UIControllerSystem<SpeedControlsPanelUIView>, IDisposable, IUpdatable
{
[InjectService]
private IGameSpeed _gameSpeed;
private SpeedControlsPanelModel _model;
public SpeedControlsPanelUIController(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
protected override SpeedControlsPanelUIView View => UIRoot.GetView<SpeedControlsPanelUIView>();
public override async UniTask InitializeAsync()
{
await base.InitializeAsync();
_model = new SpeedControlsPanelModel { SpeedLevel = _gameSpeed.SpeedLevel };
View.SetModel(_model);
View.SpeedChanged += OnSpeedChanged;
}
public void Dispose()
{
View.SpeedChanged -= OnSpeedChanged;
}
private void OnSpeedChanged(int speedLevel)
{
_gameSpeed.SetSpeedLevel(speedLevel);
}
public void Update()
{
_model.SpeedLevel = _gameSpeed.SpeedLevel;
}
}
}

View File

@@ -0,0 +1,77 @@
using System;
using Cysharp.Threading.Tasks;
using UnityEngine.UIElements;
namespace DanieleMarotta.RiversongCodeShowcase
{
[UIView("speed-controls-panel")]
public class SpeedControlsPanelUIView : UIView<SpeedControlsPanelModel>
{
private VisualElement _normalSpeedButton;
private VisualElement _fastSpeedButton;
private VisualElement _veryFastSpeedButton;
public event Action<int> SpeedChanged;
public override UniTask InitializeAsync(UIService uiService, VisualElement rootElement)
{
base.InitializeAsync(uiService, rootElement);
_normalSpeedButton = rootElement.Q(className: "speed-controls-panel__normal-speed");
_fastSpeedButton = rootElement.Q(className: "speed-controls-panel__fast-speed");
_veryFastSpeedButton = rootElement.Q(className: "speed-controls-panel__very-fast-speed");
_normalSpeedButton.RegisterCallback<ClickEvent>(OnNormalSpeedClick);
_fastSpeedButton.RegisterCallback<ClickEvent>(OnFastSpeedClick);
_veryFastSpeedButton.RegisterCallback<ClickEvent>(OnVeryFastSpeedClick);
return UniTask.CompletedTask;
}
protected override void OnNewModel(SpeedControlsPanelModel model)
{
base.OnNewModel(model);
UpdateSelected(model.SpeedLevel);
}
protected override void OnModelPropertyChanged(object sender, BindablePropertyChangedEventArgs e)
{
base.OnModelPropertyChanged(sender, e);
UpdateSelected(Model.SpeedLevel);
}
private void OnNormalSpeedClick(ClickEvent evt)
{
SpeedChanged?.Invoke(0);
}
private void OnFastSpeedClick(ClickEvent evt)
{
SpeedChanged?.Invoke(1);
}
private void OnVeryFastSpeedClick(ClickEvent evt)
{
SpeedChanged?.Invoke(2);
}
public void UpdateSelected(int speedLevel)
{
UpdateSelected(_normalSpeedButton, speedLevel == 0);
UpdateSelected(_fastSpeedButton, speedLevel == 1);
UpdateSelected(_veryFastSpeedButton, speedLevel == 2);
}
private static void UpdateSelected(VisualElement element, bool isSelected)
{
if (isSelected)
element.AddToClassList("selected");
else
element.RemoveFromClassList("selected");
}
}
}

View File

@@ -0,0 +1,125 @@
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using Unity.Mathematics;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class StorageTooltipController : UIControllerSystem<StorageTooltipUIView>, IUpdatable
{
[InjectService]
private IPointerService _pointerService;
[InjectService]
private ITileSpace _tileSpace;
[InjectService]
private World _world;
[InjectService]
private IEntityCollection _entityCollection;
[InjectService]
private IProductCatalog _productCatalog;
[InjectService]
private IScene _scene;
[InjectService]
private IEditingService _editingService;
private List<IProductAmount> _products = new();
public StorageTooltipController(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
protected override StorageTooltipUIView View => UIRoot.GetView<StorageTooltipUIView>();
public override UniTask InitializeAsync()
{
for (var i = 0; i < _productCatalog.ProductTypeCount; i++) _products.Add(new ProductAmountAuthoring());
return UniTask.CompletedTask;
}
public void Update()
{
if (_editingService.EditingState.ActiveTool != null)
{
View.Show(false);
return;
}
if (!_pointerService.TryGetPositionOnTerrain(out var pointer))
{
View.Show(false);
return;
}
var tile = _tileSpace.WorldToTile(pointer);
if (!TryGetStorageEntityId(tile, out var storageEntityId, out var storageEntityRect))
{
View.Show(false);
return;
}
IProductStorageEntity storageEntity;
switch (_entityCollection.Get(storageEntityId))
{
case Building building when building.Definition.ShowStorageTooltip:
storageEntity = building;
break;
case ProductStack productStack:
storageEntity = productStack;
break;
default:
View.Show(false);
return;
}
ref var storage = ref storageEntity.GetStorageRW();
if (storage.TotalCount <= 0)
{
View.Show(false);
return;
}
var count = 0;
foreach (var (handle, amount) in storage)
{
var productAmount = _products[count++];
productAmount.Product = _productCatalog.GetProduct(handle);
productAmount.Amount = amount;
}
View.SetProducts(_products, count);
View.Show(true);
var worldCenter = _tileSpace.GetRectWorldCenter(storageEntityRect);
var screenPoint = _scene.MainCamera.WorldToScreenPoint(worldCenter);
View.RootElement.SetAbsoluteScreenPosition(screenPoint);
}
private bool TryGetStorageEntityId(int2 tile, out int id, out TileRect rect)
{
ref var entityIdValue = ref _world.EntityIdMap.GetValueRW(tile);
if (entityIdValue.BuildingId != Entity.InvalidId)
{
id = entityIdValue.BuildingId;
rect = ((IBuildingShape)_entityCollection.Get<Entity>(id)).Rect;
return true;
}
if (_world.ProductStacks.At(tile, out var productStack))
{
id = productStack.Id;
rect = TileRect.OneTile(tile);
return true;
}
id = Entity.InvalidId;
rect = TileRect.Empty;
return false;
}
}
}

View File

@@ -0,0 +1,36 @@
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using UnityEngine.UIElements;
namespace DanieleMarotta.RiversongCodeShowcase
{
[UIView("storage-tooltip")]
public class StorageTooltipUIView : UIView
{
private VisualElement _products;
public override async UniTask InitializeAsync(UIService uiService, VisualElement rootElement)
{
await base.InitializeAsync(uiService, rootElement);
_products = rootElement.Q(className: "storage-tooltip__products");
}
public void SetProducts(List<IProductAmount> source, int count)
{
_products.Clear();
var productTemplate = UIService.TemplateLibrary.Common.ProductAmount;
for (var i = 0; i < count; i++)
{
var productAmount = source[i];
var element = productTemplate.CloneTree();
element.dataSource = productAmount;
_products.Add(element);
}
}
}
}

View File

@@ -0,0 +1,19 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public class TimePanelModel : UIModel
{
public int Week { get; private set; }
public int Month { get; private set; }
public int Year { get; private set; }
public void Update(int week, int month, int year)
{
Week = week;
Month = month;
Year = year;
NotifyPropertyChanged();
}
}
}

View File

@@ -0,0 +1,49 @@
using System;
using Cysharp.Threading.Tasks;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class TimePanelUIController : UIControllerSystem<TimePanelUIView>, IDisposable
{
[InjectService]
private World _world;
[InjectService]
private ISignalBus _signalBus;
private TimePanelModel _model;
public TimePanelUIController(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
protected override TimePanelUIView View => UIRoot.GetView<TimePanelUIView>();
public override async UniTask InitializeAsync()
{
await base.InitializeAsync();
_model = new TimePanelModel();
UpdateModel();
View.SetModel(_model);
_signalBus.Subscribe<EndOfWeekSignal>(OnEndOfWeekSignal);
}
public void Dispose()
{
_signalBus.Unsubscribe<EndOfWeekSignal>(OnEndOfWeekSignal);
}
private void OnEndOfWeekSignal(EndOfWeekSignal signal)
{
UpdateModel();
}
private void UpdateModel()
{
var timeState = _world.TimeState;
_model.Update(timeState.WeekNumber, timeState.MonthNumber, timeState.YearNumber);
}
}
}

View File

@@ -0,0 +1,45 @@
using Cysharp.Threading.Tasks;
using UnityEngine.UIElements;
namespace DanieleMarotta.RiversongCodeShowcase
{
[UIView("time-panel")]
public class TimePanelUIView : UIView<TimePanelModel>
{
private Label _weekNumberText;
private Label _monthNumberText;
private Label _yearNumberText;
public override async UniTask InitializeAsync(UIService uiService, VisualElement rootElement)
{
await base.InitializeAsync(uiService, rootElement);
_weekNumberText = rootElement.Q<Label>(className: "time-panel__week-number");
_monthNumberText = rootElement.Q<Label>(className: "time-panel__month-number");
_yearNumberText = rootElement.Q<Label>(className: "time-panel__year-number");
}
protected override void OnNewModel(TimePanelModel model)
{
base.OnNewModel(model);
UpdateText();
}
protected override void OnModelPropertyChanged(object sender, BindablePropertyChangedEventArgs e)
{
base.OnModelPropertyChanged(sender, e);
UpdateText();
}
private void UpdateText()
{
_weekNumberText.text = $"Week {Model.Week + 1},";
_monthNumberText.text = $"Month {Model.Month + 1},";
_yearNumberText.text = $"Year {Model.Year + 1}";
}
}
}

View File

@@ -0,0 +1,68 @@
using System;
using Cysharp.Threading.Tasks;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class TitleScreenUIController : UIControllerSystem<TitleScreenUIView>, IDisposable
{
[InjectService]
private BuildVersionAsset _buildVersion;
[InjectService]
private ISignalBus _signalBus;
public TitleScreenUIController(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
protected override TitleScreenUIView View => UIRoot.GetView<TitleScreenUIView>();
public override async UniTask InitializeAsync()
{
await base.InitializeAsync();
_signalBus.Subscribe<GameInitializationCompletedSignal>(OnGameInitializationCompleted);
_signalBus.Subscribe<WorldReadySignal>(OnWorldReady);
View.StartGame += OnStartGame;
View.SetVersion($"v{_buildVersion.CurrentVersion}");
}
public void Dispose()
{
_signalBus.Unsubscribe<GameInitializationCompletedSignal>(OnGameInitializationCompleted);
_signalBus.Unsubscribe<WorldReadySignal>(OnWorldReady);
View.StartGame -= OnStartGame;
}
private void OnGameInitializationCompleted(GameInitializationCompletedSignal signal)
{
_ = View.PlayIntroAsync();
}
private void OnStartGame()
{
FadeAndStartGameAsync().Forget();
}
private async UniTask FadeAndStartGameAsync()
{
await View.PlayStartFadeAsync(1);
_signalBus.Raise(new GameStartedSignal());
}
private void OnWorldReady(WorldReadySignal signal)
{
_ = HideWithDelay();
}
private async UniTask HideWithDelay()
{
await UniTask.NextFrame();
View.Show(false);
}
}
}

View File

@@ -0,0 +1,118 @@
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using PrimeTween;
using UnityEngine;
using UnityEngine.UIElements;
namespace DanieleMarotta.RiversongCodeShowcase
{
[UIView("title-screen")]
public class TitleScreenUIView : UIView
{
private VisualElement _fader;
private VisualElement _logo;
private VisualElement _panel;
private Button _startGameButton;
private Label _versionLabel;
private CancellationTokenSource _animationCancellation;
public event Action StartGame;
public override UniTask InitializeAsync(UIService uiService, VisualElement rootElement)
{
base.InitializeAsync(uiService, rootElement);
_fader = rootElement.Q(className: "title-screen__fader");
_logo = rootElement.Q(className: "title-screen__logo-image");
_panel = rootElement.Q(className: "dialog");
_startGameButton = rootElement.Q<Button>(className: "title-screen__start-button");
_versionLabel = rootElement.Q<Label>(className: "title-screen__version");
_fader.style.display = DisplayStyle.None;
_fader.style.opacity = 0;
_logo.style.display = DisplayStyle.None;
_logo.style.opacity = 0;
_panel.style.display = DisplayStyle.None;
_panel.style.opacity = 0;
_startGameButton.RegisterCallbackOnce<ClickEvent>(_ => StartGame?.Invoke());
return UniTask.CompletedTask;
}
public override void Dispose()
{
_animationCancellation?.Cancel();
_animationCancellation?.Dispose();
_animationCancellation = null;
base.Dispose();
}
public void SetVersion(string versionText)
{
_versionLabel.text = versionText;
}
public async UniTask PlayIntroAsync()
{
Show(true);
await UniTask.NextFrame();
_logo.style.display = DisplayStyle.Flex;
_logo.style.top = Length.Percent(50);
await UIVisibilityAnimation.PlayShowTween(_logo);
await UniTask.WaitForSeconds(1);
await Tween.Custom(50, 33, 0.95f, value => _logo.style.top = Length.Percent(value), Ease.OutCubic);
await UniTask.WaitForSeconds(1);
_panel.style.display = DisplayStyle.Flex;
await UIVisibilityAnimation.PlayShowTween(_panel);
_logo.style.translate = new Translate(Length.Percent(-50), Length.Percent(-50));
_logo.SetScale(1);
_animationCancellation = new CancellationTokenSource();
PlayLogoIdleAnimationAsync(_animationCancellation.Token).Forget();
}
public async UniTask PlayStartFadeAsync(float duration)
{
_fader.style.display = DisplayStyle.Flex;
await Tween.Custom(0, 1, duration, value => _fader.style.opacity = value, Ease.OutQuad);
}
private async UniTask PlayLogoIdleAnimationAsync(CancellationToken cancellationToken)
{
try
{
float t = 0;
while (!cancellationToken.IsCancellationRequested)
{
var sin = Mathf.Sin(t);
_logo.style.translate = new Translate(Length.Percent(-50), Length.Percent(-50 + 3 * sin));
_logo.SetScale(1 - 0.025f * sin);
t += Time.deltaTime;
await UniTask.NextFrame(cancellationToken);
}
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
}
}
}
}

View File

@@ -0,0 +1,125 @@
using System;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.UIElements;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class BuildingBadgeUIController : UIControllerSystem, IInitializable, IDisposable, IUpdatable
{
[InjectService]
private ISignalBus _signalBus;
[InjectService]
private IWorldUIService _worldUIService;
[InjectService]
private ICameraProperties _cameraProperties;
[InjectService]
private GameConfig _gameConfig;
[InjectService]
private World _world;
private VisualElement _container;
private readonly Dictionary<int, BadgeEntry> _entries = new();
public BuildingBadgeUIController(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public override async UniTask InitializeAsync()
{
await base.InitializeAsync();
_container = UIRoot.RootVisualElement.Q(className: "world-ui-layer");
_signalBus.Subscribe<BuildingVisualizationCreatedSignal>(OnBuildingVisualizationCreated);
_signalBus.Subscribe<BuildingDeletedSignal>(OnBuildingDeleted);
}
public void Dispose()
{
_signalBus.Unsubscribe<BuildingVisualizationCreatedSignal>(OnBuildingVisualizationCreated);
_signalBus.Unsubscribe<BuildingDeletedSignal>(OnBuildingDeleted);
}
private void OnBuildingVisualizationCreated(BuildingVisualizationCreatedSignal signal)
{
var building = signal.Building;
if (building.Definition.WorkerCount <= 0) return;
CreateBadgeAsync(building, signal.Visualization).Forget();
}
private async UniTask CreateBadgeAsync(Building building, BuildingVisualization visualization)
{
var element = UIService.TemplateLibrary.WorldUI.Badge.CloneTree();
_container.Add(element);
var view = (BuildingBadgeUIView)await UIService.CreateView(typeof(BuildingBadgeUIView), element);
view.RootElement.SetPickingModeRecursive(PickingMode.Ignore);
view.Show(false);
_worldUIService.Register(view.RootElement, visualization.transform);
_entries.Add(building.Id, new BadgeEntry(building, view));
}
private void OnBuildingDeleted(BuildingDeletedSignal signal)
{
if (!_entries.Remove(signal.Building.Id, out var entry)) return;
_worldUIService.Unregister(entry.View.RootElement);
entry.View.RootElement.RemoveFromHierarchy();
}
public void Update()
{
var zoomRange = _gameConfig.Camera.ZoomRange;
var config = UIService.TemplateLibrary.WorldUI;
var normalizedZoom = Mathf.InverseLerp(zoomRange.x, config.BadgeZoomCullThreshold, _cameraProperties.Zoom);
var offset = Mathf.Lerp(config.BadgeYOffset.x, config.BadgeYOffset.y, normalizedZoom);
var scale = Mathf.Lerp(config.BadgeScale.x, config.BadgeScale.y, normalizedZoom);
var culledByZoom = _cameraProperties.Zoom > config.BadgeZoomCullThreshold;
UpdateBadges(culledByZoom);
foreach (var entry in _entries.Values)
{
entry.View.RootElement.SetScale(scale);
_worldUIService.SetWorldOffset(entry.View.RootElement, offset * Vector3.up);
}
}
private void UpdateBadges(bool culledByZoom)
{
foreach (var entry in _entries.Values)
{
ref var sleepState = ref entry.Building.GetSleepStateRW();
var show = !culledByZoom && _world.TimeState.DayNightCycleStep == DayNightCycleStep.Day && sleepState.HasHomelessWorkers;
entry.View.Show(show);
}
}
private readonly struct BadgeEntry
{
public readonly Building Building;
public readonly BuildingBadgeUIView View;
public BadgeEntry(Building building, BuildingBadgeUIView view)
{
Building = building;
View = view;
}
}
}
}

View File

@@ -0,0 +1,6 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public class BuildingBadgeUIView : UIView
{
}
}

View File

@@ -0,0 +1,14 @@
using UnityEngine;
using UnityEngine.UIElements;
namespace DanieleMarotta.RiversongCodeShowcase
{
public interface IWorldUIService
{
void Register(VisualElement element, Transform target);
void Unregister(VisualElement element);
void SetWorldOffset(VisualElement element, Vector3 offset);
}
}

View File

@@ -0,0 +1,71 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Pool;
using UnityEngine.UIElements;
namespace DanieleMarotta.RiversongCodeShowcase
{
[Service(typeof(IWorldUIService))]
[GameSystemGroup(typeof(UISystemGroup))]
public class WorldUITrackingSystem : GameSystem, IUpdatable, IWorldUIService
{
[InjectService]
private IScene _scene;
private readonly Dictionary<VisualElement, TrackedElement> _trackedElements = new();
public WorldUITrackingSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public void Register(VisualElement element, Transform target)
{
_trackedElements[element] = new TrackedElement { Target = target };
}
public void Unregister(VisualElement element)
{
_trackedElements.Remove(element);
}
public void SetWorldOffset(VisualElement element, Vector3 offset)
{
var trackedElement = _trackedElements[element];
trackedElement.Offset = offset;
_trackedElements[element] = trackedElement;
}
public void Update()
{
if (_trackedElements.Count == 0) return;
using var invalidScope = ListPool<VisualElement>.Get(out var invalidElements);
var camera = _scene.MainCamera;
foreach (var (element, trackedElement) in _trackedElements)
{
var target = trackedElement.Target;
if (element.panel == null || target == null)
{
invalidElements.Add(element);
continue;
}
var screenPoint = camera.WorldToScreenPoint(target.position + trackedElement.Offset);
var isVisible = screenPoint.x >= 0 && screenPoint.x <= Screen.width && screenPoint.y >= 0 && screenPoint.y <= Screen.height && screenPoint.z > 0;
element.style.visibility = isVisible ? Visibility.Visible : Visibility.Hidden;
if (isVisible) element.SetAbsoluteScreenPosition(screenPoint);
}
foreach (var invalidElement in invalidElements) _trackedElements.Remove(invalidElement);
}
private struct TrackedElement
{
public Transform Target;
public Vector3 Offset;
}
}
}