Xonotic QuakeC
The free, fast arena FPS with crisp movement and a wide array of weapons
damageeffects.qc
Go to the documentation of this file.
1#include "damageeffects.qh"
2
3REGISTER_NET_LINKED(ENT_CLIENT_DAMAGEINFO)
4
5#ifdef SVQC
6
8{
9 vector org = vec3(floor(this.origin.x), floor(this.origin.y), floor(this.origin.z));
10 WriteHeader(MSG_ENTITY, ENT_CLIENT_DAMAGEINFO);
12 WriteVector(MSG_ENTITY, org);
13 WriteByte(MSG_ENTITY, bound(1, this.dmg, 255));
14 WriteByte(MSG_ENTITY, bound(0, this.dmg_radius, 255));
15 WriteByte(MSG_ENTITY, bound(1, this.dmg_edge, 255));
16 // we can't send the force vector compressed with compressShortVector as it's too inaccurate
17 // it would break decals when hit angle on a surface is small
18 // (the traceline performed by the client to spawn a decal wouldn't hit the surface at all)
19 WriteShort(MSG_ENTITY, floor(this.velocity.x / 4));
20 WriteShort(MSG_ENTITY, floor(this.velocity.y / 4));
21 WriteShort(MSG_ENTITY, floor(this.velocity.z / 4));
24 return true;
25}
26
27void Damage_DamageInfo(vector org, float coredamage, float edgedamage, float rad, vector force, int deathtype, float bloodtype, entity dmgowner)
28{
29 // TODO maybe call this from non-edgedamage too?
30 // TODO maybe make the client do the particle effects for the weapons and the impact sounds using this info?
31
32 if(!sound_allowed(MSG_BROADCAST, dmgowner))
33 deathtype |= 0x8000;
34
35 entity e = new_pure(damageinfo);
36 // origin is just data to be sent
37 //setorigin(e, org);
38 e.origin = org;
39 e.projectiledeathtype = deathtype;
40 e.dmg = coredamage;
41 e.dmg_edge = edgedamage;
42 bool rad_negative = false;
43 if(rad < 0)
44 {
45 // make it positive (unsigned) so it can be sent as byte
46 rad_negative = true;
47 rad = -rad;
48 }
49 e.dmg_radius = rad;
50 e.dmg_force = vlen(force);
51 e.velocity = force;
52
53 e.species = bloodtype & BITS(4); // it only uses bits from 0 to 3, see SPECIES_* constants
54 if(rad_negative)
55 e.species |= BIT(7);
56
57 e.colormap = (teamplay) ? dmgowner.team : dmgowner.clientcolors; // NOTE: doesn't work on non-clients
58
60}
61
62#endif
63
64#ifdef CSQC
65
68
69.entity tag_entity;
70
71.float cnt;
72.int state;
73
74.bool silent;
75
77{
78 // if particle distribution is enabled, slow ticrate by total number of damages
80 this.nextthink = time + autocvar_cl_damageeffect_ticrate * this.owner.total_damages;
81 else
83
84 if(time >= this.cnt || !this.owner || !this.owner.modelindex || !this.owner.drawmask)
85 {
86 // time is up or the player got gibbed / disconnected
87 this.owner.total_damages = max(0, this.owner.total_damages - 1);
88 delete(this);
89 return;
90 }
91 if(this.state && !this.owner.csqcmodel_isdead)
92 {
93 // if the player was dead but is now alive, it means they respawned
94 // if so, clear their damage effects, or damages from their dead body will be copied back
95 this.owner.total_damages = max(0, this.owner.total_damages - 1);
96 delete(this);
97 return;
98 }
99 this.state = this.owner.csqcmodel_isdead;
100 if((this.owner.isplayermodel & ISPLAYER_LOCAL) && !autocvar_chase_active)
101 return; // if we aren't using a third person camera, hide our own effects
102
103 // now generate the particles
104 vector org = gettaginfo(this, 0); // origin at attached location
105 __pointparticles(this.team, org, '0 0 0', 1);
106}
107
108string species_prefix(int specnum)
109{
110 switch(specnum)
111 {
112 case SPECIES_HUMAN: return "";
113 case SPECIES_ALIEN: return "alien_";
114 case SPECIES_ROBOT_SHINY: return "robot_";
115 case SPECIES_ROBOT_RUSTY: return "robot_"; // use the same effects, only different gibs
116 case SPECIES_ROBOT_SOLID: return "robot_"; // use the same effects, only different gibs
117 case SPECIES_ANIMAL: return "animal_";
118 case SPECIES_RESERVED: return "reserved_";
119 default: return "";
120 }
121}
122
123void DamageEffect(entity this, vector hitorg, float thedamage, int type, int specnum)
124{
125 // particle effects for players and objects damaged by weapons (eg: flames coming out of victims shot with rockets)
126
127 int nearestbone = 0;
128 float life;
129 string effectname;
130 entity e, wep;
131
133 return;
134 if(!this || !this.modelindex || !this.drawmask)
135 return;
136
137 // if this is a rigged mesh, the effect will show on the bone where damage was dealt
138 // we do this by choosing the skeletal bone closest to the impact, and attaching our entity to it
139 // if there's no skeleton, object origin will automatically be selected
140 FOR_EACH_TAG(this)
141 {
142 if(!tagnum)
143 continue; // skip empty bones
144 // blacklist bones positioned outside the mesh, or the effect will be floating
145 // TODO: Do we have to do it this way? Why do these bones exist at all?
146 if(gettaginfo_name == "master" || gettaginfo_name == "knee_L" || gettaginfo_name == "knee_R" || gettaginfo_name == "leg_L" || gettaginfo_name == "leg_R")
147 continue; // player model bone blacklist
148
149 // now choose the bone closest to impact origin
150 if(nearestbone == 0 || vlen2(hitorg - gettaginfo(this, tagnum)) <= vlen2(hitorg - gettaginfo(this, nearestbone)))
151 nearestbone = tagnum;
152 }
153 gettaginfo(this, nearestbone); // set gettaginfo_name
154
155 // return if we reached our damage effect limit or damages are disabled
156 // TODO: When the limit is reached, it would be better if the oldest damage was removed instead of not adding a new one
157 if(nearestbone)
158 {
160 return; // allow multiple damages on skeletal models
161 }
162 else
163 {
165 return; // allow a single damage on non-skeletal models
166 }
167
169
170 wep = DEATH_WEAPONOF(type);
171 effectname = strcat("damage_", wep.netname);
172 if((wep.spawnflags & WEP_FLAG_BLEED))
173 {
174 // if this weapon induces bleeding, use the effect name with the proper species prefix (blood type)
175 if((this.isplayermodel & ISPLAYER_MODEL))
176 {
177 string specstr = species_prefix(specnum);
178 effectname = strcat(specstr, effectname);
179 }
180 else { return; } // objects don't bleed
181 }
182
183 e = new(damage);
184 setmodel(e, MDL_Null); // necessary to attach and read origin
185 setattachment(e, this, gettaginfo_name); // attach to the given bone
186 e.owner = this;
187 e.cnt = time + life;
188 e.team = _particleeffectnum(effectname);
190 e.nextthink = time;
191 this.total_damages += 1;
192}
193
194NET_HANDLE(ENT_CLIENT_DAMAGEINFO, bool isNew)
195{
196 float thedamage, rad, edge, thisdmg;
197 bool hitplayer = false;
198 int species, forcemul;
199 vector force, thisforce;
200
201 w_deathtype = ReadShort();
202 w_issilent = (w_deathtype & 0x8000);
203 w_deathtype = (w_deathtype & 0x7FFF);
204
205 w_org = ReadVector();
206
207 thedamage = ReadByte();
208 rad = ReadByte();
209 edge = ReadByte();
210 force.x = ReadShort() * 4 + 2;
211 force.y = ReadShort() * 4 + 2;
212 force.z = ReadShort() * 4 + 2;
213
214 species = ReadByte();
215 bool rad_negative = (species & BIT(7));
216 species = (species & BITS(4));
217
218 int dmg_colors = ReadByte();
219
220 return = true;
221
222 if (!isNew)
223 return;
224
225 forcemul = (rad_negative ? -1 : 1);
226 // team color logic copied from projectiles
227 int tcolor = dmg_colors;
228 if(teamplay)
229 {
230 if(dmg_colors)
231 tcolor = (dmg_colors - 1) * 0x11;
232 else
233 tcolor = 0x00;
234 tcolor |= BIT(10); // RENDER_COLORMAPPED
235 }
236
237 // set globally so weapon impacts can use them
238 particles_colormin = colormapPaletteColor(floor(tcolor / 16), false);
239 particles_colormax = colormapPaletteColor(tcolor % 16, true);
240
241 FOREACH_ENTITY_RADIUS(w_org, rad + MAX_DAMAGEEXTRARADIUS, !it.tag_entity && !is_pure(it), {
242 vector nearest = NearestPointOnBox(it, w_org);
243 if (rad)
244 {
245 thisdmg = ((vlen(nearest - w_org) - bound(MIN_DAMAGEEXTRARADIUS, it.damageextraradius, MAX_DAMAGEEXTRARADIUS)) / rad);
246 if(thisdmg >= 1)
247 continue;
248 if(thisdmg < 0)
249 thisdmg = 0;
250 if(thedamage)
251 {
252 thisdmg = thedamage + (edge - thedamage) * thisdmg;
253 thisforce = forcemul * vlen(force) * (thisdmg / thedamage) * normalize(it.origin - w_org);
254 }
255 else
256 {
257 thisdmg = 0;
258 thisforce = forcemul * vlen(force) * normalize(it.origin - w_org);
259 }
260 }
261 else
262 {
263 if(vdist((nearest - w_org), >, bound(MIN_DAMAGEEXTRARADIUS, it.damageextraradius, MAX_DAMAGEEXTRARADIUS)))
264 continue;
265
266 thisdmg = thedamage;
267 thisforce = forcemul * force;
268 }
269
270 if(it.damageforcescale)
271 if(vdist(thisforce, !=, 0))
272 {
273 it.velocity = it.velocity + damage_explosion_calcpush(it.damageforcescale * thisforce, it.velocity, damagepush_speedfactor);
274 UNSET_ONGROUND(it);
275 }
276
277 if(w_issilent)
278 it.silent = 1;
279
280 if(it.event_damage)
281 it.event_damage(it, thisdmg, w_deathtype, w_org, thisforce);
282
283 DamageEffect(it, w_org, thisdmg, w_deathtype, species);
284
285 if(it != csqcplayer && (it.isplayermodel & ISPLAYER_MODEL))
286 hitplayer = true; // this impact damaged another player
287 });
288
290 {
291 vector force_dir = normalize(force);
292 traceline(w_org - force_dir * 16, w_org + force_dir * 16, MOVE_NOMONSTERS, NULL);
293 if(trace_plane_normal != '0 0 0')
295 else
296 w_backoff = -1 * normalize(w_org - (w_org + force_dir * 16));
297
298 setorigin(this, w_org + w_backoff * 2); // for sound() calls
299
300 switch(DEATH_ENT(w_deathtype))
301 {
302 case DEATH_VH_CRUSH:
303 break;
304
305 // spiderbot
306 case DEATH_VH_SPID_MINIGUN:
308 pointparticles(EFFECT_SPIDERBOT_MINIGUN_IMPACT, this.origin, w_backoff * 1000, 1);
309 break;
310 case DEATH_VH_SPID_ROCKET:
311 sound(this, CH_SHOTS, SND_ROCKET_IMPACT, VOL_BASE, ATTEN_NORM);
312 pointparticles(EFFECT_SPIDERBOT_ROCKET_EXPLODE, this.origin, w_backoff * 1000, 1);
313 break;
314 case DEATH_VH_SPID_DEATH:
315 sound(this, CH_SHOTS, SND_ROCKET_IMPACT, VOL_BASE, ATTEN_LOW);
316 pointparticles(EFFECT_EXPLOSION_BIG, this.origin, w_backoff * 1000, 1);
317 break;
318
319 case DEATH_VH_WAKI_GUN:
320 sound(this, CH_SHOTS, SND_LASERIMPACT, VOL_BASE, ATTEN_NORM);
321 pointparticles(EFFECT_RACER_IMPACT, this.origin, w_backoff * 1000, 1);
322 break;
323 case DEATH_VH_WAKI_ROCKET:
324 sound(this, CH_SHOTS, SND_ROCKET_IMPACT, VOL_BASE, ATTEN_NORM);
325 pointparticles(EFFECT_RACER_ROCKET_EXPLODE, this.origin, w_backoff * 1000, 1);
326 break;
327 case DEATH_VH_WAKI_DEATH:
328 sound(this, CH_SHOTS, SND_ROCKET_IMPACT, VOL_BASE, ATTEN_LOW);
329 pointparticles(EFFECT_EXPLOSION_BIG, this.origin, w_backoff * 1000, 1);
330 break;
331
332 case DEATH_VH_RAPT_CANNON:
333 sound(this, CH_SHOTS, SND_LASERIMPACT, VOL_BASE, ATTEN_NORM);
334 pointparticles(EFFECT_RAPTOR_CANNON_IMPACT, this.origin, w_backoff * 1000, 1);
335 break;
336 case DEATH_VH_RAPT_FRAGMENT:
337 float i;
338 vector ang, vel;
339 for(i = 1; i < 4; ++i)
340 {
341 vel = normalize(w_org - (w_org + force_dir * 16)) + randomvec() * 128;
342 ang = vectoangles(vel);
343 RaptorCBShellfragToss(w_org, vel, ang + '0 0 1' * (120 * i));
344 }
345 sound(this, CH_SHOTS, SND_ROCKET_IMPACT, VOL_BASE, ATTEN_NORM);
346 pointparticles(EFFECT_RAPTOR_BOMB_SPREAD, this.origin, w_backoff * 1000, 1);
347 break;
348 case DEATH_VH_RAPT_BOMB:
349 sound(this, CH_SHOTS, SND_ROCKET_IMPACT, VOL_BASE, ATTEN_NORM);
350 pointparticles(EFFECT_RAPTOR_BOMB_IMPACT, this.origin, w_backoff * 1000, 1);
351 break;
352 case DEATH_VH_RAPT_DEATH:
353 sound(this, CH_SHOTS, SND_ROCKET_IMPACT, VOL_BASE, ATTEN_LOW);
354 pointparticles(EFFECT_EXPLOSION_BIG, this.origin, w_backoff * 1000, 1);
355 break;
356 case DEATH_VH_BUMB_GUN:
357 sound(this, CH_SHOTS, SND_VEH_BUMBLEBEE_IMPACT, VOL_BASE, ATTEN_NORM);
358 pointparticles(EFFECT_BIGPLASMA_IMPACT, this.origin, w_backoff * 1000, 1);
359 break;
360 }
361 }
362
363
365 {
366 vector force_dir = normalize(force);
367 traceline(w_org - force_dir * 16, w_org + force_dir * 16, MOVE_NOMONSTERS, NULL);
368 if(trace_plane_normal != '0 0 0')
370 else
371 w_backoff = -1 * normalize(w_org - (w_org + force_dir * 16));
372
373 setorigin(this, w_org + w_backoff * 2); // for sound() calls
374
375 switch(DEATH_ENT(w_deathtype))
376 {
377 case DEATH_TURRET_EWHEEL:
378 sound(this, CH_SHOTS, SND_LASERIMPACT, VOL_BASE, ATTEN_LOW);
379 pointparticles(EFFECT_BLASTER_IMPACT, this.origin, w_backoff * 1000, 1);
380 break;
381
382 case DEATH_TURRET_FLAC:
383 pointparticles(EFFECT_HAGAR_EXPLODE, w_org, '0 0 0', 1);
385 break;
386
387 case DEATH_TURRET_MLRS:
388 case DEATH_TURRET_HK:
389 case DEATH_TURRET_WALK_ROCKET:
390 case DEATH_TURRET_HELLION:
391 sound(this, CH_SHOTS, SND_ROCKET_IMPACT, VOL_BASE, ATTEN_LOW);
392 pointparticles(EFFECT_ROCKET_EXPLODE, this.origin, w_backoff * 1000, 1);
393 break;
394
395 case DEATH_TURRET_MACHINEGUN:
396 case DEATH_TURRET_WALK_GUN:
398 pointparticles(EFFECT_MACHINEGUN_IMPACT, this.origin, w_backoff * 1000, 1);
399 break;
400
401 case DEATH_TURRET_PLASMA:
402 sound(this, CH_SHOTS, SND_TUR_PLASMA_IMPACT, VOL_BASE, ATTEN_LOW);
403 pointparticles(EFFECT_ELECTRO_IMPACT, this.origin, w_backoff * 1000, 1);
404 break;
405
406 case DEATH_TURRET_WALK_MELEE:
408 pointparticles(EFFECT_TE_SPARK, this.origin, w_backoff * 1000, 1);
409 break;
410
411 case DEATH_TURRET_PHASER:
412 break;
413
414 case DEATH_TURRET_TESLA:
415 te_smallflash(this.origin);
416 break;
417 }
418 }
419
420 MUTATOR_CALLHOOK(DamageInfo, this, w_deathtype, w_org);
421
422 // TODO spawn particle effects and sounds based on w_deathtype
424 if(!hitplayer || rad) // don't show ground impacts for hitscan weapons if a player was hit
425 {
427 w_random = prandom();
428
429 vector force_dir = normalize(force);
430 // this traceline usually starts in solid when a hitscan shot hits a surface with a very small angle
431 // if so, try another traceline starting further back (may still start in solid but only with extremely small angles)
432 traceline(w_org - force_dir * 16, w_org + force_dir * 16, MOVE_NOMONSTERS, NULL);
434 traceline(w_org - force_dir * 40, w_org + force_dir * 16, MOVE_NOMONSTERS, NULL);
435 if(trace_fraction < 1)
437 else
438 w_backoff = -force_dir;
439 setorigin(this, w_org + w_backoff * 2); // for sound() calls
440
442 {
443 if(!MUTATOR_CALLHOOK(Weapon_ImpactEffect, hitwep, this))
444 hitwep.wr_impacteffect(hitwep, this);
445 }
446 }
447}
448
449#endif
#define MUTATOR_CALLHOOK(id,...)
Definition base.qh:143
#define BIT(n)
Only ever assign into the first 24 bits in QC (so max is BIT(23)).
Definition bits.qh:8
#define BITS(n)
Definition bits.qh:9
float dmg
Definition breakable.qc:12
float dmg_edge
Definition breakable.qc:13
float dmg_radius
Definition breakable.qc:14
vector damage_explosion_calcpush(vector explosion_f, vector target_v, float speedfactor)
var entity(vector mins, vector maxs,.entity tofield) findbox_tofield_OrFallback
entity csqcplayer
Definition cl_player.qh:26
fields which are explicitly/manually set are marked with "M", fields set automatically are marked wit...
Definition weapon.qh:44
virtual void wr_impacteffect()
(CLIENT) impact effect for weapon explosion
Definition weapon.qh:108
float cnt
Definition powerups.qc:24
entity owner
Definition main.qh:87
int team
Definition main.qh:188
float damagepush_speedfactor
Definition main.qh:161
#define colormapPaletteColor(c, isPants)
Definition color.qh:5
#define setmodel(this, m)
Definition model.qh:26
bool autocvar_cl_gentle
Definition util.qh:236
#define FOR_EACH_TAG(v)
Definition util.qh:216
const int SPECIES_ANIMAL
Definition constants.qh:25
const int SPECIES_ROBOT_SOLID
Definition constants.qh:23
const int SPECIES_ROBOT_RUSTY
Definition constants.qh:26
const int SPECIES_RESERVED
Definition constants.qh:28
const int SPECIES_ROBOT_SHINY
Definition constants.qh:27
const int SPECIES_ALIEN
Definition constants.qh:24
const int SPECIES_HUMAN
Definition constants.qh:22
float Q3SURFACEFLAG_SKY
const float MOVE_NOMONSTERS
float drawmask
float modelindex
vector particles_colormin
vector velocity
string gettaginfo_name
float time
float trace_startsolid
float nextthink
float trace_dphitq3surfaceflags
float colormap
vector particles_colormax
vector origin
float trace_fraction
vector trace_plane_normal
entity tag_entity
int isplayermodel
string species_prefix(int specnum)
void DamageEffect_Think(entity this)
bool Damage_DamageInfo_SendEntity(entity this, entity to, int sf)
int state
void DamageEffect(entity this, vector hitorg, float thedamage, int type, int specnum)
bool silent
int total_damages
number of effects which currently are attached to a player
void Damage_DamageInfo(vector org, float coredamage, float edgedamage, float rad, vector force, int deathtype, float bloodtype, entity dmgowner)
vector w_org
float autocvar_cl_damageeffect_ticrate
const float MAX_DAMAGEEXTRARADIUS
float autocvar_cl_damageeffect_lifetime
bool autocvar_cl_damageeffect_distribute
float autocvar_cl_damageeffect_lifetime_max
int w_deathtype
int autocvar_cl_damageeffect
float autocvar_cl_damageeffect_bones
float autocvar_cl_damageeffect_lifetime_min
const float MIN_DAMAGEEXTRARADIUS
float w_random
vector w_backoff
float w_issilent
#define DEATH_ENT(t)
Definition all.qh:41
#define DEATH_ISTURRET(t)
Definition all.qh:43
#define DEATH_ISSPECIAL(t)
Definition all.qh:39
#define DEATH_ISVEHICLE(t)
Definition all.qh:42
#define DEATH_WEAPONOF(t)
Definition all.qh:45
#define pointparticles(effect, org, vel, howmany)
Definition effect.qh:7
#define FOREACH_ENTITY_RADIUS(org, dist, cond, body)
Definition iter.qh:160
const int ISPLAYER_LOCAL
Definition common.qh:58
const int ISPLAYER_MODEL
Definition common.qh:56
#define NET_HANDLE(id, param)
Definition net.qh:15
const int MSG_ENTITY
Definition net.qh:115
#define ReadVector()
Definition net.qh:367
#define WriteHeader(to, id)
Definition net.qh:221
#define REGISTER_NET_LINKED(id)
Definition net.qh:55
void Net_LinkEntity(entity e, bool docull, float dt, bool(entity this, entity to, int sendflags) sendfunc)
Definition net.qh:123
int ReadByte()
float bound(float min, float value, float max)
float vlen(vector v)
void WriteShort(float data, float dest, float desto)
vector vectoangles(vector v)
vector randomvec(void)
vector normalize(vector v)
void WriteByte(float data, float dest, float desto)
float floor(float f)
float MSG_BROADCAST
Definition menudefs.qc:55
float max(float f,...)
#define UNSET_ONGROUND(s)
Definition movetypes.qh:18
strcat(_("^F4Countdown stopped!"), "\n^BG", _("Teams are too unbalanced."))
#define is_pure(e)
Definition oo.qh:16
#define new_pure(class)
purely logical entities (not linked to the area grid)
Definition oo.qh:67
#define NULL
Definition post.qh:14
#define gettaginfo
Definition post.qh:32
float prandom()
Predictable random number generator (not seeded yet)
Definition random.qc:108
void RaptorCBShellfragToss(vector _org, vector _vel, vector _ang)
#define setthink(e, f)
vector
Definition self.qh:92
vector org
Definition self.qh:92
vector vector ang
Definition self.qh:92
float species
Definition main.qh:47
int projectiledeathtype
Definition common.qh:21
const float VOL_BASE
Definition sound.qh:36
const int CH_SHOTS
Definition sound.qh:14
const float ATTEN_LOW
Definition sound.qh:29
const float ATTEN_NORM
Definition sound.qh:30
#define sound(e, c, s, v, a)
Definition sound.qh:52
Sound SND_RIC_RANDOM()
Definition all.inc:33
Sound SND_FLACEXP_RANDOM()
Definition all.inc:149
bool sound_allowed(int to, entity e)
Definition all.qc:9
bool teamplay
Definition teams.qh:59
#define vlen2(v)
Definition vector.qh:4
#define vdist(v, cmp, f)
Vector distance comparison, avoids sqrt()
Definition vector.qh:8
ERASEABLE vector NearestPointOnBox(entity box, vector org)
Definition vector.qh:178
#define vec3(_x, _y, _z)
Definition vector.qh:95
int autocvar_chase_active
Definition view.qh:17
int autocvar_cl_gentle_damage
Definition view.qh:19
const int WEP_FLAG_BLEED
Definition weapon.qh:225