This commit is contained in:
Oscar
2025-06-08 23:47:32 +03:00
parent 160429eae6
commit 38754d2b59
45 changed files with 9857 additions and 0 deletions

321
Code/AI/Enemy.cs Normal file
View File

@@ -0,0 +1,321 @@
using Sandbox;
using Sandbox.Citizen;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
public sealed partial class Enemy : Component
{
public enum EnemyState
{
Idle,
Chase,
Attack,
Patrol,
Death
}
[Property, Sync] public int Health { get; set; } = 100;
[Property] public float MaxSpeed { get; set; } = 400;
[Property] public NavMeshAgent agent;
[Property] public SkinnedModelRenderer renderer;
[Property] public CitizenAnimationHelper AnimationHelper;
[Property, Sync] public EnemyState CurrentState { get; set; } = EnemyState.Idle;
[Property] public float DetectionRadius = 1000f;
[Property] public float AttackRadius = 80f;
[Property] public float AttackCooldown = 1f;
[Property] public RagdollController RagdollController;
[Property] public SoundPointComponent HitSound { get; set; }
[Property] public GameObject bloodParticle { get; set; }
private TimeSince _timeSinceLastAttack;
private Dedugan _currentTarget;
private TimeSince _lastTargetCheck;
private const float TargetCheckInterval = 1.5f;
private List<Dedugan> _potentialTargets = new();
private TimeSince _lastSearchRefresh;
private TimeSince _stateEnterTime;
public event Action<EnemyState, EnemyState> OnStateChanged;
protected override void OnStart()
{
renderer.Set( "scale_height", 2f );
_timeSinceLastAttack = 0;
_lastTargetCheck = 0;
RefreshPotentialTargets();
SwitchState( EnemyState.Idle );
// if ( !Network.IsOwner ) return;
if ( renderer is null )
return;
renderer.OnFootstepEvent += OnFootstepEvent;
}
protected override void OnUpdate()
{
// Всегда проверяем ближайшую цель
if ( _lastTargetCheck > TargetCheckInterval && CurrentState != EnemyState.Death )
{
FindClosestTarget();
_lastTargetCheck = 0;
}
switch ( CurrentState )
{
case EnemyState.Idle:
UpdateIdleState();
break;
case EnemyState.Chase:
UpdateChaseState();
break;
case EnemyState.Attack:
UpdateAttackState();
break;
case EnemyState.Patrol:
UpdatePatrolState();
break;
case EnemyState.Death:
UpdateDeathState();
break;
}
}
private void SwitchState( EnemyState newState )
{
if ( CurrentState == newState ) return;
if ( CurrentState == EnemyState.Death ) return;
var previousState = CurrentState;
CurrentState = newState;
_stateEnterTime = 0;
OnStateChanged?.Invoke( previousState, newState );
switch ( newState )
{
case EnemyState.Idle:
agent.Stop();
AnimationHelper.HoldType = CitizenAnimationHelper.HoldTypes.None;
AnimationHelper.WithVelocity( Vector3.Zero );
renderer.Set( "scale_height", 2f );
break;
case EnemyState.Chase:
agent.MaxSpeed = MaxSpeed;
AnimationHelper.HoldType = CitizenAnimationHelper.HoldTypes.Punch;
break;
case EnemyState.Patrol:
agent.MaxSpeed = MaxSpeed / 4;
break;
case EnemyState.Death:
EnterDeathState();
break;
}
}
private void RefreshPotentialTargets()
{
_potentialTargets.Clear();
foreach ( var player in Scene.Components.GetAll<Dedugan>() )
{
if ( player.IsValid() && player.Health > 0 )
{
_potentialTargets.Add( player );
}
}
_lastSearchRefresh = 0;
}
private void FindClosestTarget()
{
// Обновляем список целей при необходимости
if ( _lastSearchRefresh > 1f ) RefreshPotentialTargets();
Dedugan closestTarget = null;
float closestDistance = float.MaxValue;
foreach ( var target in _potentialTargets )
{
if ( !target.IsValid() || target.Health <= 0 ) continue;
float distance = Vector3.DistanceBetween( WorldPosition, target.WorldPosition );
if ( distance <= DetectionRadius && distance < closestDistance )
{
closestDistance = distance;
closestTarget = target;
}
else
{
_currentTarget = null;
}
}
// Если нашли более близкую цель, переключаемся на нее
if ( closestTarget != null && closestTarget != _currentTarget )
{
_currentTarget = closestTarget;
// Если мы в состоянии атаки/погони и цель сменилась, перезапускаем движение
if ( CurrentState == EnemyState.Chase || CurrentState == EnemyState.Attack )
{
agent.MoveTo( _currentTarget.WorldPosition );
}
}
}
private bool HasValidTarget()
{
return _currentTarget != null &&
_currentTarget.IsValid() &&
_currentTarget.Health > 0;
}
private void UpdateIdleState()
{
if ( HasValidTarget() )
{
Log.Info(_currentTarget);
SwitchState( EnemyState.Chase );
return;
}
if ( new Random().Next( 0, 100 ) > 90 )
{
SwitchState( EnemyState.Patrol );
}
SwitchState( EnemyState.Patrol );
AnimationHelper.WithVelocity( Vector3.Zero );
}
private void UpdatePatrolState()
{
if ( HasValidTarget() )
{
SwitchState( EnemyState.Chase );
return;
}
if ( agent.Velocity.Length <= 10 )
{
agent.MoveTo( Scene.NavMesh.GetRandomPoint() ?? WorldPosition );
}
AnimationHelper.WithVelocity( agent.Velocity );
}
private void UpdateChaseState()
{
if ( !HasValidTarget() )
{
SwitchState( EnemyState.Idle );
return;
}
float distance = Vector3.DistanceBetween( _currentTarget.WorldPosition, WorldPosition );
if ( distance <= AttackRadius )
{
SwitchState( EnemyState.Attack );
return;
}
if ( distance > DetectionRadius )
{
SwitchState( EnemyState.Idle );
return;
}
agent.MoveTo( _currentTarget.WorldPosition );
AnimationHelper.WithVelocity( agent.Velocity );
}
private void UpdateAttackState()
{
if ( !HasValidTarget() )
{
SwitchState( EnemyState.Idle );
return;
}
float distance = Vector3.DistanceBetween( _currentTarget.WorldPosition, WorldPosition );
if ( distance > AttackRadius )
{
SwitchState( EnemyState.Chase );
return;
}
agent.Stop();
AnimationHelper.WithVelocity( Vector3.Zero );
if ( _timeSinceLastAttack >= AttackCooldown )
{
var dir = _currentTarget.WorldPosition - WorldPosition;
renderer.Set( "b_attack", true );
_timeSinceLastAttack = 0;
_currentTarget.ReportHit( dir, 1, 15 );
CreateHitEffects( _currentTarget.WorldPosition + _currentTarget.WorldRotation.Up * 60, dir );
}
}
private void EnterDeathState()
{
agent.Stop();
AnimationHelper.WithVelocity( Vector3.Zero );
if ( RagdollController != null ) RagdollController.Enabled = true;
}
private void UpdateDeathState()
{
// Логика состояния смерти
}
public void Die()
{
SwitchState( EnemyState.Death );
DestroyAsync( GameObject, 5f );
}
[Rpc.Broadcast]
public void ReportHit( Vector3 dir, int boneIndex, int damage = 25 )
{
renderer.Set( "hit", true );
renderer.Set( "hit_bone", boneIndex );
renderer.Set( "hit_direction", dir );
renderer.Set( "hit_offset", dir );
renderer.Set( "hit_strength", 5 );
Health -= damage;
if ( Health <= 0 )
{
Die();
}
}
[Rpc.Broadcast]
private void CreateHitEffects( Vector3 position, Vector3 normal )
{
if ( HitSound != null ) HitSound.StartSound();
if ( bloodParticle != null )
{
var rot = Rotation.LookAt( normal );
DestroyAsync( bloodParticle.Clone( position, rot ), 0.5f );
}
}
async void DestroyAsync( GameObject go, float delay )
{
await GameTask.DelaySeconds( delay );
if ( go != null && go.IsValid() ) go.Destroy();
}
}

