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