mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
TUI support for more container
This commit is contained in:
parent
ef133f3d6d
commit
0194380fbc
17 changed files with 2045 additions and 822 deletions
152
MinecraftClient/Tui/BrewingStandView.cs
Normal file
152
MinecraftClient/Tui/BrewingStandView.cs
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
using System.Collections.ObjectModel;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using MinecraftClient.Inventory;
|
||||
|
||||
namespace MinecraftClient.Tui
|
||||
{
|
||||
public class BrewingStandView : ContainerViewBase
|
||||
{
|
||||
private readonly BrewingViewModel _brewVm;
|
||||
|
||||
public BrewingStandView(McClient handler, int windowId)
|
||||
: base(new BrewingViewModel(handler, windowId))
|
||||
{
|
||||
_brewVm = (BrewingViewModel)_vm;
|
||||
Initialize();
|
||||
}
|
||||
|
||||
protected override int GetTotalSlotRows()
|
||||
{
|
||||
return 3 + 3 + 1;
|
||||
}
|
||||
|
||||
protected override Control BuildContainerSpecificArea()
|
||||
{
|
||||
var panel = new StackPanel
|
||||
{
|
||||
Spacing = 0,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
};
|
||||
|
||||
var topRow = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
Spacing = 0,
|
||||
};
|
||||
|
||||
var fuelCol = new StackPanel
|
||||
{
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
fuelCol.Children.Add(new TextBlock
|
||||
{
|
||||
Text = Translations.tui_brewing_fuel,
|
||||
Foreground = BrEquipLbl,
|
||||
FontWeight = FontWeight.Bold,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
});
|
||||
fuelCol.Children.Add(CreateSlotCell(_brewVm.FuelSlot, 0, 0));
|
||||
topRow.Children.Add(fuelCol);
|
||||
|
||||
topRow.Children.Add(new Border { Width = 2 });
|
||||
|
||||
var ingredientCol = new StackPanel
|
||||
{
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
ingredientCol.Children.Add(new TextBlock
|
||||
{
|
||||
Text = Translations.tui_brewing_ingredient,
|
||||
Foreground = BrEquipLbl,
|
||||
FontWeight = FontWeight.Bold,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
});
|
||||
ingredientCol.Children.Add(CreateSlotCell(_brewVm.IngredientSlot, 0, 1));
|
||||
topRow.Children.Add(ingredientCol);
|
||||
|
||||
panel.Children.Add(topRow);
|
||||
|
||||
panel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "\u25bc",
|
||||
Foreground = new SolidColorBrush(Color.FromRgb(140, 140, 140)),
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
});
|
||||
|
||||
var bottleRow = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
Spacing = 0,
|
||||
};
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
var bottlePanel = new StackPanel
|
||||
{
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
bottlePanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = string.Format(Translations.tui_brewing_bottle, i + 1),
|
||||
Foreground = BrEquipLbl,
|
||||
FontWeight = FontWeight.Bold,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
});
|
||||
bottlePanel.Children.Add(CreateSlotCell(_brewVm.BottleSlots[i], 1, i));
|
||||
bottleRow.Children.Add(bottlePanel);
|
||||
}
|
||||
|
||||
panel.Children.Add(bottleRow);
|
||||
|
||||
return panel;
|
||||
}
|
||||
}
|
||||
|
||||
public class BrewingViewModel : ContainerViewModel
|
||||
{
|
||||
public ObservableCollection<SlotViewModel> BottleSlots { get; } = new();
|
||||
public SlotViewModel IngredientSlot { get; private set; } = null!;
|
||||
public SlotViewModel FuelSlot { get; private set; } = null!;
|
||||
|
||||
public BrewingViewModel(McClient handler, int windowId)
|
||||
: base(handler, windowId, ContainerType.BrewingStand)
|
||||
{
|
||||
IngredientSlot = SlotMap[3];
|
||||
FuelSlot = SlotMap[4];
|
||||
}
|
||||
|
||||
protected override void InitializeSlots()
|
||||
{
|
||||
SlotMap.Clear();
|
||||
|
||||
for (int i = 0; i <= 2; i++)
|
||||
{
|
||||
var slot = new SlotViewModel(i);
|
||||
BottleSlots.Add(slot);
|
||||
SlotMap[i] = slot;
|
||||
}
|
||||
|
||||
SlotMap[3] = new SlotViewModel(3);
|
||||
SlotMap[4] = new SlotViewModel(4);
|
||||
|
||||
for (int i = 5; i <= 31; i++)
|
||||
{
|
||||
var slot = new SlotViewModel(i);
|
||||
MainInventorySlots.Add(slot);
|
||||
SlotMap[i] = slot;
|
||||
}
|
||||
|
||||
for (int i = 32; i <= 40; i++)
|
||||
{
|
||||
int hotbarIdx = i - 32;
|
||||
var slot = new SlotViewModel(i, isHotbar: true, hotbarIndex: hotbarIdx);
|
||||
HotbarSlots.Add(slot);
|
||||
SlotMap[i] = slot;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
730
MinecraftClient/Tui/ContainerViewBase.cs
Normal file
730
MinecraftClient/Tui/ContainerViewBase.cs
Normal file
|
|
@ -0,0 +1,730 @@
|
|||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Controls.Templates;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using MinecraftClient.Inventory;
|
||||
|
||||
namespace MinecraftClient.Tui
|
||||
{
|
||||
public abstract class ContainerViewBase : UserControl
|
||||
{
|
||||
protected static readonly IBrush BrSlotEmptyA = new SolidColorBrush(Color.FromRgb(40, 40, 40));
|
||||
protected static readonly IBrush BrSlotEmptyB = new SolidColorBrush(Color.FromRgb(55, 55, 55));
|
||||
protected static readonly IBrush BrSlotFillA = new SolidColorBrush(Color.FromRgb(60, 60, 75));
|
||||
protected static readonly IBrush BrSlotFillB = new SolidColorBrush(Color.FromRgb(75, 75, 90));
|
||||
protected static readonly IBrush BrSlotHover = new SolidColorBrush(Color.FromRgb(100, 100, 140));
|
||||
protected static readonly IBrush BrName = Brushes.White;
|
||||
protected static readonly IBrush BrCount = Brushes.Yellow;
|
||||
protected static readonly IBrush BrDim = new SolidColorBrush(Color.FromRgb(80, 80, 80));
|
||||
protected static readonly IBrush BrEquipLbl = Brushes.DarkCyan;
|
||||
protected static readonly IBrush BrInfoHighlight = new SolidColorBrush(Color.FromRgb(40, 40, 60));
|
||||
protected static readonly IBrush BrHeldItemBg = new SolidColorBrush(Color.FromRgb(60, 50, 80));
|
||||
protected static readonly IBrush BrHeldItemBorder = Brushes.Yellow;
|
||||
|
||||
protected int _slotW;
|
||||
protected int _slotH;
|
||||
protected int _nameMaxLen;
|
||||
protected int _nameLines;
|
||||
protected int _termW;
|
||||
|
||||
protected readonly ContainerViewModel _vm;
|
||||
protected TextBlock _titleText = null!;
|
||||
protected Border _infoDetailBorder = null!;
|
||||
protected TextBlock _infoDetailText = null!;
|
||||
protected TextBlock _cursorItemText = null!;
|
||||
protected TextBlock _helpText = null!;
|
||||
|
||||
protected TextBlock[] _hotbarIndicators = new TextBlock[9];
|
||||
protected int _currentHotbarSlot = -1;
|
||||
|
||||
protected Border? _lastHoveredSlotBorder;
|
||||
|
||||
protected Canvas _overlayCanvas = null!;
|
||||
protected Border _heldItemFloater = null!;
|
||||
protected TextBlock _heldItemFloaterName = null!;
|
||||
protected TextBlock _heldItemFloaterCount = null!;
|
||||
|
||||
protected ScrollViewer _chatScrollViewer = null!;
|
||||
protected ObservableCollection<string>? _chatLines;
|
||||
protected int _lastTermW;
|
||||
protected int _lastTermH;
|
||||
protected bool _chatScrollToBottom = true;
|
||||
|
||||
protected ContainerViewBase(ContainerViewModel vm)
|
||||
{
|
||||
_vm = vm;
|
||||
_currentHotbarSlot = vm.Handler.GetCurrentSlot();
|
||||
|
||||
_chatLines = TuiConsoleBackend.Instance?.GetView()?.GetRecentLogLines(50)
|
||||
?? new ObservableCollection<string>();
|
||||
}
|
||||
|
||||
protected void Initialize()
|
||||
{
|
||||
RebuildUi();
|
||||
}
|
||||
|
||||
protected abstract int GetTotalSlotRows();
|
||||
|
||||
protected abstract Control BuildContainerSpecificArea();
|
||||
|
||||
protected virtual void OnContainerDataChanged() { }
|
||||
|
||||
protected virtual void RebuildUi()
|
||||
{
|
||||
int termH;
|
||||
try
|
||||
{
|
||||
_termW = System.Console.WindowWidth;
|
||||
termH = System.Console.WindowHeight;
|
||||
}
|
||||
catch
|
||||
{
|
||||
_termW = 120;
|
||||
termH = 40;
|
||||
}
|
||||
|
||||
_lastTermW = _termW;
|
||||
_lastTermH = termH;
|
||||
|
||||
int availW = _termW - 26;
|
||||
_slotW = Math.Clamp(availW / 9, 8, 18);
|
||||
_nameMaxLen = _slotW;
|
||||
|
||||
int totalRows = GetTotalSlotRows();
|
||||
int overhead = 4;
|
||||
int chatMinH = 1;
|
||||
_slotH = Math.Clamp((termH - overhead - chatMinH) / totalRows, 2, 5);
|
||||
_nameLines = _slotH;
|
||||
|
||||
_vm.SetSlotDisplayParams(_nameMaxLen, _nameLines);
|
||||
|
||||
_lastHoveredSlotBorder = null;
|
||||
|
||||
_titleText = new TextBlock
|
||||
{
|
||||
FontWeight = FontWeight.Bold,
|
||||
Foreground = Brushes.Cyan,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
};
|
||||
|
||||
_infoDetailText = new TextBlock
|
||||
{
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Foreground = Brushes.White,
|
||||
};
|
||||
|
||||
_infoDetailBorder = new Border
|
||||
{
|
||||
Background = Brushes.Transparent,
|
||||
Padding = new Thickness(0),
|
||||
Child = _infoDetailText,
|
||||
};
|
||||
|
||||
_cursorItemText = new TextBlock
|
||||
{
|
||||
Foreground = Brushes.Yellow,
|
||||
FontWeight = FontWeight.Bold,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
};
|
||||
|
||||
_helpText = new TextBlock
|
||||
{
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Foreground = new SolidColorBrush(Color.FromRgb(140, 140, 140)),
|
||||
Text = Translations.tui_inventory_controls_help,
|
||||
};
|
||||
|
||||
_heldItemFloaterName = new TextBlock
|
||||
{
|
||||
Foreground = Brushes.White,
|
||||
FontWeight = FontWeight.Bold,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
};
|
||||
_heldItemFloaterCount = new TextBlock
|
||||
{
|
||||
Foreground = BrCount,
|
||||
FontWeight = FontWeight.Bold,
|
||||
};
|
||||
_heldItemFloater = new Border
|
||||
{
|
||||
Background = BrHeldItemBg,
|
||||
BorderBrush = BrHeldItemBorder,
|
||||
BorderThickness = new Thickness(1),
|
||||
Padding = new Thickness(1, 0),
|
||||
IsVisible = false,
|
||||
MaxWidth = 24,
|
||||
Child = new StackPanel
|
||||
{
|
||||
Children = { _heldItemFloaterName, _heldItemFloaterCount },
|
||||
},
|
||||
};
|
||||
|
||||
_overlayCanvas = new Canvas { IsHitTestVisible = false };
|
||||
_overlayCanvas.Children.Add(_heldItemFloater);
|
||||
|
||||
var chatLines = _chatLines!;
|
||||
chatLines.CollectionChanged += (_, _) =>
|
||||
{
|
||||
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
var sv = _chatScrollViewer;
|
||||
if (sv.Extent.Height > sv.Viewport.Height)
|
||||
sv.Offset = new Vector(0, sv.Extent.Height - sv.Viewport.Height);
|
||||
}, Avalonia.Threading.DispatcherPriority.Background);
|
||||
};
|
||||
var chatItemsControl = new ItemsControl
|
||||
{
|
||||
ItemsSource = chatLines,
|
||||
Focusable = false,
|
||||
ItemTemplate = new FuncDataTemplate<string>((s, _) =>
|
||||
new TextBlock
|
||||
{
|
||||
Text = s,
|
||||
Foreground = Brushes.Gray,
|
||||
Padding = new Thickness(0),
|
||||
Margin = new Thickness(0),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
}),
|
||||
};
|
||||
_chatScrollViewer = new ScrollViewer
|
||||
{
|
||||
Content = chatItemsControl,
|
||||
Background = Brushes.Black,
|
||||
HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled,
|
||||
VerticalScrollBarVisibility = ScrollBarVisibility.Hidden,
|
||||
Padding = new Thickness(0),
|
||||
};
|
||||
|
||||
_hotbarIndicators = new TextBlock[9];
|
||||
|
||||
Content = BuildRootLayout();
|
||||
UpdateTitle();
|
||||
UpdateInfoPanel();
|
||||
|
||||
_chatScrollToBottom = true;
|
||||
_chatScrollViewer.ScrollChanged += OnChatScrollChanged;
|
||||
}
|
||||
|
||||
private void OnChatScrollChanged(object? sender, ScrollChangedEventArgs e)
|
||||
{
|
||||
if (!_chatScrollToBottom) return;
|
||||
var sv = _chatScrollViewer;
|
||||
if (sv.Extent.Height > sv.Viewport.Height)
|
||||
{
|
||||
sv.Offset = new Vector(0, sv.Extent.Height - sv.Viewport.Height);
|
||||
_chatScrollToBottom = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual Control BuildRootLayout()
|
||||
{
|
||||
var inventoryArea = BuildMainArea();
|
||||
DockPanel.SetDock(_titleText, Dock.Top);
|
||||
DockPanel.SetDock(inventoryArea, Dock.Top);
|
||||
|
||||
var mainContent = new DockPanel
|
||||
{
|
||||
Children = { _titleText, inventoryArea, _chatScrollViewer }
|
||||
};
|
||||
|
||||
return new Panel
|
||||
{
|
||||
Background = Brushes.Black,
|
||||
Children = { mainContent, _overlayCanvas }
|
||||
};
|
||||
}
|
||||
|
||||
protected virtual Control BuildMainArea()
|
||||
{
|
||||
var infoPanel = BuildInfoPanel();
|
||||
DockPanel.SetDock(infoPanel, Dock.Right);
|
||||
|
||||
return new DockPanel
|
||||
{
|
||||
Children = { infoPanel, BuildInventoryPanel() }
|
||||
};
|
||||
}
|
||||
|
||||
protected virtual Control BuildInventoryPanel()
|
||||
{
|
||||
var root = new StackPanel
|
||||
{
|
||||
Spacing = 0,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
};
|
||||
|
||||
root.Children.Add(BuildContainerSpecificArea());
|
||||
root.Children.Add(BuildSeparator());
|
||||
root.Children.Add(BuildSlotGrid(_vm.MainInventorySlots, 9));
|
||||
root.Children.Add(BuildHotbarSection());
|
||||
|
||||
return new Border
|
||||
{
|
||||
BorderThickness = new Thickness(1),
|
||||
BorderBrush = Brushes.Gray,
|
||||
Child = root,
|
||||
};
|
||||
}
|
||||
|
||||
protected Control BuildSeparator()
|
||||
{
|
||||
return new Border
|
||||
{
|
||||
Height = 1,
|
||||
Background = Brushes.Transparent,
|
||||
Margin = new Thickness(0, 0, 0, 0),
|
||||
};
|
||||
}
|
||||
|
||||
protected Control BuildInfoPanel()
|
||||
{
|
||||
return new Border
|
||||
{
|
||||
BorderThickness = new Thickness(1),
|
||||
BorderBrush = Brushes.Gray,
|
||||
Padding = new Thickness(1),
|
||||
Width = 24,
|
||||
Child = new StackPanel
|
||||
{
|
||||
Children =
|
||||
{
|
||||
new TextBlock { Text = Translations.tui_inventory_item_info, FontWeight = FontWeight.Bold, Foreground = Brushes.Cyan },
|
||||
_infoDetailBorder,
|
||||
new TextBlock { Text = Translations.tui_inventory_held_item, FontWeight = FontWeight.Bold, Foreground = Brushes.Yellow, Margin = new Thickness(0, 1, 0, 0) },
|
||||
_cursorItemText,
|
||||
new TextBlock { Text = Translations.tui_inventory_controls, FontWeight = FontWeight.Bold, Foreground = Brushes.Green, Margin = new Thickness(0, 1, 0, 0) },
|
||||
_helpText,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected Control BuildHotbarSection()
|
||||
{
|
||||
var panel = new StackPanel { Spacing = 0 };
|
||||
|
||||
var numberRow = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
};
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
bool active = i == _currentHotbarSlot;
|
||||
string label = active ? $"{i + 1} \u25bc" : $" {i + 1} ";
|
||||
|
||||
var tb = new TextBlock
|
||||
{
|
||||
Text = label,
|
||||
Width = _slotW,
|
||||
TextAlignment = TextAlignment.Center,
|
||||
Foreground = active ? Brushes.LightGreen : Brushes.DarkCyan,
|
||||
FontWeight = FontWeight.Bold,
|
||||
};
|
||||
_hotbarIndicators[i] = tb;
|
||||
numberRow.Children.Add(tb);
|
||||
}
|
||||
panel.Children.Add(numberRow);
|
||||
panel.Children.Add(BuildSlotGrid(_vm.HotbarSlots, 9));
|
||||
return panel;
|
||||
}
|
||||
|
||||
protected Control BuildSlotGrid(ObservableCollection<SlotViewModel> slots, int columns)
|
||||
{
|
||||
var grid = new Grid();
|
||||
int rows = (slots.Count + columns - 1) / columns;
|
||||
|
||||
for (int r = 0; r < rows; r++)
|
||||
grid.RowDefinitions.Add(new RowDefinition(GridLength.Auto));
|
||||
for (int c = 0; c < columns; c++)
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Auto));
|
||||
|
||||
for (int i = 0; i < slots.Count; i++)
|
||||
{
|
||||
int row = i / columns;
|
||||
int col = i % columns;
|
||||
var cell = CreateSlotCell(slots[i], row, col);
|
||||
Grid.SetRow(cell, row);
|
||||
Grid.SetColumn(cell, col);
|
||||
grid.Children.Add(cell);
|
||||
}
|
||||
|
||||
return grid;
|
||||
}
|
||||
|
||||
protected static IBrush GetSlotBg(bool isEmpty, int row, int col)
|
||||
{
|
||||
bool isA = (row + col) % 2 == 0;
|
||||
return isEmpty
|
||||
? (isA ? BrSlotEmptyA : BrSlotEmptyB)
|
||||
: (isA ? BrSlotFillA : BrSlotFillB);
|
||||
}
|
||||
|
||||
protected Border CreateSlotCell(SlotViewModel slot, int row = 0, int col = 0)
|
||||
{
|
||||
var nameTb = new TextBlock
|
||||
{
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Padding = new Thickness(0),
|
||||
Margin = new Thickness(0),
|
||||
VerticalAlignment = VerticalAlignment.Top,
|
||||
};
|
||||
|
||||
var countTb = new TextBlock
|
||||
{
|
||||
Foreground = BrCount,
|
||||
FontWeight = FontWeight.Bold,
|
||||
Padding = new Thickness(0),
|
||||
Margin = new Thickness(0),
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
VerticalAlignment = VerticalAlignment.Bottom,
|
||||
};
|
||||
|
||||
ApplySlotVisual(slot, nameTb, countTb);
|
||||
|
||||
int r = row, c = col;
|
||||
var border = new Border
|
||||
{
|
||||
Width = _slotW,
|
||||
Height = _slotH,
|
||||
Background = GetSlotBg(slot.IsEmpty, r, c),
|
||||
Child = new Panel
|
||||
{
|
||||
Children = { nameTb, countTb },
|
||||
},
|
||||
Tag = (slot, r, c),
|
||||
};
|
||||
|
||||
border.PointerPressed += OnSlotPointerPressed;
|
||||
border.PointerEntered += OnSlotPointerEnter;
|
||||
border.PointerExited += OnSlotPointerExit;
|
||||
border.PointerMoved += OnSlotPointerMoved;
|
||||
|
||||
slot.PropertyChanged += (_, _) =>
|
||||
{
|
||||
ApplySlotVisual(slot, nameTb, countTb);
|
||||
border.Background = GetSlotBg(slot.IsEmpty, r, c);
|
||||
};
|
||||
|
||||
return border;
|
||||
}
|
||||
|
||||
protected static void ApplySlotVisual(SlotViewModel slot, TextBlock nameTb, TextBlock countTb)
|
||||
{
|
||||
if (slot.IsEmpty)
|
||||
{
|
||||
nameTb.Text = "";
|
||||
nameTb.Foreground = BrDim;
|
||||
countTb.Text = "";
|
||||
}
|
||||
else
|
||||
{
|
||||
nameTb.Text = slot.ItemDisplayText;
|
||||
nameTb.Foreground = BrName;
|
||||
countTb.Text = slot.CountDisplay;
|
||||
}
|
||||
}
|
||||
|
||||
protected TextBlock MakeLabel(string text)
|
||||
{
|
||||
return new TextBlock
|
||||
{
|
||||
Text = text,
|
||||
Foreground = BrEquipLbl,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(1, 0, 0, 0),
|
||||
FontWeight = FontWeight.Bold,
|
||||
};
|
||||
}
|
||||
|
||||
#region Pointer / Keyboard interaction
|
||||
|
||||
private void OnSlotPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
if (sender is not Border border || border.Tag is not (SlotViewModel slot, int, int))
|
||||
return;
|
||||
|
||||
SetHover(border, slot);
|
||||
|
||||
var point = e.GetCurrentPoint(border);
|
||||
bool isShift = (e.KeyModifiers & KeyModifiers.Shift) != 0;
|
||||
|
||||
WindowActionType action;
|
||||
if (point.Properties.IsRightButtonPressed)
|
||||
action = isShift ? WindowActionType.ShiftRightClick : WindowActionType.RightClick;
|
||||
else
|
||||
action = isShift ? WindowActionType.ShiftClick : WindowActionType.LeftClick;
|
||||
|
||||
_vm.PerformAction(slot.SlotId, action);
|
||||
UpdateInfoPanel();
|
||||
UpdateHeldItemFloater(e);
|
||||
OnContainerDataChanged();
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void OnSlotPointerEnter(object? sender, PointerEventArgs e)
|
||||
{
|
||||
if (sender is Border b && b.Tag is (SlotViewModel slot, int, int))
|
||||
{
|
||||
SetHover(b, slot);
|
||||
UpdateHeldItemFloater(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSlotPointerMoved(object? sender, PointerEventArgs e)
|
||||
{
|
||||
if (sender is Border b && b.Tag is (SlotViewModel slot, int, int))
|
||||
{
|
||||
SetHover(b, slot);
|
||||
UpdateHeldItemFloater(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSlotPointerExit(object? sender, PointerEventArgs e)
|
||||
{
|
||||
if (sender is Border b && b.Tag is (SlotViewModel slot, int row, int col))
|
||||
b.Background = GetSlotBg(slot.IsEmpty, row, col);
|
||||
}
|
||||
|
||||
protected void SetHover(Border border, SlotViewModel slot)
|
||||
{
|
||||
if (_lastHoveredSlotBorder != null && _lastHoveredSlotBorder != border)
|
||||
{
|
||||
if (_lastHoveredSlotBorder.Tag is (SlotViewModel oldSlot, int or, int oc))
|
||||
_lastHoveredSlotBorder.Background = GetSlotBg(oldSlot.IsEmpty, or, oc);
|
||||
}
|
||||
|
||||
_lastHoveredSlotBorder = border;
|
||||
border.Background = BrSlotHover;
|
||||
_vm.HoveredSlot = slot;
|
||||
UpdateInfoPanel();
|
||||
}
|
||||
|
||||
protected void UpdateHeldItemFloater(PointerEventArgs e)
|
||||
{
|
||||
if (!_vm.HasCursorItem)
|
||||
{
|
||||
_heldItemFloater.IsVisible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_heldItemFloaterName.Text = _vm.CursorItemInfo;
|
||||
_heldItemFloaterCount.Text = "";
|
||||
|
||||
try
|
||||
{
|
||||
var pos = e.GetPosition(_overlayCanvas);
|
||||
double left = pos.X + 2;
|
||||
double remainingW = _termW - left - 2;
|
||||
int maxW = Math.Max(8, (int)remainingW);
|
||||
_heldItemFloater.MaxWidth = maxW;
|
||||
Canvas.SetLeft(_heldItemFloater, left);
|
||||
Canvas.SetTop(_heldItemFloater, pos.Y);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_heldItemFloater.MaxWidth = 24;
|
||||
Canvas.SetLeft(_heldItemFloater, 0);
|
||||
Canvas.SetTop(_heldItemFloater, 0);
|
||||
}
|
||||
|
||||
_heldItemFloater.IsVisible = true;
|
||||
}
|
||||
|
||||
protected void UpdateInfoPanel()
|
||||
{
|
||||
_infoDetailText.Text = _vm.HoveredSlotDetailText;
|
||||
|
||||
bool hasHoveredItem = _vm.HoveredSlot != null && !_vm.HoveredSlot.IsEmpty;
|
||||
_infoDetailBorder.Background = hasHoveredItem ? BrInfoHighlight : Brushes.Transparent;
|
||||
|
||||
if (_vm.HasCursorItem)
|
||||
{
|
||||
_cursorItemText.Text = _vm.CursorItemInfo;
|
||||
_cursorItemText.Foreground = Brushes.Yellow;
|
||||
}
|
||||
else
|
||||
{
|
||||
_cursorItemText.Text = Translations.tui_inventory_cursor_empty;
|
||||
_cursorItemText.Foreground = BrDim;
|
||||
_heldItemFloater.IsVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected void UpdateTitle()
|
||||
{
|
||||
_titleText.Text = _vm.Title;
|
||||
}
|
||||
|
||||
protected void CloseInventory()
|
||||
{
|
||||
if (_vm.WindowId != 0)
|
||||
_vm.Handler.CloseInventory(_vm.WindowId);
|
||||
|
||||
if (ConsoleIO.Backend is TuiConsoleBackend tuiBackend)
|
||||
tuiBackend.GetView()?.HideOverlay();
|
||||
else
|
||||
(Application.Current?.ApplicationLifetime as IControlledApplicationLifetime)?.Shutdown();
|
||||
}
|
||||
|
||||
protected override void OnKeyDown(KeyEventArgs e)
|
||||
{
|
||||
base.OnKeyDown(e);
|
||||
|
||||
switch (e.Key)
|
||||
{
|
||||
case Key.Escape:
|
||||
case Key.E:
|
||||
CloseInventory();
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.C:
|
||||
if ((e.KeyModifiers & KeyModifiers.Shift) != 0 &&
|
||||
_vm.HoveredSlot != null && !_vm.HoveredSlot.IsEmpty)
|
||||
{
|
||||
_vm.PerformAction(_vm.HoveredSlot.SlotId, WindowActionType.ShiftClick);
|
||||
UpdateInfoPanel();
|
||||
}
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.Q:
|
||||
if (_vm.HoveredSlot != null && !_vm.HoveredSlot.IsEmpty)
|
||||
{
|
||||
var action = (e.KeyModifiers & KeyModifiers.Control) != 0
|
||||
? WindowActionType.DropItemStack
|
||||
: WindowActionType.DropItem;
|
||||
_vm.PerformAction(_vm.HoveredSlot.SlotId, action);
|
||||
UpdateInfoPanel();
|
||||
}
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.R:
|
||||
_vm.RefreshFromContainer();
|
||||
_currentHotbarSlot = _vm.Handler.GetCurrentSlot();
|
||||
UpdateHotbarIndicators();
|
||||
UpdateInfoPanel();
|
||||
OnContainerDataChanged();
|
||||
e.Handled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected void UpdateHotbarIndicators()
|
||||
{
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
bool active = i == _currentHotbarSlot;
|
||||
_hotbarIndicators[i].Text = active ? $"{i + 1} \u25bc" : $" {i + 1} ";
|
||||
_hotbarIndicators[i].Foreground = active ? Brushes.LightGreen : Brushes.DarkCyan;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Lifecycle
|
||||
|
||||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
base.OnAttachedToVisualTree(e);
|
||||
Focusable = true;
|
||||
Focus();
|
||||
AddHandler(KeyDownEvent, OnTunnelKeyDown, Avalonia.Interactivity.RoutingStrategies.Tunnel);
|
||||
SizeChanged += OnViewSizeChanged;
|
||||
}
|
||||
|
||||
private void OnTunnelKeyDown(object? sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Escape)
|
||||
{
|
||||
CloseInventory();
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnViewSizeChanged(object? sender, SizeChangedEventArgs e)
|
||||
{
|
||||
int newW, newH;
|
||||
try
|
||||
{
|
||||
newW = System.Console.WindowWidth;
|
||||
newH = System.Console.WindowHeight;
|
||||
}
|
||||
catch { return; }
|
||||
|
||||
if (newW == _lastTermW && newH == _lastTermH) return;
|
||||
|
||||
_vm.RefreshFromContainer();
|
||||
_currentHotbarSlot = _vm.Handler.GetCurrentSlot();
|
||||
RebuildUi();
|
||||
Focus();
|
||||
}
|
||||
|
||||
protected override void OnGotFocus(GotFocusEventArgs e)
|
||||
{
|
||||
base.OnGotFocus(e);
|
||||
Focusable = true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public static bool HasTuiSupport(ContainerType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
ContainerType.PlayerInventory => true,
|
||||
ContainerType.Generic_9x1 => true,
|
||||
ContainerType.Generic_9x2 => true,
|
||||
ContainerType.Generic_9x3 => true,
|
||||
ContainerType.Generic_9x4 => true,
|
||||
ContainerType.Generic_9x5 => true,
|
||||
ContainerType.Generic_9x6 => true,
|
||||
ContainerType.Generic_3x3 => true,
|
||||
ContainerType.Crafter => true,
|
||||
ContainerType.ShulkerBox => true,
|
||||
ContainerType.Crafting => true,
|
||||
ContainerType.Furnace => true,
|
||||
ContainerType.BlastFurnace => true,
|
||||
ContainerType.Smoker => true,
|
||||
ContainerType.Enchantment => true,
|
||||
ContainerType.BrewingStand => true,
|
||||
ContainerType.Hopper => true,
|
||||
ContainerType.Grindstone => true,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
public static ContainerViewBase CreateView(ContainerType type, McClient handler, int windowId)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
ContainerType.PlayerInventory => new PlayerInventoryView(handler, windowId),
|
||||
ContainerType.Generic_9x3 or ContainerType.ShulkerBox => new GridContainerView(handler, windowId, type, 3, 9),
|
||||
ContainerType.Generic_9x6 => new GridContainerView(handler, windowId, type, 6, 9),
|
||||
ContainerType.Generic_3x3 or ContainerType.Crafter
|
||||
=> new GridContainerView(handler, windowId, type, 3, 3),
|
||||
ContainerType.Generic_9x1 => new GridContainerView(handler, windowId, type, 1, 9),
|
||||
ContainerType.Generic_9x2 => new GridContainerView(handler, windowId, type, 2, 9),
|
||||
ContainerType.Generic_9x4 => new GridContainerView(handler, windowId, type, 4, 9),
|
||||
ContainerType.Generic_9x5 => new GridContainerView(handler, windowId, type, 5, 9),
|
||||
ContainerType.Crafting => new CraftingView(handler, windowId),
|
||||
ContainerType.Furnace or ContainerType.BlastFurnace or ContainerType.Smoker
|
||||
=> new FurnaceView(handler, windowId, type),
|
||||
ContainerType.Enchantment => new EnchantingTableView(handler, windowId),
|
||||
ContainerType.BrewingStand => new BrewingStandView(handler, windowId),
|
||||
ContainerType.Hopper => new HopperView(handler, windowId),
|
||||
ContainerType.Grindstone => new GrindstoneView(handler, windowId),
|
||||
_ => throw new ArgumentException($"No TUI view for {type}"),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
271
MinecraftClient/Tui/ContainerViewModel.cs
Normal file
271
MinecraftClient/Tui/ContainerViewModel.cs
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using MinecraftClient.Inventory;
|
||||
using MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_20_6;
|
||||
|
||||
namespace MinecraftClient.Tui
|
||||
{
|
||||
public class ContainerViewModel : INotifyPropertyChanged
|
||||
{
|
||||
private SlotViewModel? _hoveredSlot;
|
||||
private string _title = "";
|
||||
private string _statusText = "";
|
||||
private string _cursorItemInfo = "";
|
||||
private bool _hasCursorItem;
|
||||
|
||||
public McClient Handler { get; }
|
||||
public int WindowId { get; }
|
||||
public ContainerType ContainerType { get; }
|
||||
|
||||
public ObservableCollection<SlotViewModel> ContainerSlots { get; } = new();
|
||||
public ObservableCollection<SlotViewModel> MainInventorySlots { get; } = new();
|
||||
public ObservableCollection<SlotViewModel> HotbarSlots { get; } = new();
|
||||
|
||||
public string Title
|
||||
{
|
||||
get => _title;
|
||||
set { _title = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
public string StatusText
|
||||
{
|
||||
get => _statusText;
|
||||
set { _statusText = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
public string CursorItemInfo
|
||||
{
|
||||
get => _cursorItemInfo;
|
||||
set { _cursorItemInfo = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
public bool HasCursorItem
|
||||
{
|
||||
get => _hasCursorItem;
|
||||
set { _hasCursorItem = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
public SlotViewModel? HoveredSlot
|
||||
{
|
||||
get => _hoveredSlot;
|
||||
set
|
||||
{
|
||||
if (_hoveredSlot != null)
|
||||
_hoveredSlot.IsHovered = false;
|
||||
_hoveredSlot = value;
|
||||
if (_hoveredSlot != null)
|
||||
_hoveredSlot.IsHovered = true;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(HoveredSlotDetailText));
|
||||
}
|
||||
}
|
||||
|
||||
public string HoveredSlotDetailText
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_hoveredSlot == null)
|
||||
return Translations.tui_inventory_hover_hint;
|
||||
|
||||
if (_hoveredSlot.IsEmpty)
|
||||
return $"Slot #{_hoveredSlot.SlotId}\n{Translations.tui_inventory_slot_empty}";
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine(_hoveredSlot.ItemTypeName);
|
||||
sb.AppendLine(string.Format(Translations.tui_inventory_slot_detail, _hoveredSlot.SlotId, _hoveredSlot.ItemCount));
|
||||
|
||||
var item = _hoveredSlot.RawItem;
|
||||
if (item != null)
|
||||
AppendItemExtras(sb, item);
|
||||
|
||||
return sb.ToString().TrimEnd();
|
||||
}
|
||||
}
|
||||
|
||||
protected Dictionary<int, SlotViewModel> SlotMap { get; } = new();
|
||||
|
||||
public ContainerViewModel(McClient handler, int windowId, ContainerType containerType)
|
||||
{
|
||||
Handler = handler;
|
||||
WindowId = windowId;
|
||||
ContainerType = containerType;
|
||||
|
||||
InitializeSlots();
|
||||
RefreshFromContainer();
|
||||
}
|
||||
|
||||
public void SetSlotDisplayParams(int maxWidth, int maxLines)
|
||||
{
|
||||
foreach (var kvp in SlotMap)
|
||||
{
|
||||
kvp.Value.NameMaxWidth = maxWidth;
|
||||
kvp.Value.NameMaxLines = maxLines;
|
||||
}
|
||||
RefreshFromContainer();
|
||||
}
|
||||
|
||||
protected virtual void InitializeSlots()
|
||||
{
|
||||
SlotMap.Clear();
|
||||
|
||||
int slotCount = ContainerType.SlotCount();
|
||||
if (slotCount == 0) return;
|
||||
|
||||
int playerInvStart = slotCount - 36;
|
||||
|
||||
for (int i = 0; i < playerInvStart; i++)
|
||||
{
|
||||
var slot = new SlotViewModel(i);
|
||||
ContainerSlots.Add(slot);
|
||||
SlotMap[i] = slot;
|
||||
}
|
||||
|
||||
for (int i = playerInvStart; i < playerInvStart + 27; i++)
|
||||
{
|
||||
var slot = new SlotViewModel(i);
|
||||
MainInventorySlots.Add(slot);
|
||||
SlotMap[i] = slot;
|
||||
}
|
||||
|
||||
for (int i = playerInvStart + 27; i < slotCount; i++)
|
||||
{
|
||||
int hotbarIdx = i - (playerInvStart + 27);
|
||||
var slot = new SlotViewModel(i, isHotbar: true, hotbarIndex: hotbarIdx);
|
||||
HotbarSlots.Add(slot);
|
||||
SlotMap[i] = slot;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void RefreshFromContainer()
|
||||
{
|
||||
Inventory.Container? container = Handler.GetInventory(WindowId);
|
||||
if (container == null)
|
||||
{
|
||||
StatusText = Translations.tui_inventory_container_not_found;
|
||||
return;
|
||||
}
|
||||
|
||||
Title = string.Format(Translations.tui_inventory_title, WindowId, container.Title);
|
||||
|
||||
foreach (var kvp in SlotMap)
|
||||
{
|
||||
Item? item = container.Items.TryGetValue(kvp.Key, out var it) ? it : null;
|
||||
kvp.Value.Update(item);
|
||||
}
|
||||
|
||||
UpdateCursorItem(container);
|
||||
int itemCount = 0;
|
||||
foreach (var kvp in container.Items)
|
||||
{
|
||||
if (kvp.Key >= 0 && !kvp.Value.IsEmpty)
|
||||
itemCount++;
|
||||
}
|
||||
StatusText = string.Format(Translations.tui_inventory_item_count, itemCount);
|
||||
|
||||
OnPropertyChanged(nameof(HoveredSlotDetailText));
|
||||
}
|
||||
|
||||
protected void UpdateCursorItem(Inventory.Container _)
|
||||
{
|
||||
var playerInv = Handler.GetInventory(0);
|
||||
if (playerInv != null && playerInv.Items.TryGetValue(-1, out var cursorItem) && !cursorItem.IsEmpty)
|
||||
{
|
||||
CursorItemInfo = FormatItemDetail(cursorItem);
|
||||
HasCursorItem = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
CursorItemInfo = "";
|
||||
HasCursorItem = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected static string FormatItemDetail(Item item)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"x{item.Count} {item.GetTypeString()}");
|
||||
AppendItemExtras(sb, item);
|
||||
if (sb.Length > 0 && sb[sb.Length - 1] == '\n')
|
||||
sb.Remove(sb.Length - 1, 1);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static void AppendItemExtras(StringBuilder sb, Item item)
|
||||
{
|
||||
int damage = item.Damage;
|
||||
if (damage != 0)
|
||||
{
|
||||
int maxDamage = item.Components?.OfType<MaxDamageComponent>().FirstOrDefault()?.MaxDamage ?? 0;
|
||||
if (maxDamage > 0)
|
||||
sb.AppendLine($"{Translations.tui_inventory_durability}: {maxDamage - damage}/{maxDamage}");
|
||||
else
|
||||
sb.AppendLine($"{Translations.cmd_inventory_damage}: {damage}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var enchList = item.EnchantmentList;
|
||||
if (enchList is not null)
|
||||
{
|
||||
bool isFirstEnchantment = true;
|
||||
foreach (var ench in enchList)
|
||||
{
|
||||
string name = EnchantmentMapping.GetEnchantmentName(ench.Type);
|
||||
string level = EnchantmentMapping.ConvertLevelToRomanNumbers(ench.Level);
|
||||
if (isFirstEnchantment)
|
||||
{
|
||||
isFirstEnchantment = false;
|
||||
sb.Append($"{name} {level}");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append($" | {name} {level}");
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (item.NBT is not null &&
|
||||
(item.NBT.TryGetValue("Enchantments", out object? enchantments) ||
|
||||
item.NBT.TryGetValue("StoredEnchantments", out enchantments)))
|
||||
{
|
||||
bool isFirstEnchantment = true;
|
||||
foreach (Dictionary<string, object> enchantment in (object[])enchantments)
|
||||
{
|
||||
short level = (short)enchantment["lvl"];
|
||||
string id = ((string)enchantment["id"]).Replace(':', '.');
|
||||
string name = Protocol.Message.ChatParser.TranslateString("enchantment." + id) ?? id;
|
||||
string levelStr = Protocol.Message.ChatParser.TranslateString("enchantment.level." + level) ?? level.ToString();
|
||||
if (isFirstEnchantment)
|
||||
{
|
||||
isFirstEnchantment = false;
|
||||
sb.Append($"{name} {levelStr}");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append($" | {name} {levelStr}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
public bool PerformAction(int slotId, WindowActionType action)
|
||||
{
|
||||
bool result = Handler.DoWindowAction(WindowId, slotId, action);
|
||||
RefreshFromContainer();
|
||||
return result;
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
protected void OnPropertyChanged([CallerMemberName] string? name = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
}
|
||||
}
|
||||
}
|
||||
112
MinecraftClient/Tui/CraftingView.cs
Normal file
112
MinecraftClient/Tui/CraftingView.cs
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
using System.Collections.ObjectModel;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using MinecraftClient.Inventory;
|
||||
|
||||
namespace MinecraftClient.Tui
|
||||
{
|
||||
public class CraftingView : ContainerViewBase
|
||||
{
|
||||
private readonly CraftingViewModel _craftVm;
|
||||
|
||||
public CraftingView(McClient handler, int windowId)
|
||||
: base(new CraftingViewModel(handler, windowId))
|
||||
{
|
||||
_craftVm = (CraftingViewModel)_vm;
|
||||
Initialize();
|
||||
}
|
||||
|
||||
protected override int GetTotalSlotRows()
|
||||
{
|
||||
return 3 + 3 + 1;
|
||||
}
|
||||
|
||||
protected override Control BuildContainerSpecificArea()
|
||||
{
|
||||
var row = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
};
|
||||
|
||||
var gridPanel = new StackPanel { Spacing = 0 };
|
||||
gridPanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = Translations.tui_crafting_grid,
|
||||
Foreground = BrEquipLbl,
|
||||
FontWeight = FontWeight.Bold,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
});
|
||||
gridPanel.Children.Add(BuildSlotGrid(_craftVm.CraftingGridSlots, 3));
|
||||
row.Children.Add(gridPanel);
|
||||
|
||||
row.Children.Add(new TextBlock
|
||||
{
|
||||
Text = " \u2192 ",
|
||||
Foreground = Brushes.White,
|
||||
FontWeight = FontWeight.Bold,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
|
||||
var outPanel = new StackPanel
|
||||
{
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
outPanel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = Translations.tui_inventory_output,
|
||||
Foreground = BrEquipLbl,
|
||||
FontWeight = FontWeight.Bold,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
});
|
||||
outPanel.Children.Add(CreateSlotCell(_craftVm.OutputSlot, 0, 0));
|
||||
row.Children.Add(outPanel);
|
||||
|
||||
return row;
|
||||
}
|
||||
}
|
||||
|
||||
public class CraftingViewModel : ContainerViewModel
|
||||
{
|
||||
public ObservableCollection<SlotViewModel> CraftingGridSlots { get; } = new();
|
||||
public SlotViewModel OutputSlot { get; private set; } = null!;
|
||||
|
||||
public CraftingViewModel(McClient handler, int windowId)
|
||||
: base(handler, windowId, ContainerType.Crafting)
|
||||
{
|
||||
OutputSlot = SlotMap[0];
|
||||
}
|
||||
|
||||
protected override void InitializeSlots()
|
||||
{
|
||||
SlotMap.Clear();
|
||||
|
||||
var output = new SlotViewModel(0);
|
||||
SlotMap[0] = output;
|
||||
|
||||
for (int i = 1; i <= 9; i++)
|
||||
{
|
||||
var slot = new SlotViewModel(i);
|
||||
CraftingGridSlots.Add(slot);
|
||||
SlotMap[i] = slot;
|
||||
}
|
||||
|
||||
for (int i = 10; i <= 36; i++)
|
||||
{
|
||||
var slot = new SlotViewModel(i);
|
||||
MainInventorySlots.Add(slot);
|
||||
SlotMap[i] = slot;
|
||||
}
|
||||
|
||||
for (int i = 37; i <= 45; i++)
|
||||
{
|
||||
int hotbarIdx = i - 37;
|
||||
var slot = new SlotViewModel(i, isHotbar: true, hotbarIndex: hotbarIdx);
|
||||
HotbarSlots.Add(slot);
|
||||
SlotMap[i] = slot;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
198
MinecraftClient/Tui/EnchantingTableView.cs
Normal file
198
MinecraftClient/Tui/EnchantingTableView.cs
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
using System.Collections.ObjectModel;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using MinecraftClient.Inventory;
|
||||
|
||||
namespace MinecraftClient.Tui
|
||||
{
|
||||
public class EnchantingTableView : ContainerViewBase
|
||||
{
|
||||
private readonly EnchantingViewModel _enchantVm;
|
||||
private readonly TextBlock[] _enchantNameLabels = new TextBlock[3];
|
||||
private readonly TextBlock[] _enchantCostLabels = new TextBlock[3];
|
||||
|
||||
public EnchantingTableView(McClient handler, int windowId)
|
||||
: base(new EnchantingViewModel(handler, windowId))
|
||||
{
|
||||
_enchantVm = (EnchantingViewModel)_vm;
|
||||
Initialize();
|
||||
}
|
||||
|
||||
private void RefreshEnchantOptions()
|
||||
{
|
||||
var container = _vm.Handler.GetInventory(_vm.WindowId);
|
||||
if (container == null) return;
|
||||
|
||||
int protocolVersion = _vm.Handler.GetProtocolVersion();
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
if (_enchantNameLabels[i] == null) continue;
|
||||
|
||||
short levelReq = container.Properties.TryGetValue(i, out var lr) ? lr : (short)0;
|
||||
short enchantId = container.Properties.TryGetValue(i + 4, out var eid) ? eid : (short)-1;
|
||||
short enchantLevel = container.Properties.TryGetValue(i + 7, out var el) ? el : (short)0;
|
||||
|
||||
if (levelReq > 0 && enchantId >= 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
var enchant = EnchantmentMapping.GetEnchantmentById(protocolVersion, enchantId);
|
||||
string name = EnchantmentMapping.GetEnchantmentName(enchant);
|
||||
string roman = EnchantmentMapping.ConvertLevelToRomanNumbers(enchantLevel);
|
||||
_enchantNameLabels[i].Text = $"{name} {roman}";
|
||||
_enchantCostLabels[i].Text = $" ({levelReq})";
|
||||
}
|
||||
catch
|
||||
{
|
||||
_enchantNameLabels[i].Text = string.Format(Translations.tui_enchanting_option_slot, i + 1);
|
||||
_enchantCostLabels[i].Text = levelReq > 0 ? $" ({levelReq})" : "";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_enchantNameLabels[i].Text = string.Format(Translations.tui_enchanting_option_slot, i + 1);
|
||||
_enchantCostLabels[i].Text = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnContainerDataChanged()
|
||||
{
|
||||
RefreshEnchantOptions();
|
||||
}
|
||||
|
||||
protected override int GetTotalSlotRows()
|
||||
{
|
||||
return 3 + 3 + 1;
|
||||
}
|
||||
|
||||
protected override Control BuildContainerSpecificArea()
|
||||
{
|
||||
var panel = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
Spacing = 0,
|
||||
};
|
||||
|
||||
var slotsCol = new StackPanel
|
||||
{
|
||||
Spacing = 0,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
|
||||
slotsCol.Children.Add(new TextBlock
|
||||
{
|
||||
Text = Translations.tui_enchanting_item,
|
||||
Foreground = BrEquipLbl,
|
||||
FontWeight = FontWeight.Bold,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
});
|
||||
slotsCol.Children.Add(CreateSlotCell(_enchantVm.ItemSlot, 0, 0));
|
||||
|
||||
slotsCol.Children.Add(new TextBlock
|
||||
{
|
||||
Text = Translations.tui_enchanting_lapis,
|
||||
Foreground = new SolidColorBrush(Color.FromRgb(60, 80, 200)),
|
||||
FontWeight = FontWeight.Bold,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
});
|
||||
slotsCol.Children.Add(CreateSlotCell(_enchantVm.LapisSlot, 1, 0));
|
||||
|
||||
panel.Children.Add(slotsCol);
|
||||
|
||||
var optionsCol = new StackPanel
|
||||
{
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(2, 0, 0, 0),
|
||||
};
|
||||
|
||||
optionsCol.Children.Add(new TextBlock
|
||||
{
|
||||
Text = Translations.tui_enchanting_options,
|
||||
Foreground = Brushes.Magenta,
|
||||
FontWeight = FontWeight.Bold,
|
||||
});
|
||||
|
||||
int optionWidth = System.Math.Max(_slotW * 4, 30);
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
var nameLabel = new TextBlock
|
||||
{
|
||||
Text = string.Format(Translations.tui_enchanting_option_slot, i + 1),
|
||||
Foreground = new SolidColorBrush(Color.FromRgb(100, 200, 70)),
|
||||
TextWrapping = TextWrapping.NoWrap,
|
||||
};
|
||||
_enchantNameLabels[i] = nameLabel;
|
||||
|
||||
var costLabel = new TextBlock
|
||||
{
|
||||
Foreground = new SolidColorBrush(Color.FromRgb(100, 200, 70)),
|
||||
FontWeight = FontWeight.Bold,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
_enchantCostLabels[i] = costLabel;
|
||||
|
||||
var content = new DockPanel();
|
||||
DockPanel.SetDock(costLabel, Dock.Right);
|
||||
content.Children.Add(costLabel);
|
||||
content.Children.Add(nameLabel);
|
||||
|
||||
optionsCol.Children.Add(new Border
|
||||
{
|
||||
Background = new SolidColorBrush(Color.FromRgb(55, 50, 40)),
|
||||
MinWidth = optionWidth,
|
||||
MinHeight = _slotH,
|
||||
Padding = new Thickness(1, 0),
|
||||
Child = content,
|
||||
});
|
||||
}
|
||||
|
||||
RefreshEnchantOptions();
|
||||
|
||||
panel.Children.Add(optionsCol);
|
||||
|
||||
return panel;
|
||||
}
|
||||
}
|
||||
|
||||
public class EnchantingViewModel : ContainerViewModel
|
||||
{
|
||||
public SlotViewModel ItemSlot { get; private set; } = null!;
|
||||
public SlotViewModel LapisSlot { get; private set; } = null!;
|
||||
|
||||
public EnchantingViewModel(McClient handler, int windowId)
|
||||
: base(handler, windowId, ContainerType.Enchantment)
|
||||
{
|
||||
ItemSlot = SlotMap[0];
|
||||
LapisSlot = SlotMap[1];
|
||||
}
|
||||
|
||||
protected override void InitializeSlots()
|
||||
{
|
||||
SlotMap.Clear();
|
||||
|
||||
SlotMap[0] = new SlotViewModel(0);
|
||||
SlotMap[1] = new SlotViewModel(1);
|
||||
|
||||
for (int i = 2; i <= 28; i++)
|
||||
{
|
||||
var slot = new SlotViewModel(i);
|
||||
MainInventorySlots.Add(slot);
|
||||
SlotMap[i] = slot;
|
||||
}
|
||||
|
||||
for (int i = 29; i <= 37; i++)
|
||||
{
|
||||
int hotbarIdx = i - 29;
|
||||
var slot = new SlotViewModel(i, isHotbar: true, hotbarIndex: hotbarIdx);
|
||||
HotbarSlots.Add(slot);
|
||||
SlotMap[i] = slot;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
133
MinecraftClient/Tui/FurnaceView.cs
Normal file
133
MinecraftClient/Tui/FurnaceView.cs
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
using System.Collections.ObjectModel;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using MinecraftClient.Inventory;
|
||||
|
||||
namespace MinecraftClient.Tui
|
||||
{
|
||||
public class FurnaceView : ContainerViewBase
|
||||
{
|
||||
private readonly FurnaceViewModel _furnaceVm;
|
||||
|
||||
public FurnaceView(McClient handler, int windowId, ContainerType type)
|
||||
: base(new FurnaceViewModel(handler, windowId, type))
|
||||
{
|
||||
_furnaceVm = (FurnaceViewModel)_vm;
|
||||
Initialize();
|
||||
}
|
||||
|
||||
protected override int GetTotalSlotRows()
|
||||
{
|
||||
return 3 + 3 + 1;
|
||||
}
|
||||
|
||||
protected override Control BuildContainerSpecificArea()
|
||||
{
|
||||
var panel = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
Spacing = 0,
|
||||
};
|
||||
|
||||
var leftCol = new StackPanel
|
||||
{
|
||||
Spacing = 0,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
|
||||
leftCol.Children.Add(new TextBlock
|
||||
{
|
||||
Text = Translations.tui_furnace_input,
|
||||
Foreground = BrEquipLbl,
|
||||
FontWeight = FontWeight.Bold,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
});
|
||||
leftCol.Children.Add(CreateSlotCell(_furnaceVm.InputSlot, 0, 0));
|
||||
|
||||
leftCol.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "\u2592\u2592\u2592",
|
||||
Foreground = new SolidColorBrush(Color.FromRgb(180, 100, 40)),
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
});
|
||||
|
||||
leftCol.Children.Add(new TextBlock
|
||||
{
|
||||
Text = Translations.tui_furnace_fuel,
|
||||
Foreground = BrEquipLbl,
|
||||
FontWeight = FontWeight.Bold,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
});
|
||||
leftCol.Children.Add(CreateSlotCell(_furnaceVm.FuelSlot, 1, 0));
|
||||
|
||||
panel.Children.Add(leftCol);
|
||||
|
||||
panel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = " \u2192 ",
|
||||
Foreground = Brushes.White,
|
||||
FontWeight = FontWeight.Bold,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
|
||||
var rightCol = new StackPanel
|
||||
{
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
rightCol.Children.Add(new TextBlock
|
||||
{
|
||||
Text = Translations.tui_furnace_output,
|
||||
Foreground = BrEquipLbl,
|
||||
FontWeight = FontWeight.Bold,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
});
|
||||
rightCol.Children.Add(CreateSlotCell(_furnaceVm.OutputSlot, 0, 1));
|
||||
|
||||
panel.Children.Add(rightCol);
|
||||
|
||||
return panel;
|
||||
}
|
||||
}
|
||||
|
||||
public class FurnaceViewModel : ContainerViewModel
|
||||
{
|
||||
public SlotViewModel InputSlot { get; private set; } = null!;
|
||||
public SlotViewModel FuelSlot { get; private set; } = null!;
|
||||
public SlotViewModel OutputSlot { get; private set; } = null!;
|
||||
|
||||
public FurnaceViewModel(McClient handler, int windowId, ContainerType type)
|
||||
: base(handler, windowId, type)
|
||||
{
|
||||
InputSlot = SlotMap[0];
|
||||
FuelSlot = SlotMap[1];
|
||||
OutputSlot = SlotMap[2];
|
||||
}
|
||||
|
||||
protected override void InitializeSlots()
|
||||
{
|
||||
SlotMap.Clear();
|
||||
|
||||
SlotMap[0] = new SlotViewModel(0);
|
||||
SlotMap[1] = new SlotViewModel(1);
|
||||
SlotMap[2] = new SlotViewModel(2);
|
||||
|
||||
for (int i = 3; i <= 29; i++)
|
||||
{
|
||||
var slot = new SlotViewModel(i);
|
||||
MainInventorySlots.Add(slot);
|
||||
SlotMap[i] = slot;
|
||||
}
|
||||
|
||||
for (int i = 30; i <= 38; i++)
|
||||
{
|
||||
int hotbarIdx = i - 30;
|
||||
var slot = new SlotViewModel(i, isHotbar: true, hotbarIndex: hotbarIdx);
|
||||
HotbarSlots.Add(slot);
|
||||
SlotMap[i] = slot;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
31
MinecraftClient/Tui/GridContainerView.cs
Normal file
31
MinecraftClient/Tui/GridContainerView.cs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using MinecraftClient.Inventory;
|
||||
|
||||
namespace MinecraftClient.Tui
|
||||
{
|
||||
public class GridContainerView : ContainerViewBase
|
||||
{
|
||||
private readonly int _gridRows;
|
||||
private readonly int _gridCols;
|
||||
|
||||
public GridContainerView(McClient handler, int windowId, ContainerType type, int rows, int cols)
|
||||
: base(new ContainerViewModel(handler, windowId, type))
|
||||
{
|
||||
_gridRows = rows;
|
||||
_gridCols = cols;
|
||||
Initialize();
|
||||
}
|
||||
|
||||
protected override int GetTotalSlotRows()
|
||||
{
|
||||
return _gridRows + 3 + 1;
|
||||
}
|
||||
|
||||
protected override Control BuildContainerSpecificArea()
|
||||
{
|
||||
return BuildSlotGrid(_vm.ContainerSlots, _gridCols);
|
||||
}
|
||||
}
|
||||
}
|
||||
126
MinecraftClient/Tui/GrindstoneView.cs
Normal file
126
MinecraftClient/Tui/GrindstoneView.cs
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
using System.Collections.ObjectModel;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using MinecraftClient.Inventory;
|
||||
|
||||
namespace MinecraftClient.Tui
|
||||
{
|
||||
public class GrindstoneView : ContainerViewBase
|
||||
{
|
||||
private readonly GrindstoneViewModel _grindVm;
|
||||
|
||||
public GrindstoneView(McClient handler, int windowId)
|
||||
: base(new GrindstoneViewModel(handler, windowId))
|
||||
{
|
||||
_grindVm = (GrindstoneViewModel)_vm;
|
||||
Initialize();
|
||||
}
|
||||
|
||||
protected override int GetTotalSlotRows()
|
||||
{
|
||||
return 2 + 3 + 1;
|
||||
}
|
||||
|
||||
protected override Control BuildContainerSpecificArea()
|
||||
{
|
||||
var row = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
Spacing = 0,
|
||||
};
|
||||
|
||||
var inputCol = new StackPanel
|
||||
{
|
||||
Spacing = 0,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
|
||||
inputCol.Children.Add(new TextBlock
|
||||
{
|
||||
Text = Translations.tui_grindstone_input1,
|
||||
Foreground = BrEquipLbl,
|
||||
FontWeight = FontWeight.Bold,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
});
|
||||
inputCol.Children.Add(CreateSlotCell(_grindVm.Input1Slot, 0, 0));
|
||||
|
||||
inputCol.Children.Add(new TextBlock
|
||||
{
|
||||
Text = Translations.tui_grindstone_input2,
|
||||
Foreground = BrEquipLbl,
|
||||
FontWeight = FontWeight.Bold,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
});
|
||||
inputCol.Children.Add(CreateSlotCell(_grindVm.Input2Slot, 1, 0));
|
||||
|
||||
row.Children.Add(inputCol);
|
||||
|
||||
row.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "=>",
|
||||
Foreground = Brushes.White,
|
||||
FontWeight = FontWeight.Bold,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Padding = new Thickness(1, 0),
|
||||
});
|
||||
|
||||
var outCol = new StackPanel
|
||||
{
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
outCol.Children.Add(new TextBlock
|
||||
{
|
||||
Text = Translations.tui_inventory_output,
|
||||
Foreground = BrEquipLbl,
|
||||
FontWeight = FontWeight.Bold,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
});
|
||||
outCol.Children.Add(CreateSlotCell(_grindVm.OutputSlot, 0, 1));
|
||||
row.Children.Add(outCol);
|
||||
|
||||
return row;
|
||||
}
|
||||
}
|
||||
|
||||
public class GrindstoneViewModel : ContainerViewModel
|
||||
{
|
||||
public SlotViewModel Input1Slot { get; private set; } = null!;
|
||||
public SlotViewModel Input2Slot { get; private set; } = null!;
|
||||
public SlotViewModel OutputSlot { get; private set; } = null!;
|
||||
|
||||
public GrindstoneViewModel(McClient handler, int windowId)
|
||||
: base(handler, windowId, ContainerType.Grindstone)
|
||||
{
|
||||
Input1Slot = SlotMap[0];
|
||||
Input2Slot = SlotMap[1];
|
||||
OutputSlot = SlotMap[2];
|
||||
}
|
||||
|
||||
protected override void InitializeSlots()
|
||||
{
|
||||
SlotMap.Clear();
|
||||
|
||||
SlotMap[0] = new SlotViewModel(0);
|
||||
SlotMap[1] = new SlotViewModel(1);
|
||||
SlotMap[2] = new SlotViewModel(2);
|
||||
|
||||
for (int i = 3; i <= 29; i++)
|
||||
{
|
||||
var slot = new SlotViewModel(i);
|
||||
MainInventorySlots.Add(slot);
|
||||
SlotMap[i] = slot;
|
||||
}
|
||||
|
||||
for (int i = 30; i <= 38; i++)
|
||||
{
|
||||
int hotbarIdx = i - 30;
|
||||
var slot = new SlotViewModel(i, isHotbar: true, hotbarIndex: hotbarIdx);
|
||||
HotbarSlots.Add(slot);
|
||||
SlotMap[i] = slot;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
30
MinecraftClient/Tui/HopperView.cs
Normal file
30
MinecraftClient/Tui/HopperView.cs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.Layout;
|
||||
using MinecraftClient.Inventory;
|
||||
|
||||
namespace MinecraftClient.Tui
|
||||
{
|
||||
public class HopperView : ContainerViewBase
|
||||
{
|
||||
public HopperView(McClient handler, int windowId)
|
||||
: base(new ContainerViewModel(handler, windowId, ContainerType.Hopper))
|
||||
{
|
||||
Initialize();
|
||||
}
|
||||
|
||||
protected override int GetTotalSlotRows()
|
||||
{
|
||||
return 1 + 3 + 1;
|
||||
}
|
||||
|
||||
protected override Control BuildContainerSpecificArea()
|
||||
{
|
||||
var grid = BuildSlotGrid(_vm.ContainerSlots, 5);
|
||||
return new StackPanel
|
||||
{
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
Children = { grid },
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ using Avalonia;
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Consolonia.Themes;
|
||||
using MinecraftClient.Inventory;
|
||||
|
||||
namespace MinecraftClient.Tui
|
||||
{
|
||||
|
|
@ -16,9 +17,15 @@ namespace MinecraftClient.Tui
|
|||
{
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
var handler = InventoryTuiHost.ActiveHandler!;
|
||||
var windowId = InventoryTuiHost.ActiveWindowId;
|
||||
var container = handler.GetInventory(windowId);
|
||||
var containerType = container?.Type ?? ContainerType.PlayerInventory;
|
||||
var view = ContainerViewBase.CreateView(containerType, handler, windowId);
|
||||
|
||||
desktop.MainWindow = new Window
|
||||
{
|
||||
Content = new InventoryMainView(),
|
||||
Content = view,
|
||||
Title = "MCC Inventory"
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,304 +1,39 @@
|
|||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Controls.Templates;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using MinecraftClient.Inventory;
|
||||
|
||||
namespace MinecraftClient.Tui
|
||||
{
|
||||
public class InventoryMainView : UserControl
|
||||
public class PlayerInventoryView : ContainerViewBase
|
||||
{
|
||||
private static readonly IBrush BrSlotEmptyA = new SolidColorBrush(Color.FromRgb(40, 40, 40));
|
||||
private static readonly IBrush BrSlotEmptyB = new SolidColorBrush(Color.FromRgb(55, 55, 55));
|
||||
private static readonly IBrush BrSlotFillA = new SolidColorBrush(Color.FromRgb(60, 60, 75));
|
||||
private static readonly IBrush BrSlotFillB = new SolidColorBrush(Color.FromRgb(75, 75, 90));
|
||||
private static readonly IBrush BrSlotHover = new SolidColorBrush(Color.FromRgb(100, 100, 140));
|
||||
private static readonly IBrush BrName = Brushes.White;
|
||||
private static readonly IBrush BrCount = Brushes.Yellow;
|
||||
private static readonly IBrush BrDim = new SolidColorBrush(Color.FromRgb(80, 80, 80));
|
||||
private static readonly IBrush BrEquipLbl = Brushes.DarkCyan;
|
||||
private static readonly IBrush BrInfoHighlight = new SolidColorBrush(Color.FromRgb(40, 40, 60));
|
||||
private static readonly IBrush BrHeldItemBg = new SolidColorBrush(Color.FromRgb(60, 50, 80));
|
||||
private static readonly IBrush BrHeldItemBorder = Brushes.Yellow;
|
||||
|
||||
private int _slotW;
|
||||
private int _slotH;
|
||||
private int _nameMaxLen;
|
||||
private int _nameLines;
|
||||
private readonly PlayerInventoryViewModel _playerVm;
|
||||
private int _topGap;
|
||||
private int _termW;
|
||||
|
||||
private readonly InventoryViewModel _vm;
|
||||
private TextBlock _titleText = null!;
|
||||
private Border _infoDetailBorder = null!;
|
||||
private TextBlock _infoDetailText = null!;
|
||||
private TextBlock _cursorItemText = null!;
|
||||
private TextBlock _helpText = null!;
|
||||
|
||||
private TextBlock[] _hotbarIndicators = new TextBlock[9];
|
||||
private int _currentHotbarSlot = -1;
|
||||
|
||||
private Border? _lastHoveredSlotBorder;
|
||||
|
||||
private Canvas _overlayCanvas = null!;
|
||||
private Border _heldItemFloater = null!;
|
||||
private TextBlock _heldItemFloaterName = null!;
|
||||
private TextBlock _heldItemFloaterCount = null!;
|
||||
|
||||
private ScrollViewer _chatScrollViewer = null!;
|
||||
private ObservableCollection<string>? _chatLines;
|
||||
private int _lastTermW;
|
||||
private int _lastTermH;
|
||||
|
||||
public InventoryMainView()
|
||||
public PlayerInventoryView(McClient handler, int windowId)
|
||||
: base(new PlayerInventoryViewModel(handler, windowId))
|
||||
{
|
||||
var handler = InventoryTuiHost.ActiveHandler
|
||||
?? throw new InvalidOperationException("No active McClient");
|
||||
int windowId = InventoryTuiHost.ActiveWindowId;
|
||||
|
||||
_vm = new InventoryViewModel(handler, windowId);
|
||||
_currentHotbarSlot = handler.GetCurrentSlot();
|
||||
|
||||
_chatLines = TuiConsoleBackend.Instance?.GetView()?.GetRecentLogLines(50)
|
||||
?? new ObservableCollection<string>();
|
||||
|
||||
RebuildUi();
|
||||
_playerVm = (PlayerInventoryViewModel)_vm;
|
||||
Initialize();
|
||||
}
|
||||
|
||||
private void RebuildUi()
|
||||
protected override int GetTotalSlotRows()
|
||||
{
|
||||
int termH;
|
||||
try
|
||||
{
|
||||
_termW = System.Console.WindowWidth;
|
||||
termH = System.Console.WindowHeight;
|
||||
}
|
||||
catch
|
||||
{
|
||||
_termW = 120;
|
||||
termH = 40;
|
||||
}
|
||||
|
||||
_lastTermW = _termW;
|
||||
_lastTermH = termH;
|
||||
|
||||
int availW = _termW - 26;
|
||||
_slotW = Math.Clamp(availW / 9, 8, 18);
|
||||
_nameMaxLen = _slotW;
|
||||
|
||||
int topUsedW = _slotW * 4 + 8 + _slotW * 2 + 4 + _slotW;
|
||||
_topGap = Math.Max(2, (_slotW * 9 - topUsedW) / 2);
|
||||
|
||||
_slotH = Math.Clamp((termH - 8) / 6, 2, 5);
|
||||
_nameLines = _slotH;
|
||||
|
||||
_vm.SetSlotDisplayParams(_nameMaxLen, _nameLines);
|
||||
|
||||
_lastHoveredSlotBorder = null;
|
||||
|
||||
_titleText = new TextBlock
|
||||
{
|
||||
FontWeight = FontWeight.Bold,
|
||||
Foreground = Brushes.Cyan,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
};
|
||||
|
||||
_infoDetailText = new TextBlock
|
||||
{
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Foreground = Brushes.White,
|
||||
};
|
||||
|
||||
_infoDetailBorder = new Border
|
||||
{
|
||||
Background = Brushes.Transparent,
|
||||
Padding = new Thickness(0),
|
||||
Child = _infoDetailText,
|
||||
};
|
||||
|
||||
_cursorItemText = new TextBlock
|
||||
{
|
||||
Foreground = Brushes.Yellow,
|
||||
FontWeight = FontWeight.Bold,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
};
|
||||
|
||||
_helpText = new TextBlock
|
||||
{
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Foreground = new SolidColorBrush(Color.FromRgb(140, 140, 140)),
|
||||
Text = Translations.tui_inventory_controls_help,
|
||||
};
|
||||
|
||||
_heldItemFloaterName = new TextBlock
|
||||
{
|
||||
Foreground = Brushes.White,
|
||||
FontWeight = FontWeight.Bold,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
};
|
||||
_heldItemFloaterCount = new TextBlock
|
||||
{
|
||||
Foreground = BrCount,
|
||||
FontWeight = FontWeight.Bold,
|
||||
};
|
||||
_heldItemFloater = new Border
|
||||
{
|
||||
Background = BrHeldItemBg,
|
||||
BorderBrush = BrHeldItemBorder,
|
||||
BorderThickness = new Thickness(1),
|
||||
Padding = new Thickness(1, 0),
|
||||
IsVisible = false,
|
||||
MaxWidth = 24,
|
||||
Child = new StackPanel
|
||||
{
|
||||
Children = { _heldItemFloaterName, _heldItemFloaterCount },
|
||||
},
|
||||
};
|
||||
|
||||
_overlayCanvas = new Canvas { IsHitTestVisible = false };
|
||||
_overlayCanvas.Children.Add(_heldItemFloater);
|
||||
|
||||
var chatLines = _chatLines!;
|
||||
chatLines.CollectionChanged += (_, _) =>
|
||||
{
|
||||
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
var sv = _chatScrollViewer;
|
||||
if (sv.Extent.Height > sv.Viewport.Height)
|
||||
sv.Offset = new Vector(0, sv.Extent.Height - sv.Viewport.Height);
|
||||
}, Avalonia.Threading.DispatcherPriority.Background);
|
||||
};
|
||||
var chatItemsControl = new ItemsControl
|
||||
{
|
||||
ItemsSource = chatLines,
|
||||
Focusable = false,
|
||||
ItemTemplate = new FuncDataTemplate<string>((s, _) =>
|
||||
new TextBlock
|
||||
{
|
||||
Text = s,
|
||||
Foreground = Brushes.Gray,
|
||||
Padding = new Thickness(0),
|
||||
Margin = new Thickness(0),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
}),
|
||||
};
|
||||
_chatScrollViewer = new ScrollViewer
|
||||
{
|
||||
Content = chatItemsControl,
|
||||
Background = Brushes.Black,
|
||||
HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled,
|
||||
VerticalScrollBarVisibility = ScrollBarVisibility.Hidden,
|
||||
Padding = new Thickness(0),
|
||||
};
|
||||
|
||||
_hotbarIndicators = new TextBlock[9];
|
||||
|
||||
Content = BuildRootLayout();
|
||||
UpdateTitle();
|
||||
UpdateInfoPanel();
|
||||
|
||||
_chatScrollToBottom = true;
|
||||
_chatScrollViewer.ScrollChanged += OnChatScrollChanged;
|
||||
return 6;
|
||||
}
|
||||
|
||||
private bool _chatScrollToBottom = true;
|
||||
|
||||
private void OnChatScrollChanged(object? sender, ScrollChangedEventArgs e)
|
||||
protected override void RebuildUi()
|
||||
{
|
||||
if (!_chatScrollToBottom) return;
|
||||
var sv = _chatScrollViewer;
|
||||
if (sv.Extent.Height > sv.Viewport.Height)
|
||||
{
|
||||
sv.Offset = new Vector(0, sv.Extent.Height - sv.Viewport.Height);
|
||||
_chatScrollToBottom = false;
|
||||
}
|
||||
int availW = 0;
|
||||
try { availW = System.Console.WindowWidth - 26; } catch { availW = 94; }
|
||||
int slotW = System.Math.Clamp(availW / 9, 8, 18);
|
||||
int topUsedW = slotW * 4 + 8 + slotW * 2 + 4 + slotW;
|
||||
_topGap = System.Math.Max(2, (slotW * 9 - topUsedW) / 2);
|
||||
|
||||
base.RebuildUi();
|
||||
}
|
||||
|
||||
private Control BuildRootLayout()
|
||||
{
|
||||
// Layout (top-down):
|
||||
// Title
|
||||
// [InfoPanel(right)] [InventoryGrid(left)] <-- inventory area
|
||||
// ChatScrollViewer (full width, fills remaining)
|
||||
|
||||
var inventoryArea = BuildMainArea();
|
||||
DockPanel.SetDock(_titleText, Dock.Top);
|
||||
DockPanel.SetDock(inventoryArea, Dock.Top);
|
||||
|
||||
var mainContent = new DockPanel
|
||||
{
|
||||
Children = { _titleText, inventoryArea, _chatScrollViewer }
|
||||
};
|
||||
|
||||
return new Panel
|
||||
{
|
||||
Background = Brushes.Black,
|
||||
Children = { mainContent, _overlayCanvas }
|
||||
};
|
||||
}
|
||||
|
||||
private Control BuildMainArea()
|
||||
{
|
||||
var infoPanel = BuildInfoPanel();
|
||||
DockPanel.SetDock(infoPanel, Dock.Right);
|
||||
|
||||
return new DockPanel
|
||||
{
|
||||
Children = { infoPanel, BuildInventoryPanel() }
|
||||
};
|
||||
}
|
||||
|
||||
private Control BuildInfoPanel()
|
||||
{
|
||||
return new Border
|
||||
{
|
||||
BorderThickness = new Thickness(1),
|
||||
BorderBrush = Brushes.Gray,
|
||||
Padding = new Thickness(1),
|
||||
Width = 24,
|
||||
Child = new StackPanel
|
||||
{
|
||||
Children =
|
||||
{
|
||||
new TextBlock { Text = Translations.tui_inventory_item_info, FontWeight = FontWeight.Bold, Foreground = Brushes.Cyan },
|
||||
_infoDetailBorder,
|
||||
new TextBlock { Text = Translations.tui_inventory_held_item, FontWeight = FontWeight.Bold, Foreground = Brushes.Yellow, Margin = new Thickness(0, 1, 0, 0) },
|
||||
_cursorItemText,
|
||||
new TextBlock { Text = Translations.tui_inventory_controls, FontWeight = FontWeight.Bold, Foreground = Brushes.Green, Margin = new Thickness(0, 1, 0, 0) },
|
||||
_helpText,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Control BuildInventoryPanel()
|
||||
{
|
||||
var root = new StackPanel
|
||||
{
|
||||
Spacing = 0,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
};
|
||||
|
||||
root.Children.Add(BuildTopSection());
|
||||
root.Children.Add(new Border { Height = 1 });
|
||||
root.Children.Add(BuildSlotGrid(_vm.MainInventorySlots, 9));
|
||||
root.Children.Add(BuildHotbarSection());
|
||||
|
||||
return new Border
|
||||
{
|
||||
BorderThickness = new Thickness(1),
|
||||
BorderBrush = Brushes.Gray,
|
||||
Child = root,
|
||||
};
|
||||
}
|
||||
|
||||
private Control BuildTopSection()
|
||||
protected override Control BuildContainerSpecificArea()
|
||||
{
|
||||
var row = new StackPanel
|
||||
{
|
||||
|
|
@ -318,7 +53,7 @@ namespace MinecraftClient.Tui
|
|||
FontWeight = FontWeight.Bold,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
});
|
||||
offPanel.Children.Add(CreateSlotCell(_vm.OffhandSlot, 0, 0));
|
||||
offPanel.Children.Add(CreateSlotCell(_playerVm.OffhandSlot, 0, 0));
|
||||
row.Children.Add(offPanel);
|
||||
|
||||
var equipGrid = new Grid
|
||||
|
|
@ -332,7 +67,7 @@ namespace MinecraftClient.Tui
|
|||
var lbl = MakeLabel(label);
|
||||
Grid.SetRow(lbl, r); Grid.SetColumn(lbl, gc);
|
||||
equipGrid.Children.Add(lbl);
|
||||
var btn = CreateSlotCell(_vm.EquipmentSlots[eqIdx], r, gc / 2);
|
||||
var btn = CreateSlotCell(_playerVm.EquipmentSlots[eqIdx], r, gc / 2);
|
||||
Grid.SetRow(btn, r); Grid.SetColumn(btn, gc + 1);
|
||||
equipGrid.Children.Add(btn);
|
||||
}
|
||||
|
|
@ -354,7 +89,7 @@ namespace MinecraftClient.Tui
|
|||
for (int ci = 0; ci < 4; ci++)
|
||||
{
|
||||
int cr = ci / 2, cc = ci % 2;
|
||||
var cs = CreateSlotCell(_vm.CraftingInputSlots[ci], cr, cc);
|
||||
var cs = CreateSlotCell(_playerVm.CraftingInputSlots[ci], cr, cc);
|
||||
Grid.SetRow(cs, cr);
|
||||
Grid.SetColumn(cs, cc);
|
||||
craftGrid.Children.Add(cs);
|
||||
|
|
@ -382,7 +117,7 @@ namespace MinecraftClient.Tui
|
|||
FontWeight = FontWeight.Bold,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
});
|
||||
craftOutPanel.Children.Add(CreateSlotCell(_vm.CraftingOutputSlot, 0, 1));
|
||||
craftOutPanel.Children.Add(CreateSlotCell(_playerVm.CraftingOutputSlot, 0, 1));
|
||||
Grid.SetRow(craftOutPanel, 0); Grid.SetColumn(craftOutPanel, 3);
|
||||
Grid.SetRowSpan(craftOutPanel, 2);
|
||||
craftGrid.Children.Add(craftOutPanel);
|
||||
|
|
@ -390,363 +125,5 @@ namespace MinecraftClient.Tui
|
|||
row.Children.Add(craftGrid);
|
||||
return row;
|
||||
}
|
||||
|
||||
private Control BuildHotbarSection()
|
||||
{
|
||||
var panel = new StackPanel { Spacing = 0 };
|
||||
|
||||
var numberRow = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
};
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
bool active = i == _currentHotbarSlot;
|
||||
string label = active ? $"{i + 1} \u25bc" : $" {i + 1} ";
|
||||
|
||||
var tb = new TextBlock
|
||||
{
|
||||
Text = label,
|
||||
Width = _slotW,
|
||||
TextAlignment = TextAlignment.Center,
|
||||
Foreground = active ? Brushes.LightGreen : Brushes.DarkCyan,
|
||||
FontWeight = FontWeight.Bold,
|
||||
};
|
||||
_hotbarIndicators[i] = tb;
|
||||
numberRow.Children.Add(tb);
|
||||
}
|
||||
panel.Children.Add(numberRow);
|
||||
panel.Children.Add(BuildSlotGrid(_vm.HotbarSlots, 9));
|
||||
return panel;
|
||||
}
|
||||
|
||||
private TextBlock MakeLabel(string text)
|
||||
{
|
||||
return new TextBlock
|
||||
{
|
||||
Text = text,
|
||||
Foreground = BrEquipLbl,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(1, 0, 0, 0),
|
||||
FontWeight = FontWeight.Bold,
|
||||
};
|
||||
}
|
||||
|
||||
private Control BuildSlotGrid(ObservableCollection<SlotViewModel> slots, int columns)
|
||||
{
|
||||
var grid = new Grid();
|
||||
int rows = (slots.Count + columns - 1) / columns;
|
||||
|
||||
for (int r = 0; r < rows; r++)
|
||||
grid.RowDefinitions.Add(new RowDefinition(GridLength.Auto));
|
||||
for (int c = 0; c < columns; c++)
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Auto));
|
||||
|
||||
for (int i = 0; i < slots.Count; i++)
|
||||
{
|
||||
int row = i / columns;
|
||||
int col = i % columns;
|
||||
var cell = CreateSlotCell(slots[i], row, col);
|
||||
Grid.SetRow(cell, row);
|
||||
Grid.SetColumn(cell, col);
|
||||
grid.Children.Add(cell);
|
||||
}
|
||||
|
||||
return grid;
|
||||
}
|
||||
|
||||
private static IBrush GetSlotBg(bool isEmpty, int row, int col)
|
||||
{
|
||||
bool isA = (row + col) % 2 == 0;
|
||||
return isEmpty
|
||||
? (isA ? BrSlotEmptyA : BrSlotEmptyB)
|
||||
: (isA ? BrSlotFillA : BrSlotFillB);
|
||||
}
|
||||
|
||||
private Border CreateSlotCell(SlotViewModel slot, int row = 0, int col = 0)
|
||||
{
|
||||
var nameTb = new TextBlock
|
||||
{
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Padding = new Thickness(0),
|
||||
Margin = new Thickness(0),
|
||||
VerticalAlignment = VerticalAlignment.Top,
|
||||
};
|
||||
|
||||
var countTb = new TextBlock
|
||||
{
|
||||
Foreground = BrCount,
|
||||
FontWeight = FontWeight.Bold,
|
||||
Padding = new Thickness(0),
|
||||
Margin = new Thickness(0),
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
VerticalAlignment = VerticalAlignment.Bottom,
|
||||
};
|
||||
|
||||
ApplySlotVisual(slot, nameTb, countTb);
|
||||
|
||||
int r = row, c = col;
|
||||
var border = new Border
|
||||
{
|
||||
Width = _slotW,
|
||||
Height = _slotH,
|
||||
Background = GetSlotBg(slot.IsEmpty, r, c),
|
||||
Child = new Panel
|
||||
{
|
||||
Children = { nameTb, countTb },
|
||||
},
|
||||
Tag = (slot, r, c),
|
||||
};
|
||||
|
||||
border.PointerPressed += OnSlotPointerPressed;
|
||||
border.PointerEntered += OnSlotPointerEnter;
|
||||
border.PointerExited += OnSlotPointerExit;
|
||||
border.PointerMoved += OnSlotPointerMoved;
|
||||
|
||||
slot.PropertyChanged += (_, _) =>
|
||||
{
|
||||
ApplySlotVisual(slot, nameTb, countTb);
|
||||
border.Background = GetSlotBg(slot.IsEmpty, r, c);
|
||||
};
|
||||
|
||||
return border;
|
||||
}
|
||||
|
||||
private void ApplySlotVisual(SlotViewModel slot, TextBlock nameTb, TextBlock countTb)
|
||||
{
|
||||
if (slot.IsEmpty)
|
||||
{
|
||||
nameTb.Text = "";
|
||||
nameTb.Foreground = BrDim;
|
||||
countTb.Text = "";
|
||||
}
|
||||
else
|
||||
{
|
||||
nameTb.Text = slot.ItemDisplayText;
|
||||
nameTb.Foreground = BrName;
|
||||
countTb.Text = slot.CountDisplay;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSlotPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
if (sender is not Border border || border.Tag is not (SlotViewModel slot, int, int))
|
||||
return;
|
||||
|
||||
SetHover(border, slot);
|
||||
|
||||
var point = e.GetCurrentPoint(border);
|
||||
bool isShift = (e.KeyModifiers & KeyModifiers.Shift) != 0;
|
||||
|
||||
WindowActionType action;
|
||||
if (point.Properties.IsRightButtonPressed)
|
||||
action = isShift ? WindowActionType.ShiftRightClick : WindowActionType.RightClick;
|
||||
else
|
||||
action = isShift ? WindowActionType.ShiftClick : WindowActionType.LeftClick;
|
||||
|
||||
_vm.PerformAction(slot.SlotId, action);
|
||||
UpdateInfoPanel();
|
||||
UpdateHeldItemFloater(e);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void OnSlotPointerEnter(object? sender, PointerEventArgs e)
|
||||
{
|
||||
if (sender is Border b && b.Tag is (SlotViewModel slot, int, int))
|
||||
{
|
||||
SetHover(b, slot);
|
||||
UpdateHeldItemFloater(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSlotPointerMoved(object? sender, PointerEventArgs e)
|
||||
{
|
||||
if (sender is Border b && b.Tag is (SlotViewModel slot, int, int))
|
||||
{
|
||||
SetHover(b, slot);
|
||||
UpdateHeldItemFloater(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSlotPointerExit(object? sender, PointerEventArgs e)
|
||||
{
|
||||
if (sender is Border b && b.Tag is (SlotViewModel slot, int row, int col))
|
||||
b.Background = GetSlotBg(slot.IsEmpty, row, col);
|
||||
}
|
||||
|
||||
private void SetHover(Border border, SlotViewModel slot)
|
||||
{
|
||||
if (_lastHoveredSlotBorder != null && _lastHoveredSlotBorder != border)
|
||||
{
|
||||
if (_lastHoveredSlotBorder.Tag is (SlotViewModel oldSlot, int or, int oc))
|
||||
_lastHoveredSlotBorder.Background = GetSlotBg(oldSlot.IsEmpty, or, oc);
|
||||
}
|
||||
|
||||
_lastHoveredSlotBorder = border;
|
||||
border.Background = BrSlotHover;
|
||||
_vm.HoveredSlot = slot;
|
||||
UpdateInfoPanel();
|
||||
}
|
||||
|
||||
private void UpdateHeldItemFloater(PointerEventArgs e)
|
||||
{
|
||||
if (!_vm.HasCursorItem)
|
||||
{
|
||||
_heldItemFloater.IsVisible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_heldItemFloaterName.Text = _vm.CursorItemInfo;
|
||||
_heldItemFloaterCount.Text = "";
|
||||
|
||||
try
|
||||
{
|
||||
var pos = e.GetPosition(_overlayCanvas);
|
||||
double left = pos.X + 2;
|
||||
double remainingW = _termW - left - 2;
|
||||
int maxW = Math.Max(8, (int)remainingW);
|
||||
_heldItemFloater.MaxWidth = maxW;
|
||||
Canvas.SetLeft(_heldItemFloater, left);
|
||||
Canvas.SetTop(_heldItemFloater, pos.Y);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_heldItemFloater.MaxWidth = 24;
|
||||
Canvas.SetLeft(_heldItemFloater, 0);
|
||||
Canvas.SetTop(_heldItemFloater, 0);
|
||||
}
|
||||
|
||||
_heldItemFloater.IsVisible = true;
|
||||
}
|
||||
|
||||
private void UpdateInfoPanel()
|
||||
{
|
||||
_infoDetailText.Text = _vm.HoveredSlotDetailText;
|
||||
|
||||
bool hasHoveredItem = _vm.HoveredSlot != null && !_vm.HoveredSlot.IsEmpty;
|
||||
_infoDetailBorder.Background = hasHoveredItem ? BrInfoHighlight : Brushes.Transparent;
|
||||
|
||||
if (_vm.HasCursorItem)
|
||||
{
|
||||
_cursorItemText.Text = _vm.CursorItemInfo;
|
||||
_cursorItemText.Foreground = Brushes.Yellow;
|
||||
}
|
||||
else
|
||||
{
|
||||
_cursorItemText.Text = Translations.tui_inventory_cursor_empty;
|
||||
_cursorItemText.Foreground = BrDim;
|
||||
_heldItemFloater.IsVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateTitle()
|
||||
{
|
||||
_titleText.Text = _vm.Title;
|
||||
}
|
||||
|
||||
private void CloseInventory()
|
||||
{
|
||||
if (ConsoleIO.Backend is TuiConsoleBackend tuiBackend)
|
||||
tuiBackend.GetView()?.HideOverlay();
|
||||
else
|
||||
(Application.Current?.ApplicationLifetime as IControlledApplicationLifetime)?.Shutdown();
|
||||
}
|
||||
|
||||
protected override void OnKeyDown(KeyEventArgs e)
|
||||
{
|
||||
base.OnKeyDown(e);
|
||||
|
||||
switch (e.Key)
|
||||
{
|
||||
case Key.Escape:
|
||||
case Key.E:
|
||||
CloseInventory();
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.C:
|
||||
if ((e.KeyModifiers & KeyModifiers.Shift) != 0 &&
|
||||
_vm.HoveredSlot != null && !_vm.HoveredSlot.IsEmpty)
|
||||
{
|
||||
_vm.PerformAction(_vm.HoveredSlot.SlotId, WindowActionType.ShiftClick);
|
||||
UpdateInfoPanel();
|
||||
}
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.Q:
|
||||
if (_vm.HoveredSlot != null && !_vm.HoveredSlot.IsEmpty)
|
||||
{
|
||||
var action = (e.KeyModifiers & KeyModifiers.Control) != 0
|
||||
? WindowActionType.DropItemStack
|
||||
: WindowActionType.DropItem;
|
||||
_vm.PerformAction(_vm.HoveredSlot.SlotId, action);
|
||||
UpdateInfoPanel();
|
||||
}
|
||||
e.Handled = true;
|
||||
break;
|
||||
|
||||
case Key.R:
|
||||
_vm.RefreshFromContainer();
|
||||
_currentHotbarSlot = _vm.Handler.GetCurrentSlot();
|
||||
UpdateHotbarIndicators();
|
||||
UpdateInfoPanel();
|
||||
e.Handled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateHotbarIndicators()
|
||||
{
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
bool active = i == _currentHotbarSlot;
|
||||
_hotbarIndicators[i].Text = active ? $"{i + 1} \u25bc" : $" {i + 1} ";
|
||||
_hotbarIndicators[i].Foreground = active ? Brushes.LightGreen : Brushes.DarkCyan;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
base.OnAttachedToVisualTree(e);
|
||||
Focusable = true;
|
||||
Focus();
|
||||
AddHandler(KeyDownEvent, OnTunnelKeyDown, Avalonia.Interactivity.RoutingStrategies.Tunnel);
|
||||
SizeChanged += OnViewSizeChanged;
|
||||
}
|
||||
|
||||
private void OnTunnelKeyDown(object? sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Escape)
|
||||
{
|
||||
CloseInventory();
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnViewSizeChanged(object? sender, SizeChangedEventArgs e)
|
||||
{
|
||||
int newW, newH;
|
||||
try
|
||||
{
|
||||
newW = System.Console.WindowWidth;
|
||||
newH = System.Console.WindowHeight;
|
||||
}
|
||||
catch { return; }
|
||||
|
||||
if (newW == _lastTermW && newH == _lastTermH) return;
|
||||
|
||||
_vm.RefreshFromContainer();
|
||||
_currentHotbarSlot = _vm.Handler.GetCurrentSlot();
|
||||
RebuildUi();
|
||||
Focus();
|
||||
}
|
||||
|
||||
protected override void OnGotFocus(GotFocusEventArgs e)
|
||||
{
|
||||
base.OnGotFocus(e);
|
||||
Focusable = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,30 @@ namespace MinecraftClient.Tui
|
|||
|
||||
public static bool IsRunning => _isRunning;
|
||||
|
||||
/// <summary>
|
||||
/// Called by McClient.OnInventoryClose when the server closes a container.
|
||||
/// If the closed window matches the active TUI window, auto-close the TUI.
|
||||
/// </summary>
|
||||
public static void NotifyInventoryClosed(int windowId)
|
||||
{
|
||||
if (!_isRunning || windowId != ActiveWindowId)
|
||||
return;
|
||||
|
||||
if (ConsoleIO.Backend is TuiConsoleBackend)
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
var view = TuiConsoleBackend.Instance?.GetView();
|
||||
view?.HideOverlay();
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
(Avalonia.Application.Current?.ApplicationLifetime
|
||||
as Avalonia.Controls.ApplicationLifetimes.IControlledApplicationLifetime)?.Shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the TUI can be launched (classic mode has a one-shot limit).
|
||||
/// </summary>
|
||||
|
|
@ -89,7 +113,10 @@ namespace MinecraftClient.Tui
|
|||
var view = TuiConsoleBackend.Instance?.GetView();
|
||||
if (view != null)
|
||||
{
|
||||
var content = new InventoryMainView();
|
||||
var container = ActiveHandler!.GetInventory(ActiveWindowId);
|
||||
var content = ContainerViewBase.CreateView(
|
||||
container?.Type ?? ContainerType.PlayerInventory,
|
||||
ActiveHandler, ActiveWindowId);
|
||||
view.ShowOverlay(content, () =>
|
||||
{
|
||||
ActiveHandler = null;
|
||||
|
|
|
|||
|
|
@ -1,152 +1,48 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using MinecraftClient.Inventory;
|
||||
|
||||
namespace MinecraftClient.Tui
|
||||
{
|
||||
public class InventoryViewModel : INotifyPropertyChanged
|
||||
public class PlayerInventoryViewModel : ContainerViewModel
|
||||
{
|
||||
private SlotViewModel? _hoveredSlot;
|
||||
private string _title = "";
|
||||
private string _statusText = "";
|
||||
private string _cursorItemInfo = "";
|
||||
private bool _hasCursorItem;
|
||||
|
||||
public McClient Handler { get; }
|
||||
public int WindowId { get; }
|
||||
|
||||
public ObservableCollection<SlotViewModel> EquipmentSlots { get; } = new();
|
||||
public ObservableCollection<SlotViewModel> CraftingInputSlots { get; } = new();
|
||||
public SlotViewModel CraftingOutputSlot { get; }
|
||||
public ObservableCollection<SlotViewModel> MainInventorySlots { get; } = new();
|
||||
public ObservableCollection<SlotViewModel> HotbarSlots { get; } = new();
|
||||
public SlotViewModel OffhandSlot { get; }
|
||||
|
||||
public string Title
|
||||
public PlayerInventoryViewModel(McClient handler, int windowId)
|
||||
: base(handler, windowId, ContainerType.PlayerInventory)
|
||||
{
|
||||
get => _title;
|
||||
set { _title = value; OnPropertyChanged(); }
|
||||
CraftingOutputSlot = SlotMap[0];
|
||||
OffhandSlot = SlotMap[45];
|
||||
}
|
||||
|
||||
public string StatusText
|
||||
protected override void InitializeSlots()
|
||||
{
|
||||
get => _statusText;
|
||||
set { _statusText = value; OnPropertyChanged(); }
|
||||
}
|
||||
SlotMap.Clear();
|
||||
|
||||
public string CursorItemInfo
|
||||
{
|
||||
get => _cursorItemInfo;
|
||||
set { _cursorItemInfo = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
public bool HasCursorItem
|
||||
{
|
||||
get => _hasCursorItem;
|
||||
set { _hasCursorItem = value; OnPropertyChanged(); }
|
||||
}
|
||||
|
||||
public SlotViewModel? HoveredSlot
|
||||
{
|
||||
get => _hoveredSlot;
|
||||
set
|
||||
{
|
||||
if (_hoveredSlot != null)
|
||||
_hoveredSlot.IsHovered = false;
|
||||
_hoveredSlot = value;
|
||||
if (_hoveredSlot != null)
|
||||
_hoveredSlot.IsHovered = true;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(HoveredSlotDetailText));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multi-line detail text for the hovered slot.
|
||||
/// </summary>
|
||||
public string HoveredSlotDetailText
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_hoveredSlot == null)
|
||||
return Translations.tui_inventory_hover_hint;
|
||||
|
||||
if (_hoveredSlot.IsEmpty)
|
||||
return $"Slot #{_hoveredSlot.SlotId}\n{Translations.tui_inventory_slot_empty}";
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine(_hoveredSlot.ItemTypeName);
|
||||
sb.AppendLine(string.Format(Translations.tui_inventory_slot_detail, _hoveredSlot.SlotId, _hoveredSlot.ItemCount));
|
||||
|
||||
string fullInfo = _hoveredSlot.FullInfo;
|
||||
if (!string.IsNullOrEmpty(fullInfo))
|
||||
{
|
||||
string[] parts = fullInfo.Split(" | ");
|
||||
for (int i = 1; i < parts.Length; i++)
|
||||
sb.AppendLine(parts[i].Trim());
|
||||
}
|
||||
|
||||
return sb.ToString().TrimEnd();
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<int, SlotViewModel> _slotMap = new();
|
||||
private int _nameMaxLen = 9;
|
||||
private int _nameMaxLines = 1;
|
||||
|
||||
public InventoryViewModel(McClient handler, int windowId)
|
||||
{
|
||||
Handler = handler;
|
||||
WindowId = windowId;
|
||||
|
||||
CraftingOutputSlot = new SlotViewModel(0);
|
||||
OffhandSlot = new SlotViewModel(45);
|
||||
|
||||
InitializeSlots();
|
||||
RefreshFromContainer();
|
||||
}
|
||||
|
||||
public void SetSlotDisplayParams(int maxWidth, int maxLines)
|
||||
{
|
||||
_nameMaxLen = maxWidth;
|
||||
_nameMaxLines = maxLines;
|
||||
foreach (var kvp in _slotMap)
|
||||
{
|
||||
kvp.Value.NameMaxWidth = maxWidth;
|
||||
kvp.Value.NameMaxLines = maxLines;
|
||||
}
|
||||
RefreshFromContainer();
|
||||
}
|
||||
|
||||
private void InitializeSlots()
|
||||
{
|
||||
_slotMap.Clear();
|
||||
|
||||
_slotMap[0] = CraftingOutputSlot;
|
||||
var craftOut = new SlotViewModel(0);
|
||||
SlotMap[0] = craftOut;
|
||||
|
||||
for (int i = 1; i <= 4; i++)
|
||||
{
|
||||
var slot = new SlotViewModel(i);
|
||||
CraftingInputSlots.Add(slot);
|
||||
_slotMap[i] = slot;
|
||||
SlotMap[i] = slot;
|
||||
}
|
||||
|
||||
for (int i = 5; i <= 8; i++)
|
||||
{
|
||||
var slot = new SlotViewModel(i);
|
||||
EquipmentSlots.Add(slot);
|
||||
_slotMap[i] = slot;
|
||||
SlotMap[i] = slot;
|
||||
}
|
||||
|
||||
for (int i = 9; i <= 35; i++)
|
||||
{
|
||||
var slot = new SlotViewModel(i);
|
||||
MainInventorySlots.Add(slot);
|
||||
_slotMap[i] = slot;
|
||||
SlotMap[i] = slot;
|
||||
}
|
||||
|
||||
for (int i = 36; i <= 44; i++)
|
||||
|
|
@ -154,67 +50,11 @@ namespace MinecraftClient.Tui
|
|||
int hotbarIdx = i - 36;
|
||||
var slot = new SlotViewModel(i, isHotbar: true, hotbarIndex: hotbarIdx);
|
||||
HotbarSlots.Add(slot);
|
||||
_slotMap[i] = slot;
|
||||
SlotMap[i] = slot;
|
||||
}
|
||||
|
||||
_slotMap[45] = OffhandSlot;
|
||||
}
|
||||
|
||||
public void RefreshFromContainer()
|
||||
{
|
||||
Inventory.Container? container = Handler.GetInventory(WindowId);
|
||||
if (container == null)
|
||||
{
|
||||
StatusText = Translations.tui_inventory_container_not_found;
|
||||
return;
|
||||
}
|
||||
|
||||
Title = string.Format(Translations.tui_inventory_title, WindowId, container.Title);
|
||||
|
||||
foreach (var kvp in _slotMap)
|
||||
{
|
||||
Item? item = container.Items.TryGetValue(kvp.Key, out var it) ? it : null;
|
||||
kvp.Value.Update(item);
|
||||
}
|
||||
|
||||
UpdateCursorItem(container);
|
||||
int itemCount = 0;
|
||||
foreach (var kvp in container.Items)
|
||||
{
|
||||
if (kvp.Key >= 0 && !kvp.Value.IsEmpty)
|
||||
itemCount++;
|
||||
}
|
||||
StatusText = string.Format(Translations.tui_inventory_item_count, itemCount);
|
||||
|
||||
OnPropertyChanged(nameof(HoveredSlotDetailText));
|
||||
}
|
||||
|
||||
private void UpdateCursorItem(Inventory.Container container)
|
||||
{
|
||||
if (container.Items.TryGetValue(-1, out var cursorItem) && !cursorItem.IsEmpty)
|
||||
{
|
||||
CursorItemInfo = $"x{cursorItem.Count} {cursorItem.GetTypeString()}";
|
||||
HasCursorItem = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
CursorItemInfo = "";
|
||||
HasCursorItem = false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool PerformAction(int slotId, WindowActionType action)
|
||||
{
|
||||
bool result = Handler.DoWindowAction(WindowId, slotId, action);
|
||||
RefreshFromContainer();
|
||||
return result;
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
private void OnPropertyChanged([CallerMemberName] string? name = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
var offhand = new SlotViewModel(45);
|
||||
SlotMap[45] = offhand;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue