riversong code showcase

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

View File

@@ -0,0 +1,292 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using Sirenix.OdinInspector;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace DanieleMarotta.RiversongCodeShowcase
{
[CreateAssetMenu(fileName = "DayNightUITheme", menuName = "Riversong Code Showcase/Day Night UI Theme")]
public class DayNightUITheme : ScriptableObject
{
private const string CommonUssAssetPath = "_Project/GameAssets/UI/Common.uss";
private const string CommonUssEditorAssetPath = "Assets/_Project/GameAssets/UI/Common.uss";
private static readonly Regex RootColorVariableRegex = new(@"^(?<name>--[a-z0-9-]+)\s*:\s*(?<value>#[0-9A-Fa-f]{6,8})\s*;$", RegexOptions.Compiled);
[TableList(AlwaysExpanded = true, ShowIndexLabels = true, HideToolbar = true)]
[ListDrawerSettings(DefaultExpandedState = true, DraggableItems = false, HideAddButton = true, HideRemoveButton = true, ShowFoldout = false)]
public List<ThemeColorProperty> Properties = new();
internal IReadOnlyList<ThemeColorProperty> OrderedProperties => Properties;
private void OnEnable()
{
EnsureProperties();
}
private void OnValidate()
{
EnsureProperties();
}
private void EnsureProperties()
{
Properties ??= new List<ThemeColorProperty>();
var definitions = GetThemePropertyDefinitions();
var existingProperties = Properties;
var existingPropertiesByName = CreatePropertiesByName(existingProperties);
var definitionNames = CreateDefinitionNames(definitions);
var orderedProperties = new List<ThemeColorProperty>(definitions.Count);
for (var i = 0; i < definitions.Count; i++)
{
var definition = definitions[i];
var property = TryGetProperty(existingPropertiesByName, definition.Name) ?? TryGetSameIndexFallbackProperty(existingProperties, definitionNames, i);
property ??= new ThemeColorProperty(definition.Name, Color.white, Color.white);
property.Name = definition.Name;
orderedProperties.Add(property);
}
Properties = orderedProperties;
}
private static Dictionary<string, ThemeColorProperty> CreatePropertiesByName(IEnumerable<ThemeColorProperty> properties)
{
var propertiesByName = new Dictionary<string, ThemeColorProperty>(StringComparer.Ordinal);
foreach (var property in properties)
{
if (property == null || string.IsNullOrWhiteSpace(property.Name)) continue;
propertiesByName.TryAdd(property.Name, property);
}
return propertiesByName;
}
private static HashSet<string> CreateDefinitionNames(IEnumerable<ThemePropertyDefinition> definitions)
{
var names = new HashSet<string>(StringComparer.Ordinal);
foreach (var definition in definitions) names.Add(definition.Name);
return names;
}
private static ThemeColorProperty TryGetProperty(Dictionary<string, ThemeColorProperty> propertiesByName, string propertyName)
{
return propertiesByName.GetValueOrDefault(propertyName);
}
private static ThemeColorProperty TryGetSameIndexFallbackProperty(IReadOnlyList<ThemeColorProperty> existingProperties, HashSet<string> definitionNames, int index)
{
if (index >= existingProperties.Count) return null;
var property = existingProperties[index];
if (property == null) return null;
return string.IsNullOrWhiteSpace(property.Name) || !definitionNames.Contains(property.Name) ? property : null;
}
private static List<ThemePropertyDefinition> GetThemePropertyDefinitions()
{
return TryReadThemePropertyDefinitionsFromCommonUss();
}
private static List<ThemePropertyDefinition> TryReadThemePropertyDefinitionsFromCommonUss()
{
var path = GetCommonUssFullPath();
if (!File.Exists(path)) return new List<ThemePropertyDefinition>();
var definitions = new List<ThemePropertyDefinition>();
var lines = File.ReadAllLines(path);
var inRootBlock = false;
foreach (var rawLine in lines)
{
var line = rawLine.Trim();
if (!inRootBlock)
{
if (line == ":root {") inRootBlock = true;
continue;
}
if (line == "}") break;
var match = RootColorVariableRegex.Match(line);
if (!match.Success) continue;
var name = match.Groups["name"].Value;
var dayColor = FromHex(match.Groups["value"].Value);
var nightColor = GetDefaultNightColor(name, dayColor);
definitions.Add(new ThemePropertyDefinition(name, dayColor, nightColor));
}
return definitions;
}
private static Color GetDefaultNightColor(string name, Color dayColor)
{
return name switch
{
"--wood-color" => FromHex("#424C54"),
_ => dayColor
};
}
private static Color FromHex(string hex)
{
if (ColorUtility.TryParseHtmlString(hex, out var color)) return color;
throw new ArgumentException($"Invalid color value: {hex}", nameof(hex));
}
private static string GetCommonUssFullPath()
{
return Path.Combine(Application.dataPath, CommonUssAssetPath.Replace('/', Path.DirectorySeparatorChar));
}
private static string ToHex(Color color)
{
var color32 = (Color32)color;
return color32.a == byte.MaxValue ? $"#{color32.r:X2}{color32.g:X2}{color32.b:X2}" : $"#{color32.r:X2}{color32.g:X2}{color32.b:X2}{color32.a:X2}";
}
private readonly struct ThemePropertyDefinition
{
public readonly string Name;
public readonly Color DayColor;
public readonly Color NightColor;
public ThemePropertyDefinition(string name, Color dayColor, Color nightColor)
{
Name = name;
DayColor = dayColor;
NightColor = nightColor;
}
}
[Serializable]
public class ThemeColorProperty
{
[ReadOnly]
public string Name;
public Color DayColor = Color.white;
public Color NightColor = Color.white;
public ThemeColorProperty()
{
}
public ThemeColorProperty(string name, Color dayColor, Color nightColor)
{
Name = name;
DayColor = dayColor;
NightColor = nightColor;
}
}
#if UNITY_EDITOR
[HorizontalGroup("Sync")]
[Button("Copy Day Colors From USS", ButtonSizes.Large)]
[GUIColor("cyan")]
private void CopyDayColorsFromUss()
{
EnsureProperties();
var definitions = TryReadThemePropertyDefinitionsFromCommonUss();
if (definitions.Count == 0)
{
Debug.LogWarning($"Could not read any theme properties from '{CommonUssEditorAssetPath}'.", this);
return;
}
var propertiesByName = CreatePropertiesByName(Properties);
foreach (var definition in definitions)
{
if (!propertiesByName.TryGetValue(definition.Name, out var property)) continue;
property.DayColor = definition.DayColor;
}
EditorUtility.SetDirty(this);
AssetDatabase.SaveAssetIfDirty(this);
}
[HorizontalGroup("Sync")]
[Button("Apply Day Colors To USS", ButtonSizes.Large)]
[GUIColor("cyan")]
private void ApplyDayColorsToUss()
{
EnsureProperties();
var path = GetCommonUssFullPath();
if (!File.Exists(path))
{
Debug.LogWarning($"Could not find '{CommonUssEditorAssetPath}'.", this);
return;
}
var propertiesByName = CreatePropertiesByName(Properties);
var lines = File.ReadAllLines(path);
var updatedAnyLine = false;
var inRootBlock = false;
for (var i = 0; i < lines.Length; i++)
{
var rawLine = lines[i];
var trimmedLine = rawLine.Trim();
if (!inRootBlock)
{
if (trimmedLine == ":root {") inRootBlock = true;
continue;
}
if (trimmedLine == "}") break;
var match = RootColorVariableRegex.Match(trimmedLine);
if (!match.Success) continue;
var propertyName = match.Groups["name"].Value;
if (!propertiesByName.TryGetValue(propertyName, out var property)) continue;
var indentationLength = rawLine.Length - rawLine.TrimStart().Length;
var indentation = rawLine.Substring(0, indentationLength);
lines[i] = $"{indentation}{propertyName}: {ToHex(property.DayColor)};";
updatedAnyLine = true;
}
if (!updatedAnyLine)
{
Debug.LogWarning($"No matching ':root' theme color properties were updated in '{CommonUssEditorAssetPath}'.", this);
return;
}
File.WriteAllLines(path, lines);
AssetDatabase.ImportAsset(CommonUssEditorAssetPath);
}
#endif
}
}

