1 #region Copyright & License Information 2 /* 3 * Copyright 2007-2020 The OpenRA Developers (see AUTHORS) 4 * This file is part of OpenRA, which is free software. It is made 5 * available to you under the terms of the GNU General Public License 6 * as published by the Free Software Foundation, either version 3 of 7 * the License, or (at your option) any later version. For more 8 * information, see COPYING. 9 */ 10 #endregion 11 12 using System; 13 using OpenRA.Graphics; 14 using OpenRA.Primitives; 15 using OpenRA.Support; 16 using OpenRA.Traits; 17 18 namespace OpenRA.Mods.Common.Traits 19 { 20 [Desc("Adds a particle-based overlay.")] 21 public class WeatherOverlayInfo : ITraitInfo, ILobbyCustomRulesIgnore 22 { 23 [Desc("Average number of particles per 100x100 px square.")] 24 public readonly int ParticleDensityFactor = 8; 25 26 [Desc("Should the level of the wind change over time, or just stick to the first value of WindLevels?")] 27 public readonly bool ChangingWindLevel = true; 28 29 [Desc("The levels of wind intensity (particles x-axis movement in px/tick).")] 30 public readonly int[] WindLevels = { -5, -3, -2, 0, 2, 3, 5 }; 31 32 [Desc("Works only if ChangingWindLevel is enabled. Min. and max. ticks needed to change the WindLevel.")] 33 public readonly int[] WindTick = { 150, 750 }; 34 35 [Desc("Hard or soft fading between the WindLevels.")] 36 public readonly bool InstantWindChanges = false; 37 38 [Desc("Particles are drawn in squares when enabled, otherwise with lines.")] 39 public readonly bool UseSquares = true; 40 41 [Desc("Size / width of the particle in px.")] 42 public readonly int[] ParticleSize = { 1, 3 }; 43 44 [Desc("Scatters falling direction on the x-axis. Scatter min. and max. value in px/tick.")] 45 public readonly int[] ScatterDirection = { -1, 1 }; 46 47 [Desc("Min. and max. speed at which particles fall in px/tick.")] 48 public readonly float[] Gravity = { 1.00f, 2.00f }; 49 50 [Desc("The current offset value for the swing movement. SwingOffset min. and max. value in px/tick.")] 51 public readonly float[] SwingOffset = { 1.0f, 1.5f }; 52 53 [Desc("The value that particles swing to the side each update. SwingSpeed min. and max. value in px/tick.")] 54 public readonly float[] SwingSpeed = { 0.001f, 0.025f }; 55 56 [Desc("The value range that can be swung to the left or right. SwingAmplitude min. and max. value in px/tick.")] 57 public readonly float[] SwingAmplitude = { 1.0f, 1.5f }; 58 59 [Desc("The randomly selected rgb(a) hex colors for the particles. Use this order: rrggbb[aa], rrggbb[aa], ...")] 60 public readonly Color[] ParticleColors = 61 { 62 Color.FromArgb(236, 236, 236), 63 Color.FromArgb(228, 228, 228), 64 Color.FromArgb(208, 208, 208), 65 Color.FromArgb(188, 188, 188) 66 }; 67 68 [Desc("Works only with line enabled and can be used to fade out the tail of the line like a contrail.")] 69 public readonly byte LineTailAlphaValue = 200; 70 Create(ActorInitializer init)71 public object Create(ActorInitializer init) { return new WeatherOverlay(init.World, this); } 72 } 73 74 public class WeatherOverlay : ITick, IRenderAboveWorld, INotifyViewportZoomExtentsChanged 75 { 76 struct Particle 77 { 78 public readonly float2 Pos; 79 public readonly int Size; 80 public readonly float DirectionScatterX; 81 public readonly float Gravity; 82 public readonly float SwingOffset; 83 public readonly float SwingSpeed; 84 public readonly int SwingDirection; 85 public readonly float SwingAmplitude; 86 public readonly Color Color; 87 public readonly Color TailColor; 88 ParticleOpenRA.Mods.Common.Traits.WeatherOverlay.Particle89 public Particle(WeatherOverlayInfo info, MersenneTwister r, Rectangle viewport) 90 { 91 var x = r.Next(viewport.Left, viewport.Right); 92 var y = r.Next(viewport.Top, viewport.Bottom); 93 94 Pos = new int2(x, y); 95 Size = r.Next(info.ParticleSize[0], info.ParticleSize[1] + 1); 96 DirectionScatterX = info.ScatterDirection[0] + r.Next(info.ScatterDirection[1] - info.ScatterDirection[0]); 97 Gravity = float2.Lerp(info.Gravity[0], info.Gravity[1], r.NextFloat()); 98 SwingOffset = float2.Lerp(info.SwingOffset[0], info.SwingOffset[1], r.NextFloat()); 99 SwingSpeed = float2.Lerp(info.SwingSpeed[0], info.SwingSpeed[1], r.NextFloat()); 100 SwingDirection = r.Next(2) == 0 ? 1 : -1; 101 SwingAmplitude = float2.Lerp(info.SwingAmplitude[0], info.SwingAmplitude[1], r.NextFloat()); 102 Color = info.ParticleColors.Random(r); 103 TailColor = Color.FromArgb(info.LineTailAlphaValue, Color.R, Color.G, Color.B); 104 } 105 ParticleOpenRA.Mods.Common.Traits.WeatherOverlay.Particle106 Particle(Particle source) 107 { 108 Pos = source.Pos; 109 Size = source.Size; 110 DirectionScatterX = source.DirectionScatterX; 111 Gravity = source.Gravity; 112 SwingOffset = source.SwingOffset; 113 SwingSpeed = source.SwingSpeed; 114 SwingDirection = source.SwingDirection; 115 SwingAmplitude = source.SwingAmplitude; 116 Color = source.Color; 117 TailColor = source.TailColor; 118 } 119 ParticleOpenRA.Mods.Common.Traits.WeatherOverlay.Particle120 public Particle(Particle source, float2 pos) 121 : this(source) 122 { 123 Pos = pos; 124 } 125 ParticleOpenRA.Mods.Common.Traits.WeatherOverlay.Particle126 public Particle(Particle source, float2 pos, int swingDirection, float swingOffset) 127 : this(source) 128 { 129 Pos = pos; 130 SwingDirection = swingDirection; 131 SwingOffset = swingOffset; 132 } 133 } 134 135 readonly WeatherOverlayInfo info; 136 readonly World world; 137 138 float windStrength; 139 int targetWindStrengthIndex; 140 long windUpdateCountdown; 141 Particle[] particles; 142 Size viewportSize; 143 WeatherOverlay(World world, WeatherOverlayInfo info)144 public WeatherOverlay(World world, WeatherOverlayInfo info) 145 { 146 this.info = info; 147 this.world = world; 148 targetWindStrengthIndex = info.ChangingWindLevel ? world.LocalRandom.Next(info.WindLevels.Length) : 0; 149 windUpdateCountdown = world.LocalRandom.Next(info.WindTick[0], info.WindTick[1]); 150 windStrength = info.WindLevels[targetWindStrengthIndex]; 151 } 152 INotifyViewportZoomExtentsChanged.ViewportZoomExtentsChanged(float minZoom, float maxZoom)153 void INotifyViewportZoomExtentsChanged.ViewportZoomExtentsChanged(float minZoom, float maxZoom) 154 { 155 // Track particles in a viewport fixed to the minimum zoom level 156 var s = (1f / minZoom * new float2(Game.Renderer.NativeResolution)).ToInt2(); 157 viewportSize = new Size(s.X, s.Y); 158 159 // Randomly distribute particles within the initial viewport 160 var particleCount = viewportSize.Width * viewportSize.Height * info.ParticleDensityFactor / 10000; 161 particles = new Particle[particleCount]; 162 var rect = new Rectangle(int2.Zero, viewportSize); 163 for (var i = 0; i < particles.Length; i++) 164 particles[i] = new Particle(info, world.LocalRandom, rect); 165 } 166 ITick.Tick(Actor self)167 void ITick.Tick(Actor self) 168 { 169 if (!info.ChangingWindLevel || info.WindLevels.Length == 1) 170 return; 171 172 if (--windUpdateCountdown <= 0) 173 { 174 windUpdateCountdown = self.World.LocalRandom.Next(info.WindTick[0], info.WindTick[1]); 175 if (targetWindStrengthIndex > 0 && self.World.LocalRandom.Next(2) == 1) 176 targetWindStrengthIndex--; 177 else if (targetWindStrengthIndex < info.WindLevels.Length - 1) 178 targetWindStrengthIndex++; 179 } 180 181 // Fading the wind in little steps towards the TargetWindOffset 182 var targetWindLevel = info.WindLevels[targetWindStrengthIndex]; 183 if (info.InstantWindChanges) 184 windStrength = targetWindLevel; 185 else if (Math.Abs(windStrength - targetWindLevel) > 0.01f) 186 { 187 if (windStrength > targetWindLevel) 188 windStrength -= 0.01f; 189 else if (windStrength < targetWindLevel) 190 windStrength += 0.01f; 191 } 192 } 193 IRenderAboveWorld.RenderAboveWorld(Actor self, WorldRenderer wr)194 void IRenderAboveWorld.RenderAboveWorld(Actor self, WorldRenderer wr) 195 { 196 var center = wr.Viewport.CenterLocation; 197 var viewport = new Rectangle(center - new int2(viewportSize) / 2, viewportSize); 198 var wcr = Game.Renderer.WorldRgbaColorRenderer; 199 200 for (var i = 0; i < particles.Length; i++) 201 { 202 // Simulate wind and gravity effects on the particle 203 var p = particles[i]; 204 if (!world.Paused) 205 { 206 var swingDirection = p.SwingDirection; 207 if (p.SwingOffset < -p.SwingAmplitude || p.SwingOffset > p.SwingAmplitude) 208 swingDirection *= -1; 209 210 var swingOffset = p.SwingOffset + p.SwingDirection * p.SwingSpeed; 211 var pos = p.Pos + new float2(p.DirectionScatterX + p.SwingOffset + windStrength, p.Gravity); 212 particles[i] = p = new Particle(p, pos, swingDirection, swingOffset); 213 } 214 215 // Move the particle back inside the viewport if necessary 216 if (!viewport.Contains(p.Pos.ToInt2())) 217 { 218 var dx = (p.Pos.X - viewport.Left) % viewport.Width; 219 var dy = (p.Pos.Y - viewport.Top) % viewport.Height; 220 221 if (dx < 0) 222 dx += viewport.Width; 223 224 if (dy < 0) 225 dy += viewport.Height; 226 227 particles[i] = p = new Particle(p, new float2(viewport.Left + dx, viewport.Top + dy)); 228 } 229 230 // Render the particle 231 // We must provide a z coordinate to stop the GL near and far Z limits from culling the geometry 232 var a = new float3(p.Pos.X, p.Pos.Y, p.Pos.Y); 233 if (info.UseSquares) 234 { 235 var b = a + new float2(p.Size, p.Size); 236 wcr.FillRect(a, b, p.Color); 237 } 238 else 239 { 240 var tail = p.Pos + new float2(-windStrength, -p.Gravity * 2 / 3); 241 242 var b = new float3(tail.X, tail.Y, tail.Y); 243 wcr.DrawLine(a, b, p.Size, p.TailColor); 244 } 245 } 246 } 247 } 248 } 249