ConcreteDestruction.cs
using Sandbox;
using System;
namespace PaintballBaddies;
// The scene's cover remains static and editable. Its replicated fracture mask
// belongs to the existing session so late joiners also see missing collision.
public sealed partial class MultiplayerSession
{
[Sync(SyncFlags.FromHost)] public string ConcreteFractures {get;private set;}="";
internal int ConcreteMask(int id)
{
var values=ConcreteFractures.Split(',');
return id>=0 && id<values.Length && int.TryParse(values[id],out int mask) ? mask : 0;
}
internal void BreakConcrete(int id,int piece)
{
if(!Authority || !IsOverdrive || !InRound || id<0 || id>=12 || piece<0 || piece>=14)return;
var values=Enumerable.Range(0,12).Select(ConcreteMask).ToArray();values[id]|=1<<piece;
ConcreteFractures=string.Join(",",values);
}
}
public sealed class ConcreteDestruction : Component
{
public const string Folder="models/foundry/foundry_concrete_barrier/fracture/";
private bool installed;
protected override void OnUpdate()
{
if(installed)return;installed=true;
foreach(var cover in Scene.GetAllComponents<CoverSurface>().ToArray())
if(cover.GameObject.Name.StartsWith("Concrete cover ") && int.TryParse(cover.GameObject.Name[15..],out int id))
{
var component=cover.Components.Get<BreakableConcrete>() ?? cover.Components.Create<BreakableConcrete>();component.CoverId=id;
}
}
}
public sealed class BreakableConcrete : Component
{
public int CoverId {get;set;}
public int Mask {get;private set;}
internal GameObject Core {get;private set;}
internal List<ConcreteChip> Chips {get;}=new();
private ModelRenderer original;
private ModelCollider collision;
private int round=-1;
protected override void OnStart()
{
original=Components.Get<ModelRenderer>();collision=Components.Get<ModelCollider>();
var layout=Json.Deserialize<ShieldBreakEffect.Layout>(FileSystem.Mounted.ReadAllText(ConcreteDestruction.Folder+"layout.json"));
Core=Create("Fractured concrete core",ConcreteDestruction.Folder+"core.vmdl",Vector3.Zero);
for(int i=0;i<layout.Pieces.Count;i++)
{
var piece=layout.Pieces[i];var chip=Create($"Concrete edge {i}",piece.Model,new Vector3(piece.Center[0],piece.Center[1],piece.Center[2])).Components.Create<ConcreteChip>();
chip.Cover=this;chip.Index=i;chip.ModelPath=piece.Model;Chips.Add(chip);
}
}
private GameObject Create(string name,string model,Vector3 center)
{
var go=new GameObject(GameObject,true,name){NetworkMode=NetworkMode.Never,LocalPosition=center};
var asset=Model.Load(model);go.Components.Create<ModelRenderer>().Model=asset;
var collider=go.Components.Create<ModelCollider>();collider.Model=asset;collider.Static=true;
return go;
}
protected override void OnUpdate()
{
var s=MultiplayerSession.Find(Scene);bool active=s?.IsOverdrive==true;
if(s is not null && round!=s.RoundId){round=s.RoundId;foreach(var chip in Chips)chip.ResetDamage();}
Mask=active ? s.ConcreteMask(CoverId) : 0;
if(original.IsValid())original.Enabled=!active;
if(collision.IsValid())collision.Enabled=!active;
if(Core.IsValid())Core.Enabled=active;
foreach(var chip in Chips)chip.GameObject.Enabled=active && (Mask&(1<<chip.Index))==0;
}
protected override void OnDestroy()
{
if(Core.IsValid())Core.Destroy();foreach(var chip in Chips)if(chip.IsValid())chip.GameObject.Destroy();
if(original.IsValid())original.Enabled=true;if(collision.IsValid())collision.Enabled=true;
}
}
public sealed class ConcreteChip : Component
{
internal BreakableConcrete Cover {get;set;}
internal int Index {get;set;}
internal string ModelPath {get;set;}
private int damage;
internal void ResetDamage()=>damage=0;
internal void Hit(int amount,Vector3 direction,PaintballCombatant shooter)
{
var s=MultiplayerSession.Find(Scene);
if(!MultiplayerSession.Authority || s?.IsOverdrive!=true || !s.InRound || (s.ConcreteMask(Cover.CoverId)&(1<<Index))!=0)return;
damage+=amount;if(damage<22)return;
s.BreakConcrete(Cover.CoverId,Index);
ConcreteFragment.Spawn(this,direction,shooter,s.RoundId);
GameObject.Enabled=false;
}
}
/// <summary>Swept host-owned rubble. Dust is cosmetic; only a fast real chunk can knock out Terry.</summary>
public sealed class ConcreteFragment : Component
{
[Sync(SyncFlags.FromHost)] public int Piece {get;set;}
[Sync(SyncFlags.FromHost)] public Vector3 Velocity {get;set;}
[Sync(SyncFlags.FromHost)] public GameObject Shooter {get;set;}
[Sync(SyncFlags.FromHost)] public int Round {get;set;}
public static int HeadKnockouts {get;private set;}
private float age;
private bool hitOnce;
private ModelRenderer renderer;
internal static void Spawn(ConcreteChip chip,Vector3 direction,PaintballCombatant shooter,int round)
{
foreach(var old in chip.Scene.GetAllComponents<ConcreteFragment>().OrderByDescending(x=>x.age).SkipLast(17).ToArray())old.GameObject.Destroy();
var go=new GameObject(false,"Flying concrete chunk"){WorldTransform=chip.WorldTransform};go.Tags.Add("paintball_debris");
var p=go.Components.Create<ConcreteFragment>();p.Piece=chip.Index;p.Round=round;p.Shooter=shooter?.GameObject;
p.Velocity=direction.Normal*430+Vector3.Up*175;go.Enabled=true;
if(MultiplayerSession.Online){go.NetworkMode=NetworkMode.Object;go.Network.SetOwnerTransfer(OwnerTransfer.Fixed);go.Network.Interpolation=false;go.NetworkSpawn();}
BreakEffect(go.WorldPosition,round);
}
[Rpc.Broadcast(NetFlags.HostOnly)] private static async void BreakEffect(Vector3 point,int round)
{
var scene=Game.ActiveScene;await GameTask.MainThread();
if(!scene.IsValid() || Game.ActiveScene!=scene || MultiplayerSession.Find(scene) is not {IsOverdrive:true} s || s.RoundId!=round)return;
KitchenCombatAudio.Play("concrete",point);
var dust=GameObject.Clone("prefabs/surface/default-bullet.prefab",new Transform(point,Rotation.LookAt(Vector3.Up)),startEnabled:false);
dust.NetworkMode=NetworkMode.Never;dust.WorldScale=new Vector3(.65f);dust.Enabled=true;
}
protected override void OnStart(){renderer=Components.Create<ModelRenderer>();renderer.Model=Model.Load(ConcreteDestruction.Folder+$"chip_{Piece:00}.vmdl");}
protected override void OnUpdate(){age+=Time.Delta;if(renderer.IsValid())renderer.Tint=Color.White.WithAlpha(((3.2f-age)/.4f).Clamp(0,1));}
protected override void OnFixedUpdate()
{
if(!MultiplayerSession.Authority)return;
var s=MultiplayerSession.Find(Scene);
if(s?.IsOverdrive!=true || !s.InRound || s.RoundId!=Round || age>3.2f){GameObject.Destroy();return;}
float dt=Time.Delta;Velocity+=Vector3.Down*500*dt;
var end=WorldPosition+Velocity*dt;
var hit=Scene.Trace.Sphere(2.2f,WorldPosition,end).WithSurfaceMeshes().WithoutTags("paintball_debris","paintball_actor").Run();
if(!hit.Hit){WorldPosition=end;WorldRotation*=Rotation.From(new Angles(180,250,110)*dt);return;}
var enemy=OverdriveEnemy.Find(hit.GameObject);
if(!hitOnce && enemy.IsValid() && enemy.Zone(hit.HitPosition)==1 && Velocity.Length>=140)
{
var actor=Shooter.IsValid() ? Shooter.Components.Get<PaintballCombatant>() : null;
if(enemy.KnockOut(Velocity.Normal,actor)){hitOnce=true;HeadKnockouts++;SausageBolt.Cue(hit.HitPosition,"bonk",Round,null);}
}
WorldPosition=hit.EndPosition+hit.Normal*.6f;
Velocity=(Velocity-2*Vector3.Dot(Velocity,hit.Normal)*hit.Normal)*.22f;
if(Velocity.Length<30)Velocity=Vector3.Zero;
}
}