UI/TreeSkillPanel/TreeSkillPanel.razor
@using System;
@using System.Linq;
@using Milaine.Managers;
@using Milaine.Settings;
@using Milaine.TreeSkills;
@using Milaine.Utils;
@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent
@namespace Milaine.UI
<root class="viewport" style="display: @(isOpened ? "flex" : "none");">
<div class="canvas"
style="left: @(Pixel.Inv(PanX))px; top: @(Pixel.Inv(PanY))px; transform: scale(@(Pixel.Inv(Zoom)));">
@foreach (var (node, neighbor) in GetConnectionsToDraw()) {
var (x1, y1) = GetNodeCender(node);
var (x2, y2) = GetNodeCender(neighbor);
float dx = x2 - x1;
float dy = y2 - y1;
float length = MathF.Sqrt(dx * dx + dy * dy);
float angle = MathF.Atan2(dy, dx) * (180f / MathF.PI);
bool bothUnlocked = Tree.IsUnlocked(node) && Tree.IsUnlocked(neighbor);
<div class="connection @(bothUnlocked ? "active" : "")"
style="left: @(Pixel.Inv(x1))px; top: @(Pixel.Inv(y1))px; width: @(Pixel.Inv(length))px; transform: rotate(@(Pixel.Inv(angle))deg);" />
}
@foreach (var node in Tree.Nodes) {
bool unlocked = Tree.IsUnlocked(node);
bool canUnlock = Tree.CanUnlock(node);
bool canPurchase = Tree.CanPurchase(node);
<button
class="skill-node @(unlocked ? "unlocked" : "") @(!unlocked && canUnlock ? "available" : "") @(!unlocked && !unlocked ? "locked" : "") @(canPurchase ? "pointer" : "")"
style="left: @(Pixel.Inv(node.Column * CELL_PITCH))px; top: @(Pixel.Inv(node.Row * CELL_PITCH))px;" role="button"
onclick=@(() => { OnNodeClicked(node); }) onmouseover=@(() => hoveredNodeId = node.Id) onmouseout=@(() =>
hoveredNodeId = "")>
<div class="icon">@node.Icon</div>
<div class="name">@TranslationsInstance.T(node.Name)</div>
@if (!unlocked) {
<div class="cost @(canPurchase ? "text-green" : "")">@node.Cost</div>
}
@if (hoveredNodeId == node.Id) {
<div class="popup">
<div class="popup-container">
<p>@TranslationsInstance.T(node.Description, data)</p>
</div>
</div>
}
</button>
}
</div>
<div class="reset-button" onclick=@ResetState>
@* #milaine.reset-position *@
@TranslationsInstance.T("milaine.reset-position")
</div>
</root>
@code
{
private const float CELL_PITCH = 220f;
private const float NODE_SIZE = 140f;
private const float MIN_ZOOM = 0.3f;
private const float MAX_ZOOM = 2f;
private const float STANDARD_ZOOM = 1f;
private const float ZOOM_SPEED = 0.1f;
private const float DEFAULT_PAN_X = 400f;
private const float DEFAULT_PAN_Y = 0f;
[Property, Title("Translations")]
public Translations TranslationsInstance { get; set; }
private float PanX = DEFAULT_PAN_X;
private float PanY = DEFAULT_PAN_Y;
private float Zoom = 1f;
private bool isOpened = false;
private Vector2 dragStartMouse;
private float dragStartPanX;
private float dragStartPanY;
private Dictionary<string, object> data = [];
private string hoveredNodeId;
private TreeSkillState Tree => TreeSkillState.Instance;
protected override int BuildHash() => System.HashCode.Combine(isOpened, MouseManager.State, hoveredNodeId,
TranslationsInstance.language);
protected override void OnTreeFirstBuilt() {
base.OnTreeFirstBuilt();
Tree.OnChanged += StateHasChanged;
foreach (SkillNodeData node in Tree.Nodes) {
data[node.KeyDescription] = node.Point;
}
}
protected override void OnDestroy() {
base.OnDestroy();
Tree.OnChanged -= StateHasChanged;
}
protected override void OnUpdate() {
base.OnUpdate();
if (!Panel.HasMouseCapture) {
return;
}
Vector2 delta = Mouse.Position - dragStartMouse;
PanX = dragStartPanX + delta.x;
PanY = dragStartPanY + delta.y;
StateHasChanged();
}
protected override void OnMouseDown(MousePanelEvent e) {
base.OnMouseDown(e);
if (e.Button == "mousemiddle") {
Panel.SetMouseCapture(true);
dragStartMouse = Mouse.Position;
dragStartPanX = PanX;
dragStartPanY = PanY;
e.StopPropagation();
}
}
protected override void OnMouseUp(MousePanelEvent e) {
base.OnMouseUp(e);
if (e.Button == "mousemiddle") {
Panel.SetMouseCapture(false);
}
}
protected override void OnMouseWheel(Vector2 delta) {
base.OnMouseWheel(delta);
float wheel = -delta.y;
if (MathF.Abs(wheel) < 0.01f) {
return;
}
float oldZoom = Zoom;
float newZoom = (Zoom + wheel * ZOOM_SPEED).Clamp(MIN_ZOOM, MAX_ZOOM);
if (newZoom.AlmostEqual(oldZoom)) {
return;
}
var rect = Panel.Box.Rect;
if (rect.Width <= 0 || rect.Height <= 0) {
Zoom = newZoom;
StateHasChanged();
return;
}
Vector2 localMouse = Mouse.Position - rect.Position;
float worldX = (localMouse.x - PanX) / oldZoom;
float worldY = (localMouse.y - PanY) / oldZoom;
Zoom = newZoom;
PanX = localMouse.x - worldX * newZoom;
PanY = localMouse.y - worldY * newZoom;
StateHasChanged();
}
public void Open() {
MouseManager.State = StateMouse.Busy;
isOpened = true;
ResetState();
Sound.Play("panelopen");
}
public void Close() {
MouseManager.State = StateMouse.Relased;
isOpened = false;
ResetState();
Sound.Play("panelopen");
}
private void ResetState() {
PanX = DEFAULT_PAN_X;
PanY = DEFAULT_PAN_Y;
Zoom = STANDARD_ZOOM;
StateHasChanged();
}
private void OnNodeClicked(SkillNodeData node) {
Tree.TryUnlock(node);
StateHasChanged();
Panel.Blur();
}
private (float x, float y) GetNodeCender(SkillNodeData node) {
float x = node.Column * CELL_PITCH + NODE_SIZE / 2f;
float y = node.Row * CELL_PITCH + NODE_SIZE / 2;
return (x, y);
}
private IEnumerable<(SkillNodeData, SkillNodeData)> GetConnectionsToDraw() {
foreach (var node in Tree.Nodes) {
foreach (var neighbor in Tree.GetExistingNeighbors(node)) {
bool isForward = neighbor.Row > node.Row || (neighbor.Row == node.Row && neighbor.Column > node.Column);
if (isForward) {
yield return (node, neighbor);
}
}
}
}
}