View File

@@ -0,0 +1,228 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.UIElements;
namespace DanieleMarotta.RiversongCodeShowcase
{
[GameSystemGroup(typeof(UISystemGroup))]
[InitializeAfter(typeof(UIInitializationSystem))]
public class DayNightUIThemeSystem : GameSystem, IInitializable, IDisposable, IUpdatable
{
[InjectService]
private GameConfig _config;
[InjectService]
private UIService _uiService;
[InjectService]
private World _world;
private VisualElement _root;
private RuntimeThemeSheet _runtimeThemeSheet;
private float _lastAppliedNightBlend = -1;
private bool _refreshClassEnabled;
public DayNightUIThemeSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public UniTask InitializeAsync()
{
_root = _uiService.UIRoot.RootVisualElement;
_runtimeThemeSheet = RuntimeThemeSheet.TryCreate(_root, _config.UI.Theme);
ApplyTheme(true);
return UniTask.CompletedTask;
}
public void Update()
{
ApplyTheme();
}
public void Dispose()
{
if (_root == null || _runtimeThemeSheet == null) return;
_runtimeThemeSheet.Apply(0);
ToggleRefreshClass();
_lastAppliedNightBlend = 0;
}
private void ApplyTheme(bool force = false)
{
if (_root == null || _config.UI.Theme == null || _runtimeThemeSheet == null) return;
var nightBlend = _world.TimeState.NightBlend;
if (!force && !Application.isEditor && Mathf.Approximately(nightBlend, _lastAppliedNightBlend)) return;
_runtimeThemeSheet.Apply(nightBlend);
ToggleRefreshClass();
_lastAppliedNightBlend = nightBlend;
}
private void ToggleRefreshClass()
{
_refreshClassEnabled = !_refreshClassEnabled;
_root.EnableInClassList(RuntimeThemeSheet.RefreshClassName, _refreshClassEnabled);
}
private sealed class RuntimeThemeSheet
{
public const string RefreshClassName = "__day-night-theme-refresh";
private readonly StyleSheet _styleSheet;
private readonly RuntimePropertyBinding[] _bindings;
private RuntimeThemeSheet(StyleSheet styleSheet, RuntimePropertyBinding[] bindings)
{
_styleSheet = styleSheet;
_bindings = bindings;
}
public static RuntimeThemeSheet TryCreate(VisualElement root, DayNightUITheme theme)
{
if (root == null || theme == null) return null;
var styleSheets = StyleSheetReflection.GetStyleSheets(root);
if (styleSheets == null) return null;
foreach (var styleSheet in styleSheets)
{
if (!StyleSheetReflection.TryCreateBindings(styleSheet, theme.OrderedProperties, out var bindings)) continue;
return new RuntimeThemeSheet(styleSheet, bindings);
}
return null;
}
public void Apply(float nightBlend)
{
foreach (var binding in _bindings) binding.Apply(nightBlend);
_styleSheet.contentHash = unchecked(_styleSheet.contentHash + 1);
}
}
private sealed class RuntimePropertyBinding
{
private readonly DayNightUITheme.ThemeColorProperty _themeProperty;
private readonly object _manipulator;
private readonly object[] _setColorArguments = new object[2];
public RuntimePropertyBinding(DayNightUITheme.ThemeColorProperty themeProperty, object manipulator)
{
_themeProperty = themeProperty;
_manipulator = manipulator;
}
public void Apply(float nightBlend)
{
_setColorArguments[0] = 0;
_setColorArguments[1] = Color.Lerp(_themeProperty.DayColor, _themeProperty.NightColor, nightBlend);
StyleSheetReflection.SetColor(_manipulator, _setColorArguments);
}
}
private static class StyleSheetReflection
{
private static readonly FieldInfo VisualElementStyleSheetListField = typeof(VisualElement).GetField("styleSheetList", BindingFlags.Instance | BindingFlags.NonPublic);
private static readonly FieldInfo StyleSheetRulesField = typeof(StyleSheet).GetField("m_Rules", BindingFlags.Instance | BindingFlags.NonPublic);
private static readonly Type StyleRuleType = StyleSheetRulesField?.FieldType.GetElementType();
private static readonly MethodInfo StyleRuleGetPropertiesMethod = StyleRuleType?.GetMethod("get_properties", BindingFlags.Instance | BindingFlags.Public);
private static readonly Type StylePropertyType = StyleRuleGetPropertiesMethod?.ReturnType.GetElementType();
private static readonly MethodInfo StylePropertyGetNameMethod = StylePropertyType?.GetMethod("get_name", BindingFlags.Instance | BindingFlags.Public);
private static readonly MethodInfo StylePropertyGetManipulatorMethod = StylePropertyType?.GetMethod("GetManipulator", BindingFlags.Instance | BindingFlags.NonPublic);
private static readonly Type ManipulatorType = StylePropertyGetManipulatorMethod?.ReturnType;
private static readonly MethodInfo ManipulatorSetColorMethod = ManipulatorType?.GetMethod(
"SetColor",
BindingFlags.Instance | BindingFlags.Public,
null,
new[]
{
typeof(int),
typeof(Color)
},
null);
public static List<StyleSheet> GetStyleSheets(VisualElement root)
{
return VisualElementStyleSheetListField?.GetValue(root) as List<StyleSheet>;
}
public static void SetColor(object manipulator, object[] arguments)
{
ManipulatorSetColorMethod?.Invoke(manipulator, arguments);
}
public static bool TryCreateBindings(StyleSheet styleSheet, IReadOnlyList<DayNightUITheme.ThemeColorProperty> themeProperties, out RuntimePropertyBinding[] bindings)
{
bindings = null;
if (styleSheet == null || themeProperties == null) return false;
if (StyleSheetRulesField == null ||
StyleRuleGetPropertiesMethod == null ||
StylePropertyGetNameMethod == null ||
StylePropertyGetManipulatorMethod == null ||
ManipulatorSetColorMethod == null)
return false;
var propertiesByName = new Dictionary<string, object>(themeProperties.Count, StringComparer.Ordinal);
var rules = StyleSheetRulesField.GetValue(styleSheet) as Array;
if (rules == null) return false;
foreach (var rule in rules)
{
if (rule == null) continue;
var styleProperties = StyleRuleGetPropertiesMethod.Invoke(rule, null) as Array;
if (styleProperties == null) continue;
foreach (var styleProperty in styleProperties)
{
if (styleProperty == null) continue;
var propertyName = StylePropertyGetNameMethod.Invoke(styleProperty, null) as string;
if (string.IsNullOrEmpty(propertyName)) continue;
if (propertiesByName.ContainsKey(propertyName)) continue;
propertiesByName[propertyName] = styleProperty;
}
}
var resolvedBindings = new RuntimePropertyBinding[themeProperties.Count];
for (var i = 0; i < themeProperties.Count; i++)
{
var themeProperty = themeProperties[i];
if (!propertiesByName.TryGetValue(themeProperty.Name, out var styleProperty)) return false;
var manipulator = StylePropertyGetManipulatorMethod.Invoke(styleProperty, new object[] { styleSheet });
resolvedBindings[i] = new RuntimePropertyBinding(themeProperty, manipulator);
}
bindings = resolvedBindings;
return true;
}
}
}
}