551
Code/Maze/Maze.cs Normal file
View File

@@ -0,0 +1,551 @@
public sealed class Maze : Component
{
[Property, Sync] public int Width { get; set; } = 10;
[Property, Sync] public int Height { get; set; } = 10;
[Property, Sync] public float WallHeight { get; set; } = 5f;
[Property, Sync] public float WallThickness { get; set; } = 5f;
[Property, Sync] public float CellSize { get; set; } = 200f;
[Property, Sync] public float PassThrougthPercent { get; set; } = 0.1f;
[Property] public int CenterClearRadius { get; set; } = 1;
[Property] public GameObject EnemyPrefab { get; set; }
private GameObject _mazeWallsObject;
public Cell[,] Cells;
private List<GameObject> _walls = new();
[Property] public GameObject Floor { get; set; }
[Property] public bool IsReady { get; set; } = false;
[Property] public Material WallMaterial { get; set; } // Добавлено свойство для материала
private static readonly string WallModelPath = "models/dev/box.vmdl_c";
private Vector3 mazeOffset =>
new Vector3( -(Width * CellSize) / 2 + CellSize / 2, -(Height * CellSize) / 2 + CellSize / 2, 0 );
[Property, Sync( SyncFlags.FromHost ), Change( "OnSeedChanged" )]
public int MazeSeed { get; set; } = -1;
void OnSeedChanged( int oldValue, int newValue )
{
if ( newValue != oldValue )
{
IsReady = false;
CreateMaze();
}
}
[Rpc.Broadcast]
public void RpcRequestMaze()
{
if ( Networking.IsHost )
{
MazeSeed = Game.Random.Int( 0, 999999999 );
}
}
public async void CreateMaze()
{
Log.Info( $"CREATE MAZE {MazeSeed}" );
ScaleFloor();
ClearMaze();
GenerateMaze();
await Task.Delay( 10 );
BuildMazeOptimized();
await Task.Delay( 10 );
SpawnEnemy();
IsReady = true;
}
private void RemoveCenterWalls()
{
if ( Cells == null )
{
Log.Warning( "RemoveCenterWalls: _cells is null" );
return;
}
int centerX = Width / 2;
int centerY = Height / 2;
int startX = Math.Max( 0, centerX - CenterClearRadius );
int endX = Math.Min( Width - 1, centerX + CenterClearRadius );
int startY = Math.Max( 0, centerY - CenterClearRadius );
int endY = Math.Min( Height - 1, centerY + CenterClearRadius );
for ( int x = startX; x <= endX; x++ )
{
for ( int y = startY; y <= endY; y++ )
{
if ( Cells[x, y] == null ) continue;
var cell = Cells[x, y];
for ( int i = 0; i < 4; i++ )
{
if ( i >= 0 && i < cell.Walls.Length )
cell.Walls[i] = false;
}
// Сброс стен у соседей
if ( x > 0 && Cells[x - 1, y] != null )
Cells[x - 1, y].Walls[1] = false; // Left neighbor's Right
if ( x < Width - 1 && Cells[x + 1, y] != null )
Cells[x + 1, y].Walls[3] = false; // Right neighbor's Left
if ( y > 0 && Cells[x, y - 1] != null )
Cells[x, y - 1].Walls[2] = false; // Bottom neighbor's Top
if ( y < Height - 1 && Cells[x, y + 1] != null )
Cells[x, y + 1].Walls[0] = false; // Top neighbor's Bottom
}
}
}
public Vector3 GetRandomCellPosition()
{
var randomCell = Cells[Game.Random.Int( 0, Width - 1 ), Game.Random.Int( 0, Height - 1 )];
return WorldPosition + new Vector3(
randomCell.X * CellSize,
randomCell.Y * CellSize,
0
) + mazeOffset;
}
private void SpawnEnemy()
{
if ( !Networking.IsHost ) return;
var enemy = EnemyPrefab.Clone();
enemy.WorldPosition = GetRandomCellPosition();
enemy.NetworkSpawn( null );
var agent = enemy.Components.Get<NavMeshAgent>();
if ( agent != null )
{
agent.Enabled = false;
agent.WorldPosition = enemy.WorldPosition;
agent.Enabled = true;
}
}
private void ClearMaze()
{
// Удаляем оптимизированные стены
if ( _mazeWallsObject.IsValid() )
{
_mazeWallsObject.Destroy();
_mazeWallsObject = null;
}
// Удаляем старые стены (для обратной совместимости)
foreach ( var wall in _walls )
{
if ( wall.IsValid ) wall.Destroy();
}
_walls.Clear();
}
private void GenerateMaze()
{
Cells = new Cell[Width, Height];
for ( int x = 0; x < Width; x++ )
{
for ( int y = 0; y < Height; y++ )
{
Cells[x, y] = new Cell( x, y );
}
}
var stack = new Stack<Cell>();
var rng = new Random( MazeSeed );
var startCell = Cells[0, 0];
startCell.Visited = true;
stack.Push( startCell );
while ( stack.Count > 0 )
{
var current = stack.Peek();
var neighbors = GetUnvisitedNeighbors( current );
if ( neighbors.Count > 0 )
{
var next = neighbors[rng.Next( neighbors.Count )];
RemoveWall( current, next );
next.Visited = true;
stack.Push( next );
}
else
{
stack.Pop();
}
}
AddExtraPassages( count: (int)(Width * Height * PassThrougthPercent) ); // 10% от клеток
RemoveCenterWalls();
}
private List<Cell> GetUnvisitedNeighbors( Cell cell )
{
var neighbors = new List<Cell>();
int x = cell.X;
int y = cell.Y;
if ( x > 0 && !Cells[x - 1, y].Visited ) neighbors.Add( Cells[x - 1, y] );
if ( x < Width - 1 && !Cells[x + 1, y].Visited ) neighbors.Add( Cells[x + 1, y] );
if ( y > 0 && !Cells[x, y - 1].Visited ) neighbors.Add( Cells[x, y - 1] );
if ( y < Height - 1 && !Cells[x, y + 1].Visited ) neighbors.Add( Cells[x, y + 1] );
return neighbors;
}
private void RemoveWall( Cell current, Cell next )
{
int dx = next.X - current.X;
int dy = next.Y - current.Y;
if ( dx == 1 )
{
current.Walls[1] = false; // Right
next.Walls[3] = false; // Left
}
else if ( dx == -1 )
{
current.Walls[3] = false; // Left
next.Walls[1] = false; // Right
}
else if ( dy == 1 )
{
current.Walls[2] = false; // Top
next.Walls[0] = false; // Bottom
}
else if ( dy == -1 )
{
current.Walls[0] = false; // Bottom
next.Walls[2] = false; // Top
}
}
private void BuildMaze()
{
for ( int x = 0; x < Width; x++ )
{
for ( int y = 0; y < Height; y++ )
{
var cell = Cells[x, y];
Vector3 cellCenter = new Vector3( x * CellSize, y * CellSize, 0 );
if ( cell.Walls[0] ) // Bottom
{
Vector3 pos = cellCenter + new Vector3( 0, -CellSize / 2, 0 );
SpawnWall( pos, Rotation.FromYaw( 0 ) );
}
if ( cell.Walls[1] ) // Right
{
Vector3 pos = cellCenter + new Vector3( CellSize / 2, 0, 0 );
SpawnWall( pos, Rotation.FromYaw( 90 ) );
}
if ( cell.Walls[2] ) // Top
{
Vector3 pos = cellCenter + new Vector3( 0, CellSize / 2, 0 );
SpawnWall( pos, Rotation.FromYaw( 0 ) );
}
if ( cell.Walls[3] ) // Left
{
Vector3 pos = cellCenter + new Vector3( -CellSize / 2, 0, 0 );
SpawnWall( pos, Rotation.FromYaw( 90 ) );
}
}
}
}
private void AddBox( ModelBuilder builder, Matrix transform, Vector3 size )
{
Vector3 halfSize = size / 2;
Vector3[] localVerts = new Vector3[]
{
new(-halfSize.x, -halfSize.y, -halfSize.z), // 0
new(halfSize.x, -halfSize.y, -halfSize.z), // 1
new(halfSize.x, halfSize.y, -halfSize.z), // 2
new(-halfSize.x, halfSize.y, -halfSize.z), // 3
new(-halfSize.x, -halfSize.y, halfSize.z), // 4
new(halfSize.x, -halfSize.y, halfSize.z), // 5
new(halfSize.x, halfSize.y, halfSize.z), // 6
new(-halfSize.x, halfSize.y, halfSize.z) // 7
};
int[] indices = new int[]
{
0, 2, 1, 0, 3, 2, // bottom (задом наперёд)
4, 5, 6, 4, 6, 7, // top
0, 5, 4, 0, 1, 5, // front
1, 6, 5, 1, 2, 6, // right
2, 7, 6, 2, 3, 7, // back
3, 4, 7, 3, 0, 4 // left
};
Vector3[] faceNormals = new Vector3[]
{
Vector3.Down, // bottom
Vector3.Up, // top
Vector3.Backward, // front
Vector3.Right, // right
Vector3.Forward, // back
Vector3.Left // left
};
var vertices = new List<Vertex>();
var finalIndices = new List<int>();
for ( int i = 0; i < indices.Length; i += 6 )
{
Vector3 localNormal = faceNormals[i / 6];
Vector3 worldNormal = transform.TransformNormal( localNormal );
for ( int j = 0; j < 6; j++ )
{
int vertexIndex = indices[i + j];
Vector3 localPos = localVerts[vertexIndex];
Vector3 worldPos = transform.Transform( localPos );
// Vector4 texCoord = new Vector4( 0, 0, 0, 0 ); // упрощённые UV
Vector4 texCoord = new Vector4( GetUVForVertex( j ).x, GetUVForVertex( j ).y, 0, 0 );
Vertex vertex = new Vertex(
position: worldPos,
normal: worldNormal,
tangent: Vector3.Right,
texCoord0: texCoord
);
vertex.Color = Color32.White;
vertex.TexCoord1 = new Vector4( 0, 0, 0, 0 );
vertices.Add( vertex );
finalIndices.Add( vertices.Count - 1 );
}
}
if ( WallMaterial == null )
{
Log.Error( "WallMaterial not set!" );
return;
}
var mesh = new Mesh( WallMaterial, MeshPrimitiveType.Triangles );
mesh.CreateVertexBuffer<Vertex>( vertices.Count, Vertex.Layout, vertices );
mesh.CreateIndexBuffer( finalIndices.Count, finalIndices );
builder.AddMesh( mesh ).AddCollisionMesh( vertices.Select( x => x.Position ).ToList(), indices.ToList() );
}
private Vector2 GetUVForVertex( int index )
{
return (index % 4) switch
{
0 => new Vector2( 0, 0 ),
1 => new Vector2( 1, 0 ),
2 => new Vector2( 1, 1 ),
3 => new Vector2( 0, 1 ),
_ => Vector2.Zero
};
}
/// <summary>
/// Создание матрицы трансформации
/// </summary>
private Matrix CreateTransform( Vector3 position, Rotation rotation, Vector3 scale )
{
return Matrix.CreateScale( scale )
* Matrix.CreateRotation( rotation )
* Matrix.CreateTranslation( position );
}
/// <summary>
/// Оптимизированное построение стен лабиринта (единый меш)
/// </summary>
private void BuildMazeOptimized()
{
var builder = Model.Builder;
// Создаем коллектор вершин для коллайдера
var collisionVertices = new List<Vector3>();
for ( int x = 0; x < Width; x++ )
{
for ( int y = 0; y < Height; y++ )
{
var cell = Cells[x, y];
Vector3 cellCenter =
new Vector3( x * CellSize, y * CellSize, 0 ) +
mazeOffset; // + new Vector3( 0, 0, WallHeight * 24);
void AddWall( Vector3 position, Rotation rotation, Vector3 size )
{
var transform = Matrix.CreateScale( size )
* Matrix.CreateRotation( rotation )
* Matrix.CreateTranslation( position );
// Добавляем меш к модели (используй свой AddBox)
AddBox( builder, transform, new Vector3( 1, 1, WallHeight * 24 ) );
// Добавляем коллизию — формируем коллизионный hull из углов куба
// Куб - 8 вершин (как в твоём AddBox), трансформируем и добавляем
Vector3 half = new Vector3( 0.5f, 0.5f, 0.5f );
Vector3[] localVerts = new Vector3[]
{
new Vector3( -half.x, -half.y, -half.z ), new Vector3( half.x, -half.y, -half.z ),
new Vector3( half.x, half.y, -half.z ), new Vector3( -half.x, half.y, -half.z ),
new Vector3( -half.x, -half.y, half.z ), new Vector3( half.x, -half.y, half.z ),
new Vector3( half.x, half.y, half.z ), new Vector3( -half.x, half.y, half.z ),
};
foreach ( var v in localVerts )
collisionVertices.Add( transform.Transform( v ) );
}
if ( cell.Walls[0] )
AddWall( cellCenter + new Vector3( 0, -CellSize / 2, 0 ), Rotation.Identity,
new Vector3( CellSize, WallThickness, WallHeight ) );
if ( cell.Walls[1] )
AddWall( cellCenter + new Vector3( CellSize / 2, 0, 0 ), Rotation.FromYaw( 90 ),
new Vector3( CellSize, WallThickness, WallHeight ) );
if ( cell.Walls[2] )
AddWall( cellCenter + new Vector3( 0, CellSize / 2, 0 ), Rotation.Identity,
new Vector3( CellSize, WallThickness, WallHeight ) );
if ( cell.Walls[3] )
AddWall( cellCenter + new Vector3( -CellSize / 2, 0, 0 ), Rotation.FromYaw( 90 ),
new Vector3( CellSize, WallThickness, WallHeight ) );
}
}
// Создаем модель с коллизией из всех добавленных hull вершин
var model = builder
.WithMass( 0 ) // Статическая модель
.Create();
if ( _mazeWallsObject.IsValid() )
_mazeWallsObject.Destroy();
_mazeWallsObject = new GameObject();
_mazeWallsObject.Name = "Maze Walls";
_mazeWallsObject.SetParent( GameObject );
_mazeWallsObject.LocalPosition = new Vector3( 0, 0, WallHeight * 24 );
var renderer = _mazeWallsObject.Components.Create<ModelRenderer>();
renderer.Model = model;
renderer.RenderType = ModelRenderer.ShadowRenderType.Off;
if ( WallMaterial.IsValid() )
renderer.MaterialOverride = WallMaterial;
var collider = _mazeWallsObject.Components.Create<ModelCollider>();
collider.Static = true;
// var navMesh = _mazeWallsObject.Components.Create<NavMeshArea>();
// navMesh.LinkedCollider = collider;
// navMesh.IsBlocker = true;
// _mazeWallsObject.NetworkSpawn( null );
Scene.NavMesh.SetDirty();
}
private void ScaleFloor()
{
Floor.LocalPosition = new Vector3( 0, 0, -WallHeight );
Floor.LocalScale = new Vector3( CellSize / 100 * Width, CellSize / 100 * Height, 0.5f );
var renderer = Floor.Components.Get<ModelRenderer>();
var material = renderer.MaterialOverride;
material?.Set( "g_vTexCoordScale", new Vector2( Width, Height ) );
Floor.NetworkSpawn( null );
}
private void SpawnWall( Vector3 position, Rotation rotation )
{
var wall = new GameObject();
wall.SetParent( GameObject );
wall.LocalPosition = position + mazeOffset + new Vector3( 0, 0, WallHeight * 24 );
wall.WorldRotation = rotation;
wall.LocalScale = new Vector3( CellSize / 50, WallThickness / 10, WallHeight );
var collider = wall.Components.Create<BoxCollider>();
var renderer = wall.Components.Create<ModelRenderer>();
var navMeshArea = wall.Components.Create<NavMeshArea>();
navMeshArea.IsBlocker = true;
navMeshArea.LinkedCollider = collider;
renderer.Model = Model.Load( WallModelPath );
collider.Static = true;
wall.NetworkSpawn( null );
_walls.Add( wall );
}
private void AddExtraPassages( int count )
{
var rng = new Random( MazeSeed );
int added = 0;
while ( added < count )
{
int x = rng.Next( 1, Width - 1 );
int y = rng.Next( 1, Height - 1 );
var cell = Cells[x, y];
// Выбери случайного соседа
List<(int dx, int dy, int dir)> directions = new()
{
(0, -1, 0), // Bottom
(1, 0, 1), // Right
(0, 1, 2), // Top
(-1, 0, 3) // Left
};
var (dx, dy, dir) = directions[rng.Next( directions.Count )];
int nx = x + dx;
int ny = y + dy;
if ( nx < 0 || nx >= Width || ny < 0 || ny >= Height )
continue;
var neighbor = Cells[nx, ny];
if ( cell.Walls[dir] )
{
RemoveWall( cell, neighbor );
added++;
}
}
}
public class Cell
{
public int X;
public int Y;
public bool Visited;
public bool[] Walls = { true, true, true, true }; // Bottom, Right, Top, Left
public Cell( int x, int y )
{
X = x;
Y = y;
}
}
}

