riversong code showcase
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
public struct BuildMenuButtonUnlockAnimationStartedSignal
|
||||
{
|
||||
}
|
||||
}
|
||||
17
Source/Riversong/Game/UI/Panels/BuildMenu/BuildMenuModel.cs
Normal file
17
Source/Riversong/Game/UI/Panels/BuildMenu/BuildMenuModel.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
141
Source/Riversong/Game/UI/Panels/BuildMenu/BuildMenuUIView.cs
Normal file
141
Source/Riversong/Game/UI/Panels/BuildMenu/BuildMenuUIView.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user