upd
This commit is contained in:
@@ -6,9 +6,14 @@ public class BaseItemDefinition : GameResource
|
||||
public string Name { get; set; }
|
||||
|
||||
public string Description { get; set; }
|
||||
[ResourceType( "prefab" )] public GameObject Prefab { get; set; }
|
||||
|
||||
[ResourceType( "prefab" )]
|
||||
public GameObject Prefab { get; set; } = GameObject.GetPrefab( "prefabs/item_parcel.prefab" );
|
||||
|
||||
public Texture ImageTexture { get; set; }
|
||||
public string ImageUrl { get; set; }
|
||||
|
||||
public int MaxCount { get; set; } = 1;
|
||||
|
||||
public virtual Inventar.InventorySlot? GetSlot() => null;
|
||||
}
|
||||
|
||||
@@ -5,4 +5,5 @@ public class ClothingItemDefinition : BaseItemDefinition, IEquipable
|
||||
{
|
||||
[Property] public string ClothUrl { get; set; }
|
||||
public Inventar.InventorySlot Slot { get; set; }
|
||||
public override Inventar.InventorySlot? GetSlot() => Slot;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ namespace Sasalka;
|
||||
public class WeaponItemDefinition : BaseItemDefinition, IEquipable
|
||||
{
|
||||
public Inventar.InventorySlot Slot { get; set; }
|
||||
|
||||
public override Inventar.InventorySlot? GetSlot() => Slot;
|
||||
public CitizenAnimationHelper.HoldTypes HoldType { get; set; } = CitizenAnimationHelper.HoldTypes.None;
|
||||
[InlineEditor, Space] public WeaponDefinition WeaponDefinition { get; set; }
|
||||
}
|
||||
|
||||
@@ -60,23 +60,28 @@ public class Inventar
|
||||
|
||||
// Экипировать новый предмет
|
||||
EquippedItems[equipable.Slot] = item;
|
||||
item.Equipped = true;
|
||||
OnEquipped?.Invoke( item );
|
||||
}
|
||||
|
||||
public void DropItem( InventoryItem item, Vector3 position )
|
||||
{
|
||||
var gO = item.Definition.Prefab.Clone( position );
|
||||
GameObject gO = item.Definition.Prefab.Clone( position );
|
||||
|
||||
if ( gO.Components.TryGet<InventoryItem>( out var inventoryItem ) )
|
||||
{
|
||||
inventoryItem.Count = item.Count;
|
||||
|
||||
if ( inventoryItem.Definition == null )
|
||||
{
|
||||
inventoryItem.Definition = item.Definition;
|
||||
}
|
||||
}
|
||||
|
||||
gO.NetworkSpawn( null );
|
||||
|
||||
RemoveItem( item );
|
||||
// Items.Remove( item );
|
||||
// OnChanged?.Invoke();
|
||||
Items.Remove( item );
|
||||
OnChanged?.Invoke();
|
||||
}
|
||||
|
||||
|
||||
@@ -87,6 +92,7 @@ public class Inventar
|
||||
EquippedItems.Remove( kvp.Key );
|
||||
}
|
||||
|
||||
item.Equipped = false;
|
||||
OnUnEquipped?.Invoke( item );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Sandbox;
|
||||
using Sandbox.UI;
|
||||
|
||||
namespace Sasalka;
|
||||
|
||||
@@ -6,4 +7,13 @@ public class InventoryItem : Component
|
||||
{
|
||||
[Property] public BaseItemDefinition Definition { get; set; }
|
||||
[Property] public int Count { get; set; } = 1;
|
||||
[Property] public bool Equipped { get; set; } = false;
|
||||
|
||||
protected override void OnStart()
|
||||
{
|
||||
if ( GameObject.Components.TryGet<PickupItem>( out var item ) ) //FindMode.EverythingInSelf
|
||||
{
|
||||
item.Label = Definition.Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
@inherits PanelComponent
|
||||
@namespace Sasalka.Ui
|
||||
|
||||
<root class="@( Inventar.IsInventoryOpen ? "" : "hidden" )">
|
||||
<root class="inventory @( Inventar.IsInventoryOpen ? "" : "hidden" )">
|
||||
<div class="inventory-panel">
|
||||
@if ( PlayerInventory.Items.Count > 0 )
|
||||
{
|
||||
|
||||
@@ -1,34 +1,30 @@
|
||||
Inventory {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 96vh;
|
||||
width: 30%;
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 24px;
|
||||
background: linear-gradient(135deg, #0a1a2b 0%, #08111f 100%);
|
||||
border: 3px solid #2a3d54;
|
||||
border-radius: 14px;
|
||||
font-family: 'Orbitron', 'Poppins', sans-serif;
|
||||
position: absolute;
|
||||
width: 30%;
|
||||
height: 96vh;
|
||||
right: 20px;
|
||||
top: 20px;
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
transition: all 0.2s ease;
|
||||
z-index: 100;
|
||||
overflow: hidden;
|
||||
|
||||
pointer-events: all;
|
||||
|
||||
&.hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.inventory-panel {
|
||||
overflow-y: scroll;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
//background-color: rgba(255, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
|
||||
.hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
@@ -3,24 +3,33 @@
|
||||
@inherits Sandbox.UI.Panel
|
||||
@namespace Sasalka.Ui
|
||||
|
||||
<root class="inventory-item @( Equipped ? "equipped" : "" )" @onclick="@(() => OnItemClick?.Invoke( Item ))" @onrightclick=@( () => OnItemRightClick?.Invoke( Item ) )>
|
||||
@{
|
||||
var definition = Item?.Definition;
|
||||
var name = definition?.Name;
|
||||
var slot = Item?.Equipped == true ? definition?.GetSlot() : null;
|
||||
var imageUrl = definition?.ImageTexture.IsValid() == true
|
||||
? definition.ImageTexture.ResourcePath
|
||||
: !string.IsNullOrWhiteSpace( definition?.ImageUrl )
|
||||
? definition.ImageUrl
|
||||
: null;
|
||||
}
|
||||
|
||||
@* <input type="checkbox" class="equipped-checkbox" checked="@Equipped" disabled/> *@
|
||||
|
||||
@if ( Item.Definition.ImageTexture.IsValid() )
|
||||
<root class="inventory-item @( Item.Equipped ? "equipped" : "" )" @onclick="@(() => OnItemClick?.Invoke( Item ))" @onrightclick=@( () => OnItemRightClick?.Invoke( Item ) )>
|
||||
@if ( slot is not null )
|
||||
{
|
||||
<img src="@Item.Definition.ImageTexture.ResourcePath" alt="@Item.Definition.Name"/>
|
||||
}
|
||||
else if ( Item.Definition.ImageUrl.Length > 0 )
|
||||
{
|
||||
<img src="@Item.Definition.ImageUrl" alt="@Item.Definition.Name">
|
||||
<div class="inventory-item__slot">@slot</div>
|
||||
}
|
||||
|
||||
<div class="inventory-item__name">@Item?.Definition.Name</div>
|
||||
|
||||
@if ( Item?.Definition.MaxCount > 1 )
|
||||
@if ( !string.IsNullOrEmpty( imageUrl ) )
|
||||
{
|
||||
<div class="inventory-item__count">@Item?.Count / @Item?.Definition.MaxCount</div>
|
||||
<img src="@imageUrl" alt="@name"/>
|
||||
}
|
||||
|
||||
<div class="inventory-item__name">@name</div>
|
||||
|
||||
@if ( definition?.MaxCount > 1 )
|
||||
{
|
||||
<div class="inventory-item__count">@Item?.Count / @definition.MaxCount</div>
|
||||
}
|
||||
</root>
|
||||
|
||||
@@ -29,16 +38,11 @@
|
||||
public Action<Sasalka.InventoryItem> OnItemClick { get; set; }
|
||||
public Action<Sasalka.InventoryItem> OnItemRightClick { get; set; }
|
||||
|
||||
public bool Equipped { get; set; }
|
||||
|
||||
protected override int BuildHash()
|
||||
{
|
||||
base.BuildHash();
|
||||
|
||||
var hash = new HashCode();
|
||||
|
||||
hash.Add( Item.Count );
|
||||
|
||||
hash.Add( Item?.Count );
|
||||
return hash.ToHashCode();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
InventoryItem {
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
//height: 64px;
|
||||
background: #2a3d53;
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
border: 1px solid #666;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
border-radius: 12px;
|
||||
padding: 12px 24px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
@@ -20,22 +20,30 @@ InventoryItem {
|
||||
img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
&.name {
|
||||
.inventory-item__name {
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.inventory-item__count {
|
||||
margin-left: auto;
|
||||
font-size: 14px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.inventory-item__slot {
|
||||
font-size: 12px;
|
||||
color: #8fc98f;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&.equipped {
|
||||
border: 2px solid #4caf50;
|
||||
background: #2e3e2e;
|
||||
}
|
||||
|
||||
.equipped-checkbox {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 6px;
|
||||
pointer-events: none;
|
||||
accent-color: #4caf50;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,12 +18,12 @@ public sealed partial class Dedugan : Component
|
||||
|
||||
// Inventory.AddItem( new InventoryItem
|
||||
// {
|
||||
// Definition = ResourceLibrary.Get<ClothingItemDefinition>( "Items/cloth_pijama.clitem" )
|
||||
// Definition = ResourceLibrary.Get<ClothingItemDefinition>( "Items/Cloth/cloth_pijama.clitem" )
|
||||
// } );
|
||||
//
|
||||
// Inventory.AddItem( new InventoryItem
|
||||
// {
|
||||
// Definition = ResourceLibrary.Get<ClothingItemDefinition>( "Items/cloth_pijama_bottom.clitem" )
|
||||
// Definition = ResourceLibrary.Get<ClothingItemDefinition>( "Items/Cloth/cloth_pijama_bottom.clitem" )
|
||||
// } );
|
||||
//
|
||||
// Inventory.AddItem( new InventoryItem
|
||||
|
||||
@@ -80,50 +80,25 @@ partial class Dedugan
|
||||
|
||||
CancellationTokenSource _cts;
|
||||
|
||||
[Rpc.Broadcast]
|
||||
public void WearWorkshop( List<string> workshopItems )
|
||||
{
|
||||
_cts = new CancellationTokenSource();
|
||||
var token = _cts.Token;
|
||||
|
||||
if ( workshopItems != null && workshopItems.Count > 0 )
|
||||
{
|
||||
Task.WhenAll( workshopItems.Select( x => InstallWorkshopClothing( x, token ) ) )
|
||||
.ContinueWith( ( tasks ) =>
|
||||
{
|
||||
foreach ( var cloth in tasks.Result )
|
||||
{
|
||||
if ( cloth is null )
|
||||
continue;
|
||||
|
||||
CurrentClothing.Add( cloth );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
CurrentClothing.Normalize();
|
||||
CurrentClothing.Apply( Renderer );
|
||||
Renderer.PostAnimationUpdate();
|
||||
}
|
||||
|
||||
// public async void AsyncWearWorkshop( List<string> workshopItems )
|
||||
// [Rpc.Broadcast]
|
||||
// public void WearWorkshop( List<string> workshopItems )
|
||||
// {
|
||||
// _cts = new CancellationTokenSource();
|
||||
// var token = _cts.Token;
|
||||
//
|
||||
// if ( workshopItems != null && workshopItems.Count > 0 )
|
||||
// {
|
||||
// var tasks = workshopItems.Select( x => InstallWorkshopClothing( x, token ) );
|
||||
//
|
||||
// foreach ( var task in tasks )
|
||||
// {
|
||||
// var c = await task;
|
||||
//
|
||||
// if ( c is null )
|
||||
// continue;
|
||||
//
|
||||
// CurrentClothing.Add( c );
|
||||
// }
|
||||
// Task.WhenAll( workshopItems.Select( x => InstallWorkshopClothing( x, token ) ) )
|
||||
// .ContinueWith( ( tasks ) =>
|
||||
// {
|
||||
// foreach ( var cloth in tasks.Result )
|
||||
// {
|
||||
// if ( cloth is null )
|
||||
// continue;
|
||||
// Log.Info( cloth.Title );
|
||||
// CurrentClothing.Add( cloth );
|
||||
// }
|
||||
// } );
|
||||
// }
|
||||
//
|
||||
// CurrentClothing.Normalize();
|
||||
@@ -131,6 +106,37 @@ partial class Dedugan
|
||||
// Renderer.PostAnimationUpdate();
|
||||
// }
|
||||
|
||||
[Rpc.Broadcast]
|
||||
public void WearWorkshop( List<string> workshopItems )
|
||||
{
|
||||
AsyncWearWorkshop( workshopItems );
|
||||
}
|
||||
|
||||
public async void AsyncWearWorkshop( List<string> workshopItems )
|
||||
{
|
||||
_cts = new CancellationTokenSource();
|
||||
var token = _cts.Token;
|
||||
|
||||
if ( workshopItems != null && workshopItems.Count > 0 )
|
||||
{
|
||||
var tasks = workshopItems.Select( x => InstallWorkshopClothing( x, token ) );
|
||||
|
||||
foreach ( var task in tasks )
|
||||
{
|
||||
var c = await task;
|
||||
|
||||
if ( c is null )
|
||||
continue;
|
||||
|
||||
CurrentClothing.Add( c );
|
||||
}
|
||||
}
|
||||
|
||||
CurrentClothing.Normalize();
|
||||
CurrentClothing.Apply( Renderer );
|
||||
Renderer.PostAnimationUpdate();
|
||||
}
|
||||
|
||||
[Rpc.Broadcast]
|
||||
public void StripByName( string name )
|
||||
{
|
||||
|
||||
@@ -27,7 +27,7 @@ public sealed partial class Dedugan : Component, IUseContext, Component.INetwork
|
||||
[Sync] public int Health { get; set; } = 100;
|
||||
private RagdollController RagdollController { get; set; }
|
||||
|
||||
public Vector3 OverrideGravity { get; set; } = Vector3.Zero;
|
||||
[Property] public Vector3 OverrideGravity { get; set; } = Vector3.Zero;
|
||||
|
||||
private Vector3 _directionToAxis = Vector3.Up;
|
||||
private Vector3 _up = Vector3.Up;
|
||||
|
||||
23
Code/SimpleJson.cs
Normal file
23
Code/SimpleJson.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Sasalka;
|
||||
|
||||
public static class SimpleJson
|
||||
{
|
||||
public static string[] ParseClothingTitles( string json )
|
||||
{
|
||||
using var doc = JsonDocument.Parse( json );
|
||||
var root = doc.RootElement;
|
||||
var list = new List<string>();
|
||||
|
||||
foreach ( var elem in root.EnumerateArray() )
|
||||
{
|
||||
if ( elem.TryGetProperty( "Title", out var title ) )
|
||||
{
|
||||
list.Add( title.GetString() );
|
||||
}
|
||||
}
|
||||
|
||||
return list.ToArray();
|
||||
}
|
||||
}
|
||||
@@ -1,138 +1,156 @@
|
||||
@using System;
|
||||
@using Sandbox.UI;
|
||||
@using Sandbox
|
||||
@using System
|
||||
@using Sandbox.UI
|
||||
@namespace Sandbox
|
||||
@inherits PanelComponent
|
||||
@implements Component.INetworkListener
|
||||
<root>
|
||||
|
||||
<root>
|
||||
<div class="output">
|
||||
@foreach (var entry in Entries)
|
||||
@foreach ( var entry in Entries )
|
||||
{
|
||||
<div class="chat_entry">
|
||||
@if (entry.steamid > 0)
|
||||
{
|
||||
<div class="avatar" style="background-image: url( avatar:@entry.steamid )"></div>
|
||||
}
|
||||
<div class="author">@entry.author</div>
|
||||
<div class="message">@entry.message</div>
|
||||
</div>
|
||||
<ChatEntry Type="@entry.Type"
|
||||
SteamID="@entry.SteamID"
|
||||
Author="@entry.Author"
|
||||
Message="@entry.Message"
|
||||
IsTemporary="@entry.IsTemporary"/>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="input">
|
||||
<TextEntry @ref="InputBox" onsubmit="@ChatFinished"></TextEntry>
|
||||
<div class="input-container">
|
||||
<TextEntry @ref=" InputBox" onsubmit="@ChatFinished"/>
|
||||
</div>
|
||||
|
||||
</root>
|
||||
|
||||
@code
|
||||
{
|
||||
|
||||
@code {
|
||||
public static Chat Instance;
|
||||
public Chat() => Instance = this;
|
||||
public static bool IsActive => Instance.InputBox.HasFocus;
|
||||
public static bool IsActive = false;
|
||||
|
||||
public static void Open()
|
||||
public enum MessageType
|
||||
{
|
||||
Instance.InputBox.Focus();
|
||||
Player,
|
||||
System,
|
||||
Admin,
|
||||
Notification
|
||||
}
|
||||
|
||||
public static void AddText(string text)
|
||||
public record Entry
|
||||
{
|
||||
Instance.AddTextInternal(text);
|
||||
public Entry( Chat.MessageType type, ulong steamID, string author, string message, RealTimeSince timeSinceAdded, bool isTemporary )
|
||||
{
|
||||
Type = type;
|
||||
SteamID = steamID;
|
||||
Author = author;
|
||||
Message = message;
|
||||
TimeSinceAdded = timeSinceAdded;
|
||||
IsTemporary = isTemporary;
|
||||
}
|
||||
|
||||
public Chat.MessageType Type { get; internal set; }
|
||||
public ulong SteamID { get; internal set; }
|
||||
public string Author { get; internal set; }
|
||||
public string Message { get; internal set; }
|
||||
public RealTimeSince TimeSinceAdded { get; internal set; }
|
||||
public bool IsTemporary { get; internal set; }
|
||||
}
|
||||
|
||||
public event Action<string> OnChat;
|
||||
public TextEntry InputBox;
|
||||
|
||||
public record Entry(ulong steamid, string author, string message, RealTimeSince timeSinceAdded);
|
||||
List<Entry> Entries = new();
|
||||
public TextEntry InputBox;
|
||||
public event Action<string> OnChat;
|
||||
|
||||
protected override void OnUpdate()
|
||||
{
|
||||
if (InputBox is null)
|
||||
return;
|
||||
|
||||
if ( InputBox is null ) return;
|
||||
Panel.AcceptsFocus = false;
|
||||
|
||||
if (Input.Pressed("chat"))
|
||||
if ( Input.Pressed( "chat" ) )
|
||||
Open();
|
||||
|
||||
if (Entries.RemoveAll(x => x.timeSinceAdded > 20.0f) > 0)
|
||||
{
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
if (InputBox.HasFocus && Input.EscapePressed)
|
||||
if ( InputBox.HasFocus && Input.EscapePressed )
|
||||
{
|
||||
Input.EscapePressed = false;
|
||||
ChatClosed();
|
||||
}
|
||||
|
||||
SetClass("open", InputBox.HasFocus);
|
||||
SetClass( "open", InputBox.HasFocus );
|
||||
}
|
||||
|
||||
public static void Open()
|
||||
{
|
||||
IsActive = true;
|
||||
Instance.InputBox?.Focus();
|
||||
|
||||
foreach ( var entry in Instance.Entries )
|
||||
entry.IsTemporary = false;
|
||||
|
||||
Instance.StateHasChanged();
|
||||
}
|
||||
|
||||
public static void AddMessage( MessageType type, string message, ulong steamId = 0, string author = "" ) => Instance?.AddMessageInternal( type, message, steamId, author );
|
||||
|
||||
[ConCmd( "say" )]
|
||||
public static void Say( string message )
|
||||
{
|
||||
Instance?.AddTextInternal( message );
|
||||
}
|
||||
|
||||
[Rpc.Broadcast]
|
||||
public void AddTextInternal( string message )
|
||||
{
|
||||
if ( string.IsNullOrWhiteSpace( message ) ) return;
|
||||
|
||||
AddMessageInternal( MessageType.Player, message.Truncate( 300 ), Rpc.Caller.SteamId, Rpc.Caller.DisplayName );
|
||||
}
|
||||
|
||||
void AddMessageInternal( MessageType type, string message, ulong steamId = 0, string author = "" )
|
||||
{
|
||||
Entries.Add( new Entry( type, steamId, author, message, 0, !IsActive ) );
|
||||
StateHasChanged();
|
||||
Log.Info( $"[{type}] {author}: {message}" );
|
||||
}
|
||||
|
||||
void ScrollToBottom()
|
||||
{
|
||||
var panel = Panel.Children.First();
|
||||
panel.ScrollVelocity = 0;
|
||||
panel.ScrollOffset = 0;
|
||||
}
|
||||
|
||||
void ChatFinished()
|
||||
{
|
||||
IsActive = false;
|
||||
var text = InputBox.Text;
|
||||
Mouse.Visibility = MouseVisibility.Auto;
|
||||
|
||||
OnChat?.Invoke(text);
|
||||
OnChat = null;
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return;
|
||||
|
||||
AddTextInternal(InputBox.Text);
|
||||
InputBox.Text = "";
|
||||
|
||||
if ( !string.IsNullOrWhiteSpace( text ) )
|
||||
{
|
||||
OnChat?.Invoke( text );
|
||||
AddTextInternal( text );
|
||||
}
|
||||
|
||||
ScrollToBottom();
|
||||
OnChat = null;
|
||||
}
|
||||
|
||||
void ChatClosed()
|
||||
{
|
||||
var text = InputBox.Text;
|
||||
IsActive = false;
|
||||
InputBox.Text = "";
|
||||
ScrollToBottom();
|
||||
OnChat = null;
|
||||
}
|
||||
|
||||
[Rpc.Broadcast]
|
||||
public void AddTextInternal(string message)
|
||||
void Component.INetworkListener.OnConnected( Connection channel )
|
||||
{
|
||||
message = message.Truncate(300);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(message))
|
||||
return;
|
||||
|
||||
var author = Rpc.Caller.DisplayName;
|
||||
var steamid = Rpc.Caller.SteamId;
|
||||
|
||||
Log.Info($"{author}: {message}");
|
||||
|
||||
Entries.Add(new Entry(steamid, author, message, 0.0f));
|
||||
StateHasChanged();
|
||||
if ( IsProxy ) return;
|
||||
AddMessageInternal( MessageType.System, $"{channel.DisplayName} has joined the game" );
|
||||
}
|
||||
|
||||
[Rpc.Broadcast]
|
||||
void AddSystemText(string message)
|
||||
void Component.INetworkListener.OnDisconnected( Connection channel )
|
||||
{
|
||||
message = message.Truncate(300);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(message))
|
||||
return;
|
||||
|
||||
Entries.Add(new Entry(0, "ℹ️", message, 0.0f));
|
||||
StateHasChanged();
|
||||
if ( IsProxy ) return;
|
||||
AddMessageInternal( MessageType.System, $"{channel.DisplayName} has left the game" );
|
||||
}
|
||||
|
||||
void Component.INetworkListener.OnConnected(Connection channel)
|
||||
{
|
||||
if (IsProxy) return;
|
||||
|
||||
AddSystemText($"{channel.DisplayName} has joined the game");
|
||||
}
|
||||
|
||||
void Component.INetworkListener.OnDisconnected(Connection channel)
|
||||
{
|
||||
if (IsProxy) return;
|
||||
|
||||
AddSystemText($"{channel.DisplayName} has left the game");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,67 +4,87 @@ Chat {
|
||||
left: 200px;
|
||||
bottom: 200px;
|
||||
width: 600px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
border-radius: 20px;
|
||||
justify-content: flex-end;
|
||||
font-family: Poppins;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
font-size: 17px;
|
||||
font-family: Poppins;
|
||||
gap: 10px;
|
||||
|
||||
|
||||
.output {
|
||||
box-shadow: none;
|
||||
padding: 2px;
|
||||
border-radius: 12px;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
align-items: stretch;
|
||||
gap: 5px;
|
||||
min-height: 256px;
|
||||
max-height: 256px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.chat_entry {
|
||||
padding: 2px;
|
||||
gap: 10px;
|
||||
text-shadow: 2px 2px 2px #000a;
|
||||
&:not(.open) {
|
||||
.output {
|
||||
ChatEntry {
|
||||
opacity: 0;
|
||||
|
||||
.avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
border-radius: 4px;
|
||||
aspect-ratio: 1;
|
||||
min-width: 32px;
|
||||
max-width: 32px;
|
||||
}
|
||||
|
||||
.author {
|
||||
color: #2d95ce;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.message {
|
||||
color: #fff;
|
||||
&.temporary {
|
||||
animation: temporaryMessageFadeOut 4s forwards;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
.input-container {
|
||||
color: white;
|
||||
|
||||
.textentry {
|
||||
align-items: flex-start;
|
||||
white-space: normal;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
transition: all 0.1s ease;
|
||||
transform: translateY(10px);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
&.open {
|
||||
.input {
|
||||
border-radius: 8px;
|
||||
background-color: rgba(0,0,0,0.2);
|
||||
backdrop-filter: blur(10px);
|
||||
padding: 8px;
|
||||
pointer-events: all;
|
||||
pointer-events: all;
|
||||
|
||||
.input-container {
|
||||
transform: translateY(0);
|
||||
|
||||
.textentry {
|
||||
background-color: rgba(30,30,40,0.8);
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.25);
|
||||
backdrop-filter: blur(15px);
|
||||
transform: translateY(0px);
|
||||
}
|
||||
}
|
||||
|
||||
.output {
|
||||
background: linear-gradient( to top, rgba(10,10,20,0.9) 0%, rgba(10,10,20,0.6) 100% );
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||||
backdrop-filter: blur(15px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes temporaryMessageFadeOut {
|
||||
0%, 90% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
41
Code/UI/ChatEntry.razor
Normal file
41
Code/UI/ChatEntry.razor
Normal file
@@ -0,0 +1,41 @@
|
||||
@using Sandbox.UI
|
||||
@namespace Sandbox
|
||||
@inherits Panel
|
||||
|
||||
<root class="@( IsTemporary ? "temporary" : "" )">
|
||||
<div class="meta">
|
||||
@if ( Type == Chat.MessageType.Player && SteamID > 0 )
|
||||
{
|
||||
<div class="avatar" style="background-image: url( avatar:@SteamID )"></div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="type-icon">
|
||||
@switch ( Type )
|
||||
{
|
||||
case Chat.MessageType.System:
|
||||
<i>system_update_alt</i>
|
||||
break;
|
||||
case Chat.MessageType.Admin:
|
||||
<i>verified_user</i>
|
||||
break;
|
||||
case Chat.MessageType.Notification:
|
||||
<i>notifications</i>
|
||||
break;
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<label class="author">@Author</label>
|
||||
</div>
|
||||
|
||||
<label class="message">@Message</label>
|
||||
</root>
|
||||
|
||||
@code {
|
||||
public Chat.MessageType Type { get; set; }
|
||||
public ulong SteamID { get; set; }
|
||||
public string Message { get; set; }
|
||||
public string Author { get; set; }
|
||||
public bool IsTemporary { get; set; }
|
||||
}
|
||||
90
Code/UI/ChatEntry.razor.scss
Normal file
90
Code/UI/ChatEntry.razor.scss
Normal file
@@ -0,0 +1,90 @@
|
||||
ChatEntry {
|
||||
border-radius: 8px;
|
||||
background-color: rgba(0, 0, 0, 0.28);
|
||||
backdrop-filter: blur(2px);
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
padding: 2px;
|
||||
|
||||
.meta {
|
||||
flex-shrink: 0;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.avatar, .type-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 6px;
|
||||
background-color: rgba(100, 150, 255, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
|
||||
i {
|
||||
font-family: 'Material Icons';
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.author {
|
||||
color: #a8e063;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
flex-shrink: 0;
|
||||
max-width: 150px;
|
||||
overflow: hidden;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.message {
|
||||
color: #fff;
|
||||
align-content: flex-end;
|
||||
text-align: left;
|
||||
justify-content: center;
|
||||
font-size: 15px;
|
||||
padding-left: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&.type-system {
|
||||
background-color: rgba(100, 200, 255, 0.12);
|
||||
|
||||
.author {
|
||||
color: #6ec6ff;
|
||||
}
|
||||
|
||||
.type-icon {
|
||||
background-color: rgba(100, 200, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
&.type-admin {
|
||||
.author {
|
||||
color: #ff9e80;
|
||||
}
|
||||
|
||||
.type-icon {
|
||||
background-color: rgba(255, 100, 100, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
&.type-notification {
|
||||
background-color: rgba(255, 224, 130, 0.15);
|
||||
|
||||
.author {
|
||||
color: #ffd54f;
|
||||
}
|
||||
|
||||
.type-icon {
|
||||
background-color: rgba(255, 200, 50, 0.25);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Sandbox;
|
||||
|
||||
public static class ChatHistory
|
||||
{
|
||||
public static List<Chat.Entry> Entries { get; private set; } = new();
|
||||
|
||||
public static void Add(ulong steamid, string author, string message)
|
||||
{
|
||||
Entries.Add(new Chat.Entry(steamid, author, message, 0.0f));
|
||||
}
|
||||
}
|
||||
@@ -13,20 +13,18 @@ public sealed class TeleportMazeButton : InteractionButton
|
||||
public override bool Press( IPressable.Event e )
|
||||
{
|
||||
base.Press( e );
|
||||
if ( Maze.IsValid() )
|
||||
{
|
||||
Maze.RpcRequestMaze();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Log.Info( "pressed teleport maze" );
|
||||
// return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
// if ( Maze.IsValid() )
|
||||
// {
|
||||
// Maze.RpcRequestMaze();
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// Log.Info( "pressed teleport maze" );
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// DoTeleport();
|
||||
// return true;
|
||||
DoTeleport();
|
||||
return true;
|
||||
}
|
||||
|
||||
private async void DoTeleport()
|
||||
|
||||
59
Code/WorlModelClothSpawner.cs
Normal file
59
Code/WorlModelClothSpawner.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using Sasalka;
|
||||
|
||||
public sealed class WorlModelClothSpawner : Component
|
||||
{
|
||||
[Property] public GameObject Prefab { get; set; }
|
||||
|
||||
[Property] public Vector3 CenterPosition { get; set; } = Vector3.Zero;
|
||||
[Property] public float Spacing { get; set; } = 50.0f;
|
||||
[Property] public float Height { get; set; } = 0.0f;
|
||||
|
||||
protected override void OnStart()
|
||||
{
|
||||
var definitions = ResourceLibrary.GetAll<ClothingItemDefinition>();
|
||||
var clothingItemDefinitions = definitions.ToList();
|
||||
int total = clothingItemDefinitions.Count();
|
||||
|
||||
if ( total == 0 )
|
||||
{
|
||||
Log.Warning( "No clothing definitions found." );
|
||||
return;
|
||||
}
|
||||
|
||||
// Автоматически вычисляем минимальный радиус круга, чтобы вместить все предметы
|
||||
// Площадь круга: πr², а площадь одного предмета: Spacing²
|
||||
// Нам нужно чтобы πr² >= total * Spacing² → r >= sqrt(total * Spacing² / π)
|
||||
float estimatedRadius = MathF.Sqrt( (total * Spacing * Spacing) / MathF.PI );
|
||||
float radius = estimatedRadius;
|
||||
|
||||
Log.Info( $"Auto-calculated radius: {radius}" );
|
||||
|
||||
int defIndex = 0;
|
||||
|
||||
for ( float x = -radius; x <= radius; x += Spacing )
|
||||
{
|
||||
for ( float y = -radius; y <= radius; y += Spacing )
|
||||
{
|
||||
if ( defIndex >= total )
|
||||
break;
|
||||
|
||||
if ( x * x + y * y <= radius * radius )
|
||||
{
|
||||
Vector3 pos = CenterPosition + new Vector3( x, y, Height );
|
||||
|
||||
var gO = Prefab.Clone( pos );
|
||||
gO.NetworkSpawn( null );
|
||||
|
||||
var item = gO.Components.Get<InventoryItem>();
|
||||
if ( item is not null )
|
||||
item.Definition = clothingItemDefinitions[defIndex];
|
||||
|
||||
defIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
if ( defIndex >= total )
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user