View File

@@ -0,0 +1,10 @@
using System;
using UnityEngine.UIElements;
namespace DanieleMarotta.RiversongCodeShowcase
{
public interface IUIModel : INotifyBindablePropertyChanged
{
event Action Changed;
}
}

View File

@@ -0,0 +1,19 @@
using System;
using Cysharp.Threading.Tasks;
using UnityEngine.UIElements;
namespace DanieleMarotta.RiversongCodeShowcase
{
public interface IUIRoot : IDisposable
{
VisualElement RootVisualElement { get; }
event Action<ClickEvent> ElementClicked;
UniTask Initialize(UIService uiService);
void MakeDraggable(VisualElement target);
T GetView<T>() where T : UIView;
}
}

View File

@@ -0,0 +1,54 @@
using Cysharp.Threading.Tasks;
using UnityEngine.UIElements;
namespace DanieleMarotta.RiversongCodeShowcase
{
[GameSystemGroup(typeof(UISystemGroup))]
[InitializeAfter(typeof(UIInitializationSystem))]
public abstract class UIControllerSystem : GameSystem, IInitializable
{
protected UIControllerSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
[field: InjectService] protected UIService UIService { get; }
protected IUIRoot UIRoot => UIService.UIRoot;
public virtual UniTask InitializeAsync()
{
return UniTask.CompletedTask;
}
}
[GameSystemGroup(typeof(UISystemGroup))]
[InitializeAfter(typeof(UIInitializationSystem))]
public abstract class UIControllerSystem<T> : UIControllerSystem where T : UIView
{
private Button _closeButton;
protected UIControllerSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
protected abstract T View { get; }
public override async UniTask InitializeAsync()
{
await base.InitializeAsync();
_closeButton = View.RootElement.Q<Button>(className: "close-button");
_closeButton?.RegisterCallback<ClickEvent>(OnCloseButtonClick);
}
private void OnCloseButtonClick(ClickEvent evt)
{
CloseView();
}
protected virtual void CloseView(bool animate = true)
{
View.Show(false, animate);
}
}
}

