Skip to content

Instantly share code, notes, and snippets.

@canoom
Created March 31, 2025 10:35
Show Gist options
  • Save canoom/26c31d68ab7388a18a47f624e38d51f3 to your computer and use it in GitHub Desktop.
Save canoom/26c31d68ab7388a18a47f624e38d51f3 to your computer and use it in GitHub Desktop.
└── code
├── Assembly.cs
├── BoardManager.cs
├── BugSegment.cs
├── CellComponent.cs
├── CoinComponent.cs
├── Components
├── DamageNumber.cs
├── FixTransparency.cs
├── RotateConstantly.cs
└── VoiceComponent.cs
├── Enums
├── BugBodyType.cs
└── GameState.cs
├── GameManager.cs
├── GameResource
├── BugResource.cs
└── WeaponResource.cs
├── Inputs
├── AttackingInput.cs
├── InspectInput.cs
└── PlacementInput.cs
├── PebbleComponent.cs
└── UI
├── Controls
├── InputGlyph.razor
└── InputGlyph.razor.scss
├── PanelComponents
├── BugHealthbar.razor
├── BugHealthbar.razor.scss
├── InspectorPanel.razor
├── InspectorPanel.razor.scss
├── MainHudPanel.razor
├── MainHudPanel.razor.scss
├── MainMenu.razor
├── MainMenu.razor.scss
├── PauseMenu.razor
└── PauseMenu.razor.scss
└── Panels
├── BugList
├── BugList.razor
├── BugList.razor.scss
├── BugListEntry.razor
├── BugListEntry.razor.scss
├── BugListSegment.razor
└── BugListSegment.razor.scss
├── GameHud
├── ControlsPanel.razor
├── ControlsPanel.razor.scss
├── GameHud.razor
├── GameHud.razor.scss
├── PlayerHud
│ ├── ChatEntry.razor
│ ├── PlayerHud.razor
│ └── PlayerHud.razor.scss
├── SidePanel.razor
├── SidePanel.razor.scss
└── WeaponHud
│ ├── WeaponButton.razor
│ ├── WeaponButton.razor.scss
│ ├── WeaponHud.razor
│ └── WeaponHud.razor.scss
├── HintPanel
├── HintPanel.razor
├── HintPanel.razor.scss
└── HintPanelEntry.razor
├── LobbyList.razor
├── LobbyList.razor.scss
├── PlacingHud.razor
├── PlacingHud.razor.scss
├── ResultsHud.razor
├── ResultsHud.razor.scss
├── ShopPanel
├── ShopPanel.razor
├── ShopPanel.razor.scss
├── ShopPanelEntry.razor
└── ShopPanelEntry.razor.scss
├── StatsPanel.razor
├── StatsPanel.razor.scss
├── WaitingHud.razor
└── WaitingHud.razor.scss
/code/Assembly.cs:
--------------------------------------------------------------------------------
1 | global using Sandbox;
2 | global using System.Collections.Generic;
3 | global using System.Linq;
4 |
--------------------------------------------------------------------------------
/code/BoardManager.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Sandbox;
3 |
4 | namespace Battlebugs;
5 |
6 | public sealed class BoardManager : Component
7 | {
8 | // Static Variables
9 | public static BoardManager Local
10 | {
11 | get
12 | {
13 | if ( !_local.IsValid() )
14 | {
15 | _local = Game.ActiveScene.GetAllComponents<BoardManager>().FirstOrDefault( x => x.Network.IsOwner );
16 | }
17 | return _local;
18 | }
19 | }
20 | private static BoardManager _local;
21 | public static BoardManager Opponent => Game.ActiveScene.GetAllComponents<BoardManager>().FirstOrDefault( x => x.Network.Owner != Connection.Local );
22 |
23 | // Properties
24 | [Property] public int GridSize { get; set; } = 64;
25 | [Property] public int Width { get; set; } = 10;
26 | [Property] public int Height { get; set; } = 10;
27 | [Property, Group( "References" )] public GameObject CameraPosition { get; set; }
28 |
29 | // Networked Variables
30 | [Sync] public int Coins { get; set; } = 100;
31 | [Sync] public bool IsReady { get; set; } = false;
32 | [Sync] public NetList<BugReference> BugReferences { get; set; } = new();
33 | [Sync] public int CoinsSpent { get; set; } = 0;
34 | [Sync] public int BugsKilled { get; set; } = 0;
35 |
36 | // Public Variables
37 | public WeaponResource SelectedWeapon = null;
38 | public Dictionary<BugResource, int> BugInventory = new();
39 | public Dictionary<WeaponResource, int> WeaponInventory = new();
40 | public int MaxPlaceableSegments => BugInventory.Where( x => x.Value > 0 ).OrderBy( x => x.Key.SegmentCount ).LastOrDefault().Key?.SegmentCount ?? 0;
41 | TimeSince timeSinceTurnStart = 0;
42 |
43 | protected override void OnStart()
44 | {
45 | if ( IsProxy ) return;
46 |
47 | InitBoard();
48 | ResetBugInventory();
49 | ResetWeaponInventory();
50 |
51 | if ( Network.Owner is null )
52 | {
53 | SetupBoardRandomly();
54 | IsReady = true;
55 | }
56 | }
57 |
58 | protected override void OnFixedUpdate()
59 | {
60 | if ( GameManager.Instance.CurrentPlayer != this ) timeSinceTurnStart = 0;
61 | if ( Network.Owner is null && timeSinceTurnStart > 2.5f && GameManager.Instance.CurrentPlayer == this && GameManager.Instance.State == GameState.Playing && GameManager.Instance.IsFiring )
62 | {
63 | AttackRandomly();
64 | }
65 | }
66 |
67 | void InitBoard()
68 | {
69 | Vector3 startingPosition = WorldPosition + new Vector3( -(Width * GridSize) / 2f + GridSize / 2f, -(Height * GridSize) / 2f + GridSize / 2f, 0 );
70 | for ( int x = 0; x < Width; x++ )
71 | {
72 | for ( int y = 0; y < Height; y++ )
73 | {
74 | var cellObj = GameManager.Instance.CellPrefab.Clone( startingPosition + new Vector3( x * GridSize, y * GridSize, 0 ) );
75 | var cell = cellObj.Components.Get<CellComponent>();
76 | var index = x + y * (Width + 1);
77 | cell.Init( this, new Vector2( x, y ), index );
78 | cellObj.SetParent( GameObject );
79 | cellObj.NetworkSpawn( Network.Owner );
80 | }
81 | }
82 | }
83 |
84 | protected override void DrawGizmos()
85 | {
86 | base.DrawGizmos();
87 |
88 | var size = new Vector3( Width * GridSize, Height * GridSize, 0 );
89 | var bounds = new BBox( size / 2f, -size / 2f );
90 | Gizmo.Draw.LineBBox( bounds );
91 | }
92 |
93 | public void ClearAllBugs( bool playSound = true )
94 | {
95 | if ( playSound ) Sound.Play( "clear-all-bugs" );
96 | var segments = Scene.GetAllComponents<BugSegment>();
97 | foreach ( var segment in segments )
98 | {
99 | if ( segment.Network.OwnerId != Network.OwnerId ) continue;
100 | segment.Cell.IsOccupied = false;
101 | segment.GameObject.Destroy();
102 | }
103 | var cells = Components.GetAll<CellComponent>( FindMode.InChildren );
104 | foreach ( var cell in cells )
105 | {
106 | cell.IsOccupied = false;
107 | cell.BroadcastUpdateHighlight();
108 | }
109 | ResetBugInventory();
110 | }
111 |
112 | public void ToggleReady()
113 | {
114 | if ( IsProxy ) return;
115 | IsReady = !IsReady;
116 | }
117 |
118 | void ResetBugInventory()
119 | {
120 | var allBugs = ResourceLibrary.GetAll<BugResource>();
121 | BugInventory.Clear();
122 | foreach ( var bug in allBugs )
123 | {
124 | BugInventory[bug] = bug.StartingAmount;
125 | }
126 | }
127 |
128 | [Rpc.Owner]
129 | public void AttackRandomly()
130 | {
131 | var targetSegment = Scene.GetAllComponents<BugSegment>().OrderBy( x => Random.Shared.Float() ).FirstOrDefault( x => x.Network.OwnerId != Network.OwnerId );
132 | var targetPosition = targetSegment.WorldPosition + (Vector3.Random.WithZ( 0 ) * (targetSegment.IsVisible ? 48 : 250));
133 | var opponentPos = targetSegment.GameObject.Root.WorldPosition;
134 | targetPosition = new Vector3(
135 | Math.Clamp( targetPosition.x, opponentPos.x - (Width * GridSize) / 2f, opponentPos.x + (Width * GridSize) / 2f ),
136 | Math.Clamp( targetPosition.y, opponentPos.y - (Height * GridSize) / 2f, opponentPos.y + (Height * GridSize) / 2f ),
137 | 0
138 | );
139 |
140 | var weapon = WeaponInventory.OrderBy( x => Random.Shared.Float() ).FirstOrDefault( x => x.Value != 0 ).Key;
141 | SelectedWeapon = weapon;
142 | WeaponInventory[SelectedWeapon]--;
143 | GameManager.Instance.BroadcastFire( Id, SelectedWeapon.ResourceId, targetPosition );
144 | }
145 |
146 | void ResetWeaponInventory()
147 | {
148 | var allWeapons = ResourceLibrary.GetAll<WeaponResource>();
149 | WeaponInventory.Clear();
150 | foreach ( var weapon in allWeapons )
151 | {
152 | WeaponInventory[weapon] = weapon.StartingAmount;
153 | if ( weapon.StartingAmount < 0 ) SelectedWeapon = weapon;
154 | }
155 | }
156 |
157 | public float GetHealthPercent()
158 | {
159 | var segments = Scene.GetAllComponents<BugSegment>().Where( x => x.Network.OwnerId == Network.OwnerId );
160 | var totalSegments = segments.Count();
161 | var totalHealth = segments.Sum( x => x.Health );
162 | var totalMaxHealth = BugReferences.Sum( x => ResourceLibrary.Get<BugResource>( x.ResourceId ).StartingHealth * x.ObjectIds.Count );
163 | return (float)totalHealth / totalMaxHealth;
164 | }
165 |
166 | public float GetScorePercent()
167 | {
168 | if ( GameManager.Instance.State < GameState.Playing ) return 0.5f;
169 | var myScore = GetHealthPercent();
170 | var opponentScore = Scene.GetAllComponents<BoardManager>().FirstOrDefault( x => x.Network.OwnerId != Network.OwnerId ).GetHealthPercent();
171 | return myScore / (myScore + opponentScore);
172 |
173 | }
174 |
175 | [Rpc.Owner]
176 | public void SaveBugReferences()
177 | {
178 | BugReferences.Clear();
179 |
180 | // Compose the list of bugs and their individual ids
181 | var references = new List<BugReference>();
182 | var segments = Scene.GetAllComponents<BugSegment>();
183 | foreach ( var segment in segments )
184 | {
185 | if ( segment.Network.OwnerId != Network.OwnerId ) continue;
186 | var existingRef = references.FirstOrDefault( x => x.BugId == segment.GameObject.Name );
187 | if ( existingRef.ResourceId != 0 ) existingRef.Add( segment.GameObject.Id );
188 | else
189 | {
190 | var reference = new BugReference( segment.BugId, segment.GameObject.Name );
191 | reference.Add( segment.GameObject.Id );
192 | references.Add( reference );
193 | }
194 | }
195 |
196 | // Add entries to the NetList
197 | foreach ( var reference in references )
198 | {
199 | BugReferences.Add( reference );
200 | }
201 | }
202 |
203 | public void PurchaseWeapon( WeaponResource weapon )
204 | {
205 | Coins -= weapon.Cost;
206 | WeaponInventory[weapon]++;
207 | Sandbox.Services.Stats.Increment( "coins_spent", weapon.Cost );
208 | }
209 |
210 | [Rpc.Owner]
211 | public void GiveCoins( int amount )
212 | {
213 | Coins += amount;
214 | CoinsSpent += amount;
215 | Sandbox.Services.Stats.Increment( "coins_earned", amount );
216 | }
217 |
218 | [Rpc.Owner]
219 | public void IncrementBugsKilled()
220 | {
221 | BugsKilled++;
222 | Sandbox.Services.Stats.Increment( "bugs_killed", 1 );
223 | }
224 |
225 | public void IncrementDamageDealt( float damage )
226 | {
227 | Sandbox.Services.Stats.Increment( "damage_dealt", (int)damage );
228 | }
229 |
230 | [Rpc.Owner]
231 | public void GiveCellCoins( Vector3 position )
232 | {
233 | GiveCoins( 5 );
234 | GameManager.Instance.SpawnCoins( position, 1 );
235 | HintPanel.Instance.CellCoinNotification();
236 | }
237 |
238 | public void SetupBoardRandomly()
239 | {
240 | ClearAllBugs( false );
241 | int attempts = 0;
242 | foreach ( var entry in BugInventory )
243 | {
244 | while ( BugInventory[entry.Key] > 0 )
245 | {
246 | while ( !TryPlaceRandomBug( entry.Key ) )
247 | {
248 | attempts++;
249 | if ( attempts > 100 )
250 | {
251 | SetupBoardRandomly();
252 | return;
253 | }
254 | }
255 | }
256 | }
257 | }
258 |
259 | bool TryPlaceRandomBug( BugResource bug )
260 | {
261 | int attempts = 0;
262 |
263 | var startingCell = Components.GetAll<CellComponent>().Where( x => !x.IsOccupied && x.GameObject.Root == GameObject ).OrderBy( x => Guid.NewGuid() ).FirstOrDefault();
264 | if ( startingCell is null ) return false;
265 |
266 | var cells = new List<CellComponent> { startingCell };
267 |
268 | while ( cells.Count < bug.SegmentCount )
269 | {
270 | var cell = cells.Last();
271 | var neighbors = cell.GetNeighbors().Where( x => !x.IsOccupied && !cells.Contains( x ) && x.GameObject.Root == GameObject ).ToList();
272 | if ( neighbors.Count == 0 )
273 | {
274 | attempts++;
275 | if ( attempts > 100 ) return false;
276 | cells = new List<CellComponent> { startingCell };
277 | continue;
278 | }
279 | var nextCell = neighbors.OrderBy( x => Random.Shared.Float() ).First();
280 | cells.Add( nextCell );
281 | }
282 |
283 | GameManager.Instance.CreateBug( this, PlacementInput.Instance.GetPlacementData( cells, bug ), Network.Owner is null );
284 | return true;
285 | }
286 |
287 | public struct BugReference
288 | {
289 | public int ResourceId { get; set; }
290 | public string BugId { get; set; }
291 | public List<string> ObjectIds { get; set; }
292 |
293 | private BugResource _bug;
294 |
295 | public BugReference( int resourceId, string bugId )
296 | {
297 | ResourceId = resourceId;
298 | BugId = bugId;
299 | ObjectIds = new List<string>();
300 | }
301 |
302 | public void Add( Guid objectId )
303 | {
304 | ObjectIds.Add( objectId.ToString() );
305 | }
306 |
307 | public BugResource GetBug()
308 | {
309 | if ( _bug is null ) _bug = ResourceLibrary.Get<BugResource>( ResourceId );
310 | return _bug;
311 | }
312 | }
313 |
314 | }
315 |
--------------------------------------------------------------------------------
/code/BugSegment.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Battlebugs;
4 |
5 | public class BugSegment : Component
6 | {
7 | [Property] public GameObject Body { get; set; }
8 | [Property] public ModelRenderer BodyRenderer { get; set; }
9 | [Property] BugHealthbar Healthbar { get; set; }
10 | [Property] bool Floating { get; set; }
11 |
12 | [Property, Group( "Prefabs" )] public GameObject BugSplatParticle { get; set; }
13 |
14 | [Sync] public int Index { get; set; } = 0;
15 | [Sync] public int BugId { get; set; }
16 | [Sync] public float Health { get; set; } = 10f;
17 | public BugResource Bug => ResourceLibrary.Get<BugResource>( BugId );
18 |
19 | public CellComponent Cell { get; set; }
20 |
21 | bool _initialized { get; set; } = false;
22 | float _targetAlpha { get; set; } = 1f;
23 | TimeSince _timeSinceLastDamage { get; set; } = 0f;
24 |
25 | public bool IsVisible => _targetAlpha > 0f;
26 |
27 | public async void Init( BugResource bug, int index )
28 | {
29 | Index = index;
30 | BugId = bug.ResourceId;
31 | Health = bug.StartingHealth;
32 | Body.LocalPosition = Vector3.Down * 250f;
33 | Body.LocalRotation = new Angles( 0, Random.Shared.Float( -180f, 180f ), 0 );
34 | await GameTask.DelaySeconds( index * 0.05f );
35 | _initialized = true;
36 | Sound.Play( "segment-drop" );
37 | }
38 |
39 | protected override void OnStart()
40 | {
41 | SetAlpha( (Network.OwnerId == Connection.Local.Id) ? 1f : 0f, true );
42 | }
43 |
44 | protected override void OnUpdate()
45 | {
46 | foreach ( var renderer in Components.GetAll<ModelRenderer>( FindMode.EverythingInSelfAndDescendants ) )
47 | {
48 | renderer.Tint = renderer.Tint.WithAlpha( renderer.Tint.a.LerpTo( _targetAlpha, Time.Delta * 5f ) );
49 | }
50 | Healthbar.Alpha = Healthbar.Alpha.LerpTo( (_targetAlpha == 1 && GameManager.Instance.State != GameState.Placing) ? 1 : 0, Time.Delta * 5f );
51 | }
52 |
53 | protected override void OnFixedUpdate()
54 | {
55 | if ( _initialized || IsProxy )
56 | {
57 | var targetPos = Vector3.Up * 2.5f;
58 | if ( Floating ) targetPos += Vector3.Up * (MathF.Sin( (Time.Now + (WorldPosition.x + (WorldPosition.y * 10f)) / 32f) * 2f ) * 0.25f) * 8f;
59 | Body.LocalPosition = Body.LocalPosition.LerpTo( targetPos, Time.Delta * 15f );
60 | Body.LocalRotation = Rotation.Slerp( Body.LocalRotation, Rotation.Identity, Time.Delta * 15f );
61 | }
62 |
63 | if ( !IsProxy )
64 | {
65 | if ( _timeSinceLastDamage > 1f && Health <= 0 )
66 | {
67 | Clear( true );
68 | }
69 | }
70 | }
71 |
72 | public void SetAlpha( float alpha, bool instant = false )
73 | {
74 | if ( instant )
75 | {
76 | foreach ( var renderer in Components.GetAll<ModelRenderer>( FindMode.EverythingInSelfAndDescendants ) )
77 | {
78 | renderer.Tint = renderer.Tint.WithAlpha( alpha );
79 | }
80 | }
81 | _targetAlpha = alpha;
82 | }
83 |
84 | public void Clear( bool dropCoin = false )
85 | {
86 | if ( IsProxy ) return;
87 | var otherBoard = Scene.GetAllComponents<BoardManager>().FirstOrDefault( x => x.Network.OwnerId != Network.OwnerId );
88 | otherBoard.GiveCoins( 20 );
89 | otherBoard.IncrementBugsKilled();
90 | if ( GameManager.Instance.State == GameState.Playing ) Cell.BroadcastClear();
91 | BroadcastDestroyFX( dropCoin );
92 | GameObject.Destroy();
93 | }
94 |
95 |
96 | public void AddHighlight( Color color = default )
97 | {
98 | var outline = Components.GetOrCreate<HighlightOutline>();
99 | outline.Color = color;
100 | outline.InsideColor = Color.Transparent;
101 | outline.ObscuredColor = Color.Transparent;
102 | outline.InsideObscuredColor = Color.Transparent;
103 | }
104 |
105 | public void RemoveHighlight()
106 | {
107 | if ( !IsValid ) return;
108 | Components.Get<HighlightOutline>()?.Destroy();
109 | }
110 |
111 | CellComponent GetCell()
112 | {
113 | return Scene.GetAllComponents<CellComponent>().OrderBy( x => x.WorldPosition.DistanceSquared( WorldPosition ) ).FirstOrDefault();
114 | }
115 |
116 | [Rpc.Broadcast]
117 | public void Damage( float damage )
118 | {
119 | if ( Health <= 0 ) return;
120 |
121 | _targetAlpha = 1f;
122 | GetCell()?.BroadcastHit();
123 |
124 | var otherBoard = Scene.GetAllComponents<BoardManager>().FirstOrDefault( x => x.Network.OwnerId != Network.OwnerId );
125 | if ( otherBoard == BoardManager.Local )
126 | {
127 | otherBoard.IncrementDamageDealt( damage );
128 | }
129 |
130 | if ( IsProxy ) return;
131 |
132 | Health -= damage;
133 | _timeSinceLastDamage = 0f;
134 | }
135 |
136 | [Rpc.Broadcast]
137 | public void BroadcastDestroyFX( bool dropCoin = false )
138 | {
139 | Sound.Play( "impact-bullet-flesh", WorldPosition );
140 | if ( dropCoin ) GameManager.Instance.SpawnCoins( WorldPosition, 3 );
141 | if ( BugSplatParticle is not null )
142 | {
143 | var obj = BugSplatParticle.Clone( WorldPosition + Vector3.Up * 16f );
144 | var part = obj.Components.Get<ParticleEffect>();
145 | part.Tint = Bug.Color;
146 | }
147 | }
148 | }
--------------------------------------------------------------------------------
/code/CellComponent.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Sandbox;
3 |
4 | namespace Battlebugs;
5 |
6 | public sealed class CellComponent : Component
7 | {
8 | [Property] ModelRenderer Renderer { get; set; }
9 |
10 | public BoardManager Board { get; set; }
11 | public Vector2 Position { get; set; }
12 | public bool IsHovering { get; private set; } = false;
13 | public bool IsSelected { get; set; } = false;
14 |
15 | [Sync] public bool WasOccupied { get; set; } = false;
16 | [Sync] public bool IsOccupied { get; set; } = false;
17 | [Sync] public bool IsHit { get; set; } = false;
18 | [Sync] bool IsOdd { get; set; } = false;
19 | Color BaseColor => IsOdd ? Color.White : Color.Gray;
20 |
21 | protected override void OnStart()
22 | {
23 | WorldRotation = new Angles( 0, Random.Shared.Int( 0, 3 ) * 90f + Random.Shared.Float( -3f, 3f ), 0 );
24 | UpdateHighlight();
25 | }
26 |
27 | public void Init( BoardManager board, Vector2 position, int index )
28 | {
29 | Board = board;
30 | Position = position;
31 | IsOdd = index % 2 == 0;
32 | }
33 |
34 | public void MousePressed()
35 | {
36 |
37 | }
38 |
39 | public void MouseReleased()
40 | {
41 |
42 | }
43 |
44 | public void MouseEnter()
45 | {
46 | IsHovering = true;
47 |
48 | var bugs = Scene.GetAllComponents<BugSegment>();
49 | var cellBug = bugs.FirstOrDefault( x => x.Cell == this );
50 | if ( cellBug is not null )
51 | {
52 | bugs = bugs.Where( x => x.GameObject.Name == cellBug.GameObject.Name ).ToList();
53 | foreach ( var bug in bugs )
54 | {
55 | bug.AddHighlight( Color.Red );
56 | }
57 | }
58 |
59 | if ( IsSelected ) return;
60 | UpdateHighlight();
61 | }
62 |
63 | public void MouseExit()
64 | {
65 | IsHovering = false;
66 |
67 | var bugs = Scene.GetAllComponents<BugSegment>();
68 | var cellBug = bugs.FirstOrDefault( x => x.Cell == this );
69 | if ( cellBug is not null )
70 | {
71 | bugs = bugs.Where( x => x.GameObject.Name == cellBug.GameObject.Name ).ToList();
72 | foreach ( var bug in bugs )
73 | {
74 | bug.RemoveHighlight();
75 | }
76 | }
77 |
78 | if ( IsSelected ) return;
79 | UpdateHighlight();
80 | }
81 |
82 | public void Select()
83 | {
84 | IsSelected = true;
85 | PlacementInput.Instance.SelectedCells.Add( this );
86 |
87 | Sound.Play( "ui-select" );
88 |
89 | foreach ( var cell in PlacementInput.Instance.SelectedCells )
90 | {
91 | cell.UpdateHighlight();
92 | }
93 | }
94 |
95 | public void Deselect( bool remove = true )
96 | {
97 | IsSelected = false;
98 |
99 | if ( remove )
100 | {
101 | PlacementInput.Instance.SelectedCells.Remove( this );
102 | Sound.Play( "ui-select-bug" );
103 | UpdateHighlight();
104 | }
105 | foreach ( var cell in PlacementInput.Instance.SelectedCells )
106 | {
107 | cell.UpdateHighlight();
108 | }
109 | }
110 |
111 | public bool IsAdjacent( CellComponent other )
112 | {
113 | // Above/below
114 | if ( Position.x == other.Position.x && MathF.Abs( Position.y - other.Position.y ) == 1 )
115 | return true;
116 |
117 | // Left/right
118 | if ( Position.y == other.Position.y && MathF.Abs( Position.x - other.Position.x ) == 1 )
119 | return true;
120 |
121 | return false;
122 | }
123 |
124 | public List<CellComponent> GetNeighbors()
125 | {
126 | var neighbors = new List<CellComponent>();
127 |
128 | var cells = Scene.GetAllComponents<CellComponent>();
129 | foreach ( var cell in cells )
130 | {
131 | if ( cell == this ) continue;
132 | if ( IsAdjacent( cell ) )
133 | {
134 | neighbors.Add( cell );
135 | }
136 | }
137 |
138 | return neighbors;
139 | }
140 |
141 | void UpdateHighlight()
142 | {
143 | if ( IsHit )
144 | {
145 | Renderer.Tint = Color.Lerp( BaseColor, IsOccupied ? Color.Orange : (WasOccupied ? Color.Green : Color.Red), 0.95f );
146 | }
147 | else if ( IsSelected )
148 | {
149 | var color = Color.Yellow;
150 | var placing = PlacementInput.Instance.AttemptingToPlace;
151 | if ( placing is not null )
152 | {
153 | if ( BoardManager.Local.BugInventory.FirstOrDefault( x => x.Key.ResourceId == placing.ResourceId ).Value <= 0 ) color = Color.Yellow;
154 | else color = placing.Color;
155 | }
156 | Renderer.Tint = Color.Lerp( BaseColor, color, 0.8f );
157 | }
158 | else if ( IsHovering )
159 | {
160 | Renderer.Tint = IsOccupied ? Color.Lerp( BaseColor, Color.Red, 0.95f ) : Color.Lerp( BaseColor, Color.Yellow, 0.95f );
161 | }
162 | else
163 | {
164 | Renderer.Tint = BaseColor;
165 | }
166 | }
167 |
168 | [Rpc.Owner]
169 | public void BroadcastHit()
170 | {
171 | if ( !IsHit && Random.Shared.Float() < 0.3f )
172 | {
173 | var otherBoard = Scene.GetAllComponents<BoardManager>().FirstOrDefault( x => x.Network.OwnerId != Network.OwnerId );
174 | otherBoard.GiveCellCoins( WorldPosition );
175 | }
176 | IsHit = true;
177 | BroadcastUpdateHighlight();
178 | }
179 |
180 | [Rpc.Owner]
181 | public void BroadcastClear()
182 | {
183 | if ( IsProxy ) return;
184 |
185 | IsHit = true;
186 | WasOccupied = IsOccupied;
187 | IsOccupied = false;
188 | BroadcastUpdateHighlight();
189 | }
190 |
191 | [Rpc.Broadcast]
192 | public void BroadcastUpdateHighlight()
193 | {
194 | UpdateHighlight();
195 |
196 | if ( IsHit && !Network.IsOwner && BoardManager.Local.IsValid() )
197 | {
198 | if ( IsOccupied )
199 | HintPanel.Instance.YellowCellNotification();
200 | else if ( !WasOccupied )
201 | HintPanel.Instance.RedCellNotification();
202 | else
203 | HintPanel.Instance.GreenCellNotification();
204 | }
205 | }
206 |
207 | }
208 |
--------------------------------------------------------------------------------
/code/CoinComponent.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Sandbox;
3 |
4 | public sealed class CoinComponent : Component
5 | {
6 | Vector3 Velocity = Vector3.Zero;
7 | TimeSince timeSinceStart = 0;
8 |
9 | protected override void OnStart()
10 | {
11 | Velocity = Vector3.Random.Normal;
12 | Velocity = Velocity.WithZ( MathF.Abs( Velocity.z ) );
13 | Velocity = Velocity.Normal * Random.Shared.Float( 20, 100 );
14 | timeSinceStart = Random.Shared.Float( 0.2f );
15 | }
16 |
17 | protected override void OnFixedUpdate()
18 | {
19 | Velocity += Vector3.Down * 100 * Time.Delta;
20 |
21 | var tr = Scene.Trace.Ray( WorldPosition, WorldPosition + Velocity * Time.Delta )
22 | .IgnoreGameObjectHierarchy( GameObject )
23 | .Run();
24 |
25 | if ( tr.Hit )
26 | {
27 | if ( Velocity.z < -10 ) Sound.Play( "coin-bounce", WorldPosition );
28 | Velocity = Vector3.Reflect( Velocity, tr.Normal );
29 | Velocity /= 1.5f;
30 | }
31 |
32 | WorldPosition += Velocity * Time.Delta;
33 |
34 | if ( timeSinceStart > 1f )
35 | {
36 | Sound.Play( "coin-collect", WorldPosition );
37 | GameObject.Destroy();
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/code/Components/DamageNumber.cs:
--------------------------------------------------------------------------------
1 | using Sandbox;
2 |
3 | namespace Battlebugs;
4 |
5 | public sealed class DamageNumber : Component
6 | {
7 | [RequireComponent] TextRenderer Renderer { get; set; }
8 |
9 | TimeSince timeSinceCreated = 0;
10 | float speed = 100f;
11 |
12 | protected override void OnUpdate()
13 | {
14 | WorldPosition += Vector3.Up * speed * Time.Delta;
15 | speed = speed.LerpTo( 0, Time.Delta * 4f );
16 |
17 | if ( timeSinceCreated > 1f )
18 | {
19 | Renderer.Color = Renderer.Color.WithAlpha( Renderer.Color.a.LerpTo( 0, Time.Delta * 4f ) );
20 | if ( Renderer.Color.a <= 0.01f )
21 | {
22 | GameObject.Destroy();
23 | }
24 | }
25 | }
26 |
27 | protected override void OnPreRender()
28 | {
29 | WorldRotation = Rotation.LookAt( WorldPosition - Scene.Camera.WorldPosition );
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/code/Components/FixTransparency.cs:
--------------------------------------------------------------------------------
1 | using Sandbox;
2 |
3 | namespace Battlebugs;
4 |
5 | public sealed class FixTransparency : Component
6 | {
7 | [Property] ModelRenderer modelRenderer { get; set; }
8 |
9 | protected override void OnStart()
10 | {
11 | if ( !modelRenderer.IsValid() ) return;
12 | modelRenderer.SceneObject.Flags.WantsFrameBufferCopy = true;
13 | modelRenderer.SceneObject.Flags.IsTranslucent = true;
14 | modelRenderer.SceneObject.Flags.IsOpaque = false;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/code/Components/RotateConstantly.cs:
--------------------------------------------------------------------------------
1 | using Sandbox;
2 |
3 | namespace Battlebugs;
4 |
5 | public sealed class RotateConstantly : Component
6 | {
7 | [Property] Angles RotationSpeed { get; set; }
8 |
9 | protected override void OnUpdate()
10 | {
11 | WorldRotation *= RotationSpeed * Time.Delta;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/code/Components/VoiceComponent.cs:
--------------------------------------------------------------------------------
1 | namespace Battlebugs;
2 |
3 | public sealed class VoiceComponent : Voice
4 | {
5 | [Property] bool IsSpectator { get; set; } = false;
6 |
7 | protected override IEnumerable<Connection> ExcludeFilter()
8 | {
9 | if ( !IsSpectator ) return Enumerable.Empty<Connection>();
10 |
11 | var connections = Connection.All.ToList();
12 | var boards = Scene.GetAllComponents<BoardManager>();
13 | for ( int i = connections.Count - 1; i >= 0; i-- )
14 | {
15 | if ( !boards.Any( x => x.Network.OwnerId == connections[i].Id ) )
16 | {
17 | connections.RemoveAt( i );
18 | }
19 | }
20 | return connections;
21 | }
22 | }
--------------------------------------------------------------------------------
/code/Enums/BugBodyType.cs:
--------------------------------------------------------------------------------
1 | public enum BugBodyType
2 | {
3 | Head,
4 | Body,
5 | Corner,
6 | Tail
7 | }
--------------------------------------------------------------------------------
/code/Enums/GameState.cs:
--------------------------------------------------------------------------------
1 | public enum GameState
2 | {
3 | Waiting,
4 | Placing,
5 | Playing,
6 | Results
7 | }
--------------------------------------------------------------------------------
/code/GameManager.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using Sandbox;
4 | using Sandbox.Network;
5 |
6 | namespace Battlebugs;
7 |
8 | public sealed class GameManager : Component, Component.INetworkListener
9 | {
10 | public static GameManager Instance { get; private set; }
11 |
12 | [RequireComponent] PlacementInput _placementInput { get; set; }
13 | [RequireComponent] AttackingInput _attackingInput { get; set; }
14 | [RequireComponent] InspectInput _inspectInput { get; set; }
15 |
16 | // Properties
17 | [Property, Group( "Prefabs" )] public GameObject BoardPrefab { get; set; }
18 | [Property, Group( "Prefabs" )] public GameObject CellPrefab { get; set; }
19 | [Property, Group( "Prefabs" )] public GameObject DamageNumberPrefab { get; set; }
20 | [Property, Group( "Prefabs" )] public GameObject CoinPrefab { get; set; }
21 |
22 | // Networked Variables
23 | [Sync( SyncFlags.FromHost )] public GameState State { get; set; }
24 | [Sync( SyncFlags.FromHost )] public Guid CurrentPlayerId { get; set; }
25 | [Sync( SyncFlags.FromHost )] public bool IsFiring { get; set; }
26 | [Sync( SyncFlags.FromHost )] public TimeSince TimeSinceTurnStart { get; set; }
27 | [Sync( SyncFlags.FromHost )] public bool CpuMode { get; set; }
28 |
29 | // Local Variables
30 | public List<BoardManager> Boards;
31 | public BoardManager CurrentPlayer => Boards.FirstOrDefault( x => x.IsValid() && x.Network.OwnerId == CurrentPlayerId );
32 | Vector3 LastPebblePosition;
33 | TimeSince TimeSincePebbleToss;
34 |
35 | protected override void OnAwake()
36 | {
37 | Instance = this;
38 | State = GameState.Waiting;
39 | Boards = new();
40 | }
41 |
42 | protected override async Task OnLoad()
43 | {
44 | if ( Scene.IsEditor ) return;
45 | if ( Networking.IsActive ) return;
46 | CpuMode = MainMenu.IsCpuGame;
47 | if ( CpuMode ) return;
48 |
49 | LoadingScreen.Title = "Creating Lobby";
50 | await Task.DelayRealtimeSeconds( 0.1f );
51 | Networking.CreateLobby( new LobbyConfig() );
52 | }
53 |
54 | protected override void OnStart()
55 | {
56 | // This is really just for late-joiners
57 | Boards = Scene.GetAllComponents<BoardManager>().ToList();
58 |
59 | // Initialize both boards right away if in CPU mode
60 | if ( CpuMode && !IsProxy )
61 | {
62 | CreateBoard( Connection.Local );
63 | CreateBoard( null );
64 | }
65 | }
66 |
67 | public void OnActive( Connection channel )
68 | {
69 | // TODO: Create a spectator pawn or something
70 | if ( Boards.Count >= 2 ) return;
71 |
72 | CreateBoard( channel );
73 | }
74 |
75 | void CreateBoard( Connection channel )
76 | {
77 | var currentBoardCount = Scene.GetAllComponents<BoardManager>().Count();
78 | var client = BoardPrefab.Clone( new CloneConfig()
79 | {
80 | Transform = new Transform( new Vector3( currentBoardCount * 1000f, 0, 2f ), new Angles( 0, currentBoardCount == 0 ? 0 : 180, 0 ) ),
81 | Name = channel?.DisplayName ?? "CPU"
82 | } );
83 | client.Network.SetOrphanedMode( NetworkOrphaned.ClearOwner );
84 | client.NetworkSpawn( channel );
85 |
86 | Boards = Scene.GetAllComponents<BoardManager>().ToList();
87 | }
88 |
89 | [Rpc.Broadcast]
90 | void StartGame()
91 | {
92 | Sound.Play( "player-join" );
93 |
94 | if ( Networking.IsHost )
95 | {
96 | State = GameState.Placing;
97 | }
98 |
99 | Boards = Scene.GetAllComponents<BoardManager>().ToList();
100 | }
101 |
102 | [Rpc.Broadcast]
103 | void StartPlaying()
104 | {
105 | if ( Networking.IsHost )
106 | {
107 | State = GameState.Playing;
108 | StartTurn();
109 |
110 | foreach ( var board in Boards )
111 | {
112 | board.SaveBugReferences();
113 | }
114 | }
115 |
116 | foreach ( var segment in Scene.GetAllComponents<BugSegment>() )
117 | {
118 | segment.SetAlpha( (segment.Network.OwnerId == Connection.Local.Id) ? 0.5f : 0f );
119 | }
120 | }
121 |
122 | void StartTurn()
123 | {
124 | TimeSinceTurnStart = 0;
125 | CurrentPlayerId = Boards.FirstOrDefault( x => x.Network.OwnerId != CurrentPlayerId ).Network.OwnerId;
126 | IsFiring = true;
127 | }
128 |
129 | [Rpc.Broadcast]
130 | void EndGame()
131 | {
132 | if ( Networking.IsHost )
133 | {
134 | State = GameState.Results;
135 | }
136 |
137 | if ( !(BoardManager.Local?.IsValid() ?? false) ) return;
138 | var didWin = BoardManager.Local.GetScorePercent() > 0.5f;
139 | Sandbox.Services.Stats.Increment( "games_played", 1 );
140 | if ( didWin ) Sandbox.Services.Stats.Increment( "games_won", 1 );
141 | else Sandbox.Services.Stats.Increment( "games_lost", 1 );
142 | }
143 |
144 | protected override void OnUpdate()
145 | {
146 | switch ( State )
147 | {
148 | case GameState.Waiting: UpdateWaiting(); break;
149 | case GameState.Placing: UpdatePlacing(); break;
150 | case GameState.Playing: UpdateGame(); break;
151 | case GameState.Results: UpdateResults(); break;
152 | }
153 | InspectInput.Instance.Enabled = State == GameState.Playing;
154 | }
155 |
156 | void UpdateWaiting()
157 | {
158 | if ( BoardManager.Local is not null )
159 | {
160 | UpdateCamera( BoardManager.Local );
161 | }
162 |
163 | if ( Networking.IsHost )
164 | {
165 | if ( Boards.Count > 1 )
166 | {
167 | StartGame();
168 | }
169 | }
170 | }
171 |
172 | void UpdatePlacing()
173 | {
174 | PlacementInput.Instance.Enabled = true;
175 |
176 | if ( BoardManager.Local is not null )
177 | {
178 | UpdateCamera( BoardManager.Local );
179 | }
180 |
181 | if ( Networking.IsHost )
182 | {
183 | if ( Boards.Any( x => x.Network.OwnerId == Guid.Empty ) && !CpuMode )
184 | {
185 | EndGame();
186 | }
187 | else if ( !Boards.Any( x => !x.IsReady ) )
188 | {
189 | StartPlaying();
190 | }
191 | }
192 | }
193 |
194 | void UpdateGame()
195 | {
196 | var currentPlayer = CurrentPlayer;
197 | PlacementInput.Instance.Enabled = false;
198 | AttackingInput.Instance.Enabled = IsFiring && (currentPlayer == BoardManager.Local);
199 |
200 | if ( currentPlayer is not null )
201 | {
202 | var healthPercent = currentPlayer.GetHealthPercent();
203 | if ( healthPercent == 0 )
204 | {
205 | EndGame();
206 | }
207 |
208 | var otherPlayer = Boards.FirstOrDefault( x => x.IsValid() && x.Network.OwnerId != CurrentPlayerId );
209 | if ( otherPlayer is null ) return;
210 | if ( IsFiring )
211 | {
212 | UpdateCamera( otherPlayer );
213 | LastPebblePosition = Scene.Camera.WorldPosition + Scene.Camera.WorldRotation.Forward * 1000f;
214 |
215 | if ( TimeSinceTurnStart >= 15f )
216 | {
217 | currentPlayer.AttackRandomly();
218 | }
219 | }
220 | else
221 | {
222 | var pebbles = Scene.GetAllComponents<PebbleComponent>();
223 | var pebble = pebbles.Where( x => x.TimeSinceCreated > 0.6f ).FirstOrDefault();
224 | if ( pebble is not null )
225 | {
226 | LastPebblePosition = pebble.WorldPosition;
227 | }
228 | if ( pebbles.Count() > 0 ) TimeSincePebbleToss = 0;
229 | UpdateCamera( otherPlayer, LastPebblePosition );
230 |
231 | if ( pebbles.Count() == 0 && TimeSincePebbleToss > 3f )
232 | {
233 | StartTurn();
234 | }
235 | }
236 | }
237 | }
238 |
239 | void UpdateResults()
240 | {
241 |
242 | }
243 |
244 | void UpdateCamera( BoardManager board )
245 | {
246 | Scene.Camera.WorldPosition = Scene.Camera.WorldPosition.LerpTo( board.CameraPosition.WorldPosition, Time.Delta * 5f );
247 | Scene.Camera.WorldRotation = Rotation.Slerp( Scene.Camera.WorldRotation, board.CameraPosition.WorldRotation, Time.Delta * 5f );
248 | }
249 |
250 | void UpdateCamera( BoardManager board, Vector3 lookAt )
251 | {
252 | Scene.Camera.WorldPosition = Scene.Camera.WorldPosition.LerpTo( board.CameraPosition.WorldPosition, Time.Delta * 5f );
253 |
254 | var rotation = Rotation.LookAt( lookAt - Scene.Camera.WorldPosition, Vector3.Up );
255 | Scene.Camera.WorldRotation = Rotation.Slerp( Scene.Camera.WorldRotation, rotation, Time.Delta * 5f );
256 | }
257 |
258 | public void CreateBug( BoardManager board, List<PlacementInput.PlacementData> cells, bool isCpu = false )
259 | {
260 | var bug = BoardManager.Local.BugInventory.FirstOrDefault( x => x.Key.SegmentCount == cells.Count );
261 | if ( bug.Value <= 0 ) return;
262 | var bugId = Guid.NewGuid();
263 |
264 | for ( int i = 0; i < cells.Count; i++ )
265 | {
266 | var segment = cells[i].Prefab.Clone( new Transform(
267 | cells[i].Cell.WorldPosition,
268 | cells[i].Rotation
269 | ) );
270 | segment.Name = bugId.ToString();
271 | var component = segment.Components.Get<BugSegment>();
272 | component.Init( bug.Key, i );
273 | component.Cell = cells[i].Cell;
274 | segment.NetworkSpawn( isCpu ? null : Connection.Local );
275 |
276 | cells[i].Cell.IsOccupied = true;
277 | }
278 |
279 | board.BugInventory[bug.Key]--;
280 | }
281 |
282 | public void SpawnCoins( Vector3 position, int amount = 1 )
283 | {
284 | for ( int i = 0; i < amount; i++ )
285 | {
286 | CoinPrefab.Clone( position + Vector3.Up * 2f );
287 | }
288 |
289 | }
290 |
291 | [Rpc.Owner]
292 | public void BroadcastFire( Guid boardId, int weaponId, Vector3 position )
293 | {
294 | if ( !CpuMode && Rpc.CallerId != CurrentPlayerId ) return;
295 | if ( IsFiring == false ) return;
296 |
297 | var weapon = ResourceLibrary.Get<WeaponResource>( weaponId );
298 | if ( weapon is null ) return;
299 |
300 | var board = Boards.FirstOrDefault( x => x.Id != boardId );
301 | if ( board is null ) return;
302 |
303 | int count = (int)MathF.Round( weapon.AmountFired.GetValue() );
304 |
305 | for ( int i = 0; i < count; i++ )
306 | {
307 | var offset = Vector3.Random.WithZ( 0 ) * weapon.Spray;
308 | var pos = board.CameraPosition.WorldPosition.WithZ( 32f ) + (board.WorldRotation.Forward * 200f) + offset;
309 | var target = position + offset;
310 | var pebbleObj = weapon.Prefab.Clone( pos );
311 | var pebble = pebbleObj.Components.Get<PebbleComponent>();
312 | pebble.Damage = weapon.Damage.GetValue();
313 | pebble.LaunchAt( target );
314 | pebbleObj.NetworkSpawn( null );
315 | }
316 | BroadcastFireSound();
317 | IsFiring = false;
318 | }
319 |
320 | [Rpc.Broadcast]
321 | void BroadcastFireSound()
322 | {
323 | Sound.Play( "fling-rocks" );
324 | }
325 |
326 | [Rpc.Broadcast]
327 | public void BroadcastDamageNumber( Vector3 position, float damage )
328 | {
329 | if ( DamageNumberPrefab is not null )
330 | {
331 | var damageNumber = DamageNumberPrefab.Clone( position );
332 | var text = damageNumber.Components.Get<TextRenderer>();
333 | text.Text = "-" + damage.ToString();
334 | text.Color = Color.Red;
335 | }
336 | }
337 |
338 | [Rpc.Broadcast]
339 | public void SendChatMessage( string message )
340 | {
341 | var playerHud = PlayerHud.Instances.FirstOrDefault( x => x.Board.Network.OwnerId == Rpc.CallerId );
342 | if ( playerHud is null ) return;
343 |
344 | playerHud.AddChatMessage( message );
345 | }
346 |
347 | }
--------------------------------------------------------------------------------
/code/GameResource/BugResource.cs:
--------------------------------------------------------------------------------
1 | namespace Battlebugs;
2 |
3 | [GameResource( "Battlebugs/Bug", "bug", "Describes a bug definition", Icon = "bug_report" )]
4 | public class BugResource : GameResource
5 | {
6 | [Group( "Information" )] public string Name { get; set; } = "Bug";
7 | [Group( "Information" )] public Color Color { get; set; } = Color.White;
8 | [Group( "Information" )] public int SegmentCount { get; set; } = 3;
9 | [Group( "Stats" )] public int StartingAmount { get; set; } = 1;
10 | [Group( "Stats" )] public float StartingHealth { get; set; } = 8f;
11 | [Group( "Prefabs" )] public GameObject HeadPrefab { get; set; }
12 | [Group( "Prefabs" )] public Model HeadModel { get; set; }
13 | [Group( "Prefabs" )] public GameObject BodyPrefab { get; set; }
14 | [Group( "Prefabs" )] public Model BodyModel { get; set; }
15 | [Group( "Prefabs" )] public GameObject CornerPrefab { get; set; }
16 | [Group( "Prefabs" )] public Model CornerModel { get; set; }
17 | [Group( "Prefabs" )] public GameObject TailPrefab { get; set; }
18 | [Group( "Prefabs" )] public Model TailModel { get; set; }
19 |
20 | public string GetHeadIcon() => "ui/thumbnails/" + ResourceName + "_head.png";
21 | public string GetBodyIcon() => "ui/thumbnails/" + ResourceName + "_body.png";
22 | public string GetCornerIcon() => "ui/thumbnails/" + ResourceName + "_corner.png";
23 | public string GetTailIcon() => "ui/thumbnails/" + ResourceName + "_tail.png";
24 |
25 |
26 |
27 | }
--------------------------------------------------------------------------------
/code/GameResource/WeaponResource.cs:
--------------------------------------------------------------------------------
1 | namespace Battlebugs;
2 |
3 | [GameResource( "Battlebugs/Weapon", "weapon", "Describes a weapon definition", Icon = "whatshot" )]
4 | public class WeaponResource : GameResource
5 | {
6 | public string Name { get; set; } = "Rock";
7 | [TextArea] public string Description { get; set; } = "";
8 | [ImageAssetPath] public string Icon { get; set; }
9 |
10 | [Group( "Stats" )] public int StartingAmount { get; set; } = 1;
11 | [Group( "Stats" )] public int Cost { get; set; } = 20;
12 | [Group( "Attack" )] public RangedFloat AmountFired { get; set; } = new RangedFloat( 1 );
13 | [Group( "Attack" )] public RangedFloat Damage { get; set; } = new RangedFloat( 8 );
14 | [Group( "Attack" )] public float Spray { get; set; } = 1f;
15 | [Group( "Attack" )] public GameObject Prefab { get; set; }
16 | }
--------------------------------------------------------------------------------
/code/Inputs/AttackingInput.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Sandbox;
3 |
4 | namespace Battlebugs;
5 |
6 | public sealed class AttackingInput : Component
7 | {
8 | public static AttackingInput Instance { get; private set; }
9 |
10 | [Property] GameObject ReticlePrefab { get; set; }
11 |
12 | GameObject Reticle = null;
13 | Vector3 ReticlePosition = Vector3.Zero;
14 | Vector3 ReticleOffset = Vector3.Zero;
15 | public int ReticleState = 0;
16 | SoundHandle AimingSound = null;
17 |
18 | protected override void OnAwake()
19 | {
20 | Instance = this;
21 | Enabled = false;
22 | }
23 |
24 | protected override void OnUpdate()
25 | {
26 | if ( !BoardManager.Local.IsValid() ) return;
27 |
28 | var tr = Scene.Trace.Ray( Scene.Camera.ScreenPixelToRay( Mouse.Position ), 8000f )
29 | .WithoutTags( "bug" )
30 | .Run();
31 |
32 | if ( ReticleState == 0 )
33 | {
34 | if ( !ShopPanel.Instance.IsOpen && !PauseMenu.Instance.IsOpen && tr.Hit && tr.GameObject.Components.TryGet<CellComponent>( out var cell ) )
35 | {
36 | if ( cell.Board != BoardManager.Local )
37 | {
38 | if ( !Reticle.IsValid() )
39 | {
40 | if ( ReticlePrefab is not null )
41 | {
42 | CreateReticle( tr.HitPosition );
43 | }
44 | }
45 | else
46 | {
47 | Reticle.WorldPosition = tr.HitPosition.WithZ( BoardManager.Local.WorldPosition.z ) + Vector3.Up * 4f;
48 | }
49 | }
50 | }
51 | else if ( Reticle.IsValid() )
52 | {
53 | DestroyReticle();
54 | }
55 | }
56 | else if ( Reticle.IsValid() )
57 | {
58 | if ( ReticleState == 1 )
59 | {
60 | Reticle.WorldPosition = ReticlePosition + Vector3.Forward * MathF.Sin( Time.Now * 5f ) * 72f;
61 | }
62 | else if ( ReticleState == 2 )
63 | {
64 | Reticle.WorldPosition = ReticlePosition + ReticleOffset + Vector3.Right * MathF.Sin( Time.Now * 5f ) * 72f;
65 | }
66 | }
67 |
68 | if ( Reticle.IsValid() && Input.Pressed( "Attack1" ) && BoardManager.Local.WeaponInventory[BoardManager.Local.SelectedWeapon] != 0 )
69 | {
70 | if ( ReticleState == 0 )
71 | {
72 | AimingSound?.Stop();
73 | AimingSound = Sound.Play( "aiming-loop" );
74 | }
75 | Sound.Play( "aiming-click" );
76 | ReticleState++;
77 | ReticleOffset = Reticle.WorldPosition - ReticlePosition;
78 | if ( ReticleState < 2 ) ReticlePosition = Reticle.WorldPosition;
79 | if ( ReticleState == 3 )
80 | {
81 | BoardManager.Local.WeaponInventory[BoardManager.Local.SelectedWeapon]--;
82 | GameManager.Instance.BroadcastFire( BoardManager.Local.Id, BoardManager.Local.SelectedWeapon.ResourceId, Reticle.WorldPosition );
83 | DestroyReticle();
84 | }
85 | }
86 |
87 | if ( Input.Pressed( "Attack2" ) && ReticleState == 1 )
88 | {
89 | Sound.Play( "ui.navigate.back" );
90 | ReticleState--;
91 | if ( ReticleState == 0 )
92 | {
93 | AimingSound?.Stop();
94 | AimingSound = null;
95 | }
96 | }
97 | }
98 |
99 | protected override void OnDisabled()
100 | {
101 | DestroyReticle();
102 | }
103 |
104 | void CreateReticle( Vector3 position )
105 | {
106 | Reticle = ReticlePrefab.Clone( position );
107 | }
108 |
109 | void DestroyReticle()
110 | {
111 | AimingSound?.Stop();
112 | AimingSound = null;
113 | Reticle?.Destroy();
114 | Reticle = null;
115 | ReticleState = 0;
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/code/Inputs/InspectInput.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Sandbox;
3 |
4 | namespace Battlebugs;
5 |
6 | public sealed class InspectInput : Component
7 | {
8 | public static InspectInput Instance { get; private set; }
9 | public BugSegment HighlightedSegment { get; private set; } = null;
10 |
11 | TimeSince timeSinceMouseMoved = 0;
12 | bool isPanelHovered = false;
13 |
14 | Vector2 lastMousePosition = Vector2.Zero;
15 |
16 | protected override void OnAwake()
17 | {
18 | Instance = this;
19 | Enabled = false;
20 | }
21 |
22 | protected override void OnUpdate()
23 | {
24 | if ( isPanelHovered ) return;
25 | if ( !BoardManager.Local.IsValid() ) return;
26 | if ( Mouse.Position != lastMousePosition )
27 | {
28 | lastMousePosition = Mouse.Position;
29 | timeSinceMouseMoved = 0;
30 | }
31 |
32 | var tr = Scene.Trace.Ray( Scene.Camera.ScreenPixelToRay( Mouse.Position ), 8000f )
33 | .WithAnyTags( "bug" )
34 | .Run();
35 |
36 | if ( tr.Hit )
37 | {
38 | var bug = tr.GameObject.Components.Get<BugSegment>();
39 | if ( bug is not null && bug.IsVisible )
40 | {
41 | Deselect();
42 | Select( bug );
43 | }
44 | }
45 | else
46 | {
47 | Deselect();
48 | }
49 |
50 | if ( HighlightedSegment is not null && timeSinceMouseMoved > 0.5f && InspectorPanel.Instance.Segment is null )
51 | {
52 | InspectorPanel.Instance.Segment = HighlightedSegment;
53 | }
54 | }
55 |
56 | protected override void OnDisabled()
57 | {
58 | Deselect();
59 | }
60 |
61 | public void Select( BugSegment bug, bool panelHovered = false )
62 | {
63 | isPanelHovered = panelHovered;
64 | HighlightedSegment = bug;
65 | HighlightedSegment.AddHighlight( Color.White );
66 | }
67 |
68 | public void Deselect()
69 | {
70 | isPanelHovered = false;
71 | if ( HighlightedSegment is null ) return;
72 |
73 | InspectorPanel.Instance.Segment = null;
74 | HighlightedSegment?.RemoveHighlight();
75 | HighlightedSegment = null;
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/code/Inputs/PlacementInput.cs:
--------------------------------------------------------------------------------
1 | using Sandbox;
2 |
3 | namespace Battlebugs;
4 |
5 | public sealed class PlacementInput : Component
6 | {
7 | public static PlacementInput Instance { get; private set; }
8 |
9 | public CellComponent HighlightedCell { get; set; } = null;
10 | public List<CellComponent> SelectedCells { get; set; } = new();
11 | public bool IsSelecting { get; private set; } = false;
12 | public BugResource AttemptingToPlace { get; private set; } = null;
13 |
14 | protected override void OnAwake()
15 | {
16 | Instance = this;
17 | Enabled = false;
18 | }
19 |
20 | protected override void OnUpdate()
21 | {
22 | if ( !BoardManager.Local.IsValid() ) return;
23 |
24 | var tr = Scene.Trace.Ray( Scene.Camera.ScreenPixelToRay( Mouse.Position ), 8000f )
25 | .WithoutTags( "bug" )
26 | .Run();
27 |
28 | if ( !PauseMenu.Instance.IsOpen && tr.Hit )
29 | {
30 | var newCell = tr.GameObject.Components.Get<CellComponent>();
31 | if ( newCell?.Board != BoardManager.Local ) return;
32 | if ( newCell != HighlightedCell )
33 | {
34 | HighlightedCell?.MouseExit();
35 | HighlightedCell = newCell;
36 | HighlightedCell?.MouseEnter();
37 |
38 | if ( IsSelecting )
39 | {
40 | if ( SelectedCells.Count > 1 && HighlightedCell == SelectedCells.ElementAt( SelectedCells.Count - 2 ) )
41 | {
42 | AttemptingToPlace = SelectedCells.Count > 0 ? SelectedCells.FirstOrDefault().Board.BugInventory.FirstOrDefault( x => x.Key.SegmentCount == SelectedCells.Count - 1 ).Key : null;
43 | SelectedCells.LastOrDefault()?.Deselect();
44 | }
45 |
46 | if ( HighlightedCell is not null && !HighlightedCell.IsOccupied && !SelectedCells.Contains( HighlightedCell ) && (SelectedCells.Count == 0 || SelectedCells.LastOrDefault().IsAdjacent( HighlightedCell )) && SelectedCells.Count < (BoardManager.Local?.MaxPlaceableSegments ?? 0) )
47 | {
48 | AttemptingToPlace = SelectedCells.Count > 0 ? SelectedCells.FirstOrDefault().Board.BugInventory.FirstOrDefault( x => x.Key.SegmentCount == SelectedCells.Count + 1 ).Key : null;
49 | HighlightedCell?.Select();
50 | }
51 | }
52 | }
53 | }
54 | else
55 | {
56 | HighlightedCell?.MouseExit();
57 | HighlightedCell = null;
58 | }
59 |
60 | if ( Input.Pressed( "Attack1" ) )
61 | {
62 | HighlightedCell?.MousePressed();
63 |
64 | IsSelecting = true;
65 | DeselectAll();
66 | if ( HighlightedCell is not null && !HighlightedCell.IsOccupied )
67 | {
68 | HighlightedCell?.Select();
69 | }
70 | }
71 |
72 | if ( HighlightedCell is not null && Input.Pressed( "Attack2" ) )
73 | {
74 | IsSelecting = false;
75 | DeselectAll();
76 |
77 | var bugs = Scene.GetAllComponents<BugSegment>();
78 | var cellBug = bugs.FirstOrDefault( x => x.Cell == HighlightedCell );
79 | if ( cellBug is not null )
80 | {
81 | BoardManager.Local.BugInventory[cellBug.Bug]++;
82 | bugs = bugs.Where( x => x.GameObject.Name == cellBug.GameObject.Name ).ToList();
83 | foreach ( var bug in bugs )
84 | {
85 | bug.Cell.IsOccupied = false;
86 | bug.BroadcastDestroyFX();
87 | bug.GameObject.Destroy();
88 | }
89 | }
90 | BoardManager.Local.IsReady = false;
91 | }
92 |
93 | if ( Input.Released( "Attack1" ) )
94 | {
95 | HighlightedCell?.MouseReleased();
96 |
97 | IsSelecting = false;
98 |
99 | if ( SelectedCells.Count > 1 )
100 | {
101 | Sound.Play( "ui-select-complete" );
102 | GameManager.Instance.CreateBug( BoardManager.Local, GetPlacementData() );
103 | }
104 | else if ( SelectedCells.Count == 1 )
105 | {
106 | Sound.Play( "ui-select-bug" );
107 | }
108 | DeselectAll();
109 | }
110 | }
111 |
112 | public List<PlacementData> GetPlacementData( List<CellComponent> cells = null, BugResource bug = null )
113 | {
114 | if ( cells is null ) cells = SelectedCells.ToList();
115 | if ( bug is null ) bug = AttemptingToPlace;
116 | var data = new List<PlacementData>();
117 |
118 | var rotation = new Angles( 0, 0, 0 );
119 | if ( cells.Count > 1 ) rotation = Rotation.LookAt( cells[1].WorldPosition - cells[0].WorldPosition, Vector3.Up );
120 |
121 | for ( int i = 0; i < cells.Count; i++ )
122 | {
123 | if ( i < cells.Count - 1 ) rotation = Rotation.LookAt( cells[i + 1].WorldPosition - cells[i].WorldPosition, Vector3.Up );
124 | else if ( i > 0 ) rotation = Rotation.LookAt( cells[i].WorldPosition - cells[i - 1].WorldPosition, Vector3.Up );
125 |
126 | // Get prefab based on rotation/position
127 | var prefab = bug.BodyPrefab;
128 | if ( i == 0 ) prefab = bug.TailPrefab;
129 | else if ( i == cells.Count - 1 ) prefab = bug.HeadPrefab;
130 | else
131 | {
132 | var last = cells[i - 1];
133 | var next = cells[i + 1];
134 | var current = cells[i];
135 | if ( !(last.Position.x == next.Position.x || last.Position.y == next.Position.y) )
136 | {
137 | prefab = bug.CornerPrefab;
138 | rotation = Rotation.LookAt( last.WorldPosition - next.WorldPosition, Vector3.Up );
139 | var previousRotation = Rotation.LookAt( last.WorldPosition - current.WorldPosition, Vector3.Up );
140 |
141 | // If previous piece is left and next piece is down
142 | if ( last.WorldPosition.x < current.WorldPosition.x && next.WorldPosition.y > current.WorldPosition.y )
143 | {
144 | rotation -= new Angles( 0, 90 + 45, 0 );
145 | }
146 | // If rotating left
147 | else if ( rotation.yaw - previousRotation.Yaw() > 0 )
148 | {
149 | rotation -= new Angles( 0, 45 + 90, 0 );
150 | }
151 | else
152 | {
153 | rotation += new Angles( 0, 45, 0 );
154 | }
155 | }
156 | }
157 |
158 | data.Add( new PlacementData
159 | {
160 | Cell = cells[i],
161 | Rotation = rotation,
162 | Prefab = prefab
163 | } );
164 | }
165 |
166 | return data;
167 | }
168 |
169 | protected override void OnDisabled()
170 | {
171 | if ( SelectedCells.Count > 0 )
172 | {
173 | DeselectAll();
174 | }
175 | if ( HighlightedCell.IsValid() )
176 | {
177 | HighlightedCell.MouseExit();
178 | }
179 | }
180 |
181 | void DeselectAll()
182 | {
183 | foreach ( var cell in SelectedCells )
184 | {
185 | cell.Deselect( false );
186 | }
187 | SelectedCells.Clear();
188 | }
189 |
190 | public class PlacementData
191 | {
192 | public CellComponent Cell { get; set; }
193 | public Rotation Rotation { get; set; }
194 | public GameObject Prefab { get; set; }
195 | }
196 | }
197 |
--------------------------------------------------------------------------------
/code/PebbleComponent.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Sandbox;
3 |
4 | namespace Battlebugs;
5 |
6 | public sealed class PebbleComponent : Component, Component.ICollisionListener
7 | {
8 | [RequireComponent] Rigidbody Rigidbody { get; set; }
9 |
10 | [Property] GameObject ParticlePrefab { get; set; }
11 | [Property] int HitCount { get; set; } = 1;
12 |
13 | [Sync] public float Damage { get; set; } = 4f;
14 | List<BugSegment> HitSegments = new();
15 | List<CellComponent> HitCells = new();
16 |
17 | public TimeSince TimeSinceCreated = 0;
18 | TimeSince timeSinceMoving = 0;
19 |
20 | public void LaunchAt( Vector3 target )
21 | {
22 | var time = Random.Shared.Float( 1.8f, 2f );
23 | var vector = target - WorldPosition.WithZ( target.z );
24 | var direction = vector.Normal;
25 | var velocity = vector / time;
26 | var verticalForce = -Scene.PhysicsWorld.Gravity.z * time / 2f;
27 | velocity += Vector3.Up * verticalForce;
28 |
29 | Rigidbody.Velocity = velocity;
30 | Rigidbody.AngularVelocity = Vector3.Random * 10f;
31 | }
32 |
33 | protected override void OnFixedUpdate()
34 | {
35 | if ( IsProxy ) return;
36 |
37 | if ( TimeSinceCreated > 10f )
38 | {
39 | GameObject.Destroy();
40 | }
41 |
42 | if ( Rigidbody.Velocity.Length > 1f )
43 | {
44 | timeSinceMoving = 0;
45 | }
46 | else if ( timeSinceMoving > 0.5f )
47 | {
48 | HitCount = 0;
49 | Hit();
50 | }
51 | }
52 |
53 | public void OnCollisionStart( Collision collision )
54 | {
55 | if ( IsProxy ) return;
56 | if ( collision.Other.GameObject.Tags.Has( "pebble" ) ) return;
57 |
58 | // Enable friction after hitting something
59 | Rigidbody.LinearDamping = 2.5f;
60 | Rigidbody.AngularDamping = 2f;
61 |
62 | if ( collision.Other.GameObject.Components.TryGet<BugSegment>( out var segment ) )
63 | {
64 | if ( HitSegments.Contains( segment ) ) return;
65 | HitSegments.Add( segment );
66 | GameManager.Instance.BroadcastDamageNumber( WorldPosition, Damage );
67 | segment.Damage( Damage );
68 | Hit();
69 | }
70 | else if ( collision.Other.GameObject.Components.TryGet<CellComponent>( out var cell ) )
71 | {
72 | if ( HitCells.Contains( cell ) ) return;
73 | if ( cell.IsHit ) return;
74 | HitCells.Add( cell );
75 | cell.BroadcastHit();
76 | HitCount--;
77 | if ( HitCount == 0 ) Hit();
78 | }
79 | }
80 |
81 | void Hit()
82 | {
83 | HitCount--;
84 | if ( HitCount <= 0 )
85 | {
86 | BroadcastDestroyEffect();
87 | GameObject.Destroy();
88 | }
89 | }
90 |
91 | [Rpc.Broadcast]
92 | void BroadcastDestroyEffect()
93 | {
94 | Sound.Play( "break-rocks", WorldPosition );
95 | if ( ParticlePrefab is not null )
96 | {
97 | ParticlePrefab.Clone( WorldPosition );
98 | }
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/code/UI/Controls/InputGlyph.razor:
--------------------------------------------------------------------------------
1 | @using Sandbox
2 | @using Sandbox.UI
3 | @namespace Battlebugs
4 | @inherits Sandbox.UI.Panel
5 | @attribute [StyleSheet]
6 |
7 | <root />
8 |
9 | @code {
10 | private string _inputAction;
11 | private InputGlyphSize _inputGlyphSize;
12 | private GlyphStyle _glyphStyle;
13 |
14 | public void SetButton(string inputAction)
15 | {
16 | if (_inputAction == inputAction)
17 | return;
18 |
19 | _inputAction = inputAction;
20 | Update();
21 | }
22 |
23 | public override void SetProperty(string name, string value)
24 | {
25 | switch (name)
26 | {
27 | case "button":
28 | {
29 | SetButton(value);
30 | Update();
31 |
32 | break;
33 | }
34 |
35 | case "size":
36 | {
37 | InputGlyphSize.TryParse(value, true, out _inputGlyphSize);
38 | Update();
39 |
40 | break;
41 | }
42 |
43 | case "style":
44 | {
45 | _glyphStyle = value switch
46 | {
47 | "knockout" => GlyphStyle.Knockout,
48 | "light" => GlyphStyle.Light,
49 | "dark" => GlyphStyle.Dark,
50 | _ => _glyphStyle
51 | };
52 |
53 | Update();
54 | break;
55 | }
56 | }
57 |
58 | base.SetProperty(name, value);
59 | }
60 |
61 | private void Update()
62 | {
63 | var texture = Input.GetGlyph(_inputAction, _inputGlyphSize, _glyphStyle);
64 | Style.BackgroundImage = texture;
65 | Style.Width = texture.Width;
66 | Style.Height = texture.Height;
67 | }
68 | }
--------------------------------------------------------------------------------
/code/UI/Controls/InputGlyph.razor.scss:
--------------------------------------------------------------------------------
1 | InputGlyph
2 | {
3 | background-size: contain;
4 | background-repeat: no-repeat;
5 | }
--------------------------------------------------------------------------------
/code/UI/PanelComponents/BugHealthbar.razor:
--------------------------------------------------------------------------------
1 | @using Sandbox;
2 | @using Sandbox.UI;
3 | @namespace Battlebugs
4 | @inherits PanelComponent
5 |
6 | <root style="opacity: @(Alpha)">
7 | <div class="inner">
8 | @if (true)
9 | {
10 | var amount = Segment.Health / Segment.Bug.StartingHealth;
11 | <div class="fill" style="background-color: @(Gradient.Evaluate(1f - amount).Hex); width: @(amount * 100f)%;" )" />
12 | }
13 | </div>
14 | </root>
15 |
16 | @code
17 | {
18 | [Property] BugSegment Segment { get; set; }
19 | [Property] public float Alpha { get; set; } = 0f;
20 | [Property] Gradient Gradient { get; set; }
21 |
22 | protected override int BuildHash() => System.HashCode.Combine(Alpha, Segment.Health);
23 | }
--------------------------------------------------------------------------------
/code/UI/PanelComponents/BugHealthbar.razor.scss:
--------------------------------------------------------------------------------
1 | BugHealthbar
2 | {
3 | position: absolute;
4 | top: 0;
5 | left: 0;
6 | right: 0;
7 | bottom: 0;
8 | background-color: #444;
9 | padding: 16px;
10 | border-radius: 32px;
11 | overflow: hidden;
12 |
13 | .inner
14 | {
15 | width: 100%;
16 | height: 100%;
17 |
18 | .fill
19 | {
20 | height: 100%;
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/code/UI/PanelComponents/InspectorPanel.razor:
--------------------------------------------------------------------------------
1 | @using Sandbox;
2 | @using Sandbox.UI;
3 | @namespace Battlebugs
4 | @inherits PanelComponent
5 |
6 |
7 | <root class="@(Segment is null ? "hidden" : "")">
8 | @if (Segment is null) return;
9 | <div class="header">
10 | <h1>@Segment.Bug.Name</h1>
11 | <div class="segments">
12 | @for (var i = 0; i < Segment.Bug.SegmentCount; i++)
13 | {
14 | var index = Segment.Bug.SegmentCount - 1 - i;
15 | var segment = Scene.GetAllComponents<BugSegment>().FirstOrDefault(x => x.GameObject.Name == Segment.GameObject.Name && x.Index == index);
16 | @* <div class="@(segment == Segment ? "this" : "")" style="background-color: @((segment?.Bug?.Color ?? Color.Black).Hex)" /> *@
17 | <img class="@(segment == Segment ? "this" : "")" src="@GetIcon(Segment.Bug, index)" />
18 | }
19 | </div>
20 | </div>
21 | <div class="healthbar">
22 | <div class="inner">
23 | @if (true)
24 | {
25 | var value = Segment.Health / Segment.Bug.StartingHealth;
26 | <div class="fill" style="background-color: @(HealthGradient.Evaluate(1f - value).Hex); width: @(value * 100f)%;" />
27 | }
28 | </div>
29 | </div>
30 | </root>
31 |
32 | @code
33 | {
34 | public static InspectorPanel Instance { get; private set; }
35 | public BugSegment Segment { get; set; }
36 |
37 | [Property] Gradient HealthGradient { get; set; }
38 |
39 | protected override void OnAwake()
40 | {
41 | Instance = this;
42 | }
43 |
44 | protected override void OnUpdate()
45 | {
46 | if (!Segment.IsValid()) return;
47 |
48 | var pos = Scene.Camera.PointToScreenPixels(Segment.WorldPosition);
49 | pos.x /= Scene.Camera.ScreenRect.Size.x;
50 | pos.y /= Scene.Camera.ScreenRect.Size.y;
51 | pos *= 100f;
52 |
53 | Panel.Style.Left = Length.Percent(pos.x + 2.5f);
54 | Panel.Style.Bottom = Length.Percent(100f - pos.y);
55 | }
56 |
57 | string GetIcon(BugResource bug, int index)
58 | {
59 | if (index == 0) return bug.GetTailIcon();
60 | if (index == bug.SegmentCount - 1) return bug.GetHeadIcon();
61 | return bug.GetBodyIcon();
62 | }
63 |
64 | protected override int BuildHash() => System.HashCode.Combine(Segment);
65 | }
--------------------------------------------------------------------------------
/code/UI/PanelComponents/InspectorPanel.razor.scss:
--------------------------------------------------------------------------------
1 | InspectorPanel {
2 | position: absolute;
3 | width: 300px;
4 | height: 100px;
5 | background-color: rgba(black, 0.7);
6 | opacity: 1;
7 | transition: opacity 0.2s ease-out;
8 | padding: 8px;
9 | color: white;
10 | font-family: "paper cuts 2";
11 | font-size: 16px;
12 | border-radius: 16px;
13 | flex-direction: column;
14 | gap: 8px;
15 | justify-content: space-evenly;
16 |
17 | .header {
18 | .segments {
19 | flex-grow: 1;
20 | justify-content: flex-end;
21 | gap: 8px;
22 |
23 | > img {
24 | width: 24px;
25 | height: 24px;
26 |
27 | &.this {
28 | background-color: rgba(white, 0.1);
29 | }
30 | }
31 | }
32 | }
33 |
34 | .healthbar {
35 | width: 100%;
36 | height: 32px;
37 | padding: 4px;
38 | background-color: #333;
39 |
40 | .inner {
41 | width: 100%;
42 | height: 100%;
43 | background-color: black;
44 |
45 | .fill {
46 | height: 100%;
47 | }
48 | }
49 | }
50 |
51 | &.hidden {
52 | opacity: 0;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/code/UI/PanelComponents/MainHudPanel.razor:
--------------------------------------------------------------------------------
1 | @using Sandbox;
2 | @using Sandbox.UI;
3 | @namespace Battlebugs
4 | @inherits PanelComponent
5 |
6 | <root>
7 | <div class="enable-mouse" />
8 |
9 | @if (!BoardManager.Local.IsValid())
10 | {
11 | @* Spectating UI *@
12 | <GameHud />
13 |
14 | return;
15 | }
16 |
17 | @if (!(GameManager.Instance.State == GameState.Waiting || GameManager.Instance.State == GameState.Results))
18 | {
19 | <GameHud />
20 | <ControlsPanel />
21 | }
22 |
23 | @if (GameManager.Instance.State == GameState.Waiting)
24 | {
25 | <WaitingHud />
26 | }
27 | else if (GameManager.Instance.State == GameState.Placing)
28 | {
29 | <PlacingHud />
30 | }
31 | else if (GameManager.Instance.State == GameState.Playing)
32 | {
33 | <SidePanel />
34 | <WeaponHud />
35 | <ShopPanel />
36 | }
37 | else if (GameManager.Instance.State == GameState.Results)
38 | {
39 | <ResultsHud />
40 | }
41 | </root>
42 |
43 | @code
44 | {
45 | protected override int BuildHash() => System.HashCode.Combine(BoardManager.Local, GameManager.Instance.State);
46 | }
--------------------------------------------------------------------------------
/code/UI/PanelComponents/MainHudPanel.razor.scss:
--------------------------------------------------------------------------------
1 | MainHudPanel {
2 | position: absolute;
3 | top: 0;
4 | left: 0;
5 | right: 0;
6 | bottom: 0;
7 | font-family: "paper cuts 2";
8 | cursor: "bug-default";
9 |
10 | .enable-mouse {
11 | position: absolute;
12 | top: 0;
13 | left: 0;
14 | width: 0px;
15 | height: 0px;
16 | pointer-events: all;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/code/UI/PanelComponents/MainMenu.razor:
--------------------------------------------------------------------------------
1 | @using Sandbox;
2 | @using Sandbox.UI;
3 | @namespace Battlebugs
4 | @inherits PanelComponent
5 |
6 | <root>
7 | <div class="title">BATTLEBUGS</div>
8 | <div class="content">
9 | <div class="button-container">
10 | @if (State == MenuState.Main)
11 | {
12 | <div class="button" onclick="@(() => ButtonPlay())">Play</div>
13 | <div class="button blue" onclick="@(() => ButtonStats())">Stats</div>
14 | <div class="button red" onclick="@(() => Quit())">Quit</div>
15 | }
16 | else if (State == MenuState.Play)
17 | {
18 | <div class="button" onclick="@(() => StartGame(true))">Bot Match</div>
19 | <div class="button" onclick="@(() => StartGame(false))">Host Game</div>
20 | <div class="button red" onclick="@(() => ButtonBack())">Back</div>
21 | }
22 | else if (State == MenuState.Stats)
23 | {
24 | <div class="button red" onclick="@(() => ButtonBack())">Back</div>
25 | }
26 | </div>
27 | @if (State == MenuState.Play)
28 | {
29 | <div class="content-panel">
30 | <LobbyList />
31 | </div>
32 | }
33 | else if (State == MenuState.Stats)
34 | {
35 | <div class="content-panel">
36 | <StatsPanel />
37 | </div>
38 | }
39 | </div>
40 | </root>
41 |
42 | @code
43 | {
44 | public static MainMenu Instance { get; private set; }
45 | public static bool IsCpuGame { get; private set; } = true;
46 |
47 | public MenuState State = MenuState.Main;
48 | Angles cameraAngles = Angles.Zero;
49 |
50 | public enum MenuState
51 | {
52 | Main,
53 | Play,
54 | Stats
55 | }
56 |
57 | protected override void OnAwake()
58 | {
59 | Instance = this;
60 | }
61 |
62 | protected override void OnStart()
63 | {
64 | cameraAngles = Scene.Camera.WorldRotation;
65 | }
66 |
67 | protected override void OnUpdate()
68 | {
69 | var camAngles = cameraAngles + new Angles(Mouse.Position.y - (Screen.Height / 2f), -Mouse.Position.x - (-Screen.Width / 2f), 0) * 0.0015f;
70 | Scene.Camera.WorldRotation = Angles.Lerp(Scene.Camera.WorldRotation, camAngles, Time.Delta * 5.0f);
71 | }
72 |
73 | void ButtonPlay()
74 | {
75 | State = MenuState.Play;
76 | Sound.Play("ui.button.press");
77 | }
78 |
79 | void ButtonStats()
80 | {
81 | State = MenuState.Stats;
82 | Sound.Play("ui.button.press");
83 | }
84 |
85 | void ButtonBack()
86 | {
87 | State = MenuState.Main;
88 | Sound.Play("ui.navigate.back");
89 | }
90 |
91 | void StartGame(bool isCpuGame = false)
92 | {
93 | IsCpuGame = isCpuGame;
94 | Game.ActiveScene.LoadFromFile("scenes/game.scene");
95 | }
96 |
97 | void Quit()
98 | {
99 | if (Game.IsEditor)
100 | {
101 | Log.Info("Quit");
102 | }
103 | else
104 | {
105 | Game.Close();
106 | }
107 | }
108 |
109 | protected override int BuildHash() => System.HashCode.Combine(State);
110 | }
111 |
--------------------------------------------------------------------------------
/code/UI/PanelComponents/MainMenu.razor.scss:
--------------------------------------------------------------------------------
1 | MainMenu
2 | {
3 | position: absolute;
4 | top: 0;
5 | left: 0;
6 | right: 0;
7 | bottom: 0;
8 | justify-content: center;
9 | align-items: center;
10 | font-weight: bold;
11 | font-family: "paper cuts 2";
12 | pointer-events: all;
13 | cursor: "bug-default";
14 | background-color: rgba(black, 0.2);
15 | backdrop-filter: blur( 1px );
16 | flex-direction: column;
17 | padding: 50px;
18 |
19 | .title
20 | {
21 | align-self: flex-end;
22 | font-size: 128px;
23 | color: #fff;
24 | text-shadow: 8px 8px 0px black;
25 | flex-shrink: 0;
26 | }
27 |
28 | > .content
29 | {
30 | gap: 32px;
31 | width: 100%;
32 | height: 100%;
33 |
34 | .button-container
35 | {
36 | flex-direction: column;
37 | justify-content: flex-end;
38 | flex-shrink: 0;
39 | gap: 8px;
40 | }
41 |
42 | .content-panel
43 | {
44 | height: 100%;
45 | flex-grow: 1;
46 | background-color: rgba(black, 0.8);
47 | }
48 | }
49 |
50 | .button
51 | {
52 | width: 500px;
53 | background-color: #4CAF50;
54 | border: none;
55 | color: white;
56 | padding: 15px 32px;
57 | text-align: center;
58 | font-size: 54px;
59 | margin: 4px 2px;
60 | cursor: pointer;
61 | box-shadow: 6px 6px 0px black;
62 | cursor: "bug-hover";
63 |
64 | &:hover
65 | {
66 | background-color: #45a049;
67 | sound-in: ui.button.over;
68 | }
69 |
70 | &.red
71 | {
72 | background-color: #f44336;
73 |
74 | &:hover
75 | {
76 | background-color: #d32f2f;
77 | }
78 | }
79 |
80 | &.blue
81 | {
82 | background-color: #2196F3;
83 |
84 | &:hover
85 | {
86 | background-color: #1e87f0;
87 | }
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/code/UI/PanelComponents/PauseMenu.razor:
--------------------------------------------------------------------------------
1 | @using Sandbox;
2 | @using Sandbox.UI;
3 | @using Sandbox.Network;
4 | @namespace Battlebugs
5 | @inherits PanelComponent
6 |
7 | <root class="@(IsOpen ? "show" : "")">
8 | <label class="title">Paused</label>
9 | <div class="content">
10 | <button onclick=@(() => IsOpen = false)>Resume</button>
11 | <button class="red" onclick=@(() => ReturnToMainMenu())>Quit to Main Menu</button>
12 | </div>
13 | </root>
14 |
15 | @code
16 | {
17 | public static PauseMenu Instance { get; private set; }
18 | [Property] SceneFile SceneFile { get; set; }
19 |
20 | public bool IsOpen { get; private set; }
21 |
22 | protected override void OnAwake()
23 | {
24 | Instance = this;
25 | }
26 |
27 | protected override void OnUpdate()
28 | {
29 | if (Input.EscapePressed)
30 | {
31 | IsOpen = !IsOpen;
32 | Input.EscapePressed = false;
33 | }
34 | }
35 |
36 | public void ReturnToMainMenu()
37 | {
38 | Networking.Disconnect();
39 | Game.ActiveScene.Load(SceneFile);
40 | }
41 |
42 | protected override int BuildHash() => System.HashCode.Combine(IsOpen);
43 | }
--------------------------------------------------------------------------------
/code/UI/PanelComponents/PauseMenu.razor.scss:
--------------------------------------------------------------------------------
1 | PauseMenu
2 | {
3 | position: absolute;
4 | top: 0;
5 | left: 0;
6 | right: 0;
7 | bottom: 0;
8 | background-color: rgba(black, 0.9);
9 | z-index: 1000;
10 | cursor: "bug-default";
11 | font-family: "paper cuts 2";
12 | color: white;
13 | font-size: 32px;
14 | padding: 256px;
15 | justify-content: center;
16 | align-items: center;
17 | flex-direction: column;
18 | pointer-events: all;
19 | opacity: 0;
20 | transition: all 0.1s ease-in-out;
21 |
22 | .title
23 | {
24 | font-size: 128px;
25 | text-shadow: 8px 8px 0px black;
26 | }
27 |
28 | > .content
29 | {
30 | flex-grow: 1;
31 | flex-direction: column;
32 | justify-content: center;
33 | align-items: center;
34 | gap: 8px;
35 |
36 | button
37 | {
38 | width: 500px;
39 | background-color: #4CAF50;
40 | border: none;
41 | color: white;
42 | padding: 15px 32px;
43 | text-align: center;
44 | font-size: 54px;
45 | margin: 4px 2px;
46 | cursor: pointer;
47 | box-shadow: 6px 6px 0px black;
48 | cursor: "bug-hover";
49 |
50 | &:hover
51 | {
52 | background-color: #45a049;
53 | sound-in: ui.button.over;
54 | }
55 |
56 | &:active
57 | {
58 | background-color: #3e8e41;
59 | sound-in: ui.button.press;
60 | }
61 |
62 | &.red
63 | {
64 | background-color: #f44336;
65 |
66 | &:hover
67 | {
68 | background-color: #d32f2f;
69 | }
70 |
71 | &:active
72 | {
73 | background-color: #c62828;
74 | }
75 | }
76 | }
77 | }
78 |
79 | &.show
80 | {
81 | opacity: 1;
82 | }
83 |
84 | }
--------------------------------------------------------------------------------
/code/UI/Panels/BugList/BugList.razor:
--------------------------------------------------------------------------------
1 | @using Sandbox;
2 | @using Sandbox.UI;
3 | @namespace Battlebugs
4 | @inherits Panel
5 | @attribute [StyleSheet]
6 |
7 | <root>
8 | <div class="section">
9 | <label class="header">Your Bugs</label>
10 | <div class="bugs">
11 | @foreach (var reference in BoardManager.Local.BugReferences.OrderBy(x => x.GetBug().SegmentCount))
12 | {
13 | <BugListEntry BugReference=@reference />
14 | }
15 | </div>
16 | </div>
17 | <div class="section">
18 | <label class="header">Opponent's Bugs</label>
19 | <div class="bugs">
20 | @foreach (var reference in BoardManager.Opponent.BugReferences.OrderBy(x => x.GetBug().SegmentCount))
21 | {
22 | <BugListEntry BugReference=@reference />
23 | }
24 | </div>
25 | </div>
26 | </root>
27 |
28 | @code
29 | {
30 | protected override int BuildHash() => System.HashCode.Combine("");
31 | }
--------------------------------------------------------------------------------
/code/UI/Panels/BugList/BugList.razor.scss:
--------------------------------------------------------------------------------
1 | BugList
2 | {
3 | padding: 8px;
4 | flex-direction: column;
5 | align-items: center;
6 | transition: left 1s ease-out;
7 |
8 | .section
9 | {
10 | flex-direction: column;
11 | }
12 |
13 | .header
14 | {
15 | font-weight: 700;
16 |
17 | &:not(:first-child) {
18 | margin-top: 16px;
19 | }
20 | }
21 |
22 | .bugs
23 | {
24 | flex-direction: column;
25 | width: 100%;
26 | gap: 4px;
27 | }
28 | }
--------------------------------------------------------------------------------
/code/UI/Panels/BugList/BugListEntry.razor:
--------------------------------------------------------------------------------
1 | @using System;
2 | @using Sandbox;
3 | @using Sandbox.UI;
4 | @namespace Battlebugs
5 | @inherits Panel
6 | @attribute [StyleSheet]
7 |
8 | <root>
9 | @for (int i = 0; i < Bug.SegmentCount; i++)
10 | {
11 | <BugListSegment GameObject=@GetObject(BugReference.ObjectIds[Bug.SegmentCount - 1 - i]) Bug=@Bug Index=@i />
12 | }
13 | </root>
14 |
15 | @code
16 | {
17 | public BoardManager.BugReference BugReference { get; set; }
18 |
19 | BugResource Bug => BugReference.GetBug();
20 | Dictionary<string, GameObject> _gameObjects = new();
21 |
22 | GameObject GetObject(string id)
23 | {
24 | if (!_gameObjects.TryGetValue(id, out var obj))
25 | {
26 | obj = Scene.Directory.FindByGuid(Guid.Parse(id));
27 | _gameObjects[id] = obj;
28 | }
29 | return obj;
30 | }
31 |
32 | protected override int BuildHash() => System.HashCode.Combine(BugReference);
33 | }
--------------------------------------------------------------------------------
/code/UI/Panels/BugList/BugListEntry.razor.scss:
--------------------------------------------------------------------------------
1 | BugListEntry
2 | {
3 | flex-direction: row;
4 | gap: 2px;
5 | pointer-events: all;
6 | }
--------------------------------------------------------------------------------
/code/UI/Panels/BugList/BugListSegment.razor:
--------------------------------------------------------------------------------
1 | @using Sandbox;
2 | @using Sandbox.UI;
3 | @namespace Battlebugs
4 | @inherits Panel
5 | @attribute [StyleSheet]
6 |
7 | <root class="@(GameObject.IsValid() ? "" : "dead")">
8 | <img src="@Icon" />
9 | </root>
10 |
11 | @code
12 | {
13 | public GameObject GameObject { get; set; }
14 | public BugResource Bug { get; set; }
15 | public int Index { get; set; }
16 | BugSegment segment = null;
17 |
18 | string Icon
19 | {
20 | get
21 | {
22 | if (Bug is not null)
23 | {
24 | if (Index == 0) return Bug.GetHeadIcon();
25 | else if (Index == Bug.SegmentCount - 1) return Bug.GetTailIcon();
26 | else return Bug.GetBodyIcon();
27 | }
28 | return "";
29 | }
30 | }
31 |
32 | protected override void OnMouseMove(MousePanelEvent e)
33 | {
34 | if (GameObject.IsValid() && GameObject.Components.TryGet<BugSegment>(out segment))
35 | {
36 | if (segment.IsVisible)
37 | {
38 | InspectInput.Instance.Deselect();
39 | InspectInput.Instance.Select(segment, true);
40 | InspectorPanel.Instance.Segment = segment;
41 | }
42 | }
43 | }
44 |
45 | protected override void OnMouseOut(MousePanelEvent e)
46 | {
47 | if (segment is not null && InspectInput.Instance.HighlightedSegment == segment)
48 | {
49 | InspectInput.Instance.Deselect();
50 | }
51 | }
52 |
53 | protected override int BuildHash() => System.HashCode.Combine(GameObject.IsValid());
54 | }
--------------------------------------------------------------------------------
/code/UI/Panels/BugList/BugListSegment.razor.scss:
--------------------------------------------------------------------------------
1 | BugListSegment
2 | {
3 | img
4 | {
5 | width: 32px;
6 | height: 32px;
7 | flex-shrink: 0;
8 | font-size: 12px;
9 | justify-content: flex-end;
10 | align-items: flex-end;
11 |
12 | label
13 | {
14 | position: relative;
15 | top: 8px;
16 | }
17 | }
18 |
19 | &.dead
20 | {
21 | img
22 | {
23 | filter: brightness(-100);
24 | }
25 | }
26 |
27 | &:hover
28 | {
29 | background-color: rgba(white, 0.1);
30 | }
31 | }
--------------------------------------------------------------------------------
/code/UI/Panels/GameHud/ControlsPanel.razor:
--------------------------------------------------------------------------------
1 | @using Sandbox;
2 | @using Sandbox.UI;
3 | @namespace Battlebugs
4 | @inherits Panel
5 | @attribute [StyleSheet]
6 |
7 | <root class="@(CanSee() ? "show" : "")">
8 | <div class="container">
9 | <label class="header">Controls</label>
10 | <div class="inputs">
11 | @if (GameManager.Instance.State == GameState.Placing)
12 | {
13 | <div class="input">
14 | <InputGlyph button="Attack1" />
15 | <label>Drag to Create Bug</label>
16 | </div>
17 | <div class="input">
18 | <InputGlyph button="Attack2" />
19 | <label>Cancel Drag / Pick Up</label>
20 | </div>
21 | }
22 | else
23 | {
24 | <div class="input">
25 | <InputGlyph button="Attack1" />
26 | <label>@(AttackingInput.Instance.ReticleState == 0 ? "Click to Fire" : "Confirm")</label>
27 | </div>
28 | @if (AttackingInput.Instance.ReticleState == 1)
29 | {
30 | <div class="input">
31 | <InputGlyph button="Attack2" />
32 | <label>Cancel</label>
33 | </div>
34 | }
35 | }
36 | </div>
37 | </div>
38 | </root>
39 |
40 | @code
41 | {
42 | bool CanSee()
43 | {
44 | return GameManager.Instance.State == GameState.Placing || (GameManager.Instance.CurrentPlayerId == BoardManager.Local.Network.OwnerId && GameManager.Instance.IsFiring);
45 | }
46 |
47 | protected override int BuildHash() => System.HashCode.Combine(GameManager.Instance.State, AttackingInput.Instance.ReticleState, GameManager.Instance.CurrentPlayerId, GameManager.Instance.IsFiring);
48 | }
--------------------------------------------------------------------------------
/code/UI/Panels/GameHud/ControlsPanel.razor.scss:
--------------------------------------------------------------------------------
1 | ControlsPanel {
2 | position: absolute;
3 | top: 0px;
4 | right: 0px;
5 | bottom: 0px;
6 | flex-direction: column;
7 | justify-content: center;
8 | color: white;
9 | font-family: "paper cuts 2";
10 | font-size: 16px;
11 |
12 | .container {
13 | position: relative;
14 | left: 300px;
15 | padding: 8px;
16 | background-color: rgba(black, 0.5);
17 | flex-direction: column;
18 | align-items: center;
19 | width: 250px;
20 | transition: left 1s ease-out;
21 |
22 | .header {
23 | font-weight: 700;
24 | }
25 |
26 | .inputs {
27 | flex-direction: column;
28 | width: 100%;
29 |
30 | .input {
31 | align-items: center;
32 | gap: 4px;
33 | }
34 | }
35 | }
36 |
37 | &.show {
38 | .container {
39 | left: 0px;
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/code/UI/Panels/GameHud/GameHud.razor:
--------------------------------------------------------------------------------
1 | @using System;
2 | @using Sandbox;
3 | @using Sandbox.UI;
4 | @namespace Battlebugs
5 | @inherits Panel
6 | @attribute [StyleSheet]
7 |
8 | <root>
9 | <div class="header">
10 | @if (GameManager.Instance.Boards.Count > 0)
11 | {
12 | <PlayerHud [email protected](0) />
13 | }
14 | <div class="middle">
15 | <div class="bar">
16 | @if (GameManager.Instance.Boards.Count > 0)
17 | {
18 | var fill = GameManager.Instance.Boards.ElementAt(0).GetScorePercent() * 100f;
19 | <div class="fill" style="width: @(fill)%" />
20 | }
21 | </div>
22 | <div class="banner">
23 | <label>@GetHeader()</label>
24 | <div class="timer-bar @(GameManager.Instance.IsFiring ? "show" : "")">
25 | <div class="fill" style="width: @(100f - GameManager.Instance.TimeSinceTurnStart / 15f * 100f)%" />
26 | </div>
27 | </div>
28 | </div>
29 | @if (GameManager.Instance.Boards.Count > 1)
30 | {
31 | <PlayerHud [email protected](1) />
32 | }
33 | </div>
34 | </root>
35 |
36 | @code
37 | {
38 | public static GameHud Instance { get; private set; }
39 |
40 | protected override void OnAfterTreeRender(bool firstTime)
41 | {
42 | base.OnAfterTreeRender(firstTime);
43 |
44 | if (firstTime)
45 | {
46 | Instance = this;
47 | }
48 | }
49 |
50 | string GetHeader()
51 | {
52 | if (!BoardManager.Local.IsValid() && GameManager.Instance.Boards.Count >= 2)
53 | {
54 | if (GameManager.Instance.State == GameState.Placing)
55 | {
56 | return "Waiting for players to place bugs...";
57 | }
58 | else if (GameManager.Instance.CurrentPlayer == GameManager.Instance.Boards.FirstOrDefault())
59 | {
60 | return
quot;It's {GameManager.Instance.Boards.FirstOrDefault().GameObject.Name}'s turn!";
61 | }
62 |
63 | return
quot;It's {GameManager.Instance.Boards.LastOrDefault().GameObject.Name}'s turn!";
64 | }
65 |
66 | if (GameManager.Instance.State == GameState.Placing)
67 | {
68 | return "Place your bugs!";
69 | }
70 | if (GameManager.Instance.CurrentPlayer == BoardManager.Local)
71 | {
72 | return "It's your turn!";
73 | }
74 | return "Your opponent is thinking...";
75 | }
76 |
77 | protected override int BuildHash() => System.HashCode.Combine(GameManager.Instance.State, GameManager.Instance.IsFiring ? GameManager.Instance.TimeSinceTurnStart.ToString() : "");
78 | }
--------------------------------------------------------------------------------
/code/UI/Panels/GameHud/GameHud.razor.scss:
--------------------------------------------------------------------------------
1 | GameHud {
2 | position: absolute;
3 | top: 0;
4 | left: 0;
5 | right: 0;
6 | bottom: 0;
7 | justify-content: center;
8 | align-items: center;
9 | font-family: "paper cuts 2";
10 | font-size: 32px;
11 | color: white;
12 |
13 | .header {
14 | position: absolute;
15 | top: 0;
16 | left: 0;
17 | right: 0;
18 | justify-content: center;
19 | font-weight: 700;
20 |
21 | .middle
22 | {
23 | flex-direction: column;
24 | align-items: center;
25 | width: 1200px;
26 |
27 | .bar
28 | {
29 | width: 100%;
30 | height: 24px;
31 | background-color: #FF3600;
32 |
33 | .fill
34 | {
35 | position: relative;
36 | top: 0;
37 | left: 0;
38 | height: 100%;
39 | background-color: #0094FF;
40 | }
41 | }
42 |
43 | .banner
44 | {
45 | width: 80%;
46 | height: 100px;
47 | justify-content: center;
48 | align-items: center;
49 | background-color: rgba(black, 0.7);
50 | font-size: 38px;
51 | border-radius: 0px 0px 32px 32px;
52 | flex-direction: column;
53 | gap: 8px;
54 |
55 | .timer-bar
56 | {
57 | width: 890;
58 | height: 8px;
59 | background-color: #ffffff3f;
60 | opacity: 0;
61 | transition: opacity 1s ease-out;
62 |
63 | .fill
64 | {
65 | position: relative;
66 | top: 0;
67 | left: 0;
68 | height: 100%;
69 | background-color: #ffffff;
70 | }
71 |
72 | &.show
73 | {
74 | opacity: 1;
75 | }
76 | }
77 | }
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/code/UI/Panels/GameHud/PlayerHud/ChatEntry.razor:
--------------------------------------------------------------------------------
1 | @using Sandbox;
2 | @using Sandbox.UI;
3 | @namespace Battlebugs
4 | @inherits Panel
5 | @attribute [StyleSheet]
6 |
7 | <root>
8 | @Message
9 | </root>
10 |
11 | @code
12 | {
13 | public string Message { get; set; }
14 |
15 | TimeSince timeSinceCreated = 0;
16 |
17 | public override void Tick()
18 | {
19 | base.Tick();
20 |
21 | if (timeSinceCreated > 10 && !HasClass("fade"))
22 | {
23 | Delete();
24 | }
25 | }
26 |
27 | protected override int BuildHash() => System.HashCode.Combine(Message);
28 | }
--------------------------------------------------------------------------------
/code/UI/Panels/GameHud/PlayerHud/PlayerHud.razor:
--------------------------------------------------------------------------------
1 | @using Sandbox;
2 | @using Sandbox.UI;
3 | @namespace Battlebugs
4 | @inherits Panel
5 | @attribute [StyleSheet]
6 |
7 | <root>
8 | <div class="player">
9 | <img src="ui/player-banner.png" />
10 | <div class="content">
11 | @if (Board.Network.Owner is null)
12 | {
13 | <img src="ui/cpu.png" />
14 | }
15 | else
16 | {
17 | <img src=@(
quot;avatar:{Board.Network.Owner.SteamId}") />
18 | }
19 | <span class="name">@Board.GameObject.Name</span>
20 | <span class="score">🪙 @Board.Coins</span>
21 | </div>
22 | </div>
23 | <div class="chat-container">
24 | @if (Board == BoardManager.Local)
25 | {
26 | <TextEntry @ref="InputBox" onsubmit=@OnMessageSend AllowEmojiReplace=@(true) />
27 | }
28 | <div class="messages" @ref="ChatMessages" />
29 | </div>
30 | </root>
31 |
32 | @code
33 | {
34 | public BoardManager Board { get; set; }
35 | TextEntry InputBox { get; set; }
36 | Panel ChatMessages { get; set; }
37 |
38 | public static List<PlayerHud> Instances { get; private set; } = new List<PlayerHud>();
39 |
40 | protected override void OnAfterTreeRender(bool firstTime)
41 | {
42 | base.OnAfterTreeRender(firstTime);
43 |
44 | if (firstTime)
45 | {
46 | Instances.Add(this);
47 | }
48 | }
49 |
50 | void OnMessageSend()
51 | {
52 | var message = InputBox.Text;
53 | if (string.IsNullOrWhiteSpace(message))
54 | {
55 | InputBox.Blur();
56 | return;
57 | }
58 |
59 | GameManager.Instance.SendChatMessage(message);
60 | InputBox.Text = "";
61 | }
62 |
63 | public override void Tick()
64 | {
65 | base.Tick();
66 |
67 | if (Input.Pressed("Chat"))
68 | {
69 | InputBox?.Focus();
70 | }
71 | }
72 |
73 | public override void OnDeleted()
74 | {
75 | Instances.Remove(this);
76 | }
77 |
78 | public void AddChatMessage(string message)
79 | {
80 | var entry = new ChatEntry();
81 | entry.Message = message;
82 | ChatMessages.AddChild(entry);
83 | StateHasChanged();
84 | }
85 |
86 | protected override int BuildHash() => System.HashCode.Combine(Board?.Coins, InputBox?.HasFocus);
87 | }
--------------------------------------------------------------------------------
/code/UI/Panels/GameHud/PlayerHud/PlayerHud.razor.scss:
--------------------------------------------------------------------------------
1 | PlayerHud {
2 | flex-direction: column;
3 | align-items: center;
4 | width: 160px;
5 | gap: 8px;
6 |
7 | .player {
8 | position: relative;
9 | flex-direction: column;
10 | color: black;
11 | font-size: 24px;
12 | width: 160px;
13 | height: 294px;
14 | color: white;
15 | text-shadow: 1px 1px 1px black;
16 |
17 | > img {
18 | position: absolute;
19 | width: 100%;
20 | height: 100%;
21 | background-size: 100%;
22 | z-index: -5;
23 | }
24 |
25 | .content {
26 | flex-direction: column;
27 | padding: 16px;
28 | height: 260px;
29 | gap: 4px;
30 | align-items: center;
31 |
32 | img {
33 | aspect-ratio: 1;
34 | width: 100%;
35 | }
36 | }
37 | }
38 |
39 | .chat-container {
40 | width: 240px;
41 | flex-direction: column;
42 | gap: 8px;
43 |
44 | TextEntry {
45 | width: 100%;
46 | min-height: 50px;
47 | height: auto;
48 | background-color: rgb(231, 208, 144);
49 | border: 2px solid rgb(155, 134, 77);
50 | border-radius: 8px;
51 | color: black;
52 | pointer-events: all;
53 | font-size: 16px;
54 | display: none;
55 | cursor: "bug-textedit";
56 |
57 | > label {
58 | max-width: 240px;
59 | white-space: normal;
60 | }
61 |
62 | &:focus {
63 | display: flex;
64 | }
65 | }
66 |
67 | .messages {
68 | flex-direction: column-reverse;
69 | gap: 8px;
70 | color: black;
71 |
72 | ChatEntry {
73 | width: 100%;
74 | background-color: rgb(255, 255, 255);
75 | border: 2px solid rgb(167, 167, 167);
76 | border-radius: 8px;
77 | font-size: 18px;
78 | padding: 2px 4px;
79 | opacity: 1;
80 | transition: opacity 0.25s ease-out;
81 | text-align: center;
82 | justify-content: center;
83 |
84 | &:intro {
85 | opacity: 0;
86 | }
87 |
88 | &:outro {
89 | transition: opacity 2s linear;
90 | opacity: 0;
91 | }
92 | }
93 | }
94 | }
95 |
96 | &:last-child {
97 | .player {
98 | > img {
99 | filter: hue-rotate(140deg);
100 | }
101 | }
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/code/UI/Panels/GameHud/SidePanel.razor:
--------------------------------------------------------------------------------
1 | @using Sandbox;
2 | @using Sandbox.UI;
3 | @namespace Battlebugs
4 | @inherits Panel
5 | @attribute [StyleSheet]
6 |
7 | <root>
8 | <div class="container">
9 | <BugList />
10 | <button onclick=@(() => ShopPanel.Instance.Open())><i>shopping_cart</i>Shop</button>
11 | </div>
12 | </root>
13 |
14 | @code
15 | {
16 | protected override int BuildHash() => System.HashCode.Combine("");
17 | }
--------------------------------------------------------------------------------
/code/UI/Panels/GameHud/SidePanel.razor.scss:
--------------------------------------------------------------------------------
1 | SidePanel {
2 | position: absolute;
3 | top: 150px;
4 | left: 0px;
5 | bottom: 0px;
6 | flex-direction: column;
7 | justify-content: center;
8 | color: white;
9 | font-family: "paper cuts 2";
10 | font-size: 16px;
11 |
12 | .container {
13 | padding: 8px;
14 | background-color: rgba(black, 0.5);
15 | flex-direction: column;
16 | align-items: center;
17 | width: 250px;
18 | transition: left 1s ease-out;
19 |
20 | button {
21 | padding: 4px;
22 | color: white;
23 | font-family: "paper cuts 2";
24 | font-size: 16px;
25 | cursor: "bug-hover";
26 | transition: background-color 0.1s ease-out;
27 | align-items: center;
28 | gap: 8px;
29 | width: 100%;
30 | justify-content: center;
31 | pointer-events: all;
32 | background-color: #4CAF50;
33 |
34 | &:hover {
35 | background-color: #45a049;
36 | }
37 |
38 | &:active {
39 | background-color: #3e8e41;
40 | sound-in: ui.button.press;
41 | }
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/code/UI/Panels/GameHud/WeaponHud/WeaponButton.razor:
--------------------------------------------------------------------------------
1 | @using Sandbox;
2 | @using Sandbox.UI;
3 | @namespace Battlebugs
4 | @inherits Panel
5 | @attribute [StyleSheet]
6 |
7 | <root class=@GetClass()>
8 | <div class="icon">
9 | <img [email protected] />
10 | <label>@(Value < 0 ? "∞" :
quot;x{Value}")</label>
11 | </div>
12 | <label>@Weapon.Name</label>
13 | <div class="border" />
14 |
15 | <div class="info-panel">
16 | <div class="header">
17 | <img [email protected] />
18 | <div class="info">
19 | <label class="name">@Weapon.Name</label>
20 | <label class="count">Count: @(Value < 0 ? "∞" : Value.ToString())</label>
21 | </div>
22 | </div>
23 | <label>@Weapon.Description</label>
24 | </div>
25 | </root>
26 |
27 | @code
28 | {
29 | public WeaponResource Weapon { get; set; }
30 |
31 | int Value => BoardManager.Local.WeaponInventory[Weapon];
32 |
33 | string GetClass()
34 | {
35 | string str = BoardManager.Local.SelectedWeapon == Weapon ? "selected" : "";
36 | if (Value == 0)
37 | {
38 | str += " disabled";
39 | }
40 | return str;
41 | }
42 | protected override void OnMouseDown(MousePanelEvent e)
43 | {
44 | if (Value == 0) return;
45 |
46 | BoardManager.Local.SelectedWeapon = Weapon;
47 | }
48 |
49 | protected override int BuildHash() => System.HashCode.Combine(BoardManager.Local.SelectedWeapon, Value);
50 | }
--------------------------------------------------------------------------------
/code/UI/Panels/GameHud/WeaponHud/WeaponButton.razor.scss:
--------------------------------------------------------------------------------
1 | WeaponButton {
2 | position: relative;
3 | flex-direction: column;
4 | align-items: center;
5 | color: white;
6 | font-family: "paper cuts 2";
7 | font-size: 24px;
8 | gap: 8px;
9 | padding: 8px 32px;
10 | text-shadow: 0 0 4px black;
11 | background-color: rgba(black, 0.2);
12 | pointer-events: all;
13 | cursor: "bug-hover";
14 | width: 160px;
15 | justify-content: space-evenly;
16 |
17 | .icon {
18 | position: relative;
19 |
20 | img {
21 | width: 64px;
22 | height: 64px;
23 | }
24 |
25 | > label {
26 | position: absolute;
27 | bottom: -8px;
28 | right: -24px;
29 | }
30 | }
31 |
32 | > label {
33 | text-align: center;
34 | }
35 |
36 | > .info-panel {
37 | position: absolute;
38 | top: 0px;
39 | left: -64px;
40 | right: -64px;
41 | height: 0px;
42 | background-color: rgba(black, 0.8);
43 | overflow: hidden;
44 | opacity: 0;
45 | transition: all 0.3s ease;
46 | flex-direction: column;
47 | padding: 8px;
48 | font-size: 18px;
49 | gap: 8px;
50 |
51 | .header {
52 | gap: 8px;
53 | font-size: 24px;
54 |
55 | img {
56 | width: 64px;
57 | height: 64px;
58 | }
59 |
60 | .info {
61 | flex-direction: column;
62 |
63 | .count {
64 | font-size: 18px;
65 | }
66 | }
67 | }
68 | }
69 |
70 | > .border {
71 | position: absolute;
72 | top: 0;
73 | left: 0;
74 | right: 0;
75 | bottom: 0;
76 | }
77 |
78 | &:hover {
79 | background-color: rgba(black, 0.5);
80 | sound-in: ui.button.over;
81 |
82 | > .info-panel {
83 | top: -215px;
84 | height: 215px;
85 | opacity: 1;
86 | }
87 | }
88 |
89 | &:active {
90 | sound-in: ui.button.press;
91 | }
92 |
93 | &.selected {
94 | > .border {
95 | border: 4px solid white;
96 | }
97 | }
98 |
99 | &.disabled {
100 | cursor: "bug-disabled";
101 | opacity: 0.5;
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/code/UI/Panels/GameHud/WeaponHud/WeaponHud.razor:
--------------------------------------------------------------------------------
1 | @using Sandbox;
2 | @using Sandbox.UI;
3 | @namespace Battlebugs
4 | @inherits Panel
5 | @attribute [StyleSheet]
6 |
7 | <root>
8 | @if (GameManager.Instance.CurrentPlayer != BoardManager.Local || !GameManager.Instance.IsFiring) return;
9 |
10 | <div class="container">
11 | <div class="weapons">
12 | @foreach (var entry in BoardManager.Local.WeaponInventory)
13 | {
14 | <WeaponButton [email protected] />
15 | }
16 | </div>
17 | </div>
18 | </root>
19 |
20 | @code
21 | {
22 | protected override int BuildHash() => System.HashCode.Combine(GameManager.Instance.IsFiring, GameManager.Instance.CurrentPlayerId);
23 | }
--------------------------------------------------------------------------------
/code/UI/Panels/GameHud/WeaponHud/WeaponHud.razor.scss:
--------------------------------------------------------------------------------
1 | WeaponHud {
2 | position: absolute;
3 | left: 0;
4 | right: 0;
5 | bottom: 0;
6 | justify-content: center;
7 | align-items: center;
8 | font-family: "paper cuts 2";
9 | font-size: 32px;
10 | pointer-events: all;
11 |
12 | .container {
13 | background-color: rgba(black, 0.5);
14 | padding: 16px;
15 | padding-bottom: 0px;
16 |
17 | .weapons {
18 | gap: 16px;
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/code/UI/Panels/HintPanel/HintPanel.razor:
--------------------------------------------------------------------------------
1 | @using Sandbox;
2 | @using Sandbox.UI;
3 | @namespace Battlebugs
4 | @inherits PanelComponent
5 |
6 | <root>
7 | </root>
8 |
9 | @code
10 | {
11 | public static HintPanel Instance { get; private set; }
12 |
13 | public record Entry(string Icon, string Text, float Duration);
14 |
15 | List<Entry> Queue = new();
16 | TimeSince timeSinceLastCheck = 0;
17 |
18 | protected override void OnAwake()
19 | {
20 | Instance = this;
21 | timeSinceLastCheck = 0;
22 | }
23 |
24 | protected override void OnFixedUpdate()
25 | {
26 | if (timeSinceLastCheck > 4f)
27 | {
28 | timeSinceLastCheck = 0;
29 | if (Queue.Count > 0)
30 | {
31 | CreateNotification(Queue[0]);
32 | Queue.RemoveAt(0);
33 | }
34 | }
35 | }
36 |
37 | public void AddEntry(string icon, string text, float duration = 6f)
38 | {
39 | Queue.Add(new Entry(icon, text, duration));
40 | }
41 |
42 | void CreateNotification(Entry entry)
43 | {
44 | var panelEntry = new HintPanelEntry();
45 | panelEntry.Entry = entry;
46 | Panel.AddChild(panelEntry);
47 | StateHasChanged();
48 | Sound.Play("notify-hint");
49 | }
50 |
51 | bool hasSeenRedCell = false;
52 | public void RedCellNotification()
53 | {
54 | if (hasSeenRedCell) return;
55 |
56 | AddEntry("color:#ff0000", "A red cell means there is no bug anywhere on that cell.", 12f);
57 | hasSeenRedCell = true;
58 | }
59 |
60 | bool hasSeenYellowCell = false;
61 | public void YellowCellNotification()
62 | {
63 | if (hasSeenYellowCell) return;
64 |
65 | AddEntry("color:#ffff00", "A yellow cell means there is still a bug on that cell.\nIf the cell is yellow and the bug is not visible, you haven't hit the bug yet.", 15f);
66 | hasSeenYellowCell = true;
67 | }
68 |
69 | bool hasSeenGreenCell = false;
70 | public void GreenCellNotification()
71 | {
72 | if (hasSeenGreenCell) return;
73 |
74 | AddEntry("color:#00ff00", "Once you've destroyed a bug on a given cell, that cell will turn green.\nThe bug panel on the left will also show the destroyed segment.", 16f);
75 | hasSeenGreenCell = true;
76 | }
77 |
78 | bool hasSeenCellCoin = false;
79 | public void CellCoinNotification()
80 | {
81 | if (hasSeenCellCoin) return;
82 |
83 | AddEntry("🪙", "Some cells contain coins. Coins can be spent at the shop to re-stock your weapons.", 12f);
84 | hasSeenCellCoin = true;
85 | }
86 |
87 | protected override int BuildHash() => System.HashCode.Combine("");
88 | }
--------------------------------------------------------------------------------
/code/UI/Panels/HintPanel/HintPanel.razor.scss:
--------------------------------------------------------------------------------
1 | HintPanel {
2 | position: absolute;
3 | bottom: 32px;
4 | right: 32px;
5 | font-family: Tahoma;
6 | font-size: 20px;
7 | font-weight: bold;
8 | color: white;
9 | text-shadow: 1px 1px 0 black;
10 | align-items: flex-end;
11 | gap: 8px;
12 | flex-direction: column;
13 | font-family: "paper cuts 2";
14 |
15 | HintPanelEntry {
16 | position: relative;
17 | top: 0px;
18 | left: 0px;
19 | opacity: 1;
20 | gap: 12px;
21 | background-color: rgba(black, 0.5);
22 | // in-out-bounce bezier
23 | transition: all 0.4s elastic-in;
24 | padding: 10px 20px;
25 | border-radius: 4px;
26 | align-items: center;
27 | white-space: pre;
28 |
29 | .color-icon
30 | {
31 | width: 32px;
32 | height: 32px;
33 | border-radius: 8px;
34 | box-shadow: 2px 2px 4px rgba(black, 0.5);
35 | }
36 |
37 | i
38 | {
39 | font-size: 28px;
40 | }
41 |
42 | &:intro {
43 | top: 32px;
44 | left: 300px;
45 | opacity: 0;
46 | }
47 |
48 | &:outro {
49 | opacity: 0;
50 | left: 300px;
51 | transition: all 0.4s ease-in;
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/code/UI/Panels/HintPanel/HintPanelEntry.razor:
--------------------------------------------------------------------------------
1 | @using Sandbox;
2 | @using Sandbox.UI;
3 | @namespace Battlebugs
4 | @inherits Panel
5 |
6 | <root>
7 | @if (Entry.Icon.StartsWith("color:"))
8 | {
9 | var color = Entry.Icon.Substring(6);
10 | <div class="color-icon" style="background-color: @color" />
11 | }
12 | else
13 | {
14 | <i>@Entry.Icon</i>
15 | }
16 | <label class="message">@Entry.Text</label>
17 | </root>
18 |
19 | @code
20 | {
21 |
22 | public HintPanel.Entry Entry { get; set; }
23 | TimeSince timeSinceCreated = 0;
24 |
25 | public override void Tick()
26 | {
27 | if (timeSinceCreated > Entry.Duration)
28 | {
29 | Delete();
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/code/UI/Panels/LobbyList.razor:
--------------------------------------------------------------------------------
1 | @using System.Threading.Tasks;
2 | @using Sandbox;
3 | @using Sandbox.UI;
4 | @using Sandbox.Network;
5 | @namespace Battlebugs
6 | @inherits Panel
7 | @attribute [StyleSheet]
8 |
9 | <root>
10 | <label class="header">Lobbies</label>
11 | <div class="lobbies">
12 | @if (!refreshing && list.Count > 0)
13 | {
14 | @foreach (var lobby in list)
15 | {
16 | <div class="lobby" onclick=@(() => JoinLobby(lobby))>
17 | <img src="ui/gamepad.png" />
18 | <div class="info">
19 | <label class="name">@lobby.Name</label>
20 | @* <label class="desc">Looking for opponent...</label> *@
21 | </div>
22 | <div class="players">
23 | <i>person</i>
24 | <label>@lobby.Members/2</label>
25 | </div>
26 | </div>
27 | }
28 | }
29 | else
30 | {
31 | <div class="no-lobbies">
32 | No lobbies found...
33 | </div>
34 | }
35 | @* Uncomment the block below to preview a lobby entry *@
36 | @* <div class="lobby">
37 | <img src="ui/gamepad.png" />
38 | <div class="info">
39 | <label class="name">Carson vs Bakscratch</label>
40 | <label class="desc">Waiting for players...</label>
41 | </div>
42 | <div class="players">
43 | <i>person</i>
44 | <label>1/2</label>
45 | </div>
46 | </div> *@
47 | </div>
48 | </root>
49 |
50 | @code
51 | {
52 | List<LobbyInformation> list = new();
53 | bool refreshing = true;
54 |
55 | protected override void OnAfterTreeRender(bool firstTime)
56 | {
57 | base.OnAfterTreeRender(firstTime);
58 |
59 | if (firstTime)
60 | {
61 | _ = RefreshLobbyList();
62 | }
63 | }
64 |
65 | async Task RefreshLobbyList()
66 | {
67 | while (true)
68 | {
69 | await Refresh();
70 | await Task.DelayRealtimeSeconds(5f);
71 | }
72 | }
73 |
74 | async Task Refresh()
75 | {
76 | refreshing = true;
77 | StateHasChanged();
78 |
79 | list = await Networking.QueryLobbies();
80 |
81 | refreshing = false;
82 | StateHasChanged();
83 | }
84 |
85 | void JoinLobby(LobbyInformation lobby)
86 | {
87 | Networking.Connect(lobby.LobbyId);
88 | }
89 |
90 | protected override int BuildHash() => System.HashCode.Combine("");
91 | }
--------------------------------------------------------------------------------
/code/UI/Panels/LobbyList.razor.scss:
--------------------------------------------------------------------------------
1 | LobbyList
2 | {
3 | width: 100%;
4 | height: 100%;
5 | padding: 32px;
6 | color: white;
7 | font-size: 42px;
8 | flex-direction: column;
9 | gap: 16px;
10 | justify-content: space-evenly;
11 | pointer-events: all;
12 |
13 | .header
14 | {
15 | font-size: 72px;
16 | }
17 |
18 | .lobbies
19 | {
20 | flex-grow: 1;
21 | background-color: rgba(black, 0.7);
22 | overflow-x: hidden;
23 | overflow-y: scroll;
24 | padding: 16px;
25 | flex-direction: column;
26 |
27 | .lobby
28 | {
29 | width: 100%;
30 | background-color: rgba(black, 0.5);
31 | padding: 16px;
32 | flex-shrink: 0;
33 | cursor: "bug-hover";
34 | align-items: center;
35 | gap: 16px;
36 |
37 | img
38 | {
39 | width: 72px;
40 | height: 72px;
41 | }
42 |
43 | > .info
44 | {
45 | flex-direction: column;
46 | font-size: 32px;
47 |
48 | .desc
49 | {
50 | font-size: 24px;
51 | }
52 | }
53 |
54 | > .players
55 | {
56 | flex-grow: 1;
57 | justify-content: flex-end;
58 | align-items: center;
59 | gap: 8px;
60 | padding-right: 16px;
61 | }
62 |
63 | &:hover
64 | {
65 | background-color: rgba(white, 0.01);
66 | }
67 |
68 | &:active
69 | {
70 | background-color: rgba(white, 0.02);
71 | }
72 | }
73 | }
74 | }
--------------------------------------------------------------------------------
/code/UI/Panels/PlacingHud.razor:
--------------------------------------------------------------------------------
1 | @using Sandbox;
2 | @using Sandbox.UI;
3 | @namespace Battlebugs
4 | @inherits Panel
5 | @attribute [StyleSheet]
6 |
7 | <root>
8 | @if (!BoardManager.Local.IsValid()) return;
9 |
10 | <div class="bug-inventory">
11 | <div class="container">
12 | <label class="header">Bug Inventory</label>
13 | <div class="content">
14 | @if (true)
15 | {
16 | @foreach (var bugItem in BoardManager.Local.BugInventory.OrderBy(x => x.Key.SegmentCount))
17 | {
18 | <div class="bug-inventory-item">
19 | <div class="bug">
20 | @for (int i = 0; i < bugItem.Key.SegmentCount; i++)
21 | {
22 | <img src=@GetIcon(bugItem.Key, i) />
23 | }
24 | </div>
25 | <label>x@(bugItem.Value)</label>
26 | </div>
27 | }
28 | }
29 | </div>
30 | </div>
31 | </div>
32 | <div class="footer">
33 | @if (GameManager.Instance.Boards.Any(x => x.IsReady))
34 | {
35 | <label>@(BoardManager.Local.IsReady ? "Waiting for opponent..." : "Opponent is ready...")</label>
36 | }
37 | <div class="buttons">
38 | <button class="place-for-me" onclick=@(() => BoardManager.Local.SetupBoardRandomly())>Place Bugs For Me</button>
39 | <button class="clear-bugs" onclick=@(() => BoardManager.Local.ClearAllBugs())>Reset Bugs</button>
40 | <button class="@ReadyClasses()" Tooltip=@ReadyTooltip() onclick=@ToggleReady>@ReadyText()</button>
41 | </div>
42 | </div>
43 | </root>
44 |
45 | @code
46 | {
47 | string ReadyClasses()
48 | {
49 | string classes = "ready-up";
50 | if (BoardManager.Local?.BugInventory?.Any(x => x.Value > 0) ?? true)
51 | {
52 | classes += " disabled";
53 | }
54 | return classes;
55 | }
56 |
57 | string ReadyTooltip()
58 | {
59 | if (BoardManager.Local?.BugInventory?.Any(x => x.Value > 0) ?? true)
60 | {
61 | return "You must place all bugs before you can ready up!";
62 | }
63 | return "";
64 | }
65 |
66 | string ReadyText()
67 | {
68 | return BoardManager.Local.IsReady ? "Un-Ready" : "Ready";
69 | }
70 |
71 | void ToggleReady()
72 | {
73 | if (BoardManager.Local?.BugInventory?.Any(x => x.Value > 0) ?? true) return;
74 |
75 | BoardManager.Local.ToggleReady();
76 | }
77 |
78 | string GetIcon(BugResource bug, int index)
79 | {
80 | if (index == 0) return bug.GetHeadIcon();
81 | if (index == bug.SegmentCount - 1) return bug.GetTailIcon();
82 | return bug.GetBodyIcon();
83 | }
84 |
85 | protected override int BuildHash() => System.HashCode.Combine(BoardManager.Local, BoardManager.Local?.BugInventory.Sum(x => x.Value), BoardManager.Local?.IsReady, GameManager.Instance.Boards.Any(x => x.IsReady));
86 | }
--------------------------------------------------------------------------------
/code/UI/Panels/PlacingHud.razor.scss:
--------------------------------------------------------------------------------
1 | PlacingHud {
2 | position: absolute;
3 | top: 0;
4 | left: 0;
5 | right: 0;
6 | bottom: 0;
7 | font-family: "paper cuts 2";
8 |
9 | .bug-inventory {
10 | position: absolute;
11 | top: 0px;
12 | bottom: 0px;
13 | left: 0px;
14 | flex-direction: column;
15 | justify-content: center;
16 | gap: 8px;
17 |
18 | .container {
19 | flex-direction: column;
20 | justify-content: center;
21 | align-items: center;
22 | color: white;
23 | font-family: "paper cuts 2";
24 | font-size: 16px;
25 | padding: 8px;
26 | background-color: rgba(black, 0.5);
27 | gap: 8px;
28 |
29 | .header {
30 | font-weight: 700;
31 | gap: 8px;
32 | }
33 |
34 | .content {
35 | flex-direction: column;
36 | gap: 4px;
37 | }
38 | }
39 |
40 | .bug-inventory-item {
41 | gap: 12px;
42 | align-items: center;
43 |
44 | .bug {
45 | gap: 2px;
46 | width: 180px;
47 | justify-content: flex-end;
48 | }
49 |
50 | img {
51 | width: 32px;
52 | height: 32px;
53 | flex-shrink: 0;
54 | font-size: 12px;
55 | justify-content: flex-end;
56 | align-items: flex-end;
57 |
58 | label {
59 | position: relative;
60 | top: 8px;
61 | }
62 | }
63 |
64 | label {
65 | color: white;
66 | font-weight: 700;
67 | }
68 | }
69 | }
70 |
71 | .footer {
72 | position: absolute;
73 | bottom: 0;
74 | left: 0;
75 | right: 0;
76 | background-color: rgba(0, 0, 0, 0.5);
77 | color: white;
78 | font-size: 24px;
79 | padding: 10px;
80 | justify-content: center;
81 | pointer-events: all;
82 | flex-direction: column;
83 | gap: 12px;
84 |
85 | > * {
86 | width: 100%;
87 | justify-content: center;
88 | text-align: center;
89 | }
90 |
91 | button {
92 | background-color: #4CAF50;
93 | color: white;
94 | padding: 10px 24px;
95 | border: none;
96 | border-radius: 4px;
97 | cursor: "bug-hover";
98 | font-size: 24px;
99 | margin: 0 8px;
100 | transition: background-color 0.3s;
101 |
102 | &:hover {
103 | background-color: #45a049;
104 | }
105 |
106 | &.clear-bugs {
107 | background-color: #f44336;
108 |
109 | &:hover {
110 | background-color: #d32f2f;
111 | }
112 | }
113 |
114 | &.place-for-me {
115 | background-color: #2196F3;
116 |
117 | &:hover {
118 | background-color: #0d8bf2;
119 | }
120 | }
121 |
122 | &.ready-up {
123 |
124 | &.disabled {
125 | background-color: #666;
126 | cursor: "bug-disabled";
127 | }
128 | }
129 | }
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/code/UI/Panels/ResultsHud.razor:
--------------------------------------------------------------------------------
1 | @using Sandbox.UI;
2 | @namespace Battlebugs
3 | @inherits Panel
4 | @attribute [StyleSheet]
5 |
6 | <root>
7 | @if (!BoardManager.Local.IsValid()) return;
8 |
9 | <div class="container">
10 | <label class="title">Results</label>
11 | <div class="jumbotron">
12 | <div class="winner">
13 | <div class="info">
14 | @if (Winner.Network.Owner is null)
15 | {
16 | <img src="ui/cpu.png" />
17 | }
18 | else
19 | {
20 | <img src=@(
quot;avatar:{Winner.Network.Owner.SteamId}") />
21 | }
22 | <div class="info">
23 | <label class="name">@(Winner.GameObject.Name)</label>
24 | <label class="subtitle">Is the winner!</label>
25 | </div>
26 | <label class="crown">👑</label>
27 | </div>
28 | <div class="stats">
29 | <span class="coins">🪙 @Winner.Coins</span>
30 | <label>@Winner.CoinsSpent Coins Spent</label>
31 | <label>@Winner.BugsKilled Bugs Killed</label>
32 | </div>
33 | </div>
34 | <div class="divider">
35 | <div class="line" />
36 | <label>VS</label>
37 | <div class="line" />
38 | </div>
39 | <div class="loser">
40 | <div class="stats">
41 | <span class="coins">🪙 @Loser.Coins</span>
42 | <label>@Loser.CoinsSpent Coins Spent</label>
43 | <label>@Loser.BugsKilled Bugs Killed</label>
44 | </div>
45 | <div class="info">
46 | <div class="info">
47 | <label class="name">@(Loser.GameObject.Name)</label>
48 | <label class="subtitle">Is the loser!</label>
49 | </div>
50 | @if (Loser.Network.Owner is null)
51 | {
52 | <img src="ui/cpu.png" />
53 | }
54 | else
55 | {
56 | <img src=@(
quot;avatar:{Loser.Network.Owner.SteamId}") />
57 | }
58 | <label class="boot">😞</label>
59 | </div>
60 | </div>
61 | </div>
62 | <div class="content">
63 | <BugList />
64 | </div>
65 | <button onclick=@(() => PauseMenu.Instance?.ReturnToMainMenu())>Return to Main Menu</button>
66 | </div>
67 | </root>
68 |
69 | @code
70 | {
71 | BoardManager Winner => GameManager.Instance.Boards.FirstOrDefault(x => x.GetHealthPercent() != 0);
72 | BoardManager Loser => GameManager.Instance.Boards.FirstOrDefault(x => x.GetHealthPercent() <= 0);
73 | protected override int BuildHash() => System.HashCode.Combine("");
74 | }
--------------------------------------------------------------------------------
/code/UI/Panels/ResultsHud.razor.scss:
--------------------------------------------------------------------------------
1 | ResultsHud
2 | {
3 | position: absolute;
4 | top: 0;
5 | left: 0;
6 | right: 0;
7 | bottom: 0;
8 | justify-content: center;
9 | align-items: center;
10 | font-family: "paper cuts 2";
11 | font-size: 32px;
12 | background-color: rgba(black, 0.4);
13 | opacity: 1;
14 | transform: scale(1);
15 | transition: all 0.5s ease-out;
16 | pointer-events: all;
17 |
18 | > .container
19 | {
20 | width: 40%;
21 | height: 80%;
22 | background-color: rgba(black, 0.8);
23 | flex-direction: column;
24 | align-items: center;
25 | padding: 32px;
26 | color: white;
27 | gap: 16px;
28 |
29 | > .title
30 | {
31 | font-size: 48px;
32 | }
33 |
34 | > .jumbotron
35 | {
36 | flex-grow: 1;
37 | width: 100%;
38 | background-color: rgba(white, 0.05);
39 | padding: 24px;
40 | flex-direction: column;
41 | justify-content: space-between;
42 |
43 | > div
44 | {
45 | position: relative;
46 | align-items: center;
47 | width: 100%;
48 |
49 | .info
50 | {
51 | gap: 16px;
52 |
53 | img
54 | {
55 | width: 128px;
56 | height: 128px;
57 | flex-shrink: 0;
58 | }
59 |
60 | .info
61 | {
62 | flex-direction: column;
63 | gap: 0px;
64 | justify-content: center;
65 |
66 | .name
67 | {
68 | font-size: 48px;
69 | }
70 |
71 | .subtitle
72 | {
73 | color: grey;
74 | }
75 | }
76 | }
77 |
78 | .stats
79 | {
80 | flex-grow: 1;
81 | flex-direction: column;
82 | align-items: flex-end;
83 | font-size: 24px;
84 |
85 | .coins
86 | {
87 | font-size: 32px;
88 | }
89 | }
90 | }
91 |
92 | .divider
93 | {
94 | width: 100%;
95 | gap: 8px;
96 | align-items: center;
97 | color: rgb(114, 114, 114);
98 |
99 | .line
100 | {
101 | width: 100%;
102 | height: 2px;
103 | background-color: rgb(114, 114, 114);
104 | }
105 |
106 | label
107 | {
108 | position: relative;
109 | flex-shrink: 0;
110 | top: 2px;
111 | }
112 | }
113 |
114 | .winner
115 | {
116 | .crown
117 | {
118 | position: absolute;
119 | top: -64px;
120 | left: 21px;
121 | font-size: 64px;
122 | transform-origin: 50% 100%;
123 | animation: tilt-left-right 2s infinite ease-in-out alternate;
124 | }
125 | }
126 |
127 | .loser
128 | {
129 | justify-content: flex-end;
130 |
131 | .info
132 | {
133 | align-items: flex-end;
134 | }
135 |
136 | .stats
137 | {
138 | align-items: flex-start;
139 | }
140 |
141 | .boot
142 | {
143 | position: absolute;
144 | top: -42px;
145 | right: 72px;
146 | font-size: 64px;
147 | animation: tilt-left-right 2s infinite ease-in-out alternate;
148 | animation-delay: 0.5s;
149 | }
150 | }
151 | }
152 |
153 | > .content
154 | {
155 | width: 100%;
156 | flex-shrink: 0;
157 | padding: 16px;
158 | overflow-x: hidden;
159 | overflow-y: scroll;
160 | flex-direction: column;
161 | align-items: center;
162 | gap: 16px;
163 |
164 |
165 | BugList
166 | {
167 | width: 100%;
168 | align-items: center;
169 | flex-direction: row;
170 | justify-content: space-evenly;
171 | }
172 | }
173 | }
174 |
175 | button
176 | {
177 | background-color: #4CAF50;
178 | border: none;
179 | color: white;
180 | padding: 8px 32px;
181 | text-align: center;
182 | font-size: 32px;
183 | cursor: pointer;
184 | box-shadow: 6px 6px 0px black;
185 | cursor: "bug-hover";
186 |
187 | &:hover
188 | {
189 | background-color: #45a049;
190 | sound-in: ui.button.over;
191 | }
192 |
193 | &:active
194 | {
195 | background-color: #3e8e41;
196 | sound-in: ui.button.press;
197 | }
198 |
199 | }
200 |
201 | &:intro
202 | {
203 | opacity: 0;
204 | transform: scale(1.1);
205 | }
206 | }
207 |
208 | @keyframes tilt-left-right
209 | {
210 | 0%
211 | {
212 | transform: rotate(-5deg);
213 | }
214 | 100%
215 | {
216 | transform: rotate(5deg);
217 | }
218 | }
--------------------------------------------------------------------------------
/code/UI/Panels/ShopPanel/ShopPanel.razor:
--------------------------------------------------------------------------------
1 | @using Sandbox;
2 | @using Sandbox.UI;
3 | @namespace Battlebugs
4 | @inherits Panel
5 | @attribute [StyleSheet]
6 |
7 | <root class="@(IsOpen ? "show" : "")">
8 | <div class="container">
9 | <span class="header"><i>shopping_cart</i>Shop</span>
10 | <i class="close" onclick=@(() => IsOpen = false)>close</i>
11 | <div class="content">
12 | @foreach (var weapon in BoardManager.Local.WeaponInventory.OrderBy(x => x.Key.Cost).Where(x => x.Key.Cost > 0))
13 | {
14 | <ShopPanelEntry [email protected] />
15 | }
16 | </div>
17 | </div>
18 | </root>
19 |
20 | @code
21 | {
22 | public static ShopPanel Instance { get; private set; }
23 |
24 | public bool IsOpen { get; private set; } = false;
25 |
26 | protected override void OnAfterTreeRender(bool firstTime)
27 | {
28 | base.OnAfterTreeRender(firstTime);
29 |
30 | if (firstTime)
31 | {
32 | Instance = this;
33 | }
34 | }
35 |
36 | public void Open()
37 | {
38 | Sound.Play("ui.navigate.forward");
39 | IsOpen = true;
40 | }
41 |
42 | protected override int BuildHash() => System.HashCode.Combine(IsOpen);
43 | }
--------------------------------------------------------------------------------
/code/UI/Panels/ShopPanel/ShopPanel.razor.scss:
--------------------------------------------------------------------------------
1 | ShopPanel {
2 | position: absolute;
3 | top: 0;
4 | left: 0;
5 | right: 0;
6 | bottom: 0;
7 | background-color: rgba(black, 0.4);
8 | opacity: 0;
9 | transform: scale(1.1);
10 | transition: all 0.2s ease-out;
11 | color: white;
12 | font-family: "paper cuts 2";
13 | font-size: 24px;
14 | pointer-events: all;
15 |
16 | > .container {
17 | position: absolute;
18 | top: 20%;
19 | left: 30%;
20 | bottom: 20%;
21 | right: 30%;
22 | background-color: rgba(black, 0.9);
23 | padding: 16px;
24 | flex-direction: column;
25 | align-items: center;
26 |
27 | > .header {
28 | font-size: 42px;
29 | gap: 16px;
30 | align-items: center;
31 | }
32 |
33 | > .content {
34 | width: 100%;
35 | flex-grow: 1;
36 | padding: 8px;
37 | // overflow-x: hidden;
38 | // overflow-y: scroll;
39 | flex-direction: column;
40 | gap: 8px;
41 | }
42 |
43 | > .close {
44 | position: absolute;
45 | top: 8px;
46 | right: 8px;
47 | font-size: 32px;
48 | cursor: "bug-hover";
49 |
50 | &:hover {
51 | sound-in: ui.button.over;
52 | background-color: rgba(white, 0.05);
53 | }
54 |
55 | &:active {
56 | sound-in: ui.navigate.back;
57 | }
58 | }
59 | }
60 |
61 | &.show {
62 | opacity: 1;
63 | transform: scale(1);
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/code/UI/Panels/ShopPanel/ShopPanelEntry.razor:
--------------------------------------------------------------------------------
1 | @using Sandbox;
2 | @using Sandbox.UI;
3 | @namespace Battlebugs
4 | @inherits Panel
5 | @attribute [StyleSheet]
6 |
7 | <root onclick=@Purchase>
8 | <img [email protected] />
9 | <div class="info">
10 | <div class="top">
11 | <label class="name">@Weapon.Name</label>
12 | @if (Weapon.Cost > 0)
13 | {
14 | <span class="cost">🪙 @Weapon.Cost</span>
15 | }
16 | else
17 | {
18 | <span class="cost">🚫</span>
19 | }
20 | </div>
21 | <div class="description">
22 | @(Weapon.Cost > 0 ? Weapon.Description : "This weapon cannot be purchased.")
23 | </div>
24 | </div>
25 | </root>
26 |
27 | @code
28 | {
29 | public WeaponResource Weapon { get; set; }
30 |
31 | void Purchase()
32 | {
33 | if (Weapon.Cost > 0 && BoardManager.Local.Coins >= Weapon.Cost)
34 | {
35 | Sound.Play("shop-purchase");
36 | BoardManager.Local.PurchaseWeapon(Weapon);
37 | }
38 | else
39 | {
40 | Sound.Play("ui.button.deny");
41 | }
42 | }
43 |
44 | protected override int BuildHash() => System.HashCode.Combine(Weapon);
45 | }
--------------------------------------------------------------------------------
/code/UI/Panels/ShopPanel/ShopPanelEntry.razor.scss:
--------------------------------------------------------------------------------
1 | ShopPanelEntry {
2 | flex-grow: 1;
3 | background-color: rgba(0, 0, 0, 0.61);
4 | padding: 16px;
5 | flex-shrink: 0;
6 | gap: 16px;
7 | align-items: center;
8 | cursor: "bug-hover";
9 |
10 | > img {
11 | width: 92px;
12 | height: 92px;
13 | flex-shrink: 0;
14 | }
15 |
16 | .info {
17 | flex-direction: column;
18 |
19 | .top {
20 | gap: 8px;
21 | }
22 |
23 | .description {
24 | font-size: 20px;
25 | opacity: 0.5;
26 | }
27 | }
28 |
29 | &:hover {
30 | sound-in: ui.button.over;
31 | background-color: rgba(0, 0, 0, 0.8);
32 | }
33 |
34 | &:active {
35 | background-color: rgba(0, 0, 0, 0.5);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/code/UI/Panels/StatsPanel.razor:
--------------------------------------------------------------------------------
1 | @using Sandbox;
2 | @using Sandbox.UI;
3 | @namespace Battlebugs
4 | @inherits Panel
5 | @attribute [StyleSheet]
6 |
7 | <root>
8 | <label class="header">Stats</label>
9 | <div class="entry">
10 | <label class="stat">Games Played</label>
11 | <label class="value">@Stats.Get("games_played").ValueString</label>
12 | </div>
13 | <div class="entry">
14 | <label class="stat">Games Won</label>
15 | <label class="value">@Stats.Get("games_won").ValueString</label>
16 | </div>
17 | <div class="entry">
18 | <label class="stat">Games Lost</label>
19 | <label class="value">@Stats.Get("games_lost").ValueString</label>
20 | </div>
21 | <div class="entry">
22 | <label class="stat">Coins Earned</label>
23 | <label class="value">@Stats.Get("coins_earned").ValueString</label>
24 | </div>
25 | <div class="entry">
26 | <label class="stat">Coins Spent</label>
27 | <label class="value">@Stats.Get("coins_spent").ValueString</label>
28 | </div>
29 | <div class="entry">
30 | <label class="stat">Bugs Killed</label>
31 | <label class="value">@Stats.Get("bugs_killed").ValueString</label>
32 | </div>
33 | <div class="entry">
34 | <label class="stat">Damage Dealt</label>
35 | <label class="value">@Stats.Get("damage_dealt").ValueString</label>
36 | </div>
37 | </root>
38 |
39 | @code
40 | {
41 | Sandbox.Services.Stats.PlayerStats Stats
42 | {
43 | get
44 | {
45 | if (_stats == null)
46 | {
47 | _stats = Sandbox.Services.Stats.GetLocalPlayerStats(Game.Ident);
48 | }
49 | return _stats;
50 |
51 | }
52 | }
53 | Sandbox.Services.Stats.PlayerStats _stats = null;
54 |
55 | protected override int BuildHash() => System.HashCode.Combine(Stats);
56 | }
--------------------------------------------------------------------------------
/code/UI/Panels/StatsPanel.razor.scss:
--------------------------------------------------------------------------------
1 | StatsPanel
2 | {
3 | width: 100%;
4 | height: 100%;
5 | padding: 32px;
6 | color: white;
7 | font-size: 42px;
8 | flex-direction: column;
9 | gap: 16px;
10 | justify-content: space-evenly;
11 |
12 | .header
13 | {
14 | font-size: 72px;
15 | }
16 |
17 | .entry
18 | {
19 | width: 100%;
20 | justify-content: space-between;
21 | }
22 | }
--------------------------------------------------------------------------------
/code/UI/Panels/WaitingHud.razor:
--------------------------------------------------------------------------------
1 | @using Sandbox;
2 | @using Sandbox.UI;
3 | @namespace Battlebugs
4 | @inherits Panel
5 | @attribute [StyleSheet]
6 |
7 | <root>
8 | <label>Waiting for an opponent...</label>
9 | <i>refresh</i>
10 | </root>
11 |
12 | @code
13 | {
14 | protected override int BuildHash() => System.HashCode.Combine("");
15 | }
--------------------------------------------------------------------------------
/code/UI/Panels/WaitingHud.razor.scss:
--------------------------------------------------------------------------------
1 | WaitingHud {
2 | position: absolute;
3 | top: 0;
4 | left: 0;
5 | right: 0;
6 | bottom: 0;
7 | background-color: rgba(black, 0.4);
8 | backdrop-filter: blur(1px);
9 | justify-content: center;
10 | align-items: center;
11 | font-family: "paper cuts 2";
12 | font-size: 58px;
13 | font-weight: 700;
14 | color: white;
15 | flex-direction: column;
16 | gap: 64px;
17 | pointer-events: all;
18 |
19 | i {
20 | font-size: 128px;
21 | animation: rotateright 1s linear infinite;
22 | }
23 | }
24 |
25 | @keyframes rotateright
26 | {
27 | from
28 | {
29 | transform: rotate(0deg);
30 | }
31 | to
32 | {
33 | transform: rotate(360deg);
34 | }
35 | }
--------------------------------------------------------------------------------
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment