1/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
2/* If you are missing that file, acquire a complete release at teeworlds.com. */
3
4#include "effects.h"
5
6#include <base/time.h>
7
8#include <engine/demo.h>
9#include <engine/shared/config.h>
10
11#include <generated/client_data.h>
12
13#include <game/client/components/damageind.h>
14#include <game/client/components/flow.h>
15#include <game/client/components/particles.h>
16#include <game/client/components/sounds.h>
17#include <game/client/gameclient.h>
18
19CEffects::CEffects()
20{
21 m_Add5hz = false;
22 m_Add50hz = false;
23 m_Add100hz = false;
24}
25
26void CEffects::AirJump(vec2 Pos, float Alpha, float Volume)
27{
28 CParticle p;
29 p.SetDefault();
30 p.m_Spr = SPRITE_PART_AIRJUMP;
31 p.m_Pos = Pos + vec2(-6.0f, 16.0f);
32 p.m_Vel = vec2(0.0f, -200.0f);
33 p.m_LifeSpan = 0.5f;
34 p.m_StartSize = 48.0f;
35 p.m_EndSize = 0.0f;
36 p.m_Rot = random_angle();
37 p.m_Rotspeed = pi * 2.0f;
38 p.m_Gravity = 500.0f;
39 p.m_Friction = 0.7f;
40 p.m_FlowAffected = 0.0f;
41 p.m_Color.a = Alpha;
42 p.m_StartAlpha = Alpha;
43 GameClient()->m_Particles.Add(Group: CParticles::GROUP_GENERAL, pPart: &p);
44
45 p.m_Pos = Pos + vec2(6.0f, 16.0f);
46 GameClient()->m_Particles.Add(Group: CParticles::GROUP_GENERAL, pPart: &p);
47
48 if(g_Config.m_SndGame)
49 GameClient()->m_Sounds.PlayAt(Channel: CSounds::CHN_WORLD, SetId: SOUND_PLAYER_AIRJUMP, Volume, Position: Pos);
50}
51
52void CEffects::DamageIndicator(vec2 Pos, vec2 Dir, float Alpha)
53{
54 GameClient()->m_DamageInd.Create(Pos, Dir, Alpha);
55}
56
57void CEffects::PowerupShine(vec2 Pos, vec2 Size, float Alpha)
58{
59 if(!m_Add50hz)
60 return;
61
62 CParticle p;
63 p.SetDefault();
64 p.m_Spr = SPRITE_PART_SLICE;
65 p.m_Pos = Pos + vec2(random_float(min: -0.5f, max: 0.5f), random_float(min: -0.5f, max: 0.5f)) * Size;
66 p.m_Vel = vec2(0.0f, 0.0f);
67 p.m_LifeSpan = 0.5f;
68 p.m_StartSize = 16.0f;
69 p.m_EndSize = 0.0f;
70 p.m_Rot = random_angle();
71 p.m_Rotspeed = pi * 2.0f;
72 p.m_Gravity = 500.0f;
73 p.m_Friction = 0.9f;
74 p.m_FlowAffected = 0.0f;
75 p.m_Color.a = Alpha;
76 p.m_StartAlpha = Alpha;
77 GameClient()->m_Particles.Add(Group: CParticles::GROUP_GENERAL, pPart: &p);
78}
79
80void CEffects::FreezingFlakes(vec2 Pos, vec2 Size, float Alpha)
81{
82 if(!m_Add5hz)
83 return;
84
85 CParticle p;
86 p.SetDefault();
87 p.m_Spr = SPRITE_PART_SNOWFLAKE;
88 p.m_Pos = Pos + vec2(random_float(min: -0.5f, max: 0.5f), random_float(min: -0.5f, max: 0.5f)) * Size;
89 p.m_Vel = vec2(0.0f, 0.0f);
90 p.m_LifeSpan = 1.5f;
91 p.m_StartSize = random_float(min: 0.5f, max: 1.5f) * 16.0f;
92 p.m_EndSize = p.m_StartSize * 0.5f;
93 p.m_UseAlphaFading = true;
94 p.m_StartAlpha = 1.0f;
95 p.m_EndAlpha = 0.0f;
96 p.m_Rot = random_angle();
97 p.m_Rotspeed = pi;
98 p.m_Gravity = random_float(max: 250.0f);
99 p.m_Friction = 0.9f;
100 p.m_FlowAffected = 0.0f;
101 p.m_Collides = false;
102 p.m_Color.a = Alpha;
103 p.m_StartAlpha = Alpha;
104 GameClient()->m_Particles.Add(Group: CParticles::GROUP_EXTRA, pPart: &p);
105}
106
107void CEffects::SparkleTrail(vec2 Pos, float Alpha)
108{
109 if(!m_Add50hz)
110 return;
111
112 CParticle p;
113 p.SetDefault();
114 p.m_Spr = SPRITE_PART_SPARKLE;
115 p.m_Pos = Pos + random_direction() * random_float(max: 40.0f);
116 p.m_Vel = vec2(0.0f, 0.0f);
117 p.m_LifeSpan = 0.5f;
118 p.m_StartSize = 0.0f;
119 p.m_EndSize = random_float(min: 20.0f, max: 30.0f);
120 p.m_UseAlphaFading = true;
121 p.m_StartAlpha = Alpha;
122 p.m_EndAlpha = std::min(a: 0.2f, b: Alpha);
123 p.m_Collides = false;
124 GameClient()->m_Particles.Add(Group: CParticles::GROUP_TRAIL_EXTRA, pPart: &p);
125}
126
127void CEffects::SmokeTrail(vec2 Pos, vec2 Vel, float Alpha, float TimePassed)
128{
129 if(!m_Add50hz && TimePassed < 0.001f)
130 return;
131
132 CParticle p;
133 p.SetDefault();
134 p.m_Spr = SPRITE_PART_SMOKE;
135 p.m_Pos = Pos;
136 p.m_Vel = Vel + random_direction() * 50.0f;
137 p.m_LifeSpan = random_float(min: 0.5f, max: 1.0f);
138 p.m_StartSize = random_float(min: 12.0f, max: 20.0f);
139 p.m_EndSize = 0.0f;
140 p.m_Friction = 0.7f;
141 p.m_Gravity = random_float(max: -500.0f);
142 p.m_Color.a = Alpha;
143 p.m_StartAlpha = Alpha;
144 GameClient()->m_Particles.Add(Group: CParticles::GROUP_PROJECTILE_TRAIL, pPart: &p, TimePassed);
145}
146
147void CEffects::SkidTrail(vec2 Pos, vec2 Vel, int Direction, float Alpha, float Volume)
148{
149 if(m_Add100hz)
150 {
151 CParticle p;
152 p.SetDefault();
153 p.m_Spr = SPRITE_PART_SMOKE;
154 p.m_Pos = Pos + vec2(-Direction * 6.0f, 12.0f);
155 p.m_Vel = vec2(-Direction * 100.0f * length(a: Vel), -50.0f) + random_direction() * 50.0f;
156 p.m_LifeSpan = random_float(min: 0.5f, max: 1.0f);
157 p.m_StartSize = random_float(min: 24.0f, max: 36.0f);
158 p.m_EndSize = 0.0f;
159 p.m_Friction = 0.7f;
160 p.m_Gravity = random_float(max: -500.0f);
161 p.m_Color = ColorRGBA(0.75f, 0.75f, 0.75f, Alpha);
162 p.m_StartAlpha = Alpha;
163 GameClient()->m_Particles.Add(Group: CParticles::GROUP_GENERAL, pPart: &p);
164 }
165 if(g_Config.m_SndGame)
166 {
167 int64_t Now = time();
168 if(Now - m_SkidSoundTimer > time_freq() / 10)
169 {
170 m_SkidSoundTimer = Now;
171 GameClient()->m_Sounds.PlayAt(Channel: CSounds::CHN_WORLD, SetId: SOUND_PLAYER_SKID, Volume, Position: Pos);
172 }
173 }
174}
175
176void CEffects::BulletTrail(vec2 Pos, float Alpha, float TimePassed)
177{
178 if(!m_Add100hz && TimePassed < 0.001f)
179 return;
180
181 CParticle p;
182 p.SetDefault();
183 p.m_Spr = SPRITE_PART_BALL;
184 p.m_Pos = Pos;
185 p.m_LifeSpan = random_float(min: 0.25f, max: 0.5f);
186 p.m_StartSize = 8.0f;
187 p.m_EndSize = 0.0f;
188 p.m_Friction = 0.7f;
189 p.m_Color.a *= Alpha;
190 p.m_StartAlpha = Alpha;
191 GameClient()->m_Particles.Add(Group: CParticles::GROUP_PROJECTILE_TRAIL, pPart: &p, TimePassed);
192}
193
194void CEffects::PlayerSpawn(vec2 Pos, float Alpha, float Volume)
195{
196 for(int i = 0; i < 32; i++)
197 {
198 CParticle p;
199 p.SetDefault();
200 p.m_Spr = SPRITE_PART_SHELL;
201 p.m_Pos = Pos;
202 p.m_Vel = random_direction() * (std::pow(x: random_float(), y: 3) * 600.0f);
203 p.m_LifeSpan = random_float(min: 0.3f, max: 0.6f);
204 p.m_StartSize = random_float(min: 64.0f, max: 96.0f);
205 p.m_EndSize = 0.0f;
206 p.m_Rot = random_angle();
207 p.m_Rotspeed = random_float();
208 p.m_Gravity = random_float(max: -400.0f);
209 p.m_Friction = 0.7f;
210 p.m_Color = ColorRGBA(0xb5 / 255.0f, 0x50 / 255.0f, 0xcb / 255.0f, Alpha);
211 p.m_StartAlpha = Alpha;
212 GameClient()->m_Particles.Add(Group: CParticles::GROUP_GENERAL, pPart: &p);
213 }
214 if(g_Config.m_SndGame)
215 GameClient()->m_Sounds.PlayAt(Channel: CSounds::CHN_WORLD, SetId: SOUND_PLAYER_SPAWN, Volume, Position: Pos);
216}
217
218void CEffects::PlayerDeath(vec2 Pos, int ClientId, float Alpha)
219{
220 ColorRGBA BloodColor(1.0f, 1.0f, 1.0f);
221
222 if(ClientId >= 0)
223 {
224 // Use m_RenderInfo.m_CustomColoredSkin instead of m_UseCustomColor
225 // m_UseCustomColor says if the player's skin has a custom color (value sent from the client side)
226
227 // m_RenderInfo.m_CustomColoredSkin Defines if in the context of the game the color is being customized,
228 // Using this value if the game is teams (red and blue), this value will be true even if the skin is with the normal color.
229 // And will use the team body color to create player death effect instead of tee color
230 if(GameClient()->Client()->IsSixup())
231 {
232 if(GameClient()->m_aClients[ClientId].m_RenderInfo.m_aSixup[g_Config.m_ClDummy].m_aUseCustomColors[protocol7::SKINPART_BODY])
233 {
234 BloodColor = GameClient()->m_aClients[ClientId].m_RenderInfo.m_aSixup[g_Config.m_ClDummy].m_aColors[protocol7::SKINPART_BODY];
235 }
236 else
237 {
238 BloodColor = GameClient()->m_aClients[ClientId].m_RenderInfo.m_aSixup[g_Config.m_ClDummy].m_BloodColor;
239 }
240 }
241 else
242 {
243 if(GameClient()->m_aClients[ClientId].m_RenderInfo.m_CustomColoredSkin)
244 {
245 BloodColor = GameClient()->m_aClients[ClientId].m_RenderInfo.m_ColorBody;
246 }
247 else
248 {
249 BloodColor = GameClient()->m_aClients[ClientId].m_RenderInfo.m_BloodColor;
250 }
251 }
252 }
253
254 for(int i = 0; i < 64; i++)
255 {
256 CParticle p;
257 p.SetDefault();
258 p.m_Spr = SPRITE_PART_SPLAT01 + (rand() % 3);
259 p.m_Pos = Pos;
260 p.m_Vel = random_direction() * (random_float(min: 0.1f, max: 1.1f) * 900.0f);
261 p.m_LifeSpan = random_float(min: 0.3f, max: 0.6f);
262 p.m_StartSize = random_float(min: 24.0f, max: 40.0f);
263 p.m_EndSize = 0.0f;
264 p.m_Rot = random_angle();
265 p.m_Rotspeed = random_float(min: -0.5f, max: 0.5f) * pi;
266 p.m_Gravity = 800.0f;
267 p.m_Friction = 0.8f;
268 p.m_Color = BloodColor.Multiply(Factor: random_float(min: 0.75f, max: 1.0f)).WithAlpha(alpha: 0.75f * Alpha);
269 p.m_StartAlpha = Alpha;
270 GameClient()->m_Particles.Add(Group: CParticles::GROUP_GENERAL, pPart: &p);
271 }
272}
273
274void CEffects::Confetti(vec2 Pos, float Alpha)
275{
276 ColorRGBA Red(1.0f, 0.4f, 0.4f);
277 ColorRGBA Green(0.4f, 1.0f, 0.4f);
278 ColorRGBA Blue(0.4f, 0.4f, 1.0f);
279 ColorRGBA Yellow(1.0f, 1.0f, 0.4f);
280 ColorRGBA Cyan(0.4f, 1.0f, 1.0f);
281 ColorRGBA Magenta(1.0f, 0.4f, 1.0f);
282
283 ColorRGBA aConfettiColors[] = {Red, Green, Blue, Yellow, Cyan, Magenta};
284
285 // powerful confettis
286 for(int i = 0; i < 32; i++)
287 {
288 CParticle p;
289 p.SetDefault();
290 p.m_Spr = SPRITE_PART_SPLAT01 + (rand() % 3);
291 p.m_Pos = Pos;
292 p.m_Vel = direction(angle: -0.5f * pi + random_float(min: -0.2f, max: 0.2f)) * random_float(min: 0.01f, max: 1.0f) * 2000.0f;
293 p.m_LifeSpan = random_float(min: 1.0f, max: 1.2f);
294 p.m_StartSize = random_float(min: 12.0f, max: 24.0f);
295 p.m_EndSize = 0.0f;
296 p.m_Rot = random_angle();
297 p.m_Rotspeed = random_float(min: -0.5f, max: 0.5f) * pi;
298 p.m_Gravity = -700.0f;
299 p.m_Friction = 0.6f;
300 ColorRGBA c = aConfettiColors[(rand() % std::size(aConfettiColors))];
301 p.m_Color = c.WithMultipliedAlpha(alpha: 0.75f * Alpha);
302 p.m_StartAlpha = Alpha;
303 GameClient()->m_Particles.Add(Group: CParticles::GROUP_GENERAL, pPart: &p);
304 }
305
306 // broader confettis
307 for(int i = 0; i < 32; i++)
308 {
309 CParticle p;
310 p.SetDefault();
311 p.m_Spr = SPRITE_PART_SPLAT01 + (rand() % 3);
312 p.m_Pos = Pos;
313 p.m_Vel = direction(angle: -0.5f * pi + random_float(min: -0.8f, max: 0.8f)) * random_float(min: 0.01f, max: 1.0f) * 1500.0f;
314 p.m_LifeSpan = random_float(min: 0.8f, max: 1.0f);
315 p.m_StartSize = random_float(min: 12.0f, max: 24.0f);
316 p.m_EndSize = 0.0f;
317 p.m_Rot = random_angle();
318 p.m_Rotspeed = random_float(min: -0.5f, max: 0.5f) * pi;
319 p.m_Gravity = -700.0f;
320 p.m_Friction = 0.6f;
321 ColorRGBA c = aConfettiColors[(rand() % std::size(aConfettiColors))];
322 p.m_Color = c.WithMultipliedAlpha(alpha: 0.75f * Alpha);
323 p.m_StartAlpha = Alpha;
324 GameClient()->m_Particles.Add(Group: CParticles::GROUP_GENERAL, pPart: &p);
325 }
326}
327
328void CEffects::Explosion(vec2 Pos, float Alpha)
329{
330 // add to flow
331 for(int y = -8; y <= 8; y++)
332 for(int x = -8; x <= 8; x++)
333 {
334 if(x == 0 && y == 0)
335 continue;
336
337 float a = 1 - (length(a: vec2(x, y)) / length(a: vec2(8.0f, 8.0f)));
338 GameClient()->m_Flow.Add(Pos: Pos + vec2(x, y) * 16.0f, Vel: normalize(v: vec2(x, y)) * 5000.0f * a, Size: 10.0f);
339 }
340
341 // add the explosion
342 CParticle p;
343 p.SetDefault();
344 p.m_Spr = SPRITE_PART_EXPL01;
345 p.m_Pos = Pos;
346 p.m_LifeSpan = 0.4f;
347 p.m_StartSize = 150.0f;
348 p.m_EndSize = 0.0f;
349 p.m_Rot = random_angle();
350 p.m_Color.a = Alpha;
351 p.m_StartAlpha = Alpha;
352 GameClient()->m_Particles.Add(Group: CParticles::GROUP_EXPLOSIONS, pPart: &p);
353
354 // Nudge position slightly to edge of closest tile so the
355 // smoke doesn't get stuck inside the tile.
356 if(Collision()->CheckPoint(Pos))
357 {
358 const vec2 DistanceToTopLeft = Pos - vec2(round_truncate(f: Pos.x / 32), round_truncate(f: Pos.y / 32)) * 32;
359
360 vec2 CheckOffset;
361 CheckOffset.x = (DistanceToTopLeft.x > 16.0f ? 32.0f : -1.0f);
362 CheckOffset.y = (DistanceToTopLeft.y > 16.0f ? 32.0f : -1.0f);
363 CheckOffset -= DistanceToTopLeft;
364
365 for(vec2 Mask : {vec2(1.0f, 0.0f), vec2(0.0f, 1.0f), vec2(1.0f, 1.0f)})
366 {
367 const vec2 NewPos = Pos + CheckOffset * Mask;
368 if(!Collision()->CheckPoint(Pos: NewPos))
369 {
370 Pos = NewPos;
371 break;
372 }
373 }
374 }
375
376 // add the smoke
377 for(int i = 0; i < 24; i++)
378 {
379 p.SetDefault();
380 p.m_Spr = SPRITE_PART_SMOKE;
381 p.m_Pos = Pos;
382 p.m_Vel = random_direction() * (random_float(min: 1.0f, max: 1.2f) * 1000.0f);
383 p.m_LifeSpan = random_float(min: 0.5f, max: 0.9f);
384 p.m_StartSize = random_float(min: 32.0f, max: 40.0f);
385 p.m_EndSize = 0.0f;
386 p.m_Gravity = random_float(max: -800.0f);
387 p.m_Friction = 0.4f;
388 p.m_Color = ColorRGBA(1.0f, 1.0f, 1.0f).Multiply(Factor: random_float(min: 0.5f, max: 0.75f)).WithAlpha(alpha: Alpha);
389 p.m_StartAlpha = p.m_Color.a;
390 GameClient()->m_Particles.Add(Group: CParticles::GROUP_GENERAL, pPart: &p);
391 }
392}
393
394void CEffects::HammerHit(vec2 Pos, float Alpha, float Volume)
395{
396 // add the explosion
397 CParticle p;
398 p.SetDefault();
399 p.m_Spr = SPRITE_PART_HIT01;
400 p.m_Pos = Pos;
401 p.m_LifeSpan = 0.3f;
402 p.m_StartSize = 120.0f;
403 p.m_EndSize = 0.0f;
404 p.m_Rot = random_angle();
405 p.m_Color.a = Alpha;
406 p.m_StartAlpha = Alpha;
407 GameClient()->m_Particles.Add(Group: CParticles::GROUP_EXPLOSIONS, pPart: &p);
408 if(g_Config.m_SndGame)
409 GameClient()->m_Sounds.PlayAt(Channel: CSounds::CHN_WORLD, SetId: SOUND_HAMMER_HIT, Volume, Position: Pos);
410}
411
412void CEffects::OnRender()
413{
414 float Speed = 1.0f;
415 if(Client()->State() == IClient::STATE_DEMOPLAYBACK)
416 Speed = DemoPlayer()->BaseInfo()->m_Speed;
417
418 const int64_t Now = time();
419 auto UpdateClock = [&](bool &Add, int64_t &LastUpdate, int Frequency) {
420 Add = Now - LastUpdate > time_freq() / ((float)Frequency * Speed);
421 if(Add)
422 LastUpdate = Now;
423 };
424 UpdateClock(m_Add5hz, m_LastUpdate5hz, 5);
425 UpdateClock(m_Add50hz, m_LastUpdate50hz, 50);
426 UpdateClock(m_Add100hz, m_LastUpdate100hz, 100);
427
428 if(m_Add50hz)
429 GameClient()->m_Flow.Update();
430}
431