View File

@@ -0,0 +1,66 @@
using System;
using Cysharp.Threading.Tasks;
using UnityEngine.UIElements;
using IServiceProvider = DanieleMarotta.RiversongCodeShowcase.IServiceProvider;
namespace DanieleMarotta.RiversongCodeShowcase
{
[GameSystemGroup(typeof(UISystemGroup))]
public class UIInitializationSystem : GameSystem, IServiceProvider, IInitializable, IDisposable
{
[InjectService]
private GameConfig _config;
[InjectService]
private ISoundPlayer _soundPlayer;
private IUIRoot _uiRoot;
private UIService _uiService;
private TextFormatHelper _textFormatHelper;
public UIInitializationSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public void RegisterServices(IServiceLocator serviceLocator)
{
serviceLocator.RegisterService(new UIState());
_uiService = new UIService();
serviceLocator.RegisterService(_uiService);
_textFormatHelper = new TextFormatHelper();
serviceLocator.RegisterService(_textFormatHelper);
}
public async UniTask InitializeAsync()
{
var uiRootGameObject = await _config.UI.RootPrefab.InstantiateAsync();
_uiRoot = uiRootGameObject.GetComponent<IUIRoot>();
await _uiRoot.Initialize(_uiService);
_uiRoot.ElementClicked += OnElementClicked;
_uiService.UIRoot = _uiRoot;
_uiService.TemplateLibrary = await _config.UI.TemplateLibrary.LoadAssetAsync<UITemplateLibrary>();
_uiService.TextFormatHelper = _textFormatHelper;
}
public void Dispose()
{
_uiRoot.ElementClicked -= OnElementClicked;
_uiRoot.Dispose();
}
private void OnElementClicked(ClickEvent e)
{
if (e.target is not VisualElement element) return;
if (element.GetFirstAncestorOrSelf(static candidate => candidate is Button || candidate.ClassListContains("ui-click")) == null) return;
_soundPlayer.Play(SystemSoundId.UIClick);
}
}
}

