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");
}
}
}
}