113
Code/NetworkManager.cs Normal file
View File

@@ -0,0 +1,113 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Sandbox;
/// <summary>
/// Creates a networked game lobby and assigns player prefabs to connected clients.
/// </summary>
[Title( "Network Manager" )]
[Category( "Networking" )]
[Icon( "electrical_services" )]
public sealed class NetworkManager : Component, Component.INetworkListener
{
/// <summary>
/// Create a server (if we're not joining one)
/// </summary>
[Property]
public bool StartServer { get; set; } = true;
/// <summary>
/// The prefab to spawn for the player to control.
/// </summary>
[Property]
public GameObject PlayerPrefab { get; set; }
/// <summary>
/// A list of points to choose from randomly to spawn the player in. If not set, we'll spawn at the
/// location of the NetworkHelper object.
/// </summary>
[Property]
public List<GameObject> SpawnPoints { get; set; }
protected override async Task OnLoad()
{
if ( Scene.IsEditor )
return;
if ( StartServer && !Networking.IsActive )
{
LoadingScreen.Title = "Creating Lobby";
await Task.DelayRealtimeSeconds( 0.1f );
Networking.CreateLobby( new() );
}
}
/// <summary>
/// A client is fully connected to the server. This is called on the host.
/// </summary>
public void OnActive( Connection channel )
{
Log.Info( $"Player '{channel.DisplayName}' has joined the game" );
if ( !PlayerPrefab.IsValid() )
return;
//
// Find a spawn location for this player
//
var startLocation = FindSpawnLocation().WithScale( 1 );
// Spawn this object and make the client the owner
var player = PlayerPrefab.Clone( startLocation, name: $"Player - {channel.DisplayName}" );
player.NetworkSpawn( channel );
var dedugan = player.Components.Get<Dedugan>( true );
dedugan.SetupConnection( channel );
}
/// <summary>
/// Called when a client disconnects from the server.
/// </summary>
public void OnDisconnected( Connection channel )
{
}
/// <summary>
/// Called when the connection is being torn down.
/// </summary>
public void OnBecomeInactive( Connection channel )
{
// Optional: Handle any cleanup before player becomes fully inactive
}
/// <summary>
/// Find the most appropriate place to respawn
/// </summary>
Transform FindSpawnLocation()
{
//
// If they have spawn point set then use those
//
if ( SpawnPoints is not null && SpawnPoints.Count > 0 )
{
return Random.Shared.FromList( SpawnPoints, default ).WorldTransform;
}
//
// If we have any SpawnPoint components in the scene, then use those
//
var spawnPoints = Scene.GetAllComponents<SpawnPoint>().ToArray();
if ( spawnPoints.Length > 0 )
{
return Random.Shared.FromArray( spawnPoints ).WorldTransform;
}
//
// Failing that, spawn where we are
//
return WorldTransform;
}
}