View File

@@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using UnityEngine.UIElements;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class UIModel : IUIModel
{
public event EventHandler<BindablePropertyChangedEventArgs> propertyChanged;
public event Action Changed;
protected void SetProperty<T>(ref T field, T value, [CallerMemberName] string property = null)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return;
field = value;
NotifyPropertyChanged(property);
}
protected void NotifyPropertyChanged(string property = null)
{
propertyChanged?.Invoke(this, new BindablePropertyChangedEventArgs(property));
}
public void NotifyChanged()
{
Changed?.Invoke();
}
}
}

View File

@@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.UIElements;
namespace DanieleMarotta.RiversongCodeShowcase
{
[RequireComponent(typeof(UIDocument))]
public class UIRoot : MonoBehaviour, IUIRoot
{
private UIDocument _document;
private Dictionary<Type, UIView> _views = new();
public VisualElement RootVisualElement { get; private set; }
public event Action<ClickEvent> ElementClicked;
private void Awake()
{
_document = GetComponent<UIDocument>();
}
public async UniTask Initialize(UIService uiService)
{
RootVisualElement = _document.rootVisualElement;
await DiscoverViewsAsync(uiService);
InitializeDragging();
RootVisualElement.RegisterCallback<ClickEvent>(OnClick);
}
public void Dispose()
{
foreach (var view in _views.Values) view.Dispose();
RootVisualElement.UnregisterCallback<ClickEvent>(OnClick);
}
private void OnClick(ClickEvent evt)
{
ElementClicked?.Invoke(evt);
}
private async UniTask DiscoverViewsAsync(UIService uiService)
{
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (var assembly in assemblies)
foreach (var type in assembly.GetTypes())
{
var viewAttribute = type.GetCustomAttribute<UIViewAttribute>();
if (viewAttribute == null) continue;
var element = RootVisualElement.Q(className: viewAttribute.UssClassName);
var view = await uiService.CreateView(type, element);
_views.Add(type, view);
}
}
private void InitializeDragging()
{
var targets = RootVisualElement.Query<VisualElement>(className: "drag-target").ToList();
foreach (var target in targets) MakeDraggable(target);
}
public void MakeDraggable(VisualElement target)
{
var handle = target.Q<VisualElement>(className: "drag-handle") ?? target;
handle.AddManipulator(new DraggableManipulator(target));
}
public T GetView<T>() where T : UIView
{
return _views.TryGetValue(typeof(T), out var view) ? (T)view : null;
}
}
}

