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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
public class StoragePanelModel : UIModel
|
||||
{
|
||||
public List<StoragePanelProductModel> Products { get; } = new();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
64
Source/Riversong/Game/UI/Panels/Demo/DemoPanelUIView.cs
Normal file
64
Source/Riversong/Game/UI/Panels/Demo/DemoPanelUIView.cs
Normal 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")}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
19
Source/Riversong/Game/UI/Panels/TimePanel/TimePanelModel.cs
Normal file
19
Source/Riversong/Game/UI/Panels/TimePanel/TimePanelModel.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
45
Source/Riversong/Game/UI/Panels/TimePanel/TimePanelUIView.cs
Normal file
45
Source/Riversong/Game/UI/Panels/TimePanel/TimePanelUIView.cs
Normal 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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
118
Source/Riversong/Game/UI/Panels/TitleScreen/TitleScreenUIView.cs
Normal file
118
Source/Riversong/Game/UI/Panels/TitleScreen/TitleScreenUIView.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
public class BuildingBadgeUIView : UIView
|
||||
{
|
||||
}
|
||||
}
|
||||
14
Source/Riversong/Game/UI/Panels/WorldUI/IWorldUIService.cs
Normal file
14
Source/Riversong/Game/UI/Panels/WorldUI/IWorldUIService.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user