138
Code/UI/Chat.razor Normal file
View File

@@ -0,0 +1,138 @@
@using System;
@using Sandbox.UI;
@namespace Sandbox
@inherits PanelComponent
@implements Component.INetworkListener
<root>
<div class="output">
@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>
}
</div>
<div class="input">
<TextEntry @ref="InputBox" onsubmit="@ChatFinished"></TextEntry>
</div>
</root>
@code
{
public static Chat Instance;
public Chat() => Instance = this;
public static bool IsActive => Instance.InputBox.HasFocus;
public static void Open()
{
Instance.InputBox.Focus();
}
public static void AddText(string text)
{
Instance.AddTextInternal(text);
}
public event Action<string> OnChat;
public TextEntry InputBox;
public record Entry(ulong steamid, string author, string message, RealTimeSince timeSinceAdded);
List<Entry> Entries = new();
protected override void OnUpdate()
{
if (InputBox is null)
return;
Panel.AcceptsFocus = false;
if (Input.Pressed("chat"))
Open();
if (Entries.RemoveAll(x => x.timeSinceAdded > 20.0f) > 0)
{
StateHasChanged();
}
if (InputBox.HasFocus && Input.EscapePressed)
{
Input.EscapePressed = false;
ChatClosed();
}
SetClass("open", InputBox.HasFocus);
}
void ChatFinished()
{
var text = InputBox.Text;
Mouse.Visibility = MouseVisibility.Auto;
OnChat?.Invoke(text);
OnChat = null;
if (string.IsNullOrWhiteSpace(text))
return;
AddTextInternal(InputBox.Text);
InputBox.Text = "";
}
void ChatClosed()
{
var text = InputBox.Text;
InputBox.Text = "";
OnChat = null;
}
[Rpc.Broadcast]
public void AddTextInternal(string message)
{
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();
}
[Rpc.Broadcast]
void AddSystemText(string message)
{
message = message.Truncate(300);
if (string.IsNullOrWhiteSpace(message))
return;
Entries.Add(new Entry(0, "", message, 0.0f));
StateHasChanged();
}
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");
}
}