View File

@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using UnityEngine.UIElements;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class UIService
{
public IUIRoot UIRoot { get; set; }
public UITemplateLibrary TemplateLibrary { get; set; }
public TextFormatHelper TextFormatHelper { get; set; }
public async UniTask<UIView> CreateView(Type type, VisualElement rootElement)
{
var view = (UIView)Activator.CreateInstance(type);
await view.InitializeAsync(this, rootElement);
return view;
}
}
public static class UIServiceExtensions
{
public static async UniTask CreateViews<TModel, TView>(this UIService uiService, List<TModel> models, VisualElement container, VisualTreeAsset template)
where TModel : UIModel where TView : UIView<TModel>
{
container.Clear();
foreach (var model in models)
{
var element = template.CloneTree();
container.Add(element);
var view = (TView)await uiService.CreateView(typeof(TView), element);
view.SetModel(model);
}
}
}
}

View File

@@ -0,0 +1,8 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
[InitializeAfter(typeof(LateGameSystemGroup))]
public class UISystemGroup : GameSystemGroup
{
}
}

View File

@@ -0,0 +1,60 @@
using System;
using UnityEngine;
using UnityEngine.UIElements;
namespace DanieleMarotta.RiversongCodeShowcase
{
[CreateAssetMenu(fileName = "UITemplateLibrary", menuName = "Riversong Code Showcase/UI Template Library")]
public class UITemplateLibrary : ScriptableObject
{
public CommonTemplateLibrary Common;
public WorldUITemplateLibrary WorldUI;
public BuildMenuTemplateLibrary BuildMenu;
public BuildingPanelTemplateLibrary BuildingPanel;
public OnboardingPanelTemplateLibrary OnboardingPanel;
[Serializable]
public class CommonTemplateLibrary
{
public VisualTreeAsset ProductAmount;
}
[Serializable]
public class WorldUITemplateLibrary
{
public VisualTreeAsset Badge;
public Vector2 BadgeYOffset = new(32, 64);
public Vector2 BadgeScale = new(4, 1);
public float BadgeZoomCullThreshold = 90;
}
[Serializable]
public class BuildMenuTemplateLibrary
{
public VisualTreeAsset Button;
}
[Serializable]
public class BuildingPanelTemplateLibrary
{
public VisualTreeAsset Need;
public VisualTreeAsset UpgradeMaterial;
public VisualTreeAsset StorageProduct;
}
[Serializable]
public class OnboardingPanelTemplateLibrary
{
public VisualTreeAsset Message;
}
}
}

View File

@@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using UnityEngine.UIElements;
namespace DanieleMarotta.RiversongCodeShowcase
{
public abstract class UIView : IDisposable
{
private UIVisibilityAnimation _visibilityAnimation;
public UIService UIService { get; private set; }
public VisualElement RootElement { get; private set; }
public event Action<bool> OpenedOrClosed;
public virtual UniTask InitializeAsync(UIService uiService, VisualElement rootElement)
{
UIService = uiService;
RootElement = rootElement;
_visibilityAnimation = new UIVisibilityAnimation(rootElement, isOpen => OpenedOrClosed?.Invoke(isOpen));
return UniTask.CompletedTask;
}
public bool IsOpen()
{
return _visibilityAnimation.IsOpen;
}
public void Show(bool show, bool animate = false)
{
_visibilityAnimation.Show(show, animate);
}
public virtual void Dispose()
{
}
}
public abstract class UIView<T> : UIView where T : class
{
public T Model { get; private set; }
public void SetModel(T model)
{
if (EqualityComparer<T>.Default.Equals(Model, model)) return;
var oldModel = Model;
Model = model;
if (oldModel is IUIModel oldUiModel) oldUiModel.Changed -= OnModelChanged;
if (oldModel is INotifyBindablePropertyChanged oldNotifyingModel) oldNotifyingModel.propertyChanged -= OnModelPropertyChanged;
if (model is IUIModel uiModel) uiModel.Changed += OnModelChanged;
if (model is INotifyBindablePropertyChanged notifyingModel) notifyingModel.propertyChanged += OnModelPropertyChanged;
RootElement.dataSource = model;
OnNewModel(model);
}
protected virtual void OnNewModel(T model)
{
}
protected virtual void OnModelChanged()
{
}
protected virtual void OnModelPropertyChanged(object sender, BindablePropertyChangedEventArgs e)
{
}
}
public static class UIViewExtensions
{
public static void Toggle(this UIView view, bool animate = false)
{
view.Show(!view.IsOpen(), animate);
}
}
}

