using Sandbox.ModelEditor.Nodes; namespace Sandbox; /// /// A prop is defined by its model. The model can define its health and what happens when it breaks. /// This component is designed to be easy to use - since you only need to define the model. Although you can /// access the procedural (hidden) components, they aren't saved, so it's a waste of time. /// [Expose] [Title( "Prop" )] [Category( "Game" )] [Icon( "toys" )] public class Prop : Component, Component.ExecuteInEditor, Component.IDamageable { Model _model; ulong _bodyGroups = ulong.MaxValue; string _materialGroup = default; Color _tint = Color.White; bool _static = false; [Property] public Model Model { get => _model; set { if ( _model == value ) return; _model = value; if ( !GameObject.Flags.Contains( GameObjectFlags.Deserializing ) ) { _bodyGroups = ulong.MaxValue; _materialGroup = default; if ( _model is not null && _model.native.GetNumMeshGroups() > 0 ) { _bodyGroups = _model.native.GetDefaultMeshGroupMask(); } } OnModelChanged(); } } [Property, Model.BodyGroupMask, ShowIf( nameof( HasBodyGroups ), true )] public ulong BodyGroups { get => _bodyGroups; set { if ( _bodyGroups == value ) return; _bodyGroups = value; if ( ModelRenderer.IsValid() ) { ModelRenderer.BodyGroups = BodyGroups; } } } [Property, Model.MaterialGroup, ShowIf( nameof( HasMaterialGroups ), true )] public string MaterialGroup { get => _materialGroup; set { if ( _materialGroup == value ) return; _materialGroup = value; if ( ModelRenderer.IsValid() ) { ModelRenderer.MaterialGroup = MaterialGroup; } } } [Property] public Color Tint { get => _tint; set { if ( _tint == value ) return; _tint = value; if ( ModelRenderer.IsValid() ) { ModelRenderer.Tint = Tint; } } } protected bool HasMaterialGroups => Model?.MaterialGroupCount > 0; protected bool HasBodyGroups => Model?.Parts.All.Sum( x => x.Choices.Count ) > 1; [Property, Sync] public float Health { get; set; } /// /// If the prop is static - it won't have dynamic physics. This is usually used for things that /// you want to be breakable but don't move. Like fences and stuff. /// [Property] public bool IsStatic { get => _static; set { if ( _static == value ) return; _static = value; if ( !Active || IsProxy || ProceduralComponents is null ) return; if ( GameObject.IsDeserializing ) return; ClearProcedurals(); UpdateComponents(); } } /// /// Physics will be asleep until it's woken up. /// [Property, ShowIf( nameof( IsStatic ), false )] public bool StartAsleep { get; set; } [Property] public Action OnPropBreak { get; set; } [Property] public Action OnPropTakeDamage { get; set; } [Property, Hide] List ProceduralComponents { get; set; } [Property, Hide] ModelRenderer ModelRenderer { get; set; } void ClearProcedurals() { if ( ProceduralComponents is null ) return; foreach ( var p in ProceduralComponents ) { p.Destroy(); Network?.Refresh( p ); } ProceduralComponents.Clear(); ProceduralComponents = null; ModelRenderer = null; } void AddProcedural( Component p ) { Assert.AreNotEqual( p, this ); ProceduralComponents ??= new(); if ( !ProceduralComponents.Contains( p ) ) { ProceduralComponents.Add( p ); } Network?.Refresh( p ); } internal override void OnEnabledInternal() { base.OnEnabledInternal(); if ( !IsProxy ) { ClearProcedurals(); UpdateComponents(); } } internal override void OnDisabledInternal() { base.OnDisabledInternal(); if ( !IsProxy ) { ClearProcedurals(); } } void OnModelChanged() { if ( IsProxy ) return; if ( Model is null ) return; if ( GameObject.IsDeserializing ) return; if ( Model.Data.Health > 0 ) { Health = Model.Data.Health; } if ( Active ) { ClearProcedurals(); UpdateComponents(); } } void UpdateComponents() { if ( Model is null ) return; bool skinned = Model.BoneCount > 0; CreateModelComponent( skinned ); CreatePhysicsComponent(); } void CreateModelComponent( bool skinned ) { ModelRenderer mr; if ( skinned ) { mr = Components.GetOrCreate(); } else { mr = Components.GetOrCreate(); } mr.Model = Model; mr.BodyGroups = BodyGroups; mr.MaterialGroup = MaterialGroup; mr.Tint = Tint; AddProcedural( mr ); ModelRenderer = mr; } void CreatePhysicsComponent() { if ( Model.Physics is null ) return; if ( Model.Physics.Parts.Count == 0 ) return; // Static shit if ( IsStatic ) { var collider = Components.GetOrCreate(); collider.Static = true; collider.Model = Model; AddProcedural( collider ); return; } // Regular prop if ( Model.Physics.Parts.Count == 1 ) { var collider = Components.GetOrCreate(); collider.Static = false; collider.Model = Model; AddProcedural( collider ); var rb = Components.GetOrCreate(); if ( StartAsleep ) { rb.StartAsleep = true; if ( rb.PhysicsBody.IsValid() ) { rb.PhysicsBody.Sleeping = true; } } AddProcedural( rb ); return; } // Ragdoll prop // in the future this will create a bunch of GameObjects with the colliders and rigidbody // but for now we have this component that does it var physics = Components.GetOrCreate(); if ( ProceduralComponents is not null ) { physics.Renderer = ProceduralComponents?.OfType().FirstOrDefault() ?? physics.Renderer; if ( physics.Renderer.IsValid() ) { physics.Renderer.Tint = Tint; } } physics.Model = Model; AddProcedural( physics ); } /// /// True if this prop can be set on fire. /// public bool IsFlammable => Model?.Data.Flammable ?? false; [Sync] public bool IsOnFire { get; protected set; } [Sync] public GameObject LastAttacker { get; set; } public void OnDamage( in DamageInfo damage ) { LastAttacker = damage.Attacker; if ( IsProxy ) return; // The dead feel nothing if ( Health <= 0.0f ) return; if ( IsFlammable && !IsOnFire && ShouldDamageIgnite( damage ) ) { // when first ignited, randomize the health a bit, so eventual breaks and explosions // don't happen in complete unison if ( Model?.Data is not null ) { Health = Model.Data.Health * Random.Shared.Float( 0.8f, 1.2f ); } Ignite(); return; } OnPropTakeDamage?.Invoke( damage ); // Take the damage Health -= damage.Damage; if ( Health <= 0 ) { Kill(); Health = 0; } } bool ShouldDamageIgnite( in DamageInfo damage ) { // Physics impacts only ignite if they do lots of damage if ( damage.Tags.Contains( "impact" ) ) { return damage.Damage > Health * 0.5f; } return true; } public void Ignite() { if ( IsProxy ) return; if ( IsOnFire ) return; IsOnFire = true; var firePrefab = Game.Resources.Get( "/prefabs/engine/ignite.prefab" ); if ( firePrefab == null ) { Log.Warning( "Can't find /prefabs/engine/ignite.prefab" ); return; } // Spawn it, and send it to children on the network var fire = GameObject.Clone( firePrefab, new CloneConfig { Parent = GameObject, Transform = global::Transform.Zero, StartEnabled = true } ); if ( !fire.IsValid() ) return; fire.RunEvent( x => x.Target = GameObject ); if ( Network.Active ) { fire.Network.Refresh( fire ); } } public void Kill() { OnBreak(); GameObject.Destroy(); } void OnBreak() { OnPropBreak?.Invoke(); PlayBreakSound(); NetworkCreateGibs(); CreateExplosion(); } public void CreateExplosion() { if ( Model?.Data.Explosive == false ) return; var radius = Model.Data.ExplosionRadius; if ( radius <= 0 ) radius = 256; var damage = Model.Data.ExplosionDamage; if ( damage <= 0 ) damage = 80; var force = Model.Data.ExplosionForce; if ( force <= 0 ) force = 1; var explosionPrefab = Game.Resources.Get( "/prefabs/engine/explosion_med.prefab" ); if ( explosionPrefab == null ) { Log.Warning( "Can't find /prefabs/engine/explosion_med.prefab" ); return; } // Spawn it, and send it to children on the network var go = GameObject.Clone( explosionPrefab, new CloneConfig { Transform = WorldTransform.WithScale( 1 ), StartEnabled = false } ); if ( !go.IsValid() ) return; // set up the damage appropriately go.RunEvent( x => { x.Radius = radius; x.PhysicsForceScale = force; x.DamageAmount = damage; x.Attacker = LastAttacker; }, FindMode.EverythingInSelfAndDescendants ); go.Enabled = true; go.NetworkSpawn( true, null ); } private void PlayBreakSound() { if ( ProceduralComponents is null ) return; var surfaces = ProceduralComponents.OfType() .SelectMany( x => x.Shapes ) .Select( x => x.Surface ) .Distinct(); foreach ( var surface in surfaces ) { if ( !surface.IsValid() ) continue; var sound = surface.SoundCollection.Break; if ( sound == null ) continue; Sound.Play( sound, WorldPosition ); } } /// /// Create the gibs for this prop breaking, over the network. This causes clients to spawn the gibs too. /// [Rpc.Broadcast( NetFlags.OwnerOnly )] public void NetworkCreateGibs() { CreateGibs(); } /// /// Create the gibs and return them. /// public List CreateGibs() { var gibs = new List(); if ( Model is null ) return gibs; var spawnServerGibs = !Network.IsProxy; var spawnClientGibs = !Application.IsDedicatedServer; var breaklist = Model.GetData(); if ( breaklist is null || breaklist.Length <= 0 ) return gibs; var rb = Components.Get(); gibs.EnsureCapacity( breaklist.Length ); foreach ( var breakModel in breaklist ) { var model = Model.Load( breakModel.Model ); if ( model is null || model.IsError ) continue; // Skip gibs we shouldn't spawn if ( !spawnServerGibs && !breakModel.IsClientOnly ) continue; if ( !spawnClientGibs && breakModel.IsClientOnly ) continue; var gib = new GameObject( false, $"{GameObject.Name} (gib)" ); var offset = breakModel.Offset; var placementOrigin = model.Attachments.GetTransform( "placementOrigin" ); if ( placementOrigin.HasValue ) offset = placementOrigin.Value.PointToLocal( offset ); gib.WorldPosition = WorldTransform.PointToWorld( offset ); gib.WorldRotation = WorldRotation; gib.WorldScale = WorldScale; foreach ( var tag in breakModel.CollisionTags.Split( ' ', StringSplitOptions.RemoveEmptyEntries ) ) { gib.Tags.Add( tag ); } var c = gib.Components.Create( false ); c.FadeTime = breakModel.FadeTime; c.Model = model; c.Enabled = true; gibs.Add( c ); if ( breakModel.IsClientOnly ) { gib.Tags.Add( "debris", "clientside" ); // no physics interactions } else if ( !IsProxy ) { // Spawn on the network gib.NetworkSpawn( true, null ); } gib.Enabled = true; var phys = gib.Components.Get( true ); if ( phys is not null && rb is not null ) { phys.Velocity = rb.Velocity; phys.AngularVelocity = rb.AngularVelocity; } } return gibs; } /// /// Delete this component and split into the procedural components that this prop created. /// [Button( "Break into separate components", "call_split" )] public void Break() { if ( !Active ) { // If we're not active, we want to restore the procedural components again ClearProcedurals(); UpdateComponents(); } if ( ProceduralComponents is null ) return; using ( Scene.Editor?.UndoScope( "Break Prop" ).WithComponentDestructions( this ).WithComponentDestructions( ProceduralComponents ).Push() ) { foreach ( var c in ProceduralComponents ) { c.Flags = 0; if ( !Active ) { c.Enabled = false; } } ProceduralComponents.Clear(); ProceduralComponents = null; ModelRenderer = null; Destroy(); } } }