70
Code/UI/Chat.razor.scss Normal file
View File

@@ -0,0 +1,70 @@
Chat {
position: absolute;
top: 100px;
left: 200px;
bottom: 200px;
width: 600px;
justify-content: center;
align-items: center;
font-weight: bold;
border-radius: 20px;
flex-direction: column;
align-items: stretch;
font-size: 17px;
font-family: Poppins;
gap: 10px;
.output {
flex-grow: 1;
flex-direction: column;
justify-content: flex-end;
align-items: stretch;
gap: 5px;
.chat_entry {
padding: 2px;
gap: 10px;
text-shadow: 2px 2px 2px #000a;
.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;
}
}
}
.input {
color: white;
.textentry {
align-items: flex-start;
white-space: normal;
}
}
&.open {
.input {
border-radius: 8px;
background-color: rgba(0,0,0,0.2);
backdrop-filter: blur(10px);
padding: 8px;
pointer-events: all;
}
}
}

25
Code/UI/GUI.razor Normal file
View File

@@ -0,0 +1,25 @@
@using Sandbox
@using Sandbox.UI
@inherits PanelComponent
@namespace Sandbox
<root>
<div class="crosshair"></div>
</root>
<style>
.crosshair {
position: absolute;
top: 50%;
left: 50%;
width: 4px;
height: 4px;
background-color: rgba(255, 255, 255, 0.5);
border-radius: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
}
</style>
@code {
}