View File

@@ -0,0 +1,15 @@
using System;
namespace DanieleMarotta.RiversongCodeShowcase
{
[AttributeUsage(AttributeTargets.Class)]
public class UIViewAttribute : Attribute
{
public UIViewAttribute(string ussClassName = null)
{
UssClassName = ussClassName;
}
public string UssClassName { get; set; }
}
}

View File

@@ -0,0 +1,112 @@
using System;
using PrimeTween;
using UnityEngine.UIElements;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class UIVisibilityAnimation
{
private VisualElement _element;
private Action<bool> _onCompleted;
private Sequence _visibilityAnimation;
public UIVisibilityAnimation(VisualElement element, Action<bool> onCompleted)
{
_element = element;
_onCompleted = onCompleted;
IsOpen = element.resolvedStyle.display != DisplayStyle.None;
TargetIsOpen = IsOpen;
ApplyCommittedState(IsOpen);
}
public bool IsOpen { get; private set; }
public bool TargetIsOpen { get; private set; }
public bool IsAnimating => _visibilityAnimation.isAlive;
public void Show(bool show, bool animate = false)
{
TargetIsOpen = show;
if (IsAnimating) return;
if (IsOpen == TargetIsOpen) return;
if (!animate)
{
ApplyCommittedState(TargetIsOpen);
IsOpen = TargetIsOpen;
_onCompleted?.Invoke(IsOpen);
return;
}
TryAdvance();
}
private void TryAdvance()
{
if (IsAnimating) return;
if (IsOpen == TargetIsOpen) return;
if (TargetIsOpen)
PlayOpenAnimation();
else
PlayCloseAnimation();
}
private void PlayOpenAnimation()
{
_element.style.display = DisplayStyle.Flex;
_visibilityAnimation = PlayShowTween(_element).OnComplete(() => CompleteTransition(true));
}
private void PlayCloseAnimation()
{
_element.style.display = DisplayStyle.Flex;
_visibilityAnimation = PlayHideTween(_element).OnComplete(() => CompleteTransition(false));
}
private void CompleteTransition(bool isOpen)
{
_visibilityAnimation = default;
ApplyCommittedState(isOpen);
IsOpen = isOpen;
_onCompleted?.Invoke(IsOpen);
TryAdvance();
}
private void ApplyCommittedState(bool isOpen)
{
_element.style.display = isOpen ? DisplayStyle.Flex : DisplayStyle.None;
_element.style.opacity = isOpen ? 1 : 0;
if (isOpen)
_element.style.scale = StyleKeyword.Null;
else
_element.SetScale(0.8f);
}
public static Sequence PlayShowTween(VisualElement element, float duration = 0.3f)
{
element.style.opacity = 0;
element.SetScale(0.8f);
return Sequence.Create(useUnscaledTime: true)
.Group(Tween.Custom(0.8f, 1, duration, element.SetScale, Ease.OutQuad))
.Group(Tween.Custom(0, 1, duration, value => element.style.opacity = value, Ease.OutQuad));
}
public static Sequence PlayHideTween(VisualElement element, float duration = 0.2f)
{
element.style.opacity = 1;
element.SetScale(1);
return Sequence.Create(useUnscaledTime: true)
.Group(Tween.Custom(1, 0.8f, duration, value => element.SetScale(value), Ease.OutQuad))
.Group(Tween.Custom(1, 0, duration, value => element.style.opacity = value, Ease.OutQuad));
}
}
}

View File