95
Code/UI/Scoreboard.razor Normal file
View File

@@ -0,0 +1,95 @@
@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent
<root class="@(Visible ? "" : "hidden")">
@* <div class="decoration top-left"></div> *@
@* <div class="decoration top-right"></div> *@
@* <div class="decoration bottom-left"></div> *@
@* <div class="decoration bottom-right"></div> *@
<label class="title">Players</label>
<div class="content">
<div class="header">
<label class="column nick">Player</label>
<label class="column status">Status</label>
<label class="column ping">Ping</label>
</div>
<div class="player-list">
@if (Dedugan.All is not null)
{
foreach (var ded in Dedugan.All)
{
<div class="player" onclick="@(() => OpenProfile(ded.Connection))">
<div class="avatar">
<img src="avatar:@ded.SteamID"/>
</div>
<label class="column nick">@ded.Connection.DisplayName</label>
<label class="column status">@GetPlayerStatus(ded.Connection)</label>
<label class="column ping">@ded.Connection.Ping</label>
</div>
}
}
</div>
</div>
</root>
@code
{
public static bool Visible => Input.Down("Score");
private NetworkManager _networkManager;
private NetworkManager NetworkManager
{
get
{
if (_networkManager == null || !_networkManager.IsValid)
{
_networkManager = Scene.Directory.FindByName("Network Manager")
.FirstOrDefault()?
.GetComponent<NetworkManager>();
}
return _networkManager;
}
}
protected override void OnEnabled()
{
// Кэшируем NetworkManager при включении компонента
_networkManager = Scene.Directory.FindByName("Network Manager")
.FirstOrDefault()?
.GetComponent<NetworkManager>();
}
private string GetPlayerStatus(Connection conn)
{
var playerObj = Dedugan.GetByID( conn.Id ).GameObject;
return playerObj?.IsValid == true ? "In Game" : "Connecting";
}
private void OpenProfile(Connection connection)
{
Log.Info($"Opening profile: {connection.SteamId}");
Game.Overlay.ShowPlayer(connection.SteamId);
}
protected override int BuildHash()
{
if (!Visible || Dedugan.All == null)
return -1;
var hash = new System.HashCode();
hash.Add(Visible);
foreach (var ded in Dedugan.All)
{
hash.Add(ded.Id);
hash.Add(ded.Connection.Ping);
hash.Add(ded.Name);
}
return hash.ToHashCode();
}
}