@@ -0,0 +1,66 @@
using UnityEngine;
using UnityEngine.UIElements;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class DraggableManipulator : PointerManipulator
{
private const string DraggedUssClassName = "dragged";
private Vector2 _startMousePosition;
private Vector2 _startElementPosition;
private VisualElement _draggedElement;
public DraggableManipulator(VisualElement draggedElement)
{
_draggedElement = draggedElement;
}
protected override void RegisterCallbacksOnTarget()
{
target.RegisterCallback<PointerDownEvent>(OnPointerDown);
target.RegisterCallback<PointerMoveEvent>(OnPointerMove);
target.RegisterCallback<PointerUpEvent>(OnPointerUp);
}
protected override void UnregisterCallbacksFromTarget()
{
target.UnregisterCallback<PointerDownEvent>(OnPointerDown);
target.UnregisterCallback<PointerMoveEvent>(OnPointerMove);
target.UnregisterCallback<PointerUpEvent>(OnPointerUp);
}
private void OnPointerDown(PointerDownEvent evt)
{
if (evt.button != 0) return;
target.CapturePointer(evt.pointerId);
evt.StopPropagation();
_startMousePosition = evt.position;
_startElementPosition = new Vector2(_draggedElement.resolvedStyle.left, _draggedElement.resolvedStyle.top);
_draggedElement.AddToClassList(DraggedUssClassName);
}
private void OnPointerMove(PointerMoveEvent evt)
{
if (!target.HasPointerCapture(evt.pointerId)) return;
var delta = (Vector2)evt.position - _startMousePosition;
_draggedElement.style.left = _startElementPosition.x + delta.x;
_draggedElement.style.top = _startElementPosition.y + delta.y;
}
private void OnPointerUp(PointerUpEvent evt)
{
if (!target.HasPointerCapture(evt.pointerId)) return;
target.ReleasePointer(evt.pointerId);
_draggedElement.RemoveFromClassList(DraggedUssClassName);
}
}
}

View File

@@ -0,0 +1,66 @@
using System;
using System.Text;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class TextFormatHelper
{
public string Pluralize(int value, string singular, string plural)
{
return value == 1 ? singular : plural;
}
public string FormatImportantText(string text)
{
return $"<b>{text}</b>";
}
public void FormatImportantText(StringBuilder sb, string text)
{
sb.Append("<b>");
sb.Append(text);
sb.Append("</b>");
}
public void FormatUnlockConditions(UnlockDefinition unlock, StringBuilder sb)
{
if (unlock.Type != UnlockType.UnlockBuilding || unlock.Conditions.Count <= 0) return;
sb.Append("To unlock the ");
FormatImportantText(sb, unlock.Building.BuildingName);
sb.AppendLine(", your village needs a little more:");
foreach (var condition in unlock.Conditions)
{
sb.AppendLine();
FormatUnlockCondition(sb, condition);
}
}
private void FormatUnlockCondition(StringBuilder sb, UnlockCondition condition)
{
sb.Append("\u2022 ");
switch (condition.Type)
{
case UnlockConditionType.BuildingPlaced:
sb.Append("Add a ");
FormatImportantText(sb, condition.Building.BuildingName);
sb.Append(" to your village");
return;
case UnlockConditionType.HouseCountAtTierOrAbove:
sb.Append("Grow ");
sb.Append(condition.HouseCount);
sb.Append(' ');
sb.Append(Pluralize(condition.HouseCount, "house", "houses"));
sb.Append(" to tier ");
sb.Append(condition.MinHouseTierIndex + 1);
return;
default:
throw new ArgumentOutOfRangeException();
}
}
}
}

View File

@@ -0,0 +1,40 @@
using System;
using UnityEngine;
using UnityEngine.UIElements;
namespace DanieleMarotta.RiversongCodeShowcase
{
public static class VisualElementExtensions
{
public static VisualElement GetFirstAncestorOrSelf(this VisualElement element, Func<VisualElement, bool> predicate)
{
while (element != null)
{
if (predicate(element)) return element;
element = element.parent;
}
return null;
}
public static void SetAbsoluteScreenPosition(this VisualElement element, Vector2 screenPoint)
{
element.style.position = Position.Absolute;
element.style.left = screenPoint.x;
element.style.top = Screen.height - screenPoint.y;
}
public static void SetScale(this VisualElement element, float value)
{
element.style.scale = new StyleScale(new Scale(Vector2.one * value));
}
public static void SetPickingModeRecursive(this VisualElement element, PickingMode pickingMode)
{
element.pickingMode = pickingMode;
var hierarchy = element.hierarchy;
for (var i = 0; i < hierarchy.childCount; i++) hierarchy.ElementAt(i).SetPickingModeRecursive(pickingMode);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public class UIState
{
public Building SelectedBuilding { get; set; }
}
}