View File

@@ -0,0 +1,191 @@
Scoreboard {
background: linear-gradient(135deg, #0a1a2b 0%, #08111f 100%);
border: 3px solid #2a3d54;
border-radius: 16px;
//box-shadow:
// 0 0 15px rgba(0, 150, 255, 0.2),
// inset 0 0 10px rgba(0, 100, 200, 0.1);
font-family: 'Orbitron', 'Poppins', sans-serif;
position: absolute;
width: 60%;
height: 70vh;
top: 15vh;
left: 20%;
padding: 30px;
display: flex;
flex-direction: column;
gap: 20px;
transition: all 0.2s ease;
z-index: 100;
overflow: hidden;
&.hidden {
opacity: 0;
}
.decoration {
position: absolute;
width: 60px;
height: 60px;
border: 2px solid rgba(0, 180, 255, 0.25);
opacity: 0.8;
box-shadow: 0 0 8px rgba(100, 200, 255, 0.1);
&.top-left {
top: 10px;
left: 10px;
border-right: none;
border-bottom: none;
border-radius: 12px 0 0 0;
}
&.top-right {
top: 10px;
right: 10px;
border-left: none;
border-bottom: none;
border-radius: 0 12px 0 0;
}
&.bottom-left {
bottom: 10px;
left: 10px;
border-right: none;
border-top: none;
border-radius: 0 0 0 12px;
}
&.bottom-right {
bottom: 10px;
right: 10px;
border-left: none;
border-top: none;
border-radius: 0 0 12px 0;
}
}
.title {
font-size: 42px;
color: #a0e0ff;
text-align: center;
//text-shadow:
// 0 0 10px rgba(100, 200, 255, 0.7),
// 0 0 20px rgba(80, 180, 255, 0.4);
letter-spacing: 4px;
margin-bottom: 15px;
font-weight: 600;
text-transform: uppercase;
}
.content {
display: flex;
flex-direction: column;
background-color: rgba(10, 25, 40, 0.6);
border-radius: 12px;
border: 1px solid #253a50;
overflow: hidden;
flex-grow: 1;
//box-shadow: inset 0 0 20px rgba(0, 30, 60, 0.5);
.header {
display: flex;
padding: 15px 20px 15px 85px;
background: linear-gradient(90deg, #0f2a42 0%, #0a1d30 100%);
border-bottom: 2px solid #1e3a5c;
text-shadow: 0 0 5px rgba(100, 200, 255, 0.5);
.column {
font-size: 18px;
color: #6eb4ff;
font-weight: 600;
letter-spacing: 1px;
&.nick {
flex: 3;
text-align: left;
}
&.status {
flex: 1;
text-align: center;
}
&.ping {
flex: 1;
text-align: right;
padding-right: 20px;
}
}
}
.player-list {
pointer-events: all;
flex-grow: 1;
height: 100%;
flex-direction: column;
overflow-x: hidden;
overflow-y: scroll;
.player {
flex-shrink: 0;
display: flex;
align-items: center;
padding: 15px 25px;
transition: all 0.2s ease;
width: 100%;
//background: rgba(15, 30, 50, 0.4);
border-bottom: 1px solid rgba(40, 80, 120, 0.2);
cursor: pointer;
&:hover {
//background: rgba(25, 60, 90, 0.4);
box-shadow: 0 0 15px rgba(0, 150, 255, 0.1);
}
.avatar {
width: 45px;
height: 45px;
border-radius: 4px;
overflow: hidden;
margin-right: 20px;
border: 2px solid #2a4a6b;
box-shadow: 0 0 8px rgba(100, 180, 255, 0.2);
background: #0c1a2a;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.column {
font-size: 17px;
color: #c0e8ff;
text-shadow: 0 0 5px rgba(100, 180, 255, 0.3);
&.nick {
flex: 3;
text-align: left;
font-weight: 500;
letter-spacing: 0.5px;
}
&.status {
flex: 1;
text-align: center;
color: #6ecbff;
}
&.ping {
flex: 1;
text-align: right;
padding-right: 20px;
font-family: 'Courier New', monospace;
color: #88d6ff;
}
}
}
}
}
}

View File

@@ -0,0 +1,60 @@
public sealed class PhysBullet : Component, Component.ICollisionListener
{
[Property] public Collider collider { get; set; }
[Property] public Rigidbody BRigidbody { get; set; }
protected override async void OnStart()
{
if ( collider != null )
{
collider.OnTriggerEnter += OnTriggerEnter;
}
await Task.DelaySeconds( 5 );
GameObject.Destroy();
}
protected override void OnUpdate()
{
}
async void ICollisionListener.OnCollisionStart( Collision collision )
{
await Task.DelaySeconds( 1 );
GameObject.Destroy();
}
// var a= Scene.Trace.Ray( collision.Contact.Point, collision.Contact.Point + collision.Contact.Normal * 10 ).Run();
// Log.Info(collision.Other.GameObject);
// Log.Info(a.GameObject);
// DebugOverlay.Line(a.StartPosition,a.EndPosition, duration: 5.5f);
// DestroyGameObject();
public void OnTriggerEnter( Collider other )
{
var components = other.Components;
if ( components.TryGet<RagdollController>( out var controller ) )
{
if ( Network.IsOwner )
return;
controller.Enabled = true;
// ModelHitboxes sas = controller.Components.Get<ModelHitboxes>();
// collider.FindClosestPoint(other.WorldPosition);
// var d = controller.GameObject.GetComponent<Dedugan>();
//
// d.Controller.Velocity = Vector3.Up * 2500;
// rb.MassOverride = 500f;
//
// rb.ApplyImpulse( Vector3.Up * 5000000 );
// rb.Destroy();
// controller.bodyPhysics.
}
}
}